// Copyright (C) 2025-2026 Emilis Bliūdžius // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::{ game::{GameId, GameTime}, message::PublicIdentity, player::PlayerId, role::RoleTitle, }; use leptos::prelude::ServerFnErrorErr; #[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)] pub enum GameError { #[error("too many roles. there's {players} players, but {roles} roles")] TooManyRoles { players: u8, roles: u8 }, #[error("no wolves?")] NoWolves, #[error("message invalid for game state")] InvalidMessageForGameState, #[error("no executions during night time")] NoExecutionsAtNight, #[error("chracter is already dead")] CharacterAlreadyDead, #[error("no matching character found")] NoMatchingCharacterFound, #[error("character not in joined player pool")] CharacterNotInJoinedPlayers, #[error("{0}")] GenericError(String), #[error("invalid cause of death")] InvalidCauseOfDeath, #[error("invalid target")] InvalidTarget, #[error("timed out")] TimedOut, #[error("host channel closed")] HostChannelClosed, #[error("too many players: there's {got} players but only {need} roles")] TooManyPlayers { got: u8, need: u8 }, #[error("it's already daytime")] AlreadyDaytime, #[error("it's not the end of the night yet")] NotEndOfNight, #[error("it's not day yet")] NotDayYet, #[error("it's not night")] NotNight, #[error("invalid role, expected {expected:?} got {got:?}")] InvalidRole { expected: RoleTitle, got: RoleTitle }, #[error("no mentor for an apprentice to be an apprentice to :(")] NoApprenticeMentor, #[error("{0} isn't a mentor role")] NotAMentor(RoleTitle), #[error("inactive game object")] InactiveGameObject, #[error("socket error: {0}")] SocketError(String), #[error("this night is over")] NightOver, #[error("no night actions")] NoNightActions, #[error("still awaiting response")] AwaitingResponse, #[error("current state already has a response")] NightNeedsNext, #[error("night zero actions can only be obtained on night zero")] NotNightZero, #[error("this action cannot happen on night zero")] CannotHappenOnNightZero, #[error("wolves intro in progress")] WolvesIntroInProgress, #[error("a game is still ongoing")] GameOngoing, #[error("needs a role reveal")] NeedRoleReveal, #[error("no previous state")] NoPreviousState, #[error("invalid original kill for guardian guard")] GuardianInvalidOriginalKill, #[error("player not assigned number: {0}")] PlayerNotAssignedNumber(String), #[error("player [{0}] has an assigned role slot, but isn't in the player list")] AssignedPlayerMissing(PlayerId), #[error(" {0} assigned to {1} roles")] AssignedMultipleTimes(PublicIdentity, usize), #[error("change set for {0} is already set")] ChangesAlreadySet(GameTime), #[error("missing {0} in game story")] MissingTime(GameTime), #[error("no previous during day")] NoPreviousDuringDay, #[error("militia already spent")] MilitiaSpent, #[error("this prompt doesn't mark anyone")] PromptDoesntMark, #[error("cannot shapeshift on a non-shapeshifter prompt")] ShapeshiftingIsForShapeshifters, #[error("must select a target")] MustSelectTarget, #[error("no current prompt in aura handling")] NoCurrentPromptForAura, #[error("you're not dead")] NotDead, #[error("invalid character id assignment for player ID {for_player}")] InvalidCharacterIdAssignment { for_player: PlayerId }, #[error("already joined")] AlreadyJoined, #[error("cannot join own game")] CannotJoinOwnGame, #[error("cannot leave a started game")] CannotLeaveOnceStarted, #[error("cannot join a started game")] CannotJoinStartedGame, #[error("game already started")] GameAlreadyStarted, #[error("you're already in another game: {0}")] AlreadyInAnotherGame(GameId), } #[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)] pub enum ServerError { #[error("internal server error")] InternalServerError, #[error("not found")] NotFound, #[error("user already exists")] UserAlreadyExists, #[error("invalid credentials")] InvalidCredentials, #[error("token expired")] ExpiredToken, #[error("connection error")] ConnectionError, #[error("invalid request: {0}")] InvalidRequest(String), #[error("you're already in an active game: {0}")] AlreadyInActiveGame(GameId), #[error("{0}")] GameError(#[from] GameError), } impl leptos::prelude::FromServerFnError for ServerError { type Encoder = leptos::server_fn::codec::JsonEncoding; fn from_server_fn_error(value: ServerFnErrorErr) -> Self { match value { ServerFnErrorErr::ServerError(err) => { log::error!("server error: {err}; truncating to ServerError::InternalServerError"); ServerError::InternalServerError } ServerFnErrorErr::MiddlewareError(err) => { log::error!( "middleware error: {err}; truncating to ServerError::InternalServerError" ); ServerError::InternalServerError } ServerFnErrorErr::Request(err) => { const CONN_ERR: &str = "TypeError: NetworkError when attempting to fetch resource."; if err == CONN_ERR { Self::ConnectionError } else { Self::InvalidRequest(err) } } err => { let t = match &err { ServerFnErrorErr::Registration(_) => "Registration", ServerFnErrorErr::UnsupportedRequestMethod(_) => "UnsupportedRequestMethod", ServerFnErrorErr::Request(_) => "Request", ServerFnErrorErr::ServerError(_) => "ServerError", ServerFnErrorErr::MiddlewareError(_) => "MiddlewareError", ServerFnErrorErr::Deserialization(_) => "Deserialization", ServerFnErrorErr::Serialization(_) => "Serialization", ServerFnErrorErr::Args(_) => "Args", ServerFnErrorErr::MissingArg(_) => "MissingArg", ServerFnErrorErr::Response(_) => "Response", }; Self::InvalidRequest(format!("[{t}]: {err}")) } } } } impl From for ServerError { fn from(err: DatabaseError) -> Self { match err { DatabaseError::NotFound => ServerError::NotFound, DatabaseError::UserAlreadyExists => ServerError::UserAlreadyExists, #[allow(unreachable_patterns)] _ => { log::error!( "converting database error into ServerError::InternalServerError: {err}" ); ServerError::InternalServerError } } } } #[derive(Debug, Clone, PartialEq, Error)] pub enum DatabaseError { #[error("user already exists")] UserAlreadyExists, #[error("password hashing error: {0}")] PasswordHashError(String), #[error("sqlx error: {0}")] SqlxError(String), #[error("not found")] NotFound, #[error("(de)serialization error: {0}")] Serialization(String), } impl From for DatabaseError { fn from(value: leptos::serde_json::Error) -> Self { Self::Serialization(value.to_string()) } } #[cfg(feature = "ssr")] impl axum::response::IntoResponse for ServerError { fn into_response(self) -> axum::response::Response { use axum::{Json, http::StatusCode}; ( match self { ServerError::AlreadyInActiveGame(_) | ServerError::GameError(_) | ServerError::InvalidCredentials | ServerError::InvalidRequest(_) | ServerError::UserAlreadyExists => StatusCode::BAD_REQUEST, ServerError::NotFound => StatusCode::NOT_FOUND, ServerError::ConnectionError | ServerError::InternalServerError => { StatusCode::INTERNAL_SERVER_ERROR } ServerError::ExpiredToken => StatusCode::UNAUTHORIZED, }, Json(self), ) .into_response() } } #[cfg(feature = "ssr")] impl From for DatabaseError { fn from(err: sqlx::Error) -> Self { match err { sqlx::Error::RowNotFound => Self::NotFound, _ => Self::SqlxError(err.to_string()), } } } #[cfg(feature = "ssr")] impl From for DatabaseError { fn from(err: argon2::password_hash::Error) -> Self { Self::PasswordHashError(err.to_string()) } }