@danhachuel/thunderbolt 0.6.13 → 0.6.15
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/main.py +8 -2
- package/app/social_networks_ui.py +334 -0
- package/hermes_ui/growth_bilibili.py +188 -0
- package/integrations/instagram_public.py +15 -8
- package/integrations/meta_social.py +73 -0
- package/package.json +2 -1
package/app/main.py
CHANGED
|
@@ -70,6 +70,7 @@ from app.modules.token_optimizer.compressor import check_installation
|
|
|
70
70
|
from app.modules.token_optimizer.config import DEFAULTS as TOKEN_OPTIMIZER_DEFAULTS
|
|
71
71
|
from app.modules.token_optimizer.metrics import get_stats as get_token_optimizer_stats
|
|
72
72
|
from app.influencers_ui import render_ai_influencer_characters, render_ai_influencer_content, render_ai_influencers_api_status, render_motion_control, render_ugc_products
|
|
73
|
+
from app.social_networks_ui import render_meta_api_cards, render_social_networks
|
|
73
74
|
from hermes_ui.blueprints import create_blueprint_from_link, list_branding_files, save_generated_blueprint
|
|
74
75
|
from hermes_ui.thumbnail_blueprints import generate_thumbnail_blueprint, list_thumbnail_blueprint_documents, resolve_thumbnail_blueprint, save_thumbnail_blueprint, save_thumbnail_blueprint_pair, thumbnail_blueprint_catalog, thumbnail_blueprint_for_blueprint, thumbnail_blueprint_for_channel
|
|
75
76
|
from hermes_ui.metadata_cleaner import build_description, clean_video_metadata, list_edit_records, metadata_manifest, normalize_tags, save_edit_record, store_external_video
|
|
@@ -112,6 +113,7 @@ from hermes_ui.growth_youtube import list_analyses, run_audit
|
|
|
112
113
|
from hermes_ui.growth_tiktok import render_growth_tiktok
|
|
113
114
|
from hermes_ui.growth_instagram import render_growth_instagram
|
|
114
115
|
from hermes_ui.growth_facebook_pages import render_growth_facebook_pages
|
|
116
|
+
from hermes_ui.growth_bilibili import render_growth_bilibili
|
|
115
117
|
from hermes_ui.canva_auth import authorization_url, create_pkce_pair, create_state, exchange_code
|
|
116
118
|
from integrations.platforms import IntegrationResult, TikTokAdapter, YouTubeAdapter, fetch_channel_videos_public
|
|
117
119
|
from integrations.tiktok_public import fetch_public_tiktok_profile, normalize_tiktok_reference
|
|
@@ -8034,6 +8036,10 @@ def render_settings():
|
|
|
8034
8036
|
st.rerun()
|
|
8035
8037
|
with st.expander("API Tiktok", expanded=False):
|
|
8036
8038
|
render_tiktok_api_cards(settings)
|
|
8039
|
+
with st.expander("API Instagram", expanded=False):
|
|
8040
|
+
render_meta_api_cards(settings, "instagram")
|
|
8041
|
+
with st.expander("API Facebook Pages", expanded=False):
|
|
8042
|
+
render_meta_api_cards(settings, "facebook_pages")
|
|
8037
8043
|
with st.expander("API Bilibili", expanded=False):
|
|
8038
8044
|
bilibili_cards = settings.get("bilibili_api_cards") if isinstance(settings.get("bilibili_api_cards"), list) else []
|
|
8039
8045
|
bilibili_ready = any(bool(card.get("active", True) and card.get("sessdata") and card.get("bili_jct") and card.get("buvid3")) for card in bilibili_cards if isinstance(card, dict))
|
|
@@ -9071,12 +9077,12 @@ def main():
|
|
|
9071
9077
|
"Geração de Conteúdo IA": lambda: render_ai_influencer_content(read_json("settings.json", {})),
|
|
9072
9078
|
"Motion Control": lambda: render_motion_control(read_json("settings.json", {})),
|
|
9073
9079
|
"UGC Products": lambda: render_ugc_products(read_json("settings.json", {})),
|
|
9074
|
-
"Redes Sociais": lambda:
|
|
9080
|
+
"Redes Sociais": lambda: render_social_networks(read_json("settings.json", {})),
|
|
9075
9081
|
"Analista Growth Youtube": render_growth_youtube,
|
|
9076
9082
|
"Analista Growth Tiktok": render_growth_tiktok,
|
|
9077
9083
|
"Analista Growth Instagram": render_growth_instagram,
|
|
9078
9084
|
"Analista Facebook Pages": render_growth_facebook_pages,
|
|
9079
|
-
"Analista Bilibili":
|
|
9085
|
+
"Analista Bilibili": render_growth_bilibili,
|
|
9080
9086
|
"Documentação": lambda: render_edit_placeholder("Documentação", "Seleccione um tutorial no menu expansível."),
|
|
9081
9087
|
"Tutorial Meta": render_models_ai_tutorial,
|
|
9082
9088
|
"Tutorial Supabase": render_supabase_tutorial,
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any, Callable, Mapping
|
|
5
|
+
|
|
6
|
+
import streamlit as st
|
|
7
|
+
|
|
8
|
+
from hermes_ui.domain import create_channel, update_channel
|
|
9
|
+
from hermes_ui.influencers import InfluencerBackendError, STANDALONE_CONTENT_INFLUENCER_ID, get_repository
|
|
10
|
+
from hermes_ui.storage import read_json, write_json
|
|
11
|
+
from integrations.instagram_public import fetch_public_instagram_profile
|
|
12
|
+
from integrations.meta_social import test_facebook_pages_api_card, test_instagram_api_card
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _clean(value: Any) -> str:
|
|
16
|
+
return str(value or "").strip()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _metric(value: Any) -> str:
|
|
20
|
+
if value in (None, ""):
|
|
21
|
+
return "—"
|
|
22
|
+
try:
|
|
23
|
+
return f"{int(value):,}".replace(",", ".")
|
|
24
|
+
except (TypeError, ValueError):
|
|
25
|
+
return _clean(value) or "—"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _api_card_defaults(kind: str) -> tuple[str, list[str], Callable[[Mapping[str, Any]], dict[str, Any]]]:
|
|
29
|
+
if kind == "instagram":
|
|
30
|
+
return "instagram_api_cards", ["id", "label", "account_id", "access_token"], test_instagram_api_card
|
|
31
|
+
return "facebook_pages_api_cards", ["id", "label", "page_id", "access_token"], test_facebook_pages_api_card
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _normalise_api_cards(settings: dict[str, Any], kind: str) -> list[dict[str, str]]:
|
|
35
|
+
key, fields, _ = _api_card_defaults(kind)
|
|
36
|
+
raw = settings.get(key)
|
|
37
|
+
cards: list[dict[str, str]] = []
|
|
38
|
+
if isinstance(raw, list):
|
|
39
|
+
for index, item in enumerate(raw):
|
|
40
|
+
if not isinstance(item, dict):
|
|
41
|
+
continue
|
|
42
|
+
card = {field: _clean(item.get(field)) for field in fields}
|
|
43
|
+
card["id"] = card["id"] or f"{kind}-api-{index + 1}"
|
|
44
|
+
cards.append(card)
|
|
45
|
+
if not cards:
|
|
46
|
+
legacy_key = "instagram_api_key" if kind == "instagram" else "facebook_pages_api_key"
|
|
47
|
+
legacy_id = "instagram_account_id" if kind == "instagram" else "facebook_page_id"
|
|
48
|
+
token = _clean(settings.get(legacy_key))
|
|
49
|
+
identifier = _clean(settings.get(legacy_id))
|
|
50
|
+
if token or identifier:
|
|
51
|
+
cards = [{"id": f"{kind}-api-1", "label": "Conta 1", "account_id" if kind == "instagram" else "page_id": identifier, "access_token": token}]
|
|
52
|
+
if cards and not isinstance(raw, list):
|
|
53
|
+
settings[key] = cards
|
|
54
|
+
write_json("settings.json", settings)
|
|
55
|
+
return cards
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _persist_api_cards(settings: dict[str, Any], kind: str, cards: list[dict[str, Any]]) -> None:
|
|
59
|
+
key, fields, _ = _api_card_defaults(kind)
|
|
60
|
+
clean_cards: list[dict[str, str]] = []
|
|
61
|
+
for index, item in enumerate(cards):
|
|
62
|
+
card = {field: _clean(item.get(field)) for field in fields}
|
|
63
|
+
card["id"] = card["id"] or f"{kind}-api-{index + 1}"
|
|
64
|
+
clean_cards.append(card)
|
|
65
|
+
settings[key] = clean_cards
|
|
66
|
+
if kind == "instagram":
|
|
67
|
+
first = next((item for item in clean_cards if item.get("account_id") and item.get("access_token")), clean_cards[0] if clean_cards else {})
|
|
68
|
+
settings["instagram_account_id"] = first.get("account_id", "")
|
|
69
|
+
settings["instagram_api_key"] = first.get("access_token", "")
|
|
70
|
+
else:
|
|
71
|
+
first = next((item for item in clean_cards if item.get("page_id") and item.get("access_token")), clean_cards[0] if clean_cards else {})
|
|
72
|
+
settings["facebook_page_id"] = first.get("page_id", "")
|
|
73
|
+
settings["facebook_pages_api_key"] = first.get("access_token", "")
|
|
74
|
+
write_json("settings.json", settings)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _api_card_status(card: Mapping[str, Any], kind: str) -> tuple[str, str]:
|
|
78
|
+
identifier = _clean(card.get("account_id" if kind == "instagram" else "page_id"))
|
|
79
|
+
token = _clean(card.get("access_token"))
|
|
80
|
+
if not token:
|
|
81
|
+
return "missing", "Missing key"
|
|
82
|
+
if not identifier:
|
|
83
|
+
return "missing", "Missing configuration"
|
|
84
|
+
return "ready", "Configured"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def render_meta_api_cards(settings: dict[str, Any], kind: str) -> None:
|
|
88
|
+
is_instagram = kind == "instagram"
|
|
89
|
+
title = "API Instagram" if is_instagram else "API Facebook Pages"
|
|
90
|
+
entity_label = "Instagram Account ID" if is_instagram else "Facebook Page ID"
|
|
91
|
+
key, _, tester = _api_card_defaults(kind)
|
|
92
|
+
cards = _normalise_api_cards(settings, kind)
|
|
93
|
+
if not cards:
|
|
94
|
+
st.info(f"Ainda não existe nenhuma conta {title.replace('API ', '')} configurada. Use o botão abaixo para criar o primeiro card.")
|
|
95
|
+
for index, card in enumerate(cards):
|
|
96
|
+
card_id = _clean(card.get("id"))
|
|
97
|
+
status_kind, status_label = _api_card_status(card, kind)
|
|
98
|
+
with st.container(border=True):
|
|
99
|
+
header_cols = st.columns([3.2, 1.2])
|
|
100
|
+
with header_cols[0]:
|
|
101
|
+
st.subheader(f"{title} {index + 1}")
|
|
102
|
+
st.caption("Cada card representa uma conta independente; o token é guardado apenas no storage local.")
|
|
103
|
+
with header_cols[1]:
|
|
104
|
+
st.markdown(
|
|
105
|
+
f'<span class="tb-api-status tb-api-status--{status_kind}">{"✓" if status_kind == "ready" else "!"} {status_label}</span>',
|
|
106
|
+
unsafe_allow_html=True,
|
|
107
|
+
)
|
|
108
|
+
with st.form(f"{kind}_api_card_form_{card_id}"):
|
|
109
|
+
label = st.text_input("Nome da conta", value=_clean(card.get("label")) or f"Conta {index + 1}", key=f"{kind}_api_{card_id}_label")
|
|
110
|
+
identifier = st.text_input(entity_label, value=_clean(card.get("account_id" if is_instagram else "page_id")), key=f"{kind}_api_{card_id}_identifier")
|
|
111
|
+
token = st.text_input("Access Token", value=_clean(card.get("access_token")), type="password", key=f"{kind}_api_{card_id}_token")
|
|
112
|
+
action_cols = st.columns(3)
|
|
113
|
+
with action_cols[0]:
|
|
114
|
+
test_clicked = st.form_submit_button("Teste chamada API", use_container_width=True)
|
|
115
|
+
with action_cols[1]:
|
|
116
|
+
save_clicked = st.form_submit_button("Guardar Card", type="primary", use_container_width=True)
|
|
117
|
+
with action_cols[2]:
|
|
118
|
+
delete_clicked = st.form_submit_button("Apagar Card", use_container_width=True)
|
|
119
|
+
edited = {
|
|
120
|
+
"id": card_id,
|
|
121
|
+
"label": label.strip(),
|
|
122
|
+
"access_token": token.strip(),
|
|
123
|
+
("account_id" if is_instagram else "page_id"): identifier.strip(),
|
|
124
|
+
}
|
|
125
|
+
if test_clicked:
|
|
126
|
+
cards[index] = edited
|
|
127
|
+
_persist_api_cards(settings, kind, cards)
|
|
128
|
+
result = tester(edited)
|
|
129
|
+
if result.get("status") == "success":
|
|
130
|
+
st.success(result.get("message") or "Chamada API concluída.")
|
|
131
|
+
elif result.get("status") == "missing":
|
|
132
|
+
st.warning(result.get("message") or "Missing configuration")
|
|
133
|
+
else:
|
|
134
|
+
st.error(result.get("message") or "A chamada API falhou.")
|
|
135
|
+
elif save_clicked:
|
|
136
|
+
cards[index] = edited
|
|
137
|
+
_persist_api_cards(settings, kind, cards)
|
|
138
|
+
st.success(f"{title} guardada.")
|
|
139
|
+
st.rerun()
|
|
140
|
+
elif delete_clicked:
|
|
141
|
+
cards = [item for item in cards if _clean(item.get("id")) != card_id]
|
|
142
|
+
_persist_api_cards(settings, kind, cards)
|
|
143
|
+
st.success(f"{title} apagada.")
|
|
144
|
+
st.rerun()
|
|
145
|
+
if st.button("Adicionar nova API", type="primary", use_container_width=True, key=f"add_{kind}_api_card"):
|
|
146
|
+
identifier_key = "account_id" if is_instagram else "page_id"
|
|
147
|
+
cards.append({"id": f"{kind}-api-{uuid.uuid4().hex[:10]}", "label": f"Conta {len(cards) + 1}", identifier_key: "", "access_token": ""})
|
|
148
|
+
_persist_api_cards(settings, kind, cards)
|
|
149
|
+
st.success(f"Novo card {title} criado.")
|
|
150
|
+
st.rerun()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _instagram_profiles() -> list[dict[str, Any]]:
|
|
154
|
+
records = read_json("channels.json", [])
|
|
155
|
+
return [item for item in records if isinstance(item, dict) and _clean(item.get("platform")).lower() == "instagram"]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _characters(settings: Mapping[str, Any]) -> tuple[list[dict[str, Any]], Any | None]:
|
|
159
|
+
try:
|
|
160
|
+
repository = get_repository(settings)
|
|
161
|
+
items = [item for item in repository.list_influencers() if _clean(item.get("id")) != STANDALONE_CONTENT_INFLUENCER_ID]
|
|
162
|
+
return items, repository
|
|
163
|
+
except (InfluencerBackendError, Exception):
|
|
164
|
+
return [], None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _save_public_profile(data: Mapping[str, Any], *, country: str, language: str, character_id: str = "") -> dict[str, Any]:
|
|
168
|
+
name = _clean(data.get("name")) or _clean(data.get("username")) or "Conta Instagram"
|
|
169
|
+
url = _clean(data.get("url"))
|
|
170
|
+
metadata = {
|
|
171
|
+
**dict(data),
|
|
172
|
+
"platform": "instagram",
|
|
173
|
+
"social_network": "Instagram",
|
|
174
|
+
"name": name,
|
|
175
|
+
"url": url,
|
|
176
|
+
"handle": _clean(data.get("handle")),
|
|
177
|
+
"country": country.strip(),
|
|
178
|
+
"language": language.strip(),
|
|
179
|
+
"post_count": data.get("post_count"),
|
|
180
|
+
"subscriber_count": data.get("subscriber_count"),
|
|
181
|
+
"following_count": data.get("following_count"),
|
|
182
|
+
"character_id": character_id.strip(),
|
|
183
|
+
"active": True,
|
|
184
|
+
"metrics_source": "instagram_public_page",
|
|
185
|
+
}
|
|
186
|
+
return create_channel(name, url, metadata)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, Any]], settings: Mapping[str, Any]) -> None:
|
|
190
|
+
profile_id = _clean(profile.get("id"))
|
|
191
|
+
edit_key = f"edit_instagram_profile_{profile_id}"
|
|
192
|
+
with st.container(border=True):
|
|
193
|
+
header_cols = st.columns([0.7, 3.1, 1.25, 1.25, 1.25, 1.65])
|
|
194
|
+
with header_cols[0]:
|
|
195
|
+
if _clean(profile.get("avatar_url")):
|
|
196
|
+
st.image(profile["avatar_url"], width=64)
|
|
197
|
+
else:
|
|
198
|
+
st.markdown("### IG")
|
|
199
|
+
with header_cols[1]:
|
|
200
|
+
st.write(f"**{_clean(profile.get('name')) or 'Sem nome'}**")
|
|
201
|
+
st.caption(f"{_clean(profile.get('handle')) or _clean(profile.get('url')) or 'sem handler'}")
|
|
202
|
+
st.caption(f"{_clean(profile.get('country')) or 'País não definido'} · {_clean(profile.get('language')) or 'Idioma não definido'}")
|
|
203
|
+
with header_cols[2]:
|
|
204
|
+
st.metric("posts", _metric(profile.get("post_count")))
|
|
205
|
+
with header_cols[3]:
|
|
206
|
+
st.metric("Seguidores", _metric(profile.get("subscriber_count")))
|
|
207
|
+
with header_cols[4]:
|
|
208
|
+
st.metric("seguindo", _metric(profile.get("following_count")))
|
|
209
|
+
with header_cols[5]:
|
|
210
|
+
refresh_col, edit_col = st.columns(2)
|
|
211
|
+
with refresh_col:
|
|
212
|
+
if st.button("↻", help="Actualizar posts, seguidores e seguindo", key=f"refresh_instagram_{profile_id}"):
|
|
213
|
+
result = fetch_public_instagram_profile(_clean(profile.get("url")) or _clean(profile.get("handle")))
|
|
214
|
+
if result.ok:
|
|
215
|
+
update_channel(profile_id, {**result.data, "platform": "instagram", "country": profile.get("country", ""), "language": profile.get("language", ""), "character_id": profile.get("character_id", "")})
|
|
216
|
+
st.success("Métricas Instagram actualizadas.")
|
|
217
|
+
st.rerun()
|
|
218
|
+
st.warning(result.message)
|
|
219
|
+
with edit_col:
|
|
220
|
+
if st.button("Editar", key=f"edit_instagram_button_{profile_id}", use_container_width=True):
|
|
221
|
+
st.session_state[edit_key] = True
|
|
222
|
+
st.rerun()
|
|
223
|
+
url = _clean(profile.get("url"))
|
|
224
|
+
if url:
|
|
225
|
+
st.link_button("Abrir Instagram", url, use_container_width=True)
|
|
226
|
+
|
|
227
|
+
if st.session_state.get(edit_key):
|
|
228
|
+
with st.form(f"edit_instagram_profile_form_{profile_id}"):
|
|
229
|
+
edit_cols = st.columns(2)
|
|
230
|
+
with edit_cols[0]:
|
|
231
|
+
name = st.text_input("Nome", value=_clean(profile.get("name")), key=f"instagram_edit_name_{profile_id}")
|
|
232
|
+
handle = st.text_input("handler", value=_clean(profile.get("handle")), key=f"instagram_edit_handle_{profile_id}")
|
|
233
|
+
country = st.text_input("País", value=_clean(profile.get("country")), key=f"instagram_edit_country_{profile_id}")
|
|
234
|
+
language = st.text_input("Idioma", value=_clean(profile.get("language")), key=f"instagram_edit_language_{profile_id}")
|
|
235
|
+
with edit_cols[1]:
|
|
236
|
+
posts = st.number_input("posts", min_value=0, value=int(profile.get("post_count") or 0), key=f"instagram_edit_posts_{profile_id}")
|
|
237
|
+
followers = st.number_input("Seguidores", min_value=0, value=int(profile.get("subscriber_count") or 0), key=f"instagram_edit_followers_{profile_id}")
|
|
238
|
+
following = st.number_input("seguindo", min_value=0, value=int(profile.get("following_count") or 0), key=f"instagram_edit_following_{profile_id}")
|
|
239
|
+
save_edit = st.form_submit_button("Guardar alterações", type="primary", use_container_width=True)
|
|
240
|
+
if save_edit:
|
|
241
|
+
update_channel(profile_id, {"name": name.strip(), "handle": handle.strip(), "country": country.strip(), "language": language.strip(), "post_count": int(posts), "subscriber_count": int(followers), "following_count": int(following)})
|
|
242
|
+
st.session_state.pop(edit_key, None)
|
|
243
|
+
st.success("Conta Instagram actualizada.")
|
|
244
|
+
st.rerun()
|
|
245
|
+
else:
|
|
246
|
+
block_cols = st.columns(4, gap="small")
|
|
247
|
+
with block_cols[0]:
|
|
248
|
+
st.markdown(f"**Nome**\n\n{_clean(profile.get('name')) or '—'}")
|
|
249
|
+
with block_cols[1]:
|
|
250
|
+
st.markdown(f"**handler**\n\n{_clean(profile.get('handle')) or '—'}")
|
|
251
|
+
with block_cols[2]:
|
|
252
|
+
st.markdown(f"**País**\n\n{_clean(profile.get('country')) or '—'}")
|
|
253
|
+
with block_cols[3]:
|
|
254
|
+
st.markdown(f"**Idioma**\n\n{_clean(profile.get('language')) or '—'}")
|
|
255
|
+
|
|
256
|
+
character_options = [""] + [_clean(item.get("id")) for item in characters if _clean(item.get("id"))]
|
|
257
|
+
character_labels = {"": "Sem personagem associado"} | {_clean(item.get("id")): _clean(item.get("name")) or _clean(item.get("id")) for item in characters}
|
|
258
|
+
current_character = _clean(profile.get("character_id"))
|
|
259
|
+
selected_character = st.selectbox("Personagem", character_options, index=character_options.index(current_character) if current_character in character_options else 0, format_func=lambda value: character_labels.get(value, value), key=f"instagram_character_{profile_id}")
|
|
260
|
+
if st.button("Atrelar Personagem", key=f"attach_instagram_character_{profile_id}", use_container_width=True):
|
|
261
|
+
update_channel(profile_id, {"character_id": selected_character})
|
|
262
|
+
st.success("Personagem associado à conta Instagram.")
|
|
263
|
+
st.rerun()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def render_social_networks(settings: dict[str, Any]) -> None:
|
|
267
|
+
st.title("Redes Sociais")
|
|
268
|
+
st.caption("Cadastre contas Instagram através de pesquisa pública e associe cada conta a um Personagem de AI Influencers.")
|
|
269
|
+
search_tab, accounts_tab = st.tabs(["Pesquisa pública Instagram", "Contas Instagram"])
|
|
270
|
+
with search_tab:
|
|
271
|
+
st.caption("A pesquisa consulta apenas a página pública do Instagram. API Key não é necessária para cadastrar o perfil.")
|
|
272
|
+
source = st.text_input("URL pública ou @handler Instagram", placeholder="https://www.instagram.com/conta/ ou @conta", key="social_instagram_source")
|
|
273
|
+
search_cols = st.columns(2)
|
|
274
|
+
with search_cols[0]:
|
|
275
|
+
search_clicked = st.button("Pesquisar conta pública", type="primary", use_container_width=True, key="social_instagram_search")
|
|
276
|
+
with search_cols[1]:
|
|
277
|
+
if st.button("Limpar pesquisa", use_container_width=True, key="social_instagram_clear"):
|
|
278
|
+
for key in ("social_instagram_result", "social_instagram_ok", "social_instagram_message"):
|
|
279
|
+
st.session_state.pop(key, None)
|
|
280
|
+
st.rerun()
|
|
281
|
+
if search_clicked:
|
|
282
|
+
result = fetch_public_instagram_profile(source)
|
|
283
|
+
st.session_state["social_instagram_result"] = result.data if isinstance(result.data, dict) else {}
|
|
284
|
+
st.session_state["social_instagram_ok"] = result.ok
|
|
285
|
+
st.session_state["social_instagram_message"] = result.message
|
|
286
|
+
if st.session_state.get("social_instagram_message"):
|
|
287
|
+
(st.success if st.session_state.get("social_instagram_ok") else st.warning)(st.session_state["social_instagram_message"])
|
|
288
|
+
data = st.session_state.get("social_instagram_result", {}) if st.session_state.get("social_instagram_ok") else {}
|
|
289
|
+
if data:
|
|
290
|
+
characters, _ = _characters(settings)
|
|
291
|
+
with st.container(border=True):
|
|
292
|
+
st.subheader("Conta Instagram encontrada")
|
|
293
|
+
preview_cols = st.columns([0.8, 2.2, 1.2, 1.2, 1.2])
|
|
294
|
+
with preview_cols[0]:
|
|
295
|
+
if _clean(data.get("avatar_url")):
|
|
296
|
+
st.image(data["avatar_url"], width=72)
|
|
297
|
+
with preview_cols[1]:
|
|
298
|
+
st.write(f"**{_clean(data.get('name')) or _clean(data.get('username'))}**")
|
|
299
|
+
st.caption(_clean(data.get("handle")))
|
|
300
|
+
with preview_cols[2]:
|
|
301
|
+
st.metric("posts", _metric(data.get("post_count")))
|
|
302
|
+
with preview_cols[3]:
|
|
303
|
+
st.metric("Seguidores", _metric(data.get("subscriber_count")))
|
|
304
|
+
with preview_cols[4]:
|
|
305
|
+
st.metric("seguindo", _metric(data.get("following_count")))
|
|
306
|
+
with st.form("social_instagram_save_public_profile"):
|
|
307
|
+
form_cols = st.columns(2)
|
|
308
|
+
with form_cols[0]:
|
|
309
|
+
name = st.text_input("Nome", value=_clean(data.get("name")), key="social_instagram_result_name")
|
|
310
|
+
country = st.text_input("País", value=_clean(data.get("country")), key="social_instagram_result_country")
|
|
311
|
+
language = st.text_input("Idioma", value=_clean(data.get("language")), key="social_instagram_result_language")
|
|
312
|
+
with form_cols[1]:
|
|
313
|
+
posts = st.number_input("posts", min_value=0, value=int(data.get("post_count") or 0), key="social_instagram_result_posts")
|
|
314
|
+
followers = st.number_input("Seguidores", min_value=0, value=int(data.get("subscriber_count") or 0), key="social_instagram_result_followers")
|
|
315
|
+
following = st.number_input("seguindo", min_value=0, value=int(data.get("following_count") or 0), key="social_instagram_result_following")
|
|
316
|
+
character_options = [""] + [_clean(item.get("id")) for item in characters if _clean(item.get("id"))]
|
|
317
|
+
character_labels = {"": "Sem personagem associado"} | {_clean(item.get("id")): _clean(item.get("name")) or _clean(item.get("id")) for item in characters}
|
|
318
|
+
selected_character = st.selectbox("Personagem", character_options, format_func=lambda value: character_labels.get(value, value), key="social_instagram_result_character")
|
|
319
|
+
save_profile = st.form_submit_button("Cadastrar conta Instagram", type="primary", use_container_width=True)
|
|
320
|
+
if save_profile:
|
|
321
|
+
profile_data = dict(data)
|
|
322
|
+
profile_data.update({"name": name, "post_count": int(posts), "subscriber_count": int(followers), "following_count": int(following)})
|
|
323
|
+
_save_public_profile(profile_data, country=country, language=language, character_id=selected_character)
|
|
324
|
+
st.success("Conta Instagram cadastrada em Redes Sociais.")
|
|
325
|
+
for key in ("social_instagram_result", "social_instagram_ok", "social_instagram_message"):
|
|
326
|
+
st.session_state.pop(key, None)
|
|
327
|
+
st.rerun()
|
|
328
|
+
with accounts_tab:
|
|
329
|
+
profiles = _instagram_profiles()
|
|
330
|
+
if not profiles:
|
|
331
|
+
st.info("Ainda não existem contas Instagram cadastradas. Use a pesquisa pública para adicionar a primeira conta.")
|
|
332
|
+
characters, _ = _characters(settings)
|
|
333
|
+
for profile in profiles:
|
|
334
|
+
_render_instagram_card(profile, characters, settings)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from html import escape
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import streamlit as st
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
BILIBILI_CARDS: tuple[tuple[str, int, bool, tuple[tuple[str, str, str, str], ...]], ...] = (
|
|
10
|
+
(
|
|
11
|
+
"Validação de Procura",
|
|
12
|
+
70,
|
|
13
|
+
True,
|
|
14
|
+
(
|
|
15
|
+
("Tecto dos concorrentes", "A medir", "> 500k views", "A verificar"),
|
|
16
|
+
("Volume de pesquisa", "A medir", "Procura crescente", "A verificar"),
|
|
17
|
+
("Tráfego de pesquisa", "A medir", "15–30%", "A verificar"),
|
|
18
|
+
("Diagnóstico", "Validar antes de produzir", "Nicho validado", "A verificar"),
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
(
|
|
22
|
+
"Thumbnail",
|
|
23
|
+
70,
|
|
24
|
+
True,
|
|
25
|
+
(
|
|
26
|
+
("CTR", "A medir", "> 6%", "A verificar"),
|
|
27
|
+
("Contraste", "Alto contraste", "Fundo escuro + cor viva", "Meta"),
|
|
28
|
+
("Texto", "3–5 palavras", "Legível no telemóvel", "Meta"),
|
|
29
|
+
("Elementos Bilibili", "ACG quando adequado", "Visual expressivo", "Meta"),
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
(
|
|
33
|
+
"Título e Intenção",
|
|
34
|
+
70,
|
|
35
|
+
True,
|
|
36
|
+
(
|
|
37
|
+
("Estrutura", "Curiosity · Clarity · Context", "Promessa clara", "Meta"),
|
|
38
|
+
("Keyword principal", "No início", "Pesquisa Bilibili", "Meta"),
|
|
39
|
+
("CTR + retenção", "A medir", "CTR alto sem queda brusca", "A verificar"),
|
|
40
|
+
("Formato", "Why / How to / VS", "Intenção explícita", "Meta"),
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
(
|
|
44
|
+
"Hook e Retenção",
|
|
45
|
+
70,
|
|
46
|
+
False,
|
|
47
|
+
(
|
|
48
|
+
("Retenção aos 15s", "A medir", "> 50%", "A verificar"),
|
|
49
|
+
("Retenção aos 30s", "A medir", "> 70%", "A verificar"),
|
|
50
|
+
("Método", "Clímax primeiro", "Resultado antes da introdução", "Meta"),
|
|
51
|
+
("Abertura", "Sem introdução longa", "Primeiro frame forte", "Meta"),
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
"Ritmo e Edição",
|
|
56
|
+
70,
|
|
57
|
+
False,
|
|
58
|
+
(
|
|
59
|
+
("Pattern interrupt", "A cada 5–8s", "Sem imagens estáticas > 4s", "Meta"),
|
|
60
|
+
("Quedas intermédias", "A medir", "Quedas pequenas", "A verificar"),
|
|
61
|
+
("Texto danmaku", "Usar com intenção", "Comentários sobrepostos", "Meta"),
|
|
62
|
+
("Cadência visual", "A medir", "Plateau de retenção", "A verificar"),
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
(
|
|
66
|
+
"Engagement 三连",
|
|
67
|
+
70,
|
|
68
|
+
False,
|
|
69
|
+
(
|
|
70
|
+
("三连率", "A medir", "> 4%", "A verificar"),
|
|
71
|
+
("Likes + Coins + Favorites", "Três cliques", "Sinal de qualidade", "Meta"),
|
|
72
|
+
("Danmaku", "A medir", "> 5/min", "A verificar"),
|
|
73
|
+
("Comentários", "A medir", "> 1/100 views", "A verificar"),
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
"Tráfego e Algoritmo",
|
|
78
|
+
70,
|
|
79
|
+
False,
|
|
80
|
+
(
|
|
81
|
+
("Recommendation 推荐", "A medir", "40–60%", "A verificar"),
|
|
82
|
+
("Search 搜索", "A medir", "15–30%", "A verificar"),
|
|
83
|
+
("Followers 粉丝", "A medir", "10–30%", "A verificar"),
|
|
84
|
+
("External 外部", "A medir", "< 10%", "A verificar"),
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
(
|
|
88
|
+
"Conversão e Saúde",
|
|
89
|
+
70,
|
|
90
|
+
False,
|
|
91
|
+
(
|
|
92
|
+
("Conversão em followers", "A medir", "> 1%", "A verificar"),
|
|
93
|
+
("Fan Playback", "A medir", "40–60%", "A verificar"),
|
|
94
|
+
("Novos viewers", "A medir", "> 20%", "A verificar"),
|
|
95
|
+
("Abandono mensal", "A medir", "< 5%", "A verificar"),
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _card_html(card: tuple[str, int, bool, tuple[tuple[str, str, str, str], ...]]) -> str:
|
|
102
|
+
title, score, featured, rows = card
|
|
103
|
+
row_html = "".join(
|
|
104
|
+
f'<tr><td>{escape(label)}</td><td><b>{escape(value)}</b></td><td>{escape(target)}</td>'
|
|
105
|
+
f'<td><span style="color:#94a3b8;font-weight:700;">● {escape(status)}</span></td></tr>'
|
|
106
|
+
for label, value, target, status in rows
|
|
107
|
+
)
|
|
108
|
+
height = "350px" if featured else "280px"
|
|
109
|
+
title_size = "19px" if featured else "16px"
|
|
110
|
+
score_size = "38px" if featured else "30px"
|
|
111
|
+
return (
|
|
112
|
+
f'<div style="min-height:{height};border:1px solid #2b3b52;border-radius:14px;padding:16px;'
|
|
113
|
+
f'background:linear-gradient(145deg,#111c2b,#0b121c);box-shadow:0 8px 22px rgba(0,0,0,.16);">'
|
|
114
|
+
f'<div style="font-size:11px;font-weight:800;letter-spacing:.08em;color:#7dd3fc;text-transform:uppercase;">BILIBILI GROWTH</div>'
|
|
115
|
+
f'<div style="font-size:{title_size};font-weight:750;color:#f8fafc;margin-top:8px;">{escape(title)}</div>'
|
|
116
|
+
f'<div style="display:flex;align-items:baseline;gap:5px;margin:8px 0 10px;">'
|
|
117
|
+
f'<span style="font-size:{score_size};font-weight:850;color:#22c55e;">{score}</span>'
|
|
118
|
+
f'<span style="font-size:13px;color:#94a3b8;">/100 · meta do agente</span></div>'
|
|
119
|
+
f'<div style="overflow-x:auto;"><table style="width:100%;border-collapse:collapse;font-size:11px;color:#cbd5e1;">'
|
|
120
|
+
f'<thead><tr><th style="text-align:left;padding:5px 3px;border-bottom:1px solid #334155;">KPI</th>'
|
|
121
|
+
f'<th style="text-align:left;padding:5px 3px;border-bottom:1px solid #334155;">Valor</th>'
|
|
122
|
+
f'<th style="text-align:left;padding:5px 3px;border-bottom:1px solid #334155;">Meta</th>'
|
|
123
|
+
f'<th style="text-align:left;padding:5px 3px;border-bottom:1px solid #334155;">Estado</th></tr></thead>'
|
|
124
|
+
f'<tbody>{row_html}</tbody></table></div></div>'
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def render_growth_bilibili() -> None:
|
|
129
|
+
st.title("Analista Bilibili")
|
|
130
|
+
st.caption("Como o agente opera e reporta: procura, click funnel, retenção, engagement 三连 e saúde da audiência Bilibili.")
|
|
131
|
+
st.info("Esta aba define o protocolo e o dashboard de reporte do agente. Os valores privados são preenchidos quando os dados autorizados do Bilibili Studio estiverem disponíveis; não são inventados a partir de páginas públicas.")
|
|
132
|
+
|
|
133
|
+
summary_col, status_col = st.columns([3.25, 1], gap="large")
|
|
134
|
+
with summary_col:
|
|
135
|
+
st.markdown(
|
|
136
|
+
'<div style="border:1px solid #263447;border-radius:12px;padding:18px;margin:12px 0 20px;'
|
|
137
|
+
'background:#101722;display:flex;align-items:center;justify-content:space-between;min-height:76px;">'
|
|
138
|
+
'<div><div style="font-size:13px;color:#94a3b8;text-transform:uppercase;letter-spacing:.08em;">Estado do agente</div>'
|
|
139
|
+
'<div style="font-size:18px;font-weight:700;margin-top:5px;">Pronto para auditoria Bilibili</div></div>'
|
|
140
|
+
'<div style="font-size:18px;font-weight:800;color:#22c55e;">PROTOCOLO ACTIVO</div></div>',
|
|
141
|
+
unsafe_allow_html=True,
|
|
142
|
+
)
|
|
143
|
+
with status_col:
|
|
144
|
+
st.write("")
|
|
145
|
+
st.button("ANALISAR CANAL", disabled=True, use_container_width=True, key="growth_bilibili_analyse_disabled")
|
|
146
|
+
st.caption("A ligação ao Studio será usada quando autorizada.")
|
|
147
|
+
|
|
148
|
+
st.subheader("Dashboard de Growth")
|
|
149
|
+
st.caption("Os três pilares críticos aparecem primeiro; os cinco blocos operacionais completam o diagnóstico. Verde: referência de execução · Cinzento: dado a verificar no Studio.")
|
|
150
|
+
st.markdown('<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin:14px 0 18px;">' + "".join(_card_html(card) for card in BILIBILI_CARDS[:3]) + '</div>', unsafe_allow_html=True)
|
|
151
|
+
st.markdown('<div style="display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;margin:0 0 18px;">' + "".join(_card_html(card) for card in BILIBILI_CARDS[3:]) + '</div>', unsafe_allow_html=True)
|
|
152
|
+
st.caption("Legenda: o agente compara os dados autorizados com as metas Bilibili; quando um dado não existe, apresenta “A verificar” em vez de estimar.")
|
|
153
|
+
|
|
154
|
+
st.divider()
|
|
155
|
+
operation_tab, report_tab, monetization_tab = st.tabs(["Como o agente vai operar", "Como vai reportar", "Contexto de monetização"])
|
|
156
|
+
with operation_tab:
|
|
157
|
+
st.subheader("Protocolo de auditoria em 10 passos")
|
|
158
|
+
st.markdown("""
|
|
159
|
+
1. **Validar procura:** compara o tecto de concorrentes, pesquisa e origem do tráfego.
|
|
160
|
+
2. **Avaliar thumbnail:** verifica CTR, contraste, legibilidade móvel, emoção e elementos ACG quando adequados.
|
|
161
|
+
3. **Avaliar título:** cruza CTR com retenção inicial e aplica Curiosity, Clarity e Context.
|
|
162
|
+
4. **Verificar engagement:** mede a taxa 三连 (Like + Coin + Favorite), danmaku e qualidade dos comentários.
|
|
163
|
+
5. **Diagnosticar hook:** localiza a queda dos primeiros 15–30 segundos e aplica “clímax primeiro”.
|
|
164
|
+
6. **Diagnosticar ritmo:** identifica quedas recorrentes e recomenda pattern interrupts a cada 5–8 segundos.
|
|
165
|
+
7. **Ler o algoritmo:** compara Recommendation, Search, Followers e External com as metas da plataforma.
|
|
166
|
+
8. **Medir conversão:** calcula novos followers por visualização e posiciona a CTA aos 40% do vídeo.
|
|
167
|
+
9. **Verificar cadência:** acompanha 1–2 vídeos por semana, consistência e uso de posts/live para comunidade.
|
|
168
|
+
10. **Avaliar saúde:** cruza Fan Playback, New Viewer, abandono mensal e sinais moonshot.
|
|
169
|
+
""")
|
|
170
|
+
with report_tab:
|
|
171
|
+
st.subheader("Formato do reporte entregue pelo agente")
|
|
172
|
+
st.markdown("""
|
|
173
|
+
O agente reporta sempre em cinco blocos: **projecção financeira e contexto**, **resumo dos dez pilares**, **Top 3 prioridades accionáveis**, **estratégia de longo prazo** e **checklist dos próximos passos**. Cada KPI recebe valor observado, meta, estado e origem do dado.
|
|
174
|
+
|
|
175
|
+
As três prioridades são escolhidas por impacto: primeiro **Thumbnail + Título**, depois **retenção e engagement**, e só depois produção ou expansão. O relatório distingue claramente `DADO OBSERVADO`, `A VERIFICAR` e `RECOMENDAÇÃO`, sem transformar uma meta em resultado real.
|
|
176
|
+
|
|
177
|
+
> Regra de reporte: um vídeo com conteúdo forte, mas sem click funnel, não é considerado optimizado. O agente corrige primeiro a combinação thumbnail + título e acompanha a evolução no ciclo seguinte.
|
|
178
|
+
""")
|
|
179
|
+
st.markdown("**Checklist padrão:** validar nicho · corrigir as últimas 5 thumbnails · reescrever títulos · cortar a introdução · inserir prompt danmaku · publicar em dia fixo · rever analytics em 30 dias.")
|
|
180
|
+
with monetization_tab:
|
|
181
|
+
st.subheader("Referências internas usadas pelo agente")
|
|
182
|
+
st.markdown("""
|
|
183
|
+
A projecção considera Video Incentive Program, Charging, Paid Subscriptions e Huahuo. O cálculo interno é: `Playbacks × Base Rate × Zone Multiplier × Quality Multiplier + Ad Revenue + Tips + Subscriptions`.
|
|
184
|
+
|
|
185
|
+
Para o diagnóstico inicial, o agente acompanha originalidade do conteúdo, verificação de identidade, estado da conta, direitos ≥ 3, score de crédito ≥ 80 e os limiares de audiência definidos no material Bilibili. Estes itens são **contexto de elegibilidade**, não promessa de rendimento.
|
|
186
|
+
|
|
187
|
+
Os sinais moonshot acompanhados são densidade de danmaku (> 5 por minuto), crescimento do Search Index (+20% mensal), Long-tail Traffic (> 30%), Shares (> 1%) e Collect Rate (> 5%).
|
|
188
|
+
""")
|
|
@@ -62,14 +62,21 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
62
62
|
title = _meta(response.text, 'og:title') or reference['username']
|
|
63
63
|
description = _meta(response.text, 'og:description')
|
|
64
64
|
avatar_url = _meta(response.text, 'og:image')
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
def metric(patterns: tuple[str, ...]) -> int | None:
|
|
66
|
+
for pattern in patterns:
|
|
67
|
+
found = re.search(pattern, description, flags=re.IGNORECASE)
|
|
68
|
+
if not found:
|
|
69
|
+
continue
|
|
70
|
+
try:
|
|
71
|
+
return int(re.sub(r'[^0-9]', '', found.group(1)))
|
|
72
|
+
except ValueError:
|
|
73
|
+
continue
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
followers = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:followers|seguidores)',))
|
|
77
|
+
following = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:following|seguindo)',))
|
|
78
|
+
posts = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:posts|publicações|publications)',))
|
|
79
|
+
data = {'id': f"instagram_{reference['username']}", **reference, 'name': title.split('(')[0].strip() or reference['username'], 'bio': description, 'avatar_url': avatar_url, 'subscriber_count': followers, 'following_count': following, 'post_count': posts, 'public_lookup': True, 'metrics_source': 'instagram_public_page', 'last_public_lookup_at': datetime.now(timezone.utc).isoformat()}
|
|
73
80
|
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', data)
|
|
74
81
|
|
|
75
82
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
META_GRAPH_BASE_URL = "https://graph.facebook.com/v20.0"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _result(status: str, message: str, *, status_code: int | None = None, data: Mapping[str, Any] | None = None) -> dict[str, Any]:
|
|
13
|
+
payload: dict[str, Any] = {
|
|
14
|
+
"status": status,
|
|
15
|
+
"message": message[:240],
|
|
16
|
+
"checked_at": datetime.now(timezone.utc).isoformat(),
|
|
17
|
+
}
|
|
18
|
+
if status_code is not None:
|
|
19
|
+
payload["status_code"] = int(status_code)
|
|
20
|
+
if data:
|
|
21
|
+
payload["data"] = dict(data)
|
|
22
|
+
return payload
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_graph(identifier: str, access_token: str, fields: str) -> dict[str, Any]:
|
|
26
|
+
identifier = str(identifier or "").strip()
|
|
27
|
+
access_token = str(access_token or "").strip()
|
|
28
|
+
if not access_token:
|
|
29
|
+
return _result("missing", "Informe a API key/token antes do teste.")
|
|
30
|
+
if not identifier:
|
|
31
|
+
return _result("missing", "Informe o identificador da conta antes do teste.")
|
|
32
|
+
try:
|
|
33
|
+
response = requests.get(
|
|
34
|
+
f"{META_GRAPH_BASE_URL}/{identifier}",
|
|
35
|
+
params={"fields": fields, "access_token": access_token},
|
|
36
|
+
timeout=15,
|
|
37
|
+
)
|
|
38
|
+
except requests.RequestException:
|
|
39
|
+
return _result("error", "Não foi possível contactar a Meta Graph API.")
|
|
40
|
+
try:
|
|
41
|
+
payload = response.json()
|
|
42
|
+
except ValueError:
|
|
43
|
+
payload = {}
|
|
44
|
+
if response.status_code >= 400 or payload.get("error"):
|
|
45
|
+
error = payload.get("error") if isinstance(payload, dict) else {}
|
|
46
|
+
message = str(error.get("message") or f"A Meta Graph API devolveu HTTP {response.status_code}.")
|
|
47
|
+
return _result("error", message, status_code=response.status_code)
|
|
48
|
+
return _result("success", "Chamada Meta Graph API concluída.", status_code=response.status_code, data={
|
|
49
|
+
"id": payload.get("id"),
|
|
50
|
+
"name": payload.get("name"),
|
|
51
|
+
"username": payload.get("username"),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_instagram_api_card(card: Mapping[str, Any]) -> dict[str, Any]:
|
|
56
|
+
"""Validate an Instagram Graph API card without persisting its token."""
|
|
57
|
+
return _get_graph(
|
|
58
|
+
str(card.get("account_id") or ""),
|
|
59
|
+
str(card.get("access_token") or ""),
|
|
60
|
+
"id,username,name,profile_picture_url,followers_count,media_count",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_facebook_pages_api_card(card: Mapping[str, Any]) -> dict[str, Any]:
|
|
65
|
+
"""Validate a Facebook Pages Graph API card without persisting its token."""
|
|
66
|
+
return _get_graph(
|
|
67
|
+
str(card.get("page_id") or ""),
|
|
68
|
+
str(card.get("access_token") or ""),
|
|
69
|
+
"id,name,fan_count,followers_count,link",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = ["META_GRAPH_BASE_URL", "test_instagram_api_card", "test_facebook_pages_api_card"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danhachuel/thunderbolt",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15",
|
|
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",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"app/main.py",
|
|
13
13
|
"app/influencers_ui.py",
|
|
14
|
+
"app/social_networks_ui.py",
|
|
14
15
|
"app/modules/**/*.py",
|
|
15
16
|
"hermes_ui/*.py",
|
|
16
17
|
"integrations/*.py",
|