Eluza133 commited on
Commit
c196225
·
verified ·
1 Parent(s): 69566fb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +516 -490
app.py CHANGED
@@ -2,17 +2,17 @@
2
  # -*- coding: utf-8 -*-
3
 
4
  import os
5
- import io
6
- from flask import Flask, request, Response, render_template_string, jsonify, session
7
  import hmac
8
  import hashlib
9
  import json
10
- from urllib.parse import unquote, parse_qs
11
  import time
12
  from datetime import datetime
13
  import logging
 
14
  from huggingface_hub import HfApi, hf_hub_download, list_repo_files
15
- from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError
16
  from werkzeug.utils import secure_filename
17
 
18
  # --- Configuration ---
@@ -21,26 +21,18 @@ HOST = '0.0.0.0'
21
  PORT = 7860
22
 
23
  # Hugging Face Settings
24
- REPO_ID = os.getenv("HF_REPO_ID", "Eluza133/Z1e1u")
25
  HF_UPLOAD_FOLDER = "uploads" # Base folder 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", HF_TOKEN_WRITE) # Read token (can be same as write)
 
 
 
28
 
29
  app = Flask(__name__)
30
  logging.basicConfig(level=logging.INFO)
31
- app.secret_key = os.getenv("FLASK_SECRET_KEY", os.urandom(24)) # Use env var or random
32
-
33
- # --- Hugging Face API Initialization ---
34
- hf_api = None
35
- if HF_TOKEN_WRITE:
36
- hf_api = HfApi(token=HF_TOKEN_WRITE)
37
- logging.info(f"Hugging Face API initialized for repo: {REPO_ID} with Write Token.")
38
- elif HF_TOKEN_READ:
39
- hf_api = HfApi(token=HF_TOKEN_READ)
40
- logging.info(f"Hugging Face API initialized for repo: {REPO_ID} with Read-Only Token.")
41
- else:
42
- logging.warning("HF_TOKEN_WRITE or HF_TOKEN_READ not set. Hugging Face operations will fail.")
43
-
44
 
45
  # --- Telegram Verification ---
46
  def verify_telegram_data(init_data_str):
@@ -49,8 +41,8 @@ def verify_telegram_data(init_data_str):
49
  received_hash = parsed_data.pop('hash', [None])[0]
50
 
51
  if not received_hash:
52
- logging.warning("Verification failed: Hash missing.")
53
- return None, False
54
 
55
  data_check_list = []
56
  for key, value in sorted(parsed_data.items()):
@@ -63,40 +55,38 @@ def verify_telegram_data(init_data_str):
63
  if calculated_hash == received_hash:
64
  auth_date = int(parsed_data.get('auth_date', [0])[0])
65
  current_time = int(time.time())
66
- # Allow data up to 24 hours old, adjust as needed
67
  if current_time - auth_date > 86400:
68
- logging.warning(f"Verification warning: Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}).")
69
- # Decide if this should be a failure: return parsed_data, False
70
- return parsed_data, True
 
71
  else:
72
- logging.warning(f"Verification failed: Hash mismatch. Calculated: {calculated_hash}, Received: {received_hash}")
73
- return parsed_data, False
74
  except Exception as e:
75
- logging.error(f"Error verifying Telegram data: {e}", exc_info=True)
76
- return None, False
77
 
78
- def get_user_id_from_initdata(init_data_str):
79
- parsed_data, is_valid = verify_telegram_data(init_data_str)
80
- if not is_valid or not parsed_data or 'user' not in parsed_data:
81
  return None
82
 
 
 
 
83
  try:
84
- user_json_str = unquote(parsed_data['user'][0])
85
- user_info_dict = json.loads(user_json_str)
86
- user_id = user_info_dict.get('id')
87
- if user_id:
88
- return str(user_id) # Return as string
89
- else:
90
- logging.error("User ID not found in parsed user data.")
91
- return None
92
- except Exception as e:
93
- logging.error(f"Could not parse user JSON or get ID: {e}", exc_info=True)
94
  return None
95
 
96
  # --- HTML Templates ---
97
  TEMPLATE = """
98
  <!DOCTYPE html>
99
- <html lang="en">
100
  <head>
101
  <meta charset="UTF-8">
102
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
@@ -104,7 +94,7 @@ TEMPLATE = """
104
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
105
  <link rel="preconnect" href="https://fonts.googleapis.com">
106
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
107
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
108
  <style>
109
  :root {
110
  --tg-theme-bg-color: {{ theme.bg_color | default('#ffffff') }};
@@ -114,473 +104,542 @@ TEMPLATE = """
114
  --tg-theme-button-color: {{ theme.button_color | default('#2481cc') }};
115
  --tg-theme-button-text-color: {{ theme.button_text_color | default('#ffffff') }};
116
  --tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#f1f1f1') }};
117
- --tg-color-scheme: {{ color_scheme | default('light') }};
118
-
119
- --bg-color: var(--tg-theme-bg-color);
120
- --text-color: var(--tg-theme-text-color);
121
- --hint-color: var(--tg-theme-hint-color);
122
- --button-bg: var(--tg-theme-button-color);
123
- --button-text: var(--tg-theme-button-text-color);
124
- --secondary-bg: var(--tg-theme-secondary-bg-color);
125
- --border-color: var(--tg-theme-hint-color);
126
- --link-color: var(--tg-theme-link-color);
127
- --error-color: #dc3545;
128
- --success-color: #198754;
129
- --font-family: 'Inter', sans-serif;
130
  --border-radius: 12px;
131
  --padding: 16px;
132
- }
133
-
134
- [data-theme="dark"] {
135
- --bg-color: #121212;
136
- --text-color: #ffffff;
137
- --hint-color: #aaaaaa;
138
- --link-color: #62bcf9;
139
- --button-bg: #31a5f5;
140
- --button-text: #ffffff;
141
- --secondary-bg: #1e1e1e;
142
- --border-color: #333333;
143
  }
144
 
145
  * { box-sizing: border-box; margin: 0; padding: 0; }
146
- html {
147
- background-color: var(--bg-color);
148
- color-scheme: var(--tg-color-scheme);
149
- }
150
  body {
151
  font-family: var(--font-family);
152
- background-color: var(--bg-color);
153
- color: var(--text-color);
154
  padding: var(--padding);
155
  overscroll-behavior-y: none;
156
- line-height: 1.6;
157
- visibility: hidden; /* Hide until ready */
 
158
  min-height: 100vh;
159
  }
 
160
  .container {
161
  max-width: 700px;
162
  margin: 0 auto;
163
  display: flex;
164
  flex-direction: column;
165
- gap: calc(var(--padding) * 1.5);
166
  }
167
- h1 {
 
168
  text-align: center;
169
- font-size: 1.8em;
 
 
 
 
170
  font-weight: 700;
171
- color: var(--link-color);
172
- margin-bottom: var(--padding);
 
 
 
 
 
173
  }
 
174
  .upload-section, .files-section {
175
- background-color: var(--secondary-bg);
176
  border-radius: var(--border-radius);
177
  padding: var(--padding);
178
- border: 1px solid var(--border-color);
 
179
  }
180
- h2 {
181
- font-size: 1.3em;
 
182
  font-weight: 600;
183
- margin-bottom: var(--padding);
184
- border-bottom: 1px solid var(--border-color);
185
- padding-bottom: 8px;
 
 
186
  }
187
- .file-input-wrapper {
188
- position: relative;
189
- overflow: hidden;
190
- display: inline-block;
191
- border: 2px dashed var(--hint-color);
 
 
 
 
 
 
 
 
 
 
 
 
192
  border-radius: var(--border-radius);
193
- padding: var(--padding);
194
- text-align: center;
195
- cursor: pointer;
196
- transition: border-color 0.3s ease;
197
- width: 100%;
198
- margin-bottom: var(--padding);
199
- }
200
- .file-input-wrapper:hover {
201
- border-color: var(--link-color);
202
- }
203
- .file-input-wrapper input[type=file] {
204
- position: absolute;
205
- left: 0; top: 0; opacity: 0;
206
- width: 100%; height: 100%;
207
  cursor: pointer;
208
- }
209
- .file-input-label {
210
- color: var(--hint-color);
211
  font-weight: 500;
 
 
212
  }
213
- .file-input-label span {
214
- display: block;
215
- font-size: 1.5em;
216
- margin-bottom: 8px;
217
  }
 
 
 
 
218
  #file-list-preview {
 
219
  font-size: 0.9em;
220
- color: var(--text-color);
221
- margin-top: 10px;
222
- max-height: 100px;
223
- overflow-y: auto;
224
- text-align: left;
225
- }
226
- #file-list-preview div {
227
- padding: 2px 0;
228
  white-space: nowrap;
229
  overflow: hidden;
230
  text-overflow: ellipsis;
231
- }
 
232
  .btn {
233
- display: block; width: 100%;
234
- padding: 12px var(--padding); border-radius: var(--border-radius);
235
- background: var(--button-bg); color: var(--button-text);
236
- text-decoration: none; font-weight: 600;
237
- border: none; cursor: pointer;
238
- transition: background-color 0.2s ease;
239
- font-size: 1em; text-align: center;
240
- }
241
- .btn:hover {
242
- opacity: 0.9;
 
 
243
  }
 
244
  .btn:disabled {
245
- background-color: var(--hint-color);
246
  cursor: not-allowed;
247
- }
248
- #status-message {
249
- margin-top: var(--padding);
250
- padding: 10px;
251
- border-radius: var(--border-radius);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  text-align: center;
253
- font-weight: 500;
254
- display: none; /* Hidden by default */
255
  }
256
- #status-message.success { background-color: var(--success-color); color: white; }
257
- #status-message.error { background-color: var(--error-color); color: white; }
258
- #status-message.loading { background-color: var(--secondary-bg); color: var(--text-color); border: 1px solid var(--border-color); }
259
 
260
- .file-list-container {
261
  list-style: none;
262
  padding: 0;
 
263
  }
 
264
  .file-item {
 
 
 
 
265
  display: flex;
266
- justify-content: space-between;
267
  align-items: center;
268
- padding: 10px;
269
- border-bottom: 1px solid var(--border-color);
 
270
  transition: background-color 0.2s ease;
271
  }
272
- .file-item:last-child { border-bottom: none; }
273
- .file-item:hover { background-color: rgba(128, 128, 128, 0.1); }
 
 
 
 
 
 
 
 
 
 
 
274
  .file-name {
 
 
 
 
275
  flex-grow: 1;
276
- margin-right: 15px;
277
- word-break: break-all; /* Wrap long names */
278
  }
279
- .file-link {
280
- color: var(--link-color);
 
281
  text-decoration: none;
282
- font-weight: 500;
283
- white-space: nowrap;
 
 
284
  }
285
- .file-link:hover { text-decoration: underline; }
286
- .loading-files, .no-files {
 
287
  text-align: center;
288
- color: var(--hint-color);
289
  padding: var(--padding);
290
- font-style: italic;
291
  }
292
- .loader {
293
- border: 4px solid var(--secondary-bg); border-radius: 50%; border-top: 4px solid var(--link-color);
294
- width: 20px; height: 20px; animation: spin 1s linear infinite; display: inline-block; margin-left: 10px; vertical-align: middle;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  }
296
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
297
  </style>
298
  </head>
299
  <body>
300
  <div class="container">
301
- <h1>☁️ Zeus Cloud</h1>
 
 
 
 
 
302
 
303
  <section class="upload-section">
304
- <h2>Upload Files</h2>
305
- <div class="file-input-wrapper">
306
- <input type="file" id="fileInput" multiple accept="*/*">
307
- <label for="fileInput" class="file-input-label">
308
- <span>📤</span>
309
- Click or Drag & Drop<br>
310
- (Max 20 files)
311
- </label>
312
- </div>
313
- <div id="file-list-preview">No files selected.</div>
314
- <button id="uploadButton" class="btn" disabled>Upload</button>
315
- <div id="status-message"></div>
316
  </section>
317
 
318
  <section class="files-section">
319
- <h2>My Files</h2>
320
- <ul class="file-list-container" id="myFilesList">
321
- <li class="loading-files">Loading files...</li>
322
- </ul>
323
  </section>
324
 
 
 
325
  </div>
326
 
327
  <script>
328
  const tg = window.Telegram.WebApp;
329
- const fileInput = document.getElementById('fileInput');
330
- const uploadButton = document.getElementById('uploadButton');
331
- const statusMessage = document.getElementById('status-message');
332
- const myFilesList = document.getElementById('myFilesList');
333
- const fileListPreview = document.getElementById('fileList-preview');
334
- let currentUserId = null;
335
- let currentInitData = null;
 
 
 
 
 
 
 
336
 
337
  function applyTheme(themeParams) {
338
  const root = document.documentElement;
339
- const colorScheme = themeParams.color_scheme || 'light';
340
- root.dataset.theme = colorScheme;
341
- root.style.setProperty('--tg-theme-bg-color', themeParams.bg_color || (colorScheme === 'dark' ? '#121212' : '#ffffff'));
342
- root.style.setProperty('--tg-theme-text-color', themeParams.text_color || (colorScheme === 'dark' ? '#ffffff' : '#000000'));
343
- root.style.setProperty('--tg-theme-hint-color', themeParams.hint_color || (colorScheme === 'dark' ? '#aaaaaa' : '#999999'));
344
- root.style.setProperty('--tg-theme-link-color', themeParams.link_color || (colorScheme === 'dark' ? '#62bcf9' : '#2481cc'));
345
- root.style.setProperty('--tg-theme-button-color', themeParams.button_color || (colorScheme === 'dark' ? '#31a5f5' : '#2481cc'));
346
- root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || (colorScheme === 'dark' ? '#ffffff' : '#ffffff'));
347
- root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || (colorScheme === 'dark' ? '#1e1e1e' : '#f1f1f1'));
348
- root.style.setProperty('--tg-color-scheme', colorScheme);
349
- }
350
-
351
- function showStatus(message, type = 'loading', duration = null) {
352
- statusMessage.textContent = message;
353
- statusMessage.className = type; // 'success', 'error', 'loading'
354
- statusMessage.style.display = 'block';
355
-
356
- // Add loader only for loading type
357
- const loader = statusMessage.querySelector('.loader');
358
- if (type === 'loading' && !loader) {
359
- const loaderSpan = document.createElement('span');
360
- loaderSpan.className = 'loader';
361
- statusMessage.appendChild(loaderSpan);
362
- } else if (type !== 'loading' && loader) {
363
- loader.remove();
364
- }
365
 
 
 
 
 
 
366
 
367
- if (duration) {
368
- setTimeout(() => {
369
- statusMessage.style.display = 'none';
370
- }, duration);
371
- }
372
  }
373
 
374
- function hideStatus() {
375
- statusMessage.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
376
  }
377
 
378
- async function fetchFiles() {
379
- if (!currentInitData) {
380
- console.error("Cannot fetch files: initData not available.");
381
- myFilesList.innerHTML = '<li class="no-files">Error: Could not verify user.</li>';
382
- return;
383
- }
384
- myFilesList.innerHTML = '<li class="loading-files">Loading files...</li>';
385
 
386
  try {
387
  const response = await fetch('/list_files', {
388
- method: 'GET',
389
- headers: {
390
- 'X-Telegram-Init-Data': currentInitData,
391
- 'Accept': 'application/json'
392
- }
393
- });
 
394
 
395
  if (!response.ok) {
396
- const errorData = await response.json().catch(() => ({ message: `HTTP error! status: ${response.status}` }));
397
- throw new Error(errorData.message || `Failed to fetch files (${response.status})`);
398
  }
399
 
400
  const data = await response.json();
401
 
402
  if (data.status === 'ok') {
403
- displayFiles(data.files);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  } else {
405
- throw new Error(data.message || 'Failed to list files from server.');
406
  }
407
- } catch (error) {
408
- console.error('Error fetching files:', error);
409
- myFilesList.innerHTML = `<li class="no-files">Error loading files: ${error.message}</li>`;
410
- }
411
- }
412
 
413
- function displayFiles(files) {
414
- myFilesList.innerHTML = ''; // Clear previous list
415
- if (!files || files.length === 0) {
416
- myFilesList.innerHTML = '<li class="no-files">No files uploaded yet.</li>';
417
- return;
 
418
  }
419
-
420
- files.sort().forEach(fileInfo => { // Sort alphabetically by path
421
- const li = document.createElement('li');
422
- li.className = 'file-item';
423
-
424
- const fileNameSpan = document.createElement('span');
425
- fileNameSpan.className = 'file-name';
426
- // Display only the filename part
427
- fileNameSpan.textContent = fileInfo.name.split('/').pop();
428
-
429
- const fileLink = document.createElement('a');
430
- fileLink.className = 'file-link';
431
- fileLink.href = fileInfo.url;
432
- fileLink.textContent = 'Open';
433
- fileLink.target = '_blank'; // Open in new tab
434
-
435
- li.appendChild(fileNameSpan);
436
- li.appendChild(fileLink);
437
- myFilesList.appendChild(li);
438
- });
439
  }
440
 
441
  function handleFileSelection() {
442
  const files = fileInput.files;
 
443
  if (files.length === 0) {
444
- fileListPreview.textContent = 'No files selected.';
 
 
 
 
445
  uploadButton.disabled = true;
446
- } else if (files.length > 20) {
447
- fileListPreview.textContent = `Error: Too many files selected (${files.length}). Maximum is 20.`;
448
- uploadButton.disabled = true;
449
- tg.HapticFeedback.notificationOccurred('error');
 
450
  } else {
451
- let previewHTML = `Selected ${files.length} file(s):<div>`;
 
 
452
  for (let i = 0; i < files.length; i++) {
453
- previewHTML += `<div>${files[i].name} (${(files[i].size / 1024 / 1024).toFixed(2)} MB)</div>`;
454
- }
455
- previewHTML += '</div>';
456
- fileListPreview.innerHTML = previewHTML;
457
- uploadButton.disabled = false;
458
  }
459
- }
460
-
461
 
462
- async function handleUpload() {
463
- const files = fileInput.files;
464
- if (files.length === 0 || files.length > 20 || !currentInitData) {
465
- showStatus('Invalid selection or missing user data.', 'error', 3000);
466
  return;
467
  }
468
 
 
 
469
  uploadButton.disabled = true;
470
- showStatus('Uploading...', 'loading');
471
- tg.MainButton.showProgress();
472
- tg.MainButton.setParams({ text: "UPLOADING...", is_active: false });
473
-
474
 
475
  const formData = new FormData();
476
- for (let i = 0; i < files.length; i++) {
477
- formData.append('files', files[i]);
 
 
478
  }
479
- // Send initData in header for verification
480
- // formData.append('initData', currentInitData); // Avoid sending in body if using header
481
 
482
  try {
483
- const response = await fetch('/upload', {
484
- method: 'POST',
485
- headers: {
486
- 'X-Telegram-Init-Data': currentInitData // Send initData securely in header
487
- },
488
- body: formData,
489
- });
490
 
491
  const data = await response.json();
492
 
493
  if (response.ok && data.status === 'ok') {
494
- showStatus(`Successfully uploaded ${data.uploaded_count} file(s)!`, 'success', 3000);
495
- fileInput.value = ''; // Clear the input
496
- handleFileSelection(); // Update preview and button state
497
- await fetchFiles(); // Refresh the file list
498
- tg.HapticFeedback.notificationOccurred('success');
499
  } else {
500
- throw new Error(data.message || `Upload failed (${response.status})`);
501
  }
502
  } catch (error) {
503
  console.error('Upload error:', error);
504
- showStatus(`Upload Error: ${error.message}`, 'error', 5000);
505
- tg.HapticFeedback.notificationOccurred('error');
 
506
  } finally {
507
- uploadButton.disabled = false; // Re-enable after success/error, unless no files selected
508
- handleFileSelection(); // Ensure button state is correct based on selection
509
- tg.MainButton.hideProgress();
510
- tg.MainButton.setParams({ text: "UPLOAD COMPLETE", is_active: true }); // Or revert to original text
511
- // Maybe hide main button after completion? tg.MainButton.hide();
512
- setTimeout(() => tg.MainButton.setParams({text: "UPLOAD", is_active: true }), 2000); // Revert button text
513
  }
514
  }
515
 
516
- function setupTelegram() {
 
517
  if (!tg || !tg.initData) {
518
- console.error("Telegram WebApp script not loaded or initData is missing.");
519
- document.body.innerHTML = '<p style="color: red; text-align: center; padding-top: 50px;">Error: Could not initialize Telegram Mini App interface. Please try reopening.</p>';
520
- document.body.style.visibility = 'visible';
 
521
  return;
522
  }
523
 
524
- tg.ready();
525
- tg.expand();
526
-
527
- applyTheme(tg.themeParams);
528
- tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
529
-
530
- currentInitData = tg.initData;
531
-
532
- // Send initData for initial verification (optional, can rely on upload/list requests)
533
- fetch('/verify', {
534
- method: 'POST',
535
- headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
536
- body: JSON.stringify({ initData: currentInitData }),
537
- })
538
- .then(response => response.json())
539
- .then(data => {
540
- if (data.status === 'ok' && data.verified && data.user_id) {
541
- currentUserId = data.user_id;
542
- console.log('Initial verification successful. User ID:', currentUserId);
543
- fetchFiles(); // Fetch files now that we potentially have the user ID context
544
- } else {
545
- console.warn('Initial verification failed or user ID missing:', data.message);
546
- // Handle case where initial verify fails - maybe show error?
547
- myFilesList.innerHTML = `<li class="no-files">Error: Could not verify your Telegram account. ${data.message || ''}</li>`;
548
- }
549
- })
550
- .catch(error => {
551
- console.error('Error during initial verification:', error);
552
- myFilesList.innerHTML = `<li class="no-files">Error verifying account: ${error.message}</li>`;
553
- });
554
-
555
-
556
- fileInput.addEventListener('change', handleFileSelection);
557
- uploadButton.addEventListener('click', handleUpload);
558
-
559
- // Configure Main Button (Optional)
560
- // tg.MainButton.setText("Upload Selected");
561
- // tg.MainButton.onClick(handleUpload); // Trigger upload via main button
562
- // fileInput.addEventListener('change', () => {
563
- // if (fileInput.files.length > 0 && fileInput.files.length <= 20) {
564
- // tg.MainButton.show().enable();
565
- // } else {
566
- // tg.MainButton.hide(); // Or disable: tg.MainButton.disable();
567
- // }
568
- // });
569
-
570
-
571
- document.body.style.visibility = 'visible';
 
572
  }
573
 
 
 
574
  if (window.Telegram && window.Telegram.WebApp) {
575
  setupTelegram();
576
  } else {
577
  console.warn("Telegram WebApp script not immediately available, waiting for window.onload");
578
  window.addEventListener('load', setupTelegram);
579
- // Fallback if load event doesn't fire or script fails
580
- setTimeout(() => {
581
  if (document.body.style.visibility !== 'visible') {
582
  console.error("Telegram WebApp script fallback timeout triggered.");
583
- document.body.innerHTML = '<p style="color: red; text-align: center; padding-top: 50px;">Error: Failed to load Telegram interface components.</p>';
 
584
  document.body.style.visibility = 'visible';
585
  }
586
  }, 3500);
@@ -594,155 +653,134 @@ TEMPLATE = """
594
  # --- Flask Routes ---
595
  @app.route('/')
596
  def index():
597
- theme_params_json = request.args.get('themeParams', '{}')
598
- try:
599
- theme_params = json.loads(theme_params_json)
600
- except json.JSONDecodeError:
601
- theme_params = {}
602
- color_scheme = request.args.get('colorScheme', 'light')
603
- return render_template_string(TEMPLATE, theme=theme_params, color_scheme=color_scheme)
604
 
605
  @app.route('/verify', methods=['POST'])
606
  def verify_route():
607
- try:
608
- req_data = request.get_json()
609
- init_data_str = req_data.get('initData')
610
- if not init_data_str:
611
- return jsonify({"status": "error", "verified": False, "message": "Missing initData"}), 400
612
 
613
- user_id = get_user_id_from_initdata(init_data_str)
614
 
615
- if user_id:
616
- return jsonify({"status": "ok", "verified": True, "user_id": user_id}), 200
617
- else:
618
- logging.warning(f"Verification failed for initData: {init_data_str[:50]}...")
619
- return jsonify({"status": "error", "verified": False, "message": "Invalid or unverifiable data"}), 403
 
 
 
 
 
 
 
620
 
621
- except Exception as e:
622
- logging.exception("Error in /verify endpoint")
623
- return jsonify({"status": "error", "verified": False, "message": "Internal server error"}), 500
624
 
625
  @app.route('/upload', methods=['POST'])
626
  def upload_files():
627
- if not hf_api or not HF_TOKEN_WRITE:
628
- return jsonify({"status": "error", "message": "Server configuration error: Hugging Face write access not available."}), 503
629
 
630
- init_data_str = request.headers.get('X-Telegram-Init-Data')
631
  if not init_data_str:
632
- return jsonify({"status": "error", "message": "Authentication required (Missing InitData Header)."}), 401
633
 
634
- user_id = get_user_id_from_initdata(init_data_str)
635
  if not user_id:
636
- return jsonify({"status": "error", "message": "Authentication failed (Invalid InitData)."}), 403
 
637
 
638
  files = request.files.getlist('files')
639
  if not files:
640
- return jsonify({"status": "error", "message": "No files provided in the request."}), 400
641
- if len(files) > 20:
642
- return jsonify({"status": "error", "message": "Too many files. Maximum is 20."}), 400
643
 
644
- uploaded_count = 0
 
 
 
645
  errors = []
646
- user_folder_path = f"{HF_UPLOAD_FOLDER}/{user_id}"
647
 
648
  for file in files:
649
  if file and file.filename:
650
  filename = secure_filename(file.filename)
651
- path_in_repo = f"{user_folder_path}/{filename}"
652
- logging.info(f"Attempting to upload '{filename}' for user {user_id} to {REPO_ID}/{path_in_repo}")
653
-
654
  try:
655
- # Read file content into memory for upload
656
- file_content = file.read()
657
- file_like_object = io.BytesIO(file_content)
658
-
659
  hf_api.upload_file(
660
- path_or_fileobj=file_like_object,
661
  path_in_repo=path_in_repo,
662
  repo_id=REPO_ID,
663
  repo_type="dataset",
 
664
  commit_message=f"Upload {filename} by user {user_id}"
665
- # token=HF_TOKEN_WRITE is implicitly used by hf_api instance
666
  )
667
- uploaded_count += 1
668
- logging.info(f"Successfully uploaded '{filename}' for user {user_id}.")
 
 
 
 
669
  except Exception as e:
670
- error_msg = f"Failed to upload {filename}: {str(e)}"
671
- logging.error(error_msg, exc_info=True)
672
- errors.append(error_msg)
673
  else:
674
- errors.append("Received an empty file part.")
675
-
676
 
677
- if errors:
678
- if uploaded_count > 0:
679
- return jsonify({
680
- "status": "partial_error",
681
- "message": f"Uploaded {uploaded_count} file(s) with errors on others.",
682
- "errors": errors,
683
- "uploaded_count": uploaded_count
684
- }), 207 # Multi-Status
685
- else:
686
- return jsonify({
687
- "status": "error",
688
- "message": "Failed to upload any files.",
689
- "errors": errors,
690
- "uploaded_count": 0
691
- }), 500
692
  else:
693
- return jsonify({"status": "ok", "message": f"Successfully uploaded {uploaded_count} files.", "uploaded_count": uploaded_count}), 200
 
694
 
695
 
696
- @app.route('/list_files', methods=['GET'])
697
  def list_user_files():
698
- if not hf_api:
699
- return jsonify({"status": "error", "message": "Server configuration error: Hugging Face access not available."}), 503
700
 
701
- init_data_str = request.headers.get('X-Telegram-Init-Data')
 
702
  if not init_data_str:
703
- return jsonify({"status": "error", "message": "Authentication required (Missing InitData Header)."}), 401
704
 
705
- user_id = get_user_id_from_initdata(init_data_str)
706
  if not user_id:
707
- return jsonify({"status": "error", "message": "Authentication failed (Invalid InitData)."}), 403
 
708
 
709
- user_folder_path = f"{HF_UPLOAD_FOLDER}/{user_id}"
710
- logging.info(f"Listing files for user {user_id} in path: {user_folder_path}")
711
 
712
  try:
713
- # Use list_repo_files which is efficient for listing
714
- repo_files = list_repo_files(
715
- repo_id=REPO_ID,
716
- repo_type="dataset",
717
- token=HF_TOKEN_READ or HF_TOKEN_WRITE, # Use read or write token
718
- paths=[user_folder_path] # Specify the directory to list
719
- )
720
-
721
- # Filter out potential directory markers if any, though list_repo_files usually doesn't return them for subdirs
722
- user_files_info = []
723
- base_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main"
724
 
725
  for file_path in repo_files:
726
- # Ensure we only list files directly under the user's folder
727
- if file_path.startswith(user_folder_path + "/") and file_path != user_folder_path + "/":
728
- file_info = {
729
- "name": file_path, # Full path from repo root
730
- "url": f"{base_url}/{file_path}"
731
- }
732
- user_files_info.append(file_info)
 
 
 
733
 
734
- logging.info(f"Found {len(user_files_info)} file(s) for user {user_id}.")
735
- return jsonify({"status": "ok", "files": user_files_info})
736
-
737
- except EntryNotFoundError:
738
- logging.info(f"No files found or directory does not exist for user {user_id} at path {user_folder_path}.")
739
- return jsonify({"status": "ok", "files": []}) # Return empty list if user folder doesn't exist yet
740
  except RepositoryNotFoundError:
741
- logging.error(f"Repository '{REPO_ID}' not found.")
742
- return jsonify({"status": "error", "message": "Server configuration error: Target repository not found."}), 500
 
743
  except Exception as e:
744
- logging.error(f"Error listing files for user {user_id}: {e}", exc_info=True)
745
- return jsonify({"status": "error", "message": "Internal server error while listing files."}), 500
746
 
747
 
748
  # --- App Initialization ---
@@ -752,30 +790,18 @@ if __name__ == '__main__':
752
  print("---")
753
  print(f"Flask server starting on http://{HOST}:{PORT}")
754
  print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
755
- print(f"Target Hugging Face Repo: {REPO_ID}")
756
- print(f"Upload Base Folder: {HF_UPLOAD_FOLDER}")
757
- if not HF_TOKEN_WRITE and not HF_TOKEN_READ:
758
- print("---")
759
- print("--- CRITICAL WARNING: NO HUGGING FACE TOKEN SET ---")
760
- print("--- File operations WILL FAIL. Set HF_TOKEN_WRITE or HF_TOKEN_READ environment variables.")
761
  print("---")
762
- elif not HF_TOKEN_WRITE:
763
- print("---")
764
- print("--- WARNING: HF_TOKEN_WRITE NOT SET ---")
765
- print("--- File uploads will be disabled. Read-only mode.")
766
  print("---")
767
  else:
768
- print("--- Hugging Face Write Token found. Uploads enabled.")
769
-
770
- if not app.secret_key or app.secret_key == b'default_secret':
771
- print("---")
772
- print("--- WARNING: Using default or insecure FLASK_SECRET_KEY ---")
773
- print("--- Set a strong FLASK_SECRET_KEY environment variable for production.")
774
- print("---")
775
 
776
  print("--- Server Ready ---")
777
- # For production, use a proper WSGI server like Gunicorn or Waitress
778
- # Example using waitress:
779
  # from waitress import serve
780
  # serve(app, host=HOST, port=PORT)
781
- app.run(host=HOST, port=PORT, debug=False) # debug=False is crucial for production
 
2
  # -*- coding: utf-8 -*-
3
 
4
  import os
5
+ from flask import Flask, request, Response, render_template_string, jsonify, redirect, url_for, make_response
 
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, list_repo_files
15
+ from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
16
  from werkzeug.utils import secure_filename
17
 
18
  # --- Configuration ---
 
21
  PORT = 7860
22
 
23
  # Hugging Face Settings
24
+ REPO_ID = "Eluza133/Z1e1u"
25
  HF_UPLOAD_FOLDER = "uploads" # Base folder 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
+ # File Upload Settings
30
+ MAX_FILES_PER_UPLOAD = 20
31
 
32
  app = Flask(__name__)
33
  logging.basicConfig(level=logging.INFO)
34
+ app.secret_key = os.urandom(24)
35
+ hf_api = HfApi()
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  # --- Telegram Verification ---
38
  def verify_telegram_data(init_data_str):
 
41
  received_hash = parsed_data.pop('hash', [None])[0]
42
 
43
  if not received_hash:
44
+ logging.warning("Verification failed: Hash missing")
45
+ return None, False, "Хэш отсутствует"
46
 
47
  data_check_list = []
48
  for key, value in sorted(parsed_data.items()):
 
55
  if calculated_hash == received_hash:
56
  auth_date = int(parsed_data.get('auth_date', [0])[0])
57
  current_time = int(time.time())
58
+ # Allow data up to 24 hours old, adjust if needed
59
  if current_time - auth_date > 86400:
60
+ logging.warning(f"Telegram InitData is older than 24 hours (Auth Date: {auth_date}, Current: {current_time}). Allowing.")
61
+ # return parsed_data, False, "Данные авторизации устарели" # Uncomment to enforce stricter timeout
62
+
63
+ return parsed_data, True, "Верификация успешна"
64
  else:
65
+ logging.warning(f"Verification failed. Calculated: {calculated_hash}, Received: {received_hash}")
66
+ return parsed_data, False, "Неверный хэш"
67
  except Exception as e:
68
+ logging.error(f"Error verifying Telegram data: {e}")
69
+ return None, False, f"Ошибка верификации: {e}"
70
 
71
+ def get_user_id_from_init_data(init_data_str):
72
+ parsed_data, is_valid, _ = verify_telegram_data(init_data_str)
73
+ if not is_valid:
74
  return None
75
 
76
+ user_data_json = parsed_data.get('user', [None])[0]
77
+ if not user_data_json:
78
+ return None
79
  try:
80
+ user_info = json.loads(unquote(user_data_json))
81
+ return user_info.get('id')
82
+ except (json.JSONDecodeError, TypeError) as e:
83
+ logging.error(f"Could not parse user data from initData: {e}")
 
 
 
 
 
 
84
  return None
85
 
86
  # --- HTML Templates ---
87
  TEMPLATE = """
88
  <!DOCTYPE html>
89
+ <html lang="ru">
90
  <head>
91
  <meta charset="UTF-8">
92
  <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, user-scalable=no, viewport-fit=cover">
 
94
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
95
  <link rel="preconnect" href="https://fonts.googleapis.com">
96
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
97
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
98
  <style>
99
  :root {
100
  --tg-theme-bg-color: {{ theme.bg_color | default('#ffffff') }};
 
104
  --tg-theme-button-color: {{ theme.button_color | default('#2481cc') }};
105
  --tg-theme-button-text-color: {{ theme.button_text_color | default('#ffffff') }};
106
  --tg-theme-secondary-bg-color: {{ theme.secondary_bg_color | default('#f1f1f1') }};
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  --border-radius: 12px;
108
  --padding: 16px;
109
+ --gap: 16px;
110
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
111
+ --shadow-color: rgba(0, 0, 0, 0.1);
 
 
 
 
 
 
 
 
112
  }
113
 
114
  * { box-sizing: border-box; margin: 0; padding: 0; }
115
+
 
 
 
116
  body {
117
  font-family: var(--font-family);
118
+ background-color: var(--tg-theme-bg-color);
119
+ color: var(--tg-theme-text-color);
120
  padding: var(--padding);
121
  overscroll-behavior-y: none;
122
+ -webkit-font-smoothing: antialiased;
123
+ -moz-osx-font-smoothing: grayscale;
124
+ visibility: hidden;
125
  min-height: 100vh;
126
  }
127
+
128
  .container {
129
  max-width: 700px;
130
  margin: 0 auto;
131
  display: flex;
132
  flex-direction: column;
133
+ gap: calc(var(--gap) * 1.5);
134
  }
135
+
136
+ .header {
137
  text-align: center;
138
+ margin-bottom: var(--gap);
139
+ }
140
+
141
+ .header h1 {
142
+ font-size: 2em;
143
  font-weight: 700;
144
+ margin-bottom: 0.2em;
145
+ color: var(--tg-theme-text-color);
146
+ }
147
+
148
+ .header p {
149
+ font-size: 1em;
150
+ color: var(--tg-theme-hint-color);
151
  }
152
+
153
  .upload-section, .files-section {
154
+ background-color: var(--tg-theme-secondary-bg-color);
155
  border-radius: var(--border-radius);
156
  padding: var(--padding);
157
+ box-shadow: 0 4px 15px var(--shadow-color);
158
+ border: 1px solid rgba(0,0,0,0.05);
159
  }
160
+
161
+ .section-title {
162
+ font-size: 1.5em;
163
  font-weight: 600;
164
+ margin-bottom: var(--gap);
165
+ color: var(--tg-theme-text-color);
166
+ display: flex;
167
+ align-items: center;
168
+ gap: 8px;
169
  }
170
+ .section-title .icon { font-size: 1.2em; opacity: 0.8; }
171
+ .icon-upload::before { content: '☁️'; }
172
+ .icon-files::before { content: '📁'; }
173
+ .icon-link::before { content: '🔗'; }
174
+ .icon-image::before { content: '🖼️'; }
175
+ .icon-video::before { content: '🎬'; }
176
+ .icon-audio::before { content: '🎵'; }
177
+ .icon-doc::before { content: '📄'; }
178
+ .icon-archive::before { content: '📦'; }
179
+ .icon-generic::before { content: '❔'; }
180
+
181
+
182
+ #upload-form label {
183
+ display: block;
184
+ padding: 12px 20px;
185
+ background-color: var(--tg-theme-button-color);
186
+ color: var(--tg-theme-button-text-color);
187
  border-radius: var(--border-radius);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  cursor: pointer;
189
+ text-align: center;
 
 
190
  font-weight: 500;
191
+ margin-bottom: var(--gap);
192
+ transition: background-color 0.2s ease;
193
  }
194
+ #upload-form label:hover {
195
+ filter: brightness(1.1);
 
 
196
  }
197
+
198
+
199
+ #file-input { display: none; }
200
+
201
  #file-list-preview {
202
+ margin-bottom: var(--gap);
203
  font-size: 0.9em;
204
+ color: var(--tg-theme-hint-color);
205
+ }
206
+ #file-list-preview div {
207
+ padding: 5px 0;
 
 
 
 
208
  white-space: nowrap;
209
  overflow: hidden;
210
  text-overflow: ellipsis;
211
+ }
212
+
213
  .btn {
214
+ display: block; /* Make button full width */
215
+ width: 100%;
216
+ padding: 12px 20px;
217
+ border: none;
218
+ background: var(--tg-theme-button-color);
219
+ color: var(--tg-theme-button-text-color);
220
+ font-size: 1em;
221
+ font-weight: 600;
222
+ border-radius: var(--border-radius);
223
+ cursor: pointer;
224
+ transition: all 0.2s ease;
225
+ text-align: center;
226
  }
227
+
228
  .btn:disabled {
229
+ background-color: var(--tg-theme-hint-color);
230
  cursor: not-allowed;
231
+ opacity: 0.7;
232
+ }
233
+ .btn:not(:disabled):hover {
234
+ filter: brightness(1.1);
235
+ transform: translateY(-1px);
236
+ }
237
+ .btn .loader {
238
+ border: 3px solid rgba(255, 255, 255, 0.3);
239
+ border-radius: 50%;
240
+ border-top: 3px solid var(--tg-theme-button-text-color);
241
+ width: 18px;
242
+ height: 18px;
243
+ animation: spin 1s linear infinite;
244
+ display: inline-block;
245
+ margin-left: 10px;
246
+ vertical-align: middle;
247
+ }
248
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
249
+
250
+
251
+ #upload-status {
252
+ margin-top: var(--gap);
253
+ font-size: 0.9em;
254
  text-align: center;
255
+ min-height: 1.2em; /* Reserve space */
 
256
  }
257
+ .status-success { color: #28a745; } /* Green */
258
+ .status-error { color: #dc3545; } /* Red */
259
+ .status-info { color: var(--tg-theme-hint-color); }
260
 
261
+ #user-files-list {
262
  list-style: none;
263
  padding: 0;
264
+ margin: 0;
265
  }
266
+
267
  .file-item {
268
+ background-color: var(--tg-theme-bg-color);
269
+ padding: 12px var(--padding);
270
+ border-radius: calc(var(--border-radius) - 4px);
271
+ margin-bottom: calc(var(--gap) / 2);
272
  display: flex;
 
273
  align-items: center;
274
+ justify-content: space-between;
275
+ gap: var(--gap);
276
+ border: 1px solid rgba(0,0,0,0.08);
277
  transition: background-color 0.2s ease;
278
  }
279
+ .file-item:hover {
280
+ background-color: rgba(0,0,0,0.03);
281
+ }
282
+
283
+ .file-info {
284
+ display: flex;
285
+ align-items: center;
286
+ gap: 10px;
287
+ overflow: hidden; /* Prevent long names from breaking layout */
288
+ }
289
+
290
+ .file-icon { font-size: 1.4em; opacity: 0.7; }
291
+
292
  .file-name {
293
+ font-weight: 500;
294
+ white-space: nowrap;
295
+ overflow: hidden;
296
+ text-overflow: ellipsis;
297
  flex-grow: 1;
298
+ color: var(--tg-theme-text-color);
 
299
  }
300
+
301
+ .file-actions a {
302
+ color: var(--tg-theme-link-color);
303
  text-decoration: none;
304
+ font-size: 1.5em; /* Larger icon */
305
+ opacity: 0.8;
306
+ transition: opacity 0.2s ease;
307
+ padding: 5px; /* Easier to tap */
308
  }
309
+ .file-actions a:hover { opacity: 1; }
310
+
311
+ #loading-files, #no-files {
312
  text-align: center;
313
+ color: var(--tg-theme-hint-color);
314
  padding: var(--padding);
315
+ font-size: 1em;
316
  }
317
+
318
+ #error-message {
319
+ background-color: #f8d7da;
320
+ color: #721c24;
321
+ padding: var(--padding);
322
+ border: 1px solid #f5c6cb;
323
+ border-radius: var(--border-radius);
324
+ margin-bottom: var(--gap);
325
+ text-align: center;
326
+ display: none; /* Hidden by default */
327
+ }
328
+
329
+ #greeting {
330
+ text-align: center;
331
+ color: var(--tg-theme-hint-color);
332
+ font-size: 0.95em;
333
+ margin-top: var(--gap);
334
  }
 
335
  </style>
336
  </head>
337
  <body>
338
  <div class="container">
339
+ <div class="header">
340
+ <h1>Zeus Cloud</h1>
341
+ <p>Ваше персональное облачное хранилище</p>
342
+ </div>
343
+
344
+ <div id="error-message"></div>
345
 
346
  <section class="upload-section">
347
+ <h2 class="section-title"><span class="icon icon-upload"></span>Загрузить файлы</h2>
348
+ <form id="upload-form">
349
+ <label for="file-input">Выбрать файлы (до {{ max_files }} шт.)</label>
350
+ <input type="file" id="file-input" name="files" multiple accept="*.*">
351
+ <div id="file-list-preview"></div>
352
+ <button type="submit" id="upload-button" class="btn" disabled>
353
+ <span id="upload-button-text">Выберите файлы для загрузки</span>
354
+ <span class="loader" id="upload-loader" style="display: none;"></span>
355
+ </button>
356
+ <div id="upload-status"></div>
357
+ </form>
 
358
  </section>
359
 
360
  <section class="files-section">
361
+ <h2 class="section-title"><span class="icon icon-files"></span>Мои файлы</h2>
362
+ <div id="loading-files">Загрузка списка файлов...</div>
363
+ <ul id="user-files-list"></ul>
364
+ <div id="no-files" style="display: none;">У вас пока нет загруженных файлов.</div>
365
  </section>
366
 
367
+ <footer id="greeting">Инициализация...</footer>
368
+
369
  </div>
370
 
371
  <script>
372
  const tg = window.Telegram.WebApp;
373
+ const uploadForm = document.getElementById('upload-form');
374
+ const fileInput = document.getElementById('file-input');
375
+ const fileListPreview = document.getElementById('file-list-preview');
376
+ const uploadButton = document.getElementById('upload-button');
377
+ const uploadButtonText = document.getElementById('upload-button-text');
378
+ const uploadLoader = document.getElementById('upload-loader');
379
+ const uploadStatus = document.getElementById('upload-status');
380
+ const userFilesList = document.getElementById('user-files-list');
381
+ const loadingFiles = document.getElementById('loading-files');
382
+ const noFiles = document.getElementById('no-files');
383
+ const greetingElement = document.getElementById('greeting');
384
+ const errorMessageDiv = document.getElementById('error-message');
385
+ const MAX_FILES = {{ max_files }};
386
+ let isUploading = false;
387
 
388
  function applyTheme(themeParams) {
389
  const root = document.documentElement;
390
+ Object.keys(themeParams).forEach(key => {
391
+ const cssVar = `--tg-theme-${key.replace(/_/g, '-')}`;
392
+ root.style.setProperty(cssVar, themeParams[key]);
393
+ });
394
+ // Update root variables directly for template rendering fallback
395
+ root.style.setProperty('--tg-theme-bg-color', themeParams.bg_color || '#ffffff');
396
+ root.style.setProperty('--tg-theme-text-color', themeParams.text_color || '#000000');
397
+ root.style.setProperty('--tg-theme-hint-color', themeParams.hint_color || '#999999');
398
+ root.style.setProperty('--tg-theme-link-color', themeParams.link_color || '#2481cc');
399
+ root.style.setProperty('--tg-theme-button-color', themeParams.button_color || '#2481cc');
400
+ root.style.setProperty('--tg-theme-button-text-color', themeParams.button_text_color || '#ffffff');
401
+ root.style.setProperty('--tg-theme-secondary-bg-color', themeParams.secondary_bg_color || '#f1f1f1');
402
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
403
 
404
+ function showError(message) {
405
+ errorMessageDiv.textContent = message;
406
+ errorMessageDiv.style.display = 'block';
407
+ if (tg.HapticFeedback) tg.HapticFeedback.notificationOccurred('error');
408
+ }
409
 
410
+ function clearError() {
411
+ errorMessageDiv.style.display = 'none';
412
+ errorMessageDiv.textContent = '';
 
 
413
  }
414
 
415
+ function showStatus(message, type = 'info') {
416
+ uploadStatus.textContent = message;
417
+ uploadStatus.className = `status-${type}`; // Sets class like status-info, status-success, status-error
418
+ }
419
+
420
+ function getFileIcon(filename) {
421
+ const extension = filename.split('.').pop().toLowerCase();
422
+ if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension)) return 'icon-image';
423
+ if (['mp4', 'avi', 'mov', 'wmv', 'mkv', 'webm'].includes(extension)) return 'icon-video';
424
+ if (['mp3', 'wav', 'ogg', 'aac', 'flac'].includes(extension)) return 'icon-audio';
425
+ if (['doc', 'docx', 'pdf', 'txt', 'rtf', 'odt'].includes(extension)) return 'icon-doc';
426
+ if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) return 'icon-archive';
427
+ return 'icon-generic'; // Default icon
428
  }
429
 
430
+ async function loadFiles() {
431
+ loadingFiles.style.display = 'block';
432
+ userFilesList.innerHTML = '';
433
+ noFiles.style.display = 'none';
434
+ clearError();
 
 
435
 
436
  try {
437
  const response = await fetch('/list_files', {
438
+ method: 'POST', // Sending initData securely
439
+ headers: {
440
+ 'Content-Type': 'application/json',
441
+ 'Accept': 'application/json'
442
+ },
443
+ body: JSON.stringify({ initData: tg.initData }),
444
+ });
445
 
446
  if (!response.ok) {
447
+ const errorData = await response.json().catch(() => ({ message: `Ошибка сервера: ${response.status}` }));
448
+ throw new Error(errorData.message || `Ошибка загрузки файлов: ${response.status}`);
449
  }
450
 
451
  const data = await response.json();
452
 
453
  if (data.status === 'ok') {
454
+ if (data.files && data.files.length > 0) {
455
+ data.files.forEach(file => {
456
+ const li = document.createElement('li');
457
+ li.className = 'file-item';
458
+ const fileIconClass = getFileIcon(file.name);
459
+ li.innerHTML = `
460
+ <div class="file-info">
461
+ <span class="file-icon ${fileIconClass}"></span>
462
+ <span class="file-name" title="${file.name}">${file.name}</span>
463
+ </div>
464
+ <div class="file-actions">
465
+ <a href="${file.url}" target="_blank" title="Открыть/Скачать">
466
+ <span class="icon-link"></span>
467
+ </a>
468
+ </div>
469
+ `;
470
+ userFilesList.appendChild(li);
471
+ });
472
+ noFiles.style.display = 'none';
473
+ } else {
474
+ noFiles.style.display = 'block';
475
+ }
476
  } else {
477
+ throw new Error(data.message || 'Не удалось получить список файлов.');
478
  }
 
 
 
 
 
479
 
480
+ } catch (error) {
481
+ console.error('Error loading files:', error);
482
+ showError(`Не удалось загрузить список файлов: ${error.message}`);
483
+ noFiles.style.display = 'block'; // Show no files message on error
484
+ } finally {
485
+ loadingFiles.style.display = 'none';
486
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  }
488
 
489
  function handleFileSelection() {
490
  const files = fileInput.files;
491
+ fileListPreview.innerHTML = ''; // Clear previous preview
492
  if (files.length === 0) {
493
+ uploadButtonText.textContent = 'Выберите файлы для загрузки';
494
+ uploadButton.disabled = true;
495
+ } else if (files.length > MAX_FILES) {
496
+ uploadButtonText.textContent = `Слишком много файлов (${files.length})`;
497
+ showStatus(`Выбрано ${files.length} файлов. Максимум ${MAX_FILES}.`, 'error');
498
  uploadButton.disabled = true;
499
+ for (let i = 0; i < files.length; i++) {
500
+ const div = document.createElement('div');
501
+ div.textContent = `❌ ${files[i].name}`; // Indicate error visually
502
+ fileListPreview.appendChild(div);
503
+ }
504
  } else {
505
+ uploadButtonText.textContent = `Загрузить ${files.length} файл(а)`;
506
+ uploadButton.disabled = false;
507
+ showStatus(''); // Clear status
508
  for (let i = 0; i < files.length; i++) {
509
+ const div = document.createElement('div');
510
+ div.textContent = `• ${files[i].name}`;
511
+ fileListPreview.appendChild(div);
512
+ }
 
513
  }
514
+ }
 
515
 
516
+ async function handleUpload(event) {
517
+ event.preventDefault();
518
+ if (isUploading || fileInput.files.length === 0 || fileInput.files.length > MAX_FILES) {
 
519
  return;
520
  }
521
 
522
+ isUploading = true;
523
+ clearError();
524
  uploadButton.disabled = true;
525
+ uploadLoader.style.display = 'inline-block';
526
+ uploadButtonText.textContent = 'Загрузка...';
527
+ showStatus('Подготовка к загрузке...', 'info');
 
528
 
529
  const formData = new FormData();
530
+ formData.append('initData', tg.initData); // Send initData securely with the upload
531
+
532
+ for (let i = 0; i < fileInput.files.length; i++) {
533
+ formData.append('files', fileInput.files[i]);
534
  }
 
 
535
 
536
  try {
537
+ showStatus(`Загрузка ${fileInput.files.length} файл(ов)...`, 'info');
538
+ const response = await fetch('/upload', {
539
+ method: 'POST',
540
+ body: formData,
541
+ // Content-Type is automatically set by browser for FormData
542
+ });
 
543
 
544
  const data = await response.json();
545
 
546
  if (response.ok && data.status === 'ok') {
547
+ showStatus(data.message || 'Файлы успешно загружены!', 'success');
548
+ if (tg.HapticFeedback) tg.HapticFeedback.notificationOccurred('success');
549
+ fileInput.value = ''; // Reset file input
550
+ handleFileSelection(); // Update button state and preview
551
+ await loadFiles(); // Refresh the file list
552
  } else {
553
+ throw new Error(data.message || `Ошибка загрузки: ${response.status}`);
554
  }
555
  } catch (error) {
556
  console.error('Upload error:', error);
557
+ showStatus(`Ошибка: ${error.message}`, 'error');
558
+ showError(`Не удалось загрузить файлы: ${error.message}`);
559
+ if (tg.HapticFeedback) tg.HapticFeedback.notificationOccurred('error');
560
  } finally {
561
+ isUploading = false;
562
+ uploadLoader.style.display = 'none';
563
+ // Restore button text based on current file selection (might be empty now)
564
+ handleFileSelection();
565
+ // Re-enable button only if files are selected and valid count
566
+ uploadButton.disabled = (fileInput.files.length === 0 || fileInput.files.length > MAX_FILES);
567
  }
568
  }
569
 
570
+
571
+ function setupTelegram() {
572
  if (!tg || !tg.initData) {
573
+ console.error("Telegram WebApp script не загружен или initData отсутствует.");
574
+ greetingElement.textContent = 'Не удалось связаться с Telegram.';
575
+ document.body.style.visibility = 'visible'; // Show body even if TG fails
576
+ showError('Не удалось инициализировать приложение Telegram.');
577
  return;
578
  }
579
 
580
+ try {
581
+ tg.ready();
582
+ tg.expand();
583
+
584
+ applyTheme(tg.themeParams);
585
+ tg.onEvent('themeChanged', () => applyTheme(tg.themeParams));
586
+
587
+ // Initial verification call (optional, as actions verify anyway)
588
+ fetch('/verify', {
589
+ method: 'POST',
590
+ headers: { 'Content-Type': 'application/json' },
591
+ body: JSON.stringify({ initData: tg.initData }),
592
+ }).then(response => response.json())
593
+ .then(data => {
594
+ if (data.status === 'ok' && data.verified) {
595
+ console.log('Initial verification successful.');
596
+ const user = data.user || tg.initDataUnsafe?.user;
597
+ if (user) {
598
+ const name = user.first_name || user.username || 'Пользователь';
599
+ greetingElement.textContent = `Привет, ${name}! 👋`;
600
+ } else {
601
+ greetingElement.textContent = 'Добро пожаловать!';
602
+ }
603
+ // Load files only after successful verification potentially
604
+ loadFiles();
605
+ } else {
606
+ console.warn('Initial verification failed:', data.message);
607
+ greetingElement.textContent = 'Добро пожаловать!';
608
+ showError(`Ошибка авторизации: ${data.message || 'Неверные данные'}. Обновите приложение.`);
609
+ // Don't load files if initial verify fails strictly
610
+ }
611
+ }).catch(error => {
612
+ console.error('Initial verification request failed:', error);
613
+ greetingElement.textContent = 'Ошибка соединения.';
614
+ showError('Не удалось проверить авторизацию. Проверьте интернет-соединение.');
615
+ });
616
+
617
+
618
+ fileInput.addEventListener('change', handleFileSelection);
619
+ uploadForm.addEventListener('submit', handleUpload);
620
+
621
+ document.body.style.visibility = 'visible';
622
+
623
+ } catch (error) {
624
+ console.error("Error during Telegram setup:", error);
625
+ greetingElement.textContent = 'Ошибка инициализации.';
626
+ showError(`Критическая ошибка: ${error.message}`);
627
+ document.body.style.visibility = 'visible';
628
+ }
629
  }
630
 
631
+
632
+ // Attempt immediate setup, fallback to onload
633
  if (window.Telegram && window.Telegram.WebApp) {
634
  setupTelegram();
635
  } else {
636
  console.warn("Telegram WebApp script not immediately available, waiting for window.onload");
637
  window.addEventListener('load', setupTelegram);
638
+ setTimeout(() => {
 
639
  if (document.body.style.visibility !== 'visible') {
640
  console.error("Telegram WebApp script fallback timeout triggered.");
641
+ greetingElement.textContent = 'Ошибка загрузки интерфейса Telegram.';
642
+ showError('Не удалось загрузить интерфейс Telegram. Попробуйте перезапустить.');
643
  document.body.style.visibility = 'visible';
644
  }
645
  }, 3500);
 
653
  # --- Flask Routes ---
654
  @app.route('/')
655
  def index():
656
+ theme_params = {} # Let JS handle theme application
657
+ return render_template_string(TEMPLATE, theme=theme_params, max_files=MAX_FILES_PER_UPLOAD)
 
 
 
 
 
658
 
659
  @app.route('/verify', methods=['POST'])
660
  def verify_route():
661
+ req_data = request.get_json()
662
+ init_data_str = req_data.get('initData')
663
+ if not init_data_str:
664
+ return jsonify({"status": "error", "verified": False, "message": "Missing initData"}), 400
 
665
 
666
+ user_data_parsed, is_valid, message = verify_telegram_data(init_data_str)
667
 
668
+ user_info_dict = {}
669
+ if user_data_parsed and 'user' in user_data_parsed:
670
+ try:
671
+ user_json_str = unquote(user_data_parsed['user'][0])
672
+ user_info_dict = json.loads(user_json_str)
673
+ except Exception as e:
674
+ logging.error(f"Could not parse user JSON in /verify: {e}")
675
+
676
+ if is_valid:
677
+ return jsonify({"status": "ok", "verified": True, "user": user_info_dict}), 200
678
+ else:
679
+ return jsonify({"status": "error", "verified": False, "message": message, "user": user_info_dict}), 403
680
 
 
 
 
681
 
682
  @app.route('/upload', methods=['POST'])
683
  def upload_files():
684
+ if not HF_TOKEN_WRITE:
685
+ return jsonify({"status": "error", "message": "Функция загрузки недоступна (ошибка конфигурации сервера)."}), 503
686
 
687
+ init_data_str = request.form.get('initData')
688
  if not init_data_str:
689
+ return jsonify({"status": "error", "message": "Ошибка авторизации: initData отсутствует."}), 400
690
 
691
+ user_id = get_user_id_from_init_data(init_data_str)
692
  if not user_id:
693
+ _, _, verification_message = verify_telegram_data(init_data_str) # Get specific reason
694
+ return jsonify({"status": "error", "message": f"Ошибка авторизации: {verification_message}"}), 403
695
 
696
  files = request.files.getlist('files')
697
  if not files:
698
+ return jsonify({"status": "error", "message": "Файлы для загрузки не найдены."}), 400
 
 
699
 
700
+ if len(files) > MAX_FILES_PER_UPLOAD:
701
+ return jsonify({"status": "error", "message": f"Слишком много файлов. Максимум {MAX_FILES_PER_UPLOAD} за раз."}), 400
702
+
703
+ successful_uploads = 0
704
  errors = []
705
+ user_folder = f"{HF_UPLOAD_FOLDER}/{user_id}"
706
 
707
  for file in files:
708
  if file and file.filename:
709
  filename = secure_filename(file.filename)
710
+ path_in_repo = f"{user_folder}/{filename}"
 
 
711
  try:
712
+ logging.info(f"Attempting to upload '{filename}' for user {user_id} to {REPO_ID}/{path_in_repo}")
713
+ # Pass file object directly
 
 
714
  hf_api.upload_file(
715
+ path_or_fileobj=file.stream, # Use file.stream
716
  path_in_repo=path_in_repo,
717
  repo_id=REPO_ID,
718
  repo_type="dataset",
719
+ token=HF_TOKEN_WRITE,
720
  commit_message=f"Upload {filename} by user {user_id}"
 
721
  )
722
+ successful_uploads += 1
723
+ logging.info(f"Successfully uploaded '{filename}' for user {user_id}")
724
+ except HfHubHTTPError as e:
725
+ logging.error(f"HTTP error uploading {filename} for user {user_id}: {e}")
726
+ # Check for specific errors like 413 Payload Too Large if needed
727
+ errors.append(f"{filename}: Ошибка сервера ({e.response.status_code if e.response else 'N/A'})")
728
  except Exception as e:
729
+ logging.exception(f"Error uploading {filename} for user {user_id}")
730
+ errors.append(f"{filename}: {e}")
 
731
  else:
732
+ errors.append("Получен пустой файл")
 
733
 
734
+ if successful_uploads > 0 and not errors:
735
+ return jsonify({"status": "ok", "message": f"Загружено {successful_uploads} файл(ов)."}), 200
736
+ elif successful_uploads > 0 and errors:
737
+ error_details = "; ".join(errors)
738
+ return jsonify({"status": "partial", "message": f"Загружено {successful_uploads} файл(ов). Ошибки: {error_details}"}), 207 # Multi-Status
 
 
 
 
 
 
 
 
 
 
739
  else:
740
+ error_details = "; ".join(errors)
741
+ return jsonify({"status": "error", "message": f"Не удалось загрузить файлы. Ошибки: {error_details}"}), 500
742
 
743
 
744
+ @app.route('/list_files', methods=['POST'])
745
  def list_user_files():
746
+ if not HF_TOKEN_READ:
747
+ return jsonify({"status": "error", "message": "Функция просмотра файлов недоступна (ошибка конфигурации сервера)."}), 503
748
 
749
+ req_data = request.get_json()
750
+ init_data_str = req_data.get('initData')
751
  if not init_data_str:
752
+ return jsonify({"status": "error", "message": "Ошибка авторизации: initData отсутствует."}), 400
753
 
754
+ user_id = get_user_id_from_init_data(init_data_str)
755
  if not user_id:
756
+ _, _, verification_message = verify_telegram_data(init_data_str)
757
+ return jsonify({"status": "error", "message": f"Ошибка авторизации: {verification_message}"}), 403
758
 
759
+ user_folder = f"{HF_UPLOAD_FOLDER}/{user_id}"
760
+ files_list = []
761
 
762
  try:
763
+ repo_files = list_repo_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_READ, path_in_repo=user_folder)
 
 
 
 
 
 
 
 
 
 
764
 
765
  for file_path in repo_files:
766
+ # Ensure we only list files directly in the user's folder, not subfolders if any were created manually
767
+ if file_path.startswith(user_folder + "/") and file_path.count('/') == user_folder.count('/') + 1:
768
+ filename = os.path.basename(file_path)
769
+ file_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{quote(file_path)}"
770
+ files_list.append({
771
+ "name": filename,
772
+ "url": file_url,
773
+ "path": file_path # Keep internal path if needed later
774
+ })
775
+ return jsonify({"status": "ok", "files": files_list})
776
 
 
 
 
 
 
 
777
  except RepositoryNotFoundError:
778
+ logging.warning(f"Repository {REPO_ID} or user folder {user_folder} not found.")
779
+ # This is expected if the user hasn't uploaded anything yet, return empty list
780
+ return jsonify({"status": "ok", "files": []})
781
  except Exception as e:
782
+ logging.exception(f"Error listing files for user {user_id}")
783
+ return jsonify({"status": "error", "message": f"Не удалось получить список файлов: {e}"}), 500
784
 
785
 
786
  # --- App Initialization ---
 
790
  print("---")
791
  print(f"Flask server starting on http://{HOST}:{PORT}")
792
  print(f"Using Bot Token ID: {BOT_TOKEN.split(':')[0]}")
793
+ print(f"Hugging Face Repo: {REPO_ID}")
794
+ print(f"HF Upload Folder: {HF_UPLOAD_FOLDER}")
795
+ if not HF_TOKEN_READ or not HF_TOKEN_WRITE:
 
 
 
796
  print("---")
797
+ print("--- WARNING: HUGGING FACE TOKEN(S) NOT SET ---")
798
+ print("--- File upload/listing WILL NOT WORK. Set HF_TOKEN_READ and HF_TOKEN_WRITE environment variables.")
 
 
799
  print("---")
800
  else:
801
+ print("--- Hugging Face tokens found.")
 
 
 
 
 
 
802
 
803
  print("--- Server Ready ---")
804
+ # Use a production server like Waitress or Gunicorn instead of app.run() for deployment
 
805
  # from waitress import serve
806
  # serve(app, host=HOST, port=PORT)
807
+ app.run(host=HOST, port=PORT, debug=False)