From 56e26fc3d4325ca2179207392414051813e17842 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 31 Aug 2023 09:01:08 -0400 Subject: [PATCH] Adding a post_view mode. Fixes #3730 (#3731) * Adding a post_view mode. Fixes #3730 * Fix test. * Addressing PR comments. * Adding a post_view mode. Fixes #3730 * Fix test. * Addressing PR comments. * Fixing column order. * Fix default Ok returns. * Removing return Err(... where feasible. --- crates/api/src/community/add_mod.rs | 2 +- crates/api/src/community/transfer.rs | 2 +- crates/api/src/lib.rs | 34 +++++++------- crates/api/src/local_user/block.rs | 4 +- crates/api/src/local_user/change_password.rs | 4 +- .../local_user/change_password_after_reset.rs | 2 +- crates/api/src/local_user/login.rs | 4 +- .../notifications/mark_mention_read.rs | 2 +- .../notifications/mark_reply_read.rs | 2 +- crates/api/src/local_user/reset_password.rs | 2 +- crates/api/src/local_user/save_settings.rs | 2 +- crates/api/src/private_message/mark_read.rs | 2 +- crates/api/src/site/leave_admin.rs | 2 +- crates/api_common/src/request.rs | 2 +- crates/api_common/src/utils.rs | 47 ++++++++++--------- crates/api_crud/src/comment/create.rs | 4 +- crates/api_crud/src/comment/delete.rs | 4 +- crates/api_crud/src/comment/update.rs | 2 +- crates/api_crud/src/community/create.rs | 6 +-- crates/api_crud/src/community/update.rs | 4 +- crates/api_crud/src/post/create.rs | 2 +- crates/api_crud/src/post/delete.rs | 4 +- crates/api_crud/src/post/update.rs | 2 +- crates/api_crud/src/private_message/delete.rs | 2 +- crates/api_crud/src/private_message/update.rs | 2 +- crates/api_crud/src/site/create.rs | 2 +- crates/api_crud/src/user/create.rs | 14 +++--- crates/api_crud/src/user/delete.rs | 2 +- .../apub/src/activities/community/announce.rs | 4 +- .../src/activities/create_or_update/post.rs | 2 +- crates/apub/src/activities/deletion/delete.rs | 2 +- .../src/activities/deletion/undo_delete.rs | 2 +- crates/apub/src/activities/mod.rs | 27 ++++++----- crates/apub/src/activities/voting/vote.rs | 5 +- crates/apub/src/api/read_community.rs | 2 +- crates/apub/src/api/read_person.rs | 4 +- crates/apub/src/api/resolve_object.rs | 5 +- crates/apub/src/http/comment.rs | 6 +-- crates/apub/src/http/community.rs | 6 +-- crates/apub/src/http/post.rs | 6 +-- crates/apub/src/lib.rs | 8 ++-- crates/apub/src/objects/comment.rs | 5 +- crates/apub/src/objects/private_message.rs | 7 +-- crates/apub/src/protocol/objects/page.rs | 2 +- crates/db_schema/src/diesel_ltree.patch | 17 ------- crates/db_schema/src/lib.rs | 18 +++++++ crates/db_schema/src/schema.rs | 10 ++++ crates/db_schema/src/source/local_user.rs | 4 ++ .../src/registration_application_view.rs | 1 + crates/utils/src/settings/mod.rs | 6 +-- crates/utils/src/utils/validation.rs | 27 ++++++----- .../down.sql | 5 ++ .../up.sql | 9 ++++ 53 files changed, 198 insertions(+), 155 deletions(-) create mode 100644 migrations/2023-08-08-163911_add_post_listing_mode_setting/down.sql create mode 100644 migrations/2023-08-08-163911_add_post_listing_mode_setting/up.sql diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs index 937af22df..f5fac1503 100644 --- a/crates/api/src/community/add_mod.rs +++ b/crates/api/src/community/add_mod.rs @@ -29,7 +29,7 @@ pub async fn add_mod_to_community( is_mod_or_admin(&mut context.pool(), local_user_view.person.id, community_id).await?; let community = Community::read(&mut context.pool(), community_id).await?; if local_user_view.local_user.admin && !community.local { - return Err(LemmyErrorType::NotAModerator)?; + Err(LemmyErrorType::NotAModerator)? } // Update in local database diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs index c63c25750..351389941 100644 --- a/crates/api/src/community/transfer.rs +++ b/crates/api/src/community/transfer.rs @@ -42,7 +42,7 @@ impl Perform for TransferCommunity { if !(is_top_mod(&local_user_view, &community_mods).is_ok() || is_admin(&local_user_view).is_ok()) { - return Err(LemmyErrorType::NotAnAdmin)?; + Err(LemmyErrorType::NotAnAdmin)? } // You have to re-do the community_moderator table, reordering it. diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index daec7cbc6..8a45b41c9 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -41,24 +41,24 @@ pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> Result header, - None => return Err(LemmyErrorType::CouldntCreateAudioCaptcha)?, - }; - wav::write( - header, - &wav::BitDepth::Sixteen(concat_samples), - &mut output_buffer, - ) - .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; + if let Some(header) = any_header { + wav::write( + header, + &wav::BitDepth::Sixteen(concat_samples), + &mut output_buffer, + ) + .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; - Ok(base64.encode(output_buffer.into_inner())) + Ok(base64.encode(output_buffer.into_inner())) + } else { + Err(LemmyErrorType::CouldntCreateAudioCaptcha)? + } } /// Check size of report @@ -67,12 +67,12 @@ pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Resul check_slurs(reason, slur_regex)?; if reason.is_empty() { - Err(LemmyErrorType::ReportReasonRequired)?; + Err(LemmyErrorType::ReportReasonRequired)? + } else if reason.chars().count() > 1000 { + Err(LemmyErrorType::ReportTooLong)? + } else { + Ok(()) } - if reason.chars().count() > 1000 { - Err(LemmyErrorType::ReportTooLong)?; - } - Ok(()) } #[cfg(test)] diff --git a/crates/api/src/local_user/block.rs b/crates/api/src/local_user/block.rs index 395ab0a15..3d7acd4ba 100644 --- a/crates/api/src/local_user/block.rs +++ b/crates/api/src/local_user/block.rs @@ -27,7 +27,7 @@ impl Perform for BlockPerson { // Don't let a person block themselves if target_id == person_id { - return Err(LemmyErrorType::CantBlockYourself)?; + Err(LemmyErrorType::CantBlockYourself)? } let person_block_form = PersonBlockForm { @@ -37,7 +37,7 @@ impl Perform for BlockPerson { let target_user = LocalUserView::read_person(&mut context.pool(), target_id).await; if target_user.map(|t| t.local_user.admin) == Ok(true) { - return Err(LemmyErrorType::CantBlockAdmin)?; + Err(LemmyErrorType::CantBlockAdmin)? } if data.block { diff --git a/crates/api/src/local_user/change_password.rs b/crates/api/src/local_user/change_password.rs index ea6f9b3e6..fa5797514 100644 --- a/crates/api/src/local_user/change_password.rs +++ b/crates/api/src/local_user/change_password.rs @@ -25,7 +25,7 @@ impl Perform for ChangePassword { // Make sure passwords match if data.new_password != data.new_password_verify { - return Err(LemmyErrorType::PasswordsDoNotMatch)?; + Err(LemmyErrorType::PasswordsDoNotMatch)? } // Check the old password @@ -35,7 +35,7 @@ impl Perform for ChangePassword { ) .unwrap_or(false); if !valid { - return Err(LemmyErrorType::IncorrectLogin)?; + Err(LemmyErrorType::IncorrectLogin)? } let local_user_id = local_user_view.local_user.id; diff --git a/crates/api/src/local_user/change_password_after_reset.rs b/crates/api/src/local_user/change_password_after_reset.rs index 65587bcbf..f89d5f78f 100644 --- a/crates/api/src/local_user/change_password_after_reset.rs +++ b/crates/api/src/local_user/change_password_after_reset.rs @@ -29,7 +29,7 @@ impl Perform for PasswordChangeAfterReset { // Make sure passwords match if data.password != data.password_verify { - return Err(LemmyErrorType::PasswordsDoNotMatch)?; + Err(LemmyErrorType::PasswordsDoNotMatch)? } // Update the user with the new password diff --git a/crates/api/src/local_user/login.rs b/crates/api/src/local_user/login.rs index 62449c424..9b71f74c3 100644 --- a/crates/api/src/local_user/login.rs +++ b/crates/api/src/local_user/login.rs @@ -37,7 +37,7 @@ impl Perform for Login { ) .unwrap_or(false); if !valid { - return Err(LemmyErrorType::IncorrectLogin)?; + Err(LemmyErrorType::IncorrectLogin)? } check_user_valid( local_user_view.person.banned, @@ -51,7 +51,7 @@ impl Perform for Login { && site_view.local_site.require_email_verification && !local_user_view.local_user.email_verified { - return Err(LemmyErrorType::EmailNotVerified)?; + Err(LemmyErrorType::EmailNotVerified)? } check_registration_application(&local_user_view, &site_view.local_site, &mut context.pool()) diff --git a/crates/api/src/local_user/notifications/mark_mention_read.rs b/crates/api/src/local_user/notifications/mark_mention_read.rs index 35feea645..95233213b 100644 --- a/crates/api/src/local_user/notifications/mark_mention_read.rs +++ b/crates/api/src/local_user/notifications/mark_mention_read.rs @@ -28,7 +28,7 @@ impl Perform for MarkPersonMentionAsRead { let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id).await?; if local_user_view.person.id != read_person_mention.recipient_id { - return Err(LemmyErrorType::CouldntUpdateComment)?; + Err(LemmyErrorType::CouldntUpdateComment)? } let person_mention_id = read_person_mention.id; diff --git a/crates/api/src/local_user/notifications/mark_reply_read.rs b/crates/api/src/local_user/notifications/mark_reply_read.rs index 9ae9f5251..4439a6cfb 100644 --- a/crates/api/src/local_user/notifications/mark_reply_read.rs +++ b/crates/api/src/local_user/notifications/mark_reply_read.rs @@ -22,7 +22,7 @@ pub async fn mark_reply_as_read( let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?; if local_user_view.person.id != read_comment_reply.recipient_id { - return Err(LemmyErrorType::CouldntUpdateComment)?; + Err(LemmyErrorType::CouldntUpdateComment)? } let comment_reply_id = read_comment_reply.id; diff --git a/crates/api/src/local_user/reset_password.rs b/crates/api/src/local_user/reset_password.rs index 348035c24..6f45a8ffd 100644 --- a/crates/api/src/local_user/reset_password.rs +++ b/crates/api/src/local_user/reset_password.rs @@ -33,7 +33,7 @@ impl Perform for PasswordReset { ) .await?; if recent_resets_count >= 3 { - return Err(LemmyErrorType::PasswordResetLimitReached)?; + Err(LemmyErrorType::PasswordResetLimitReached)? } // Email the pure token to the user. diff --git a/crates/api/src/local_user/save_settings.rs b/crates/api/src/local_user/save_settings.rs index 946b5e8b6..40328ff31 100644 --- a/crates/api/src/local_user/save_settings.rs +++ b/crates/api/src/local_user/save_settings.rs @@ -65,7 +65,7 @@ impl Perform for SaveUserSettings { // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value if let Some(email) = &email { if email.is_none() && site_view.local_site.require_email_verification { - return Err(LemmyErrorType::EmailRequired)?; + Err(LemmyErrorType::EmailRequired)? } } diff --git a/crates/api/src/private_message/mark_read.rs b/crates/api/src/private_message/mark_read.rs index 54fb4790c..e88ced04f 100644 --- a/crates/api/src/private_message/mark_read.rs +++ b/crates/api/src/private_message/mark_read.rs @@ -29,7 +29,7 @@ impl Perform for MarkPrivateMessageAsRead { let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; if local_user_view.person.id != orig_private_message.recipient_id { - return Err(LemmyErrorType::CouldntUpdatePrivateMessage)?; + Err(LemmyErrorType::CouldntUpdatePrivateMessage)? } // Doing the update diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index 13e1c05dd..432e3eb14 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -36,7 +36,7 @@ impl Perform for LeaveAdmin { // Make sure there isn't just one admin (so if one leaves, there will still be one left) let admins = PersonView::admins(&mut context.pool()).await?; if admins.len() == 1 { - return Err(LemmyErrorType::CannotLeaveAdmin)?; + Err(LemmyErrorType::CannotLeaveAdmin)? } LocalUser::update( diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index 8fe302741..90364a978 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -44,7 +44,7 @@ fn html_to_site_metadata(html_bytes: &[u8], url: &Url) -> Result") { - Err(LemmyErrorType::SiteMetadataPageIsNotDoctypeHtml)?; + Err(LemmyErrorType::SiteMetadataPageIsNotDoctypeHtml)? } let mut page = HTML::from_string(html.to_string(), None)?; diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 61cd2a26a..3a1f1f945 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -54,9 +54,10 @@ pub async fn is_mod_or_admin( ) -> Result<(), LemmyError> { let is_mod_or_admin = CommunityView::is_mod_or_admin(pool, person_id, community_id).await?; if !is_mod_or_admin { - return Err(LemmyErrorType::NotAModOrAdmin)?; + Err(LemmyErrorType::NotAModOrAdmin)? + } else { + Ok(()) } - Ok(()) } #[tracing::instrument(skip_all)] @@ -78,9 +79,10 @@ pub async fn is_mod_or_admin_opt( pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> { if !local_user_view.local_user.admin { - return Err(LemmyErrorType::NotAnAdmin)?; + Err(LemmyErrorType::NotAnAdmin)? + } else { + Ok(()) } - Ok(()) } pub fn is_top_mod( @@ -93,9 +95,10 @@ pub fn is_top_mod( .map(|cm| cm.moderator.id) .unwrap_or(PersonId(0)) { - Err(LemmyErrorType::NotTopMod)?; + Err(LemmyErrorType::NotTopMod)? + } else { + Ok(()) } - Ok(()) } #[tracing::instrument(skip_all)] @@ -190,15 +193,14 @@ pub fn check_user_valid( ) -> Result<(), LemmyError> { // Check for a site ban if is_banned(banned, ban_expires) { - Err(LemmyErrorType::SiteBan)?; + Err(LemmyErrorType::SiteBan)? } - // check for account deletion - if deleted { - Err(LemmyErrorType::Deleted)?; + else if deleted { + Err(LemmyErrorType::Deleted)? + } else { + Ok(()) } - - Ok(()) } #[tracing::instrument(skip_all)] @@ -259,9 +261,10 @@ pub async fn check_person_block( #[tracing::instrument(skip_all)] pub fn check_downvotes_enabled(score: i16, local_site: &LocalSite) -> Result<(), LemmyError> { if score == -1 && !local_site.enable_downvotes { - Err(LemmyErrorType::DownvotesAreDisabled)?; + Err(LemmyErrorType::DownvotesAreDisabled)? + } else { + Ok(()) } - Ok(()) } #[tracing::instrument(skip_all)] @@ -270,9 +273,10 @@ pub fn check_private_instance( local_site: &LocalSite, ) -> Result<(), LemmyError> { if local_user_view.is_none() && local_site.private_instance { - Err(LemmyErrorType::InstanceIsPrivate)?; + Err(LemmyErrorType::InstanceIsPrivate)? + } else { + Ok(()) } - Ok(()) } #[tracing::instrument(skip_all)] @@ -517,11 +521,11 @@ pub async fn check_registration_application( if let Some(deny_reason) = registration.deny_reason { let lang = get_interface_language(local_user_view); let registration_denied_message = format!("{}: {}", lang.registration_denied(), deny_reason); - return Err(LemmyErrorType::RegistrationDenied( + Err(LemmyErrorType::RegistrationDenied( registration_denied_message, - ))?; + ))? } else { - return Err(LemmyErrorType::RegistrationApplicationIsPending)?; + Err(LemmyErrorType::RegistrationApplicationIsPending)? } } Ok(()) @@ -531,9 +535,10 @@ pub fn check_private_instance_and_federation_enabled( local_site: &LocalSite, ) -> Result<(), LemmyError> { if local_site.private_instance && local_site.federation_enabled { - Err(LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether)?; + Err(LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether)? + } else { + Ok(()) } - Ok(()) } pub async fn purge_image_posts_for_person( diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index 33e480a9b..00762a31b 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -65,7 +65,7 @@ pub async fn create_comment( // Check if post is locked, no new comments if post.locked { - return Err(LemmyErrorType::Locked)?; + Err(LemmyErrorType::Locked)? } // Fetch the parent, if it exists @@ -79,7 +79,7 @@ pub async fn create_comment( // Strange issue where sometimes the post ID of the parent comment is incorrect if let Some(parent) = parent_opt.as_ref() { if parent.post_id != post_id { - return Err(LemmyErrorType::CouldntCreateComment)?; + Err(LemmyErrorType::CouldntCreateComment)? } check_comment_depth(parent)?; } diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs index 96c22b604..5f5db905e 100644 --- a/crates/api_crud/src/comment/delete.rs +++ b/crates/api_crud/src/comment/delete.rs @@ -29,7 +29,7 @@ pub async fn delete_comment( // Dont delete it if its already been deleted. if orig_comment.comment.deleted == data.deleted { - return Err(LemmyErrorType::CouldntUpdateComment)?; + Err(LemmyErrorType::CouldntUpdateComment)? } check_community_ban( @@ -41,7 +41,7 @@ pub async fn delete_comment( // Verify that only the creator can delete if local_user_view.person.id != orig_comment.creator.id { - return Err(LemmyErrorType::NoCommentEditAllowed)?; + Err(LemmyErrorType::NoCommentEditAllowed)? } // Do the delete diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index a07ee4548..16f56092f 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -51,7 +51,7 @@ pub async fn update_comment( // Verify that only the creator can edit if local_user_view.person.id != orig_comment.creator.id { - return Err(LemmyErrorType::NoCommentEditAllowed)?; + Err(LemmyErrorType::NoCommentEditAllowed)? } let language_id = data.language_id; diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs index 7bfeabd6d..ef6a317e9 100644 --- a/crates/api_crud/src/community/create.rs +++ b/crates/api_crud/src/community/create.rs @@ -51,7 +51,7 @@ pub async fn create_community( let local_site = site_view.local_site; if local_site.community_creation_admin_only && is_admin(&local_user_view).is_err() { - return Err(LemmyErrorType::OnlyAdminsCanCreateCommunities)?; + Err(LemmyErrorType::OnlyAdminsCanCreateCommunities)? } // Check to make sure the icon and banners are urls @@ -79,7 +79,7 @@ pub async fn create_community( let community_dupe = Community::read_from_apub_id(&mut context.pool(), &community_actor_id).await?; if community_dupe.is_some() { - return Err(LemmyErrorType::CommunityAlreadyExists)?; + Err(LemmyErrorType::CommunityAlreadyExists)? } // When you create a community, make sure the user becomes a moderator and a follower @@ -135,7 +135,7 @@ pub async fn create_community( // https://stackoverflow.com/a/64227550 let is_subset = languages.iter().all(|item| site_languages.contains(item)); if !is_subset { - return Err(LemmyErrorType::LanguageNotAllowed)?; + Err(LemmyErrorType::LanguageNotAllowed)? } CommunityLanguage::update(&mut context.pool(), languages, community_id).await?; } diff --git a/crates/api_crud/src/community/update.rs b/crates/api_crud/src/community/update.rs index cf53ff891..b55693598 100644 --- a/crates/api_crud/src/community/update.rs +++ b/crates/api_crud/src/community/update.rs @@ -50,7 +50,7 @@ pub async fn update_community( .await .map(|v| v.into_iter().map(|m| m.moderator.id).collect())?; if !mods.contains(&local_user_view.person.id) { - return Err(LemmyErrorType::NotAModerator)?; + Err(LemmyErrorType::NotAModerator)? } let community_id = data.community_id; @@ -60,7 +60,7 @@ pub async fn update_community( // https://stackoverflow.com/a/64227550 let is_subset = languages.iter().all(|item| site_languages.contains(item)); if !is_subset { - return Err(LemmyErrorType::LanguageNotAllowed)?; + Err(LemmyErrorType::LanguageNotAllowed)? } CommunityLanguage::update(&mut context.pool(), languages, community_id).await?; } diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 07e2cc27b..9d27f5d62 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -82,7 +82,7 @@ pub async fn create_post( ) .await?; if !is_mod { - return Err(LemmyErrorType::OnlyModsCanPostInCommunity)?; + Err(LemmyErrorType::OnlyModsCanPostInCommunity)? } } diff --git a/crates/api_crud/src/post/delete.rs b/crates/api_crud/src/post/delete.rs index 79c8af0f0..0f471a527 100644 --- a/crates/api_crud/src/post/delete.rs +++ b/crates/api_crud/src/post/delete.rs @@ -25,7 +25,7 @@ pub async fn delete_post( // Dont delete it if its already been deleted. if orig_post.deleted == data.deleted { - return Err(LemmyErrorType::CouldntUpdatePost)?; + Err(LemmyErrorType::CouldntUpdatePost)? } check_community_ban( @@ -38,7 +38,7 @@ pub async fn delete_post( // Verify that only the creator can delete if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) { - return Err(LemmyErrorType::NoPostEditAllowed)?; + Err(LemmyErrorType::NoPostEditAllowed)? } // Update the post diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index a0f56aeb9..c475d7a26 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -68,7 +68,7 @@ pub async fn update_post( // Verify that only the creator can edit if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) { - return Err(LemmyErrorType::NoPostEditAllowed)?; + Err(LemmyErrorType::NoPostEditAllowed)? } // Fetch post links and Pictrs cached image diff --git a/crates/api_crud/src/private_message/delete.rs b/crates/api_crud/src/private_message/delete.rs index 2a50dc0a2..65db3b1c1 100644 --- a/crates/api_crud/src/private_message/delete.rs +++ b/crates/api_crud/src/private_message/delete.rs @@ -24,7 +24,7 @@ pub async fn delete_private_message( let private_message_id = data.private_message_id; let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; if local_user_view.person.id != orig_private_message.creator_id { - return Err(LemmyErrorType::EditPrivateMessageNotAllowed)?; + Err(LemmyErrorType::EditPrivateMessageNotAllowed)? } // Doing the update diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index d9e53776d..44cc1ab3b 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -32,7 +32,7 @@ pub async fn update_private_message( let private_message_id = data.private_message_id; let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; if local_user_view.person.id != orig_private_message.creator_id { - return Err(LemmyErrorType::EditPrivateMessageNotAllowed)?; + Err(LemmyErrorType::EditPrivateMessageNotAllowed)? } // Doing the update diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 38d6111ce..78bbb64ef 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -147,7 +147,7 @@ pub async fn create_site( fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) -> LemmyResult<()> { // Make sure the site hasn't already been set up... if local_site.site_setup { - Err(LemmyErrorType::SiteAlreadyExists)?; + Err(LemmyErrorType::SiteAlreadyExists)? }; // Check that the slur regex compiles, and returns the regex if valid... diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 8475dfa9d..d855434ec 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -48,23 +48,23 @@ pub async fn register( local_site.registration_mode == RegistrationMode::RequireApplication; if local_site.registration_mode == RegistrationMode::Closed { - return Err(LemmyErrorType::RegistrationClosed)?; + Err(LemmyErrorType::RegistrationClosed)? } password_length_check(&data.password)?; honeypot_check(&data.honeypot)?; if local_site.require_email_verification && data.email.is_none() { - return Err(LemmyErrorType::EmailRequired)?; + Err(LemmyErrorType::EmailRequired)? } if local_site.site_setup && require_registration_application && data.answer.is_none() { - return Err(LemmyErrorType::RegistrationApplicationAnswerRequired)?; + Err(LemmyErrorType::RegistrationApplicationAnswerRequired)? } // Make sure passwords match if data.password != data.password_verify { - return Err(LemmyErrorType::PasswordsDoNotMatch)?; + Err(LemmyErrorType::PasswordsDoNotMatch)? } if local_site.site_setup && local_site.captcha_enabled { @@ -79,10 +79,10 @@ pub async fn register( ) .await?; if !check { - return Err(LemmyErrorType::CaptchaIncorrect)?; + Err(LemmyErrorType::CaptchaIncorrect)? } } else { - return Err(LemmyErrorType::CaptchaIncorrect)?; + Err(LemmyErrorType::CaptchaIncorrect)? } } @@ -101,7 +101,7 @@ pub async fn register( if let Some(email) = &data.email { if LocalUser::is_email_taken(&mut context.pool(), email).await? { - return Err(LemmyErrorType::EmailAlreadyExists)?; + Err(LemmyErrorType::EmailAlreadyExists)? } } diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs index 20796c34b..bf1dcdab1 100644 --- a/crates/api_crud/src/user/delete.rs +++ b/crates/api_crud/src/user/delete.rs @@ -24,7 +24,7 @@ pub async fn delete_account( ) .unwrap_or(false); if !valid { - return Err(LemmyErrorType::IncorrectLogin)?; + Err(LemmyErrorType::IncorrectLogin)? } if data.delete_content { diff --git a/crates/apub/src/activities/community/announce.rs b/crates/apub/src/activities/community/announce.rs index 6eb23f8da..9ead36188 100644 --- a/crates/apub/src/activities/community/announce.rs +++ b/crates/apub/src/activities/community/announce.rs @@ -48,7 +48,7 @@ impl ActivityHandler for RawAnnouncableActivities { let activity: AnnouncableActivities = self.clone().try_into()?; // This is only for sending, not receiving so we reject it. if let AnnouncableActivities::Page(_) = activity { - return Err(LemmyErrorType::CannotReceivePage)?; + Err(LemmyErrorType::CannotReceivePage)? } // verify and receive activity @@ -146,7 +146,7 @@ impl ActivityHandler for AnnounceActivity { let object: AnnouncableActivities = self.object.object(context).await?.try_into()?; // This is only for sending, not receiving so we reject it. if let AnnouncableActivities::Page(_) = object { - return Err(LemmyErrorType::CannotReceivePage)?; + Err(LemmyErrorType::CannotReceivePage)? } // verify here in order to avoid fetching the object twice over http diff --git a/crates/apub/src/activities/create_or_update/post.rs b/crates/apub/src/activities/create_or_update/post.rs index a9ac79364..cf4a6af1f 100644 --- a/crates/apub/src/activities/create_or_update/post.rs +++ b/crates/apub/src/activities/create_or_update/post.rs @@ -120,7 +120,7 @@ impl ActivityHandler for CreateOrUpdatePage { // because then we will definitely receive all create and update activities separately. let is_locked = self.object.comments_enabled == Some(false); if community.local && is_locked { - return Err(LemmyErrorType::NewPostCannotBeLocked)?; + Err(LemmyErrorType::NewPostCannotBeLocked)? } } CreateOrUpdateType::Update => { diff --git a/crates/apub/src/activities/deletion/delete.rs b/crates/apub/src/activities/deletion/delete.rs index fa0a44aa3..537173b5f 100644 --- a/crates/apub/src/activities/deletion/delete.rs +++ b/crates/apub/src/activities/deletion/delete.rs @@ -112,7 +112,7 @@ pub(in crate::activities) async fn receive_remove_action( match DeletableObjects::read_from_db(object, context).await? { DeletableObjects::Community(community) => { if community.local { - return Err(LemmyErrorType::OnlyLocalAdminCanRemoveCommunity)?; + Err(LemmyErrorType::OnlyLocalAdminCanRemoveCommunity)? } let form = ModRemoveCommunityForm { mod_person_id: actor.id, diff --git a/crates/apub/src/activities/deletion/undo_delete.rs b/crates/apub/src/activities/deletion/undo_delete.rs index 4d7822042..6572938dd 100644 --- a/crates/apub/src/activities/deletion/undo_delete.rs +++ b/crates/apub/src/activities/deletion/undo_delete.rs @@ -100,7 +100,7 @@ impl UndoDelete { match DeletableObjects::read_from_db(object, context).await? { DeletableObjects::Community(community) => { if community.local { - return Err(LemmyErrorType::OnlyLocalAdminCanRestoreCommunity)?; + Err(LemmyErrorType::OnlyLocalAdminCanRestoreCommunity)? } let form = ModRemoveCommunityForm { mod_person_id: actor.id, diff --git a/crates/apub/src/activities/mod.rs b/crates/apub/src/activities/mod.rs index 2fe0a82d1..c6696725e 100644 --- a/crates/apub/src/activities/mod.rs +++ b/crates/apub/src/activities/mod.rs @@ -81,10 +81,11 @@ async fn verify_person( ) -> Result<(), LemmyError> { let person = person_id.dereference(context).await?; if person.banned { - return Err(anyhow!("Person {} is banned", person_id)) - .with_lemmy_type(LemmyErrorType::CouldntUpdateComment); + Err(anyhow!("Person {} is banned", person_id)) + .with_lemmy_type(LemmyErrorType::CouldntUpdateComment) + } else { + Ok(()) } - Ok(()) } /// Fetches the person and community to verify their type, then checks if person is banned from site @@ -97,9 +98,9 @@ pub(crate) async fn verify_person_in_community( ) -> Result<(), LemmyError> { let person = person_id.dereference(context).await?; if person.banned { - return Err(LemmyErrorType::PersonIsBannedFromSite( + Err(LemmyErrorType::PersonIsBannedFromSite( person.actor_id.to_string(), - ))?; + ))? } let person_id = person.id; let community_id = community.id; @@ -107,10 +108,10 @@ pub(crate) async fn verify_person_in_community( .await .is_ok(); if is_banned { - return Err(LemmyErrorType::PersonIsBannedFromCommunity)?; + Err(LemmyErrorType::PersonIsBannedFromCommunity)? + } else { + Ok(()) } - - Ok(()) } /// Verify that mod action in community was performed by a moderator. @@ -145,9 +146,10 @@ pub(crate) async fn verify_mod_action( pub(crate) fn verify_is_public(to: &[Url], cc: &[Url]) -> Result<(), LemmyError> { if ![to, cc].iter().any(|set| set.contains(&public())) { - Err(LemmyErrorType::ObjectIsNotPublic)?; + Err(LemmyErrorType::ObjectIsNotPublic)? + } else { + Ok(()) } - Ok(()) } pub(crate) fn verify_community_matches( @@ -159,9 +161,10 @@ where { let b: ObjectId = b.into(); if a != &b { - Err(LemmyErrorType::InvalidCommunity)?; + Err(LemmyErrorType::InvalidCommunity)? + } else { + Ok(()) } - Ok(()) } pub(crate) fn check_community_deleted_or_removed(community: &Community) -> Result<(), LemmyError> { diff --git a/crates/apub/src/activities/voting/vote.rs b/crates/apub/src/activities/voting/vote.rs index ef4572986..926c29302 100644 --- a/crates/apub/src/activities/voting/vote.rs +++ b/crates/apub/src/activities/voting/vote.rs @@ -64,9 +64,10 @@ impl ActivityHandler for Vote { .map(|l| l.enable_downvotes) .unwrap_or(true); if self.kind == VoteType::Dislike && !enable_downvotes { - return Err(anyhow!("Downvotes disabled").into()); + Err(anyhow!("Downvotes disabled").into()) + } else { + Ok(()) } - Ok(()) } #[tracing::instrument(skip_all)] diff --git a/crates/apub/src/api/read_community.rs b/crates/apub/src/api/read_community.rs index c4db1a298..76f7f580d 100644 --- a/crates/apub/src/api/read_community.rs +++ b/crates/apub/src/api/read_community.rs @@ -26,7 +26,7 @@ pub async fn get_community( let local_site = LocalSite::read(&mut context.pool()).await?; if data.name.is_none() && data.id.is_none() { - return Err(LemmyErrorType::NoIdGiven)?; + Err(LemmyErrorType::NoIdGiven)? } check_private_instance(&local_user_view, &local_site)?; diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index 735ae77b9..754a942f1 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -22,7 +22,7 @@ pub async fn read_person( ) -> Result, LemmyError> { // Check to make sure a person name or an id is given if data.username.is_none() && data.person_id.is_none() { - return Err(LemmyErrorType::NoIdGiven)?; + Err(LemmyErrorType::NoIdGiven)? } local_user_view_from_jwt_opt_new(&mut local_user_view, data.auth.as_ref(), &context).await; @@ -39,7 +39,7 @@ pub async fn read_person( .with_lemmy_type(LemmyErrorType::CouldntFindPerson)? .id } else { - return Err(LemmyErrorType::CouldntFindPerson)?; + Err(LemmyErrorType::CouldntFindPerson)? } } }; diff --git a/crates/apub/src/api/resolve_object.rs b/crates/apub/src/api/resolve_object.rs index d1cf2546c..b8c0cef14 100644 --- a/crates/apub/src/api/resolve_object.rs +++ b/crates/apub/src/api/resolve_object.rs @@ -72,7 +72,8 @@ async fn convert_response( }; // if the object was deleted from database, dont return it if removed_or_deleted { - return Err(NotFound {}.into()); + Err(NotFound {}.into()) + } else { + Ok(Json(res)) } - Ok(Json(res)) } diff --git a/crates/apub/src/http/comment.rs b/crates/apub/src/http/comment.rs index 66794f90c..931caaee4 100644 --- a/crates/apub/src/http/comment.rs +++ b/crates/apub/src/http/comment.rs @@ -23,10 +23,8 @@ pub(crate) async fn get_apub_comment( let id = CommentId(info.comment_id.parse::()?); let comment: ApubComment = Comment::read(&mut context.pool(), id).await?.into(); if !comment.local { - return Err(err_object_not_local()); - } - - if !comment.deleted && !comment.removed { + Err(err_object_not_local()) + } else if !comment.deleted && !comment.removed { create_apub_response(&comment.into_json(&context).await?) } else { create_apub_tombstone_response(comment.ap_id.clone()) diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index 18ad860b0..d004bac56 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -81,7 +81,7 @@ pub(crate) async fn get_apub_community_outbox( .await? .into(); if community.deleted || community.removed { - return Err(LemmyErrorType::Deleted)?; + Err(LemmyErrorType::Deleted)? } let outbox = ApubCommunityOutbox::read_local(&community, &context).await?; create_apub_response(&outbox) @@ -97,7 +97,7 @@ pub(crate) async fn get_apub_community_moderators( .await? .into(); if community.deleted || community.removed { - return Err(LemmyErrorType::Deleted)?; + Err(LemmyErrorType::Deleted)? } let moderators = ApubCommunityModerators::read_local(&community, &context).await?; create_apub_response(&moderators) @@ -113,7 +113,7 @@ pub(crate) async fn get_apub_community_featured( .await? .into(); if community.deleted || community.removed { - return Err(LemmyErrorType::Deleted)?; + Err(LemmyErrorType::Deleted)? } let featured = ApubCommunityFeatured::read_local(&community, &context).await?; create_apub_response(&featured) diff --git a/crates/apub/src/http/post.rs b/crates/apub/src/http/post.rs index 4da3dc14f..f65968f15 100644 --- a/crates/apub/src/http/post.rs +++ b/crates/apub/src/http/post.rs @@ -23,10 +23,8 @@ pub(crate) async fn get_apub_post( let id = PostId(info.post_id.parse::()?); let post: ApubPost = Post::read(&mut context.pool(), id).await?.into(); if !post.local { - return Err(err_object_not_local()); - } - - if !post.deleted && !post.removed { + Err(err_object_not_local()) + } else if !post.deleted && !post.removed { create_apub_response(&post.into_json(&context).await?) } else { create_apub_tombstone_response(post.ap_id.clone()) diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index 6b9bb1ff6..23a61b613 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -79,7 +79,7 @@ fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> Result .map(|l| l.federation_enabled) .unwrap_or(true) { - Err(LemmyErrorType::FederationDisabled)?; + Err(LemmyErrorType::FederationDisabled)? } if local_site_data @@ -87,7 +87,7 @@ fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> Result .iter() .any(|i| domain.to_lowercase().eq(&i.domain.to_lowercase())) { - Err(LemmyErrorType::DomainBlocked(domain.clone()))?; + Err(LemmyErrorType::DomainBlocked(domain.clone()))? } // Only check this if there are instances in the allowlist @@ -97,7 +97,7 @@ fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> Result .iter() .any(|i| domain.to_lowercase().eq(&i.domain.to_lowercase())) { - Err(LemmyErrorType::DomainNotInAllowList(domain))?; + Err(LemmyErrorType::DomainNotInAllowList(domain))? } Ok(()) @@ -176,7 +176,7 @@ pub(crate) async fn check_apub_id_valid_with_strictness( let domain = apub_id.domain().expect("apud id has domain").to_string(); if !allowed_and_local.contains(&domain) { - return Err(LemmyErrorType::FederationDisabledByStrictAllowList)?; + Err(LemmyErrorType::FederationDisabledByStrictAllowList)? } } Ok(()) diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index 9be6639c0..888e0fffb 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -143,9 +143,10 @@ impl Object for ApubComment { verify_person_in_community(¬e.attributed_to, &community, context).await?; let (post, _) = note.get_parents(context).await?; if post.locked { - return Err(LemmyErrorType::PostIsLocked)?; + Err(LemmyErrorType::PostIsLocked)? + } else { + Ok(()) } - Ok(()) } /// Converts a `Note` to `Comment`. diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 2210e0b0e..3d637d624 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -107,11 +107,12 @@ impl Object for ApubPrivateMessage { check_apub_id_valid_with_strictness(note.id.inner(), false, context).await?; let person = note.attributed_to.dereference(context).await?; if person.banned { - return Err(LemmyErrorType::PersonIsBannedFromSite( + Err(LemmyErrorType::PersonIsBannedFromSite( person.actor_id.to_string(), - ))?; + ))? + } else { + Ok(()) } - Ok(()) } #[tracing::instrument(skip_all)] diff --git a/crates/apub/src/protocol/objects/page.rs b/crates/apub/src/protocol/objects/page.rs index f7952885e..0d4880383 100644 --- a/crates/apub/src/protocol/objects/page.rs +++ b/crates/apub/src/protocol/objects/page.rs @@ -208,7 +208,7 @@ impl InCommunity for Page { break c; } } else { - return Err(LemmyErrorType::NoCommunityFoundInCc)?; + Err(LemmyErrorType::NoCommunityFoundInCc)? } } } diff --git a/crates/db_schema/src/diesel_ltree.patch b/crates/db_schema/src/diesel_ltree.patch index 0e84eeceb..05ccf6324 100644 --- a/crates/db_schema/src/diesel_ltree.patch +++ b/crates/db_schema/src/diesel_ltree.patch @@ -2,23 +2,6 @@ diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 255c6422..f2ccf5e2 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs -@@ -2,16 +2,12 @@ - - pub mod sql_types { - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "listing_type_enum"))] - pub struct ListingTypeEnum; - -- #[derive(diesel::sql_types::SqlType)] -- #[diesel(postgres_type(name = "ltree"))] -- pub struct Ltree; -- - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "registration_mode_enum"))] - pub struct RegistrationModeEnum; - - #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "sort_type_enum"))] @@ -76,13 +76,13 @@ diesel::table! { published -> Timestamptz, } diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index ce1827211..079a1fe39 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -143,6 +143,24 @@ pub enum RegistrationMode { Open, } +#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "full", derive(DbEnum, TS))] +#[cfg_attr( + feature = "full", + ExistingTypePath = "crate::schema::sql_types::PostListingModeEnum" +)] +#[cfg_attr(feature = "full", DbValueStyle = "verbatim")] +#[cfg_attr(feature = "full", ts(export))] +/// A post-view mode that changes how multiple post listings look. +pub enum PostListingMode { + /// A compact, list-type view. + List, + /// A larger card-type view. + Card, + /// A smaller card-type view, usually with images as thumbnails + SmallCard, +} + #[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 317a06250..ffcdbebe8 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -5,6 +5,14 @@ pub mod sql_types { #[diesel(postgres_type(name = "listing_type_enum"))] pub struct ListingTypeEnum; + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "ltree"))] + pub struct Ltree; + + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "post_listing_mode_enum"))] + pub struct PostListingModeEnum; + #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "registration_mode_enum"))] pub struct RegistrationModeEnum; @@ -372,6 +380,7 @@ diesel::table! { use diesel::sql_types::*; use super::sql_types::SortTypeEnum; use super::sql_types::ListingTypeEnum; + use super::sql_types::PostListingModeEnum; local_user (id) { id -> Int4, @@ -400,6 +409,7 @@ diesel::table! { auto_expand -> Bool, infinite_scroll_enabled -> Bool, admin -> Bool, + post_listing_mode -> PostListingModeEnum, } } diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 8440531c6..84ada46af 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -3,6 +3,7 @@ use crate::schema::local_user; use crate::{ newtypes::{LocalUserId, PersonId}, ListingType, + PostListingMode, SortType, }; use chrono::{DateTime, Utc}; @@ -60,6 +61,7 @@ pub struct LocalUser { pub infinite_scroll_enabled: bool, /// Whether the person is an admin. pub admin: bool, + pub post_listing_mode: PostListingMode, } #[derive(Clone, TypedBuilder)] @@ -92,6 +94,7 @@ pub struct LocalUserInsertForm { pub auto_expand: Option, pub infinite_scroll_enabled: Option, pub admin: Option, + pub post_listing_mode: Option, } #[derive(Clone, Default)] @@ -120,4 +123,5 @@ pub struct LocalUserUpdateForm { pub auto_expand: Option, pub infinite_scroll_enabled: Option, pub admin: Option, + pub post_listing_mode: Option, } diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs index 13317ad1f..03898d7f9 100644 --- a/crates/db_views/src/registration_application_view.rs +++ b/crates/db_views/src/registration_application_view.rs @@ -290,6 +290,7 @@ mod tests { open_links_in_new_tab: inserted_sara_local_user.open_links_in_new_tab, infinite_scroll_enabled: inserted_sara_local_user.infinite_scroll_enabled, admin: false, + post_listing_mode: inserted_sara_local_user.post_listing_mode, }, creator: Person { id: inserted_sara_person.id, diff --git a/crates/utils/src/settings/mod.rs b/crates/utils/src/settings/mod.rs index b0389a520..6b8982a11 100644 --- a/crates/utils/src/settings/mod.rs +++ b/crates/utils/src/settings/mod.rs @@ -38,10 +38,10 @@ impl Settings { let config = from_str::(&Self::read_config_file()?)?; if config.hostname == "unset" { - return Err(anyhow!("Hostname variable is not set!").into()); + Err(anyhow!("Hostname variable is not set!").into()) + } else { + Ok(config) } - - Ok(config) } pub fn get_database_url(&self) -> String { diff --git a/crates/utils/src/utils/validation.rs b/crates/utils/src/utils/validation.rs index 0c955b122..44464bc9f 100644 --- a/crates/utils/src/utils/validation.rs +++ b/crates/utils/src/utils/validation.rs @@ -216,10 +216,10 @@ pub fn build_and_check_regex(regex_str_opt: &Option<&str>) -> LemmyResult