@danhachuel/thunderbolt 0.6.24 → 0.6.26
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.
|
@@ -42,6 +42,12 @@ def _metric_input(value: Any) -> int | None:
|
|
|
42
42
|
return normalize_instagram_metric(value)
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _render_instagram_bio(value: Any) -> None:
|
|
46
|
+
bio = normalize_instagram_bio(value)
|
|
47
|
+
if bio:
|
|
48
|
+
st.markdown(bio.replace("\n", " \n"))
|
|
49
|
+
|
|
50
|
+
|
|
45
51
|
def _language_index(value: Any) -> int:
|
|
46
52
|
code = language_code(value)
|
|
47
53
|
return list(LANGUAGE_CODES).index(code) if code in LANGUAGE_CODES else list(LANGUAGE_CODES).index("pt")
|
|
@@ -326,9 +332,7 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
|
|
|
326
332
|
with header_cols[1]:
|
|
327
333
|
st.write(f"**{_clean(profile.get('name')) or 'Sem nome'}**")
|
|
328
334
|
st.caption(f"{_clean(profile.get('handle')) or _clean(profile.get('url')) or 'sem handler'}")
|
|
329
|
-
|
|
330
|
-
if bio:
|
|
331
|
-
st.caption(f"Bio: {bio}")
|
|
335
|
+
_render_instagram_bio(profile.get("bio"))
|
|
332
336
|
st.caption(f"{_clean(profile.get('country')) or 'País não definido'} · {_clean(profile.get('language')) or 'Idioma não definido'}")
|
|
333
337
|
with header_cols[2]:
|
|
334
338
|
st.metric("posts", _metric(profile.get("post_count")))
|
|
@@ -447,9 +451,7 @@ def render_social_networks(settings: dict[str, Any]) -> None:
|
|
|
447
451
|
with preview_cols[1]:
|
|
448
452
|
st.write(f"**{_clean(data.get('name')) or _clean(data.get('username'))}**")
|
|
449
453
|
st.caption(_clean(data.get("handle")))
|
|
450
|
-
|
|
451
|
-
if bio:
|
|
452
|
-
st.caption(f"Bio: {bio}")
|
|
454
|
+
_render_instagram_bio(data.get("bio"))
|
|
453
455
|
with preview_cols[2]:
|
|
454
456
|
st.metric("posts", _metric(data.get("post_count")))
|
|
455
457
|
with preview_cols[3]:
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import json
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
5
7
|
from dataclasses import dataclass
|
|
6
8
|
from datetime import datetime, timezone
|
|
7
9
|
from html import unescape
|
|
@@ -96,24 +98,47 @@ def _structured_metric_from_json(document: str, *keys: str) -> int | None:
|
|
|
96
98
|
|
|
97
99
|
|
|
98
100
|
def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
101
|
+
headers = {
|
|
102
|
+
'x-ig-app-id': '936619743392459',
|
|
103
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
|
|
104
|
+
'Accept': 'application/json, text/plain, */*',
|
|
105
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
106
|
+
}
|
|
107
|
+
# The www host is the current public web endpoint. The legacy i host is
|
|
108
|
+
# retained as a fallback because Instagram rate-limits the two hosts
|
|
109
|
+
# independently and their availability can vary by region.
|
|
110
|
+
endpoints = (
|
|
111
|
+
f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
112
|
+
f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
113
|
+
)
|
|
114
|
+
for endpoint in endpoints:
|
|
115
|
+
try:
|
|
116
|
+
response = requests.get(endpoint, headers=headers, timeout=15)
|
|
117
|
+
if response.status_code >= 400:
|
|
118
|
+
continue
|
|
119
|
+
payload = response.json()
|
|
120
|
+
except (requests.RequestException, ValueError, AttributeError):
|
|
121
|
+
continue
|
|
122
|
+
user = ((payload.get('data') or {}).get('user') if isinstance(payload, dict) else None)
|
|
123
|
+
if isinstance(user, dict):
|
|
124
|
+
return user
|
|
125
|
+
curl = shutil.which('curl')
|
|
126
|
+
if curl:
|
|
127
|
+
try:
|
|
128
|
+
completed = subprocess.run(
|
|
129
|
+
[curl, '-L', '--max-time', '20', '-sS', '-A', headers['User-Agent'], '-H', f"x-ig-app-id: {headers['x-ig-app-id']}", endpoint],
|
|
130
|
+
capture_output=True,
|
|
131
|
+
text=True,
|
|
132
|
+
timeout=25,
|
|
133
|
+
check=False,
|
|
134
|
+
)
|
|
135
|
+
payload = json.loads(completed.stdout) if completed.returncode == 0 and completed.stdout else None
|
|
136
|
+
user = ((payload.get('data') or {}).get('user') if isinstance(payload, dict) else None)
|
|
137
|
+
if isinstance(user, dict):
|
|
138
|
+
return user
|
|
139
|
+
except (OSError, subprocess.SubprocessError, ValueError):
|
|
140
|
+
pass
|
|
141
|
+
return None
|
|
117
142
|
|
|
118
143
|
|
|
119
144
|
def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]) -> dict[str, Any]:
|
|
@@ -131,6 +156,7 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
|
|
|
131
156
|
'subscriber_count': followers,
|
|
132
157
|
'following_count': following,
|
|
133
158
|
'post_count': media,
|
|
159
|
+
'is_private': bool(user.get('is_private')),
|
|
134
160
|
'public_lookup': True,
|
|
135
161
|
'metrics_source': 'instagram_web_profile_info',
|
|
136
162
|
'last_public_lookup_at': datetime.now(timezone.utc).isoformat(),
|
|
@@ -161,15 +187,15 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
161
187
|
reference = normalize_instagram_reference(source)
|
|
162
188
|
except ValueError as exc:
|
|
163
189
|
return IntegrationResult(False, str(exc), {})
|
|
190
|
+
api_user = _fetch_web_profile_user(reference['username'])
|
|
191
|
+
if api_user:
|
|
192
|
+
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', _profile_data_from_api(api_user, reference))
|
|
164
193
|
try:
|
|
165
194
|
response = requests.get(reference['url'], headers={'User-Agent': 'Mozilla/5.0', 'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8'}, timeout=12)
|
|
166
195
|
except requests.RequestException as exc:
|
|
167
196
|
return IntegrationResult(False, f'Não foi possível consultar o perfil público do Instagram: {exc}', reference)
|
|
168
197
|
if response.status_code >= 400:
|
|
169
198
|
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})
|
|
170
|
-
api_user = _fetch_web_profile_user(reference['username'])
|
|
171
|
-
if api_user:
|
|
172
|
-
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', _profile_data_from_api(api_user, reference))
|
|
173
199
|
title = _meta(response.text, 'og:title') or reference['username']
|
|
174
200
|
description = _meta(response.text, 'og:description')
|
|
175
201
|
avatar_url = _meta(response.text, 'og:image')
|
|
@@ -264,6 +290,8 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
264
290
|
api_posts.append(post)
|
|
265
291
|
if api_posts:
|
|
266
292
|
return IntegrationResult(True, 'Posts públicos encontrados.', reference | {'posts': api_posts[:max(1, int(limit))]})
|
|
293
|
+
if api_user.get('is_private'):
|
|
294
|
+
return IntegrationResult(False, 'Esta conta é privada. O Instagram não disponibiliza posts sem uma sessão autenticada.', reference | {'posts': [], 'is_private': True})
|
|
267
295
|
try:
|
|
268
296
|
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)
|
|
269
297
|
except requests.RequestException as exc:
|
package/package.json
CHANGED