@danhachuel/thunderbolt 0.6.22 → 0.6.24

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.
@@ -8,11 +8,12 @@ from typing import Any, Callable, Mapping
8
8
  import requests
9
9
  import streamlit as st
10
10
 
11
- from hermes_ui.domain import create_channel, update_channel
11
+ from hermes_ui.domain import create_channel, delete_channel, update_channel
12
12
  from hermes_ui.influencers import InfluencerBackendError, STANDALONE_CONTENT_INFLUENCER_ID, get_repository
13
13
  from hermes_ui.storage import read_json, write_json
14
+ from hermes_ui.countries import COUNTRY_OPTIONS
14
15
  from hermes_ui.languages import LANGUAGE_CODES, language_code, language_label
15
- from integrations.instagram_public import fetch_public_instagram_posts, fetch_public_instagram_profile, normalize_instagram_bio
16
+ from integrations.instagram_public import fetch_public_instagram_posts, fetch_public_instagram_profile, normalize_instagram_bio, normalize_instagram_metric
16
17
  from integrations.meta_social import test_facebook_pages_api_card, test_instagram_api_card
17
18
 
18
19
 
@@ -37,11 +38,29 @@ def _profile_metric(profile: Mapping[str, Any], *keys: str) -> Any:
37
38
  return None
38
39
 
39
40
 
41
+ def _metric_input(value: Any) -> int | None:
42
+ return normalize_instagram_metric(value)
43
+
44
+
40
45
  def _language_index(value: Any) -> int:
41
46
  code = language_code(value)
42
47
  return list(LANGUAGE_CODES).index(code) if code in LANGUAGE_CODES else list(LANGUAGE_CODES).index("pt")
43
48
 
44
49
 
50
+ def _country_options() -> tuple[str, ...]:
51
+ return ("",) + COUNTRY_OPTIONS
52
+
53
+
54
+ def _country_index(value: Any) -> int:
55
+ options = _country_options()
56
+ value = _clean(value)
57
+ return options.index(value) if value in options else 0
58
+
59
+
60
+ def _country_label(value: str) -> str:
61
+ return value or "Não definido"
62
+
63
+
45
64
  def _instagram_posts_key(profile_id: str) -> str:
46
65
  return f"instagram_posts_{profile_id}"
47
66
 
@@ -273,6 +292,7 @@ def _save_public_profile(data: Mapping[str, Any], *, country: str, language: str
273
292
  "name": name,
274
293
  "url": url,
275
294
  "handle": _clean(data.get("handle")),
295
+ "bio": normalize_instagram_bio(data.get("bio")),
276
296
  "country": country.strip(),
277
297
  "language": language.strip(),
278
298
  "post_count": data.get("post_count"),
@@ -285,6 +305,14 @@ def _save_public_profile(data: Mapping[str, Any], *, country: str, language: str
285
305
  return create_channel(name, url, metadata)
286
306
 
287
307
 
308
+ def _merge_instagram_refresh(existing: Mapping[str, Any], refreshed: Mapping[str, Any]) -> dict[str, Any]:
309
+ merged = dict(refreshed)
310
+ for field in ("subscriber_count", "following_count", "post_count", "bio", "avatar_url"):
311
+ if merged.get(field) in (None, "") and existing.get(field) not in (None, ""):
312
+ merged[field] = existing[field]
313
+ return merged
314
+
315
+
288
316
  def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, Any]], settings: Mapping[str, Any]) -> None:
289
317
  profile_id = _clean(profile.get("id"))
290
318
  edit_key = f"edit_instagram_profile_{profile_id}"
@@ -314,10 +342,7 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
314
342
  if st.button("↻", help="Actualizar posts, seguidores e seguindo", key=f"refresh_instagram_{profile_id}"):
315
343
  result = fetch_public_instagram_profile(_clean(profile.get("url")) or _clean(profile.get("handle")))
316
344
  if result.ok:
317
- refreshed = dict(result.data)
318
- for field in ("subscriber_count", "following_count", "post_count", "bio", "avatar_url"):
319
- if refreshed.get(field) in (None, "") and profile.get(field) not in (None, ""):
320
- refreshed[field] = profile[field]
345
+ refreshed = _merge_instagram_refresh(profile, result.data)
321
346
  update_channel(profile_id, {**refreshed, "platform": "instagram", "country": profile.get("country", ""), "language": profile.get("language", ""), "character_id": profile.get("character_id", "")})
322
347
  st.success("Métricas Instagram actualizadas.")
323
348
  st.rerun()
@@ -326,25 +351,43 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
326
351
  if st.button("Editar", key=f"edit_instagram_button_{profile_id}", use_container_width=True):
327
352
  st.session_state[edit_key] = True
328
353
  st.rerun()
354
+ if st.button("Apagar", key=f"delete_instagram_button_{profile_id}", use_container_width=True):
355
+ st.session_state[f"confirm_delete_instagram_{profile_id}"] = True
356
+ st.rerun()
329
357
  url = _clean(profile.get("url"))
330
358
  if url:
331
359
  st.link_button("Abrir Instagram", url, use_container_width=True)
332
360
 
361
+ if st.session_state.get(f"confirm_delete_instagram_{profile_id}"):
362
+ st.warning("Apagar este card remove o cadastro da conta Instagram, mas não os ficheiros associados.")
363
+ confirm_cols = st.columns(2)
364
+ with confirm_cols[0]:
365
+ if st.button("Confirmar apagar", type="primary", key=f"confirm_delete_instagram_button_{profile_id}", use_container_width=True):
366
+ delete_channel(profile_id)
367
+ st.session_state.pop(f"confirm_delete_instagram_{profile_id}", None)
368
+ st.success("Conta Instagram apagada.")
369
+ st.rerun()
370
+ with confirm_cols[1]:
371
+ if st.button("Cancelar", key=f"cancel_delete_instagram_button_{profile_id}", use_container_width=True):
372
+ st.session_state.pop(f"confirm_delete_instagram_{profile_id}", None)
373
+ st.rerun()
374
+
333
375
  if st.session_state.get(edit_key):
334
376
  with st.form(f"edit_instagram_profile_form_{profile_id}"):
335
377
  edit_cols = st.columns(2)
336
378
  with edit_cols[0]:
337
379
  name = st.text_input("Nome", value=_clean(profile.get("name")), key=f"instagram_edit_name_{profile_id}")
338
380
  handle = st.text_input("handler", value=_clean(profile.get("handle")), key=f"instagram_edit_handle_{profile_id}")
339
- country = st.text_input("País", value=_clean(profile.get("country")), key=f"instagram_edit_country_{profile_id}")
381
+ bio = st.text_area("Bio", value=normalize_instagram_bio(profile.get("bio")), key=f"instagram_edit_bio_{profile_id}")
382
+ country = st.selectbox("País", _country_options(), index=_country_index(profile.get("country")), format_func=_country_label, key=f"instagram_edit_country_{profile_id}")
340
383
  language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=_language_index(profile.get("language")), format_func=language_label, key=f"instagram_edit_language_{profile_id}")
341
384
  with edit_cols[1]:
342
- posts = st.number_input("posts", min_value=0, value=int(profile.get("post_count") or 0), key=f"instagram_edit_posts_{profile_id}")
343
- followers = st.number_input("Seguidores", min_value=0, value=int(profile.get("subscriber_count") or 0), key=f"instagram_edit_followers_{profile_id}")
344
- following = st.number_input("seguindo", min_value=0, value=int(profile.get("following_count") or 0), key=f"instagram_edit_following_{profile_id}")
385
+ posts = st.number_input("posts", min_value=0, value=_metric_input(profile.get("post_count")), placeholder="Não encontrado", key=f"instagram_edit_posts_{profile_id}")
386
+ followers = st.number_input("Seguidores", min_value=0, value=_metric_input(profile.get("subscriber_count")), placeholder="Não encontrado", key=f"instagram_edit_followers_{profile_id}")
387
+ 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}")
345
388
  save_edit = st.form_submit_button("Guardar alterações", type="primary", use_container_width=True)
346
389
  if save_edit:
347
- update_channel(profile_id, {"name": name.strip(), "handle": handle.strip(), "country": country.strip(), "language": language, "post_count": int(posts), "subscriber_count": int(followers), "following_count": int(following)})
390
+ 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)})
348
391
  st.session_state.pop(edit_key, None)
349
392
  st.success("Conta Instagram actualizada.")
350
393
  st.rerun()
@@ -404,6 +447,9 @@ def render_social_networks(settings: dict[str, Any]) -> None:
404
447
  with preview_cols[1]:
405
448
  st.write(f"**{_clean(data.get('name')) or _clean(data.get('username'))}**")
406
449
  st.caption(_clean(data.get("handle")))
450
+ bio = normalize_instagram_bio(data.get("bio"))
451
+ if bio:
452
+ st.caption(f"Bio: {bio}")
407
453
  with preview_cols[2]:
408
454
  st.metric("posts", _metric(data.get("post_count")))
409
455
  with preview_cols[3]:
@@ -414,19 +460,20 @@ def render_social_networks(settings: dict[str, Any]) -> None:
414
460
  form_cols = st.columns(2)
415
461
  with form_cols[0]:
416
462
  name = st.text_input("Nome", value=_clean(data.get("name")), key="social_instagram_result_name")
417
- country = st.text_input("País", value=_clean(data.get("country")), key="social_instagram_result_country")
463
+ bio = st.text_area("Bio", value=normalize_instagram_bio(data.get("bio")), key="social_instagram_result_bio")
464
+ country = st.selectbox("País", _country_options(), index=_country_index(data.get("country")), format_func=_country_label, key="social_instagram_result_country")
418
465
  language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=_language_index(data.get("language")), format_func=language_label, key="social_instagram_result_language")
419
466
  with form_cols[1]:
420
- posts = st.number_input("posts", min_value=0, value=int(data.get("post_count") or 0), key="social_instagram_result_posts")
421
- followers = st.number_input("Seguidores", min_value=0, value=int(data.get("subscriber_count") or 0), key="social_instagram_result_followers")
422
- following = st.number_input("seguindo", min_value=0, value=int(data.get("following_count") or 0), key="social_instagram_result_following")
467
+ posts = st.number_input("posts", min_value=0, value=_metric_input(data.get("post_count")), placeholder="Não encontrado", key="social_instagram_result_posts")
468
+ followers = st.number_input("Seguidores", min_value=0, value=_metric_input(data.get("subscriber_count")), placeholder="Não encontrado", key="social_instagram_result_followers")
469
+ following = st.number_input("seguindo", min_value=0, value=_metric_input(data.get("following_count")), placeholder="Não encontrado", key="social_instagram_result_following")
423
470
  character_options = [""] + [_clean(item.get("id")) for item in characters if _clean(item.get("id"))]
424
471
  character_labels = {"": "Sem personagem associado"} | {_clean(item.get("id")): _clean(item.get("name")) or _clean(item.get("id")) for item in characters}
425
472
  selected_character = st.selectbox("Personagem", character_options, format_func=lambda value: character_labels.get(value, value), key="social_instagram_result_character")
426
473
  save_profile = st.form_submit_button("Cadastrar conta Instagram", type="primary", use_container_width=True)
427
474
  if save_profile:
428
475
  profile_data = dict(data)
429
- profile_data.update({"name": name, "post_count": int(posts), "subscriber_count": int(followers), "following_count": int(following)})
476
+ 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)})
430
477
  _save_public_profile(profile_data, country=country, language=language, character_id=selected_character)
431
478
  st.success("Conta Instagram cadastrada em Redes Sociais.")
432
479
  for key in ("social_instagram_result", "social_instagram_ok", "social_instagram_message"):
@@ -0,0 +1,43 @@
1
+ """Canonical country catalog for social-account metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ COUNTRY_OPTIONS: tuple[str, ...] = (
6
+ "Abecásia", "Afeganistão", "África do Sul", "Albânia", "Alemanha", "Andorra", "Angola",
7
+ "Antígua e Barbuda", "Arábia Saudita", "Argélia", "Argentina", "Armênia", "Austrália", "Áustria",
8
+ "Azerbaijão", "Bahamas", "Bahrein", "Bangladesh", "Barbados", "Bélgica", "Belize", "Benim",
9
+ "Bielorrússia", "Bolívia", "Bósnia e Herzegovina", "Botsuana", "Brasil", "Brunei", "Bulgária",
10
+ "Burquina Fasso", "Burundi", "Butão", "Cabo Verde", "Camarões", "Camboja", "Canadá", "Catar",
11
+ "Cazaquistão", "Chade", "Chile", "China", "Chipre", "Chipre do Norte", "Cingapura", "Colômbia",
12
+ "Comores", "Congo", "Coreia do Norte", "Coreia do Sul", "Costa do Marfim", "Costa Rica", "Croácia",
13
+ "Cuba", "Dinamarca", "Djibouti", "Dominica", "República Dominicana", "Egito", "El Salvador",
14
+ "Emirados Árabes Unidos", "Equador", "Eritreia", "Eslováquia", "Eslovênia", "Espanha",
15
+ "Estados Federados da Micronésia", "Estados Unidos", "Estônia", "Essuatíni (Suazilândia)", "Etiópia",
16
+ "Fiji", "Filipinas", "Finlândia", "França", "Gabão", "Gâmbia", "Gana", "Geórgia", "Granada",
17
+ "Grécia", "Guatemala", "Guiana", "Guiné", "Guiné-Bissau", "Guiné Equatorial", "Haiti", "Honduras",
18
+ "Hungria", "Iêmen", "Ilhas Cook", "Índia", "Indonésia", "Irã", "Irlanda", "Islândia", "Israel",
19
+ "Itália", "Jamaica", "Japão", "Jordânia", "Kiribati", "Kosovo", "Kuwait", "Laos", "Lesoto",
20
+ "Letônia", "Líbano", "Libéria", "Líbia", "Liechtenstein", "Lituânia", "Luxemburgo", "Macedônia do Norte",
21
+ "Madagascar", "Malásia", "Malawi", "Maldivas", "Mali", "Malta", "Marrocos", "Ilhas Marshall",
22
+ "Maurício", "Mauritânia", "México", "Mianmar", "Moçambique", "Moldávia", "Mônaco", "Mongólia",
23
+ "Montenegro", "Namíbia", "Nauru", "Nepal", "Nicarágua", "Níger", "Nigéria", "Niue", "Noruega",
24
+ "Nova Zelândia", "Omã", "Ossétia do Sul", "Países Baixos", "Palau", "Palestina", "Panamá",
25
+ "Papua-Nova Guiné", "Paquistão", "Paraguai", "Peru", "Polônia", "Portugal", "Quênia", "Quirguistão",
26
+ "Reino Unido", "República Centro-Africana", "República Checa", "República Democrática do Congo", "Romênia",
27
+ "Ruanda", "Rússia", "Saara Ocidental (República Árabe Saharaui Democrática)", "Samoa", "San Marino",
28
+ "Santa Lúcia", "São Cristóvão e Nevis", "São Tomé e Príncipe", "São Vicente e Granadinas", "Senegal",
29
+ "Serra Leoa", "Sérvia", "Seicheles", "Somália", "Somalilândia", "Sri Lanka", "Sudão", "Sudão do Sul",
30
+ "Suécia", "Suíça", "Suriname", "Síria", "Tajiquistão", "Tailândia", "Taiwan", "Tanzânia",
31
+ "Timor-Leste", "Togo", "Tonga", "Transnístria", "Trinidad e Tobago", "Tunísia", "Turcomenistão",
32
+ "Turquia", "Tuvalu", "Ucrânia", "Uganda", "Uruguai", "Uzbequistão", "Vanuatu", "Vaticano (Santa Sé)",
33
+ "Venezuela", "Vietnã", "Zâmbia", "Zimbábue",
34
+ )
35
+
36
+
37
+ def country_index(value: object) -> int:
38
+ """Return the selector index while keeping legacy/empty values usable."""
39
+ value = str(value or "").strip()
40
+ return COUNTRY_OPTIONS.index(value) if value in COUNTRY_OPTIONS else 0
41
+
42
+
43
+ __all__ = ["COUNTRY_OPTIONS", "country_index"]
@@ -36,6 +36,7 @@ def create_channel(name: str, url: str = "", metadata: dict[str, Any] | None = N
36
36
  "platform": "youtube",
37
37
  "social_network": "YouTube",
38
38
  "handle": "",
39
+ "bio": "",
39
40
  "description": "",
40
41
  "description_source": "",
41
42
  "niche": "",
@@ -117,8 +117,10 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
117
117
 
118
118
 
119
119
  def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]) -> dict[str, Any]:
120
- followers = _structured_metric_from_json(json.dumps(user), 'edge_followed_by', 'followers', 'follower_count')
121
- following = _structured_metric_from_json(json.dumps(user), 'edge_follow', 'follows', 'following', 'following_count')
120
+ followers = _profile_metric(user, 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
121
+ following = _profile_metric(user, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
122
+ followers = followers if followers is not None else _structured_metric_from_json(json.dumps(user), 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
123
+ following = following if following is not None else _structured_metric_from_json(json.dumps(user), 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
122
124
  media = ((user.get('edge_owner_to_timeline_media') or {}).get('count') if isinstance(user.get('edge_owner_to_timeline_media'), dict) else None)
123
125
  return {
124
126
  'id': f"instagram_{reference['username']}",
@@ -136,6 +138,24 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
136
138
  }
137
139
 
138
140
 
141
+ def normalize_instagram_metric(value: Any) -> int | None:
142
+ """Normalize an Instagram counter without turning an unknown value into zero."""
143
+ if isinstance(value, Mapping):
144
+ value = value.get('count') or value.get('value')
145
+ if value in (None, ''):
146
+ return None
147
+ digits = re.sub(r'[^0-9]', '', str(value))
148
+ return int(digits) if digits else None
149
+
150
+
151
+ def _profile_metric(user: Mapping[str, Any], *keys: str) -> int | None:
152
+ for key in keys:
153
+ value = normalize_instagram_metric(user.get(key))
154
+ if value is not None:
155
+ return value
156
+ return None
157
+
158
+
139
159
  def fetch_public_instagram_profile(source: str) -> IntegrationResult:
140
160
  try:
141
161
  reference = normalize_instagram_reference(source)
@@ -266,4 +286,4 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
266
286
  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))]})
267
287
 
268
288
 
269
- __all__ = ['IntegrationResult', 'fetch_public_instagram_posts', 'fetch_public_instagram_profile', 'normalize_instagram_bio', 'normalize_instagram_reference']
289
+ __all__ = ['IntegrationResult', 'fetch_public_instagram_posts', 'fetch_public_instagram_profile', 'normalize_instagram_bio', 'normalize_instagram_metric', 'normalize_instagram_reference']
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.22",
3
+ "version": "0.6.24",
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",