@danhachuel/thunderbolt 0.6.31 → 0.6.33
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 +172 -47
- package/hermes_ui/domain.py +1 -1
- package/integrations/instagram_public.py +48 -2
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
|
+
import re
|
|
4
5
|
import uuid
|
|
5
6
|
import zipfile
|
|
7
|
+
import unicodedata
|
|
6
8
|
from typing import Any, Callable, Mapping
|
|
7
9
|
|
|
8
10
|
import requests
|
|
@@ -33,8 +35,9 @@ def _metric(value: Any) -> str:
|
|
|
33
35
|
def _profile_metric(profile: Mapping[str, Any], *keys: str) -> Any:
|
|
34
36
|
for key in keys:
|
|
35
37
|
value = profile.get(key)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
normalized = normalize_instagram_metric(value)
|
|
39
|
+
if normalized is not None:
|
|
40
|
+
return normalized
|
|
38
41
|
return None
|
|
39
42
|
|
|
40
43
|
|
|
@@ -49,7 +52,48 @@ def _render_instagram_bio(value: Any) -> None:
|
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
def _profile_bio(profile: Mapping[str, Any]) -> str:
|
|
52
|
-
|
|
55
|
+
for key in ("bio", "bio_raw", "biography", "description"):
|
|
56
|
+
bio = normalize_instagram_bio(profile.get(key))
|
|
57
|
+
if bio:
|
|
58
|
+
return bio
|
|
59
|
+
return ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _country_key(value: Any) -> str:
|
|
63
|
+
normalized = unicodedata.normalize("NFKD", _clean(value))
|
|
64
|
+
return "".join(char for char in normalized if not unicodedata.combining(char)).casefold()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
COUNTRY_ALIASES = {
|
|
68
|
+
"brazil": "Brasil",
|
|
69
|
+
"brasil": "Brasil",
|
|
70
|
+
"usa": "Estados Unidos",
|
|
71
|
+
"us": "Estados Unidos",
|
|
72
|
+
"united states": "Estados Unidos",
|
|
73
|
+
"united states of america": "Estados Unidos",
|
|
74
|
+
"portugal": "Portugal",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _normalise_country(value: Any) -> str:
|
|
79
|
+
clean_value = _clean(value)
|
|
80
|
+
if not clean_value:
|
|
81
|
+
return ""
|
|
82
|
+
if clean_value in COUNTRY_OPTIONS:
|
|
83
|
+
return clean_value
|
|
84
|
+
key = _country_key(clean_value)
|
|
85
|
+
for country in COUNTRY_OPTIONS:
|
|
86
|
+
if _country_key(country) == key:
|
|
87
|
+
return country
|
|
88
|
+
return COUNTRY_ALIASES.get(key, "")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _profile_country(profile: Mapping[str, Any]) -> str:
|
|
92
|
+
for key in ("country", "country_name", "country_of_registration", "account_country", "account_based_in"):
|
|
93
|
+
country = _normalise_country(profile.get(key))
|
|
94
|
+
if country:
|
|
95
|
+
return country
|
|
96
|
+
return ""
|
|
53
97
|
|
|
54
98
|
|
|
55
99
|
def _language_index(value: Any) -> int:
|
|
@@ -63,7 +107,7 @@ def _country_options() -> tuple[str, ...]:
|
|
|
63
107
|
|
|
64
108
|
def _country_index(value: Any) -> int:
|
|
65
109
|
options = _country_options()
|
|
66
|
-
value =
|
|
110
|
+
value = _normalise_country(value)
|
|
67
111
|
return options.index(value) if value in options else 0
|
|
68
112
|
|
|
69
113
|
|
|
@@ -75,6 +119,46 @@ def _instagram_posts_key(profile_id: str) -> str:
|
|
|
75
119
|
return f"instagram_posts_{profile_id}"
|
|
76
120
|
|
|
77
121
|
|
|
122
|
+
def _instagram_profile_storage_id(profile: Mapping[str, Any]) -> str:
|
|
123
|
+
value = _clean(profile.get("id")) or _clean(profile.get("username")) or _clean(profile.get("handle")).lstrip("@") or _clean(profile.get("url"))
|
|
124
|
+
return re.sub(r"[^A-Za-z0-9_.-]+", "_", value) or "unknown"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _normalise_instagram_posts(value: Any) -> list[dict[str, Any]]:
|
|
128
|
+
if not isinstance(value, list):
|
|
129
|
+
return []
|
|
130
|
+
return [dict(post) for post in value if isinstance(post, Mapping) and _clean(post.get("id") or post.get("url"))]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _stored_instagram_posts(profile: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
134
|
+
profile_id = _instagram_profile_storage_id(profile)
|
|
135
|
+
state_key = _instagram_posts_key(profile_id)
|
|
136
|
+
posts = _normalise_instagram_posts(st.session_state.get(state_key, []))
|
|
137
|
+
if posts:
|
|
138
|
+
return posts
|
|
139
|
+
for key in ("instagram_posts", "recent_posts", "posts"):
|
|
140
|
+
posts = _normalise_instagram_posts(profile.get(key))
|
|
141
|
+
if posts:
|
|
142
|
+
st.session_state[state_key] = posts
|
|
143
|
+
return posts
|
|
144
|
+
stored = read_json("instagram_posts.json", {})
|
|
145
|
+
if isinstance(stored, dict):
|
|
146
|
+
posts = _normalise_instagram_posts(stored.get(profile_id))
|
|
147
|
+
if posts:
|
|
148
|
+
st.session_state[state_key] = posts
|
|
149
|
+
return posts
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _persist_instagram_posts(profile: Mapping[str, Any], posts: list[dict[str, Any]]) -> None:
|
|
153
|
+
profile_id = _instagram_profile_storage_id(profile)
|
|
154
|
+
stored = read_json("instagram_posts.json", {})
|
|
155
|
+
if not isinstance(stored, dict):
|
|
156
|
+
stored = {}
|
|
157
|
+
stored[profile_id] = _normalise_instagram_posts(posts)
|
|
158
|
+
write_json("instagram_posts.json", stored)
|
|
159
|
+
st.session_state[_instagram_posts_key(profile_id)] = stored[profile_id]
|
|
160
|
+
|
|
161
|
+
|
|
78
162
|
def _instagram_posts_archive(posts: list[dict[str, Any]]) -> bytes:
|
|
79
163
|
archive = io.BytesIO()
|
|
80
164
|
with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as bundle:
|
|
@@ -100,9 +184,9 @@ def _instagram_posts_archive(posts: list[dict[str, Any]]) -> bytes:
|
|
|
100
184
|
|
|
101
185
|
|
|
102
186
|
def _render_instagram_posts(profile: Mapping[str, Any]) -> None:
|
|
103
|
-
profile_id =
|
|
187
|
+
profile_id = _instagram_profile_storage_id(profile)
|
|
104
188
|
state_key = _instagram_posts_key(profile_id)
|
|
105
|
-
posts =
|
|
189
|
+
posts = _stored_instagram_posts(profile)
|
|
106
190
|
with st.expander("Últimos posts do Instagram", expanded=False):
|
|
107
191
|
action_cols = st.columns(3)
|
|
108
192
|
with action_cols[0]:
|
|
@@ -116,7 +200,7 @@ def _render_instagram_posts(profile: Mapping[str, Any]) -> None:
|
|
|
116
200
|
posts_ok, posts_message, loaded_posts = _load_instagram_posts(profile, limit=target)
|
|
117
201
|
if posts_ok:
|
|
118
202
|
posts = loaded_posts
|
|
119
|
-
|
|
203
|
+
_persist_instagram_posts(profile, posts)
|
|
120
204
|
st.success(f"{len(posts)} posts carregados.")
|
|
121
205
|
st.rerun()
|
|
122
206
|
else:
|
|
@@ -139,7 +223,7 @@ def _render_instagram_posts(profile: Mapping[str, Any]) -> None:
|
|
|
139
223
|
if st.button("Mostrar + 10", key=f"more_instagram_posts_{profile_id}", use_container_width=True):
|
|
140
224
|
posts_ok, posts_message, loaded_posts = _load_instagram_posts(profile, limit=len(posts) + 10)
|
|
141
225
|
if posts_ok:
|
|
142
|
-
|
|
226
|
+
_persist_instagram_posts(profile, loaded_posts)
|
|
143
227
|
st.rerun()
|
|
144
228
|
st.warning(posts_message)
|
|
145
229
|
|
|
@@ -292,24 +376,55 @@ def _characters(settings: Mapping[str, Any]) -> tuple[list[dict[str, Any]], Any
|
|
|
292
376
|
return [], None
|
|
293
377
|
|
|
294
378
|
|
|
379
|
+
def _canonical_instagram_metrics(profile: Mapping[str, Any]) -> dict[str, int | None]:
|
|
380
|
+
return {
|
|
381
|
+
"post_count": _profile_metric(profile, "post_count", "posts", "media_count"),
|
|
382
|
+
"subscriber_count": _profile_metric(profile, "subscriber_count", "followers_count", "follower_count", "followers", "edge_followed_by"),
|
|
383
|
+
"following_count": _profile_metric(profile, "following_count", "following", "follows", "followingCount", "edge_follow"),
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _canonical_instagram_profile(data: Mapping[str, Any], existing: Mapping[str, Any] | None = None) -> dict[str, Any]:
|
|
388
|
+
current = dict(existing or {})
|
|
389
|
+
incoming = dict(data)
|
|
390
|
+
merged = {**current, **incoming}
|
|
391
|
+
for field, value in _canonical_instagram_metrics(incoming).items():
|
|
392
|
+
if value is not None:
|
|
393
|
+
merged[field] = value
|
|
394
|
+
elif field not in merged or merged.get(field) in (None, ""):
|
|
395
|
+
merged[field] = _canonical_instagram_metrics(current).get(field)
|
|
396
|
+
bio = _profile_bio(incoming) or _profile_bio(current)
|
|
397
|
+
if bio:
|
|
398
|
+
merged["bio"] = bio
|
|
399
|
+
raw_bio = _clean(incoming.get("bio_raw")) or _clean(incoming.get("biography")) or _clean(current.get("bio_raw"))
|
|
400
|
+
if raw_bio:
|
|
401
|
+
merged["bio_raw"] = raw_bio
|
|
402
|
+
country = _profile_country(incoming) or _profile_country(current)
|
|
403
|
+
if country:
|
|
404
|
+
merged["country"] = country
|
|
405
|
+
return merged
|
|
406
|
+
|
|
407
|
+
|
|
295
408
|
def _save_public_profile(data: Mapping[str, Any], *, country: str, language: str, character_id: str = "") -> dict[str, Any]:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
409
|
+
canonical = _canonical_instagram_profile(data)
|
|
410
|
+
name = _clean(canonical.get("name")) or _clean(canonical.get("username")) or "Conta Instagram"
|
|
411
|
+
url = _clean(canonical.get("url"))
|
|
412
|
+
metrics = _canonical_instagram_metrics(canonical)
|
|
413
|
+
bio = _profile_bio(canonical)
|
|
299
414
|
metadata = {
|
|
300
|
-
**
|
|
415
|
+
**canonical,
|
|
301
416
|
"platform": "instagram",
|
|
302
417
|
"social_network": "Instagram",
|
|
303
418
|
"name": name,
|
|
304
419
|
"url": url,
|
|
305
|
-
"handle": _clean(
|
|
306
|
-
"bio":
|
|
307
|
-
"bio_raw": _clean(data.get("bio_raw")),
|
|
308
|
-
"country":
|
|
420
|
+
"handle": _clean(canonical.get("handle")) or _clean(canonical.get("username")),
|
|
421
|
+
"bio": bio,
|
|
422
|
+
"bio_raw": _clean(data.get("bio_raw")) or _clean(canonical.get("bio_raw")) or bio,
|
|
423
|
+
"country": _normalise_country(country) or _profile_country(canonical),
|
|
309
424
|
"language": language.strip(),
|
|
310
|
-
"post_count":
|
|
311
|
-
"subscriber_count":
|
|
312
|
-
"following_count":
|
|
425
|
+
"post_count": metrics["post_count"],
|
|
426
|
+
"subscriber_count": metrics["subscriber_count"],
|
|
427
|
+
"following_count": metrics["following_count"],
|
|
313
428
|
"character_id": character_id.strip(),
|
|
314
429
|
"active": True,
|
|
315
430
|
"metrics_source": "instagram_public_page",
|
|
@@ -318,10 +433,10 @@ def _save_public_profile(data: Mapping[str, Any], *, country: str, language: str
|
|
|
318
433
|
|
|
319
434
|
|
|
320
435
|
def _merge_instagram_refresh(existing: Mapping[str, Any], refreshed: Mapping[str, Any]) -> dict[str, Any]:
|
|
321
|
-
merged =
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
436
|
+
merged = _canonical_instagram_profile(refreshed, existing)
|
|
437
|
+
merged["id"] = existing.get("id") or refreshed.get("id")
|
|
438
|
+
merged["platform"] = "instagram"
|
|
439
|
+
merged["social_network"] = "Instagram"
|
|
325
440
|
return merged
|
|
326
441
|
|
|
327
442
|
|
|
@@ -354,30 +469,38 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
|
|
|
354
469
|
st.write(f"**{_clean(profile.get('name')) or 'Sem nome'}**")
|
|
355
470
|
st.caption(f"{_clean(profile.get('handle')) or _clean(profile.get('url')) or 'sem handler'}")
|
|
356
471
|
_render_instagram_bio(_profile_bio(profile))
|
|
357
|
-
st.caption(f"{
|
|
472
|
+
st.caption(f"{_profile_country(profile) or 'País não definido'} · {_clean(profile.get('language')) or 'Idioma não definido'}")
|
|
358
473
|
with header_cols[2]:
|
|
359
|
-
st.metric("posts", _metric(profile
|
|
474
|
+
st.metric("posts", _metric(_profile_metric(profile, "post_count", "posts", "media_count")))
|
|
360
475
|
with header_cols[3]:
|
|
361
|
-
|
|
476
|
+
followers = _profile_metric(profile, "subscriber_count", "followers_count", "follower_count")
|
|
477
|
+
followers = followers if followers is not None else _profile_metric(profile, "followers", "edge_followed_by")
|
|
478
|
+
st.metric("Seguidores", _metric(followers))
|
|
362
479
|
with header_cols[4]:
|
|
363
|
-
|
|
480
|
+
following = _profile_metric(profile, "following_count", "following", "follows")
|
|
481
|
+
following = following if following is not None else _profile_metric(profile, "followingCount", "edge_follow")
|
|
482
|
+
st.metric("seguindo", _metric(following))
|
|
364
483
|
with header_cols[5]:
|
|
365
484
|
refresh_col, edit_col = st.columns(2)
|
|
366
485
|
with refresh_col:
|
|
367
|
-
if st.button("
|
|
486
|
+
if st.button("Actualizar posts, seguidores e seguindo", key=f"refresh_instagram_{profile_id}", use_container_width=True):
|
|
368
487
|
refreshed_ok, refreshed_message, refreshed = _refresh_instagram_profile(profile)
|
|
369
488
|
if refreshed_ok:
|
|
370
|
-
|
|
371
|
-
current_posts = st.session_state.get(posts_key, [])
|
|
489
|
+
current_posts = _stored_instagram_posts(profile)
|
|
372
490
|
posts_ok, _, refreshed_posts = _load_instagram_posts(refreshed, limit=max(10, len(current_posts)))
|
|
373
491
|
if posts_ok:
|
|
374
|
-
|
|
375
|
-
update_channel(profile_id, {**refreshed, "
|
|
492
|
+
_persist_instagram_posts(profile, refreshed_posts)
|
|
493
|
+
updated = update_channel(profile_id, {**refreshed, "id": profile_id, "country": _profile_country(refreshed) or _profile_country(profile), "language": profile.get("language", ""), "character_id": profile.get("character_id", "")})
|
|
494
|
+
if updated is None:
|
|
495
|
+
st.error("A conta Instagram já não existe no armazenamento local.")
|
|
496
|
+
return
|
|
376
497
|
st.success("Métricas Instagram actualizadas.")
|
|
377
498
|
st.rerun()
|
|
378
499
|
st.warning(refreshed_message)
|
|
379
500
|
with edit_col:
|
|
380
501
|
if st.button("Editar", key=f"edit_instagram_button_{profile_id}", use_container_width=True):
|
|
502
|
+
for field in ("name", "handle", "bio", "country", "language", "posts", "followers", "following"):
|
|
503
|
+
st.session_state.pop(f"instagram_edit_{field}_{profile_id}", None)
|
|
381
504
|
st.session_state[edit_key] = True
|
|
382
505
|
st.rerun()
|
|
383
506
|
if st.button("Apagar", key=f"delete_instagram_button_{profile_id}", use_container_width=True):
|
|
@@ -408,15 +531,15 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
|
|
|
408
531
|
name = st.text_input("Nome", value=_clean(profile.get("name")), key=f"instagram_edit_name_{profile_id}")
|
|
409
532
|
handle = st.text_input("handler", value=_clean(profile.get("handle")), key=f"instagram_edit_handle_{profile_id}")
|
|
410
533
|
bio = st.text_area("Bio", value=_profile_bio(profile), key=f"instagram_edit_bio_{profile_id}")
|
|
411
|
-
country = st.selectbox("País", _country_options(), index=_country_index(profile
|
|
534
|
+
country = st.selectbox("País", _country_options(), index=_country_index(_profile_country(profile)), format_func=_country_label, key=f"instagram_edit_country_{profile_id}")
|
|
412
535
|
language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=_language_index(profile.get("language")), format_func=language_label, key=f"instagram_edit_language_{profile_id}")
|
|
413
536
|
with edit_cols[1]:
|
|
414
|
-
posts = st.number_input("posts", min_value=0, value=_metric_input(profile
|
|
415
|
-
followers = st.number_input("Seguidores", min_value=0, value=_metric_input(profile
|
|
416
|
-
following = st.number_input("seguindo", min_value=0, value=_metric_input(profile
|
|
537
|
+
posts = st.number_input("posts", min_value=0, value=_metric_input(_profile_metric(profile, "post_count", "posts", "media_count")), placeholder="Não encontrado", key=f"instagram_edit_posts_{profile_id}")
|
|
538
|
+
followers = st.number_input("Seguidores", min_value=0, value=_metric_input(_profile_metric(profile, "subscriber_count", "followers_count", "follower_count", "followers", "edge_followed_by")), placeholder="Não encontrado", key=f"instagram_edit_followers_{profile_id}")
|
|
539
|
+
following = st.number_input("seguindo", min_value=0, value=_metric_input(_profile_metric(profile, "following_count", "following", "follows", "followingCount", "edge_follow")), placeholder="Não encontrado", key=f"instagram_edit_following_{profile_id}")
|
|
417
540
|
save_edit = st.form_submit_button("Guardar alterações", type="primary", use_container_width=True)
|
|
418
541
|
if save_edit:
|
|
419
|
-
update_channel(profile_id, {"name": name.strip(), "handle": handle.strip(), "bio": normalize_instagram_bio(bio), "bio_raw": bio, "country": country
|
|
542
|
+
update_channel(profile_id, {"name": name.strip(), "handle": handle.strip(), "bio": normalize_instagram_bio(bio), "bio_raw": bio, "country": _normalise_country(country), "language": language, "post_count": _metric_input(posts), "subscriber_count": _metric_input(followers), "following_count": _metric_input(following)})
|
|
420
543
|
st.session_state.pop(edit_key, None)
|
|
421
544
|
st.success("Conta Instagram actualizada.")
|
|
422
545
|
st.rerun()
|
|
@@ -427,7 +550,7 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
|
|
|
427
550
|
with block_cols[1]:
|
|
428
551
|
st.markdown(f"**handler**\n\n{_clean(profile.get('handle')) or '—'}")
|
|
429
552
|
with block_cols[2]:
|
|
430
|
-
st.markdown(f"**País**\n\n{
|
|
553
|
+
st.markdown(f"**País**\n\n{_profile_country(profile) or '—'}")
|
|
431
554
|
with block_cols[3]:
|
|
432
555
|
st.markdown(f"**Idioma**\n\n{_clean(profile.get('language')) or '—'}")
|
|
433
556
|
|
|
@@ -459,7 +582,8 @@ def render_social_networks(settings: dict[str, Any]) -> None:
|
|
|
459
582
|
st.rerun()
|
|
460
583
|
if search_clicked:
|
|
461
584
|
result = fetch_public_instagram_profile(source)
|
|
462
|
-
|
|
585
|
+
result_data = dict(result.data) if isinstance(result.data, dict) else {}
|
|
586
|
+
st.session_state["social_instagram_result"] = result_data
|
|
463
587
|
st.session_state["social_instagram_ok"] = result.ok
|
|
464
588
|
st.session_state["social_instagram_message"] = result.message
|
|
465
589
|
if st.session_state.get("social_instagram_message"):
|
|
@@ -478,22 +602,22 @@ def render_social_networks(settings: dict[str, Any]) -> None:
|
|
|
478
602
|
st.caption(_clean(data.get("handle")))
|
|
479
603
|
_render_instagram_bio(_profile_bio(data))
|
|
480
604
|
with preview_cols[2]:
|
|
481
|
-
st.metric("posts", _metric(data
|
|
605
|
+
st.metric("posts", _metric(_profile_metric(data, "post_count", "posts", "media_count")))
|
|
482
606
|
with preview_cols[3]:
|
|
483
|
-
st.metric("Seguidores", _metric(data
|
|
607
|
+
st.metric("Seguidores", _metric(_profile_metric(data, "subscriber_count", "followers_count", "follower_count", "followers", "edge_followed_by")))
|
|
484
608
|
with preview_cols[4]:
|
|
485
|
-
st.metric("seguindo", _metric(data
|
|
609
|
+
st.metric("seguindo", _metric(_profile_metric(data, "following_count", "following", "follows", "followingCount", "edge_follow")))
|
|
486
610
|
with st.form("social_instagram_save_public_profile"):
|
|
487
611
|
form_cols = st.columns(2)
|
|
488
612
|
with form_cols[0]:
|
|
489
613
|
name = st.text_input("Nome", value=_clean(data.get("name")), key="social_instagram_result_name")
|
|
490
614
|
bio = st.text_area("Bio", value=_profile_bio(data), key="social_instagram_result_bio")
|
|
491
|
-
country = st.selectbox("País", _country_options(), index=_country_index(data
|
|
615
|
+
country = st.selectbox("País", _country_options(), index=_country_index(_profile_country(data)), format_func=_country_label, key="social_instagram_result_country")
|
|
492
616
|
language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=_language_index(data.get("language")), format_func=language_label, key="social_instagram_result_language")
|
|
493
617
|
with form_cols[1]:
|
|
494
|
-
posts = st.number_input("posts", min_value=0, value=_metric_input(data
|
|
495
|
-
followers = st.number_input("Seguidores", min_value=0, value=_metric_input(data
|
|
496
|
-
following = st.number_input("seguindo", min_value=0, value=_metric_input(data
|
|
618
|
+
posts = st.number_input("posts", min_value=0, value=_metric_input(_profile_metric(data, "post_count", "posts", "media_count")), placeholder="Não encontrado", key="social_instagram_result_posts")
|
|
619
|
+
followers = st.number_input("Seguidores", min_value=0, value=_metric_input(_profile_metric(data, "subscriber_count", "followers_count", "follower_count", "followers", "edge_followed_by")), placeholder="Não encontrado", key="social_instagram_result_followers")
|
|
620
|
+
following = st.number_input("seguindo", min_value=0, value=_metric_input(_profile_metric(data, "following_count", "following", "follows", "followingCount", "edge_follow")), placeholder="Não encontrado", key="social_instagram_result_following")
|
|
497
621
|
character_options = [""] + [_clean(item.get("id")) for item in characters if _clean(item.get("id"))]
|
|
498
622
|
character_labels = {"": "Sem personagem associado"} | {_clean(item.get("id")): _clean(item.get("name")) or _clean(item.get("id")) for item in characters}
|
|
499
623
|
selected_character = st.selectbox("Personagem", character_options, format_func=lambda value: character_labels.get(value, value), key="social_instagram_result_character")
|
|
@@ -501,7 +625,8 @@ def render_social_networks(settings: dict[str, Any]) -> None:
|
|
|
501
625
|
if save_profile:
|
|
502
626
|
profile_data = dict(data)
|
|
503
627
|
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)})
|
|
504
|
-
|
|
628
|
+
profile_data["country"] = _normalise_country(country)
|
|
629
|
+
_save_public_profile(profile_data, country=profile_data["country"], language=language, character_id=selected_character)
|
|
505
630
|
st.success("Conta Instagram cadastrada em Redes Sociais.")
|
|
506
631
|
for key in ("social_instagram_result", "social_instagram_ok", "social_instagram_message"):
|
|
507
632
|
st.session_state.pop(key, None)
|
package/hermes_ui/domain.py
CHANGED
|
@@ -87,7 +87,7 @@ def create_channel(name: str, url: str = "", metadata: dict[str, Any] | None = N
|
|
|
87
87
|
"updated_at": now(),
|
|
88
88
|
}
|
|
89
89
|
if metadata:
|
|
90
|
-
channel.update({k: v for k, v in metadata.items() if k in channel})
|
|
90
|
+
channel.update({k: v for k, v in metadata.items() if k in channel and k != "id"})
|
|
91
91
|
channels = read_json("channels.json", [])
|
|
92
92
|
channels.append(channel)
|
|
93
93
|
write_json("channels.json", channels)
|
|
@@ -157,12 +157,14 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
|
|
|
157
157
|
following = following if following is not None else _structured_metric_from_json(json.dumps(user), 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
|
|
158
158
|
media = ((user.get('edge_owner_to_timeline_media') or {}).get('count') if isinstance(user.get('edge_owner_to_timeline_media'), dict) else None)
|
|
159
159
|
biography = str(user.get('biography') or '').strip()
|
|
160
|
+
country = extract_public_instagram_country(user)
|
|
160
161
|
return {
|
|
161
162
|
'id': f"instagram_{reference['username']}",
|
|
162
163
|
**reference,
|
|
163
164
|
'name': str(user.get('full_name') or user.get('username') or reference['username']).strip(),
|
|
164
165
|
'bio': normalize_instagram_bio(biography),
|
|
165
166
|
'bio_raw': biography,
|
|
167
|
+
'country': country,
|
|
166
168
|
'avatar_url': str(user.get('profile_pic_url_hd') or user.get('profile_pic_url') or '').strip(),
|
|
167
169
|
'subscriber_count': followers,
|
|
168
170
|
'following_count': following,
|
|
@@ -185,6 +187,45 @@ def normalize_instagram_metric(value: Any) -> int | None:
|
|
|
185
187
|
return int(digits) if digits else None
|
|
186
188
|
|
|
187
189
|
|
|
190
|
+
PUBLIC_COUNTRY_KEYS = {
|
|
191
|
+
'account_based_in', 'account_country', 'country_name',
|
|
192
|
+
'country_of_origin', 'country_of_registration', 'location_country', 'transparency_country',
|
|
193
|
+
}
|
|
194
|
+
PUBLIC_TRANSPARENCY_KEYS = {'about', 'about_account', 'account_transparency', 'transparency', 'account_info'}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _country_value(value: Any) -> str:
|
|
198
|
+
if isinstance(value, Mapping):
|
|
199
|
+
for key in ('name', 'label', 'display_name', 'country', 'country_name', 'code'):
|
|
200
|
+
candidate = str(value.get(key) or '').strip()
|
|
201
|
+
if candidate and not candidate.isdigit() and len(candidate) <= 80:
|
|
202
|
+
return candidate
|
|
203
|
+
return ''
|
|
204
|
+
candidate = str(value or '').strip()
|
|
205
|
+
return candidate if candidate and not candidate.isdigit() and len(candidate) <= 80 else ''
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def extract_public_instagram_country(value: Any) -> str:
|
|
209
|
+
"""Read only the account-country field exposed by Instagram transparency data."""
|
|
210
|
+
if isinstance(value, Mapping):
|
|
211
|
+
for key, candidate in value.items():
|
|
212
|
+
if str(key).casefold() in PUBLIC_COUNTRY_KEYS:
|
|
213
|
+
country = _country_value(candidate)
|
|
214
|
+
if country:
|
|
215
|
+
return country
|
|
216
|
+
for key, candidate in value.items():
|
|
217
|
+
if str(key).casefold() in PUBLIC_TRANSPARENCY_KEYS:
|
|
218
|
+
country = extract_public_instagram_country(candidate)
|
|
219
|
+
if country:
|
|
220
|
+
return country
|
|
221
|
+
elif isinstance(value, list):
|
|
222
|
+
for candidate in value:
|
|
223
|
+
country = extract_public_instagram_country(candidate)
|
|
224
|
+
if country:
|
|
225
|
+
return country
|
|
226
|
+
return ''
|
|
227
|
+
|
|
228
|
+
|
|
188
229
|
def _profile_metric(user: Mapping[str, Any], *keys: str) -> int | None:
|
|
189
230
|
for key in keys:
|
|
190
231
|
value = normalize_instagram_metric(user.get(key))
|
|
@@ -224,7 +265,12 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
224
265
|
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')
|
|
225
266
|
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')
|
|
226
267
|
posts = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:posts|publicações|publications)',)) or _structured_metric(response.text, 'edge_owner_to_timeline_media', 'posts', 'post_count')
|
|
227
|
-
|
|
268
|
+
embedded_country = ''
|
|
269
|
+
for document in _embedded_json_documents(response.text):
|
|
270
|
+
embedded_country = extract_public_instagram_country(document)
|
|
271
|
+
if embedded_country:
|
|
272
|
+
break
|
|
273
|
+
data = {'id': f"instagram_{reference['username']}", **reference, 'name': title.split('(')[0].strip() or reference['username'], 'bio': normalize_instagram_bio(description), 'bio_raw': description, 'country': embedded_country, '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()}
|
|
228
274
|
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', data)
|
|
229
275
|
|
|
230
276
|
|
|
@@ -325,4 +371,4 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
325
371
|
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))]})
|
|
326
372
|
|
|
327
373
|
|
|
328
|
-
__all__ = ['IntegrationResult', 'fetch_public_instagram_posts', 'fetch_public_instagram_profile', 'normalize_instagram_bio', 'normalize_instagram_metric', 'normalize_instagram_reference']
|
|
374
|
+
__all__ = ['IntegrationResult', 'extract_public_instagram_country', 'fetch_public_instagram_posts', 'fetch_public_instagram_profile', 'normalize_instagram_bio', 'normalize_instagram_metric', 'normalize_instagram_reference']
|
package/package.json
CHANGED