]> Untitled Git - lemmy.git/blob - crates/utils/src/error.rs
Add person name to PersonIsBannedFromSite error (#3786) (#3855)
[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(String),
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(String),
199   DomainNotInAllowList(String),
200   FederationDisabledByStrictAllowList,
201   SiteNameRequired,
202   SiteNameLengthOverflow,
203   PermissiveRegex,
204   InvalidRegex,
205   CaptchaIncorrect,
206   PasswordResetLimitReached,
207   CouldntCreateAudioCaptcha,
208   InvalidUrlScheme,
209   CouldntSendWebmention,
210   ContradictingFilters,
211   Unknown(String),
212 }
213
214 impl From<LemmyErrorType> for LemmyError {
215   fn from(error_type: LemmyErrorType) -> Self {
216     let inner = anyhow::anyhow!("{}", error_type);
217     LemmyError {
218       error_type,
219       inner,
220       context: SpanTrace::capture(),
221     }
222   }
223 }
224
225 pub trait LemmyErrorExt<T, E: Into<anyhow::Error>> {
226   fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>;
227 }
228
229 impl<T, E: Into<anyhow::Error>> LemmyErrorExt<T, E> for Result<T, E> {
230   fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
231     self.map_err(|error| LemmyError {
232       error_type,
233       inner: error.into(),
234       context: SpanTrace::capture(),
235     })
236   }
237 }
238 pub trait LemmyErrorExt2<T> {
239   fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError>;
240 }
241
242 impl<T> LemmyErrorExt2<T> for Result<T, LemmyError> {
243   fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
244     self.map_err(|mut e| {
245       e.error_type = error_type;
246       e
247     })
248   }
249 }
250
251 #[cfg(test)]
252 mod tests {
253   #![allow(clippy::unwrap_used)]
254   #![allow(clippy::indexing_slicing)]
255   use super::*;
256   use actix_web::{body::MessageBody, ResponseError};
257   use std::fs::read_to_string;
258   use strum::IntoEnumIterator;
259
260   #[test]
261   fn deserializes_no_message() {
262     let err = LemmyError::from(LemmyErrorType::Banned).error_response();
263     let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
264     assert_eq!(&json, "{\"error\":\"banned\"}")
265   }
266
267   #[test]
268   fn deserializes_with_message() {
269     let reg_denied = LemmyErrorType::RegistrationDenied(String::from("reason"));
270     let err = LemmyError::from(reg_denied).error_response();
271     let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
272     assert_eq!(
273       &json,
274       "{\"error\":\"registration_denied\",\"message\":\"reason\"}"
275     )
276   }
277
278   /// Check if errors match translations. Disabled because many are not translated at all.
279   #[test]
280   #[ignore]
281   fn test_translations_match() {
282     #[derive(Deserialize)]
283     struct Err {
284       error: String,
285     }
286
287     let translations = read_to_string("translations/translations/en.json").unwrap();
288     LemmyErrorType::iter().for_each(|e| {
289       let msg = serde_json::to_string(&e).unwrap();
290       let msg: Err = serde_json::from_str(&msg).unwrap();
291       let msg = msg.error;
292       assert!(translations.contains(&format!("\"{msg}\"")), "{msg}");
293     });
294   }
295 }