@danhachuel/thunderbolt 0.6.33 → 0.6.35

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.
@@ -483,7 +483,7 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
483
483
  with header_cols[5]:
484
484
  refresh_col, edit_col = st.columns(2)
485
485
  with refresh_col:
486
- if st.button("Actualizar posts, seguidores e seguindo", key=f"refresh_instagram_{profile_id}", use_container_width=True):
486
+ if st.button("↻", help="Actualizar posts, seguidores e seguindo", key=f"refresh_instagram_{profile_id}"):
487
487
  refreshed_ok, refreshed_message, refreshed = _refresh_instagram_profile(profile)
488
488
  if refreshed_ok:
489
489
  current_posts = _stored_instagram_posts(profile)
@@ -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("social_instagram_save_public_profile"):
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="social_instagram_result_name")
614
- bio = st.text_area("Bio", value=_profile_bio(data), key="social_instagram_result_bio")
615
- country = st.selectbox("País", _country_options(), index=_country_index(_profile_country(data)), format_func=_country_label, key="social_instagram_result_country")
616
- language = st.selectbox("Idioma", list(LANGUAGE_CODES), index=_language_index(data.get("language")), format_func=language_label, key="social_instagram_result_language")
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="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")
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="social_instagram_result_character")
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
- 'x-ig-app-id': '936619743392459',
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.
@@ -150,6 +172,60 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
150
172
  return None
151
173
 
152
174
 
175
+ def _country_from_bloks(value: Any) -> str:
176
+ if isinstance(value, Mapping):
177
+ data_key = str(value.get('key') or '').casefold()
178
+ if data_key in PUBLIC_COUNTRY_KEYS:
179
+ serialized = value.get('initial_lispy') or value.get('value')
180
+ if isinstance(serialized, str) and 'bk.action.array.Make' in serialized:
181
+ country = _country_value(serialized.rsplit('bk.action.array.Make,', 1)[-1].split(')', 1)[0].replace('\\"', '"').replace('"', '').strip())
182
+ else:
183
+ country = _country_value(serialized)
184
+ if country:
185
+ return country
186
+ for key, candidate in value.items():
187
+ if str(key).casefold() in PUBLIC_COUNTRY_KEYS:
188
+ if isinstance(candidate, str) and 'bk.action.array.Make' in candidate:
189
+ country = _country_value(candidate.rsplit('bk.action.array.Make,', 1)[-1].split(')', 1)[0].replace('\\"', '"').replace('"', '').strip())
190
+ else:
191
+ country = _country_value(candidate)
192
+ if country:
193
+ return country
194
+ country = _country_from_bloks(candidate)
195
+ if country:
196
+ return country
197
+ elif isinstance(value, list):
198
+ for candidate in value:
199
+ country = _country_from_bloks(candidate)
200
+ if country:
201
+ return country
202
+ elif isinstance(value, str) and 'about_this_account_country' in value:
203
+ marker = 'bk.action.array.Make,'
204
+ if marker in value:
205
+ country = value.rsplit(marker, 1)[-1].split(')', 1)[0].replace('\\"', '"').replace('"', '').strip()
206
+ return _country_value(country)
207
+ return ''
208
+
209
+
210
+ def _fetch_instagram_about_country(user_id: Any) -> str:
211
+ cookies = _instagram_cookies()
212
+ if not cookies or not str(user_id or '').strip():
213
+ return ''
214
+ payload = {
215
+ 'referer_type': 'ProfileUsername',
216
+ 'target_user_id': str(user_id),
217
+ 'bk_client_context': json.dumps({'bloks_version': ABOUT_ACCOUNT_BLOKS_VERSION, 'style_id': 'instagram'}, separators=(',', ':')),
218
+ 'bloks_versioning_id': ABOUT_ACCOUNT_BLOKS_VERSION,
219
+ }
220
+ try:
221
+ response = requests.post(ABOUT_ACCOUNT_URL, data=payload, headers=_instagram_headers(), cookies=cookies, timeout=20)
222
+ if response.status_code >= 400:
223
+ return ''
224
+ return _country_from_bloks(response.json())
225
+ except (requests.RequestException, ValueError, TypeError):
226
+ return ''
227
+
228
+
153
229
  def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]) -> dict[str, Any]:
154
230
  followers = _profile_metric(user, 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
155
231
  following = _profile_metric(user, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
@@ -157,7 +233,7 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
157
233
  following = following if following is not None else _structured_metric_from_json(json.dumps(user), 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
158
234
  media = ((user.get('edge_owner_to_timeline_media') or {}).get('count') if isinstance(user.get('edge_owner_to_timeline_media'), dict) else None)
159
235
  biography = str(user.get('biography') or '').strip()
160
- country = extract_public_instagram_country(user)
236
+ country = extract_public_instagram_country(user) or _fetch_instagram_about_country(user.get('id') or user.get('pk'))
161
237
  return {
162
238
  'id': f"instagram_{reference['username']}",
163
239
  **reference,
@@ -188,7 +264,7 @@ def normalize_instagram_metric(value: Any) -> int | None:
188
264
 
189
265
 
190
266
  PUBLIC_COUNTRY_KEYS = {
191
- 'account_based_in', 'account_country', 'country_name',
267
+ 'about_this_account_country', 'account_based_in', 'account_country', 'country_name',
192
268
  'country_of_origin', 'country_of_registration', 'location_country', 'transparency_country',
193
269
  }
194
270
  PUBLIC_TRANSPARENCY_KEYS = {'about', 'about_account', 'account_transparency', 'transparency', 'account_info'}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.33",
3
+ "version": "0.6.35",
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",