| import os |
| import json |
| import uuid |
| from datetime import datetime |
| from flask import Flask, Response, request, jsonify |
| from werkzeug.security import generate_password_hash, check_password_hash |
|
|
| app = Flask(__name__) |
|
|
| DB_FILE = 'db.json' |
|
|
| def init_db(): |
| if not os.path.exists(DB_FILE): |
| with open(DB_FILE, 'w') as f: |
| json.dump({ |
| "users": {}, |
| "chatrooms": {}, |
| "messages": {} |
| }, f, indent=4) |
|
|
| def read_db(): |
| with open(DB_FILE, 'r') as f: |
| return json.load(f) |
|
|
| def write_db(data): |
| with open(DB_FILE, 'w') as f: |
| json.dump(data, f, indent=4) |
|
|
| @app.route('/') |
| def index(): |
| html_content = ''' |
| <!DOCTYPE html> |
| <html lang="ru"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>Virton Messenger</title> |
| <script src="https://unpkg.com/@tonconnect/ui@latest/dist/tonconnect-ui.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script> |
| <script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> |
| <style> |
| :root { |
| --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; |
| --radius-s: 4px; |
| --radius-m: 8px; |
| --radius-l: 12px; |
| --radius-xl: 20px; |
| --transition-speed: 0.3s; |
| } |
| |
| body { |
| --bg-primary: #000000; |
| --bg-secondary: #121212; |
| --bg-tertiary: #1d1d1d; |
| --bg-elevated: #282828; |
| --bg-modal-overlay: rgba(0, 0, 0, 0.7); |
| --text-primary: #ffffff; |
| --text-secondary: #a8a8a8; |
| --text-tertiary: #757575; |
| --border-primary: #2d2d2d; |
| --accent-blue: #007aff; |
| --accent-blue-rgb: 0, 122, 255; |
| --accent-gradient: linear-gradient(135deg, #007aff, #5856d6); |
| --success-color: #34c759; |
| --error-color: #ff3b30; |
| --white: #ffffff; |
| --black: #000000; |
| } |
| |
| body.light-theme { |
| --bg-primary: #f2f2f7; |
| --bg-secondary: #ffffff; |
| --bg-tertiary: #f2f2f7; |
| --bg-elevated: #ffffff; |
| --bg-modal-overlay: rgba(0, 0, 0, 0.4); |
| --text-primary: #000000; |
| --text-secondary: #636366; |
| --text-tertiary: #aeaeb2; |
| --border-primary: #e5e5ea; |
| --accent-blue: #007aff; |
| --accent-blue-rgb: 0, 122, 255; |
| --accent-gradient: linear-gradient(135deg, #007aff, #5856d6); |
| } |
| |
| * { |
| box-sizing: border-box; |
| margin: 0; |
| padding: 0; |
| -webkit-tap-highlight-color: transparent; |
| } |
| |
| html, body { |
| height: 100vh; |
| width: 100vw; |
| overflow: hidden; |
| } |
| |
| body { |
| font-family: var(--font-family); |
| background-color: var(--bg-primary); |
| color: var(--text-primary); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| transition: background-color var(--transition-speed), color var(--transition-speed); |
| } |
| |
| .app-wrapper { |
| width: 100%; |
| height: 100%; |
| display: flex; |
| flex-direction: column; |
| transition: opacity 0.5s ease; |
| } |
| |
| #login-view { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| text-align: center; |
| width: 100%; |
| height: 100%; |
| background: var(--bg-primary); |
| z-index: 100; |
| } |
| #login-view img { |
| width: 120px; |
| height: 120px; |
| margin-bottom: 24px; |
| filter: drop-shadow(0 0 25px rgba(var(--accent-blue-rgb), 0.4)); |
| } |
| #login-view h1 { |
| font-size: 3rem; |
| font-weight: 700; |
| background: var(--accent-gradient); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| margin-bottom: 12px; |
| } |
| #login-view p { |
| font-size: 1.1rem; |
| color: var(--text-secondary); |
| margin-bottom: 40px; |
| } |
| |
| #app-view { |
| display: none; |
| width: 100%; |
| height: 100%; |
| flex-direction: column-reverse; |
| } |
| |
| .main-content { |
| flex-grow: 1; |
| position: relative; |
| overflow: hidden; |
| } |
| .view { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| display: flex; |
| flex-direction: column; |
| opacity: 0; |
| visibility: hidden; |
| transition: opacity var(--transition-speed) ease-in-out, visibility var(--transition-speed); |
| background-color: var(--bg-primary); |
| } |
| .view.active { |
| opacity: 1; |
| visibility: visible; |
| z-index: 10; |
| } |
| |
| .view-header { |
| padding: 12px 16px; |
| flex-shrink: 0; |
| background-color: rgba(var(--bg-secondary-rgb), 0.8); |
| backdrop-filter: blur(10px); |
| -webkit-backdrop-filter: blur(10px); |
| border-bottom: 1px solid var(--border-primary); |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| z-index: 5; |
| } |
| .view-header h2 { |
| font-size: 1.5rem; |
| font-weight: 700; |
| } |
| |
| .list-container { |
| flex-grow: 1; |
| overflow-y: auto; |
| padding: 8px 0; |
| } |
| .list-item { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| padding: 12px 16px; |
| cursor: pointer; |
| transition: background-color 0.2s ease; |
| } |
| .list-item:hover { background-color: var(--bg-tertiary); } |
| .item-info { flex-grow: 1; overflow: hidden; } |
| .item-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| .item-detail { font-size: 0.9rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| .lock-icon { width: 16px; height: 16px; fill: var(--text-tertiary); flex-shrink: 0; } |
| |
| .avatar { |
| width: 48px; |
| height: 48px; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 1.2rem; |
| font-weight: 600; |
| color: var(--white); |
| flex-shrink: 0; |
| } |
| |
| #chat-window-view { |
| display: none; |
| flex-direction: column; |
| height: 100%; |
| width: 100%; |
| background-color: var(--bg-primary); |
| position: fixed; |
| top: 0; |
| left: 100%; |
| z-index: 50; |
| transition: transform var(--transition-speed) ease-in-out; |
| } |
| #chat-window-view.visible { transform: translateX(-100%); } |
| |
| .chat-header { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| padding: 12px; |
| background-color: rgba(var(--bg-secondary-rgb), 0.8); |
| backdrop-filter: blur(10px); |
| border-bottom: 1px solid var(--border-primary); |
| flex-shrink: 0; |
| } |
| .back-btn { background: none; border: none; cursor: pointer; padding: 4px; } |
| .back-btn svg { width: 28px; height: 28px; fill: var(--accent-blue); } |
| #chat-header-title { font-size: 1.1rem; font-weight: 600; } |
| |
| #messages-container { |
| flex-grow: 1; |
| padding: 16px; |
| overflow-y: auto; |
| display: flex; |
| flex-direction: column; |
| gap: 16px; |
| } |
| .message { display: flex; gap: 10px; max-width: 85%; } |
| .message .avatar { width: 36px; height: 36px; font-size: 1rem; align-self: flex-end; } |
| .message-content { display: flex; flex-direction: column; gap: 4px; } |
| .message-sender { font-size: 0.8rem; font-weight: 500; color: var(--text-secondary); padding: 0 12px; cursor: pointer; } |
| .message-bubble { padding: 10px 14px; border-radius: var(--radius-xl); line-height: 1.4; word-wrap: break-word; } |
| |
| .message.sent { align-self: flex-end; flex-direction: row-reverse; } |
| .message.sent .message-sender { text-align: right; color: var(--accent-blue); } |
| .message.sent .message-bubble { background: var(--accent-gradient); color: var(--white); border-bottom-right-radius: var(--radius-s); } |
| |
| .message.received { align-self: flex-start; } |
| .message.received .message-bubble { background-color: var(--bg-elevated); border-bottom-left-radius: var(--radius-s); } |
| |
| .message-form { |
| display: flex; |
| padding: 8px 12px; |
| gap: 12px; |
| background-color: var(--bg-secondary); |
| border-top: 1px solid var(--border-primary); |
| flex-shrink: 0; |
| } |
| #message-input { |
| flex-grow: 1; |
| padding: 10px 18px; |
| border: none; |
| background-color: var(--bg-tertiary); |
| color: var(--text-primary); |
| border-radius: 20px; |
| outline: none; |
| font-size: 1rem; |
| font-family: var(--font-family); |
| } |
| .send-btn { |
| width: 40px; |
| height: 40px; |
| border-radius: 50%; |
| flex-shrink: 0; |
| padding: 0; |
| background: var(--accent-blue); |
| color: var(--white); |
| border: none; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| .send-btn svg { width: 20px; height: 20px; fill: var(--white); } |
| |
| #my-profile-view .profile-card { |
| background-color: var(--bg-secondary); |
| margin: 16px; |
| padding: 24px; |
| border-radius: var(--radius-l); |
| text-align: center; |
| box-shadow: 0 4px 20px rgba(0,0,0,0.1); |
| } |
| #my-profile-view .avatar { |
| width: 100px; |
| height: 100px; |
| font-size: 3rem; |
| margin: 0 auto 16px; |
| } |
| #my-profile-view #profile-username-display { font-size: 1.5rem; font-weight: 600; } |
| #my-profile-view #profile-address-display { font-size: 0.9rem; color: var(--text-secondary); word-break: break-all; margin-top: 4px; } |
| #my-profile-view #profile-balance-display { font-size: 1.1rem; margin-top: 12px; color: var(--text-tertiary); } |
| #my-profile-view .username-form { display: flex; gap: 8px; margin-top: 24px; } |
| .action-btn { |
| background: var(--accent-blue); |
| color: var(--white); |
| border: none; |
| padding: 12px 18px; |
| border-radius: var(--radius-m); |
| cursor: pointer; |
| font-weight: 500; |
| font-size: 1rem; |
| transition: transform 0.2s ease, box-shadow 0.2s ease; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 8px; |
| } |
| .username-input { |
| flex-grow: 1; |
| background-color: var(--bg-tertiary); |
| border: 1px solid var(--border-primary); |
| color: var(--text-primary); |
| border-radius: var(--radius-m); |
| padding: 10px 14px; |
| font-size: 1rem; |
| } |
| .username-input:focus { outline: none; border-color: var(--accent-blue); } |
| |
| .bottom-nav { |
| display: flex; |
| justify-content: space-around; |
| align-items: center; |
| height: 84px; |
| padding-bottom: 30px; |
| background-color: rgba(var(--bg-secondary-rgb), 0.8); |
| backdrop-filter: blur(10px); |
| -webkit-backdrop-filter: blur(10px); |
| border-top: 1px solid var(--border-primary); |
| flex-shrink: 0; |
| } |
| .nav-item { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| gap: 4px; |
| cursor: pointer; |
| color: var(--text-tertiary); |
| transition: color 0.2s; |
| padding: 8px 12px; |
| border-radius: var(--radius-l); |
| } |
| .nav-item.active { color: var(--accent-blue); } |
| .nav-item:hover:not(.active) { color: var(--text-secondary); } |
| .nav-item svg { |
| width: 26px; |
| height: 26px; |
| fill: currentColor; |
| } |
| .nav-item span { font-size: 0.7rem; font-weight: 500; } |
| #nav-scan-btn { |
| transform: translateY(-20px); |
| background: var(--accent-gradient); |
| width: 64px; |
| height: 64px; |
| border-radius: 50%; |
| color: var(--white); |
| box-shadow: 0 5px 15px rgba(var(--accent-blue-rgb), 0.4); |
| } |
| #nav-scan-btn svg { width: 32px; height: 32px; } |
| |
| .modal-overlay { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background-color: var(--bg-modal-overlay); |
| display: none; |
| align-items: center; |
| justify-content: center; |
| z-index: 1000; |
| backdrop-filter: blur(5px); |
| -webkit-backdrop-filter: blur(5px); |
| } |
| .modal-content { |
| background-color: var(--bg-secondary); |
| padding: 24px; |
| border-radius: var(--radius-l); |
| width: 90%; |
| max-width: 400px; |
| border: 1px solid var(--border-primary); |
| box-shadow: 0 10px 30px rgba(0,0,0,0.2); |
| } |
| .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; } |
| .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); } |
| .modal-content input { |
| width: 100%; |
| padding: 12px; |
| margin-bottom: 16px; |
| background-color: var(--bg-tertiary); |
| border: 1px solid var(--border-primary); |
| color: var(--text-primary); |
| border-radius: var(--radius-m); |
| font-size: 1rem; |
| } |
| .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; } |
| .modal-btn { |
| padding: 10px 20px; |
| border-radius: var(--radius-m); |
| border: none; |
| cursor: pointer; |
| font-weight: 500; |
| } |
| .secondary-btn { background-color: var(--bg-elevated); color: var(--text-primary); } |
| |
| #profile-qr-code { background: var(--white); padding: 10px; margin: 20px auto; width: fit-content; border-radius: var(--radius-m); } |
| |
| #status-bar { |
| position: fixed; |
| top: 20px; |
| left: 50%; |
| transform: translateX(-50%); |
| background-color: var(--bg-elevated); |
| color: var(--text-primary); |
| padding: 12px 20px; |
| border-radius: var(--radius-xl); |
| font-size: 0.9rem; |
| opacity: 0; |
| visibility: hidden; |
| transition: opacity 0.3s, visibility 0.3s, transform 0.3s; |
| z-index: 2000; |
| box-shadow: 0 5px 15px rgba(0,0,0,0.2); |
| } |
| #status-bar.visible { opacity: 1; visibility: visible; transform: translate(-50%, 10px); } |
| #status-bar.success { background-color: var(--success-color); color: var(--white); } |
| #status-bar.error { background-color: var(--error-color); color: var(--white); } |
| |
| .theme-toggle-btn { |
| background-color: var(--bg-tertiary); |
| border: 1px solid var(--border-primary); |
| border-radius: 50%; |
| width: 40px; |
| height: 40px; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| .theme-toggle-btn svg { width: 20px; height: 20px; fill: var(--text-secondary); } |
| |
| @media (min-width: 768px) { |
| #login-view { background: var(--bg-secondary); } |
| .app-wrapper { max-width: 1200px; max-height: 900px; border-radius: var(--radius-xl); overflow: hidden; box-shadow: 0 10px 50px rgba(0,0,0,0.2); border: 1px solid var(--border-primary); } |
| #app-view { flex-direction: row; } |
| .bottom-nav { |
| flex-direction: column; |
| width: 80px; |
| height: 100%; |
| padding-bottom: 0; |
| padding-top: 20px; |
| justify-content: flex-start; |
| gap: 16px; |
| border-top: none; |
| border-right: 1px solid var(--border-primary); |
| } |
| .nav-item { gap: 6px; } |
| .nav-item span { font-size: 0.75rem; } |
| #nav-scan-btn { transform: none; margin-top: 16px; } |
| .main-content { display: flex; } |
| .view { position: static; opacity: 1; visibility: visible; width: 350px; flex-shrink: 0; border-right: 1px solid var(--border-primary); } |
| .view.full-width { width: 100%; border-right: none; } |
| .view.hidden-desktop { display: none; } |
| .view.active { display: flex; } |
| #chat-window-view { position: static; transform: none !important; width: 100%; display: flex !important; } |
| #chat-window-view .back-btn { display: none; } |
| } |
| |
| </style> |
| </head> |
| <body> |
| |
| <div id="login-view" class="app-wrapper"> |
| <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol"> |
| <h1>Virton</h1> |
| <p>Децентрализованный и анонимный мессенджер</p> |
| <div id="ton-connect-button"></div> |
| </div> |
| |
| <div id="app-view" class="app-wrapper"> |
| <div class="main-content"> |
| <div id="chats-view" class="view active"> |
| <div class="view-header"> |
| <h2>Чаты</h2> |
| <button id="create-room-show-modal" class="action-btn"> |
| <svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 0 24 24" width="20" fill="currentColor"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg> |
| </button> |
| </div> |
| <div id="chatroom-list" class="list-container"></div> |
| </div> |
| |
| <div id="users-view" class="view hidden-desktop"> |
| <div class="view-header"> |
| <h2>Пользователи</h2> |
| </div> |
| <div id="user-list" class="list-container"></div> |
| </div> |
| |
| <div id="my-profile-view" class="view hidden-desktop full-width"> |
| <div class="view-header"> |
| <h2>Профиль</h2> |
| <button id="theme-toggle-btn" class="theme-toggle-btn"> |
| <svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.64 5.64c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l1.06 1.06c.39.39 1.02.39 1.41 0s.39-1.02 0-1.41L5.64 5.64zm12.73 12.73c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l1.06 1.06c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-1.06-1.06zM5.64 18.36l1.06-1.06c.39-.39.39-1.02 0-1.41s-1.02-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.02 0 1.41s1.02.39 1.41 0zM18.36 5.64l1.06-1.06c.39-.39.39-1.02 0-1.41s-1.02-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.02 0 1.41s1.02.39 1.41 0z"/></svg> |
| <svg id="theme-icon-moon" style="display: none;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12.3 4.88c.15.42.21.87.18 1.32-.1 1.54-1.09 2.87-2.48 3.48-2.73 1.2-5.73-.55-5.73-3.53 0-.52.08-1.03.23-1.52.2-.64.55-1.22 1-1.72.45-.49 1-.9 1.58-1.22.58-.32 1.2-.5 1.83-.53.43-.02.86.03 1.28.14.36.09.7.24 1.01.44.31.2.59.45.82.74s.42.61.56.95zM9.5 2c-1.82 0-3.53.5-5 1.35C2.39 3.97 1.25 5.26 1.03 6.8c-.21 1.48.19 2.98 1.12 4.24.93 1.27 2.36 2.14 3.95 2.42 2.73.48 5.3-1.12 6.33-3.65.8-1.95.42-4.24-1.01-5.83C12.52 2.69 11.04 2 9.5 2z"/></svg> |
| </button> |
| </div> |
| <div class="profile-card"> |
| <div id="profile-avatar-placeholder" class="avatar"></div> |
| <h3 id="profile-username-display"></h3> |
| <p id="profile-address-display"></p> |
| <p id="profile-balance-display">Баланс: <span id="ton-balance">N/A</span> TON</p> |
| <form id="username-form" class="username-form"> |
| <input type="text" id="username-input" class="username-input" placeholder="Новый никнейм" autocomplete="off"> |
| <button type="submit" class="action-btn">✓</button> |
| </form> |
| <button id="show-my-qr-btn" class="action-btn" style="margin-top: 16px; width: 100%;">Мой QR-код</button> |
| </div> |
| </div> |
| |
| <div id="chat-window-view"> |
| <div id="chat-placeholder" class="chat-placeholder" style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; text-align: center; color: var(--text-secondary); padding: 20px;"> |
| <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol" style="width: 80px; margin-bottom: 20px; opacity: 0.5;"> |
| <h2>Выберите чат для начала общения</h2> |
| </div> |
| <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;"> |
| <div class="chat-header"> |
| <button class="back-btn" id="back-to-list-btn"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.41 7.41 14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg> |
| </button> |
| <div id="chat-header-avatar" class="avatar"></div> |
| <span id="chat-header-title"></span> |
| </div> |
| <div id="messages-container"></div> |
| <form id="message-form" class="message-form"> |
| <input type="text" id="message-input" placeholder="Сообщение" autocomplete="off"> |
| <button type="submit" class="send-btn" id="send-btn"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> |
| </button> |
| </form> |
| </div> |
| </div> |
| </div> |
| |
| <nav class="bottom-nav"> |
| <div id="nav-chats-btn" class="nav-item active"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg> |
| <span>Чаты</span> |
| </div> |
| <div id="nav-users-btn" class="nav-item"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg> |
| <span>Люди</span> |
| </div> |
| <div id="nav-scan-btn" class="nav-item"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zM13 21h8v-2h-8v2zm4-4h4v-2h-4v2z"/></svg> |
| </div> |
| <div id="nav-profile-btn" class="nav-item"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg> |
| <span>Профиль</span> |
| </div> |
| </nav> |
| </div> |
| |
| <div id="create-room-modal" class="modal-overlay"> |
| <div class="modal-content"> |
| <h3>Создать новый чат</h3> |
| <form id="create-room-form"> |
| <label for="room-name">Название чата</label> |
| <input type="text" id="room-name" required> |
| <label for="room-password">Пароль (оставьте пустым для открытого)</label> |
| <input type="password" id="room-password"> |
| <div class="modal-actions"> |
| <button type="button" class="modal-btn secondary-btn" data-close>Отмена</button> |
| <button type="submit" class="modal-btn action-btn">Создать</button> |
| </div> |
| </form> |
| </div> |
| </div> |
| |
| <div id="password-modal" class="modal-overlay"> |
| <div class="modal-content"> |
| <h3>Вход в приватный чат</h3> |
| <form id="password-form"> |
| <label for="password-input">Введите пароль</label> |
| <input type="password" id="password-input" required> |
| <div class="modal-actions"> |
| <button type="button" class="modal-btn secondary-btn" data-close>Отмена</button> |
| <button type="submit" class="modal-btn action-btn">Войти</button> |
| </div> |
| </form> |
| </div> |
| </div> |
| |
| <div id="profile-modal" class="modal-overlay"> |
| <div class="modal-content" style="text-align: center;"> |
| <h3 id="profile-modal-title">Профиль пользователя</h3> |
| <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div> |
| <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p> |
| <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p> |
| <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div> |
| <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">Отсканируйте для открытия профиля</p> |
| <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;"> |
| <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button> |
| <button class="modal-btn secondary-btn" data-close>Закрыть</button> |
| </div> |
| </div> |
| </div> |
| |
| <div id="scanner-modal" class="modal-overlay"> |
| <div class="modal-content"> |
| <h3>Сканировать QR-код</h3> |
| <div id="qr-reader" style="width: 100%; border-radius: var(--radius-l); overflow: hidden; margin-top: 16px;"></div> |
| <div class="modal-actions"> |
| <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button> |
| </div> |
| </div> |
| </div> |
| |
| <div id="status-bar"></div> |
| |
| <script> |
| document.addEventListener('DOMContentLoaded', () => { |
| const tonConnectUI = new TON_CONNECT_UI.TonConnectUI({ |
| manifestUrl: 'https://huggingface.co/spaces/Aleksmorshen/MorshenGroup/resolve/main/tonconnect-manifest.json', |
| buttonRootId: 'ton-connect-button' |
| }); |
| |
| let currentUser = { address: null, username: null }; |
| let activeChatroomId = null; |
| let messagePollingInterval = null; |
| let chatroomsData = {}; |
| let html5QrCode = null; |
| let profileQrCode = null; |
| let isMobile = window.innerWidth < 768; |
| |
| const loginView = document.getElementById('login-view'); |
| const appView = document.getElementById('app-view'); |
| const chatWindowView = document.getElementById('chat-window-view'); |
| |
| const body = document.body; |
| const themeToggleBtn = document.getElementById('theme-toggle-btn'); |
| const sunIcon = document.getElementById('theme-icon-sun'); |
| const moonIcon = document.getElementById('theme-icon-moon'); |
| |
| const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292']; |
| |
| const getAvatar = (name, size) => { |
| const initial = (name ? name[0] : '?').toUpperCase(); |
| const charCode = initial.charCodeAt(0); |
| const color = AVATAR_COLORS[charCode % AVATAR_COLORS.length]; |
| const avatar = document.createElement('div'); |
| avatar.className = 'avatar'; |
| if(size) { |
| avatar.style.width = `${size}px`; |
| avatar.style.height = `${size}px`; |
| avatar.style.fontSize = `${size*0.4}px`; |
| } |
| avatar.style.backgroundColor = color; |
| avatar.textContent = initial; |
| return avatar; |
| }; |
| |
| const showStatus = (message, type = 'info', duration = 3000) => { |
| const statusBar = document.getElementById('status-bar'); |
| statusBar.textContent = message; |
| statusBar.className = 'status-bar'; |
| if (type === 'success') statusBar.classList.add('success'); |
| else if (type === 'error') statusBar.classList.add('error'); |
| statusBar.classList.add('visible'); |
| setTimeout(() => statusBar.classList.remove('visible'), duration); |
| }; |
| |
| const apiCall = async (endpoint, options = {}) => { |
| try { |
| const response = await fetch(endpoint, options); |
| if (!response.ok) { |
| const errorData = await response.json().catch(() => ({ error: 'Request failed with status ' + response.status })); |
| throw new Error(errorData.error || 'Unknown error'); |
| } |
| if (response.status === 204) return null; |
| return await response.json(); |
| } catch (error) { |
| showStatus(`Ошибка: ${error.message}`, 'error'); |
| throw error; |
| } |
| }; |
| |
| const truncateAddress = (address) => address ? `${address.substring(0, 6)}...${address.substring(address.length - 6)}` : ''; |
| |
| const updateCurrentUserInfo = () => { |
| document.getElementById('profile-username-display').textContent = currentUser.username || 'Аноним'; |
| document.getElementById('profile-address-display').textContent = truncateAddress(currentUser.address); |
| document.getElementById('username-input').value = currentUser.username || ''; |
| const avatarPlaceholder = document.getElementById('profile-avatar-placeholder'); |
| avatarPlaceholder.innerHTML = ''; |
| avatarPlaceholder.appendChild(getAvatar(currentUser.username || currentUser.address, 100)); |
| }; |
| |
| document.getElementById('username-form').addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| const newUsername = document.getElementById('username-input').value.trim(); |
| if (!newUsername || newUsername.length < 3) { |
| showStatus('Никнейм должен быть не короче 3 символов.', 'error'); return; |
| } |
| try { |
| await apiCall('/api/set_username', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ address: currentUser.address, username: newUsername }) |
| }); |
| currentUser.username = newUsername; |
| updateCurrentUserInfo(); |
| showStatus('Никнейм успешно обновлен!', 'success'); |
| fetchChatrooms(); |
| fetchUsers(); |
| if (activeChatroomId) fetchMessages(activeChatroomId); |
| } catch (err) {} |
| }); |
| |
| const initializeUser = async (address) => { |
| currentUser.address = address; |
| try { |
| const data = await apiCall('/api/user_data', { |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ address: currentUser.address }) |
| }); |
| currentUser.username = data.username; |
| } catch (err) { currentUser.username = null; } |
| updateCurrentUserInfo(); |
| loginView.style.display = 'none'; |
| appView.style.display = isMobile ? 'flex' : 'flex'; |
| showView('chats-view'); |
| fetchChatrooms(); |
| fetchUsers(); |
| }; |
| |
| const renderChatrooms = (rooms) => { |
| const list = document.getElementById('chatroom-list'); |
| list.innerHTML = ''; |
| chatroomsData = {}; |
| rooms.forEach(room => { |
| chatroomsData[room.id] = room; |
| const item = document.createElement('div'); |
| item.className = 'list-item'; |
| item.dataset.id = room.id; |
| item.appendChild(getAvatar(room.name)); |
| const infoDiv = document.createElement('div'); |
| infoDiv.className = 'item-info'; |
| infoDiv.innerHTML = `<div class="item-name">${room.name}</div>`; |
| item.appendChild(infoDiv); |
| if (room.is_private) { |
| item.innerHTML += `<svg class="lock-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM9 8V6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9z"/></svg>`; |
| } |
| item.addEventListener('click', () => selectChatroom(room.id, room.is_private)); |
| list.appendChild(item); |
| }); |
| }; |
| |
| const fetchChatrooms = async () => { try { const data = await apiCall('/api/chatrooms'); renderChatrooms(data.chatrooms); } catch (err) {} }; |
| |
| const renderUsers = (users) => { |
| const list = document.getElementById('user-list'); |
| list.innerHTML = ''; |
| users.forEach(user => { |
| const item = document.createElement('div'); |
| item.className = 'list-item'; |
| item.appendChild(getAvatar(user.username || user.address)); |
| const infoDiv = document.createElement('div'); |
| infoDiv.className = 'item-info'; |
| infoDiv.innerHTML = `<div class="item-name">${user.username || 'Аноним'}</div><div class="item-detail">${truncateAddress(user.address)}</div>`; |
| item.appendChild(infoDiv); |
| item.addEventListener('click', () => showProfile(user.address)); |
| list.appendChild(item); |
| }); |
| }; |
| |
| const fetchUsers = async () => { try { const data = await apiCall('/api/users'); renderUsers(data.users); } catch (err) {} }; |
| |
| const renderMessages = (messages) => { |
| const container = document.getElementById('messages-container'); |
| const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 50; |
| container.innerHTML = ''; |
| messages.forEach(msg => { |
| const msgDiv = document.createElement('div'); |
| msgDiv.className = 'message ' + (msg.sender_address === currentUser.address ? 'sent' : 'received'); |
| const avatar = getAvatar(msg.display_name); |
| avatar.onclick = () => showProfile(msg.sender_address); |
| const contentDiv = document.createElement('div'); |
| contentDiv.className = 'message-content'; |
| const senderDiv = document.createElement('div'); |
| senderDiv.className = 'message-sender'; |
| senderDiv.textContent = msg.display_name; |
| senderDiv.onclick = () => showProfile(msg.sender_address); |
| const bubbleDiv = document.createElement('div'); |
| bubbleDiv.className = 'message-bubble'; |
| bubbleDiv.textContent = msg.text; |
| contentDiv.appendChild(senderDiv); contentDiv.appendChild(bubbleDiv); |
| msgDiv.appendChild(contentDiv); msgDiv.insertBefore(avatar, contentDiv); |
| container.appendChild(msgDiv); |
| }); |
| if(shouldScroll) { container.scrollTop = container.scrollHeight; } |
| }; |
| |
| const fetchMessages = async (roomId) => { |
| try { |
| const data = await apiCall(`/api/messages/${roomId}`); |
| renderMessages(data.messages); |
| } catch (err) { |
| if (messagePollingInterval) clearInterval(messagePollingInterval); |
| } |
| }; |
| |
| const selectChatroom = (roomId, isPrivate) => { |
| const roomData = chatroomsData[roomId]; |
| if (!roomData) return; |
| const proceedToRoom = () => { |
| if (messagePollingInterval) clearInterval(messagePollingInterval); |
| activeChatroomId = roomId; |
| document.getElementById('chat-header-title').textContent = roomData.name; |
| const headerAvatar = document.getElementById('chat-header-avatar'); |
| headerAvatar.innerHTML = ''; headerAvatar.appendChild(getAvatar(roomData.name)); |
| document.getElementById('chat-placeholder').style.display = 'none'; |
| document.getElementById('active-chat').style.display = 'flex'; |
| if (isMobile) chatWindowView.classList.add('visible'); |
| fetchMessages(roomId); |
| messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000); |
| }; |
| |
| if (isPrivate) { |
| const passwordModal = document.getElementById('password-modal'); |
| const passwordForm = document.getElementById('password-form'); |
| const passwordInput = document.getElementById('password-input'); |
| passwordModal.style.display = 'flex'; |
| passwordInput.value = ''; passwordInput.focus(); |
| |
| const formSubmitHandler = async (e) => { |
| e.preventDefault(); |
| passwordForm.removeEventListener('submit', formSubmitHandler); |
| const password = passwordInput.value; |
| passwordModal.style.display = 'none'; |
| try { |
| await apiCall('/api/join_chatroom', { |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ chatroom_id: roomId, password }) |
| }); |
| proceedToRoom(); |
| } catch (err) {} |
| }; |
| passwordForm.addEventListener('submit', formSubmitHandler); |
| } else { |
| proceedToRoom(); |
| } |
| }; |
| document.getElementById('back-to-list-btn').addEventListener('click', () => { |
| chatWindowView.classList.remove('visible'); |
| if (messagePollingInterval) clearInterval(messagePollingInterval); |
| activeChatroomId = null; |
| }); |
| |
| document.getElementById('message-form').addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| const input = document.getElementById('message-input'); |
| const text = input.value.trim(); |
| if (text && activeChatroomId) { |
| const originalText = text; |
| input.value = ''; |
| try { |
| await apiCall('/api/send_message', { |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ chatroom_id: activeChatroomId, sender_address: currentUser.address, text: originalText }) |
| }); |
| await fetchMessages(activeChatroomId); |
| document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight; |
| } catch (err) { |
| input.value = originalText; |
| } |
| } |
| }); |
| |
| const showModal = (modalId) => document.getElementById(modalId).style.display = 'flex'; |
| const hideModal = (modalId) => document.getElementById(modalId).style.display = 'none'; |
| |
| document.getElementById('create-room-show-modal').addEventListener('click', () => showModal('create-room-modal')); |
| document.getElementById('create-room-form').addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| const name = document.getElementById('room-name').value.trim(); |
| const password = document.getElementById('room-password').value; |
| if (!name) return; |
| try { |
| await apiCall('/api/create_chatroom', { |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ name, password: password || null, creator_address: currentUser.address }) |
| }); |
| hideModal('create-room-modal'); |
| showStatus('Чат успешно создан!', 'success'); |
| fetchChatrooms(); |
| } catch (err) {} |
| }); |
| |
| const showProfile = async (address) => { |
| const profileModal = document.getElementById('profile-modal'); |
| try { |
| const userData = await apiCall('/api/user_data', { |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address }) |
| }); |
| const username = userData.username || `User ${truncateAddress(address)}`; |
| document.getElementById('profile-modal-title').textContent = (address === currentUser.address) ? "Мой профиль" : "Профиль пользователя"; |
| const avatarContainer = document.getElementById('profile-avatar-container'); |
| avatarContainer.innerHTML = ''; avatarContainer.appendChild(getAvatar(username, 80)); |
| document.getElementById('profile-username').textContent = username; |
| document.getElementById('profile-address').textContent = address; |
| const qrCodeEl = document.getElementById('profile-qr-code'); |
| qrCodeEl.innerHTML = ''; |
| if (profileQrCode) profileQrCode.clear(); |
| profileQrCode = new QRCode(qrCodeEl, { text: address, width: 150, height: 150, correctLevel : QRCode.CorrectLevel.H }); |
| |
| const sendTonBtn = document.getElementById('send-ton-btn'); |
| sendTonBtn.onclick = async () => { |
| if (!tonConnectUI.connected) { showStatus('Подключите кошелек для отправки TON.', 'error'); return; } |
| const amountString = prompt("Введите сумму в TON для отправки:", "0.1"); |
| if (amountString === null) return; |
| const amount = parseFloat(amountString); |
| if (isNaN(amount) || amount <= 0) { showStatus('Неверная сумма.', 'error'); return; } |
| const transaction = { |
| validUntil: Math.floor(Date.now() / 1000) + 600, |
| messages: [ { address: address, amount: Math.floor(amount * 1e9).toString() } ] |
| }; |
| try { |
| await tonConnectUI.sendTransaction(transaction); |
| showStatus(`Транзакция отправлена успешно!`, 'success'); |
| hideModal('profile-modal'); |
| } catch (error) { showStatus('Транзакция отклонена.', 'error'); } |
| }; |
| sendTonBtn.style.display = (address === currentUser.address) ? 'none' : 'flex'; |
| showModal('profile-modal'); |
| } catch (err) { showStatus('Не удалось загрузить профиль.', 'error'); } |
| }; |
| |
| document.getElementById('show-my-qr-btn').addEventListener('click', () => showProfile(currentUser.address)); |
| |
| const showScanner = () => { |
| showModal('scanner-modal'); |
| html5QrCode = new Html5Qrcode("qr-reader"); |
| html5QrCode.start({ facingMode: "environment" }, { fps: 10, qrbox: { width: 250, height: 250 } }, |
| (decodedText) => { |
| hideScanner(); |
| if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) { |
| showProfile(decodedText); |
| } else { showStatus('Отсканирован недействительный QR-код.', 'error'); } |
| }) |
| .catch(() => { showStatus('Не удалось запустить сканер.', 'error'); hideScanner(); }); |
| }; |
| const hideScanner = () => { |
| if (html5QrCode && html5QrCode.isScanning) { |
| html5QrCode.stop().catch(err => console.error("Failed to stop QR scanner.", err)); |
| } |
| hideModal('scanner-modal'); |
| }; |
| document.getElementById('nav-scan-btn').addEventListener('click', showScanner); |
| document.getElementById('scanner-close-btn').addEventListener('click', hideScanner); |
| |
| document.querySelectorAll('[data-close]').forEach(btn => btn.addEventListener('click', (e) => { |
| e.target.closest('.modal-overlay').style.display = 'none'; |
| })); |
| |
| const showView = (viewId) => { |
| document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); |
| document.getElementById(viewId).classList.add('active'); |
| |
| document.querySelectorAll('.nav-item').forEach(v => v.classList.remove('active')); |
| let navBtnId; |
| if (viewId === 'chats-view') navBtnId = 'nav-chats-btn'; |
| else if (viewId === 'users-view') navBtnId = 'nav-users-btn'; |
| else if (viewId === 'my-profile-view') navBtnId = 'nav-profile-btn'; |
| if(navBtnId) document.getElementById(navBtnId).classList.add('active'); |
| |
| if(!isMobile) { |
| document.querySelectorAll('.view').forEach(v => { |
| v.classList.remove('hidden-desktop'); |
| v.classList.remove('full-width'); |
| }); |
| document.getElementById(viewId).classList.add('active'); |
| if(viewId === 'my-profile-view' || viewId === 'users-view') { |
| document.getElementById('chats-view').classList.remove('active'); |
| document.getElementById(viewId).classList.add('full-width'); |
| } else { |
| document.getElementById('chats-view').classList.add('active'); |
| } |
| } |
| }; |
| |
| document.getElementById('nav-chats-btn').addEventListener('click', () => showView('chats-view')); |
| document.getElementById('nav-users-btn').addEventListener('click', () => showView('users-view')); |
| document.getElementById('nav-profile-btn').addEventListener('click', () => showView('my-profile-view')); |
| |
| const applyTheme = (theme) => { |
| if (theme === 'light') { |
| body.classList.add('light-theme'); |
| sunIcon.style.display = 'none'; |
| moonIcon.style.display = 'block'; |
| } else { |
| body.classList.remove('light-theme'); |
| sunIcon.style.display = 'block'; |
| moonIcon.style.display = 'none'; |
| } |
| localStorage.setItem('theme', theme); |
| const themeColor = getComputedStyle(body).getPropertyValue('--bg-secondary').trim(); |
| body.style.backgroundColor = themeColor; |
| document.querySelector('meta[name="theme-color"]')?.setAttribute('content', themeColor); |
| }; |
| themeToggleBtn.addEventListener('click', () => { |
| const currentTheme = body.classList.contains('light-theme') ? 'light' : 'dark'; |
| applyTheme(currentTheme === 'light' ? 'dark' : 'light'); |
| }); |
| applyTheme(localStorage.getItem('theme') || 'dark'); |
| |
| window.addEventListener('resize', () => { |
| const newIsMobile = window.innerWidth < 768; |
| if (newIsMobile !== isMobile) { |
| isMobile = newIsMobile; |
| location.reload(); |
| } |
| }); |
| |
| tonConnectUI.onStatusChange(wallet => { |
| if (wallet) { |
| const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false); |
| initializeUser(address); |
| } else { |
| currentUser = { address: null, username: null }; |
| appView.style.display = 'none'; |
| loginView.style.display = 'flex'; |
| if (messagePollingInterval) clearInterval(messagePollingInterval); |
| activeChatroomId = null; |
| } |
| }); |
| |
| document.body.style.setProperty('--bg-secondary-rgb', body.classList.contains('light-theme') ? '255, 255, 255' : '18, 18, 18'); |
| |
| showView('chats-view'); |
| }); |
| </script> |
| </body> |
| </html> |
| ''' |
| return Response(html_content, mimetype='text/html') |
|
|
| @app.route('/api/users', methods=['GET']) |
| def get_users(): |
| db = read_db() |
| users_list = [{'address': addr, 'username': data.get('username')} for addr, data in db['users'].items()] |
| return jsonify({'users': users_list}) |
|
|
| @app.route('/api/user_data', methods=['POST']) |
| def get_user_data(): |
| data = request.get_json() |
| address = data.get('address') |
| if not address: |
| return jsonify({'error': 'Address is required'}), 400 |
| db = read_db() |
| user_info = db['users'].get(address) |
| username = user_info.get('username') if user_info else None |
| return jsonify({'username': username}) |
|
|
| @app.route('/api/set_username', methods=['POST']) |
| def set_username(): |
| data = request.get_json() |
| address = data.get('address') |
| username = data.get('username') |
| if not address or not username: |
| return jsonify({'error': 'Address and username are required'}), 400 |
| if len(username) < 3 or len(username) > 20: |
| return jsonify({'error': 'Username must be between 3 and 20 characters'}), 400 |
|
|
| db = read_db() |
| if address not in db['users']: |
| db['users'][address] = {} |
| db['users'][address]['username'] = username |
| write_db(db) |
| return jsonify({'success': True}) |
|
|
| @app.route('/api/chatrooms', methods=['GET']) |
| def get_chatrooms(): |
| db = read_db() |
| chatrooms_list = [] |
| for room_id, room_data in db['chatrooms'].items(): |
| chatrooms_list.append({ |
| 'id': room_id, |
| 'name': room_data['name'], |
| 'is_private': room_data['is_private'] |
| }) |
| return jsonify({'chatrooms': sorted(chatrooms_list, key=lambda x: x['name'])}) |
|
|
| @app.route('/api/create_chatroom', methods=['POST']) |
| def create_chatroom(): |
| data = request.get_json() |
| name = data.get('name') |
| password = data.get('password') |
| creator_address = data.get('creator_address') |
| if not name or not creator_address: |
| return jsonify({'error': 'Name and creator address are required'}), 400 |
|
|
| db = read_db() |
| room_id = str(uuid.uuid4()) |
| db['chatrooms'][room_id] = { |
| 'name': name, |
| 'creator': creator_address, |
| 'is_private': bool(password), |
| 'password_hash': generate_password_hash(password) if password else None |
| } |
| db['messages'][room_id] = [] |
| write_db(db) |
| return jsonify({'success': True, 'chatroom_id': room_id}) |
|
|
| @app.route('/api/join_chatroom', methods=['POST']) |
| def join_chatroom(): |
| data = request.get_json() |
| chatroom_id = data.get('chatroom_id') |
| password = data.get('password') |
| db = read_db() |
| chatroom = db['chatrooms'].get(chatroom_id) |
| if not chatroom: |
| return jsonify({'error': 'Chatroom not found'}), 404 |
| if chatroom['is_private']: |
| if not password or not check_password_hash(chatroom['password_hash'], password): |
| return jsonify({'error': 'Invalid password'}), 403 |
| return jsonify({'success': True}) |
|
|
| @app.route('/api/messages/<chatroom_id>', methods=['GET']) |
| def get_messages(chatroom_id): |
| db = read_db() |
| if chatroom_id not in db['messages']: |
| return jsonify({'error': 'Chatroom not found'}), 404 |
| |
| messages_with_names = [] |
| room_messages = db['messages'].get(chatroom_id, []) |
| |
| for msg in room_messages: |
| sender_address = msg['sender_address'] |
| user_info = db['users'].get(sender_address) |
| display_name = (user_info.get('username') if user_info and user_info.get('username') |
| else f"{sender_address[:4]}...{sender_address[-4:]}") |
| |
| msg_copy = msg.copy() |
| msg_copy['display_name'] = display_name |
| messages_with_names.append(msg_copy) |
|
|
| return jsonify({'messages': messages_with_names}) |
|
|
| @app.route('/api/send_message', methods=['POST']) |
| def send_message(): |
| data = request.get_json() |
| chatroom_id = data.get('chatroom_id') |
| sender_address = data.get('sender_address') |
| text = data.get('text') |
|
|
| if not all([chatroom_id, sender_address, text]): |
| return jsonify({'error': 'Missing data'}), 400 |
|
|
| db = read_db() |
| if chatroom_id not in db['messages']: |
| return jsonify({'error': 'Chatroom not found'}), 404 |
|
|
| message = { |
| 'id': str(uuid.uuid4()), |
| 'sender_address': sender_address, |
| 'text': text, |
| 'timestamp': datetime.utcnow().isoformat() + "Z" |
| } |
| |
| if len(db['messages'][chatroom_id]) >= 100: |
| db['messages'][chatroom_id].pop(0) |
|
|
| db['messages'][chatroom_id].append(message) |
| write_db(db) |
| return jsonify({'success': True}) |
|
|
|
|
| if __name__ == '__main__': |
| init_db() |
| app.run(host='0.0.0.0', port=7860) |