2026-10-06 02:36:54 +00:00
|
|
|
from flask import Blueprint, request, jsonify, Response
|
2026-05-12 23:50:49 +00:00
|
|
|
from datetime import datetime, timezone
|
2026-10-06 02:36:54 +00:00
|
|
|
from typing import Dict, Any, Tuple
|
2026-05-12 23:50:49 +00:00
|
|
|
import time
|
|
|
|
|
|
2026-10-06 02:36:54 +00:00
|
|
|
from blueprints.auth import verify_user_key
|
2026-05-12 23:50:49 +00:00
|
|
|
from models.state import (
|
|
|
|
|
messages,
|
|
|
|
|
room_users,
|
2026-10-06 02:36:54 +00:00
|
|
|
authorized_room_users,
|
2026-05-12 23:50:49 +00:00
|
|
|
kicked_users,
|
|
|
|
|
room_owners,
|
|
|
|
|
banned_ips,
|
|
|
|
|
banned_usernames,
|
|
|
|
|
user_ips,
|
2026-10-06 02:36:54 +00:00
|
|
|
user_sessions,
|
2026-05-12 23:50:49 +00:00
|
|
|
nuked_rooms,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
chat_bp = Blueprint("chat", __name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@chat_bp.route("/chat/<room>", methods=["GET", "POST"])
|
2026-10-06 02:36:54 +00:00
|
|
|
def chat_room(room: str) -> Tuple[Response, int] | Response:
|
2026-05-12 23:50:49 +00:00
|
|
|
room = room.strip()
|
|
|
|
|
|
|
|
|
|
if request.method == "POST":
|
2026-10-06 02:36:54 +00:00
|
|
|
data: Dict[str, Any] = request.get_json() or {}
|
|
|
|
|
username: str = str(data.get("username", "")).strip()
|
|
|
|
|
key: str = str(data.get("key", "")).strip()
|
|
|
|
|
ciphertext: str = str(data.get("ciphertext", "")).strip()
|
|
|
|
|
iv: str = str(data.get("iv", "")).strip()
|
2026-05-12 23:50:49 +00:00
|
|
|
user_ip = request.remote_addr
|
|
|
|
|
|
2026-10-06 02:36:54 +00:00
|
|
|
# Verify authentication
|
|
|
|
|
if not username or not key or not verify_user_key(username, key):
|
|
|
|
|
return jsonify({"error": "Invalid user credentials"}), 403
|
|
|
|
|
|
|
|
|
|
# Verify room authorization
|
|
|
|
|
if username not in authorized_room_users.get(room, set()):
|
|
|
|
|
return jsonify({"error": "Room password authorization required", "code": "AUTH_REQUIRED"}), 401
|
|
|
|
|
|
|
|
|
|
# Check bans and kicks
|
2026-05-12 23:50:49 +00:00
|
|
|
if (
|
|
|
|
|
user_ip in banned_ips
|
|
|
|
|
or username in banned_usernames
|
|
|
|
|
or (username in user_ips and user_ips[username] in banned_ips)
|
|
|
|
|
):
|
|
|
|
|
return jsonify({"error": "banned"}), 403
|
|
|
|
|
|
2026-10-06 02:36:54 +00:00
|
|
|
if username in kicked_users.get(room, set()):
|
|
|
|
|
return jsonify({"error": "kicked"}), 403
|
|
|
|
|
|
|
|
|
|
# Encryption-first enforcement: all messages stored in memory MUST be encrypted
|
|
|
|
|
if not ciphertext or not iv:
|
|
|
|
|
return jsonify({"error": "Encryption required: ciphertext and IV missing"}), 400
|
|
|
|
|
|
|
|
|
|
# Ephemeral encrypted message in server memory
|
2026-05-12 23:50:49 +00:00
|
|
|
message = {
|
|
|
|
|
"username": username,
|
2026-10-06 02:36:54 +00:00
|
|
|
"ciphertext": ciphertext,
|
|
|
|
|
"iv": iv,
|
2026-05-12 23:50:49 +00:00
|
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
2026-06-23 16:43:52 +00:00
|
|
|
|
2026-05-12 23:50:49 +00:00
|
|
|
messages.setdefault(room, []).append(message)
|
|
|
|
|
if len(messages[room]) > 100:
|
|
|
|
|
messages[room] = messages[room][-100:]
|
|
|
|
|
|
2026-10-06 02:36:54 +00:00
|
|
|
return jsonify({"status": "sent"})
|
|
|
|
|
|
|
|
|
|
# GET endpoint (optional polling fallback)
|
2026-05-12 23:50:49 +00:00
|
|
|
since = request.args.get("since")
|
|
|
|
|
room_messages = messages.setdefault(room, [])
|
|
|
|
|
|
|
|
|
|
if since:
|
|
|
|
|
try:
|
|
|
|
|
since_time = datetime.fromisoformat(since)
|
|
|
|
|
new_msgs = [
|
|
|
|
|
m
|
|
|
|
|
for m in room_messages
|
|
|
|
|
if datetime.fromisoformat(m["timestamp"]) > since_time
|
|
|
|
|
]
|
|
|
|
|
return jsonify(new_msgs)
|
|
|
|
|
except ValueError:
|
2026-10-06 02:36:54 +00:00
|
|
|
pass
|
2026-05-12 23:50:49 +00:00
|
|
|
|
|
|
|
|
return jsonify(room_messages)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@chat_bp.route("/chat/<room>/users", methods=["GET", "POST"])
|
2026-10-06 02:36:54 +00:00
|
|
|
def room_user_list(room: str) -> Tuple[Response, int] | Response:
|
2026-05-12 23:50:49 +00:00
|
|
|
room = room.strip()
|
|
|
|
|
|
|
|
|
|
if room in nuked_rooms:
|
|
|
|
|
if time.time() < nuked_rooms[room]:
|
|
|
|
|
return jsonify({"error": "Room is unavailable"}), 403
|
|
|
|
|
else:
|
|
|
|
|
nuked_rooms.pop(room, None)
|
|
|
|
|
|
|
|
|
|
if request.method == "POST":
|
2026-10-06 02:36:54 +00:00
|
|
|
data: Dict[str, Any] = request.get_json() or {}
|
|
|
|
|
username: str = str(data.get("username", "")).strip()
|
|
|
|
|
key: str = str(data.get("key", "")).strip()
|
|
|
|
|
|
2026-05-12 23:50:49 +00:00
|
|
|
if not username:
|
|
|
|
|
return jsonify({"error": "No username"}), 400
|
|
|
|
|
|
2026-10-06 02:36:54 +00:00
|
|
|
if key and not verify_user_key(username, key):
|
|
|
|
|
return jsonify({"error": "Invalid user credentials"}), 403
|
|
|
|
|
|
|
|
|
|
if username not in authorized_room_users.get(room, set()):
|
|
|
|
|
return jsonify({"error": "Room authorization required", "code": "AUTH_REQUIRED"}), 401
|
|
|
|
|
|
2026-05-12 23:50:49 +00:00
|
|
|
if username in kicked_users.get(room, set()):
|
|
|
|
|
return jsonify({"error": "Kicked from room"}), 403
|
|
|
|
|
|
|
|
|
|
room_users.setdefault(room, {})
|
|
|
|
|
room_users[room][username] = datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
|
|
|
|
users = room_users.get(room, {})
|
2026-10-06 02:36:54 +00:00
|
|
|
response: Dict[str, Any] = {"users": list(users.keys())}
|
2026-05-12 23:50:49 +00:00
|
|
|
if room in room_owners:
|
|
|
|
|
response["owner"] = room_owners[room]
|
|
|
|
|
return jsonify(response)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@chat_bp.route("/chat/<room>/kick", methods=["POST"])
|
2026-10-06 02:36:54 +00:00
|
|
|
def kick_user(room: str) -> Tuple[Response, int] | Response:
|
|
|
|
|
room = room.strip()
|
|
|
|
|
data: Dict[str, Any] = request.get_json() or {}
|
|
|
|
|
owner: str = str(data.get("owner", "")).strip()
|
|
|
|
|
key: str = str(data.get("key", "")).strip()
|
|
|
|
|
target: str = str(data.get("target", "")).strip()
|
2026-05-12 23:50:49 +00:00
|
|
|
|
|
|
|
|
if not owner or not target:
|
|
|
|
|
return jsonify({"error": "Missing owner or target"}), 400
|
2026-10-06 02:36:54 +00:00
|
|
|
|
|
|
|
|
if key and not verify_user_key(owner, key):
|
|
|
|
|
return jsonify({"error": "Invalid credentials"}), 403
|
|
|
|
|
|
2026-05-12 23:50:49 +00:00
|
|
|
if room_owners.get(room) != owner:
|
|
|
|
|
return jsonify({"error": "Not authorized"}), 403
|
|
|
|
|
|
|
|
|
|
kicked_users.setdefault(room, set()).add(target)
|
2026-10-06 02:36:54 +00:00
|
|
|
if room in authorized_room_users:
|
|
|
|
|
authorized_room_users[room].discard(target)
|
|
|
|
|
if room in room_users:
|
|
|
|
|
room_users[room].pop(target, None)
|
|
|
|
|
|
2026-05-12 23:50:49 +00:00
|
|
|
return jsonify({"kicked": target})
|
2026-06-23 16:43:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@chat_bp.route("/chat/<room>/poll", methods=["POST"])
|
2026-10-06 02:36:54 +00:00
|
|
|
def poll_room(room: str) -> Tuple[Response, int] | Response:
|
2026-06-23 16:43:52 +00:00
|
|
|
room = room.strip()
|
2026-10-06 02:36:54 +00:00
|
|
|
data: Dict[str, Any] = request.get_json() or {}
|
|
|
|
|
username: str = str(data.get("username", "")).strip()
|
|
|
|
|
key: str = str(data.get("key", "")).strip()
|
|
|
|
|
since: str = data.get("since")
|
2026-06-23 16:43:52 +00:00
|
|
|
|
|
|
|
|
if not username or not key or not verify_user_key(username, key):
|
|
|
|
|
return jsonify({"error": "Invalid credentials"}), 403
|
|
|
|
|
|
|
|
|
|
if room in nuked_rooms:
|
|
|
|
|
if time.time() < nuked_rooms[room]:
|
|
|
|
|
return jsonify({"error": "Room is unavailable"}), 403
|
|
|
|
|
else:
|
|
|
|
|
nuked_rooms.pop(room, None)
|
|
|
|
|
|
|
|
|
|
if username in kicked_users.get(room, set()):
|
|
|
|
|
return jsonify({"error": "Kicked from room"}), 403
|
|
|
|
|
|
2026-10-06 02:36:54 +00:00
|
|
|
# Check room authorization
|
|
|
|
|
if username not in authorized_room_users.get(room, set()):
|
|
|
|
|
return jsonify({"error": "Room authorization required", "code": "AUTH_REQUIRED"}), 401
|
|
|
|
|
|
2026-06-23 16:43:52 +00:00
|
|
|
# Update user presence
|
|
|
|
|
room_users.setdefault(room, {})
|
|
|
|
|
room_users[room][username] = datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
|
|
|
|
user_sessions[username] = {
|
|
|
|
|
"key": key,
|
|
|
|
|
"last_active": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-06 02:36:54 +00:00
|
|
|
# Fetch encrypted messages from memory
|
2026-06-23 16:43:52 +00:00
|
|
|
room_messages = messages.setdefault(room, [])
|
|
|
|
|
filtered_messages = room_messages
|
|
|
|
|
if since:
|
|
|
|
|
try:
|
|
|
|
|
since_time = datetime.fromisoformat(since)
|
|
|
|
|
filtered_messages = [
|
|
|
|
|
m
|
|
|
|
|
for m in room_messages
|
|
|
|
|
if datetime.fromisoformat(m["timestamp"]) > since_time
|
|
|
|
|
]
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
users_list = room_users.get(room, {})
|
2026-10-06 02:36:54 +00:00
|
|
|
response: Dict[str, Any] = {
|
2026-06-23 16:43:52 +00:00
|
|
|
"messages": filtered_messages,
|
2026-10-06 02:36:54 +00:00
|
|
|
"users": list(users_list.keys()),
|
2026-06-23 16:43:52 +00:00
|
|
|
}
|
|
|
|
|
if room in room_owners:
|
|
|
|
|
response["owner"] = room_owners[room]
|
|
|
|
|
|
|
|
|
|
return jsonify(response)
|