Aleksmorshen commited on
Commit
adf3645
·
verified ·
1 Parent(s): d3dfd70

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +171 -228
app.py CHANGED
@@ -13,6 +13,7 @@ import logging
13
  import threading
14
  from huggingface_hub import HfApi, hf_hub_download
15
  from huggingface_hub.utils import RepositoryNotFoundError
 
16
 
17
  BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvbTVsqEJVG5SYK5hUlc_Ewo")
18
  HOST = '0.0.0.0'
@@ -83,11 +84,10 @@ def load_visitor_data():
83
  visitor_data_cache = {}
84
  return visitor_data_cache
85
 
86
- def save_visitor_data(data_to_update):
87
- global visitor_data_cache
88
  with _data_lock:
89
  try:
90
- visitor_data_cache.update(data_to_update)
91
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
92
  json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
93
  logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
@@ -157,7 +157,7 @@ def verify_telegram_data(init_data_str):
157
  auth_date = int(parsed_data.get('auth_date', [0])[0])
158
  current_time = int(time.time())
159
  if current_time - auth_date > 86400:
160
- logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}).")
161
  return parsed_data, True
162
  else:
163
  logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
@@ -416,12 +416,21 @@ TEMPLATE = """
416
  .icon-link::before { content: '🔗'; }
417
  .icon-leader::before { content: '🏆'; }
418
  .icon-company::before { content: '🏢'; }
419
- .icon-wallet::before { content: '💎'; } /* TON Wallet Icon */
420
-
421
- #ton-connect-button-container { margin-top: var(--padding-m); }
422
- #ton-wallet-info { margin-top: var(--padding-s); font-size: 0.9em; color: var(--text-secondary-color); text-align: center;}
423
- #ton-wallet-info b { color: var(--tg-theme-link-color); }
424
-
 
 
 
 
 
 
 
 
 
425
  @media (max-width: 480px) {
426
  .section-title { font-size: 1.8em; }
427
  .logo span { font-size: 1.4em; }
@@ -455,13 +464,23 @@ TEMPLATE = """
455
  <a href="#" class="btn contact-link" style="background: var(--accent-gradient-green); width: 100%; margin-top: var(--padding-s);">
456
  <i class="icon icon-contact"></i>Написать нам в Telegram
457
  </a>
458
-
459
- <div id="ton-connect-button-container">
460
- <button id="ton-connect-btn" class="btn" style="width: 100%; background: var(--tg-theme-button-color);"><i class="icon icon-wallet"></i>Подключить TON кошелек</button>
461
- </div>
462
- <div id="ton-wallet-info" style="display: none;">
463
- <p>TON кошелек: <b id="ton-wallet-address"></b></p>
 
 
 
 
 
 
 
 
 
464
  </div>
 
465
  </section>
466
 
467
  <section class="ecosystem-header">
@@ -568,9 +587,9 @@ TEMPLATE = """
568
  </div>
569
  </div>
570
 
 
571
  <script>
572
  const tg = window.Telegram.WebApp;
573
- let tonConnectUI;
574
 
575
  function applyTheme(themeParams) {
576
  const root = document.documentElement;
@@ -581,6 +600,7 @@ TEMPLATE = """
581
  root.style.setProperty('--tg-theme-button-color', themeParams.button_color || '#31a5f5');
582
  root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
583
  root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#1e1e1e');
 
584
  try {
585
  const bgColor = themeParams.bg_color || '#121212';
586
  const r = parseInt(bgColor.slice(1, 3), 16);
@@ -592,50 +612,61 @@ TEMPLATE = """
592
  }
593
  }
594
 
595
- function updateTonWalletUI(address) {
596
- const tonConnectBtnContainer = document.getElementById('ton-connect-button-container');
597
- const tonWalletInfoDiv = document.getElementById('ton-wallet-info');
598
- const tonWalletAddressEl = document.getElementById('ton-wallet-address');
599
-
600
- if (address) {
601
- tonConnectBtnContainer.style.display = 'none';
602
- tonWalletAddressEl.textContent = `${address.slice(0, 6)}...${address.slice(-4)}`;
603
- tonWalletInfoDiv.style.display = 'block';
604
- } else {
605
- tonConnectBtnContainer.style.display = 'block';
606
- tonWalletInfoDiv.style.display = 'none';
607
- }
608
- }
609
-
610
- async function connectTonWalletBackend(walletAddress) {
611
  try {
612
- const response = await fetch('/connect_ton_wallet', {
613
  method: 'POST',
614
  headers: {
615
  'Content-Type': 'application/json',
616
  'Accept': 'application/json'
617
  },
618
- body: JSON.stringify({ initData: tg.initData, walletAddress: walletAddress }),
619
  });
 
620
  if (!response.ok) {
621
- const errorData = await response.json().catch(() => ({}));
622
- throw new Error(`HTTP error ${response.status}: ${errorData.message || 'Failed to connect wallet on backend'}`);
623
  }
 
624
  const data = await response.json();
625
  if (data.status === 'ok') {
626
- console.log('TON Wallet connected successfully on backend.');
627
- updateTonWalletUI(walletAddress);
628
- tg.HapticFeedback.notificationOccurred('success');
629
  } else {
630
- throw new Error(data.message || 'Backend rejected TON wallet connection.');
631
  }
632
  } catch (error) {
633
- console.error('Error connecting TON wallet with backend:', error);
634
- tg.showAlert(`Ошибка подключения кошелька: ${error.message}`);
635
- // Revert UI if needed, or keep button visible for retry
636
  }
637
  }
638
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
 
640
  function setupTelegram() {
641
  if (!tg || !tg.initData) {
@@ -667,19 +698,12 @@ TEMPLATE = """
667
  .then(data => {
668
  if (data.status === 'ok' && data.verified) {
669
  console.log('Backend verification successful.');
670
- if (data.ton_wallet_address) {
671
- updateTonWalletUI(data.ton_wallet_address);
672
- } else {
673
- updateTonWalletUI(null);
674
- }
675
  } else {
676
  console.warn('Backend verification failed:', data.message);
677
- updateTonWalletUI(null);
678
  }
679
  })
680
  .catch(error => {
681
  console.error('Error sending initData for verification:', error);
682
- updateTonWalletUI(null);
683
  });
684
 
685
  const user = tg.initDataUnsafe?.user;
@@ -712,58 +736,21 @@ TEMPLATE = """
712
  tg.HapticFeedback.impactOccurred('light');
713
  }
714
  });
715
- closeBtn.addEventListener('click', () => { modal.style.display = "none"; });
 
 
 
 
716
  window.addEventListener('click', (event) => {
717
- if (event.target == modal) { modal.style.display = "none"; }
 
 
718
  });
719
  } else {
720
  console.error("Modal elements not found!");
721
  }
722
-
723
- // TON Connect UI Initialization
724
- // The manifest an absolute URL to the manifest.json
725
- const manifestUrl = new URL('/tonconnect-manifest.json', window.location.origin).toString();
726
- tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
727
- manifestUrl: manifestUrl,
728
- buttonRootId: 'ton-connect-button-container', // Optional: if you want SDK to render button
729
- actionsConfiguration: {
730
- twaReturnUrl: `https://t.me/${tg.WebApp. Gastgeberanwendung}/${tg.WebApp.Startparam}` // Optional
731
- }
732
- });
733
-
734
- // Handle TON Connect button click manually if not using buttonRootId or want custom button
735
- const tonConnectBtnManual = document.getElementById('ton-connect-btn');
736
- if (tonConnectBtnManual) {
737
- tonConnectBtnManual.addEventListener('click', async () => {
738
- try {
739
- const connectedWallet = await tonConnectUI.connectWallet();
740
- if (connectedWallet && connectedWallet.account && connectedWallet.account.address) {
741
- const rawAddress = TON_CONNECT_UI.Address.parse(connectedWallet.account.address).toString({ testOnly: false }); // mainnet address
742
- console.log('TON Wallet connected:', rawAddress);
743
- await connectTonWalletBackend(rawAddress);
744
- } else {
745
- console.warn('TON Wallet connection cancelled or failed.');
746
- }
747
- } catch (error) {
748
- console.error('Error during TON wallet connection process:', error);
749
- tg.showAlert('Не удалось подключить TON кошелек.');
750
- }
751
- });
752
- }
753
-
754
- // Subscribe to connection status changes (optional, good for advanced UI updates)
755
- tonConnectUI.onStatusChange(walletAndAccount => {
756
- if (walletAndAccount && walletAndAccount.account) {
757
- const rawAddress = TON_CONNECT_UI.Address.parse(walletAndAccount.account.address).toString({ testOnly: false });
758
- console.log('TON Connect status change, connected:', rawAddress);
759
- // connectTonWalletBackend(rawAddress); // Potentially connect here too, or rely on button click
760
- updateTonWalletUI(rawAddress);
761
- } else {
762
- console.log('TON Connect status change, disconnected.');
763
- updateTonWalletUI(null);
764
- }
765
- });
766
-
767
 
768
  document.body.style.visibility = 'visible';
769
  }
@@ -782,6 +769,7 @@ TEMPLATE = """
782
  }
783
  }, 3500);
784
  }
 
785
  </script>
786
  </body>
787
  </html>
@@ -825,7 +813,7 @@ ADMIN_TEMPLATE = """
825
  h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
826
  .user-grid {
827
  display: grid;
828
- grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); /* Wider cards */
829
  gap: var(--padding);
830
  margin-top: var(--padding);
831
  }
@@ -853,8 +841,8 @@ ADMIN_TEMPLATE = """
853
  }
854
  .user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
855
  .user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
856
- .user-card .details { font-size: 0.9em; color: #495057; word-break: break-all; width:100%; } /* break-all */
857
- .user-card .detail-item { margin-bottom: 0.3rem; text-align: left; padding-left: 10px; }
858
  .user-card .detail-item strong { color: var(--admin-text); }
859
  .user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
860
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
@@ -942,7 +930,6 @@ ADMIN_TEMPLATE = """
942
  <div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
943
  <div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
944
  <div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
945
- <div class="detail-item"><strong>TON Wallet:</strong> {{ user.ton_wallet_address or 'N/A' }}</div>
946
  </div>
947
  <div class="timestamp">Визит: {{ user.visited_at_str }}</div>
948
  </div>
@@ -981,8 +968,14 @@ ADMIN_TEMPLATE = """
981
  loader.style.display = 'none';
982
  }
983
  }
984
- function triggerDownload() { handleFetch('/admin/download_data', 'скачивание'); }
985
- function triggerUpload() { handleFetch('/admin/upload_data', 'загрузка'); }
 
 
 
 
 
 
986
  </script>
987
  </body>
988
  </html>
@@ -993,24 +986,6 @@ def index():
993
  theme_params = {}
994
  return render_template_string(TEMPLATE, theme=theme_params)
995
 
996
- @app.route('/tonconnect-manifest.json')
997
- def ton_manifest():
998
- # Ensure this URL is correctly pointing to your app's domain
999
- # For local dev, request.host_url should work. For production, ensure HOST is correct.
1000
- app_url = request.host_url.strip('/')
1001
- # Use a publicly accessible icon URL
1002
- icon_url = "https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg"
1003
-
1004
- manifest = {
1005
- "url": app_url,
1006
- "name": "Morshen Group Mini App",
1007
- "iconUrl": icon_url,
1008
- "termsOfUseUrl": f"{app_url}/terms-of-use", # Placeholder
1009
- "privacyPolicyUrl": f"{app_url}/privacy-policy" # Placeholder
1010
- }
1011
- return jsonify(manifest)
1012
-
1013
-
1014
  @app.route('/verify', methods=['POST'])
1015
  def verify_data():
1016
  try:
@@ -1020,6 +995,7 @@ def verify_data():
1020
  return jsonify({"status": "error", "message": "Missing initData"}), 400
1021
 
1022
  user_data_parsed, is_valid = verify_telegram_data(init_data_str)
 
1023
  user_info_dict = {}
1024
  if user_data_parsed and 'user' in user_data_parsed:
1025
  try:
@@ -1027,107 +1003,36 @@ def verify_data():
1027
  user_info_dict = json.loads(user_json_str)
1028
  except Exception as e:
1029
  logging.error(f"Could not parse user JSON: {e}")
1030
-
1031
- if is_valid and user_info_dict.get('id'):
1032
- user_id = user_info_dict['id']
1033
- user_id_str = str(user_id)
1034
- now = time.time()
1035
-
1036
- new_telegram_data = {
1037
- 'id': user_id,
1038
- 'first_name': user_info_dict.get('first_name'),
1039
- 'last_name': user_info_dict.get('last_name'),
1040
- 'username': user_info_dict.get('username'),
1041
- 'photo_url': user_info_dict.get('photo_url'),
1042
- 'language_code': user_info_dict.get('language_code'),
1043
- 'is_premium': user_info_dict.get('is_premium', False),
1044
- 'phone_number': user_info_dict.get('phone_number'),
1045
- 'visited_at': now,
1046
- 'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
1047
- }
1048
-
1049
- ton_wallet_address_to_return = None
1050
- with _data_lock:
1051
- current_user_data = visitor_data_cache.get(user_id_str, {})
1052
- current_user_data.update(new_telegram_data)
1053
- visitor_data_cache[user_id_str] = current_user_data
1054
- ton_wallet_address_to_return = current_user_data.get('ton_wallet_address')
1055
-
1056
- save_visitor_data({user_id_str: current_user_data})
1057
-
1058
- return jsonify({
1059
- "status": "ok",
1060
- "verified": True,
1061
- "user": user_info_dict,
1062
- "ton_wallet_address": ton_wallet_address_to_return
1063
- }), 200
1064
  else:
1065
- logging.warning(f"Verification failed for user: {user_info_dict.get('id') if user_info_dict else 'Unknown'}")
1066
  return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
1067
 
1068
  except Exception as e:
1069
  logging.exception("Error in /verify endpoint")
1070
  return jsonify({"status": "error", "message": "Internal server error"}), 500
1071
 
1072
- @app.route('/connect_ton_wallet', methods=['POST'])
1073
- def connect_ton_wallet():
1074
- try:
1075
- req_data = request.get_json()
1076
- init_data_str = req_data.get('initData')
1077
- wallet_address = req_data.get('walletAddress')
1078
-
1079
- if not init_data_str or not wallet_address:
1080
- return jsonify({"status": "error", "message": "Missing initData or walletAddress"}), 400
1081
-
1082
- user_data_parsed, is_valid = verify_telegram_data(init_data_str)
1083
- if not is_valid:
1084
- return jsonify({"status": "error", "message": "Invalid Telegram data"}), 403
1085
-
1086
- user_info_dict = {}
1087
- if user_data_parsed and 'user' in user_data_parsed:
1088
- try:
1089
- user_json_str = unquote(user_data_parsed['user'][0])
1090
- user_info_dict = json.loads(user_json_str)
1091
- except Exception as e:
1092
- logging.error(f"Could not parse user JSON during TON connect: {e}")
1093
- return jsonify({"status": "error", "message": "Failed to parse user data"}), 400
1094
-
1095
- user_id = user_info_dict.get('id')
1096
- if not user_id:
1097
- return jsonify({"status": "error", "message": "User ID not found in Telegram data"}), 400
1098
-
1099
- user_id_str = str(user_id)
1100
- now = time.time()
1101
-
1102
- with _data_lock:
1103
- user_record = visitor_data_cache.get(user_id_str)
1104
- if not user_record:
1105
- logging.info(f"User {user_id_str} not in cache, creating basic entry for TON connect.")
1106
- user_record = {
1107
- 'id': user_id,
1108
- 'first_name': user_info_dict.get('first_name'),
1109
- 'last_name': user_info_dict.get('last_name'),
1110
- 'username': user_info_dict.get('username'),
1111
- 'visited_at': now, # Or use a specific 'created_at'
1112
- 'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
1113
- }
1114
-
1115
- user_record['ton_wallet_address'] = wallet_address
1116
- user_record['ton_wallet_connected_at'] = now
1117
- user_record['ton_wallet_connected_at_str'] = datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
1118
-
1119
- visitor_data_cache[user_id_str] = user_record
1120
- data_to_save = {user_id_str: user_record}
1121
-
1122
- save_visitor_data(data_to_save)
1123
-
1124
- return jsonify({"status": "ok", "message": "TON wallet connected and saved successfully."}), 200
1125
-
1126
- except Exception as e:
1127
- logging.exception("Error in /connect_ton_wallet endpoint")
1128
- return jsonify({"status": "error", "message": "Internal server error"}), 500
1129
-
1130
-
1131
  @app.route('/admin')
1132
  def admin_panel():
1133
  current_data = load_visitor_data()
@@ -1149,28 +1054,66 @@ def admin_trigger_upload():
1149
  upload_data_to_hf_async()
1150
  return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
1151
 
1152
- # Placeholder routes for terms and privacy for tonconnect-manifest.json
1153
- @app.route('/terms-of-use')
1154
- def terms_of_use():
1155
- return "Terms of Use: To be defined.", 200
1156
-
1157
- @app.route('/privacy-policy')
1158
- def privacy_policy():
1159
- return "Privacy Policy: To be defined.", 200
1160
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1161
 
1162
  if __name__ == '__main__':
 
1163
  print("--- MORSHEN GROUP MINI APP SERVER ---")
 
1164
  print(f"Flask server starting on http://{HOST}:{PORT}")
1165
  print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
 
 
 
1166
  if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
1167
- print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET. Backup/restore limited. ---")
 
 
 
1168
  else:
1169
- print("--- Hugging Face tokens found. Attempting initial data download...")
 
1170
  download_data_from_hf()
1171
 
1172
  load_visitor_data()
1173
- print("--- SECURITY WARNING: /admin route and sub-routes are NOT protected. ---")
 
 
 
 
 
1174
 
1175
  if HF_TOKEN_WRITE:
1176
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
 
13
  import threading
14
  from huggingface_hub import HfApi, hf_hub_download
15
  from huggingface_hub.utils import RepositoryNotFoundError
16
+ import requests
17
 
18
  BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvbTVsqEJVG5SYK5hUlc_Ewo")
19
  HOST = '0.0.0.0'
 
84
  visitor_data_cache = {}
85
  return visitor_data_cache
86
 
87
+ def save_visitor_data(data):
 
88
  with _data_lock:
89
  try:
90
+ visitor_data_cache.update(data)
91
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
92
  json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
93
  logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
 
157
  auth_date = int(parsed_data.get('auth_date', [0])[0])
158
  current_time = int(time.time())
159
  if current_time - auth_date > 86400:
160
+ logging.warning(f"Telegram InitData is older than 1 hour (Auth Date: {auth_date}, Current: {current_time}).")
161
  return parsed_data, True
162
  else:
163
  logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
 
416
  .icon-link::before { content: '🔗'; }
417
  .icon-leader::before { content: '🏆'; }
418
  .icon-company::before { content: '🏢'; }
419
+ .icon-wallet::before { content: '💎'; }
420
+ .wallet-info-item {
421
+ font-size: 1em; color: var(--text-secondary-color); word-break: break-all;
422
+ margin-top: var(--padding-s);
423
+ }
424
+ .wallet-info-item strong {
425
+ color: var(--text-color);
426
+ }
427
+ .wallet-balance {
428
+ font-size: 1.8em; font-weight: 700; color: var(--tg-theme-link-color);
429
+ margin-top: var(--padding-s);
430
+ }
431
+ #ton-connect-button > button {
432
+ width: 100%;
433
+ }
434
  @media (max-width: 480px) {
435
  .section-title { font-size: 1.8em; }
436
  .logo span { font-size: 1.4em; }
 
464
  <a href="#" class="btn contact-link" style="background: var(--accent-gradient-green); width: 100%; margin-top: var(--padding-s);">
465
  <i class="icon icon-contact"></i>Написать нам в Telegram
466
  </a>
467
+ </section>
468
+
469
+ <section class="section-card">
470
+ <h2 class="section-title"><i class="icon icon-wallet"></i>TON Кошелек</h2>
471
+ <p class="description">
472
+ Подключите ваш TON кошелек, чтобы проверить баланс прямо в приложении.
473
+ </p>
474
+ <div id="wallet-info" style="display: none; margin-top: var(--padding-m);">
475
+ <div class="wallet-info-item">
476
+ <strong>Баланс:</strong>
477
+ <div class="wallet-balance"><span id="wallet-balance">...</span> TON</div>
478
+ </div>
479
+ <div class="wallet-info-item">
480
+ <strong>Адрес:</strong> <span id="wallet-address"></span>
481
+ </div>
482
  </div>
483
+ <div id="ton-connect-button" style="margin-top: var(--padding-m);"></div>
484
  </section>
485
 
486
  <section class="ecosystem-header">
 
587
  </div>
588
  </div>
589
 
590
+
591
  <script>
592
  const tg = window.Telegram.WebApp;
 
593
 
594
  function applyTheme(themeParams) {
595
  const root = document.documentElement;
 
600
  root.style.setProperty('--tg-theme-button-color', themeParams.button_color || '#31a5f5');
601
  root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
602
  root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#1e1e1e');
603
+
604
  try {
605
  const bgColor = themeParams.bg_color || '#121212';
606
  const r = parseInt(bgColor.slice(1, 3), 16);
 
612
  }
613
  }
614
 
615
+ async function fetchBalance(address) {
616
+ const balanceEl = document.getElementById('wallet-balance');
617
+ balanceEl.textContent = '...';
 
 
 
 
 
 
 
 
 
 
 
 
 
618
  try {
619
+ const response = await fetch('/ton_balance', {
620
  method: 'POST',
621
  headers: {
622
  'Content-Type': 'application/json',
623
  'Accept': 'application/json'
624
  },
625
+ body: JSON.stringify({ address: address }),
626
  });
627
+
628
  if (!response.ok) {
629
+ throw new Error(`HTTP error! status: ${response.status}`);
 
630
  }
631
+
632
  const data = await response.json();
633
  if (data.status === 'ok') {
634
+ balanceEl.textContent = data.balance;
 
 
635
  } else {
636
+ throw new Error(data.message || 'Failed to fetch balance');
637
  }
638
  } catch (error) {
639
+ console.error('Error fetching balance:', error);
640
+ balanceEl.textContent = 'Ошибка';
641
+ if (tg.HapticFeedback) tg.HapticFeedback.notificationOccurred('error');
642
  }
643
  }
644
 
645
+ function setupTonConnect() {
646
+ const tonConnectUI = new TonConnectUI.TonConnectUI({
647
+ manifestUrl: window.location.origin + '/tonconnect-manifest.json',
648
+ uiOptions: {
649
+ buttonRootId: 'ton-connect-button'
650
+ }
651
+ });
652
+
653
+ const walletInfoEl = document.getElementById('wallet-info');
654
+ const walletAddressEl = document.getElementById('wallet-address');
655
+
656
+ tonConnectUI.onStatusChange(wallet => {
657
+ if (wallet) {
658
+ const address = TonConnectUI.toUserFriendlyAddress(wallet.account.address);
659
+ walletAddressEl.textContent = `${address.slice(0, 6)}...${address.slice(-4)}`;
660
+ walletInfoEl.style.display = 'block';
661
+ fetchBalance(wallet.account.address);
662
+ } else {
663
+ walletInfoEl.style.display = 'none';
664
+ document.getElementById('wallet-balance').textContent = '...';
665
+ walletAddressEl.textContent = '';
666
+ }
667
+ });
668
+ }
669
+
670
 
671
  function setupTelegram() {
672
  if (!tg || !tg.initData) {
 
698
  .then(data => {
699
  if (data.status === 'ok' && data.verified) {
700
  console.log('Backend verification successful.');
 
 
 
 
 
701
  } else {
702
  console.warn('Backend verification failed:', data.message);
 
703
  }
704
  })
705
  .catch(error => {
706
  console.error('Error sending initData for verification:', error);
 
707
  });
708
 
709
  const user = tg.initDataUnsafe?.user;
 
736
  tg.HapticFeedback.impactOccurred('light');
737
  }
738
  });
739
+
740
+ closeBtn.addEventListener('click', () => {
741
+ modal.style.display = "none";
742
+ });
743
+
744
  window.addEventListener('click', (event) => {
745
+ if (event.target == modal) {
746
+ modal.style.display = "none";
747
+ }
748
  });
749
  } else {
750
  console.error("Modal elements not found!");
751
  }
752
+
753
+ setupTonConnect();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
 
755
  document.body.style.visibility = 'visible';
756
  }
 
769
  }
770
  }, 3500);
771
  }
772
+
773
  </script>
774
  </body>
775
  </html>
 
813
  h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
814
  .user-grid {
815
  display: grid;
816
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
817
  gap: var(--padding);
818
  margin-top: var(--padding);
819
  }
 
841
  }
842
  .user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
843
  .user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
844
+ .user-card .details { font-size: 0.9em; color: #495057; word-break: break-word; }
845
+ .user-card .detail-item { margin-bottom: 0.3rem; }
846
  .user-card .detail-item strong { color: var(--admin-text); }
847
  .user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
848
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
 
930
  <div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
931
  <div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
932
  <div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
 
933
  </div>
934
  <div class="timestamp">Визит: {{ user.visited_at_str }}</div>
935
  </div>
 
968
  loader.style.display = 'none';
969
  }
970
  }
971
+
972
+ function triggerDownload() {
973
+ handleFetch('/admin/download_data', 'скачивание');
974
+ }
975
+
976
+ function triggerUpload() {
977
+ handleFetch('/admin/upload_data', 'загрузка');
978
+ }
979
  </script>
980
  </body>
981
  </html>
 
986
  theme_params = {}
987
  return render_template_string(TEMPLATE, theme=theme_params)
988
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
989
  @app.route('/verify', methods=['POST'])
990
  def verify_data():
991
  try:
 
995
  return jsonify({"status": "error", "message": "Missing initData"}), 400
996
 
997
  user_data_parsed, is_valid = verify_telegram_data(init_data_str)
998
+
999
  user_info_dict = {}
1000
  if user_data_parsed and 'user' in user_data_parsed:
1001
  try:
 
1003
  user_info_dict = json.loads(user_json_str)
1004
  except Exception as e:
1005
  logging.error(f"Could not parse user JSON: {e}")
1006
+ user_info_dict = {}
1007
+
1008
+ if is_valid:
1009
+ user_id = user_info_dict.get('id')
1010
+ if user_id:
1011
+ now = time.time()
1012
+ user_entry = {
1013
+ str(user_id): {
1014
+ 'id': user_id,
1015
+ 'first_name': user_info_dict.get('first_name'),
1016
+ 'last_name': user_info_dict.get('last_name'),
1017
+ 'username': user_info_dict.get('username'),
1018
+ 'photo_url': user_info_dict.get('photo_url'),
1019
+ 'language_code': user_info_dict.get('language_code'),
1020
+ 'is_premium': user_info_dict.get('is_premium', False),
1021
+ 'phone_number': user_info_dict.get('phone_number'),
1022
+ 'visited_at': now,
1023
+ 'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
1024
+ }
1025
+ }
1026
+ save_visitor_data(user_entry)
1027
+ return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
 
 
 
 
 
 
 
 
 
 
 
 
1028
  else:
1029
+ logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
1030
  return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
1031
 
1032
  except Exception as e:
1033
  logging.exception("Error in /verify endpoint")
1034
  return jsonify({"status": "error", "message": "Internal server error"}), 500
1035
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1036
  @app.route('/admin')
1037
  def admin_panel():
1038
  current_data = load_visitor_data()
 
1054
  upload_data_to_hf_async()
1055
  return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
1056
 
1057
+ @app.route('/tonconnect-manifest.json')
1058
+ def tonconnect_manifest():
1059
+ return jsonify({
1060
+ "url": request.host_url.rstrip('/'),
1061
+ "name": "Morshen Group App",
1062
+ "iconUrl": "https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg"
1063
+ })
1064
+
1065
+ @app.route('/ton_balance', methods=['POST'])
1066
+ def get_ton_balance():
1067
+ req_data = request.get_json()
1068
+ address = req_data.get('address')
1069
+ if not address:
1070
+ return jsonify({"status": "error", "message": "Address is required"}), 400
1071
+
1072
+ try:
1073
+ api_url = f"https://toncenter.com/api/v2/getWalletInformation?address={address}"
1074
+ response = requests.get(api_url)
1075
+ response.raise_for_status()
1076
+ data = response.json()
1077
+
1078
+ if data.get("ok"):
1079
+ balance_nanotons = int(data["result"]["balance"])
1080
+ balance_ton = balance_nanotons / 1_000_000_000
1081
+ return jsonify({"status": "ok", "balance": f"{balance_ton:.4f}"})
1082
+ else:
1083
+ return jsonify({"status": "error", "message": "Failed to get balance from API"}), 500
1084
+ except requests.exceptions.RequestException as e:
1085
+ logging.error(f"Error fetching TON balance from Toncenter: {e}")
1086
+ return jsonify({"status": "error", "message": "Could not connect to TON API"}), 503
1087
+ except (KeyError, ValueError) as e:
1088
+ logging.error(f"Error parsing Toncenter response: {e}")
1089
+ return jsonify({"status": "error", "message": "Invalid API response from Toncenter"}), 500
1090
 
1091
  if __name__ == '__main__':
1092
+ print("---")
1093
  print("--- MORSHEN GROUP MINI APP SERVER ---")
1094
+ print("---")
1095
  print(f"Flask server starting on http://{HOST}:{PORT}")
1096
  print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
1097
+ print(f"Visitor data file: {DATA_FILE}")
1098
+ print(f"Hugging Face Repo: {REPO_ID}")
1099
+ print(f"HF Data Path: {HF_DATA_FILE_PATH}")
1100
  if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
1101
+ print("---")
1102
+ print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---")
1103
+ print("--- Backup/restore functionality will be limited. Set HF_TOKEN_READ and HF_TOKEN_WRITE environment variables.")
1104
+ print("---")
1105
  else:
1106
+ print("--- Hugging Face tokens found.")
1107
+ print("--- Attempting initial data download from Hugging Face...")
1108
  download_data_from_hf()
1109
 
1110
  load_visitor_data()
1111
+
1112
+ print("---")
1113
+ print("--- SECURITY WARNING ---")
1114
+ print("--- The /admin route and its sub-routes are NOT protected.")
1115
+ print("--- Implement proper authentication before deploying.")
1116
+ print("---")
1117
 
1118
  if HF_TOKEN_WRITE:
1119
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)