Aleksmorshen commited on
Commit
52c3f76
·
verified ·
1 Parent(s): 089e63e

Upload app (29).py

Browse files
Files changed (1) hide show
  1. app (29).py +1047 -0
app (29).py ADDED
@@ -0,0 +1,1047 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for
6
+ import hmac
7
+ import hashlib
8
+ import json
9
+ from urllib.parse import unquote, parse_qs, quote
10
+ import time
11
+ from datetime import datetime
12
+ import logging
13
+ 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
39
+ if not HF_TOKEN_READ:
40
+ logging.warning("HF_TOKEN_READ not set. Skipping Hugging Face download.")
41
+ return False
42
+ try:
43
+ logging.info(f"Attempting to download {HF_DATA_FILE_PATH} from {REPO_ID}...")
44
+ hf_hub_download(
45
+ repo_id=REPO_ID,
46
+ filename=HF_DATA_FILE_PATH,
47
+ repo_type="dataset",
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:
59
+ visitor_data_cache = json.load(f)
60
+ logging.info("Successfully loaded downloaded data into cache.")
61
+ except (FileNotFoundError, json.JSONDecodeError) as e:
62
+ logging.error(f"Error reading downloaded data file: {e}. Starting with empty cache.")
63
+ visitor_data_cache = {}
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)
80
+ logging.info("Visitor data loaded from local JSON.")
81
+ except FileNotFoundError:
82
+ logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.")
83
+ visitor_data_cache = {}
84
+ except json.JSONDecodeError:
85
+ logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.")
86
+ visitor_data_cache = {}
87
+ except Exception as e:
88
+ logging.error(f"Unexpected error loading visitor data: {e}")
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
+
106
+ def upload_data_to_hf():
107
+ if not HF_TOKEN_WRITE:
108
+ logging.warning("HF_TOKEN_WRITE not set. Skipping Hugging Face upload.")
109
+ return
110
+ if not os.path.exists(DATA_FILE):
111
+ logging.warning(f"{DATA_FILE} does not exist. Skipping upload.")
112
+ return
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.")
120
+ return
121
+
122
+ logging.info(f"Attempting to upload {DATA_FILE} to {REPO_ID}/{HF_DATA_FILE_PATH}...")
123
+ api.upload_file(
124
+ path_or_fileobj=DATA_FILE,
125
+ path_in_repo=HF_DATA_FILE_PATH,
126
+ repo_id=REPO_ID,
127
+ repo_type="dataset",
128
+ token=HF_TOKEN_WRITE,
129
+ commit_message=f"Update visitor data {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
130
+ )
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
+
141
+ def periodic_backup():
142
+ if not HF_TOKEN_WRITE:
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)
154
+ received_hash = parsed_data.pop('hash', [None])[0]
155
+
156
+ if not received_hash:
157
+ return None, False
158
+
159
+ data_check_list = []
160
+ for key, value in sorted(parsed_data.items()):
161
+ data_check_list.append(f"{key}={value[0]}")
162
+ data_check_string = "\n".join(data_check_list)
163
+
164
+ secret_key = hmac.new("WebAppData".encode(), BOT_TOKEN.encode(), hashlib.sha256).digest()
165
+ calculated_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
166
+
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}")
175
+ return parsed_data, False
176
+ except Exception as e:
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">
184
+ <head>
185
+ <meta charset="UTF-8">
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">
192
+ <style>
193
+ :root {
194
+ --tg-theme-bg-color: {{ theme.bg_color | default('#121212') }};
195
+ --tg-theme-text-color: {{ theme.text_color | default('#ffffff') }};
196
+ --tg-theme-hint-color: {{ theme.hint_color | default('#aaaaaa') }};
197
+ --tg-theme-link-color: {{ theme.link_color | default('#62bcf9') }};
198
+ --tg-theme-button-color: {{ theme.button_color | default('#31a5f5') }};
199
+ --tg-theme-button-text-color: {{ theme.button_text_color | default('#ffffff') }};
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);
207
+ --accent-gradient: linear-gradient(95deg, var(--tg-theme-button-color, #007aff), #5856d6);
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 {
224
+ background-color: var(--tg-theme-bg-color);
225
+ scroll-behavior: smooth;
226
+ }
227
+ body {
228
+ font-family: var(--font-family);
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 {
240
+ max-width: 650px;
241
+ margin: 0 auto;
242
+ display: flex;
243
+ flex-direction: column;
244
+ gap: var(--padding-l);
245
+ }
246
+ .header {
247
+ display: flex;
248
+ justify-content: space-between;
249
+ align-items: center;
250
+ margin-bottom: var(--padding-m);
251
+ padding: var(--padding-s) 0;
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);
259
+ object-fit: cover;
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;
272
+ box-shadow: var(--shadow-light);
273
+ letter-spacing: 0.3px;
274
+ }
275
+ .btn:hover {
276
+ opacity: 0.9;
277
+ box-shadow: var(--shadow-medium);
278
+ transform: translateY(-2px);
279
+ }
280
+ .btn-secondary {
281
+ background: var(--card-bg);
282
+ color: var(--tg-theme-link-color);
283
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1), var(--shadow-light);
284
+ }
285
+ .btn-secondary:hover {
286
+ background: rgba(44, 44, 46, 0.95);
287
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2), var(--shadow-medium);
288
+ }
289
+ .tag {
290
+ display: inline-block; background: var(--tag-bg); color: var(--text-secondary-color);
291
+ padding: 6px 12px; border-radius: var(--border-radius-s); font-size: 0.85em;
292
+ margin: 4px 6px 4px 0; white-space: nowrap;
293
+ font-weight: 500;
294
+ border: 1px solid rgba(255, 255, 255, 0.05);
295
+ }
296
+ .tag i { margin-right: 5px; opacity: 0.8; }
297
+ .section-card {
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
+ }
312
+ .section-subtitle {
313
+ font-size: 1.2em; font-weight: 500; color: var(--text-secondary-color);
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 {
321
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
322
+ gap: var(--padding-m); margin-top: var(--padding-m); text-align: center;
323
+ }
324
+ .stat-item {
325
+ background-color: rgba(255, 255, 255, 0.05);
326
+ padding: var(--padding-m); border-radius: var(--border-radius-m);
327
+ border: 1px solid rgba(255, 255, 255, 0.05);
328
+ transition: background-color 0.2s ease;
329
+ }
330
+ .stat-item:hover { background-color: rgba(255, 255, 255, 0.08); }
331
+ .stat-value { font-size: 1.7em; font-weight: 600; display: block; color: var(--tg-theme-link-color);}
332
+ .stat-label { font-size: 0.9em; color: var(--text-secondary-color); display: block; margin-top: 4px;}
333
+ .list-item {
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;
341
+ }
342
+ .list-item:hover {
343
+ background-color: rgba(44, 44, 46, 0.9);
344
+ transform: translateX(3px);
345
+ }
346
+ .list-item i { font-size: 1.4em; color: var(--accent-gradient-start, #34c759); opacity: 0.9; width: 25px; text-align: center;}
347
+ .footer-greeting {
348
+ text-align: center; color: var(--text-secondary-color); font-size: 0.95em;
349
+ margin-top: var(--padding-l); padding-bottom: var(--padding-l);
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;
361
+ font-weight: 600;
362
+ border: none;
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;
389
+ }
390
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
391
+ .modal-content {
392
+ background-color: var(--card-bg-solid); color: var(--text-color);
393
+ margin: 15% auto; padding: var(--padding-l);
394
+ border: 1px solid rgba(255, 255, 255, 0.1);
395
+ width: 88%; max-width: 480px;
396
+ border-radius: var(--border-radius-l);
397
+ text-align: center; position: relative;
398
+ box-shadow: var(--shadow-medium);
399
+ animation: scaleUp 0.3s ease-out;
400
+ }
401
+ @keyframes scaleUp { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
402
+ .modal-close {
403
+ color: var(--text-secondary-color);
404
+ position: absolute; top: 15px; right: 20px;
405
+ font-size: 32px; font-weight: bold; cursor: pointer;
406
+ line-height: 1; transition: color 0.2s ease;
407
+ }
408
+ .modal-close:hover, .modal-close:focus { color: var(--text-color); }
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: '🌐'; }
417
+ .icon-mobile::before { content: '📱'; }
418
+ .icon-code::before { content: '💻'; }
419
+ .icon-ai::before { content: '🧠'; }
420
+ .icon-quantum::before { content: '⚛️'; }
421
+ .icon-business::before { content: '💼'; }
422
+ .icon-speed::before { content: '⚡️'; }
423
+ .icon-complexity::before { content: '🧩'; }
424
+ .icon-experience::before { content: '⏳'; }
425
+ .icon-clients::before { content: '👥'; }
426
+ .icon-market::before { content: '📈'; }
427
+ .icon-location::before { content: '📍'; }
428
+ .icon-global::before { content: '🌍'; }
429
+ .icon-innovation::before { content: '💡'; }
430
+ .icon-contact::before { content: '💬'; }
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; }
439
+ .btn { padding: 10px var(--padding-m); font-size: 0.95em; }
440
+ .save-card-button { padding: 12px 24px; font-size: 1em; bottom: 20px; }
441
+ .stats-grid { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: var(--padding-s); }
442
+ .stat-value { font-size: 1.5em; }
443
+ .modal-content { margin: 25% auto; width: 92%; }
444
+ }
445
+ </style>
446
+ </head>
447
+ <body>
448
+ <div class="container">
449
+
450
+ <section class="morshen-group-intro section-card">
451
+ <div class="header">
452
+ <div class="logo">
453
+ <img src="https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg" alt="Morshen Group Logo">
454
+ <span>Morshen Group</span>
455
+ </div>
456
+ <a href="#" class="btn contact-link"><i class="icon icon-contact"></i>Связаться</a>
457
+ </div>
458
+ <div>
459
+ <span class="tag"><i class="icon icon-leader"></i>Лидер инноваций 2025</span>
460
+ </div>
461
+ <h1 class="section-title">Международный IT холдинг</h1>
462
+ <p class="description">
463
+ Объединяем передовые технологические компании для создания инновационных
464
+ решений мирового уровня. Мы строим будущее технологий сегодня.
465
+ </p>
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">
472
+ <h2 class="section-title"><i class="icon icon-company"></i>Экосистема инноваций</h2>
473
+ <p class="description">
474
+ В состав холдинга входят компании, специализирующиеся на различных
475
+ направлениях передовых технологий, создавая синергию для прорывных решений.
476
+ </p>
477
+ </section>
478
+
479
+ <section class="section-card">
480
+ <div class="logo" style="margin-bottom: var(--padding-m);">
481
+ <img src="https://huggingface.co/spaces/Aleksmorshen/Telemap8/resolve/main/morshengroup.jpg" alt="Morshen Alpha Logo">
482
+ <span style="font-size: 1.4em; font-weight: 600;">Morshen Alpha</span>
483
+ </div>
484
+ <div style="margin-bottom: var(--padding-m);">
485
+ <span class="tag"><i class="icon icon-ai"></i>Искусственный интеллект</span>
486
+ <span class="tag"><i class="icon icon-quantum"></i>Квантовые технологии</span>
487
+ <span class="tag"><i class="icon icon-business"></i>Бизнес-решения</span>
488
+ </div>
489
+ <p class="description">
490
+ Флагманская компания холдинга. Разрабатываем передовые бизнес-решения, проводим R&D в сфере AI
491
+ и квантовых технологий. Наши инновации формируют будущее индустрии.
492
+ </p>
493
+ <div class="stats-grid">
494
+ <div class="stat-item">
495
+ <span class="stat-value"><i class="icon icon-global"></i> 3+</span>
496
+ <span class="stat-label">Страны присутствия</span>
497
+ </div>
498
+ <div class="stat-item">
499
+ <span class="stat-value"><i class="icon icon-clients"></i> 3K+</span>
500
+ <span class="stat-label">Готовых клиентов</span>
501
+ </div>
502
+ <div class="stat-item">
503
+ <span class="stat-value"><i class="icon icon-market"></i> 5+</span>
504
+ <span class="stat-label">Лет на рынке</span>
505
+ </div>
506
+ </div>
507
+ </section>
508
+
509
+ <section class="section-card">
510
+ <div class="logo" style="margin-bottom: var(--padding-m);">
511
+ <img src="https://huggingface.co/spaces/holmgardstudio/dev/resolve/main/image.jpg" alt="Holmgard Logo" style="width: 50px; height: 50px;">
512
+ <span style="font-size: 1.4em; font-weight: 600;">Holmgard Studio</span>
513
+ </div>
514
+ <div style="margin-bottom: var(--padding-m);">
515
+ <span class="tag"><i class="icon icon-web"></i>Веб-разработка</span>
516
+ <span class="tag"><i class="icon icon-mobile"></i>Мобильные приложения</span>
517
+ <span class="tag"><i class="icon icon-code"></i>ПО на заказ</span>
518
+ </div>
519
+ <p class="description">
520
+ Инновационная студия разработки, создающая высокотехнологичные веб-сайты,
521
+ мобильные приложения и ПО для бизнеса любого масштаба.
522
+ Используем передовые технологии и гибкие методологии.
523
+ </p>
524
+ <div class="stats-grid">
525
+ <div class="stat-item">
526
+ <span class="stat-value"><i class="icon icon-experience"></i> 10+</span>
527
+ <span class="stat-label">Лет опыта</span>
528
+ </div>
529
+ <div class="stat-item">
530
+ <span class="stat-value"><i class="icon icon-complexity"></i> PRO</span>
531
+ <span class="stat-label">Любая сложность</span>
532
+ </div>
533
+ <div class="stat-item">
534
+ <span class="stat-value"><i class="icon icon-speed"></i> FAST</span>
535
+ <span class="stat-label">Высокая скорость</span>
536
+ </div>
537
+ </div>
538
+ <div style="display: flex; gap: var(--padding-s); margin-top: var(--padding-m); flex-wrap: wrap;">
539
+ <a href="https://holmgard.ru" target="_blank" class="btn btn-secondary" style="flex-grow: 1;"><i class="icon icon-link"></i>Веб-сайт студии</a>
540
+ <a href="#" class="btn contact-link" style="flex-grow: 1;"><i class="icon icon-contact"></i>Связаться</a>
541
+ </div>
542
+ </section>
543
+
544
+ <section class="section-card">
545
+ <h2 class="section-title"><i class="icon icon-global"></i>Глобальное присутствие</h2>
546
+ <p class="description">Наши инновационные решения и экспертиза доступны в странах Центральной Азии и за ее пределами:</p>
547
+ <div>
548
+ <div class="list-item"><i class="icon icon-location"></i>Узбекистан</div>
549
+ <div class="list-item"><i class="icon icon-location"></i>Казахстан</div>
550
+ <div class="list-item"><i class="icon icon-location"></i>Кыргызстан</div>
551
+ <div class="list-item"><i class="icon icon-innovation"></i>Расширяем горизонты...</div>
552
+ </div>
553
+ </section>
554
+
555
+ <footer class="footer-greeting">
556
+ <p id="greeting">Инициализация...</p>
557
+ </footer>
558
+
559
+ </div>
560
+
561
+ <button class="save-card-button" id="save-card-btn">
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>
569
+ <p class="modal-text"><b>+996 500 398 754</b></p>
570
+ <p class="modal-text">Morshen Group</p>
571
+ <p class="modal-text" style="font-size: 1em; color: var(--text-secondary-color);">Международный IT Холдинг</p>
572
+ <p class="modal-instruction">Сделайте скриншот экрана, чтобы сохранить контакт.</p>
573
+ </div>
574
+ </div>
575
+
576
+
577
+ <script>
578
+ const tg = window.Telegram.WebApp;
579
+
580
+ function applyTheme(themeParams) {
581
+ const root = document.documentElement;
582
+ root.style.setProperty('--tg-theme-bg-color', themeParams.bg_color || '#121212');
583
+ root.style.setProperty('--tg-theme-text-color', themeParams.text_color || '#ffffff');
584
+ root.style.setProperty('--tg-theme-hint-color', themeParams.hint_color || '#aaaaaa');
585
+ root.style.setProperty('--tg-theme-link-color', themeParams.link_color || '#62bcf9');
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);
594
+ const g = parseInt(bgColor.slice(3, 5), 16);
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
+ }
611
+
612
+ tg.ready();
613
+ tg.expand();
614
+
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: {
622
+ 'Content-Type': 'application/json',
623
+ 'Accept': 'application/json'
624
+ },
625
+ body: JSON.stringify({ initData: tg.initData }),
626
+ })
627
+ .then(response => {
628
+ if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
629
+ return response.json();
630
+ })
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) {
649
+ const name = user.first_name || user.username || 'Гость';
650
+ greetingElement.textContent = `Добро пожаловать, ${name}! 👋`;
651
+ } else {
652
+ greetingElement.textContent = 'Добро пожаловать!';
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");
669
+
670
+ if (saveCardBtn && modal && closeBtn) {
671
+ saveCardBtn.addEventListener('click', (e) => {
672
+ e.preventDefault();
673
+ modal.style.display = "block";
674
+ if (tg.HapticFeedback) {
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
+
695
+ if (window.Telegram && window.Telegram.WebApp) {
696
+ setupTelegram();
697
+ } else {
698
+ console.warn("Telegram WebApp script not immediately available, waiting for window.onload");
699
+ window.addEventListener('load', setupTelegram);
700
+ setTimeout(() => {
701
+ if (document.body.style.visibility !== 'visible') {
702
+ console.error("Telegram WebApp script fallback timeout triggered.");
703
+ const greetingElement = document.getElementById('greeting');
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>
713
+ """
714
+
715
+ ADMIN_TEMPLATE = """
716
+ <!DOCTYPE html>
717
+ <html lang="ru">
718
+ <head>
719
+ <meta charset="UTF-8">
720
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
721
+ <title>Admin - Посетители</title>
722
+ <link rel="preconnect" href="https://fonts.googleapis.com">
723
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
724
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
725
+ <style>
726
+ :root {
727
+ --admin-bg: #f8f9fa;
728
+ --admin-text: #212529;
729
+ --admin-card-bg: #ffffff;
730
+ --admin-border: #dee2e6;
731
+ --admin-shadow: rgba(0, 0, 0, 0.05);
732
+ --admin-primary: #0d6efd;
733
+ --admin-secondary: #6c757d;
734
+ --admin-success: #198754;
735
+ --admin-danger: #dc3545;
736
+ --admin-warning: #ffc107;
737
+ --border-radius: 12px;
738
+ --padding: 1.5rem;
739
+ --font-family: 'Inter', sans-serif;
740
+ }
741
+ body {
742
+ font-family: var(--font-family);
743
+ background-color: var(--admin-bg);
744
+ color: var(--admin-text);
745
+ margin: 0;
746
+ padding: var(--padding);
747
+ line-height: 1.6;
748
+ }
749
+ .container { max-width: 1140px; margin: 0 auto; }
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
+ }
757
+ .user-card {
758
+ background-color: var(--admin-card-bg);
759
+ border-radius: var(--border-radius);
760
+ padding: var(--padding);
761
+ box-shadow: 0 4px 15px var(--admin-shadow);
762
+ border: 1px solid var(--admin-border);
763
+ display: flex;
764
+ flex-direction: column;
765
+ align-items: center;
766
+ text-align: center;
767
+ transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
768
+ }
769
+ .user-card:hover {
770
+ transform: translateY(-5px);
771
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
772
+ }
773
+ .user-card img {
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; }
786
+ .alert {
787
+ background-color: #fff3cd; border-left: 6px solid var(--admin-warning);
788
+ margin-bottom: var(--padding); padding: 1rem 1.5rem;
789
+ color: #664d03; border-radius: 8px; text-align: center; font-weight: 500;
790
+ }
791
+ .refresh-btn {
792
+ display: block;
793
+ margin: 1rem auto;
794
+ padding: 10px 20px;
795
+ font-size: 1em;
796
+ font-weight: 500;
797
+ color: #fff;
798
+ background-color: var(--admin-primary);
799
+ border: none;
800
+ border-radius: 8px;
801
+ cursor: pointer;
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);
810
+ border-radius: var(--border-radius);
811
+ box-shadow: 0 4px 15px var(--admin-shadow);
812
+ border: 1px solid var(--admin-border);
813
+ margin-bottom: var(--padding);
814
+ text-align: center;
815
+ }
816
+ .admin-controls h2 { margin-top: 0; margin-bottom: 1rem; font-weight: 600; color: var(--admin-secondary); }
817
+ .admin-controls .btn {
818
+ padding: 10px 18px;
819
+ font-size: 0.95em;
820
+ font-weight: 500;
821
+ border: none;
822
+ border-radius: 8px;
823
+ cursor: pointer;
824
+ transition: all 0.2s ease;
825
+ margin: 0.5rem;
826
+ color: #fff;
827
+ }
828
+ .admin-controls .btn-backup { background-color: var(--admin-success); }
829
+ .admin-controls .btn-backup:hover { background-color: #157347; }
830
+ .admin-controls .btn-download { background-color: var(--admin-primary); }
831
+ .admin-controls .btn-download:hover { background-color: #0b5ed7; }
832
+ .admin-controls .status { font-size: 0.9em; margin-top: 1rem; color: var(--admin-secondary); }
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>
840
+ </head>
841
+ <body>
842
+ <div class="container">
843
+ <h1>Посетители Mini App</h1>
844
+ <div class="alert">ВНИМАНИЕ: Этот раздел не защищен! Добавьте аутентификацию для реального использования.</div>
845
+
846
+ <div class="admin-controls">
847
+ <h2>Управление данными</h2>
848
+ <button class="btn btn-download" onclick="triggerDownload()">Скачать данные с HF</button>
849
+ <button class="btn btn-backup" onclick="triggerUpload()">Загрузить данные на HF</button>
850
+ <div class="loader" id="loader"></div>
851
+ <div class="status" id="status-message"></div>
852
+ </div>
853
+
854
+ <button class="refresh-btn" onclick="location.reload()">Обновить список</button>
855
+
856
+ {% if users %}
857
+ <div class="user-grid">
858
+ {% for user in users|sort(attribute='visited_at', reverse=true) %}
859
+ <div class="user-card">
860
+ <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">
861
+ <div class="name">{{ user.first_name or '' }} {{ user.last_name or '' }}</div>
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>
875
+ {% endfor %}
876
+ </div>
877
+ {% else %}
878
+ <p class="no-users">Данных о посетителях пока нет.</p>
879
+ {% endif %}
880
+ </div>
881
+
882
+ <script>
883
+ const loader = document.getElementById('loader');
884
+ const statusMessage = document.getElementById('status-message');
885
+
886
+ async function handleFetch(url, action) {
887
+ loader.style.display = 'inline-block';
888
+ statusMessage.textContent = `Выполняется ${action}...`;
889
+ statusMessage.style.color = 'var(--admin-secondary)';
890
+ try {
891
+ const response = await fetch(url, { method: 'POST' });
892
+ const data = await response.json();
893
+ if (response.ok && data.status === 'ok') {
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 || 'Произошла ошибка');
901
+ }
902
+ } catch (error) {
903
+ statusMessage.textContent = `Ошибка ${action}: ${error.message}`;
904
+ statusMessage.style.color = 'var(--admin-danger)';
905
+ console.error(`Error during ${action}:`, error);
906
+ } finally {
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:
934
+ req_data = request.get_json()
935
+ init_data_str = req_data.get('initData')
936
+ if not init_data_str:
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:
944
+ user_json_str = unquote(user_data_parsed['user'][0])
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 завершено. Страница будет обновлена."})
993
+ else:
994
+ return jsonify({"status": "error", "message": "Ошибка скачивания данных с Hugging Face. Проверьте логи."}), 500
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()
1039
+ print("--- Periodic backup thread started (every hour).")
1040
+ else:
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