| import json |
| import uuid |
| import os |
| import subprocess |
| import threading |
| import time |
| import socket |
| import hashlib |
| from pathlib import Path |
| from fastapi import FastAPI, Request |
| from fastapi.responses import HTMLResponse, JSONResponse |
| import uvicorn |
|
|
| |
| PORT = int(os.getenv("PORT", 7860)) |
| DATA_DIR = Path("/data") |
| DATA_DIR.mkdir(exist_ok=True) |
|
|
| USERS_FILE = DATA_DIR / "users.json" |
| FRIENDS_FILE = DATA_DIR / "friends.json" |
|
|
| for f in [USERS_FILE, FRIENDS_FILE]: |
| if not f.exists(): |
| with open(f, "w") as fp: |
| json.dump({}, fp) |
|
|
| |
| def load_json(file): |
| with open(file, "r") as f: |
| return json.load(f) |
|
|
| def save_json(file, data): |
| with open(file, "w") as f: |
| json.dump(data, f, indent=2) |
|
|
| |
| WORDS = [ |
| "солнце", "луна", "звезда", "небо", "море", "ветер", "дождь", "снег", |
| "гора", "река", "лес", "поле", "цветок", "трава", "дерево", "птица", |
| "рыба", "волк", "лиса", "медведь", "заяц", "ёжик", "белка", "сова", |
| "орёл", "сокол", "дельфин", "кит", "тигр", "лев", "пантера", "гепард", |
| "радуга", "молния", "гром", "туча", "роса", "иней", "туман", "буря", |
| "мир", "друг", "свет", "тепло", "радость", "счастье", "любовь", "надежда" |
| ] |
|
|
| def generate_word_code(): |
| import random |
| word1 = random.choice(WORDS) |
| word2 = random.choice(WORDS) |
| word3 = str(random.randint(10, 99)) |
| return f"{word1}-{word2}-{word3}".upper() |
|
|
| |
| def start_peerjs(): |
| try: |
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| result = sock.connect_ex(('127.0.0.1', 9000)) |
| sock.close() |
| |
| if result != 0: |
| print("🚀 Запускаем PeerJS Server...") |
| subprocess.Popen( |
| ["peer", "--port", "9000", "--path", "/peerjs"], |
| stdout=subprocess.DEVNULL, |
| stderr=subprocess.DEVNULL, |
| start_new_session=True |
| ) |
| time.sleep(3) |
| print("✅ PeerJS Server запущен") |
| except Exception as e: |
| print(f"⚠️ Ошибка запуска PeerJS: {e}") |
|
|
| threading.Thread(target=start_peerjs, daemon=True).start() |
|
|
| |
| app = FastAPI() |
|
|
| @app.get("/", response_class=HTMLResponse) |
| async def index(request: Request): |
| html = """ |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>🔐 HF Message</title> |
| <script src="https://unpkg.com/peerjs@1.5.1/dist/peerjs.min.js"></script> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { |
| font-family: 'Segoe UI', sans-serif; |
| background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); |
| min-height: 100vh; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| padding: 20px; |
| } |
| .container { |
| background: rgba(255,255,255,0.95); |
| border-radius: 20px; |
| box-shadow: 0 20px 60px rgba(0,0,0,0.5); |
| max-width: 650px; |
| width: 100%; |
| padding: 30px; |
| } |
| h1 { |
| color: #2d3748; |
| text-align: center; |
| font-size: 28px; |
| } |
| .subtitle { |
| text-align: center; |
| color: #718096; |
| font-size: 14px; |
| margin-bottom: 20px; |
| } |
| .section { |
| background: #f7fafc; |
| border-radius: 12px; |
| padding: 20px; |
| margin-bottom: 15px; |
| border: 1px solid #e2e8f0; |
| } |
| .section h3 { |
| color: #2d3748; |
| margin-bottom: 12px; |
| font-size: 15px; |
| } |
| input, button { |
| width: 100%; |
| padding: 12px; |
| border: 2px solid #e2e8f0; |
| border-radius: 8px; |
| font-size: 14px; |
| margin-bottom: 8px; |
| transition: all 0.3s; |
| } |
| input:focus { |
| outline: none; |
| border-color: #6c63ff; |
| } |
| button { |
| background: linear-gradient(135deg, #6c63ff, #5a52d5); |
| color: white; |
| border: none; |
| font-weight: 600; |
| cursor: pointer; |
| } |
| button:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 5px 20px rgba(108, 99, 255, 0.4); |
| } |
| .btn-success { background: linear-gradient(135deg, #48bb78, #38a169); } |
| .btn-danger { background: linear-gradient(135deg, #fc8181, #e53e3e); } |
| .btn-copy { background: linear-gradient(135deg, #4299e1, #3182ce); } |
| .btn-small { width: auto; padding: 6px 16px; font-size: 12px; } |
| .code-display { |
| background: #2d3748; |
| color: #f7fafc; |
| padding: 15px; |
| border-radius: 8px; |
| font-family: 'Courier New', monospace; |
| font-size: 20px; |
| text-align: center; |
| letter-spacing: 1px; |
| margin: 10px 0; |
| word-break: break-all; |
| } |
| .status { |
| padding: 10px; |
| border-radius: 8px; |
| margin-top: 8px; |
| font-size: 13px; |
| } |
| .status.success { background: #c6f6d5; color: #22543d; } |
| .status.error { background: #fed7d7; color: #9b2c2c; } |
| .status.info { background: #bee3f8; color: #2a69ac; } |
| .chat-box { |
| border: 2px solid #e2e8f0; |
| border-radius: 8px; |
| height: 300px; |
| overflow-y: auto; |
| padding: 15px; |
| background: white; |
| margin-bottom: 10px; |
| display: none; |
| } |
| .chat-box.active { display: block; } |
| .message { |
| margin-bottom: 8px; |
| padding: 8px 12px; |
| border-radius: 8px; |
| max-width: 80%; |
| word-wrap: break-word; |
| } |
| .message.sent { background: #6c63ff; color: white; margin-left: auto; } |
| .message.received { background: #e2e8f0; color: #2d3748; margin-right: auto; } |
| .message.system { background: #fefcbf; color: #744210; text-align: center; max-width: 100%; font-style: italic; } |
| .chat-input { |
| display: none; |
| gap: 10px; |
| } |
| .chat-input.active { display: flex; } |
| .chat-input input { flex: 1; margin-bottom: 0; } |
| .chat-input button { width: auto; padding: 12px 24px; } |
| .peer-id { font-size: 12px; color: #718096; text-align: center; margin-top: 10px; word-break: break-all; } |
| .hidden { display: none; } |
| .flex { display: flex; gap: 8px; } |
| .flex button { width: auto; flex: 1; } |
| .stats { |
| text-align: center; |
| font-size: 12px; |
| color: #718096; |
| margin-top: 15px; |
| padding-top: 15px; |
| border-top: 1px solid #e2e8f0; |
| } |
| .stats span { font-weight: 600; color: #2d3748; } |
| .badge { |
| display: inline-block; |
| background: #6c63ff; |
| color: white; |
| font-size: 11px; |
| padding: 2px 10px; |
| border-radius: 20px; |
| margin-left: 8px; |
| } |
| .friend-item { |
| background: white; |
| padding: 10px; |
| border-radius: 8px; |
| margin-bottom: 8px; |
| border: 1px solid #e2e8f0; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| .friend-item .name { font-weight: 600; color: #2d3748; } |
| .friend-item .id { font-size: 11px; color: #718096; } |
| .friend-item button { width: auto; padding: 6px 16px; font-size: 12px; } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h1>🔐 HF Message</h1> |
| <div class="subtitle">Код-слово = вход в аккаунт · ID = добавление в друзья</div> |
| |
| <!-- Секция: Вход по коду-слову --> |
| <div class="section" id="login-section"> |
| <h3>🔑 Войти в аккаунт по коду-слову</h3> |
| <input type="text" id="login-code" placeholder="СОЛНЦЕ-ЛУНА-42" style="text-transform:uppercase;"> |
| <button onclick="loginWithCode()">Войти / Создать аккаунт</button> |
| <div id="login-status"></div> |
| </div> |
| |
| <!-- Секция: Мой профиль (появляется после входа) --> |
| <div class="section hidden" id="profile-section"> |
| <h3>👤 Мой профиль</h3> |
| <div style="background:#edf2f7;padding:10px;border-radius:8px;text-align:center;"> |
| <div style="font-weight:600;color:#2d3748;" id="profile-name">Имя</div> |
| <div style="font-family:monospace;font-size:13px;color:#4a5568;margin-top:4px;" id="profile-id">ID</div> |
| </div> |
| <button class="btn-copy" onclick="copyProfileId()" style="margin-top:8px;">📋 Копировать ID</button> |
| <button class="btn-success" onclick="generateNewCode()" style="margin-top:8px;">🔄 Сгенерировать новый код-слово</button> |
| <div id="new-code-result" class="hidden" style="margin-top:8px;"> |
| <div class="code-display" id="new-code-display"></div> |
| <button class="btn-copy" onclick="copyNewCode()">📋 Копировать новый код</button> |
| </div> |
| </div> |
| |
| <!-- Секция: Добавить друга по ID --> |
| <div class="section hidden" id="friends-section"> |
| <h3>➕ Добавить друга по ID</h3> |
| <div class="flex"> |
| <input type="text" id="friend-id-input" placeholder="Вставь Peer ID друга"> |
| <button onclick="addFriend()" style="width:auto;padding:12px 20px;">➕</button> |
| </div> |
| <div id="friends-list" style="margin-top:10px;"></div> |
| <div id="friend-status"></div> |
| </div> |
| |
| <!-- Секция: Чат --> |
| <div class="section hidden" id="chat-section"> |
| <h3>💬 Чат с <span id="chat-peer-id" style="color:#6c63ff;">...</span></h3> |
| <div class="chat-box" id="chat-box"></div> |
| <div class="chat-input" id="chat-input"> |
| <input type="text" id="message-input" placeholder="Введите сообщение..." onkeypress="if(event.key==='Enter') sendMessage()"> |
| <button onclick="sendMessage()">Отправить</button> |
| </div> |
| <button class="btn-danger" onclick="disconnect()">❌ Отключиться</button> |
| </div> |
| |
| <div class="stats"> |
| 👥 <span id="stats-users">0</span> пользователей |
| </div> |
| </div> |
| |
| <script> |
| // ========== ГЛОБАЛЬНЫЕ ========== |
| let myPeer = null; |
| let myId = null; |
| let myName = null; |
| let targetPeerId = null; |
| let connection = null; |
| let friends = []; |
| let currentCode = null; |
| |
| // ========== ИНИЦИАЛИЗАЦИЯ PEER ========== |
| function initPeer(callback) { |
| if (myPeer && myPeer.open) { |
| if (callback) callback(); |
| return; |
| } |
| |
| let savedId = localStorage.getItem('hf_peer_id'); |
| if (!savedId) { |
| savedId = 'user-' + Math.random().toString(36).substring(2, 10); |
| localStorage.setItem('hf_peer_id', savedId); |
| } |
| myId = savedId; |
| |
| myPeer = new Peer(myId, { |
| host: window.location.hostname, |
| port: window.location.port || (window.location.protocol === 'https:' ? 443 : 80), |
| path: '/peerjs', |
| secure: window.location.protocol === 'https:' |
| }); |
| |
| myPeer.on('open', (id) => { |
| console.log('✅ Peer открыт:', id); |
| if (callback) callback(); |
| }); |
| |
| myPeer.on('connection', (conn) => { |
| handleConnection(conn); |
| }); |
| |
| myPeer.on('error', (err) => { |
| console.error('Peer error:', err); |
| }); |
| |
| // Если Peer уже открыт |
| if (myPeer.open) { |
| if (callback) callback(); |
| } |
| } |
| |
| // ========== ВХОД ПО КОДУ-СЛОВУ ========== |
| async function loginWithCode() { |
| const code = document.getElementById('login-code').value.trim().toUpperCase(); |
| const status = document.getElementById('login-status'); |
| |
| if (!code) { |
| status.innerHTML = '<div class="status error">❌ Введи код-слово</div>'; |
| return; |
| } |
| |
| // Проверяем формат: СЛОВО-СЛОВО-ЧИСЛО |
| const parts = code.split('-'); |
| if (parts.length !== 3 || isNaN(parts[2])) { |
| status.innerHTML = '<div class="status error">❌ Неверный формат. Пример: СОЛНЦЕ-ЛУНА-42</div>'; |
| return; |
| } |
| |
| status.innerHTML = '<div class="status info">⏳ Вход...</div>'; |
| |
| try { |
| const r = await fetch('/api/login', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ code }) |
| }); |
| |
| const d = await r.json(); |
| |
| if (d.success) { |
| currentCode = code; |
| myName = d.name; |
| localStorage.setItem('hf_code', code); |
| localStorage.setItem('hf_name', d.name); |
| localStorage.setItem('hf_peer_id', d.peer_id); |
| myId = d.peer_id; |
| |
| status.innerHTML = `<div class="status success">✅ Добро пожаловать, ${d.name}!</div>`; |
| |
| // Показываем профиль |
| document.getElementById('profile-section').classList.remove('hidden'); |
| document.getElementById('friends-section').classList.remove('hidden'); |
| document.getElementById('profile-name').textContent = d.name; |
| document.getElementById('profile-id').textContent = d.peer_id; |
| document.getElementById('login-section').style.display = 'none'; |
| |
| // Инициализируем Peer |
| initPeer(() => { |
| loadFriends(); |
| }); |
| |
| updateStats(); |
| } else { |
| status.innerHTML = `<div class="status error">❌ ${d.error}</div>`; |
| } |
| } catch (e) { |
| status.innerHTML = `<div class="status error">❌ ${e.message}</div>`; |
| } |
| } |
| |
| // ========== ГЕНЕРАЦИЯ НОВОГО КОДА ========== |
| async function generateNewCode() { |
| const status = document.getElementById('login-status'); |
| status.innerHTML = '<div class="status info">⏳ Генерация...</div>'; |
| |
| try { |
| const r = await fetch('/api/generate_code', { method: 'POST' }); |
| const d = await r.json(); |
| |
| if (d.success) { |
| document.getElementById('new-code-display').textContent = d.code; |
| document.getElementById('new-code-result').classList.remove('hidden'); |
| status.innerHTML = '<div class="status success">✅ Новый код создан! Сохрани его.</div>'; |
| } else { |
| status.innerHTML = `<div class="status error">❌ ${d.error}</div>`; |
| } |
| } catch (e) { |
| status.innerHTML = `<div class="status error">❌ ${e.message}</div>`; |
| } |
| } |
| |
| function copyNewCode() { |
| const code = document.getElementById('new-code-display').textContent; |
| navigator.clipboard.writeText(code); |
| } |
| |
| function copyProfileId() { |
| const id = document.getElementById('profile-id').textContent; |
| navigator.clipboard.writeText(id); |
| } |
| |
| // ========== ДРУЗЬЯ ========== |
| async function addFriend() { |
| const input = document.getElementById('friend-id-input'); |
| const id = input.value.trim(); |
| const status = document.getElementById('friend-status'); |
| |
| if (!id) { |
| status.innerHTML = '<div class="status error">❌ Введи ID друга</div>'; |
| return; |
| } |
| |
| try { |
| const r = await fetch('/api/add_friend', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ peer_id: id, name: 'Друг' }) |
| }); |
| |
| const d = await r.json(); |
| |
| if (d.success) { |
| status.innerHTML = '<div class="status success">✅ Друг добавлен!</div>'; |
| input.value = ''; |
| loadFriends(); |
| } else { |
| status.innerHTML = `<div class="status error">❌ ${d.error}</div>`; |
| } |
| } catch (e) { |
| status.innerHTML = `<div class="status error">❌ ${e.message}</div>`; |
| } |
| } |
| |
| async function loadFriends() { |
| try { |
| const r = await fetch('/api/friends'); |
| const d = await r.json(); |
| friends = d.friends || []; |
| renderFriends(); |
| } catch(e) {} |
| } |
| |
| function renderFriends() { |
| const container = document.getElementById('friends-list'); |
| if (!friends.length) { |
| container.innerHTML = '<p style="color:#718096;font-size:13px;">Нет добавленных друзей</p>'; |
| return; |
| } |
| |
| container.innerHTML = friends.map(f => ` |
| <div class="friend-item"> |
| <div> |
| <div class="name">${f.name}</div> |
| <div class="id">${f.peer_id}</div> |
| </div> |
| <button onclick="connectToFriend('${f.peer_id}')" class="btn-small"> |
| 💬 Чат |
| </button> |
| </div> |
| `).join(''); |
| } |
| |
| function connectToFriend(peerId) { |
| initPeer(() => { |
| connectToPeer(peerId); |
| }); |
| } |
| |
| // ========== P2P ПОДКЛЮЧЕНИЕ ========== |
| function connectToPeer(targetId) { |
| targetPeerId = targetId; |
| |
| if (!myPeer) { |
| initPeer(() => connectToPeer(targetId)); |
| return; |
| } |
| |
| try { |
| connection = myPeer.connect(targetId, { reliable: true }); |
| handleConnection(connection); |
| } catch (e) { |
| document.getElementById('login-status').innerHTML = |
| `<div class="status error">❌ Ошибка: ${e.message}</div>`; |
| } |
| } |
| |
| function handleConnection(conn) { |
| connection = conn; |
| |
| conn.on('open', () => { |
| document.getElementById('chat-section').classList.remove('hidden'); |
| document.getElementById('chat-box').classList.add('active'); |
| document.getElementById('chat-input').classList.add('active'); |
| document.getElementById('chat-peer-id').textContent = targetPeerId; |
| |
| addMessage('system', '🔗 Соединение установлено!'); |
| }); |
| |
| conn.on('data', (data) => { |
| if (data.type === 'message') { |
| addMessage('received', data.text); |
| } |
| }); |
| |
| conn.on('close', () => { |
| addMessage('system', '❌ Соединение разорвано'); |
| document.getElementById('chat-input').classList.remove('active'); |
| document.getElementById('chat-box').classList.remove('active'); |
| }); |
| } |
| |
| // ========== ОТПРАВКА ========== |
| function sendMessage() { |
| const input = document.getElementById('message-input'); |
| const text = input.value.trim(); |
| if (!text || !connection) return; |
| |
| connection.send({ type: 'message', text }); |
| addMessage('sent', text); |
| input.value = ''; |
| } |
| |
| function addMessage(type, text) { |
| const box = document.getElementById('chat-box'); |
| const div = document.createElement('div'); |
| div.className = `message ${type}`; |
| div.textContent = text; |
| box.appendChild(div); |
| box.scrollTop = box.scrollHeight; |
| } |
| |
| function disconnect() { |
| if (connection) connection.close(); |
| if (myPeer) myPeer.destroy(); |
| location.reload(); |
| } |
| |
| // ========== СТАТИСТИКА ========== |
| async function updateStats() { |
| try { |
| const r = await fetch('/api/stats'); |
| const d = await r.json(); |
| document.getElementById('stats-users').textContent = d.total_users; |
| } catch(e) {} |
| } |
| updateStats(); |
| setInterval(updateStats, 30000); |
| |
| // ========== АВТОВХОД ========== |
| window.onload = function() { |
| const savedCode = localStorage.getItem('hf_code'); |
| const savedName = localStorage.getItem('hf_name'); |
| const savedId = localStorage.getItem('hf_peer_id'); |
| |
| if (savedCode && savedName && savedId) { |
| document.getElementById('login-code').value = savedCode; |
| document.getElementById('login-section').style.display = 'none'; |
| document.getElementById('profile-section').classList.remove('hidden'); |
| document.getElementById('friends-section').classList.remove('hidden'); |
| document.getElementById('profile-name').textContent = savedName; |
| document.getElementById('profile-id').textContent = savedId; |
| myId = savedId; |
| |
| initPeer(() => { |
| loadFriends(); |
| }); |
| updateStats(); |
| } |
| }; |
| </script> |
| </body> |
| </html> |
| """ |
| return HTMLResponse(html) |
|
|
| |
| @app.post("/api/login") |
| async def login(data: dict): |
| try: |
| code = data.get("code", "").upper() |
| users = load_json(USERS_FILE) |
| |
| if code in users: |
| |
| return JSONResponse({ |
| "success": True, |
| "name": users[code]["name"], |
| "peer_id": users[code]["peer_id"] |
| }) |
| else: |
| |
| peer_id = str(uuid.uuid4()) |
| name = f"User_{len(users) + 1}" |
| users[code] = { |
| "name": name, |
| "peer_id": peer_id, |
| "created_at": time.time() |
| } |
| save_json(USERS_FILE, users) |
| return JSONResponse({ |
| "success": True, |
| "name": name, |
| "peer_id": peer_id |
| }) |
| except Exception as e: |
| return JSONResponse({"success": False, "error": str(e)}) |
|
|
| @app.post("/api/generate_code") |
| async def generate_code(): |
| try: |
| users = load_json(USERS_FILE) |
| code = generate_word_code() |
| |
| while code in users: |
| code = generate_word_code() |
| return JSONResponse({"success": True, "code": code}) |
| except Exception as e: |
| return JSONResponse({"success": False, "error": str(e)}) |
|
|
| @app.post("/api/add_friend") |
| async def add_friend(data: dict): |
| try: |
| peer_id = data.get("peer_id", "").strip() |
| name = data.get("name", "Друг") |
| |
| if not peer_id: |
| return JSONResponse({"success": False, "error": "ID не указан"}) |
| |
| friends = load_json(FRIENDS_FILE) |
| if peer_id not in friends: |
| friends[peer_id] = {"name": name, "added_at": time.time()} |
| save_json(FRIENDS_FILE, friends) |
| |
| return JSONResponse({"success": True}) |
| except Exception as e: |
| return JSONResponse({"success": False, "error": str(e)}) |
|
|
| @app.get("/api/friends") |
| async def get_friends(): |
| friends = load_json(FRIENDS_FILE) |
| return JSONResponse({ |
| "friends": [{"peer_id": k, "name": v.get("name", "Друг")} for k, v in friends.items()] |
| }) |
|
|
| @app.get("/api/stats") |
| async def get_stats(): |
| users = load_json(USERS_FILE) |
| return JSONResponse({"total_users": len(users)}) |
|
|
| |
| if __name__ == "__main__": |
| print("=" * 50) |
| print("🔐 HF Message - Безопасный P2P Чат") |
| print("=" * 50) |
| print(f"📁 Данные в: {DATA_DIR}") |
| print("🚀 Запуск...") |
| uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info") |