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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +295 -159
app.py CHANGED
@@ -14,25 +14,22 @@ import threading
14
  from huggingface_hub import HfApi, hf_hub_download
15
  from huggingface_hub.utils import RepositoryNotFoundError
16
 
17
- # --- Configuration ---
18
- BOT_TOKEN = os.getenv("BOT_TOKEN", "7566834146:AAGiG4MaTZZvvbTVsqEJVG5SYK5hUlc_Ewo") # Use environment variable or default
19
  HOST = '0.0.0.0'
20
  PORT = 7860
21
- DATA_FILE = 'data.json' # Local file for visitor data
22
 
23
- # Hugging Face Settings
24
  REPO_ID = "flpolprojects/teledata"
25
- HF_DATA_FILE_PATH = "data.json" # Path within the HF repo
26
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE") # Token with write access
27
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Token with read access (can be same as write)
28
 
29
  app = Flask(__name__)
30
  logging.basicConfig(level=logging.INFO)
31
- app.secret_key = os.urandom(24) # For potential future session use
32
 
33
- # --- Hugging Face & Data Handling ---
34
  _data_lock = threading.Lock()
35
- visitor_data_cache = {} # In-memory cache
36
 
37
  def download_data_from_hf():
38
  global visitor_data_cache
@@ -48,11 +45,10 @@ def download_data_from_hf():
48
  token=HF_TOKEN_READ,
49
  local_dir=".",
50
  local_dir_use_symlinks=False,
51
- force_download=True, # Ensure we get the latest version
52
- etag_timeout=10 # Shorter timeout to avoid hanging
53
  )
54
  logging.info("Data file successfully downloaded from Hugging Face.")
55
- # Force reload from downloaded file
56
  with _data_lock:
57
  try:
58
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
@@ -64,16 +60,14 @@ def download_data_from_hf():
64
  return True
65
  except RepositoryNotFoundError:
66
  logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.")
67
- # Don't clear local cache if repo not found, might have local data
68
  except Exception as e:
69
  logging.error(f"Error downloading data from Hugging Face: {e}")
70
- # Don't clear local cache on generic download errors
71
  return False
72
 
73
  def load_visitor_data():
74
  global visitor_data_cache
75
  with _data_lock:
76
- if not visitor_data_cache: # Only load from file if cache is empty
77
  try:
78
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
79
  visitor_data_cache = json.load(f)
@@ -89,17 +83,15 @@ def load_visitor_data():
89
  visitor_data_cache = {}
90
  return visitor_data_cache
91
 
92
- def save_visitor_data(data):
 
93
  with _data_lock:
94
  try:
95
- # Update cache first
96
- visitor_data_cache.update(data)
97
- # Save updated cache to file
98
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
99
  json.dump(visitor_data_cache, f, ensure_ascii=False, indent=4)
100
  logging.info(f"Visitor data successfully saved to {DATA_FILE}.")
101
- # Trigger upload after successful local save
102
- upload_data_to_hf_async() # Use async upload
103
  except Exception as e:
104
  logging.error(f"Error saving visitor data: {e}")
105
 
@@ -113,7 +105,7 @@ def upload_data_to_hf():
113
 
114
  try:
115
  api = HfApi()
116
- with _data_lock: # Ensure file isn't being written while reading for upload
117
  file_content_exists = os.path.getsize(DATA_FILE) > 0
118
  if not file_content_exists:
119
  logging.warning(f"{DATA_FILE} is empty. Skipping upload.")
@@ -131,10 +123,8 @@ def upload_data_to_hf():
131
  logging.info("Visitor data successfully uploaded to Hugging Face.")
132
  except Exception as e:
133
  logging.error(f"Error uploading data to Hugging Face: {e}")
134
- # Consider adding retry logic here if needed
135
 
136
  def upload_data_to_hf_async():
137
- # Run upload in a separate thread to avoid blocking web requests
138
  upload_thread = threading.Thread(target=upload_data_to_hf, daemon=True)
139
  upload_thread.start()
140
 
@@ -143,11 +133,10 @@ def periodic_backup():
143
  logging.info("Periodic backup disabled: HF_TOKEN_WRITE not set.")
144
  return
145
  while True:
146
- time.sleep(3600) # Backup every hour
147
  logging.info("Initiating periodic backup...")
148
  upload_data_to_hf()
149
 
150
- # --- Telegram Verification ---
151
  def verify_telegram_data(init_data_str):
152
  try:
153
  parsed_data = parse_qs(init_data_str)
@@ -167,8 +156,8 @@ def verify_telegram_data(init_data_str):
167
  if calculated_hash == received_hash:
168
  auth_date = int(parsed_data.get('auth_date', [0])[0])
169
  current_time = int(time.time())
170
- if current_time - auth_date > 86400: # Allow data up to 24 hours old
171
- logging.warning(f"Telegram InitData is older than 1 hour (Auth Date: {auth_date}, Current: {current_time}).")
172
  return parsed_data, True
173
  else:
174
  logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
@@ -177,7 +166,6 @@ def verify_telegram_data(init_data_str):
177
  logging.error(f"Error verifying Telegram data: {e}")
178
  return None, False
179
 
180
- # --- HTML Templates ---
181
  TEMPLATE = """
182
  <!DOCTYPE html>
183
  <html lang="ru">
@@ -186,6 +174,7 @@ TEMPLATE = """
186
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
187
  <title>Morshen Group</title>
188
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
 
189
  <link rel="preconnect" href="https://fonts.googleapis.com">
190
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
191
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
@@ -200,7 +189,7 @@ TEMPLATE = """
200
  --tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#1e1e1e') }};
201
 
202
  --bg-gradient: linear-gradient(160deg, #1a232f 0%, #121212 100%);
203
- --card-bg: rgba(44, 44, 46, 0.8); /* Semi-transparent card */
204
  --card-bg-solid: #2c2c2e;
205
  --text-color: var(--tg-theme-text-color);
206
  --text-secondary-color: var(--tg-theme-hint-color);
@@ -208,16 +197,16 @@ TEMPLATE = """
208
  --accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
209
  --tag-bg: rgba(255, 255, 255, 0.1);
210
  --border-radius-s: 8px;
211
- --border-radius-m: 14px; /* Increased radius */
212
- --border-radius-l: 18px; /* Increased radius */
213
  --padding-s: 10px;
214
- --padding-m: 18px; /* Increased padding */
215
- --padding-l: 28px; /* Increased padding */
216
  --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
217
  --shadow-color: rgba(0, 0, 0, 0.3);
218
  --shadow-light: 0 4px 15px var(--shadow-color);
219
  --shadow-medium: 0 6px 25px var(--shadow-color);
220
- --backdrop-blur: 10px; /* Glassmorphism effect */
221
  }
222
  * { box-sizing: border-box; margin: 0; padding: 0; }
223
  html {
@@ -229,11 +218,11 @@ TEMPLATE = """
229
  background: var(--bg-gradient);
230
  color: var(--text-color);
231
  padding: var(--padding-m);
232
- padding-bottom: 120px; /* More space for fixed button */
233
  overscroll-behavior-y: none;
234
  -webkit-font-smoothing: antialiased;
235
  -moz-osx-font-smoothing: grayscale;
236
- visibility: hidden; /* Hide until ready */
237
  min-height: 100vh;
238
  }
239
  .container {
@@ -252,7 +241,7 @@ TEMPLATE = """
252
  }
253
  .logo { display: flex; align-items: center; gap: var(--padding-s); }
254
  .logo img {
255
- width: 50px; /* Larger logo */
256
  height: 50px;
257
  border-radius: 50%;
258
  background-color: var(--card-bg-solid);
@@ -260,12 +249,12 @@ TEMPLATE = """
260
  border: 2px solid rgba(255, 255, 255, 0.15);
261
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
262
  }
263
- .logo span { font-size: 1.6em; font-weight: 700; letter-spacing: -0.5px; } /* Bold, slightly larger */
264
  .btn {
265
  display: inline-flex; align-items: center; justify-content: center;
266
  padding: 12px var(--padding-m); border-radius: var(--border-radius-m);
267
  background: var(--accent-gradient); color: var(--tg-theme-button-text-color);
268
- text-decoration: none; font-weight: 600; /* Bolder */
269
  border: none; cursor: pointer;
270
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
271
  gap: 8px; font-size: 1em;
@@ -298,14 +287,14 @@ TEMPLATE = """
298
  background-color: var(--card-bg);
299
  border-radius: var(--border-radius-l);
300
  padding: var(--padding-l);
301
- margin-bottom: 0; /* Removed bottom margin, gap handles spacing */
302
  box-shadow: var(--shadow-medium);
303
  border: 1px solid rgba(255, 255, 255, 0.08);
304
  backdrop-filter: blur(var(--backdrop-blur));
305
  -webkit-backdrop-filter: blur(var(--backdrop-blur));
306
  }
307
  .section-title {
308
- font-size: 2em; /* Larger titles */
309
  font-weight: 700; margin-bottom: var(--padding-s); line-height: 1.25;
310
  letter-spacing: -0.6px;
311
  }
@@ -314,7 +303,7 @@ TEMPLATE = """
314
  margin-bottom: var(--padding-m);
315
  }
316
  .description {
317
- font-size: 1.05em; line-height: 1.6; color: var(--text-secondary-color); /* Slightly larger desc */
318
  margin-bottom: var(--padding-m);
319
  }
320
  .stats-grid {
@@ -334,7 +323,7 @@ TEMPLATE = """
334
  background-color: var(--card-bg-solid);
335
  padding: var(--padding-m); border-radius: var(--border-radius-m);
336
  margin-bottom: var(--padding-s); display: flex; align-items: center;
337
- gap: var(--padding-m); /* Increased gap */
338
  font-size: 1.1em; font-weight: 500;
339
  border: 1px solid rgba(255, 255, 255, 0.08);
340
  transition: background-color 0.2s ease, transform 0.2s ease;
@@ -350,11 +339,11 @@ TEMPLATE = """
350
  }
351
  .save-card-button {
352
  position: fixed;
353
- bottom: 30px; /* Raised */
354
  left: 50%;
355
  transform: translateX(-50%);
356
- padding: 14px 28px; /* Larger padding */
357
- border-radius: 30px; /* More rounded */
358
  background: var(--accent-gradient-green);
359
  color: var(--tg-theme-button-text-color);
360
  text-decoration: none;
@@ -363,26 +352,24 @@ TEMPLATE = """
363
  cursor: pointer;
364
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
365
  z-index: 1000;
366
- box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.5); /* Add outer glow */
367
- font-size: 1.05em; /* Slightly larger text */
368
  display: flex;
369
  align-items: center;
370
- gap: 10px; /* Increased gap */
371
  backdrop-filter: blur(5px);
372
  -webkit-backdrop-filter: blur(5px);
373
  }
374
  .save-card-button:hover {
375
  opacity: 0.95;
376
- transform: translateX(-50%) scale(1.05); /* Slightly larger scale */
377
  box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.3);
378
  }
379
  .save-card-button i { font-size: 1.2em; }
380
-
381
- /* Modal Styles */
382
  .modal {
383
  display: none; position: fixed; z-index: 1001;
384
  left: 0; top: 0; width: 100%; height: 100%;
385
- overflow: auto; background-color: rgba(0,0,0,0.7); /* Darker backdrop */
386
  backdrop-filter: blur(8px);
387
  -webkit-backdrop-filter: blur(8px);
388
  animation: fadeIn 0.3s ease-out;
@@ -409,8 +396,6 @@ TEMPLATE = """
409
  .modal-text { font-size: 1.2em; line-height: 1.6; margin-bottom: var(--padding-s); word-wrap: break-word; }
410
  .modal-text b { color: var(--tg-theme-link-color); font-weight: 600; }
411
  .modal-instruction { font-size: 1em; color: var(--text-secondary-color); margin-top: var(--padding-m); }
412
-
413
- /* Icons */
414
  .icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
415
  .icon-save::before { content: '💾'; }
416
  .icon-web::before { content: '🌐'; }
@@ -431,8 +416,12 @@ TEMPLATE = """
431
  .icon-link::before { content: '🔗'; }
432
  .icon-leader::before { content: '🏆'; }
433
  .icon-company::before { content: '🏢'; }
 
 
 
 
 
434
 
435
- /* Responsive adjustments */
436
  @media (max-width: 480px) {
437
  .section-title { font-size: 1.8em; }
438
  .logo span { font-size: 1.4em; }
@@ -466,6 +455,13 @@ TEMPLATE = """
466
  <a href="#" class="btn contact-link" style="background: var(--accent-gradient-green); width: 100%; margin-top: var(--padding-s);">
467
  <i class="icon icon-contact"></i>Написать нам в Telegram
468
  </a>
 
 
 
 
 
 
 
469
  </section>
470
 
471
  <section class="ecosystem-header">
@@ -562,7 +558,6 @@ TEMPLATE = """
562
  <i class="icon icon-save"></i>Сохранить визитку
563
  </button>
564
 
565
- <!-- The Modal -->
566
  <div id="saveModal" class="modal">
567
  <div class="modal-content">
568
  <span class="modal-close" id="modal-close-btn">×</span>
@@ -573,9 +568,9 @@ TEMPLATE = """
573
  </div>
574
  </div>
575
 
576
-
577
  <script>
578
  const tg = window.Telegram.WebApp;
 
579
 
580
  function applyTheme(themeParams) {
581
  const root = document.documentElement;
@@ -586,8 +581,6 @@ TEMPLATE = """
586
  root.style.setProperty('--tg-theme-button-color', themeParams.button_color || '#31a5f5');
587
  root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
588
  root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#1e1e1e');
589
-
590
- // Optional: Convert main bg color to RGB for glow effect alpha
591
  try {
592
  const bgColor = themeParams.bg_color || '#121212';
593
  const r = parseInt(bgColor.slice(1, 3), 16);
@@ -595,16 +588,60 @@ TEMPLATE = """
595
  const b = parseInt(bgColor.slice(5, 7), 16);
596
  root.style.setProperty('--tg-theme-bg-color-rgb', `${r}, ${g}, ${b}`);
597
  } catch (e) {
598
- root.style.setProperty('--tg-theme-bg-color-rgb', `18, 18, 18`); // Fallback
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
599
  }
600
  }
601
 
 
602
  function setupTelegram() {
603
  if (!tg || !tg.initData) {
604
  console.error("Telegram WebApp script not loaded or initData is missing.");
605
  const greetingElement = document.getElementById('greeting');
606
  if(greetingElement) greetingElement.textContent = 'Не удалось связаться с Telegram.';
607
- // Apply default dark theme maybe? Or leave as is.
608
  document.body.style.visibility = 'visible';
609
  return;
610
  }
@@ -615,7 +652,6 @@ TEMPLATE = """
615
  applyTheme(tg.themeParams);
616
  tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
617
 
618
- // Send initData for verification and user logging
619
  fetch('/verify', {
620
  method: 'POST',
621
  headers: {
@@ -631,18 +667,21 @@ TEMPLATE = """
631
  .then(data => {
632
  if (data.status === 'ok' && data.verified) {
633
  console.log('Backend verification successful.');
 
 
 
 
 
634
  } else {
635
  console.warn('Backend verification failed:', data.message);
636
- // Potentially show a non-blocking warning to user if needed
637
  }
638
  })
639
  .catch(error => {
640
  console.error('Error sending initData for verification:', error);
641
- // Display a more user-friendly error?
642
  });
643
 
644
-
645
- // User Greeting (using unsafe data for immediate feedback)
646
  const user = tg.initDataUnsafe?.user;
647
  const greetingElement = document.getElementById('greeting');
648
  if (user) {
@@ -653,16 +692,14 @@ TEMPLATE = """
653
  console.warn('Telegram User data not available (initDataUnsafe.user is empty).');
654
  }
655
 
656
- // Contact Links
657
  const contactButtons = document.querySelectorAll('.contact-link');
658
  contactButtons.forEach(button => {
659
  button.addEventListener('click', (e) => {
660
  e.preventDefault();
661
- tg.openTelegramLink('https://t.me/morshenkhan'); // Use actual contact username
662
  });
663
  });
664
 
665
- // Modal Setup
666
  const modal = document.getElementById("saveModal");
667
  const saveCardBtn = document.getElementById("save-card-btn");
668
  const closeBtn = document.getElementById("modal-close-btn");
@@ -675,20 +712,59 @@ TEMPLATE = """
675
  tg.HapticFeedback.impactOccurred('light');
676
  }
677
  });
678
-
679
- closeBtn.addEventListener('click', () => {
680
- modal.style.display = "none";
681
- });
682
-
683
  window.addEventListener('click', (event) => {
684
- if (event.target == modal) {
685
- modal.style.display = "none";
686
- }
687
  });
688
  } else {
689
  console.error("Modal elements not found!");
690
  }
691
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
  document.body.style.visibility = 'visible';
693
  }
694
 
@@ -704,9 +780,8 @@ TEMPLATE = """
704
  if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
705
  document.body.style.visibility = 'visible';
706
  }
707
- }, 3500); // Slightly longer timeout
708
  }
709
-
710
  </script>
711
  </body>
712
  </html>
@@ -750,7 +825,7 @@ ADMIN_TEMPLATE = """
750
  h1 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
751
  .user-grid {
752
  display: grid;
753
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
754
  gap: var(--padding);
755
  margin-top: var(--padding);
756
  }
@@ -774,12 +849,12 @@ ADMIN_TEMPLATE = """
774
  width: 80px; height: 80px;
775
  border-radius: 50%; margin-bottom: 1rem;
776
  object-fit: cover; border: 3px solid var(--admin-border);
777
- background-color: #eee; /* Placeholder bg */
778
  }
779
  .user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
780
  .user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
781
- .user-card .details { font-size: 0.9em; color: #495057; word-break: break-word; }
782
- .user-card .detail-item { margin-bottom: 0.3rem; }
783
  .user-card .detail-item strong { color: var(--admin-text); }
784
  .user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
785
  .no-users { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
@@ -802,8 +877,6 @@ ADMIN_TEMPLATE = """
802
  transition: background-color 0.2s ease;
803
  }
804
  .refresh-btn:hover { background-color: #0b5ed7; }
805
-
806
- /* Admin Controls */
807
  .admin-controls {
808
  background: var(--admin-card-bg);
809
  padding: var(--padding);
@@ -833,7 +906,7 @@ ADMIN_TEMPLATE = """
833
  .admin-controls .loader {
834
  border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid var(--admin-primary);
835
  width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle;
836
- display: none; /* Hidden by default */
837
  }
838
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
839
  </style>
@@ -862,13 +935,14 @@ ADMIN_TEMPLATE = """
862
  {% if user.username %}
863
  <div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" style="color: inherit; text-decoration: none;">@{{ user.username }}</a></div>
864
  {% else %}
865
- <div class="username" style="height: 1.3em;"></div> {# Placeholder for spacing #}
866
  {% endif %}
867
  <div class="details">
868
  <div class="detail-item"><strong>ID:</strong> {{ user.id }}</div>
869
  <div class="detail-item"><strong>Язык:</strong> {{ user.language_code or 'N/A' }}</div>
870
  <div class="detail-item"><strong>Premium:</strong> {{ 'Да' if user.is_premium else 'Нет' }}</div>
871
  <div class="detail-item"><strong>Телефон:</strong> {{ user.phone_number or 'Недоступен' }}</div>
 
872
  </div>
873
  <div class="timestamp">Визит: {{ user.visited_at_str }}</div>
874
  </div>
@@ -894,7 +968,7 @@ ADMIN_TEMPLATE = """
894
  statusMessage.textContent = data.message;
895
  statusMessage.style.color = 'var(--admin-success)';
896
  if (action === 'скачивание') {
897
- setTimeout(() => location.reload(), 1500); // Reload after download success
898
  }
899
  } else {
900
  throw new Error(data.message || 'Произошла ошибка');
@@ -907,27 +981,36 @@ ADMIN_TEMPLATE = """
907
  loader.style.display = 'none';
908
  }
909
  }
910
-
911
- function triggerDownload() {
912
- handleFetch('/admin/download_data', 'скачивание');
913
- }
914
-
915
- function triggerUpload() {
916
- handleFetch('/admin/upload_data', 'загрузка');
917
- }
918
  </script>
919
  </body>
920
  </html>
921
  """
922
 
923
- # --- Flask Routes ---
924
  @app.route('/')
925
  def index():
926
- # Pass theme parameters for initial render if available (e.g., from query params or session)
927
- # For simplicity, we let the JS handle theme application after tg.ready()
928
- theme_params = {} # Or load from request if needed
929
  return render_template_string(TEMPLATE, theme=theme_params)
930
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
931
  @app.route('/verify', methods=['POST'])
932
  def verify_data():
933
  try:
@@ -937,7 +1020,6 @@ def verify_data():
937
  return jsonify({"status": "error", "message": "Missing initData"}), 400
938
 
939
  user_data_parsed, is_valid = verify_telegram_data(init_data_str)
940
-
941
  user_info_dict = {}
942
  if user_data_parsed and 'user' in user_data_parsed:
943
  try:
@@ -945,48 +1027,115 @@ def verify_data():
945
  user_info_dict = json.loads(user_json_str)
946
  except Exception as e:
947
  logging.error(f"Could not parse user JSON: {e}")
948
- user_info_dict = {}
949
-
950
- if is_valid:
951
- user_id = user_info_dict.get('id')
952
- if user_id:
953
- now = time.time()
954
- # Create data entry for the specific user
955
- user_entry = {
956
- str(user_id): { # Use string keys for JSON compatibility
957
- 'id': user_id,
958
- 'first_name': user_info_dict.get('first_name'),
959
- 'last_name': user_info_dict.get('last_name'),
960
- 'username': user_info_dict.get('username'),
961
- 'photo_url': user_info_dict.get('photo_url'),
962
- 'language_code': user_info_dict.get('language_code'),
963
- 'is_premium': user_info_dict.get('is_premium', False),
964
- 'phone_number': user_info_dict.get('phone_number'), # Note: Only available if requested via button
965
- 'visited_at': now,
966
- 'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
967
- }
968
- }
969
- # Save/update this specific user's data
970
- save_visitor_data(user_entry)
971
- return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
 
 
 
 
 
 
 
 
 
 
972
  else:
973
- logging.warning(f"Verification failed for user: {user_info_dict.get('id')}")
974
  return jsonify({"status": "error", "verified": False, "message": "Invalid data"}), 403
975
 
976
  except Exception as e:
977
  logging.exception("Error in /verify endpoint")
978
  return jsonify({"status": "error", "message": "Internal server error"}), 500
979
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
980
  @app.route('/admin')
981
  def admin_panel():
982
- # WARNING: This route is unprotected! Add proper authentication/authorization.
983
- current_data = load_visitor_data() # Load from cache/file
984
  users_list = list(current_data.values())
985
  return render_template_string(ADMIN_TEMPLATE, users=users_list)
986
 
987
  @app.route('/admin/download_data', methods=['POST'])
988
  def admin_trigger_download():
989
- # WARNING: Unprotected endpoint
990
  success = download_data_from_hf()
991
  if success:
992
  return jsonify({"status": "ok", "message": "Скачивание данных с Hugging Face завершено. Страница будет обновлена."})
@@ -995,44 +1144,34 @@ def admin_trigger_download():
995
 
996
  @app.route('/admin/upload_data', methods=['POST'])
997
  def admin_trigger_upload():
998
- # WARNING: Unprotected endpoint
999
  if not HF_TOKEN_WRITE:
1000
  return jsonify({"status": "error", "message": "HF_TOKEN_WRITE не настроен на сервере."}), 400
1001
- upload_data_to_hf_async() # Trigger async upload
1002
  return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
1003
 
 
 
 
 
 
 
 
 
 
1004
 
1005
- # --- App Initialization ---
1006
  if __name__ == '__main__':
1007
- print("---")
1008
  print("--- MORSHEN GROUP MINI APP SERVER ---")
1009
- print("---")
1010
  print(f"Flask server starting on http://{HOST}:{PORT}")
1011
  print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
1012
- print(f"Visitor data file: {DATA_FILE}")
1013
- print(f"Hugging Face Repo: {REPO_ID}")
1014
- print(f"HF Data Path: {HF_DATA_FILE_PATH}")
1015
  if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
1016
- print("---")
1017
- print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---")
1018
- print("--- Backup/restore functionality will be limited. Set HF_TOKEN_READ and HF_TOKEN_WRITE environment variables.")
1019
- print("---")
1020
  else:
1021
- print("--- Hugging Face tokens found.")
1022
- # Initial attempt to download data on startup
1023
- print("--- Attempting initial data download from Hugging Face...")
1024
  download_data_from_hf()
1025
 
1026
- # Load initial data from local file (might have been updated by download)
1027
  load_visitor_data()
 
1028
 
1029
- print("---")
1030
- print("--- SECURITY WARNING ---")
1031
- print("--- The /admin route and its sub-routes are NOT protected.")
1032
- print("--- Implement proper authentication before deploying.")
1033
- print("---")
1034
-
1035
- # Start periodic backup thread if write token is available
1036
  if HF_TOKEN_WRITE:
1037
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1038
  backup_thread.start()
@@ -1041,7 +1180,4 @@ if __name__ == '__main__':
1041
  print("--- Periodic backup disabled (HF_TOKEN_WRITE missing).")
1042
 
1043
  print("--- Server Ready ---")
1044
- # Use a production server like Waitress or Gunicorn instead of app.run() for deployment
1045
- # from waitress import serve
1046
- # serve(app, host=HOST, port=PORT)
1047
- app.run(host=HOST, port=PORT, debug=False) # debug=False for production recommended
 
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'
19
  PORT = 7860
20
+ DATA_FILE = 'data.json'
21
 
 
22
  REPO_ID = "flpolprojects/teledata"
23
+ 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)
30
 
 
31
  _data_lock = threading.Lock()
32
+ visitor_data_cache = {}
33
 
34
  def download_data_from_hf():
35
  global visitor_data_cache
 
45
  token=HF_TOKEN_READ,
46
  local_dir=".",
47
  local_dir_use_symlinks=False,
48
+ force_download=True,
49
+ etag_timeout=10
50
  )
51
  logging.info("Data file successfully downloaded from Hugging Face.")
 
52
  with _data_lock:
53
  try:
54
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
 
60
  return True
61
  except RepositoryNotFoundError:
62
  logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.")
 
63
  except Exception as e:
64
  logging.error(f"Error downloading data from Hugging Face: {e}")
 
65
  return False
66
 
67
  def load_visitor_data():
68
  global visitor_data_cache
69
  with _data_lock:
70
+ if not visitor_data_cache:
71
  try:
72
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
73
  visitor_data_cache = json.load(f)
 
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}.")
94
+ upload_data_to_hf_async()
 
95
  except Exception as e:
96
  logging.error(f"Error saving visitor data: {e}")
97
 
 
105
 
106
  try:
107
  api = HfApi()
108
+ with _data_lock:
109
  file_content_exists = os.path.getsize(DATA_FILE) > 0
110
  if not file_content_exists:
111
  logging.warning(f"{DATA_FILE} is empty. Skipping upload.")
 
123
  logging.info("Visitor data successfully uploaded to Hugging Face.")
124
  except Exception as e:
125
  logging.error(f"Error uploading data to Hugging Face: {e}")
 
126
 
127
  def upload_data_to_hf_async():
 
128
  upload_thread = threading.Thread(target=upload_data_to_hf, daemon=True)
129
  upload_thread.start()
130
 
 
133
  logging.info("Periodic backup disabled: HF_TOKEN_WRITE not set.")
134
  return
135
  while True:
136
+ time.sleep(3600)
137
  logging.info("Initiating periodic backup...")
138
  upload_data_to_hf()
139
 
 
140
  def verify_telegram_data(init_data_str):
141
  try:
142
  parsed_data = parse_qs(init_data_str)
 
156
  if calculated_hash == received_hash:
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}")
 
166
  logging.error(f"Error verifying Telegram data: {e}")
167
  return None, False
168
 
 
169
  TEMPLATE = """
170
  <!DOCTYPE html>
171
  <html lang="ru">
 
174
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
175
  <title>Morshen Group</title>
176
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
177
+ <script src="https://unpkg.com/@tonconnect/ui@latest/dist/tonconnect-ui.min.js"></script>
178
  <link rel="preconnect" href="https://fonts.googleapis.com">
179
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
180
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
 
189
  --tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#1e1e1e') }};
190
 
191
  --bg-gradient: linear-gradient(160deg, #1a232f 0%, #121212 100%);
192
+ --card-bg: rgba(44, 44, 46, 0.8);
193
  --card-bg-solid: #2c2c2e;
194
  --text-color: var(--tg-theme-text-color);
195
  --text-secondary-color: var(--tg-theme-hint-color);
 
197
  --accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
198
  --tag-bg: rgba(255, 255, 255, 0.1);
199
  --border-radius-s: 8px;
200
+ --border-radius-m: 14px;
201
+ --border-radius-l: 18px;
202
  --padding-s: 10px;
203
+ --padding-m: 18px;
204
+ --padding-l: 28px;
205
  --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
206
  --shadow-color: rgba(0, 0, 0, 0.3);
207
  --shadow-light: 0 4px 15px var(--shadow-color);
208
  --shadow-medium: 0 6px 25px var(--shadow-color);
209
+ --backdrop-blur: 10px;
210
  }
211
  * { box-sizing: border-box; margin: 0; padding: 0; }
212
  html {
 
218
  background: var(--bg-gradient);
219
  color: var(--text-color);
220
  padding: var(--padding-m);
221
+ padding-bottom: 120px;
222
  overscroll-behavior-y: none;
223
  -webkit-font-smoothing: antialiased;
224
  -moz-osx-font-smoothing: grayscale;
225
+ visibility: hidden;
226
  min-height: 100vh;
227
  }
228
  .container {
 
241
  }
242
  .logo { display: flex; align-items: center; gap: var(--padding-s); }
243
  .logo img {
244
+ width: 50px;
245
  height: 50px;
246
  border-radius: 50%;
247
  background-color: var(--card-bg-solid);
 
249
  border: 2px solid rgba(255, 255, 255, 0.15);
250
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
251
  }
252
+ .logo span { font-size: 1.6em; font-weight: 700; letter-spacing: -0.5px; }
253
  .btn {
254
  display: inline-flex; align-items: center; justify-content: center;
255
  padding: 12px var(--padding-m); border-radius: var(--border-radius-m);
256
  background: var(--accent-gradient); color: var(--tg-theme-button-text-color);
257
+ text-decoration: none; font-weight: 600;
258
  border: none; cursor: pointer;
259
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
260
  gap: 8px; font-size: 1em;
 
287
  background-color: var(--card-bg);
288
  border-radius: var(--border-radius-l);
289
  padding: var(--padding-l);
290
+ margin-bottom: 0;
291
  box-shadow: var(--shadow-medium);
292
  border: 1px solid rgba(255, 255, 255, 0.08);
293
  backdrop-filter: blur(var(--backdrop-blur));
294
  -webkit-backdrop-filter: blur(var(--backdrop-blur));
295
  }
296
  .section-title {
297
+ font-size: 2em;
298
  font-weight: 700; margin-bottom: var(--padding-s); line-height: 1.25;
299
  letter-spacing: -0.6px;
300
  }
 
303
  margin-bottom: var(--padding-m);
304
  }
305
  .description {
306
+ font-size: 1.05em; line-height: 1.6; color: var(--text-secondary-color);
307
  margin-bottom: var(--padding-m);
308
  }
309
  .stats-grid {
 
323
  background-color: var(--card-bg-solid);
324
  padding: var(--padding-m); border-radius: var(--border-radius-m);
325
  margin-bottom: var(--padding-s); display: flex; align-items: center;
326
+ gap: var(--padding-m);
327
  font-size: 1.1em; font-weight: 500;
328
  border: 1px solid rgba(255, 255, 255, 0.08);
329
  transition: background-color 0.2s ease, transform 0.2s ease;
 
339
  }
340
  .save-card-button {
341
  position: fixed;
342
+ bottom: 30px;
343
  left: 50%;
344
  transform: translateX(-50%);
345
+ padding: 14px 28px;
346
+ border-radius: 30px;
347
  background: var(--accent-gradient-green);
348
  color: var(--tg-theme-button-text-color);
349
  text-decoration: none;
 
352
  cursor: pointer;
353
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
354
  z-index: 1000;
355
+ box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.5);
356
+ font-size: 1.05em;
357
  display: flex;
358
  align-items: center;
359
+ gap: 10px;
360
  backdrop-filter: blur(5px);
361
  -webkit-backdrop-filter: blur(5px);
362
  }
363
  .save-card-button:hover {
364
  opacity: 0.95;
365
+ transform: translateX(-50%) scale(1.05);
366
  box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.3);
367
  }
368
  .save-card-button i { font-size: 1.2em; }
 
 
369
  .modal {
370
  display: none; position: fixed; z-index: 1001;
371
  left: 0; top: 0; width: 100%; height: 100%;
372
+ overflow: auto; background-color: rgba(0,0,0,0.7);
373
  backdrop-filter: blur(8px);
374
  -webkit-backdrop-filter: blur(8px);
375
  animation: fadeIn 0.3s ease-out;
 
396
  .modal-text { font-size: 1.2em; line-height: 1.6; margin-bottom: var(--padding-s); word-wrap: break-word; }
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
  .icon { display: inline-block; width: 1.2em; text-align: center; margin-right: 8px; opacity: 0.9; }
400
  .icon-save::before { content: '💾'; }
401
  .icon-web::before { content: '🌐'; }
 
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
  <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">
 
558
  <i class="icon icon-save"></i>Сохранить визитку
559
  </button>
560
 
 
561
  <div id="saveModal" class="modal">
562
  <div class="modal-content">
563
  <span class="modal-close" id="modal-close-btn">×</span>
 
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
  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);
 
588
  const b = parseInt(bgColor.slice(5, 7), 16);
589
  root.style.setProperty('--tg-theme-bg-color-rgb', `${r}, ${g}, ${b}`);
590
  } catch (e) {
591
+ root.style.setProperty('--tg-theme-bg-color-rgb', `18, 18, 18`);
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) {
642
  console.error("Telegram WebApp script not loaded or initData is missing.");
643
  const greetingElement = document.getElementById('greeting');
644
  if(greetingElement) greetingElement.textContent = 'Не удалось связаться с Telegram.';
 
645
  document.body.style.visibility = 'visible';
646
  return;
647
  }
 
652
  applyTheme(tg.themeParams);
653
  tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
654
 
 
655
  fetch('/verify', {
656
  method: 'POST',
657
  headers: {
 
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;
686
  const greetingElement = document.getElementById('greeting');
687
  if (user) {
 
692
  console.warn('Telegram User data not available (initDataUnsafe.user is empty).');
693
  }
694
 
 
695
  const contactButtons = document.querySelectorAll('.contact-link');
696
  contactButtons.forEach(button => {
697
  button.addEventListener('click', (e) => {
698
  e.preventDefault();
699
+ tg.openTelegramLink('https://t.me/morshenkhan');
700
  });
701
  });
702
 
 
703
  const modal = document.getElementById("saveModal");
704
  const saveCardBtn = document.getElementById("save-card-btn");
705
  const closeBtn = document.getElementById("modal-close-btn");
 
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
  }
770
 
 
780
  if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
781
  document.body.style.visibility = 'visible';
782
  }
783
+ }, 3500);
784
  }
 
785
  </script>
786
  </body>
787
  </html>
 
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
  }
 
849
  width: 80px; height: 80px;
850
  border-radius: 50%; margin-bottom: 1rem;
851
  object-fit: cover; border: 3px solid var(--admin-border);
852
+ background-color: #eee;
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; }
 
877
  transition: background-color 0.2s ease;
878
  }
879
  .refresh-btn:hover { background-color: #0b5ed7; }
 
 
880
  .admin-controls {
881
  background: var(--admin-card-bg);
882
  padding: var(--padding);
 
906
  .admin-controls .loader {
907
  border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid var(--admin-primary);
908
  width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle;
909
+ display: none;
910
  }
911
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
912
  </style>
 
935
  {% if user.username %}
936
  <div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" style="color: inherit; text-decoration: none;">@{{ user.username }}</a></div>
937
  {% else %}
938
+ <div class="username" style="height: 1.3em;"></div>
939
  {% endif %}
940
  <div class="details">
941
  <div class="detail-item"><strong>ID:</strong> {{ user.id }}</div>
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>
 
968
  statusMessage.textContent = data.message;
969
  statusMessage.style.color = 'var(--admin-success)';
970
  if (action === 'скачивание') {
971
+ setTimeout(() => location.reload(), 1500);
972
  }
973
  } else {
974
  throw new Error(data.message || 'Произошла ошибка');
 
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>
989
  """
990
 
 
991
  @app.route('/')
992
  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
  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
  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()
 
1134
  users_list = list(current_data.values())
1135
  return render_template_string(ADMIN_TEMPLATE, users=users_list)
1136
 
1137
  @app.route('/admin/download_data', methods=['POST'])
1138
  def admin_trigger_download():
 
1139
  success = download_data_from_hf()
1140
  if success:
1141
  return jsonify({"status": "ok", "message": "Скачивание данных с Hugging Face завершено. Страница будет обновлена."})
 
1144
 
1145
  @app.route('/admin/upload_data', methods=['POST'])
1146
  def admin_trigger_upload():
 
1147
  if not HF_TOKEN_WRITE:
1148
  return jsonify({"status": "error", "message": "HF_TOKEN_WRITE не настроен на сервере."}), 400
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)
1177
  backup_thread.start()
 
1180
  print("--- Periodic backup disabled (HF_TOKEN_WRITE missing).")
1181
 
1182
  print("--- Server Ready ---")
1183
+ app.run(host=HOST, port=PORT, debug=False)