Shveiauto commited on
Commit
6a4a656
·
verified ·
1 Parent(s): 4c1ad66

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +941 -409
app.py CHANGED
@@ -1,4 +1,4 @@
1
- from flask import Flask, render_template_string, request, jsonify, redirect, url_for, flash
2
  import json
3
  import os
4
  import logging
@@ -18,15 +18,16 @@ import requests
18
  load_dotenv()
19
 
20
  app = Flask(__name__)
21
- app.secret_key = os.getenv("FLASK_SECRET_KEY", 'tontalent_secret_key_for_flash_messages_only')
22
  DATA_FILE = 'wall_data.json'
23
  SYNC_FILES = [DATA_FILE]
24
 
25
- REPO_ID = os.getenv("HF_REPO_ID", "Kgshop/tontalent2")
26
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
27
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
28
 
29
- TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "7549355625:AAGYWatM-nUVQirgBiBwoAtWZgzfp3QnQjY")
 
30
 
31
  DOWNLOAD_RETRIES = 3
32
  DOWNLOAD_DELAY = 5
@@ -40,11 +41,11 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
40
  files_to_download = [specific_file] if specific_file else SYNC_FILES
41
  logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
42
  all_successful = True
 
43
  for file_name in files_to_download:
44
  success = False
45
  for attempt in range(retries + 1):
46
  try:
47
- logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...")
48
  local_path = hf_hub_download(
49
  repo_id=REPO_ID, filename=file_name, repo_type="dataset",
50
  token=token_to_use, local_dir=".", local_dir_use_symlinks=False,
@@ -63,8 +64,10 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
63
  try:
64
  if file_name == DATA_FILE:
65
  with open(file_name, 'w', encoding='utf-8') as f:
66
- json.dump({'users': {}}, f)
67
  logging.info(f"Created empty local file {file_name} because it was not found on HF.")
 
 
68
  except Exception as create_e:
69
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
70
  success = True
@@ -115,34 +118,59 @@ def periodic_backup():
115
  logging.info("Periodic backup finished.")
116
 
117
  def load_data():
118
- default_data = {'users': {}}
119
  try:
120
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
121
  data = json.load(file)
122
- if not isinstance(data.get('users'), dict):
123
- logging.warning(f"Data in {DATA_FILE} is not in the correct format. Resetting.")
124
- return default_data
 
 
 
125
  return data
126
- except (FileNotFoundError, json.JSONDecodeError):
127
- if download_db_from_hf(specific_file=DATA_FILE):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  try:
129
- with open(DATA_FILE, 'r', encoding='utf-8') as file:
130
- data = json.load(file)
131
- if not isinstance(data.get('users'), dict):
132
- logging.warning(f"Downloaded data in {DATA_FILE} is not correct. Resetting.")
133
- return default_data
134
- return data
135
- except (FileNotFoundError, json.JSONDecodeError):
136
- return default_data
137
  return default_data
138
 
139
  def save_data(data):
140
  try:
 
 
 
 
 
 
 
141
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
142
  json.dump(data, file, ensure_ascii=False, indent=4)
 
143
  upload_db_to_hf(specific_file=DATA_FILE)
144
  except Exception as e:
145
- logging.error(f"Error saving data: {e}", exc_info=True)
146
 
147
  def verify_telegram_auth_data(auth_data_str, bot_token):
148
  if not auth_data_str:
@@ -172,34 +200,416 @@ def verify_telegram_auth_data(auth_data_str, bot_token):
172
  return False, None
173
  return False, None
174
 
175
- def send_telegram_notification(chat_id, message_text):
176
  if not TELEGRAM_BOT_TOKEN:
177
- logging.warning("TELEGRAM_BOT_TOKEN not set, cannot send notification.")
178
- return
179
 
180
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
181
  payload = {
182
  'chat_id': chat_id,
183
- 'text': message_text,
184
- 'parse_mode': 'HTML'
185
  }
 
186
  try:
187
  response = requests.post(url, json=payload)
188
- response_json = response.json()
189
- if not response_json.get('ok'):
190
- logging.error(f"Failed to send Telegram notification: {response_json.get('description')}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  except Exception as e:
192
- logging.error(f"Exception while sending Telegram notification: {e}")
 
 
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
- WALL_TEMPLATE = '''
196
  <!DOCTYPE html>
197
  <html lang="en">
198
  <head>
199
  <meta charset="UTF-8">
200
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
201
- <title>Telegram Wall</title>
202
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
 
203
  <style>
204
  :root {
205
  --tg-theme-bg-color: #ffffff;
@@ -211,16 +621,25 @@ WALL_TEMPLATE = '''
211
  --tg-theme-secondary-bg-color: #f0f0f0;
212
  --tg-theme-header-bg-color: #efeff4;
213
  --tg-theme-section-bg-color: #ffffff;
 
 
 
214
  }
215
  body {
216
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
217
  margin: 0;
218
  padding: 0;
219
  background-color: var(--tg-theme-bg-color);
220
  color: var(--tg-theme-text-color);
221
  overscroll-behavior-y: none;
 
 
 
 
 
 
222
  }
223
- .app-container { display: flex; flex-direction: column; min-height: 100vh; }
224
  .header {
225
  background-color: var(--tg-theme-header-bg-color);
226
  padding: 12px 15px;
@@ -232,338 +651,578 @@ WALL_TEMPLATE = '''
232
  top: 0;
233
  z-index: 100;
234
  }
235
- .tabs { display: flex; background-color: var(--tg-theme-secondary-bg-color); padding: 5px; }
236
- .tab-button {
237
- flex: 1; padding: 12px 10px; text-align: center; cursor: pointer;
238
- background: none; border: none; color: var(--tg-theme-hint-color);
239
- font-size: 15px; font-weight: 500; border-bottom: 2.5px solid transparent;
240
- transition: color 0.2s ease, border-bottom-color 0.2s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  -webkit-tap-highlight-color: transparent;
242
  }
243
- .tab-button.active { color: var(--tg-theme-link-color); border-bottom-color: var(--tg-theme-link-color); }
244
- .content { flex-grow: 1; padding: 15px; }
245
- .loading, .empty-state { text-align: center; padding: 50px 15px; color: var(--tg-theme-hint-color); font-size: 16px; }
246
-
247
- .post-form {
248
  background-color: var(--tg-theme-section-bg-color);
249
  padding: 15px;
 
250
  border-radius: 10px;
251
- margin-bottom: 20px;
 
 
252
  }
253
- .post-form textarea {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  width: 100%;
255
- padding: 12px;
256
  border: 1px solid var(--tg-theme-secondary-bg-color);
257
  border-radius: 8px;
258
  font-size: 16px;
259
  background-color: var(--tg-theme-bg-color);
260
  color: var(--tg-theme-text-color);
261
  box-sizing: border-box;
262
- min-height: 80px;
263
- resize: vertical;
264
- }
265
- .post-form .form-actions {
266
- display: flex;
267
- justify-content: space-between;
268
- align-items: center;
269
- margin-top: 10px;
270
- }
271
- .post-form .file-inputs label {
272
- color: var(--tg-theme-link-color);
273
- cursor: pointer;
274
- margin-right: 15px;
275
- font-size: 14px;
276
- }
277
- .post-form .file-inputs input { display: none; }
278
-
279
- .post-form button {
280
- background-color: var(--tg-theme-button-color);
281
- color: var(--tg-theme-button-text-color);
282
- border: none;
283
- padding: 10px 20px;
284
- border-radius: 8px;
285
- font-size: 15px;
286
- font-weight: 500;
287
- cursor: pointer;
288
  }
 
 
289
 
290
- .post-card {
291
- background-color: var(--tg-theme-section-bg-color);
292
- padding: 15px;
293
- border-radius: 10px;
294
- margin-bottom: 15px;
295
- box-shadow: 0 2px 8px rgba(0,0,0,0.06);
296
- }
297
- .post-card .post-header {
298
- display: flex;
299
- align-items: center;
300
- margin-bottom: 10px;
301
- }
302
- .post-card .post-header img {
303
- width: 36px;
304
- height: 36px;
305
- border-radius: 50%;
306
- margin-right: 10px;
307
- background-color: var(--tg-theme-secondary-bg-color);
308
- }
309
- .post-card .post-header-info {
310
- display: flex;
311
- flex-direction: column;
312
- }
313
- .post-card .post-header .author { font-weight: 600; color: var(--tg-theme-text-color); }
314
- .post-card .post-header .timestamp { font-size: 13px; color: var(--tg-theme-hint-color); }
315
- .post-card .post-content {
316
- font-size: 15px;
317
- line-height: 1.5;
318
- white-space: pre-wrap;
319
- word-wrap: break-word;
320
- }
321
- .post-card .post-media { margin-top: 10px; }
322
- .post-card .post-media img, .post-card .post-media video { max-width: 100%; border-radius: 8px; }
323
- .post-card .post-media a { color: var(--tg-theme-link-color); }
324
 
325
- .user-list-item {
326
- display: flex;
327
- align-items: center;
328
- padding: 12px 0;
329
- border-bottom: 1px solid var(--tg-theme-secondary-bg-color);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  cursor: pointer;
 
331
  }
332
- .user-list-item:last-child { border-bottom: none; }
333
- .user-list-item img {
334
- width: 45px;
335
- height: 45px;
336
- border-radius: 50%;
337
- margin-right: 12px;
338
- background-color: var(--tg-theme-secondary-bg-color);
339
- }
340
- .user-list-item .user-info span { font-size: 16px; font-weight: 500; }
341
- .user-list-item .user-info p { margin: 2px 0 0 0; font-size: 14px; color: var(--tg-theme-hint-color); }
342
  </style>
343
  </head>
344
  <body>
345
  <div class="app-container">
346
- <div class="header" id="appHeader">My Wall</div>
347
- <div class="tabs">
348
- <button class="tab-button active" id="myWallTab">My Wall</button>
349
- <button class="tab-button" id="usersTab">Users</button>
350
- </div>
351
  <div class="content" id="mainContent">
352
  <div class="loading">Loading...</div>
353
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  </div>
355
 
356
  <script>
357
  const tg = window.Telegram.WebApp;
358
  let currentUser = null;
359
- let currentView = 'my_wall';
360
- let viewingUserId = null;
361
-
362
  const mainContent = document.getElementById('mainContent');
363
  const appHeader = document.getElementById('appHeader');
364
- const myWallTab = document.getElementById('myWallTab');
365
- const usersTab = document.getElementById('usersTab');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
- function applyThemeParams() {
368
- const root = document.documentElement.style;
369
- Object.keys(tg.themeParams).forEach(key => {
370
- const cssVar = `--tg-theme-${key.replace(/_/g, '-')}`;
371
- root.setProperty(cssVar, tg.themeParams[key]);
372
- });
 
 
373
  }
374
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  async function apiCall(endpoint, method = 'GET', body = null) {
376
  const headers = { 'Content-Type': 'application/json' };
377
- if (tg.initData) headers['X-Telegram-Auth'] = tg.initData;
378
-
 
379
  const options = { method, headers };
380
  if (body) options.body = JSON.stringify(body);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
 
382
- const response = await fetch(endpoint, options);
383
- if (!response.ok) {
384
- const errorData = await response.json().catch(() => ({ error: 'Request failed' }));
385
- throw new Error(errorData.error || `HTTP error ${response.status}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  }
387
- return response.json();
388
  }
389
 
390
- function renderPostForm(wallOwnerId) {
391
- return `
392
- <div class="post-form">
393
- <textarea id="postText" placeholder="Write something on the wall..."></textarea>
394
- <div class="form-actions">
395
- <div class="file-inputs">
396
- <label for="photo-upload">Photo</label>
397
- <input type="file" id="photo-upload" accept="image/*" disabled>
398
- <label for="video-upload">Video</label>
399
- <input type="file" id="video-upload" accept="video/*" disabled>
400
- <label for="doc-upload">File</label>
401
- <input type="file" id="doc-upload" disabled>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  </div>
403
- <button onclick="submitPost('${wallOwnerId}')">Post</button>
404
  </div>
405
- <p style="font-size:12px; color:var(--tg-theme-hint-color); margin-top:8px;">File uploads are not yet implemented.</p>
 
 
 
 
406
  </div>
407
  `;
 
 
 
 
408
  }
409
 
410
- function renderPostCard(post) {
411
- const authorUsername = post.author_username ? `@${post.author_username}` : 'Anonymous';
412
- const postDate = new Date(post.timestamp).toLocaleString();
413
- return `
414
- <div class="post-card">
415
- <div class="post-header">
416
- <img src="${post.author_photo_url || ''}" alt="Avatar">
417
- <div class="post-header-info">
418
- <span class="author">${post.author_first_name || 'User'}</span>
419
- <span class="timestamp">${authorUsername} • ${postDate}</span>
420
- </div>
421
- </div>
422
- <div class="post-content">${post.text}</div>
 
 
 
 
 
423
  </div>
424
  `;
 
 
 
 
425
  }
426
-
427
- function renderWallView(wallOwnerId, wallOwnerName, posts) {
428
- let wallHtml = renderPostForm(wallOwnerId);
429
- if (posts && posts.length > 0) {
430
- wallHtml += posts.map(renderPostCard).join('');
 
 
 
 
 
 
 
 
 
431
  } else {
432
- wallHtml += `<div class="empty-state">This wall is empty. Be the first to post!</div>`;
433
  }
434
- mainContent.innerHTML = wallHtml;
435
- }
436
 
437
- async function loadMyWall() {
438
- if (!currentUser) return;
439
- currentView = 'my_wall';
440
- viewingUserId = null;
441
- appHeader.textContent = 'My Wall';
442
- myWallTab.classList.add('active');
443
- usersTab.classList.remove('active');
444
- mainContent.innerHTML = '<div class="loading">Loading your wall...</div>';
445
  tg.BackButton.hide();
446
-
447
- try {
448
- const wallData = await apiCall(`/api/wall/${currentUser.id}`);
449
- renderWallView(currentUser.id, 'My', wallData.posts);
450
- } catch (error) {
451
- tg.showAlert(`Error loading your wall: ${error.message}`);
452
- mainContent.innerHTML = `<div class="empty-state">Could not load your wall.</div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  }
454
  }
455
-
456
- async function loadUsersList() {
457
- currentView = 'users_list';
458
- viewingUserId = null;
459
- appHeader.textContent = 'Users';
460
- myWallTab.classList.remove('active');
461
- usersTab.classList.add('active');
462
- mainContent.innerHTML = '<div class="loading">Loading users...</div>';
463
 
464
- tg.BackButton.show();
465
- tg.BackButton.onClick(loadMyWall);
 
 
 
466
 
467
- try {
468
- const users = await apiCall('/api/users');
469
- if (users.length <= 1) { // Only current user exists
470
- mainContent.innerHTML = `<div class="empty-state">No other users have joined yet.</div>`;
 
 
 
 
471
  } else {
472
- mainContent.innerHTML = users
473
- .filter(user => String(user.id) !== String(currentUser.id))
474
- .map(user => `
475
- <div class="user-list-item" onclick="loadUserWall('${user.id}')">
476
- <img src="${user.photo_url || ''}" alt="Avatar">
477
- <div class="user-info">
478
- <span>${user.first_name || ''} ${user.last_name || ''}</span>
479
- <p>${user.username ? `@${user.username}` : `ID: ${user.id}`}</p>
480
- </div>
481
- </div>
482
- `).join('');
483
  }
484
- } catch (error) {
485
- tg.showAlert(`Error loading users: ${error.message}`);
486
- mainContent.innerHTML = `<div class="empty-state">Could not load users.</div>`;
487
- }
488
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
 
490
- async function loadUserWall(userId) {
491
- if (!userId) return;
492
- currentView = 'user_wall';
493
- viewingUserId = userId;
494
- mainContent.innerHTML = '<div class="loading">Loading wall...</div>';
495
 
496
- tg.BackButton.show();
497
- tg.BackButton.onClick(loadUsersList);
 
 
 
 
 
498
 
499
- try {
500
- const wallData = await apiCall(`/api/wall/${userId}`);
501
- const wallOwner = wallData.owner;
502
- const ownerName = wallOwner.first_name || `User ${wallOwner.id}`;
503
- appHeader.textContent = `${ownerName}'s Wall`;
504
- renderWallView(userId, ownerName, wallData.posts);
505
- } catch (error) {
506
- tg.showAlert(`Error loading wall: ${error.message}`);
507
- mainContent.innerHTML = `<div class="empty-state">Could not load this wall.</div>`;
508
- }
509
  }
510
-
511
- async function submitPost(wallOwnerId) {
512
- const postTextArea = document.getElementById('postText');
513
- const text = postTextArea.value.trim();
514
 
515
- if (!text) {
516
- tg.showAlert('Please write something before posting.');
 
 
 
 
 
 
 
 
517
  return;
518
  }
 
 
 
 
 
 
519
 
520
  tg.MainButton.showProgress();
521
  tg.HapticFeedback.impactOccurred('light');
522
 
523
- try {
524
- const newPost = await apiCall(`/api/wall/${wallOwnerId}/posts`, 'POST', { text });
525
- tg.HapticFeedback.notificationOccurred('success');
526
- postTextArea.value = '';
527
-
528
- if (currentView === 'my_wall') {
529
- loadMyWall();
530
- } else if (currentView === 'user_wall') {
531
- loadUserWall(wallOwnerId);
532
- }
533
- } catch (error) {
534
- tg.HapticFeedback.notificationOccurred('error');
535
- tg.showAlert(`Failed to post: ${error.message}`);
536
- } finally {
537
- tg.MainButton.hideProgress();
538
- }
 
 
 
 
 
 
 
539
  }
540
 
541
  async function init() {
542
  tg.ready();
 
 
 
 
 
543
  applyThemeParams();
544
  tg.expand();
545
  tg.enableClosingConfirmation();
546
 
547
  tg.onEvent('themeChanged', applyThemeParams);
548
 
549
- myWallTab.addEventListener('click', () => {
550
- if (currentView !== 'my_wall') loadMyWall();
551
- });
552
- usersTab.addEventListener('click', () => {
553
- if (currentView !== 'users_list') loadUsersList();
554
- });
555
-
556
  try {
557
  const authResponse = await apiCall('/api/auth_user', 'POST', { init_data: tg.initData });
558
  currentUser = authResponse.user;
559
- if (!currentUser) throw new Error("Authentication failed, user data not received.");
560
-
561
- loadMyWall();
562
  } catch (error) {
563
  console.error("Auth error:", error);
564
- mainContent.innerHTML = `<div class="empty-state">Error: Could not authenticate with server. Please try reloading.</div>`;
565
- tg.showAlert(error.message);
566
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
567
  }
568
 
569
  init();
@@ -572,143 +1231,16 @@ WALL_TEMPLATE = '''
572
  </html>
573
  '''
574
 
 
575
  @app.route('/')
576
  def main_app_view():
577
- return render_template_string(WALL_TEMPLATE)
578
-
579
- @app.route('/api/auth_user', methods=['POST'])
580
- def auth_user():
581
- auth_data_str = request.headers.get('X-Telegram-Auth') or request.json.get('init_data')
582
- if not auth_data_str:
583
- return jsonify({"error": "Authentication data not provided"}), 401
584
-
585
- is_valid, user_data_from_auth = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
586
-
587
- if not is_valid or not user_data_from_auth:
588
- return jsonify({"error": "Invalid authentication data"}), 403
589
-
590
- data = load_data()
591
- users = data.get('users', {})
592
- user_id_str = str(user_data_from_auth.get('id'))
593
-
594
- if user_id_str not in users:
595
- users[user_id_str] = {
596
- 'id': user_data_from_auth.get('id'),
597
- 'first_name': user_data_from_auth.get('first_name'),
598
- 'last_name': user_data_from_auth.get('last_name'),
599
- 'username': user_data_from_auth.get('username'),
600
- 'photo_url': user_data_from_auth.get('photo_url'),
601
- 'first_seen': datetime.now().isoformat(),
602
- 'wall_posts': []
603
- }
604
-
605
- users[user_id_str]['last_seen'] = datetime.now().isoformat()
606
- if user_data_from_auth.get('photo_url'):
607
- users[user_id_str]['photo_url'] = user_data_from_auth.get('photo_url')
608
-
609
- data['users'] = users
610
- save_data(data)
611
-
612
- return jsonify({"message": "User authenticated", "user": users[user_id_str]}), 200
613
-
614
- def get_authenticated_user(request_headers):
615
- auth_data_str = request_headers.get('X-Telegram-Auth')
616
- if not auth_data_str: return None
617
- is_valid, user_data_from_auth = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
618
- if is_valid and user_data_from_auth:
619
- return user_data_from_auth
620
- return None
621
-
622
- @app.route('/api/users', methods=['GET'])
623
- def get_users():
624
- user = get_authenticated_user(request.headers)
625
- if not user: return jsonify({"error": "Authentication required"}), 401
626
-
627
- data = load_data()
628
- users_list = [
629
- {
630
- 'id': u.get('id'),
631
- 'first_name': u.get('first_name'),
632
- 'last_name': u.get('last_name'),
633
- 'username': u.get('username'),
634
- 'photo_url': u.get('photo_url')
635
- }
636
- for u in data.get('users', {}).values()
637
- ]
638
- return jsonify(users_list), 200
639
-
640
- @app.route('/api/wall/<wall_owner_id>', methods=['GET'])
641
- def get_wall(wall_owner_id):
642
- user = get_authenticated_user(request.headers)
643
- if not user: return jsonify({"error": "Authentication required"}), 401
644
-
645
- data = load_data()
646
- owner_data = data.get('users', {}).get(wall_owner_id)
647
-
648
- if not owner_data:
649
- return jsonify({"error": "Wall owner not found"}), 404
650
-
651
- owner_info = {
652
- 'id': owner_data.get('id'),
653
- 'first_name': owner_data.get('first_name'),
654
- 'username': owner_data.get('username')
655
- }
656
-
657
- posts = sorted(owner_data.get('wall_posts', []), key=lambda x: x['timestamp'], reverse=True)
658
-
659
- return jsonify({"owner": owner_info, "posts": posts}), 200
660
-
661
- @app.route('/api/wall/<wall_owner_id>/posts', methods=['POST'])
662
- def create_post(wall_owner_id):
663
- author = get_authenticated_user(request.headers)
664
- if not author: return jsonify({"error": "Authentication required"}), 401
665
-
666
- req_data = request.json
667
- post_text = req_data.get('text')
668
- if not post_text:
669
- return jsonify({"error": "Post text cannot be empty"}), 400
670
-
671
- data = load_data()
672
- wall_owner = data.get('users', {}).get(wall_owner_id)
673
- if not wall_owner:
674
- return jsonify({"error": "Wall owner not found"}), 404
675
-
676
- author_id_str = str(author.get('id'))
677
- author_full_info = data.get('users', {}).get(author_id_str, {})
678
-
679
- new_post = {
680
- "post_id": str(uuid.uuid4()),
681
- "author_id": author.get('id'),
682
- "author_first_name": author.get('first_name'),
683
- "author_username": author.get('username'),
684
- "author_photo_url": author_full_info.get('photo_url'),
685
- "text": post_text,
686
- "timestamp": datetime.now().isoformat(),
687
- }
688
-
689
- if 'wall_posts' not in wall_owner:
690
- wall_owner['wall_posts'] = []
691
-
692
- wall_owner['wall_posts'].append(new_post)
693
- save_data(data)
694
-
695
- if str(wall_owner_id) != str(author.get('id')):
696
- author_name = author.get('first_name', 'Someone')
697
- author_link = f"@{author.get('username')}" if author.get('username') else f"A user (ID: {author.get('id')})"
698
- message = f"<b>New post on your wall!</b>\n\n{author_name} ({author_link}) wrote:\n<i>{post_text}</i>"
699
- send_telegram_notification(wall_owner_id, message)
700
-
701
- return jsonify(new_post), 201
702
 
703
  if __name__ == '__main__':
704
- logging.info("Application starting up...")
705
- initial_download_success = download_db_from_hf()
706
- if initial_download_success:
707
- logging.info("Initial data sync from Hugging Face was successful.")
708
- else:
709
- logging.warning("Initial data sync from Hugging Face failed. Starting with local or empty data.")
710
-
711
  load_data()
 
712
 
713
  if HF_TOKEN_WRITE:
714
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
 
1
+ from flask import Flask, render_template_string, request, redirect, url_for, jsonify, flash
2
  import json
3
  import os
4
  import logging
 
18
  load_dotenv()
19
 
20
  app = Flask(__name__)
21
+ app.secret_key = os.getenv("FLASK_SECRET_KEY", 'telegram_wall_secret_key_for_flash_messages')
22
  DATA_FILE = 'wall_data.json'
23
  SYNC_FILES = [DATA_FILE]
24
 
25
+ REPO_ID = os.getenv("HF_REPO_ID", "Kgshop/tontalent2") # Using existing repo config
26
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
27
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
28
 
29
+ # IMPORTANT: Replace with your actual Bot Token from .env or config if necessary
30
+ TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "7549355625:8283649768:AAGYWatM-nUVQirgBiBwoAtWZgzfp3QnQjY")
31
 
32
  DOWNLOAD_RETRIES = 3
33
  DOWNLOAD_DELAY = 5
 
41
  files_to_download = [specific_file] if specific_file else SYNC_FILES
42
  logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
43
  all_successful = True
44
+ default_data = {'users': {}, 'posts': {}}
45
  for file_name in files_to_download:
46
  success = False
47
  for attempt in range(retries + 1):
48
  try:
 
49
  local_path = hf_hub_download(
50
  repo_id=REPO_ID, filename=file_name, repo_type="dataset",
51
  token=token_to_use, local_dir=".", local_dir_use_symlinks=False,
 
64
  try:
65
  if file_name == DATA_FILE:
66
  with open(file_name, 'w', encoding='utf-8') as f:
67
+ json.dump(default_data, f)
68
  logging.info(f"Created empty local file {file_name} because it was not found on HF.")
69
+ success = True # Consider a 404 for DATA_FILE as successful if we create it
70
+ break
71
  except Exception as create_e:
72
  logging.error(f"Failed to create empty local file {file_name}: {create_e}")
73
  success = True
 
118
  logging.info("Periodic backup finished.")
119
 
120
  def load_data():
121
+ default_data = {'users': {}, 'posts': {}}
122
  try:
123
  with open(DATA_FILE, 'r', encoding='utf-8') as file:
124
  data = json.load(file)
125
+ logging.info(f"Local data loaded successfully from {DATA_FILE}")
126
+ if not isinstance(data, dict):
127
+ logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
128
+ raise FileNotFoundError
129
+ for key in default_data:
130
+ if key not in data: data[key] = default_data[key]
131
  return data
132
+ except (FileNotFoundError, json.JSONDecodeError) as e:
133
+ logging.warning(f"Error loading local data ({e}). Attempting download from HF.")
134
+
135
+ if download_db_from_hf(specific_file=DATA_FILE):
136
+ try:
137
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
138
+ data = json.load(file)
139
+ logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
140
+ if not isinstance(data, dict):
141
+ logging.error(f"Downloaded {DATA_FILE} is not a dictionary. Using default.")
142
+ return default_data
143
+ for key in default_data:
144
+ if key not in data: data[key] = default_data[key]
145
+ return data
146
+ except Exception as load_e:
147
+ logging.error(f"Error loading downloaded {DATA_FILE}: {load_e}. Using default.", exc_info=True)
148
+ return default_data
149
+ else:
150
+ logging.error(f"Failed to download {DATA_FILE} from HF. Using empty default data structure.")
151
+ if not os.path.exists(DATA_FILE):
152
  try:
153
+ with open(DATA_FILE, 'w', encoding='utf-8') as f: json.dump(default_data, f)
154
+ logging.info(f"Created empty local file {DATA_FILE} after failed download.")
155
+ except Exception as create_e:
156
+ logging.error(f"Failed to create empty local file {DATA_FILE}: {create_e}")
 
 
 
 
157
  return default_data
158
 
159
  def save_data(data):
160
  try:
161
+ if not isinstance(data, dict):
162
+ logging.error("Attempted to save invalid data structure (not a dict). Aborting save.")
163
+ return
164
+ default_keys = {'users': {}, 'posts': {}}
165
+ for key in default_keys:
166
+ if key not in data: data[key] = default_keys[key]
167
+
168
  with open(DATA_FILE, 'w', encoding='utf-8') as file:
169
  json.dump(data, file, ensure_ascii=False, indent=4)
170
+ logging.info(f"Data successfully saved to {DATA_FILE}")
171
  upload_db_to_hf(specific_file=DATA_FILE)
172
  except Exception as e:
173
+ logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
174
 
175
  def verify_telegram_auth_data(auth_data_str, bot_token):
176
  if not auth_data_str:
 
200
  return False, None
201
  return False, None
202
 
203
+ def send_telegram_notification(chat_id, text):
204
  if not TELEGRAM_BOT_TOKEN:
205
+ logging.warning("TELEGRAM_BOT_TOKEN not set. Skipping notification.")
206
+ return False
207
 
208
  url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
209
  payload = {
210
  'chat_id': chat_id,
211
+ 'text': text,
212
+ 'parse_mode': 'Markdown'
213
  }
214
+
215
  try:
216
  response = requests.post(url, json=payload)
217
+ if response.status_code == 200:
218
+ logging.info(f"Notification sent to chat ID {chat_id} successfully.")
219
+ return True
220
+ else:
221
+ logging.error(f"Failed to send notification to {chat_id}: {response.text}")
222
+ return False
223
+ except requests.RequestException as e:
224
+ logging.error(f"Request error sending notification to {chat_id}: {e}")
225
+ return False
226
+
227
+ def get_authenticated_user_details(request_headers):
228
+ auth_data_str = request_headers.get('X-Telegram-Auth')
229
+ if not auth_data_str:
230
+ return None
231
+ is_valid, user_data_from_auth = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
232
+ if is_valid and user_data_from_auth:
233
+ data = load_data()
234
+ user_id_str = str(user_data_from_auth.get('id'))
235
+ return data.get('users', {}).get(user_id_str)
236
+ return None
237
+
238
+ # --- API Endpoints ---
239
+
240
+ @app.route('/api/auth_user', methods=['POST'])
241
+ def auth_user():
242
+ auth_data_str = request.headers.get('X-Telegram-Auth')
243
+ if not auth_data_str:
244
+ init_data_payload = request.json.get('init_data')
245
+ if init_data_payload:
246
+ auth_data_str = init_data_payload
247
+ else:
248
+ return jsonify({"error": "Authentication data not provided"}), 401
249
+
250
+ is_valid, user_data_from_auth = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
251
+
252
+ if not is_valid or not user_data_from_auth:
253
+ return jsonify({"error": "Invalid authentication data"}), 403
254
+
255
+ data = load_data()
256
+ users = data.get('users', {})
257
+ user_id_str = str(user_data_from_auth.get('id'))
258
+ chat_id = str(user_data_from_auth.get('id'))
259
+
260
+ if user_id_str not in users:
261
+ users[user_id_str] = {
262
+ 'id': user_data_from_auth.get('id'),
263
+ 'first_name': user_data_from_auth.get('first_name'),
264
+ 'last_name': user_data_from_auth.get('last_name'),
265
+ 'username': user_data_from_auth.get('username'),
266
+ 'language_code': user_data_from_auth.get('language_code'),
267
+ 'photo_url': user_data_from_auth.get('photo_url'),
268
+ 'chat_id': chat_id, # Use user ID as chat_id for notifications
269
+ 'first_seen': datetime.now().isoformat()
270
+ }
271
+
272
+ user_updates = {
273
+ 'last_seen': datetime.now().isoformat(),
274
+ 'language_code': user_data_from_auth.get('language_code'),
275
+ 'username': user_data_from_auth.get('username'),
276
+ 'first_name': user_data_from_auth.get('first_name'),
277
+ 'last_name': user_data_from_auth.get('last_name'),
278
+ }
279
+ if user_data_from_auth.get('photo_url'):
280
+ user_updates['photo_url'] = user_data_from_auth.get('photo_url')
281
+
282
+ users[user_id_str].update(user_updates)
283
+
284
+ data['users'] = users
285
+ save_data(data)
286
+
287
+ return jsonify({"message": "User authenticated", "user": users[user_id_str]}), 200
288
+
289
+ @app.route('/api/users', methods=['GET'])
290
+ def get_users():
291
+ data = load_data()
292
+ users = list(data.get('users', {}).values())
293
+
294
+ # Sort users by last seen, most recent first
295
+ sorted_users = sorted(users, key=lambda x: x.get('last_seen', ''), reverse=True)
296
+
297
+ # Simple list of users
298
+ user_list = [
299
+ {
300
+ 'id': u['id'],
301
+ 'first_name': u['first_name'],
302
+ 'last_name': u['last_name'],
303
+ 'username': u['username'],
304
+ 'photo_url': u.get('photo_url'),
305
+ 'last_seen': u['last_seen']
306
+ } for u in sorted_users
307
+ ]
308
+ return jsonify(user_list), 200
309
+
310
+ @app.route('/api/posts', methods=['GET'])
311
+ def get_feed_posts():
312
+ data = load_data()
313
+ all_posts = []
314
+
315
+ # Flatten all posts from all users
316
+ for wall_owner_id, posts in data.get('posts', {}).items():
317
+ all_posts.extend(posts)
318
+
319
+ # Sort all posts by timestamp, newest first
320
+ sorted_posts = sorted(all_posts, key=lambda x: x.get('timestamp', ''), reverse=True)
321
+
322
+ # Enrich with user info
323
+ users = data.get('users', {})
324
+ for post in sorted_posts:
325
+ author = users.get(str(post['author_id']), {})
326
+ post['author_name'] = f"{author.get('first_name', 'Unknown')} {author.get('last_name', '')}".strip() or "Unknown User"
327
+ post['author_username'] = author.get('username')
328
+ wall_owner = users.get(str(post['wall_owner_id']), {})
329
+ post['wall_owner_name'] = f"{wall_owner.get('first_name', 'Unknown')} {wall_owner.get('last_name', '')}".strip() or "Unknown User"
330
+ post['wall_owner_username'] = wall_owner.get('username')
331
+
332
+ return jsonify(sorted_posts), 200
333
+
334
+ @app.route('/api/posts/<user_id>', methods=['GET'])
335
+ def get_user_wall_posts(user_id):
336
+ data = load_data()
337
+ user_posts = data.get('posts', {}).get(user_id, [])
338
+
339
+ # Sort posts by timestamp, newest first
340
+ sorted_posts = sorted(user_posts, key=lambda x: x.get('timestamp', ''), reverse=True)
341
+
342
+ # Enrich with author info
343
+ users = data.get('users', {})
344
+ wall_owner = users.get(user_id)
345
+
346
+ for post in sorted_posts:
347
+ author = users.get(str(post['author_id']), {})
348
+ post['author_name'] = f"{author.get('first_name', 'Unknown')} {author.get('last_name', '')}".strip() or "Unknown User"
349
+ post['author_username'] = author.get('username')
350
+ post['wall_owner_name'] = f"{wall_owner.get('first_name', 'Unknown')} {wall_owner.get('last_name', '')}".strip() if wall_owner else "Unknown User"
351
+ post['wall_owner_username'] = wall_owner.get('username')
352
+
353
+ return jsonify(sorted_posts), 200
354
+
355
+ @app.route('/api/post_to_wall/<wall_owner_id>', methods=['POST'])
356
+ def create_post(wall_owner_id):
357
+ user = get_authenticated_user_details(request.headers)
358
+ if not user:
359
+ return jsonify({"error": "Authentication required"}), 401
360
+
361
+ if str(wall_owner_id) not in load_data().get('users', {}):
362
+ return jsonify({"error": "Wall owner not found"}), 404
363
+
364
+ req_data = request.json
365
+ text = req_data.get('text', '').strip()
366
+ media_type = req_data.get('media_type') # e.g., 'photo', 'video', 'document'
367
+ media_url = req_data.get('media_url') # Placeholder for uploaded file URL
368
+
369
+ if not text and not media_url:
370
+ return jsonify({"error": "Post must contain text or media"}), 400
371
+
372
+ data = load_data()
373
+ wall_owner = data['users'].get(str(wall_owner_id))
374
+
375
+ new_post = {
376
+ "id": str(uuid.uuid4()),
377
+ "wall_owner_id": str(wall_owner_id),
378
+ "author_id": str(user['id']),
379
+ "text": text,
380
+ "media_type": media_type if media_type and media_url else None,
381
+ "media_url": media_url if media_url else None,
382
+ "timestamp": datetime.now().isoformat(),
383
+ }
384
+
385
+ if wall_owner_id not in data['posts']:
386
+ data['posts'][wall_owner_id] = []
387
+
388
+ data['posts'][wall_owner_id].append(new_post)
389
+ save_data(data)
390
+
391
+ # --- Notification Logic ---
392
+ if str(wall_owner_id) != str(user['id']):
393
+ author_name = f"{user.get('first_name', 'Someone')} {user.get('last_name', '')}".strip()
394
+ author_username = user.get('username')
395
+
396
+ # Build notification text
397
+ notification_text = f"🚨 *New Post on your Wall!* 🚨\n\n"
398
+ if author_username:
399
+ notification_text += f"From: @{author_username}\n"
400
+ else:
401
+ notification_text += f"From: {author_name} (ID: {user['id']})\n"
402
+
403
+ if text:
404
+ notification_text += f"Text: \"{text[:100]}...\"" if len(text) > 100 else f"Text: \"{text}\""
405
+
406
+ if new_post['media_type']:
407
+ notification_text += f"\n_Contains a {new_post['media_type']}_"
408
+
409
+ # NOTE: Cannot open the mini-app directly from a notification without a specific bot setup
410
+ # notification_text += f"\n\n[Open Wall](https://t.me/your_bot_username/your_miniapp?startapp={wall_owner_id})"
411
+
412
+ if wall_owner and wall_owner.get('chat_id'):
413
+ send_telegram_notification(wall_owner['chat_id'], notification_text)
414
+
415
+ # Enrich post for immediate response
416
+ new_post['author_name'] = f"{user.get('first_name', 'Unknown')} {user.get('last_name', '')}".strip() or "Unknown User"
417
+ new_post['author_username'] = user.get('username')
418
+ new_post['wall_owner_name'] = f"{wall_owner.get('first_name', 'Unknown')} {wall_owner.get('last_name', '')}".strip() or "Unknown User"
419
+ new_post['wall_owner_username'] = wall_owner.get('username')
420
+
421
+ return jsonify(new_post), 201
422
+
423
+ # --- Admin Panel (Simplified) ---
424
+
425
+ @app.route('/admin')
426
+ def admin_panel():
427
+ data = load_data()
428
+ users = data.get('users', {})
429
+
430
+ # Flatten all posts for admin view
431
+ all_posts = []
432
+ for wall_owner_id, posts in data.get('posts', {}).items():
433
+ all_posts.extend(posts)
434
+
435
+ sorted_posts = sorted(all_posts, key=lambda x: x.get('timestamp', ''), reverse=True)
436
+
437
+ admin_template = '''
438
+ <!DOCTYPE html>
439
+ <html lang="en">
440
+ <head>
441
+ <meta charset="UTF-8">
442
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
443
+ <title>Telegram Wall Admin</title>
444
+ <style>
445
+ body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
446
+ .container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
447
+ h1, h2 { color: #333; }
448
+ .section { margin-bottom: 30px; padding: 15px; border: 1px solid #ddd; border-radius: 5px; background-color: #f9f9f9;}
449
+ .item { border-bottom: 1px solid #eee; padding: 10px 0; }
450
+ .item:last-child { border-bottom: none; }
451
+ .item h3 { margin: 0 0 5px 0; }
452
+ .item p { margin: 3px 0; font-size: 0.9em; color: #555; }
453
+ .button { padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9em; margin-right: 5px; }
454
+ .button-primary { background-color: #007bff; color: white; }
455
+ .button-danger { background-color: #dc3545; color: white; }
456
+ .button-secondary { background-color: #6c757d; color: white; }
457
+ .message { padding: 10px; margin-bottom: 15px; border-radius: 4px; }
458
+ .message.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
459
+ .message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
460
+ .message.warning { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
461
+ .sync-buttons form { display: inline-block; margin-right: 10px; }
462
+ </style>
463
+ </head>
464
+ <body>
465
+ <div class="container">
466
+ <h1>Telegram Wall Admin Panel</h1>
467
+
468
+ {% with messages = get_flashed_messages(with_categories=true) %}
469
+ {% if messages %}
470
+ {% for category, message in messages %}
471
+ <div class="message {{ category }}">{{ message }}</div>
472
+ {% endfor %}
473
+ {% endif %}
474
+ {% endwith %}
475
+
476
+ <div class="section">
477
+ <h2>Data Synchronization with Hugging Face</h2>
478
+ <div class="sync-buttons">
479
+ <form method="POST" action="{{ url_for('force_upload_admin') }}" onsubmit="return confirm('Upload local data to Hugging Face? This will overwrite server data.');">
480
+ <button type="submit" class="button button-primary">Upload DB to HF</button>
481
+ </form>
482
+ <form method="POST" action="{{ url_for('force_download_admin') }}" onsubmit="return confirm('Download data from Hugging Face? This will overwrite local data.');">
483
+ <button type="submit" class="button button-secondary">Download DB from HF</button>
484
+ </form>
485
+ </div>
486
+ <p style="font-size: 0.8em; color: #666;">Automatic backup runs every 30 minutes if HF_TOKEN_WRITE is set.</p>
487
+ </div>
488
+
489
+ <div class="section">
490
+ <h2>Users ({{ users|length }})</h2>
491
+ {% for user_id, user in users.items() %}
492
+ <div class="item">
493
+ <h3>{{ user.first_name }} {{ user.last_name }} (@{{ user.username or 'No Username' }})</h3>
494
+ <p>ID: {{ user_id }} | Last Seen: {{ user.last_seen }}</p>
495
+ <p>Posts on wall: {{ (data.posts.get(user_id) or [])|length }}</p>
496
+ </div>
497
+ {% endfor %}
498
+ </div>
499
+
500
+ <div class="section">
501
+ <h2>All Posts ({{ sorted_posts|length }})</h2>
502
+ {% for post in sorted_posts %}
503
+ {% set author = users.get(post.author_id) %}
504
+ {% set wall_owner = users.get(post.wall_owner_id) %}
505
+ <div class="item">
506
+ <h3>
507
+ {% if post.text %}
508
+ {{ post.text[:50] }}{% if post.text|length > 50 %}...{% endif %}
509
+ {% else %}
510
+ [No Text]
511
+ {% endif %}
512
+ </h3>
513
+ <p>
514
+ Author:
515
+ {% if author %}
516
+ {{ author.first_name }} {{ author.last_name }} (@{{ author.username or 'No Username' }})
517
+ {% else %}
518
+ Unknown Author ({{ post.author_id }})
519
+ {% endif %}
520
+ | On Wall of:
521
+ {% if wall_owner %}
522
+ {{ wall_owner.first_name }} {{ wall_owner.last_name }}
523
+ {% else %}
524
+ Unknown Wall Owner ({{ post.wall_owner_id }})
525
+ {% endif %}
526
+ </p>
527
+ {% if post.media_type %}<p>Media: {{ post.media_type|capitalize }} (URL: {{ post.media_url }})</p>{% endif %}
528
+ <p>Posted: {{ post.timestamp }}</p>
529
+ <form method="POST" action="{{ url_for('admin_delete_post') }}" style="display:inline;" onsubmit="return confirm('Delete this post?');">
530
+ <input type="hidden" name="wall_owner_id" value="{{ post.wall_owner_id }}">
531
+ <input type="hidden" name="post_id" value="{{ post.id }}">
532
+ <button type="submit" class="button button-danger">Delete Post</button>
533
+ </form>
534
+ </div>
535
+ {% else %}
536
+ <p>No posts found.</p>
537
+ {% endfor %}
538
+ </div>
539
+ </div>
540
+ </body>
541
+ </html>
542
+ '''
543
+ return render_template_string(admin_template,
544
+ users=users,
545
+ sorted_posts=sorted_posts,
546
+ data=data)
547
+
548
+ @app.route('/admin/delete_post', methods=['POST'])
549
+ def admin_delete_post():
550
+ wall_owner_id = request.form.get('wall_owner_id')
551
+ post_id = request.form.get('post_id')
552
+
553
+ if not wall_owner_id or not post_id:
554
+ flash('Invalid Post ID or Wall Owner ID for deletion.', 'error')
555
+ return redirect(url_for('admin_panel'))
556
+
557
+ data = load_data()
558
+ posts_list = data.get('posts', {}).get(wall_owner_id, [])
559
+ original_length = len(posts_list)
560
+
561
+ data['posts'][wall_owner_id] = [p for p in posts_list if p['id'] != post_id]
562
+
563
+ if len(data['posts'][wall_owner_id]) < original_length:
564
+ save_data(data)
565
+ flash(f'Post {post_id} deleted successfully from wall {wall_owner_id}.', 'success')
566
+ else:
567
+ flash(f'Post {post_id} not found on wall {wall_owner_id} or already deleted.', 'warning')
568
+
569
+ # Clean up empty wall list
570
+ if not data['posts'][wall_owner_id]:
571
+ del data['posts'][wall_owner_id]
572
+ save_data(data)
573
+
574
+ return redirect(url_for('admin_panel'))
575
+
576
+ @app.route('/admin/force_upload', methods=['POST'])
577
+ def force_upload_admin():
578
+ logging.info("Admin forcing upload to Hugging Face...")
579
+ try:
580
+ upload_db_to_hf()
581
+ flash("Data successfully uploaded to Hugging Face.", 'success')
582
  except Exception as e:
583
+ logging.error(f"Error during forced upload: {e}", exc_info=True)
584
+ flash(f"Error uploading to Hugging Face: {e}", 'error')
585
+ return redirect(url_for('admin_panel'))
586
 
587
+ @app.route('/admin/force_download', methods=['POST'])
588
+ def force_download_admin():
589
+ logging.info("Admin forcing download from Hugging Face...")
590
+ try:
591
+ if download_db_from_hf():
592
+ flash("Data successfully downloaded from Hugging Face. Local files updated.", 'success')
593
+ load_data()
594
+ else:
595
+ flash("Failed to download data from Hugging Face. Check logs.", 'error')
596
+ except Exception as e:
597
+ logging.error(f"Error during forced download: {e}", exc_info=True)
598
+ flash(f"Error downloading from Hugging Face: {e}", 'error')
599
+ return redirect(url_for('admin_panel'))
600
+
601
+
602
+ # --- MAIN APP TEMPLATE (New Wall UI) ---
603
 
604
+ MAIN_APP_TEMPLATE = '''
605
  <!DOCTYPE html>
606
  <html lang="en">
607
  <head>
608
  <meta charset="UTF-8">
609
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
610
+ <title id="appTitle">Telegram Wall</title>
611
  <script src="https://telegram.org/js/telegram-web-app.js"></script>
612
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLMDJdZ4wPuyzYfFw4u3Q23oNqgqFj5Lh2mX5L+R5E5u+5F5A2V5a5S+5A5A5Q==" crossorigin="anonymous" referrerpolicy="no-referrer" />
613
  <style>
614
  :root {
615
  --tg-theme-bg-color: #ffffff;
 
621
  --tg-theme-secondary-bg-color: #f0f0f0;
622
  --tg-theme-header-bg-color: #efeff4;
623
  --tg-theme-section-bg-color: #ffffff;
624
+ --tg-theme-section-header-text-color: #8e8e93;
625
+ --tg-theme-destructive-text-color: #ff3b30;
626
+ --tg-theme-accent-text-color: #007aff;
627
  }
628
  body {
629
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif;
630
  margin: 0;
631
  padding: 0;
632
  background-color: var(--tg-theme-bg-color);
633
  color: var(--tg-theme-text-color);
634
  overscroll-behavior-y: none;
635
+ -webkit-font-smoothing: antialiased;
636
+ -moz-osx-font-smoothing: grayscale;
637
+ line-height: 1.4;
638
+ padding-bottom: 60px; /* Space for bottom nav */
639
+ min-height: 100vh;
640
+ min-height: -webkit-fill-available;
641
  }
642
+ .app-container { display: flex; flex-direction: column; }
643
  .header {
644
  background-color: var(--tg-theme-header-bg-color);
645
  padding: 12px 15px;
 
651
  top: 0;
652
  z-index: 100;
653
  }
654
+ .content { flex-grow: 1; padding: 10px 0; overflow-x: hidden; transition: opacity 0.2s ease-out; }
655
+ .nav-bar {
656
+ position: fixed;
657
+ bottom: 0;
658
+ left: 0;
659
+ right: 0;
660
+ height: 56px;
661
+ background-color: var(--tg-theme-section-bg-color);
662
+ border-top: 0.5px solid var(--tg-theme-secondary-bg-color);
663
+ display: flex;
664
+ justify-content: space-around;
665
+ align-items: center;
666
+ z-index: 1000;
667
+ }
668
+ .nav-item {
669
+ flex: 1;
670
+ display: flex;
671
+ flex-direction: column;
672
+ align-items: center;
673
+ justify-content: center;
674
+ cursor: pointer;
675
+ color: var(--tg-theme-hint-color);
676
+ font-size: 11px;
677
+ padding-top: 5px;
678
+ height: 100%;
679
+ transition: color 0.2s ease;
680
  -webkit-tap-highlight-color: transparent;
681
  }
682
+ .nav-item i { font-size: 20px; margin-bottom: 3px; }
683
+ .nav-item.active { color: var(--tg-theme-link-color); }
684
+
685
+ /* General Item Styles */
686
+ .list-item, .post-item {
687
  background-color: var(--tg-theme-section-bg-color);
688
  padding: 15px;
689
+ margin: 10px 15px;
690
  border-radius: 10px;
691
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
692
+ cursor: pointer;
693
+ transition: background-color 0.1s ease;
694
  }
695
+ .list-item:active, .post-item:active { background-color: var(--tg-theme-secondary-bg-color); }
696
+ .list-item h3 { margin: 0; font-size: 17px; font-weight: 600; color: var(--tg-theme-text-color); }
697
+ .list-item p { margin: 3px 0; font-size: 14px; color: var(--tg-theme-hint-color); }
698
+ .user-avatar { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; margin-right: 10px; background-color: var(--tg-theme-secondary-bg-color); }
699
+ .user-info { display: flex; align-items: center; }
700
+
701
+ /* Post Specific Styles */
702
+ .post-header { display: flex; align-items: center; margin-bottom: 10px; }
703
+ .post-author-name { font-weight: 600; font-size: 15px; color: var(--tg-theme-text-color); }
704
+ .post-username { font-size: 13px; color: var(--tg-theme-hint-color); margin-left: 5px; }
705
+ .post-text { margin: 10px 0; font-size: 16px; white-space: pre-wrap; word-wrap: break-word; color: var(--tg-theme-text-color); }
706
+ .post-meta { font-size: 12px; color: var(--tg-theme-hint-color); margin-top: 10px; border-top: 0.5px solid var(--tg-theme-secondary-bg-color); padding-top: 5px; }
707
+ .post-media { max-width: 100%; height: auto; border-radius: 8px; margin-top: 10px; }
708
+ .post-media-placeholder { font-size: 14px; color: var(--tg-theme-link-color); margin-top: 10px; display: block; }
709
+ .post-wall-owner-link { font-size: 13px; color: var(--tg-theme-link-color); margin-left: 5px; }
710
+
711
+ /* Form/View Styles */
712
+ .view-container { padding: 15px; }
713
+ .form-group { margin-bottom: 15px; }
714
+ .form-group label { display: block; font-size: 14px; color: var(--tg-theme-section-header-text-color); margin-bottom: 5px; font-weight: 500; }
715
+ .form-group input, .form-group textarea, .form-group select {
716
  width: 100%;
717
+ padding: 10px;
718
  border: 1px solid var(--tg-theme-secondary-bg-color);
719
  border-radius: 8px;
720
  font-size: 16px;
721
  background-color: var(--tg-theme-bg-color);
722
  color: var(--tg-theme-text-color);
723
  box-sizing: border-box;
724
+ transition: border-color 0.2s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
  }
726
+ .form-group input:focus, .form-group textarea:focus, .form-group select:focus { border-color: var(--tg-theme-link-color); outline: none; }
727
+ .form-group textarea { min-height: 80px; resize: vertical; }
728
 
729
+ .loading, .empty-state { text-align: center; padding: 50px 15px; color: var(--tg-theme-hint-color); font-size: 16px; }
730
+ .error-message { color: var(--tg-theme-destructive-text-color); font-size: 14px; margin-top: 10px; text-align: center; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
 
732
+ /* Profile View Styles */
733
+ .profile-header { text-align: center; padding: 20px 15px; background-color: var(--tg-theme-secondary-bg-color); border-radius: 10px; margin: 10px 15px; }
734
+ .profile-avatar { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; margin-bottom: 10px; background-color: var(--tg-theme-bg-color); }
735
+ .profile-name { font-size: 20px; font-weight: 600; margin: 0; color: var(--tg-theme-text-color); }
736
+ .profile-username { font-size: 15px; color: var(--tg-theme-hint-color); margin: 5px 0 0 0; }
737
+ .profile-stats { display: flex; justify-content: space-around; margin-top: 15px; }
738
+ .profile-stat span { display: block; font-size: 16px; font-weight: 600; color: var(--tg-theme-text-color); }
739
+ .profile-stat small { font-size: 12px; color: var(--tg-theme-hint-color); }
740
+ .profile-post-section h2 { margin: 10px 15px; font-size: 18px; color: var(--tg-theme-section-header-text-color); font-weight: 500;}
741
+ .profile-post-button {
742
+ background-color: var(--tg-theme-button-color);
743
+ color: var(--tg-theme-button-text-color);
744
+ padding: 12px 15px;
745
+ border: none;
746
+ border-radius: 8px;
747
+ font-size: 16px;
748
+ font-weight: 500;
749
+ width: 100%;
750
+ margin-top: 15px;
751
  cursor: pointer;
752
+ transition: opacity 0.2s;
753
  }
754
+ .profile-post-button:active { opacity: 0.8; }
755
+
 
 
 
 
 
 
 
 
756
  </style>
757
  </head>
758
  <body>
759
  <div class="app-container">
760
+ <div class="header" id="appHeader">Telegram Wall</div>
 
 
 
 
761
  <div class="content" id="mainContent">
762
  <div class="loading">Loading...</div>
763
  </div>
764
+
765
+ <div class="nav-bar" id="bottomNav">
766
+ <div class="nav-item active" data-view="feed">
767
+ <i class="fas fa-home"></i>
768
+ <span id="nav-feed-text">Feed</span>
769
+ </div>
770
+ <div class="nav-item" data-view="my_wall">
771
+ <i class="fas fa-user-circle"></i>
772
+ <span id="nav-mywall-text">My Wall</span>
773
+ </div>
774
+ <div class="nav-item" data-view="users">
775
+ <i class="fas fa-users"></i>
776
+ <span id="nav-users-text">Users</span>
777
+ </div>
778
+ </div>
779
  </div>
780
 
781
  <script>
782
  const tg = window.Telegram.WebApp;
783
  let currentUser = null;
784
+ let currentView = 'feed';
785
+ let currentWallOwnerId = null;
786
+
787
  const mainContent = document.getElementById('mainContent');
788
  const appHeader = document.getElementById('appHeader');
789
+ const bottomNav = document.getElementById('bottomNav');
790
+
791
+ const langStrings = {
792
+ en: {
793
+ appTitle: "Telegram Wall", feed: "Feed", myWall: "My Wall", users: "Users",
794
+ welcome: "Welcome to the Wall!", loading: "Loading...", emptyFeed: "No posts yet. Be the first!",
795
+ emptyWall: "This wall is empty. Post something!", userList: "User List",
796
+ writePost: "Write a Post", writeOnWall: "Write on Wall", postText: "Text", postMedia: "Media Type", postMediaUrl: "Media URL",
797
+ postPlaceholder: "What's on your mind?", postButton: "Post to Wall",
798
+ postedBy: "Posted by", onWallOf: "on wall of", postedOn: "Posted on", mediaPhoto: "Photo", mediaVideo: "Video", mediaDoc: "Document", mediaNone: "None/Text Only",
799
+ profilePosts: "Wall Posts", editProfile: "Edit Profile",
800
+ authFailed: "Authentication with the server failed. Some features might be limited.",
801
+ },
802
+ ru: {
803
+ appTitle: "Стена в Telegram", feed: "Лента", myWall: "Моя Стена", users: "Пользователи",
804
+ welcome: "Добро пожаловать на Стену!", loading: "Загрузка...", emptyFeed: "Постов пока нет. Будьте первыми!",
805
+ emptyWall: "Эта стена пуста. Оставьте запись!", userList: "Список Пользователей",
806
+ writePost: "Написать Пост", writeOnWall: "Написать на Стене", postText: "Текст", postMedia: "Тип Медиа", postMediaUrl: "URL Медиа",
807
+ postPlaceholder: "О чем вы думаете?", postButton: "Опубликовать",
808
+ postedBy: "Опубликовал", onWallOf: "на стене", postedOn: "Опубликовано", mediaPhoto: "Фотография", mediaVideo: "Видео", mediaDoc: "Документ", mediaNone: "Нет/Только Текст",
809
+ profilePosts: "Записи на Стене", editProfile: "Редактировать Профиль",
810
+ authFailed: "Не удалось авторизоваться на сервере. Функционал может быть ограничен.",
811
+ }
812
+ };
813
+ let lang = 'en';
814
+ let T = langStrings[lang];
815
 
816
+ function setLanguage(langCode) {
817
+ lang = langCode === 'ru' ? 'ru' : 'en';
818
+ T = langStrings[lang];
819
+ document.getElementById('appTitle').textContent = T.appTitle;
820
+ document.getElementById('nav-feed-text').textContent = T.feed;
821
+ document.getElementById('nav-mywall-text').textContent = T.myWall;
822
+ document.getElementById('nav-users-text').textContent = T.users;
823
+ appHeader.textContent = T.appTitle;
824
  }
825
 
826
+ function applyThemeParams() {
827
+ const rootStyle = document.documentElement.style;
828
+ rootStyle.setProperty('--tg-theme-bg-color', tg.themeParams.bg_color || '#ffffff');
829
+ rootStyle.setProperty('--tg-theme-text-color', tg.themeParams.text_color || '#000000');
830
+ rootStyle.setProperty('--tg-theme-hint-color', tg.themeParams.hint_color || '#999999');
831
+ rootStyle.setProperty('--tg-theme-link-color', tg.themeParams.link_color || '#007aff');
832
+ rootStyle.setProperty('--tg-theme-button-color', tg.themeParams.button_color || '#007aff');
833
+ rootStyle.setProperty('--tg-theme-button-text-color', tg.themeParams.button_text_color || '#ffffff');
834
+ rootStyle.setProperty('--tg-theme-secondary-bg-color', tg.themeParams.secondary_bg_color || '#f0f0f0');
835
+ rootStyle.setProperty('--tg-theme-header-bg-color', tg.themeParams.header_bg_color || tg.themeParams.secondary_bg_color || '#efeff4');
836
+ rootStyle.setProperty('--tg-theme-section-bg-color', tg.themeParams.section_bg_color || tg.themeParams.bg_color || '#ffffff');
837
+ rootStyle.setProperty('--tg-theme-section-header-text-color', tg.themeParams.section_header_text_color || tg.themeParams.hint_color || '#8e8e93');
838
+ rootStyle.setProperty('--tg-theme-destructive-text-color', tg.themeParams.destructive_text_color || '#ff3b30');
839
+ rootStyle.setProperty('--tg-theme-accent-text-color', tg.themeParams.accent_text_color || tg.themeParams.link_color || '#007aff');
840
+ }
841
+
842
  async function apiCall(endpoint, method = 'GET', body = null) {
843
  const headers = { 'Content-Type': 'application/json' };
844
+ if (tg.initData) {
845
+ headers['X-Telegram-Auth'] = tg.initData;
846
+ }
847
  const options = { method, headers };
848
  if (body) options.body = JSON.stringify(body);
849
+ try {
850
+ const response = await fetch(endpoint, options);
851
+ if (!response.ok) {
852
+ const errorData = await response.json().catch(() => ({ error: 'Request failed without JSON body' }));
853
+ throw new Error(errorData.error || `HTTP error ${response.status}`);
854
+ }
855
+ return response.json();
856
+ } catch (error) {
857
+ console.error('API Call Error:', error);
858
+ // tg.showAlert(error.message || 'An API error occurred.'); // Don't block flow on every minor error
859
+ throw error;
860
+ }
861
+ }
862
+
863
+ function getAvatarUrl(user) {
864
+ return user.photo_url || 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; // Default placeholder
865
+ }
866
+
867
+ function formatUserLink(username) {
868
+ return username ? `<a href="https://t.me/${username}" target="_blank">@${username}</a>` : 'Unknown';
869
+ }
870
+
871
+ function formatDate(isoString) {
872
+ try {
873
+ const date = new Date(isoString);
874
+ return date.toLocaleDateString(lang, { day: 'numeric', month: 'short', year: 'numeric' }) + ' ' +
875
+ date.toLocaleTimeString(lang, { hour: '2-digit', minute: '2-digit' });
876
+ } catch (e) {
877
+ return isoString || 'N/A';
878
+ }
879
+ }
880
+
881
+ // --- Renderer Functions ---
882
+
883
+ function renderPostList(posts, containerId = 'mainContent') {
884
+ const container = document.getElementById(containerId);
885
+ container.style.opacity = 0;
886
+
887
+ if (!posts || posts.length === 0) {
888
+ const emptyMessage = currentView === 'feed' ? T.emptyFeed : T.emptyWall;
889
+ container.innerHTML = `<div class="empty-state">${emptyMessage}</div>`;
890
+ } else {
891
+ container.innerHTML = posts.map(post => {
892
+ const authorLink = formatUserLink(post.author_username);
893
+ const wallOwnerLink = formatUserLink(post.wall_owner_username);
894
+
895
+ let mediaHtml = '';
896
+ if (post.media_type === 'photo' && post.media_url) {
897
+ mediaHtml = `<img src="${post.media_url}" alt="${T.mediaPhoto}" class="post-media" />`;
898
+ } else if (post.media_url) {
899
+ // Simple link/placeholder for other media types
900
+ const mediaTypeDisplay = T['media' + post.media_type.charAt(0).toUpperCase() + post.media_type.slice(1)] || post.media_type;
901
+ mediaHtml = `<a href="${post.media_url}" target="_blank" rel="noopener noreferrer" class="post-media-placeholder"><i class="fas fa-paperclip"></i> ${mediaTypeDisplay} Link</a>`;
902
+ }
903
 
904
+ return `
905
+ <div class="post-item">
906
+ <div class="post-header">
907
+ <img src="${getAvatarUrl(post)}" alt="Avatar" class="user-avatar">
908
+ <div>
909
+ <div class="post-author-name">${post.author_name}</div>
910
+ <div class="post-username">${authorLink}</div>
911
+ </div>
912
+ </div>
913
+ ${mediaHtml}
914
+ ${post.text ? `<p class="post-text">${post.text.replace(/\\n/g, '<br>')}</p>` : ''}
915
+ <div class="post-meta">
916
+ ${T.postedOn}: ${formatDate(post.timestamp)}
917
+ ${post.wall_owner_id !== post.author_id
918
+ ? ` | ${T.onWallOf}: ${wallOwnerLink}`
919
+ : ''}
920
+ </div>
921
+ </div>
922
+ `;
923
+ }).join('');
924
  }
925
+ setTimeout(() => { container.style.opacity = 1; }, 50);
926
  }
927
 
928
+ function renderUserList(users) {
929
+ mainContent.style.opacity = 0;
930
+ appHeader.textContent = T.userList;
931
+
932
+ if (!users || users.length === 0) {
933
+ mainContent.innerHTML = `<div class="empty-state">No registered users found.</div>`;
934
+ } else {
935
+ mainContent.innerHTML = `<div class="view-container">` + users.map(user => {
936
+ const fullName = `${user.first_name || ''} ${user.last_name || ''}`.trim() || 'No Name';
937
+ const username = user.username ? `@${user.username}` : 'ID: ' + user.id;
938
+
939
+ return `
940
+ <div class="list-item user-info" onclick="loadView('user_wall', '${user.id}')">
941
+ <img src="${getAvatarUrl(user)}" alt="Avatar" class="user-avatar">
942
+ <div>
943
+ <h3>${fullName}</h3>
944
+ <p>${username}</p>
945
+ </div>
946
+ </div>
947
+ `;
948
+ }).join('') + `</div>`;
949
+ }
950
+ setTimeout(() => { mainContent.style.opacity = 1; }, 50);
951
+ }
952
+
953
+ function renderMyWallView(user, posts) {
954
+ mainContent.style.opacity = 0;
955
+ appHeader.textContent = T.myWall;
956
+ currentWallOwnerId = user.id;
957
+
958
+ const fullName = `${user.first_name || ''} ${user.last_name || ''}`.trim() || 'No Name';
959
+ const username = user.username ? `@${user.username}` : '';
960
+
961
+ let profileHtml = `
962
+ <div class="profile-header">
963
+ <img src="${getAvatarUrl(user)}" alt="Avatar" class="profile-avatar">
964
+ <h1 class="profile-name">${fullName}</h1>
965
+ <p class="profile-username">${username}</p>
966
+ <div class="profile-stats">
967
+ <div class="profile-stat">
968
+ <span>${posts.length}</span>
969
+ <small>${T.profilePosts}</small>
970
  </div>
 
971
  </div>
972
+ <button class="profile-post-button" onclick="showPostForm('${user.id}')">${T.writePost}</button>
973
+ </div>
974
+ <div class="profile-post-section">
975
+ <h2>${T.profilePosts}</h2>
976
+ <div id="wallPostsContainer"></div>
977
  </div>
978
  `;
979
+
980
+ mainContent.innerHTML = profileHtml;
981
+ renderPostList(posts, 'wallPostsContainer');
982
+ setTimeout(() => { mainContent.style.opacity = 1; }, 50);
983
  }
984
 
985
+ function renderUserWallView(wallOwner, posts) {
986
+ mainContent.style.opacity = 0;
987
+ const fullName = `${wallOwner.first_name || ''} ${wallOwner.last_name || ''}`.trim() || 'No Name';
988
+ appHeader.textContent = fullName + (wallOwner.username ? ` (@${wallOwner.username})` : '');
989
+ currentWallOwnerId = wallOwner.id;
990
+
991
+ const username = wallOwner.username ? `@${wallOwner.username}` : '';
992
+
993
+ let profileHtml = `
994
+ <div class="profile-header">
995
+ <img src="${getAvatarUrl(wallOwner)}" alt="Avatar" class="profile-avatar">
996
+ <h1 class="profile-name">${fullName}</h1>
997
+ <p class="profile-username">${username}</p>
998
+ <button class="profile-post-button" onclick="showPostForm('${wallOwner.id}')">${T.writeOnWall}</button>
999
+ </div>
1000
+ <div class="profile-post-section">
1001
+ <h2>${T.profilePosts}</h2>
1002
+ <div id="wallPostsContainer"></div>
1003
  </div>
1004
  `;
1005
+
1006
+ mainContent.innerHTML = profileHtml;
1007
+ renderPostList(posts, 'wallPostsContainer');
1008
+ setTimeout(() => { mainContent.style.opacity = 1; }, 50);
1009
  }
1010
+
1011
+ // --- Navigation Logic ---
1012
+
1013
+ function loadView(viewName, userId = null) {
1014
+ if (currentView === viewName && viewName !== 'user_wall') return;
1015
+ tg.HapticFeedback.impactOccurred('light');
1016
+
1017
+ currentView = viewName;
1018
+ currentWallOwnerId = null;
1019
+
1020
+ document.querySelectorAll('.nav-item').forEach(btn => btn.classList.remove('active'));
1021
+ const activeNav = document.querySelector(`.nav-item[data-view="${viewName}"]`);
1022
+ if (activeNav) {
1023
+ activeNav.classList.add('active');
1024
  } else {
1025
+ appHeader.textContent = T.appTitle; // Reset header for sub-views
1026
  }
 
 
1027
 
1028
+ mainContent.style.opacity = 0;
1029
+ mainContent.innerHTML = `<div class="loading">${T.loading}</div>`;
 
 
 
 
 
 
1030
  tg.BackButton.hide();
1031
+ tg.MainButton.hide();
1032
+
1033
+ if (viewName === 'feed') {
1034
+ appHeader.textContent = T.feed;
1035
+ apiCall('/api/posts')
1036
+ .then(posts => renderPostList(posts))
1037
+ .catch(err => mainContent.innerHTML = `<div class="empty-state">Error loading feed.</div>`);
1038
+ } else if (viewName === 'users') {
1039
+ appHeader.textContent = T.userList;
1040
+ apiCall('/api/users')
1041
+ .then(users => renderUserList(users.filter(u => u.id !== currentUser.id)))
1042
+ .catch(err => mainContent.innerHTML = `<div class="empty-state">Error loading user list.</div>`);
1043
+ } else if (viewName === 'my_wall') {
1044
+ if (!currentUser) {
1045
+ mainContent.innerHTML = `<div class="empty-state">${T.authFailed}</div>`;
1046
+ setTimeout(() => { mainContent.style.opacity = 1; }, 50);
1047
+ return;
1048
+ }
1049
+ Promise.all([
1050
+ apiCall(`/api/users`),
1051
+ apiCall(`/api/posts/${currentUser.id}`)
1052
+ ]).then(([allUsers, posts]) => {
1053
+ renderMyWallView(currentUser, posts);
1054
+ }).catch(err => mainContent.innerHTML = `<div class="empty-state">Error loading wall.</div>`);
1055
+ } else if (viewName === 'user_wall' && userId) {
1056
+ tg.BackButton.show();
1057
+ tg.BackButton.onClick(() => loadView('users'));
1058
+
1059
+ Promise.all([
1060
+ apiCall('/api/users'),
1061
+ apiCall(`/api/posts/${userId}`)
1062
+ ]).then(([allUsers, posts]) => {
1063
+ const wallOwner = allUsers.find(u => u.id === userId);
1064
+ if (wallOwner) {
1065
+ renderUserWallView(wallOwner, posts);
1066
+ } else {
1067
+ mainContent.innerHTML = `<div class="empty-state">User not found.</div>`;
1068
+ setTimeout(() => { mainContent.style.opacity = 1; }, 50);
1069
+ }
1070
+ }).catch(err => mainContent.innerHTML = `<div class="empty-state">Error loading user wall.</div>`);
1071
  }
1072
  }
 
 
 
 
 
 
 
 
1073
 
1074
+ function showPostForm(wallOwnerId) {
1075
+ if (!currentUser) {
1076
+ tg.showAlert(T.authFailed);
1077
+ return;
1078
+ }
1079
 
1080
+ mainContent.style.opacity = 0;
1081
+ currentWallOwnerId = wallOwnerId;
1082
+
1083
+ tg.BackButton.show();
1084
+ tg.BackButton.onClick(() => {
1085
+ tg.HapticFeedback.impactOccurred('light');
1086
+ if (wallOwnerId === currentUser.id) {
1087
+ loadView('my_wall');
1088
  } else {
1089
+ loadView('user_wall', wallOwnerId);
 
 
 
 
 
 
 
 
 
 
1090
  }
1091
+ });
1092
+
1093
+ appHeader.textContent = wallOwnerId === currentUser.id ? T.writePost : T.writeOnWall;
1094
+
1095
+ const mediaOptions = [
1096
+ { value: '', text: T.mediaNone },
1097
+ { value: 'photo', text: T.mediaPhoto },
1098
+ { value: 'video', text: T.mediaVideo },
1099
+ { value: 'document', text: T.mediaDoc }
1100
+ ];
1101
+
1102
+ const formHtml = `
1103
+ <div class="view-container">
1104
+ <form id="postForm">
1105
+ <div class="form-group">
1106
+ <label for="postText">${T.postText}</label>
1107
+ <textarea id="postText" placeholder="${T.postPlaceholder}"></textarea>
1108
+ </div>
1109
+ <div class="form-group">
1110
+ <label for="mediaType">${T.postMedia}</label>
1111
+ <select id="mediaType">
1112
+ ${mediaOptions.map(opt => `<option value="${opt.value}">${opt.text}</option>`).join('')}
1113
+ </select>
1114
+ </div>
1115
+ <div class="form-group" id="mediaUrlGroup" style="display:none;">
1116
+ <label for="mediaUrl">${T.postMediaUrl} (HTTPS link)</label>
1117
+ <input type="url" id="mediaUrl" placeholder="https://example.com/media.jpg">
1118
+ </div>
1119
+ <div id="formError" class="error-message"></div>
1120
+ </form>
1121
+ </div>
1122
+ `;
1123
+ mainContent.innerHTML = formHtml;
1124
+ setTimeout(() => { mainContent.style.opacity = 1; }, 50);
1125
 
1126
+ const mediaTypeSelect = document.getElementById('mediaType');
1127
+ const mediaUrlGroup = document.getElementById('mediaUrlGroup');
 
 
 
1128
 
1129
+ mediaTypeSelect.addEventListener('change', (e) => {
1130
+ if (e.target.value) {
1131
+ mediaUrlGroup.style.display = 'block';
1132
+ } else {
1133
+ mediaUrlGroup.style.display = 'none';
1134
+ }
1135
+ });
1136
 
1137
+ tg.MainButton.setText(T.postButton);
1138
+ tg.MainButton.show();
1139
+ tg.MainButton.onClick(() => handleSubmitPost(wallOwnerId));
 
 
 
 
 
 
 
1140
  }
 
 
 
 
1141
 
1142
+ function handleSubmitPost(wallOwnerId) {
1143
+ const text = document.getElementById('postText').value.trim();
1144
+ const mediaType = document.getElementById('mediaType').value;
1145
+ const mediaUrl = document.getElementById('mediaUrl').value.trim();
1146
+ const formError = document.getElementById('formError');
1147
+ formError.textContent = '';
1148
+
1149
+ if (!text && !mediaUrl) {
1150
+ formError.textContent = T.postPlaceholder;
1151
+ tg.HapticFeedback.notificationOccurred('error');
1152
  return;
1153
  }
1154
+
1155
+ if (mediaType && !mediaUrl) {
1156
+ formError.textContent = `${T.postMediaUrl} is required for media post.`;
1157
+ tg.HapticFeedback.notificationOccurred('error');
1158
+ return;
1159
+ }
1160
 
1161
  tg.MainButton.showProgress();
1162
  tg.HapticFeedback.impactOccurred('light');
1163
 
1164
+ const payload = {
1165
+ text: text,
1166
+ media_type: mediaType,
1167
+ media_url: mediaUrl
1168
+ };
1169
+
1170
+ apiCall(`/api/post_to_wall/${wallOwnerId}`, 'POST', payload)
1171
+ .then(response => {
1172
+ tg.HapticFeedback.notificationOccurred('success');
1173
+ tg.MainButton.hideProgress();
1174
+ tg.MainButton.hide();
1175
+
1176
+ if (wallOwnerId === currentUser.id) {
1177
+ loadView('my_wall');
1178
+ } else {
1179
+ loadView('user_wall', wallOwnerId);
1180
+ }
1181
+ })
1182
+ .catch(err => {
1183
+ tg.HapticFeedback.notificationOccurred('error');
1184
+ tg.MainButton.hideProgress();
1185
+ formError.textContent = err.message || 'Failed to submit post.';
1186
+ });
1187
  }
1188
 
1189
  async function init() {
1190
  tg.ready();
1191
+
1192
+ // 1. Language detection
1193
+ const langCode = tg.initDataUnsafe.user?.language_code;
1194
+ setLanguage(langCode);
1195
+
1196
  applyThemeParams();
1197
  tg.expand();
1198
  tg.enableClosingConfirmation();
1199
 
1200
  tg.onEvent('themeChanged', applyThemeParams);
1201
 
1202
+ // 2. Auth user
 
 
 
 
 
 
1203
  try {
1204
  const authResponse = await apiCall('/api/auth_user', 'POST', { init_data: tg.initData });
1205
  currentUser = authResponse.user;
 
 
 
1206
  } catch (error) {
1207
  console.error("Auth error:", error);
1208
+ // tg.showAlert(T.authFailed);
1209
+ // Proceed with limited functionality if auth fails
1210
  }
1211
+
1212
+ // 3. Setup Nav Bar
1213
+ document.querySelectorAll('.nav-item').forEach(button => {
1214
+ button.addEventListener('click', () => {
1215
+ const view = button.dataset.view;
1216
+ if (view === 'my_wall' && !currentUser) {
1217
+ tg.showAlert(T.authFailed);
1218
+ return;
1219
+ }
1220
+ loadView(view);
1221
+ });
1222
+ });
1223
+
1224
+ // 4. Load initial view
1225
+ loadView('feed');
1226
  }
1227
 
1228
  init();
 
1231
  </html>
1232
  '''
1233
 
1234
+
1235
  @app.route('/')
1236
  def main_app_view():
1237
+ return render_template_string(MAIN_APP_TEMPLATE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1238
 
1239
  if __name__ == '__main__':
1240
+ logging.info("Application starting up. Performing initial data load/download...")
1241
+ download_db_from_hf()
 
 
 
 
 
1242
  load_data()
1243
+ logging.info("Initial data load complete.")
1244
 
1245
  if HF_TOKEN_WRITE:
1246
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)