]> Untitled Git - lemmy.git/blobdiff - crates/utils/src/error.rs
Cache & Optimize Woodpecker CI (#3450)
[lemmy.git] / crates / utils / src / error.rs
index 441c0422e6637ac2b6c4f8c566147aeb27c01fc4..ffc1723b4f75e79e8c440787cf72b5156a806ed6 100644 (file)
@@ -1,73 +1,29 @@
+use serde::{Deserialize, Serialize};
 use std::{
   fmt,
   fmt::{Debug, Display},
 };
 use tracing_error::SpanTrace;
+#[cfg(feature = "full")]
+use ts_rs::TS;
 
-#[derive(serde::Serialize)]
-struct ApiError {
-  error: String,
-}
+pub type LemmyResult<T> = Result<T, LemmyError>;
 
 pub struct LemmyError {
-  pub message: Option<String>,
+  pub error_type: LemmyErrorType,
   pub inner: anyhow::Error,
   pub context: SpanTrace,
 }
 
-impl LemmyError {
-  /// Create LemmyError from a message, including stack trace
-  pub fn from_message(message: &str) -> Self {
-    let inner = anyhow::anyhow!("{}", message);
-    LemmyError {
-      message: Some(message.into()),
-      inner,
-      context: SpanTrace::capture(),
-    }
-  }
-
-  /// Create a LemmyError from error and message, including stack trace
-  pub fn from_error_message<E>(error: E, message: &str) -> Self
-  where
-    E: Into<anyhow::Error>,
-  {
-    LemmyError {
-      message: Some(message.into()),
-      inner: error.into(),
-      context: SpanTrace::capture(),
-    }
-  }
-
-  /// Add message to existing LemmyError (or overwrite existing error)
-  pub fn with_message(self, message: &str) -> Self {
-    LemmyError {
-      message: Some(message.into()),
-      ..self
-    }
-  }
-
-  pub fn to_json(&self) -> Result<String, Self> {
-    let api_error = match &self.message {
-      Some(error) => ApiError {
-        error: error.into(),
-      },
-      None => ApiError {
-        error: "Unknown".into(),
-      },
-    };
-
-    Ok(serde_json::to_string(&api_error)?)
-  }
-}
-
 impl<T> From<T> for LemmyError
 where
   T: Into<anyhow::Error>,
 {
   fn from(t: T) -> Self {
+    let cause = t.into();
     LemmyError {
-      message: None,
-      inner: t.into(),
+      error_type: LemmyErrorType::Unknown(format!("{}", &cause)),
+      inner: cause,
       context: SpanTrace::capture(),
     }
   }
@@ -76,19 +32,21 @@ where
 impl Debug for LemmyError {
   fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     f.debug_struct("LemmyError")
-      .field("message", &self.message)
+      .field("message", &self.error_type)
       .field("inner", &self.inner)
-      .field("context", &"SpanTrace")
+      .field("context", &self.context)
       .finish()
   }
 }
 
 impl Display for LemmyError {
   fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-    if let Some(message) = &self.message {
-      write!(f, "{}: ", message)?;
-    }
-    writeln!(f, "{}", self.inner)?;
+    write!(f, "{}: ", &self.error_type)?;
+    // print anyhow including trace
+    // https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations
+    // this will print the anyhow trace (only if it exists)
+    // and if RUST_BACKTRACE=1, also a full backtrace
+    writeln!(f, "{:?}", self.inner)?;
     fmt::Display::fmt(&self.context, f)
   }
 }
@@ -102,14 +60,235 @@ impl actix_web::error::ResponseError for LemmyError {
   }
 
   fn error_response(&self) -> actix_web::HttpResponse {
-    if let Some(message) = &self.message {
-      actix_web::HttpResponse::build(self.status_code()).json(ApiError {
-        error: message.into(),
-      })
-    } else {
-      actix_web::HttpResponse::build(self.status_code())
-        .content_type("text/plain")
-        .body(self.inner.to_string())
+    actix_web::HttpResponse::build(self.status_code()).json(&self.error_type)
+  }
+}
+
+#[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, EnumIter)]
+#[cfg_attr(feature = "full", derive(TS))]
+#[cfg_attr(feature = "full", ts(export))]
+#[serde(tag = "error", content = "message", rename_all = "snake_case")]
+// TODO: order these based on the crate they belong to (utils, federation, db, api)
+pub enum LemmyErrorType {
+  ReportReasonRequired,
+  ReportTooLong,
+  NotAModerator,
+  NotAnAdmin,
+  CantBlockYourself,
+  CantBlockAdmin,
+  CouldntUpdateUser,
+  PasswordsDoNotMatch,
+  EmailNotVerified,
+  EmailRequired,
+  CouldntUpdateComment,
+  CouldntUpdatePrivateMessage,
+  CannotLeaveAdmin,
+  NoLinesInHtml,
+  SiteMetadataPageIsNotDoctypeHtml,
+  PictrsResponseError(String),
+  PictrsPurgeResponseError(String),
+  ImageUrlMissingPathSegments,
+  ImageUrlMissingLastPathSegment,
+  PictrsApiKeyNotProvided,
+  NoContentTypeHeader,
+  NotAnImageType,
+  NotAModOrAdmin,
+  NoAdmins,
+  NotTopAdmin,
+  NotTopMod,
+  NotLoggedIn,
+  SiteBan,
+  Deleted,
+  BannedFromCommunity,
+  CouldntFindCommunity,
+  CouldntFindPerson,
+  PersonIsBlocked,
+  DownvotesAreDisabled,
+  InstanceIsPrivate,
+  InvalidPassword,
+  SiteDescriptionLengthOverflow,
+  HoneypotFailed,
+  RegistrationApplicationIsPending,
+  CantEnablePrivateInstanceAndFederationTogether,
+  Locked,
+  CouldntCreateComment,
+  MaxCommentDepthReached,
+  NoCommentEditAllowed,
+  OnlyAdminsCanCreateCommunities,
+  CommunityAlreadyExists,
+  LanguageNotAllowed,
+  OnlyModsCanPostInCommunity,
+  CouldntUpdatePost,
+  NoPostEditAllowed,
+  CouldntFindPost,
+  EditPrivateMessageNotAllowed,
+  SiteAlreadyExists,
+  ApplicationQuestionRequired,
+  InvalidDefaultPostListingType,
+  RegistrationClosed,
+  RegistrationApplicationAnswerRequired,
+  EmailAlreadyExists,
+  FederationForbiddenByStrictAllowList,
+  PersonIsBannedFromCommunity,
+  ObjectIsNotPublic,
+  InvalidCommunity,
+  CannotCreatePostOrCommentInDeletedOrRemovedCommunity,
+  CannotReceivePage,
+  NewPostCannotBeLocked,
+  OnlyLocalAdminCanRemoveCommunity,
+  OnlyLocalAdminCanRestoreCommunity,
+  NoIdGiven,
+  IncorrectLogin,
+  InvalidQuery,
+  ObjectNotLocal,
+  PostIsLocked,
+  PersonIsBannedFromSite,
+  InvalidVoteValue,
+  PageDoesNotSpecifyCreator,
+  PageDoesNotSpecifyGroup,
+  NoCommunityFoundInCc,
+  NoEmailSetup,
+  EmailSmtpServerNeedsAPort,
+  MissingAnEmail,
+  RateLimitError,
+  InvalidName,
+  InvalidDisplayName,
+  InvalidMatrixId,
+  InvalidPostTitle,
+  InvalidBodyField,
+  BioLengthOverflow,
+  MissingTotpToken,
+  IncorrectTotpToken,
+  CouldntParseTotpSecret,
+  CouldntLikeComment,
+  CouldntSaveComment,
+  CouldntCreateReport,
+  CouldntResolveReport,
+  CommunityModeratorAlreadyExists,
+  CommunityUserAlreadyBanned,
+  CommunityBlockAlreadyExists,
+  CommunityFollowerAlreadyExists,
+  CouldntUpdateCommunityHiddenStatus,
+  PersonBlockAlreadyExists,
+  UserAlreadyExists,
+  TokenNotFound,
+  CouldntLikePost,
+  CouldntSavePost,
+  CouldntMarkPostAsRead,
+  CouldntUpdateCommunity,
+  CouldntUpdateReplies,
+  CouldntUpdatePersonMentions,
+  PostTitleTooLong,
+  CouldntCreatePost,
+  CouldntCreatePrivateMessage,
+  CouldntUpdatePrivate,
+  SystemErrLogin,
+  CouldntSetAllRegistrationsAccepted,
+  CouldntSetAllEmailVerified,
+  Banned,
+  CouldntGetComments,
+  CouldntGetPosts,
+  InvalidUrl,
+  EmailSendFailed,
+  Slurs,
+  CouldntGenerateTotp,
+  CouldntFindObject,
+  RegistrationDenied(String),
+  FederationDisabled,
+  DomainBlocked,
+  DomainNotInAllowList,
+  FederationDisabledByStrictAllowList,
+  SiteNameRequired,
+  SiteNameLengthOverflow,
+  PermissiveRegex,
+  InvalidRegex,
+  CaptchaIncorrect,
+  PasswordResetLimitReached,
+  CouldntCreateAudioCaptcha,
+  InvalidUrlScheme,
+  CouldntSendWebmention,
+  Unknown(String),
+}
+
+impl From<LemmyErrorType> for LemmyError {
+  fn from(error_type: LemmyErrorType) -> Self {
+    let inner = anyhow::anyhow!("{}", error_type);
+    LemmyError {
+      error_type,
+      inner,
+      context: SpanTrace::capture(),
     }
   }
 }
+
+pub trait LemmyErrorExt<T, E: Into<anyhow::Error>> {
+  fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>;
+}
+
+impl<T, E: Into<anyhow::Error>> LemmyErrorExt<T, E> for Result<T, E> {
+  fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
+    self.map_err(|error| LemmyError {
+      error_type,
+      inner: error.into(),
+      context: SpanTrace::capture(),
+    })
+  }
+}
+pub trait LemmyErrorExt2<T> {
+  fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>;
+}
+
+impl<T> LemmyErrorExt2<T> for Result<T, LemmyError> {
+  fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
+    self.map_err(|mut e| {
+      e.error_type = error_type;
+      e
+    })
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  #![allow(clippy::unwrap_used)]
+  #![allow(clippy::indexing_slicing)]
+  use super::*;
+  use actix_web::{body::MessageBody, ResponseError};
+  use std::fs::read_to_string;
+  use strum::IntoEnumIterator;
+
+  #[test]
+  fn deserializes_no_message() {
+    let err = LemmyError::from(LemmyErrorType::Banned).error_response();
+    let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
+    assert_eq!(&json, "{\"error\":\"banned\"}")
+  }
+
+  #[test]
+  fn deserializes_with_message() {
+    let reg_denied = LemmyErrorType::RegistrationDenied(String::from("reason"));
+    let err = LemmyError::from(reg_denied).error_response();
+    let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
+    assert_eq!(
+      &json,
+      "{\"error\":\"registration_denied\",\"message\":\"reason\"}"
+    )
+  }
+
+  /// Check if errors match translations. Disabled because many are not translated at all.
+  #[test]
+  #[ignore]
+  fn test_translations_match() {
+    #[derive(Deserialize)]
+    struct Err {
+      error: String,
+    }
+
+    let translations = read_to_string("translations/translations/en.json").unwrap();
+    LemmyErrorType::iter().for_each(|e| {
+      let msg = serde_json::to_string(&e).unwrap();
+      let msg: Err = serde_json::from_str(&msg).unwrap();
+      let msg = msg.error;
+      assert!(translations.contains(&format!("\"{msg}\"")), "{msg}");
+    });
+  }
+}