@danhachuel/thunderbolt 0.6.34 → 0.6.36
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 +10 -9
- package/integrations/instagram_public.py +117 -22
- package/package.json +1 -1
|
@@ -591,6 +591,7 @@ def render_social_networks(settings: dict[str, Any]) -> None:
|
|
|
591
591
|
data = st.session_state.get("social_instagram_result", {}) if st.session_state.get("social_instagram_ok") else {}
|
|
592
592
|
if data:
|
|
593
593
|
characters, _ = _characters(settings)
|
|
594
|
+
result_widget_id = _instagram_profile_storage_id(data)
|
|
594
595
|
with st.container(border=True):
|
|
595
596
|
st.subheader("Conta Instagram encontrada")
|
|
596
597
|
preview_cols = st.columns([0.8, 2.2, 1.2, 1.2, 1.2])
|
|
@@ -607,20 +608,20 @@ def render_social_networks(settings: dict[str, Any]) -> None:
|
|
|
607
608
|
st.metric("Seguidores", _metric(_profile_metric(data, "subscriber_count", "followers_count", "follower_count", "followers", "edge_followed_by")))
|
|
608
609
|
with preview_cols[4]:
|
|
609
610
|
st.metric("seguindo", _metric(_profile_metric(data, "following_count", "following", "follows", "followingCount", "edge_follow")))
|
|
610
|
-
with st.form("
|
|
611
|
+
with st.form(f"social_instagram_save_public_profile_{result_widget_id}"):
|
|
611
612
|
form_cols = st.columns(2)
|
|
612
613
|
with form_cols[0]:
|
|
613
|
-
name = st.text_input("Nome", value=_clean(data.get("name")), key="
|
|
614
|
-
bio = st.text_area("Bio", value=_profile_bio(data), key="
|
|
615
|
-
country = st.selectbox("País", _country_options(), index=_country_index(_profile_country(data)), format_func=_country_label, key="
|
|
616
|
-
language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=_language_index(data.get("language")), format_func=language_label, key="
|
|
614
|
+
name = st.text_input("Nome", value=_clean(data.get("name")), key=f"social_instagram_result_name_{result_widget_id}")
|
|
615
|
+
bio = st.text_area("Bio", value=_profile_bio(data), key=f"social_instagram_result_bio_{result_widget_id}")
|
|
616
|
+
country = st.selectbox("País", _country_options(), index=_country_index(_profile_country(data)), format_func=_country_label, key=f"social_instagram_result_country_{result_widget_id}")
|
|
617
|
+
language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=_language_index(data.get("language")), format_func=language_label, key=f"social_instagram_result_language_{result_widget_id}")
|
|
617
618
|
with form_cols[1]:
|
|
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="
|
|
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="
|
|
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="
|
|
619
|
+
posts = st.number_input("posts", min_value=0, value=_metric_input(_profile_metric(data, "post_count", "posts", "media_count")), placeholder="Não encontrado", key=f"social_instagram_result_posts_{result_widget_id}")
|
|
620
|
+
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=f"social_instagram_result_followers_{result_widget_id}")
|
|
621
|
+
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=f"social_instagram_result_following_{result_widget_id}")
|
|
621
622
|
character_options = [""] + [_clean(item.get("id")) for item in characters if _clean(item.get("id"))]
|
|
622
623
|
character_labels = {"": "Sem personagem associado"} | {_clean(item.get("id")): _clean(item.get("name")) or _clean(item.get("id")) for item in characters}
|
|
623
|
-
selected_character = st.selectbox("Personagem", character_options, format_func=lambda value: character_labels.get(value, value), key="
|
|
624
|
+
selected_character = st.selectbox("Personagem", character_options, format_func=lambda value: character_labels.get(value, value), key=f"social_instagram_result_character_{result_widget_id}")
|
|
624
625
|
save_profile = st.form_submit_button("Cadastrar conta Instagram", type="primary", use_container_width=True)
|
|
625
626
|
if save_profile:
|
|
626
627
|
profile_data = dict(data)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
import shutil
|
|
6
7
|
import subprocess
|
|
7
8
|
from dataclasses import dataclass
|
|
@@ -13,6 +14,31 @@ from urllib.parse import urlparse
|
|
|
13
14
|
import requests
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
ABOUT_ACCOUNT_URL = 'https://i.instagram.com/api/v1/bloks/apps/com.instagram.interactions.about_this_account/'
|
|
18
|
+
ABOUT_ACCOUNT_BLOKS_VERSION = '8ca96ca267e30c02cf90888d91eeff09627f0e3fd2bd9df472278c9a6c022cbb'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _instagram_cookies() -> dict[str, str]:
|
|
22
|
+
cookies = {}
|
|
23
|
+
sessionid = os.getenv('INSTAGRAM_SESSIONID') or os.getenv('IG_SESSIONID')
|
|
24
|
+
if sessionid:
|
|
25
|
+
cookies['sessionid'] = sessionid.strip()
|
|
26
|
+
for name in ('csrftoken', 'ds_user_id', 'mid', 'ig_did'):
|
|
27
|
+
value = os.getenv(f'INSTAGRAM_{name.upper()}') or os.getenv(f'IG_{name.upper()}')
|
|
28
|
+
if value:
|
|
29
|
+
cookies[name] = value.strip()
|
|
30
|
+
return cookies
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _instagram_headers() -> dict[str, str]:
|
|
34
|
+
return {
|
|
35
|
+
'x-ig-app-id': '936619743392459',
|
|
36
|
+
'User-Agent': 'Instagram 390.0.0.0.50 Android',
|
|
37
|
+
'Accept': 'application/json, text/plain, */*',
|
|
38
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
16
42
|
@dataclass
|
|
17
43
|
class IntegrationResult:
|
|
18
44
|
ok: bool
|
|
@@ -104,12 +130,8 @@ def _structured_metric_from_json(document: str, *keys: str) -> int | None:
|
|
|
104
130
|
|
|
105
131
|
|
|
106
132
|
def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
107
|
-
headers =
|
|
108
|
-
|
|
109
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
|
|
110
|
-
'Accept': 'application/json, text/plain, */*',
|
|
111
|
-
'Accept-Language': 'en-US,en;q=0.9',
|
|
112
|
-
}
|
|
133
|
+
headers = _instagram_headers()
|
|
134
|
+
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
|
|
113
135
|
# The www host is the current public web endpoint. The legacy i host is
|
|
114
136
|
# retained as a fallback because Instagram rate-limits the two hosts
|
|
115
137
|
# independently and their availability can vary by region.
|
|
@@ -117,12 +139,40 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
117
139
|
f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
118
140
|
f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
119
141
|
)
|
|
142
|
+
def profile_quality(user: Mapping[str, Any]) -> int:
|
|
143
|
+
return sum(bool(user.get(key)) for key in ('biography', 'edge_follow', 'edge_followed_by', 'edge_owner_to_timeline_media', 'username'))
|
|
144
|
+
|
|
145
|
+
def merge_profile(base: dict[str, Any] | None, candidate: Mapping[str, Any]) -> dict[str, Any]:
|
|
146
|
+
merged = dict(base or {})
|
|
147
|
+
for key, value in candidate.items():
|
|
148
|
+
if value not in (None, '', [], {}):
|
|
149
|
+
if isinstance(value, Mapping) and isinstance(merged.get(key), Mapping):
|
|
150
|
+
merged[key] = {**merged[key], **value}
|
|
151
|
+
else:
|
|
152
|
+
merged[key] = value
|
|
153
|
+
return merged
|
|
154
|
+
|
|
155
|
+
best_user: dict[str, Any] | None = None
|
|
156
|
+
for endpoint in endpoints:
|
|
157
|
+
try:
|
|
158
|
+
response = requests.get(endpoint, headers=headers, timeout=15)
|
|
159
|
+
if response.status_code >= 400:
|
|
160
|
+
continue
|
|
161
|
+
payload = response.json()
|
|
162
|
+
except (requests.RequestException, ValueError, AttributeError):
|
|
163
|
+
continue
|
|
164
|
+
user = ((payload.get('data') or {}).get('user') if isinstance(payload, dict) else None)
|
|
165
|
+
if isinstance(user, dict):
|
|
166
|
+
best_user = merge_profile(best_user, user)
|
|
167
|
+
if profile_quality(best_user) >= 4:
|
|
168
|
+
return best_user
|
|
169
|
+
|
|
120
170
|
curl = shutil.which('curl')
|
|
121
171
|
if curl:
|
|
122
172
|
for endpoint in endpoints:
|
|
123
173
|
try:
|
|
124
174
|
completed = subprocess.run(
|
|
125
|
-
[curl, '-L', '--max-time', '20', '-sS', '-A', headers['User-Agent'], '-H', f"x-ig-app-id: {headers['x-ig-app-id']}", '-H', 'Accept
|
|
175
|
+
[curl, '-L', '--max-time', '20', '-sS', '-A', headers['User-Agent'], '-H', f"x-ig-app-id: {headers['x-ig-app-id']}", '-H', 'Accept', 'application/json, text/plain, */*', endpoint],
|
|
126
176
|
capture_output=True,
|
|
127
177
|
text=True,
|
|
128
178
|
encoding='utf-8',
|
|
@@ -133,21 +183,66 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
133
183
|
payload = json.loads(completed.stdout) if completed.returncode == 0 and completed.stdout else None
|
|
134
184
|
user = ((payload.get('data') or {}).get('user') if isinstance(payload, dict) else None)
|
|
135
185
|
if isinstance(user, dict):
|
|
136
|
-
|
|
186
|
+
best_user = merge_profile(best_user, user)
|
|
187
|
+
if profile_quality(best_user) >= 4:
|
|
188
|
+
return best_user
|
|
137
189
|
except (OSError, subprocess.SubprocessError, ValueError):
|
|
138
190
|
continue
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
191
|
+
return best_user
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _country_from_bloks(value: Any) -> str:
|
|
195
|
+
if isinstance(value, Mapping):
|
|
196
|
+
data_key = str(value.get('key') or '').casefold()
|
|
197
|
+
if data_key in PUBLIC_COUNTRY_KEYS:
|
|
198
|
+
serialized = value.get('initial_lispy') or value.get('value')
|
|
199
|
+
if isinstance(serialized, str) and 'bk.action.array.Make' in serialized:
|
|
200
|
+
country = _country_value(serialized.rsplit('bk.action.array.Make,', 1)[-1].split(')', 1)[0].replace('\\"', '"').replace('"', '').strip())
|
|
201
|
+
else:
|
|
202
|
+
country = _country_value(serialized)
|
|
203
|
+
if country:
|
|
204
|
+
return country
|
|
205
|
+
for key, candidate in value.items():
|
|
206
|
+
if str(key).casefold() in PUBLIC_COUNTRY_KEYS:
|
|
207
|
+
if isinstance(candidate, str) and 'bk.action.array.Make' in candidate:
|
|
208
|
+
country = _country_value(candidate.rsplit('bk.action.array.Make,', 1)[-1].split(')', 1)[0].replace('\\"', '"').replace('"', '').strip())
|
|
209
|
+
else:
|
|
210
|
+
country = _country_value(candidate)
|
|
211
|
+
if country:
|
|
212
|
+
return country
|
|
213
|
+
country = _country_from_bloks(candidate)
|
|
214
|
+
if country:
|
|
215
|
+
return country
|
|
216
|
+
elif isinstance(value, list):
|
|
217
|
+
for candidate in value:
|
|
218
|
+
country = _country_from_bloks(candidate)
|
|
219
|
+
if country:
|
|
220
|
+
return country
|
|
221
|
+
elif isinstance(value, str) and 'about_this_account_country' in value:
|
|
222
|
+
marker = 'bk.action.array.Make,'
|
|
223
|
+
if marker in value:
|
|
224
|
+
country = value.rsplit(marker, 1)[-1].split(')', 1)[0].replace('\\"', '"').replace('"', '').strip()
|
|
225
|
+
return _country_value(country)
|
|
226
|
+
return ''
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _fetch_instagram_about_country(user_id: Any) -> str:
|
|
230
|
+
cookies = _instagram_cookies()
|
|
231
|
+
if not cookies or not str(user_id or '').strip():
|
|
232
|
+
return ''
|
|
233
|
+
payload = {
|
|
234
|
+
'referer_type': 'ProfileUsername',
|
|
235
|
+
'target_user_id': str(user_id),
|
|
236
|
+
'bk_client_context': json.dumps({'bloks_version': ABOUT_ACCOUNT_BLOKS_VERSION, 'style_id': 'instagram'}, separators=(',', ':')),
|
|
237
|
+
'bloks_versioning_id': ABOUT_ACCOUNT_BLOKS_VERSION,
|
|
238
|
+
}
|
|
239
|
+
try:
|
|
240
|
+
response = requests.post(ABOUT_ACCOUNT_URL, data=payload, headers=_instagram_headers(), cookies=cookies, timeout=20)
|
|
241
|
+
if response.status_code >= 400:
|
|
242
|
+
return ''
|
|
243
|
+
return _country_from_bloks(response.json())
|
|
244
|
+
except (requests.RequestException, ValueError, TypeError):
|
|
245
|
+
return ''
|
|
151
246
|
|
|
152
247
|
|
|
153
248
|
def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]) -> dict[str, Any]:
|
|
@@ -157,7 +252,7 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
|
|
|
157
252
|
following = following if following is not None else _structured_metric_from_json(json.dumps(user), 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
|
|
158
253
|
media = ((user.get('edge_owner_to_timeline_media') or {}).get('count') if isinstance(user.get('edge_owner_to_timeline_media'), dict) else None)
|
|
159
254
|
biography = str(user.get('biography') or '').strip()
|
|
160
|
-
country = extract_public_instagram_country(user)
|
|
255
|
+
country = extract_public_instagram_country(user) or _fetch_instagram_about_country(user.get('id') or user.get('pk'))
|
|
161
256
|
return {
|
|
162
257
|
'id': f"instagram_{reference['username']}",
|
|
163
258
|
**reference,
|
|
@@ -188,7 +283,7 @@ def normalize_instagram_metric(value: Any) -> int | None:
|
|
|
188
283
|
|
|
189
284
|
|
|
190
285
|
PUBLIC_COUNTRY_KEYS = {
|
|
191
|
-
'account_based_in', 'account_country', 'country_name',
|
|
286
|
+
'about_this_account_country', 'account_based_in', 'account_country', 'country_name',
|
|
192
287
|
'country_of_origin', 'country_of_registration', 'location_country', 'transparency_country',
|
|
193
288
|
}
|
|
194
289
|
PUBLIC_TRANSPARENCY_KEYS = {'about', 'about_account', 'account_transparency', 'transparency', 'account_info'}
|
package/package.json
CHANGED