@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