Dink0 commited on
Commit
c8c60e7
·
verified ·
1 Parent(s): e91124e

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +316 -0
app.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import asyncio
3
+ import nest_asyncio
4
+ nest_asyncio.apply() # Позволяет переиспользовать один event loop
5
+
6
+ import re
7
+ import os
8
+ import requests
9
+ from telethon import TelegramClient
10
+ from telethon.sessions import StringSession
11
+ import streamlit.components.v1 as components
12
+
13
+ # ----------- Настройки Telegram -----------
14
+ TELEGRAM_API_ID = 23646321
15
+ TELEGRAM_API_HASH = "102edf31d404cb72d17ee4831d4e8c7b"
16
+ # Строка сессии (сгенерируйте заранее, если нужно)
17
+ TELEGRAM_STRING_SESSION = "1ApWapzMBu8a9o9Wu1GexPdkO7Jsr6po1xym67EcfAxkEbVx1oxw6wW7LINb9Xf0VYcyEAP-b-D0RQ_4x0RDAW8Di_0P2r4bj7xNeV9xO3wBMcaeq7FJux3UMq5buq89gqG8JarZG04tIyWoVnH__dq1_6G2Zkr0GF4WJCS2UN7mmwBacIIUEjUryEA97tXYC8jD14J-1MDgFsNNZTJ6uurD5RzH2DIhphChebxOkDBdXmpLGlanEzYzVOlx4p6Mo2zYorRG968o64CxKEh-WiJaPSNrS3IEfIVWFVd_tHKWzQSPD1zHHhcw0v2_ZjCKbZnTMcT3uMYuyLwA55_dckzcw0wZgAZQ="
18
+
19
+ # ----------- Настройки ВКонтакте -----------
20
+ # Вставляем ваши токены
21
+ VK_ACCESS_TOKEN = "vk2.a.ankDHXsA1GZodkWhbdptbHT3c3avdPOZySVHuXC5Akt0W1ACTG8X1D0P1gZ7X3vGH9dGcFhIUqqYwcmmXWiXEjHn5F9k4SaDJ29l4TSdzLVprYg8_XB-VmzgaMaA15eTakPELNVuAqh0_Z10MkPX-rol0SfvJiHX1oyjUpZ8jGERVdxNHUYQ2zF_YCRCuL0uOwp6jIjWY3kbBOUNr9TAGGCfWwjDI9j1LqU6Z_CAQWljTLesDa7d5G2CqLDpR8WV"
22
+ VK_REFRESH_TOKEN = "vk2.a.J9NgIEDDjePSb5ucS6wQGJzExbipqWs_wanF5cbaYuSVT0ztWId67eUvqG5y0nPQZ4YZmGiI3QqrLFYTKmM_5qNUaML3ulwFwOg_sJc8QopBije3CIHJDKGBGK1wQA_UCHVZL5Non75ZMH8qmx6EBIu1sGHnnwn5tl58QNtiMw1pUkebr3sZdjXpnDRPoC_b-BJ3_0lUfFmQPaeS3mo3sspi38Gn3Ev6PPpRY8GBDBLcjC1Jmm-Tj5ALll66AdcK"
23
+
24
+ # Остальные настройки ВКонтакте
25
+ VK_CLIENT_ID = os.environ.get("VK_CLIENT_ID", "")
26
+ VK_CLIENT_SECRET = os.environ.get("VK_CLIENT_SECRET", "")
27
+ VK_API_VERSION = "5.131"
28
+
29
+ if not VK_ACCESS_TOKEN:
30
+ st.warning("Не найден VK_ACCESS_TOKEN. Добавьте его в Secrets или переменные окружения!")
31
+ if not VK_REFRESH_TOKEN:
32
+ st.warning("Не найден VK_REFRESH_TOKEN. Без него не сможем обновлять токен!")
33
+ if not VK_CLIENT_ID or not VK_CLIENT_SECRET:
34
+ st.warning("Не найдены VK_CLIENT_ID / VK_CLIENT_SECRET. Без них не сможем обновлять токен!")
35
+
36
+ # --- Инициализация Telethon-клиента (Telegram) ---
37
+ def init_telethon():
38
+ loop = asyncio.new_event_loop()
39
+ asyncio.set_event_loop(loop)
40
+ client = TelegramClient(StringSession(TELEGRAM_STRING_SESSION), TELEGRAM_API_ID, TELEGRAM_API_HASH)
41
+ loop.run_until_complete(client.connect())
42
+ if not loop.run_until_complete(client.is_user_authorized()):
43
+ st.error("Telegram-сессия не авторизована. Проверьте StringSession или выполните login_telethon.py.")
44
+ st.stop()
45
+ return client, loop
46
+
47
+ if "telethon_client" not in st.session_state:
48
+ st.session_state["telethon_client"], st.session_state["telethon_loop"] = init_telethon()
49
+
50
+ tg_client = st.session_state["telethon_client"]
51
+ tg_loop = st.session_state["telethon_loop"]
52
+
53
+ # Сохраняем в session_state, чтобы при обновлении токена переопределять
54
+ if "VK_ACCESS_TOKEN" not in st.session_state:
55
+ st.session_state["VK_ACCESS_TOKEN"] = VK_ACCESS_TOKEN
56
+ if "VK_REFRESH_TOKEN" not in st.session_state:
57
+ st.session_state["VK_REFRESH_TOKEN"] = VK_REFRESH_TOKEN
58
+
59
+ def human_format_views(num: int) -> str:
60
+ if num < 1000:
61
+ return f"{num / 1000:.1f}K"
62
+ elif num < 10_000:
63
+ return f"{num / 1000:.1f}K"
64
+ elif num < 1_000_000:
65
+ return f"{int(num / 1000)}K"
66
+ else:
67
+ return f"{num / 1_000_000:.1f}M"
68
+
69
+ # ---------- Функция обновления токена через refresh_token ----------
70
+ def refresh_vk_token() -> bool:
71
+ """
72
+ Отправляем запрос на обновление токена через refresh_token.
73
+ По новой схеме VK ID:
74
+ grant_type=refresh_token, endpoint может быть https://id.vk.com/oauth2/auth (или другой, согласно документации).
75
+ """
76
+ # Берём текущие значения из session_state
77
+ current_refresh = st.session_state["VK_REFRESH_TOKEN"]
78
+ if not current_refresh or not VK_CLIENT_ID or not VK_CLIENT_SECRET:
79
+ st.error("Невозможно обновить токен: нет refresh_token, client_id или client_secret.")
80
+ return False
81
+
82
+ url = "https://id.vk.com/oauth2/auth" # либо https://id.vk.com/vkid/oauth2/token - зависит от вашей схемы
83
+ data = {
84
+ "grant_type": "refresh_token",
85
+ "client_id": VK_CLIENT_ID,
86
+ "client_secret": VK_CLIENT_SECRET,
87
+ "refresh_token": current_refresh
88
+ # Возможно, нужно device_id, если схема требует
89
+ }
90
+
91
+ resp = requests.post(url, data=data)
92
+ if resp.status_code != 200:
93
+ st.error(f"Ошибка обновления токена (HTTP {resp.status_code}): {resp.text}")
94
+ return False
95
+
96
+ try:
97
+ result = resp.json()
98
+ except Exception as e:
99
+ st.error(f"Не удалось распарсить ответ при обновлении токена: {e}")
100
+ return False
101
+
102
+ if "access_token" not in result:
103
+ err_desc = result.get("error_description", result)
104
+ st.error(f"Не удалось получить новый access_token: {err_desc}")
105
+ return False
106
+
107
+ # Сохраняем новые значения
108
+ new_access = result["access_token"]
109
+ st.session_state["VK_ACCESS_TOKEN"] = new_access
110
+ if "refresh_token" in result:
111
+ st.session_state["VK_REFRESH_TOKEN"] = result["refresh_token"]
112
+
113
+ st.success("VK-токен обновлён успешно!")
114
+ return True
115
+
116
+ # ------------- Парсер Telegram -------------
117
+ def process_telegram_link(link: str) -> tuple[str, int]:
118
+ link = link.strip()
119
+ if not link:
120
+ return "", 0
121
+
122
+ match = re.match(r'https?://(?:t(?:elegram)?\.me)/([^/]+)/(\d+)', link, re.IGNORECASE)
123
+ if not match:
124
+ return f"**Ошибка (TG)**: не распознал ссылку — {link}", 0
125
+
126
+ channel_username = match.group(1)
127
+ message_id = int(match.group(2))
128
+
129
+ try:
130
+ msg = tg_loop.run_until_complete(tg_client.get_messages(channel_username, ids=message_id))
131
+ if msg is None:
132
+ return f"**Ошибка (TG)**: сообщение не найдено — {link}", 0
133
+
134
+ channel_entity = tg_loop.run_until_complete(tg_client.get_entity(channel_username))
135
+ channel_title = channel_entity.title
136
+ views = msg.views or 0
137
+ result = f"- [{channel_title}]({link}) — {human_format_views(views)}"
138
+ return result, views
139
+ except Exception as e:
140
+ return f"**Ошибка (TG)** при обработке [{link}]({link}): {e}", 0
141
+
142
+ # ------------- Парсер VK (с обновлением токена при ошибке) -------------
143
+ def process_vk_link(link: str) -> tuple[str, int]:
144
+ link = link.strip()
145
+ if not link:
146
+ return "", 0
147
+
148
+ match = re.search(r'vk\.com/wall(-?\d+)_(\d+)', link)
149
+ if not match:
150
+ return f"**Ошибка (VK)**: не распознал ссылку — {link}", 0
151
+
152
+ owner_id = int(match.group(1))
153
+ post_id = match.group(2)
154
+ posts_param = f"{owner_id}_{post_id}"
155
+ api_url = "https://api.vk.com/method/wall.getById"
156
+
157
+ def do_request(token):
158
+ params = {
159
+ "posts": posts_param,
160
+ "access_token": token,
161
+ "v": VK_API_VERSION,
162
+ "extended": 1
163
+ }
164
+ return requests.get(api_url, params=params)
165
+
166
+ # Первый запрос с текущим access_token
167
+ token = st.session_state["VK_ACCESS_TOKEN"]
168
+ resp = do_request(token)
169
+
170
+ if resp.status_code != 200:
171
+ return f"**Ошибка (VK)**: HTTP {resp.status_code} — {link}", 0
172
+
173
+ data = resp.json()
174
+ if "error" in data:
175
+ error_msg = data["error"].get("error_msg", "")
176
+ # Если токен истёк, пробуем обновить
177
+ if "access_token has expired" in error_msg.lower():
178
+ if refresh_vk_token():
179
+ # Повторяем запрос с новым токеном
180
+ new_token = st.session_state["VK_ACCESS_TOKEN"]
181
+ resp = do_request(new_token)
182
+ data = resp.json()
183
+ if "error" in data:
184
+ # Если опять ошибка - выходим
185
+ err_msg2 = data["error"].get("error_msg", "")
186
+ return f"**Ошибка (VK)**: {err_msg2} — {link}", 0
187
+ else:
188
+ return f"**Ошибка (VK)**: не удалось обновить токен — {link}", 0
189
+ else:
190
+ return f"**Ошибка (VK)**: {error_msg} — {link}", 0
191
+
192
+ response_data = data.get("response", {})
193
+ items = response_data.get("items", [])
194
+ if not items:
195
+ return f"**Ошибка (VK)**: пост не найден — {link}", 0
196
+
197
+ post = items[0]
198
+ views = post.get("views", {}).get("count", 0)
199
+ post_owner_id = post.get("owner_id", owner_id)
200
+
201
+ title = None
202
+ if post_owner_id < 0:
203
+ group_id = -post_owner_id
204
+ for g in response_data.get("groups", []):
205
+ if g["id"] == group_id:
206
+ title = g["name"]
207
+ break
208
+ else:
209
+ user_id = post_owner_id
210
+ for p in response_data.get("profiles", []):
211
+ if p["id"] == user_id:
212
+ first_name = p.get("first_name", "")
213
+ last_name = p.get("last_name", "")
214
+ title = (first_name + " " + last_name).strip()
215
+ break
216
+ if not title:
217
+ title = "VK пост"
218
+
219
+ result = f"- [{title}]({link}) — {human_format_views(views)}"
220
+ return result, views
221
+
222
+ # ------------- Определение платформы -------------
223
+ def detect_platform(link: str) -> str:
224
+ link = link.strip().lower()
225
+ if "t.me/" in link or "telegram.me/" in link:
226
+ return "telegram"
227
+ elif "vk.com/wall" in link:
228
+ return "vk"
229
+ return ""
230
+
231
+ # ---------- Интерфейс Streamlit ----------
232
+ st.title("PR Tool made by @Dina")
233
+
234
+ st.write("Вставьте ссылки (каждая на новой строке) — как Telegram, так и ВКонтакте. Пример:")
235
+ st.code("""
236
+ https://t.me/meduzalive/122479
237
+ https://vk.com/wall-12256840_3220312
238
+ https://telegram.me/za_traktor/27423
239
+ https://vk.com/wall-237426_1643234
240
+ """)
241
+
242
+ links_input = st.text_area("Ссылки:", height=200)
243
+
244
+ if st.button("Обработать"):
245
+ lines = links_input.strip().split('\n')
246
+ tg_results = []
247
+ tg_total_views = 0
248
+ vk_results = []
249
+ vk_total_views = 0
250
+
251
+ for link in lines:
252
+ platform = detect_platform(link)
253
+ if platform == "telegram":
254
+ res_line, views = process_telegram_link(link)
255
+ tg_results.append((res_line, views))
256
+ tg_total_views += views
257
+ elif platform == "vk":
258
+ res_line, views = process_vk_link(link)
259
+ vk_results.append((res_line, views))
260
+ vk_total_views += views
261
+ else:
262
+ st.markdown(f"**Ошибка**: не распознал платформу — {link}", unsafe_allow_html=True)
263
+
264
+ tg_results.sort(key=lambda x: x[1], reverse=True)
265
+ vk_results.sort(key=lambda x: x[1], reverse=True)
266
+
267
+ md_lines = []
268
+
269
+ # --- Telegram ---
270
+ md_lines.append("## Telegram")
271
+ md_lines.append(f"**Суммарный охват постов — {human_format_views(tg_total_views)} просмотров**")
272
+ if tg_results:
273
+ for r_line, _ in tg_results:
274
+ md_lines.append(r_line)
275
+ else:
276
+ md_lines.append("_Нет ссылок на Telegram_")
277
+ md_lines.append("")
278
+
279
+ # --- ВКонтакте ---
280
+ md_lines.append("## ВКонтакте")
281
+ md_lines.append(f"**Суммарный охват постов — {human_format_views(vk_total_views)} просмотров**")
282
+ if vk_results:
283
+ for r_line, _ in vk_results:
284
+ md_lines.append(r_line)
285
+ else:
286
+ md_lines.append("_Нет ссылок на ВКонтакте_")
287
+ md_lines.append("")
288
+
289
+ st.markdown("### Результат:")
290
+ for line in md_lines:
291
+ st.markdown(line, unsafe_allow_html=True)
292
+
293
+ markdown_result = "\n".join(md_lines)
294
+
295
+ # Кнопка копирования
296
+ copy_button_html = f"""
297
+ <html>
298
+ <head>
299
+ <script>
300
+ function copyToClipboard() {{
301
+ const btn = document.getElementById("copy-btn");
302
+ const text = `{markdown_result}`;
303
+ navigator.clipboard.writeText(text).then(function() {{
304
+ btn.innerText = "Скопировано успешно";
305
+ }}, function(err) {{
306
+ btn.innerText = "Ошибка при копировании";
307
+ }});
308
+ }}
309
+ </script>
310
+ </head>
311
+ <body>
312
+ <button id="copy-btn" onclick="copyToClipboard()" style="padding: 8px 16px; font-size: 16px;">Скопировать код</button>
313
+ </body>
314
+ </html>
315
+ """
316
+ components.html(copy_button_html, height=80)