Dink0 commited on
Commit
ef8b120
·
verified ·
1 Parent(s): c9d1278

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +420 -0
app.py ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import asyncio
3
+ import nest_asyncio
4
+ import re
5
+ import os
6
+ import requests
7
+ import urllib.parse
8
+ import hashlib
9
+ import base64
10
+ import secrets
11
+ from telethon import TelegramClient
12
+ from telethon.sessions import StringSession
13
+ import streamlit.components.v1 as components
14
+
15
+ # Применяем nest_asyncio для работы с asyncio в Streamlit
16
+ nest_asyncio.apply()
17
+
18
+ # ----------- Настройки Telegram -----------
19
+ TELEGRAM_API_ID = 23646321
20
+ TELEGRAM_API_HASH = "102edf31d404cb72d17ee4831d4e8c7b"
21
+ TELEGRAM_STRING_SESSION = "1ApWapzMBu8a9o9Wu1GexPdkO7Jsr6po1xym67EcfAxkEbVx1oxw6wW7LINb9Xf0VYcyEAP-b-D0RQ_4x0RDAW8Di_0P2r4bj7xNeV9xO3wBMcaeq7FJux3UMq5buq89gqG8JarZG04tIyWoVnH__dq1_6G2Zkr0GF4WJCS2UN7mmwBacIIUEjUryEA97tXYC8jD14J-1MDgFsNNZTJ6uurD5RzH2DIhphChebxOkDBdXmpLGlanEzYzVOlx4p6Mo2zYorRG968o64CxKEh-WiJaPSNrS3IEfIVWFVd_tHKWzQSPD1zHHhcw0v2_ZjCKbZnTMcT3uMYuyLwA55_dckzcw0wZgAZQ="
22
+
23
+ # ----------- Настройки ВКонтакте -----------
24
+ VK_CLIENT_ID = 53202706
25
+ VK_CLIENT_SECRET = "piaMXHEDj0afa4QObJCD"
26
+ VK_API_VERSION = "5.131"
27
+ REDIRECT_URI = "https://huggingface.co/spaces/PRAnalytics/prtooltest"
28
+
29
+ # Инициализация токенов в session_state
30
+ if "VK_ACCESS_TOKEN" not in st.session_state:
31
+ st.session_state["VK_ACCESS_TOKEN"] = ""
32
+ if "VK_REFRESH_TOKEN" not in st.session_state:
33
+ st.session_state["VK_REFRESH_TOKEN"] = ""
34
+ if "VK_DEVICE_ID" not in st.session_state:
35
+ st.session_state["VK_DEVICE_ID"] = ""
36
+
37
+ # Функции для генерации code_verifier и code_challenge
38
+ def generate_code_verifier(length=128):
39
+ return secrets.token_urlsafe(length)[:length]
40
+
41
+ def generate_code_challenge(code_verifier):
42
+ hashed = hashlib.sha256(code_verifier.encode('utf-8')).digest()
43
+ return base64.urlsafe_b64encode(hashed).decode('utf-8').rstrip('=')
44
+
45
+ # Функция для авторизации через VK ID
46
+ def vk_auth():
47
+ if 'code_verifier' not in st.session_state:
48
+ st.session_state.code_verifier = generate_code_verifier()
49
+
50
+ st.title("VK ID Authorization with PKCE")
51
+
52
+ # Генерация code_challenge
53
+ code_challenge = generate_code_challenge(st.session_state.code_verifier)
54
+
55
+ # Шаг 1: Авторизация
56
+ st.markdown("### 1) Авторизация")
57
+ auth_params = {
58
+ "client_id": VK_CLIENT_ID,
59
+ "redirect_uri": REDIRECT_URI,
60
+ "response_type": "code",
61
+ "scope": "wall",
62
+ "state": st.session_state.code_verifier,
63
+ "code_challenge": code_challenge,
64
+ "code_challenge_method": "S256",
65
+ "response_mode": "query"
66
+ }
67
+ auth_url = "https://id.vk.com/authorize?" + urllib.parse.urlencode(auth_params, safe="-._~")
68
+ st.markdown(f"[Перейти к авторизации]({auth_url})")
69
+
70
+ st.markdown("""
71
+ После авторизации VK вернёт вас на redirect_uri с параметрами:
72
+ `?code=...&device_id=...&state=...`
73
+ """)
74
+
75
+ # Шаг 2: Автоматическое извлечение параметров из URL
76
+ query_params = st.experimental_get_query_params()
77
+ user_code = query_params.get("code", [None])[0]
78
+ user_device_id = query_params.get("device_id", [None])[0]
79
+ user_state = query_params.get("state", [None])[0]
80
+
81
+ # Если параметры есть, выполняем обмен кода на токены
82
+ if user_code and user_state:
83
+ st.write("Автоматически извлечены параметры из URL:")
84
+ st.write(f"code: {user_code}")
85
+ st.write(f"device_id: {user_device_id}")
86
+ st.write(f"state: {user_state}")
87
+
88
+ # Используем code_verifier из параметра state
89
+ code_verifier = user_state
90
+
91
+ url = "https://id.vk.com/oauth2/auth"
92
+ data = {
93
+ "grant_type": "authorization_code",
94
+ "client_id": VK_CLIENT_ID,
95
+ "client_secret": VK_CLIENT_SECRET,
96
+ "redirect_uri": REDIRECT_URI,
97
+ "code": user_code,
98
+ "code_verifier": code_verifier
99
+ }
100
+ if user_device_id:
101
+ data["device_id"] = user_device_id
102
+
103
+ st.write("Отправляем запрос на:", url)
104
+ st.write("Параметры запроса:", data)
105
+
106
+ # Отправка запроса на обмен кода на токены
107
+ resp = requests.post(url, data=data)
108
+ st.write("Status code:", resp.status_code)
109
+ st.write("Response text:", resp.text)
110
+
111
+ try:
112
+ tokens = resp.json()
113
+ st.write("Ответ (JSON):")
114
+ st.json(tokens)
115
+
116
+ # Сохраняем токены и device_id в session_state
117
+ st.session_state["VK_ACCESS_TOKEN"] = tokens["access_token"]
118
+ st.session_state["VK_REFRESH_TOKEN"] = tokens["refresh_token"]
119
+ st.session_state["VK_DEVICE_ID"] = user_device_id
120
+ st.success("Токены успешно получены и сохранены!")
121
+ except Exception as e:
122
+ st.error(f"Ошибка при декодировании ответа: {e}")
123
+ st.text(resp.text)
124
+ else:
125
+ st.warning("Параметры code, device_id и state не найдены в URL. Пройдите авторизацию.")
126
+
127
+ # Функция для обновления токена через refresh_token
128
+ def refresh_vk_token() -> bool:
129
+ st.write("Пытаемся обновить токен...")
130
+
131
+ current_refresh = st.session_state["VK_REFRESH_TOKEN"]
132
+ device_id = st.session_state.get("VK_DEVICE_ID", "")
133
+
134
+ if not current_refresh or not VK_CLIENT_ID or not VK_CLIENT_SECRET or not device_id:
135
+ st.error("Невозможно обновить токен: нет refresh_token, client_id, client_secret или device_id.")
136
+ return False
137
+
138
+ url = "https://id.vk.com/oauth2/auth"
139
+ data = {
140
+ "grant_type": "refresh_token",
141
+ "client_id": VK_CLIENT_ID,
142
+ "client_secret": VK_CLIENT_SECRET,
143
+ "refresh_token": current_refresh,
144
+ "device_id": device_id
145
+ }
146
+
147
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
148
+
149
+ resp = requests.post(url, data=data, headers=headers)
150
+ st.write(f"Статус ответа: {resp.status_code}")
151
+ st.write(f"Текст ответа: {resp.text}")
152
+
153
+ if resp.status_code != 200:
154
+ st.error(f"Ошибка обновления токена (HTTP {resp.status_code}): {resp.text}")
155
+ return False
156
+
157
+ try:
158
+ result = resp.json()
159
+ st.write(f"Ответ от VK API: {result}")
160
+ except Exception as e:
161
+ st.error(f"Не удалось распарсить ответ при обновлении токена: {e}")
162
+ return False
163
+
164
+ if "access_token" not in result:
165
+ err_desc = result.get("error_description", result)
166
+ st.error(f"Не удалось получить новый access_token: {err_desc}")
167
+ return False
168
+
169
+ # Сохраняем новые значения
170
+ new_access = result["access_token"]
171
+ st.session_state["VK_ACCESS_TOKEN"] = new_access
172
+
173
+ if "refresh_token" in result:
174
+ new_refresh = result["refresh_token"]
175
+ st.session_state["VK_REFRESH_TOKEN"] = new_refresh
176
+ st.write(f"Новый refresh_token: {new_refresh}")
177
+ else:
178
+ st.warning("Новый refresh_token не был получен. Убедитесь, что это ожидаемо.")
179
+
180
+ st.success("VK-токен обновлён успешно!")
181
+ st.write(f"Новый access_token: {new_access}")
182
+
183
+ return True
184
+
185
+ # Инициализация Telethon-клиента (Telegram)
186
+ def init_telethon():
187
+ loop = asyncio.new_event_loop()
188
+ asyncio.set_event_loop(loop)
189
+ client = TelegramClient(StringSession(TELEGRAM_STRING_SESSION), TELEGRAM_API_ID, TELEGRAM_API_HASH)
190
+ loop.run_until_complete(client.connect())
191
+ if not loop.run_until_complete(client.is_user_authorized()):
192
+ st.error("Telegram-сессия не авторизована. Проверьте StringSession или выполните login_telethon.py.")
193
+ st.stop()
194
+ return client, loop
195
+
196
+ if "telethon_client" not in st.session_state:
197
+ st.session_state["telethon_client"], st.session_state["telethon_loop"] = init_telethon()
198
+
199
+ tg_client = st.session_state["telethon_client"]
200
+ tg_loop = st.session_state["telethon_loop"]
201
+
202
+ # Функция для форматирования числа просмотров
203
+ def human_format_views(num: int) -> str:
204
+ if num < 1000:
205
+ return f"{num / 1000:.1f}K"
206
+ elif num < 10_000:
207
+ return f"{num / 1000:.1f}K"
208
+ elif num < 1_000_000:
209
+ return f"{int(num / 1000)}K"
210
+ else:
211
+ return f"{num / 1_000_000:.1f}M"
212
+
213
+ # Функция для обработки ссылок Telegram
214
+ def process_telegram_link(link: str) -> tuple[str, int]:
215
+ link = link.strip()
216
+ if not link:
217
+ return "", 0
218
+
219
+ match = re.match(r'https?://(?:t(?:elegram)?\.me)/([^/]+)/(\d+)', link, re.IGNORECASE)
220
+ if not match:
221
+ return f"**Ошибка (TG)**: не распознал ссылку — {link}", 0
222
+
223
+ channel_username = match.group(1)
224
+ message_id = int(match.group(2))
225
+
226
+ try:
227
+ msg = tg_loop.run_until_complete(tg_client.get_messages(channel_username, ids=message_id))
228
+ if msg is None:
229
+ return f"**Ошибка (TG)**: сообщение не найдено — {link}", 0
230
+
231
+ channel_entity = tg_loop.run_until_complete(tg_client.get_entity(channel_username))
232
+ channel_title = channel_entity.title
233
+ views = msg.views or 0
234
+ result = f"- [{channel_title}]({link}) — {human_format_views(views)}"
235
+ return result, views
236
+ except Exception as e:
237
+ return f"**Ошибка (TG)** при обработке [{link}]({link}): {e}", 0
238
+
239
+ # Функция для обработки ссылок VK
240
+ def process_vk_link(link: str) -> tuple[str, int]:
241
+ link = link.strip()
242
+ if not link:
243
+ return "", 0
244
+
245
+ match = re.search(r'vk\.com/wall(-?\d+)_(\d+)', link)
246
+ if not match:
247
+ return f"**Ошибка (VK)**: не распознал ссылку — {link}", 0
248
+
249
+ owner_id = int(match.group(1))
250
+ post_id = match.group(2)
251
+ posts_param = f"{owner_id}_{post_id}"
252
+ api_url = "https://api.vk.com/method/wall.getById"
253
+
254
+ def do_request(token):
255
+ params = {
256
+ "posts": posts_param,
257
+ "access_token": token,
258
+ "v": VK_API_VERSION,
259
+ "extended": 1
260
+ }
261
+ return requests.get(api_url, params=params)
262
+
263
+ # Первый запрос с текущим access_token
264
+ token = st.session_state["VK_ACCESS_TOKEN"]
265
+ resp = do_request(token)
266
+
267
+ if resp.status_code != 200:
268
+ return f"**Ошибка (VK)**: HTTP {resp.status_code} — {link}", 0
269
+
270
+ data = resp.json()
271
+ if "error" in data:
272
+ error_msg = data["error"].get("error_msg", "")
273
+ # Если токен истёк, пробуем обновить
274
+ if "access_token has expired" in error_msg.lower():
275
+ if refresh_vk_token():
276
+ # Повторяем запрос с новым токеном
277
+ new_token = st.session_state["VK_ACCESS_TOKEN"]
278
+ resp = do_request(new_token)
279
+ data = resp.json()
280
+ if "error" in data:
281
+ # Если опять ошибка - выходим
282
+ err_msg2 = data["error"].get("error_msg", "")
283
+ return f"**Ошибка (VK)**: {err_msg2} — {link}", 0
284
+ else:
285
+ return f"**Ошибка (VK)**: не удалось обновить токен — {link}", 0
286
+ else:
287
+ return f"**Ошибка (VK)**: {error_msg} — {link}", 0
288
+
289
+ response_data = data.get("response", {})
290
+ items = response_data.get("items", [])
291
+ if not items:
292
+ return f"**Ошибка (VK)**: пост не найден — {link}", 0
293
+
294
+ post = items[0]
295
+ views = post.get("views", {}).get("count", 0)
296
+ post_owner_id = post.get("owner_id", owner_id)
297
+
298
+ title = None
299
+ if post_owner_id < 0:
300
+ group_id = -post_owner_id
301
+ for g in response_data.get("groups", []):
302
+ if g["id"] == group_id:
303
+ title = g["name"]
304
+ break
305
+ else:
306
+ user_id = post_owner_id
307
+ for p in response_data.get("profiles", []):
308
+ if p["id"] == user_id:
309
+ first_name = p.get("first_name", "")
310
+ last_name = p.get("last_name", "")
311
+ title = (first_name + " " + last_name).strip()
312
+ break
313
+ if not title:
314
+ title = "VK пост"
315
+
316
+ result = f"- [{title}]({link}) — {human_format_views(views)}"
317
+ return result, views
318
+
319
+ # Функция для определения платформы
320
+ def detect_platform(link: str) -> str:
321
+ link = link.strip().lower()
322
+ if "t.me/" in link or "telegram.me/" in link:
323
+ return "telegram"
324
+ elif "vk.com/wall" in link:
325
+ return "vk"
326
+ return ""
327
+
328
+ # Интерфейс Streamlit
329
+ def main():
330
+ st.title("PR Tool made by @Dina")
331
+
332
+ # Если токены не получены, показываем авторизацию
333
+ if not st.session_state.get("VK_ACCESS_TOKEN"):
334
+ vk_auth()
335
+ return
336
+
337
+ # Если токены есть, показываем парсер
338
+ st.write("Вставьте ссылки (каждая на новой строке) — как Telegram, так и ВКонтакте. Пример:")
339
+ st.code("""
340
+ https://t.me/meduzalive/122479
341
+ https://vk.com/wall-12256840_3220312
342
+ https://telegram.me/za_traktor/27423
343
+ https://vk.com/wall-237426_1643234
344
+ """)
345
+
346
+ links_input = st.text_area("Ссылки:", height=200)
347
+
348
+ if st.button("Обработать"):
349
+ lines = links_input.strip().split('\n')
350
+ tg_results = []
351
+ tg_total_views = 0
352
+ vk_results = []
353
+ vk_total_views = 0
354
+
355
+ for link in lines:
356
+ platform = detect_platform(link)
357
+ if platform == "telegram":
358
+ res_line, views = process_telegram_link(link)
359
+ tg_results.append((res_line, views))
360
+ tg_total_views += views
361
+ elif platform == "vk":
362
+ res_line, views = process_vk_link(link)
363
+ vk_results.append((res_line, views))
364
+ vk_total_views += views
365
+ else:
366
+ st.markdown(f"**Ошибка**: не распознал платформу — {link}", unsafe_allow_html=True)
367
+
368
+ tg_results.sort(key=lambda x: x[1], reverse=True)
369
+ vk_results.sort(key=lambda x: x[1], reverse=True)
370
+
371
+ md_lines = []
372
+
373
+ # --- Telegram ---
374
+ md_lines.append("## Telegram")
375
+ md_lines.append(f"**Суммарный охват постов — {human_format_views(tg_total_views)} просмотров**")
376
+ if tg_results:
377
+ for r_line, _ in tg_results:
378
+ md_lines.append(r_line)
379
+ else:
380
+ md_lines.append("_Нет ссылок на Telegram_")
381
+ md_lines.append("")
382
+
383
+ # --- ВКонтакте ---
384
+ md_lines.append("## ВКонтакте")
385
+ md_lines.append(f"**Суммарный охват постов — {human_format_views(vk_total_views)} просмотров**")
386
+ if vk_results:
387
+ for r_line, _ in vk_results:
388
+ md_lines.append(r_line)
389
+ else:
390
+ md_lines.append("_Нет ссылок на ВКонтакте_")
391
+ md_lines.append("")
392
+
393
+ st.markdown("### Результат:")
394
+ for line in md_lines:
395
+ st.markdown(line, unsafe_allow_html=True)
396
+
397
+ markdown_result = "\n".join(md_lines)
398
+
399
+ # Кнопка копирования
400
+ copy_button_html = f"""
401
+ <html>
402
+ <head>
403
+ <script>
404
+ function copyToClipboard() {{
405
+ const btn = document.getElementById("copy-btn");
406
+ const text = `{markdown_result}`;
407
+ navigator.clipboard.writeText(text).then(function() {{
408
+ btn.innerText = "Скопировано успешно";
409
+ }}, function(err) {{
410
+ btn.innerText = "Ошибка при копировании";
411
+ }});
412
+ }}
413
+ </script>
414
+ </head>
415
+ <body>
416
+ <button id="copy-btn" onclick="copyToClipboard()" style="padding: 8px 16px; font-size: 16px;">Скопировать код</button>
417
+ </body>
418
+ </html>
419
+ """
420
+ components.html(copy_button_html, height=80)