@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.
- package/app/social_networks_ui.py +48 -23
- package/hermes_ui/domain.py +1 -0
- package/integrations/instagram_public.py +11 -3
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
-
|
|
114
|
-
if
|
|
115
|
-
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(
|
|
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
|
-
|
|
137
|
-
if
|
|
138
|
-
st.session_state[state_key] =
|
|
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(
|
|
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":
|
|
302
|
-
"
|
|
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
|
|
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
|
-
|
|
348
|
-
if
|
|
349
|
-
|
|
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(
|
|
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=
|
|
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
|
|
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=
|
|
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"):
|
package/hermes_ui/domain.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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