Aleksmorshen commited on
Commit
46bdacb
·
verified ·
1 Parent(s): 9134166

Upload app (1) (19).py

Browse files
Files changed (1) hide show
  1. app (1) (19).py +1174 -0
app (1) (19).py ADDED
@@ -0,0 +1,1174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import uuid
4
+ from datetime import datetime
5
+ from flask import Flask, Response, request, jsonify
6
+ from werkzeug.security import generate_password_hash, check_password_hash
7
+
8
+ app = Flask(__name__)
9
+
10
+ DB_FILE = 'db.json'
11
+
12
+ def init_db():
13
+ if not os.path.exists(DB_FILE):
14
+ with open(DB_FILE, 'w') as f:
15
+ json.dump({
16
+ "users": {},
17
+ "chatrooms": {},
18
+ "messages": {}
19
+ }, f, indent=4)
20
+
21
+ def read_db():
22
+ with open(DB_FILE, 'r') as f:
23
+ return json.load(f)
24
+
25
+ def write_db(data):
26
+ with open(DB_FILE, 'w') as f:
27
+ json.dump(data, f, indent=4)
28
+
29
+ @app.route('/')
30
+ def index():
31
+ html_content = '''
32
+ <!DOCTYPE html>
33
+ <html lang="ru">
34
+ <head>
35
+ <meta charset="UTF-8">
36
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
37
+ <title>Virton Messenger</title>
38
+ <script src="https://unpkg.com/@tonconnect/ui@latest/dist/tonconnect-ui.min.js"></script>
39
+ <script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
40
+ <script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
41
+ <link rel="preconnect" href="https://fonts.googleapis.com">
42
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
43
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
44
+ <style>
45
+ :root {
46
+ --bg-primary: #0F0F11;
47
+ --bg-secondary: #18191C;
48
+ --bg-tertiary: #212328;
49
+ --bg-hover: #2A2C33;
50
+ --text-primary: #EAECEF;
51
+ --text-secondary: #8E9297;
52
+ --accent-blue: #0088CC;
53
+ --accent-blue-light: #33AADD;
54
+ --border-color: #2D2F34;
55
+ --success-color: #28a745;
56
+ --error-color: #dc3545;
57
+ --font-family: 'Inter', sans-serif;
58
+ }
59
+
60
+ * {
61
+ box-sizing: border-box;
62
+ margin: 0;
63
+ padding: 0;
64
+ -webkit-tap-highlight-color: transparent;
65
+ }
66
+
67
+ html {
68
+ font-size: 16px;
69
+ }
70
+
71
+ body {
72
+ font-family: var(--font-family);
73
+ background-color: var(--bg-primary);
74
+ color: var(--text-primary);
75
+ overflow: hidden;
76
+ height: 100vh;
77
+ width: 100vw;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ }
82
+
83
+ .main-container {
84
+ width: 100%;
85
+ height: 100%;
86
+ display: flex;
87
+ flex-direction: column;
88
+ transition: opacity 0.3s ease;
89
+ }
90
+
91
+ #login-view {
92
+ display: flex;
93
+ flex-direction: column;
94
+ align-items: center;
95
+ justify-content: center;
96
+ text-align: center;
97
+ width: 100%;
98
+ height: 100%;
99
+ background: radial-gradient(circle, #1a2a3a 0%, var(--bg-primary) 70%);
100
+ }
101
+ #login-view img {
102
+ width: 100px;
103
+ height: 100px;
104
+ margin-bottom: 24px;
105
+ filter: drop-shadow(0 0 15px rgba(0, 136, 204, 0.5));
106
+ }
107
+ #login-view h1 {
108
+ font-size: 2.8rem;
109
+ font-weight: 700;
110
+ background: linear-gradient(45deg, var(--accent-blue-light), var(--accent-blue));
111
+ -webkit-background-clip: text;
112
+ -webkit-text-fill-color: transparent;
113
+ margin-bottom: 12px;
114
+ }
115
+ #login-view p {
116
+ font-size: 1.1rem;
117
+ color: var(--text-secondary);
118
+ margin-bottom: 40px;
119
+ }
120
+
121
+ #app-view {
122
+ display: none;
123
+ width: 100%;
124
+ height: 100%;
125
+ }
126
+
127
+ #chatroom-list-view {
128
+ display: flex;
129
+ flex-direction: column;
130
+ height: 100%;
131
+ width: 100%;
132
+ background-color: var(--bg-secondary);
133
+ }
134
+
135
+ .list-header {
136
+ padding: 16px;
137
+ border-bottom: 1px solid var(--border-color);
138
+ flex-shrink: 0;
139
+ }
140
+ .list-header-top {
141
+ display: flex;
142
+ justify-content: space-between;
143
+ align-items: center;
144
+ margin-bottom: 16px;
145
+ }
146
+ .list-header-top h2 {
147
+ font-size: 1.5rem;
148
+ font-weight: 600;
149
+ }
150
+
151
+ .user-profile {
152
+ padding: 12px;
153
+ background-color: var(--bg-tertiary);
154
+ border-radius: 8px;
155
+ margin-bottom: 16px;
156
+ }
157
+ #user-wallet, #user-nickname {
158
+ font-size: 0.9rem;
159
+ color: var(--text-secondary);
160
+ margin-bottom: 8px;
161
+ word-break: break-all;
162
+ }
163
+
164
+ .username-form { display: flex; gap: 8px; }
165
+ .username-input {
166
+ flex-grow: 1;
167
+ background-color: var(--bg-primary);
168
+ border: 1px solid var(--border-color);
169
+ color: var(--text-primary);
170
+ border-radius: 6px;
171
+ padding: 8px 12px;
172
+ font-size: 0.9rem;
173
+ }
174
+ .username-input:focus { outline: none; border-color: var(--accent-blue); }
175
+
176
+ .action-btn {
177
+ background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
178
+ color: white;
179
+ border: none;
180
+ padding: 10px 16px;
181
+ border-radius: 6px;
182
+ cursor: pointer;
183
+ font-weight: 500;
184
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ gap: 8px;
189
+ }
190
+ .action-btn:hover {
191
+ transform: translateY(-2px);
192
+ box-shadow: 0 4px 15px rgba(0, 136, 204, 0.3);
193
+ }
194
+ .action-btn.small {
195
+ padding: 8px 12px;
196
+ font-size: 0.9rem;
197
+ }
198
+
199
+ #chatroom-list {
200
+ flex-grow: 1;
201
+ overflow-y: auto;
202
+ }
203
+ .chatroom-item {
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 12px;
207
+ padding: 12px 16px;
208
+ cursor: pointer;
209
+ border-bottom: 1px solid var(--border-color);
210
+ transition: background-color 0.2s ease;
211
+ }
212
+ .chatroom-item:hover { background-color: var(--bg-hover); }
213
+
214
+ .avatar {
215
+ width: 40px;
216
+ height: 40px;
217
+ border-radius: 50%;
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: center;
221
+ font-weight: 600;
222
+ color: white;
223
+ flex-shrink: 0;
224
+ }
225
+ .chatroom-info {
226
+ flex-grow: 1;
227
+ overflow: hidden;
228
+ }
229
+ .chatroom-name {
230
+ font-weight: 500;
231
+ white-space: nowrap;
232
+ overflow: hidden;
233
+ text-overflow: ellipsis;
234
+ }
235
+ .lock-icon {
236
+ width: 16px;
237
+ height: 16px;
238
+ fill: var(--text-secondary);
239
+ flex-shrink: 0;
240
+ }
241
+
242
+ #chat-window-view {
243
+ display: none;
244
+ flex-direction: column;
245
+ height: 100%;
246
+ width: 100%;
247
+ background-color: var(--bg-primary);
248
+ }
249
+
250
+ .chat-header {
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 12px;
254
+ padding: 12px 16px;
255
+ background-color: var(--bg-secondary);
256
+ border-bottom: 1px solid var(--border-color);
257
+ flex-shrink: 0;
258
+ }
259
+ .back-btn {
260
+ background: none;
261
+ border: none;
262
+ cursor: pointer;
263
+ display: none;
264
+ }
265
+ .back-btn svg {
266
+ width: 24px;
267
+ height: 24px;
268
+ fill: var(--text-primary);
269
+ }
270
+ #chat-header-title {
271
+ font-size: 1.2rem;
272
+ font-weight: 600;
273
+ }
274
+
275
+ #messages-container {
276
+ flex-grow: 1;
277
+ padding: 16px;
278
+ overflow-y: auto;
279
+ display: flex;
280
+ flex-direction: column;
281
+ gap: 12px;
282
+ }
283
+
284
+ .message {
285
+ display: flex;
286
+ gap: 10px;
287
+ max-width: 80%;
288
+ }
289
+ .message .avatar {
290
+ width: 36px;
291
+ height: 36px;
292
+ align-self: flex-end;
293
+ }
294
+ .message-content {
295
+ display: flex;
296
+ flex-direction: column;
297
+ gap: 4px;
298
+ }
299
+ .message-sender {
300
+ font-size: 0.8rem;
301
+ font-weight: 500;
302
+ color: var(--text-secondary);
303
+ word-break: break-all;
304
+ cursor: pointer;
305
+ }
306
+ .message-bubble {
307
+ padding: 10px 14px;
308
+ border-radius: 18px;
309
+ line-height: 1.4;
310
+ word-wrap: break-word;
311
+ }
312
+
313
+ .message.sent { align-self: flex-end; flex-direction: row-reverse; }
314
+ .message.sent .message-sender { text-align: right; color: var(--accent-blue-light); }
315
+ .message.sent .message-bubble {
316
+ background: linear-gradient(45deg, var(--accent-blue), var(--accent-blue-light));
317
+ color: white;
318
+ border-bottom-right-radius: 4px;
319
+ }
320
+
321
+ .message.received { align-self: flex-start; }
322
+ .message.received .message-bubble {
323
+ background-color: var(--bg-tertiary);
324
+ border-bottom-left-radius: 4px;
325
+ }
326
+
327
+ .chat-placeholder {
328
+ display: flex;
329
+ flex-direction: column;
330
+ align-items: center;
331
+ justify-content: center;
332
+ height: 100%;
333
+ text-align: center;
334
+ color: var(--text-secondary);
335
+ padding: 20px;
336
+ }
337
+ .chat-placeholder img { width: 80px; margin-bottom: 20px; opacity: 0.5; }
338
+
339
+ .message-form {
340
+ display: flex;
341
+ padding: 16px;
342
+ gap: 12px;
343
+ background-color: var(--bg-secondary);
344
+ border-top: 1px solid var(--border-color);
345
+ flex-shrink: 0;
346
+ }
347
+ #message-input {
348
+ flex-grow: 1;
349
+ padding: 12px 16px;
350
+ border: 1px solid var(--border-color);
351
+ background-color: var(--bg-tertiary);
352
+ color: var(--text-primary);
353
+ border-radius: 22px;
354
+ outline: none;
355
+ font-size: 1rem;
356
+ transition: border-color 0.2s;
357
+ }
358
+ #message-input:focus { border-color: var(--accent-blue); }
359
+
360
+ .send-btn {
361
+ width: 44px;
362
+ height: 44px;
363
+ border-radius: 50%;
364
+ flex-shrink: 0;
365
+ padding: 0;
366
+ }
367
+ .send-btn svg { width: 20px; height: 20px; fill: white; }
368
+
369
+ .modal-overlay {
370
+ position: fixed;
371
+ top: 0;
372
+ left: 0;
373
+ width: 100%;
374
+ height: 100%;
375
+ background-color: rgba(0,0,0,0.7);
376
+ display: none;
377
+ align-items: center;
378
+ justify-content: center;
379
+ z-index: 1000;
380
+ -webkit-backdrop-filter: blur(5px);
381
+ backdrop-filter: blur(5px);
382
+ }
383
+ .modal-content {
384
+ background-color: var(--bg-secondary);
385
+ padding: 24px;
386
+ border-radius: 12px;
387
+ width: 90%;
388
+ max-width: 400px;
389
+ border: 1px solid var(--border-color);
390
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
391
+ }
392
+ .modal-content h3 { margin-bottom: 20px; font-weight: 600; font-size: 1.3rem; }
393
+ .modal-content label { display: block; margin-bottom: 8px; font-size: 0.9rem; color: var(--text-secondary); }
394
+ .modal-content input {
395
+ width: 100%;
396
+ padding: 12px;
397
+ margin-bottom: 16px;
398
+ background-color: var(--bg-tertiary);
399
+ border: 1px solid var(--border-color);
400
+ color: white;
401
+ border-radius: 6px;
402
+ font-size: 1rem;
403
+ }
404
+ .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
405
+ .modal-btn {
406
+ padding: 10px 20px;
407
+ border-radius: 6px;
408
+ border: none;
409
+ cursor: pointer;
410
+ font-weight: 500;
411
+ }
412
+ .secondary-btn { background-color: var(--bg-hover); color: white; }
413
+
414
+ #status-bar {
415
+ position: fixed;
416
+ bottom: 20px;
417
+ left: 50%;
418
+ transform: translateX(-50%);
419
+ background-color: var(--bg-tertiary);
420
+ color: white;
421
+ padding: 12px 20px;
422
+ border-radius: 8px;
423
+ font-size: 0.9rem;
424
+ opacity: 0;
425
+ visibility: hidden;
426
+ transition: opacity 0.3s, visibility 0.3s;
427
+ z-index: 2000;
428
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
429
+ }
430
+ #status-bar.success { background-color: var(--success-color); }
431
+ #status-bar.error { background-color: var(--error-color); }
432
+ #status-bar.visible { opacity: 1; visibility: visible; }
433
+
434
+ @media (min-width: 768px) {
435
+ .main-container {
436
+ max-width: 1100px;
437
+ max-height: 800px;
438
+ border-radius: 12px;
439
+ overflow: hidden;
440
+ box-shadow: 0 10px 40px rgba(0,0,0,0.3);
441
+ border: 1px solid var(--border-color);
442
+ }
443
+ #app-view {
444
+ flex-direction: row;
445
+ }
446
+ #chatroom-list-view {
447
+ width: 320px;
448
+ flex-shrink: 0;
449
+ border-right: 1px solid var(--border-color);
450
+ display: flex !important;
451
+ }
452
+ #chat-window-view {
453
+ width: auto;
454
+ flex-grow: 1;
455
+ display: flex !important;
456
+ }
457
+ #chat-window-view.hidden-on-desktop {
458
+ display: flex !important;
459
+ }
460
+ .back-btn { display: none !important; }
461
+ }
462
+ </style>
463
+ </head>
464
+ <body>
465
+
466
+ <div id="login-view" class="main-container">
467
+ <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
468
+ <h1>Virton</h1>
469
+ <p>Децентрализованный и анонимный мессенджер</p>
470
+ <div id="ton-connect-button"></div>
471
+ </div>
472
+
473
+ <div id="app-view" class="main-container">
474
+ <div id="chatroom-list-view">
475
+ <div class="list-header">
476
+ <div class="list-header-top">
477
+ <h2>Чаты</h2>
478
+ <div style="display: flex; align-items: center; gap: 8px;">
479
+ <button id="my-profile-btn" class="action-btn small" title="Мой профиль">
480
+ <svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 24 24" width="16" fill="white"><path d="M0 0h24v24H0V0z" fill="none"/><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>
481
+ </button>
482
+ <button id="scan-qr-btn" class="action-btn small" title="Сканировать QR">
483
+ <svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 24 24" width="16" fill="white"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm-2 10a2 2 0 100-4 2 2 0 000 4z"/></svg>
484
+ </button>
485
+ <button id="create-room-show-modal" class="action-btn small">
486
+ <svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 24 24" width="16" fill="white"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
487
+ <span>Новый</span>
488
+ </button>
489
+ </div>
490
+ </div>
491
+ <div class="user-profile">
492
+ <div id="user-wallet"></div>
493
+ <div id="user-nickname"></div>
494
+ <form class="username-form" id="username-form">
495
+ <input type="text" id="username-input" class="username-input" placeholder="Установить никнейм" autocomplete="off">
496
+ <button type="submit" class="action-btn small">✓</button>
497
+ </form>
498
+ </div>
499
+ </div>
500
+ <div id="chatroom-list"></div>
501
+ </div>
502
+
503
+ <div id="chat-window-view" class="hidden-on-desktop">
504
+ <div id="chat-placeholder" class="chat-placeholder">
505
+ <img src="https://ton.org/download/ton_symbol.svg" alt="TON Symbol">
506
+ <h2>Выберите чат</h2>
507
+ <p>Начните общение в одном из существующих чатов или создайте свой собственный.</p>
508
+ </div>
509
+ <div id="active-chat" style="display: none; width: 100%; height: 100%; flex-direction: column;">
510
+ <div class="chat-header">
511
+ <button class="back-btn" id="back-to-list-btn">
512
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
513
+ </button>
514
+ <div id="chat-header-avatar" class="avatar"></div>
515
+ <span id="chat-header-title"></span>
516
+ </div>
517
+ <div id="messages-container"></div>
518
+ <form id="message-form" class="message-form">
519
+ <input type="text" id="message-input" placeholder="Сообщение..." autocomplete="off">
520
+ <button type="submit" class="action-btn send-btn" id="send-btn">
521
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
522
+ </button>
523
+ </form>
524
+ </div>
525
+ </div>
526
+ </div>
527
+
528
+ <div id="create-room-modal" class="modal-overlay">
529
+ <div class="modal-content">
530
+ <h3>Создать новый чат</h3>
531
+ <form id="create-room-form">
532
+ <label for="room-name">Название чата</label>
533
+ <input type="text" id="room-name" required>
534
+ <label for="room-password">Пароль (оставьте пустым для открытого)</label>
535
+ <input type="password" id="room-password">
536
+ <div class="modal-actions">
537
+ <button type="button" id="create-room-cancel" class="modal-btn secondary-btn">Отмена</button>
538
+ <button type="submit" class="modal-btn action-btn">Создать</button>
539
+ </div>
540
+ </form>
541
+ </div>
542
+ </div>
543
+
544
+ <div id="password-modal" class="modal-overlay">
545
+ <div class="modal-content">
546
+ <h3>Вход в приватный чат</h3>
547
+ <form id="password-form">
548
+ <label for="password-input">Введите пароль</label>
549
+ <input type="password" id="password-input" required>
550
+ <div class="modal-actions">
551
+ <button type="button" id="password-cancel" class="modal-btn secondary-btn">Отмена</button>
552
+ <button type="submit" class="modal-btn action-btn">Войти</button>
553
+ </div>
554
+ </form>
555
+ </div>
556
+ </div>
557
+
558
+ <div id="profile-modal" class="modal-overlay">
559
+ <div class="modal-content" style="text-align: center;">
560
+ <h3 id="profile-modal-title">Профиль пользователя</h3>
561
+ <div id="profile-avatar-container" style="margin: 20px auto; display: inline-block;"></div>
562
+ <p id="profile-username" style="font-size: 1.2rem; font-weight: 600;"></p>
563
+ <p id="profile-address" style="color: var(--text-secondary); font-size: 0.9rem; word-break: break-all; margin-top: 8px;"></p>
564
+ <div id="profile-qr-code" style="background: white; padding: 10px; margin: 20px auto; width: fit-content; border-radius: 8px;"></div>
565
+ <p style="text-align: center; color: var(--text-secondary); font-size: 0.8rem; margin-top: -10px; margin-bottom: 20px;">Отсканируйте для открытия профиля</p>
566
+ <div class="modal-actions" style="flex-direction: column; gap: 12px; align-items: stretch;">
567
+ <button id="send-ton-btn" class="modal-btn action-btn">Отправить TON</button>
568
+ <button id="profile-close-btn" class="modal-btn secondary-btn">Закрыть</button>
569
+ </div>
570
+ </div>
571
+ </div>
572
+
573
+ <div id="scanner-modal" class="modal-overlay">
574
+ <div class="modal-content">
575
+ <h3>Сканировать QR-код</h3>
576
+ <div id="qr-reader" style="width: 100%; border: 1px solid var(--border-color); margin-top: 16px; border-radius: 8px; overflow: hidden;"></div>
577
+ <div class="modal-actions">
578
+ <button id="scanner-close-btn" class="modal-btn secondary-btn">Отмена</button>
579
+ </div>
580
+ </div>
581
+ </div>
582
+
583
+ <div id="status-bar"></div>
584
+
585
+ <script>
586
+ document.addEventListener('DOMContentLoaded', () => {
587
+ const tonConnectUI = new TON_CONNECT_UI.TonConnectUI({
588
+ manifestUrl: 'https://huggingface.co/spaces/Aleksmorshen/MorshenGroup/resolve/main/tonconnect-manifest.json',
589
+ buttonRootId: 'ton-connect-button'
590
+ });
591
+
592
+ let currentUser = { address: null, username: null };
593
+ let activeChatroomId = null;
594
+ let messagePollingInterval = null;
595
+ let chatroomsData = {};
596
+ let html5QrCode = null;
597
+ let profileQrCode = null;
598
+
599
+ const loginView = document.getElementById('login-view');
600
+ const appView = document.getElementById('app-view');
601
+ const chatroomListView = document.getElementById('chatroom-list-view');
602
+ const chatWindowView = document.getElementById('chat-window-view');
603
+ const chatPlaceholder = document.getElementById('chat-placeholder');
604
+ const activeChat = document.getElementById('active-chat');
605
+ const profileModal = document.getElementById('profile-modal');
606
+ const scannerModal = document.getElementById('scanner-modal');
607
+
608
+ const AVATAR_COLORS = ['#e57373', '#81c784', '#64b5f6', '#ffb74d', '#9575cd', '#4db6ac', '#f06292'];
609
+
610
+ const getAvatar = (name) => {
611
+ const initial = (name ? name[0] : '?').toUpperCase();
612
+ const charCode = initial.charCodeAt(0);
613
+ const color = AVATAR_COLORS[charCode % AVATAR_COLORS.length];
614
+ const avatar = document.createElement('div');
615
+ avatar.className = 'avatar';
616
+ avatar.style.backgroundColor = color;
617
+ avatar.textContent = initial;
618
+ return avatar;
619
+ };
620
+
621
+ const showStatus = (message, type = 'info', duration = 3000) => {
622
+ const statusBar = document.getElementById('status-bar');
623
+ statusBar.textContent = message;
624
+ statusBar.className = 'status-bar';
625
+ if (type === 'success') statusBar.classList.add('success');
626
+ else if (type === 'error') statusBar.classList.add('error');
627
+
628
+ statusBar.classList.add('visible');
629
+ setTimeout(() => statusBar.classList.remove('visible'), duration);
630
+ };
631
+
632
+ const apiCall = async (endpoint, options = {}) => {
633
+ try {
634
+ const response = await fetch(endpoint, options);
635
+ if (!response.ok) {
636
+ const errorData = await response.json().catch(() => ({ error: 'Request failed with status ' + response.status }));
637
+ throw new Error(errorData.error || 'Unknown error');
638
+ }
639
+ if (response.status === 204) return null;
640
+ return await response.json();
641
+ } catch (error) {
642
+ showStatus(`Ошибка: ${error.message}`, 'error');
643
+ throw error;
644
+ }
645
+ };
646
+
647
+ const truncateAddress = (address) => address ? `${address.substring(0, 4)}...${address.substring(address.length - 4)}` : '';
648
+
649
+ const updateUserInfo = () => {
650
+ document.getElementById('user-wallet').textContent = `Кошелек: ${truncateAddress(currentUser.address)}`;
651
+ const nicknameEl = document.getElementById('user-nickname');
652
+ const usernameInput = document.getElementById('username-input');
653
+ nicknameEl.textContent = currentUser.username ? `Ник: ${currentUser.username}` : `Никнейм не установлен`;
654
+ usernameInput.value = currentUser.username || '';
655
+ };
656
+
657
+ document.getElementById('username-form').addEventListener('submit', async (e) => {
658
+ e.preventDefault();
659
+ const newUsername = document.getElementById('username-input').value.trim();
660
+ if (!newUsername || newUsername.length < 3) {
661
+ showStatus('Никнейм должен быть не короче 3 символов.', 'error');
662
+ return;
663
+ }
664
+ try {
665
+ await apiCall('/api/set_username', {
666
+ method: 'POST',
667
+ headers: { 'Content-Type': 'application/json' },
668
+ body: JSON.stringify({ address: currentUser.address, username: newUsername })
669
+ });
670
+ currentUser.username = newUsername;
671
+ updateUserInfo();
672
+ showStatus('Никнейм успешно обновлен!', 'success');
673
+ fetchChatrooms();
674
+ if (activeChatroomId) fetchMessages(activeChatroomId);
675
+ } catch (err) {}
676
+ });
677
+
678
+ const initializeUser = async (address) => {
679
+ currentUser.address = address;
680
+ try {
681
+ const data = await apiCall('/api/user_data', {
682
+ method: 'POST',
683
+ headers: { 'Content-Type': 'application/json' },
684
+ body: JSON.stringify({ address: currentUser.address })
685
+ });
686
+ currentUser.username = data.username;
687
+ } catch (err) {
688
+ currentUser.username = null;
689
+ }
690
+ updateUserInfo();
691
+ loginView.style.display = 'none';
692
+ appView.style.display = 'flex';
693
+ fetchChatrooms();
694
+ };
695
+
696
+ const renderChatrooms = (rooms) => {
697
+ const list = document.getElementById('chatroom-list');
698
+ list.innerHTML = '';
699
+ chatroomsData = {};
700
+ rooms.forEach(room => {
701
+ chatroomsData[room.id] = room;
702
+ const item = document.createElement('div');
703
+ item.className = 'chatroom-item';
704
+ item.dataset.id = room.id;
705
+
706
+ item.appendChild(getAvatar(room.name));
707
+
708
+ const infoDiv = document.createElement('div');
709
+ infoDiv.className = 'chatroom-info';
710
+ const nameSpan = document.createElement('div');
711
+ nameSpan.className = 'chatroom-name';
712
+ nameSpan.textContent = room.name;
713
+ infoDiv.appendChild(nameSpan);
714
+ item.appendChild(infoDiv);
715
+
716
+ if (room.is_private) {
717
+ const lockIcon = document.createElement('div');
718
+ lockIcon.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>`;
719
+ item.appendChild(lockIcon);
720
+ }
721
+
722
+ item.addEventListener('click', () => selectChatroom(room.id, room.is_private));
723
+ list.appendChild(item);
724
+ });
725
+ };
726
+
727
+ const fetchChatrooms = async () => {
728
+ try {
729
+ const data = await apiCall('/api/chatrooms');
730
+ renderChatrooms(data.chatrooms);
731
+ } catch (err) {}
732
+ };
733
+
734
+ const renderMessages = (messages) => {
735
+ const container = document.getElementById('messages-container');
736
+ const shouldScroll = container.scrollTop + container.clientHeight >= container.scrollHeight - 30;
737
+ container.innerHTML = '';
738
+ messages.forEach(msg => {
739
+ const msgDiv = document.createElement('div');
740
+ msgDiv.className = 'message ' + (msg.sender_address === currentUser.address ? 'sent' : 'received');
741
+
742
+ const avatar = getAvatar(msg.display_name);
743
+ avatar.classList.add('message-avatar');
744
+ avatar.style.cursor = 'pointer';
745
+ avatar.onclick = () => showProfile(msg.sender_address);
746
+
747
+ const contentDiv = document.createElement('div');
748
+ contentDiv.className = 'message-content';
749
+
750
+ const senderDiv = document.createElement('div');
751
+ senderDiv.className = 'message-sender';
752
+ senderDiv.textContent = msg.display_name;
753
+ senderDiv.onclick = () => showProfile(msg.sender_address);
754
+
755
+ const bubbleDiv = document.createElement('div');
756
+ bubbleDiv.className = 'message-bubble';
757
+ bubbleDiv.textContent = msg.text;
758
+
759
+ contentDiv.appendChild(senderDiv);
760
+ contentDiv.appendChild(bubbleDiv);
761
+
762
+ msgDiv.appendChild(contentDiv);
763
+ msgDiv.insertBefore(avatar, contentDiv);
764
+ container.appendChild(msgDiv);
765
+ });
766
+
767
+ if(shouldScroll) {
768
+ container.scrollTop = container.scrollHeight;
769
+ }
770
+ };
771
+
772
+ const fetchMessages = async (roomId) => {
773
+ try {
774
+ const data = await apiCall(`/api/messages/${roomId}`);
775
+ renderMessages(data.messages);
776
+ } catch (err) {
777
+ if (messagePollingInterval) clearInterval(messagePollingInterval);
778
+ }
779
+ };
780
+
781
+ const showChatView = () => {
782
+ chatWindowView.style.display = 'flex';
783
+ if (window.innerWidth < 768) {
784
+ chatroomListView.style.display = 'none';
785
+ }
786
+ };
787
+
788
+ const showListView = () => {
789
+ chatroomListView.style.display = 'flex';
790
+ if (window.innerWidth < 768) {
791
+ chatWindowView.style.display = 'none';
792
+ }
793
+ };
794
+
795
+ const selectChatroom = (roomId, isPrivate) => {
796
+ const roomData = chatroomsData[roomId];
797
+ if (!roomData) return;
798
+
799
+ const proceedToRoom = () => {
800
+ if (messagePollingInterval) clearInterval(messagePollingInterval);
801
+ activeChatroomId = roomId;
802
+
803
+ document.getElementById('chat-header-title').textContent = roomData.name;
804
+ const headerAvatar = document.getElementById('chat-header-avatar');
805
+ headerAvatar.innerHTML = '';
806
+ headerAvatar.appendChild(getAvatar(roomData.name));
807
+
808
+ chatPlaceholder.style.display = 'none';
809
+ activeChat.style.display = 'flex';
810
+ showChatView();
811
+
812
+ fetchMessages(roomId);
813
+ messagePollingInterval = setInterval(() => fetchMessages(roomId), 3000);
814
+ };
815
+
816
+ if (isPrivate) {
817
+ const passwordModal = document.getElementById('password-modal');
818
+ const passwordForm = document.getElementById('password-form');
819
+ const passwordInput = document.getElementById('password-input');
820
+ passwordModal.style.display = 'flex';
821
+ passwordInput.value = '';
822
+ passwordInput.focus();
823
+
824
+ const formSubmitHandler = async (e) => {
825
+ e.preventDefault();
826
+ passwordForm.removeEventListener('submit', formSubmitHandler);
827
+ const password = passwordInput.value;
828
+ passwordModal.style.display = 'none';
829
+ try {
830
+ await apiCall('/api/join_chatroom', {
831
+ method: 'POST',
832
+ headers: { 'Content-Type': 'application/json' },
833
+ body: JSON.stringify({ chatroom_id: roomId, password })
834
+ });
835
+ proceedToRoom();
836
+ } catch (err) {}
837
+ };
838
+ passwordForm.addEventListener('submit', formSubmitHandler);
839
+
840
+ document.getElementById('password-cancel').onclick = () => {
841
+ passwordModal.style.display = 'none';
842
+ passwordForm.removeEventListener('submit', formSubmitHandler);
843
+ };
844
+ } else {
845
+ proceedToRoom();
846
+ }
847
+ };
848
+
849
+ document.getElementById('message-form').addEventListener('submit', async (e) => {
850
+ e.preventDefault();
851
+ const input = document.getElementById('message-input');
852
+ const sendBtn = document.getElementById('send-btn');
853
+ const text = input.value.trim();
854
+ if (text && activeChatroomId) {
855
+ input.value = '';
856
+ input.disabled = true;
857
+ sendBtn.disabled = true;
858
+ try {
859
+ await apiCall('/api/send_message', {
860
+ method: 'POST',
861
+ headers: { 'Content-Type': 'application/json' },
862
+ body: JSON.stringify({
863
+ chatroom_id: activeChatroomId,
864
+ sender_address: currentUser.address,
865
+ text: text
866
+ })
867
+ });
868
+ await fetchMessages(activeChatroomId);
869
+ document.getElementById('messages-container').scrollTop = document.getElementById('messages-container').scrollHeight;
870
+ } finally {
871
+ input.disabled = false;
872
+ sendBtn.disabled = false;
873
+ input.focus();
874
+ }
875
+ }
876
+ });
877
+
878
+ const createRoomModal = document.getElementById('create-room-modal');
879
+ document.getElementById('create-room-show-modal').addEventListener('click', () => {
880
+ createRoomModal.style.display = 'flex';
881
+ document.getElementById('create-room-form').reset();
882
+ });
883
+ document.getElementById('create-room-cancel').addEventListener('click', () => {
884
+ createRoomModal.style.display = 'none';
885
+ });
886
+ document.getElementById('create-room-form').addEventListener('submit', async (e) => {
887
+ e.preventDefault();
888
+ const name = document.getElementById('room-name').value.trim();
889
+ const password = document.getElementById('room-password').value;
890
+ if (!name) return;
891
+
892
+ try {
893
+ await apiCall('/api/create_chatroom', {
894
+ method: 'POST',
895
+ headers: { 'Content-Type': 'application/json' },
896
+ body: JSON.stringify({ name, password: password || null, creator_address: currentUser.address })
897
+ });
898
+ createRoomModal.style.display = 'none';
899
+ showStatus('Чат успешно создан!', 'success');
900
+ fetchChatrooms();
901
+ } catch (err) {}
902
+ });
903
+
904
+ const showProfile = async (address) => {
905
+ try {
906
+ const userData = await apiCall('/api/user_data', {
907
+ method: 'POST',
908
+ headers: { 'Content-Type': 'application/json' },
909
+ body: JSON.stringify({ address: address })
910
+ });
911
+
912
+ const username = userData.username || `User ${truncateAddress(address)}`;
913
+ const avatarContainer = document.getElementById('profile-avatar-container');
914
+ const usernameEl = document.getElementById('profile-username');
915
+ const addressEl = document.getElementById('profile-address');
916
+ const qrCodeEl = document.getElementById('profile-qr-code');
917
+ const sendTonBtn = document.getElementById('send-ton-btn');
918
+
919
+ avatarContainer.innerHTML = '';
920
+ avatarContainer.appendChild(getAvatar(username));
921
+
922
+ usernameEl.textContent = username;
923
+ addressEl.textContent = address;
924
+
925
+ qrCodeEl.innerHTML = '';
926
+ if (profileQrCode) {
927
+ profileQrCode.clear();
928
+ }
929
+ profileQrCode = new QRCode(qrCodeEl, {
930
+ text: address,
931
+ width: 150,
932
+ height: 150,
933
+ colorDark : "#000000",
934
+ colorLight : "#ffffff",
935
+ correctLevel : QRCode.CorrectLevel.H
936
+ });
937
+
938
+ sendTonBtn.onclick = async () => {
939
+ if (!tonConnectUI.connected) {
940
+ showStatus('Подключите кошелек для отправки TON.', 'error');
941
+ return;
942
+ }
943
+ const amountString = prompt("Введите сумму в TON для отправки:", "0.1");
944
+ if (amountString === null) return;
945
+
946
+ const amount = parseFloat(amountString);
947
+ if (isNaN(amount) || amount <= 0) {
948
+ showStatus('Неверная сумма.', 'error');
949
+ return;
950
+ }
951
+
952
+ const amountInNanoTon = Math.floor(amount * 1_000_000_000).toString();
953
+
954
+ const transaction = {
955
+ validUntil: Math.floor(Date.now() / 1000) + 600,
956
+ messages: [ { address: address, amount: amountInNanoTon } ]
957
+ };
958
+
959
+ try {
960
+ await tonConnectUI.sendTransaction(transaction);
961
+ showStatus(`Транзакция отправлена успешно!`, 'success');
962
+ profileModal.style.display = 'none';
963
+ } catch (error) {
964
+ showStatus('Транзакция отклонена.', 'error');
965
+ }
966
+ };
967
+
968
+ sendTonBtn.style.display = (address === currentUser.address) ? 'none' : 'block';
969
+ profileModal.style.display = 'flex';
970
+ } catch (err) {
971
+ showStatus('Не удалось загрузить профиль.', 'error');
972
+ }
973
+ };
974
+
975
+ const showScanner = () => {
976
+ scannerModal.style.display = 'flex';
977
+ html5QrCode = new Html5Qrcode("qr-reader");
978
+ const qrCodeSuccessCallback = (decodedText, decodedResult) => {
979
+ hideScanner();
980
+ if (decodedText && decodedText.length > 40 && (decodedText.startsWith('EQ') || decodedText.startsWith('UQ'))) {
981
+ showProfile(decodedText);
982
+ } else {
983
+ showStatus('Отсканирован недействительный QR-код.', 'error');
984
+ }
985
+ };
986
+ const config = { fps: 10, qrbox: { width: 250, height: 250 } };
987
+ html5QrCode.start({ facingMode: "environment" }, config, qrCodeSuccessCallback)
988
+ .catch(err => {
989
+ showStatus('Не удалось запустить сканер.', 'error');
990
+ hideScanner();
991
+ });
992
+ };
993
+
994
+ const hideScanner = () => {
995
+ if (html5QrCode && html5QrCode.isScanning) {
996
+ html5QrCode.stop().catch(err => console.error("Failed to stop QR scanner.", err));
997
+ }
998
+ scannerModal.style.display = 'none';
999
+ };
1000
+
1001
+ document.getElementById('my-profile-btn').addEventListener('click', () => {
1002
+ if (currentUser.address) showProfile(currentUser.address);
1003
+ });
1004
+ document.getElementById('scan-qr-btn').addEventListener('click', showScanner);
1005
+ document.getElementById('profile-close-btn').addEventListener('click', () => profileModal.style.display = 'none');
1006
+ document.getElementById('scanner-close-btn').addEventListener('click', hideScanner);
1007
+ document.getElementById('back-to-list-btn').addEventListener('click', showListView);
1008
+
1009
+ const handleResize = () => {
1010
+ const isMobile = window.innerWidth < 768;
1011
+ document.getElementById('back-to-list-btn').style.display = isMobile ? 'block' : 'none';
1012
+ if (!isMobile) {
1013
+ chatroomListView.style.display = 'flex';
1014
+ chatWindowView.style.display = 'flex';
1015
+ } else {
1016
+ if (activeChat.style.display === 'flex') {
1017
+ chatroomListView.style.display = 'none';
1018
+ } else {
1019
+ chatWindowView.style.display = 'none';
1020
+ }
1021
+ }
1022
+ };
1023
+
1024
+ window.addEventListener('resize', handleResize);
1025
+ handleResize();
1026
+
1027
+ tonConnectUI.onStatusChange(wallet => {
1028
+ if (wallet) {
1029
+ const address = TON_CONNECT_UI.toUserFriendlyAddress(wallet.account.address, false);
1030
+ initializeUser(address);
1031
+ } else {
1032
+ currentUser = { address: null, username: null };
1033
+ appView.style.display = 'none';
1034
+ loginView.style.display = 'flex';
1035
+ if (messagePollingInterval) clearInterval(messagePollingInterval);
1036
+ activeChatroomId = null;
1037
+ }
1038
+ });
1039
+ });
1040
+ </script>
1041
+ </body>
1042
+ </html>
1043
+ '''
1044
+ return Response(html_content, mimetype='text/html')
1045
+
1046
+
1047
+ @app.route('/api/user_data', methods=['POST'])
1048
+ def get_user_data():
1049
+ data = request.get_json()
1050
+ address = data.get('address')
1051
+ if not address:
1052
+ return jsonify({'error': 'Address is required'}), 400
1053
+ db = read_db()
1054
+ user_info = db['users'].get(address)
1055
+ username = user_info.get('username') if user_info else None
1056
+ return jsonify({'username': username})
1057
+
1058
+ @app.route('/api/set_username', methods=['POST'])
1059
+ def set_username():
1060
+ data = request.get_json()
1061
+ address = data.get('address')
1062
+ username = data.get('username')
1063
+ if not address or not username:
1064
+ return jsonify({'error': 'Address and username are required'}), 400
1065
+ if len(username) < 3 or len(username) > 20:
1066
+ return jsonify({'error': 'Username must be between 3 and 20 characters'}), 400
1067
+
1068
+ db = read_db()
1069
+ if address not in db['users']:
1070
+ db['users'][address] = {}
1071
+ db['users'][address]['username'] = username
1072
+ write_db(db)
1073
+ return jsonify({'success': True})
1074
+
1075
+ @app.route('/api/chatrooms', methods=['GET'])
1076
+ def get_chatrooms():
1077
+ db = read_db()
1078
+ chatrooms_list = []
1079
+ for room_id, room_data in db['chatrooms'].items():
1080
+ chatrooms_list.append({
1081
+ 'id': room_id,
1082
+ 'name': room_data['name'],
1083
+ 'is_private': room_data['is_private']
1084
+ })
1085
+ return jsonify({'chatrooms': sorted(chatrooms_list, key=lambda x: x['name'])})
1086
+
1087
+ @app.route('/api/create_chatroom', methods=['POST'])
1088
+ def create_chatroom():
1089
+ data = request.get_json()
1090
+ name = data.get('name')
1091
+ password = data.get('password')
1092
+ creator_address = data.get('creator_address')
1093
+ if not name or not creator_address:
1094
+ return jsonify({'error': 'Name and creator address are required'}), 400
1095
+
1096
+ db = read_db()
1097
+ room_id = str(uuid.uuid4())
1098
+ db['chatrooms'][room_id] = {
1099
+ 'name': name,
1100
+ 'creator': creator_address,
1101
+ 'is_private': bool(password),
1102
+ 'password_hash': generate_password_hash(password) if password else None
1103
+ }
1104
+ db['messages'][room_id] = []
1105
+ write_db(db)
1106
+ return jsonify({'success': True, 'chatroom_id': room_id})
1107
+
1108
+ @app.route('/api/join_chatroom', methods=['POST'])
1109
+ def join_chatroom():
1110
+ data = request.get_json()
1111
+ chatroom_id = data.get('chatroom_id')
1112
+ password = data.get('password')
1113
+ db = read_db()
1114
+ chatroom = db['chatrooms'].get(chatroom_id)
1115
+ if not chatroom:
1116
+ return jsonify({'error': 'Chatroom not found'}), 404
1117
+ if chatroom['is_private']:
1118
+ if not password or not check_password_hash(chatroom['password_hash'], password):
1119
+ return jsonify({'error': 'Invalid password'}), 403
1120
+ return jsonify({'success': True})
1121
+
1122
+ @app.route('/api/messages/<chatroom_id>', methods=['GET'])
1123
+ def get_messages(chatroom_id):
1124
+ db = read_db()
1125
+ if chatroom_id not in db['messages']:
1126
+ return jsonify({'error': 'Chatroom not found'}), 404
1127
+
1128
+ messages_with_names = []
1129
+ room_messages = db['messages'].get(chatroom_id, [])
1130
+
1131
+ for msg in room_messages:
1132
+ sender_address = msg['sender_address']
1133
+ user_info = db['users'].get(sender_address)
1134
+ display_name = (user_info.get('username') if user_info and user_info.get('username')
1135
+ else f"{sender_address[:4]}...{sender_address[-4:]}")
1136
+
1137
+ msg_copy = msg.copy()
1138
+ msg_copy['display_name'] = display_name
1139
+ messages_with_names.append(msg_copy)
1140
+
1141
+ return jsonify({'messages': messages_with_names})
1142
+
1143
+ @app.route('/api/send_message', methods=['POST'])
1144
+ def send_message():
1145
+ data = request.get_json()
1146
+ chatroom_id = data.get('chatroom_id')
1147
+ sender_address = data.get('sender_address')
1148
+ text = data.get('text')
1149
+
1150
+ if not all([chatroom_id, sender_address, text]):
1151
+ return jsonify({'error': 'Missing data'}), 400
1152
+
1153
+ db = read_db()
1154
+ if chatroom_id not in db['messages']:
1155
+ return jsonify({'error': 'Chatroom not found'}), 404
1156
+
1157
+ message = {
1158
+ 'id': str(uuid.uuid4()),
1159
+ 'sender_address': sender_address,
1160
+ 'text': text,
1161
+ 'timestamp': datetime.utcnow().isoformat() + "Z"
1162
+ }
1163
+
1164
+ if len(db['messages'][chatroom_id]) >= 100:
1165
+ db['messages'][chatroom_id].pop(0)
1166
+
1167
+ db['messages'][chatroom_id].append(message)
1168
+ write_db(db)
1169
+ return jsonify({'success': True})
1170
+
1171
+
1172
+ if __name__ == '__main__':
1173
+ init_db()
1174
+ app.run(host='0.0.0.0', port=7860)