Lobby: desktop layout with chat column, dice hotbar and shared dice tray, server-side rolls

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
Claude
2026-10-21 16:51:16 +00:00
parent 94d3505b82
commit a88b8d2fa3
4 changed files with 315 additions and 108 deletions
+48 -11
View File
@@ -521,6 +521,19 @@ def read_lobby(lid: str, user: dict = Depends(current_user)):
"code": lobby["code"] if is_dm else None, "members": members}
def _recipient(con, lobby, user: dict, to):
"""User id to whisper to (the DM or a member, not yourself), None for everybody."""
to = str(to or "").strip()
if not to:
return None
target = con.execute("""SELECT u.id FROM users u WHERE u.username=? AND (u.id=? OR EXISTS
(SELECT 1 FROM lobby_members m WHERE m.lobby_id=? AND m.user_id=u.id))""",
(to, lobby["owner_id"], lobby["id"])).fetchone()
if not target or target["id"] == user["id"]:
raise HTTPException(422, "You can only whisper to other people in this lobby")
return target["id"]
@api.get("/lobbies/{lid}/messages")
def read_messages(lid: str, after: int = 0, user: dict = Depends(current_user)):
"""Chat and event log. Whispers are only returned to the sender and the recipient (not even the DM sees them).
@@ -534,8 +547,14 @@ def read_messages(lid: str, after: int = 0, user: dict = Depends(current_user)):
rows = con.execute(q + " AND m.id>? ORDER BY m.id LIMIT 200", (lid, user["id"], user["id"], after)).fetchall()
else:
rows = list(reversed(con.execute(q + " ORDER BY m.id DESC LIMIT 100", (lid, user["id"], user["id"])).fetchall()))
return [{"id": r["id"], "kind": r["kind"], "text": r["text"], "time": r["created"], "from": r["sender"] or "",
"to": r["target"] or "", "mine": r["user_id"] == user["id"] and r["kind"] != "event"} for r in rows]
out = []
for r in rows:
item = {"id": r["id"], "kind": r["kind"], "text": r["text"], "time": r["created"], "from": r["sender"] or "",
"to": r["target"] or "", "mine": r["user_id"] == user["id"] and r["kind"] != "event"}
if r["kind"] == "roll":
item["roll"], item["text"] = json.loads(r["text"]), ""
out.append(item)
return out
@api.post("/lobbies/{lid}/messages")
@@ -549,15 +568,7 @@ def send_message(lid: str, body: dict = Body(...), user: dict = Depends(current_
limiter.fail(f"chat:{user['id']}")
with db() as con:
lobby = _lobby_for(con, lid, user)
to = str(body.get("to") or "").strip()
to_id = None
if to:
target = con.execute("""SELECT u.id FROM users u WHERE u.username=? AND (u.id=? OR EXISTS
(SELECT 1 FROM lobby_members m WHERE m.lobby_id=? AND m.user_id=u.id))""",
(to, lobby["owner_id"], lid)).fetchone()
if not target or target["id"] == user["id"]:
raise HTTPException(422, "You can only whisper to other people in this lobby")
to_id = target["id"]
to_id = _recipient(con, lobby, user, body.get("to"))
cur = con.execute("INSERT INTO lobby_messages (lobby_id, user_id, to_user_id, kind, text, created) VALUES (?,?,?,?,?,?)",
(lid, user["id"], to_id, "whisper" if to_id else "chat", text, time.time()))
con.execute("""DELETE FROM lobby_messages WHERE lobby_id=? AND id <= COALESCE(
@@ -565,6 +576,32 @@ def send_message(lid: str, body: dict = Body(...), user: dict = Depends(current_
return {"id": cur.lastrowid}
DICE = (4, 6, 8, 10, 12, 20, 100)
MAX_DICE = 10
@api.post("/lobbies/{lid}/rolls")
def roll_dice(lid: str, body: dict = Body(...), user: dict = Depends(current_user)):
"""Rolls on the server (so nobody can cheat) and posts the result in the chat; with `to` it is a private roll."""
try:
sides, count = int(body.get("sides")), int(body.get("count", 1))
except (TypeError, ValueError):
raise HTTPException(422, "Invalid dice")
if sides not in DICE or not 1 <= count <= MAX_DICE:
raise HTTPException(422, f"Roll 1 to {MAX_DICE} dice of: " + ", ".join(f"d{d}" for d in DICE))
throttle(f"chat:{user['id']}", 30, 60)
limiter.fail(f"chat:{user['id']}")
results = [secrets.randbelow(sides) + 1 for _ in range(count)]
with db() as con:
lobby = _lobby_for(con, lid, user)
to_id = _recipient(con, lobby, user, body.get("to"))
cur = con.execute("INSERT INTO lobby_messages (lobby_id, user_id, to_user_id, kind, text, created) VALUES (?,?,?,?,?,?)",
(lid, user["id"], to_id, "roll", json.dumps({"sides": sides, "results": results}), time.time()))
con.execute("""DELETE FROM lobby_messages WHERE lobby_id=? AND id <= COALESCE(
(SELECT id FROM lobby_messages WHERE lobby_id=? ORDER BY id DESC LIMIT 1 OFFSET ?), 0)""", (lid, lid, KEEP_MESSAGES))
return {"id": cur.lastrowid, "sides": sides, "results": results}
@api.put("/lobbies/{lid}")
def rename_lobby(lid: str, body: dict = Body(...), user: dict = Depends(current_user)):
name = str(body.get("name", "")).strip()[:60]