Aleksmorshen commited on
Commit
d99d567
·
verified ·
1 Parent(s): 7e98c44

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +537 -137
app.py CHANGED
@@ -14,28 +14,103 @@ 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
@@ -48,60 +123,76 @@ 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:
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:
@@ -113,7 +204,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.")
@@ -126,15 +217,13 @@ def upload_data_to_hf():
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
 
@@ -143,11 +232,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 +255,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 +265,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">
@@ -199,8 +286,8 @@ TEMPLATE = """
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);
@@ -208,16 +295,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 +316,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 +339,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 +347,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 +385,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 +401,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 +421,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 +437,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,17 +450,17 @@ 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; }
@@ -382,7 +469,7 @@ TEMPLATE = """
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;
@@ -587,7 +674,6 @@ TEMPLATE = """
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,7 +681,7 @@ 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
 
@@ -604,7 +690,6 @@ TEMPLATE = """
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 +700,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: {
@@ -633,16 +717,12 @@ TEMPLATE = """
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 +733,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");
@@ -704,7 +782,7 @@ TEMPLATE = """
704
  if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
705
  document.body.style.visibility = 'visible';
706
  }
707
- }, 3500); // Slightly longer timeout
708
  }
709
 
710
  </script>
@@ -718,7 +796,7 @@ ADMIN_TEMPLATE = """
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">
@@ -734,6 +812,7 @@ ADMIN_TEMPLATE = """
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;
@@ -747,25 +826,28 @@ ADMIN_TEMPLATE = """
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);
@@ -774,7 +856,7 @@ 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; }
@@ -782,7 +864,7 @@ ADMIN_TEMPLATE = """
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;
@@ -833,14 +915,105 @@ 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>
840
  </head>
841
  <body>
842
  <div class="container">
843
- <h1>Посетители Mini App</h1>
844
  <div class="alert">ВНИМАНИЕ: Этот раздел не защищен! Добавьте аутентификацию для реального использования.</div>
845
 
846
  <div class="admin-controls">
@@ -851,8 +1024,9 @@ ADMIN_TEMPLATE = """
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) %}
@@ -862,7 +1036,7 @@ 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>
@@ -877,11 +1051,75 @@ ADMIN_TEMPLATE = """
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';
@@ -894,7 +1132,9 @@ 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 || 'Произошла ошибка');
@@ -903,6 +1143,7 @@ ADMIN_TEMPLATE = """
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
  }
@@ -915,17 +1156,115 @@ ADMIN_TEMPLATE = """
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'])
@@ -951,9 +1290,8 @@ def verify_data():
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'),
@@ -961,12 +1299,11 @@ def verify_data():
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:
@@ -979,14 +1316,29 @@ def verify_data():
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,53 +1347,101 @@ 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()
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
 
14
  from huggingface_hub import HfApi, hf_hub_download
15
  from huggingface_hub.utils import RepositoryNotFoundError
16
 
17
+ from aiogram import Bot, Dispatcher, executor, types
18
+ import asyncio
19
+
20
+ import base64
21
+ from google import genai
22
+ from google.genai import types
23
+
24
+ BOT_TOKEN = os.getenv("BOT_TOKEN", "YOUR_DEFAULT_BOT_TOKEN")
25
  HOST = '0.0.0.0'
26
  PORT = 7860
27
+ DATA_FILE = 'data.json'
28
 
 
29
  REPO_ID = "flpolprojects/teledata"
30
+ HF_DATA_FILE_PATH = "data.json"
31
+ HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
32
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
33
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
34
 
35
  app = Flask(__name__)
36
  logging.basicConfig(level=logging.INFO)
37
+ app.secret_key = os.urandom(24)
38
 
 
39
  _data_lock = threading.Lock()
40
+ visitor_data_cache = {}
41
+ chat_history_cache = []
42
+
43
+ bot = Bot(token=BOT_TOKEN)
44
+ dp = Dispatcher(bot)
45
+
46
+ APP_CONTEXT_TEXT = """
47
+ Morshen Group: Международный IT холдинг.
48
+ Объединяем передовые технологические компании для создания инновационных
49
+ решений мирового уровня. Мы строим будущее технологий сегодня.
50
+ Лидер инноваций 2025.
51
+ Контакт: Telegram @morshenkhan, Телефон +996 500 398 754.
52
+
53
+ Экосистема инноваций: В состав холдинга входят компании, специализирующиеся на различных
54
+ направлениях передовых технологий, создавая синергию для прорывных решений.
55
+
56
+ Morshen Alpha: Флагманская компания холдинга.
57
+ Специализация: Искусственный интеллект, Квантовые технологии, Бизнес-решения.
58
+ Деятельность: Разрабатываем передовые бизнес-решения, проводим R&D в сфере AI
59
+ и квантовых технологий. Наши инновации формируют будущее индустрии.
60
+ Статистика: 3+ Страны присутствия, 3K+ Готовых клиентов, 5+ Лет на рынке.
61
+
62
+ Holmgard Studio: Инновационная студия разработки.
63
+ Специализация: Веб-разработка, Мобильные приложения, ПО на заказ.
64
+ Деятельность: Создает высокотехнологичные веб-сайты,
65
+ мобильные приложения и ПО для бизнеса любого масштаба.
66
+ Использует передовые технологии и гибкие методологии.
67
+ Статистика: 10+ Лет опыта, PRO Любая сложность, FAST Высокая скорость.
68
+ Веб-сайт: https://holmgard.ru. Контакт через @morshenkhan.
69
+
70
+ Глобальное присутствие: Наши инновационные решения и экспертиза доступны в странах Центральной Азии и за ее пределами:
71
+ Узбекистан
72
+ Казахстан
73
+ Кыргызстан
74
+ Расширяем горизонты...
75
+
76
+ Сохранить визитку: Телефон +996 500 398 754, Morshen Group, Международный IT Холдинг. Сделайте скриншот экрана.
77
+ """
78
+
79
+ def generate_ai_response(prompt_text):
80
+ if not GEMINI_API_KEY:
81
+ logging.error("GEMINI_API_KEY not set. Cannot generate AI response.")
82
+ return "Ошибка: Сервер не настроен для ответа на вопросы (отсутствует API ключ)."
83
+ try:
84
+ client = genai.Client(api_key=GEMINI_API_KEY)
85
+ model = "learnlm-2.0-flash-experimental"
86
+ contextualized_prompt = f"""
87
+ Основываясь на следующей информации о Morshen Group и ее дочерних компаниях, ответь на вопрос пользователя.
88
+ Если информация отсутствует, так и скажи. Не придумывай информацию, которой нет в тексте.
89
+ Отвечай на русском языке.
90
+
91
+ Информация о Morshen Group:
92
+ {APP_CONTEXT_TEXT}
93
+
94
+ Вопрос пользователя: {prompt_text}
95
+
96
+ Ответ:
97
+ """
98
+ contents = [types.Content(role="user", parts=[types.Part.from_text(text=contextualized_prompt)])]
99
+ generate_content_config = types.GenerateContentConfig(response_mime_type="text/plain")
100
+
101
+ response = client.models.generate_content(
102
+ model=model,
103
+ contents=contents,
104
+ config=generate_content_config,
105
+ stream=False # Use non-streaming for simplicity in this context
106
+ )
107
+ return response.text.strip()
108
+ except Exception as e:
109
+ logging.error(f"Error generating AI response: {e}")
110
+ return f"Произошла ошибка при генерации ответа: {e}"
111
 
112
  def download_data_from_hf():
113
+ global visitor_data_cache, chat_history_cache
114
  if not HF_TOKEN_READ:
115
  logging.warning("HF_TOKEN_READ not set. Skipping Hugging Face download.")
116
  return False
 
123
  token=HF_TOKEN_READ,
124
  local_dir=".",
125
  local_dir_use_symlinks=False,
126
+ force_download=True,
127
+ etag_timeout=10
128
  )
129
  logging.info("Data file successfully downloaded from Hugging Face.")
 
130
  with _data_lock:
131
  try:
132
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
133
+ full_data = json.load(f)
134
+ visitor_data_cache = full_data.get("visitors", {})
135
+ chat_history_cache = full_data.get("chats", [])
136
  logging.info("Successfully loaded downloaded data into cache.")
137
  except (FileNotFoundError, json.JSONDecodeError) as e:
138
  logging.error(f"Error reading downloaded data file: {e}. Starting with empty cache.")
139
  visitor_data_cache = {}
140
+ chat_history_cache = []
141
  return True
142
  except RepositoryNotFoundError:
143
  logging.error(f"Hugging Face repository '{REPO_ID}' not found. Cannot download data.")
 
144
  except Exception as e:
145
  logging.error(f"Error downloading data from Hugging Face: {e}")
 
146
  return False
147
 
148
  def load_visitor_data():
149
+ global visitor_data_cache, chat_history_cache
150
  with _data_lock:
151
+ if not visitor_data_cache or not chat_history_cache:
152
  try:
153
  with open(DATA_FILE, 'r', encoding='utf-8') as f:
154
+ full_data = json.load(f)
155
+ visitor_data_cache = full_data.get("visitors", {})
156
+ chat_history_cache = full_data.get("chats", [])
157
+ logging.info("Data loaded from local JSON.")
158
  except FileNotFoundError:
159
  logging.warning(f"{DATA_FILE} not found locally. Starting with empty data.")
160
  visitor_data_cache = {}
161
+ chat_history_cache = []
162
  except json.JSONDecodeError:
163
  logging.error(f"Error decoding {DATA_FILE}. Starting with empty data.")
164
  visitor_data_cache = {}
165
+ chat_history_cache = []
166
  except Exception as e:
167
+ logging.error(f"Unexpected error loading data: {e}")
168
  visitor_data_cache = {}
169
+ chat_history_cache = []
170
+ return {"visitors": visitor_data_cache, "chats": chat_history_cache}
171
 
172
+
173
+ def save_visitor_data(data_to_update):
174
  with _data_lock:
175
  try:
176
+ visitor_data_cache.update(data_to_update)
177
+ full_data = {"visitors": visitor_data_cache, "chats": chat_history_cache}
 
178
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
179
+ json.dump(full_data, f, ensure_ascii=False, indent=4)
180
+ logging.info(f"Data successfully saved to {DATA_FILE}.")
181
+ upload_data_to_hf_async()
 
182
  except Exception as e:
183
+ logging.error(f"Error saving data: {e}")
184
+
185
+ def save_chat_message(chat_entry):
186
+ with _data_lock:
187
+ try:
188
+ chat_history_cache.append(chat_entry)
189
+ full_data = {"visitors": visitor_data_cache, "chats": chat_history_cache}
190
+ with open(DATA_FILE, 'w', encoding='utf-8') as f:
191
+ json.dump(full_data, f, ensure_ascii=False, indent=4)
192
+ logging.info(f"Chat message successfully saved to {DATA_FILE}.")
193
+ upload_data_to_hf_async()
194
+ except Exception as e:
195
+ logging.error(f"Error saving chat message: {e}")
196
 
197
  def upload_data_to_hf():
198
  if not HF_TOKEN_WRITE:
 
204
 
205
  try:
206
  api = HfApi()
207
+ with _data_lock:
208
  file_content_exists = os.path.getsize(DATA_FILE) > 0
209
  if not file_content_exists:
210
  logging.warning(f"{DATA_FILE} is empty. Skipping upload.")
 
217
  repo_id=REPO_ID,
218
  repo_type="dataset",
219
  token=HF_TOKEN_WRITE,
220
+ commit_message=f"Update data {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
221
  )
222
+ logging.info("Data successfully uploaded to Hugging Face.")
223
  except Exception as e:
224
  logging.error(f"Error uploading data to Hugging Face: {e}")
 
225
 
226
  def upload_data_to_hf_async():
 
227
  upload_thread = threading.Thread(target=upload_data_to_hf, daemon=True)
228
  upload_thread.start()
229
 
 
232
  logging.info("Periodic backup disabled: HF_TOKEN_WRITE not set.")
233
  return
234
  while True:
235
+ time.sleep(3600)
236
  logging.info("Initiating periodic backup...")
237
  upload_data_to_hf()
238
 
 
239
  def verify_telegram_data(init_data_str):
240
  try:
241
  parsed_data = parse_qs(init_data_str)
 
255
  if calculated_hash == received_hash:
256
  auth_date = int(parsed_data.get('auth_date', [0])[0])
257
  current_time = int(time.time())
258
+ if current_time - auth_date > 86400:
259
+ logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}).")
260
  return parsed_data, True
261
  else:
262
  logging.warning(f"Data verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
 
265
  logging.error(f"Error verifying Telegram data: {e}")
266
  return None, False
267
 
 
268
  TEMPLATE = """
269
  <!DOCTYPE html>
270
  <html lang="ru">
 
286
  --tg-theme-button-text-color: {{ theme.button_text_color | default('#ffffff') }};
287
  --tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#1e1e1e') }};
288
 
289
+ --bg-gradient: linear-gradient(160deg, #1a232f 0%, var(--tg-theme-bg-color, #121212) 100%);
290
+ --card-bg: rgba(44, 44, 46, 0.8);
291
  --card-bg-solid: #2c2c2e;
292
  --text-color: var(--tg-theme-text-color);
293
  --text-secondary-color: var(--tg-theme-hint-color);
 
295
  --accent-gradient-green: linear-gradient(95deg, #34c759, #30d158);
296
  --tag-bg: rgba(255, 255, 255, 0.1);
297
  --border-radius-s: 8px;
298
+ --border-radius-m: 14px;
299
+ --border-radius-l: 18px;
300
  --padding-s: 10px;
301
+ --padding-m: 18px;
302
+ --padding-l: 28px;
303
  --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
304
  --shadow-color: rgba(0, 0, 0, 0.3);
305
  --shadow-light: 0 4px 15px var(--shadow-color);
306
  --shadow-medium: 0 6px 25px var(--shadow-color);
307
+ --backdrop-blur: 10px;
308
  }
309
  * { box-sizing: border-box; margin: 0; padding: 0; }
310
  html {
 
316
  background: var(--bg-gradient);
317
  color: var(--text-color);
318
  padding: var(--padding-m);
319
+ padding-bottom: 120px;
320
  overscroll-behavior-y: none;
321
  -webkit-font-smoothing: antialiased;
322
  -moz-osx-font-smoothing: grayscale;
323
+ visibility: hidden;
324
  min-height: 100vh;
325
  }
326
  .container {
 
339
  }
340
  .logo { display: flex; align-items: center; gap: var(--padding-s); }
341
  .logo img {
342
+ width: 50px;
343
  height: 50px;
344
  border-radius: 50%;
345
  background-color: var(--card-bg-solid);
 
347
  border: 2px solid rgba(255, 255, 255, 0.15);
348
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
349
  }
350
+ .logo span { font-size: 1.6em; font-weight: 700; letter-spacing: -0.5px; }
351
  .btn {
352
  display: inline-flex; align-items: center; justify-content: center;
353
  padding: 12px var(--padding-m); border-radius: var(--border-radius-m);
354
  background: var(--accent-gradient); color: var(--tg-theme-button-text-color);
355
+ text-decoration: none; font-weight: 600;
356
  border: none; cursor: pointer;
357
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
358
  gap: 8px; font-size: 1em;
 
385
  background-color: var(--card-bg);
386
  border-radius: var(--border-radius-l);
387
  padding: var(--padding-l);
388
+ margin-bottom: 0;
389
  box-shadow: var(--shadow-medium);
390
  border: 1px solid rgba(255, 255, 255, 0.08);
391
  backdrop-filter: blur(var(--backdrop-blur));
392
  -webkit-backdrop-filter: blur(var(--backdrop-blur));
393
  }
394
  .section-title {
395
+ font-size: 2em;
396
  font-weight: 700; margin-bottom: var(--padding-s); line-height: 1.25;
397
  letter-spacing: -0.6px;
398
  }
 
401
  margin-bottom: var(--padding-m);
402
  }
403
  .description {
404
+ font-size: 1.05em; line-height: 1.6; color: var(--text-secondary-color);
405
  margin-bottom: var(--padding-m);
406
  }
407
  .stats-grid {
 
421
  background-color: var(--card-bg-solid);
422
  padding: var(--padding-m); border-radius: var(--border-radius-m);
423
  margin-bottom: var(--padding-s); display: flex; align-items: center;
424
+ gap: var(--padding-m);
425
  font-size: 1.1em; font-weight: 500;
426
  border: 1px solid rgba(255, 255, 255, 0.08);
427
  transition: background-color 0.2s ease, transform 0.2s ease;
 
437
  }
438
  .save-card-button {
439
  position: fixed;
440
+ bottom: 30px;
441
  left: 50%;
442
  transform: translateX(-50%);
443
+ padding: 14px 28px;
444
+ border-radius: 30px;
445
  background: var(--accent-gradient-green);
446
  color: var(--tg-theme-button-text-color);
447
  text-decoration: none;
 
450
  cursor: pointer;
451
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
452
  z-index: 1000;
453
+ box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.5);
454
+ font-size: 1.05em;
455
  display: flex;
456
  align-items: center;
457
+ gap: 10px;
458
  backdrop-filter: blur(5px);
459
  -webkit-backdrop-filter: blur(5px);
460
  }
461
  .save-card-button:hover {
462
  opacity: 0.95;
463
+ transform: translateX(-50%) scale(1.05);
464
  box-shadow: var(--shadow-medium), 0 0 0 4px rgba(var(--tg-theme-bg-color-rgb, 18, 18, 18), 0.3);
465
  }
466
  .save-card-button i { font-size: 1.2em; }
 
469
  .modal {
470
  display: none; position: fixed; z-index: 1001;
471
  left: 0; top: 0; width: 100%; height: 100%;
472
+ overflow: auto; background-color: rgba(0,0,0,0.7);
473
  backdrop-filter: blur(8px);
474
  -webkit-backdrop-filter: blur(8px);
475
  animation: fadeIn 0.3s ease-out;
 
674
  root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
675
  root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#1e1e1e');
676
 
 
677
  try {
678
  const bgColor = themeParams.bg_color || '#121212';
679
  const r = parseInt(bgColor.slice(1, 3), 16);
 
681
  const b = parseInt(bgColor.slice(5, 7), 16);
682
  root.style.setProperty('--tg-theme-bg-color-rgb', `${r}, ${g}, ${b}`);
683
  } catch (e) {
684
+ root.style.setProperty('--tg-theme-bg-color-rgb', `18, 18, 18`);
685
  }
686
  }
687
 
 
690
  console.error("Telegram WebApp script not loaded or initData is missing.");
691
  const greetingElement = document.getElementById('greeting');
692
  if(greetingElement) greetingElement.textContent = 'Не удалось связаться с Telegram.';
 
693
  document.body.style.visibility = 'visible';
694
  return;
695
  }
 
700
  applyTheme(tg.themeParams);
701
  tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
702
 
 
703
  fetch('/verify', {
704
  method: 'POST',
705
  headers: {
 
717
  console.log('Backend verification successful.');
718
  } else {
719
  console.warn('Backend verification failed:', data.message);
 
720
  }
721
  })
722
  .catch(error => {
723
  console.error('Error sending initData for verification:', error);
 
724
  });
725
 
 
 
726
  const user = tg.initDataUnsafe?.user;
727
  const greetingElement = document.getElementById('greeting');
728
  if (user) {
 
733
  console.warn('Telegram User data not available (initDataUnsafe.user is empty).');
734
  }
735
 
 
736
  const contactButtons = document.querySelectorAll('.contact-link');
737
  contactButtons.forEach(button => {
738
  button.addEventListener('click', (e) => {
739
  e.preventDefault();
740
+ tg.openTelegramLink('https://t.me/morshenkhan');
741
  });
742
  });
743
 
 
744
  const modal = document.getElementById("saveModal");
745
  const saveCardBtn = document.getElementById("save-card-btn");
746
  const closeBtn = document.getElementById("modal-close-btn");
 
782
  if(greetingElement) greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
783
  document.body.style.visibility = 'visible';
784
  }
785
+ }, 3500);
786
  }
787
 
788
  </script>
 
796
  <head>
797
  <meta charset="UTF-8">
798
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
799
+ <title>Admin - Посетители и Чаты</title>
800
  <link rel="preconnect" href="https://fonts.googleapis.com">
801
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
802
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
 
812
  --admin-success: #198754;
813
  --admin-danger: #dc3545;
814
  --admin-warning: #ffc107;
815
+ --admin-info: #0dcaf0;
816
  --border-radius: 12px;
817
  --padding: 1.5rem;
818
  --font-family: 'Inter', sans-serif;
 
826
  line-height: 1.6;
827
  }
828
  .container { max-width: 1140px; margin: 0 auto; }
829
+ h1, h2 { text-align: center; color: var(--admin-secondary); margin-bottom: var(--padding); font-weight: 600; }
830
+ h2 { margin-top: var(--padding); border-top: 1px solid var(--admin-border); padding-top: var(--padding); }
831
  .user-grid {
832
  display: grid;
833
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
834
  gap: var(--padding);
835
  margin-top: var(--padding);
836
  }
837
+ .user-card, .chat-card {
838
  background-color: var(--admin-card-bg);
839
  border-radius: var(--border-radius);
840
  padding: var(--padding);
841
  box-shadow: 0 4px 15px var(--admin-shadow);
842
  border: 1px solid var(--admin-border);
 
 
 
 
843
  transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
844
  }
845
+ .user-card {
846
+ display: flex;
847
+ flex-direction: column;
848
+ align-items: center;
849
+ text-align: center;
850
+ }
851
  .user-card:hover {
852
  transform: translateY(-5px);
853
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
 
856
  width: 80px; height: 80px;
857
  border-radius: 50%; margin-bottom: 1rem;
858
  object-fit: cover; border: 3px solid var(--admin-border);
859
+ background-color: #eee;
860
  }
861
  .user-card .name { font-weight: 600; font-size: 1.2em; margin-bottom: 0.3rem; color: var(--admin-primary); }
862
  .user-card .username { color: var(--admin-secondary); margin-bottom: 0.8rem; font-size: 0.95em; }
 
864
  .user-card .detail-item { margin-bottom: 0.3rem; }
865
  .user-card .detail-item strong { color: var(--admin-text); }
866
  .user-card .timestamp { font-size: 0.8em; color: var(--admin-secondary); margin-top: 1rem; }
867
+ .no-users, .no-chats { text-align: center; color: var(--admin-secondary); margin-top: 2rem; font-size: 1.1em; }
868
  .alert {
869
  background-color: #fff3cd; border-left: 6px solid var(--admin-warning);
870
  margin-bottom: var(--padding); padding: 1rem 1.5rem;
 
915
  .admin-controls .loader {
916
  border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid var(--admin-primary);
917
  width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle;
918
+ display: none;
919
  }
920
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
921
+
922
+ /* Chat History Styles */
923
+ .chat-history { margin-top: var(--padding); }
924
+ .chat-message {
925
+ margin-bottom: var(--padding-s);
926
+ padding: var(--padding-s) var(--padding-m);
927
+ border-radius: var(--border-radius-s);
928
+ max-width: 85%;
929
+ word-wrap: break-word;
930
+ }
931
+ .chat-message.user {
932
+ background-color: var(--admin-primary);
933
+ color: white;
934
+ align-self: flex-start;
935
+ margin-right: auto;
936
+ }
937
+ .chat-message.bot {
938
+ background-color: #e9ecef;
939
+ color: var(--admin-text);
940
+ align-self: flex-end;
941
+ margin-left: auto;
942
+ }
943
+ .chat-message .sender { font-weight: 600; margin-bottom: 4px; font-size: 0.9em;}
944
+ .chat-message .text { margin-bottom: 4px; font-size: 1em;}
945
+ .chat-message .time { font-size: 0.75em; color: rgba(255, 255, 255, 0.7); text-align: right; display: block;}
946
+ .chat-message.bot .time { color: #495057; }
947
+
948
+ .chat-thread {
949
+ background-color: var(--admin-card-bg);
950
+ border-radius: var(--border-radius);
951
+ padding: var(--padding);
952
+ box-shadow: 0 4px 15px var(--admin-shadow);
953
+ border: 1px solid var(--admin-border);
954
+ margin-bottom: var(--padding);
955
+ }
956
+ .chat-thread-header {
957
+ font-weight: 600;
958
+ font-size: 1.1em;
959
+ margin-bottom: var(--padding-m);
960
+ padding-bottom: var(--padding-s);
961
+ border-bottom: 1px solid var(--admin-border);
962
+ display: flex;
963
+ justify-content: space-between;
964
+ align-items: center;
965
+ }
966
+ .chat-thread-header span { color: var(--admin-primary); }
967
+ .chat-thread-messages {
968
+ display: flex;
969
+ flex-direction: column;
970
+ gap: var(--padding-s);
971
+ }
972
+
973
+ /* Send Message Form */
974
+ .send-message-form {
975
+ margin-top: var(--padding);
976
+ padding: var(--padding);
977
+ background: var(--admin-card-bg);
978
+ border-radius: var(--border-radius);
979
+ box-shadow: 0 4px 15px var(--admin-shadow);
980
+ border: 1px solid var(--admin-border);
981
+ }
982
+ .send-message-form h3 { margin-top: 0; margin-bottom: 1rem; color: var(--admin-secondary); font-weight: 600;}
983
+ .send-message-form label { display: block; margin-bottom: 0.5rem; font-weight: 500;}
984
+ .send-message-form select, .send-message-form textarea {
985
+ width: 100%;
986
+ padding: 0.75rem;
987
+ margin-bottom: 1rem;
988
+ border: 1px solid var(--admin-border);
989
+ border-radius: 8px;
990
+ font-family: var(--font-family);
991
+ font-size: 1em;
992
+ box-sizing: border-box;
993
+ }
994
+ .send-message-form textarea { min-height: 100px; resize: vertical; }
995
+ .send-message-form button {
996
+ display: inline-block;
997
+ padding: 10px 20px;
998
+ font-size: 1em;
999
+ font-weight: 500;
1000
+ color: #fff;
1001
+ background-color: var(--admin-success);
1002
+ border: none;
1003
+ border-radius: 8px;
1004
+ cursor: pointer;
1005
+ transition: background-color 0.2s ease;
1006
+ }
1007
+ .send-message-form button:hover { background-color: #157347; }
1008
+ .send-message-status { margin-top: 1rem; text-align: center; font-weight: 500;}
1009
+ .status-success { color: var(--admin-success); }
1010
+ .status-error { color: var(--admin-danger); }
1011
+ .status-info { color: var(--admin-info); }
1012
  </style>
1013
  </head>
1014
  <body>
1015
  <div class="container">
1016
+ <h1>Admin Panel</h1>
1017
  <div class="alert">ВНИМАНИЕ: Этот раздел не защищен! Добавьте аутентификацию для реального использования.</div>
1018
 
1019
  <div class="admin-controls">
 
1024
  <div class="status" id="status-message"></div>
1025
  </div>
1026
 
1027
+ <button class="refresh-btn" onclick="location.reload()">Обновить страницу</button>
1028
 
1029
+ <h2>Посетители Mini App</h2>
1030
  {% if users %}
1031
  <div class="user-grid">
1032
  {% for user in users|sort(attribute='visited_at', reverse=true) %}
 
1036
  {% if user.username %}
1037
  <div class="username"><a href="https://t.me/{{ user.username }}" target="_blank" style="color: inherit; text-decoration: none;">@{{ user.username }}</a></div>
1038
  {% else %}
1039
+ <div class="username" style="height: 1.3em;"></div>
1040
  {% endif %}
1041
  <div class="details">
1042
  <div class="detail-item"><strong>ID:</strong> {{ user.id }}</div>
 
1051
  {% else %}
1052
  <p class="no-users">Данных о посетителях пока нет.</p>
1053
  {% endif %}
1054
+
1055
+ <h2>История чатов с ботом</h2>
1056
+ {% if chats %}
1057
+ <div class="chat-history">
1058
+ {% for chat_id, messages in chats|dictsort %}
1059
+ <div class="chat-thread">
1060
+ <div class="chat-thread-header">
1061
+ <span>Чат ID: {{ chat_id }}</span>
1062
+ {% set chat_user = users_by_id.get(chat_id|int) %}
1063
+ {% if chat_user %}
1064
+ <span>Пользователь:
1065
+ {% if chat_user.username %}
1066
+ <a href="https://t.me/{{ chat_user.username }}" target="_blank" style="color: var(--admin-secondary); text-decoration: none;">@{{ chat_user.username }}</a>
1067
+ {% else %}
1068
+ {{ chat_user.first_name or 'Неизвестный' }}
1069
+ {% endif %}
1070
+ ({{ chat_user.id }})
1071
+ </span>
1072
+ {% else %}
1073
+ <span>Пользователь: Неизвестен ({{ chat_id }})</span>
1074
+ {% endif %}
1075
+ </div>
1076
+ <div class="chat-thread-messages">
1077
+ {% for message in messages %}
1078
+ <div class="chat-message {{ 'user' if message.role == 'user' else 'bot' }}">
1079
+ <div class="sender">{{ message.sender_name }}</div>
1080
+ <div class="text">{{ message.text }}</div>
1081
+ <div class="time">{{ message.timestamp_str }}</div>
1082
+ </div>
1083
+ {% endfor %}
1084
+ </div>
1085
+ </div>
1086
+ {% endfor %}
1087
+ </div>
1088
+ {% else %}
1089
+ <p class="no-chats">История чатов пуста.</p>
1090
+ {% endif %}
1091
+
1092
+ <div class="send-message-form">
1093
+ <h3>Отправить сообщение пользователю</h3>
1094
+ <form id="sendMessageForm">
1095
+ <label for="chat_id">Выберите пользователя (Chat ID):</label>
1096
+ <select id="chat_id" name="chat_id" required>
1097
+ <option value="">-- Выберите --</option>
1098
+ {% for user in users|sort(attribute='first_name') %}
1099
+ <option value="{{ user.id }}">
1100
+ {{ user.first_name or '' }} {{ user.last_name or '' }}
1101
+ {% if user.username %} (@{{ user.username }}) {% endif %}
1102
+ (ID: {{ user.id }})
1103
+ </option>
1104
+ {% endfor %}
1105
+ </select>
1106
+
1107
+ <label for="message_text">Текст сообщения:</label>
1108
+ <textarea id="message_text" name="message_text" required></textarea>
1109
+
1110
+ <button type="submit">Отправить</button>
1111
+ </form>
1112
+ <div class="send-message-status" id="sendMessageStatus"></div>
1113
+ </div>
1114
+
1115
+
1116
  </div>
1117
 
1118
  <script>
1119
  const loader = document.getElementById('loader');
1120
  const statusMessage = document.getElementById('status-message');
1121
+ const sendMessageForm = document.getElementById('sendMessageForm');
1122
+ const sendMessageStatus = document.getElementById('sendMessageStatus');
1123
 
1124
  async function handleFetch(url, action) {
1125
  loader.style.display = 'inline-block';
 
1132
  statusMessage.textContent = data.message;
1133
  statusMessage.style.color = 'var(--admin-success)';
1134
  if (action === 'скачивание') {
1135
+ setTimeout(() => location.reload(), 1500);
1136
+ } else {
1137
+ setTimeout(() => statusMessage.textContent = '', 3000);
1138
  }
1139
  } else {
1140
  throw new Error(data.message || 'Произошла ошибка');
 
1143
  statusMessage.textContent = `Ошибка ${action}: ${error.message}`;
1144
  statusMessage.style.color = 'var(--admin-danger)';
1145
  console.error(`Error during ${action}:`, error);
1146
+ setTimeout(() => statusMessage.textContent = '', 5000);
1147
  } finally {
1148
  loader.style.display = 'none';
1149
  }
 
1156
  function triggerUpload() {
1157
  handleFetch('/admin/upload_data', 'загрузка');
1158
  }
1159
+
1160
+ sendMessageForm.addEventListener('submit', async function(event) {
1161
+ event.preventDefault();
1162
+ const chatId = document.getElementById('chat_id').value;
1163
+ const messageText = document.getElementById('message_text').value;
1164
+
1165
+ if (!chatId || !messageText.trim()) {
1166
+ sendMessageStatus.textContent = 'Выберите пользователя и введите текст сообщения.';
1167
+ sendMessageStatus.className = 'send-message-status status-warning';
1168
+ return;
1169
+ }
1170
+
1171
+ sendMessageStatus.textContent = 'Отправка сообщения...';
1172
+ sendMessageStatus.className = 'send-message-status status-info';
1173
+
1174
+ try {
1175
+ const response = await fetch('/admin/send_message', {
1176
+ method: 'POST',
1177
+ headers: { 'Content-Type': 'application/json' },
1178
+ body: JSON.stringify({ chat_id: chatId, message: messageText })
1179
+ });
1180
+ const data = await response.json();
1181
+
1182
+ if (response.ok && data.status === 'ok') {
1183
+ sendMessageStatus.textContent = 'Сообщение успешно отправлено!';
1184
+ sendMessageStatus.className = 'send-message-status status-success';
1185
+ document.getElementById('message_text').value = '';
1186
+ setTimeout(() => location.reload(), 1500); // Reload to show sent message
1187
+ } else {
1188
+ throw new Error(data.message || 'Неизвестная ошибка');
1189
+ }
1190
+ } catch (error) {
1191
+ sendMessageStatus.textContent = `Ошибка отправки: ${error.message}`;
1192
+ sendMessageStatus.className = 'send-message-status status-error';
1193
+ console.error('Error sending message:', error);
1194
+ }
1195
+ });
1196
+
1197
  </script>
1198
  </body>
1199
  </html>
1200
  """
1201
 
1202
+ @dp.message_handler()
1203
+ async def handle_messages(message: types.Message):
1204
+ user_id = message.from_user.id
1205
+ user_full_name = message.from_user.full_name or f"User {user_id}"
1206
+ chat_id = message.chat.id
1207
+ message_text = message.text
1208
+
1209
+ now = time.time()
1210
+ timestamp_str = datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
1211
+
1212
+ save_chat_message({
1213
+ 'chat_id': chat_id,
1214
+ 'role': 'user',
1215
+ 'sender_id': user_id,
1216
+ 'sender_name': user_full_name,
1217
+ 'text': message_text,
1218
+ 'timestamp': now,
1219
+ 'timestamp_str': timestamp_str
1220
+ })
1221
+
1222
+ logging.info(f"Received message from user {user_id} ({user_full_name}): {message_text}")
1223
+
1224
+ try:
1225
+ ai_response_text = generate_ai_response(message_text)
1226
+ await message.answer(ai_response_text)
1227
+
1228
+ now_bot = time.time()
1229
+ timestamp_str_bot = datetime.fromtimestamp(now_bot).strftime('%Y-%m-%d %H:%M:%S')
1230
+ save_chat_message({
1231
+ 'chat_id': chat_id,
1232
+ 'role': 'bot',
1233
+ 'sender_id': bot.id,
1234
+ 'sender_name': "Bot",
1235
+ 'text': ai_response_text,
1236
+ 'timestamp': now_bot,
1237
+ 'timestamp_str': timestamp_str_bot
1238
+ })
1239
+ logging.info(f"Sent AI response to user {user_id}: {ai_response_text}")
1240
+
1241
+ except Exception as e:
1242
+ logging.error(f"Error handling message for user {user_id}: {e}")
1243
+ await message.answer("Произошла ошибка при обработке вашего запроса.")
1244
+ now_bot = time.time()
1245
+ timestamp_str_bot = datetime.fromtimestamp(now_bot).strftime('%Y-%m-%d %H:%M:%S')
1246
+ save_chat_message({
1247
+ 'chat_id': chat_id,
1248
+ 'role': 'bot',
1249
+ 'sender_id': bot.id,
1250
+ 'sender_name': "Bot",
1251
+ 'text': "Произошла ошибка при обработке вашего запроса.",
1252
+ 'timestamp': now_bot,
1253
+ 'timestamp_str': timestamp_str_bot
1254
+ })
1255
+
1256
+
1257
+ def run_bot():
1258
+ try:
1259
+ logging.info("Starting aiogram bot polling...")
1260
+ executor.start_polling(dp, skip_updates=True)
1261
+ except Exception as e:
1262
+ logging.error(f"Error starting aiogram bot polling: {e}")
1263
+
1264
+
1265
  @app.route('/')
1266
  def index():
1267
+ theme_params = {}
 
 
1268
  return render_template_string(TEMPLATE, theme=theme_params)
1269
 
1270
  @app.route('/verify', methods=['POST'])
 
1290
  user_id = user_info_dict.get('id')
1291
  if user_id:
1292
  now = time.time()
 
1293
  user_entry = {
1294
+ str(user_id): {
1295
  'id': user_id,
1296
  'first_name': user_info_dict.get('first_name'),
1297
  'last_name': user_info_dict.get('last_name'),
 
1299
  'photo_url': user_info_dict.get('photo_url'),
1300
  'language_code': user_info_dict.get('language_code'),
1301
  'is_premium': user_info_dict.get('is_premium', False),
1302
+ 'phone_number': user_info_dict.get('phone_number'),
1303
  'visited_at': now,
1304
  'visited_at_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
1305
  }
1306
  }
 
1307
  save_visitor_data(user_entry)
1308
  return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
1309
  else:
 
1316
 
1317
  @app.route('/admin')
1318
  def admin_panel():
1319
+ full_data = load_visitor_data()
1320
+ users_list = list(full_data.get("visitors", {}).values())
1321
+ chat_entries = full_data.get("chats", [])
1322
+
1323
+ chats_by_id = {}
1324
+ for chat in chat_entries:
1325
+ chat_id = str(chat.get('chat_id'))
1326
+ if chat_id not in chats_by_id:
1327
+ chats_by_id[chat_id] = []
1328
+ chats_by_id[chat_id].append(chat)
1329
+
1330
+ for chat_id in chats_by_id:
1331
+ chats_by_id[chat_id].sort(key=lambda x: x.get('timestamp', 0))
1332
+
1333
+ users_by_id = {user['id']: user for user in users_list}
1334
+
1335
+ return render_template_string(ADMIN_TEMPLATE,
1336
+ users=users_list,
1337
+ chats=chats_by_id,
1338
+ users_by_id=users_by_id)
1339
 
1340
  @app.route('/admin/download_data', methods=['POST'])
1341
  def admin_trigger_download():
 
1342
  success = download_data_from_hf()
1343
  if success:
1344
  return jsonify({"status": "ok", "message": "Скачивание данных с Hugging Face завершено. Страница будет обновлена."})
 
1347
 
1348
  @app.route('/admin/upload_data', methods=['POST'])
1349
  def admin_trigger_upload():
 
1350
  if not HF_TOKEN_WRITE:
1351
  return jsonify({"status": "error", "message": "HF_TOKEN_WRITE не настроен на сервере."}), 400
1352
+ upload_data_to_hf_async()
1353
  return jsonify({"status": "ok", "message": "Загрузка данных на Hugging Face запущена в фоновом режиме."})
1354
 
1355
+ @app.route('/admin/send_message', methods=['POST'])
1356
+ def admin_send_message():
1357
+ try:
1358
+ req_data = request.get_json()
1359
+ chat_id = req_data.get('chat_id')
1360
+ message_text = req_data.get('message')
1361
+
1362
+ if not chat_id or not message_text:
1363
+ return jsonify({"status": "error", "message": "Missing chat_id or message text"}), 400
1364
+
1365
+ try:
1366
+ chat_id = int(chat_id)
1367
+ except ValueError:
1368
+ return jsonify({"status": "error", "message": "Invalid chat ID"}), 400
1369
+
1370
+ loop = asyncio.new_event_loop()
1371
+ asyncio.set_event_loop(loop)
1372
+ try:
1373
+ loop.run_until_complete(bot.send_message(chat_id=chat_id, text=message_text))
1374
+
1375
+ now = time.time()
1376
+ timestamp_str = datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S')
1377
+ save_chat_message({
1378
+ 'chat_id': chat_id,
1379
+ 'role': 'admin_sent',
1380
+ 'sender_id': 'admin', # Or a specific admin identifier
1381
+ 'sender_name': 'Admin',
1382
+ 'text': message_text,
1383
+ 'timestamp': now,
1384
+ 'timestamp_str': timestamp_str
1385
+ })
1386
+
1387
+ return jsonify({"status": "ok", "message": "Сообщение успешно отправлено!"}), 200
1388
+ except Exception as e:
1389
+ logging.error(f"Error sending message via bot API: {e}")
1390
+ return jsonify({"status": "error", "message": f"Ошибка отправки сообщения через Bot API: {e}"}), 500
1391
+ finally:
1392
+ loop.close()
1393
+
1394
+
1395
+ except Exception as e:
1396
+ logging.exception("Error in /admin/send_message endpoint")
1397
+ return jsonify({"status": "error", "message": "Internal server error"}), 500
1398
+
1399
 
 
1400
  if __name__ == '__main__':
1401
+ logging.info("---")
1402
+ logging.info("--- MORSHEN GROUP MINI APP & BOT SERVER ---")
1403
+ logging.info("---")
1404
+ logging.info(f"Flask server starting on http://{HOST}:{PORT}")
1405
+ logging.info(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
1406
+ logging.info(f"Visitor data file: {DATA_FILE}")
1407
+ logging.info(f"Hugging Face Repo: {REPO_ID}")
1408
+ logging.info(f"HF Data Path: {HF_DATA_FILE_PATH}")
1409
  if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
1410
+ logging.warning("---")
1411
+ logging.warning("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---")
1412
+ logging.warning("--- Backup/restore functionality will be limited. Set HF_TOKEN_READ and HF_TOKEN_WRITE environment variables.")
1413
+ logging.warning("---")
1414
  else:
1415
+ logging.info("--- Hugging Face tokens found.")
1416
+ logging.info("--- Attempting initial data download from Hugging Face...")
 
1417
  download_data_from_hf()
1418
 
1419
+ if not GEMINI_API_KEY:
1420
+ logging.warning("---")
1421
+ logging.warning("--- WARNING: GEMINI_API_KEY NOT SET ---")
1422
+ logging.warning("--- AI response functionality will be disabled. Set GEMINI_API_KEY environment variable.")
1423
+ logging.warning("---")
1424
+ else:
1425
+ logging.info("--- GEMINI_API_KEY found.")
1426
+
1427
  load_visitor_data()
1428
 
1429
+ logging.warning("---")
1430
+ logging.warning("--- SECURITY WARNING ---")
1431
+ logging.warning("--- The /admin route and its sub-routes are NOT protected.")
1432
+ logging.warning("--- Implement proper authentication before deploying.")
1433
+ logging.warning("---")
1434
 
 
1435
  if HF_TOKEN_WRITE:
1436
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
1437
  backup_thread.start()
1438
+ logging.info("--- Periodic backup thread started (every hour).")
1439
  else:
1440
+ logging.info("--- Periodic backup disabled (HF_TOKEN_WRITE missing).")
1441
+
1442
+ bot_thread = threading.Thread(target=run_bot, daemon=True)
1443
+ bot_thread.start()
1444
+ logging.info("--- Telegram bot polling thread started.")
1445
 
1446
+ logging.info("--- Server Ready ---")
1447
+ app.run(host=HOST, port=PORT, debug=False)