@danhachuel/thunderbolt 0.6.59 → 0.6.61
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.
- package/integrations/instagram_public.py +44 -11
- package/package.json +1 -1
|
@@ -185,23 +185,45 @@ def _metric_text_value(value: Any) -> int | None:
|
|
|
185
185
|
return int(digits) * multiplier
|
|
186
186
|
|
|
187
187
|
|
|
188
|
+
def _extract_bio_from_html(document: str) -> str:
|
|
189
|
+
"""Extract only the JSON biography value; never use an SEO description as Bio."""
|
|
190
|
+
patterns = (
|
|
191
|
+
r'"biography"\s*:\s*"((?:\\.|[^"\\])*)"',
|
|
192
|
+
r"'biography'\s*:\s*'((?:\\.|[^'\\])*)'",
|
|
193
|
+
)
|
|
194
|
+
for pattern in patterns:
|
|
195
|
+
for match in re.finditer(pattern, document, flags=re.IGNORECASE | re.DOTALL):
|
|
196
|
+
raw_value = match.group(1)
|
|
197
|
+
try:
|
|
198
|
+
decoded = json.loads(f'"{raw_value}"')
|
|
199
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
200
|
+
decoded = bytes(raw_value, 'utf-8').decode('unicode_escape')
|
|
201
|
+
if str(decoded or '').strip():
|
|
202
|
+
return str(decoded).strip()
|
|
203
|
+
return ''
|
|
204
|
+
|
|
205
|
+
|
|
188
206
|
def _embedded_profile_user(document: str, username: str) -> Mapping[str, Any]:
|
|
189
|
-
wanted = username.casefold()
|
|
207
|
+
wanted = str(username or '').strip().lstrip('@').casefold()
|
|
190
208
|
for payload in _embedded_json_documents(document):
|
|
191
209
|
for node in _walk_json(payload):
|
|
192
210
|
if not isinstance(node, Mapping):
|
|
193
211
|
continue
|
|
194
|
-
node_username = str(node.get('username') or node.get('handle') or '').lstrip('@').casefold()
|
|
195
|
-
|
|
212
|
+
node_username = str(node.get('username') or node.get('handle') or '').strip().lstrip('@').casefold()
|
|
213
|
+
has_profile_fields = any(key in node for key in ('biography', 'bio', 'edge_followed_by', 'edge_follow', 'followers', 'following'))
|
|
214
|
+
if node_username == wanted and has_profile_fields:
|
|
196
215
|
return node
|
|
197
216
|
return {}
|
|
198
217
|
|
|
199
218
|
|
|
200
219
|
def _extract_profile_user_from_html(document: str, username: str) -> dict[str, Any]:
|
|
201
220
|
embedded_user = _embedded_profile_user(document, username)
|
|
221
|
+
regex_biography = _extract_bio_from_html(document)
|
|
202
222
|
seo_description = _meta(document, 'og:description')
|
|
203
|
-
biography = str(embedded_user.get('biography') or embedded_user.get('bio') or '').strip()
|
|
204
|
-
|
|
223
|
+
biography = str(embedded_user.get('biography') or embedded_user.get('bio') or regex_biography or '').strip()
|
|
224
|
+
print(f'embedded_user for @{username}: {embedded_user}')
|
|
225
|
+
print(f'biography extracted for @{username}: {biography!r}')
|
|
226
|
+
description = biography
|
|
205
227
|
title = str(embedded_user.get('full_name') or embedded_user.get('name') or _meta(document, 'og:title') or username).split('(')[0].strip() or username
|
|
206
228
|
avatar_url = str(embedded_user.get('profile_pic_url_hd') or embedded_user.get('profile_pic_url') or _meta(document, 'og:image')).strip()
|
|
207
229
|
|
|
@@ -214,9 +236,9 @@ def _extract_profile_user_from_html(document: str, username: str) -> dict[str, A
|
|
|
214
236
|
return parsed
|
|
215
237
|
return None
|
|
216
238
|
|
|
217
|
-
followers = _metric_text_value(embedded_user.get('edge_followed_by')) or _metric_text_value(embedded_user.get('followers')) or metric((r'([\d.,]+)\s*(k|mil)?\s*(?:followers|seguidores)',)) or _structured_metric(document, 'edge_followed_by', 'followers', 'follower_count') or _structured_metric_from_json(document, 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
|
|
218
|
-
following = _metric_text_value(embedded_user.get('edge_follow')) or _metric_text_value(embedded_user.get('following')) or metric((r'([\d.,]+)\s*(k|mil)?\s*(?:following|seguindo)',)) or _structured_metric(document, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount') or _structured_metric_from_json(document, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
|
|
219
|
-
post_count = _metric_text_value(embedded_user.get('edge_owner_to_timeline_media')) or _metric_text_value(embedded_user.get('posts')) or metric((r'([\d.,]+)\s*(k|mil)?\s*(?:posts|publicações|publications)',)) or _structured_metric(document, 'edge_owner_to_timeline_media', 'posts', 'post_count')
|
|
239
|
+
followers = _metric_text_value(embedded_user.get('edge_followed_by')) or _metric_text_value(embedded_user.get('followers')) or metric((r'([\d.,]+)\s*(k|mil)?\s*(?:followers|seguidores)', r'(?:followers|seguidores)\s*([\d.,]+)\s*(k|mil)?')) or _structured_metric(document, 'edge_followed_by', 'followers', 'follower_count') or _structured_metric_from_json(document, 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
|
|
240
|
+
following = _metric_text_value(embedded_user.get('edge_follow')) or _metric_text_value(embedded_user.get('following')) or metric((r'([\d.,]+)\s*(k|mil)?\s*(?:following|seguindo)', r'(?:following|seguindo)\s*([\d.,]+)\s*(k|mil)?')) or _structured_metric(document, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount') or _structured_metric_from_json(document, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
|
|
241
|
+
post_count = _metric_text_value(embedded_user.get('edge_owner_to_timeline_media')) or _metric_text_value(embedded_user.get('posts')) or metric((r'([\d.,]+)\s*(k|mil)?\s*(?:posts|publicações|publications)', r'(?:posts|publicações|publications)\s*([\d.,]+)\s*(k|mil)?')) or _structured_metric(document, 'edge_owner_to_timeline_media', 'posts', 'post_count')
|
|
220
242
|
country = _country_value(embedded_user.get('country') or embedded_user.get('country_code')) or extract_public_instagram_country(embedded_user)
|
|
221
243
|
if not country:
|
|
222
244
|
for payload in _embedded_json_documents(document):
|
|
@@ -227,7 +249,7 @@ def _extract_profile_user_from_html(document: str, username: str) -> dict[str, A
|
|
|
227
249
|
user: dict[str, Any] = {
|
|
228
250
|
'username': username,
|
|
229
251
|
'full_name': title.split('(')[0].strip() or username,
|
|
230
|
-
'biography':
|
|
252
|
+
'biography': biography,
|
|
231
253
|
'edge_followed_by': {'count': followers} if followers is not None else {},
|
|
232
254
|
'edge_follow': {'count': following} if following is not None else {},
|
|
233
255
|
'edge_owner_to_timeline_media': {'count': post_count, 'edges': [{'node': post} for post in posts]},
|
|
@@ -235,7 +257,7 @@ def _extract_profile_user_from_html(document: str, username: str) -> dict[str, A
|
|
|
235
257
|
'profile_pic_url': avatar_url,
|
|
236
258
|
'_html_posts': posts,
|
|
237
259
|
}
|
|
238
|
-
return user if any((
|
|
260
|
+
return user if any((biography, followers, following, post_count, country, posts)) else {}
|
|
239
261
|
|
|
240
262
|
|
|
241
263
|
def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
@@ -261,6 +283,15 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
261
283
|
merged[key] = value
|
|
262
284
|
return merged
|
|
263
285
|
|
|
286
|
+
def profile_is_complete(user: Mapping[str, Any]) -> bool:
|
|
287
|
+
if profile_quality(user) < 4:
|
|
288
|
+
return False
|
|
289
|
+
if platform.system() != 'Windows':
|
|
290
|
+
return True
|
|
291
|
+
has_bio = bool(str(user.get('biography') or user.get('bio') or '').strip())
|
|
292
|
+
has_following = any(user.get(key) not in (None, '', {}, []) for key in ('edge_follow', 'follows', 'following', 'following_count', 'followingCount'))
|
|
293
|
+
return has_bio and has_following
|
|
294
|
+
|
|
264
295
|
best_user: dict[str, Any] | None = None
|
|
265
296
|
for endpoint in endpoints:
|
|
266
297
|
try:
|
|
@@ -273,7 +304,7 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
273
304
|
user = ((payload.get('data') or {}).get('user') if isinstance(payload, dict) else None)
|
|
274
305
|
if isinstance(user, dict):
|
|
275
306
|
best_user = merge_profile(best_user, user)
|
|
276
|
-
if
|
|
307
|
+
if profile_is_complete(best_user):
|
|
277
308
|
return best_user
|
|
278
309
|
|
|
279
310
|
curl = shutil.which('curl')
|
|
@@ -453,6 +484,8 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
453
484
|
api_user = _fetch_web_profile_user(reference['username'])
|
|
454
485
|
if api_user:
|
|
455
486
|
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', _profile_data_from_api(api_user, reference))
|
|
487
|
+
if platform.system() == 'Windows':
|
|
488
|
+
return IntegrationResult(False, 'Não foi possível encontrar o perfil público estruturado do Instagram.', reference)
|
|
456
489
|
try:
|
|
457
490
|
response = requests.get(reference['url'], headers={'User-Agent': 'Mozilla/5.0', 'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8'}, timeout=12)
|
|
458
491
|
except requests.RequestException as exc:
|
package/package.json
CHANGED