Aleksmorshen commited on
Commit
ebbc020
·
verified ·
1 Parent(s): d9e03d7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +505 -223
app.py CHANGED
@@ -11,37 +11,35 @@ 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
- from pytonapi.client import Tonapi # Corrected import
17
- from pytonapi.exceptions import AccountNotFoundError
18
 
19
- BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvvTVsqEJVG5SYK5hUlc_Ewo")
 
20
  HOST = '0.0.0.0'
21
  PORT = 7860
22
- DATA_FILE = 'data.json'
23
 
 
24
  REPO_ID = "flpolprojects/teledata"
25
- HF_DATA_FILE_PATH = "data.json"
26
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
27
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
28
 
29
- TON_API_KEY = os.getenv("TON_API_KEY", "AE7WM7YGSNNKW5YAAAABMLCTU2KXDSRSNZM3Y4GF27OXBPZLAJPKXB6237ZFNQVLSX6F5NA")
 
 
 
30
 
31
  app = Flask(__name__)
32
  logging.basicConfig(level=logging.INFO)
33
  app.secret_key = os.urandom(24)
34
 
 
35
  _data_lock = threading.Lock()
36
- visitor_data_cache = {}
37
-
38
- try:
39
- tonapi = Tonapi(api_key=TON_API_KEY)
40
- logging.info("pytonapi initialized successfully.")
41
- except Exception as e:
42
- logging.error(f"Failed to initialize pytonapi: {e}")
43
- tonapi = None
44
-
45
 
46
  def download_data_from_hf():
47
  global visitor_data_cache
@@ -95,10 +93,10 @@ def load_visitor_data():
95
  visitor_data_cache = {}
96
  return visitor_data_cache
97
 
98
- def save_visitor_data(data):
99
  with _data_lock:
100
  try:
101
- visitor_data_cache.update(data)
102
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
103
  json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
104
  logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
@@ -118,9 +116,13 @@ def upload_data_to_hf():
118
  api = HfApi()
119
  with _data_lock:
120
  file_content_exists = os.path.getsize(DATA_FILE) > 0
121
- if not file_content_exists:
122
- logging.warning(f"{DATA_FILE} is empty. Skipping upload.")
123
- return
 
 
 
 
124
 
125
  logging.info(f"Attempting to upload {DATA_FILE} to {REPO_ID}/{HF_DATA_FILE_PATH}...")
126
  api.upload_file(
@@ -148,7 +150,11 @@ def periodic_backup():
148
  logging.info("Initiating periodic backup...")
149
  upload_data_to_hf()
150
 
 
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,8 +173,9 @@ 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}).")
172
  return parsed_data, True
173
  else:
174
  logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
@@ -177,6 +184,47 @@ def verify_telegram_data(init_data_str):
177
  logging.error(f"Error verifying Telegram data: {e}")
178
  return None, False
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  TEMPLATE = """
181
  <!DOCTYPE html>
182
  <html lang="ru">
@@ -185,6 +233,8 @@ TEMPLATE = """
185
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
186
  <title>Morshen Group</title>
187
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
 
 
188
  <link rel="preconnect" href="https://fonts.googleapis.com">
189
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
190
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
@@ -199,7 +249,7 @@ TEMPLATE = """
199
  --tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#1e1e1e') }};
200
 
201
  --bg-gradient: linear-gradient(160deg, #1a232f 0%, #121212 100%);
202
- --card-bg: rgba(44, 44, 46, 0.8);
203
  --card-bg-solid: #2c2c2e;
204
  --text-color: var(--tg-theme-text-color);
205
  --text-secondary-color: var(--tg-theme-hint-color);
@@ -207,16 +257,16 @@ TEMPLATE = """
207
  --accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
208
  --tag-bg: rgba(255, 255, 255, 0.1);
209
  --border-radius-s: 8px;
210
- --border-radius-m: 14px;
211
- --border-radius-l: 18px;
212
  --padding-s: 10px;
213
- --padding-m: 18px;
214
- --padding-l: 28px;
215
  --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
216
  --shadow-color: rgba(0, 0, 0, 0.3);
217
  --shadow-light: 0 4px 15px var(--shadow-color);
218
  --shadow-medium: 0 6px 25px var(--shadow-color);
219
- --backdrop-blur: 10px;
220
  }
221
  * { box-sizing: border-box; margin: 0; padding: 0; }
222
  html {
@@ -228,11 +278,11 @@ TEMPLATE = """
228
  background: var(--bg-gradient);
229
  color: var(--text-color);
230
  padding: var(--padding-m);
231
- padding-bottom: 120px;
232
  overscroll-behavior-y: none;
233
  -webkit-font-smoothing: antialiased;
234
  -moz-osx-font-smoothing: grayscale;
235
- visibility: hidden;
236
  min-height: 100vh;
237
  }
238
  .container {
@@ -251,7 +301,7 @@ TEMPLATE = """
251
  }
252
  .logo { display: flex; align-items: center; gap: var(--padding-s); }
253
  .logo img {
254
- width: 50px;
255
  height: 50px;
256
  border-radius: 50%;
257
  background-color: var(--card-bg-solid);
@@ -259,23 +309,30 @@ TEMPLATE = """
259
  border: 2px solid rgba(255, 255, 255, 0.15);
260
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
261
  }
262
- .logo span { font-size: 1.6em; font-weight: 700; letter-spacing: -0.5px; }
263
  .btn {
264
  display: inline-flex; align-items: center; justify-content: center;
265
  padding: 12px var(--padding-m); border-radius: var(--border-radius-m);
266
  background: var(--accent-gradient); color: var(--tg-theme-button-text-color);
267
- text-decoration: none; font-weight: 600;
268
  border: none; cursor: pointer;
269
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
270
  gap: 8px; font-size: 1em;
271
  box-shadow: var(--shadow-light);
272
  letter-spacing: 0.3px;
 
273
  }
274
  .btn:hover {
275
  opacity: 0.9;
276
  box-shadow: var(--shadow-medium);
277
  transform: translateY(-2px);
278
  }
 
 
 
 
 
 
279
  .btn-secondary {
280
  background: var(--card-bg);
281
  color: var(--tg-theme-link-color);
@@ -285,6 +342,14 @@ TEMPLATE = """
285
  background: rgba(44, 44, 46, 0.95);
286
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), var(--shadow-medium);
287
  }
 
 
 
 
 
 
 
 
288
  .tag {
289
  display: inline-block; background: var(--tag-bg); color: var(--text-secondary-color);
290
  padding: 6px 12px; border-radius: var(--border-radius-s); font-size: 0.85em;
@@ -297,14 +362,14 @@ TEMPLATE = """
297
  background-color: var(--card-bg);
298
  border-radius: var(--border-radius-l);
299
  padding: var(--padding-l);
300
- margin-bottom: 0;
301
  box-shadow: var(--shadow-medium);
302
  border: 1px solid rgba(255, 255, 255, 0.08);
303
  backdrop-filter: blur(var(--backdrop-blur));
304
  -webkit-backdrop-filter: blur(var(--backdrop-blur));
305
  }
306
  .section-title {
307
- font-size: 2em;
308
  font-weight: 700; margin-bottom: var(--padding-s); line-height: 1.25;
309
  letter-spacing: -0.6px;
310
  }
@@ -313,7 +378,7 @@ TEMPLATE = """
313
  margin-bottom: var(--padding-m);
314
  }
315
  .description {
316
- font-size: 1.05em; line-height: 1.6; color: var(--text-secondary-color);
317
  margin-bottom: var(--padding-m);
318
  }
319
  .stats-grid {
@@ -333,7 +398,7 @@ TEMPLATE = """
333
  background-color: var(--card-bg-solid);
334
  padding: var(--padding-m); border-radius: var(--border-radius-m);
335
  margin-bottom: var(--padding-s); display: flex; align-items: center;
336
- gap: var(--padding-m);
337
  font-size: 1.1em; font-weight: 500;
338
  border: 1px solid rgba(255, 255, 255, 0.08);
339
  transition: background-color 0.2s ease, transform 0.2s ease;
@@ -349,11 +414,11 @@ TEMPLATE = """
349
  }
350
  .save-card-button {
351
  position: fixed;
352
- bottom: 30px;
353
  left: 50%;
354
  transform: translateX(-50%);
355
- padding: 14px 28px;
356
- border-radius: 30px;
357
  background: var(--accent-gradient-green);
358
  color: var(--tg-theme-button-text-color);
359
  text-decoration: none;
@@ -362,25 +427,59 @@ TEMPLATE = """
362
  cursor: pointer;
363
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
364
  z-index: 1000;
365
- box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.5);
366
- font-size: 1.05em;
367
  display: flex;
368
  align-items: center;
369
- gap: 10px;
370
  backdrop-filter: blur(5px);
371
  -webkit-backdrop-filter: blur(5px);
372
  }
373
  .save-card-button:hover {
374
  opacity: 0.95;
375
- transform: translateX(-50%) scale(1.05);
376
  box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.3);
377
  }
378
  .save-card-button i { font-size: 1.2em; }
379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  .modal {
381
  display: none; position: fixed; z-index: 1001;
382
  left: 0; top: 0; width: 100%; height: 100%;
383
- overflow: auto; background-color: rgba(0,0,0,0.7);
384
  backdrop-filter: blur(8px);
385
  -webkit-backdrop-filter: blur(8px);
386
  animation: fadeIn 0.3s ease-out;
@@ -407,21 +506,8 @@ TEMPLATE = """
407
  .modal-text { font-size: 1.2em; line-height: 1.6; margin-bottom: var(--padding-s); word-wrap: break-word; }
408
  .modal-text b { color: var(--tg-theme-link-color); font-weight: 600; }
409
  .modal-instruction { font-size: 1em; color: var(--text-secondary-color); margin-top: var(--padding-m); }
410
- .ton-info {
411
- margin-top: var(--padding-m);
412
- padding: var(--padding-m);
413
- border-radius: var(--border-radius-m);
414
- background-color: rgba(255, 255, 255, 0.08);
415
- border: 1px solid rgba(255, 255, 255, 0.1);
416
- text-align: center;
417
- }
418
- .ton-info p { margin-bottom: 0.5rem; }
419
- .ton-info strong { color: var(--tg-theme-link-color); }
420
- .ton-address { font-size: 0.9em; color: var(--text-secondary-color); word-break: break-all; margin-top: 0.5rem; }
421
- .ton-balance { font-size: 1.4em; font-weight: 600; color: var(--accent-gradient-green-end, #30d158); margin-top: 0.5rem; }
422
- .ton-error { font-size: 1em; color: var(--admin-danger, #dc3545); margin-top: 0.5rem; }
423
-
424
 
 
425
  .icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
426
  .icon-save::before { content: '💾'; }
427
  .icon-web::before { content: '🌐'; }
@@ -442,9 +528,11 @@ TEMPLATE = """
442
  .icon-link::before { content: '🔗'; }
443
  .icon-leader::before { content: '🏆'; }
444
  .icon-company::before { content: '🏢'; }
 
445
  .icon-ton::before { content: '💎'; }
 
446
 
447
-
448
  @media (max-width: 480px) {
449
  .section-title { font-size: 1.8em; }
450
  .logo span { font-size: 1.4em; }
@@ -453,6 +541,8 @@ TEMPLATE = """
453
  .stats-grid { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: var(--padding-s); }
454
  .stat-value { font-size: 1.5em; }
455
  .modal-content { margin: 25% auto; width: 92%; }
 
 
456
  }
457
  </style>
458
  </head>
@@ -475,11 +565,29 @@ TEMPLATE = """
475
  Объединяем передовые технологические компании для создания инновационных
476
  решений мирового уровня. Мы строим будущее технологий сегодня.
477
  </p>
478
- <a href="#" class="btn contact-link" style="background: var(--accent-gradient-green); width: 100%; margin-top: var(--padding-s);">
479
  <i class="icon icon-contact"></i>Написать нам в Telegram
480
  </a>
481
  </section>
482
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  <section class="ecosystem-header">
484
  <h2 class="section-title"><i class="icon icon-company"></i>Экосистема инноваций</h2>
485
  <p class="description">
@@ -553,20 +661,6 @@ TEMPLATE = """
553
  </div>
554
  </section>
555
 
556
- <section class="section-card">
557
- <h2 class="section-title"><i class="icon icon-ton"></i>TON Кошелек</h2>
558
- <p class="description">Проверьте баланс вашего TON кошелька.</p>
559
- <div style="text-align: center; margin-bottom: var(--padding-m);">
560
- <button id="ton-button" class="btn"><i class="icon icon-ton"></i> Проверить баланс</button>
561
- </div>
562
- <div id="ton-info-display" class="ton-info" style="display: none;">
563
- <p><strong>Адрес:</strong> <span id="ton-address-display"></span></p>
564
- <p><strong>Баланс:</strong> <span id="ton-balance-display"></span></p>
565
- <p id="ton-error-display" class="ton-error" style="display: none;"></p>
566
- </div>
567
- </section>
568
-
569
-
570
  <section class="section-card">
571
  <h2 class="section-title"><i class="icon icon-global"></i>Глобальное присутствие</h2>
572
  <p class="description">Наши инновационные решения и экспертиза доступны в странах Центральной Азии и за ее пределами:</p>
@@ -588,6 +682,7 @@ TEMPLATE = """
588
  <i class="icon icon-save"></i>Сохранить визитку
589
  </button>
590
 
 
591
  <div id="saveModal" class="modal">
592
  <div class="modal-content">
593
  <span class="modal-close" id="modal-close-btn">×</span>
@@ -601,6 +696,8 @@ TEMPLATE = """
601
 
602
  <script>
603
  const tg = window.Telegram.WebApp;
 
 
604
 
605
  function applyTheme(themeParams) {
606
  const root = document.documentElement;
@@ -621,71 +718,9 @@ TEMPLATE = """
621
  } catch (e) {
622
  root.style.setProperty('--tg-theme-bg-color-rgb', `18, 18, 18`);
623
  }
624
- try {
625
- const greenColor = themeParams.button_color || '#34c759';
626
- const greenGradientEnd = '#' + (parseInt(greenColor.slice(1), 16) + 0x101010).toString(16).padStart(6, '0');
627
- root.style.setProperty('--accent-gradient-green', `linear-gradient(95deg, #34c759, ${themeParams.button_color || '#30d158'})`);
628
- root.style.setProperty('--accent-gradient-green-end', themeParams.button_color || '#30d158');
629
- } catch (e) {
630
- root.style.setProperty('--accent-gradient-green', 'linear-gradient(95deg, #34c759, #30d158)');
631
- root.style.setProperty('--accent-gradient-green-end', '#30d158');
632
- }
633
-
634
  }
635
 
636
- async function getTonBalance(address) {
637
- const tonInfoDisplay = document.getElementById('ton-info-display');
638
- const tonAddressDisplay = document.getElementById('ton-address-display');
639
- const tonBalanceDisplay = document.getElementById('ton-balance-display');
640
- const tonErrorDisplay = document.getElementById('ton-error-display');
641
- const tonButton = document.getElementById('ton-button');
642
-
643
- tonInfoDisplay.style.display = 'block';
644
- tonAddressDisplay.textContent = address;
645
- tonBalanceDisplay.textContent = 'Загрузка...';
646
- tonBalanceDisplay.style.color = 'var(--tg-theme-hint-color)';
647
- tonErrorDisplay.style.display = 'none';
648
- tonButton.disabled = true;
649
- tonButton.style.opacity = 0.7;
650
-
651
-
652
- try {
653
- const response = await fetch('/get_ton_balance', {
654
- method: 'POST',
655
- headers: { 'Content-Type': 'application/json' },
656
- body: JSON.stringify({ wallet_address: address })
657
- });
658
-
659
- const data = await response.json();
660
-
661
- if (response.ok && data.status === 'ok') {
662
- tonBalanceDisplay.textContent = `${data.balance} TON`;
663
- tonBalanceDisplay.style.color = 'var(--accent-gradient-green-end, #30d158)';
664
- if (tg.HapticFeedback) tg.HapticFeedback.notificationOccurred('success');
665
- } else {
666
- tonBalanceDisplay.textContent = '';
667
- tonErrorDisplay.textContent = data.message || 'Неизвестная ошибка';
668
- tonErrorDisplay.style.display = 'block';
669
- tonBalanceDisplay.style.color = 'var(--admin-danger, #dc3545)';
670
- console.error('TON Balance Error:', data.message);
671
- if (tg.HapticFeedback) tg.HapticFeedback.notificationOccurred('error');
672
-
673
- }
674
- } catch (error) {
675
- tonBalanceDisplay.textContent = '';
676
- tonErrorDisplay.textContent = `Ошибка сети: ${error.message}`;
677
- tonErrorDisplay.style.display = 'block';
678
- tonBalanceDisplay.style.color = 'var(--admin-danger, #dc3545)';
679
- console.error('Fetch Error:', error);
680
- if (tg.HapticFeedback) tg.HapticFeedback.notificationOccurred('error');
681
- } finally {
682
- tonButton.disabled = false;
683
- tonButton.style.opacity = 1;
684
- }
685
- }
686
-
687
-
688
- function setupTelegram() {
689
  if (!tg || !tg.initData) {
690
  console.error("Telegram WebApp script not loaded or initData is missing.");
691
  const greetingElement = document.getElementById('greeting');
@@ -700,36 +735,43 @@ TEMPLATE = """
700
  applyTheme(tg.themeParams);
701
  tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
702
 
703
- fetch('/verify', {
704
- method: 'POST',
705
- headers: {
706
- 'Content-Type': 'application/json',
707
- 'Accept': 'application/json'
708
- },
709
- body: JSON.stringify({ initData: tg.initData }),
710
- })
711
- .then(response => response.json())
712
- .then(data => {
713
- if (data.status === 'ok' && data.verified) {
 
 
714
  console.log('Backend verification successful.');
 
 
 
 
 
 
 
 
 
 
 
 
715
  } else {
716
  console.warn('Backend verification failed:', data.message);
 
717
  }
718
- })
719
- .catch(error => {
720
  console.error('Error sending initData for verification:', error);
721
- });
722
-
723
- const user = tg.initDataUnsafe?.user;
724
- const greetingElement = document.getElementById('greeting');
725
- if (user) {
726
- const name = user.first_name || user.username || 'Гость';
727
- greetingElement.textContent = `Добро пожаловать, ${name}! 👋`;
728
- } else {
729
- greetingElement.textContent = 'Добро пожаловать!';
730
- console.warn('Telegram User data not available (initDataUnsafe.user is empty).');
731
  }
732
 
 
 
733
  const contactButtons = document.querySelectorAll('.contact-link');
734
  contactButtons.forEach(button => {
735
  button.addEventListener('click', (e) => {
@@ -738,6 +780,7 @@ TEMPLATE = """
738
  });
739
  });
740
 
 
741
  const modal = document.getElementById("saveModal");
742
  const saveCardBtn = document.getElementById("save-card-btn");
743
  const closeBtn = document.getElementById("modal-close-btn");
@@ -764,23 +807,190 @@ TEMPLATE = """
764
  console.error("Modal elements not found!");
765
  }
766
 
767
- const tonButton = document.getElementById('ton-button');
768
- if (tonButton) {
769
- tonButton.addEventListener('click', async () => {
770
- const address = prompt("Пожалуйста, введите адрес вашего TON кошелька:");
771
- if (address) {
772
- await getTonBalance(address.trim());
773
- } else {
774
- alert("Адрес кошелька не введен.");
 
 
 
 
 
 
 
 
775
  }
776
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
777
  } else {
778
- console.error("TON button not found!");
779
  }
 
780
 
 
 
 
 
 
 
781
 
782
- document.body.style.visibility = 'visible';
783
- }
784
 
785
  if (window.Telegram && window.Telegram.WebApp) {
786
  setupTelegram();
@@ -864,7 +1074,7 @@ ADMIN_TEMPLATE = """
864
  width: 80px; height: 80px;
865
  border-radius: 50%; margin-bottom: 1rem;
866
  object-fit: cover; border: 3px solid var(--admin-border);
867
- background-color: #eee;
868
  }
869
  .user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
870
  .user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
@@ -893,6 +1103,7 @@ ADMIN_TEMPLATE = """
893
  }
894
  .refresh-btn:hover { background-color: #0b5ed7; }
895
 
 
896
  .admin-controls {
897
  background: var(--admin-card-bg);
898
  padding: var(--padding);
@@ -922,7 +1133,7 @@ ADMIN_TEMPLATE = """
922
  .admin-controls .loader {
923
  border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid var(--admin-primary);
924
  width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle;
925
- display: none;
926
  }
927
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
928
  </style>
@@ -951,13 +1162,14 @@ ADMIN_TEMPLATE = """
951
  {% if user.username %}
952
  <div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" style="color: inherit; text-decoration: none;">@{{ user.username }}</a></div>
953
  {% else %}
954
- <div class="username" style="height: 1.3em;"></div>
955
  {% endif %}
956
  <div class="details">
957
  <div class="detail-item"><strong>ID:</strong> {{ user.id }}</div>
958
  <div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
959
  <div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
960
  <div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
 
961
  </div>
962
  <div class="timestamp">Визит: {{ user.visited_at_str }}</div>
963
  </div>
@@ -983,7 +1195,7 @@ ADMIN_TEMPLATE = """
983
  statusMessage.textContent = data.message;
984
  statusMessage.style.color = 'var(--admin-success)';
985
  if (action === 'скачивание') {
986
- setTimeout(() => location.reload(), 1500);
987
  }
988
  } else {
989
  throw new Error(data.message || 'Произошла ошибка');
@@ -1009,6 +1221,15 @@ ADMIN_TEMPLATE = """
1009
  </html>
1010
  """
1011
 
 
 
 
 
 
 
 
 
 
1012
  @app.route('/')
1013
  def index():
1014
  theme_params = {}
@@ -1020,86 +1241,132 @@ def verify_data():
1020
  req_data = request.get_json()
1021
  init_data_str = req_data.get('initData')
1022
  if not init_data_str:
 
1023
  return jsonify({"status": "error", "message": "Missing initData"}), 400
1024
 
1025
  user_data_parsed, is_valid = verify_telegram_data(init_data_str)
1026
 
1027
  user_info_dict = {}
 
1028
  if user_data_parsed and 'user' in user_data_parsed:
1029
  try:
1030
  user_json_str = unquote(user_data_parsed['user'][0])
1031
  user_info_dict = json.loads(user_json_str)
 
1032
  except Exception as e:
1033
- logging.error(f"Could not parse user JSON: {e}")
1034
  user_info_dict = {}
1035
 
1036
- if is_valid:
1037
- user_id = user_info_dict.get('id')
1038
- if user_id:
1039
- now = time.time()
1040
- user_entry = {
1041
- str(user_id): {
1042
- 'id': user_id,
1043
- 'first_name': user_info_dict.get('first_name'),
1044
- 'last_name': user_info_dict.get('last_name'),
1045
- 'username': user_info_dict.get('username'),
1046
- 'photo_url': user_info_dict.get('photo_url'),
1047
- 'language_code': user_info_dict.get('language_code'),
1048
- 'is_premium': user_info_dict.get('is_premium', False),
1049
- 'phone_number': user_info_dict.get('phone_number'),
1050
- 'visited_at': now,
1051
- 'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
1052
- }
 
 
 
 
 
1053
  }
1054
- save_visitor_data(user_entry)
 
1055
  return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
1056
  else:
1057
- logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
1058
- return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
1059
 
1060
  except Exception as e:
1061
  logging.exception("Error in /verify endpoint")
1062
  return jsonify({"status": "error", "message": "Internal server error"}), 500
1063
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1064
  @app.route('/get_ton_balance', methods=['POST'])
1065
  def get_ton_balance_route():
1066
- if tonapi is None:
1067
- return jsonify({"status": "error", "message": "TON API not initialized on server."}), 500
1068
-
1069
  try:
1070
  req_data = request.get_json()
1071
- wallet_address = req_data.get('wallet_address')
1072
 
1073
- if not wallet_address:
1074
- return jsonify({"status": "error", "message": "Адрес кошелька не предоставлен."}), 400
1075
 
1076
- wallet_address = wallet_address.strip()
 
1077
 
1078
- try:
1079
- account_info = tonapi.account.get_info(account_id=wallet_address)
1080
- balance_nano = account_info.balance
1081
- balance_ton = balance_nano / 1e9
1082
- return jsonify({"status": "ok", "balance": f"{balance_ton:,.9f}"}), 200
1083
- except AccountNotFoundError:
1084
- logging.warning(f"Account not found for address: {wallet_address}")
1085
- return jsonify({"status": "error", "message": "TON кошелек не найден."}), 404
1086
- except Exception as e:
1087
- logging.error(f"Error fetching TON balance for {wallet_address}: {e}")
1088
- return jsonify({"status": "error", "message": f"Ошибка получения баланса: {str(e)}"}), 500
 
 
 
 
 
1089
 
1090
  except Exception as e:
1091
  logging.exception("Error in /get_ton_balance endpoint")
1092
- return jsonify({"status": "error", "message": "Внутренняя ошибка сервера."}), 500
1093
 
1094
 
1095
  @app.route('/admin')
1096
  def admin_panel():
 
1097
  current_data = load_visitor_data()
1098
  users_list = list(current_data.values())
1099
  return render_template_string(ADMIN_TEMPLATE, users=users_list)
1100
 
1101
  @app.route('/admin/download_data', methods=['POST'])
1102
  def admin_trigger_download():
 
1103
  success = download_data_from_hf()
1104
  if success:
1105
  return jsonify({"status": "ok", "message": "Скачивание данных с Hugging Face завершено. Страница будет обновлена."})
@@ -1108,18 +1375,32 @@ def admin_trigger_download():
1108
 
1109
  @app.route('/admin/upload_data', methods=['POST'])
1110
  def admin_trigger_upload():
 
1111
  if not HF_TOKEN_WRITE:
1112
  return jsonify({"status": "error", "message": "HF_TOKEN_WRITE не настроен на сервере."}), 400
1113
  upload_data_to_hf_async()
1114
  return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
1115
 
 
 
 
 
 
 
 
 
 
1116
 
 
1117
  if __name__ == '__main__':
1118
  print("---")
1119
  print("--- MORSHEN GROUP MINI APP SERVER ---")
1120
  print("---")
1121
  print(f"Flask server starting on http://{HOST}:{PORT}")
1122
- print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
 
 
 
1123
  print(f"Visitor data file: {DATA_FILE}")
1124
  print(f"Hugging Face Repo: {REPO_ID}")
1125
  print(f"HF Data Path: {HF_DATA_FILE_PATH}")
@@ -1134,12 +1415,11 @@ if __name__ == '__main__':
1134
  download_data_from_hf()
1135
 
1136
  if not TON_API_KEY:
1137
- print("---")
1138
- print("--- WARNING: TON_API_KEY NOT SET ---")
1139
- print("--- TON balance check functionality will not work. Set TON_API_KEY environment variable.")
1140
- print("---")
1141
  else:
1142
- print("--- TON_API_KEY found.")
 
1143
 
1144
  load_visitor_data()
1145
 
@@ -1147,6 +1427,8 @@ if __name__ == '__main__':
1147
  print("--- SECURITY WARNING ---")
1148
  print("--- The /admin route and its sub-routes are NOT protected.")
1149
  print("--- Implement proper authentication before deploying.")
 
 
1150
  print("---")
1151
 
1152
  if HF_TOKEN_WRITE:
 
11
  from datetime import datetime
12
  import logging
13
  import threading
14
+ import requests
15
  from huggingface_hub import HfApi, hf_hub_download
16
  from huggingface_hub.utils import RepositoryNotFoundError
17
+ import decimal
 
18
 
19
+ # --- Configuration ---
20
+ BOT_TOKEN = os.getenv("BOT_TOKEN") # Telegram Bot Token
21
  HOST = '0.0.0.0'
22
  PORT = 7860
23
+ DATA_FILE = 'data.json' # Local file for visitor data
24
 
25
+ # Hugging Face Settings
26
  REPO_ID = "flpolprojects/teledata"
27
+ HF_DATA_FILE_PATH = "data.json" # Path within the HF repo
28
+ HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # Token with write access
29
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Token with read access
30
 
31
+ # TON Configuration
32
+ TON_API_KEY = os.getenv("TON_API_KEY", "AE7WM7YGSNNKW5YAAAABMLCTU2KXDSRSNZM3Y4GF27OXBPZLAJPKXB6237ZFNQVLSX6F5NA") # TON API Key
33
+ TON_API_URL = f"https://go.getblock.io/{TON_API_KEY}" # GetBlock API URL (adjust if using a different provider)
34
+ NANOTON_TO_TON = Decimal('1000000000')
35
 
36
  app = Flask(__name__)
37
  logging.basicConfig(level=logging.INFO)
38
  app.secret_key = os.urandom(24)
39
 
40
+ # --- Hugging Face & Data Handling ---
41
  _data_lock = threading.Lock()
42
+ visitor_data_cache = {} # In-memory cache
 
 
 
 
 
 
 
 
43
 
44
  def download_data_from_hf():
45
  global visitor_data_cache
 
93
  visitor_data_cache = {}
94
  return visitor_data_cache
95
 
96
+ def save_visitor_data(data_update):
97
  with _data_lock:
98
  try:
99
+ visitor_data_cache.update(data_update)
100
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
101
  json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
102
  logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
 
116
  api = HfApi()
117
  with _data_lock:
118
  file_content_exists = os.path.getsize(DATA_FILE) > 0
119
+ if not file_content_exists and not visitor_data_cache: # Handle empty cache/file scenario
120
+ logging.info(f"{DATA_FILE} is empty and cache is empty. Skipping upload.")
121
+ return
122
+ elif not file_content_exists: # File empty but cache has data, write cache first
123
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
124
+ json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
125
+ logging.info("Empty local file found, wrote cache to file before upload.")
126
 
127
  logging.info(f"Attempting to upload {DATA_FILE} to {REPO_ID}/{HF_DATA_FILE_PATH}...")
128
  api.upload_file(
 
150
  logging.info("Initiating periodic backup...")
151
  upload_data_to_hf()
152
 
153
+ # --- Telegram Verification ---
154
  def verify_telegram_data(init_data_str):
155
+ if not BOT_TOKEN:
156
+ logging.error("BOT_TOKEN not set. Telegram data verification skipped.")
157
+ return None, False
158
  try:
159
  parsed_data = parse_qs(init_data_str)
160
  received_hash = parsed_data.pop('hash', [None])[0]
 
173
  if calculated_hash == received_hash:
174
  auth_date = int(parsed_data.get('auth_date', [0])[0])
175
  current_time = int(time.time())
176
+ # Allow data up to 24 hours old (86400 seconds)
177
  if current_time - auth_date > 86400:
178
+ logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}). Verification passed but data is old.")
179
  return parsed_data, True
180
  else:
181
  logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
 
184
  logging.error(f"Error verifying Telegram data: {e}")
185
  return None, False
186
 
187
+ # --- TON Integration ---
188
+ def get_ton_balance(address):
189
+ if not TON_API_KEY:
190
+ logging.error("TON_API_KEY not set. Cannot fetch balance.")
191
+ return None, "TON API Key not configured."
192
+
193
+ headers = {'Content-Type': 'application/json'}
194
+ payload = {
195
+ "jsonrpc": "2.0",
196
+ "method": "getAccountState",
197
+ "params": [address],
198
+ "id": 1
199
+ }
200
+ try:
201
+ response = requests.post(TON_API_URL, headers=headers, json=payload, timeout=10)
202
+ response.raise_for_status() # Raise an exception for bad status codes
203
+ result = response.json()
204
+
205
+ if 'error' in result:
206
+ logging.error(f"TON API Error for address {address}: {result['error']}")
207
+ return None, f"TON API Error: {result['error'].get('message', 'Unknown error')}"
208
+
209
+ account_state = result.get('result')
210
+ if not account_state or 'balance' not in account_state:
211
+ # Account might be inactive or not exist
212
+ logging.warning(f"Account state or balance not found for address {address}. Result: {account_state}")
213
+ return '0', "Аккаунт неактивен или баланс 0." # Assume 0 balance for inactive/unknown state
214
+
215
+ balance_nanoton = Decimal(account_state['balance'])
216
+ balance_ton = balance_nanoton / NANOTON_TO_TON
217
+ return str(balance_ton), None # Return balance as string
218
+
219
+ except requests.exceptions.RequestException as e:
220
+ logging.error(f"Error fetching TON balance for address {address}: {e}")
221
+ return None, f"Network Error: Could not connect to TON API."
222
+ except Exception as e:
223
+ logging.exception(f"Unexpected error fetching TON balance for address {address}: {e}")
224
+ return None, "Internal error fetching balance."
225
+
226
+
227
+ # --- HTML Templates ---
228
  TEMPLATE = """
229
  <!DOCTYPE html>
230
  <html lang="ru">
 
233
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
234
  <title>Morshen Group</title>
235
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
236
+ <script src="https://unpkg.com/@tonconnect/sdk@latest/dist/tonconnect-sdk.min.js"></script>
237
+ <script src="https://unpkg.com/@tonconnect/ui@latest/dist/tonconnect-ui.min.js"></script>
238
  <link rel="preconnect" href="https://fonts.googleapis.com">
239
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
240
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
 
249
  --tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#1e1e1e') }};
250
 
251
  --bg-gradient: linear-gradient(160deg, #1a232f 0%, #121212 100%);
252
+ --card-bg: rgba(44, 44, 46, 0.8); /* Semi-transparent card */
253
  --card-bg-solid: #2c2c2e;
254
  --text-color: var(--tg-theme-text-color);
255
  --text-secondary-color: var(--tg-theme-hint-color);
 
257
  --accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
258
  --tag-bg: rgba(255, 255, 255, 0.1);
259
  --border-radius-s: 8px;
260
+ --border-radius-m: 14px; /* Increased radius */
261
+ --border-radius-l: 18px; /* Increased radius */
262
  --padding-s: 10px;
263
+ --padding-m: 18px; /* Increased padding */
264
+ --padding-l: 28px; /* Increased padding */
265
  --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
266
  --shadow-color: rgba(0, 0, 0, 0.3);
267
  --shadow-light: 0 4px 15px var(--shadow-color);
268
  --shadow-medium: 0 6px 25px var(--shadow-color);
269
+ --backdrop-blur: 10px; /* Glassmorphism effect */
270
  }
271
  * { box-sizing: border-box; margin: 0; padding: 0; }
272
  html {
 
278
  background: var(--bg-gradient);
279
  color: var(--text-color);
280
  padding: var(--padding-m);
281
+ padding-bottom: 120px; /* More space for fixed button */
282
  overscroll-behavior-y: none;
283
  -webkit-font-smoothing: antialiased;
284
  -moz-osx-font-smoothing: grayscale;
285
+ visibility: hidden; /* Hide until ready */
286
  min-height: 100vh;
287
  }
288
  .container {
 
301
  }
302
  .logo { display: flex; align-items: center; gap: var(--padding-s); }
303
  .logo img {
304
+ width: 50px; /* Larger logo */
305
  height: 50px;
306
  border-radius: 50%;
307
  background-color: var(--card-bg-solid);
 
309
  border: 2px solid rgba(255, 255, 255, 0.15);
310
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
311
  }
312
+ .logo span { font-size: 1.6em; font-weight: 700; letter-spacing: -0.5px; } /* Bold, slightly larger */
313
  .btn {
314
  display: inline-flex; align-items: center; justify-content: center;
315
  padding: 12px var(--padding-m); border-radius: var(--border-radius-m);
316
  background: var(--accent-gradient); color: var(--tg-theme-button-text-color);
317
+ text-decoration: none; font-weight: 600; /* Bolder */
318
  border: none; cursor: pointer;
319
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
320
  gap: 8px; font-size: 1em;
321
  box-shadow: var(--shadow-light);
322
  letter-spacing: 0.3px;
323
+ text-align: center;
324
  }
325
  .btn:hover {
326
  opacity: 0.9;
327
  box-shadow: var(--shadow-medium);
328
  transform: translateY(-2px);
329
  }
330
+ .btn:disabled {
331
+ opacity: 0.5;
332
+ cursor: not-allowed;
333
+ transform: none;
334
+ box-shadow: var(--shadow-light);
335
+ }
336
  .btn-secondary {
337
  background: var(--card-bg);
338
  color: var(--tg-theme-link-color);
 
342
  background: rgba(44, 44, 46, 0.95);
343
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), var(--shadow-medium);
344
  }
345
+ .btn-green {
346
+ background: var(--accent-gradient-green);
347
+ color: var(--tg-theme-button-text-color);
348
+ box-shadow: var(--shadow-light);
349
+ }
350
+ .btn-green:hover {
351
+ background: linear-gradient(95deg, #28a745, #218838); /* Darker green */
352
+ }
353
  .tag {
354
  display: inline-block; background: var(--tag-bg); color: var(--text-secondary-color);
355
  padding: 6px 12px; border-radius: var(--border-radius-s); font-size: 0.85em;
 
362
  background-color: var(--card-bg);
363
  border-radius: var(--border-radius-l);
364
  padding: var(--padding-l);
365
+ margin-bottom: 0; /* Removed bottom margin, gap handles spacing */
366
  box-shadow: var(--shadow-medium);
367
  border: 1px solid rgba(255, 255, 255, 0.08);
368
  backdrop-filter: blur(var(--backdrop-blur));
369
  -webkit-backdrop-filter: blur(var(--backdrop-blur));
370
  }
371
  .section-title {
372
+ font-size: 2em; /* Larger titles */
373
  font-weight: 700; margin-bottom: var(--padding-s); line-height: 1.25;
374
  letter-spacing: -0.6px;
375
  }
 
378
  margin-bottom: var(--padding-m);
379
  }
380
  .description {
381
+ font-size: 1.05em; line-height: 1.6; color: var(--text-secondary-color); /* Slightly larger desc */
382
  margin-bottom: var(--padding-m);
383
  }
384
  .stats-grid {
 
398
  background-color: var(--card-bg-solid);
399
  padding: var(--padding-m); border-radius: var(--border-radius-m);
400
  margin-bottom: var(--padding-s); display: flex; align-items: center;
401
+ gap: var(--padding-m); /* Increased gap */
402
  font-size: 1.1em; font-weight: 500;
403
  border: 1px solid rgba(255, 255, 255, 0.08);
404
  transition: background-color 0.2s ease, transform 0.2s ease;
 
414
  }
415
  .save-card-button {
416
  position: fixed;
417
+ bottom: 30px; /* Raised */
418
  left: 50%;
419
  transform: translateX(-50%);
420
+ padding: 14px 28px; /* Larger padding */
421
+ border-radius: 30px; /* More rounded */
422
  background: var(--accent-gradient-green);
423
  color: var(--tg-theme-button-text-color);
424
  text-decoration: none;
 
427
  cursor: pointer;
428
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
429
  z-index: 1000;
430
+ box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.5); /* Add outer glow */
431
+ font-size: 1.05em; /* Slightly larger text */
432
  display: flex;
433
  align-items: center;
434
+ gap: 10px; /* Increased gap */
435
  backdrop-filter: blur(5px);
436
  -webkit-backdrop-filter: blur(5px);
437
  }
438
  .save-card-button:hover {
439
  opacity: 0.95;
440
+ transform: translateX(-50%) scale(1.05); /* Slightly larger scale */
441
  box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.3);
442
  }
443
  .save-card-button i { font-size: 1.2em; }
444
 
445
+ /* TON Wallet Section */
446
+ .ton-wallet-section h2 {
447
+ font-size: 1.6em;
448
+ font-weight: 600;
449
+ margin-bottom: var(--padding-s);
450
+ }
451
+ .ton-wallet-section .wallet-info {
452
+ margin-top: var(--padding-m);
453
+ padding: var(--padding-m);
454
+ background-color: rgba(255, 255, 255, 0.05);
455
+ border-radius: var(--border-radius-m);
456
+ border: 1px solid rgba(255, 255, 255, 0.08);
457
+ word-break: break-all;
458
+ }
459
+ .ton-wallet-section .wallet-info p { margin-bottom: 8px; font-size: 1em; }
460
+ .ton-wallet-section .wallet-info strong { color: var(--tg-theme-link-color); }
461
+ .ton-wallet-section .wallet-info .balance {
462
+ font-size: 1.3em;
463
+ font-weight: 600;
464
+ margin-top: 12px;
465
+ color: var(--accent-gradient-start, #34c759);
466
+ }
467
+ .ton-wallet-section .wallet-info .balance span { font-size: 0.8em; color: var(--text-secondary-color); }
468
+ .ton-wallet-actions {
469
+ display: flex;
470
+ gap: var(--padding-s);
471
+ margin-top: var(--padding-m);
472
+ flex-wrap: wrap;
473
+ justify-content: center;
474
+ }
475
+ .ton-wallet-actions .btn { flex-grow: 1; }
476
+
477
+
478
+ /* Modal Styles */
479
  .modal {
480
  display: none; position: fixed; z-index: 1001;
481
  left: 0; top: 0; width: 100%; height: 100%;
482
+ overflow: auto; background-color: rgba(0,0,0,0.7); /* Darker backdrop */
483
  backdrop-filter: blur(8px);
484
  -webkit-backdrop-filter: blur(8px);
485
  animation: fadeIn 0.3s ease-out;
 
506
  .modal-text { font-size: 1.2em; line-height: 1.6; margin-bottom: var(--padding-s); word-wrap: break-word; }
507
  .modal-text b { color: var(--tg-theme-link-color); font-weight: 600; }
508
  .modal-instruction { font-size: 1em; color: var(--text-secondary-color); margin-top: var(--padding-m); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
 
510
+ /* Icons */
511
  .icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
512
  .icon-save::before { content: '💾'; }
513
  .icon-web::before { content: '🌐'; }
 
528
  .icon-link::before { content: '🔗'; }
529
  .icon-leader::before { content: '🏆'; }
530
  .icon-company::before { content: '🏢'; }
531
+ .icon-wallet::before { content: '👛'; }
532
  .icon-ton::before { content: '💎'; }
533
+ .icon-refresh::before { content: '🔄'; }
534
 
535
+ /* Responsive adjustments */
536
  @media (max-width: 480px) {
537
  .section-title { font-size: 1.8em; }
538
  .logo span { font-size: 1.4em; }
 
541
  .stats-grid { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: var(--padding-s); }
542
  .stat-value { font-size: 1.5em; }
543
  .modal-content { margin: 25% auto; width: 92%; }
544
+ .ton-wallet-section h2 { font-size: 1.4em; }
545
+ .ton-wallet-section .wallet-info .balance { font-size: 1.1em; }
546
  }
547
  </style>
548
  </head>
 
565
  Объединяем передовые технологические компании для создания инновационных
566
  решений мирового уровня. Мы строим будущее технологий сегодня.
567
  </p>
568
+ <a href="#" class="btn contact-link btn-green" style="width: 100%; margin-top: var(--padding-s);">
569
  <i class="icon icon-contact"></i>Написать нам в Telegram
570
  </a>
571
  </section>
572
 
573
+ <section class="ton-wallet-section section-card">
574
+ <h2><i class="icon icon-ton"></i>Интеграция с TON</h2>
575
+ <p class="description" id="wallet-connect-status">
576
+ Подключите ваш TON кошелек, чтобы увидеть баланс.
577
+ </p>
578
+ <div id="ton-connect-button"></div> {# TonConnectUI renders button here #}
579
+
580
+ <div id="wallet-info" class="wallet-info" style="display: none;">
581
+ <p><strong>Кошелек:</strong> <span id="wallet-address">-</span></p>
582
+ <p id="wallet-balance-display"><i class="icon icon-ton"></i> <strong>Баланс:</strong> <span class="balance">- TON</span></p>
583
+ <div class="ton-wallet-actions">
584
+ <button id="fetch-balance-btn" class="btn btn-secondary" style="display: none;"><i class="icon icon-refresh"></i>Обновить баланс</button>
585
+ <button id="disconnect-wallet-btn" class="btn btn-danger" style="display: none;">Отключить</button>
586
+ </div>
587
+ </div>
588
+ </section>
589
+
590
+
591
  <section class="ecosystem-header">
592
  <h2 class="section-title"><i class="icon icon-company"></i>Экосистема инноваций</h2>
593
  <p class="description">
 
661
  </div>
662
  </section>
663
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
664
  <section class="section-card">
665
  <h2 class="section-title"><i class="icon icon-global"></i>Глобальное присутствие</h2>
666
  <p class="description">Наши инновационные решения и экспертиза доступны в странах Центральной Азии и за ее пределами:</p>
 
682
  <i class="icon icon-save"></i>Сохранить визитку
683
  </button>
684
 
685
+ <!-- The Modal -->
686
  <div id="saveModal" class="modal">
687
  <div class="modal-content">
688
  <span class="modal-close" id="modal-close-btn">×</span>
 
696
 
697
  <script>
698
  const tg = window.Telegram.WebApp;
699
+ let telegramUserId = null;
700
+ let tonConnectUI = null;
701
 
702
  function applyTheme(themeParams) {
703
  const root = document.documentElement;
 
718
  } catch (e) {
719
  root.style.setProperty('--tg-theme-bg-color-rgb', `18, 18, 18`);
720
  }
 
 
 
 
 
 
 
 
 
 
721
  }
722
 
723
+ async function setupTelegram() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
724
  if (!tg || !tg.initData) {
725
  console.error("Telegram WebApp script not loaded or initData is missing.");
726
  const greetingElement = document.getElementById('greeting');
 
735
  applyTheme(tg.themeParams);
736
  tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
737
 
738
+ // Send initData for verification and user logging
739
+ try {
740
+ const response = await fetch('/verify', {
741
+ method: 'POST',
742
+ headers: {
743
+ 'Content-Type': 'application/json',
744
+ 'Accept': 'application/json'
745
+ },
746
+ body: JSON.stringify({ initData: tg.initData }),
747
+ });
748
+
749
+ const data = await response.json();
750
+ if (response.ok && data.status === 'ok' && data.verified) {
751
  console.log('Backend verification successful.');
752
+ if (data.user && data.user.id) {
753
+ telegramUserId = data.user.id;
754
+ const name = data.user.first_name || data.user.username || 'Гость';
755
+ document.getElementById('greeting').textContent = `Добро пожаловать, ${name}! 👋`;
756
+
757
+ // Initialize TON Connect UI after user ID is confirmed
758
+ setupTonConnect(telegramUserId);
759
+
760
+ } else {
761
+ document.getElementById('greeting').textContent = 'Добро пожаловать!';
762
+ console.warn('Telegram User data not available (initDataUnsafe.user is empty) after verification.');
763
+ }
764
  } else {
765
  console.warn('Backend verification failed:', data.message);
766
+ document.getElementById('greeting').textContent = 'Добро пожаловать! (Ошибка верификации)';
767
  }
768
+ } catch (error) {
 
769
  console.error('Error sending initData for verification:', error);
770
+ document.getElementById('greeting').textContent = 'Добро пожаловать! (Ошибка сервера)';
 
 
 
 
 
 
 
 
 
771
  }
772
 
773
+
774
+ // Contact Links
775
  const contactButtons = document.querySelectorAll('.contact-link');
776
  contactButtons.forEach(button => {
777
  button.addEventListener('click', (e) => {
 
780
  });
781
  });
782
 
783
+ // Modal Setup
784
  const modal = document.getElementById("saveModal");
785
  const saveCardBtn = document.getElementById("save-card-btn");
786
  const closeBtn = document.getElementById("modal-close-btn");
 
807
  console.error("Modal elements not found!");
808
  }
809
 
810
+ document.body.style.visibility = 'visible';
811
+ }
812
+
813
+ async function setupTonConnect(userId) {
814
+ if (!userId) {
815
+ console.error("Telegram User ID is required to setup TON Connect.");
816
+ document.getElementById('wallet-connect-status').textContent = 'Не удалось инициализировать интеграцию с TON (нет User ID).';
817
+ return;
818
+ }
819
+ document.getElementById('wallet-connect-status').textContent = 'Инициализация TON Connect...';
820
+
821
+ try {
822
+ tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
823
+ manifestUrl: window.location.origin + '/tonconnect-manifest.json',
824
+ connector: {
825
+ timeout: 10000 // 10 seconds timeout for connectors
826
  }
827
  });
828
+
829
+ tonConnectUI.uiOptions = {
830
+ uiPreferences: {
831
+ theme: tg.themeParams.bg_color === '#121212' || tg.themeParams.bg_color === '#000000' ? 'dark' : 'light',
832
+ }
833
+ };
834
+
835
+
836
+ tonConnectUI.onStatusChange(wallet => {
837
+ updateWalletInfo(wallet, userId);
838
+ }, error => {
839
+ console.error("TON Connect onStatusChange error:", error);
840
+ document.getElementById('wallet-connect-status').textContent = 'Ошибка TON Connect. Попробуйте позже.';
841
+ hideWalletInfo();
842
+ // Potentially re-render button or show specific error
843
+ if (tonConnectUI) {
844
+ tonConnectUI.renderButton(document.getElementById('ton-connect-button'), {
845
+ onClick: () => { tonConnectUI.connectWallet(); }
846
+ });
847
+ }
848
+ });
849
+
850
+ tonConnectUI.renderButton(document.getElementById('ton-connect-button'), {
851
+ onClick: () => { tonConnectUI.connectWallet(); }
852
+ });
853
+
854
+ // Initial check in case wallet is already connected
855
+ updateWalletInfo(tonConnectUI.wallet, userId);
856
+
857
+ } catch (e) {
858
+ console.error("Failed to initialize TON Connect UI:", e);
859
+ document.getElementById('wallet-connect-status').textContent = 'Ошибка при запуске TON Connect.';
860
+ hideWalletInfo();
861
+ }
862
+ }
863
+
864
+ async function updateWalletInfo(wallet, userId) {
865
+ const walletInfoDiv = document.getElementById('wallet-info');
866
+ const connectStatusText = document.getElementById('wallet-connect-status');
867
+ const walletAddressSpan = document.getElementById('wallet-address');
868
+ const balanceDisplay = document.getElementById('wallet-balance-display');
869
+ const fetchBalanceBtn = document.getElementById('fetch-balance-btn');
870
+ const disconnectBtn = document.getElementById('disconnect-wallet-btn');
871
+ const tonConnectButtonContainer = document.getElementById('ton-connect-button');
872
+
873
+ if (wallet) {
874
+ console.log("Wallet connected:", wallet);
875
+ // Hide TonConnectUI button
876
+ if (tonConnectButtonContainer) tonConnectButtonContainer.style.display = 'none';
877
+
878
+ connectStatusText.style.display = 'none';
879
+ walletInfoDiv.style.display = 'block';
880
+ walletAddressSpan.textContent = wallet.account.address;
881
+ disconnectBtn.style.display = 'inline-flex';
882
+ fetchBalanceBtn.style.display = 'inline-flex';
883
+ balanceDisplay.innerHTML = '<i class="icon icon-ton"></i> <strong>Баланс:</strong> <span class="balance">Загрузка...</span>';
884
+ fetchBalanceBtn.disabled = false; // Enable fetch button
885
+
886
+ // Save address to backend
887
+ saveTonAddress(userId, wallet.account.address);
888
+
889
+ // Automatically fetch balance after connection
890
+ fetchTonBalance(userId, wallet.account.address);
891
+
892
+ } else {
893
+ console.log("Wallet disconnected.");
894
+ // Show TonConnectUI button
895
+ if (tonConnectButtonContainer) tonConnectButtonContainer.style.display = 'block';
896
+ connectStatusText.style.display = 'block';
897
+ connectStatusText.textContent = 'Подключите ваш TON кошелек, чтобы увидеть баланс.';
898
+ hideWalletInfo();
899
+ // Potentially clear saved address on backend? Depends on desired behavior.
900
+ }
901
+ }
902
+
903
+ function hideWalletInfo() {
904
+ const walletInfoDiv = document.getElementById('wallet-info');
905
+ const walletAddressSpan = document.getElementById('wallet-address');
906
+ const balanceDisplay = document.getElementById('wallet-balance-display');
907
+ const fetchBalanceBtn = document.getElementById('fetch-balance-btn');
908
+ const disconnectBtn = document.getElementById('disconnect-wallet-btn');
909
+
910
+ walletInfoDiv.style.display = 'none';
911
+ walletAddressSpan.textContent = '-';
912
+ balanceDisplay.innerHTML = '<i class="icon icon-ton"></i> <strong>Баланс:</strong> <span class="balance">- TON</span>';
913
+ fetchBalanceBtn.style.display = 'none';
914
+ disconnectBtn.style.display = 'none';
915
+ }
916
+
917
+ async function saveTonAddress(userId, address) {
918
+ if (!userId) {
919
+ console.error("Cannot save TON address: Telegram User ID is missing.");
920
+ return;
921
+ }
922
+ try {
923
+ const response = await fetch('/save_ton_address', {
924
+ method: 'POST',
925
+ headers: { 'Content-Type': 'application/json' },
926
+ body: JSON.stringify({ tg_user_id: userId, ton_address: address }),
927
+ });
928
+ const data = await response.json();
929
+ if (response.ok && data.status === 'ok') {
930
+ console.log('TON address saved successfully.');
931
+ } else {
932
+ console.error('Failed to save TON address:', data.message);
933
+ }
934
+ } catch (error) {
935
+ console.error('Error saving TON address:', error);
936
+ }
937
+ }
938
+
939
+
940
+ async function fetchTonBalance(userId, address) {
941
+ const balanceDisplay = document.getElementById('wallet-balance-display').querySelector('.balance');
942
+ const fetchBalanceBtn = document.getElementById('fetch-balance-btn');
943
+ if (!userId || !address) {
944
+ balanceDisplay.textContent = 'Ошибка (нет данных)';
945
+ fetchBalanceBtn.disabled = true;
946
+ return;
947
+ }
948
+
949
+ balanceDisplay.textContent = 'Загрузка...';
950
+ fetchBalanceBtn.disabled = true; // Disable button during fetch
951
+
952
+ try {
953
+ const response = await fetch('/get_ton_balance', {
954
+ method: 'POST',
955
+ headers: { 'Content-Type': 'application/json' },
956
+ body: JSON.stringify({ tg_user_id: userId }),
957
+ });
958
+ const data = await response.json();
959
+
960
+ if (response.ok && data.status === 'ok') {
961
+ const balance = parseFloat(data.balance).toFixed(4); // Format balance
962
+ balanceDisplay.textContent = `${balance} TON`;
963
+ balanceDisplay.style.color = 'var(--accent-gradient-start, #34c759)';
964
+ } else {
965
+ balanceDisplay.textContent = data.message || 'Ошибка загрузки';
966
+ balanceDisplay.style.color = 'var(--admin-danger, #dc3545)'; // Use error color
967
+ console.error('Failed to fetch balance:', data.message);
968
+ }
969
+ } catch (error) {
970
+ balanceDisplay.textContent = 'Сетевая ошибка';
971
+ balanceDisplay.style.color = 'var(--admin-danger, #dc3545)';
972
+ console.error('Error fetching balance:', error);
973
+ } finally {
974
+ fetchBalanceBtn.disabled = false; // Re-enable button
975
+ }
976
+ }
977
+
978
+ // Add event listener for the fetch balance button
979
+ document.getElementById('fetch-balance-btn').addEventListener('click', () => {
980
+ if (telegramUserId && tonConnectUI && tonConnectUI.wallet && tonConnectUI.wallet.account) {
981
+ fetchTonBalance(telegramUserId, tonConnectUI.wallet.account.address);
982
  } else {
983
+ console.warn("Cannot fetch balance: Wallet not connected or user ID missing.");
984
  }
985
+ });
986
 
987
+ // Add event listener for the disconnect button
988
+ document.getElementById('disconnect-wallet-btn').addEventListener('click', () => {
989
+ if (tonConnectUI) {
990
+ tonConnectUI.disconnect();
991
+ }
992
+ });
993
 
 
 
994
 
995
  if (window.Telegram && window.Telegram.WebApp) {
996
  setupTelegram();
 
1074
  width: 80px; height: 80px;
1075
  border-radius: 50%; margin-bottom: 1rem;
1076
  object-fit: cover; border: 3px solid var(--admin-border);
1077
+ background-color: #eee; /* Placeholder bg */
1078
  }
1079
  .user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
1080
  .user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
 
1103
  }
1104
  .refresh-btn:hover { background-color: #0b5ed7; }
1105
 
1106
+ /* Admin Controls */
1107
  .admin-controls {
1108
  background: var(--admin-card-bg);
1109
  padding: var(--padding);
 
1133
  .admin-controls .loader {
1134
  border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid var(--admin-primary);
1135
  width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle;
1136
+ display: none; /* Hidden by default */
1137
  }
1138
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
1139
  </style>
 
1162
  {% if user.username %}
1163
  <div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" style="color: inherit; text-decoration: none;">@{{ user.username }}</a></div>
1164
  {% else %}
1165
+ <div class="username" style="height: 1.3em;"></div> {# Placeholder for spacing #}
1166
  {% endif %}
1167
  <div class="details">
1168
  <div class="detail-item"><strong>ID:</strong> {{ user.id }}</div>
1169
  <div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
1170
  <div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
1171
  <div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
1172
+ <div class="detail-item"><strong>TON:</strong> {{ user.ton_wallet_address or 'Не подключен' }}</div>
1173
  </div>
1174
  <div class="timestamp">Визит: {{ user.visited_at_str }}</div>
1175
  </div>
 
1195
  statusMessage.textContent = data.message;
1196
  statusMessage.style.color = 'var(--admin-success)';
1197
  if (action === 'скачивание') {
1198
+ setTimeout(() => location.reload(), 1500); // Reload after download success
1199
  }
1200
  } else {
1201
  throw new Error(data.message || 'Произошла ошибка');
 
1221
  </html>
1222
  """
1223
 
1224
+ TON_MANIFEST_TEMPLATE = """
1225
+ {
1226
+ "url": "{{ app_url }}",
1227
+ "name": "Morshen Group TMA",
1228
+ "iconUrl": "https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg"
1229
+ }
1230
+ """
1231
+
1232
+ # --- Flask Routes ---
1233
  @app.route('/')
1234
  def index():
1235
  theme_params = {}
 
1241
  req_data = request.get_json()
1242
  init_data_str = req_data.get('initData')
1243
  if not init_data_str:
1244
+ logging.warning("Missing initData in /verify request.")
1245
  return jsonify({"status": "error", "message": "Missing initData"}), 400
1246
 
1247
  user_data_parsed, is_valid = verify_telegram_data(init_data_str)
1248
 
1249
  user_info_dict = {}
1250
+ user_id = None
1251
  if user_data_parsed and 'user' in user_data_parsed:
1252
  try:
1253
  user_json_str = unquote(user_data_parsed['user'][0])
1254
  user_info_dict = json.loads(user_json_str)
1255
+ user_id = user_info_dict.get('id')
1256
  except Exception as e:
1257
+ logging.error(f"Could not parse user JSON in /verify: {e}")
1258
  user_info_dict = {}
1259
 
1260
+ if is_valid and user_id:
1261
+ str_user_id = str(user_id)
1262
+ now = time.time()
1263
+ current_data = load_visitor_data()
1264
+
1265
+ # Preserve existing TON address if user already exists
1266
+ existing_user_data = current_data.get(str_user_id, {})
1267
+ ton_wallet_address = existing_user_data.get('ton_wallet_address')
1268
+
1269
+ user_entry = {
1270
+ str_user_id: {
1271
+ 'id': user_id,
1272
+ 'first_name': user_info_dict.get('first_name'),
1273
+ 'last_name': user_info_dict.get('last_name'),
1274
+ 'username': user_info_dict.get('username'),
1275
+ 'photo_url': user_info_dict.get('photo_url'),
1276
+ 'language_code': user_info_dict.get('language_code'),
1277
+ 'is_premium': user_info_dict.get('is_premium', False),
1278
+ 'phone_number': user_info_dict.get('phone_number'),
1279
+ 'visited_at': now,
1280
+ 'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S'),
1281
+ 'ton_wallet_address': ton_wallet_address # Keep existing or None
1282
  }
1283
+ }
1284
+ save_visitor_data(user_entry)
1285
  return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
1286
  else:
1287
+ logging.warning(f"Verification failed for user: {user_id}. InitData Valid: {is_valid}")
1288
+ return jsonify({"status": "error", "verified": False, "message": "Invalid data or user ID missing"}), 403
1289
 
1290
  except Exception as e:
1291
  logging.exception("Error in /verify endpoint")
1292
  return jsonify({"status": "error", "message": "Internal server error"}), 500
1293
 
1294
+ @app.route('/save_ton_address', methods=['POST'])
1295
+ def save_ton_address():
1296
+ try:
1297
+ req_data = request.get_json()
1298
+ tg_user_id = req_data.get('tg_user_id')
1299
+ ton_address = req_data.get('ton_address')
1300
+
1301
+ if not tg_user_id or not ton_address:
1302
+ return jsonify({"status": "error", "message": "Missing tg_user_id or ton_address"}), 400
1303
+
1304
+ str_user_id = str(tg_user_id)
1305
+ current_data = load_visitor_data()
1306
+
1307
+ if str_user_id not in current_data:
1308
+ logging.warning(f"Attempted to save TON address for unknown user ID: {tg_user_id}")
1309
+ # Optionally create a minimal entry if user is not in cache yet
1310
+ 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')}
1311
+
1312
+ # Update the TON address for the specific user
1313
+ current_data[str_user_id]['ton_wallet_address'] = ton_address
1314
+
1315
+ # Save the updated data
1316
+ save_visitor_data({str_user_id: current_data[str_user_id]})
1317
+
1318
+ logging.info(f"TON address saved for user {tg_user_id}: {ton_address}")
1319
+ return jsonify({"status": "ok", "message": "TON address saved successfully."}), 200
1320
+
1321
+ except Exception as e:
1322
+ logging.exception("Error in /save_ton_address endpoint")
1323
+ return jsonify({"status": "error", "message": "Internal server error"}), 500
1324
+
1325
+
1326
  @app.route('/get_ton_balance', methods=['POST'])
1327
  def get_ton_balance_route():
 
 
 
1328
  try:
1329
  req_data = request.get_json()
1330
+ tg_user_id = req_data.get('tg_user_id')
1331
 
1332
+ if not tg_user_id:
1333
+ return jsonify({"status": "error", "message": "Missing tg_user_id"}), 400
1334
 
1335
+ str_user_id = str(tg_user_id)
1336
+ current_data = load_visitor_data()
1337
 
1338
+ user_data = current_data.get(str_user_id)
1339
+
1340
+ if not user_data or 'ton_wallet_address' not in user_data or not user_data['ton_wallet_address']:
1341
+ logging.warning(f"TON address not found for user {tg_user_id} when attempting to fetch balance.")
1342
+ return jsonify({"status": "error", "message": "TON кошелек не привязан"}), 404
1343
+
1344
+ ton_address = user_data['ton_wallet_address']
1345
+
1346
+ balance, error_message = get_ton_balance(ton_address)
1347
+
1348
+ if balance is not None:
1349
+ logging.info(f"Fetched balance for user {tg_user_id} ({ton_address}): {balance} TON")
1350
+ return jsonify({"status": "ok", "balance": balance}), 200
1351
+ else:
1352
+ logging.error(f"Failed to fetch balance for user {tg_user_id} ({ton_address}): {error_message}")
1353
+ return jsonify({"status": "error", "message": error_message or "Не удалось получить баланс"}), 500 # Use 500 for API/internal errors
1354
 
1355
  except Exception as e:
1356
  logging.exception("Error in /get_ton_balance endpoint")
1357
+ return jsonify({"status": "error", "message": "Internal server error"}), 500
1358
 
1359
 
1360
  @app.route('/admin')
1361
  def admin_panel():
1362
+ # WARNING: This route is unprotected! Add proper authentication/authorization.
1363
  current_data = load_visitor_data()
1364
  users_list = list(current_data.values())
1365
  return render_template_string(ADMIN_TEMPLATE, users=users_list)
1366
 
1367
  @app.route('/admin/download_data', methods=['POST'])
1368
  def admin_trigger_download():
1369
+ # WARNING: Unprotected endpoint
1370
  success = download_data_from_hf()
1371
  if success:
1372
  return jsonify({"status": "ok", "message": "Скачивание данных с Hugging Face завершено. Страница будет обновлена."})
 
1375
 
1376
  @app.route('/admin/upload_data', methods=['POST'])
1377
  def admin_trigger_upload():
1378
+ # WARNING: Unprotected endpoint
1379
  if not HF_TOKEN_WRITE:
1380
  return jsonify({"status": "error", "message": "HF_TOKEN_WRITE не настроен на сервере."}), 400
1381
  upload_data_to_hf_async()
1382
  return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
1383
 
1384
+ @app.route('/tonconnect-manifest.json')
1385
+ def tonconnect_manifest():
1386
+ # This route serves the tonconnect manifest file required by tonconnect-ui
1387
+ app_url = request.url_root # Gets the base URL of the app
1388
+ return Response(
1389
+ render_template_string(TON_MANIFEST_TEMPLATE, app_url=app_url),
1390
+ mimetype='application/json'
1391
+ )
1392
+
1393
 
1394
+ # --- App Initialization ---
1395
  if __name__ == '__main__':
1396
  print("---")
1397
  print("--- MORSHEN GROUP MINI APP SERVER ---")
1398
  print("---")
1399
  print(f"Flask server starting on http://{HOST}:{PORT}")
1400
+ if BOT_TOKEN:
1401
+ print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
1402
+ else:
1403
+ print("--- WARNING: BOT_TOKEN not set. Telegram data verification disabled. ---")
1404
  print(f"Visitor data file: {DATA_FILE}")
1405
  print(f"Hugging Face Repo: {REPO_ID}")
1406
  print(f"HF Data Path: {HF_DATA_FILE_PATH}")
 
1415
  download_data_from_hf()
1416
 
1417
  if not TON_API_KEY:
1418
+ print("--- WARNING: TON_API_KEY not set. TON balance fetching disabled. ---")
1419
+ print("--- Set TON_API_KEY environment variable.")
 
 
1420
  else:
1421
+ print("--- TON_API_KEY found.")
1422
+
1423
 
1424
  load_visitor_data()
1425
 
 
1427
  print("--- SECURITY WARNING ---")
1428
  print("--- The /admin route and its sub-routes are NOT protected.")
1429
  print("--- Implement proper authentication before deploying.")
1430
+ print("--- Hardcoded default TON_API_KEY is used if environment variable is not set. This is INSECURE.")
1431
+ print("--- Always set TON_API_KEY via environment variables in production.")
1432
  print("---")
1433
 
1434
  if HF_TOKEN_WRITE: