@danhachuel/thunderbolt 0.6.20 → 0.6.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import json
5
5
  from dataclasses import dataclass
6
6
  from datetime import datetime, timezone
7
7
  from html import unescape
8
- from typing import Any
8
+ from typing import Any, Mapping
9
9
  from urllib.parse import urlparse
10
10
 
11
11
  import requests
@@ -85,6 +85,47 @@ def _structured_metric_from_json(document: str, *keys: str) -> int | None:
85
85
  return None
86
86
 
87
87
 
88
+ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
89
+ try:
90
+ response = requests.get(
91
+ f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
92
+ headers={
93
+ 'x-ig-app-id': '936619743392459',
94
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
95
+ 'Accept': '*/*',
96
+ 'Accept-Language': 'en-US,en;q=0.9',
97
+ },
98
+ timeout=15,
99
+ )
100
+ if response.status_code >= 400:
101
+ return None
102
+ payload = response.json()
103
+ except (requests.RequestException, ValueError, AttributeError):
104
+ return None
105
+ user = ((payload.get('data') or {}).get('user') if isinstance(payload, dict) else None)
106
+ return user if isinstance(user, dict) else None
107
+
108
+
109
+ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]) -> dict[str, Any]:
110
+ followers = _structured_metric_from_json(json.dumps(user), 'edge_followed_by', 'followers', 'follower_count')
111
+ following = _structured_metric_from_json(json.dumps(user), 'edge_follow', 'follows', 'following', 'following_count')
112
+ media = ((user.get('edge_owner_to_timeline_media') or {}).get('count') if isinstance(user.get('edge_owner_to_timeline_media'), dict) else None)
113
+ return {
114
+ 'id': f"instagram_{reference['username']}",
115
+ **reference,
116
+ 'name': str(user.get('full_name') or user.get('username') or reference['username']).strip(),
117
+ 'bio': str(user.get('biography') or '').strip(),
118
+ 'avatar_url': str(user.get('profile_pic_url_hd') or user.get('profile_pic_url') or '').strip(),
119
+ 'subscriber_count': followers,
120
+ 'following_count': following,
121
+ 'post_count': media,
122
+ 'public_lookup': True,
123
+ 'metrics_source': 'instagram_web_profile_info',
124
+ 'last_public_lookup_at': datetime.now(timezone.utc).isoformat(),
125
+ '_api_posts': (((user.get('edge_owner_to_timeline_media') or {}).get('edges') or []) if isinstance(user.get('edge_owner_to_timeline_media'), dict) else []),
126
+ }
127
+
128
+
88
129
  def fetch_public_instagram_profile(source: str) -> IntegrationResult:
89
130
  try:
90
131
  reference = normalize_instagram_reference(source)
@@ -113,6 +154,10 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
113
154
  followers = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:followers|seguidores)',)) or _structured_metric(response.text, 'edge_followed_by', 'followers', 'follower_count') or _structured_metric_from_json(response.text, 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
114
155
  following = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:following|seguindo)',)) or _structured_metric(response.text, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount') or _structured_metric_from_json(response.text, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
115
156
  posts = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:posts|publicações|publications)',)) or _structured_metric(response.text, 'edge_owner_to_timeline_media', 'posts', 'post_count')
157
+ if followers is None or following is None or not avatar_url:
158
+ api_user = _fetch_web_profile_user(reference['username'])
159
+ if api_user:
160
+ return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', _profile_data_from_api(api_user, reference))
116
161
  data = {'id': f"instagram_{reference['username']}", **reference, 'name': title.split('(')[0].strip() or reference['username'], 'bio': description, 'avatar_url': avatar_url, 'subscriber_count': followers, 'following_count': following, 'post_count': posts, 'public_lookup': True, 'metrics_source': 'instagram_public_page', 'last_public_lookup_at': datetime.now(timezone.utc).isoformat()}
117
162
  return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', data)
118
163
 
@@ -179,6 +224,17 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
179
224
  reference = normalize_instagram_reference(source)
180
225
  except ValueError as exc:
181
226
  return IntegrationResult(False, str(exc), {})
227
+ api_user = _fetch_web_profile_user(reference['username'])
228
+ if api_user:
229
+ api_posts: list[dict[str, Any]] = []
230
+ for edge in ((api_user.get('edge_owner_to_timeline_media') or {}).get('edges') or []):
231
+ node = edge.get('node') if isinstance(edge, dict) else None
232
+ if isinstance(node, dict):
233
+ post = _post_from_node(node)
234
+ if post:
235
+ api_posts.append(post)
236
+ if api_posts:
237
+ return IntegrationResult(True, 'Posts públicos encontrados.', reference | {'posts': api_posts[:max(1, int(limit))]})
182
238
  try:
183
239
  response = requests.get(reference['url'], headers={'User-Agent': 'Mozilla/5.0', 'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8'}, timeout=15)
184
240
  except requests.RequestException as exc:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.20",
3
+ "version": "0.6.21",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",