@danhachuel/thunderbolt 0.6.40 → 0.6.42

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,6 +5,7 @@ import re
5
5
  import uuid
6
6
  import zipfile
7
7
  import unicodedata
8
+ import hashlib
8
9
  from typing import Any, Callable, Mapping
9
10
 
10
11
  import requests
@@ -124,6 +125,19 @@ def _instagram_profile_storage_id(profile: Mapping[str, Any]) -> str:
124
125
  return re.sub(r"[^A-Za-z0-9_.-]+", "_", value) or "unknown"
125
126
 
126
127
 
128
+ def _clear_instagram_result_widget_state(profile: Mapping[str, Any]) -> None:
129
+ widget_id = _instagram_profile_storage_id(profile)
130
+ for field in ("name", "bio", "country", "language", "posts", "followers", "following", "character"):
131
+ st.session_state.pop(f"social_instagram_result_{field}_{widget_id}", None)
132
+
133
+
134
+ def _instagram_result_widget_id(profile: Mapping[str, Any]) -> str:
135
+ base_id = _instagram_profile_storage_id(profile)
136
+ signature = "|".join(_clean(profile.get(key)) for key in ("bio", "bio_raw", "following_count", "subscriber_count", "post_count", "country"))
137
+ digest = hashlib.sha1(signature.encode("utf-8")).hexdigest()[:10]
138
+ return f"{base_id}_{digest}"
139
+
140
+
127
141
  def _normalise_instagram_posts(value: Any) -> list[dict[str, Any]]:
128
142
  if not isinstance(value, list):
129
143
  return []
@@ -583,6 +597,8 @@ def render_social_networks(settings: dict[str, Any]) -> None:
583
597
  if search_clicked:
584
598
  result = fetch_public_instagram_profile(source)
585
599
  result_data = dict(result.data) if isinstance(result.data, dict) else {}
600
+ if result_data:
601
+ _clear_instagram_result_widget_state(result_data)
586
602
  st.session_state["social_instagram_result"] = result_data
587
603
  st.session_state["social_instagram_ok"] = result.ok
588
604
  st.session_state["social_instagram_message"] = result.message
@@ -591,7 +607,7 @@ def render_social_networks(settings: dict[str, Any]) -> None:
591
607
  data = st.session_state.get("social_instagram_result", {}) if st.session_state.get("social_instagram_ok") else {}
592
608
  if data:
593
609
  characters, _ = _characters(settings)
594
- result_widget_id = _instagram_profile_storage_id(data)
610
+ result_widget_id = _instagram_result_widget_id(data)
595
611
  with st.container(border=True):
596
612
  st.subheader("Conta Instagram encontrada")
597
613
  preview_cols = st.columns([0.8, 2.2, 1.2, 1.2, 1.2])
@@ -140,7 +140,14 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
140
140
  f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
141
141
  )
142
142
  def profile_quality(user: Mapping[str, Any]) -> int:
143
- return sum(bool(user.get(key)) for key in ('biography', 'edge_follow', 'edge_followed_by', 'edge_owner_to_timeline_media', 'username'))
143
+ groups = (
144
+ ('biography', 'bio', 'description'),
145
+ ('edge_follow', 'following', 'following_count', 'followingCount', 'follows'),
146
+ ('edge_followed_by', 'followers', 'follower_count', 'followers_count', 'followerCount'),
147
+ ('edge_owner_to_timeline_media', 'posts', 'post_count', 'media_count'),
148
+ ('username', 'user_name', 'handle'),
149
+ )
150
+ return sum(any(user.get(key) not in (None, '', {}, []) for key in group) for group in groups)
144
151
 
145
152
  def merge_profile(base: dict[str, Any] | None, candidate: Mapping[str, Any]) -> dict[str, Any]:
146
153
  merged = dict(base or {})
@@ -300,12 +307,13 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
300
307
  followers = followers if followers is not None else _structured_metric_from_json(json.dumps(user), 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
301
308
  following = following if following is not None else _structured_metric_from_json(json.dumps(user), 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
302
309
  media = ((user.get('edge_owner_to_timeline_media') or {}).get('count') if isinstance(user.get('edge_owner_to_timeline_media'), dict) else None)
303
- biography = str(user.get('biography') or '').strip()
310
+ media = media if media is not None else _profile_metric(user, 'posts', 'post_count', 'media_count')
311
+ biography = str(user.get('biography') or user.get('bio') or user.get('description') or '').strip()
304
312
  country = extract_public_instagram_country(user) or _fetch_instagram_about_country(user.get('id') or user.get('pk'))
305
313
  return {
306
314
  'id': f"instagram_{reference['username']}",
307
315
  **reference,
308
- 'name': str(user.get('full_name') or user.get('username') or reference['username']).strip(),
316
+ 'name': str(user.get('full_name') or user.get('name') or user.get('username') or reference['username']).strip(),
309
317
  'bio': normalize_instagram_bio(biography),
310
318
  'bio_raw': biography,
311
319
  'country': country,
@@ -317,7 +325,7 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
317
325
  'public_lookup': True,
318
326
  'metrics_source': 'instagram_web_profile_info',
319
327
  'last_public_lookup_at': datetime.now(timezone.utc).isoformat(),
320
- '_api_posts': (((user.get('edge_owner_to_timeline_media') or {}).get('edges') or []) if isinstance(user.get('edge_owner_to_timeline_media'), dict) else []),
328
+ '_api_posts': (((user.get('edge_owner_to_timeline_media') or {}).get('edges') or []) if isinstance(user.get('edge_owner_to_timeline_media'), dict) else (user.get('posts') if isinstance(user.get('posts'), list) else [])),
321
329
  }
322
330
 
323
331
 
@@ -512,6 +520,33 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
512
520
  break
513
521
  if len(posts) >= max(1, int(limit)):
514
522
  break
523
+ if not posts:
524
+ try:
525
+ from playwright.sync_api import sync_playwright
526
+ with sync_playwright() as playwright:
527
+ browser = playwright.chromium.launch(headless=True)
528
+ try:
529
+ page = browser.new_page(
530
+ user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
531
+ extra_http_headers={'x-ig-app-id': '936619743392459', 'Accept-Language': 'en-US,en;q=0.9'},
532
+ )
533
+ page.goto(reference['url'], wait_until='domcontentloaded', timeout=30000)
534
+ browser_document = page.content()
535
+ for document in _embedded_json_documents(browser_document):
536
+ for node in _walk_json(document):
537
+ post = _post_from_node(node)
538
+ if not post or post['id'] in seen:
539
+ continue
540
+ seen.add(post['id'])
541
+ posts.append(post)
542
+ if len(posts) >= max(1, int(limit)):
543
+ break
544
+ if len(posts) >= max(1, int(limit)):
545
+ break
546
+ finally:
547
+ browser.close()
548
+ except (ImportError, OSError, RuntimeError, TimeoutError):
549
+ pass
515
550
  return IntegrationResult(bool(posts), 'Posts públicos encontrados.' if posts else 'Não foi possível encontrar posts públicos nesta página do Instagram.', reference | {'posts': posts[:max(1, int(limit))]})
516
551
 
517
552
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.40",
3
+ "version": "0.6.42",
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",