Aleksmorshen commited on
Commit
047a9f2
·
verified ·
1 Parent(s): 6e30c9b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +149 -501
app.py CHANGED
@@ -1,40 +1,38 @@
1
  #!/usr/bin/env python3
 
2
 
3
  import os
4
- from flask import Flask, request, Response, render_template_string, jsonify
5
  import hmac
6
  import hashlib
7
  import json
8
- from urllib.parse import unquote, parse_qs
9
  import time
10
  from datetime import datetime
11
  import logging
12
  import threading
13
- import requests
14
  from huggingface_hub import HfApi, hf_hub_download
15
  from huggingface_hub.utils import RepositoryNotFoundError
16
- import decimal
17
 
18
- BOT_TOKEN = os.getenv("BOT_TOKEN")
 
19
  HOST = '0.0.0.0'
20
  PORT = 7860
21
- DATA_FILE = 'data.json'
22
 
 
23
  REPO_ID = "flpolprojects/teledata"
24
- HF_DATA_FILE_PATH = "data.json"
25
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
26
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
27
-
28
- TON_API_KEY = os.getenv("TON_API_KEY", "AE7WM7YGSNNKW5YAAAABMLCTU2KXDSRSNZM3Y4GF27OXBPZLAJPKXB6237ZFNQVLSX6F5NA")
29
- TON_API_URL = f"https://go.getblock.io/{TON_API_KEY}"
30
- NANOTON_TO_TON = decimal.Decimal('1000000000')
31
 
32
  app = Flask(__name__)
33
  logging.basicConfig(level=logging.INFO)
34
- app.secret_key = os.urandom(24)
35
 
 
36
  _data_lock = threading.Lock()
37
- visitor_data_cache = {}
38
 
39
  def download_data_from_hf():
40
  global visitor_data_cache
@@ -50,10 +48,11 @@ def download_data_from_hf():
50
  token=HF_TOKEN_READ,
51
  local_dir=".",
52
  local_dir_use_symlinks=False,
53
- force_download=True,
54
- etag_timeout=10
55
  )
56
  logging.info("Data file successfully downloaded from Hugging Face.")
 
57
  with _data_lock:
58
  try:
59
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
@@ -65,14 +64,16 @@ def download_data_from_hf():
65
  return True
66
  except RepositoryNotFoundError:
67
  logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.")
 
68
  except Exception as e:
69
  logging.error(f"Error downloading data from Hugging Face: {e}")
 
70
  return False
71
 
72
  def load_visitor_data():
73
  global visitor_data_cache
74
  with _data_lock:
75
- if not visitor_data_cache:
76
  try:
77
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
78
  visitor_data_cache = json.load(f)
@@ -88,14 +89,17 @@ def load_visitor_data():
88
  visitor_data_cache = {}
89
  return visitor_data_cache
90
 
91
- def save_visitor_data(data_update):
92
  with _data_lock:
93
  try:
94
- visitor_data_cache.update(data_update)
 
 
95
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
96
  json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
97
  logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
98
- upload_data_to_hf_async()
 
99
  except Exception as e:
100
  logging.error(f"Error saving visitor data: {e}")
101
 
@@ -109,15 +113,11 @@ def upload_data_to_hf():
109
 
110
  try:
111
  api = HfApi()
112
- with _data_lock:
113
  file_content_exists = os.path.getsize(DATA_FILE) > 0
114
- if not file_content_exists and not visitor_data_cache:
115
- logging.info(f"{DATA_FILE} is empty and cache is empty. Skipping upload.")
116
- return
117
- elif not file_content_exists:
118
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
119
- json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
120
- logging.info("Empty local file found, wrote cache to file before upload.")
121
 
122
  logging.info(f"Attempting to upload {DATA_FILE} to {REPO_ID}/{HF_DATA_FILE_PATH}...")
123
  api.upload_file(
@@ -131,8 +131,10 @@ def upload_data_to_hf():
131
  logging.info("Visitor data successfully uploaded to Hugging Face.")
132
  except Exception as e:
133
  logging.error(f"Error uploading data to Hugging Face: {e}")
 
134
 
135
  def upload_data_to_hf_async():
 
136
  upload_thread = threading.Thread(target=upload_data_to_hf, daemon=True)
137
  upload_thread.start()
138
 
@@ -141,14 +143,12 @@ def periodic_backup():
141
  logging.info("Periodic backup disabled: HF_TOKEN_WRITE not set.")
142
  return
143
  while True:
144
- time.sleep(3600)
145
  logging.info("Initiating periodic backup...")
146
  upload_data_to_hf()
147
 
 
148
  def verify_telegram_data(init_data_str):
149
- if not BOT_TOKEN:
150
- logging.error("BOT_TOKEN not set. Telegram data verification skipped.")
151
- return None, False
152
  try:
153
  parsed_data = parse_qs(init_data_str)
154
  received_hash = parsed_data.pop('hash', [None])[0]
@@ -167,8 +167,8 @@ def verify_telegram_data(init_data_str):
167
  if calculated_hash == received_hash:
168
  auth_date = int(parsed_data.get('auth_date', [0])[0])
169
  current_time = int(time.time())
170
- if current_time - auth_date > 86400:
171
- logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}). Verification passed but data is old.")
172
  return parsed_data, True
173
  else:
174
  logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
@@ -177,43 +177,7 @@ def verify_telegram_data(init_data_str):
177
  logging.error(f"Error verifying Telegram data: {e}")
178
  return None, False
179
 
180
- def get_ton_balance(address):
181
- if not TON_API_KEY:
182
- logging.error("TON_API_KEY not set. Cannot fetch balance.")
183
- return None, "TON API Key not configured."
184
-
185
- headers = {'Content-Type': 'application/json'}
186
- payload = {
187
- "jsonrpc": "2.0",
188
- "method": "getAccountState",
189
- "params": [address],
190
- "id": 1
191
- }
192
- try:
193
- response = requests.post(TON_API_URL, headers=headers, json=payload, timeout=10)
194
- response.raise_for_status()
195
- result = response.json()
196
-
197
- if 'error' in result:
198
- logging.error(f"TON API Error for address {address}: {result['error']}")
199
- return None, f"TON API Error: {result['error'].get('message', 'Unknown error')}"
200
-
201
- account_state = result.get('result')
202
- if not account_state or 'balance' not in account_state:
203
- logging.warning(f"Account state or balance not found for address {address}. Result: {account_state}")
204
- return '0', "Аккаунт неактивен или баланс 0."
205
-
206
- balance_nanoton = decimal.Decimal(account_state['balance'])
207
- balance_ton = balance_nanoton / NANOTON_TO_TON
208
- return str(balance_ton), None
209
-
210
- except requests.exceptions.RequestException as e:
211
- logging.error(f"Error fetching TON balance for address {address}: {e}")
212
- return None, f"Network Error: Could not connect to TON API."
213
- except Exception as e:
214
- logging.exception(f"Unexpected error fetching TON balance for address {address}: {e}")
215
- return None, "Internal error fetching balance."
216
-
217
  TEMPLATE = """
218
  <!DOCTYPE html>
219
  <html lang="ru">
@@ -222,8 +186,6 @@ TEMPLATE = """
222
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
223
  <title>Morshen Group</title>
224
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
225
- <script src="https://unpkg.com/@tonconnect/sdk@latest/dist/tonconnect-sdk.min.js"></script>
226
- <script src="https://unpkg.com/@tonconnect/ui@latest/dist/tonconnect-ui.min.js"></script>
227
  <link rel="preconnect" href="https://fonts.googleapis.com">
228
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
229
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
@@ -238,7 +200,7 @@ TEMPLATE = """
238
  --tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#1e1e1e') }};
239
 
240
  --bg-gradient: linear-gradient(160deg, #1a232f 0%, #121212 100%);
241
- --card-bg: rgba(44, 44, 46, 0.8);
242
  --card-bg-solid: #2c2c2e;
243
  --text-color: var(--tg-theme-text-color);
244
  --text-secondary-color: var(--tg-theme-hint-color);
@@ -246,16 +208,16 @@ TEMPLATE = """
246
  --accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
247
  --tag-bg: rgba(255, 255, 255, 0.1);
248
  --border-radius-s: 8px;
249
- --border-radius-m: 14px;
250
- --border-radius-l: 18px;
251
  --padding-s: 10px;
252
- --padding-m: 18px;
253
- --padding-l: 28px;
254
  --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
255
  --shadow-color: rgba(0, 0, 0, 0.3);
256
  --shadow-light: 0 4px 15px var(--shadow-color);
257
  --shadow-medium: 0 6px 25px var(--shadow-color);
258
- --backdrop-blur: 10px;
259
  }
260
  * { box-sizing: border-box; margin: 0; padding: 0; }
261
  html {
@@ -267,11 +229,11 @@ TEMPLATE = """
267
  background: var(--bg-gradient);
268
  color: var(--text-color);
269
  padding: var(--padding-m);
270
- padding-bottom: 120px;
271
  overscroll-behavior-y: none;
272
  -webkit-font-smoothing: antialiased;
273
  -moz-osx-font-smoothing: grayscale;
274
- visibility: hidden;
275
  min-height: 100vh;
276
  }
277
  .container {
@@ -290,7 +252,7 @@ TEMPLATE = """
290
  }
291
  .logo { display: flex; align-items: center; gap: var(--padding-s); }
292
  .logo img {
293
- width: 50px;
294
  height: 50px;
295
  border-radius: 50%;
296
  background-color: var(--card-bg-solid);
@@ -298,30 +260,23 @@ TEMPLATE = """
298
  border: 2px solid rgba(255, 255, 255, 0.15);
299
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
300
  }
301
- .logo span { font-size: 1.6em; font-weight: 700; letter-spacing: -0.5px; }
302
  .btn {
303
  display: inline-flex; align-items: center; justify-content: center;
304
  padding: 12px var(--padding-m); border-radius: var(--border-radius-m);
305
  background: var(--accent-gradient); color: var(--tg-theme-button-text-color);
306
- text-decoration: none; font-weight: 600;
307
  border: none; cursor: pointer;
308
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
309
  gap: 8px; font-size: 1em;
310
  box-shadow: var(--shadow-light);
311
  letter-spacing: 0.3px;
312
- text-align: center;
313
  }
314
  .btn:hover {
315
  opacity: 0.9;
316
  box-shadow: var(--shadow-medium);
317
  transform: translateY(-2px);
318
  }
319
- .btn:disabled {
320
- opacity: 0.5;
321
- cursor: not-allowed;
322
- transform: none;
323
- box-shadow: var(--shadow-light);
324
- }
325
  .btn-secondary {
326
  background: var(--card-bg);
327
  color: var(--tg-theme-link-color);
@@ -331,14 +286,6 @@ TEMPLATE = """
331
  background: rgba(44, 44, 46, 0.95);
332
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), var(--shadow-medium);
333
  }
334
- .btn-green {
335
- background: var(--accent-gradient-green);
336
- color: var(--tg-theme-button-text-color);
337
- box-shadow: var(--shadow-light);
338
- }
339
- .btn-green:hover {
340
- background: linear-gradient(95deg, #28a745, #218838);
341
- }
342
  .tag {
343
  display: inline-block; background: var(--tag-bg); color: var(--text-secondary-color);
344
  padding: 6px 12px; border-radius: var(--border-radius-s); font-size: 0.85em;
@@ -351,14 +298,14 @@ TEMPLATE = """
351
  background-color: var(--card-bg);
352
  border-radius: var(--border-radius-l);
353
  padding: var(--padding-l);
354
- margin-bottom: 0;
355
  box-shadow: var(--shadow-medium);
356
  border: 1px solid rgba(255, 255, 255, 0.08);
357
  backdrop-filter: blur(var(--backdrop-blur));
358
  -webkit-backdrop-filter: blur(var(--backdrop-blur));
359
  }
360
  .section-title {
361
- font-size: 2em;
362
  font-weight: 700; margin-bottom: var(--padding-s); line-height: 1.25;
363
  letter-spacing: -0.6px;
364
  }
@@ -367,7 +314,7 @@ TEMPLATE = """
367
  margin-bottom: var(--padding-m);
368
  }
369
  .description {
370
- font-size: 1.05em; line-height: 1.6; color: var(--text-secondary-color);
371
  margin-bottom: var(--padding-m);
372
  }
373
  .stats-grid {
@@ -387,7 +334,7 @@ TEMPLATE = """
387
  background-color: var(--card-bg-solid);
388
  padding: var(--padding-m); border-radius: var(--border-radius-m);
389
  margin-bottom: var(--padding-s); display: flex; align-items: center;
390
- gap: var(--padding-m);
391
  font-size: 1.1em; font-weight: 500;
392
  border: 1px solid rgba(255, 255, 255, 0.08);
393
  transition: background-color 0.2s ease, transform 0.2s ease;
@@ -403,11 +350,11 @@ TEMPLATE = """
403
  }
404
  .save-card-button {
405
  position: fixed;
406
- bottom: 30px;
407
  left: 50%;
408
  transform: translateX(-50%);
409
- padding: 14px 28px;
410
- border-radius: 30px;
411
  background: var(--accent-gradient-green);
412
  color: var(--tg-theme-button-text-color);
413
  text-decoration: none;
@@ -416,57 +363,26 @@ TEMPLATE = """
416
  cursor: pointer;
417
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
418
  z-index: 1000;
419
- box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.5);
420
- font-size: 1.05em;
421
  display: flex;
422
  align-items: center;
423
- gap: 10px;
424
  backdrop-filter: blur(5px);
425
  -webkit-backdrop-filter: blur(5px);
426
  }
427
  .save-card-button:hover {
428
  opacity: 0.95;
429
- transform: translateX(-50%) scale(1.05);
430
  box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.3);
431
  }
432
-
433
  .save-card-button i { font-size: 1.2em; }
434
 
435
- .ton-wallet-section h2 {
436
- font-size: 1.6em;
437
- font-weight: 600;
438
- margin-bottom: var(--padding-s);
439
- }
440
- .ton-wallet-section .wallet-info {
441
- margin-top: var(--padding-m);
442
- padding: var(--padding-m);
443
- background-color: rgba(255, 255, 255, 0.05);
444
- border-radius: var(--border-radius-m);
445
- border: 1px solid rgba(255, 255, 255, 0.08);
446
- word-break: break-all;
447
- }
448
- .ton-wallet-section .wallet-info p { margin-bottom: 8px; font-size: 1em; }
449
- .ton-wallet-section .wallet-info strong { color: var(--tg-theme-link-color); }
450
- .ton-wallet-section .wallet-info .balance {
451
- font-size: 1.3em;
452
- font-weight: 600;
453
- margin-top: 12px;
454
- color: var(--accent-gradient-start, #34c759);
455
- }
456
- .ton-wallet-section .wallet-info .balance span { font-size: 0.8em; color: var(--text-secondary-color); }
457
- .ton-wallet-actions {
458
- display: flex;
459
- gap: var(--padding-s);
460
- margin-top: var(--padding-m);
461
- flex-wrap: wrap;
462
- justify-content: center;
463
- }
464
- .ton-wallet-actions .btn { flex-grow: 1; }
465
-
466
  .modal {
467
  display: none; position: fixed; z-index: 1001;
468
  left: 0; top: 0; width: 100%; height: 100%;
469
- overflow: auto; background-color: rgba(0,0,0,0.7);
470
  backdrop-filter: blur(8px);
471
  -webkit-backdrop-filter: blur(8px);
472
  animation: fadeIn 0.3s ease-out;
@@ -494,6 +410,7 @@ TEMPLATE = """
494
  .modal-text b { color: var(--tg-theme-link-color); font-weight: 600; }
495
  .modal-instruction { font-size: 1em; color: var(--text-secondary-color); margin-top: var(--padding-m); }
496
 
 
497
  .icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
498
  .icon-save::before { content: '💾'; }
499
  .icon-web::before { content: '🌐'; }
@@ -514,10 +431,8 @@ TEMPLATE = """
514
  .icon-link::before { content: '🔗'; }
515
  .icon-leader::before { content: '🏆'; }
516
  .icon-company::before { content: '🏢'; }
517
- .icon-wallet::before { content: '👛'; }
518
- .icon-ton::before { content: '💎'; }
519
- .icon-refresh::before { content: '🔄'; }
520
 
 
521
  @media (max-width: 480px) {
522
  .section-title { font-size: 1.8em; }
523
  .logo span { font-size: 1.4em; }
@@ -526,8 +441,6 @@ TEMPLATE = """
526
  .stats-grid { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: var(--padding-s); }
527
  .stat-value { font-size: 1.5em; }
528
  .modal-content { margin: 25% auto; width: 92%; }
529
- .ton-wallet-section h2 { font-size: 1.4em; }
530
- .ton-wallet-section .wallet-info .balance { font-size: 1.1em; }
531
  }
532
  </style>
533
  </head>
@@ -550,29 +463,11 @@ TEMPLATE = """
550
  Объединяем передовые технологические компании для создания инновационных
551
  решений мирового уровня. Мы строим будущее технологий сегодня.
552
  </p>
553
- <a href="#" class="btn contact-link btn-green" style="width: 100%; margin-top: var(--padding-s);">
554
  <i class="icon icon-contact"></i>Написать нам в Telegram
555
  </a>
556
  </section>
557
 
558
- <section class="ton-wallet-section section-card">
559
- <h2><i class="icon icon-ton"></i>Интеграция с TON</h2>
560
- <p class="description" id="wallet-connect-status">
561
- Подключите ваш TON кошелек, чтобы увидеть баланс.
562
- </p>
563
- <div id="ton-connect-button"></div>
564
-
565
- <div id="wallet-info" class="wallet-info" style="display: none;">
566
- <p><strong>Кошелек:</strong> <span id="wallet-address">-</span></p>
567
- <p id="wallet-balance-display"><i class="icon icon-ton"></i> <strong>Баланс:</strong> <span class="balance">- TON</span></p>
568
- <div class="ton-wallet-actions">
569
- <button id="fetch-balance-btn" class="btn btn-secondary" style="display: none;"><i class="icon icon-refresh"></i>Обновить баланс</button>
570
- <button id="disconnect-wallet-btn" class="btn btn-danger" style="display: none;">Отключить</button>
571
- </div>
572
- </div>
573
- </section>
574
-
575
-
576
  <section class="ecosystem-header">
577
  <h2 class="section-title"><i class="icon icon-company"></i>Экосистема инноваций</h2>
578
  <p class="description">
@@ -667,6 +562,7 @@ TEMPLATE = """
667
  <i class="icon icon-save"></i>Сохранить визитку
668
  </button>
669
 
 
670
  <div id="saveModal" class="modal">
671
  <div class="modal-content">
672
  <span class="modal-close" id="modal-close-btn">×</span>
@@ -680,8 +576,6 @@ TEMPLATE = """
680
 
681
  <script>
682
  const tg = window.Telegram.WebApp;
683
- let telegramUserId = null;
684
- let tonConnectUI = null;
685
 
686
  function applyTheme(themeParams) {
687
  const root = document.documentElement;
@@ -693,6 +587,7 @@ TEMPLATE = """
693
  root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
694
  root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#1e1e1e');
695
 
 
696
  try {
697
  const bgColor = themeParams.bg_color || '#121212';
698
  const r = parseInt(bgColor.slice(1, 3), 16);
@@ -700,15 +595,16 @@ TEMPLATE = """
700
  const b = parseInt(bgColor.slice(5, 7), 16);
701
  root.style.setProperty('--tg-theme-bg-color-rgb', `${r}, ${g}, ${b}`);
702
  } catch (e) {
703
- root.style.setProperty('--tg-theme-bg-color-rgb', `18, 18, 18`);
704
  }
705
  }
706
 
707
- async function setupTelegram() {
708
  if (!tg || !tg.initData) {
709
  console.error("Telegram WebApp script not loaded or initData is missing.");
710
  const greetingElement = document.getElementById('greeting');
711
  if(greetingElement) greetingElement.textContent = 'Не удалось связаться с Telegram.';
 
712
  document.body.style.visibility = 'visible';
713
  return;
714
  }
@@ -719,47 +615,54 @@ TEMPLATE = """
719
  applyTheme(tg.themeParams);
720
  tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
721
 
722
- try {
723
- const response = await fetch('/verify', {
724
- method: 'POST',
725
- headers: {
726
- 'Content-Type': 'application/json',
727
- 'Accept': 'application/json'
728
- },
729
- body: JSON.stringify({ initData: tg.initData }),
730
- });
731
-
732
- const data = await response.json();
733
- if (response.ok && data.status === 'ok' && data.verified) {
 
 
 
734
  console.log('Backend verification successful.');
735
- if (data.user && data.user.id) {
736
- telegramUserId = data.user.id;
737
- const name = data.user.first_name || data.user.username || 'Гость';
738
- document.getElementById('greeting').textContent = `Добро пожаловать, ${name}! 👋`;
739
-
740
- setupTonConnect(telegramUserId);
741
-
742
- } else {
743
- document.getElementById('greeting').textContent = 'Добро пожаловать!';
744
- console.warn('Telegram User data not available (initDataUnsafe.user is empty) after verification.');
745
- }
746
  } else {
747
  console.warn('Backend verification failed:', data.message);
748
- document.getElementById('greeting').textContent = 'Добро пожаловать! (Ошибка верификации)';
749
  }
750
- } catch (error) {
 
751
  console.error('Error sending initData for verification:', error);
752
- document.getElementById('greeting').textContent = 'Добро пожаловать! (Ошибка сервера)';
 
 
 
 
 
 
 
 
 
 
 
 
753
  }
754
 
 
755
  const contactButtons = document.querySelectorAll('.contact-link');
756
  contactButtons.forEach(button => {
757
  button.addEventListener('click', (e) => {
758
  e.preventDefault();
759
- tg.openTelegramLink('https://t.me/morshenkhan');
760
  });
761
  });
762
 
 
763
  const modal = document.getElementById("saveModal");
764
  const saveCardBtn = document.getElementById("save-card-btn");
765
  const closeBtn = document.getElementById("modal-close-btn");
@@ -789,179 +692,6 @@ TEMPLATE = """
789
  document.body.style.visibility = 'visible';
790
  }
791
 
792
- async function setupTonConnect(userId) {
793
- if (!userId) {
794
- console.error("Telegram User ID is required to setup TON Connect.");
795
- document.getElementById('wallet-connect-status').textContent = 'Не удалось инициализировать интеграцию с TON (нет User ID).';
796
- return;
797
- }
798
- document.getElementById('wallet-connect-status').textContent = 'Инициализация TON Connect...';
799
-
800
- try {
801
- tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
802
- manifestUrl: window.location.origin + '/tonconnect-manifest.json',
803
- connector: {
804
- timeout: 10000
805
- }
806
- });
807
-
808
- tonConnectUI.uiOptions = {
809
- uiPreferences: {
810
- theme: tg.themeParams.bg_color === '#121212' || tg.themeParams.bg_color === '#000000' ? 'dark' : 'light',
811
- }
812
- };
813
-
814
-
815
- tonConnectUI.onStatusChange(wallet => {
816
- updateWalletInfo(wallet, userId);
817
- }, error => {
818
- console.error("TON Connect onStatusChange error:", error);
819
- document.getElementById('wallet-connect-status').textContent = 'Ошибка TON Connect. Попробуйте позже.';
820
- hideWalletInfo();
821
- if (tonConnectUI) {
822
- tonConnectUI.renderButton(document.getElementById('ton-connect-button'), {
823
- onClick: () => { tonConnectUI.connectWallet(); }
824
- });
825
- }
826
- });
827
-
828
- tonConnectUI.renderButton(document.getElementById('ton-connect-button'), {
829
- onClick: () => { tonConnectUI.connectWallet(); }
830
- });
831
-
832
- updateWalletInfo(tonConnectUI.wallet, userId);
833
-
834
- } catch (e) {
835
- console.error("Failed to initialize TON Connect UI:", e);
836
- document.getElementById('wallet-connect-status').textContent = 'Ошибка при запуске TON Connect.';
837
- hideWalletInfo();
838
- }
839
- }
840
-
841
- async function updateWalletInfo(wallet, userId) {
842
- const walletInfoDiv = document.getElementById('wallet-info');
843
- const connectStatusText = document.getElementById('wallet-connect-status');
844
- const walletAddressSpan = document.getElementById('wallet-address');
845
- const balanceDisplay = document.getElementById('wallet-balance-display');
846
- const fetchBalanceBtn = document.getElementById('fetch-balance-btn');
847
- const disconnectBtn = document.getElementById('disconnect-wallet-btn');
848
- const tonConnectButtonContainer = document.getElementById('ton-connect-button');
849
-
850
- if (wallet) {
851
- console.log("Wallet connected:", wallet);
852
- if (tonConnectButtonContainer) tonConnectButtonContainer.style.display = 'none';
853
-
854
- connectStatusText.style.display = 'none';
855
- walletInfoDiv.style.display = 'block';
856
- walletAddressSpan.textContent = wallet.account.address;
857
- disconnectBtn.style.display = 'inline-flex';
858
- fetchBalanceBtn.style.display = 'inline-flex';
859
- balanceDisplay.innerHTML = '<i class="icon icon-ton"></i> <strong>Баланс:</strong> <span class="balance">Загрузка...</span>';
860
- fetchBalanceBtn.disabled = false;
861
-
862
- saveTonAddress(userId, wallet.account.address);
863
-
864
- fetchTonBalance(userId, wallet.account.address);
865
-
866
- } else {
867
- console.log("Wallet disconnected.");
868
- if (tonConnectButtonContainer) tonConnectButtonContainer.style.display = 'block';
869
- connectStatusText.style.display = 'block';
870
- connectStatusText.textContent = 'Подключите ваш TON кошелек, чтобы увидеть баланс.';
871
- hideWalletInfo();
872
- }
873
- }
874
-
875
- function hideWalletInfo() {
876
- const walletInfoDiv = document.getElementById('wallet-info');
877
- const walletAddressSpan = document.getElementById('wallet-address');
878
- const balanceDisplay = document.getElementById('wallet-balance-display');
879
- const fetchBalanceBtn = document.getElementById('fetch-balance-btn');
880
- const disconnectBtn = document.getElementById('disconnect-wallet-btn');
881
-
882
- walletInfoDiv.style.display = 'none';
883
- walletAddressSpan.textContent = '-';
884
- balanceDisplay.innerHTML = '<i class="icon icon-ton"></i> <strong>Баланс:</strong> <span class="balance">- TON</span>';
885
- fetchBalanceBtn.style.display = 'none';
886
- disconnectBtn.style.display = 'none';
887
- }
888
-
889
- async function saveTonAddress(userId, address) {
890
- if (!userId) {
891
- console.error("Cannot save TON address: Telegram User ID is missing.");
892
- return;
893
- }
894
- try {
895
- const response = await fetch('/save_ton_address', {
896
- method: 'POST',
897
- headers: { 'Content-Type': 'application/json' },
898
- body: JSON.stringify({ tg_user_id: userId, ton_address: address }),
899
- });
900
- const data = await response.json();
901
- if (response.ok && data.status === 'ok') {
902
- console.log('TON address saved successfully.');
903
- } else {
904
- console.error('Failed to save TON address:', data.message);
905
- }
906
- } catch (error) {
907
- console.error('Error saving TON address:', error);
908
- }
909
- }
910
-
911
-
912
- async function fetchTonBalance(userId, address) {
913
- const balanceDisplay = document.getElementById('wallet-balance-display').querySelector('.balance');
914
- const fetchBalanceBtn = document.getElementById('fetch-balance-btn');
915
- if (!userId || !address) {
916
- balanceDisplay.textContent = 'Ошибка (нет данных)';
917
- fetchBalanceBtn.disabled = true;
918
- return;
919
- }
920
-
921
- balanceDisplay.textContent = 'Загрузка...';
922
- fetchBalanceBtn.disabled = true;
923
-
924
- try {
925
- const response = await fetch('/get_ton_balance', {
926
- method: 'POST',
927
- headers: { 'Content-Type': 'application/json' },
928
- body: JSON.stringify({ tg_user_id: userId }),
929
- });
930
- const data = await response.json();
931
-
932
- if (response.ok && data.status === 'ok') {
933
- const balance = parseFloat(data.balance).toFixed(4);
934
- balanceDisplay.textContent = `${balance} TON`;
935
- balanceDisplay.style.color = 'var(--accent-gradient-start, #34c759)';
936
- } else {
937
- balanceDisplay.textContent = data.message || 'Ошибка загрузки';
938
- balanceDisplay.style.color = 'var(--admin-danger, #dc3545)';
939
- console.error('Failed to fetch balance:', data.message);
940
- }
941
- } catch (error) {
942
- balanceDisplay.textContent = 'Сетевая ошибка';
943
- balanceDisplay.style.color = 'var(--admin-danger, #dc3545)';
944
- console.error('Error fetching balance:', error);
945
- } finally {
946
- fetchBalanceBtn.disabled = false;
947
- }
948
- }
949
-
950
- document.getElementById('fetch-balance-btn').addEventListener('click', () => {
951
- if (telegramUserId && tonConnectUI && tonConnectUI.wallet && tonConnectUI.wallet.account) {
952
- fetchTonBalance(telegramUserId, tonConnectUI.wallet.account.address);
953
- } else {
954
- console.warn("Cannot fetch balance: Wallet not connected or user ID missing.");
955
- }
956
- });
957
-
958
- document.getElementById('disconnect-wallet-btn').addEventListener('click', () => {
959
- if (tonConnectUI) {
960
- tonConnectUI.disconnect();
961
- }
962
- });
963
-
964
-
965
  if (window.Telegram && window.Telegram.WebApp) {
966
  setupTelegram();
967
  } else {
@@ -974,7 +704,7 @@ TEMPLATE = """
974
  if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
975
  document.body.style.visibility = 'visible';
976
  }
977
- }, 3500);
978
  }
979
 
980
  </script>
@@ -1044,7 +774,7 @@ ADMIN_TEMPLATE = """
1044
  width: 80px; height: 80px;
1045
  border-radius: 50%; margin-bottom: 1rem;
1046
  object-fit: cover; border: 3px solid var(--admin-border);
1047
- background-color: #eee;
1048
  }
1049
  .user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
1050
  .user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
@@ -1073,6 +803,7 @@ ADMIN_TEMPLATE = """
1073
  }
1074
  .refresh-btn:hover { background-color: #0b5ed7; }
1075
 
 
1076
  .admin-controls {
1077
  background: var(--admin-card-bg);
1078
  padding: var(--padding);
@@ -1102,7 +833,7 @@ ADMIN_TEMPLATE = """
1102
  .admin-controls .loader {
1103
  border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid var(--admin-primary);
1104
  width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle;
1105
- display: none;
1106
  }
1107
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
1108
  </style>
@@ -1131,14 +862,13 @@ ADMIN_TEMPLATE = """
1131
  {% if user.username %}
1132
  <div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" style="color: inherit; text-decoration: none;">@{{ user.username }}</a></div>
1133
  {% else %}
1134
- <div class="username" style="height: 1.3em;"></div>
1135
  {% endif %}
1136
  <div class="details">
1137
  <div class="detail-item"><strong>ID:</strong> {{ user.id }}</div>
1138
  <div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
1139
  <div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
1140
  <div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
1141
- <div class="detail-item"><strong>TON:</strong> {{ user.ton_wallet_address or 'Не подключен' }}</div>
1142
  </div>
1143
  <div class="timestamp">Визит: {{ user.visited_at_str }}</div>
1144
  </div>
@@ -1164,7 +894,7 @@ ADMIN_TEMPLATE = """
1164
  statusMessage.textContent = data.message;
1165
  statusMessage.style.color = 'var(--admin-success)';
1166
  if (action === 'скачивание') {
1167
- setTimeout(() => location.reload(), 1500);
1168
  }
1169
  } else {
1170
  throw new Error(data.message || 'Произошла ошибка');
@@ -1190,17 +920,12 @@ ADMIN_TEMPLATE = """
1190
  </html>
1191
  """
1192
 
1193
- TON_MANIFEST_TEMPLATE = """
1194
- {
1195
- "url": "https://huggingface.co/spaces/Aleksmorshen/MorshenGroup",
1196
- "name": "Morshen Group TMA",
1197
- "iconUrl": "https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg"
1198
- }
1199
- """
1200
-
1201
  @app.route('/')
1202
  def index():
1203
- theme_params = {}
 
 
1204
  return render_template_string(TEMPLATE, theme=theme_params)
1205
 
1206
  @app.route('/verify', methods=['POST'])
@@ -1209,126 +934,59 @@ def verify_data():
1209
  req_data = request.get_json()
1210
  init_data_str = req_data.get('initData')
1211
  if not init_data_str:
1212
- logging.warning("Missing initData in /verify request.")
1213
  return jsonify({"status": "error", "message": "Missing initData"}), 400
1214
 
1215
  user_data_parsed, is_valid = verify_telegram_data(init_data_str)
1216
 
1217
  user_info_dict = {}
1218
- user_id = None
1219
  if user_data_parsed and 'user' in user_data_parsed:
1220
  try:
1221
  user_json_str = unquote(user_data_parsed['user'][0])
1222
  user_info_dict = json.loads(user_json_str)
1223
- user_id = user_info_dict.get('id')
1224
  except Exception as e:
1225
- logging.error(f"Could not parse user JSON in /verify: {e}")
1226
  user_info_dict = {}
1227
 
1228
- if is_valid and user_id:
1229
- str_user_id = str(user_id)
1230
- now = time.time()
1231
- current_data = load_visitor_data()
1232
-
1233
- existing_user_data = current_data.get(str_user_id, {})
1234
- ton_wallet_address = existing_user_data.get('ton_wallet_address')
1235
-
1236
- user_entry = {
1237
- str_user_id: {
1238
- 'id': user_id,
1239
- 'first_name': user_info_dict.get('first_name'),
1240
- 'last_name': user_info_dict.get('last_name'),
1241
- 'username': user_info_dict.get('username'),
1242
- 'photo_url': user_info_dict.get('photo_url'),
1243
- 'language_code': user_info_dict.get('language_code'),
1244
- 'is_premium': user_info_dict.get('is_premium', False),
1245
- 'phone_number': user_info_dict.get('phone_number'),
1246
- 'visited_at': now,
1247
- 'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S'),
1248
- 'ton_wallet_address': ton_wallet_address
1249
  }
1250
- }
1251
- save_visitor_data(user_entry)
1252
  return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
1253
  else:
1254
- logging.warning(f"Verification failed for user: {user_id}. InitData Valid: {is_valid}")
1255
- return jsonify({"status": "error", "verified": False, "message": "Invalid data or user ID missing"}), 403
1256
 
1257
  except Exception as e:
1258
  logging.exception("Error in /verify endpoint")
1259
  return jsonify({"status": "error", "message": "Internal server error"}), 500
1260
 
1261
- @app.route('/save_ton_address', methods=['POST'])
1262
- def save_ton_address():
1263
- try:
1264
- req_data = request.get_json()
1265
- tg_user_id = req_data.get('tg_user_id')
1266
- ton_address = req_data.get('ton_address')
1267
-
1268
- if not tg_user_id or not ton_address:
1269
- return jsonify({"status": "error", "message": "Missing tg_user_id or ton_address"}), 400
1270
-
1271
- str_user_id = str(tg_user_id)
1272
- current_data = load_visitor_data()
1273
-
1274
- if str_user_id not in current_data:
1275
- logging.warning(f"Attempted to save TON address for unknown user ID: {tg_user_id}")
1276
- current_data[str_user_id] = {'id': tg_user_id, 'visited_at': time.time(), 'visited_at_str': datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S')}
1277
-
1278
- current_data[str_user_id]['ton_wallet_address'] = ton_address
1279
-
1280
- save_visitor_data({str_user_id: current_data[str_user_id]})
1281
-
1282
- logging.info(f"TON address saved for user {tg_user_id}: {ton_address}")
1283
- return jsonify({"status": "ok", "message": "TON address saved successfully."}), 200
1284
-
1285
- except Exception as e:
1286
- logging.exception("Error in /save_ton_address endpoint")
1287
- return jsonify({"status": "error", "message": "Internal server error"}), 500
1288
-
1289
-
1290
- @app.route('/get_ton_balance', methods=['POST'])
1291
- def get_ton_balance_route():
1292
- try:
1293
- req_data = request.get_json()
1294
- tg_user_id = req_data.get('tg_user_id')
1295
-
1296
- if not tg_user_id:
1297
- return jsonify({"status": "error", "message": "Missing tg_user_id"}), 400
1298
-
1299
- str_user_id = str(tg_user_id)
1300
- current_data = load_visitor_data()
1301
-
1302
- user_data = current_data.get(str_user_id)
1303
-
1304
- if not user_data or 'ton_wallet_address' not in user_data or not user_data['ton_wallet_address']:
1305
- logging.warning(f"TON address not found for user {tg_user_id} when attempting to fetch balance.")
1306
- return jsonify({"status": "error", "message": "TON кошелек не привязан"}), 404
1307
-
1308
- ton_address = user_data['ton_wallet_address']
1309
-
1310
- balance, error_message = get_ton_balance(ton_address)
1311
-
1312
- if balance is not None:
1313
- logging.info(f"Fetched balance for user {tg_user_id} ({ton_address}): {balance} TON")
1314
- return jsonify({"status": "ok", "balance": balance}), 200
1315
- else:
1316
- logging.error(f"Failed to fetch balance for user {tg_user_id} ({ton_address}): {error_message}")
1317
- return jsonify({"status": "error", "message": error_message or "Не удалось получить баланс"}), 500
1318
-
1319
- except Exception as e:
1320
- logging.exception("Error in /get_ton_balance endpoint")
1321
- return jsonify({"status": "error", "message": "Internal server error"}), 500
1322
-
1323
-
1324
  @app.route('/admin')
1325
  def admin_panel():
1326
- current_data = load_visitor_data()
 
1327
  users_list = list(current_data.values())
1328
  return render_template_string(ADMIN_TEMPLATE, users=users_list)
1329
 
1330
  @app.route('/admin/download_data', methods=['POST'])
1331
  def admin_trigger_download():
 
1332
  success = download_data_from_hf()
1333
  if success:
1334
  return jsonify({"status": "ok", "message": "Скачивание данных с Hugging Face завершено. Страница будет обновлена."})
@@ -1337,27 +995,20 @@ def admin_trigger_download():
1337
 
1338
  @app.route('/admin/upload_data', methods=['POST'])
1339
  def admin_trigger_upload():
 
1340
  if not HF_TOKEN_WRITE:
1341
  return jsonify({"status": "error", "message": "HF_TOKEN_WRITE не настроен на сервере."}), 400
1342
- upload_data_to_hf_async()
1343
  return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
1344
 
1345
- @app.route('/tonconnect-manifest.json')
1346
- def tonconnect_manifest():
1347
- return Response(
1348
- render_template_string(TON_MANIFEST_TEMPLATE),
1349
- mimetype='application/json'
1350
- )
1351
 
 
1352
  if __name__ == '__main__':
1353
  print("---")
1354
  print("--- MORSHEN GROUP MINI APP SERVER ---")
1355
  print("---")
1356
  print(f"Flask server starting on http://{HOST}:{PORT}")
1357
- if BOT_TOKEN:
1358
- print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
1359
- else:
1360
- print("--- WARNING: BOT_TOKEN not set. Telegram data verification disabled. ---")
1361
  print(f"Visitor data file: {DATA_FILE}")
1362
  print(f"Hugging Face Repo: {REPO_ID}")
1363
  print(f"HF Data Path: {HF_DATA_FILE_PATH}")
@@ -1368,26 +1019,20 @@ if __name__ == '__main__':
1368
  print("---")
1369
  else:
1370
  print("--- Hugging Face tokens found.")
 
1371
  print("--- Attempting initial data download from Hugging Face...")
1372
  download_data_from_hf()
1373
 
1374
- if not TON_API_KEY:
1375
- print("--- WARNING: TON_API_KEY not set. TON balance fetching disabled. ---")
1376
- print("--- Set TON_API_KEY environment variable.")
1377
- else:
1378
- print("--- TON_API_KEY found.")
1379
-
1380
-
1381
  load_visitor_data()
1382
 
1383
  print("---")
1384
  print("--- SECURITY WARNING ---")
1385
  print("--- The /admin route and its sub-routes are NOT protected.")
1386
  print("--- Implement proper authentication before deploying.")
1387
- print("--- Hardcoded default TON_API_KEY is used if environment variable is not set. This is INSECURE.")
1388
- print("--- Always set TON_API_KEY via environment variables in production.")
1389
  print("---")
1390
 
 
1391
  if HF_TOKEN_WRITE:
1392
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1393
  backup_thread.start()
@@ -1396,4 +1041,7 @@ if __name__ == '__main__':
1396
  print("--- Periodic backup disabled (HF_TOKEN_WRITE missing).")
1397
 
1398
  print("--- Server Ready ---")
1399
- app.run(host=HOST, port=PORT, debug=False)
 
 
 
 
1
  #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
 
4
  import os
5
+ from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for
6
  import hmac
7
  import hashlib
8
  import json
9
+ from urllib.parse import unquote, parse_qs, quote
10
  import time
11
  from datetime import datetime
12
  import logging
13
  import threading
 
14
  from huggingface_hub import HfApi, hf_hub_download
15
  from huggingface_hub.utils import RepositoryNotFoundError
 
16
 
17
+ # --- Configuration ---
18
+ BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvbTVsqEJVG5SYK5hUlc_Ewo") # Use environment variable or default
19
  HOST = '0.0.0.0'
20
  PORT = 7860
21
+ DATA_FILE = 'data.json' # Local file for visitor data
22
 
23
+ # Hugging Face Settings
24
  REPO_ID = "flpolprojects/teledata"
25
+ HF_DATA_FILE_PATH = "data.json" # Path within the HF repo
26
+ HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # Token with write access
27
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Token with read access (can be same as write)
 
 
 
 
28
 
29
  app = Flask(__name__)
30
  logging.basicConfig(level=logging.INFO)
31
+ app.secret_key = os.urandom(24) # For potential future session use
32
 
33
+ # --- Hugging Face & Data Handling ---
34
  _data_lock = threading.Lock()
35
+ visitor_data_cache = {} # In-memory cache
36
 
37
  def download_data_from_hf():
38
  global visitor_data_cache
 
48
  token=HF_TOKEN_READ,
49
  local_dir=".",
50
  local_dir_use_symlinks=False,
51
+ force_download=True, # Ensure we get the latest version
52
+ etag_timeout=10 # Shorter timeout to avoid hanging
53
  )
54
  logging.info("Data file successfully downloaded from Hugging Face.")
55
+ # Force reload from downloaded file
56
  with _data_lock:
57
  try:
58
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
 
64
  return True
65
  except RepositoryNotFoundError:
66
  logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.")
67
+ # Don't clear local cache if repo not found, might have local data
68
  except Exception as e:
69
  logging.error(f"Error downloading data from Hugging Face: {e}")
70
+ # Don't clear local cache on generic download errors
71
  return False
72
 
73
  def load_visitor_data():
74
  global visitor_data_cache
75
  with _data_lock:
76
+ if not visitor_data_cache: # Only load from file if cache is empty
77
  try:
78
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
79
  visitor_data_cache = json.load(f)
 
89
  visitor_data_cache = {}
90
  return visitor_data_cache
91
 
92
+ def save_visitor_data(data):
93
  with _data_lock:
94
  try:
95
+ # Update cache first
96
+ visitor_data_cache.update(data)
97
+ # Save updated cache to file
98
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
99
  json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
100
  logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
101
+ # Trigger upload after successful local save
102
+ upload_data_to_hf_async() # Use async upload
103
  except Exception as e:
104
  logging.error(f"Error saving visitor data: {e}")
105
 
 
113
 
114
  try:
115
  api = HfApi()
116
+ with _data_lock: # Ensure file isn't being written while reading for upload
117
  file_content_exists = os.path.getsize(DATA_FILE) > 0
118
+ if not file_content_exists:
119
+ logging.warning(f"{DATA_FILE} is empty. Skipping upload.")
120
+ return
 
 
 
 
121
 
122
  logging.info(f"Attempting to upload {DATA_FILE} to {REPO_ID}/{HF_DATA_FILE_PATH}...")
123
  api.upload_file(
 
131
  logging.info("Visitor data successfully uploaded to Hugging Face.")
132
  except Exception as e:
133
  logging.error(f"Error uploading data to Hugging Face: {e}")
134
+ # Consider adding retry logic here if needed
135
 
136
  def upload_data_to_hf_async():
137
+ # Run upload in a separate thread to avoid blocking web requests
138
  upload_thread = threading.Thread(target=upload_data_to_hf, daemon=True)
139
  upload_thread.start()
140
 
 
143
  logging.info("Periodic backup disabled: HF_TOKEN_WRITE not set.")
144
  return
145
  while True:
146
+ time.sleep(3600) # Backup every hour
147
  logging.info("Initiating periodic backup...")
148
  upload_data_to_hf()
149
 
150
+ # --- Telegram Verification ---
151
  def verify_telegram_data(init_data_str):
 
 
 
152
  try:
153
  parsed_data = parse_qs(init_data_str)
154
  received_hash = parsed_data.pop('hash', [None])[0]
 
167
  if calculated_hash == received_hash:
168
  auth_date = int(parsed_data.get('auth_date', [0])[0])
169
  current_time = int(time.time())
170
+ if current_time - auth_date > 86400: # Allow data up to 24 hours old
171
+ logging.warning(f"Telegram InitData is older than 1 hour (Auth Date: {auth_date}, Current: {current_time}).")
172
  return parsed_data, True
173
  else:
174
  logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
 
177
  logging.error(f"Error verifying Telegram data: {e}")
178
  return None, False
179
 
180
+ # --- HTML Templates ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  TEMPLATE = """
182
  <!DOCTYPE html>
183
  <html lang="ru">
 
186
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
187
  <title>Morshen Group</title>
188
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
 
 
189
  <link rel="preconnect" href="https://fonts.googleapis.com">
190
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
191
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
 
200
  --tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#1e1e1e') }};
201
 
202
  --bg-gradient: linear-gradient(160deg, #1a232f 0%, #121212 100%);
203
+ --card-bg: rgba(44, 44, 46, 0.8); /* Semi-transparent card */
204
  --card-bg-solid: #2c2c2e;
205
  --text-color: var(--tg-theme-text-color);
206
  --text-secondary-color: var(--tg-theme-hint-color);
 
208
  --accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
209
  --tag-bg: rgba(255, 255, 255, 0.1);
210
  --border-radius-s: 8px;
211
+ --border-radius-m: 14px; /* Increased radius */
212
+ --border-radius-l: 18px; /* Increased radius */
213
  --padding-s: 10px;
214
+ --padding-m: 18px; /* Increased padding */
215
+ --padding-l: 28px; /* Increased padding */
216
  --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
217
  --shadow-color: rgba(0, 0, 0, 0.3);
218
  --shadow-light: 0 4px 15px var(--shadow-color);
219
  --shadow-medium: 0 6px 25px var(--shadow-color);
220
+ --backdrop-blur: 10px; /* Glassmorphism effect */
221
  }
222
  * { box-sizing: border-box; margin: 0; padding: 0; }
223
  html {
 
229
  background: var(--bg-gradient);
230
  color: var(--text-color);
231
  padding: var(--padding-m);
232
+ padding-bottom: 120px; /* More space for fixed button */
233
  overscroll-behavior-y: none;
234
  -webkit-font-smoothing: antialiased;
235
  -moz-osx-font-smoothing: grayscale;
236
+ visibility: hidden; /* Hide until ready */
237
  min-height: 100vh;
238
  }
239
  .container {
 
252
  }
253
  .logo { display: flex; align-items: center; gap: var(--padding-s); }
254
  .logo img {
255
+ width: 50px; /* Larger logo */
256
  height: 50px;
257
  border-radius: 50%;
258
  background-color: var(--card-bg-solid);
 
260
  border: 2px solid rgba(255, 255, 255, 0.15);
261
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
262
  }
263
+ .logo span { font-size: 1.6em; font-weight: 700; letter-spacing: -0.5px; } /* Bold, slightly larger */
264
  .btn {
265
  display: inline-flex; align-items: center; justify-content: center;
266
  padding: 12px var(--padding-m); border-radius: var(--border-radius-m);
267
  background: var(--accent-gradient); color: var(--tg-theme-button-text-color);
268
+ text-decoration: none; font-weight: 600; /* Bolder */
269
  border: none; cursor: pointer;
270
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
271
  gap: 8px; font-size: 1em;
272
  box-shadow: var(--shadow-light);
273
  letter-spacing: 0.3px;
 
274
  }
275
  .btn:hover {
276
  opacity: 0.9;
277
  box-shadow: var(--shadow-medium);
278
  transform: translateY(-2px);
279
  }
 
 
 
 
 
 
280
  .btn-secondary {
281
  background: var(--card-bg);
282
  color: var(--tg-theme-link-color);
 
286
  background: rgba(44, 44, 46, 0.95);
287
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), var(--shadow-medium);
288
  }
 
 
 
 
 
 
 
 
289
  .tag {
290
  display: inline-block; background: var(--tag-bg); color: var(--text-secondary-color);
291
  padding: 6px 12px; border-radius: var(--border-radius-s); font-size: 0.85em;
 
298
  background-color: var(--card-bg);
299
  border-radius: var(--border-radius-l);
300
  padding: var(--padding-l);
301
+ margin-bottom: 0; /* Removed bottom margin, gap handles spacing */
302
  box-shadow: var(--shadow-medium);
303
  border: 1px solid rgba(255, 255, 255, 0.08);
304
  backdrop-filter: blur(var(--backdrop-blur));
305
  -webkit-backdrop-filter: blur(var(--backdrop-blur));
306
  }
307
  .section-title {
308
+ font-size: 2em; /* Larger titles */
309
  font-weight: 700; margin-bottom: var(--padding-s); line-height: 1.25;
310
  letter-spacing: -0.6px;
311
  }
 
314
  margin-bottom: var(--padding-m);
315
  }
316
  .description {
317
+ font-size: 1.05em; line-height: 1.6; color: var(--text-secondary-color); /* Slightly larger desc */
318
  margin-bottom: var(--padding-m);
319
  }
320
  .stats-grid {
 
334
  background-color: var(--card-bg-solid);
335
  padding: var(--padding-m); border-radius: var(--border-radius-m);
336
  margin-bottom: var(--padding-s); display: flex; align-items: center;
337
+ gap: var(--padding-m); /* Increased gap */
338
  font-size: 1.1em; font-weight: 500;
339
  border: 1px solid rgba(255, 255, 255, 0.08);
340
  transition: background-color 0.2s ease, transform 0.2s ease;
 
350
  }
351
  .save-card-button {
352
  position: fixed;
353
+ bottom: 30px; /* Raised */
354
  left: 50%;
355
  transform: translateX(-50%);
356
+ padding: 14px 28px; /* Larger padding */
357
+ border-radius: 30px; /* More rounded */
358
  background: var(--accent-gradient-green);
359
  color: var(--tg-theme-button-text-color);
360
  text-decoration: none;
 
363
  cursor: pointer;
364
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
365
  z-index: 1000;
366
+ box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.5); /* Add outer glow */
367
+ font-size: 1.05em; /* Slightly larger text */
368
  display: flex;
369
  align-items: center;
370
+ gap: 10px; /* Increased gap */
371
  backdrop-filter: blur(5px);
372
  -webkit-backdrop-filter: blur(5px);
373
  }
374
  .save-card-button:hover {
375
  opacity: 0.95;
376
+ transform: translateX(-50%) scale(1.05); /* Slightly larger scale */
377
  box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.3);
378
  }
 
379
  .save-card-button i { font-size: 1.2em; }
380
 
381
+ /* Modal Styles */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  .modal {
383
  display: none; position: fixed; z-index: 1001;
384
  left: 0; top: 0; width: 100%; height: 100%;
385
+ overflow: auto; background-color: rgba(0,0,0,0.7); /* Darker backdrop */
386
  backdrop-filter: blur(8px);
387
  -webkit-backdrop-filter: blur(8px);
388
  animation: fadeIn 0.3s ease-out;
 
410
  .modal-text b { color: var(--tg-theme-link-color); font-weight: 600; }
411
  .modal-instruction { font-size: 1em; color: var(--text-secondary-color); margin-top: var(--padding-m); }
412
 
413
+ /* Icons */
414
  .icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
415
  .icon-save::before { content: '💾'; }
416
  .icon-web::before { content: '🌐'; }
 
431
  .icon-link::before { content: '🔗'; }
432
  .icon-leader::before { content: '🏆'; }
433
  .icon-company::before { content: '🏢'; }
 
 
 
434
 
435
+ /* Responsive adjustments */
436
  @media (max-width: 480px) {
437
  .section-title { font-size: 1.8em; }
438
  .logo span { font-size: 1.4em; }
 
441
  .stats-grid { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: var(--padding-s); }
442
  .stat-value { font-size: 1.5em; }
443
  .modal-content { margin: 25% auto; width: 92%; }
 
 
444
  }
445
  </style>
446
  </head>
 
463
  Объединяем передовые технологические компании для создания инновационных
464
  решений мирового уровня. Мы строим будущее технологий сегодня.
465
  </p>
466
+ <a href="#" class="btn contact-link" style="background: var(--accent-gradient-green); width: 100%; margin-top: var(--padding-s);">
467
  <i class="icon icon-contact"></i>Написать нам в Telegram
468
  </a>
469
  </section>
470
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  <section class="ecosystem-header">
472
  <h2 class="section-title"><i class="icon icon-company"></i>Экосистема инноваций</h2>
473
  <p class="description">
 
562
  <i class="icon icon-save"></i>Сохранить визитку
563
  </button>
564
 
565
+ <!-- The Modal -->
566
  <div id="saveModal" class="modal">
567
  <div class="modal-content">
568
  <span class="modal-close" id="modal-close-btn">×</span>
 
576
 
577
  <script>
578
  const tg = window.Telegram.WebApp;
 
 
579
 
580
  function applyTheme(themeParams) {
581
  const root = document.documentElement;
 
587
  root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
588
  root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#1e1e1e');
589
 
590
+ // Optional: Convert main bg color to RGB for glow effect alpha
591
  try {
592
  const bgColor = themeParams.bg_color || '#121212';
593
  const r = parseInt(bgColor.slice(1, 3), 16);
 
595
  const b = parseInt(bgColor.slice(5, 7), 16);
596
  root.style.setProperty('--tg-theme-bg-color-rgb', `${r}, ${g}, ${b}`);
597
  } catch (e) {
598
+ root.style.setProperty('--tg-theme-bg-color-rgb', `18, 18, 18`); // Fallback
599
  }
600
  }
601
 
602
+ function setupTelegram() {
603
  if (!tg || !tg.initData) {
604
  console.error("Telegram WebApp script not loaded or initData is missing.");
605
  const greetingElement = document.getElementById('greeting');
606
  if(greetingElement) greetingElement.textContent = 'Не удалось связаться с Telegram.';
607
+ // Apply default dark theme maybe? Or leave as is.
608
  document.body.style.visibility = 'visible';
609
  return;
610
  }
 
615
  applyTheme(tg.themeParams);
616
  tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
617
 
618
+ // Send initData for verification and user logging
619
+ fetch('/verify', {
620
+ method: 'POST',
621
+ headers: {
622
+ 'Content-Type': 'application/json',
623
+ 'Accept': 'application/json'
624
+ },
625
+ body: JSON.stringify({ initData: tg.initData }),
626
+ })
627
+ .then(response => {
628
+ if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
629
+ return response.json();
630
+ })
631
+ .then(data => {
632
+ if (data.status === 'ok' && data.verified) {
633
  console.log('Backend verification successful.');
 
 
 
 
 
 
 
 
 
 
 
634
  } else {
635
  console.warn('Backend verification failed:', data.message);
636
+ // Potentially show a non-blocking warning to user if needed
637
  }
638
+ })
639
+ .catch(error => {
640
  console.error('Error sending initData for verification:', error);
641
+ // Display a more user-friendly error?
642
+ });
643
+
644
+
645
+ // User Greeting (using unsafe data for immediate feedback)
646
+ const user = tg.initDataUnsafe?.user;
647
+ const greetingElement = document.getElementById('greeting');
648
+ if (user) {
649
+ const name = user.first_name || user.username || 'Гость';
650
+ greetingElement.textContent = `Добро пожаловать, ${name}! 👋`;
651
+ } else {
652
+ greetingElement.textContent = 'Добро пожаловать!';
653
+ console.warn('Telegram User data not available (initDataUnsafe.user is empty).');
654
  }
655
 
656
+ // Contact Links
657
  const contactButtons = document.querySelectorAll('.contact-link');
658
  contactButtons.forEach(button => {
659
  button.addEventListener('click', (e) => {
660
  e.preventDefault();
661
+ tg.openTelegramLink('https://t.me/morshenkhan'); // Use actual contact username
662
  });
663
  });
664
 
665
+ // Modal Setup
666
  const modal = document.getElementById("saveModal");
667
  const saveCardBtn = document.getElementById("save-card-btn");
668
  const closeBtn = document.getElementById("modal-close-btn");
 
692
  document.body.style.visibility = 'visible';
693
  }
694
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
  if (window.Telegram && window.Telegram.WebApp) {
696
  setupTelegram();
697
  } else {
 
704
  if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
705
  document.body.style.visibility = 'visible';
706
  }
707
+ }, 3500); // Slightly longer timeout
708
  }
709
 
710
  </script>
 
774
  width: 80px; height: 80px;
775
  border-radius: 50%; margin-bottom: 1rem;
776
  object-fit: cover; border: 3px solid var(--admin-border);
777
+ background-color: #eee; /* Placeholder bg */
778
  }
779
  .user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
780
  .user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
 
803
  }
804
  .refresh-btn:hover { background-color: #0b5ed7; }
805
 
806
+ /* Admin Controls */
807
  .admin-controls {
808
  background: var(--admin-card-bg);
809
  padding: var(--padding);
 
833
  .admin-controls .loader {
834
  border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid var(--admin-primary);
835
  width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle;
836
+ display: none; /* Hidden by default */
837
  }
838
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
839
  </style>
 
862
  {% if user.username %}
863
  <div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" style="color: inherit; text-decoration: none;">@{{ user.username }}</a></div>
864
  {% else %}
865
+ <div class="username" style="height: 1.3em;"></div> {# Placeholder for spacing #}
866
  {% endif %}
867
  <div class="details">
868
  <div class="detail-item"><strong>ID:</strong> {{ user.id }}</div>
869
  <div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
870
  <div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
871
  <div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
 
872
  </div>
873
  <div class="timestamp">Визит: {{ user.visited_at_str }}</div>
874
  </div>
 
894
  statusMessage.textContent = data.message;
895
  statusMessage.style.color = 'var(--admin-success)';
896
  if (action === 'скачивание') {
897
+ setTimeout(() => location.reload(), 1500); // Reload after download success
898
  }
899
  } else {
900
  throw new Error(data.message || 'Произошла ошибка');
 
920
  </html>
921
  """
922
 
923
+ # --- Flask Routes ---
 
 
 
 
 
 
 
924
  @app.route('/')
925
  def index():
926
+ # Pass theme parameters for initial render if available (e.g., from query params or session)
927
+ # For simplicity, we let the JS handle theme application after tg.ready()
928
+ theme_params = {} # Or load from request if needed
929
  return render_template_string(TEMPLATE, theme=theme_params)
930
 
931
  @app.route('/verify', methods=['POST'])
 
934
  req_data = request.get_json()
935
  init_data_str = req_data.get('initData')
936
  if not init_data_str:
 
937
  return jsonify({"status": "error", "message": "Missing initData"}), 400
938
 
939
  user_data_parsed, is_valid = verify_telegram_data(init_data_str)
940
 
941
  user_info_dict = {}
 
942
  if user_data_parsed and 'user' in user_data_parsed:
943
  try:
944
  user_json_str = unquote(user_data_parsed['user'][0])
945
  user_info_dict = json.loads(user_json_str)
 
946
  except Exception as e:
947
+ logging.error(f"Could not parse user JSON: {e}")
948
  user_info_dict = {}
949
 
950
+ if is_valid:
951
+ user_id = user_info_dict.get('id')
952
+ if user_id:
953
+ now = time.time()
954
+ # Create data entry for the specific user
955
+ user_entry = {
956
+ str(user_id): { # Use string keys for JSON compatibility
957
+ 'id': user_id,
958
+ 'first_name': user_info_dict.get('first_name'),
959
+ 'last_name': user_info_dict.get('last_name'),
960
+ 'username': user_info_dict.get('username'),
961
+ 'photo_url': user_info_dict.get('photo_url'),
962
+ 'language_code': user_info_dict.get('language_code'),
963
+ 'is_premium': user_info_dict.get('is_premium', False),
964
+ 'phone_number': user_info_dict.get('phone_number'), # Note: Only available if requested via button
965
+ 'visited_at': now,
966
+ 'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
967
+ }
 
 
 
968
  }
969
+ # Save/update this specific user's data
970
+ save_visitor_data(user_entry)
971
  return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
972
  else:
973
+ logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
974
+ return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
975
 
976
  except Exception as e:
977
  logging.exception("Error in /verify endpoint")
978
  return jsonify({"status": "error", "message": "Internal server error"}), 500
979
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
980
  @app.route('/admin')
981
  def admin_panel():
982
+ # WARNING: This route is unprotected! Add proper authentication/authorization.
983
+ current_data = load_visitor_data() # Load from cache/file
984
  users_list = list(current_data.values())
985
  return render_template_string(ADMIN_TEMPLATE, users=users_list)
986
 
987
  @app.route('/admin/download_data', methods=['POST'])
988
  def admin_trigger_download():
989
+ # WARNING: Unprotected endpoint
990
  success = download_data_from_hf()
991
  if success:
992
  return jsonify({"status": "ok", "message": "Скачивание данных с Hugging Face завершено. Страница будет обновлена."})
 
995
 
996
  @app.route('/admin/upload_data', methods=['POST'])
997
  def admin_trigger_upload():
998
+ # WARNING: Unprotected endpoint
999
  if not HF_TOKEN_WRITE:
1000
  return jsonify({"status": "error", "message": "HF_TOKEN_WRITE не настроен на сервере."}), 400
1001
+ upload_data_to_hf_async() # Trigger async upload
1002
  return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
1003
 
 
 
 
 
 
 
1004
 
1005
+ # --- App Initialization ---
1006
  if __name__ == '__main__':
1007
  print("---")
1008
  print("--- MORSHEN GROUP MINI APP SERVER ---")
1009
  print("---")
1010
  print(f"Flask server starting on http://{HOST}:{PORT}")
1011
+ print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
 
 
 
1012
  print(f"Visitor data file: {DATA_FILE}")
1013
  print(f"Hugging Face Repo: {REPO_ID}")
1014
  print(f"HF Data Path: {HF_DATA_FILE_PATH}")
 
1019
  print("---")
1020
  else:
1021
  print("--- Hugging Face tokens found.")
1022
+ # Initial attempt to download data on startup
1023
  print("--- Attempting initial data download from Hugging Face...")
1024
  download_data_from_hf()
1025
 
1026
+ # Load initial data from local file (might have been updated by download)
 
 
 
 
 
 
1027
  load_visitor_data()
1028
 
1029
  print("---")
1030
  print("--- SECURITY WARNING ---")
1031
  print("--- The /admin route and its sub-routes are NOT protected.")
1032
  print("--- Implement proper authentication before deploying.")
 
 
1033
  print("---")
1034
 
1035
+ # Start periodic backup thread if write token is available
1036
  if HF_TOKEN_WRITE:
1037
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1038
  backup_thread.start()
 
1041
  print("--- Periodic backup disabled (HF_TOKEN_WRITE missing).")
1042
 
1043
  print("--- Server Ready ---")
1044
+ # Use a production server like Waitress or Gunicorn instead of app.run() for deployment
1045
+ # from waitress import serve
1046
+ # serve(app, host=HOST, port=PORT)
1047
+ app.run(host=HOST, port=PORT, debug=False) # debug=False for production recommended