X commited on
Commit
d6f1914
·
verified ·
1 Parent(s): 7cbdc7d

Update app.py

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