Aleksmorshen commited on
Commit
dcb96db
·
verified ·
1 Parent(s): 0db68ac

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +418 -231
app.py CHANGED
@@ -13,8 +13,10 @@ 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:AAGiG4MaTZZvvtVsqEJVG5SYK5hUlc_Ewo")
 
18
  HOST = '0.0.0.0'
19
  PORT = 7860
20
  DATA_FILE = 'data.json'
@@ -24,6 +26,8 @@ HF_DATA_FILE_PATH = "data.json"
24
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
25
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
26
 
 
 
27
  app = Flask(__name__)
28
  logging.basicConfig(level=logging.INFO)
29
  app.secret_key = os.urandom(24)
@@ -31,10 +35,21 @@ app.secret_key = os.urandom(24)
31
  _data_lock = threading.Lock()
32
  visitor_data_cache = {}
33
 
 
 
 
 
 
 
 
 
 
 
 
34
  def download_data_from_hf():
35
  global visitor_data_cache
36
  if not HF_TOKEN_READ:
37
- logging.warning("HF_TOKEN_READ not set.")
38
  return False
39
  try:
40
  logging.info(f"Attempting to download {HF_DATA_FILE_PATH} from {REPO_ID}...")
@@ -83,12 +98,14 @@ def load_visitor_data():
83
  visitor_data_cache = {}
84
  return visitor_data_cache
85
 
86
- def save_visitor_data(data):
87
  with _data_lock:
88
  try:
89
- visitor_data_cache.update(data)
 
 
90
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
91
- json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
92
  logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
93
  upload_data_to_hf_async()
94
  except Exception as e:
@@ -173,7 +190,7 @@ TEMPLATE = """
173
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
174
  <title>Morshen Group</title>
175
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
176
- <script src="https://unpkg.com/@tonconnect/ui@latest/dist/tonconnect-ui.min.js"></script>
177
  <link rel="preconnect" href="https://fonts.googleapis.com">
178
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
179
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
@@ -194,6 +211,7 @@ TEMPLATE = """
194
  --text-secondary-color: var(--tg-theme-hint-color);
195
  --accent-gradient: linear-gradient(95deg, var(--tg-theme-button-color, #007aff), #5856d6);
196
  --accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
 
197
  --tag-bg: rgba(255, 255, 255, 0.1);
198
  --border-radius-s: 8px;
199
  --border-radius-m: 14px;
@@ -274,6 +292,14 @@ TEMPLATE = """
274
  background: rgba(44, 44, 46, 0.95);
275
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), var(--shadow-medium);
276
  }
 
 
 
 
 
 
 
 
277
  .tag {
278
  display: inline-block; background: var(--tag-bg); color: var(--text-secondary-color);
279
  padding: 6px 12px; border-radius: var(--border-radius-s); font-size: 0.85em;
@@ -366,6 +392,59 @@ TEMPLATE = """
366
  }
367
  .save-card-button i { font-size: 1.2em; }
368
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  .modal {
370
  display: none; position: fixed; z-index: 1001;
371
  left: 0; top: 0; width: 100%; height: 100%;
@@ -397,37 +476,7 @@ TEMPLATE = """
397
  .modal-text b { color: var(--tg-theme-link-color); font-weight: 600; }
398
  .modal-instruction { font-size: 1em; color: var(--text-secondary-color); margin-top: var(--padding-m); }
399
 
400
- .ton-section {
401
- margin-top: var(--padding-l);
402
- text-align: center;
403
- padding: var(--padding-l);
404
- border-radius: var(--border-radius-l);
405
- background-color: var(--card-bg);
406
- box-shadow: var(--shadow-medium);
407
- border: 1px solid rgba(255, 255, 255, 0.08);
408
- backdrop-filter: blur(var(--backdrop-blur));
409
- -webkit-backdrop-filter: blur(var(--backdrop-blur));
410
- }
411
- .ton-section h3 {
412
- font-size: 1.5em;
413
- font-weight: 700;
414
- margin-bottom: var(--padding-s);
415
- color: var(--text-color);
416
- }
417
- .ton-connect-widget-container {
418
- margin-top: var(--padding-m);
419
- }
420
- .ton-info {
421
- margin-top: var(--padding-m);
422
- font-size: 1.1em;
423
- color: var(--text-secondary-color);
424
- }
425
- .ton-info div { margin-bottom: var(--padding-s); word-break: break-all; }
426
- .ton-info strong { color: var(--text-color); font-weight: 600; }
427
- .ton-address { color: var(--tg-theme-link-color); font-weight: 500;}
428
- .ton-balance { color: var(--accent-gradient-green, #34c759); font-weight: 600; }
429
-
430
-
431
  .icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
432
  .icon-save::before { content: '💾'; }
433
  .icon-web::before { content: '🌐'; }
@@ -448,8 +497,10 @@ TEMPLATE = """
448
  .icon-link::before { content: '🔗'; }
449
  .icon-leader::before { content: '🏆'; }
450
  .icon-company::before { content: '🏢'; }
451
- .icon-ton::before { content: '💎'; }
452
 
 
 
453
  @media (max-width: 480px) {
454
  .section-title { font-size: 1.8em; }
455
  .logo span { font-size: 1.4em; }
@@ -458,8 +509,8 @@ TEMPLATE = """
458
  .stats-grid { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: var(--padding-s); }
459
  .stat-value { font-size: 1.5em; }
460
  .modal-content { margin: 25% auto; width: 92%; }
461
- .ton-section h3 { font-size: 1.3em; }
462
- .ton-info { font-size: 1em; }
463
  }
464
  </style>
465
  </head>
@@ -487,6 +538,19 @@ TEMPLATE = """
487
  </a>
488
  </section>
489
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  <section class="ecosystem-header">
491
  <h2 class="section-title"><i class="icon icon-company"></i>Экосистема инноваций</h2>
492
  <p class="description">
@@ -571,18 +635,6 @@ TEMPLATE = """
571
  </div>
572
  </section>
573
 
574
- <section class="ton-section">
575
- <h3><i class="icon icon-ton"></i> TON Wallet</h3>
576
- <p class="description" style="margin-bottom: var(--padding-m); font-size: 1em;">Подключите свой TON кошелек для авторизации и просмотра баланса.</p>
577
- <div id="ton-connect-ui" class="ton-connect-widget-container"></div>
578
- <div class="ton-info">
579
- <div id="ton-status">Статус: Не подключен</div>
580
- <div id="ton-address" style="display: none;">Адрес: <span class="ton-address"></span></div>
581
- <div id="ton-balance" style="display: none;">Баланс: <span class="ton-balance"></span></div>
582
- </div>
583
- </section>
584
-
585
-
586
  <footer class="footer-greeting">
587
  <p id="greeting">Инициализация...</p>
588
  </footer>
@@ -593,6 +645,7 @@ TEMPLATE = """
593
  <i class="icon icon-save"></i>Сохранить визитку
594
  </button>
595
 
 
596
  <div id="saveModal" class="modal">
597
  <div class="modal-content">
598
  <span class="modal-close" id="modal-close-btn">×</span>
@@ -603,6 +656,7 @@ TEMPLATE = """
603
  </div>
604
  </div>
605
 
 
606
  <script>
607
  const tg = window.Telegram.WebApp;
608
 
@@ -627,28 +681,135 @@ TEMPLATE = """
627
  }
628
  }
629
 
630
- let tonConnectUI = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
 
632
- async function fetchTonBalance(address) {
633
- const tonCenterUrl = `https://toncenter.com/api/v3/account/getBalance?address=${address}`;
634
- try {
635
- const response = await fetch(tonCenterUrl);
636
- if (!response.ok) {
637
- throw new Error(`HTTP error! status: ${response.status}`);
638
- }
639
- const data = await response.json();
640
- if (data.error) {
641
- throw new Error(data.error);
642
- }
643
- // Balance is in NanoTONs, convert to TON
644
- const balanceNano = BigInt(data.result.balance);
645
- const balanceTon = Number(balanceNano) / 1e9;
646
- return balanceTon.toFixed(2); // Format to 2 decimal places
647
- } catch (error) {
648
- console.error("Error fetching TON balance:", error);
649
- return "Ошибка";
650
- }
651
- }
652
 
653
  function setupTelegram() {
654
  if (!tg || !tg.initData) {
@@ -656,6 +817,7 @@ TEMPLATE = """
656
  const greetingElement = document.getElementById('greeting');
657
  if(greetingElement) greetingElement.textContent = 'Не удалось связаться с Telegram.';
658
  document.body.style.visibility = 'visible';
 
659
  return;
660
  }
661
 
@@ -680,12 +842,18 @@ TEMPLATE = """
680
  .then(data => {
681
  if (data.status === 'ok' && data.verified) {
682
  console.log('Backend verification successful.');
 
 
683
  } else {
684
  console.warn('Backend verification failed:', data.message);
 
 
685
  }
686
  })
687
  .catch(error => {
688
  console.error('Error sending initData for verification:', error);
 
 
689
  });
690
 
691
 
@@ -733,87 +901,9 @@ TEMPLATE = """
733
  console.error("Modal elements not found!");
734
  }
735
 
736
- setupTonConnect();
737
-
738
- document.body.style.visibility = 'visible';
739
  }
740
 
741
- function setupTonConnect() {
742
- if (!window.TonConnectUI) {
743
- console.error("TonConnectUI script not loaded.");
744
- document.getElementById('ton-status').textContent = 'Ошибка загрузки TonConnect.';
745
- return;
746
- }
747
-
748
- try {
749
- tonConnectUI = new window.TonConnectUI({
750
- manifestUrl: window.location.origin + '/tonconnect-manifest.json',
751
- uiPreferences: {
752
- colorsSet: tg.themeParams.bg_color ? 'STANDARD' : 'DARK', // Use standard or dark based on TG theme
753
- // You can customize colors further based on tg.themeParams if needed
754
- // colors: {
755
- // dark: { ... },
756
- // light: { ... }
757
- // }
758
- },
759
- });
760
-
761
- tonConnectUI.onStatusChange(async wallet => {
762
- const tonStatusElement = document.getElementById('ton-status');
763
- const tonAddressElement = document.getElementById('ton-address');
764
- const tonBalanceElement = document.getElementById('ton-balance');
765
-
766
- if (wallet) {
767
- const address = wallet.account.address;
768
- tonStatusElement.textContent = 'Статус: Подключен';
769
- tonAddressElement.style.display = 'block';
770
- tonAddressElement.querySelector('span').textContent = address;
771
-
772
- tonBalanceElement.style.display = 'block';
773
- tonBalanceElement.querySelector('span').textContent = 'Загрузка...';
774
-
775
- const balance = await fetchTonBalance(address);
776
- tonBalanceElement.querySelector('span').textContent = balance + ' TON';
777
-
778
- console.log('TON Wallet connected:', address);
779
- console.log('TON Account:', wallet.account);
780
- // You might want to send this address to the backend here
781
- // to link it to the Telegram user ID if needed for your application logic.
782
-
783
- } else {
784
- tonStatusElement.textContent = 'Статус: Не подключен';
785
- tonAddressElement.style.display = 'none';
786
- tonBalanceElement.style.display = 'none';
787
- console.log('TON Wallet disconnected.');
788
- }
789
- });
790
-
791
- // Render the TonConnect button/widget
792
- const tonConnectUiContainer = document.getElementById('ton-connect-ui');
793
- if (tonConnectUiContainer) {
794
- tonConnectUI.renderButtons(tonConnectUiContainer, {
795
- messages: {
796
- connectingModalWithoutWallets: 'Пожалуйста, установите кошелек, поддерживающий TonConnect.',
797
- connectingModalTitle: 'Подключение кошелька TON',
798
- walletConnectRequestTemporarilyUnavailable: 'Запрос подключения временно недоступен. Попробуйте позже.',
799
- },
800
- labels: {
801
- disconnect: 'Отключить кошелек',
802
- connectWallet: 'Подключить TON Кошелек',
803
- connectedWallet: 'Подключен {{walletName}}',
804
- }
805
- });
806
- } else {
807
- console.error("TON Connect UI container not found!");
808
- }
809
-
810
- } catch (e) {
811
- console.error("Error initializing TonConnectUI:", e);
812
- document.getElementById('ton-status').textContent = 'Ошибка инициализации TonConnect.';
813
- }
814
- }
815
-
816
-
817
  if (window.Telegram && window.Telegram.WebApp) {
818
  setupTelegram();
819
  } else {
@@ -825,7 +915,7 @@ TEMPLATE = """
825
  const greetingElement = document.getElementById('greeting');
826
  if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
827
  document.body.style.visibility = 'visible';
828
- setupTonConnect(); // Still try to setup TON even without TG
829
  }
830
  }, 3500);
831
  }
@@ -901,10 +991,17 @@ ADMIN_TEMPLATE = """
901
  }
902
  .user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
903
  .user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
904
- .user-card .username a { color: inherit; text-decoration: none; }
905
- .user-card .details { font-size: 0.9em; color: #495057; word-break: break-word; }
906
  .user-card .detail-item { margin-bottom: 0.3rem; }
907
- .user-card .detail-item strong { color: var(--admin-text); }
 
 
 
 
 
 
 
 
908
  .user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
909
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
910
  .alert {
@@ -927,6 +1024,7 @@ ADMIN_TEMPLATE = """
927
  }
928
  .refresh-btn:hover { background-color: #0b5ed7; }
929
 
 
930
  .admin-controls {
931
  background: var(--admin-card-bg);
932
  padding: var(--padding);
@@ -983,7 +1081,7 @@ ADMIN_TEMPLATE = """
983
  <img src="{{ user.photo_url if user.photo_url else 'data:image/svg+xml;charset=UTF-8,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27%3e%3crect width=%27100%27 height=%27100%27 fill=%27%23e9ecef%27/%3e%3ctext x=%2750%25%27 y=%2755%25%27 dominant-baseline=%27middle%27 text-anchor=%27middle%27 font-size=%2745%27 font-family=%27sans-serif%27 fill=%27%23adb5bd%27%3e?%3c/text%3e%3c/svg%3e' }}" alt="User Avatar">
984
  <div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
985
  {% if user.username %}
986
- <div class="username"><a href="https://t.me/{{ user.username }}" target="_blank">@{{ user.username }}</a></div>
987
  {% else %}
988
  <div class="username" style="height: 1.3em;"></div>
989
  {% endif %}
@@ -992,9 +1090,14 @@ ADMIN_TEMPLATE = """
992
  <div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
993
  <div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
994
  <div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
995
- {% if user.ton_address %}
996
- <div class="detail-item"><strong>TON:</strong> {{ user.ton_address }}</div>
997
- {% endif %}
 
 
 
 
 
998
  </div>
999
  <div class="timestamp">Визит: {{ user.visited_at_str }}</div>
1000
  </div>
@@ -1046,19 +1149,22 @@ ADMIN_TEMPLATE = """
1046
  </html>
1047
  """
1048
 
1049
- TON_MANIFEST = {
1050
- "url": os.getenv("APP_BASE_URL", "https://localhost:7860") + "/",
1051
- "name": "Morshen Group TMA",
1052
- "iconUrl": os.getenv("APP_BASE_URL", "https://localhost:7860") + "/static/morshengroup.jpg", # Or a publicly hosted image
1053
- "termsOfServiceUrl": "https://example.com/terms", # Replace with your actual terms
1054
- "privacyPolicyUrl": "https://example.com/privacy" # Replace with your actual privacy policy
1055
- }
1056
-
1057
  @app.route('/')
1058
  def index():
1059
  theme_params = {}
1060
  return render_template_string(TEMPLATE, theme=theme_params)
1061
 
 
 
 
 
 
 
 
 
 
 
 
1062
  @app.route('/verify', methods=['POST'])
1063
  def verify_data():
1064
  try:
@@ -1077,48 +1183,129 @@ def verify_data():
1077
  user_info_dict = json.loads(user_json_str)
1078
  user_id = user_info_dict.get('id')
1079
  except Exception as e:
1080
- logging.error(f"Could not parse user JSON: {e}")
1081
  user_info_dict = {}
1082
 
1083
  if is_valid and user_id:
1084
  now = time.time()
1085
- # Load current data to potentially merge
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1086
  current_data = load_visitor_data()
1087
- user_key = str(user_id)
1088
-
1089
- # Get existing user data if any, and update it
1090
- existing_user_entry = current_data.get(user_key, {})
1091
-
1092
- # Update with latest TG data
1093
- updated_user_entry = {
1094
- 'id': user_id,
1095
- 'first_name': user_info_dict.get('first_name', existing_user_entry.get('first_name')),
1096
- 'last_name': user_info_dict.get('last_name', existing_user_entry.get('last_name')),
1097
- 'username': user_info_dict.get('username', existing_user_entry.get('username')),
1098
- 'photo_url': user_info_dict.get('photo_url', existing_user_entry.get('photo_url')),
1099
- 'language_code': user_info_dict.get('language_code', existing_user_entry.get('language_code')),
1100
- 'is_premium': user_info_dict.get('is_premium', existing_user_entry.get('is_premium', False)),
1101
- 'phone_number': user_info_dict.get('phone_number', existing_user_entry.get('phone_number')),
1102
- 'visited_at': now,
1103
- 'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
1104
  }
 
1105
 
1106
- # Keep existing TON address if present
1107
- if 'ton_address' in existing_user_entry:
1108
- updated_user_entry['ton_address'] = existing_user_entry['ton_address']
 
1109
 
1110
- # Save the updated entry
1111
- save_visitor_data({user_key: updated_user_entry})
 
1112
 
1113
- return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1114
  else:
1115
- logging.warning(f"Verification failed or user ID missing for data: {user_info_dict}")
1116
- return jsonify({"status": "error", "verified": False, "message": "Invalid data or missing user ID"}), 403
1117
 
1118
  except Exception as e:
1119
- logging.exception("Error in /verify endpoint")
1120
  return jsonify({"status": "error", "message": "Internal server error"}), 500
1121
 
 
1122
  @app.route('/admin')
1123
  def admin_panel():
1124
  current_data = load_visitor_data()
@@ -1140,37 +1327,15 @@ def admin_trigger_upload():
1140
  upload_data_to_hf_async()
1141
  return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
1142
 
1143
- @app.route('/tonconnect-manifest.json')
1144
- def tonconnect_manifest():
1145
- manifest_url = os.getenv("APP_BASE_URL")
1146
- if not manifest_url:
1147
- # Fallback for local testing or if env var is not set
1148
- manifest_url = f"http://{request.host}"
1149
- logging.warning(f"APP_BASE_URL not set. Using {manifest_url} for manifest.")
1150
-
1151
- manifest = {
1152
- "url": manifest_url + "/",
1153
- "name": "Morshen Group TMA",
1154
- "iconUrl": manifest_url + "/static/morshengroup.jpg",
1155
- "termsOfServiceUrl": manifest_url + "/terms", # Placeholder
1156
- "privacyPolicyUrl": manifest_url + "/privacy" # Placeholder
1157
- }
1158
- return jsonify(manifest)
1159
-
1160
- @app.route('/static/morshengroup.jpg')
1161
- def serve_logo():
1162
- # This route serves the logo locally if needed,
1163
- # but the HF link in the template is likely better.
1164
- # If you put morshengroup.jpg in a 'static' folder next to app.py,
1165
- # Flask can serve it. For Hugging Face Spaces, serving static files
1166
- # from the repo path directly via URL (like in the template) is common.
1167
- # Keeping this route as a placeholder or for local testing.
1168
- try:
1169
- # Attempt to serve from local static folder
1170
- return app.send_static_file('morshengroup.jpg')
1171
- except FileNotFoundError:
1172
- # Fallback or error if file isn't there
1173
- return "Logo not found", 404
1174
 
1175
 
1176
  if __name__ == '__main__':
@@ -1182,10 +1347,6 @@ if __name__ == '__main__':
1182
  print(f"Visitor data file: {DATA_FILE}")
1183
  print(f"Hugging Face Repo: {REPO_ID}")
1184
  print(f"HF Data Path: {HF_DATA_FILE_PATH}")
1185
- if os.getenv("APP_BASE_URL"):
1186
- print(f"App Base URL (for manifest): {os.getenv('APP_BASE_URL')}")
1187
- else:
1188
- print("APP_BASE_URL not set. Manifest URL will be based on request host.")
1189
  if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
1190
  print("---")
1191
  print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---")
@@ -1196,6 +1357,15 @@ if __name__ == '__main__':
1196
  print("--- Attempting initial data download from Hugging Face...")
1197
  download_data_from_hf()
1198
 
 
 
 
 
 
 
 
 
 
1199
  load_visitor_data()
1200
 
1201
  print("---")
@@ -1211,5 +1381,22 @@ if __name__ == '__main__':
1211
  else:
1212
  print("--- Periodic backup disabled (HF_TOKEN_WRITE missing).")
1213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1214
  print("--- Server Ready ---")
 
1215
  app.run(host=HOST, port=PORT, debug=False)
 
13
  import threading
14
  from huggingface_hub import HfApi, hf_hub_download
15
  from huggingface_hub.utils import RepositoryNotFoundError
16
+ from pytonapi import Tonapi
17
 
18
+
19
+ BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvtTVsqEJVG5SYK5hUlc_Ewo")
20
  HOST = '0.0.0.0'
21
  PORT = 7860
22
  DATA_FILE = 'data.json'
 
26
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
27
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
28
 
29
+ TONAPI_KEY = os.getenv("TONAPI_KEY")
30
+
31
  app = Flask(__name__)
32
  logging.basicConfig(level=logging.INFO)
33
  app.secret_key = os.urandom(24)
 
35
  _data_lock = threading.Lock()
36
  visitor_data_cache = {}
37
 
38
+ tonapi = None
39
+ if TONAPI_KEY:
40
+ try:
41
+ tonapi = Tonapi(api_key=TONAPI_KEY)
42
+ logging.info("TONAPI initialized.")
43
+ except Exception as e:
44
+ logging.error(f"Failed to initialize TONAPI: {e}")
45
+ else:
46
+ logging.warning("TONAPI_KEY not set. TON wallet functionality will be limited.")
47
+
48
+
49
  def download_data_from_hf():
50
  global visitor_data_cache
51
  if not HF_TOKEN_READ:
52
+ logging.warning("HF_TOKEN_READ not set. Skipping Hugging Face download.")
53
  return False
54
  try:
55
  logging.info(f"Attempting to download {HF_DATA_FILE_PATH} from {REPO_ID}...")
 
98
  visitor_data_cache = {}
99
  return visitor_data_cache
100
 
101
+ def save_visitor_data(data_entry):
102
  with _data_lock:
103
  try:
104
+ current_data = visitor_data_cache.copy()
105
+ current_data.update(data_entry)
106
+ visitor_data_cache.update(data_entry)
107
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
108
+ json.dump(current_data, f, ensure_ascii=False, indent=4)
109
  logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
110
  upload_data_to_hf_async()
111
  except Exception as e:
 
190
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
191
  <title>Morshen Group</title>
192
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
193
+ <script src="https://unpkg.com/@tonconnect/sdk@2.0.0/dist/tonconnect-sdk.min.js"></script>
194
  <link rel="preconnect" href="https://fonts.googleapis.com">
195
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
196
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
 
211
  --text-secondary-color: var(--tg-theme-hint-color);
212
  --accent-gradient: linear-gradient(95deg, var(--tg-theme-button-color, #007aff), #5856d6);
213
  --accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
214
+ --accent-gradient-ton: linear-gradient(95deg, #0098EA, #00baff); /* TON Gradient */
215
  --tag-bg: rgba(255, 255, 255, 0.1);
216
  --border-radius-s: 8px;
217
  --border-radius-m: 14px;
 
292
  background: rgba(44, 44, 46, 0.95);
293
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), var(--shadow-medium);
294
  }
295
+ .btn-ton {
296
+ background: var(--accent-gradient-ton);
297
+ color: var(--tg-theme-button-text-color);
298
+ }
299
+ .btn-ton:hover {
300
+ background: linear-gradient(95deg, #00baff, #0098EA);
301
+ }
302
+
303
  .tag {
304
  display: inline-block; background: var(--tag-bg); color: var(--text-secondary-color);
305
  padding: 6px 12px; border-radius: var(--border-radius-s); font-size: 0.85em;
 
392
  }
393
  .save-card-button i { font-size: 1.2em; }
394
 
395
+ .ton-wallet-section {
396
+ margin-top: var(--padding-l);
397
+ text-align: center;
398
+ background-color: var(--card-bg);
399
+ border-radius: var(--border-radius-l);
400
+ padding: var(--padding-l);
401
+ box-shadow: var(--shadow-medium);
402
+ border: 1px solid rgba(255, 255, 255, 0.08);
403
+ backdrop-filter: blur(var(--backdrop-blur));
404
+ -webkit-backdrop-filter: blur(var(--backdrop-blur));
405
+ }
406
+ .ton-wallet-section h3 {
407
+ font-size: 1.5em;
408
+ font-weight: 600;
409
+ margin-bottom: var(--padding-s);
410
+ color: var(--tg-theme-link-color);
411
+ }
412
+ .ton-wallet-section .status-message {
413
+ font-size: 1em;
414
+ color: var(--text-secondary-color);
415
+ margin-bottom: var(--padding-m);
416
+ }
417
+ .ton-wallet-section .wallet-info {
418
+ margin-top: var(--padding-m);
419
+ padding: var(--padding-m);
420
+ background-color: var(--card-bg-solid);
421
+ border-radius: var(--border-radius-m);
422
+ text-align: left;
423
+ border: 1px solid rgba(255, 255, 255, 0.08);
424
+ }
425
+ .ton-wallet-section .wallet-info p {
426
+ margin-bottom: 0.5rem;
427
+ word-break: break-all;
428
+ }
429
+ .ton-wallet-section .wallet-info strong {
430
+ color: var(--tg-theme-text-color);
431
+ font-weight: 600;
432
+ }
433
+ .ton-wallet-section .wallet-info .balance {
434
+ font-size: 1.3em;
435
+ font-weight: 700;
436
+ color: var(--accent-gradient-ton-start, #0098EA); /* Use TON color */
437
+ margin-top: 1rem;
438
+ }
439
+ .ton-wallet-section .wallet-info .balance span {
440
+ font-size: 0.8em;
441
+ font-weight: 500;
442
+ color: var(--text-secondary-color);
443
+ margin-left: 5px;
444
+ }
445
+
446
+
447
+ /* Modal Styles */
448
  .modal {
449
  display: none; position: fixed; z-index: 1001;
450
  left: 0; top: 0; width: 100%; height: 100%;
 
476
  .modal-text b { color: var(--tg-theme-link-color); font-weight: 600; }
477
  .modal-instruction { font-size: 1em; color: var(--text-secondary-color); margin-top: var(--padding-m); }
478
 
479
+ /* Icons */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  .icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
481
  .icon-save::before { content: '💾'; }
482
  .icon-web::before { content: '🌐'; }
 
497
  .icon-link::before { content: '🔗'; }
498
  .icon-leader::before { content: '🏆'; }
499
  .icon-company::before { content: '🏢'; }
500
+ .icon-ton::before { content: '💎'; }
501
 
502
+
503
+ /* Responsive adjustments */
504
  @media (max-width: 480px) {
505
  .section-title { font-size: 1.8em; }
506
  .logo span { font-size: 1.4em; }
 
509
  .stats-grid { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: var(--padding-s); }
510
  .stat-value { font-size: 1.5em; }
511
  .modal-content { margin: 25% auto; width: 92%; }
512
+ .ton-wallet-section h3 { font-size: 1.3em; }
513
+ .ton-wallet-section .wallet-info .balance { font-size: 1.1em; }
514
  }
515
  </style>
516
  </head>
 
538
  </a>
539
  </section>
540
 
541
+ <!-- TON Wallet Section -->
542
+ <section class="ton-wallet-section">
543
+ <h3><i class="icon icon-ton"></i>TON Wallet</h3>
544
+ <div id="ton-status" class="status-message">Подключение к TON...</div>
545
+ <button id="connect-ton-btn" class="btn btn-ton" style="display: none; width: 100%;">Подключить TON Кошелек</button>
546
+ <div id="wallet-info" class="wallet-info" style="display: none;">
547
+ <p><strong>Адрес:</strong> <span id="ton-address"></span></p>
548
+ <p><strong>Баланс:</strong> <span id="ton-balance" class="balance"></span></p>
549
+ </div>
550
+ </section>
551
+ <!-- End TON Wallet Section -->
552
+
553
+
554
  <section class="ecosystem-header">
555
  <h2 class="section-title"><i class="icon icon-company"></i>Экосистема инноваций</h2>
556
  <p class="description">
 
635
  </div>
636
  </section>
637
 
 
 
 
 
 
 
 
 
 
 
 
 
638
  <footer class="footer-greeting">
639
  <p id="greeting">Инициализация...</p>
640
  </footer>
 
645
  <i class="icon icon-save"></i>Сохранить визитку
646
  </button>
647
 
648
+ <!-- The Modal -->
649
  <div id="saveModal" class="modal">
650
  <div class="modal-content">
651
  <span class="modal-close" id="modal-close-btn">×</span>
 
656
  </div>
657
  </div>
658
 
659
+
660
  <script>
661
  const tg = window.Telegram.WebApp;
662
 
 
681
  }
682
  }
683
 
684
+ const TON_MANIFEST_URL = window.location.origin + '/.well-known/ton-connect/manifest.json';
685
+ let connector = null;
686
+
687
+ function setupTonConnect() {
688
+ if (!window.TonConnectSDK) {
689
+ console.error("TON Connect SDK not loaded.");
690
+ document.getElementById('ton-status').textContent = 'Ошибка загрузки TON SDK.';
691
+ document.getElementById('connect-ton-btn').style.display = 'none';
692
+ return;
693
+ }
694
+ document.getElementById('ton-status').textContent = 'Инициализация TON Connect...';
695
+
696
+ try {
697
+ connector = new TonConnectSDK.TonConnect({
698
+ manifestUrl: TON_MANIFEST_URL,
699
+ });
700
+
701
+ connector.onStatusChange(wallet => {
702
+ console.log('TON Wallet status changed:', wallet);
703
+ updateWalletUI(wallet);
704
+ if (wallet) {
705
+ // Wallet connected or changed, save address and fetch balance
706
+ saveTonWalletAddress(wallet.account.address);
707
+ fetchTonBalance();
708
+ } else {
709
+ // Wallet disconnected
710
+ document.getElementById('ton-status').textContent = 'TON Кошелек не подключен.';
711
+ }
712
+ });
713
+
714
+ document.getElementById('connect-ton-btn').addEventListener('click', () => {
715
+ if (connector) {
716
+ document.getElementById('ton-status').textContent = 'Подключение... Ожидайте в кошельке.';
717
+ connector.connect();
718
+ if (tg.HapticFeedback) {
719
+ tg.HapticFeedback.impactOccurred('light');
720
+ }
721
+ }
722
+ });
723
+
724
+ // Attempt to restore existing connection
725
+ connector.restoreConnection();
726
+
727
+ } catch (e) {
728
+ console.error("Failed to initialize TON Connect:", e);
729
+ document.getElementById('ton-status').textContent = 'Ошибка инициализации TON Connect.';
730
+ document.getElementById('connect-ton-btn').style.display = 'none';
731
+ }
732
+ }
733
+
734
+ function updateWalletUI(wallet) {
735
+ const connectBtn = document.getElementById('connect-ton-btn');
736
+ const walletInfoDiv = document.getElementById('wallet-info');
737
+ const statusDiv = document.getElementById('ton-status');
738
+ const addressSpan = document.getElementById('ton-address');
739
+ const balanceSpan = document.getElementById('ton-balance');
740
+
741
+ if (wallet) {
742
+ connectBtn.style.display = 'none';
743
+ walletInfoDiv.style.display = 'block';
744
+ statusDiv.textContent = 'TON Кошелек подключен.';
745
+ addressSpan.textContent = TonConnectSDK.toUserFriendlyAddress(wallet.account.address, wallet.account.chain);
746
+ balanceSpan.textContent = 'Загрузка...'; // Placeholder while fetching balance
747
+ } else {
748
+ connectBtn.style.display = 'block';
749
+ walletInfoDiv.style.display = 'none';
750
+ addressSpan.textContent = '';
751
+ balanceSpan.textContent = '';
752
+ statusDiv.textContent = 'TON Кошелек не подключен.';
753
+ }
754
+ }
755
+
756
+ function saveTonWalletAddress(address) {
757
+ if (!tg || !tg.initData) {
758
+ console.error("Telegram WebApp initData is missing for saving wallet.");
759
+ return;
760
+ }
761
+ fetch('/save_ton_wallet', {
762
+ method: 'POST',
763
+ headers: { 'Content-Type': 'application/json' },
764
+ body: JSON.stringify({ initData: tg.initData, tonAddress: address }),
765
+ })
766
+ .then(response => response.json())
767
+ .then(data => {
768
+ if (data.status === 'ok') {
769
+ console.log('TON wallet address saved successfully.');
770
+ } else {
771
+ console.error('Failed to save TON wallet address:', data.message);
772
+ }
773
+ })
774
+ .catch(error => {
775
+ console.error('Error sending TON wallet address to backend:', error);
776
+ });
777
+ }
778
+
779
+ function fetchTonBalance() {
780
+ if (!tg || !tg.initData) {
781
+ console.error("Telegram WebApp initData is missing for fetching balance.");
782
+ document.getElementById('ton-balance').textContent = 'Ошибка (нет данных ТГ)';
783
+ return;
784
+ }
785
+ const balanceSpan = document.getElementById('ton-balance');
786
+ balanceSpan.textContent = 'Загрузка...';
787
+
788
+ fetch('/get_ton_balance', {
789
+ method: 'POST',
790
+ headers: { 'Content-Type': 'application/json' },
791
+ body: JSON.stringify({ initData: tg.initData }),
792
+ })
793
+ .then(response => {
794
+ if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
795
+ return response.json();
796
+ })
797
+ .then(data => {
798
+ if (data.status === 'ok') {
799
+ // Balance is in nanoton, convert to TON
800
+ const balanceTon = (data.balance / 1e9).toFixed(4);
801
+ balanceSpan.innerHTML = `${balanceTon} <span>TON</span>`;
802
+ } else {
803
+ balanceSpan.textContent = `Ошибка: ${data.message}`;
804
+ console.error('Failed to fetch TON balance:', data.message);
805
+ }
806
+ })
807
+ .catch(error => {
808
+ balanceSpan.textContent = `Ошибка: ${error.message}`;
809
+ console.error('Error fetching TON balance:', error);
810
+ });
811
+ }
812
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
813
 
814
  function setupTelegram() {
815
  if (!tg || !tg.initData) {
 
817
  const greetingElement = document.getElementById('greeting');
818
  if(greetingElement) greetingElement.textContent = 'Не удалось связаться с Telegram.';
819
  document.body.style.visibility = 'visible';
820
+ setupTonConnect(); // Try setting up TON anyway, it might work in standard browser
821
  return;
822
  }
823
 
 
842
  .then(data => {
843
  if (data.status === 'ok' && data.verified) {
844
  console.log('Backend verification successful.');
845
+ // Verification successful, now setup TON Connect
846
+ setupTonConnect();
847
  } else {
848
  console.warn('Backend verification failed:', data.message);
849
+ document.getElementById('ton-status').textContent = 'Проверка Telegram данных не пройдена.';
850
+ document.getElementById('connect-ton-btn').style.display = 'none';
851
  }
852
  })
853
  .catch(error => {
854
  console.error('Error sending initData for verification:', error);
855
+ document.getElementById('ton-status').textContent = 'Ошибка связи с сервером для проверки.';
856
+ document.getElementById('connect-ton-btn').style.display = 'none';
857
  });
858
 
859
 
 
901
  console.error("Modal elements not found!");
902
  }
903
 
904
+ document.body.style.visibility = 'visible';
 
 
905
  }
906
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  if (window.Telegram && window.Telegram.WebApp) {
908
  setupTelegram();
909
  } else {
 
915
  const greetingElement = document.getElementById('greeting');
916
  if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
917
  document.body.style.visibility = 'visible';
918
+ setupTonConnect(); // Still try TON Connect even without Telegram initData fully
919
  }
920
  }, 3500);
921
  }
 
991
  }
992
  .user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
993
  .user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
994
+ .user-card .details { font-size: 0.9em; color: #495057; word-break: break-word; text-align: left; width: 100%; }
 
995
  .user-card .detail-item { margin-bottom: 0.3rem; }
996
+ .user-card .detail-item strong { color: var(--admin-text); margin-right: 5px;}
997
+ .user-card .detail-item .ton-address-text {
998
+ display: block;
999
+ font-size: 0.8em;
1000
+ color: #6c757d;
1001
+ margin-top: 5px;
1002
+ word-break: break-all;
1003
+ }
1004
+
1005
  .user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
1006
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
1007
  .alert {
 
1024
  }
1025
  .refresh-btn:hover { background-color: #0b5ed7; }
1026
 
1027
+ /* Admin Controls */
1028
  .admin-controls {
1029
  background: var(--admin-card-bg);
1030
  padding: var(--padding);
 
1081
  <img src="{{ user.photo_url if user.photo_url else 'data:image/svg+xml;charset=UTF-8,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 100 100%27%3e%3crect width=%27100%27 height=%27100%27 fill=%27%23e9ecef%27/%3e%3ctext x=%2750%25%27 y=%2755%25%27 dominant-baseline=%27middle%27 text-anchor=%27middle%27 font-size=%2745%27 font-family=%27sans-serif%27 fill=%27%23adb5bd%27%3e?%3c/text%3e%3c/svg%3e' }}" alt="User Avatar">
1082
  <div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
1083
  {% if user.username %}
1084
+ <div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" style="color: inherit; text-decoration: none;">@{{ user.username }}</a></div>
1085
  {% else %}
1086
  <div class="username" style="height: 1.3em;"></div>
1087
  {% endif %}
 
1090
  <div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
1091
  <div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
1092
  <div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
1093
+ <div class="detail-item">
1094
+ <strong>TON:</strong>
1095
+ {% if user.ton_wallet %}
1096
+ <span class="ton-address-text">{{ user.ton_wallet }}</span>
1097
+ {% else %}
1098
+ Не подключен
1099
+ {% endif %}
1100
+ </div>
1101
  </div>
1102
  <div class="timestamp">Визит: {{ user.visited_at_str }}</div>
1103
  </div>
 
1149
  </html>
1150
  """
1151
 
 
 
 
 
 
 
 
 
1152
  @app.route('/')
1153
  def index():
1154
  theme_params = {}
1155
  return render_template_string(TEMPLATE, theme=theme_params)
1156
 
1157
+ @app.route('/.well-known/ton-connect/manifest.json')
1158
+ def tonconnect_manifest():
1159
+ manifest = {
1160
+ "url": request.url_root.rstrip('/') ,
1161
+ "name": "Morshen Group TMA",
1162
+ "iconUrl": request.url_root.rstrip('/') + "/static/morshengroup_icon.png", # Ensure you have a static folder and this icon
1163
+ "termsOfUseUrl": "", # Optional
1164
+ "privacyPolicyUrl": "" # Optional
1165
+ }
1166
+ return jsonify(manifest)
1167
+
1168
  @app.route('/verify', methods=['POST'])
1169
  def verify_data():
1170
  try:
 
1183
  user_info_dict = json.loads(user_json_str)
1184
  user_id = user_info_dict.get('id')
1185
  except Exception as e:
1186
+ logging.error(f"Could not parse user JSON from initData: {e}")
1187
  user_info_dict = {}
1188
 
1189
  if is_valid and user_id:
1190
  now = time.time()
1191
+ user_entry = {
1192
+ str(user_id): {
1193
+ 'id': user_id,
1194
+ 'first_name': user_info_dict.get('first_name'),
1195
+ 'last_name': user_info_dict.get('last_name'),
1196
+ 'username': user_info_dict.get('username'),
1197
+ 'photo_url': user_info_dict.get('photo_url'),
1198
+ 'language_code': user_info_dict.get('language_code'),
1199
+ 'is_premium': user_info_dict.get('is_premium', False),
1200
+ 'phone_number': user_info_dict.get('phone_number'),
1201
+ 'visited_at': now,
1202
+ 'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
1203
+ }
1204
+ }
1205
+ save_visitor_data(user_entry)
1206
+ return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
1207
+ else:
1208
+ logging.warning(f"Verification failed or user_id missing for initData.")
1209
+ return jsonify({"status": "error", "verified": False, "message": "Invalid data or missing user ID"}), 403
1210
+
1211
+ except Exception as e:
1212
+ logging.exception("Error in /verify endpoint")
1213
+ return jsonify({"status": "error", "message": "Internal server error"}), 500
1214
+
1215
+ @app.route('/save_ton_wallet', methods=['POST'])
1216
+ def save_ton_wallet():
1217
+ try:
1218
+ req_data = request.get_json()
1219
+ init_data_str = req_data.get('initData')
1220
+ ton_address = req_data.get('tonAddress')
1221
+
1222
+ if not init_data_str or not ton_address:
1223
+ return jsonify({"status": "error", "message": "Missing initData or tonAddress"}), 400
1224
+
1225
+ user_data_parsed, is_valid = verify_telegram_data(init_data_str)
1226
+
1227
+ user_id = None
1228
+ if user_data_parsed and 'user' in user_data_parsed:
1229
+ try:
1230
+ user_json_str = unquote(user_data_parsed['user'][0])
1231
+ user_info_dict = json.loads(user_json_str)
1232
+ user_id = user_info_dict.get('id')
1233
+ except Exception as e:
1234
+ logging.error(f"Could not parse user JSON from initData during save_ton_wallet: {e}")
1235
+
1236
+
1237
+ if is_valid and user_id:
1238
+ user_id_str = str(user_id)
1239
  current_data = load_visitor_data()
1240
+ if user_id_str not in current_data:
1241
+ current_data[user_id_str] = {'id': user_id} # Ensure user entry exists
1242
+
1243
+ # Update only the ton_wallet field
1244
+ user_entry_update = {
1245
+ user_id_str: {
1246
+ 'ton_wallet': ton_address
1247
+ }
 
 
 
 
 
 
 
 
 
1248
  }
1249
+ save_visitor_data(user_entry_update)
1250
 
1251
+ return jsonify({"status": "ok", "message": "TON wallet address saved."}), 200
1252
+ else:
1253
+ logging.warning(f"Invalid initData or missing user ID during save_ton_wallet.")
1254
+ return jsonify({"status": "error", "message": "Invalid Telegram data or missing user ID"}), 403
1255
 
1256
+ except Exception as e:
1257
+ logging.exception("Error in /save_ton_wallet endpoint")
1258
+ return jsonify({"status": "error", "message": "Internal server error"}), 500
1259
 
1260
+
1261
+ @app.route('/get_ton_balance', methods=['POST'])
1262
+ def get_ton_balance():
1263
+ if not tonapi:
1264
+ return jsonify({"status": "error", "message": "TONAPI is not initialized (check TONAPI_KEY)."}), 503
1265
+
1266
+ try:
1267
+ req_data = request.get_json()
1268
+ init_data_str = req_data.get('initData')
1269
+
1270
+ if not init_data_str:
1271
+ return jsonify({"status": "error", "message": "Missing initData"}), 400
1272
+
1273
+ user_data_parsed, is_valid = verify_telegram_data(init_data_str)
1274
+
1275
+ user_id = None
1276
+ if user_data_parsed and 'user' in user_data_parsed:
1277
+ try:
1278
+ user_json_str = unquote(user_data_parsed['user'][0])
1279
+ user_info_dict = json.loads(user_json_str)
1280
+ user_id = user_info_dict.get('id')
1281
+ except Exception as e:
1282
+ logging.error(f"Could not parse user JSON from initData during get_ton_balance: {e}")
1283
+
1284
+ if is_valid and user_id:
1285
+ user_id_str = str(user_id)
1286
+ current_data = load_visitor_data()
1287
+ user_data = current_data.get(user_id_str)
1288
+
1289
+ if user_data and 'ton_wallet' in user_data and user_data['ton_wallet']:
1290
+ ton_address = user_data['ton_wallet']
1291
+ try:
1292
+ account_info = tonapi.account.get_wallet_v4(account_id=ton_address)
1293
+ balance_nanoton = account_info.balance
1294
+ return jsonify({"status": "ok", "balance": balance_nanoton, "address": ton_address}), 200
1295
+ except Exception as e:
1296
+ logging.error(f"Error fetching TON balance for {ton_address}: {e}")
1297
+ return jsonify({"status": "error", "message": f"Failed to fetch balance: {e}"}), 500
1298
+ else:
1299
+ return jsonify({"status": "error", "message": "TON wallet not linked for this user."}), 404
1300
  else:
1301
+ logging.warning(f"Invalid initData or missing user ID during get_ton_balance.")
1302
+ return jsonify({"status": "error", "message": "Invalid Telegram data or missing user ID"}), 403
1303
 
1304
  except Exception as e:
1305
+ logging.exception("Error in /get_ton_balance endpoint")
1306
  return jsonify({"status": "error", "message": "Internal server error"}), 500
1307
 
1308
+
1309
  @app.route('/admin')
1310
  def admin_panel():
1311
  current_data = load_visitor_data()
 
1327
  upload_data_to_hf_async()
1328
  return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
1329
 
1330
+ @app.route('/static/<filename>')
1331
+ def static_files(filename):
1332
+ if filename == 'morshengroup_icon.png':
1333
+ # Basic handling for the manifest icon
1334
+ try:
1335
+ return app.send_static_file(filename)
1336
+ except FileNotFoundError:
1337
+ return "Icon not found", 404
1338
+ return "Not found", 404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1339
 
1340
 
1341
  if __name__ == '__main__':
 
1347
  print(f"Visitor data file: {DATA_FILE}")
1348
  print(f"Hugging Face Repo: {REPO_ID}")
1349
  print(f"HF Data Path: {HF_DATA_FILE_PATH}")
 
 
 
 
1350
  if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
1351
  print("---")
1352
  print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---")
 
1357
  print("--- Attempting initial data download from Hugging Face...")
1358
  download_data_from_hf()
1359
 
1360
+ if not TONAPI_KEY:
1361
+ print("---")
1362
+ print("--- WARNING: TONAPI_KEY NOT SET ---")
1363
+ print("--- TON wallet functionality will be disabled. Set TONAPI_KEY environment variable.")
1364
+ print("---")
1365
+ else:
1366
+ print("--- TONAPI_KEY found. TON wallet functionality enabled.")
1367
+
1368
+
1369
  load_visitor_data()
1370
 
1371
  print("---")
 
1381
  else:
1382
  print("--- Periodic backup disabled (HF_TOKEN_WRITE missing).")
1383
 
1384
+ # Create a static directory if it doesn't exist and add a dummy icon
1385
+ if not os.path.exists('static'):
1386
+ os.makedirs('static')
1387
+ dummy_icon_path = os.path.join('static', 'morshengroup_icon.png')
1388
+ if not os.path.exists(dummy_icon_path):
1389
+ try:
1390
+ # Create a minimal dummy PNG (1x1 transparent)
1391
+ import base64
1392
+ dummy_png = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\xfc\xff\xff?\x03\x00\x08\xfb\x02\xfe\xa7\xcd\x8c\x00\x00\x00\x00IEND\xaeB`\x82'
1393
+ with open(dummy_icon_path, 'wb') as f:
1394
+ f.write(dummy_png)
1395
+ print(f"Created dummy icon at {dummy_icon_path} for TON Connect manifest.")
1396
+ except Exception as e:
1397
+ print(f"Could not create dummy icon: {e}. TON Connect manifest icon might fail.")
1398
+
1399
+
1400
  print("--- Server Ready ---")
1401
+
1402
  app.run(host=HOST, port=PORT, debug=False)