@danhachuel/thunderbolt 0.6.32 → 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 +158 -53
- package/hermes_ui/domain.py +1 -1
- package/integrations/instagram_public.py +48 -2
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import io
|
|
|
4
4
|
import re
|
|
5
5
|
import uuid
|
|
6
6
|
import zipfile
|
|
7
|
+
import unicodedata
|
|
7
8
|
from typing import Any, Callable, Mapping
|
|
8
9
|
|
|
9
10
|
import requests
|
|
@@ -34,8 +35,9 @@ def _metric(value: Any) -> str:
|
|
|
34
35
|
def _profile_metric(profile: Mapping[str, Any], *keys: str) -> Any:
|
|
35
36
|
for key in keys:
|
|
36
37
|
value = profile.get(key)
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
normalized = normalize_instagram_metric(value)
|
|
39
|
+
if normalized is not None:
|
|
40
|
+
return normalized
|
|
39
41
|
return None
|
|
40
42
|
|
|
41
43
|
|
|
@@ -50,21 +52,48 @@ def _render_instagram_bio(value: Any) -> None:
|
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
def _profile_bio(profile: Mapping[str, Any]) -> str:
|
|
53
|
-
|
|
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()
|
|
54
65
|
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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:
|
|
59
81
|
return ""
|
|
60
|
-
|
|
61
|
-
|
|
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:
|
|
62
87
|
return country
|
|
63
|
-
return ""
|
|
88
|
+
return COUNTRY_ALIASES.get(key, "")
|
|
64
89
|
|
|
65
90
|
|
|
66
91
|
def _profile_country(profile: Mapping[str, Any]) -> str:
|
|
67
|
-
|
|
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 ""
|
|
68
97
|
|
|
69
98
|
|
|
70
99
|
def _language_index(value: Any) -> int:
|
|
@@ -78,7 +107,7 @@ def _country_options() -> tuple[str, ...]:
|
|
|
78
107
|
|
|
79
108
|
def _country_index(value: Any) -> int:
|
|
80
109
|
options = _country_options()
|
|
81
|
-
value =
|
|
110
|
+
value = _normalise_country(value)
|
|
82
111
|
return options.index(value) if value in options else 0
|
|
83
112
|
|
|
84
113
|
|
|
@@ -90,6 +119,46 @@ def _instagram_posts_key(profile_id: str) -> str:
|
|
|
90
119
|
return f"instagram_posts_{profile_id}"
|
|
91
120
|
|
|
92
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
|
+
|
|
93
162
|
def _instagram_posts_archive(posts: list[dict[str, Any]]) -> bytes:
|
|
94
163
|
archive = io.BytesIO()
|
|
95
164
|
with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as bundle:
|
|
@@ -115,9 +184,9 @@ def _instagram_posts_archive(posts: list[dict[str, Any]]) -> bytes:
|
|
|
115
184
|
|
|
116
185
|
|
|
117
186
|
def _render_instagram_posts(profile: Mapping[str, Any]) -> None:
|
|
118
|
-
profile_id =
|
|
187
|
+
profile_id = _instagram_profile_storage_id(profile)
|
|
119
188
|
state_key = _instagram_posts_key(profile_id)
|
|
120
|
-
posts =
|
|
189
|
+
posts = _stored_instagram_posts(profile)
|
|
121
190
|
with st.expander("Últimos posts do Instagram", expanded=False):
|
|
122
191
|
action_cols = st.columns(3)
|
|
123
192
|
with action_cols[0]:
|
|
@@ -131,7 +200,7 @@ def _render_instagram_posts(profile: Mapping[str, Any]) -> None:
|
|
|
131
200
|
posts_ok, posts_message, loaded_posts = _load_instagram_posts(profile, limit=target)
|
|
132
201
|
if posts_ok:
|
|
133
202
|
posts = loaded_posts
|
|
134
|
-
|
|
203
|
+
_persist_instagram_posts(profile, posts)
|
|
135
204
|
st.success(f"{len(posts)} posts carregados.")
|
|
136
205
|
st.rerun()
|
|
137
206
|
else:
|
|
@@ -154,7 +223,7 @@ def _render_instagram_posts(profile: Mapping[str, Any]) -> None:
|
|
|
154
223
|
if st.button("Mostrar + 10", key=f"more_instagram_posts_{profile_id}", use_container_width=True):
|
|
155
224
|
posts_ok, posts_message, loaded_posts = _load_instagram_posts(profile, limit=len(posts) + 10)
|
|
156
225
|
if posts_ok:
|
|
157
|
-
|
|
226
|
+
_persist_instagram_posts(profile, loaded_posts)
|
|
158
227
|
st.rerun()
|
|
159
228
|
st.warning(posts_message)
|
|
160
229
|
|
|
@@ -307,24 +376,55 @@ def _characters(settings: Mapping[str, Any]) -> tuple[list[dict[str, Any]], Any
|
|
|
307
376
|
return [], None
|
|
308
377
|
|
|
309
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
|
+
|
|
310
408
|
def _save_public_profile(data: Mapping[str, Any], *, country: str, language: str, character_id: str = "") -> dict[str, Any]:
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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)
|
|
314
414
|
metadata = {
|
|
315
|
-
**
|
|
415
|
+
**canonical,
|
|
316
416
|
"platform": "instagram",
|
|
317
417
|
"social_network": "Instagram",
|
|
318
418
|
"name": name,
|
|
319
419
|
"url": url,
|
|
320
|
-
"handle": _clean(
|
|
321
|
-
"bio":
|
|
322
|
-
"bio_raw": _clean(data.get("bio_raw")),
|
|
323
|
-
"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),
|
|
324
424
|
"language": language.strip(),
|
|
325
|
-
"post_count":
|
|
326
|
-
"subscriber_count":
|
|
327
|
-
"following_count":
|
|
425
|
+
"post_count": metrics["post_count"],
|
|
426
|
+
"subscriber_count": metrics["subscriber_count"],
|
|
427
|
+
"following_count": metrics["following_count"],
|
|
328
428
|
"character_id": character_id.strip(),
|
|
329
429
|
"active": True,
|
|
330
430
|
"metrics_source": "instagram_public_page",
|
|
@@ -333,12 +433,10 @@ def _save_public_profile(data: Mapping[str, Any], *, country: str, language: str
|
|
|
333
433
|
|
|
334
434
|
|
|
335
435
|
def _merge_instagram_refresh(existing: Mapping[str, Any], refreshed: Mapping[str, Any]) -> dict[str, Any]:
|
|
336
|
-
merged =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if not _clean(merged.get("country")):
|
|
341
|
-
merged["country"] = _infer_country_from_bio(_profile_bio(merged))
|
|
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"
|
|
342
440
|
return merged
|
|
343
441
|
|
|
344
442
|
|
|
@@ -373,28 +471,36 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
|
|
|
373
471
|
_render_instagram_bio(_profile_bio(profile))
|
|
374
472
|
st.caption(f"{_profile_country(profile) or 'País não definido'} · {_clean(profile.get('language')) or 'Idioma não definido'}")
|
|
375
473
|
with header_cols[2]:
|
|
376
|
-
st.metric("posts", _metric(profile
|
|
474
|
+
st.metric("posts", _metric(_profile_metric(profile, "post_count", "posts", "media_count")))
|
|
377
475
|
with header_cols[3]:
|
|
378
|
-
|
|
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))
|
|
379
479
|
with header_cols[4]:
|
|
380
|
-
|
|
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))
|
|
381
483
|
with header_cols[5]:
|
|
382
484
|
refresh_col, edit_col = st.columns(2)
|
|
383
485
|
with refresh_col:
|
|
384
|
-
if st.button("
|
|
486
|
+
if st.button("Actualizar posts, seguidores e seguindo", key=f"refresh_instagram_{profile_id}", use_container_width=True):
|
|
385
487
|
refreshed_ok, refreshed_message, refreshed = _refresh_instagram_profile(profile)
|
|
386
488
|
if refreshed_ok:
|
|
387
|
-
|
|
388
|
-
current_posts = st.session_state.get(posts_key, [])
|
|
489
|
+
current_posts = _stored_instagram_posts(profile)
|
|
389
490
|
posts_ok, _, refreshed_posts = _load_instagram_posts(refreshed, limit=max(10, len(current_posts)))
|
|
390
491
|
if posts_ok:
|
|
391
|
-
|
|
392
|
-
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
|
|
393
497
|
st.success("Métricas Instagram actualizadas.")
|
|
394
498
|
st.rerun()
|
|
395
499
|
st.warning(refreshed_message)
|
|
396
500
|
with edit_col:
|
|
397
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)
|
|
398
504
|
st.session_state[edit_key] = True
|
|
399
505
|
st.rerun()
|
|
400
506
|
if st.button("Apagar", key=f"delete_instagram_button_{profile_id}", use_container_width=True):
|
|
@@ -428,12 +534,12 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
|
|
|
428
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}")
|
|
429
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}")
|
|
430
536
|
with edit_cols[1]:
|
|
431
|
-
posts = st.number_input("posts", min_value=0, value=_metric_input(profile
|
|
432
|
-
followers = st.number_input("Seguidores", min_value=0, value=_metric_input(profile
|
|
433
|
-
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}")
|
|
434
540
|
save_edit = st.form_submit_button("Guardar alterações", type="primary", use_container_width=True)
|
|
435
541
|
if save_edit:
|
|
436
|
-
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)})
|
|
437
543
|
st.session_state.pop(edit_key, None)
|
|
438
544
|
st.success("Conta Instagram actualizada.")
|
|
439
545
|
st.rerun()
|
|
@@ -477,8 +583,6 @@ def render_social_networks(settings: dict[str, Any]) -> None:
|
|
|
477
583
|
if search_clicked:
|
|
478
584
|
result = fetch_public_instagram_profile(source)
|
|
479
585
|
result_data = dict(result.data) if isinstance(result.data, dict) else {}
|
|
480
|
-
if result_data and not _clean(result_data.get("country")):
|
|
481
|
-
result_data["country"] = _infer_country_from_bio(_profile_bio(result_data))
|
|
482
586
|
st.session_state["social_instagram_result"] = result_data
|
|
483
587
|
st.session_state["social_instagram_ok"] = result.ok
|
|
484
588
|
st.session_state["social_instagram_message"] = result.message
|
|
@@ -498,11 +602,11 @@ def render_social_networks(settings: dict[str, Any]) -> None:
|
|
|
498
602
|
st.caption(_clean(data.get("handle")))
|
|
499
603
|
_render_instagram_bio(_profile_bio(data))
|
|
500
604
|
with preview_cols[2]:
|
|
501
|
-
st.metric("posts", _metric(data
|
|
605
|
+
st.metric("posts", _metric(_profile_metric(data, "post_count", "posts", "media_count")))
|
|
502
606
|
with preview_cols[3]:
|
|
503
|
-
st.metric("Seguidores", _metric(data
|
|
607
|
+
st.metric("Seguidores", _metric(_profile_metric(data, "subscriber_count", "followers_count", "follower_count", "followers", "edge_followed_by")))
|
|
504
608
|
with preview_cols[4]:
|
|
505
|
-
st.metric("seguindo", _metric(data
|
|
609
|
+
st.metric("seguindo", _metric(_profile_metric(data, "following_count", "following", "follows", "followingCount", "edge_follow")))
|
|
506
610
|
with st.form("social_instagram_save_public_profile"):
|
|
507
611
|
form_cols = st.columns(2)
|
|
508
612
|
with form_cols[0]:
|
|
@@ -511,9 +615,9 @@ def render_social_networks(settings: dict[str, Any]) -> None:
|
|
|
511
615
|
country = st.selectbox("País", _country_options(), index=_country_index(_profile_country(data)), format_func=_country_label, key="social_instagram_result_country")
|
|
512
616
|
language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=_language_index(data.get("language")), format_func=language_label, key="social_instagram_result_language")
|
|
513
617
|
with form_cols[1]:
|
|
514
|
-
posts = st.number_input("posts", min_value=0, value=_metric_input(data
|
|
515
|
-
followers = st.number_input("Seguidores", min_value=0, value=_metric_input(data
|
|
516
|
-
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")
|
|
517
621
|
character_options = [""] + [_clean(item.get("id")) for item in characters if _clean(item.get("id"))]
|
|
518
622
|
character_labels = {"": "Sem personagem associado"} | {_clean(item.get("id")): _clean(item.get("name")) or _clean(item.get("id")) for item in characters}
|
|
519
623
|
selected_character = st.selectbox("Personagem", character_options, format_func=lambda value: character_labels.get(value, value), key="social_instagram_result_character")
|
|
@@ -521,7 +625,8 @@ def render_social_networks(settings: dict[str, Any]) -> None:
|
|
|
521
625
|
if save_profile:
|
|
522
626
|
profile_data = dict(data)
|
|
523
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)})
|
|
524
|
-
|
|
628
|
+
profile_data["country"] = _normalise_country(country)
|
|
629
|
+
_save_public_profile(profile_data, country=profile_data["country"], language=language, character_id=selected_character)
|
|
525
630
|
st.success("Conta Instagram cadastrada em Redes Sociais.")
|
|
526
631
|
for key in ("social_instagram_result", "social_instagram_ok", "social_instagram_message"):
|
|
527
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