276 lines
9.5 KiB
Rust
276 lines
9.5 KiB
Rust
// 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 <https://www.gnu.org/licenses/>.
|
|
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<DatabaseError> 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<leptos::serde_json::Error> 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<sqlx::Error> 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<argon2::password_hash::Error> for DatabaseError {
|
|
fn from(err: argon2::password_hash::Error) -> Self {
|
|
Self::PasswordHashError(err.to_string())
|
|
}
|
|
}
|