X commited on
Commit
b83bc51
·
verified ·
1 Parent(s): 9ddaadb

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +692 -0
app.py ADDED
@@ -0,0 +1,692 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import uuid
3
+ import os
4
+ import subprocess
5
+ import threading
6
+ import time
7
+ import socket
8
+ from pathlib import Path
9
+ from fastapi import FastAPI, Request, Form
10
+ from fastapi.responses import HTMLResponse, JSONResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+ from fastapi.templating import Jinja2Templates
13
+ import uvicorn
14
+
15
+ # ========== КОНФИГ ==========
16
+ PORT = int(os.getenv("PORT", 7860))
17
+ DATA_DIR = Path("/data")
18
+ DATA_DIR.mkdir(exist_ok=True)
19
+
20
+ INVITES_FILE = DATA_DIR / "invites.json"
21
+ PEERS_FILE = DATA_DIR / "peers.json"
22
+
23
+ # Создаем файлы если их нет
24
+ if not INVITES_FILE.exists():
25
+ with open(INVITES_FILE, "w") as f:
26
+ json.dump({}, f)
27
+
28
+ if not PEERS_FILE.exists():
29
+ with open(PEERS_FILE, "w") as f:
30
+ json.dump({}, f)
31
+
32
+ # ========== РАБОТА С ДАННЫМИ ==========
33
+ def load_invites():
34
+ with open(INVITES_FILE, "r") as f:
35
+ return json.load(f)
36
+
37
+ def save_invites(invites):
38
+ with open(INVITES_FILE, "w") as f:
39
+ json.dump(invites, f, indent=2)
40
+
41
+ def load_peers():
42
+ with open(PEERS_FILE, "r") as f:
43
+ return json.load(f)
44
+
45
+ def save_peers(peers):
46
+ with open(PEERS_FILE, "w") as f:
47
+ json.dump(peers, f, indent=2)
48
+
49
+ # ========== ЗАПУСК PEERJS SERVER ==========
50
+ def start_peerjs():
51
+ """Запускает PeerJS Server в фоновом процессе"""
52
+ try:
53
+ # Проверяем, запущен ли уже PeerJS
54
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
55
+ result = sock.connect_ex(('127.0.0.1', 9000))
56
+ sock.close()
57
+
58
+ if result != 0:
59
+ # Порт 9000 свободен - запускаем PeerJS
60
+ print("🚀 Запускаем PeerJS Server...")
61
+ subprocess.Popen(
62
+ ["peer", "--port", "9000", "--path", "/peerjs"],
63
+ stdout=subprocess.DEVNULL,
64
+ stderr=subprocess.DEVNULL,
65
+ start_new_session=True
66
+ )
67
+ time.sleep(2) # Даем время запуститься
68
+ print("✅ PeerJS Server запущен на порту 9000")
69
+ except Exception as e:
70
+ print(f"⚠️ Ошибка запуска PeerJS: {e}")
71
+
72
+ # Запускаем PeerJS в фоне (при старте приложения)
73
+ threading.Thread(target=start_peerjs, daemon=True).start()
74
+
75
+ # ========== FASTAPI ==========
76
+ app = FastAPI()
77
+
78
+ # ========== HTML СТРАНИЦА ==========
79
+ @app.get("/", response_class=HTMLResponse)
80
+ async def index(request: Request):
81
+ html = """
82
+ <!DOCTYPE html>
83
+ <html>
84
+ <head>
85
+ <meta charset="UTF-8">
86
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
87
+ <title>💬 HF Message</title>
88
+ <script src="https://unpkg.com/peerjs@1.5.1/dist/peerjs.min.js"></script>
89
+ <style>
90
+ * {
91
+ margin: 0;
92
+ padding: 0;
93
+ box-sizing: border-box;
94
+ }
95
+ body {
96
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
97
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
98
+ min-height: 100vh;
99
+ display: flex;
100
+ justify-content: center;
101
+ align-items: center;
102
+ padding: 20px;
103
+ }
104
+ .container {
105
+ background: white;
106
+ border-radius: 20px;
107
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
108
+ max-width: 600px;
109
+ width: 100%;
110
+ padding: 30px;
111
+ }
112
+ h1 {
113
+ color: #333;
114
+ text-align: center;
115
+ margin-bottom: 5px;
116
+ font-size: 28px;
117
+ }
118
+ .subtitle {
119
+ text-align: center;
120
+ color: #6c757d;
121
+ font-size: 14px;
122
+ margin-bottom: 20px;
123
+ }
124
+ .section {
125
+ background: #f8f9fa;
126
+ border-radius: 12px;
127
+ padding: 20px;
128
+ margin-bottom: 20px;
129
+ }
130
+ .section h3 {
131
+ color: #495057;
132
+ margin-bottom: 15px;
133
+ font-size: 16px;
134
+ }
135
+ input {
136
+ width: 100%;
137
+ padding: 12px;
138
+ border: 2px solid #dee2e6;
139
+ border-radius: 8px;
140
+ font-size: 14px;
141
+ margin-bottom: 10px;
142
+ transition: border-color 0.3s;
143
+ }
144
+ input:focus {
145
+ outline: none;
146
+ border-color: #667eea;
147
+ }
148
+ button {
149
+ width: 100%;
150
+ padding: 12px;
151
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
152
+ color: white;
153
+ border: none;
154
+ border-radius: 8px;
155
+ font-size: 16px;
156
+ font-weight: 600;
157
+ cursor: pointer;
158
+ transition: transform 0.2s, box-shadow 0.2s;
159
+ }
160
+ button:hover {
161
+ transform: translateY(-2px);
162
+ box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
163
+ }
164
+ button:active {
165
+ transform: translateY(0);
166
+ }
167
+ .code-display {
168
+ background: #2d3748;
169
+ color: #f7fafc;
170
+ padding: 15px;
171
+ border-radius: 8px;
172
+ font-family: 'Courier New', monospace;
173
+ font-size: 24px;
174
+ text-align: center;
175
+ letter-spacing: 3px;
176
+ word-break: break-all;
177
+ margin: 10px 0;
178
+ }
179
+ .status {
180
+ padding: 10px;
181
+ border-radius: 8px;
182
+ margin-top: 10px;
183
+ font-size: 14px;
184
+ }
185
+ .status.success {
186
+ background: #d4edda;
187
+ color: #155724;
188
+ }
189
+ .status.error {
190
+ background: #f8d7da;
191
+ color: #721c24;
192
+ }
193
+ .status.info {
194
+ background: #d1ecf1;
195
+ color: #0c5460;
196
+ }
197
+ .chat-box {
198
+ border: 2px solid #dee2e6;
199
+ border-radius: 8px;
200
+ height: 300px;
201
+ overflow-y: auto;
202
+ padding: 15px;
203
+ background: white;
204
+ margin-bottom: 10px;
205
+ display: none;
206
+ }
207
+ .chat-box.active {
208
+ display: block;
209
+ }
210
+ .message {
211
+ margin-bottom: 8px;
212
+ padding: 8px 12px;
213
+ border-radius: 8px;
214
+ max-width: 80%;
215
+ word-wrap: break-word;
216
+ }
217
+ .message.sent {
218
+ background: #667eea;
219
+ color: white;
220
+ margin-left: auto;
221
+ }
222
+ .message.received {
223
+ background: #e9ecef;
224
+ color: #333;
225
+ margin-right: auto;
226
+ }
227
+ .message.system {
228
+ background: #fff3cd;
229
+ color: #856404;
230
+ text-align: center;
231
+ font-style: italic;
232
+ max-width: 100%;
233
+ }
234
+ .chat-input {
235
+ display: none;
236
+ gap: 10px;
237
+ }
238
+ .chat-input.active {
239
+ display: flex;
240
+ }
241
+ .chat-input input {
242
+ flex: 1;
243
+ margin-bottom: 0;
244
+ }
245
+ .chat-input button {
246
+ width: auto;
247
+ padding: 12px 24px;
248
+ white-space: nowrap;
249
+ }
250
+ .peer-id {
251
+ font-size: 12px;
252
+ color: #6c757d;
253
+ text-align: center;
254
+ margin-top: 10px;
255
+ word-break: break-all;
256
+ }
257
+ .hidden {
258
+ display: none;
259
+ }
260
+ .btn-copy {
261
+ background: #28a745 !important;
262
+ margin-top: 10px;
263
+ }
264
+ .btn-copy:hover {
265
+ background: #218838 !important;
266
+ box-shadow: 0 5px 20px rgba(40, 167, 69, 0.4) !important;
267
+ }
268
+ .btn-danger {
269
+ background: #dc3545 !important;
270
+ margin-top: 10px;
271
+ }
272
+ .btn-danger:hover {
273
+ background: #c82333 !important;
274
+ box-shadow: 0 5px 20px rgba(220, 53, 69, 0.4) !important;
275
+ }
276
+ .stats {
277
+ text-align: center;
278
+ font-size: 12px;
279
+ color: #6c757d;
280
+ margin-top: 15px;
281
+ padding-top: 15px;
282
+ border-top: 1px solid #dee2e6;
283
+ }
284
+ .stats span {
285
+ font-weight: 600;
286
+ color: #333;
287
+ }
288
+ @media (max-width: 500px) {
289
+ .container {
290
+ padding: 20px;
291
+ }
292
+ .code-display {
293
+ font-size: 18px;
294
+ letter-spacing: 2px;
295
+ }
296
+ }
297
+ </style>
298
+ </head>
299
+ <body>
300
+ <div class="container">
301
+ <h1>💬 HF Message</h1>
302
+ <div class="subtitle">Peer‑to‑Peer чат с инвайт‑кодами</div>
303
+
304
+ <!-- Секция 1: Инвайт-код -->
305
+ <div class="section" id="invite-section">
306
+ <h3>🎫 Создать инвайт‑код</h3>
307
+ <button onclick="generateInvite()">Сгенерировать код</button>
308
+ <div id="invite-result" class="hidden">
309
+ <div class="code-display" id="invite-code"></div>
310
+ <p style="font-size:12px;color:#6c757d;text-align:center;margin:5px 0;">
311
+ Код сохраняется в /data и не пропадает после перезапуска
312
+ </p>
313
+ <button class="btn-copy" onclick="copyInvite()">📋 Копировать код</button>
314
+ </div>
315
+ <div id="invite-status"></div>
316
+ </div>
317
+
318
+ <!-- Секция 2: Вход по коду -->
319
+ <div class="section" id="join-section">
320
+ <h3>🔑 Войти по коду</h3>
321
+ <input type="text" id="invite-input" placeholder="Введи инвайт‑код друга" style="text-transform:uppercase;">
322
+ <button onclick="joinWithInvite()">Подключиться</button>
323
+ <div id="join-status"></div>
324
+ </div>
325
+
326
+ <!-- Секция 3: Чат -->
327
+ <div class="section hidden" id="chat-section">
328
+ <h3>💬 Чат с <span id="chat-peer-id" style="color:#667eea;">...</span></h3>
329
+ <div class="chat-box" id="chat-box"></div>
330
+ <div class="chat-input" id="chat-input">
331
+ <input type="text" id="message-input" placeholder="Введите сообщение..." onkeypress="if(event.key==='Enter') sendMessage()">
332
+ <button onclick="sendMessage()">Отправить</button>
333
+ </div>
334
+ <div class="peer-id" id="my-peer-id"></div>
335
+ <button class="btn-danger" onclick="disconnect()">❌ Отключиться</button>
336
+ </div>
337
+
338
+ <div class="stats">
339
+ 📊 <span id="stats-total">0</span> кодов ·
340
+ <span id="stats-used">0</span> использовано ·
341
+ <span id="stats-peers">0</span> подключений
342
+ </div>
343
+ </div>
344
+
345
+ <script>
346
+ // ========== ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ==========
347
+ let myPeer = null;
348
+ let myId = null;
349
+ let targetPeerId = null;
350
+ let connection = null;
351
+
352
+ // Загружаем статистику
353
+ async function loadStats() {
354
+ try {
355
+ const response = await fetch('/api/stats');
356
+ const data = await response.json();
357
+ document.getElementById('stats-total').textContent = data.total_invites;
358
+ document.getElementById('stats-used').textContent = data.used_invites;
359
+ document.getElementById('stats-peers').textContent = data.active_peers;
360
+ } catch (e) {
361
+ // Игнорируем
362
+ }
363
+ }
364
+ loadStats();
365
+
366
+ // ========== ГЕНЕРАЦИЯ ИНВАЙТА ==========
367
+ async function generateInvite() {
368
+ const status = document.getElementById('invite-status');
369
+ status.innerHTML = '<div class="status info">⏳ Генерация...</div>';
370
+
371
+ try {
372
+ const response = await fetch('/api/generate_invite', { method: 'POST' });
373
+ const data = await response.json();
374
+
375
+ if (data.success) {
376
+ document.getElementById('invite-code').textContent = data.code;
377
+ document.getElementById('invite-result').classList.remove('hidden');
378
+ status.innerHTML = '<div class="status success">✅ Код создан! Отправь его другу.</div>';
379
+ loadStats();
380
+ } else {
381
+ status.innerHTML = `<div class="status error">❌ ${data.error}</div>`;
382
+ }
383
+ } catch (e) {
384
+ status.innerHTML = `<div class="status error">❌ Ошибка: ${e.message}</div>`;
385
+ }
386
+ }
387
+
388
+ async function copyInvite() {
389
+ const code = document.getElementById('invite-code').textContent;
390
+ try {
391
+ await navigator.clipboard.writeText(code);
392
+ const btn = document.querySelector('.btn-copy');
393
+ const originalText = btn.textContent;
394
+ btn.textContent = '✅ Скопировано!';
395
+ setTimeout(() => { btn.textContent = originalText; }, 2000);
396
+ } catch (e) {
397
+ // fallback
398
+ const textarea = document.createElement('textarea');
399
+ textarea.value = code;
400
+ document.body.appendChild(textarea);
401
+ textarea.select();
402
+ document.execCommand('copy');
403
+ document.body.removeChild(textarea);
404
+ const btn = document.querySelector('.btn-copy');
405
+ const originalText = btn.textContent;
406
+ btn.textContent = '✅ Скопи��овано!';
407
+ setTimeout(() => { btn.textContent = originalText; }, 2000);
408
+ }
409
+ }
410
+
411
+ // ========== ВХОД ПО КОДУ ==========
412
+ async function joinWithInvite() {
413
+ const code = document.getElementById('invite-input').value.trim().toUpperCase();
414
+ const status = document.getElementById('join-status');
415
+
416
+ if (!code) {
417
+ status.innerHTML = '<div class="status error">❌ Введи код</div>';
418
+ return;
419
+ }
420
+
421
+ status.innerHTML = '<div class="status info">⏳ Проверка кода...</div>';
422
+
423
+ try {
424
+ const response = await fetch('/api/use_invite', {
425
+ method: 'POST',
426
+ headers: { 'Content-Type': 'application/json' },
427
+ body: JSON.stringify({ code: code })
428
+ });
429
+
430
+ const data = await response.json();
431
+
432
+ if (data.success) {
433
+ status.innerHTML = '<div class="status success">✅ Код принят! Подключаемся...</div>';
434
+ loadStats();
435
+ connectToPeer(data.peer_id);
436
+ } else {
437
+ status.innerHTML = `<div class="status error">❌ ${data.error}</div>`;
438
+ }
439
+ } catch (e) {
440
+ status.innerHTML = `<div class="status error">❌ Ошибка: ${e.message}</div>`;
441
+ }
442
+ }
443
+
444
+ // ========== P2P ПОДКЛЮЧЕНИЕ ==========
445
+ function connectToPeer(targetId) {
446
+ targetPeerId = targetId;
447
+
448
+ // Создаем своего Peer
449
+ const peerjsServer = `${window.location.protocol}//${window.location.host}/peerjs`;
450
+
451
+ // Генерируем уникальный ID для себя (сохраняем в localStorage)
452
+ let mySavedId = localStorage.getItem('hf_message_id');
453
+ if (!mySavedId) {
454
+ mySavedId = 'user-' + Math.random().toString(36).substring(7);
455
+ localStorage.setItem('hf_message_id', mySavedId);
456
+ }
457
+
458
+ myPeer = new Peer(mySavedId, {
459
+ host: window.location.hostname,
460
+ port: window.location.port || (window.location.protocol === 'https:' ? 443 : 80),
461
+ path: '/peerjs',
462
+ secure: window.location.protocol === 'https:'
463
+ });
464
+
465
+ myPeer.on('open', (id) => {
466
+ myId = id;
467
+ document.getElementById('my-peer-id').textContent = `Твой ID: ${id}`;
468
+ connectToTarget();
469
+ });
470
+
471
+ myPeer.on('connection', (conn) => {
472
+ handleConnection(conn);
473
+ });
474
+
475
+ myPeer.on('error', (err) => {
476
+ console.error('Peer error:', err);
477
+ document.getElementById('join-status').innerHTML =
478
+ `<div class="status error">❌ Ошибка P2P: ${err.message}</div>`;
479
+ });
480
+ }
481
+
482
+ function connectToTarget() {
483
+ if (!myPeer || !targetPeerId) return;
484
+
485
+ try {
486
+ connection = myPeer.connect(targetPeerId, {
487
+ reliable: true
488
+ });
489
+ handleConnection(connection);
490
+ } catch (e) {
491
+ console.error('Connection error:', e);
492
+ document.getElementById('join-status').innerHTML =
493
+ `<div class="status error">❌ Не удалось подключиться: ${e.message}</div>`;
494
+ }
495
+ }
496
+
497
+ function handleConnection(conn) {
498
+ connection = conn;
499
+
500
+ connection.on('open', () => {
501
+ document.getElementById('join-status').innerHTML =
502
+ '<div class="status success">✅ Подключено!</div>';
503
+ document.getElementById('chat-section').classList.remove('hidden');
504
+ document.getElementById('chat-box').classList.add('active');
505
+ document.getElementById('chat-input').classList.add('active');
506
+ document.getElementById('chat-peer-id').textContent = targetPeerId;
507
+ document.getElementById('invite-section').style.display = 'none';
508
+ document.getElementById('join-section').style.display = 'none';
509
+
510
+ addMessage('system', '🔗 Соединение установлено! Можете общаться.');
511
+ });
512
+
513
+ connection.on('data', (data) => {
514
+ if (data.type === 'message') {
515
+ addMessage('received', data.text);
516
+ }
517
+ });
518
+
519
+ connection.on('close', () => {
520
+ addMessage('system', '❌ Соединение разорвано');
521
+ document.getElementById('chat-input').classList.remove('active');
522
+ document.getElementById('chat-box').classList.remove('active');
523
+ });
524
+
525
+ connection.on('error', (err) => {
526
+ console.error('Connection error:', err);
527
+ addMessage('system', '⚠️ Ошибка соединения');
528
+ });
529
+ }
530
+
531
+ // ========== ОТПРАВКА СООБЩЕНИЙ ==========
532
+ function sendMessage() {
533
+ const input = document.getElementById('message-input');
534
+ const text = input.value.trim();
535
+
536
+ if (!text || !connection) return;
537
+
538
+ connection.send({
539
+ type: 'message',
540
+ text: text
541
+ });
542
+
543
+ addMessage('sent', text);
544
+ input.value = '';
545
+ }
546
+
547
+ function addMessage(type, text) {
548
+ const box = document.getElementById('chat-box');
549
+ const div = document.createElement('div');
550
+ div.className = `message ${type}`;
551
+ div.textContent = text;
552
+ box.appendChild(div);
553
+ box.scrollTop = box.scrollHeight;
554
+ }
555
+
556
+ function disconnect() {
557
+ if (connection) {
558
+ connection.close();
559
+ }
560
+ if (myPeer) {
561
+ myPeer.destroy();
562
+ }
563
+ location.reload();
564
+ }
565
+
566
+ // Автоподключение при загрузке (если есть сохраненный код)
567
+ window.onload = function() {
568
+ const savedCode = localStorage.getItem('hf_invite_code');
569
+ if (savedCode) {
570
+ document.getElementById('invite-input').value = savedCode;
571
+ }
572
+ // Периодически обновляем статистику
573
+ setInterval(loadStats, 30000);
574
+ };
575
+ </script>
576
+ </body>
577
+ </html>
578
+ """
579
+ return HTMLResponse(html)
580
+
581
+ # ========== API ЭНДПОИНТЫ ==========
582
+ @app.post("/api/generate_invite")
583
+ async def generate_invite():
584
+ try:
585
+ invites = load_invites()
586
+
587
+ # Генерируем уникальный код
588
+ code = str(uuid.uuid4())[:8].upper()
589
+ peer_id = str(uuid.uuid4())
590
+
591
+ invites[code] = {
592
+ "used": False,
593
+ "peer_id": peer_id,
594
+ "created_at": time.time()
595
+ }
596
+
597
+ save_invites(invites)
598
+
599
+ return JSONResponse({
600
+ "success": True,
601
+ "code": code,
602
+ "peer_id": peer_id
603
+ })
604
+ except Exception as e:
605
+ return JSONResponse({
606
+ "success": False,
607
+ "error": str(e)
608
+ })
609
+
610
+ @app.post("/api/use_invite")
611
+ async def use_invite(data: dict):
612
+ try:
613
+ code = data.get("code", "").upper()
614
+
615
+ if not code:
616
+ return JSONResponse({
617
+ "success": False,
618
+ "error": "Код не указан"
619
+ })
620
+
621
+ invites = load_invites()
622
+
623
+ if code not in invites:
624
+ return JSONResponse({
625
+ "success": False,
626
+ "error": "Неверный код"
627
+ })
628
+
629
+ if invites[code]["used"]:
630
+ return JSONResponse({
631
+ "success": False,
632
+ "error": "Код уже использован"
633
+ })
634
+
635
+ # Помечаем код как использованный
636
+ peer_id = invites[code]["peer_id"]
637
+ invites[code]["used"] = True
638
+ save_invites(invites)
639
+
640
+ # Сохраняем Peer ID
641
+ peers = load_peers()
642
+ peers[peer_id] = {
643
+ "invite_code": code,
644
+ "connected_at": time.time()
645
+ }
646
+ save_peers(peers)
647
+
648
+ return JSONResponse({
649
+ "success": True,
650
+ "peer_id": peer_id,
651
+ "message": "Код активирован!"
652
+ })
653
+ except Exception as e:
654
+ return JSONResponse({
655
+ "success": False,
656
+ "error": str(e)
657
+ })
658
+
659
+ @app.get("/api/stats")
660
+ async def get_stats():
661
+ invites = load_invites()
662
+ peers = load_peers()
663
+
664
+ used = sum(1 for v in invites.values() if v["used"])
665
+
666
+ return JSONResponse({
667
+ "total_invites": len(invites),
668
+ "used_invites": used,
669
+ "active_peers": len(peers)
670
+ })
671
+
672
+ # ========== ЗАПУСК ==========
673
+ if __name__ == "__main__":
674
+ print("=" * 50)
675
+ print("💬 HF Message - P2P Чат")
676
+ print("=" * 50)
677
+ print(f"📁 Данные хранятся в: {DATA_DIR}")
678
+ print(f"🌐 Сервер на порту: {PORT}")
679
+ print("🚀 Запускаем PeerJS...")
680
+
681
+ # Еще раз пробуем запустить PeerJS (на всякий случай)
682
+ threading.Thread(target=start_peerjs, daemon=True).start()
683
+
684
+ print("✅ Готово! Переходи на главную страницу.")
685
+ print("=" * 50)
686
+
687
+ uvicorn.run(
688
+ app,
689
+ host="0.0.0.0",
690
+ port=PORT,
691
+ log_level="info"
692
+ )