@danhachuel/thunderbolt 0.6.28 → 0.6.30

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.
@@ -45,7 +45,11 @@ def _metric_input(value: Any) -> int | None:
45
45
  def _render_instagram_bio(value: Any) -> None:
46
46
  bio = normalize_instagram_bio(value)
47
47
  if bio:
48
- st.markdown(bio.replace("\n", " \n"))
48
+ st.text(bio)
49
+
50
+
51
+ def _profile_bio(profile: Mapping[str, Any]) -> str:
52
+ return normalize_instagram_bio(profile.get("bio")) or normalize_instagram_bio(profile.get("bio_raw"))
49
53
 
50
54
 
51
55
  def _language_index(value: Any) -> int:
@@ -99,7 +103,6 @@ def _render_instagram_posts(profile: Mapping[str, Any]) -> None:
99
103
  profile_id = _clean(profile.get("id"))
100
104
  state_key = _instagram_posts_key(profile_id)
101
105
  posts = st.session_state.get(state_key, [])
102
- source = _clean(profile.get("url")) or _clean(profile.get("handle"))
103
106
  with st.expander("Últimos posts do Instagram", expanded=False):
104
107
  action_cols = st.columns(3)
105
108
  with action_cols[0]:
@@ -110,13 +113,14 @@ def _render_instagram_posts(profile: Mapping[str, Any]) -> None:
110
113
  download_clicked = bool(posts) and st.download_button("Baixar todos", data=_instagram_posts_archive(posts), file_name=f"instagram-{profile_id}-posts.zip", mime="application/zip", key=f"download_instagram_posts_{profile_id}", use_container_width=True)
111
114
  if load_clicked or refresh_clicked:
112
115
  target = max(10, len(posts)) if refresh_clicked else 10
113
- result = fetch_public_instagram_posts(source, limit=target)
114
- if result.ok:
115
- posts = result.data.get("posts", [])
116
+ posts_ok, posts_message, loaded_posts = _load_instagram_posts(profile, limit=target)
117
+ if posts_ok:
118
+ posts = loaded_posts
116
119
  st.session_state[state_key] = posts
117
120
  st.success(f"{len(posts)} posts carregados.")
121
+ st.rerun()
118
122
  else:
119
- st.warning(result.message)
123
+ st.warning(posts_message)
120
124
  posts = st.session_state.get(state_key, posts)
121
125
  if not posts:
122
126
  st.info("Clique em Carregar últimos 10 para consultar os posts públicos desta conta.")
@@ -133,11 +137,11 @@ def _render_instagram_posts(profile: Mapping[str, Any]) -> None:
133
137
  if _clean(post.get("caption")):
134
138
  st.caption(post["caption"])
135
139
  if st.button("Mostrar + 10", key=f"more_instagram_posts_{profile_id}", use_container_width=True):
136
- result = fetch_public_instagram_posts(source, limit=len(posts) + 10)
137
- if result.ok:
138
- st.session_state[state_key] = result.data.get("posts", posts)
140
+ posts_ok, posts_message, loaded_posts = _load_instagram_posts(profile, limit=len(posts) + 10)
141
+ if posts_ok:
142
+ st.session_state[state_key] = loaded_posts
139
143
  st.rerun()
140
- st.warning(result.message)
144
+ st.warning(posts_message)
141
145
 
142
146
 
143
147
  def _api_card_defaults(kind: str) -> tuple[str, list[str], Callable[[Mapping[str, Any]], dict[str, Any]]]:
@@ -291,6 +295,7 @@ def _characters(settings: Mapping[str, Any]) -> tuple[list[dict[str, Any]], Any
291
295
  def _save_public_profile(data: Mapping[str, Any], *, country: str, language: str, character_id: str = "") -> dict[str, Any]:
292
296
  name = _clean(data.get("name")) or _clean(data.get("username")) or "Conta Instagram"
293
297
  url = _clean(data.get("url"))
298
+ selected_country = _clean(country) if _clean(country) in COUNTRY_OPTIONS else ""
294
299
  metadata = {
295
300
  **dict(data),
296
301
  "platform": "instagram",
@@ -298,8 +303,9 @@ def _save_public_profile(data: Mapping[str, Any], *, country: str, language: str
298
303
  "name": name,
299
304
  "url": url,
300
305
  "handle": _clean(data.get("handle")),
301
- "bio": normalize_instagram_bio(data.get("bio")),
302
- "country": country.strip(),
306
+ "bio": _profile_bio(data),
307
+ "bio_raw": _clean(data.get("bio_raw")),
308
+ "country": selected_country,
303
309
  "language": language.strip(),
304
310
  "post_count": data.get("post_count"),
305
311
  "subscriber_count": data.get("subscriber_count"),
@@ -313,12 +319,27 @@ def _save_public_profile(data: Mapping[str, Any], *, country: str, language: str
313
319
 
314
320
  def _merge_instagram_refresh(existing: Mapping[str, Any], refreshed: Mapping[str, Any]) -> dict[str, Any]:
315
321
  merged = dict(refreshed)
316
- for field in ("subscriber_count", "following_count", "post_count", "bio", "avatar_url"):
322
+ for field in ("subscriber_count", "following_count", "post_count", "bio", "bio_raw", "avatar_url"):
317
323
  if merged.get(field) in (None, "") and existing.get(field) not in (None, ""):
318
324
  merged[field] = existing[field]
319
325
  return merged
320
326
 
321
327
 
328
+ def _refresh_instagram_profile(profile: Mapping[str, Any]) -> tuple[bool, str, dict[str, Any]]:
329
+ source = _clean(profile.get("url")) or _clean(profile.get("handle"))
330
+ result = fetch_public_instagram_profile(source)
331
+ if not result.ok:
332
+ return False, result.message, {}
333
+ return True, result.message, _merge_instagram_refresh(profile, result.data)
334
+
335
+
336
+ def _load_instagram_posts(profile: Mapping[str, Any], limit: int = 10) -> tuple[bool, str, list[dict[str, Any]]]:
337
+ source = _clean(profile.get("url")) or _clean(profile.get("handle"))
338
+ result = fetch_public_instagram_posts(source, limit=limit)
339
+ posts = result.data.get("posts", []) if isinstance(result.data, dict) else []
340
+ return result.ok, result.message, posts
341
+
342
+
322
343
  def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, Any]], settings: Mapping[str, Any]) -> None:
323
344
  profile_id = _clean(profile.get("id"))
324
345
  edit_key = f"edit_instagram_profile_{profile_id}"
@@ -332,7 +353,7 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
332
353
  with header_cols[1]:
333
354
  st.write(f"**{_clean(profile.get('name')) or 'Sem nome'}**")
334
355
  st.caption(f"{_clean(profile.get('handle')) or _clean(profile.get('url')) or 'sem handler'}")
335
- _render_instagram_bio(profile.get("bio"))
356
+ _render_instagram_bio(_profile_bio(profile))
336
357
  st.caption(f"{_clean(profile.get('country')) or 'País não definido'} · {_clean(profile.get('language')) or 'Idioma não definido'}")
337
358
  with header_cols[2]:
338
359
  st.metric("posts", _metric(profile.get("post_count")))
@@ -344,13 +365,17 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
344
365
  refresh_col, edit_col = st.columns(2)
345
366
  with refresh_col:
346
367
  if st.button("↻", help="Actualizar posts, seguidores e seguindo", key=f"refresh_instagram_{profile_id}"):
347
- result = fetch_public_instagram_profile(_clean(profile.get("url")) or _clean(profile.get("handle")))
348
- if result.ok:
349
- refreshed = _merge_instagram_refresh(profile, result.data)
368
+ refreshed_ok, refreshed_message, refreshed = _refresh_instagram_profile(profile)
369
+ if refreshed_ok:
370
+ posts_key = _instagram_posts_key(profile_id)
371
+ current_posts = st.session_state.get(posts_key, [])
372
+ posts_ok, _, refreshed_posts = _load_instagram_posts(refreshed, limit=max(10, len(current_posts)))
373
+ if posts_ok:
374
+ st.session_state[posts_key] = refreshed_posts
350
375
  update_channel(profile_id, {**refreshed, "platform": "instagram", "country": profile.get("country", ""), "language": profile.get("language", ""), "character_id": profile.get("character_id", "")})
351
376
  st.success("Métricas Instagram actualizadas.")
352
377
  st.rerun()
353
- st.warning(result.message)
378
+ st.warning(refreshed_message)
354
379
  with edit_col:
355
380
  if st.button("Editar", key=f"edit_instagram_button_{profile_id}", use_container_width=True):
356
381
  st.session_state[edit_key] = True
@@ -382,7 +407,7 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
382
407
  with edit_cols[0]:
383
408
  name = st.text_input("Nome", value=_clean(profile.get("name")), key=f"instagram_edit_name_{profile_id}")
384
409
  handle = st.text_input("handler", value=_clean(profile.get("handle")), key=f"instagram_edit_handle_{profile_id}")
385
- bio = st.text_area("Bio", value=normalize_instagram_bio(profile.get("bio")), key=f"instagram_edit_bio_{profile_id}")
410
+ bio = st.text_area("Bio", value=_profile_bio(profile), key=f"instagram_edit_bio_{profile_id}")
386
411
  country = st.selectbox("País", _country_options(), index=_country_index(profile.get("country")), format_func=_country_label, key=f"instagram_edit_country_{profile_id}")
387
412
  language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=_language_index(profile.get("language")), format_func=language_label, key=f"instagram_edit_language_{profile_id}")
388
413
  with edit_cols[1]:
@@ -391,7 +416,7 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
391
416
  following = st.number_input("seguindo", min_value=0, value=_metric_input(profile.get("following_count")), placeholder="Não encontrado", key=f"instagram_edit_following_{profile_id}")
392
417
  save_edit = st.form_submit_button("Guardar alterações", type="primary", use_container_width=True)
393
418
  if save_edit:
394
- update_channel(profile_id, {"name": name.strip(), "handle": handle.strip(), "bio": normalize_instagram_bio(bio), "country": country.strip(), "language": language, "post_count": _metric_input(posts), "subscriber_count": _metric_input(followers), "following_count": _metric_input(following)})
419
+ update_channel(profile_id, {"name": name.strip(), "handle": handle.strip(), "bio": normalize_instagram_bio(bio), "bio_raw": bio, "country": country.strip(), "language": language, "post_count": _metric_input(posts), "subscriber_count": _metric_input(followers), "following_count": _metric_input(following)})
395
420
  st.session_state.pop(edit_key, None)
396
421
  st.success("Conta Instagram actualizada.")
397
422
  st.rerun()
@@ -451,7 +476,7 @@ def render_social_networks(settings: dict[str, Any]) -> None:
451
476
  with preview_cols[1]:
452
477
  st.write(f"**{_clean(data.get('name')) or _clean(data.get('username'))}**")
453
478
  st.caption(_clean(data.get("handle")))
454
- _render_instagram_bio(data.get("bio"))
479
+ _render_instagram_bio(_profile_bio(data))
455
480
  with preview_cols[2]:
456
481
  st.metric("posts", _metric(data.get("post_count")))
457
482
  with preview_cols[3]:
@@ -462,7 +487,7 @@ def render_social_networks(settings: dict[str, Any]) -> None:
462
487
  form_cols = st.columns(2)
463
488
  with form_cols[0]:
464
489
  name = st.text_input("Nome", value=_clean(data.get("name")), key="social_instagram_result_name")
465
- bio = st.text_area("Bio", value=normalize_instagram_bio(data.get("bio")), key="social_instagram_result_bio")
490
+ bio = st.text_area("Bio", value=_profile_bio(data), key="social_instagram_result_bio")
466
491
  country = st.selectbox("País", _country_options(), index=_country_index(data.get("country")), format_func=_country_label, key="social_instagram_result_country")
467
492
  language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=_language_index(data.get("language")), format_func=language_label, key="social_instagram_result_language")
468
493
  with form_cols[1]:
@@ -475,7 +500,7 @@ def render_social_networks(settings: dict[str, Any]) -> None:
475
500
  save_profile = st.form_submit_button("Cadastrar conta Instagram", type="primary", use_container_width=True)
476
501
  if save_profile:
477
502
  profile_data = dict(data)
478
- profile_data.update({"name": name, "bio": normalize_instagram_bio(bio), "post_count": _metric_input(posts), "subscriber_count": _metric_input(followers), "following_count": _metric_input(following)})
503
+ profile_data.update({"name": name, "bio": normalize_instagram_bio(bio), "bio_raw": bio, "post_count": _metric_input(posts), "subscriber_count": _metric_input(followers), "following_count": _metric_input(following)})
479
504
  _save_public_profile(profile_data, country=country, language=language, character_id=selected_character)
480
505
  st.success("Conta Instagram cadastrada em Redes Sociais.")
481
506
  for key in ("social_instagram_result", "social_instagram_ok", "social_instagram_message"):
@@ -37,6 +37,7 @@ def create_channel(name: str, url: str = "", metadata: dict[str, Any] | None = N
37
37
  "social_network": "YouTube",
38
38
  "handle": "",
39
39
  "bio": "",
40
+ "bio_raw": "",
40
41
  "description": "",
41
42
  "description_source": "",
42
43
  "niche": "",
@@ -25,7 +25,13 @@ def normalize_instagram_bio(value: Any) -> str:
25
25
  bio = str(value or '').strip()
26
26
  if not bio:
27
27
  return ''
28
- if re.search(r'\b(?:followers|following|seguidores|seguindo|posts|publicações)\b', bio, flags=re.IGNORECASE) and re.search(r'\d', bio):
28
+ metric_words = r'followers|following|seguidores|seguindo|posts|publicações|publications'
29
+ metric_hits = re.findall(
30
+ rf'(?:\d[\d.,]*\s*(?:{metric_words})\b|\b(?:{metric_words})\s*\d[\d.,]*)',
31
+ bio,
32
+ flags=re.IGNORECASE,
33
+ )
34
+ if len(metric_hits) >= 2:
29
35
  return ''
30
36
  return bio
31
37
 
@@ -148,11 +154,13 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
148
154
  followers = followers if followers is not None else _structured_metric_from_json(json.dumps(user), 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
149
155
  following = following if following is not None else _structured_metric_from_json(json.dumps(user), 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
150
156
  media = ((user.get('edge_owner_to_timeline_media') or {}).get('count') if isinstance(user.get('edge_owner_to_timeline_media'), dict) else None)
157
+ biography = str(user.get('biography') or '').strip()
151
158
  return {
152
159
  'id': f"instagram_{reference['username']}",
153
160
  **reference,
154
161
  'name': str(user.get('full_name') or user.get('username') or reference['username']).strip(),
155
- 'bio': normalize_instagram_bio(user.get('biography')),
162
+ 'bio': normalize_instagram_bio(biography),
163
+ 'bio_raw': biography,
156
164
  'avatar_url': str(user.get('profile_pic_url_hd') or user.get('profile_pic_url') or '').strip(),
157
165
  'subscriber_count': followers,
158
166
  'following_count': following,
@@ -214,7 +222,7 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
214
222
  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')
215
223
  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')
216
224
  posts = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:posts|publicações|publications)',)) or _structured_metric(response.text, 'edge_owner_to_timeline_media', 'posts', 'post_count')
217
- data = {'id': f"instagram_{reference['username']}", **reference, 'name': title.split('(')[0].strip() or reference['username'], 'bio': normalize_instagram_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()}
225
+ data = {'id': f"instagram_{reference['username']}", **reference, 'name': title.split('(')[0].strip() or reference['username'], 'bio': normalize_instagram_bio(description), 'bio_raw': 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()}
218
226
  return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', data)
219
227
 
220
228
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.28",
3
+ "version": "0.6.30",
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",