@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.
- package/app/social_networks_ui.py +63 -16
- package/hermes_ui/countries.py +43 -0
- package/hermes_ui/domain.py +1 -0
- package/integrations/instagram_public.py +23 -3
- package/package.json +1 -1
|
@@ -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 =
|
|
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
|
-
|
|
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=
|
|
343
|
-
followers = st.number_input("Seguidores", min_value=0, value=
|
|
344
|
-
following = st.number_input("seguindo", min_value=0, value=
|
|
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":
|
|
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
|
-
|
|
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=
|
|
421
|
-
followers = st.number_input("Seguidores", min_value=0, value=
|
|
422
|
-
following = st.number_input("seguindo", min_value=0, value=
|
|
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":
|
|
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"]
|
package/hermes_ui/domain.py
CHANGED
|
@@ -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 =
|
|
121
|
-
following =
|
|
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