Nutomic
ad90cd77f9
* add private visibility * filter private communities in post_view.rs * also filter in comment_view * community follower state * remove unused method * sql fmt * add CommunityFollower.approved_by * implement api endpoints * api changes * only admins can create private community for now * add local api tests * fix api tests * follow remote private community * use authorized fetch for content in private community * federate community visibility * dont mark content in private community as public * expose ApprovalRequired in api * also check content fetchable for outbox/featured * address private community content to followers * implement reject activity * fix tests * add files * remove local api tests * dont use delay * is_new_instance * single query for is_new_instance * return subscribed type for pending follow * working * need to catch errors in waitUntil * clippy * fix query * lint for unused async * diesel.toml comment * add comment * avoid db reads * rename approved_by to approver_id * add helper * form init * list pending follows should return items for all communities * clippy * ci * fix down migration * fix api tests * references * rename * run git diff * ci * fix schema check * fix joins * ci * ci * skip_serializing_none * fix test --------- Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
374 lines
11 KiB
Rust
374 lines
11 KiB
Rust
use cfg_if::cfg_if;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{backtrace::Backtrace, fmt::Debug};
|
|
use strum::{Display, EnumIter};
|
|
|
|
#[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, EnumIter, Hash)]
|
|
#[cfg_attr(feature = "full", derive(ts_rs::TS))]
|
|
#[cfg_attr(feature = "full", ts(export))]
|
|
#[serde(tag = "error", content = "message", rename_all = "snake_case")]
|
|
#[non_exhaustive]
|
|
// 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,
|
|
// TODO: also remove the translations of unused errors
|
|
PictrsResponseError(String),
|
|
PictrsPurgeResponseError(String),
|
|
ImageUrlMissingPathSegments,
|
|
ImageUrlMissingLastPathSegment,
|
|
PictrsApiKeyNotProvided,
|
|
NoContentTypeHeader,
|
|
NotAnImageType,
|
|
NotAModOrAdmin,
|
|
NotTopMod,
|
|
NotLoggedIn,
|
|
NotHigherMod,
|
|
NotHigherAdmin,
|
|
SiteBan,
|
|
Deleted,
|
|
PersonIsBlocked,
|
|
CommunityIsBlocked,
|
|
InstanceIsBlocked,
|
|
InstanceIsPrivate,
|
|
/// Password must be between 10 and 60 characters
|
|
InvalidPassword,
|
|
SiteDescriptionLengthOverflow,
|
|
HoneypotFailed,
|
|
RegistrationApplicationIsPending,
|
|
CantEnablePrivateInstanceAndFederationTogether,
|
|
Locked,
|
|
CouldntCreateComment,
|
|
MaxCommentDepthReached,
|
|
NoCommentEditAllowed,
|
|
OnlyAdminsCanCreateCommunities,
|
|
CommunityAlreadyExists,
|
|
LanguageNotAllowed,
|
|
CouldntUpdatePost,
|
|
NoPostEditAllowed,
|
|
EditPrivateMessageNotAllowed,
|
|
SiteAlreadyExists,
|
|
ApplicationQuestionRequired,
|
|
InvalidDefaultPostListingType,
|
|
RegistrationClosed,
|
|
RegistrationApplicationAnswerRequired,
|
|
RegistrationUsernameRequired,
|
|
EmailAlreadyExists,
|
|
UsernameAlreadyExists,
|
|
PersonIsBannedFromCommunity,
|
|
NoIdGiven,
|
|
IncorrectLogin,
|
|
ObjectNotLocal,
|
|
NoEmailSetup,
|
|
LocalSiteNotSetup,
|
|
EmailSmtpServerNeedsAPort,
|
|
MissingAnEmail,
|
|
RateLimitError,
|
|
InvalidName,
|
|
InvalidDisplayName,
|
|
InvalidMatrixId,
|
|
InvalidPostTitle,
|
|
InvalidBodyField,
|
|
BioLengthOverflow,
|
|
AltTextLengthOverflow,
|
|
MissingTotpToken,
|
|
MissingTotpSecret,
|
|
IncorrectTotpToken,
|
|
CouldntParseTotpSecret,
|
|
CouldntGenerateTotp,
|
|
TotpAlreadyEnabled,
|
|
CouldntLikeComment,
|
|
CouldntSaveComment,
|
|
CouldntCreateReport,
|
|
CouldntResolveReport,
|
|
CommunityModeratorAlreadyExists,
|
|
CommunityUserAlreadyBanned,
|
|
CommunityBlockAlreadyExists,
|
|
CommunityFollowerAlreadyExists,
|
|
CouldntUpdateCommunityHiddenStatus,
|
|
PersonBlockAlreadyExists,
|
|
UserAlreadyExists,
|
|
CouldntLikePost,
|
|
CouldntSavePost,
|
|
CouldntMarkPostAsRead,
|
|
CouldntHidePost,
|
|
CouldntUpdateCommunity,
|
|
CouldntUpdateReplies,
|
|
CouldntUpdatePersonMentions,
|
|
CouldntCreatePost,
|
|
CouldntCreatePrivateMessage,
|
|
CouldntUpdatePrivate,
|
|
SystemErrLogin,
|
|
CouldntSetAllRegistrationsAccepted,
|
|
CouldntSetAllEmailVerified,
|
|
Banned,
|
|
BlockedUrl,
|
|
CouldntGetComments,
|
|
CouldntGetPosts,
|
|
InvalidUrl,
|
|
EmailSendFailed,
|
|
Slurs,
|
|
RegistrationDenied {
|
|
#[cfg_attr(feature = "full", ts(optional))]
|
|
reason: Option<String>,
|
|
},
|
|
SiteNameRequired,
|
|
SiteNameLengthOverflow,
|
|
PermissiveRegex,
|
|
InvalidRegex,
|
|
CaptchaIncorrect,
|
|
CouldntCreateAudioCaptcha,
|
|
InvalidUrlScheme,
|
|
CouldntSendWebmention,
|
|
ContradictingFilters,
|
|
InstanceBlockAlreadyExists,
|
|
/// Thrown when an API call is submitted with more than 1000 array elements, see
|
|
/// [[MAX_API_PARAM_ELEMENTS]]
|
|
TooManyItems,
|
|
BanExpirationInPast,
|
|
InvalidUnixTime,
|
|
InvalidBotAction,
|
|
CantBlockLocalInstance,
|
|
Unknown(String),
|
|
UrlLengthOverflow,
|
|
OauthAuthorizationInvalid,
|
|
OauthLoginFailed,
|
|
OauthRegistrationClosed,
|
|
CouldntDeleteOauthProvider,
|
|
NotFound,
|
|
CommunityHasNoFollowers,
|
|
PostScheduleTimeMustBeInFuture,
|
|
TooManyScheduledPosts,
|
|
FederationError {
|
|
#[cfg_attr(feature = "full", ts(optional))]
|
|
error: Option<FederationError>,
|
|
},
|
|
}
|
|
|
|
/// Federation related errors, these dont need to be translated.
|
|
#[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, EnumIter, Hash)]
|
|
#[cfg_attr(feature = "full", derive(ts_rs::TS))]
|
|
#[cfg_attr(feature = "full", ts(export))]
|
|
#[non_exhaustive]
|
|
pub enum FederationError {
|
|
// TODO: merge into a single NotFound error
|
|
CouldntFindActivity,
|
|
InvalidCommunity,
|
|
CannotCreatePostOrCommentInDeletedOrRemovedCommunity,
|
|
CannotReceivePage,
|
|
OnlyLocalAdminCanRemoveCommunity,
|
|
OnlyLocalAdminCanRestoreCommunity,
|
|
PostIsLocked,
|
|
PersonIsBannedFromSite(String),
|
|
InvalidVoteValue,
|
|
PageDoesNotSpecifyCreator,
|
|
CouldntGetComments,
|
|
CouldntGetPosts,
|
|
FederationDisabled,
|
|
DomainBlocked(String),
|
|
DomainNotInAllowList(String),
|
|
FederationDisabledByStrictAllowList,
|
|
ContradictingFilters,
|
|
UrlWithoutDomain,
|
|
InboxTimeout,
|
|
CantDeleteSite,
|
|
ObjectIsNotPublic,
|
|
ObjectIsNotPrivate,
|
|
}
|
|
|
|
cfg_if! {
|
|
if #[cfg(feature = "full")] {
|
|
|
|
use std::fmt;
|
|
pub type LemmyResult<T> = Result<T, LemmyError>;
|
|
|
|
pub struct LemmyError {
|
|
pub error_type: LemmyErrorType,
|
|
pub inner: anyhow::Error,
|
|
pub context: Backtrace,
|
|
}
|
|
|
|
/// Maximum number of items in an array passed as API parameter. See [[LemmyErrorType::TooManyItems]]
|
|
pub const MAX_API_PARAM_ELEMENTS: usize = 10_000;
|
|
|
|
impl<T> From<T> for LemmyError
|
|
where
|
|
T: Into<anyhow::Error>,
|
|
{
|
|
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,
|
|
inner: cause,
|
|
context: Backtrace::capture(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Debug for LemmyError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("LemmyError")
|
|
.field("message", &self.error_type)
|
|
.field("inner", &self.inner)
|
|
.field("context", &self.context)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for LemmyError {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
write!(f, "{}: ", &self.error_type)?;
|
|
writeln!(f, "{}", self.inner)?;
|
|
fmt::Display::fmt(&self.context, f)
|
|
}
|
|
}
|
|
|
|
impl actix_web::error::ResponseError for LemmyError {
|
|
fn status_code(&self) -> actix_web::http::StatusCode {
|
|
if self.error_type == LemmyErrorType::IncorrectLogin {
|
|
return actix_web::http::StatusCode::UNAUTHORIZED;
|
|
}
|
|
match self.inner.downcast_ref::<diesel::result::Error>() {
|
|
Some(diesel::result::Error::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.error_type)
|
|
}
|
|
}
|
|
|
|
impl From<LemmyErrorType> for LemmyError {
|
|
fn from(error_type: LemmyErrorType) -> Self {
|
|
let inner = anyhow::anyhow!("{}", error_type);
|
|
LemmyError {
|
|
error_type,
|
|
inner,
|
|
context: Backtrace::capture(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<FederationError> for LemmyError {
|
|
fn from(error_type: FederationError) -> Self {
|
|
let inner = anyhow::anyhow!("{}", error_type);
|
|
LemmyError {
|
|
error_type: LemmyErrorType::FederationError { error: Some(error_type) },
|
|
inner,
|
|
context: Backtrace::capture(),
|
|
}
|
|
}
|
|
}
|
|
|
|
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> {
|
|
fn with_lemmy_type(self, error_type: LemmyErrorType) -> LemmyResult<T> {
|
|
self.map_err(|error| LemmyError {
|
|
error_type,
|
|
inner: error.into(),
|
|
context: Backtrace::capture(),
|
|
})
|
|
}
|
|
}
|
|
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
|
|
})
|
|
}
|
|
// this function can't be an impl From or similar because it would conflict with one of the other broad Into<> implementations
|
|
fn into_anyhow(self) -> Result<T, anyhow::Error> {
|
|
self.map_err(|e| e.inner)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#![allow(clippy::indexing_slicing)]
|
|
use super::*;
|
|
use actix_web::{body::MessageBody, ResponseError};
|
|
use pretty_assertions::assert_eq;
|
|
use std::fs::read_to_string;
|
|
use strum::IntoEnumIterator;
|
|
|
|
#[test]
|
|
fn deserializes_no_message() -> LemmyResult<()> {
|
|
let err = LemmyError::from(LemmyErrorType::Banned).error_response();
|
|
let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?;
|
|
assert_eq!(&json, "{\"error\":\"banned\"}");
|
|
|
|
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,
|
|
"{\"error\":\"pictrs_response_error\",\"message\":\"reason\"}"
|
|
);
|
|
|
|
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());
|
|
}
|
|
|
|
/// Check if errors match translations. Disabled because many are not translated at all.
|
|
#[test]
|
|
#[ignore]
|
|
fn test_translations_match() -> LemmyResult<()> {
|
|
#[derive(Deserialize)]
|
|
struct Err {
|
|
error: String,
|
|
}
|
|
|
|
let translations = read_to_string("translations/translations/en.json")?;
|
|
|
|
for e in LemmyErrorType::iter() {
|
|
let msg = serde_json::to_string(&e)?;
|
|
let msg: Err = serde_json::from_str(&msg)?;
|
|
let msg = msg.error;
|
|
assert!(translations.contains(&format!("\"{msg}\"")), "{msg}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
}
|