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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +56 -98
app.py CHANGED
@@ -1,12 +1,11 @@
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
@@ -16,30 +15,26 @@ 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
@@ -116,10 +111,10 @@ def upload_data_to_hf():
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.")
@@ -150,7 +145,6 @@ def periodic_backup():
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.")
@@ -173,7 +167,6 @@ def verify_telegram_data(init_data_str):
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
@@ -184,7 +177,6 @@ def verify_telegram_data(init_data_str):
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.")
@@ -199,7 +191,7 @@ def get_ton_balance(address):
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:
@@ -208,13 +200,12 @@ def get_ton_balance(address):
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}")
@@ -223,8 +214,6 @@ def get_ton_balance(address):
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">
@@ -249,7 +238,7 @@ TEMPLATE = """
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,16 +246,16 @@ TEMPLATE = """
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,11 +267,11 @@ TEMPLATE = """
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,7 +290,7 @@ TEMPLATE = """
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,12 +298,12 @@ TEMPLATE = """
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;
@@ -348,7 +337,7 @@ TEMPLATE = """
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);
@@ -362,14 +351,14 @@ TEMPLATE = """
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,7 +367,7 @@ TEMPLATE = """
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,7 +387,7 @@ TEMPLATE = """
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,11 +403,11 @@ TEMPLATE = """
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,22 +416,22 @@ TEMPLATE = """
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;
@@ -474,12 +463,10 @@ TEMPLATE = """
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;
@@ -507,7 +494,6 @@ TEMPLATE = """
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: '🌐'; }
@@ -532,7 +518,6 @@ TEMPLATE = """
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; }
@@ -575,7 +560,7 @@ TEMPLATE = """
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>
@@ -682,7 +667,6 @@ TEMPLATE = """
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>
@@ -735,7 +719,6 @@ TEMPLATE = """
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',
@@ -754,7 +737,6 @@ TEMPLATE = """
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 {
@@ -770,8 +752,6 @@ TEMPLATE = """
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,7 +760,6 @@ TEMPLATE = """
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");
@@ -822,7 +801,7 @@ TEMPLATE = """
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
 
@@ -839,7 +818,6 @@ TEMPLATE = """
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(); }
@@ -851,7 +829,6 @@ TEMPLATE = """
851
  onClick: () => { tonConnectUI.connectWallet(); }
852
  });
853
 
854
- // Initial check in case wallet is already connected
855
  updateWalletInfo(tonConnectUI.wallet, userId);
856
 
857
  } catch (e) {
@@ -872,7 +849,6 @@ TEMPLATE = """
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';
@@ -881,22 +857,18 @@ TEMPLATE = """
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
 
@@ -947,7 +919,7 @@ TEMPLATE = """
947
  }
948
 
949
  balanceDisplay.textContent = 'Загрузка...';
950
- fetchBalanceBtn.disabled = true; // Disable button during fetch
951
 
952
  try {
953
  const response = await fetch('/get_ton_balance', {
@@ -958,12 +930,12 @@ TEMPLATE = """
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) {
@@ -971,11 +943,10 @@ TEMPLATE = """
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);
@@ -984,7 +955,6 @@ TEMPLATE = """
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();
@@ -1074,7 +1044,7 @@ ADMIN_TEMPLATE = """
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,7 +1073,6 @@ ADMIN_TEMPLATE = """
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,7 +1102,7 @@ ADMIN_TEMPLATE = """
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,7 +1131,7 @@ ADMIN_TEMPLATE = """
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>
@@ -1195,7 +1164,7 @@ ADMIN_TEMPLATE = """
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 || 'Произошла ошибка');
@@ -1229,7 +1198,6 @@ TON_MANIFEST_TEMPLATE = """
1229
  }
1230
  """
1231
 
1232
- # --- Flask Routes ---
1233
  @app.route('/')
1234
  def index():
1235
  theme_params = {}
@@ -1262,7 +1230,6 @@ def verify_data():
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
 
@@ -1278,7 +1245,7 @@ def verify_data():
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)
@@ -1306,13 +1273,10 @@ def save_ton_address():
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}")
@@ -1350,7 +1314,7 @@ def get_ton_balance_route():
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")
@@ -1359,14 +1323,12 @@ def get_ton_balance_route():
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,7 +1337,6 @@ def admin_trigger_download():
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()
@@ -1383,15 +1344,12 @@ def admin_trigger_upload():
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 ---")
 
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
 
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
 
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.")
 
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.")
 
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
 
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.")
 
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:
 
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}")
 
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">
 
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
  --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
  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
  }
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
  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;
 
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);
 
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
  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
  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
  }
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
  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;
 
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
  .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: '🌐'; }
 
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; }
 
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>
 
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>
 
719
  applyTheme(tg.themeParams);
720
  tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
721
 
 
722
  try {
723
  const response = await fetch('/verify', {
724
  method: 'POST',
 
737
  const name = data.user.first_name || data.user.username || 'Гость';
738
  document.getElementById('greeting').textContent = `Добро пожаловать, ${name}! 👋`;
739
 
 
740
  setupTonConnect(telegramUserId);
741
 
742
  } else {
 
752
  document.getElementById('greeting').textContent = 'Добро пожаловать! (Ошибка сервера)';
753
  }
754
 
 
 
755
  const contactButtons = document.querySelectorAll('.contact-link');
756
  contactButtons.forEach(button => {
757
  button.addEventListener('click', (e) => {
 
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");
 
801
  tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
802
  manifestUrl: window.location.origin + '/tonconnect-manifest.json',
803
  connector: {
804
+ timeout: 10000
805
  }
806
  });
807
 
 
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(); }
 
829
  onClick: () => { tonConnectUI.connectWallet(); }
830
  });
831
 
 
832
  updateWalletInfo(tonConnectUI.wallet, userId);
833
 
834
  } catch (e) {
 
849
 
850
  if (wallet) {
851
  console.log("Wallet connected:", wallet);
 
852
  if (tonConnectButtonContainer) tonConnectButtonContainer.style.display = 'none';
853
 
854
  connectStatusText.style.display = 'none';
 
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
 
 
919
  }
920
 
921
  balanceDisplay.textContent = 'Загрузка...';
922
+ fetchBalanceBtn.disabled = true;
923
 
924
  try {
925
  const response = await fetch('/get_ton_balance', {
 
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) {
 
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);
 
955
  }
956
  });
957
 
 
958
  document.getElementById('disconnect-wallet-btn').addEventListener('click', () => {
959
  if (tonConnectUI) {
960
  tonConnectUI.disconnect();
 
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
  }
1074
  .refresh-btn:hover { background-color: #0b5ed7; }
1075
 
 
1076
  .admin-controls {
1077
  background: var(--admin-card-bg);
1078
  padding: var(--padding);
 
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
  {% 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>
 
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 || 'Произошла ошибка');
 
1198
  }
1199
  """
1200
 
 
1201
  @app.route('/')
1202
  def index():
1203
  theme_params = {}
 
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
 
 
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)
 
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}")
 
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")
 
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
 
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()
 
1344
 
1345
  @app.route('/tonconnect-manifest.json')
1346
  def tonconnect_manifest():
1347
+ app_url = request.url_root
 
1348
  return Response(
1349
  render_template_string(TON_MANIFEST_TEMPLATE, app_url=app_url),
1350
  mimetype='application/json'
1351
  )
1352
 
 
 
1353
  if __name__ == '__main__':
1354
  print("---")
1355
  print("--- MORSHEN GROUP MINI APP SERVER ---")