Web3 / app.py
Aleksmorshen's picture
Update app.py
d00a31f verified
raw
history blame
61.2 kB
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)