X commited on
Update app.py
Browse files
app.py
CHANGED
|
@@ -16,12 +16,10 @@ PORT = int(os.getenv("PORT", 7860))
|
|
| 16 |
DATA_DIR = Path("/data")
|
| 17 |
DATA_DIR.mkdir(exist_ok=True)
|
| 18 |
|
| 19 |
-
|
| 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)
|
|
@@ -35,15 +33,14 @@ 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():
|
|
@@ -51,16 +48,10 @@ def generate_word_code():
|
|
| 51 |
word1 = random.choice(WORDS)
|
| 52 |
word2 = random.choice(WORDS)
|
| 53 |
word3 = str(random.randint(10, 99))
|
| 54 |
-
|
| 55 |
-
return code.upper()
|
| 56 |
|
| 57 |
-
|
| 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))
|
|
@@ -84,7 +75,6 @@ threading.Thread(target=start_peerjs, daemon=True).start()
|
|
| 84 |
# ========== FASTAPI ==========
|
| 85 |
app = FastAPI()
|
| 86 |
|
| 87 |
-
# ========== HTML СТРАНИЦА ==========
|
| 88 |
@app.get("/", response_class=HTMLResponse)
|
| 89 |
async def index(request: Request):
|
| 90 |
html = """
|
|
@@ -164,6 +154,7 @@ async def index(request: Request):
|
|
| 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;
|
|
@@ -253,44 +244,36 @@ async def index(request: Request):
|
|
| 253 |
<body>
|
| 254 |
<div class="container">
|
| 255 |
<h1>🔐 HF Message</h1>
|
| 256 |
-
<div class="subtitle">
|
| 257 |
-
|
| 258 |
-
<!-- Секция:
|
| 259 |
-
<div class="section" id="
|
| 260 |
-
<h3>
|
| 261 |
-
<
|
| 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="
|
| 269 |
-
<h3>
|
| 270 |
-
<
|
| 271 |
-
|
| 272 |
-
<div
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
| 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="
|
| 291 |
<h3>➕ Добавить друга по ID</h3>
|
| 292 |
<div class="flex">
|
| 293 |
-
<input type="text" id="friend-id-input" placeholder="Вставь Peer ID друга"
|
| 294 |
<button onclick="addFriend()" style="width:auto;padding:12px 20px;">➕</button>
|
| 295 |
</div>
|
| 296 |
<div id="friends-list" style="margin-top:10px;"></div>
|
|
@@ -305,14 +288,11 @@ async def index(request: Request):
|
|
| 305 |
<input type="text" id="message-input" placeholder="Введите сообщение..." onkeypress="if(event.key==='Enter') sendMessage()">
|
| 306 |
<button onclick="sendMessage()">Отправить</button>
|
| 307 |
</div>
|
| 308 |
-
<div class="peer-id" id="my-peer-id"></div>
|
| 309 |
<button class="btn-danger" onclick="disconnect()">❌ Отключиться</button>
|
| 310 |
</div>
|
| 311 |
|
| 312 |
<div class="stats">
|
| 313 |
-
|
| 314 |
-
<span id="stats-used">0</span> использовано ·
|
| 315 |
-
<span id="stats-peers">0</span> подключений
|
| 316 |
</div>
|
| 317 |
</div>
|
| 318 |
|
|
@@ -320,46 +300,26 @@ async def index(request: Request):
|
|
| 320 |
// ========== ГЛОБАЛЬНЫЕ ==========
|
| 321 |
let myPeer = null;
|
| 322 |
let myId = null;
|
|
|
|
| 323 |
let targetPeerId = null;
|
| 324 |
let connection = null;
|
| 325 |
let friends = [];
|
|
|
|
| 326 |
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 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 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 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),
|
|
@@ -369,8 +329,7 @@ async def index(request: Request):
|
|
| 369 |
|
| 370 |
myPeer.on('open', (id) => {
|
| 371 |
console.log('✅ Peer открыт:', id);
|
| 372 |
-
|
| 373 |
-
loadFriends();
|
| 374 |
});
|
| 375 |
|
| 376 |
myPeer.on('connection', (conn) => {
|
|
@@ -380,22 +339,64 @@ async def index(request: Request):
|
|
| 380 |
myPeer.on('error', (err) => {
|
| 381 |
console.error('Peer error:', err);
|
| 382 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
}
|
| 384 |
|
| 385 |
-
// ==========
|
| 386 |
-
async function
|
| 387 |
-
const
|
| 388 |
-
status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
|
| 390 |
try {
|
| 391 |
-
const r = await fetch('/api/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
const d = await r.json();
|
| 393 |
|
| 394 |
if (d.success) {
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
} else {
|
| 400 |
status.innerHTML = `<div class="status error">❌ ${d.error}</div>`;
|
| 401 |
}
|
|
@@ -404,42 +405,19 @@ async def index(request: Request):
|
|
| 404 |
}
|
| 405 |
}
|
| 406 |
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
const orig = btn.textContent;
|
| 412 |
-
btn.textContent = '✅ Скопировано!';
|
| 413 |
-
setTimeout(() => btn.textContent = orig, 2000);
|
| 414 |
-
});
|
| 415 |
-
}
|
| 416 |
-
|
| 417 |
-
// ========== ВХОД ПО КОДУ ==========
|
| 418 |
-
async function joinWithInvite() {
|
| 419 |
-
const code = document.getElementById('invite-input').value.trim().toUpperCase();
|
| 420 |
-
const status = document.getElementById('join-status');
|
| 421 |
-
|
| 422 |
-
if (!code) {
|
| 423 |
-
status.innerHTML = '<div class="status error">❌ Введи код</div>';
|
| 424 |
-
return;
|
| 425 |
-
}
|
| 426 |
-
|
| 427 |
-
status.innerHTML = '<div class="status info">⏳ Проверка...</div>';
|
| 428 |
|
| 429 |
try {
|
| 430 |
-
const r = await fetch('/api/
|
| 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 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
setTimeout(() => connectToPeer(d.peer_id), 500);
|
| 443 |
} else {
|
| 444 |
status.innerHTML = `<div class="status error">❌ ${d.error}</div>`;
|
| 445 |
}
|
|
@@ -448,7 +426,17 @@ async def index(request: Request):
|
|
| 448 |
}
|
| 449 |
}
|
| 450 |
|
| 451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
async function addFriend() {
|
| 453 |
const input = document.getElementById('friend-id-input');
|
| 454 |
const id = input.value.trim();
|
|
@@ -463,7 +451,7 @@ async def index(request: Request):
|
|
| 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();
|
|
@@ -499,10 +487,10 @@ async def index(request: Request):
|
|
| 499 |
container.innerHTML = friends.map(f => `
|
| 500 |
<div class="friend-item">
|
| 501 |
<div>
|
| 502 |
-
<div class="name">${f.name
|
| 503 |
<div class="id">${f.peer_id}</div>
|
| 504 |
</div>
|
| 505 |
-
<button onclick="connectToFriend('${f.peer_id}')"
|
| 506 |
💬 Чат
|
| 507 |
</button>
|
| 508 |
</div>
|
|
@@ -510,8 +498,9 @@ async def index(request: Request):
|
|
| 510 |
}
|
| 511 |
|
| 512 |
function connectToFriend(peerId) {
|
| 513 |
-
initPeer()
|
| 514 |
-
|
|
|
|
| 515 |
}
|
| 516 |
|
| 517 |
// ========== P2P ПОДКЛЮЧЕНИЕ ==========
|
|
@@ -519,8 +508,7 @@ async def index(request: Request):
|
|
| 519 |
targetPeerId = targetId;
|
| 520 |
|
| 521 |
if (!myPeer) {
|
| 522 |
-
initPeer();
|
| 523 |
-
setTimeout(() => connectToPeer(targetId), 1000);
|
| 524 |
return;
|
| 525 |
}
|
| 526 |
|
|
@@ -528,7 +516,7 @@ async def index(request: Request):
|
|
| 528 |
connection = myPeer.connect(targetId, { reliable: true });
|
| 529 |
handleConnection(connection);
|
| 530 |
} catch (e) {
|
| 531 |
-
document.getElementById('
|
| 532 |
`<div class="status error">❌ Ошибка: ${e.message}</div>`;
|
| 533 |
}
|
| 534 |
}
|
|
@@ -537,15 +525,10 @@ async def index(request: Request):
|
|
| 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');
|
| 543 |
document.getElementById('chat-box').classList.add('active');
|
| 544 |
document.getElementById('chat-input').classList.add('active');
|
| 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 |
});
|
|
@@ -589,10 +572,37 @@ async def index(request: Request):
|
|
| 589 |
location.reload();
|
| 590 |
}
|
| 591 |
|
| 592 |
-
// ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
window.onload = function() {
|
| 594 |
-
|
| 595 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 596 |
};
|
| 597 |
</script>
|
| 598 |
</body>
|
|
@@ -601,44 +611,46 @@ async def index(request: Request):
|
|
| 601 |
return HTMLResponse(html)
|
| 602 |
|
| 603 |
# ========== API ==========
|
| 604 |
-
@app.post("/api/
|
| 605 |
-
async def
|
| 606 |
try:
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
peer_id = str(uuid.uuid4())
|
| 610 |
-
|
| 611 |
-
invites[code] = {
|
| 612 |
-
"used": False,
|
| 613 |
-
"peer_id": peer_id,
|
| 614 |
-
"created_at": time.time()
|
| 615 |
-
}
|
| 616 |
-
save_json(INVITES_FILE, invites)
|
| 617 |
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
except Exception as e:
|
| 620 |
return JSONResponse({"success": False, "error": str(e)})
|
| 621 |
|
| 622 |
-
@app.post("/api/
|
| 623 |
-
async def
|
| 624 |
try:
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 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 |
|
|
@@ -646,12 +658,14 @@ async def use_invite(data: dict):
|
|
| 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":
|
| 655 |
save_json(FRIENDS_FILE, friends)
|
| 656 |
|
| 657 |
return JSONResponse({"success": True})
|
|
@@ -667,14 +681,8 @@ async def get_friends():
|
|
| 667 |
|
| 668 |
@app.get("/api/stats")
|
| 669 |
async def get_stats():
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
used = sum(1 for v in invites.values() if v["used"])
|
| 673 |
-
return JSONResponse({
|
| 674 |
-
"total_invites": len(invites),
|
| 675 |
-
"used_invites": used,
|
| 676 |
-
"active_peers": len(peers)
|
| 677 |
-
})
|
| 678 |
|
| 679 |
# ========== ЗАПУСК ==========
|
| 680 |
if __name__ == "__main__":
|
|
@@ -683,5 +691,4 @@ if __name__ == "__main__":
|
|
| 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")
|
|
|
|
| 16 |
DATA_DIR = Path("/data")
|
| 17 |
DATA_DIR.mkdir(exist_ok=True)
|
| 18 |
|
| 19 |
+
USERS_FILE = DATA_DIR / "users.json"
|
|
|
|
| 20 |
FRIENDS_FILE = DATA_DIR / "friends.json"
|
| 21 |
|
| 22 |
+
for f in [USERS_FILE, FRIENDS_FILE]:
|
|
|
|
| 23 |
if not f.exists():
|
| 24 |
with open(f, "w") as fp:
|
| 25 |
json.dump({}, fp)
|
|
|
|
| 33 |
with open(file, "w") as f:
|
| 34 |
json.dump(data, f, indent=2)
|
| 35 |
|
| 36 |
+
# ========== СЛОВА ДЛЯ КОДОВ ==========
|
| 37 |
WORDS = [
|
| 38 |
"солнце", "луна", "звезда", "небо", "море", "ветер", "дождь", "снег",
|
| 39 |
"гора", "река", "лес", "поле", "цветок", "трава", "дерево", "птица",
|
| 40 |
"рыба", "волк", "лиса", "медведь", "заяц", "ёжик", "белка", "сова",
|
| 41 |
"орёл", "сокол", "дельфин", "кит", "тигр", "лев", "пантера", "гепард",
|
| 42 |
"радуга", "молния", "гром", "туча", "роса", "иней", "туман", "буря",
|
| 43 |
+
"мир", "друг", "свет", "тепло", "радость", "счастье", "любовь", "надежда"
|
|
|
|
| 44 |
]
|
| 45 |
|
| 46 |
def generate_word_code():
|
|
|
|
| 48 |
word1 = random.choice(WORDS)
|
| 49 |
word2 = random.choice(WORDS)
|
| 50 |
word3 = str(random.randint(10, 99))
|
| 51 |
+
return f"{word1}-{word2}-{word3}".upper()
|
|
|
|
| 52 |
|
| 53 |
+
# ========== ЗАПУСК PEERJS ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
def start_peerjs():
|
|
|
|
| 55 |
try:
|
| 56 |
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
| 57 |
result = sock.connect_ex(('127.0.0.1', 9000))
|
|
|
|
| 75 |
# ========== FASTAPI ==========
|
| 76 |
app = FastAPI()
|
| 77 |
|
|
|
|
| 78 |
@app.get("/", response_class=HTMLResponse)
|
| 79 |
async def index(request: Request):
|
| 80 |
html = """
|
|
|
|
| 154 |
.btn-success { background: linear-gradient(135deg, #48bb78, #38a169); }
|
| 155 |
.btn-danger { background: linear-gradient(135deg, #fc8181, #e53e3e); }
|
| 156 |
.btn-copy { background: linear-gradient(135deg, #4299e1, #3182ce); }
|
| 157 |
+
.btn-small { width: auto; padding: 6px 16px; font-size: 12px; }
|
| 158 |
.code-display {
|
| 159 |
background: #2d3748;
|
| 160 |
color: #f7fafc;
|
|
|
|
| 244 |
<body>
|
| 245 |
<div class="container">
|
| 246 |
<h1>🔐 HF Message</h1>
|
| 247 |
+
<div class="subtitle">Код-слово = вход в аккаунт · ID = добавление в друзья</div>
|
| 248 |
+
|
| 249 |
+
<!-- Секция: Вход по коду-слову -->
|
| 250 |
+
<div class="section" id="login-section">
|
| 251 |
+
<h3>🔑 Войти в аккаунт по коду-слову</h3>
|
| 252 |
+
<input type="text" id="login-code" placeholder="СОЛНЦЕ-ЛУНА-42" style="text-transform:uppercase;">
|
| 253 |
+
<button onclick="loginWithCode()">Войти / Создать аккаунт</button>
|
| 254 |
+
<div id="login-status"></div>
|
|
|
|
| 255 |
</div>
|
| 256 |
|
| 257 |
+
<!-- Секция: Мой профиль (появляется после входа) -->
|
| 258 |
+
<div class="section hidden" id="profile-section">
|
| 259 |
+
<h3>👤 Мой профиль</h3>
|
| 260 |
+
<div style="background:#edf2f7;padding:10px;border-radius:8px;text-align:center;">
|
| 261 |
+
<div style="font-weight:600;color:#2d3748;" id="profile-name">Имя</div>
|
| 262 |
+
<div style="font-family:monospace;font-size:13px;color:#4a5568;margin-top:4px;" id="profile-id">ID</div>
|
| 263 |
+
</div>
|
| 264 |
+
<button class="btn-copy" onclick="copyProfileId()" style="margin-top:8px;">📋 Копировать ID</button>
|
| 265 |
+
<button class="btn-success" onclick="generateNewCode()" style="margin-top:8px;">🔄 Сгенерировать новый код-слово</button>
|
| 266 |
+
<div id="new-code-result" class="hidden" style="margin-top:8px;">
|
| 267 |
+
<div class="code-display" id="new-code-display"></div>
|
| 268 |
+
<button class="btn-copy" onclick="copyNewCode()">📋 Копировать новый код</button>
|
| 269 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
</div>
|
| 271 |
|
| 272 |
<!-- Секция: Добавить друга по ID -->
|
| 273 |
+
<div class="section hidden" id="friends-section">
|
| 274 |
<h3>➕ Добавить друга по ID</h3>
|
| 275 |
<div class="flex">
|
| 276 |
+
<input type="text" id="friend-id-input" placeholder="Вставь Peer ID друга">
|
| 277 |
<button onclick="addFriend()" style="width:auto;padding:12px 20px;">➕</button>
|
| 278 |
</div>
|
| 279 |
<div id="friends-list" style="margin-top:10px;"></div>
|
|
|
|
| 288 |
<input type="text" id="message-input" placeholder="Введите сообщение..." onkeypress="if(event.key==='Enter') sendMessage()">
|
| 289 |
<button onclick="sendMessage()">Отправить</button>
|
| 290 |
</div>
|
|
|
|
| 291 |
<button class="btn-danger" onclick="disconnect()">❌ Отключиться</button>
|
| 292 |
</div>
|
| 293 |
|
| 294 |
<div class="stats">
|
| 295 |
+
👥 <span id="stats-users">0</span> пользователей
|
|
|
|
|
|
|
| 296 |
</div>
|
| 297 |
</div>
|
| 298 |
|
|
|
|
| 300 |
// ========== ГЛОБАЛЬНЫЕ ==========
|
| 301 |
let myPeer = null;
|
| 302 |
let myId = null;
|
| 303 |
+
let myName = null;
|
| 304 |
let targetPeerId = null;
|
| 305 |
let connection = null;
|
| 306 |
let friends = [];
|
| 307 |
+
let currentCode = null;
|
| 308 |
|
| 309 |
+
// ========== ИНИЦИАЛИЗАЦИЯ PEER ==========
|
| 310 |
+
function initPeer(callback) {
|
| 311 |
+
if (myPeer && myPeer.open) {
|
| 312 |
+
if (callback) callback();
|
| 313 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
+
let savedId = localStorage.getItem('hf_peer_id');
|
| 317 |
+
if (!savedId) {
|
| 318 |
+
savedId = 'user-' + Math.random().toString(36).substring(2, 10);
|
| 319 |
+
localStorage.setItem('hf_peer_id', savedId);
|
| 320 |
+
}
|
| 321 |
+
myId = savedId;
|
|
|
|
|
|
|
| 322 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
myPeer = new Peer(myId, {
|
| 324 |
host: window.location.hostname,
|
| 325 |
port: window.location.port || (window.location.protocol === 'https:' ? 443 : 80),
|
|
|
|
| 329 |
|
| 330 |
myPeer.on('open', (id) => {
|
| 331 |
console.log('✅ Peer открыт:', id);
|
| 332 |
+
if (callback) callback();
|
|
|
|
| 333 |
});
|
| 334 |
|
| 335 |
myPeer.on('connection', (conn) => {
|
|
|
|
| 339 |
myPeer.on('error', (err) => {
|
| 340 |
console.error('Peer error:', err);
|
| 341 |
});
|
| 342 |
+
|
| 343 |
+
// Если Peer уже открыт
|
| 344 |
+
if (myPeer.open) {
|
| 345 |
+
if (callback) callback();
|
| 346 |
+
}
|
| 347 |
}
|
| 348 |
|
| 349 |
+
// ========== ВХОД ПО КОДУ-СЛОВУ ==========
|
| 350 |
+
async function loginWithCode() {
|
| 351 |
+
const code = document.getElementById('login-code').value.trim().toUpperCase();
|
| 352 |
+
const status = document.getElementById('login-status');
|
| 353 |
+
|
| 354 |
+
if (!code) {
|
| 355 |
+
status.innerHTML = '<div class="status error">❌ Введи код-слово</div>';
|
| 356 |
+
return;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
// Проверяем формат: СЛОВО-СЛОВО-ЧИСЛО
|
| 360 |
+
const parts = code.split('-');
|
| 361 |
+
if (parts.length !== 3 || isNaN(parts[2])) {
|
| 362 |
+
status.innerHTML = '<div class="status error">❌ Неверный формат. Пример: СОЛНЦЕ-ЛУНА-42</div>';
|
| 363 |
+
return;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
status.innerHTML = '<div class="status info">⏳ Вход...</div>';
|
| 367 |
|
| 368 |
try {
|
| 369 |
+
const r = await fetch('/api/login', {
|
| 370 |
+
method: 'POST',
|
| 371 |
+
headers: { 'Content-Type': 'application/json' },
|
| 372 |
+
body: JSON.stringify({ code })
|
| 373 |
+
});
|
| 374 |
+
|
| 375 |
const d = await r.json();
|
| 376 |
|
| 377 |
if (d.success) {
|
| 378 |
+
currentCode = code;
|
| 379 |
+
myName = d.name;
|
| 380 |
+
localStorage.setItem('hf_code', code);
|
| 381 |
+
localStorage.setItem('hf_name', d.name);
|
| 382 |
+
localStorage.setItem('hf_peer_id', d.peer_id);
|
| 383 |
+
myId = d.peer_id;
|
| 384 |
+
|
| 385 |
+
status.innerHTML = `<div class="status success">✅ Добро пожаловать, ${d.name}!</div>`;
|
| 386 |
+
|
| 387 |
+
// Показываем профиль
|
| 388 |
+
document.getElementById('profile-section').classList.remove('hidden');
|
| 389 |
+
document.getElementById('friends-section').classList.remove('hidden');
|
| 390 |
+
document.getElementById('profile-name').textContent = d.name;
|
| 391 |
+
document.getElementById('profile-id').textContent = d.peer_id;
|
| 392 |
+
document.getElementById('login-section').style.display = 'none';
|
| 393 |
+
|
| 394 |
+
// Инициализируем Peer
|
| 395 |
+
initPeer(() => {
|
| 396 |
+
loadFriends();
|
| 397 |
+
});
|
| 398 |
+
|
| 399 |
+
updateStats();
|
| 400 |
} else {
|
| 401 |
status.innerHTML = `<div class="status error">❌ ${d.error}</div>`;
|
| 402 |
}
|
|
|
|
| 405 |
}
|
| 406 |
}
|
| 407 |
|
| 408 |
+
// ========== ГЕНЕРАЦИЯ НОВОГО КОДА ==========
|
| 409 |
+
async function generateNewCode() {
|
| 410 |
+
const status = document.getElementById('login-status');
|
| 411 |
+
status.innerHTML = '<div class="status info">⏳ Генерация...</div>';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
|
| 413 |
try {
|
| 414 |
+
const r = await fetch('/api/generate_code', { method: 'POST' });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
const d = await r.json();
|
| 416 |
|
| 417 |
if (d.success) {
|
| 418 |
+
document.getElementById('new-code-display').textContent = d.code;
|
| 419 |
+
document.getElementById('new-code-result').classList.remove('hidden');
|
| 420 |
+
status.innerHTML = '<div class="status success">✅ Новый код создан! Сохрани его.</div>';
|
|
|
|
| 421 |
} else {
|
| 422 |
status.innerHTML = `<div class="status error">❌ ${d.error}</div>`;
|
| 423 |
}
|
|
|
|
| 426 |
}
|
| 427 |
}
|
| 428 |
|
| 429 |
+
function copyNewCode() {
|
| 430 |
+
const code = document.getElementById('new-code-display').textContent;
|
| 431 |
+
navigator.clipboard.writeText(code);
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
function copyProfileId() {
|
| 435 |
+
const id = document.getElementById('profile-id').textContent;
|
| 436 |
+
navigator.clipboard.writeText(id);
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
// ========== ДРУЗЬЯ ==========
|
| 440 |
async function addFriend() {
|
| 441 |
const input = document.getElementById('friend-id-input');
|
| 442 |
const id = input.value.trim();
|
|
|
|
| 451 |
const r = await fetch('/api/add_friend', {
|
| 452 |
method: 'POST',
|
| 453 |
headers: { 'Content-Type': 'application/json' },
|
| 454 |
+
body: JSON.stringify({ peer_id: id, name: 'Друг' })
|
| 455 |
});
|
| 456 |
|
| 457 |
const d = await r.json();
|
|
|
|
| 487 |
container.innerHTML = friends.map(f => `
|
| 488 |
<div class="friend-item">
|
| 489 |
<div>
|
| 490 |
+
<div class="name">${f.name}</div>
|
| 491 |
<div class="id">${f.peer_id}</div>
|
| 492 |
</div>
|
| 493 |
+
<button onclick="connectToFriend('${f.peer_id}')" class="btn-small">
|
| 494 |
💬 Чат
|
| 495 |
</button>
|
| 496 |
</div>
|
|
|
|
| 498 |
}
|
| 499 |
|
| 500 |
function connectToFriend(peerId) {
|
| 501 |
+
initPeer(() => {
|
| 502 |
+
connectToPeer(peerId);
|
| 503 |
+
});
|
| 504 |
}
|
| 505 |
|
| 506 |
// ========== P2P ПОДКЛЮЧЕНИЕ ==========
|
|
|
|
| 508 |
targetPeerId = targetId;
|
| 509 |
|
| 510 |
if (!myPeer) {
|
| 511 |
+
initPeer(() => connectToPeer(targetId));
|
|
|
|
| 512 |
return;
|
| 513 |
}
|
| 514 |
|
|
|
|
| 516 |
connection = myPeer.connect(targetId, { reliable: true });
|
| 517 |
handleConnection(connection);
|
| 518 |
} catch (e) {
|
| 519 |
+
document.getElementById('login-status').innerHTML =
|
| 520 |
`<div class="status error">❌ Ошибка: ${e.message}</div>`;
|
| 521 |
}
|
| 522 |
}
|
|
|
|
| 525 |
connection = conn;
|
| 526 |
|
| 527 |
conn.on('open', () => {
|
|
|
|
|
|
|
| 528 |
document.getElementById('chat-section').classList.remove('hidden');
|
| 529 |
document.getElementById('chat-box').classList.add('active');
|
| 530 |
document.getElementById('chat-input').classList.add('active');
|
| 531 |
document.getElementById('chat-peer-id').textContent = targetPeerId;
|
|
|
|
|
|
|
|
|
|
| 532 |
|
| 533 |
addMessage('system', '🔗 Соединение установлено!');
|
| 534 |
});
|
|
|
|
| 572 |
location.reload();
|
| 573 |
}
|
| 574 |
|
| 575 |
+
// ========== СТАТИСТИКА ==========
|
| 576 |
+
async function updateStats() {
|
| 577 |
+
try {
|
| 578 |
+
const r = await fetch('/api/stats');
|
| 579 |
+
const d = await r.json();
|
| 580 |
+
document.getElementById('stats-users').textContent = d.total_users;
|
| 581 |
+
} catch(e) {}
|
| 582 |
+
}
|
| 583 |
+
updateStats();
|
| 584 |
+
setInterval(updateStats, 30000);
|
| 585 |
+
|
| 586 |
+
// ========== АВТОВХОД ==========
|
| 587 |
window.onload = function() {
|
| 588 |
+
const savedCode = localStorage.getItem('hf_code');
|
| 589 |
+
const savedName = localStorage.getItem('hf_name');
|
| 590 |
+
const savedId = localStorage.getItem('hf_peer_id');
|
| 591 |
+
|
| 592 |
+
if (savedCode && savedName && savedId) {
|
| 593 |
+
document.getElementById('login-code').value = savedCode;
|
| 594 |
+
document.getElementById('login-section').style.display = 'none';
|
| 595 |
+
document.getElementById('profile-section').classList.remove('hidden');
|
| 596 |
+
document.getElementById('friends-section').classList.remove('hidden');
|
| 597 |
+
document.getElementById('profile-name').textContent = savedName;
|
| 598 |
+
document.getElementById('profile-id').textContent = savedId;
|
| 599 |
+
myId = savedId;
|
| 600 |
+
|
| 601 |
+
initPeer(() => {
|
| 602 |
+
loadFriends();
|
| 603 |
+
});
|
| 604 |
+
updateStats();
|
| 605 |
+
}
|
| 606 |
};
|
| 607 |
</script>
|
| 608 |
</body>
|
|
|
|
| 611 |
return HTMLResponse(html)
|
| 612 |
|
| 613 |
# ========== API ==========
|
| 614 |
+
@app.post("/api/login")
|
| 615 |
+
async def login(data: dict):
|
| 616 |
try:
|
| 617 |
+
code = data.get("code", "").upper()
|
| 618 |
+
users = load_json(USERS_FILE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
|
| 620 |
+
if code in users:
|
| 621 |
+
# Вход в существующий аккаунт
|
| 622 |
+
return JSONResponse({
|
| 623 |
+
"success": True,
|
| 624 |
+
"name": users[code]["name"],
|
| 625 |
+
"peer_id": users[code]["peer_id"]
|
| 626 |
+
})
|
| 627 |
+
else:
|
| 628 |
+
# Создание нового аккаунта
|
| 629 |
+
peer_id = str(uuid.uuid4())
|
| 630 |
+
name = f"User_{len(users) + 1}"
|
| 631 |
+
users[code] = {
|
| 632 |
+
"name": name,
|
| 633 |
+
"peer_id": peer_id,
|
| 634 |
+
"created_at": time.time()
|
| 635 |
+
}
|
| 636 |
+
save_json(USERS_FILE, users)
|
| 637 |
+
return JSONResponse({
|
| 638 |
+
"success": True,
|
| 639 |
+
"name": name,
|
| 640 |
+
"peer_id": peer_id
|
| 641 |
+
})
|
| 642 |
except Exception as e:
|
| 643 |
return JSONResponse({"success": False, "error": str(e)})
|
| 644 |
|
| 645 |
+
@app.post("/api/generate_code")
|
| 646 |
+
async def generate_code():
|
| 647 |
try:
|
| 648 |
+
users = load_json(USERS_FILE)
|
| 649 |
+
code = generate_word_code()
|
| 650 |
+
# Проверяем уникальность
|
| 651 |
+
while code in users:
|
| 652 |
+
code = generate_word_code()
|
| 653 |
+
return JSONResponse({"success": True, "code": code})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
except Exception as e:
|
| 655 |
return JSONResponse({"success": False, "error": str(e)})
|
| 656 |
|
|
|
|
| 658 |
async def add_friend(data: dict):
|
| 659 |
try:
|
| 660 |
peer_id = data.get("peer_id", "").strip()
|
| 661 |
+
name = data.get("name", "Друг")
|
| 662 |
+
|
| 663 |
if not peer_id:
|
| 664 |
return JSONResponse({"success": False, "error": "ID не указан"})
|
| 665 |
|
| 666 |
friends = load_json(FRIENDS_FILE)
|
| 667 |
if peer_id not in friends:
|
| 668 |
+
friends[peer_id] = {"name": name, "added_at": time.time()}
|
| 669 |
save_json(FRIENDS_FILE, friends)
|
| 670 |
|
| 671 |
return JSONResponse({"success": True})
|
|
|
|
| 681 |
|
| 682 |
@app.get("/api/stats")
|
| 683 |
async def get_stats():
|
| 684 |
+
users = load_json(USERS_FILE)
|
| 685 |
+
return JSONResponse({"total_users": len(users)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 686 |
|
| 687 |
# ========== ЗАПУСК ==========
|
| 688 |
if __name__ == "__main__":
|
|
|
|
| 691 |
print("=" * 50)
|
| 692 |
print(f"📁 Данные в: {DATA_DIR}")
|
| 693 |
print("🚀 Запуск...")
|
|
|
|
| 694 |
uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")
|