use serde::{Deserialize, Serialize};
use std::{fmt::Debug, panic::Location};
use strum::{Display, EnumIter};
#[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, EnumIter, Eq, Hash)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
#[serde(tag = "error", content = "message", rename_all = "snake_case")]
#[non_exhaustive]
pub enum LemmyErrorType {
BlockKeywordTooShort,
BlockKeywordTooLong,
CouldntUpdate,
CouldntCreate,
ReportReasonRequired,
ReportTooLong,
NotAModerator,
NotAnAdmin,
CantBlockYourself,
CantNoteYourself,
CantBlockAdmin,
PasswordsDoNotMatch,
EmailNotVerified,
EmailRequired,
CannotLeaveAdmin,
CannotLeaveMod,
PictrsResponseError(String),
PictrsPurgeResponseError(String),
PictrsApiKeyNotProvided,
PictrsInvalidImageUpload(String),
NoContentTypeHeader,
NotAnImageType,
ImageUploadDisabled,
NotAModOrAdmin,
NotTopMod,
NotLoggedIn,
NotHigherMod,
NotHigherAdmin,
SiteBan,
Deleted,
PersonIsBlocked,
CommunityIsBlocked,
InstanceIsBlocked,
InstanceIsPrivate,
InvalidPassword,
SiteDescriptionLengthOverflow,
HoneypotFailed,
RegistrationApplicationIsPending,
Locked,
MaxCommentDepthReached,
NoCommentEditAllowed,
OnlyAdminsCanCreateCommunities,
AlreadyExists,
LanguageNotAllowed,
NoPostEditAllowed,
NsfwNotAllowed,
EditPrivateMessageNotAllowed,
ApplicationQuestionRequired,
InvalidDefaultPostListingType,
RegistrationClosed,
RegistrationApplicationAnswerRequired,
RegistrationUsernameRequired,
EmailAlreadyTaken,
UsernameAlreadyTaken,
PersonIsBannedFromCommunity,
NoIdGiven,
IncorrectLogin,
NoEmailSetup,
LocalSiteNotSetup,
InvalidEmailAddress(String),
InvalidName,
InvalidCodeVerifier,
InvalidDisplayName,
InvalidMatrixId,
InvalidPostTitle,
InvalidBodyField,
BioLengthOverflow,
AltTextLengthOverflow,
CouldntParseTotpSecret,
CouldntGenerateTotp,
MissingTotpToken,
MissingTotpSecret,
IncorrectTotpToken,
TotpAlreadyEnabled,
BlockedUrl,
InvalidUrl,
EmailSendFailed,
Slurs,
RegistrationDenied(String),
SiteNameRequired,
SiteNameLengthOverflow,
PermissiveRegex,
InvalidRegex,
InvalidUrlScheme,
ContradictingFilters,
TooManyItems,
BanExpirationInPast,
InvalidUnixTime,
InvalidBotAction,
TagNotInCommunity,
CantBlockLocalInstance,
Unknown(String),
UrlLengthOverflow,
OauthAuthorizationInvalid,
OauthLoginFailed,
OauthRegistrationClosed,
NotFound,
PostScheduleTimeMustBeInFuture,
TooManyScheduledPosts,
CannotCombineFederationBlocklistAndAllowlist,
CouldntParsePaginationToken,
PluginError(String),
InvalidFetchLimit,
EmailNotificationsDisabled,
MultiCommunityUpdateWrongUser,
CannotCombineCommunityIdAndMultiCommunityId,
MultiCommunityEntryLimitReached,
TooManyRequests,
ResolveObjectFailed(String),
#[serde(untagged)]
#[cfg_attr(feature = "ts-rs", ts(skip))]
UntranslatedError(Option<UntranslatedError>),
}
#[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[serde(tag = "error", content = "message", rename_all = "snake_case")]
#[non_exhaustive]
pub enum UntranslatedError {
InvalidCommunity,
CannotCreatePostOrCommentInDeletedOrRemovedCommunity,
CannotReceivePage,
OnlyLocalAdminCanRemoveCommunity,
OnlyLocalAdminCanRestoreCommunity,
PostIsLocked,
PersonIsBannedFromSite(String),
InvalidVoteValue,
PageDoesNotSpecifyCreator,
FederationDisabled,
DomainBlocked(String),
DomainNotInAllowList(String),
FederationDisabledByStrictAllowList,
ContradictingFilters,
UrlWithoutDomain,
InboxTimeout,
CantDeleteSite,
ObjectIsNotPublic,
ObjectIsNotPrivate,
InvalidFollow(String),
PurgeInvalidImageUrl,
Unreachable,
CouldntSendWebmention,
CommunityHasNoFollowers(String),
}
cfg_select! {
feature = "full" => {
use std::fmt;
use serde_with::serde_as;
use serde_with::DisplayFromStr;
pub type LemmyResult<T> = Result<T, LemmyError>;
#[serde_as]
#[derive(Serialize)]
pub struct LemmyError {
#[serde(flatten)]
pub error_type: LemmyErrorType,
#[serde_as(as = "DisplayFromStr")]
pub cause: anyhow::Error,
#[serde(skip)]
pub caller: Location<'static>,
}
pub(crate) const MAX_API_PARAM_ELEMENTS: usize = 10_000;
impl<T> From<T> for LemmyError
where
T: Into<anyhow::Error>,
{
#[track_caller]
fn from(t: T) -> Self {
let cause = t.into();
let error_type = match cause.downcast_ref::<diesel::result::Error>() {
Some(&diesel::NotFound) => LemmyErrorType::NotFound,
_ => LemmyErrorType::Unknown(format!("{}", &cause))
};
LemmyError {
error_type,
cause,
caller: *Location::caller(),
}
}
}
impl Debug for LemmyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LemmyError")
.field("message", &self.error_type)
.field("caller", &format_args!("{}", self.caller))
.field("inner", &self.cause)
.finish()
}
}
impl fmt::Display for LemmyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: ", &self.error_type)?;
write!(f, "{}", self.caller)?;
write!(f, "{}", self.cause)?;
Ok(())
}
}
impl actix_web::error::ResponseError for LemmyError {
fn status_code(&self) -> actix_web::http::StatusCode {
match self.error_type {
LemmyErrorType::IncorrectLogin => actix_web::http::StatusCode::UNAUTHORIZED,
LemmyErrorType::NotFound => actix_web::http::StatusCode::NOT_FOUND,
_ => actix_web::http::StatusCode::BAD_REQUEST,
}
}
fn error_response(&self) -> actix_web::HttpResponse {
actix_web::HttpResponse::build(self.status_code()).json(self)
}
}
impl From<LemmyErrorType> for LemmyError {
#[track_caller]
fn from(error_type: LemmyErrorType) -> Self {
let cause = anyhow::anyhow!("{}", error_type);
LemmyError {
error_type,
cause,
caller: *Location::caller(),
}
}
}
impl From<UntranslatedError> for LemmyError {
#[track_caller]
fn from(error_type: UntranslatedError) -> Self {
let cause = anyhow::anyhow!("{}", error_type);
LemmyError {
error_type: LemmyErrorType::UntranslatedError( Some(error_type) ),
cause,
caller: *Location::caller(),
}
}
}
impl From<UntranslatedError> for LemmyErrorType {
fn from(error: UntranslatedError) -> Self {
LemmyErrorType::UntranslatedError (Some(error) )
}
}
pub trait LemmyErrorExt<T, E: Into<anyhow::Error>> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> LemmyResult<T>;
}
impl<T, E: Into<anyhow::Error>> LemmyErrorExt<T, E> for Result<T, E> {
#[track_caller]
fn with_lemmy_type(self, error_type: LemmyErrorType) -> LemmyResult<T> {
self.map_err(|error| LemmyError {
error_type,
cause: error.into(),
caller: *Location::caller(),
})
}
}
pub trait LemmyErrorExt2<T> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> LemmyResult<T>;
fn into_anyhow(self) -> Result<T, anyhow::Error>;
}
impl<T> LemmyErrorExt2<T> for LemmyResult<T> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> LemmyResult<T> {
self.map_err(|mut e| {
e.error_type = error_type;
e
})
}
fn into_anyhow(self) -> Result<T, anyhow::Error> {
self.map_err(|e| e.cause)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use actix_web::{body::MessageBody, ResponseError};
use pretty_assertions::assert_eq;
#[test]
fn untranslated_error_format() -> LemmyResult<()> {
let err = LemmyError::from(UntranslatedError::DomainBlocked("test".to_string())).error_response();
let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?;
assert_eq!(&json, r#"{"error":"domain_blocked","message":"test","cause":"DomainBlocked"}"#);
Ok(())
}
#[test]
fn deserializes_no_message() -> LemmyResult<()> {
let err = LemmyError::from(LemmyErrorType::BlockedUrl).error_response();
let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?;
assert_eq!(&json, r#"{"error":"blocked_url","cause":"BlockedUrl"}"#);
Ok(())
}
#[test]
fn deserializes_with_message() -> LemmyResult<()> {
let reg_banned = LemmyErrorType::PictrsResponseError(String::from("reason"));
let err = LemmyError::from(reg_banned).error_response();
let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?;
assert_eq!(
&json,
r#"{"error":"pictrs_response_error","message":"reason","cause":"PictrsResponseError"}"#
);
Ok(())
}
#[test]
fn test_convert_diesel_errors() {
let not_found_error = LemmyError::from(diesel::NotFound);
assert_eq!(LemmyErrorType::NotFound, not_found_error.error_type);
assert_eq!(404, not_found_error.status_code());
let other_error = LemmyError::from(diesel::result::Error::NotInTransaction);
assert!(matches!(other_error.error_type, LemmyErrorType::Unknown{..}));
assert_eq!(400, other_error.status_code());
}
}
}
_ => {}
}