|
|
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" data-theme="dark"> |
|
|
<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; |
|
|
--transition-fast: 0.2s ease; |
|
|
--transition-medium: 0.3s ease; |
|
|
} |
|
|
|
|
|
html[data-theme='light'] { |
|
|
--bg-primary: #FFFFFF; |
|
|
--bg-secondary: #F0F0F0; |
|
|
--bg-tertiary: #E3E3E3; |
|
|
--bg-hover: #DCDCDC; |
|
|
--bg-modal: rgba(255, 255, 255, 0.8); |
|
|
--text-primary: #000000; |
|
|
--text-secondary: #6D6D72; |
|
|
--text-tertiary: #AEAEB2; |
|
|
--border-color: #D1D1D6; |
|
|
--accent-blue: #007AFF; |
|
|
--accent-blue-light: #3395FF; |
|
|
--accent-blue-gradient: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light)); |
|
|
--success-color: #34C759; |
|
|
--error-color: #FF3B30; |
|
|
--shadow-color: rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
|
|
|
html[data-theme='dark'] { |
|
|
--bg-primary: #000000; |
|
|
--bg-secondary: #1C1C1E; |
|
|
--bg-tertiary: #2C2C2E; |
|
|
--bg-hover: #3A3A3C; |
|
|
--bg-modal: rgba(28, 28, 30, 0.8); |
|
|
--text-primary: #FFFFFF; |
|
|
--text-secondary: #8E8E93; |
|
|
--text-tertiary: #636366; |
|
|
--border-color: #38383A; |
|
|
--accent-blue: #0A84FF; |
|
|
--accent-blue-light: #339dff; |
|
|
--accent-blue-gradient: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light)); |
|
|
--success-color: #30D158; |
|
|
--error-color: #FF453A; |
|
|
--shadow-color: rgba(0, 0, 0, 0.5); |
|
|
} |
|
|
|
|
|
* { |
|
|
box-sizing: border-box; |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
-webkit-tap-highlight-color: transparent; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: var(--font-family); |
|
|
background-color: var(--bg-primary); |
|
|
color: var(--text-primary); |
|
|
overflow: hidden; |
|
|
height: 100vh; |
|
|
width: 100vw; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
-webkit-font-smoothing: antialiased; |
|
|
-moz-osx-font-smoothing: grayscale; |
|
|
} |
|
|
|
|
|
.main-container { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
transition: opacity 0.3s ease; |
|
|
} |
|
|
|
|
|
#login-view { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
text-align: center; |
|
|
padding: 20px; |
|
|
background: var(--bg-primary); |
|
|
} |
|
|
#login-view img { width: 120px; height: 120px; margin-bottom: 24px; filter: drop-shadow(0 0 20px rgba(10, 132, 255, 0.5)); } |
|
|
#login-view h1 { font-size: 3rem; font-weight: 700; background: var(--accent-blue-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; max-width: 300px; } |
|
|
|
|
|
#app-container { display: none; width: 100%; height: 100%; } |
|
|
|
|
|
.app-layout { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
display: flex; |
|
|
} |
|
|
|
|
|
.main-content { |
|
|
flex-grow: 1; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
position: relative; |
|
|
height: 100%; |
|
|
overflow: hidden; |
|
|
} |
|
|
.content-view { |
|
|
display: none; |
|
|
flex-direction: column; |
|
|
height: 100%; |
|
|
width: 100%; |
|
|
background-color: var(--bg-primary); |
|
|
animation: fadeIn 0.3s ease; |
|
|
} |
|
|
.content-view.active { display: flex; } |
|
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } |
|
|
|
|
|
.view-header { |
|
|
padding: 16px; |
|
|
flex-shrink: 0; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: space-between; |
|
|
border-bottom: 1px solid var(--border-color); |
|
|
} |
|
|
.view-header h2 { font-size: 1.8rem; font-weight: 700; } |
|
|
|
|
|
.item-list { |
|
|
flex-grow: 1; |
|
|
overflow-y: auto; |
|
|
padding: 8px 0; |
|
|
} |
|
|
|
|
|
.list-item { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 12px; |
|
|
padding: 10px 16px; |
|
|
cursor: pointer; |
|
|
transition: background-color var(--transition-fast); |
|
|
} |
|
|
.list-item:hover { background-color: var(--bg-hover); } |
|
|
.list-item .item-info { flex-grow: 1; overflow: hidden; } |
|
|
.list-item .item-name { font-weight: 500; font-size: 1.1rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
|
|
.list-item .item-subtext { font-size: 0.9rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
|
|
.list-item .lock-icon { width: 16px; height: 16px; fill: var(--text-secondary); 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: white; |
|
|
flex-shrink: 0; |
|
|
} |
|
|
|
|
|
#chat-window-view { display: none; } |
|
|
.chat-header { |
|
|
display: flex; align-items: center; gap: 12px; padding: 12px 16px; background-color: var(--bg-secondary); border-bottom: 1px solid var(--border-color); flex-shrink: 0; |
|
|
} |
|
|
.back-btn { background: none; border: none; cursor: pointer; display: none; } |
|
|
.back-btn svg { width: 28px; height: 28px; fill: var(--accent-blue); } |
|
|
.chat-header .avatar { width: 40px; height: 40px; } |
|
|
#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: 12px; |
|
|
} |
|
|
.message { display: flex; gap: 10px; max-width: 85%; } |
|
|
.message .avatar { width: 36px; height: 36px; align-self: flex-end; } |
|
|
.message-content { display: flex; flex-direction: column; gap: 4px; } |
|
|
.message-sender { font-size: 0.85rem; font-weight: 600; color: var(--text-secondary); word-break: break-all; cursor: pointer; padding: 0 12px; } |
|
|
.message-bubble { padding: 10px 14px; border-radius: 20px; line-height: 1.4; word-wrap: break-word; font-size: 1.05rem; } |
|
|
.message.sent { align-self: flex-end; } |
|
|
.message.sent .message-bubble { background: var(--accent-blue-gradient); color: white; border-bottom-right-radius: 4px; } |
|
|
.message.received { align-self: flex-start; } |
|
|
.message.received .message-sender { color: var(--accent-blue); } |
|
|
.message.received .message-bubble { background-color: var(--bg-tertiary); border-bottom-left-radius: 4px; } |
|
|
|
|
|
.message-form-container { |
|
|
display: flex; padding: 8px 16px; gap: 12px; background-color: var(--bg-secondary); border-top: 1px solid var(--border-color); flex-shrink: 0; |
|
|
} |
|
|
#message-input { |
|
|
flex-grow: 1; padding: 12px 18px; border: none; background-color: var(--bg-tertiary); color: var(--text-primary); border-radius: 22px; outline: none; font-size: 1rem; resize: none; |
|
|
} |
|
|
|
|
|
.action-btn { |
|
|
background: var(--accent-blue-gradient); color: white; border: none; padding: 12px 18px; border-radius: 12px; cursor: pointer; font-weight: 600; font-size: 1rem; transition: transform var(--transition-fast), box-shadow var(--transition-fast); |
|
|
} |
|
|
.action-btn:hover { transform: scale(1.03); box-shadow: 0 4px 15px var(--shadow-color); } |
|
|
.action-btn.send-btn { width: 44px; height: 44px; border-radius: 50%; flex-shrink: 0; padding: 0; display: flex; align-items: center; justify-content: center; } |
|
|
.action-btn.send-btn svg { width: 22px; height: 22px; fill: white; } |
|
|
|
|
|
.bottom-nav { |
|
|
position: fixed; bottom: 0; left: 0; right: 0; height: 84px; |
|
|
background: var(--bg-modal); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); |
|
|
border-top: 1px solid var(--border-color); display: flex; justify-content: space-around; align-items: flex-start; padding-top: 10px; z-index: 100; |
|
|
} |
|
|
.nav-item { |
|
|
display: flex; flex-direction: column; align-items: center; gap: 4px; color: var(--text-secondary); cursor: pointer; transition: color var(--transition-fast); |
|
|
} |
|
|
.nav-item.active { color: var(--accent-blue); } |
|
|
.nav-item.qr-btn { margin-top: -30px; } |
|
|
.nav-item .nav-icon { fill: currentColor; width: 28px; height: 28px; } |
|
|
.nav-item .nav-label { font-size: 0.7rem; font-weight: 500; } |
|
|
.nav-item .qr-icon-wrapper { |
|
|
width: 60px; height: 60px; border-radius: 50%; background: var(--accent-blue-gradient); display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 15px var(--shadow-color); |
|
|
} |
|
|
.nav-item .qr-icon-wrapper .nav-icon { fill: white; width: 32px; height: 32px; } |
|
|
|
|
|
.main-content-wrapper { |
|
|
width: 100%; height: 100%; display: flex; |
|
|
} |
|
|
.sidebar { display: none; } |
|
|
|
|
|
#my-profile-view { |
|
|
align-items: center; justify-content: center; text-align: center; padding: 20px; |
|
|
} |
|
|
#my-profile-view .avatar { width: 120px; height: 120px; font-size: 4rem; margin-bottom: 20px; } |
|
|
#my-profile-username { font-size: 1.8rem; font-weight: 600; } |
|
|
#my-profile-address { font-size: 0.9rem; color: var(--text-secondary); word-break: break-all; margin-top: 8px; max-width: 90%; } |
|
|
#my-profile-wallet-info { font-size: 1rem; color: var(--text-secondary); margin-top: 16px; } |
|
|
.profile-actions { margin-top: 30px; display: flex; flex-direction: column; gap: 15px; width: 100%; max-width: 300px; } |
|
|
.theme-switcher { display: flex; justify-content: space-between; align-items: center; background: var(--bg-secondary); padding: 10px 16px; border-radius: 12px; } |
|
|
|
|
|
.switch { position: relative; display: inline-block; width: 51px; height: 31px; } |
|
|
.switch input { opacity: 0; width: 0; height: 0; } |
|
|
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--bg-tertiary); transition: var(--transition-medium); border-radius: 34px; } |
|
|
.slider:before { position: absolute; content: ""; height: 27px; width: 27px; left: 2px; bottom: 2px; background-color: white; transition: var(--transition-medium); border-radius: 50%; } |
|
|
input:checked + .slider { background-color: var(--success-color); } |
|
|
input:checked + .slider:before { transform: translateX(20px); } |
|
|
|
|
|
.modal-overlay { |
|
|
position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; |
|
|
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); animation: fadeIn 0.3s; |
|
|
} |
|
|
.modal-content { |
|
|
background-color: var(--bg-modal); padding: 24px; border-radius: 16px; width: 90%; max-width: 400px; border: 1px solid var(--border-color); box-shadow: 0 10px 40px var(--shadow-color); |
|
|
} |
|
|
.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-color); color: var(--text-primary); border-radius: 8px; font-size: 1rem; |
|
|
} |
|
|
.modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; } |
|
|
.modal-btn { padding: 10px 20px; border-radius: 8px; border: none; cursor: pointer; font-weight: 500; } |
|
|
.secondary-btn { background-color: var(--bg-hover); color: var(--text-primary); } |
|
|
|
|
|
#status-bar { |
|
|
position: fixed; bottom: 90px; left: 50%; transform: translateX(-50%); background-color: rgba(44, 44, 46, 0.85); color: white; padding: 12px 20px; border-radius: 12px; font-size: 0.9rem; |
|
|
opacity: 0; visibility: hidden; transition: opacity var(--transition-medium), visibility var(--transition-medium), transform var(--transition-medium); z-index: 2000; |
|
|
box-shadow: 0 5px 15px var(--shadow-color); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); |
|
|
} |
|
|
#status-bar.visible { opacity: 1; visibility: visible; transform: translate(-50%, -10px); } |
|
|
|
|
|
@media (min-width: 768px) { |
|
|
body { padding: 20px; } |
|
|
.app-layout { max-width: 1200px; max-height: 900px; margin: auto; border-radius: 20px; overflow: hidden; box-shadow: 0 20px 60px var(--shadow-color); border: 1px solid var(--border-color); } |
|
|
.bottom-nav { display: none; } |
|
|
.sidebar { |
|
|
display: flex; flex-direction: column; align-items: center; width: 80px; flex-shrink: 0; padding: 20px 0; |
|
|
background-color: var(--bg-secondary); border-right: 1px solid var(--border-color); z-index: 1; |
|
|
} |
|
|
.sidebar .nav-item { gap: 8px; padding: 15px 0; width: 100%; } |
|
|
.sidebar .nav-item .nav-icon { width: 24px; height: 24px; } |
|
|
.sidebar .nav-item .nav-label { font-size: 0.75rem; } |
|
|
.sidebar .nav-item.qr-btn { margin-top: 0; order: -1; margin-bottom: 20px; } |
|
|
.sidebar .nav-item .qr-icon-wrapper { width: 52px; height: 52px; } |
|
|
.sidebar .nav-item .qr-icon-wrapper .nav-icon { width: 28px; height: 28px; } |
|
|
|
|
|
.content-view { border-right: 1px solid var(--border-color); } |
|
|
#chat-list-view, #user-list-view, #my-profile-view { width: 340px; flex-shrink: 0; } |
|
|
#chat-list-view.active, #user-list-view.active, #my-profile-view.active { display: flex; } |
|
|
.main-content { |
|
|
flex-direction: row; |
|
|
} |
|
|
.back-btn { display: none !important; } |
|
|
|
|
|
#chat-window-view { display: flex; flex-grow: 1; } |
|
|
.placeholder-view { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; text-align: center; color: var(--text-secondary); padding: 20px; background: var(--bg-primary); } |
|
|
.placeholder-view img { width: 80px; margin-bottom: 20px; opacity: 0.5; } |
|
|
|
|
|
#app-container { padding-bottom: 0; } |
|
|
#status-bar { bottom: 30px; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div id="login-view" class="main-container"> |
|
|
<img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol"> |
|
|
<h1>Virton</h1> |
|
|
<p>Децентрализованный и анонимный мессенджер на TON</p> |
|
|
<div id="ton-connect-button"></div> |
|
|
</div> |
|
|
|
|
|
<div id="app-container"> |
|
|
<div class="app-layout"> |
|
|
<nav class="sidebar" id="sidebar-nav"></nav> |
|
|
<div class="main-content"> |
|
|
<div id="chat-list-view" class="content-view"> |
|
|
<div class="view-header"> |
|
|
<h2>Чаты</h2> |
|
|
<button id="create-room-show-modal" class="action-btn" style="padding: 8px 14px; font-size: 0.9rem;">Новый чат</button> |
|
|
</div> |
|
|
<div class="item-list" id="chatroom-list"></div> |
|
|
</div> |
|
|
|
|
|
<div id="user-list-view" class="content-view"> |
|
|
<div class="view-header"> |
|
|
<h2>Пользователи</h2> |
|
|
</div> |
|
|
<div class="item-list" id="user-list"></div> |
|
|
</div> |
|
|
|
|
|
<div id="my-profile-view" class="content-view"> |
|
|
<div id="my-profile-content" style="width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; padding: 20px;"> |
|
|
<div class="avatar" id="my-profile-avatar"></div> |
|
|
<h2 id="my-profile-username"></h2> |
|
|
<p id="my-profile-address"></p> |
|
|
<p id="my-profile-wallet-info"></p> |
|
|
<div class="profile-actions"> |
|
|
<form id="username-form" style="display: flex; gap: 8px;"> |
|
|
<input type="text" id="username-input" class="username-input" placeholder="Новый никнейм" autocomplete="off" style="width: 100%; padding: 12px; background-color: var(--bg-tertiary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 8px; font-size: 1rem;"> |
|
|
<button type="submit" class="action-btn" style="padding: 0 16px;">✓</button> |
|
|
</form> |
|
|
<button id="my-qr-code-btn" class="action-btn secondary-btn" style="background: var(--bg-tertiary);">Мой QR-код</button> |
|
|
<div class="theme-switcher"> |
|
|
<span>Тёмная тема</span> |
|
|
<label class="switch"> |
|
|
<input type="checkbox" id="theme-toggle"> |
|
|
<span class="slider"></span> |
|
|
</label> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="chat-window-view"> |
|
|
<div class="placeholder-view" id="placeholder-view"> |
|
|
<img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol"> |
|
|
<h2>Выберите чат</h2> |
|
|
<p>Начните общение или просмотрите список пользователей</p> |
|
|
</div> |
|
|
<div id="active-chat-content" style="display: none; width: 100%; height: 100%; flex-direction: column;"> |
|
|
<div class="chat-header"> |
|
|
<button class="back-btn" id="back-to-list-btn"> |
|
|
<svg viewBox="0 0 24 24"><path d="M15.41 7.41L14 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> |
|
|
<div class="message-form-container"> |
|
|
<input id="message-input" placeholder="Сообщение..." autocomplete="off"> |
|
|
<button type="button" class="action-btn send-btn" id="send-btn"> |
|
|
<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<nav class="bottom-nav" id="bottom-nav-bar"></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-modal-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-modal-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>Профиль</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;">QR для открытия профиля</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 data-modal-close class="modal-btn secondary-btn">Закрыть</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: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden;"></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, wallet: null }; |
|
|
let activeChatroomId = null; |
|
|
let messagePollingInterval = null; |
|
|
let chatroomsData = {}; |
|
|
let html5QrCode = null; |
|
|
let profileQrCode = null; |
|
|
|
|
|
const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292']; |
|
|
|
|
|
const getAvatar = (name, size = '48px') => { |
|
|
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'; |
|
|
avatar.style.backgroundColor = color; |
|
|
avatar.style.width = size; |
|
|
avatar.style.height = size; |
|
|
avatar.textContent = initial; |
|
|
return avatar; |
|
|
}; |
|
|
|
|
|
const apiCall = async (endpoint, options = {}) => { |
|
|
try { |
|
|
const response = await fetch(endpoint, options); |
|
|
if (!response.ok) { |
|
|
const errorData = await response.json().catch(() => ({ error: 'Request failed: ' + 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, 4)}...${address.substring(address.length - 4)}` : ''; |
|
|
|
|
|
const showStatus = (message, type = 'info', duration = 3000) => { |
|
|
const statusBar = document.getElementById('status-bar'); |
|
|
statusBar.textContent = message; |
|
|
statusBar.className = 'status-bar'; |
|
|
if (type === 'success') statusBar.style.backgroundColor = 'var(--success-color)'; |
|
|
else if (type === 'error') statusBar.style.backgroundColor = 'var(--error-color)'; |
|
|
else statusBar.style.backgroundColor = 'rgba(44, 44, 46, 0.85)'; |
|
|
|
|
|
statusBar.classList.add('visible'); |
|
|
setTimeout(() => statusBar.classList.remove('visible'), duration); |
|
|
}; |
|
|
|
|
|
const showView = (viewId) => { |
|
|
document.querySelectorAll('.content-view').forEach(v => v.classList.remove('active')); |
|
|
document.getElementById(viewId)?.classList.add('active'); |
|
|
|
|
|
document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active')); |
|
|
document.querySelectorAll(`[data-view='${viewId}']`).forEach(item => item.classList.add('active')); |
|
|
|
|
|
const isMobile = window.innerWidth < 768; |
|
|
if (isMobile) { |
|
|
document.getElementById('app-container').scrollTo(0,0); |
|
|
if (viewId === 'chat-window-view') { |
|
|
document.querySelector('.app-layout').style.transform = 'translateX(-100%)'; |
|
|
} else { |
|
|
document.querySelector('.app-layout').style.transform = 'translateX(0%)'; |
|
|
document.querySelectorAll('.content-view').forEach(v => v.style.display = 'none'); |
|
|
const targetView = document.getElementById(viewId); |
|
|
if(targetView) targetView.style.display = 'flex'; |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
const renderNav = () => { |
|
|
const navItems = [ |
|
|
{ id: 'chat-list-view', label: 'Чаты', icon: '<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/>' }, |
|
|
{ id: 'user-list-view', label: 'Контакты', icon: '<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"/>' }, |
|
|
{ id: 'scanner', label: 'Scan', icon: '<path d="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm-2 10a2 2 0 100-4 2 2 0 000 4z"/>', isQr: true }, |
|
|
{ id: 'my-profile-view', label: 'Профиль', icon: '<path d="M12 5.9c1.16 0 2.1.94 2.1 2.1s-.94 2.1-2.1 2.1S9.9 9.16 9.9 8s.94-2.1 2.1-2.1m0 9c2.97 0 6.1 1.46 6.1 2.1v1.1H5.9V17c0-.64 3.13-2.1 6.1-2.1M12 4C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 9c-2.67 0-8 1.34-8 4v3h16v-3c0-2.66-5.33-4-8-4z"/>' } |
|
|
]; |
|
|
|
|
|
const createNavItem = (item) => { |
|
|
const el = document.createElement('div'); |
|
|
el.className = 'nav-item'; |
|
|
if (item.isQr) el.classList.add('qr-btn'); |
|
|
el.dataset.view = item.id; |
|
|
if (item.isQr) { |
|
|
el.innerHTML = `<div class="qr-icon-wrapper"><svg class="nav-icon" viewBox="0 0 24 24">${item.icon}</svg></div>`; |
|
|
el.onclick = showScanner; |
|
|
} else { |
|
|
el.innerHTML = `<svg class="nav-icon" viewBox="0 0 24 24">${item.icon}</svg><span class="nav-label">${item.label}</span>`; |
|
|
el.onclick = () => showView(item.id); |
|
|
} |
|
|
return el; |
|
|
}; |
|
|
|
|
|
const bottomNav = document.getElementById('bottom-nav-bar'); |
|
|
const sidebarNav = document.getElementById('sidebar-nav'); |
|
|
bottomNav.innerHTML = ''; |
|
|
sidebarNav.innerHTML = ''; |
|
|
|
|
|
navItems.forEach(item => { |
|
|
bottomNav.appendChild(createNavItem(item)); |
|
|
sidebarNav.appendChild(createNavItem(item)); |
|
|
}); |
|
|
}; |
|
|
|
|
|
const initializeUser = async (wallet) => { |
|
|
currentUser.address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false); |
|
|
currentUser.wallet = wallet; |
|
|
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; } |
|
|
|
|
|
document.getElementById('login-view').style.display = 'none'; |
|
|
document.getElementById('app-container').style.display = 'block'; |
|
|
|
|
|
renderNav(); |
|
|
renderMyProfile(); |
|
|
fetchChatrooms(); |
|
|
fetchAndRenderUsers(); |
|
|
showView('chat-list-view'); |
|
|
}; |
|
|
|
|
|
const renderMyProfile = () => { |
|
|
const username = currentUser.username || `User ${truncateAddress(currentUser.address)}`; |
|
|
document.getElementById('my-profile-avatar').innerHTML = getAvatar(username, '120px').innerHTML; |
|
|
document.getElementById('my-profile-username').textContent = username; |
|
|
document.getElementById('my-profile-address').textContent = currentUser.address; |
|
|
document.getElementById('username-input').value = currentUser.username || ''; |
|
|
if(currentUser.wallet?.device) { |
|
|
document.getElementById('my-profile-wallet-info').textContent = `Кошелек: ${currentUser.wallet.device.appName}`; |
|
|
} |
|
|
}; |
|
|
|
|
|
document.getElementById('username-form').addEventListener('submit', async (e) => { |
|
|
e.preventDefault(); |
|
|
const newUsername = document.getElementById('username-input').value.trim(); |
|
|
if (!newUsername || newUsername.length < 3) return showStatus('Никнейм должен быть не короче 3 символов.', 'error'); |
|
|
|
|
|
await apiCall('/api/set_username', { |
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ address: currentUser.address, username: newUsername }) |
|
|
}); |
|
|
currentUser.username = newUsername; |
|
|
renderMyProfile(); |
|
|
fetchChatrooms(); |
|
|
fetchAndRenderUsers(); |
|
|
if (activeChatroomId) fetchMessages(activeChatroomId); |
|
|
showStatus('Никнейм успешно обновлен!', 'success'); |
|
|
}); |
|
|
|
|
|
const renderList = (containerId, items, clickHandler, itemRenderer) => { |
|
|
const list = document.getElementById(containerId); |
|
|
list.innerHTML = ''; |
|
|
items.forEach(item => { |
|
|
const itemEl = itemRenderer(item); |
|
|
itemEl.onclick = () => clickHandler(item); |
|
|
list.appendChild(itemEl); |
|
|
}); |
|
|
}; |
|
|
|
|
|
const fetchChatrooms = async () => { |
|
|
try { |
|
|
const data = await apiCall('/api/chatrooms'); |
|
|
chatroomsData = data.chatrooms.reduce((acc, room) => ({...acc, [room.id]: room}), {}); |
|
|
renderList('chatroom-list', data.chatrooms, (room) => selectChatroom(room.id, room.is_private), (room) => { |
|
|
const itemEl = document.createElement('div'); |
|
|
itemEl.className = 'list-item'; |
|
|
itemEl.appendChild(getAvatar(room.name, '48px')); |
|
|
itemEl.innerHTML += `<div class="item-info"><div class="item-name">${room.name}</div><div class="item-subtext">${room.is_private ? "Приватный чат" : "Открытый чат"}</div></div>`; |
|
|
if (room.is_private) itemEl.innerHTML += `<svg class="lock-icon" 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>`; |
|
|
return itemEl; |
|
|
}); |
|
|
} catch (err) {} |
|
|
}; |
|
|
|
|
|
const fetchAndRenderUsers = async () => { |
|
|
try { |
|
|
const data = await apiCall('/api/users'); |
|
|
renderList('user-list', data.users, (user) => showProfile(user.address), (user) => { |
|
|
const itemEl = document.createElement('div'); |
|
|
itemEl.className = 'list-item'; |
|
|
itemEl.appendChild(getAvatar(user.username, '48px')); |
|
|
itemEl.innerHTML += `<div class="item-info"><div class="item-name">${user.username || 'Без имени'}</div><div class="item-subtext">${truncateAddress(user.address)}</div></div>`; |
|
|
return itemEl; |
|
|
}); |
|
|
} 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'); |
|
|
const isSent = msg.sender_address === currentUser.address; |
|
|
msgDiv.className = 'message ' + (isSent ? 'sent' : 'received'); |
|
|
const avatar = getAvatar(msg.display_name, '36px'); |
|
|
avatar.style.cursor = 'pointer'; |
|
|
avatar.onclick = () => showProfile(msg.sender_address); |
|
|
|
|
|
msgDiv.innerHTML = ` |
|
|
<div class="message-content"> |
|
|
<div class="message-sender" onclick="showProfile('${msg.sender_address}')">${isSent ? 'Вы' : msg.display_name}</div> |
|
|
<div class="message-bubble">${msg.text.replace(/</g, "<").replace(/>/g, ">")}</div> |
|
|
</div> |
|
|
`; |
|
|
msgDiv.insertBefore(avatar, msgDiv.firstChild); |
|
|
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; |
|
|
document.getElementById('chat-header-avatar').innerHTML = getAvatar(roomData.name, '40px').innerHTML; |
|
|
|
|
|
document.getElementById('placeholder-view').style.display = 'none'; |
|
|
document.getElementById('active-chat-content').style.display = 'flex'; |
|
|
|
|
|
showView('chat-window-view'); |
|
|
|
|
|
fetchMessages(roomId).then(() => { |
|
|
document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight; |
|
|
}); |
|
|
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(); |
|
|
|
|
|
passwordForm.onsubmit = async (e) => { |
|
|
e.preventDefault(); |
|
|
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) {} |
|
|
}; |
|
|
} else { |
|
|
proceedToRoom(); |
|
|
} |
|
|
}; |
|
|
|
|
|
const sendMessage = async () => { |
|
|
const input = document.getElementById('message-input'); |
|
|
const text = input.value.trim(); |
|
|
if (text && activeChatroomId) { |
|
|
const tempText = input.value; |
|
|
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: text }) |
|
|
}); |
|
|
fetchMessages(activeChatroomId).then(() => { |
|
|
document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight; |
|
|
}); |
|
|
} catch { |
|
|
input.value = tempText; |
|
|
} |
|
|
} |
|
|
}; |
|
|
document.getElementById('send-btn').addEventListener('click', sendMessage); |
|
|
document.getElementById('message-input').addEventListener('keypress', (e) => { |
|
|
if(e.key === 'Enter' && !e.shiftKey) { |
|
|
e.preventDefault(); |
|
|
sendMessage(); |
|
|
} |
|
|
}); |
|
|
|
|
|
const showModal = (modalId) => document.getElementById(modalId).style.display = 'flex'; |
|
|
const hideAllModals = () => document.querySelectorAll('.modal-overlay').forEach(m => m.style.display = 'none'); |
|
|
|
|
|
document.getElementById('create-room-show-modal').onclick = () => showModal('create-room-modal'); |
|
|
document.querySelectorAll('[data-modal-close]').forEach(btn => btn.onclick = hideAllModals); |
|
|
|
|
|
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; |
|
|
await apiCall('/api/create_chatroom', { |
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ name, password: password || null, creator_address: currentUser.address }) |
|
|
}); |
|
|
hideAllModals(); |
|
|
showStatus('Чат успешно создан!', 'success'); |
|
|
fetchChatrooms(); |
|
|
}); |
|
|
|
|
|
const showProfile = async (address) => { |
|
|
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-avatar-container').innerHTML = getAvatar(username, '80px').outerHTML; |
|
|
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, colorDark : "#000000", colorLight : "#ffffff", correctLevel : QRCode.CorrectLevel.H }); |
|
|
|
|
|
const sendTonBtn = document.getElementById('send-ton-btn'); |
|
|
sendTonBtn.onclick = async () => { |
|
|
const amountString = prompt("Введите сумму в TON:", "0.1"); |
|
|
if (amountString === null) return; |
|
|
const amount = parseFloat(amountString); |
|
|
if (isNaN(amount) || amount <= 0) return showStatus('Неверная сумма.', 'error'); |
|
|
|
|
|
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'); |
|
|
hideAllModals(); |
|
|
} catch (error) { showStatus('Транзакция отклонена.', 'error'); } |
|
|
}; |
|
|
sendTonBtn.style.display = (address === currentUser.address) ? 'none' : 'block'; |
|
|
showModal('profile-modal'); |
|
|
} catch (err) {} |
|
|
}; |
|
|
|
|
|
document.getElementById('my-qr-code-btn').onclick = () => { |
|
|
if (currentUser.address) 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.startsWith('EQ') || decodedText.startsWith('UQ'))) { |
|
|
showProfile(decodedText); |
|
|
} else { |
|
|
showStatus('Отсканирован недействительный QR-код.', 'error'); |
|
|
} |
|
|
}) |
|
|
.catch(err => showStatus('Не удалось запустить сканер.', 'error')); |
|
|
}; |
|
|
|
|
|
const hideScanner = () => { |
|
|
if (html5QrCode && html5QrCode.isScanning) { |
|
|
html5QrCode.stop().catch(err => {}); |
|
|
} |
|
|
hideAllModals(); |
|
|
}; |
|
|
document.getElementById('scanner-close-btn').onclick = hideScanner; |
|
|
|
|
|
document.getElementById('back-to-list-btn').addEventListener('click', () => { |
|
|
document.querySelector('.app-layout').style.transform = 'translateX(0%)'; |
|
|
}); |
|
|
|
|
|
const themeToggle = document.getElementById('theme-toggle'); |
|
|
const setTeam = (theme) => { |
|
|
document.documentElement.setAttribute('data-theme', theme); |
|
|
localStorage.setItem('theme', theme); |
|
|
themeToggle.checked = theme === 'dark'; |
|
|
}; |
|
|
themeToggle.addEventListener('change', () => { |
|
|
setTeam(themeToggle.checked ? 'dark' : 'light'); |
|
|
}); |
|
|
const savedTheme = localStorage.getItem('theme'); |
|
|
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; |
|
|
setTeam(savedTheme || (prefersDark ? 'dark' : 'light')); |
|
|
|
|
|
tonConnectUI.onStatusChange(wallet => { |
|
|
if (wallet) { |
|
|
initializeUser(wallet); |
|
|
} else { |
|
|
currentUser = { address: null, username: null, wallet: null }; |
|
|
document.getElementById('app-container').style.display = 'none'; |
|
|
document.getElementById('login-view').style.display = 'flex'; |
|
|
if (messagePollingInterval) clearInterval(messagePollingInterval); |
|
|
activeChatroomId = null; |
|
|
} |
|
|
}); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
''' |
|
|
return Response(html_content, mimetype='text/html') |
|
|
|
|
|
@app.route('/api/users', methods=['GET']) |
|
|
def get_users(): |
|
|
db = read_db() |
|
|
users_list = [] |
|
|
for address, user_data in db['users'].items(): |
|
|
users_list.append({ |
|
|
'address': address, |
|
|
'username': user_data.get('username') |
|
|
}) |
|
|
return jsonify({'users': sorted(users_list, key=lambda x: x.get('username') or 'zzzz')}) |
|
|
|
|
|
@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 |
|
|
if len(text) > 1000: |
|
|
return jsonify({'error': 'Message is too long'}), 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) |