lemmy/crates/utils/src/error.rs
Nutomic ad90cd77f9
Implement private communities (#5076)
* 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>
2024-11-07 05:49:05 -05:00

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(())
}
}
}
}