Skip to content
This repository was archived by the owner on Jul 6, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/api/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
rest_api_router = APIRouter()

from app.api.rest.v1.accounts.controllers import router as v1_accounts_router
from app.api.rest.v1.sessions.controllers import router as v1_sessions_router
from app.api.rest.v1.scores.controllers import router as v1_scores_router

rest_api_router.include_router(v1_accounts_router)
rest_api_router.include_router(v1_sessions_router)
rest_api_router.include_router(v1_scores_router)
83 changes: 83 additions & 0 deletions app/api/rest/v1/sessions/controllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from uuid import UUID

from fastapi import APIRouter
from fastapi import status

from app import logger
from app.api.rest import responses
from app.api.rest.responses import Success
from app.api.rest.v1.sessions.models import Session
from app.errors import ServiceError
from app.services import sessions

router = APIRouter()


def determine_status_code(error: ServiceError) -> int:
match error:
case ServiceError.CREDENTIALS_NOT_FOUND:
return status.HTTP_401_UNAUTHORIZED
case ServiceError.CREDENTIALS_INCORRECT:
return status.HTTP_401_UNAUTHORIZED
case ServiceError.INTERNAL_SERVER_ERROR:
return status.HTTP_500_INTERNAL_SERVER_ERROR
case ServiceError.SESSIONS_NOT_FOUND:
return status.HTTP_404_NOT_FOUND
case _:
logger.warning(
"Unhandled error code in sessions rest api controller",
service_error=error,
)
return status.HTTP_500_INTERNAL_SERVER_ERROR


# TODO: fetch_many, down the stack
@router.get("/v1/sessions")
async def fetch_all() -> Success[list[Session]]:
data = await sessions.fetch_all()
if isinstance(data, ServiceError):
status_code = determine_status_code(data)
return responses.failure(
error=data,
message="Failed to fetch sessions",
status_code=status_code,
)

resp = [Session.parse_obj(rec) for rec in data]
return responses.success(
content=resp,
meta={},
)


@router.get("/v1/sessions/{session_id}")
async def fetch_one(session_id: UUID) -> Success[Session]:
data = await sessions.fetch_one(session_id)
if isinstance(data, ServiceError):
status_code = determine_status_code(data)
return responses.failure(
error=data,
message="Failed to fetch session",
status_code=status_code,
)

resp = Session.parse_obj(data)
return responses.success(content=resp)


# TODO: PATCH /v1/sessions/{session_id}


@router.delete("/v1/sessions/{session_id}")
async def delete(session_id: UUID) -> Success[Session]:
data = await sessions.delete(session_id)
if isinstance(data, ServiceError):
status_code = determine_status_code(data)
return responses.failure(
error=data,
message="Failed to delete session",
status_code=status_code,
)

resp = Session.parse_obj(data)
return responses.success(content=resp)
49 changes: 49 additions & 0 deletions app/api/rest/v1/sessions/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from datetime import datetime
from uuid import UUID

from pydantic import BaseModel


# input models


class LoginForm(BaseModel):
username: str
password: str


# TODO: PresenceUpdate, SessionUpdate models


# output models


class Presence(BaseModel):
account_id: int
username: str
utc_offset: int
country: str
privileges: int
game_mode: int
latitude: float
longitude: float
action: int
info_text: str
beatmap_md5: str
beatmap_id: int
mods: int
receive_match_updates: bool
spectator_host_session_id: UUID | None
away_message: str | None
multiplayer_match_id: int | None
last_communicated_at: datetime
last_np_beatmap_id: int | None


class Session(BaseModel):
session_id: UUID
account_id: int
presence: Presence
expires_at: datetime
created_at: datetime
# updated_at: datetime
97 changes: 97 additions & 0 deletions app/services/sessions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from datetime import datetime
from uuid import UUID
from uuid import uuid4

from app import logger
from app import security
from app.errors import ServiceError
from app.repositories import accounts
from app.repositories import sessions
from app.repositories.sessions import Session


async def create(
username: str,
password: str,
utc_offset: int,
latitude: float,
longitude: float,
) -> Session | ServiceError:
try:
account = await accounts.fetch_by_username(username)
if account is None:
return ServiceError.CREDENTIALS_NOT_FOUND

if not security.check_password(
password=password,
hashword=account["password"].encode(),
):
return ServiceError.CREDENTIALS_INCORRECT

session_id = uuid4()
session = await sessions.create(
session_id,
account["account_id"],
presence={
"account_id": account["account_id"],
"username": account["username"],
"utc_offset": utc_offset,
"country": account["country"],
"privileges": account["privileges"],
"game_mode": 0, # TODO?
"latitude": latitude,
"longitude": longitude,
"action": 0,
"info_text": "",
"beatmap_md5": "",
"beatmap_id": 0,
"mods": 0,
"receive_match_updates": False,
"spectator_host_session_id": None,
"away_message": None,
"multiplayer_match_id": None,
"last_communicated_at": datetime.now(),
"last_np_beatmap_id": None,
},
)
except Exception as exc: # pragma: no cover
logger.error("Failed to create session", exc_info=exc)
return ServiceError.INTERNAL_SERVER_ERROR

return session


async def fetch_all() -> list[Session] | ServiceError:
try:
_sessions = await sessions.fetch_all()
except Exception as exc: # pragma: no cover
logger.error("Failed to fetch sessions", exc_info=exc)
return ServiceError.INTERNAL_SERVER_ERROR

return _sessions


async def fetch_one(session_id: UUID) -> Session | ServiceError:
try:
session = await sessions.fetch_by_id(session_id)
except Exception as exc: # pragma: no cover
logger.error("Failed to fetch session", exc_info=exc)
return ServiceError.INTERNAL_SERVER_ERROR

if session is None:
return ServiceError.SESSIONS_NOT_FOUND

return session


async def delete(session_id: UUID) -> Session | ServiceError:
try:
session = await sessions.delete_by_id(session_id)
except Exception as exc: # pragma: no cover
logger.error("Failed to delete session", exc_info=exc)
return ServiceError.INTERNAL_SERVER_ERROR

if session is None:
return ServiceError.SESSIONS_NOT_FOUND

return session