From ef07fa7157f93740a1441a0d465d0f91ba5db85d Mon Sep 17 00:00:00 2001 From: Asger Geel Weirsoee Date: Fri, 27 Feb 2026 13:22:12 +0100 Subject: [PATCH] feat(lobby): add create/join/detail session endpoints --- lobby/urls.py | 11 ++++ lobby/views.py | 135 ++++++++++++++++++++++++++++++++++++++++++++++- partyhub/urls.py | 9 ++-- 3 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 lobby/urls.py diff --git a/lobby/urls.py b/lobby/urls.py new file mode 100644 index 0000000..b63dc6e --- /dev/null +++ b/lobby/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from . import views + +app_name = "lobby" + +urlpatterns = [ + path("sessions/create", views.create_session, name="create_session"), + path("sessions/join", views.join_session, name="join_session"), + path("sessions/", views.session_detail, name="session_detail"), +] diff --git a/lobby/views.py b/lobby/views.py index 91ea44a..127417a 100644 --- a/lobby/views.py +++ b/lobby/views.py @@ -1,3 +1,134 @@ -from django.shortcuts import render +import json +import random -# Create your views here. +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest, JsonResponse +from django.views.decorators.http import require_GET, require_POST + +from fupogfakta.models import GameSession, Player + +SESSION_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" +SESSION_CODE_LENGTH = 6 +MAX_CODE_GENERATION_ATTEMPTS = 20 +JOINABLE_STATUSES = { + GameSession.Status.LOBBY, + GameSession.Status.LIE, + GameSession.Status.GUESS, + GameSession.Status.REVEAL, +} + + +def _json_body(request: HttpRequest) -> dict: + if not request.body: + return {} + + try: + return json.loads(request.body) + except json.JSONDecodeError: + return {} + + +def _generate_session_code() -> str: + return "".join(random.choices(SESSION_CODE_ALPHABET, k=SESSION_CODE_LENGTH)) + + +def _create_unique_session_code() -> str: + for _ in range(MAX_CODE_GENERATION_ATTEMPTS): + code = _generate_session_code() + if not GameSession.objects.filter(code=code).exists(): + return code + + raise RuntimeError("Could not generate unique session code") + + +@require_POST +@login_required +def create_session(request: HttpRequest) -> JsonResponse: + code = _create_unique_session_code() + session = GameSession.objects.create(host=request.user, code=code) + + return JsonResponse( + { + "session": { + "code": session.code, + "status": session.status, + "host_id": session.host_id, + "current_round": session.current_round, + } + }, + status=201, + ) + + +@require_POST +def join_session(request: HttpRequest) -> JsonResponse: + payload = _json_body(request) + + code = str(payload.get("code", "")).strip().upper() + nickname = str(payload.get("nickname", "")).strip() + + if not code: + return JsonResponse({"error": "Session code is required"}, status=400) + + if len(nickname) < 2 or len(nickname) > 40: + return JsonResponse({"error": "Nickname must be between 2 and 40 characters"}, status=400) + + try: + session = GameSession.objects.get(code=code) + except GameSession.DoesNotExist: + return JsonResponse({"error": "Session not found"}, status=404) + + if session.status not in JOINABLE_STATUSES: + return JsonResponse({"error": "Session is not joinable"}, status=400) + + if Player.objects.filter(session=session, nickname__iexact=nickname).exists(): + return JsonResponse({"error": "Nickname already taken"}, status=409) + + player = Player.objects.create(session=session, nickname=nickname) + + return JsonResponse( + { + "player": { + "id": player.id, + "nickname": player.nickname, + "score": player.score, + }, + "session": { + "code": session.code, + "status": session.status, + }, + }, + status=201, + ) + + +@require_GET +def session_detail(request: HttpRequest, code: str) -> JsonResponse: + session_code = code.strip().upper() + + try: + session = GameSession.objects.get(code=session_code) + except GameSession.DoesNotExist: + return JsonResponse({"error": "Session not found"}, status=404) + + players = list( + session.players.order_by("nickname").values( + "id", + "nickname", + "score", + "is_connected", + ) + ) + + return JsonResponse( + { + "session": { + "code": session.code, + "status": session.status, + "host_id": session.host_id, + "current_round": session.current_round, + "players_count": len(players), + }, + "players": players, + } + ) diff --git a/partyhub/urls.py b/partyhub/urls.py index 4a4b4d3..dbc31a6 100644 --- a/partyhub/urls.py +++ b/partyhub/urls.py @@ -1,13 +1,14 @@ from django.contrib import admin from django.http import JsonResponse -from django.urls import path +from django.urls import include, path def health(_request): - return JsonResponse({'ok': True, 'service': 'weirsoe-party-protocol'}) + return JsonResponse({"ok": True, "service": "weirsoe-party-protocol"}) urlpatterns = [ - path('admin/', admin.site.urls), - path('healthz', health, name='healthz'), + path("admin/", admin.site.urls), + path("healthz", health, name="healthz"), + path("lobby/", include("lobby.urls")), ]