@danhachuel/thunderbolt 0.6.20 → 0.6.22
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.
|
@@ -12,7 +12,7 @@ from hermes_ui.domain import create_channel, update_channel
|
|
|
12
12
|
from hermes_ui.influencers import InfluencerBackendError, STANDALONE_CONTENT_INFLUENCER_ID, get_repository
|
|
13
13
|
from hermes_ui.storage import read_json, write_json
|
|
14
14
|
from hermes_ui.languages import LANGUAGE_CODES, language_code, language_label
|
|
15
|
-
from integrations.instagram_public import fetch_public_instagram_posts, fetch_public_instagram_profile
|
|
15
|
+
from integrations.instagram_public import fetch_public_instagram_posts, fetch_public_instagram_profile, normalize_instagram_bio
|
|
16
16
|
from integrations.meta_social import test_facebook_pages_api_card, test_instagram_api_card
|
|
17
17
|
|
|
18
18
|
|
|
@@ -298,7 +298,7 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
|
|
|
298
298
|
with header_cols[1]:
|
|
299
299
|
st.write(f"**{_clean(profile.get('name')) or 'Sem nome'}**")
|
|
300
300
|
st.caption(f"{_clean(profile.get('handle')) or _clean(profile.get('url')) or 'sem handler'}")
|
|
301
|
-
bio =
|
|
301
|
+
bio = normalize_instagram_bio(profile.get("bio"))
|
|
302
302
|
if bio:
|
|
303
303
|
st.caption(f"Bio: {bio}")
|
|
304
304
|
st.caption(f"{_clean(profile.get('country')) or 'País não definido'} · {_clean(profile.get('language')) or 'Idioma não definido'}")
|
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
7
|
from html import unescape
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any, Mapping
|
|
9
9
|
from urllib.parse import urlparse
|
|
10
10
|
|
|
11
11
|
import requests
|
|
@@ -18,6 +18,16 @@ class IntegrationResult:
|
|
|
18
18
|
data: dict[str, Any]
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def normalize_instagram_bio(value: Any) -> str:
|
|
22
|
+
"""Keep the profile biography, never Instagram's metrics summary."""
|
|
23
|
+
bio = str(value or '').strip()
|
|
24
|
+
if not bio:
|
|
25
|
+
return ''
|
|
26
|
+
if re.search(r'\b(?:followers|following|seguidores|seguindo|posts|publicações)\b', bio, flags=re.IGNORECASE) and re.search(r'\d', bio):
|
|
27
|
+
return ''
|
|
28
|
+
return bio
|
|
29
|
+
|
|
30
|
+
|
|
21
31
|
def normalize_instagram_reference(source: str) -> dict[str, str]:
|
|
22
32
|
value = str(source or '').strip()
|
|
23
33
|
if not value:
|
|
@@ -85,6 +95,47 @@ def _structured_metric_from_json(document: str, *keys: str) -> int | None:
|
|
|
85
95
|
return None
|
|
86
96
|
|
|
87
97
|
|
|
98
|
+
def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
99
|
+
try:
|
|
100
|
+
response = requests.get(
|
|
101
|
+
f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
102
|
+
headers={
|
|
103
|
+
'x-ig-app-id': '936619743392459',
|
|
104
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
|
|
105
|
+
'Accept': '*/*',
|
|
106
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
107
|
+
},
|
|
108
|
+
timeout=15,
|
|
109
|
+
)
|
|
110
|
+
if response.status_code >= 400:
|
|
111
|
+
return None
|
|
112
|
+
payload = response.json()
|
|
113
|
+
except (requests.RequestException, ValueError, AttributeError):
|
|
114
|
+
return None
|
|
115
|
+
user = ((payload.get('data') or {}).get('user') if isinstance(payload, dict) else None)
|
|
116
|
+
return user if isinstance(user, dict) else None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]) -> dict[str, Any]:
|
|
120
|
+
followers = _structured_metric_from_json(json.dumps(user), 'edge_followed_by', 'followers', 'follower_count')
|
|
121
|
+
following = _structured_metric_from_json(json.dumps(user), 'edge_follow', 'follows', 'following', 'following_count')
|
|
122
|
+
media = ((user.get('edge_owner_to_timeline_media') or {}).get('count') if isinstance(user.get('edge_owner_to_timeline_media'), dict) else None)
|
|
123
|
+
return {
|
|
124
|
+
'id': f"instagram_{reference['username']}",
|
|
125
|
+
**reference,
|
|
126
|
+
'name': str(user.get('full_name') or user.get('username') or reference['username']).strip(),
|
|
127
|
+
'bio': normalize_instagram_bio(user.get('biography')),
|
|
128
|
+
'avatar_url': str(user.get('profile_pic_url_hd') or user.get('profile_pic_url') or '').strip(),
|
|
129
|
+
'subscriber_count': followers,
|
|
130
|
+
'following_count': following,
|
|
131
|
+
'post_count': media,
|
|
132
|
+
'public_lookup': True,
|
|
133
|
+
'metrics_source': 'instagram_web_profile_info',
|
|
134
|
+
'last_public_lookup_at': datetime.now(timezone.utc).isoformat(),
|
|
135
|
+
'_api_posts': (((user.get('edge_owner_to_timeline_media') or {}).get('edges') or []) if isinstance(user.get('edge_owner_to_timeline_media'), dict) else []),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
88
139
|
def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
89
140
|
try:
|
|
90
141
|
reference = normalize_instagram_reference(source)
|
|
@@ -96,6 +147,9 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
96
147
|
return IntegrationResult(False, f'Não foi possível consultar o perfil público do Instagram: {exc}', reference)
|
|
97
148
|
if response.status_code >= 400:
|
|
98
149
|
return IntegrationResult(False, f'O Instagram devolveu HTTP {response.status_code}. Confirme o @handle ou use o cadastro manual.', reference | {'status_code': response.status_code})
|
|
150
|
+
api_user = _fetch_web_profile_user(reference['username'])
|
|
151
|
+
if api_user:
|
|
152
|
+
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', _profile_data_from_api(api_user, reference))
|
|
99
153
|
title = _meta(response.text, 'og:title') or reference['username']
|
|
100
154
|
description = _meta(response.text, 'og:description')
|
|
101
155
|
avatar_url = _meta(response.text, 'og:image')
|
|
@@ -113,7 +167,7 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
113
167
|
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')
|
|
114
168
|
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')
|
|
115
169
|
posts = metric((r'([\d,.]+)\s*(?:mil\s+)?(?:posts|publicações|publications)',)) or _structured_metric(response.text, 'edge_owner_to_timeline_media', 'posts', 'post_count')
|
|
116
|
-
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()}
|
|
170
|
+
data = {'id': f"instagram_{reference['username']}", **reference, 'name': title.split('(')[0].strip() or reference['username'], 'bio': normalize_instagram_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()}
|
|
117
171
|
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', data)
|
|
118
172
|
|
|
119
173
|
|
|
@@ -179,6 +233,17 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
179
233
|
reference = normalize_instagram_reference(source)
|
|
180
234
|
except ValueError as exc:
|
|
181
235
|
return IntegrationResult(False, str(exc), {})
|
|
236
|
+
api_user = _fetch_web_profile_user(reference['username'])
|
|
237
|
+
if api_user:
|
|
238
|
+
api_posts: list[dict[str, Any]] = []
|
|
239
|
+
for edge in ((api_user.get('edge_owner_to_timeline_media') or {}).get('edges') or []):
|
|
240
|
+
node = edge.get('node') if isinstance(edge, dict) else None
|
|
241
|
+
if isinstance(node, dict):
|
|
242
|
+
post = _post_from_node(node)
|
|
243
|
+
if post:
|
|
244
|
+
api_posts.append(post)
|
|
245
|
+
if api_posts:
|
|
246
|
+
return IntegrationResult(True, 'Posts públicos encontrados.', reference | {'posts': api_posts[:max(1, int(limit))]})
|
|
182
247
|
try:
|
|
183
248
|
response = requests.get(reference['url'], headers={'User-Agent': 'Mozilla/5.0', 'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8'}, timeout=15)
|
|
184
249
|
except requests.RequestException as exc:
|
|
@@ -201,4 +266,4 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
201
266
|
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))]})
|
|
202
267
|
|
|
203
268
|
|
|
204
|
-
__all__ = ['IntegrationResult', 'fetch_public_instagram_posts', 'fetch_public_instagram_profile', 'normalize_instagram_reference']
|
|
269
|
+
__all__ = ['IntegrationResult', 'fetch_public_instagram_posts', 'fetch_public_instagram_profile', 'normalize_instagram_bio', 'normalize_instagram_reference']
|
package/package.json
CHANGED