FuckingChat/controllers/chat.py

134 lines
4.2 KiB
Python

"""
controllers/chat.py — chat API endpoints.
"""
import hashlib
import json
import os
import re
import time
from bottle import route, request, response
from models.db import DBfrontend
VALID_CHANNELS = {"general", "games", "anime", "politics"}
CHANNEL_NAMES = {
"general": "General",
"games": "Games",
"anime": "Anime",
"politics": "Politics",
}
# In-memory rate store: ip → [timestamps]
rate_store: dict[str, list[float]] = {}
_rate_limit: int = 5
_rate_window: int = 10
_admin_token: str = ""
_salt: str = ""
_db: DBfrontend | None = None
def init(db: DBfrontend, sec_cfg):
global _db, _rate_limit, _rate_window, _admin_token, _salt
_db = db
_rate_limit = sec_cfg.rate_limit
_rate_window = sec_cfg.rate_window
_admin_token = sec_cfg.admin_token
_salt = sec_cfg.salt
def _check_rate(ip: str) -> bool:
now = time.time()
cutoff = now - _rate_window
if ip in rate_store:
rate_store[ip] = [t for t in rate_store[ip] if t > cutoff]
if len(rate_store[ip]) >= _rate_limit:
return False
rate_store[ip].append(now)
else:
rate_store[ip] = [now]
return True
def _strip_xss(content: str) -> str:
"""Strip HTML tags except allowed <img class="inline-gif"> for Tenor."""
allowed = r'<img class="inline-gif" src="[^"]+">'
saved = re.findall(allowed, content)
content = re.sub(r"<[^>]*>", "", content)
if saved:
content = (content + "\n" + "".join(saved)).strip()
return content
def register_routes(app):
from bottle import Bottle
@app.route("/api/channels", method="GET")
def list_channels():
response.content_type = "application/json"
return {"success": True, "channels": [{"id": k, "name": v} for k, v in CHANNEL_NAMES.items()]}
@app.route("/api/messages", method="POST")
def post_message():
ip = request.environ.get("REMOTE_ADDR", "127.0.0.1")
if not _check_rate(ip):
response.status = 429
return {"success": False, "error": "Rate limit exceeded. Slow down."}
try:
data = request.json
if not data:
response.status = 400
return {"success": False, "error": "No data"}
channel = data.get("channel", "general")
content = data.get("message", "").strip()
gif_url = data.get("gif_url") or None
if channel not in VALID_CHANNELS:
response.status = 400
return {"success": False, "error": "Invalid channel"}
if not content:
response.status = 400
return {"success": False, "error": "Empty message"}
if len(content) > 2000:
response.status = 400
return {"success": False, "error": "Message too long"}
content = _strip_xss(content)
ip_hash = hashlib.sha256((_salt + ip).encode()).hexdigest()[:16]
msg_id = _db.add_message(channel, ip_hash, content, gif_url=gif_url)
return {"success": True, "message_id": msg_id, "ip": ip_hash}
except Exception as e:
response.status = 500
return {"success": False, "error": str(e)}
@app.route("/api/messages", method="GET")
def get_messages():
try:
channel = request.query.get("channel", "general")
limit = min(int(request.query.get("limit", 50)), 200)
if channel not in VALID_CHANNELS:
channel = "general"
msgs = _db.get_messages(channel=channel, limit=limit)
return {"success": True, "messages": msgs}
except Exception as e:
response.status = 500
return {"success": False, "error": str(e)}
@app.route("/api/messages/<message_id:int>", method="DELETE")
def delete_message(message_id):
auth = request.get_header("Authorization", "")
token = auth.replace("Bearer ", "")
if _admin_token and token != _admin_token:
response.status = 403
return {"success": False, "error": "Forbidden"}
try:
ok = _db.delete_message(message_id)
return {"success": ok}
except Exception as e:
response.status = 500
return {"success": False, "error": str(e)}