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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +565 -247
app.py CHANGED
@@ -1,49 +1,148 @@
1
- from flask import Flask, render_template_string, request, redirect, url_for, jsonify
2
  import json
3
  import os
 
 
 
 
 
 
 
 
 
4
  import hmac
5
  import hashlib
6
  import urllib.parse
7
- from datetime import datetime
8
  import requests
9
- import uuid
 
10
 
11
  app = Flask(__name__)
12
- app.secret_key = os.getenv("FLASK_SECRET_KEY", 'your_very_secret_key')
13
- DATA_FILE = 'telegram_wall_data.json'
 
 
 
 
 
14
 
15
  TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "7549355625:AAGYWatM-nUVQirgBiBwoAtWZgzfp3QnQjY")
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  def load_data():
18
- if not os.path.exists(DATA_FILE):
19
- return {'users': {}, 'walls': {}}
20
  try:
21
- with open(DATA_FILE, 'r', encoding='utf-8') as f:
22
- return json.load(f)
 
 
 
 
23
  except (FileNotFoundError, json.JSONDecodeError):
24
- return {'users': {}, 'walls': {}}
 
 
 
 
 
 
 
 
 
 
25
 
26
  def save_data(data):
27
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
28
- json.dump(data, f, ensure_ascii=False, indent=4)
29
-
30
- def send_telegram_notification(chat_id, message_text):
31
- if not TELEGRAM_BOT_TOKEN or not chat_id:
32
- print(f"Skipping notification for chat_id {chat_id}: Bot token or chat_id missing.")
33
- return
34
- url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
35
- payload = {
36
- 'chat_id': chat_id,
37
- 'text': message_text,
38
- 'parse_mode': 'HTML'
39
- }
40
  try:
41
- response = requests.post(url, json=payload)
42
- response.raise_for_status()
43
- print(f"Notification sent to {chat_id}. Response: {response.json()}")
44
- except requests.exceptions.RequestException as e:
45
- print(f"Failed to send notification to {chat_id}: {e}")
46
-
47
 
48
  def verify_telegram_auth_data(auth_data_str, bot_token):
49
  if not auth_data_str:
@@ -73,16 +172,27 @@ def verify_telegram_auth_data(auth_data_str, bot_token):
73
  return False, None
74
  return False, None
75
 
76
- def get_authenticated_user(request_headers):
77
- auth_data_str = request_headers.get('X-Telegram-Auth')
78
- if not auth_data_str:
79
- return None
80
- is_valid, user_data = verify_telegram_auth_data(auth_data_str, TELEGRAM_BOT_TOKEN)
81
- if is_valid and user_data:
82
- return user_data
83
- return None
 
 
 
 
 
 
 
 
 
 
 
84
 
85
- INDEX_TEMPLATE = """
86
  <!DOCTYPE html>
87
  <html lang="en">
88
  <head>
@@ -99,306 +209,514 @@ INDEX_TEMPLATE = """
99
  --tg-theme-button-color: #007aff;
100
  --tg-theme-button-text-color: #ffffff;
101
  --tg-theme-secondary-bg-color: #f0f0f0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }
103
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 0; background-color: var(--tg-theme-bg-color); color: var(--tg-theme-text-color); }
104
- .container { padding: 15px; }
105
- .header { text-align: center; font-size: 20px; font-weight: 600; padding: 10px 0; border-bottom: 1px solid var(--tg-theme-secondary-bg-color); }
106
- .nav { display: flex; justify-content: space-around; padding: 10px; background-color: var(--tg-theme-secondary-bg-color); }
107
- .nav a { text-decoration: none; color: var(--tg-theme-link-color); font-weight: 500; }
108
- .post-form textarea { width: 100%; box-sizing: border-box; padding: 10px; border-radius: 8px; border: 1px solid var(--tg-theme-hint-color); min-height: 80px; font-size: 16px; margin-top: 15px; }
109
- .post-form button { width: 100%; padding: 12px; background-color: var(--tg-theme-button-color); color: var(--tg-theme-button-text-color); border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; margin-top: 10px; }
110
- .wall-post { background-color: var(--tg-theme-secondary-bg-color); padding: 12px; margin: 15px 0; border-radius: 8px; }
111
- .post-author { font-weight: 600; color: var(--tg-theme-text-color); }
112
- .post-author a { color: var(--tg-theme-link-color); text-decoration: none; }
113
- .post-content { margin: 8px 0; white-space: pre-wrap; word-wrap: break-word; }
114
- .post-timestamp { font-size: 12px; color: var(--tg-theme-hint-color); text-align: right; }
115
- .user-list-item { padding: 10px; border-bottom: 1px solid var(--tg-theme-secondary-bg-color); }
116
- .user-list-item a { text-decoration: none; color: var(--tg-theme-text-color); font-size: 16px; }
 
 
 
 
 
 
 
117
  </style>
118
  </head>
119
  <body>
120
- <div id="view-container"></div>
 
 
 
 
 
 
 
 
 
121
 
122
  <script>
123
  const tg = window.Telegram.WebApp;
 
 
 
 
 
 
 
 
124
 
125
  function applyThemeParams() {
126
- document.documentElement.style.setProperty('--tg-theme-bg-color', tg.themeParams.bg_color || '#ffffff');
127
- document.documentElement.style.setProperty('--tg-theme-text-color', tg.themeParams.text_color || '#000000');
128
- document.documentElement.style.setProperty('--tg-theme-hint-color', tg.themeParams.hint_color || '#999999');
129
- document.documentElement.style.setProperty('--tg-theme-link-color', tg.themeParams.link_color || '#007aff');
130
- document.documentElement.style.setProperty('--tg-theme-button-color', tg.themeParams.button_color || '#007aff');
131
- document.documentElement.style.setProperty('--tg-theme-button-text-color', tg.themeParams.button_text_color || '#ffffff');
132
- document.documentElement.style.setProperty('--tg-theme-secondary-bg-color', tg.themeParams.secondary_bg_color || '#f0f0f0');
133
  }
134
 
135
  async function apiCall(endpoint, method = 'GET', body = null) {
136
- const headers = { 'Content-Type': 'application/json', 'X-Telegram-Auth': tg.initData };
 
 
137
  const options = { method, headers };
138
  if (body) options.body = JSON.stringify(body);
 
139
  const response = await fetch(endpoint, options);
140
  if (!response.ok) {
141
- const errorData = await response.json().catch(() => ({ error: 'API Error' }));
142
- throw new Error(errorData.error);
143
  }
144
  return response.json();
145
  }
146
 
147
- function renderWall(data) {
148
- const { wall_owner, posts, current_user } = data;
149
- let html = `
150
- <div class="header">${wall_owner.first_name}'s Wall</div>
151
- <div class="nav">
152
- <a href="#" onclick="showMyWall()">My Wall</a>
153
- <a href="#" onclick="showUserList()">Users</a>
 
 
 
 
 
 
 
 
 
154
  </div>
155
- <div class="container">
156
  `;
157
- if (current_user.id !== wall_owner.id) {
158
- html += `
159
- <div class="post-form">
160
- <form onsubmit="handlePostSubmit(event, '${wall_owner.id}')">
161
- <textarea name="content" placeholder="Write something on ${wall_owner.first_name}'s wall..."></textarea>
162
- <button type="submit">Post</button>
163
- </form>
164
- </div>
165
- `;
166
- } else {
167
- html += `
168
- <div class="post-form">
169
- <form onsubmit="handlePostSubmit(event, '${wall_owner.id}')">
170
- <textarea name="content" placeholder="What's on your mind?"></textarea>
171
- <button type="submit">Post</button>
172
- </form>
173
- </div>
174
- `;
175
- }
176
-
177
- if (posts && posts.length > 0) {
178
- posts.forEach(post => {
179
- html += `
180
- <div class="wall-post">
181
- <div class="post-author">
182
- <a href="#" onclick="showUserWall('${post.author_id}')">${post.author_name}</a>
183
- </div>
184
- <div class="post-content">${post.content}</div>
185
- <div class="post-timestamp">${new Date(post.timestamp).toLocaleString()}</div>
186
- </div>
187
- `;
188
- });
189
- } else {
190
- html += '<p>This wall is empty.</p>';
191
- }
192
-
193
- html += '</div>';
194
- document.getElementById('view-container').innerHTML = html;
195
  }
196
 
197
- function renderUserList(users) {
198
- let html = `
199
- <div class="header">Users</div>
200
- <div class="nav">
201
- <a href="#" onclick="showMyWall()">My Wall</a>
202
- <a href="#" onclick="showUserList()">Users</a>
 
 
 
 
 
 
 
203
  </div>
204
- <div class="container">
205
  `;
206
- users.forEach(user => {
207
- html += `
208
- <div class="user-list-item">
209
- <a href="#" onclick="showUserWall('${user.id}')">${user.first_name} ${user.last_name || ''} (@${user.username || '...'})</a>
210
- </div>
211
- `;
212
- });
213
- html += '</div>';
214
- document.getElementById('view-container').innerHTML = html;
 
215
  }
216
 
217
- async function handlePostSubmit(event, wallOwnerId) {
218
- event.preventDefault();
219
- const content = event.target.elements.content.value;
220
- if (!content.trim()) return;
 
 
 
 
 
221
 
222
  try {
223
- await apiCall('/api/post', 'POST', { content: content, wall_owner_id: wallOwnerId });
224
- event.target.elements.content.value = '';
225
- showUserWall(wallOwnerId);
226
  } catch (error) {
227
- tg.showAlert('Failed to post message: ' + error.message);
 
228
  }
229
  }
230
 
231
- async function showMyWall() {
 
 
 
 
 
 
 
 
 
 
232
  try {
233
- const data = await apiCall('/api/wall/me');
234
- renderWall(data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  } catch (error) {
236
- tg.showAlert('Could not load your wall: ' + error.message);
 
237
  }
238
  }
239
 
240
- async function showUserWall(userId) {
 
 
 
 
 
 
 
 
241
  try {
242
- const data = await apiCall(`/api/wall/${userId}`);
243
- renderWall(data);
 
 
 
244
  } catch (error) {
245
- tg.showAlert('Could not load user wall: ' + error.message);
 
246
  }
247
  }
248
 
249
- async function showUserList() {
250
- try {
251
- const users = await apiCall('/api/users');
252
- renderUserList(users);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  } catch (error) {
254
- tg.showAlert('Could not load user list: ' + error.message);
 
 
 
255
  }
256
  }
257
 
258
  async function init() {
259
  tg.ready();
260
- tg.expand();
261
  applyThemeParams();
 
 
 
262
  tg.onEvent('themeChanged', applyThemeParams);
263
 
 
 
 
 
 
 
 
264
  try {
265
- await apiCall('/api/auth', 'POST');
266
- showMyWall();
 
 
 
267
  } catch (error) {
268
- document.getElementById('view-container').innerHTML = '<div class="container"><p>Authentication failed. Please reload the app.</p></div>';
 
 
269
  }
270
  }
271
 
272
- window.onload = init;
273
  </script>
274
  </body>
275
  </html>
276
- """
277
 
278
  @app.route('/')
279
- def index():
280
- return render_template_string(INDEX_TEMPLATE)
281
 
282
- @app.route('/api/auth', methods=['POST'])
283
  def auth_user():
284
- user_data = get_authenticated_user(request.headers)
285
- if not user_data:
286
- return jsonify({"error": "Invalid Telegram data"}), 403
287
 
288
- user_id_str = str(user_data['id'])
289
- data = load_data()
290
 
291
- if user_id_str not in data['users']:
292
- data['users'][user_id_str] = {
293
- 'id': user_id_str,
294
- 'first_name': user_data.get('first_name'),
295
- 'last_name': user_data.get('last_name'),
296
- 'username': user_data.get('username'),
297
- 'chat_id': user_data.get('id'),
298
- 'first_seen': datetime.utcnow().isoformat()
299
- }
300
- data['walls'][user_id_str] = []
301
 
302
- data['users'][user_id_str]['last_seen'] = datetime.utcnow().isoformat()
303
- save_data(data)
 
 
 
 
 
 
 
 
 
 
 
 
304
 
305
- return jsonify(data['users'][user_id_str]), 200
306
-
307
- @app.route('/api/wall/me', methods=['GET'])
308
- def get_my_wall():
309
- current_user = get_authenticated_user(request.headers)
310
- if not current_user:
311
- return jsonify({"error": "Authentication required"}), 401
312
 
313
- user_id_str = str(current_user['id'])
314
- data = load_data()
315
 
316
- wall_owner = data['users'].get(user_id_str)
317
- posts = sorted(data['walls'].get(user_id_str, []), key=lambda p: p['timestamp'], reverse=True)
318
-
319
- return jsonify({
320
- "wall_owner": wall_owner,
321
- "posts": posts,
322
- "current_user": current_user
323
- })
324
 
325
- @app.route('/api/wall/<user_id>', methods=['GET'])
326
- def get_user_wall(user_id):
327
- current_user = get_authenticated_user(request.headers)
328
- if not current_user:
329
- return jsonify({"error": "Authentication required"}), 401
330
-
331
- data = load_data()
332
- if user_id not in data['users']:
333
- return jsonify({"error": "User not found"}), 404
334
-
335
- wall_owner = data['users'][user_id]
336
- posts = sorted(data['walls'].get(user_id, []), key=lambda p: p['timestamp'], reverse=True)
337
-
338
- return jsonify({
339
- "wall_owner": wall_owner,
340
- "posts": posts,
341
- "current_user": current_user
342
- })
343
 
344
  @app.route('/api/users', methods=['GET'])
345
  def get_users():
346
- current_user = get_authenticated_user(request.headers)
347
- if not current_user:
348
- return jsonify({"error": "Authentication required"}), 401
349
 
350
  data = load_data()
351
- users = list(data['users'].values())
352
- return jsonify(users)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
- @app.route('/api/post', methods=['POST'])
355
- def create_post():
356
- author = get_authenticated_user(request.headers)
357
- if not author:
358
- return jsonify({"error": "Authentication required"}), 401
 
 
 
 
 
 
359
 
360
- req_data = request.json
361
- content = req_data.get('content')
362
- wall_owner_id = str(req_data.get('wall_owner_id'))
363
 
364
- if not content or not wall_owner_id:
365
- return jsonify({"error": "Missing content or wall owner ID"}), 400
 
 
366
 
367
- data = load_data()
 
 
 
368
 
369
- if wall_owner_id not in data['users']:
 
 
370
  return jsonify({"error": "Wall owner not found"}), 404
371
-
372
- author_id_str = str(author['id'])
373
- author_info = data['users'].get(author_id_str, {})
374
- author_name = author_info.get('first_name', 'Unknown User')
375
 
376
  new_post = {
377
- "id": str(uuid.uuid4()),
378
- "author_id": author_id_str,
379
- "author_name": author_name,
380
- "content": content,
381
- "timestamp": datetime.utcnow().isoformat()
 
 
382
  }
383
-
384
- if wall_owner_id not in data['walls']:
385
- data['walls'][wall_owner_id] = []
386
 
387
- data['walls'][wall_owner_id].append(new_post)
 
 
 
388
  save_data(data)
389
 
390
- wall_owner = data['users'].get(wall_owner_id)
391
- if wall_owner and str(wall_owner['id']) != author_id_str:
392
- wall_owner_chat_id = wall_owner.get('chat_id')
393
- notification_message = (
394
- f"<b>New message on your wall!</b>\n"
395
- f"From: {author_name}\n"
396
- f"Message: {content}"
397
- )
398
- send_telegram_notification(wall_owner_chat_id, notification_message)
399
 
400
  return jsonify(new_post), 201
401
 
402
  if __name__ == '__main__':
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  port = int(os.environ.get('PORT', 7860))
404
- app.run(debug=True, host='0.0.0.0', port=port)
 
 
1
+ from flask import Flask, render_template_string, request, jsonify, redirect, url_for, flash
2
  import json
3
  import os
4
+ import logging
5
+ import threading
6
+ import time
7
+ from datetime import datetime
8
+ from huggingface_hub import HfApi, hf_hub_download
9
+ from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
10
+ from werkzeug.utils import secure_filename
11
+ from dotenv import load_dotenv
12
+ import uuid
13
  import hmac
14
  import hashlib
15
  import urllib.parse
 
16
  import requests
17
+
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
33
+
34
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
35
+
36
+ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
37
+ if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
38
+ logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
39
+ token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
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,
51
+ force_download=True, resume_download=False
52
+ )
53
+ logging.info(f"Successfully downloaded {file_name} to {local_path}.")
54
+ success = True
55
+ break
56
+ except RepositoryNotFoundError:
57
+ logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.")
58
+ return False
59
+ except HfHubHTTPError as e:
60
+ if e.response.status_code == 404:
61
+ logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Skipping this file.")
62
+ if attempt == 0 and not os.path.exists(file_name):
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
71
+ break
72
+ else:
73
+ logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
74
+ except Exception as e:
75
+ logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
76
+ if attempt < retries: time.sleep(delay)
77
+ if not success:
78
+ logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
79
+ all_successful = False
80
+ logging.info(f"Download process finished. Overall success: {all_successful}")
81
+ return all_successful
82
+
83
+ def upload_db_to_hf(specific_file=None):
84
+ if not HF_TOKEN_WRITE:
85
+ logging.warning("HF_TOKEN_WRITE not set. Skipping upload to Hugging Face.")
86
+ return
87
+ try:
88
+ api = HfApi()
89
+ files_to_upload = [specific_file] if specific_file else SYNC_FILES
90
+ logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
91
+ for file_name in files_to_upload:
92
+ if os.path.exists(file_name):
93
+ try:
94
+ api.upload_file(
95
+ path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID,
96
+ repo_type="dataset", token=HF_TOKEN_WRITE,
97
+ commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
98
+ )
99
+ logging.info(f"File {file_name} successfully uploaded to Hugging Face.")
100
+ except Exception as e:
101
+ logging.error(f"Error uploading file {file_name} to Hugging Face: {e}")
102
+ else:
103
+ logging.warning(f"File {file_name} not found locally, skipping upload.")
104
+ logging.info("Finished uploading files to HF.")
105
+ except Exception as e:
106
+ logging.error(f"General error during Hugging Face upload initialization or process: {e}", exc_info=True)
107
+
108
+ def periodic_backup():
109
+ backup_interval = 1800
110
+ logging.info(f"Setting up periodic backup every {backup_interval} seconds.")
111
+ while True:
112
+ time.sleep(backup_interval)
113
+ logging.info("Starting periodic backup...")
114
+ upload_db_to_hf()
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
  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>
 
209
  --tg-theme-button-color: #007aff;
210
  --tg-theme-button-text-color: #ffffff;
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;
227
+ text-align: center;
228
+ font-weight: 600;
229
+ font-size: 17px;
230
+ border-bottom: 0.5px solid var(--tg-theme-secondary-bg-color);
231
+ position: sticky;
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();
570
  </script>
571
  </body>
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)
715
+ backup_thread.start()
716
+ logging.info("Periodic backup thread started.")
717
+ else:
718
+ logging.warning("Periodic backup will NOT run (HF_TOKEN_WRITE not set).")
719
+
720
  port = int(os.environ.get('PORT', 7860))
721
+ logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
722
+ app.run(debug=False, host='0.0.0.0', port=port)