]> Untitled Git - lemmy.git/blob - crates/utils/src/error.rs
78590a6a74d71e00bc9fe6892c5a6d2f46d50b69
[lemmy.git] / crates / utils / src / error.rs
1 use serde::{Deserialize, Serialize};
2 use std::{
3   fmt,
4   fmt::{Debug, Display},
5 };
6 use tracing_error::SpanTrace;
7 #[cfg(feature = "full")]
8 use ts_rs::TS;
9
10 pub type LemmyResult<T> = Result<T, LemmyError>;
11
12 pub struct LemmyError {
13   pub error_type: LemmyErrorType,
14   pub inner: anyhow::Error,
15   pub context: SpanTrace,
16 }
17
18 impl<T> From<T> for LemmyError
19 where
20   T: Into<anyhow::Error>,
21 {
22   fn from(t: T) -> Self {
23     let cause = t.into();
24     LemmyError {
25       error_type: LemmyErrorType::Unknown(format!("{}", &cause)),
26       inner: cause,
27       context: SpanTrace::capture(),
28     }
29   }
30 }
31
32 impl Debug for LemmyError {
33   fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34     f.debug_struct("LemmyError")
35       .field("message", &self.error_type)
36       .field("inner", &self.inner)
37       .field("context", &self.context)
38       .finish()
39   }
40 }
41
42 impl Display for LemmyError {
43   fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
44     write!(f, "{}: ", &self.error_type)?;
45     // print anyhow including trace
46     // https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations
47     // this will print the anyhow trace (only if it exists)
48     // and if RUST_BACKTRACE=1, also a full backtrace
49     writeln!(f, "{:?}", self.inner)?;
50     fmt::Display::fmt(&self.context, f)
51   }
52 }
53
54 impl actix_web::error::ResponseError for LemmyError {
55   fn status_code(&self) -> http::StatusCode {
56     match self.inner.downcast_ref::<diesel::result::Error>() {
57       Some(diesel::result::Error::NotFound) => http::StatusCode::NOT_FOUND,
58       _ => http::StatusCode::BAD_REQUEST,
59     }
60   }
61
62   fn error_response(&self) -> actix_web::HttpResponse {
63     actix_web::HttpResponse::build(self.status_code()).json(&self.error_type)
64   }
65 }
66
67 #[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, EnumIter)]
68 #[cfg_attr(feature = "full", derive(TS))]
69 #[cfg_attr(feature = "full", ts(export))]
70 #[serde(tag = "error", content = "message", rename_all = "snake_case")]
71 // TODO: order these based on the crate they belong to (utils, federation, db, api)
72 pub enum LemmyErrorType {
73   ReportReasonRequired,
74   ReportTooLong,
75   NotAModerator,
76   NotAnAdmin,
77   CantBlockYourself,
78   CantBlockAdmin,
79   CouldntUpdateUser,
80   PasswordsDoNotMatch,
81   EmailNotVerified,
82   EmailRequired,
83   CouldntUpdateComment,
84   CouldntUpdatePrivateMessage,
85   CannotLeaveAdmin,
86   NoLinesInHtml,
87   SiteMetadataPageIsNotDoctypeHtml,
88   PictrsResponseError(String),
89   PictrsPurgeResponseError(String),
90   ImageUrlMissingPathSegments,
91   ImageUrlMissingLastPathSegment,
92   PictrsApiKeyNotProvided,
93   NoContentTypeHeader,
94   NotAnImageType,
95   NotAModOrAdmin,
96   NoAdmins,
97   NotTopAdmin,
98   NotTopMod,
99   NotLoggedIn,
100   SiteBan,
101   Deleted,
102   BannedFromCommunity,
103   CouldntFindCommunity,
104   CouldntFindPerson,
105   PersonIsBlocked,
106   DownvotesAreDisabled,
107   InstanceIsPrivate,
108   InvalidPassword,
109   SiteDescriptionLengthOverflow,
110   HoneypotFailed,
111   RegistrationApplicationIsPending,
112   CantEnablePrivateInstanceAndFederationTogether,
113   Locked,
114   CouldntCreateComment,
115   MaxCommentDepthReached,
116   NoCommentEditAllowed,
117   OnlyAdminsCanCreateCommunities,
118   CommunityAlreadyExists,
119   LanguageNotAllowed,
120   OnlyModsCanPostInCommunity,
121   CouldntUpdatePost,
122   NoPostEditAllowed,
123   CouldntFindPost,
124   EditPrivateMessageNotAllowed,
125   SiteAlreadyExists,
126   ApplicationQuestionRequired,
127   InvalidDefaultPostListingType,
128   RegistrationClosed,
129   RegistrationApplicationAnswerRequired,
130   EmailAlreadyExists,
131   FederationForbiddenByStrictAllowList,
132   PersonIsBannedFromCommunity,
133   ObjectIsNotPublic,
134   InvalidCommunity,
135   CannotCreatePostOrCommentInDeletedOrRemovedCommunity,
136   CannotReceivePage,
137   NewPostCannotBeLocked,
138   OnlyLocalAdminCanRemoveCommunity,
139   OnlyLocalAdminCanRestoreCommunity,
140   NoIdGiven,
141   IncorrectLogin,
142   InvalidQuery,
143   ObjectNotLocal,
144   PostIsLocked,
145   PersonIsBannedFromSite,
146   InvalidVoteValue,
147   PageDoesNotSpecifyCreator,
148   PageDoesNotSpecifyGroup,
149   NoCommunityFoundInCc,
150   NoEmailSetup,
151   EmailSmtpServerNeedsAPort,
152   MissingAnEmail,
153   RateLimitError,
154   InvalidName,
155   InvalidDisplayName,
156   InvalidMatrixId,
157   InvalidPostTitle,
158   InvalidBodyField,
159   BioLengthOverflow,
160   MissingTotpToken,
161   IncorrectTotpToken,
162   CouldntParseTotpSecret,
163   CouldntLikeComment,
164   CouldntSaveComment,
165   CouldntCreateReport,
166   CouldntResolveReport,
167   CommunityModeratorAlreadyExists,
168   CommunityUserAlreadyBanned,
169   CommunityBlockAlreadyExists,
170   CommunityFollowerAlreadyExists,
171   CouldntUpdateCommunityHiddenStatus,
172   PersonBlockAlreadyExists,
173   UserAlreadyExists,
174   TokenNotFound,
175   CouldntLikePost,
176   CouldntSavePost,
177   CouldntMarkPostAsRead,
178   CouldntUpdateCommunity,
179   CouldntUpdateReplies,
180   CouldntUpdatePersonMentions,
181   PostTitleTooLong,
182   CouldntCreatePost,
183   CouldntCreatePrivateMessage,
184   CouldntUpdatePrivate,
185   SystemErrLogin,
186   CouldntSetAllRegistrationsAccepted,
187   CouldntSetAllEmailVerified,
188   Banned,
189   CouldntGetComments,
190   CouldntGetPosts,
191   InvalidUrl,
192   EmailSendFailed,
193   Slurs,
194   CouldntGenerateTotp,
195   CouldntFindObject,
196   RegistrationDenied(String),
197   FederationDisabled,
198   DomainBlocked,
199   DomainNotInAllowList,
200   FederationDisabledByStrictAllowList,
201   SiteNameRequired,
202   SiteNameLengthOverflow,
203   PermissiveRegex,
204   InvalidRegex,
205   CaptchaIncorrect,
206   PasswordResetLimitReached,
207   CouldntCreateAudioCaptcha,
208   InvalidUrlScheme,
209   CouldntSendWebmention,
210   Unknown(String),
211 }
212
213 impl From<LemmyErrorType> for LemmyError {
214   fn from(error_type: LemmyErrorType) -> Self {
215     let inner = anyhow::anyhow!("{}", error_type);
216     LemmyError {
217       error_type,
218       inner,
219       context: SpanTrace::capture(),
220     }
221   }
222 }
223
224 pub trait LemmyErrorExt<T, E: Into<anyhow::Error>> {
225   fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>;
226 }
227
228 impl<T, E: Into<anyhow::Error>> LemmyErrorExt<T, E> for Result<T, E> {
229   fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
230     self.map_err(|error| LemmyError {
231       error_type,
232       inner: error.into(),
233       context: SpanTrace::capture(),
234     })
235   }
236 }
237 pub trait LemmyErrorExt2<T> {
238   fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>;
239 }
240
241 impl<T> LemmyErrorExt2<T> for Result<T, LemmyError> {
242   fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
243     self.map_err(|mut e| {
244       e.error_type = error_type;
245       e
246     })
247   }
248 }
249
250 #[cfg(test)]
251 mod tests {
252   use super::*;
253   use actix_web::{body::MessageBody, ResponseError};
254   use std::fs::read_to_string;
255   use strum::IntoEnumIterator;
256
257   #[test]
258   fn deserializes_no_message() {
259     let err = LemmyError::from(LemmyErrorType::Banned).error_response();
260     let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
261     assert_eq!(&json, "{\"error\":\"banned\"}")
262   }
263
264   #[test]
265   fn deserializes_with_message() {
266     let reg_denied = LemmyErrorType::RegistrationDenied(String::from("reason"));
267     let err = LemmyError::from(reg_denied).error_response();
268     let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
269     assert_eq!(
270       &json,
271       "{\"error\":\"registration_denied\",\"message\":\"reason\"}"
272     )
273   }
274
275   /// Check if errors match translations. Disabled because many are not translated at all.
276   #[test]
277   #[ignore]
278   fn test_translations_match() {
279     #[derive(Deserialize)]
280     struct Err {
281       error: String,
282     }
283
284     let translations = read_to_string("translations/translations/en.json").unwrap();
285     LemmyErrorType::iter().for_each(|e| {
286       let msg = serde_json::to_string(&e).unwrap();
287       let msg: Err = serde_json::from_str(&msg).unwrap();
288       let msg = msg.error;
289       assert!(translations.contains(&format!("\"{msg}\"")), "{msg}");
290     });
291   }
292 }