From: Nutomic Date: Wed, 13 Apr 2022 18:12:25 +0000 (+0000) Subject: Split apart api files (#2216) X-Git-Url: http://these/git/%22https:/lemmy.ml/u/Liwott/%7B%60%24%7BrepoUrl%7D?a=commitdiff_plain;h=3951a16447fdccfde3b0ec2ef8fcbf9f89562adc;p=lemmy.git Split apart api files (#2216) --- diff --git a/crates/api/src/comment.rs b/crates/api/src/comment/like.rs similarity index 53% rename from crates/api/src/comment.rs rename to crates/api/src/comment/like.rs index d5056692..a5bb368a 100644 --- a/crates/api/src/comment.rs +++ b/crates/api/src/comment/like.rs @@ -1,12 +1,10 @@ -use std::convert::TryInto; - +use crate::Perform; use actix_web::web::Data; - use lemmy_api_common::{ blocking, check_community_ban, check_downvotes_enabled, - comment::*, + comment::{CommentResponse, CreateCommentLike}, get_local_user_view_from_jwt, }; use lemmy_apub::{ @@ -18,111 +16,13 @@ use lemmy_apub::{ }; use lemmy_db_schema::{ newtypes::LocalUserId, - source::comment::*, - traits::{Likeable, Saveable}, + source::comment::{CommentLike, CommentLikeForm}, + traits::Likeable, }; use lemmy_db_views::{comment_view::CommentView, local_user_view::LocalUserView}; use lemmy_utils::{ConnectionId, LemmyError}; use lemmy_websocket::{send::send_comment_ws_message, LemmyContext, UserOperation}; - -use crate::Perform; - -#[async_trait::async_trait(?Send)] -impl Perform for MarkCommentAsRead { - type Response = CommentResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &MarkCommentAsRead = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let comment_id = data.comment_id; - let orig_comment = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment_id, None) - }) - .await??; - - // Verify that only the recipient can mark as read - if local_user_view.person.id != orig_comment.get_recipient_id() { - return Err(LemmyError::from_message("no_comment_edit_allowed")); - } - - // Do the mark as read - let read = data.read; - blocking(context.pool(), move |conn| { - Comment::update_read(conn, comment_id, read) - }) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?; - - // Refetch it - let comment_id = data.comment_id; - let person_id = local_user_view.person.id; - let comment_view = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment_id, Some(person_id)) - }) - .await??; - - let res = CommentResponse { - comment_view, - recipient_ids: Vec::new(), - form_id: None, - }; - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for SaveComment { - type Response = CommentResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &SaveComment = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let comment_saved_form = CommentSavedForm { - comment_id: data.comment_id, - person_id: local_user_view.person.id, - }; - - if data.save { - let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form); - blocking(context.pool(), save_comment) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_comment"))?; - } else { - let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form); - blocking(context.pool(), unsave_comment) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_comment"))?; - } - - let comment_id = data.comment_id; - let person_id = local_user_view.person.id; - let comment_view = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment_id, Some(person_id)) - }) - .await??; - - Ok(CommentResponse { - comment_view, - recipient_ids: Vec::new(), - form_id: None, - }) - } -} +use std::convert::TryInto; #[async_trait::async_trait(?Send)] impl Perform for CreateCommentLike { diff --git a/crates/api/src/comment/mark_as_read.rs b/crates/api/src/comment/mark_as_read.rs new file mode 100644 index 00000000..25c4fb43 --- /dev/null +++ b/crates/api/src/comment/mark_as_read.rs @@ -0,0 +1,62 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + comment::{CommentResponse, MarkCommentAsRead}, + get_local_user_view_from_jwt, +}; +use lemmy_db_schema::source::comment::Comment; +use lemmy_db_views::comment_view::CommentView; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for MarkCommentAsRead { + type Response = CommentResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &MarkCommentAsRead = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let comment_id = data.comment_id; + let orig_comment = blocking(context.pool(), move |conn| { + CommentView::read(conn, comment_id, None) + }) + .await??; + + // Verify that only the recipient can mark as read + if local_user_view.person.id != orig_comment.get_recipient_id() { + return Err(LemmyError::from_message("no_comment_edit_allowed")); + } + + // Do the mark as read + let read = data.read; + blocking(context.pool(), move |conn| { + Comment::update_read(conn, comment_id, read) + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?; + + // Refetch it + let comment_id = data.comment_id; + let person_id = local_user_view.person.id; + let comment_view = blocking(context.pool(), move |conn| { + CommentView::read(conn, comment_id, Some(person_id)) + }) + .await??; + + let res = CommentResponse { + comment_view, + recipient_ids: Vec::new(), + form_id: None, + }; + + Ok(res) + } +} diff --git a/crates/api/src/comment/mod.rs b/crates/api/src/comment/mod.rs new file mode 100644 index 00000000..93a71b4e --- /dev/null +++ b/crates/api/src/comment/mod.rs @@ -0,0 +1,3 @@ +mod like; +mod mark_as_read; +mod save; diff --git a/crates/api/src/comment/save.rs b/crates/api/src/comment/save.rs new file mode 100644 index 00000000..6fc0f498 --- /dev/null +++ b/crates/api/src/comment/save.rs @@ -0,0 +1,60 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + comment::{CommentResponse, SaveComment}, + get_local_user_view_from_jwt, +}; +use lemmy_db_schema::{ + source::comment::{CommentSaved, CommentSavedForm}, + traits::Saveable, +}; +use lemmy_db_views::comment_view::CommentView; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for SaveComment { + type Response = CommentResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &SaveComment = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let comment_saved_form = CommentSavedForm { + comment_id: data.comment_id, + person_id: local_user_view.person.id, + }; + + if data.save { + let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form); + blocking(context.pool(), save_comment) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_comment"))?; + } else { + let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form); + blocking(context.pool(), unsave_comment) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_comment"))?; + } + + let comment_id = data.comment_id; + let person_id = local_user_view.person.id; + let comment_view = blocking(context.pool(), move |conn| { + CommentView::read(conn, comment_id, Some(person_id)) + }) + .await??; + + Ok(CommentResponse { + comment_view, + recipient_ids: Vec::new(), + form_id: None, + }) + } +} diff --git a/crates/api/src/comment_report.rs b/crates/api/src/comment_report.rs deleted file mode 100644 index 515c7103..00000000 --- a/crates/api/src/comment_report.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::Perform; -use actix_web::web::Data; -use lemmy_api_common::{ - blocking, - check_community_ban, - comment::*, - get_local_user_view_from_jwt, - is_mod_or_admin, -}; -use lemmy_apub::protocol::activities::community::report::Report; -use lemmy_apub_lib::object_id::ObjectId; -use lemmy_db_schema::{source::comment_report::*, traits::Reportable}; -use lemmy_db_views::{ - comment_report_view::{CommentReportQueryBuilder, CommentReportView}, - comment_view::CommentView, -}; -use lemmy_utils::{ConnectionId, LemmyError}; -use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation}; - -/// Creates a comment report and notifies the moderators of the community -#[async_trait::async_trait(?Send)] -impl Perform for CreateCommentReport { - type Response = CommentReportResponse; - - #[tracing::instrument(skip(context, websocket_id))] - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &CreateCommentReport = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - // check size of report and check for whitespace - let reason = data.reason.trim(); - if reason.is_empty() { - return Err(LemmyError::from_message("report_reason_required")); - } - if reason.chars().count() > 1000 { - return Err(LemmyError::from_message("report_too_long")); - } - - let person_id = local_user_view.person.id; - let comment_id = data.comment_id; - let comment_view = blocking(context.pool(), move |conn| { - CommentView::read(conn, comment_id, None) - }) - .await??; - - check_community_ban(person_id, comment_view.community.id, context.pool()).await?; - - let report_form = CommentReportForm { - creator_id: person_id, - comment_id, - original_comment_text: comment_view.comment.content, - reason: data.reason.to_owned(), - }; - - let report = blocking(context.pool(), move |conn| { - CommentReport::report(conn, &report_form) - }) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_create_report"))?; - - let comment_report_view = blocking(context.pool(), move |conn| { - CommentReportView::read(conn, report.id, person_id) - }) - .await??; - - let res = CommentReportResponse { - comment_report_view, - }; - - context.chat_server().do_send(SendModRoomMessage { - op: UserOperation::CreateCommentReport, - response: res.clone(), - community_id: comment_view.community.id, - websocket_id, - }); - - Report::send( - ObjectId::new(comment_view.comment.ap_id), - &local_user_view.person.into(), - ObjectId::new(comment_view.community.actor_id), - reason.to_string(), - context, - ) - .await?; - - Ok(res) - } -} - -/// Resolves or unresolves a comment report and notifies the moderators of the community -#[async_trait::async_trait(?Send)] -impl Perform for ResolveCommentReport { - type Response = CommentReportResponse; - - #[tracing::instrument(skip(context, websocket_id))] - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &ResolveCommentReport = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let report_id = data.report_id; - let person_id = local_user_view.person.id; - let report = blocking(context.pool(), move |conn| { - CommentReportView::read(conn, report_id, person_id) - }) - .await??; - - let person_id = local_user_view.person.id; - is_mod_or_admin(context.pool(), person_id, report.community.id).await?; - - let resolved = data.resolved; - let resolve_fun = move |conn: &'_ _| { - if resolved { - CommentReport::resolve(conn, report_id, person_id) - } else { - CommentReport::unresolve(conn, report_id, person_id) - } - }; - - blocking(context.pool(), resolve_fun) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_resolve_report"))?; - - let report_id = data.report_id; - let comment_report_view = blocking(context.pool(), move |conn| { - CommentReportView::read(conn, report_id, person_id) - }) - .await??; - - let res = CommentReportResponse { - comment_report_view, - }; - - context.chat_server().do_send(SendModRoomMessage { - op: UserOperation::ResolveCommentReport, - response: res.clone(), - community_id: report.community.id, - websocket_id, - }); - - Ok(res) - } -} - -/// Lists comment reports for a community if an id is supplied -/// or returns all comment reports for communities a user moderates -#[async_trait::async_trait(?Send)] -impl Perform for ListCommentReports { - type Response = ListCommentReportsResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &ListCommentReports = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let person_id = local_user_view.person.id; - let admin = local_user_view.person.admin; - let community_id = data.community_id; - let unresolved_only = data.unresolved_only; - - let page = data.page; - let limit = data.limit; - let comment_reports = blocking(context.pool(), move |conn| { - CommentReportQueryBuilder::create(conn, person_id, admin) - .community_id(community_id) - .unresolved_only(unresolved_only) - .page(page) - .limit(limit) - .list() - }) - .await??; - - let res = ListCommentReportsResponse { comment_reports }; - - Ok(res) - } -} diff --git a/crates/api/src/comment_report/create.rs b/crates/api/src/comment_report/create.rs new file mode 100644 index 00000000..4f4ef3bb --- /dev/null +++ b/crates/api/src/comment_report/create.rs @@ -0,0 +1,92 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_community_ban, + comment::{CommentReportResponse, CreateCommentReport}, + get_local_user_view_from_jwt, +}; +use lemmy_apub::protocol::activities::community::report::Report; +use lemmy_apub_lib::object_id::ObjectId; +use lemmy_db_schema::{ + source::comment_report::{CommentReport, CommentReportForm}, + traits::Reportable, +}; +use lemmy_db_views::{comment_report_view::CommentReportView, comment_view::CommentView}; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation}; + +/// Creates a comment report and notifies the moderators of the community +#[async_trait::async_trait(?Send)] +impl Perform for CreateCommentReport { + type Response = CommentReportResponse; + + #[tracing::instrument(skip(context, websocket_id))] + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &CreateCommentReport = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + // check size of report and check for whitespace + let reason = data.reason.trim(); + if reason.is_empty() { + return Err(LemmyError::from_message("report_reason_required")); + } + if reason.chars().count() > 1000 { + return Err(LemmyError::from_message("report_too_long")); + } + + let person_id = local_user_view.person.id; + let comment_id = data.comment_id; + let comment_view = blocking(context.pool(), move |conn| { + CommentView::read(conn, comment_id, None) + }) + .await??; + + check_community_ban(person_id, comment_view.community.id, context.pool()).await?; + + let report_form = CommentReportForm { + creator_id: person_id, + comment_id, + original_comment_text: comment_view.comment.content, + reason: data.reason.to_owned(), + }; + + let report = blocking(context.pool(), move |conn| { + CommentReport::report(conn, &report_form) + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_create_report"))?; + + let comment_report_view = blocking(context.pool(), move |conn| { + CommentReportView::read(conn, report.id, person_id) + }) + .await??; + + let res = CommentReportResponse { + comment_report_view, + }; + + context.chat_server().do_send(SendModRoomMessage { + op: UserOperation::CreateCommentReport, + response: res.clone(), + community_id: comment_view.community.id, + websocket_id, + }); + + Report::send( + ObjectId::new(comment_view.comment.ap_id), + &local_user_view.person.into(), + ObjectId::new(comment_view.community.actor_id), + reason.to_string(), + context, + ) + .await?; + + Ok(res) + } +} diff --git a/crates/api/src/comment_report/list.rs b/crates/api/src/comment_report/list.rs new file mode 100644 index 00000000..b88aced7 --- /dev/null +++ b/crates/api/src/comment_report/list.rs @@ -0,0 +1,49 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + comment::{ListCommentReports, ListCommentReportsResponse}, + get_local_user_view_from_jwt, +}; +use lemmy_db_views::comment_report_view::CommentReportQueryBuilder; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +/// Lists comment reports for a community if an id is supplied +/// or returns all comment reports for communities a user moderates +#[async_trait::async_trait(?Send)] +impl Perform for ListCommentReports { + type Response = ListCommentReportsResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &ListCommentReports = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let person_id = local_user_view.person.id; + let admin = local_user_view.person.admin; + let community_id = data.community_id; + let unresolved_only = data.unresolved_only; + + let page = data.page; + let limit = data.limit; + let comment_reports = blocking(context.pool(), move |conn| { + CommentReportQueryBuilder::create(conn, person_id, admin) + .community_id(community_id) + .unresolved_only(unresolved_only) + .page(page) + .limit(limit) + .list() + }) + .await??; + + let res = ListCommentReportsResponse { comment_reports }; + + Ok(res) + } +} diff --git a/crates/api/src/comment_report/mod.rs b/crates/api/src/comment_report/mod.rs new file mode 100644 index 00000000..375fde4c --- /dev/null +++ b/crates/api/src/comment_report/mod.rs @@ -0,0 +1,3 @@ +mod create; +mod list; +mod resolve; diff --git a/crates/api/src/comment_report/resolve.rs b/crates/api/src/comment_report/resolve.rs new file mode 100644 index 00000000..9446fb36 --- /dev/null +++ b/crates/api/src/comment_report/resolve.rs @@ -0,0 +1,71 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + comment::{CommentReportResponse, ResolveCommentReport}, + get_local_user_view_from_jwt, + is_mod_or_admin, +}; +use lemmy_db_schema::{source::comment_report::CommentReport, traits::Reportable}; +use lemmy_db_views::comment_report_view::CommentReportView; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation}; + +/// Resolves or unresolves a comment report and notifies the moderators of the community +#[async_trait::async_trait(?Send)] +impl Perform for ResolveCommentReport { + type Response = CommentReportResponse; + + #[tracing::instrument(skip(context, websocket_id))] + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &ResolveCommentReport = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let report_id = data.report_id; + let person_id = local_user_view.person.id; + let report = blocking(context.pool(), move |conn| { + CommentReportView::read(conn, report_id, person_id) + }) + .await??; + + let person_id = local_user_view.person.id; + is_mod_or_admin(context.pool(), person_id, report.community.id).await?; + + let resolved = data.resolved; + let resolve_fun = move |conn: &'_ _| { + if resolved { + CommentReport::resolve(conn, report_id, person_id) + } else { + CommentReport::unresolve(conn, report_id, person_id) + } + }; + + blocking(context.pool(), resolve_fun) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_resolve_report"))?; + + let report_id = data.report_id; + let comment_report_view = blocking(context.pool(), move |conn| { + CommentReportView::read(conn, report_id, person_id) + }) + .await??; + + let res = CommentReportResponse { + comment_report_view, + }; + + context.chat_server().do_send(SendModRoomMessage { + op: UserOperation::ResolveCommentReport, + response: res.clone(), + community_id: report.community.id, + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api/src/community.rs b/crates/api/src/community.rs deleted file mode 100644 index 82ee8072..00000000 --- a/crates/api/src/community.rs +++ /dev/null @@ -1,513 +0,0 @@ -use crate::Perform; -use actix_web::web::Data; -use anyhow::Context; -use lemmy_api_common::{ - blocking, - check_community_ban, - check_community_deleted_or_removed, - community::*, - get_local_user_view_from_jwt, - is_mod_or_admin, - remove_user_data_in_community, -}; -use lemmy_apub::{ - activities::block::SiteOrCommunity, - objects::{community::ApubCommunity, person::ApubPerson}, - protocol::activities::{ - block::{block_user::BlockUser, undo_block_user::UndoBlockUser}, - community::{add_mod::AddMod, remove_mod::RemoveMod}, - following::{follow::FollowCommunity as FollowCommunityApub, undo_follow::UndoFollowCommunity}, - }, -}; -use lemmy_db_schema::{ - source::{ - community::{ - Community, - CommunityFollower, - CommunityFollowerForm, - CommunityModerator, - CommunityModeratorForm, - CommunityPersonBan, - CommunityPersonBanForm, - }, - community_block::{CommunityBlock, CommunityBlockForm}, - moderator::{ - ModAddCommunity, - ModAddCommunityForm, - ModBanFromCommunity, - ModBanFromCommunityForm, - ModTransferCommunity, - ModTransferCommunityForm, - }, - person::Person, - }, - traits::{Bannable, Blockable, Crud, Followable, Joinable}, -}; -use lemmy_db_views_actor::{ - community_moderator_view::CommunityModeratorView, - community_view::CommunityView, - person_view::PersonViewSafe, -}; -use lemmy_utils::{location_info, utils::naive_from_unix, ConnectionId, LemmyError}; -use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation}; - -#[async_trait::async_trait(?Send)] -impl Perform for FollowCommunity { - type Response = CommunityResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &FollowCommunity = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let community_id = data.community_id; - let community: ApubCommunity = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await?? - .into(); - let community_follower_form = CommunityFollowerForm { - community_id: data.community_id, - person_id: local_user_view.person.id, - pending: false, - }; - - if community.local { - if data.follow { - check_community_ban(local_user_view.person.id, community_id, context.pool()).await?; - check_community_deleted_or_removed(community_id, context.pool()).await?; - - let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); - blocking(context.pool(), follow) - .await? - .map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?; - } else { - let unfollow = - move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form); - blocking(context.pool(), unfollow) - .await? - .map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?; - } - } else if data.follow { - // Dont actually add to the community followers here, because you need - // to wait for the accept - FollowCommunityApub::send(&local_user_view.person.clone().into(), &community, context) - .await?; - } else { - UndoFollowCommunity::send(&local_user_view.person.clone().into(), &community, context) - .await?; - let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form); - blocking(context.pool(), unfollow) - .await? - .map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?; - } - - let community_id = data.community_id; - let person_id = local_user_view.person.id; - let mut community_view = blocking(context.pool(), move |conn| { - CommunityView::read(conn, community_id, Some(person_id)) - }) - .await??; - - // TODO: this needs to return a "pending" state, until Accept is received from the remote server - // For now, just assume that remote follows are accepted. - // Otherwise, the subscribed will be null - if !community.local { - community_view.subscribed = data.follow; - } - - Ok(CommunityResponse { community_view }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for BlockCommunity { - type Response = BlockCommunityResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &BlockCommunity = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let community_id = data.community_id; - let person_id = local_user_view.person.id; - let community_block_form = CommunityBlockForm { - person_id, - community_id, - }; - - if data.block { - let block = move |conn: &'_ _| CommunityBlock::block(conn, &community_block_form); - blocking(context.pool(), block) - .await? - .map_err(|e| LemmyError::from_error_message(e, "community_block_already_exists"))?; - - // Also, unfollow the community, and send a federated unfollow - let community_follower_form = CommunityFollowerForm { - community_id: data.community_id, - person_id, - pending: false, - }; - blocking(context.pool(), move |conn: &'_ _| { - CommunityFollower::unfollow(conn, &community_follower_form) - }) - .await? - .ok(); - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - UndoFollowCommunity::send(&local_user_view.person.into(), &community.into(), context).await?; - } else { - let unblock = move |conn: &'_ _| CommunityBlock::unblock(conn, &community_block_form); - blocking(context.pool(), unblock) - .await? - .map_err(|e| LemmyError::from_error_message(e, "community_block_already_exists"))?; - } - - let community_view = blocking(context.pool(), move |conn| { - CommunityView::read(conn, community_id, Some(person_id)) - }) - .await??; - - Ok(BlockCommunityResponse { - blocked: data.block, - community_view, - }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for BanFromCommunity { - type Response = BanFromCommunityResponse; - - #[tracing::instrument(skip(context, websocket_id))] - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &BanFromCommunity = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let community_id = data.community_id; - let banned_person_id = data.person_id; - let remove_data = data.remove_data.unwrap_or(false); - let expires = data.expires.map(naive_from_unix); - - // Verify that only mods or admins can ban - is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?; - - let community_user_ban_form = CommunityPersonBanForm { - community_id: data.community_id, - person_id: data.person_id, - expires: Some(expires), - }; - - let community: ApubCommunity = blocking(context.pool(), move |conn: &'_ _| { - Community::read(conn, community_id) - }) - .await?? - .into(); - let banned_person: ApubPerson = blocking(context.pool(), move |conn: &'_ _| { - Person::read(conn, banned_person_id) - }) - .await?? - .into(); - - if data.ban { - let ban = move |conn: &'_ _| CommunityPersonBan::ban(conn, &community_user_ban_form); - blocking(context.pool(), ban) - .await? - .map_err(|e| LemmyError::from_error_message(e, "community_user_already_banned"))?; - - // Also unsubscribe them from the community, if they are subscribed - let community_follower_form = CommunityFollowerForm { - community_id: data.community_id, - person_id: banned_person_id, - pending: false, - }; - blocking(context.pool(), move |conn: &'_ _| { - CommunityFollower::unfollow(conn, &community_follower_form) - }) - .await? - .ok(); - - BlockUser::send( - &SiteOrCommunity::Community(community), - &banned_person, - &local_user_view.person.clone().into(), - remove_data, - data.reason.clone(), - expires, - context, - ) - .await?; - } else { - let unban = move |conn: &'_ _| CommunityPersonBan::unban(conn, &community_user_ban_form); - blocking(context.pool(), unban) - .await? - .map_err(|e| LemmyError::from_error_message(e, "community_user_already_banned"))?; - UndoBlockUser::send( - &SiteOrCommunity::Community(community), - &banned_person, - &local_user_view.person.clone().into(), - data.reason.clone(), - context, - ) - .await?; - } - - // Remove/Restore their data if that's desired - if remove_data { - remove_user_data_in_community(community_id, banned_person_id, context.pool()).await?; - } - - // Mod tables - let form = ModBanFromCommunityForm { - mod_person_id: local_user_view.person.id, - other_person_id: data.person_id, - community_id: data.community_id, - reason: data.reason.to_owned(), - banned: Some(data.ban), - expires, - }; - blocking(context.pool(), move |conn| { - ModBanFromCommunity::create(conn, &form) - }) - .await??; - - let person_id = data.person_id; - let person_view = blocking(context.pool(), move |conn| { - PersonViewSafe::read(conn, person_id) - }) - .await??; - - let res = BanFromCommunityResponse { - person_view, - banned: data.ban, - }; - - context.chat_server().do_send(SendCommunityRoomMessage { - op: UserOperation::BanFromCommunity, - response: res.clone(), - community_id, - websocket_id, - }); - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for AddModToCommunity { - type Response = AddModToCommunityResponse; - - #[tracing::instrument(skip(context, websocket_id))] - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &AddModToCommunity = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let community_id = data.community_id; - - // Verify that only mods or admins can add mod - is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?; - let community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - if local_user_view.person.admin && !community.local { - return Err(LemmyError::from_message("not_a_moderator")); - } - - // Update in local database - let community_moderator_form = CommunityModeratorForm { - community_id: data.community_id, - person_id: data.person_id, - }; - if data.added { - let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); - blocking(context.pool(), join) - .await? - .map_err(|e| LemmyError::from_error_message(e, "community_moderator_already_exists"))?; - } else { - let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form); - blocking(context.pool(), leave) - .await? - .map_err(|e| LemmyError::from_error_message(e, "community_moderator_already_exists"))?; - } - - // Mod tables - let form = ModAddCommunityForm { - mod_person_id: local_user_view.person.id, - other_person_id: data.person_id, - community_id: data.community_id, - removed: Some(!data.added), - }; - blocking(context.pool(), move |conn| { - ModAddCommunity::create(conn, &form) - }) - .await??; - - // Send to federated instances - let updated_mod_id = data.person_id; - let updated_mod: ApubPerson = blocking(context.pool(), move |conn| { - Person::read(conn, updated_mod_id) - }) - .await?? - .into(); - let community: ApubCommunity = community.into(); - if data.added { - AddMod::send( - &community, - &updated_mod, - &local_user_view.person.into(), - context, - ) - .await?; - } else { - RemoveMod::send( - &community, - &updated_mod, - &local_user_view.person.into(), - context, - ) - .await?; - } - - // Note: in case a remote mod is added, this returns the old moderators list, it will only get - // updated once we receive an activity from the community (like `Announce/Add/Moderator`) - let community_id = data.community_id; - let moderators = blocking(context.pool(), move |conn| { - CommunityModeratorView::for_community(conn, community_id) - }) - .await??; - - let res = AddModToCommunityResponse { moderators }; - context.chat_server().do_send(SendCommunityRoomMessage { - op: UserOperation::AddModToCommunity, - response: res.clone(), - community_id, - websocket_id, - }); - Ok(res) - } -} - -// TODO: we dont do anything for federation here, it should be updated the next time the community -// gets fetched. i hope we can get rid of the community creator role soon. -#[async_trait::async_trait(?Send)] -impl Perform for TransferCommunity { - type Response = GetCommunityResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &TransferCommunity = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let admins = blocking(context.pool(), PersonViewSafe::admins).await??; - - // Fetch the community mods - let community_id = data.community_id; - let mut community_mods = blocking(context.pool(), move |conn| { - CommunityModeratorView::for_community(conn, community_id) - }) - .await??; - - // Make sure transferrer is either the top community mod, or an admin - if local_user_view.person.id != community_mods[0].moderator.id - && !admins - .iter() - .map(|a| a.person.id) - .any(|x| x == local_user_view.person.id) - { - return Err(LemmyError::from_message("not_an_admin")); - } - - // You have to re-do the community_moderator table, reordering it. - // Add the transferee to the top - let creator_index = community_mods - .iter() - .position(|r| r.moderator.id == data.person_id) - .context(location_info!())?; - let creator_person = community_mods.remove(creator_index); - community_mods.insert(0, creator_person); - - // Delete all the mods - let community_id = data.community_id; - blocking(context.pool(), move |conn| { - CommunityModerator::delete_for_community(conn, community_id) - }) - .await??; - - // TODO: this should probably be a bulk operation - // Re-add the mods, in the new order - for cmod in &community_mods { - let community_moderator_form = CommunityModeratorForm { - community_id: cmod.community.id, - person_id: cmod.moderator.id, - }; - - let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); - blocking(context.pool(), join) - .await? - .map_err(|e| LemmyError::from_error_message(e, "community_moderator_already_exists"))?; - } - - // Mod tables - let form = ModTransferCommunityForm { - mod_person_id: local_user_view.person.id, - other_person_id: data.person_id, - community_id: data.community_id, - removed: Some(false), - }; - blocking(context.pool(), move |conn| { - ModTransferCommunity::create(conn, &form) - }) - .await??; - - let community_id = data.community_id; - let person_id = local_user_view.person.id; - let community_view = blocking(context.pool(), move |conn| { - CommunityView::read(conn, community_id, Some(person_id)) - }) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_community"))?; - - let community_id = data.community_id; - let moderators = blocking(context.pool(), move |conn| { - CommunityModeratorView::for_community(conn, community_id) - }) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_community"))?; - - // Return the jwt - Ok(GetCommunityResponse { - community_view, - site: None, - moderators, - online: 0, - }) - } -} diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs new file mode 100644 index 00000000..6e790a0c --- /dev/null +++ b/crates/api/src/community/add_mod.rs @@ -0,0 +1,123 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + community::{AddModToCommunity, AddModToCommunityResponse}, + get_local_user_view_from_jwt, + is_mod_or_admin, +}; +use lemmy_apub::{ + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::community::{add_mod::AddMod, remove_mod::RemoveMod}, +}; +use lemmy_db_schema::{ + source::{ + community::{Community, CommunityModerator, CommunityModeratorForm}, + moderator::{ModAddCommunity, ModAddCommunityForm}, + person::Person, + }, + traits::{Crud, Joinable}, +}; +use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl Perform for AddModToCommunity { + type Response = AddModToCommunityResponse; + + #[tracing::instrument(skip(context, websocket_id))] + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &AddModToCommunity = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let community_id = data.community_id; + + // Verify that only mods or admins can add mod + is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?; + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + if local_user_view.person.admin && !community.local { + return Err(LemmyError::from_message("not_a_moderator")); + } + + // Update in local database + let community_moderator_form = CommunityModeratorForm { + community_id: data.community_id, + person_id: data.person_id, + }; + if data.added { + let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); + blocking(context.pool(), join) + .await? + .map_err(|e| LemmyError::from_error_message(e, "community_moderator_already_exists"))?; + } else { + let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form); + blocking(context.pool(), leave) + .await? + .map_err(|e| LemmyError::from_error_message(e, "community_moderator_already_exists"))?; + } + + // Mod tables + let form = ModAddCommunityForm { + mod_person_id: local_user_view.person.id, + other_person_id: data.person_id, + community_id: data.community_id, + removed: Some(!data.added), + }; + blocking(context.pool(), move |conn| { + ModAddCommunity::create(conn, &form) + }) + .await??; + + // Send to federated instances + let updated_mod_id = data.person_id; + let updated_mod: ApubPerson = blocking(context.pool(), move |conn| { + Person::read(conn, updated_mod_id) + }) + .await?? + .into(); + let community: ApubCommunity = community.into(); + if data.added { + AddMod::send( + &community, + &updated_mod, + &local_user_view.person.into(), + context, + ) + .await?; + } else { + RemoveMod::send( + &community, + &updated_mod, + &local_user_view.person.into(), + context, + ) + .await?; + } + + // Note: in case a remote mod is added, this returns the old moderators list, it will only get + // updated once we receive an activity from the community (like `Announce/Add/Moderator`) + let community_id = data.community_id; + let moderators = blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(conn, community_id) + }) + .await??; + + let res = AddModToCommunityResponse { moderators }; + context.chat_server().do_send(SendCommunityRoomMessage { + op: UserOperation::AddModToCommunity, + response: res.clone(), + community_id, + websocket_id, + }); + Ok(res) + } +} diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs new file mode 100644 index 00000000..ccdc1b9b --- /dev/null +++ b/crates/api/src/community/ban.rs @@ -0,0 +1,154 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + community::{BanFromCommunity, BanFromCommunityResponse}, + get_local_user_view_from_jwt, + is_mod_or_admin, + remove_user_data_in_community, +}; +use lemmy_apub::{ + activities::block::SiteOrCommunity, + objects::{community::ApubCommunity, person::ApubPerson}, + protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser}, +}; +use lemmy_db_schema::{ + source::{ + community::{ + Community, + CommunityFollower, + CommunityFollowerForm, + CommunityPersonBan, + CommunityPersonBanForm, + }, + moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, + person::Person, + }, + traits::{Bannable, Crud, Followable}, +}; +use lemmy_db_views_actor::person_view::PersonViewSafe; +use lemmy_utils::{utils::naive_from_unix, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl Perform for BanFromCommunity { + type Response = BanFromCommunityResponse; + + #[tracing::instrument(skip(context, websocket_id))] + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &BanFromCommunity = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let community_id = data.community_id; + let banned_person_id = data.person_id; + let remove_data = data.remove_data.unwrap_or(false); + let expires = data.expires.map(naive_from_unix); + + // Verify that only mods or admins can ban + is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?; + + let community_user_ban_form = CommunityPersonBanForm { + community_id: data.community_id, + person_id: data.person_id, + expires: Some(expires), + }; + + let community: ApubCommunity = blocking(context.pool(), move |conn: &'_ _| { + Community::read(conn, community_id) + }) + .await?? + .into(); + let banned_person: ApubPerson = blocking(context.pool(), move |conn: &'_ _| { + Person::read(conn, banned_person_id) + }) + .await?? + .into(); + + if data.ban { + let ban = move |conn: &'_ _| CommunityPersonBan::ban(conn, &community_user_ban_form); + blocking(context.pool(), ban) + .await? + .map_err(|e| LemmyError::from_error_message(e, "community_user_already_banned"))?; + + // Also unsubscribe them from the community, if they are subscribed + let community_follower_form = CommunityFollowerForm { + community_id: data.community_id, + person_id: banned_person_id, + pending: false, + }; + blocking(context.pool(), move |conn: &'_ _| { + CommunityFollower::unfollow(conn, &community_follower_form) + }) + .await? + .ok(); + + BlockUser::send( + &SiteOrCommunity::Community(community), + &banned_person, + &local_user_view.person.clone().into(), + remove_data, + data.reason.clone(), + expires, + context, + ) + .await?; + } else { + let unban = move |conn: &'_ _| CommunityPersonBan::unban(conn, &community_user_ban_form); + blocking(context.pool(), unban) + .await? + .map_err(|e| LemmyError::from_error_message(e, "community_user_already_banned"))?; + UndoBlockUser::send( + &SiteOrCommunity::Community(community), + &banned_person, + &local_user_view.person.clone().into(), + data.reason.clone(), + context, + ) + .await?; + } + + // Remove/Restore their data if that's desired + if remove_data { + remove_user_data_in_community(community_id, banned_person_id, context.pool()).await?; + } + + // Mod tables + let form = ModBanFromCommunityForm { + mod_person_id: local_user_view.person.id, + other_person_id: data.person_id, + community_id: data.community_id, + reason: data.reason.to_owned(), + banned: Some(data.ban), + expires, + }; + blocking(context.pool(), move |conn| { + ModBanFromCommunity::create(conn, &form) + }) + .await??; + + let person_id = data.person_id; + let person_view = blocking(context.pool(), move |conn| { + PersonViewSafe::read(conn, person_id) + }) + .await??; + + let res = BanFromCommunityResponse { + person_view, + banned: data.ban, + }; + + context.chat_server().do_send(SendCommunityRoomMessage { + op: UserOperation::BanFromCommunity, + response: res.clone(), + community_id, + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api/src/community/block.rs b/crates/api/src/community/block.rs new file mode 100644 index 00000000..02ece47f --- /dev/null +++ b/crates/api/src/community/block.rs @@ -0,0 +1,80 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + community::{BlockCommunity, BlockCommunityResponse}, + get_local_user_view_from_jwt, +}; +use lemmy_apub::protocol::activities::following::undo_follow::UndoFollowCommunity; +use lemmy_db_schema::{ + source::{ + community::{Community, CommunityFollower, CommunityFollowerForm}, + community_block::{CommunityBlock, CommunityBlockForm}, + }, + traits::{Blockable, Crud, Followable}, +}; +use lemmy_db_views_actor::community_view::CommunityView; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for BlockCommunity { + type Response = BlockCommunityResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &BlockCommunity = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let community_id = data.community_id; + let person_id = local_user_view.person.id; + let community_block_form = CommunityBlockForm { + person_id, + community_id, + }; + + if data.block { + let block = move |conn: &'_ _| CommunityBlock::block(conn, &community_block_form); + blocking(context.pool(), block) + .await? + .map_err(|e| LemmyError::from_error_message(e, "community_block_already_exists"))?; + + // Also, unfollow the community, and send a federated unfollow + let community_follower_form = CommunityFollowerForm { + community_id: data.community_id, + person_id, + pending: false, + }; + blocking(context.pool(), move |conn: &'_ _| { + CommunityFollower::unfollow(conn, &community_follower_form) + }) + .await? + .ok(); + let community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + UndoFollowCommunity::send(&local_user_view.person.into(), &community.into(), context).await?; + } else { + let unblock = move |conn: &'_ _| CommunityBlock::unblock(conn, &community_block_form); + blocking(context.pool(), unblock) + .await? + .map_err(|e| LemmyError::from_error_message(e, "community_block_already_exists"))?; + } + + let community_view = blocking(context.pool(), move |conn| { + CommunityView::read(conn, community_id, Some(person_id)) + }) + .await??; + + Ok(BlockCommunityResponse { + blocked: data.block, + community_view, + }) + } +} diff --git a/crates/api/src/community/follow.rs b/crates/api/src/community/follow.rs new file mode 100644 index 00000000..62eb34bb --- /dev/null +++ b/crates/api/src/community/follow.rs @@ -0,0 +1,97 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_community_ban, + check_community_deleted_or_removed, + community::{CommunityResponse, FollowCommunity}, + get_local_user_view_from_jwt, +}; +use lemmy_apub::{ + objects::community::ApubCommunity, + protocol::activities::following::{ + follow::FollowCommunity as FollowCommunityApub, + undo_follow::UndoFollowCommunity, + }, +}; +use lemmy_db_schema::{ + source::community::{Community, CommunityFollower, CommunityFollowerForm}, + traits::{Crud, Followable}, +}; +use lemmy_db_views_actor::community_view::CommunityView; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for FollowCommunity { + type Response = CommunityResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &FollowCommunity = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let community_id = data.community_id; + let community: ApubCommunity = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await?? + .into(); + let community_follower_form = CommunityFollowerForm { + community_id: data.community_id, + person_id: local_user_view.person.id, + pending: false, + }; + + if community.local { + if data.follow { + check_community_ban(local_user_view.person.id, community_id, context.pool()).await?; + check_community_deleted_or_removed(community_id, context.pool()).await?; + + let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); + blocking(context.pool(), follow) + .await? + .map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?; + } else { + let unfollow = + move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form); + blocking(context.pool(), unfollow) + .await? + .map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?; + } + } else if data.follow { + // Dont actually add to the community followers here, because you need + // to wait for the accept + FollowCommunityApub::send(&local_user_view.person.clone().into(), &community, context) + .await?; + } else { + UndoFollowCommunity::send(&local_user_view.person.clone().into(), &community, context) + .await?; + let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form); + blocking(context.pool(), unfollow) + .await? + .map_err(|e| LemmyError::from_error_message(e, "community_follower_already_exists"))?; + } + + let community_id = data.community_id; + let person_id = local_user_view.person.id; + let mut community_view = blocking(context.pool(), move |conn| { + CommunityView::read(conn, community_id, Some(person_id)) + }) + .await??; + + // TODO: this needs to return a "pending" state, until Accept is received from the remote server + // For now, just assume that remote follows are accepted. + // Otherwise, the subscribed will be null + if !community.local { + community_view.subscribed = data.follow; + } + + Ok(CommunityResponse { community_view }) + } +} diff --git a/crates/api/src/community/mod.rs b/crates/api/src/community/mod.rs new file mode 100644 index 00000000..8bf2ed54 --- /dev/null +++ b/crates/api/src/community/mod.rs @@ -0,0 +1,5 @@ +mod add_mod; +mod ban; +mod block; +mod follow; +mod transfer; diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs new file mode 100644 index 00000000..05282f45 --- /dev/null +++ b/crates/api/src/community/transfer.rs @@ -0,0 +1,124 @@ +use crate::Perform; +use actix_web::web::Data; +use anyhow::Context; +use lemmy_api_common::{ + blocking, + community::{GetCommunityResponse, TransferCommunity}, + get_local_user_view_from_jwt, +}; +use lemmy_db_schema::{ + source::{ + community::{CommunityModerator, CommunityModeratorForm}, + moderator::{ModTransferCommunity, ModTransferCommunityForm}, + }, + traits::{Crud, Joinable}, +}; +use lemmy_db_views_actor::{ + community_moderator_view::CommunityModeratorView, + community_view::CommunityView, + person_view::PersonViewSafe, +}; +use lemmy_utils::{location_info, ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +// TODO: we dont do anything for federation here, it should be updated the next time the community +// gets fetched. i hope we can get rid of the community creator role soon. +#[async_trait::async_trait(?Send)] +impl Perform for TransferCommunity { + type Response = GetCommunityResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &TransferCommunity = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let admins = blocking(context.pool(), PersonViewSafe::admins).await??; + + // Fetch the community mods + let community_id = data.community_id; + let mut community_mods = blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(conn, community_id) + }) + .await??; + + // Make sure transferrer is either the top community mod, or an admin + if local_user_view.person.id != community_mods[0].moderator.id + && !admins + .iter() + .map(|a| a.person.id) + .any(|x| x == local_user_view.person.id) + { + return Err(LemmyError::from_message("not_an_admin")); + } + + // You have to re-do the community_moderator table, reordering it. + // Add the transferee to the top + let creator_index = community_mods + .iter() + .position(|r| r.moderator.id == data.person_id) + .context(location_info!())?; + let creator_person = community_mods.remove(creator_index); + community_mods.insert(0, creator_person); + + // Delete all the mods + let community_id = data.community_id; + blocking(context.pool(), move |conn| { + CommunityModerator::delete_for_community(conn, community_id) + }) + .await??; + + // TODO: this should probably be a bulk operation + // Re-add the mods, in the new order + for cmod in &community_mods { + let community_moderator_form = CommunityModeratorForm { + community_id: cmod.community.id, + person_id: cmod.moderator.id, + }; + + let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); + blocking(context.pool(), join) + .await? + .map_err(|e| LemmyError::from_error_message(e, "community_moderator_already_exists"))?; + } + + // Mod tables + let form = ModTransferCommunityForm { + mod_person_id: local_user_view.person.id, + other_person_id: data.person_id, + community_id: data.community_id, + removed: Some(false), + }; + blocking(context.pool(), move |conn| { + ModTransferCommunity::create(conn, &form) + }) + .await??; + + let community_id = data.community_id; + let person_id = local_user_view.person.id; + let community_view = blocking(context.pool(), move |conn| { + CommunityView::read(conn, community_id, Some(person_id)) + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_community"))?; + + let community_id = data.community_id; + let moderators = blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(conn, community_id) + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_community"))?; + + // Return the jwt + Ok(GetCommunityResponse { + community_view, + site: None, + moderators, + online: 0, + }) + } +} diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index c7c24062..d6de25cc 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -67,7 +67,7 @@ pub async fn match_websocket_operation( do_websocket_operation::(context, id, op, data).await } UserOperation::PasswordChange => { - do_websocket_operation::(context, id, op, data).await + do_websocket_operation::(context, id, op, data).await } UserOperation::UserJoin => do_websocket_operation::(context, id, op, data).await, UserOperation::PostJoin => do_websocket_operation::(context, id, op, data).await, diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs deleted file mode 100644 index 731f7438..00000000 --- a/crates/api/src/local_user.rs +++ /dev/null @@ -1,966 +0,0 @@ -use crate::{captcha_as_wav_base64, Perform}; -use actix_web::web::Data; -use bcrypt::verify; -use captcha::{gen, Difficulty}; -use chrono::Duration; -use lemmy_api_common::{ - blocking, - check_image_has_local_domain, - check_registration_application, - get_local_user_view_from_jwt, - is_admin, - password_length_check, - person::*, - remove_user_data, - send_email_verification_success, - send_password_reset_email, - send_verification_email, -}; -use lemmy_apub::{ - activities::block::SiteOrCommunity, - protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser}, -}; -use lemmy_db_schema::{ - diesel_option_overwrite, - diesel_option_overwrite_to_url, - from_opt_str_to_opt_enum, - naive_now, - source::{ - comment::Comment, - email_verification::EmailVerification, - local_user::{LocalUser, LocalUserForm}, - moderator::*, - password_reset_request::*, - person::*, - person_block::{PersonBlock, PersonBlockForm}, - person_mention::*, - private_message::PrivateMessage, - site::*, - }, - traits::{Blockable, Crud}, - SortType, -}; -use lemmy_db_views::{ - comment_report_view::CommentReportView, - comment_view::{CommentQueryBuilder, CommentView}, - local_user_view::LocalUserView, - post_report_view::PostReportView, - private_message_view::PrivateMessageView, -}; -use lemmy_db_views_actor::{ - person_mention_view::{PersonMentionQueryBuilder, PersonMentionView}, - person_view::PersonViewSafe, -}; -use lemmy_utils::{ - claims::Claims, - utils::{is_valid_display_name, is_valid_matrix_id, naive_from_unix}, - ConnectionId, - LemmyError, -}; -use lemmy_websocket::{ - messages::{CaptchaItem, SendAllMessage}, - LemmyContext, - UserOperation, -}; - -#[async_trait::async_trait(?Send)] -impl Perform for Login { - type Response = LoginResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &Login = self; - - // Fetch that username / email - let username_or_email = data.username_or_email.clone(); - let local_user_view = blocking(context.pool(), move |conn| { - LocalUserView::find_by_email_or_name(conn, &username_or_email) - }) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_that_username_or_email"))?; - - // Verify the password - let valid: bool = verify( - &data.password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); - if !valid { - return Err(LemmyError::from_message("password_incorrect")); - } - - let site = blocking(context.pool(), Site::read_local_site).await??; - if site.require_email_verification && !local_user_view.local_user.email_verified { - return Err(LemmyError::from_message("email_not_verified")); - } - - check_registration_application(&site, &local_user_view, context.pool()).await?; - - // Return the jwt - Ok(LoginResponse { - jwt: Some( - Claims::jwt( - local_user_view.local_user.id.0, - &context.secret().jwt_secret, - &context.settings().hostname, - )? - .into(), - ), - verify_email_sent: false, - registration_created: false, - }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for GetCaptcha { - type Response = GetCaptchaResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let captcha_settings = context.settings().captcha; - - if !captcha_settings.enabled { - return Ok(GetCaptchaResponse { ok: None }); - } - - let captcha = match captcha_settings.difficulty.as_str() { - "easy" => gen(Difficulty::Easy), - "medium" => gen(Difficulty::Medium), - "hard" => gen(Difficulty::Hard), - _ => gen(Difficulty::Medium), - }; - - let answer = captcha.chars_as_string(); - - let png = captcha.as_base64().expect("failed to generate captcha"); - - let uuid = uuid::Uuid::new_v4().to_string(); - - let wav = captcha_as_wav_base64(&captcha); - - let captcha_item = CaptchaItem { - answer, - uuid: uuid.to_owned(), - expires: naive_now() + Duration::minutes(10), // expires in 10 minutes - }; - - // Stores the captcha item on the queue - context.chat_server().do_send(captcha_item); - - Ok(GetCaptchaResponse { - ok: Some(CaptchaResponse { png, wav, uuid }), - }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for SaveUserSettings { - type Response = LoginResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &SaveUserSettings = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let avatar = diesel_option_overwrite_to_url(&data.avatar)?; - let banner = diesel_option_overwrite_to_url(&data.banner)?; - let bio = diesel_option_overwrite(&data.bio); - let display_name = diesel_option_overwrite(&data.display_name); - let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id); - let bot_account = data.bot_account; - let email_deref = data.email.as_deref().map(|e| e.to_owned()); - let email = diesel_option_overwrite(&email_deref); - - check_image_has_local_domain(avatar.as_ref().unwrap_or(&None))?; - check_image_has_local_domain(banner.as_ref().unwrap_or(&None))?; - - if let Some(Some(email)) = &email { - let previous_email = local_user_view.local_user.email.clone().unwrap_or_default(); - // Only send the verification email if there was an email change - if previous_email.ne(email) { - send_verification_email(&local_user_view, email, context.pool(), &context.settings()) - .await?; - } - } - - // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value - if let Some(email) = &email { - let site_fut = blocking(context.pool(), Site::read_local_site); - if email.is_none() && site_fut.await??.require_email_verification { - return Err(LemmyError::from_message("email_required")); - } - } - - if let Some(Some(bio)) = &bio { - if bio.chars().count() > 300 { - return Err(LemmyError::from_message("bio_length_overflow")); - } - } - - if let Some(Some(display_name)) = &display_name { - if !is_valid_display_name( - display_name.trim(), - context.settings().actor_name_max_length, - ) { - return Err(LemmyError::from_message("invalid_username")); - } - } - - if let Some(Some(matrix_user_id)) = &matrix_user_id { - if !is_valid_matrix_id(matrix_user_id) { - return Err(LemmyError::from_message("invalid_matrix_id")); - } - } - - let local_user_id = local_user_view.local_user.id; - let person_id = local_user_view.person.id; - let default_listing_type = data.default_listing_type; - let default_sort_type = data.default_sort_type; - let password_encrypted = local_user_view.local_user.password_encrypted; - let public_key = local_user_view.person.public_key; - - let person_form = PersonForm { - name: local_user_view.person.name, - avatar, - banner, - inbox_url: None, - display_name, - published: None, - updated: Some(naive_now()), - banned: None, - deleted: None, - actor_id: None, - bio, - local: None, - admin: None, - private_key: None, - public_key, - last_refreshed_at: None, - shared_inbox_url: None, - matrix_user_id, - bot_account, - ban_expires: None, - }; - - blocking(context.pool(), move |conn| { - Person::update(conn, person_id, &person_form) - }) - .await? - .map_err(|e| LemmyError::from_error_message(e, "user_already_exists"))?; - - let local_user_form = LocalUserForm { - person_id: Some(person_id), - email, - password_encrypted: Some(password_encrypted), - show_nsfw: data.show_nsfw, - show_bot_accounts: data.show_bot_accounts, - show_scores: data.show_scores, - theme: data.theme.to_owned(), - default_sort_type, - default_listing_type, - lang: data.lang.to_owned(), - show_avatars: data.show_avatars, - show_read_posts: data.show_read_posts, - show_new_post_notifs: data.show_new_post_notifs, - send_notifications_to_email: data.send_notifications_to_email, - email_verified: None, - accepted_application: None, - }; - - let local_user_res = blocking(context.pool(), move |conn| { - LocalUser::update(conn, local_user_id, &local_user_form) - }) - .await?; - let updated_local_user = match local_user_res { - Ok(u) => u, - Err(e) => { - let err_type = if e.to_string() - == "duplicate key value violates unique constraint \"local_user_email_key\"" - { - "email_already_exists" - } else { - "user_already_exists" - }; - - return Err(LemmyError::from_error_message(e, err_type)); - } - }; - - // Return the jwt - Ok(LoginResponse { - jwt: Some( - Claims::jwt( - updated_local_user.id.0, - &context.secret().jwt_secret, - &context.settings().hostname, - )? - .into(), - ), - verify_email_sent: false, - registration_created: false, - }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for ChangePassword { - type Response = LoginResponse; - - #[tracing::instrument(skip(self, context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &ChangePassword = self; - let local_user_view = - get_local_user_view_from_jwt(data.auth.as_ref(), context.pool(), context.secret()).await?; - - password_length_check(&data.new_password)?; - - // Make sure passwords match - if data.new_password != data.new_password_verify { - return Err(LemmyError::from_message("passwords_dont_match")); - } - - // Check the old password - let valid: bool = verify( - &data.old_password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); - if !valid { - return Err(LemmyError::from_message("password_incorrect")); - } - - let local_user_id = local_user_view.local_user.id; - let new_password = data.new_password.to_owned(); - let updated_local_user = blocking(context.pool(), move |conn| { - LocalUser::update_password(conn, local_user_id, &new_password) - }) - .await??; - - // Return the jwt - Ok(LoginResponse { - jwt: Some( - Claims::jwt( - updated_local_user.id.0, - &context.secret().jwt_secret, - &context.settings().hostname, - )? - .into(), - ), - verify_email_sent: false, - registration_created: false, - }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for AddAdmin { - type Response = AddAdminResponse; - - #[tracing::instrument(skip(context, websocket_id))] - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &AddAdmin = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - // Make sure user is an admin - is_admin(&local_user_view)?; - - let added = data.added; - let added_person_id = data.person_id; - let added_admin = blocking(context.pool(), move |conn| { - Person::add_admin(conn, added_person_id, added) - }) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_user"))?; - - // Mod tables - let form = ModAddForm { - mod_person_id: local_user_view.person.id, - other_person_id: added_admin.id, - removed: Some(!data.added), - }; - - blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??; - - let admins = blocking(context.pool(), PersonViewSafe::admins).await??; - - let res = AddAdminResponse { admins }; - - context.chat_server().do_send(SendAllMessage { - op: UserOperation::AddAdmin, - response: res.clone(), - websocket_id, - }); - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for BanPerson { - type Response = BanPersonResponse; - - #[tracing::instrument(skip(context, websocket_id))] - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &BanPerson = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - // Make sure user is an admin - is_admin(&local_user_view)?; - - let ban = data.ban; - let banned_person_id = data.person_id; - let expires = data.expires.map(naive_from_unix); - - let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban, expires); - let person = blocking(context.pool(), ban_person) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_user"))?; - - // Remove their data if that's desired - let remove_data = data.remove_data.unwrap_or(false); - if remove_data { - remove_user_data(person.id, context.pool()).await?; - } - - // Mod tables - let form = ModBanForm { - mod_person_id: local_user_view.person.id, - other_person_id: data.person_id, - reason: data.reason.to_owned(), - banned: Some(data.ban), - expires, - }; - - blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??; - - let person_id = data.person_id; - let person_view = blocking(context.pool(), move |conn| { - PersonViewSafe::read(conn, person_id) - }) - .await??; - - let site = SiteOrCommunity::Site( - blocking(context.pool(), Site::read_local_site) - .await?? - .into(), - ); - // if the action affects a local user, federate to other instances - if person.local { - if ban { - BlockUser::send( - &site, - &person.into(), - &local_user_view.person.into(), - remove_data, - data.reason.clone(), - expires, - context, - ) - .await?; - } else { - UndoBlockUser::send( - &site, - &person.into(), - &local_user_view.person.into(), - data.reason.clone(), - context, - ) - .await?; - } - } - - let res = BanPersonResponse { - person_view, - banned: data.ban, - }; - - context.chat_server().do_send(SendAllMessage { - op: UserOperation::BanPerson, - response: res.clone(), - websocket_id, - }); - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for GetBannedPersons { - type Response = BannedPersonsResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetBannedPersons = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - // Make sure user is an admin - is_admin(&local_user_view)?; - - let banned = blocking(context.pool(), PersonViewSafe::banned).await??; - - let res = Self::Response { banned }; - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for BlockPerson { - type Response = BlockPersonResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &BlockPerson = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let target_id = data.person_id; - let person_id = local_user_view.person.id; - - // Don't let a person block themselves - if target_id == person_id { - return Err(LemmyError::from_message("cant_block_yourself")); - } - - let person_block_form = PersonBlockForm { - person_id, - target_id, - }; - - if data.block { - let block = move |conn: &'_ _| PersonBlock::block(conn, &person_block_form); - blocking(context.pool(), block) - .await? - .map_err(|e| LemmyError::from_error_message(e, "person_block_already_exists"))?; - } else { - let unblock = move |conn: &'_ _| PersonBlock::unblock(conn, &person_block_form); - blocking(context.pool(), unblock) - .await? - .map_err(|e| LemmyError::from_error_message(e, "person_block_already_exists"))?; - } - - // TODO does any federated stuff need to be done here? - - let person_view = blocking(context.pool(), move |conn| { - PersonViewSafe::read(conn, target_id) - }) - .await??; - - let res = BlockPersonResponse { - person_view, - blocked: data.block, - }; - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for GetReplies { - type Response = GetRepliesResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetReplies = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let sort: Option = from_opt_str_to_opt_enum(&data.sort); - - let page = data.page; - let limit = data.limit; - let unread_only = data.unread_only; - let person_id = local_user_view.person.id; - let show_bot_accounts = local_user_view.local_user.show_bot_accounts; - - let replies = blocking(context.pool(), move |conn| { - CommentQueryBuilder::create(conn) - .sort(sort) - .unread_only(unread_only) - .recipient_id(person_id) - .show_bot_accounts(show_bot_accounts) - .my_person_id(person_id) - .page(page) - .limit(limit) - .list() - }) - .await??; - - Ok(GetRepliesResponse { replies }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for GetPersonMentions { - type Response = GetPersonMentionsResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetPersonMentions = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let sort: Option = from_opt_str_to_opt_enum(&data.sort); - - let page = data.page; - let limit = data.limit; - let unread_only = data.unread_only; - let person_id = local_user_view.person.id; - let mentions = blocking(context.pool(), move |conn| { - PersonMentionQueryBuilder::create(conn) - .recipient_id(person_id) - .my_person_id(person_id) - .sort(sort) - .unread_only(unread_only) - .page(page) - .limit(limit) - .list() - }) - .await??; - - Ok(GetPersonMentionsResponse { mentions }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for MarkPersonMentionAsRead { - type Response = PersonMentionResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &MarkPersonMentionAsRead = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let person_mention_id = data.person_mention_id; - let read_person_mention = blocking(context.pool(), move |conn| { - PersonMention::read(conn, person_mention_id) - }) - .await??; - - if local_user_view.person.id != read_person_mention.recipient_id { - return Err(LemmyError::from_message("couldnt_update_comment")); - } - - let person_mention_id = read_person_mention.id; - let read = data.read; - let update_mention = - move |conn: &'_ _| PersonMention::update_read(conn, person_mention_id, read); - blocking(context.pool(), update_mention) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?; - - let person_mention_id = read_person_mention.id; - let person_id = local_user_view.person.id; - let person_mention_view = blocking(context.pool(), move |conn| { - PersonMentionView::read(conn, person_mention_id, Some(person_id)) - }) - .await??; - - Ok(PersonMentionResponse { - person_mention_view, - }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for MarkAllAsRead { - type Response = GetRepliesResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &MarkAllAsRead = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let person_id = local_user_view.person.id; - let replies = blocking(context.pool(), move |conn| { - CommentQueryBuilder::create(conn) - .my_person_id(person_id) - .recipient_id(person_id) - .unread_only(true) - .page(1) - .limit(999) - .list() - }) - .await??; - - // TODO: this should probably be a bulk operation - // Not easy to do as a bulk operation, - // because recipient_id isn't in the comment table - for comment_view in &replies { - let reply_id = comment_view.comment.id; - let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true); - blocking(context.pool(), mark_as_read) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?; - } - - // Mark all user mentions as read - let update_person_mentions = - move |conn: &'_ _| PersonMention::mark_all_as_read(conn, person_id); - blocking(context.pool(), update_person_mentions) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?; - - // Mark all private_messages as read - let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, person_id); - blocking(context.pool(), update_pm) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_private_message"))?; - - Ok(GetRepliesResponse { replies: vec![] }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for PasswordReset { - type Response = PasswordResetResponse; - - #[tracing::instrument(skip(self, context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &PasswordReset = self; - - // Fetch that email - let email = data.email.clone(); - let local_user_view = blocking(context.pool(), move |conn| { - LocalUserView::find_by_email(conn, &email) - }) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_that_username_or_email"))?; - - // Email the pure token to the user. - send_password_reset_email(&local_user_view, context.pool(), &context.settings()).await?; - Ok(PasswordResetResponse {}) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for PasswordChange { - type Response = LoginResponse; - - #[tracing::instrument(skip(self, context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &PasswordChange = self; - - // Fetch the user_id from the token - let token = data.token.clone(); - let local_user_id = blocking(context.pool(), move |conn| { - PasswordResetRequest::read_from_token(conn, &token).map(|p| p.local_user_id) - }) - .await??; - - password_length_check(&data.password)?; - - // Make sure passwords match - if data.password != data.password_verify { - return Err(LemmyError::from_message("passwords_dont_match")); - } - - // Update the user with the new password - let password = data.password.clone(); - let updated_local_user = blocking(context.pool(), move |conn| { - LocalUser::update_password(conn, local_user_id, &password) - }) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_user"))?; - - // Return the jwt - Ok(LoginResponse { - jwt: Some( - Claims::jwt( - updated_local_user.id.0, - &context.secret().jwt_secret, - &context.settings().hostname, - )? - .into(), - ), - verify_email_sent: false, - registration_created: false, - }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for GetReportCount { - type Response = GetReportCountResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetReportCount = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let person_id = local_user_view.person.id; - let admin = local_user_view.person.admin; - let community_id = data.community_id; - - let comment_reports = blocking(context.pool(), move |conn| { - CommentReportView::get_report_count(conn, person_id, admin, community_id) - }) - .await??; - - let post_reports = blocking(context.pool(), move |conn| { - PostReportView::get_report_count(conn, person_id, admin, community_id) - }) - .await??; - - let res = GetReportCountResponse { - community_id, - comment_reports, - post_reports, - }; - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for GetUnreadCount { - type Response = GetUnreadCountResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let person_id = local_user_view.person.id; - - let replies = blocking(context.pool(), move |conn| { - CommentView::get_unread_replies(conn, person_id) - }) - .await??; - - let mentions = blocking(context.pool(), move |conn| { - PersonMentionView::get_unread_mentions(conn, person_id) - }) - .await??; - - let private_messages = blocking(context.pool(), move |conn| { - PrivateMessageView::get_unread_messages(conn, person_id) - }) - .await??; - - let res = Self::Response { - replies, - mentions, - private_messages, - }; - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for VerifyEmail { - type Response = VerifyEmailResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let token = self.token.clone(); - let verification = blocking(context.pool(), move |conn| { - EmailVerification::read_for_token(conn, &token) - }) - .await? - .map_err(|e| LemmyError::from_error_message(e, "token_not_found"))?; - - let form = LocalUserForm { - // necessary in case this is a new signup - email_verified: Some(true), - // necessary in case email of an existing user was changed - email: Some(Some(verification.email)), - ..LocalUserForm::default() - }; - let local_user_id = verification.local_user_id; - blocking(context.pool(), move |conn| { - LocalUser::update(conn, local_user_id, &form) - }) - .await??; - - let local_user_view = blocking(context.pool(), move |conn| { - LocalUserView::read(conn, local_user_id) - }) - .await??; - - send_email_verification_success(&local_user_view, &context.settings())?; - - blocking(context.pool(), move |conn| { - EmailVerification::delete_old_tokens_for_local_user(conn, local_user_id) - }) - .await??; - - Ok(VerifyEmailResponse {}) - } -} diff --git a/crates/api/src/local_user/add_admin.rs b/crates/api/src/local_user/add_admin.rs new file mode 100644 index 00000000..0630e241 --- /dev/null +++ b/crates/api/src/local_user/add_admin.rs @@ -0,0 +1,66 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + is_admin, + person::{AddAdmin, AddAdminResponse}, +}; +use lemmy_db_schema::{ + source::{ + moderator::{ModAdd, ModAddForm}, + person::Person, + }, + traits::Crud, +}; +use lemmy_db_views_actor::person_view::PersonViewSafe; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendAllMessage, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl Perform for AddAdmin { + type Response = AddAdminResponse; + + #[tracing::instrument(skip(context, websocket_id))] + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &AddAdmin = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + // Make sure user is an admin + is_admin(&local_user_view)?; + + let added = data.added; + let added_person_id = data.person_id; + let added_admin = blocking(context.pool(), move |conn| { + Person::add_admin(conn, added_person_id, added) + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_user"))?; + + // Mod tables + let form = ModAddForm { + mod_person_id: local_user_view.person.id, + other_person_id: added_admin.id, + removed: Some(!data.added), + }; + + blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??; + + let admins = blocking(context.pool(), PersonViewSafe::admins).await??; + + let res = AddAdminResponse { admins }; + + context.chat_server().do_send(SendAllMessage { + op: UserOperation::AddAdmin, + response: res.clone(), + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs new file mode 100644 index 00000000..0397d090 --- /dev/null +++ b/crates/api/src/local_user/ban_person.rs @@ -0,0 +1,118 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + is_admin, + person::{BanPerson, BanPersonResponse}, + remove_user_data, +}; +use lemmy_apub::{ + activities::block::SiteOrCommunity, + protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser}, +}; +use lemmy_db_schema::{ + source::{ + moderator::{ModBan, ModBanForm}, + person::Person, + site::Site, + }, + traits::Crud, +}; +use lemmy_db_views_actor::person_view::PersonViewSafe; +use lemmy_utils::{utils::naive_from_unix, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendAllMessage, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl Perform for BanPerson { + type Response = BanPersonResponse; + + #[tracing::instrument(skip(context, websocket_id))] + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &BanPerson = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + // Make sure user is an admin + is_admin(&local_user_view)?; + + let ban = data.ban; + let banned_person_id = data.person_id; + let expires = data.expires.map(naive_from_unix); + + let ban_person = move |conn: &'_ _| Person::ban_person(conn, banned_person_id, ban, expires); + let person = blocking(context.pool(), ban_person) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_user"))?; + + // Remove their data if that's desired + let remove_data = data.remove_data.unwrap_or(false); + if remove_data { + remove_user_data(person.id, context.pool()).await?; + } + + // Mod tables + let form = ModBanForm { + mod_person_id: local_user_view.person.id, + other_person_id: data.person_id, + reason: data.reason.to_owned(), + banned: Some(data.ban), + expires, + }; + + blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??; + + let person_id = data.person_id; + let person_view = blocking(context.pool(), move |conn| { + PersonViewSafe::read(conn, person_id) + }) + .await??; + + let site = SiteOrCommunity::Site( + blocking(context.pool(), Site::read_local_site) + .await?? + .into(), + ); + // if the action affects a local user, federate to other instances + if person.local { + if ban { + BlockUser::send( + &site, + &person.into(), + &local_user_view.person.into(), + remove_data, + data.reason.clone(), + expires, + context, + ) + .await?; + } else { + UndoBlockUser::send( + &site, + &person.into(), + &local_user_view.person.into(), + data.reason.clone(), + context, + ) + .await?; + } + } + + let res = BanPersonResponse { + person_view, + banned: data.ban, + }; + + context.chat_server().do_send(SendAllMessage { + op: UserOperation::BanPerson, + response: res.clone(), + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api/src/local_user/block.rs b/crates/api/src/local_user/block.rs new file mode 100644 index 00000000..5dc68ddd --- /dev/null +++ b/crates/api/src/local_user/block.rs @@ -0,0 +1,67 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + person::{BlockPerson, BlockPersonResponse}, +}; +use lemmy_db_schema::{ + source::person_block::{PersonBlock, PersonBlockForm}, + traits::Blockable, +}; +use lemmy_db_views_actor::person_view::PersonViewSafe; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for BlockPerson { + type Response = BlockPersonResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &BlockPerson = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let target_id = data.person_id; + let person_id = local_user_view.person.id; + + // Don't let a person block themselves + if target_id == person_id { + return Err(LemmyError::from_message("cant_block_yourself")); + } + + let person_block_form = PersonBlockForm { + person_id, + target_id, + }; + + if data.block { + let block = move |conn: &'_ _| PersonBlock::block(conn, &person_block_form); + blocking(context.pool(), block) + .await? + .map_err(|e| LemmyError::from_error_message(e, "person_block_already_exists"))?; + } else { + let unblock = move |conn: &'_ _| PersonBlock::unblock(conn, &person_block_form); + blocking(context.pool(), unblock) + .await? + .map_err(|e| LemmyError::from_error_message(e, "person_block_already_exists"))?; + } + + let person_view = blocking(context.pool(), move |conn| { + PersonViewSafe::read(conn, target_id) + }) + .await??; + + let res = BlockPersonResponse { + person_view, + blocked: data.block, + }; + + Ok(res) + } +} diff --git a/crates/api/src/local_user/change_password.rs b/crates/api/src/local_user/change_password.rs new file mode 100644 index 00000000..2b66f2ea --- /dev/null +++ b/crates/api/src/local_user/change_password.rs @@ -0,0 +1,66 @@ +use crate::Perform; +use actix_web::web::Data; +use bcrypt::verify; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + password_length_check, + person::{ChangePassword, LoginResponse}, +}; +use lemmy_db_schema::source::local_user::LocalUser; +use lemmy_utils::{claims::Claims, ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for ChangePassword { + type Response = LoginResponse; + + #[tracing::instrument(skip(self, context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &ChangePassword = self; + let local_user_view = + get_local_user_view_from_jwt(data.auth.as_ref(), context.pool(), context.secret()).await?; + + password_length_check(&data.new_password)?; + + // Make sure passwords match + if data.new_password != data.new_password_verify { + return Err(LemmyError::from_message("passwords_dont_match")); + } + + // Check the old password + let valid: bool = verify( + &data.old_password, + &local_user_view.local_user.password_encrypted, + ) + .unwrap_or(false); + if !valid { + return Err(LemmyError::from_message("password_incorrect")); + } + + let local_user_id = local_user_view.local_user.id; + let new_password = data.new_password.to_owned(); + let updated_local_user = blocking(context.pool(), move |conn| { + LocalUser::update_password(conn, local_user_id, &new_password) + }) + .await??; + + // Return the jwt + Ok(LoginResponse { + jwt: Some( + Claims::jwt( + updated_local_user.id.0, + &context.secret().jwt_secret, + &context.settings().hostname, + )? + .into(), + ), + verify_email_sent: false, + registration_created: false, + }) + } +} diff --git a/crates/api/src/local_user/change_password_after_reset.rs b/crates/api/src/local_user/change_password_after_reset.rs new file mode 100644 index 00000000..533a0d19 --- /dev/null +++ b/crates/api/src/local_user/change_password_after_reset.rs @@ -0,0 +1,63 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + password_length_check, + person::{LoginResponse, PasswordChangeAfterReset}, +}; +use lemmy_db_schema::source::{ + local_user::LocalUser, + password_reset_request::PasswordResetRequest, +}; +use lemmy_utils::{claims::Claims, ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for PasswordChangeAfterReset { + type Response = LoginResponse; + + #[tracing::instrument(skip(self, context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &PasswordChangeAfterReset = self; + + // Fetch the user_id from the token + let token = data.token.clone(); + let local_user_id = blocking(context.pool(), move |conn| { + PasswordResetRequest::read_from_token(conn, &token).map(|p| p.local_user_id) + }) + .await??; + + password_length_check(&data.password)?; + + // Make sure passwords match + if data.password != data.password_verify { + return Err(LemmyError::from_message("passwords_dont_match")); + } + + // Update the user with the new password + let password = data.password.clone(); + let updated_local_user = blocking(context.pool(), move |conn| { + LocalUser::update_password(conn, local_user_id, &password) + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_user"))?; + + // Return the jwt + Ok(LoginResponse { + jwt: Some( + Claims::jwt( + updated_local_user.id.0, + &context.secret().jwt_secret, + &context.settings().hostname, + )? + .into(), + ), + verify_email_sent: false, + registration_created: false, + }) + } +} diff --git a/crates/api/src/local_user/get_captcha.rs b/crates/api/src/local_user/get_captcha.rs new file mode 100644 index 00000000..efcb6689 --- /dev/null +++ b/crates/api/src/local_user/get_captcha.rs @@ -0,0 +1,53 @@ +use crate::{captcha_as_wav_base64, Perform}; +use actix_web::web::Data; +use captcha::{gen, Difficulty}; +use chrono::Duration; +use lemmy_api_common::person::{CaptchaResponse, GetCaptcha, GetCaptchaResponse}; +use lemmy_db_schema::naive_now; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::{messages::CaptchaItem, LemmyContext}; + +#[async_trait::async_trait(?Send)] +impl Perform for GetCaptcha { + type Response = GetCaptchaResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let captcha_settings = context.settings().captcha; + + if !captcha_settings.enabled { + return Ok(GetCaptchaResponse { ok: None }); + } + + let captcha = gen(match captcha_settings.difficulty.as_str() { + "easy" => Difficulty::Easy, + "hard" => Difficulty::Hard, + _ => Difficulty::Medium, + }); + + let answer = captcha.chars_as_string(); + + let png = captcha.as_base64().expect("failed to generate captcha"); + + let uuid = uuid::Uuid::new_v4().to_string(); + + let wav = captcha_as_wav_base64(&captcha); + + let captcha_item = CaptchaItem { + answer, + uuid: uuid.to_owned(), + expires: naive_now() + Duration::minutes(10), // expires in 10 minutes + }; + + // Stores the captcha item on the queue + context.chat_server().do_send(captcha_item); + + Ok(GetCaptchaResponse { + ok: Some(CaptchaResponse { png, wav, uuid }), + }) + } +} diff --git a/crates/api/src/local_user/list_banned.rs b/crates/api/src/local_user/list_banned.rs new file mode 100644 index 00000000..1fef5593 --- /dev/null +++ b/crates/api/src/local_user/list_banned.rs @@ -0,0 +1,35 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + is_admin, + person::{BannedPersonsResponse, GetBannedPersons}, +}; +use lemmy_db_views_actor::person_view::PersonViewSafe; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for GetBannedPersons { + type Response = BannedPersonsResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &GetBannedPersons = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + // Make sure user is an admin + is_admin(&local_user_view)?; + + let banned = blocking(context.pool(), PersonViewSafe::banned).await??; + + let res = Self::Response { banned }; + + Ok(res) + } +} diff --git a/crates/api/src/local_user/login.rs b/crates/api/src/local_user/login.rs new file mode 100644 index 00000000..5455b867 --- /dev/null +++ b/crates/api/src/local_user/login.rs @@ -0,0 +1,65 @@ +use crate::Perform; +use actix_web::web::Data; +use bcrypt::verify; +use lemmy_api_common::{ + blocking, + check_registration_application, + person::{Login, LoginResponse}, +}; +use lemmy_db_schema::source::site::Site; +use lemmy_db_views::local_user_view::LocalUserView; +use lemmy_utils::{claims::Claims, ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for Login { + type Response = LoginResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &Login = self; + + // Fetch that username / email + let username_or_email = data.username_or_email.clone(); + let local_user_view = blocking(context.pool(), move |conn| { + LocalUserView::find_by_email_or_name(conn, &username_or_email) + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_that_username_or_email"))?; + + // Verify the password + let valid: bool = verify( + &data.password, + &local_user_view.local_user.password_encrypted, + ) + .unwrap_or(false); + if !valid { + return Err(LemmyError::from_message("password_incorrect")); + } + + let site = blocking(context.pool(), Site::read_local_site).await??; + if site.require_email_verification && !local_user_view.local_user.email_verified { + return Err(LemmyError::from_message("email_not_verified")); + } + + check_registration_application(&site, &local_user_view, context.pool()).await?; + + // Return the jwt + Ok(LoginResponse { + jwt: Some( + Claims::jwt( + local_user_view.local_user.id.0, + &context.secret().jwt_secret, + &context.settings().hostname, + )? + .into(), + ), + verify_email_sent: false, + registration_created: false, + }) + } +} diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs new file mode 100644 index 00000000..3a92beda --- /dev/null +++ b/crates/api/src/local_user/mod.rs @@ -0,0 +1,13 @@ +mod add_admin; +mod ban_person; +mod block; +mod change_password; +mod change_password_after_reset; +mod get_captcha; +mod list_banned; +mod login; +mod notifications; +mod report_count; +mod reset_password; +mod save_settings; +mod verify_email; diff --git a/crates/api/src/local_user/notifications/list_mentions.rs b/crates/api/src/local_user/notifications/list_mentions.rs new file mode 100644 index 00000000..f47d3cc7 --- /dev/null +++ b/crates/api/src/local_user/notifications/list_mentions.rs @@ -0,0 +1,47 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + person::{GetPersonMentions, GetPersonMentionsResponse}, +}; +use lemmy_db_schema::{from_opt_str_to_opt_enum, SortType}; +use lemmy_db_views_actor::person_mention_view::PersonMentionQueryBuilder; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for GetPersonMentions { + type Response = GetPersonMentionsResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &GetPersonMentions = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let sort: Option = from_opt_str_to_opt_enum(&data.sort); + + let page = data.page; + let limit = data.limit; + let unread_only = data.unread_only; + let person_id = local_user_view.person.id; + let mentions = blocking(context.pool(), move |conn| { + PersonMentionQueryBuilder::create(conn) + .recipient_id(person_id) + .my_person_id(person_id) + .sort(sort) + .unread_only(unread_only) + .page(page) + .limit(limit) + .list() + }) + .await??; + + Ok(GetPersonMentionsResponse { mentions }) + } +} diff --git a/crates/api/src/local_user/notifications/list_replies.rs b/crates/api/src/local_user/notifications/list_replies.rs new file mode 100644 index 00000000..9199421c --- /dev/null +++ b/crates/api/src/local_user/notifications/list_replies.rs @@ -0,0 +1,50 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + person::{GetReplies, GetRepliesResponse}, +}; +use lemmy_db_schema::{from_opt_str_to_opt_enum, SortType}; +use lemmy_db_views::comment_view::CommentQueryBuilder; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for GetReplies { + type Response = GetRepliesResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &GetReplies = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let sort: Option = from_opt_str_to_opt_enum(&data.sort); + + let page = data.page; + let limit = data.limit; + let unread_only = data.unread_only; + let person_id = local_user_view.person.id; + let show_bot_accounts = local_user_view.local_user.show_bot_accounts; + + let replies = blocking(context.pool(), move |conn| { + CommentQueryBuilder::create(conn) + .sort(sort) + .unread_only(unread_only) + .recipient_id(person_id) + .show_bot_accounts(show_bot_accounts) + .my_person_id(person_id) + .page(page) + .limit(limit) + .list() + }) + .await??; + + Ok(GetRepliesResponse { replies }) + } +} diff --git a/crates/api/src/local_user/notifications/mark_all_read.rs b/crates/api/src/local_user/notifications/mark_all_read.rs new file mode 100644 index 00000000..dc3b00d1 --- /dev/null +++ b/crates/api/src/local_user/notifications/mark_all_read.rs @@ -0,0 +1,69 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + person::{GetRepliesResponse, MarkAllAsRead}, +}; +use lemmy_db_schema::source::{ + comment::Comment, + person_mention::PersonMention, + private_message::PrivateMessage, +}; +use lemmy_db_views::comment_view::CommentQueryBuilder; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for MarkAllAsRead { + type Response = GetRepliesResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &MarkAllAsRead = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let person_id = local_user_view.person.id; + let replies = blocking(context.pool(), move |conn| { + CommentQueryBuilder::create(conn) + .my_person_id(person_id) + .recipient_id(person_id) + .unread_only(true) + .page(1) + .limit(999) + .list() + }) + .await??; + + // TODO: this should probably be a bulk operation + // Not easy to do as a bulk operation, + // because recipient_id isn't in the comment table + for comment_view in &replies { + let reply_id = comment_view.comment.id; + let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true); + blocking(context.pool(), mark_as_read) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?; + } + + // Mark all user mentions as read + let update_person_mentions = + move |conn: &'_ _| PersonMention::mark_all_as_read(conn, person_id); + blocking(context.pool(), update_person_mentions) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?; + + // Mark all private_messages as read + let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, person_id); + blocking(context.pool(), update_pm) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_private_message"))?; + + Ok(GetRepliesResponse { replies: vec![] }) + } +} diff --git a/crates/api/src/local_user/notifications/mark_mention_read.rs b/crates/api/src/local_user/notifications/mark_mention_read.rs new file mode 100644 index 00000000..33e427a6 --- /dev/null +++ b/crates/api/src/local_user/notifications/mark_mention_read.rs @@ -0,0 +1,56 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + person::{MarkPersonMentionAsRead, PersonMentionResponse}, +}; +use lemmy_db_schema::{source::person_mention::PersonMention, traits::Crud}; +use lemmy_db_views_actor::person_mention_view::PersonMentionView; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for MarkPersonMentionAsRead { + type Response = PersonMentionResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &MarkPersonMentionAsRead = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let person_mention_id = data.person_mention_id; + let read_person_mention = blocking(context.pool(), move |conn| { + PersonMention::read(conn, person_mention_id) + }) + .await??; + + if local_user_view.person.id != read_person_mention.recipient_id { + return Err(LemmyError::from_message("couldnt_update_comment")); + } + + let person_mention_id = read_person_mention.id; + let read = data.read; + let update_mention = + move |conn: &'_ _| PersonMention::update_read(conn, person_mention_id, read); + blocking(context.pool(), update_mention) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?; + + let person_mention_id = read_person_mention.id; + let person_id = local_user_view.person.id; + let person_mention_view = blocking(context.pool(), move |conn| { + PersonMentionView::read(conn, person_mention_id, Some(person_id)) + }) + .await??; + + Ok(PersonMentionResponse { + person_mention_view, + }) + } +} diff --git a/crates/api/src/local_user/notifications/mod.rs b/crates/api/src/local_user/notifications/mod.rs new file mode 100644 index 00000000..ba90293d --- /dev/null +++ b/crates/api/src/local_user/notifications/mod.rs @@ -0,0 +1,5 @@ +mod list_mentions; +mod list_replies; +mod mark_all_read; +mod mark_mention_read; +mod unread_count; diff --git a/crates/api/src/local_user/notifications/unread_count.rs b/crates/api/src/local_user/notifications/unread_count.rs new file mode 100644 index 00000000..be805563 --- /dev/null +++ b/crates/api/src/local_user/notifications/unread_count.rs @@ -0,0 +1,52 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + person::{GetUnreadCount, GetUnreadCountResponse}, +}; +use lemmy_db_views::{comment_view::CommentView, private_message_view::PrivateMessageView}; +use lemmy_db_views_actor::person_mention_view::PersonMentionView; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for GetUnreadCount { + type Response = GetUnreadCountResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let person_id = local_user_view.person.id; + + let replies = blocking(context.pool(), move |conn| { + CommentView::get_unread_replies(conn, person_id) + }) + .await??; + + let mentions = blocking(context.pool(), move |conn| { + PersonMentionView::get_unread_mentions(conn, person_id) + }) + .await??; + + let private_messages = blocking(context.pool(), move |conn| { + PrivateMessageView::get_unread_messages(conn, person_id) + }) + .await??; + + let res = Self::Response { + replies, + mentions, + private_messages, + }; + + Ok(res) + } +} diff --git a/crates/api/src/local_user/report_count.rs b/crates/api/src/local_user/report_count.rs new file mode 100644 index 00000000..5aa2f46d --- /dev/null +++ b/crates/api/src/local_user/report_count.rs @@ -0,0 +1,48 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + person::{GetReportCount, GetReportCountResponse}, +}; +use lemmy_db_views::{comment_report_view::CommentReportView, post_report_view::PostReportView}; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for GetReportCount { + type Response = GetReportCountResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &GetReportCount = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let person_id = local_user_view.person.id; + let admin = local_user_view.person.admin; + let community_id = data.community_id; + + let comment_reports = blocking(context.pool(), move |conn| { + CommentReportView::get_report_count(conn, person_id, admin, community_id) + }) + .await??; + + let post_reports = blocking(context.pool(), move |conn| { + PostReportView::get_report_count(conn, person_id, admin, community_id) + }) + .await??; + + let res = GetReportCountResponse { + community_id, + comment_reports, + post_reports, + }; + + Ok(res) + } +} diff --git a/crates/api/src/local_user/reset_password.rs b/crates/api/src/local_user/reset_password.rs new file mode 100644 index 00000000..78b0364c --- /dev/null +++ b/crates/api/src/local_user/reset_password.rs @@ -0,0 +1,36 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + person::{PasswordReset, PasswordResetResponse}, + send_password_reset_email, +}; +use lemmy_db_views::local_user_view::LocalUserView; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for PasswordReset { + type Response = PasswordResetResponse; + + #[tracing::instrument(skip(self, context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &PasswordReset = self; + + // Fetch that email + let email = data.email.clone(); + let local_user_view = blocking(context.pool(), move |conn| { + LocalUserView::find_by_email(conn, &email) + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_that_username_or_email"))?; + + // Email the pure token to the user. + send_password_reset_email(&local_user_view, context.pool(), &context.settings()).await?; + Ok(PasswordResetResponse {}) + } +} diff --git a/crates/api/src/local_user/save_settings.rs b/crates/api/src/local_user/save_settings.rs new file mode 100644 index 00000000..15a65b01 --- /dev/null +++ b/crates/api/src/local_user/save_settings.rs @@ -0,0 +1,181 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_image_has_local_domain, + get_local_user_view_from_jwt, + person::{LoginResponse, SaveUserSettings}, + send_verification_email, +}; +use lemmy_db_schema::{ + diesel_option_overwrite, + diesel_option_overwrite_to_url, + naive_now, + source::{ + local_user::{LocalUser, LocalUserForm}, + person::{Person, PersonForm}, + site::Site, + }, + traits::Crud, +}; +use lemmy_utils::{ + claims::Claims, + utils::{is_valid_display_name, is_valid_matrix_id}, + ConnectionId, + LemmyError, +}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for SaveUserSettings { + type Response = LoginResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &SaveUserSettings = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let avatar = diesel_option_overwrite_to_url(&data.avatar)?; + let banner = diesel_option_overwrite_to_url(&data.banner)?; + let bio = diesel_option_overwrite(&data.bio); + let display_name = diesel_option_overwrite(&data.display_name); + let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id); + let bot_account = data.bot_account; + let email_deref = data.email.as_deref().map(|e| e.to_owned()); + let email = diesel_option_overwrite(&email_deref); + + check_image_has_local_domain(avatar.as_ref().unwrap_or(&None))?; + check_image_has_local_domain(banner.as_ref().unwrap_or(&None))?; + + if let Some(Some(email)) = &email { + let previous_email = local_user_view.local_user.email.clone().unwrap_or_default(); + // Only send the verification email if there was an email change + if previous_email.ne(email) { + send_verification_email(&local_user_view, email, context.pool(), &context.settings()) + .await?; + } + } + + // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value + if let Some(email) = &email { + let site_fut = blocking(context.pool(), Site::read_local_site); + if email.is_none() && site_fut.await??.require_email_verification { + return Err(LemmyError::from_message("email_required")); + } + } + + if let Some(Some(bio)) = &bio { + if bio.chars().count() > 300 { + return Err(LemmyError::from_message("bio_length_overflow")); + } + } + + if let Some(Some(display_name)) = &display_name { + if !is_valid_display_name( + display_name.trim(), + context.settings().actor_name_max_length, + ) { + return Err(LemmyError::from_message("invalid_username")); + } + } + + if let Some(Some(matrix_user_id)) = &matrix_user_id { + if !is_valid_matrix_id(matrix_user_id) { + return Err(LemmyError::from_message("invalid_matrix_id")); + } + } + + let local_user_id = local_user_view.local_user.id; + let person_id = local_user_view.person.id; + let default_listing_type = data.default_listing_type; + let default_sort_type = data.default_sort_type; + let password_encrypted = local_user_view.local_user.password_encrypted; + let public_key = local_user_view.person.public_key; + + let person_form = PersonForm { + name: local_user_view.person.name, + avatar, + banner, + inbox_url: None, + display_name, + published: None, + updated: Some(naive_now()), + banned: None, + deleted: None, + actor_id: None, + bio, + local: None, + admin: None, + private_key: None, + public_key, + last_refreshed_at: None, + shared_inbox_url: None, + matrix_user_id, + bot_account, + ban_expires: None, + }; + + blocking(context.pool(), move |conn| { + Person::update(conn, person_id, &person_form) + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "user_already_exists"))?; + + let local_user_form = LocalUserForm { + person_id: Some(person_id), + email, + password_encrypted: Some(password_encrypted), + show_nsfw: data.show_nsfw, + show_bot_accounts: data.show_bot_accounts, + show_scores: data.show_scores, + theme: data.theme.to_owned(), + default_sort_type, + default_listing_type, + lang: data.lang.to_owned(), + show_avatars: data.show_avatars, + show_read_posts: data.show_read_posts, + show_new_post_notifs: data.show_new_post_notifs, + send_notifications_to_email: data.send_notifications_to_email, + email_verified: None, + accepted_application: None, + }; + + let local_user_res = blocking(context.pool(), move |conn| { + LocalUser::update(conn, local_user_id, &local_user_form) + }) + .await?; + let updated_local_user = match local_user_res { + Ok(u) => u, + Err(e) => { + let err_type = if e.to_string() + == "duplicate key value violates unique constraint \"local_user_email_key\"" + { + "email_already_exists" + } else { + "user_already_exists" + }; + + return Err(LemmyError::from_error_message(e, err_type)); + } + }; + + // Return the jwt + Ok(LoginResponse { + jwt: Some( + Claims::jwt( + updated_local_user.id.0, + &context.secret().jwt_secret, + &context.settings().hostname, + )? + .into(), + ), + verify_email_sent: false, + registration_created: false, + }) + } +} diff --git a/crates/api/src/local_user/verify_email.rs b/crates/api/src/local_user/verify_email.rs new file mode 100644 index 00000000..852e1cfa --- /dev/null +++ b/crates/api/src/local_user/verify_email.rs @@ -0,0 +1,62 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + person::{VerifyEmail, VerifyEmailResponse}, + send_email_verification_success, +}; +use lemmy_db_schema::{ + source::{ + email_verification::EmailVerification, + local_user::{LocalUser, LocalUserForm}, + }, + traits::Crud, +}; +use lemmy_db_views::local_user_view::LocalUserView; +use lemmy_utils::LemmyError; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for VerifyEmail { + type Response = VerifyEmailResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let token = self.token.clone(); + let verification = blocking(context.pool(), move |conn| { + EmailVerification::read_for_token(conn, &token) + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "token_not_found"))?; + + let form = LocalUserForm { + // necessary in case this is a new signup + email_verified: Some(true), + // necessary in case email of an existing user was changed + email: Some(Some(verification.email)), + ..LocalUserForm::default() + }; + let local_user_id = verification.local_user_id; + blocking(context.pool(), move |conn| { + LocalUser::update(conn, local_user_id, &form) + }) + .await??; + + let local_user_view = blocking(context.pool(), move |conn| { + LocalUserView::read(conn, local_user_id) + }) + .await??; + + send_email_verification_success(&local_user_view, &context.settings())?; + + blocking(context.pool(), move |conn| { + EmailVerification::delete_old_tokens_for_local_user(conn, local_user_id) + }) + .await??; + + Ok(VerifyEmailResponse {}) + } +} diff --git a/crates/api/src/post.rs b/crates/api/src/post.rs deleted file mode 100644 index 6f0ab00e..00000000 --- a/crates/api/src/post.rs +++ /dev/null @@ -1,361 +0,0 @@ -use crate::Perform; -use actix_web::web::Data; -use lemmy_api_common::{ - blocking, - check_community_ban, - check_community_deleted_or_removed, - check_downvotes_enabled, - get_local_user_view_from_jwt, - is_mod_or_admin, - mark_post_as_read, - mark_post_as_unread, - post::*, -}; -use lemmy_apub::{ - fetcher::post_or_comment::PostOrComment, - objects::post::ApubPost, - protocol::activities::{ - create_or_update::post::CreateOrUpdatePost, - voting::{ - undo_vote::UndoVote, - vote::{Vote, VoteType}, - }, - CreateOrUpdateType, - }, -}; -use lemmy_db_schema::{ - source::{moderator::*, post::*}, - traits::{Crud, Likeable, Saveable}, -}; -use lemmy_db_views::post_view::PostView; -use lemmy_utils::{request::fetch_site_metadata, ConnectionId, LemmyError}; -use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperation}; -use std::convert::TryInto; - -#[async_trait::async_trait(?Send)] -impl Perform for CreatePostLike { - type Response = PostResponse; - - #[tracing::instrument(skip(context, websocket_id))] - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &CreatePostLike = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - // Don't do a downvote if site has downvotes disabled - check_downvotes_enabled(data.score, context.pool()).await?; - - // Check for a community ban - let post_id = data.post_id; - let post: ApubPost = blocking(context.pool(), move |conn| Post::read(conn, post_id)) - .await?? - .into(); - - check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?; - check_community_deleted_or_removed(post.community_id, context.pool()).await?; - - let like_form = PostLikeForm { - post_id: data.post_id, - person_id: local_user_view.person.id, - score: data.score, - }; - - // Remove any likes first - let person_id = local_user_view.person.id; - blocking(context.pool(), move |conn| { - PostLike::remove(conn, person_id, post_id) - }) - .await??; - - let community_id = post.community_id; - let object = PostOrComment::Post(Box::new(post)); - - // Only add the like if the score isnt 0 - let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1); - if do_add { - let like_form2 = like_form.clone(); - let like = move |conn: &'_ _| PostLike::like(conn, &like_form2); - blocking(context.pool(), like) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_like_post"))?; - - Vote::send( - &object, - &local_user_view.person.clone().into(), - community_id, - like_form.score.try_into()?, - context, - ) - .await?; - } else { - // API doesn't distinguish between Undo/Like and Undo/Dislike - UndoVote::send( - &object, - &local_user_view.person.clone().into(), - community_id, - VoteType::Like, - context, - ) - .await?; - } - - // Mark the post as read - mark_post_as_read(person_id, post_id, context.pool()).await?; - - send_post_ws_message( - data.post_id, - UserOperation::CreatePostLike, - websocket_id, - Some(local_user_view.person.id), - context, - ) - .await - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for MarkPostAsRead { - type Response = PostResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let post_id = data.post_id; - let person_id = local_user_view.person.id; - - // Mark the post as read / unread - if data.read { - mark_post_as_read(person_id, post_id, context.pool()).await?; - } else { - mark_post_as_unread(person_id, post_id, context.pool()).await?; - } - - // Fetch it - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, Some(person_id)) - }) - .await??; - - let res = Self::Response { post_view }; - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for LockPost { - type Response = PostResponse; - - #[tracing::instrument(skip(context, websocket_id))] - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &LockPost = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let post_id = data.post_id; - let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - - check_community_ban( - local_user_view.person.id, - orig_post.community_id, - context.pool(), - ) - .await?; - check_community_deleted_or_removed(orig_post.community_id, context.pool()).await?; - - // Verify that only the mods can lock - is_mod_or_admin( - context.pool(), - local_user_view.person.id, - orig_post.community_id, - ) - .await?; - - // Update the post - let post_id = data.post_id; - let locked = data.locked; - let updated_post: ApubPost = blocking(context.pool(), move |conn| { - Post::update_locked(conn, post_id, locked) - }) - .await?? - .into(); - - // Mod tables - let form = ModLockPostForm { - mod_person_id: local_user_view.person.id, - post_id: data.post_id, - locked: Some(locked), - }; - blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??; - - // apub updates - CreateOrUpdatePost::send( - updated_post, - &local_user_view.person.clone().into(), - CreateOrUpdateType::Update, - context, - ) - .await?; - - send_post_ws_message( - data.post_id, - UserOperation::LockPost, - websocket_id, - Some(local_user_view.person.id), - context, - ) - .await - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for StickyPost { - type Response = PostResponse; - - #[tracing::instrument(skip(context, websocket_id))] - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &StickyPost = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let post_id = data.post_id; - let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; - - check_community_ban( - local_user_view.person.id, - orig_post.community_id, - context.pool(), - ) - .await?; - check_community_deleted_or_removed(orig_post.community_id, context.pool()).await?; - - // Verify that only the mods can sticky - is_mod_or_admin( - context.pool(), - local_user_view.person.id, - orig_post.community_id, - ) - .await?; - - // Update the post - let post_id = data.post_id; - let stickied = data.stickied; - let updated_post: ApubPost = blocking(context.pool(), move |conn| { - Post::update_stickied(conn, post_id, stickied) - }) - .await?? - .into(); - - // Mod tables - let form = ModStickyPostForm { - mod_person_id: local_user_view.person.id, - post_id: data.post_id, - stickied: Some(stickied), - }; - blocking(context.pool(), move |conn| { - ModStickyPost::create(conn, &form) - }) - .await??; - - // Apub updates - // TODO stickied should pry work like locked for ease of use - CreateOrUpdatePost::send( - updated_post, - &local_user_view.person.clone().into(), - CreateOrUpdateType::Update, - context, - ) - .await?; - - send_post_ws_message( - data.post_id, - UserOperation::StickyPost, - websocket_id, - Some(local_user_view.person.id), - context, - ) - .await - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for SavePost { - type Response = PostResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &SavePost = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let post_saved_form = PostSavedForm { - post_id: data.post_id, - person_id: local_user_view.person.id, - }; - - if data.save { - let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form); - blocking(context.pool(), save) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_post"))?; - } else { - let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form); - blocking(context.pool(), unsave) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_post"))?; - } - - let post_id = data.post_id; - let person_id = local_user_view.person.id; - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, Some(person_id)) - }) - .await??; - - // Mark the post as read - mark_post_as_read(person_id, post_id, context.pool()).await?; - - Ok(PostResponse { post_view }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for GetSiteMetadata { - type Response = GetSiteMetadataResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &Self = self; - - let metadata = fetch_site_metadata(context.client(), &data.url).await?; - - Ok(GetSiteMetadataResponse { metadata }) - } -} diff --git a/crates/api/src/post/get_link_metadata.rs b/crates/api/src/post/get_link_metadata.rs new file mode 100644 index 00000000..a016bd2f --- /dev/null +++ b/crates/api/src/post/get_link_metadata.rs @@ -0,0 +1,23 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::post::{GetSiteMetadata, GetSiteMetadataResponse}; +use lemmy_utils::{request::fetch_site_metadata, ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for GetSiteMetadata { + type Response = GetSiteMetadataResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &Self = self; + + let metadata = fetch_site_metadata(context.client(), &data.url).await?; + + Ok(GetSiteMetadataResponse { metadata }) + } +} diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs new file mode 100644 index 00000000..22eb4775 --- /dev/null +++ b/crates/api/src/post/like.rs @@ -0,0 +1,110 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_community_ban, + check_community_deleted_or_removed, + check_downvotes_enabled, + get_local_user_view_from_jwt, + mark_post_as_read, + post::{CreatePostLike, PostResponse}, +}; +use lemmy_apub::{ + fetcher::post_or_comment::PostOrComment, + objects::post::ApubPost, + protocol::activities::voting::{ + undo_vote::UndoVote, + vote::{Vote, VoteType}, + }, +}; +use lemmy_db_schema::{ + source::post::{Post, PostLike, PostLikeForm}, + traits::{Crud, Likeable}, +}; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl Perform for CreatePostLike { + type Response = PostResponse; + + #[tracing::instrument(skip(context, websocket_id))] + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &CreatePostLike = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + // Don't do a downvote if site has downvotes disabled + check_downvotes_enabled(data.score, context.pool()).await?; + + // Check for a community ban + let post_id = data.post_id; + let post: ApubPost = blocking(context.pool(), move |conn| Post::read(conn, post_id)) + .await?? + .into(); + + check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?; + check_community_deleted_or_removed(post.community_id, context.pool()).await?; + + let like_form = PostLikeForm { + post_id: data.post_id, + person_id: local_user_view.person.id, + score: data.score, + }; + + // Remove any likes first + let person_id = local_user_view.person.id; + blocking(context.pool(), move |conn| { + PostLike::remove(conn, person_id, post_id) + }) + .await??; + + let community_id = post.community_id; + let object = PostOrComment::Post(Box::new(post)); + + // Only add the like if the score isnt 0 + let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1); + if do_add { + let like_form2 = like_form.clone(); + let like = move |conn: &'_ _| PostLike::like(conn, &like_form2); + blocking(context.pool(), like) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_like_post"))?; + + Vote::send( + &object, + &local_user_view.person.clone().into(), + community_id, + like_form.score.try_into()?, + context, + ) + .await?; + } else { + // API doesn't distinguish between Undo/Like and Undo/Dislike + UndoVote::send( + &object, + &local_user_view.person.clone().into(), + community_id, + VoteType::Like, + context, + ) + .await?; + } + + // Mark the post as read + mark_post_as_read(person_id, post_id, context.pool()).await?; + + send_post_ws_message( + data.post_id, + UserOperation::CreatePostLike, + websocket_id, + Some(local_user_view.person.id), + context, + ) + .await + } +} diff --git a/crates/api/src/post/lock.rs b/crates/api/src/post/lock.rs new file mode 100644 index 00000000..7210d443 --- /dev/null +++ b/crates/api/src/post/lock.rs @@ -0,0 +1,93 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_community_ban, + check_community_deleted_or_removed, + get_local_user_view_from_jwt, + is_mod_or_admin, + post::{LockPost, PostResponse}, +}; +use lemmy_apub::{ + objects::post::ApubPost, + protocol::activities::{create_or_update::post::CreateOrUpdatePost, CreateOrUpdateType}, +}; +use lemmy_db_schema::{ + source::{ + moderator::{ModLockPost, ModLockPostForm}, + post::Post, + }, + traits::Crud, +}; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl Perform for LockPost { + type Response = PostResponse; + + #[tracing::instrument(skip(context, websocket_id))] + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &LockPost = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let post_id = data.post_id; + let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; + + check_community_ban( + local_user_view.person.id, + orig_post.community_id, + context.pool(), + ) + .await?; + check_community_deleted_or_removed(orig_post.community_id, context.pool()).await?; + + // Verify that only the mods can lock + is_mod_or_admin( + context.pool(), + local_user_view.person.id, + orig_post.community_id, + ) + .await?; + + // Update the post + let post_id = data.post_id; + let locked = data.locked; + let updated_post: ApubPost = blocking(context.pool(), move |conn| { + Post::update_locked(conn, post_id, locked) + }) + .await?? + .into(); + + // Mod tables + let form = ModLockPostForm { + mod_person_id: local_user_view.person.id, + post_id: data.post_id, + locked: Some(locked), + }; + blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??; + + // apub updates + CreateOrUpdatePost::send( + updated_post, + &local_user_view.person.clone().into(), + CreateOrUpdateType::Update, + context, + ) + .await?; + + send_post_ws_message( + data.post_id, + UserOperation::LockPost, + websocket_id, + Some(local_user_view.person.id), + context, + ) + .await + } +} diff --git a/crates/api/src/post/mark_read.rs b/crates/api/src/post/mark_read.rs new file mode 100644 index 00000000..927fc651 --- /dev/null +++ b/crates/api/src/post/mark_read.rs @@ -0,0 +1,48 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + mark_post_as_read, + mark_post_as_unread, + post::{MarkPostAsRead, PostResponse}, +}; +use lemmy_db_views::post_view::PostView; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for MarkPostAsRead { + type Response = PostResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let post_id = data.post_id; + let person_id = local_user_view.person.id; + + // Mark the post as read / unread + if data.read { + mark_post_as_read(person_id, post_id, context.pool()).await?; + } else { + mark_post_as_unread(person_id, post_id, context.pool()).await?; + } + + // Fetch it + let post_view = blocking(context.pool(), move |conn| { + PostView::read(conn, post_id, Some(person_id)) + }) + .await??; + + let res = Self::Response { post_view }; + + Ok(res) + } +} diff --git a/crates/api/src/post/mod.rs b/crates/api/src/post/mod.rs new file mode 100644 index 00000000..40cea54a --- /dev/null +++ b/crates/api/src/post/mod.rs @@ -0,0 +1,6 @@ +mod get_link_metadata; +mod like; +mod lock; +mod mark_read; +mod save; +mod sticky; diff --git a/crates/api/src/post/save.rs b/crates/api/src/post/save.rs new file mode 100644 index 00000000..973b99de --- /dev/null +++ b/crates/api/src/post/save.rs @@ -0,0 +1,60 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + mark_post_as_read, + post::{PostResponse, SavePost}, +}; +use lemmy_db_schema::{ + source::post::{PostSaved, PostSavedForm}, + traits::Saveable, +}; +use lemmy_db_views::post_view::PostView; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for SavePost { + type Response = PostResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &SavePost = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let post_saved_form = PostSavedForm { + post_id: data.post_id, + person_id: local_user_view.person.id, + }; + + if data.save { + let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form); + blocking(context.pool(), save) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_post"))?; + } else { + let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form); + blocking(context.pool(), unsave) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_save_post"))?; + } + + let post_id = data.post_id; + let person_id = local_user_view.person.id; + let post_view = blocking(context.pool(), move |conn| { + PostView::read(conn, post_id, Some(person_id)) + }) + .await??; + + // Mark the post as read + mark_post_as_read(person_id, post_id, context.pool()).await?; + + Ok(PostResponse { post_view }) + } +} diff --git a/crates/api/src/post/sticky.rs b/crates/api/src/post/sticky.rs new file mode 100644 index 00000000..db669252 --- /dev/null +++ b/crates/api/src/post/sticky.rs @@ -0,0 +1,97 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_community_ban, + check_community_deleted_or_removed, + get_local_user_view_from_jwt, + is_mod_or_admin, + post::{PostResponse, StickyPost}, +}; +use lemmy_apub::{ + objects::post::ApubPost, + protocol::activities::{create_or_update::post::CreateOrUpdatePost, CreateOrUpdateType}, +}; +use lemmy_db_schema::{ + source::{ + moderator::{ModStickyPost, ModStickyPostForm}, + post::Post, + }, + traits::Crud, +}; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl Perform for StickyPost { + type Response = PostResponse; + + #[tracing::instrument(skip(context, websocket_id))] + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &StickyPost = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let post_id = data.post_id; + let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; + + check_community_ban( + local_user_view.person.id, + orig_post.community_id, + context.pool(), + ) + .await?; + check_community_deleted_or_removed(orig_post.community_id, context.pool()).await?; + + // Verify that only the mods can sticky + is_mod_or_admin( + context.pool(), + local_user_view.person.id, + orig_post.community_id, + ) + .await?; + + // Update the post + let post_id = data.post_id; + let stickied = data.stickied; + let updated_post: ApubPost = blocking(context.pool(), move |conn| { + Post::update_stickied(conn, post_id, stickied) + }) + .await?? + .into(); + + // Mod tables + let form = ModStickyPostForm { + mod_person_id: local_user_view.person.id, + post_id: data.post_id, + stickied: Some(stickied), + }; + blocking(context.pool(), move |conn| { + ModStickyPost::create(conn, &form) + }) + .await??; + + // Apub updates + // TODO stickied should pry work like locked for ease of use + CreateOrUpdatePost::send( + updated_post, + &local_user_view.person.clone().into(), + CreateOrUpdateType::Update, + context, + ) + .await?; + + send_post_ws_message( + data.post_id, + UserOperation::StickyPost, + websocket_id, + Some(local_user_view.person.id), + context, + ) + .await + } +} diff --git a/crates/api/src/post_report.rs b/crates/api/src/post_report.rs deleted file mode 100644 index 26c4d7ac..00000000 --- a/crates/api/src/post_report.rs +++ /dev/null @@ -1,197 +0,0 @@ -use crate::Perform; -use actix_web::web::Data; -use lemmy_api_common::{ - blocking, - check_community_ban, - get_local_user_view_from_jwt, - is_mod_or_admin, - post::{ - CreatePostReport, - ListPostReports, - ListPostReportsResponse, - PostReportResponse, - ResolvePostReport, - }, -}; -use lemmy_apub::protocol::activities::community::report::Report; -use lemmy_apub_lib::object_id::ObjectId; -use lemmy_db_schema::{ - source::post_report::{PostReport, PostReportForm}, - traits::Reportable, -}; -use lemmy_db_views::{ - post_report_view::{PostReportQueryBuilder, PostReportView}, - post_view::PostView, -}; -use lemmy_utils::{ConnectionId, LemmyError}; -use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation}; - -/// Creates a post report and notifies the moderators of the community -#[async_trait::async_trait(?Send)] -impl Perform for CreatePostReport { - type Response = PostReportResponse; - - #[tracing::instrument(skip(context, websocket_id))] - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &CreatePostReport = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - // check size of report and check for whitespace - let reason = data.reason.trim(); - if reason.is_empty() { - return Err(LemmyError::from_message("report_reason_required")); - } - if reason.chars().count() > 1000 { - return Err(LemmyError::from_message("report_too_long")); - } - - let person_id = local_user_view.person.id; - let post_id = data.post_id; - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, None) - }) - .await??; - - check_community_ban(person_id, post_view.community.id, context.pool()).await?; - - let report_form = PostReportForm { - creator_id: person_id, - post_id, - original_post_name: post_view.post.name, - original_post_url: post_view.post.url, - original_post_body: post_view.post.body, - reason: data.reason.to_owned(), - }; - - let report = blocking(context.pool(), move |conn| { - PostReport::report(conn, &report_form) - }) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_create_report"))?; - - let post_report_view = blocking(context.pool(), move |conn| { - PostReportView::read(conn, report.id, person_id) - }) - .await??; - - let res = PostReportResponse { post_report_view }; - - context.chat_server().do_send(SendModRoomMessage { - op: UserOperation::CreatePostReport, - response: res.clone(), - community_id: post_view.community.id, - websocket_id, - }); - - Report::send( - ObjectId::new(post_view.post.ap_id), - &local_user_view.person.into(), - ObjectId::new(post_view.community.actor_id), - reason.to_string(), - context, - ) - .await?; - - Ok(res) - } -} - -/// Resolves or unresolves a post report and notifies the moderators of the community -#[async_trait::async_trait(?Send)] -impl Perform for ResolvePostReport { - type Response = PostReportResponse; - - #[tracing::instrument(skip(context, websocket_id))] - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &ResolvePostReport = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let report_id = data.report_id; - let person_id = local_user_view.person.id; - let report = blocking(context.pool(), move |conn| { - PostReportView::read(conn, report_id, person_id) - }) - .await??; - - let person_id = local_user_view.person.id; - is_mod_or_admin(context.pool(), person_id, report.community.id).await?; - - let resolved = data.resolved; - let resolve_fun = move |conn: &'_ _| { - if resolved { - PostReport::resolve(conn, report_id, person_id) - } else { - PostReport::unresolve(conn, report_id, person_id) - } - }; - - blocking(context.pool(), resolve_fun) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_resolve_report"))?; - - let post_report_view = blocking(context.pool(), move |conn| { - PostReportView::read(conn, report_id, person_id) - }) - .await??; - - let res = PostReportResponse { post_report_view }; - - context.chat_server().do_send(SendModRoomMessage { - op: UserOperation::ResolvePostReport, - response: res.clone(), - community_id: report.community.id, - websocket_id, - }); - - Ok(res) - } -} - -/// Lists post reports for a community if an id is supplied -/// or returns all post reports for communities a user moderates -#[async_trait::async_trait(?Send)] -impl Perform for ListPostReports { - type Response = ListPostReportsResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &ListPostReports = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let person_id = local_user_view.person.id; - let admin = local_user_view.person.admin; - let community_id = data.community_id; - let unresolved_only = data.unresolved_only; - - let page = data.page; - let limit = data.limit; - let post_reports = blocking(context.pool(), move |conn| { - PostReportQueryBuilder::create(conn, person_id, admin) - .community_id(community_id) - .unresolved_only(unresolved_only) - .page(page) - .limit(limit) - .list() - }) - .await??; - - let res = ListPostReportsResponse { post_reports }; - - Ok(res) - } -} diff --git a/crates/api/src/post_report/create.rs b/crates/api/src/post_report/create.rs new file mode 100644 index 00000000..a85aa9a9 --- /dev/null +++ b/crates/api/src/post_report/create.rs @@ -0,0 +1,92 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_community_ban, + get_local_user_view_from_jwt, + post::{CreatePostReport, PostReportResponse}, +}; +use lemmy_apub::protocol::activities::community::report::Report; +use lemmy_apub_lib::object_id::ObjectId; +use lemmy_db_schema::{ + source::post_report::{PostReport, PostReportForm}, + traits::Reportable, +}; +use lemmy_db_views::{post_report_view::PostReportView, post_view::PostView}; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation}; + +/// Creates a post report and notifies the moderators of the community +#[async_trait::async_trait(?Send)] +impl Perform for CreatePostReport { + type Response = PostReportResponse; + + #[tracing::instrument(skip(context, websocket_id))] + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &CreatePostReport = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + // check size of report and check for whitespace + let reason = data.reason.trim(); + if reason.is_empty() { + return Err(LemmyError::from_message("report_reason_required")); + } + if reason.chars().count() > 1000 { + return Err(LemmyError::from_message("report_too_long")); + } + + let person_id = local_user_view.person.id; + let post_id = data.post_id; + let post_view = blocking(context.pool(), move |conn| { + PostView::read(conn, post_id, None) + }) + .await??; + + check_community_ban(person_id, post_view.community.id, context.pool()).await?; + + let report_form = PostReportForm { + creator_id: person_id, + post_id, + original_post_name: post_view.post.name, + original_post_url: post_view.post.url, + original_post_body: post_view.post.body, + reason: data.reason.to_owned(), + }; + + let report = blocking(context.pool(), move |conn| { + PostReport::report(conn, &report_form) + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_create_report"))?; + + let post_report_view = blocking(context.pool(), move |conn| { + PostReportView::read(conn, report.id, person_id) + }) + .await??; + + let res = PostReportResponse { post_report_view }; + + context.chat_server().do_send(SendModRoomMessage { + op: UserOperation::CreatePostReport, + response: res.clone(), + community_id: post_view.community.id, + websocket_id, + }); + + Report::send( + ObjectId::new(post_view.post.ap_id), + &local_user_view.person.into(), + ObjectId::new(post_view.community.actor_id), + reason.to_string(), + context, + ) + .await?; + + Ok(res) + } +} diff --git a/crates/api/src/post_report/list.rs b/crates/api/src/post_report/list.rs new file mode 100644 index 00000000..7dfbb107 --- /dev/null +++ b/crates/api/src/post_report/list.rs @@ -0,0 +1,49 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + post::{ListPostReports, ListPostReportsResponse}, +}; +use lemmy_db_views::post_report_view::PostReportQueryBuilder; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +/// Lists post reports for a community if an id is supplied +/// or returns all post reports for communities a user moderates +#[async_trait::async_trait(?Send)] +impl Perform for ListPostReports { + type Response = ListPostReportsResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &ListPostReports = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let person_id = local_user_view.person.id; + let admin = local_user_view.person.admin; + let community_id = data.community_id; + let unresolved_only = data.unresolved_only; + + let page = data.page; + let limit = data.limit; + let post_reports = blocking(context.pool(), move |conn| { + PostReportQueryBuilder::create(conn, person_id, admin) + .community_id(community_id) + .unresolved_only(unresolved_only) + .page(page) + .limit(limit) + .list() + }) + .await??; + + let res = ListPostReportsResponse { post_reports }; + + Ok(res) + } +} diff --git a/crates/api/src/post_report/mod.rs b/crates/api/src/post_report/mod.rs new file mode 100644 index 00000000..375fde4c --- /dev/null +++ b/crates/api/src/post_report/mod.rs @@ -0,0 +1,3 @@ +mod create; +mod list; +mod resolve; diff --git a/crates/api/src/post_report/resolve.rs b/crates/api/src/post_report/resolve.rs new file mode 100644 index 00000000..f0eb3fdb --- /dev/null +++ b/crates/api/src/post_report/resolve.rs @@ -0,0 +1,68 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + is_mod_or_admin, + post::{PostReportResponse, ResolvePostReport}, +}; +use lemmy_db_schema::{source::post_report::PostReport, traits::Reportable}; +use lemmy_db_views::post_report_view::PostReportView; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendModRoomMessage, LemmyContext, UserOperation}; + +/// Resolves or unresolves a post report and notifies the moderators of the community +#[async_trait::async_trait(?Send)] +impl Perform for ResolvePostReport { + type Response = PostReportResponse; + + #[tracing::instrument(skip(context, websocket_id))] + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &ResolvePostReport = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let report_id = data.report_id; + let person_id = local_user_view.person.id; + let report = blocking(context.pool(), move |conn| { + PostReportView::read(conn, report_id, person_id) + }) + .await??; + + let person_id = local_user_view.person.id; + is_mod_or_admin(context.pool(), person_id, report.community.id).await?; + + let resolved = data.resolved; + let resolve_fun = move |conn: &'_ _| { + if resolved { + PostReport::resolve(conn, report_id, person_id) + } else { + PostReport::unresolve(conn, report_id, person_id) + } + }; + + blocking(context.pool(), resolve_fun) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_resolve_report"))?; + + let post_report_view = blocking(context.pool(), move |conn| { + PostReportView::read(conn, report_id, person_id) + }) + .await??; + + let res = PostReportResponse { post_report_view }; + + context.chat_server().do_send(SendModRoomMessage { + op: UserOperation::ResolvePostReport, + response: res.clone(), + community_id: report.community.id, + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api/src/private_message.rs b/crates/api/src/private_message/mark_read.rs similarity index 100% rename from crates/api/src/private_message.rs rename to crates/api/src/private_message/mark_read.rs diff --git a/crates/api/src/private_message/mod.rs b/crates/api/src/private_message/mod.rs new file mode 100644 index 00000000..ce8c6245 --- /dev/null +++ b/crates/api/src/private_message/mod.rs @@ -0,0 +1 @@ +mod mark_read; diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs deleted file mode 100644 index 54b7e0c7..00000000 --- a/crates/api/src/site.rs +++ /dev/null @@ -1,710 +0,0 @@ -use crate::Perform; -use actix_web::web::Data; -use diesel::NotFound; -use lemmy_api_common::{ - blocking, - build_federated_instances, - check_private_instance, - get_local_user_view_from_jwt, - get_local_user_view_from_jwt_opt, - is_admin, - send_application_approved_email, - site::*, -}; -use lemmy_apub::{ - fetcher::{ - resolve_actor_identifier, - search::{search_by_apub_id, SearchableObjects}, - }, - objects::community::ApubCommunity, -}; -use lemmy_db_schema::{ - diesel_option_overwrite, - from_opt_str_to_opt_enum, - newtypes::PersonId, - source::{ - community::Community, - local_user::{LocalUser, LocalUserForm}, - moderator::*, - person::Person, - registration_application::{RegistrationApplication, RegistrationApplicationForm}, - site::Site, - }, - traits::{Crud, DeleteableOrRemoveable}, - DbPool, - ListingType, - SearchType, - SortType, -}; -use lemmy_db_views::{ - comment_view::{CommentQueryBuilder, CommentView}, - local_user_view::LocalUserView, - post_view::{PostQueryBuilder, PostView}, - registration_application_view::{ - RegistrationApplicationQueryBuilder, - RegistrationApplicationView, - }, - site_view::SiteView, -}; -use lemmy_db_views_actor::{ - community_view::{CommunityQueryBuilder, CommunityView}, - person_view::{PersonQueryBuilder, PersonViewSafe}, -}; -use lemmy_db_views_moderator::{ - mod_add_community_view::ModAddCommunityView, - mod_add_view::ModAddView, - mod_ban_from_community_view::ModBanFromCommunityView, - mod_ban_view::ModBanView, - mod_hide_community_view::ModHideCommunityView, - mod_lock_post_view::ModLockPostView, - mod_remove_comment_view::ModRemoveCommentView, - mod_remove_community_view::ModRemoveCommunityView, - mod_remove_post_view::ModRemovePostView, - mod_sticky_post_view::ModStickyPostView, - mod_transfer_community_view::ModTransferCommunityView, -}; -use lemmy_utils::{settings::structs::Settings, version, ConnectionId, LemmyError}; -use lemmy_websocket::LemmyContext; - -#[async_trait::async_trait(?Send)] -impl Perform for GetModlog { - type Response = GetModlogResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetModlog = self; - - let local_user_view = - get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) - .await?; - - check_private_instance(&local_user_view, context.pool()).await?; - - let community_id = data.community_id; - let mod_person_id = data.mod_person_id; - let page = data.page; - let limit = data.limit; - let removed_posts = blocking(context.pool(), move |conn| { - ModRemovePostView::list(conn, community_id, mod_person_id, page, limit) - }) - .await??; - - let locked_posts = blocking(context.pool(), move |conn| { - ModLockPostView::list(conn, community_id, mod_person_id, page, limit) - }) - .await??; - - let stickied_posts = blocking(context.pool(), move |conn| { - ModStickyPostView::list(conn, community_id, mod_person_id, page, limit) - }) - .await??; - - let removed_comments = blocking(context.pool(), move |conn| { - ModRemoveCommentView::list(conn, community_id, mod_person_id, page, limit) - }) - .await??; - - let banned_from_community = blocking(context.pool(), move |conn| { - ModBanFromCommunityView::list(conn, community_id, mod_person_id, page, limit) - }) - .await??; - - let added_to_community = blocking(context.pool(), move |conn| { - ModAddCommunityView::list(conn, community_id, mod_person_id, page, limit) - }) - .await??; - - let transferred_to_community = blocking(context.pool(), move |conn| { - ModTransferCommunityView::list(conn, community_id, mod_person_id, page, limit) - }) - .await??; - - let hidden_communities = blocking(context.pool(), move |conn| { - ModHideCommunityView::list(conn, community_id, mod_person_id, page, limit) - }) - .await??; - - // These arrays are only for the full modlog, when a community isn't given - let (removed_communities, banned, added) = if data.community_id.is_none() { - blocking(context.pool(), move |conn| { - Ok(( - ModRemoveCommunityView::list(conn, mod_person_id, page, limit)?, - ModBanView::list(conn, mod_person_id, page, limit)?, - ModAddView::list(conn, mod_person_id, page, limit)?, - )) as Result<_, LemmyError> - }) - .await?? - } else { - (Vec::new(), Vec::new(), Vec::new()) - }; - - // Return the jwt - Ok(GetModlogResponse { - removed_posts, - locked_posts, - stickied_posts, - removed_comments, - removed_communities, - banned_from_community, - banned, - added_to_community, - added, - transferred_to_community, - hidden_communities, - }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for Search { - type Response = SearchResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &Search = self; - - let local_user_view = - get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) - .await?; - - check_private_instance(&local_user_view, context.pool()).await?; - - let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw); - let show_bot_accounts = local_user_view - .as_ref() - .map(|t| t.local_user.show_bot_accounts); - let show_read_posts = local_user_view - .as_ref() - .map(|t| t.local_user.show_read_posts); - - let person_id = local_user_view.map(|u| u.person.id); - - let mut posts = Vec::new(); - let mut comments = Vec::new(); - let mut communities = Vec::new(); - let mut users = Vec::new(); - - // TODO no clean / non-nsfw searching rn - - let q = data.q.to_owned(); - let page = data.page; - let limit = data.limit; - let sort: Option = from_opt_str_to_opt_enum(&data.sort); - let listing_type: Option = from_opt_str_to_opt_enum(&data.listing_type); - let search_type: SearchType = from_opt_str_to_opt_enum(&data.type_).unwrap_or(SearchType::All); - let community_id = data.community_id; - let community_actor_id = if let Some(name) = &data.community_name { - resolve_actor_identifier::(name, context) - .await - .ok() - .map(|c| c.actor_id) - } else { - None - }; - let creator_id = data.creator_id; - match search_type { - SearchType::Posts => { - posts = blocking(context.pool(), move |conn| { - PostQueryBuilder::create(conn) - .sort(sort) - .show_nsfw(show_nsfw) - .show_bot_accounts(show_bot_accounts) - .show_read_posts(show_read_posts) - .listing_type(listing_type) - .community_id(community_id) - .community_actor_id(community_actor_id) - .creator_id(creator_id) - .my_person_id(person_id) - .search_term(q) - .page(page) - .limit(limit) - .list() - }) - .await??; - } - SearchType::Comments => { - comments = blocking(context.pool(), move |conn| { - CommentQueryBuilder::create(conn) - .sort(sort) - .listing_type(listing_type) - .search_term(q) - .show_bot_accounts(show_bot_accounts) - .community_id(community_id) - .community_actor_id(community_actor_id) - .creator_id(creator_id) - .my_person_id(person_id) - .page(page) - .limit(limit) - .list() - }) - .await??; - } - SearchType::Communities => { - communities = blocking(context.pool(), move |conn| { - CommunityQueryBuilder::create(conn) - .sort(sort) - .listing_type(listing_type) - .search_term(q) - .my_person_id(person_id) - .page(page) - .limit(limit) - .list() - }) - .await??; - } - SearchType::Users => { - users = blocking(context.pool(), move |conn| { - PersonQueryBuilder::create(conn) - .sort(sort) - .search_term(q) - .page(page) - .limit(limit) - .list() - }) - .await??; - } - SearchType::All => { - // If the community or creator is included, dont search communities or users - let community_or_creator_included = - data.community_id.is_some() || data.community_name.is_some() || data.creator_id.is_some(); - let community_actor_id_2 = community_actor_id.to_owned(); - - posts = blocking(context.pool(), move |conn| { - PostQueryBuilder::create(conn) - .sort(sort) - .show_nsfw(show_nsfw) - .show_bot_accounts(show_bot_accounts) - .show_read_posts(show_read_posts) - .listing_type(listing_type) - .community_id(community_id) - .community_actor_id(community_actor_id_2) - .creator_id(creator_id) - .my_person_id(person_id) - .search_term(q) - .page(page) - .limit(limit) - .list() - }) - .await??; - - let q = data.q.to_owned(); - let community_actor_id = community_actor_id.to_owned(); - - comments = blocking(context.pool(), move |conn| { - CommentQueryBuilder::create(conn) - .sort(sort) - .listing_type(listing_type) - .search_term(q) - .show_bot_accounts(show_bot_accounts) - .community_id(community_id) - .community_actor_id(community_actor_id) - .creator_id(creator_id) - .my_person_id(person_id) - .page(page) - .limit(limit) - .list() - }) - .await??; - - let q = data.q.to_owned(); - - communities = if community_or_creator_included { - vec![] - } else { - blocking(context.pool(), move |conn| { - CommunityQueryBuilder::create(conn) - .sort(sort) - .listing_type(listing_type) - .search_term(q) - .my_person_id(person_id) - .page(page) - .limit(limit) - .list() - }) - .await?? - }; - - let q = data.q.to_owned(); - - users = if community_or_creator_included { - vec![] - } else { - blocking(context.pool(), move |conn| { - PersonQueryBuilder::create(conn) - .sort(sort) - .search_term(q) - .page(page) - .limit(limit) - .list() - }) - .await?? - }; - } - SearchType::Url => { - posts = blocking(context.pool(), move |conn| { - PostQueryBuilder::create(conn) - .sort(sort) - .show_nsfw(show_nsfw) - .show_bot_accounts(show_bot_accounts) - .show_read_posts(show_read_posts) - .listing_type(listing_type) - .my_person_id(person_id) - .community_id(community_id) - .community_actor_id(community_actor_id) - .creator_id(creator_id) - .url_search(q) - .page(page) - .limit(limit) - .list() - }) - .await??; - } - }; - - // Blank out deleted or removed info for non logged in users - if person_id.is_none() { - for cv in communities - .iter_mut() - .filter(|cv| cv.community.deleted || cv.community.removed) - { - cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info(); - } - - for pv in posts - .iter_mut() - .filter(|p| p.post.deleted || p.post.removed) - { - pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info(); - } - - for cv in comments - .iter_mut() - .filter(|cv| cv.comment.deleted || cv.comment.removed) - { - cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info(); - } - } - - // Return the jwt - Ok(SearchResponse { - type_: search_type.to_string(), - comments, - posts, - communities, - users, - }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for ResolveObject { - type Response = ResolveObjectResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let local_user_view = - get_local_user_view_from_jwt_opt(self.auth.as_ref(), context.pool(), context.secret()) - .await?; - check_private_instance(&local_user_view, context.pool()).await?; - - let res = search_by_apub_id(&self.q, context) - .await - .map_err(|e| e.with_message("couldnt_find_object"))?; - convert_response(res, local_user_view.map(|l| l.person.id), context.pool()) - .await - .map_err(|e| e.with_message("couldnt_find_object")) - } -} - -async fn convert_response( - object: SearchableObjects, - user_id: Option, - pool: &DbPool, -) -> Result { - let removed_or_deleted; - let mut res = ResolveObjectResponse { - comment: None, - post: None, - community: None, - person: None, - }; - use SearchableObjects::*; - match object { - Person(p) => { - removed_or_deleted = p.deleted; - res.person = Some(blocking(pool, move |conn| PersonViewSafe::read(conn, p.id)).await??) - } - Community(c) => { - removed_or_deleted = c.deleted || c.removed; - res.community = - Some(blocking(pool, move |conn| CommunityView::read(conn, c.id, user_id)).await??) - } - Post(p) => { - removed_or_deleted = p.deleted || p.removed; - res.post = Some(blocking(pool, move |conn| PostView::read(conn, p.id, user_id)).await??) - } - Comment(c) => { - removed_or_deleted = c.deleted || c.removed; - res.comment = Some(blocking(pool, move |conn| CommentView::read(conn, c.id, user_id)).await??) - } - }; - // if the object was deleted from database, dont return it - if removed_or_deleted { - return Err(NotFound {}.into()); - } - Ok(res) -} - -#[async_trait::async_trait(?Send)] -impl Perform for LeaveAdmin { - type Response = GetSiteResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &LeaveAdmin = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - is_admin(&local_user_view)?; - - // Make sure there isn't just one admin (so if one leaves, there will still be one left) - let admins = blocking(context.pool(), PersonViewSafe::admins).await??; - if admins.len() == 1 { - return Err(LemmyError::from_message("cannot_leave_admin")); - } - - let person_id = local_user_view.person.id; - blocking(context.pool(), move |conn| { - Person::leave_admin(conn, person_id) - }) - .await??; - - // Mod tables - let form = ModAddForm { - mod_person_id: person_id, - other_person_id: person_id, - removed: Some(true), - }; - - blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??; - - // Reread site and admins - let site_view = blocking(context.pool(), SiteView::read_local).await??; - let admins = blocking(context.pool(), PersonViewSafe::admins).await??; - - let federated_instances = - build_federated_instances(context.pool(), &context.settings()).await?; - - Ok(GetSiteResponse { - site_view: Some(site_view), - admins, - online: 0, - version: version::VERSION.to_string(), - my_user: None, - federated_instances, - }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for GetSiteConfig { - type Response = GetSiteConfigResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetSiteConfig = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - // Only let admins read this - is_admin(&local_user_view)?; - - let config_hjson = Settings::read_config_file()?; - - Ok(GetSiteConfigResponse { config_hjson }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for SaveSiteConfig { - type Response = GetSiteConfigResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &SaveSiteConfig = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - // Only let admins read this - is_admin(&local_user_view)?; - - // Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem - let config_hjson = Settings::save_config_file(&data.config_hjson) - .map_err(|e| e.with_message("couldnt_update_site"))?; - - Ok(GetSiteConfigResponse { config_hjson }) - } -} - -/// Lists registration applications, filterable by undenied only. -#[async_trait::async_trait(?Send)] -impl Perform for ListRegistrationApplications { - type Response = ListRegistrationApplicationsResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - // Make sure user is an admin - is_admin(&local_user_view)?; - - let unread_only = data.unread_only; - let verified_email_only = blocking(context.pool(), Site::read_local_site) - .await?? - .require_email_verification; - - let page = data.page; - let limit = data.limit; - let registration_applications = blocking(context.pool(), move |conn| { - RegistrationApplicationQueryBuilder::create(conn) - .unread_only(unread_only) - .verified_email_only(verified_email_only) - .page(page) - .limit(limit) - .list() - }) - .await??; - - let res = Self::Response { - registration_applications, - }; - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for ApproveRegistrationApplication { - type Response = RegistrationApplicationResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - let app_id = data.id; - - // Only let admins do this - is_admin(&local_user_view)?; - - // Update the registration with reason, admin_id - let deny_reason = diesel_option_overwrite(&data.deny_reason); - let app_form = RegistrationApplicationForm { - admin_id: Some(local_user_view.person.id), - deny_reason, - ..RegistrationApplicationForm::default() - }; - - let registration_application = blocking(context.pool(), move |conn| { - RegistrationApplication::update(conn, app_id, &app_form) - }) - .await??; - - // Update the local_user row - let local_user_form = LocalUserForm { - accepted_application: Some(data.approve), - ..LocalUserForm::default() - }; - - let approved_user_id = registration_application.local_user_id; - blocking(context.pool(), move |conn| { - LocalUser::update(conn, approved_user_id, &local_user_form) - }) - .await??; - - if data.approve { - let approved_local_user_view = blocking(context.pool(), move |conn| { - LocalUserView::read(conn, approved_user_id) - }) - .await??; - - if approved_local_user_view.local_user.email.is_some() { - send_application_approved_email(&approved_local_user_view, &context.settings())?; - } - } - - // Read the view - let registration_application = blocking(context.pool(), move |conn| { - RegistrationApplicationView::read(conn, app_id) - }) - .await??; - - Ok(Self::Response { - registration_application, - }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for GetUnreadRegistrationApplicationCount { - type Response = GetUnreadRegistrationApplicationCountResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data = self; - let local_user_view = - get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; - - // Only let admins do this - is_admin(&local_user_view)?; - - let verified_email_only = blocking(context.pool(), Site::read_local_site) - .await?? - .require_email_verification; - - let registration_applications = blocking(context.pool(), move |conn| { - RegistrationApplicationView::get_unread_count(conn, verified_email_only) - }) - .await??; - - Ok(Self::Response { - registration_applications, - }) - } -} diff --git a/crates/api/src/site/config/mod.rs b/crates/api/src/site/config/mod.rs new file mode 100644 index 00000000..d538ff20 --- /dev/null +++ b/crates/api/src/site/config/mod.rs @@ -0,0 +1,2 @@ +mod read; +mod update; diff --git a/crates/api/src/site/config/read.rs b/crates/api/src/site/config/read.rs new file mode 100644 index 00000000..76117d49 --- /dev/null +++ b/crates/api/src/site/config/read.rs @@ -0,0 +1,32 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + get_local_user_view_from_jwt, + is_admin, + site::{GetSiteConfig, GetSiteConfigResponse}, +}; +use lemmy_utils::{settings::structs::Settings, ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for GetSiteConfig { + type Response = GetSiteConfigResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &GetSiteConfig = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + // Only let admins read this + is_admin(&local_user_view)?; + + let config_hjson = Settings::read_config_file()?; + + Ok(GetSiteConfigResponse { config_hjson }) + } +} diff --git a/crates/api/src/site/config/update.rs b/crates/api/src/site/config/update.rs new file mode 100644 index 00000000..b36c69ce --- /dev/null +++ b/crates/api/src/site/config/update.rs @@ -0,0 +1,34 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + get_local_user_view_from_jwt, + is_admin, + site::{GetSiteConfigResponse, SaveSiteConfig}, +}; +use lemmy_utils::{settings::structs::Settings, ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for SaveSiteConfig { + type Response = GetSiteConfigResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &SaveSiteConfig = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + // Only let admins read this + is_admin(&local_user_view)?; + + // Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem + let config_hjson = Settings::save_config_file(&data.config_hjson) + .map_err(|e| e.with_message("couldnt_update_site"))?; + + Ok(GetSiteConfigResponse { config_hjson }) + } +} diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs new file mode 100644 index 00000000..ee714e86 --- /dev/null +++ b/crates/api/src/site/leave_admin.rs @@ -0,0 +1,75 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + build_federated_instances, + get_local_user_view_from_jwt, + is_admin, + site::{GetSiteResponse, LeaveAdmin}, +}; +use lemmy_db_schema::{ + source::{ + moderator::{ModAdd, ModAddForm}, + person::Person, + }, + traits::Crud, +}; +use lemmy_db_views::site_view::SiteView; +use lemmy_db_views_actor::person_view::PersonViewSafe; +use lemmy_utils::{version, ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for LeaveAdmin { + type Response = GetSiteResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &LeaveAdmin = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + is_admin(&local_user_view)?; + + // Make sure there isn't just one admin (so if one leaves, there will still be one left) + let admins = blocking(context.pool(), PersonViewSafe::admins).await??; + if admins.len() == 1 { + return Err(LemmyError::from_message("cannot_leave_admin")); + } + + let person_id = local_user_view.person.id; + blocking(context.pool(), move |conn| { + Person::leave_admin(conn, person_id) + }) + .await??; + + // Mod tables + let form = ModAddForm { + mod_person_id: person_id, + other_person_id: person_id, + removed: Some(true), + }; + + blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??; + + // Reread site and admins + let site_view = blocking(context.pool(), SiteView::read_local).await??; + let admins = blocking(context.pool(), PersonViewSafe::admins).await??; + + let federated_instances = + build_federated_instances(context.pool(), &context.settings()).await?; + + Ok(GetSiteResponse { + site_view: Some(site_view), + admins, + online: 0, + version: version::VERSION.to_string(), + my_user: None, + federated_instances, + }) + } +} diff --git a/crates/api/src/site/mod.rs b/crates/api/src/site/mod.rs new file mode 100644 index 00000000..b8b9dd78 --- /dev/null +++ b/crates/api/src/site/mod.rs @@ -0,0 +1,6 @@ +mod config; +mod leave_admin; +mod mod_log; +mod registration_applications; +mod resolve_object; +mod search; diff --git a/crates/api/src/site/mod_log.rs b/crates/api/src/site/mod_log.rs new file mode 100644 index 00000000..6dd06c16 --- /dev/null +++ b/crates/api/src/site/mod_log.rs @@ -0,0 +1,116 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_private_instance, + get_local_user_view_from_jwt_opt, + site::{GetModlog, GetModlogResponse}, +}; +use lemmy_db_views_moderator::{ + mod_add_community_view::ModAddCommunityView, + mod_add_view::ModAddView, + mod_ban_from_community_view::ModBanFromCommunityView, + mod_ban_view::ModBanView, + mod_hide_community_view::ModHideCommunityView, + mod_lock_post_view::ModLockPostView, + mod_remove_comment_view::ModRemoveCommentView, + mod_remove_community_view::ModRemoveCommunityView, + mod_remove_post_view::ModRemovePostView, + mod_sticky_post_view::ModStickyPostView, + mod_transfer_community_view::ModTransferCommunityView, +}; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for GetModlog { + type Response = GetModlogResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &GetModlog = self; + + let local_user_view = + get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) + .await?; + + check_private_instance(&local_user_view, context.pool()).await?; + + let community_id = data.community_id; + let mod_person_id = data.mod_person_id; + let page = data.page; + let limit = data.limit; + let removed_posts = blocking(context.pool(), move |conn| { + ModRemovePostView::list(conn, community_id, mod_person_id, page, limit) + }) + .await??; + + let locked_posts = blocking(context.pool(), move |conn| { + ModLockPostView::list(conn, community_id, mod_person_id, page, limit) + }) + .await??; + + let stickied_posts = blocking(context.pool(), move |conn| { + ModStickyPostView::list(conn, community_id, mod_person_id, page, limit) + }) + .await??; + + let removed_comments = blocking(context.pool(), move |conn| { + ModRemoveCommentView::list(conn, community_id, mod_person_id, page, limit) + }) + .await??; + + let banned_from_community = blocking(context.pool(), move |conn| { + ModBanFromCommunityView::list(conn, community_id, mod_person_id, page, limit) + }) + .await??; + + let added_to_community = blocking(context.pool(), move |conn| { + ModAddCommunityView::list(conn, community_id, mod_person_id, page, limit) + }) + .await??; + + let transferred_to_community = blocking(context.pool(), move |conn| { + ModTransferCommunityView::list(conn, community_id, mod_person_id, page, limit) + }) + .await??; + + let hidden_communities = blocking(context.pool(), move |conn| { + ModHideCommunityView::list(conn, community_id, mod_person_id, page, limit) + }) + .await??; + + // These arrays are only for the full modlog, when a community isn't given + let (removed_communities, banned, added) = if data.community_id.is_none() { + blocking(context.pool(), move |conn| { + Ok(( + ModRemoveCommunityView::list(conn, mod_person_id, page, limit)?, + ModBanView::list(conn, mod_person_id, page, limit)?, + ModAddView::list(conn, mod_person_id, page, limit)?, + )) as Result<_, LemmyError> + }) + .await?? + } else { + (Vec::new(), Vec::new(), Vec::new()) + }; + + // Return the jwt + Ok(GetModlogResponse { + removed_posts, + locked_posts, + stickied_posts, + removed_comments, + removed_communities, + banned_from_community, + banned, + added_to_community, + added, + transferred_to_community, + hidden_communities, + }) + } +} diff --git a/crates/api/src/site/registration_applications/approve.rs b/crates/api/src/site/registration_applications/approve.rs new file mode 100644 index 00000000..238da7e5 --- /dev/null +++ b/crates/api/src/site/registration_applications/approve.rs @@ -0,0 +1,89 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + is_admin, + send_application_approved_email, + site::*, +}; +use lemmy_db_schema::{ + diesel_option_overwrite, + source::{ + local_user::{LocalUser, LocalUserForm}, + registration_application::{RegistrationApplication, RegistrationApplicationForm}, + }, + traits::Crud, +}; +use lemmy_db_views::{ + local_user_view::LocalUserView, + registration_application_view::RegistrationApplicationView, +}; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for ApproveRegistrationApplication { + type Response = RegistrationApplicationResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + let app_id = data.id; + + // Only let admins do this + is_admin(&local_user_view)?; + + // Update the registration with reason, admin_id + let deny_reason = diesel_option_overwrite(&data.deny_reason); + let app_form = RegistrationApplicationForm { + admin_id: Some(local_user_view.person.id), + deny_reason, + ..RegistrationApplicationForm::default() + }; + + let registration_application = blocking(context.pool(), move |conn| { + RegistrationApplication::update(conn, app_id, &app_form) + }) + .await??; + + // Update the local_user row + let local_user_form = LocalUserForm { + accepted_application: Some(data.approve), + ..LocalUserForm::default() + }; + + let approved_user_id = registration_application.local_user_id; + blocking(context.pool(), move |conn| { + LocalUser::update(conn, approved_user_id, &local_user_form) + }) + .await??; + + if data.approve { + let approved_local_user_view = blocking(context.pool(), move |conn| { + LocalUserView::read(conn, approved_user_id) + }) + .await??; + + if approved_local_user_view.local_user.email.is_some() { + send_application_approved_email(&approved_local_user_view, &context.settings())?; + } + } + + // Read the view + let registration_application = blocking(context.pool(), move |conn| { + RegistrationApplicationView::read(conn, app_id) + }) + .await??; + + Ok(Self::Response { + registration_application, + }) + } +} diff --git a/crates/api/src/site/registration_applications/list.rs b/crates/api/src/site/registration_applications/list.rs new file mode 100644 index 00000000..bd5170d6 --- /dev/null +++ b/crates/api/src/site/registration_applications/list.rs @@ -0,0 +1,54 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + is_admin, + site::{ListRegistrationApplications, ListRegistrationApplicationsResponse}, +}; +use lemmy_db_schema::source::site::Site; +use lemmy_db_views::registration_application_view::RegistrationApplicationQueryBuilder; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +/// Lists registration applications, filterable by undenied only. +#[async_trait::async_trait(?Send)] +impl Perform for ListRegistrationApplications { + type Response = ListRegistrationApplicationsResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + // Make sure user is an admin + is_admin(&local_user_view)?; + + let unread_only = data.unread_only; + let verified_email_only = blocking(context.pool(), Site::read_local_site) + .await?? + .require_email_verification; + + let page = data.page; + let limit = data.limit; + let registration_applications = blocking(context.pool(), move |conn| { + RegistrationApplicationQueryBuilder::create(conn) + .unread_only(unread_only) + .verified_email_only(verified_email_only) + .page(page) + .limit(limit) + .list() + }) + .await??; + + let res = Self::Response { + registration_applications, + }; + + Ok(res) + } +} diff --git a/crates/api/src/site/registration_applications/mod.rs b/crates/api/src/site/registration_applications/mod.rs new file mode 100644 index 00000000..0d330ba8 --- /dev/null +++ b/crates/api/src/site/registration_applications/mod.rs @@ -0,0 +1,3 @@ +mod approve; +mod list; +mod unread_count; diff --git a/crates/api/src/site/registration_applications/unread_count.rs b/crates/api/src/site/registration_applications/unread_count.rs new file mode 100644 index 00000000..03584baf --- /dev/null +++ b/crates/api/src/site/registration_applications/unread_count.rs @@ -0,0 +1,43 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + is_admin, + site::{GetUnreadRegistrationApplicationCount, GetUnreadRegistrationApplicationCountResponse}, +}; +use lemmy_db_schema::source::site::Site; +use lemmy_db_views::registration_application_view::RegistrationApplicationView; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for GetUnreadRegistrationApplicationCount { + type Response = GetUnreadRegistrationApplicationCountResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data = self; + let local_user_view = + get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; + + // Only let admins do this + is_admin(&local_user_view)?; + + let verified_email_only = blocking(context.pool(), Site::read_local_site) + .await?? + .require_email_verification; + + let registration_applications = blocking(context.pool(), move |conn| { + RegistrationApplicationView::get_unread_count(conn, verified_email_only) + }) + .await??; + + Ok(Self::Response { + registration_applications, + }) + } +} diff --git a/crates/api/src/site/resolve_object.rs b/crates/api/src/site/resolve_object.rs new file mode 100644 index 00000000..87f3c859 --- /dev/null +++ b/crates/api/src/site/resolve_object.rs @@ -0,0 +1,78 @@ +use crate::Perform; +use actix_web::web::Data; +use diesel::NotFound; +use lemmy_api_common::{ + blocking, + check_private_instance, + get_local_user_view_from_jwt_opt, + site::{ResolveObject, ResolveObjectResponse}, +}; +use lemmy_apub::fetcher::search::{search_by_apub_id, SearchableObjects}; +use lemmy_db_schema::{newtypes::PersonId, DbPool}; +use lemmy_db_views::{comment_view::CommentView, post_view::PostView}; +use lemmy_db_views_actor::{community_view::CommunityView, person_view::PersonViewSafe}; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for ResolveObject { + type Response = ResolveObjectResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let local_user_view = + get_local_user_view_from_jwt_opt(self.auth.as_ref(), context.pool(), context.secret()) + .await?; + check_private_instance(&local_user_view, context.pool()).await?; + + let res = search_by_apub_id(&self.q, context) + .await + .map_err(|e| e.with_message("couldnt_find_object"))?; + convert_response(res, local_user_view.map(|l| l.person.id), context.pool()) + .await + .map_err(|e| e.with_message("couldnt_find_object")) + } +} + +async fn convert_response( + object: SearchableObjects, + user_id: Option, + pool: &DbPool, +) -> Result { + let removed_or_deleted; + let mut res = ResolveObjectResponse { + comment: None, + post: None, + community: None, + person: None, + }; + use SearchableObjects::*; + match object { + Person(p) => { + removed_or_deleted = p.deleted; + res.person = Some(blocking(pool, move |conn| PersonViewSafe::read(conn, p.id)).await??) + } + Community(c) => { + removed_or_deleted = c.deleted || c.removed; + res.community = + Some(blocking(pool, move |conn| CommunityView::read(conn, c.id, user_id)).await??) + } + Post(p) => { + removed_or_deleted = p.deleted || p.removed; + res.post = Some(blocking(pool, move |conn| PostView::read(conn, p.id, user_id)).await??) + } + Comment(c) => { + removed_or_deleted = c.deleted || c.removed; + res.comment = Some(blocking(pool, move |conn| CommentView::read(conn, c.id, user_id)).await??) + } + }; + // if the object was deleted from database, dont return it + if removed_or_deleted { + return Err(NotFound {}.into()); + } + Ok(res) +} diff --git a/crates/api/src/site/search.rs b/crates/api/src/site/search.rs new file mode 100644 index 00000000..f4f998d5 --- /dev/null +++ b/crates/api/src/site/search.rs @@ -0,0 +1,269 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_private_instance, + get_local_user_view_from_jwt_opt, + site::{Search, SearchResponse}, +}; +use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity}; +use lemmy_db_schema::{ + from_opt_str_to_opt_enum, + source::community::Community, + traits::DeleteableOrRemoveable, + ListingType, + SearchType, + SortType, +}; +use lemmy_db_views::{comment_view::CommentQueryBuilder, post_view::PostQueryBuilder}; +use lemmy_db_views_actor::{ + community_view::CommunityQueryBuilder, + person_view::PersonQueryBuilder, +}; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl Perform for Search { + type Response = SearchResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &Search = self; + + let local_user_view = + get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) + .await?; + + check_private_instance(&local_user_view, context.pool()).await?; + + let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw); + let show_bot_accounts = local_user_view + .as_ref() + .map(|t| t.local_user.show_bot_accounts); + let show_read_posts = local_user_view + .as_ref() + .map(|t| t.local_user.show_read_posts); + + let person_id = local_user_view.map(|u| u.person.id); + + let mut posts = Vec::new(); + let mut comments = Vec::new(); + let mut communities = Vec::new(); + let mut users = Vec::new(); + + // TODO no clean / non-nsfw searching rn + + let q = data.q.to_owned(); + let page = data.page; + let limit = data.limit; + let sort: Option = from_opt_str_to_opt_enum(&data.sort); + let listing_type: Option = from_opt_str_to_opt_enum(&data.listing_type); + let search_type: SearchType = from_opt_str_to_opt_enum(&data.type_).unwrap_or(SearchType::All); + let community_id = data.community_id; + let community_actor_id = if let Some(name) = &data.community_name { + resolve_actor_identifier::(name, context) + .await + .ok() + .map(|c| c.actor_id) + } else { + None + }; + let creator_id = data.creator_id; + match search_type { + SearchType::Posts => { + posts = blocking(context.pool(), move |conn| { + PostQueryBuilder::create(conn) + .sort(sort) + .show_nsfw(show_nsfw) + .show_bot_accounts(show_bot_accounts) + .show_read_posts(show_read_posts) + .listing_type(listing_type) + .community_id(community_id) + .community_actor_id(community_actor_id) + .creator_id(creator_id) + .my_person_id(person_id) + .search_term(q) + .page(page) + .limit(limit) + .list() + }) + .await??; + } + SearchType::Comments => { + comments = blocking(context.pool(), move |conn| { + CommentQueryBuilder::create(conn) + .sort(sort) + .listing_type(listing_type) + .search_term(q) + .show_bot_accounts(show_bot_accounts) + .community_id(community_id) + .community_actor_id(community_actor_id) + .creator_id(creator_id) + .my_person_id(person_id) + .page(page) + .limit(limit) + .list() + }) + .await??; + } + SearchType::Communities => { + communities = blocking(context.pool(), move |conn| { + CommunityQueryBuilder::create(conn) + .sort(sort) + .listing_type(listing_type) + .search_term(q) + .my_person_id(person_id) + .page(page) + .limit(limit) + .list() + }) + .await??; + } + SearchType::Users => { + users = blocking(context.pool(), move |conn| { + PersonQueryBuilder::create(conn) + .sort(sort) + .search_term(q) + .page(page) + .limit(limit) + .list() + }) + .await??; + } + SearchType::All => { + // If the community or creator is included, dont search communities or users + let community_or_creator_included = + data.community_id.is_some() || data.community_name.is_some() || data.creator_id.is_some(); + let community_actor_id_2 = community_actor_id.to_owned(); + + posts = blocking(context.pool(), move |conn| { + PostQueryBuilder::create(conn) + .sort(sort) + .show_nsfw(show_nsfw) + .show_bot_accounts(show_bot_accounts) + .show_read_posts(show_read_posts) + .listing_type(listing_type) + .community_id(community_id) + .community_actor_id(community_actor_id_2) + .creator_id(creator_id) + .my_person_id(person_id) + .search_term(q) + .page(page) + .limit(limit) + .list() + }) + .await??; + + let q = data.q.to_owned(); + let community_actor_id = community_actor_id.to_owned(); + + comments = blocking(context.pool(), move |conn| { + CommentQueryBuilder::create(conn) + .sort(sort) + .listing_type(listing_type) + .search_term(q) + .show_bot_accounts(show_bot_accounts) + .community_id(community_id) + .community_actor_id(community_actor_id) + .creator_id(creator_id) + .my_person_id(person_id) + .page(page) + .limit(limit) + .list() + }) + .await??; + + let q = data.q.to_owned(); + + communities = if community_or_creator_included { + vec![] + } else { + blocking(context.pool(), move |conn| { + CommunityQueryBuilder::create(conn) + .sort(sort) + .listing_type(listing_type) + .search_term(q) + .my_person_id(person_id) + .page(page) + .limit(limit) + .list() + }) + .await?? + }; + + let q = data.q.to_owned(); + + users = if community_or_creator_included { + vec![] + } else { + blocking(context.pool(), move |conn| { + PersonQueryBuilder::create(conn) + .sort(sort) + .search_term(q) + .page(page) + .limit(limit) + .list() + }) + .await?? + }; + } + SearchType::Url => { + posts = blocking(context.pool(), move |conn| { + PostQueryBuilder::create(conn) + .sort(sort) + .show_nsfw(show_nsfw) + .show_bot_accounts(show_bot_accounts) + .show_read_posts(show_read_posts) + .listing_type(listing_type) + .my_person_id(person_id) + .community_id(community_id) + .community_actor_id(community_actor_id) + .creator_id(creator_id) + .url_search(q) + .page(page) + .limit(limit) + .list() + }) + .await??; + } + }; + + // Blank out deleted or removed info for non logged in users + if person_id.is_none() { + for cv in communities + .iter_mut() + .filter(|cv| cv.community.deleted || cv.community.removed) + { + cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info(); + } + + for pv in posts + .iter_mut() + .filter(|p| p.post.deleted || p.post.removed) + { + pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info(); + } + + for cv in comments + .iter_mut() + .filter(|cv| cv.comment.deleted || cv.comment.removed) + { + cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info(); + } + } + + // Return the jwt + Ok(SearchResponse { + type_: search_type.to_string(), + comments, + posts, + communities, + users, + }) + } +} diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 0b5da49f..898efbf5 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -222,7 +222,7 @@ pub struct PasswordReset { pub struct PasswordResetResponse {} #[derive(Debug, Serialize, Deserialize)] -pub struct PasswordChange { +pub struct PasswordChangeAfterReset { pub token: Sensitive, pub password: Sensitive, pub password_verify: Sensitive, diff --git a/crates/api_crud/src/comment/list.rs b/crates/api_crud/src/comment/list.rs new file mode 100644 index 00000000..cd91a3b0 --- /dev/null +++ b/crates/api_crud/src/comment/list.rs @@ -0,0 +1,84 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_private_instance, + comment::*, + get_local_user_view_from_jwt_opt, +}; +use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity}; +use lemmy_db_schema::{ + from_opt_str_to_opt_enum, + source::community::Community, + traits::DeleteableOrRemoveable, + ListingType, + SortType, +}; +use lemmy_db_views::comment_view::CommentQueryBuilder; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for GetComments { + type Response = GetCommentsResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &GetComments = self; + let local_user_view = + get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) + .await?; + + check_private_instance(&local_user_view, context.pool()).await?; + + let show_bot_accounts = local_user_view + .as_ref() + .map(|t| t.local_user.show_bot_accounts); + let person_id = local_user_view.map(|u| u.person.id); + + let sort: Option = from_opt_str_to_opt_enum(&data.sort); + let listing_type: Option = from_opt_str_to_opt_enum(&data.type_); + + let community_id = data.community_id; + let community_actor_id = if let Some(name) = &data.community_name { + resolve_actor_identifier::(name, context) + .await + .ok() + .map(|c| c.actor_id) + } else { + None + }; + let saved_only = data.saved_only; + let page = data.page; + let limit = data.limit; + let mut comments = blocking(context.pool(), move |conn| { + CommentQueryBuilder::create(conn) + .listing_type(listing_type) + .sort(sort) + .saved_only(saved_only) + .community_id(community_id) + .community_actor_id(community_actor_id) + .my_person_id(person_id) + .show_bot_accounts(show_bot_accounts) + .page(page) + .limit(limit) + .list() + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_get_comments"))?; + + // Blank out deleted or removed info + for cv in comments + .iter_mut() + .filter(|cv| cv.comment.deleted || cv.comment.removed) + { + cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info(); + } + + Ok(GetCommentsResponse { comments }) + } +} diff --git a/crates/api_crud/src/comment/mod.rs b/crates/api_crud/src/comment/mod.rs index 71683237..7003bdd8 100644 --- a/crates/api_crud/src/comment/mod.rs +++ b/crates/api_crud/src/comment/mod.rs @@ -1,4 +1,5 @@ mod create; mod delete; +mod list; mod read; mod update; diff --git a/crates/api_crud/src/comment/read.rs b/crates/api_crud/src/comment/read.rs index d04a0b45..5b2155da 100644 --- a/crates/api_crud/src/comment/read.rs +++ b/crates/api_crud/src/comment/read.rs @@ -6,15 +6,7 @@ use lemmy_api_common::{ comment::*, get_local_user_view_from_jwt_opt, }; -use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity}; -use lemmy_db_schema::{ - from_opt_str_to_opt_enum, - source::community::Community, - traits::DeleteableOrRemoveable, - ListingType, - SortType, -}; -use lemmy_db_views::comment_view::{CommentQueryBuilder, CommentView}; +use lemmy_db_views::comment_view::CommentView; use lemmy_utils::{ConnectionId, LemmyError}; use lemmy_websocket::LemmyContext; @@ -50,68 +42,3 @@ impl PerformCrud for GetComment { }) } } - -#[async_trait::async_trait(?Send)] -impl PerformCrud for GetComments { - type Response = GetCommentsResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetComments = self; - let local_user_view = - get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) - .await?; - - check_private_instance(&local_user_view, context.pool()).await?; - - let show_bot_accounts = local_user_view - .as_ref() - .map(|t| t.local_user.show_bot_accounts); - let person_id = local_user_view.map(|u| u.person.id); - - let sort: Option = from_opt_str_to_opt_enum(&data.sort); - let listing_type: Option = from_opt_str_to_opt_enum(&data.type_); - - let community_id = data.community_id; - let community_actor_id = if let Some(name) = &data.community_name { - resolve_actor_identifier::(name, context) - .await - .ok() - .map(|c| c.actor_id) - } else { - None - }; - let saved_only = data.saved_only; - let page = data.page; - let limit = data.limit; - let mut comments = blocking(context.pool(), move |conn| { - CommentQueryBuilder::create(conn) - .listing_type(listing_type) - .sort(sort) - .saved_only(saved_only) - .community_id(community_id) - .community_actor_id(community_actor_id) - .my_person_id(person_id) - .show_bot_accounts(show_bot_accounts) - .page(page) - .limit(limit) - .list() - }) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_get_comments"))?; - - // Blank out deleted or removed info - for cv in comments - .iter_mut() - .filter(|cv| cv.comment.deleted || cv.comment.removed) - { - cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info(); - } - - Ok(GetCommentsResponse { comments }) - } -} diff --git a/crates/api_crud/src/community/list.rs b/crates/api_crud/src/community/list.rs new file mode 100644 index 00000000..24c154ad --- /dev/null +++ b/crates/api_crud/src/community/list.rs @@ -0,0 +1,74 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_private_instance, + community::*, + get_local_user_view_from_jwt_opt, +}; +use lemmy_db_schema::{ + from_opt_str_to_opt_enum, + traits::DeleteableOrRemoveable, + ListingType, + SortType, +}; +use lemmy_db_views_actor::community_view::CommunityQueryBuilder; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for ListCommunities { + type Response = ListCommunitiesResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &ListCommunities = self; + let local_user_view = + get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) + .await?; + + check_private_instance(&local_user_view, context.pool()).await?; + + let person_id = local_user_view.to_owned().map(|l| l.person.id); + + // Don't show NSFW by default + let show_nsfw = match &local_user_view { + Some(uv) => uv.local_user.show_nsfw, + None => false, + }; + + let sort: Option = from_opt_str_to_opt_enum(&data.sort); + let listing_type: Option = from_opt_str_to_opt_enum(&data.type_); + + let page = data.page; + let limit = data.limit; + let mut communities = blocking(context.pool(), move |conn| { + CommunityQueryBuilder::create(conn) + .listing_type(listing_type) + .sort(sort) + .show_nsfw(show_nsfw) + .my_person_id(person_id) + .page(page) + .limit(limit) + .list() + }) + .await??; + + // Blank out deleted or removed info for non-logged in users + if person_id.is_none() { + for cv in communities + .iter_mut() + .filter(|cv| cv.community.deleted || cv.community.removed) + { + cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info(); + } + } + + // Return the jwt + Ok(ListCommunitiesResponse { communities }) + } +} diff --git a/crates/api_crud/src/community/mod.rs b/crates/api_crud/src/community/mod.rs index 71683237..7003bdd8 100644 --- a/crates/api_crud/src/community/mod.rs +++ b/crates/api_crud/src/community/mod.rs @@ -1,4 +1,5 @@ mod create; mod delete; +mod list; mod read; mod update; diff --git a/crates/api_crud/src/community/read.rs b/crates/api_crud/src/community/read.rs index d4a28b78..9c4ba333 100644 --- a/crates/api_crud/src/community/read.rs +++ b/crates/api_crud/src/community/read.rs @@ -11,15 +11,12 @@ use lemmy_apub::{ objects::{community::ApubCommunity, instance::instance_actor_id_from_url}, }; use lemmy_db_schema::{ - from_opt_str_to_opt_enum, source::{community::Community, site::Site}, traits::DeleteableOrRemoveable, - ListingType, - SortType, }; use lemmy_db_views_actor::{ community_moderator_view::CommunityModeratorView, - community_view::{CommunityQueryBuilder, CommunityView}, + community_view::CommunityView, }; use lemmy_utils::{ConnectionId, LemmyError}; use lemmy_websocket::{messages::GetCommunityUsersOnline, LemmyContext}; @@ -102,60 +99,3 @@ impl PerformCrud for GetCommunity { Ok(res) } } - -#[async_trait::async_trait(?Send)] -impl PerformCrud for ListCommunities { - type Response = ListCommunitiesResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &ListCommunities = self; - let local_user_view = - get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) - .await?; - - check_private_instance(&local_user_view, context.pool()).await?; - - let person_id = local_user_view.to_owned().map(|l| l.person.id); - - // Don't show NSFW by default - let show_nsfw = match &local_user_view { - Some(uv) => uv.local_user.show_nsfw, - None => false, - }; - - let sort: Option = from_opt_str_to_opt_enum(&data.sort); - let listing_type: Option = from_opt_str_to_opt_enum(&data.type_); - - let page = data.page; - let limit = data.limit; - let mut communities = blocking(context.pool(), move |conn| { - CommunityQueryBuilder::create(conn) - .listing_type(listing_type) - .sort(sort) - .show_nsfw(show_nsfw) - .my_person_id(person_id) - .page(page) - .limit(limit) - .list() - }) - .await??; - - // Blank out deleted or removed info for non-logged in users - if person_id.is_none() { - for cv in communities - .iter_mut() - .filter(|cv| cv.community.deleted || cv.community.removed) - { - cv.community = cv.to_owned().community.blank_out_deleted_or_removed_info(); - } - } - - // Return the jwt - Ok(ListCommunitiesResponse { communities }) - } -} diff --git a/crates/api_crud/src/post/list.rs b/crates/api_crud/src/post/list.rs new file mode 100644 index 00000000..8a714e99 --- /dev/null +++ b/crates/api_crud/src/post/list.rs @@ -0,0 +1,101 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_private_instance, + get_local_user_view_from_jwt_opt, + post::*, +}; +use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity}; +use lemmy_db_schema::{ + from_opt_str_to_opt_enum, + source::community::Community, + traits::DeleteableOrRemoveable, + ListingType, + SortType, +}; +use lemmy_db_views::post_view::PostQueryBuilder; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for GetPosts { + type Response = GetPostsResponse; + + #[tracing::instrument(skip(context, _websocket_id))] + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &GetPosts = self; + let local_user_view = + get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) + .await?; + + check_private_instance(&local_user_view, context.pool()).await?; + + let person_id = local_user_view.to_owned().map(|l| l.person.id); + + let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw); + let show_bot_accounts = local_user_view + .as_ref() + .map(|t| t.local_user.show_bot_accounts); + let show_read_posts = local_user_view + .as_ref() + .map(|t| t.local_user.show_read_posts); + + let sort: Option = from_opt_str_to_opt_enum(&data.sort); + let listing_type: Option = from_opt_str_to_opt_enum(&data.type_); + + let page = data.page; + let limit = data.limit; + let community_id = data.community_id; + let community_actor_id = if let Some(name) = &data.community_name { + resolve_actor_identifier::(name, context) + .await + .ok() + .map(|c| c.actor_id) + } else { + None + }; + let saved_only = data.saved_only; + + let mut posts = blocking(context.pool(), move |conn| { + PostQueryBuilder::create(conn) + .listing_type(listing_type) + .sort(sort) + .show_nsfw(show_nsfw) + .show_bot_accounts(show_bot_accounts) + .show_read_posts(show_read_posts) + .community_id(community_id) + .community_actor_id(community_actor_id) + .saved_only(saved_only) + .my_person_id(person_id) + .page(page) + .limit(limit) + .list() + }) + .await? + .map_err(|e| LemmyError::from_error_message(e, "couldnt_get_posts"))?; + + // Blank out deleted or removed info for non-logged in users + if person_id.is_none() { + for pv in posts + .iter_mut() + .filter(|p| p.post.deleted || p.post.removed) + { + pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info(); + } + + for pv in posts + .iter_mut() + .filter(|p| p.community.deleted || p.community.removed) + { + pv.community = pv.to_owned().community.blank_out_deleted_or_removed_info(); + } + } + + Ok(GetPostsResponse { posts }) + } +} diff --git a/crates/api_crud/src/post/mod.rs b/crates/api_crud/src/post/mod.rs index 71683237..7003bdd8 100644 --- a/crates/api_crud/src/post/mod.rs +++ b/crates/api_crud/src/post/mod.rs @@ -1,4 +1,5 @@ mod create; mod delete; +mod list; mod read; mod update; diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index 72444224..0d4f94ff 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -7,18 +7,8 @@ use lemmy_api_common::{ mark_post_as_read, post::*, }; -use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity}; -use lemmy_db_schema::{ - from_opt_str_to_opt_enum, - source::community::Community, - traits::DeleteableOrRemoveable, - ListingType, - SortType, -}; -use lemmy_db_views::{ - comment_view::CommentQueryBuilder, - post_view::{PostQueryBuilder, PostView}, -}; +use lemmy_db_schema::traits::DeleteableOrRemoveable; +use lemmy_db_views::{comment_view::CommentQueryBuilder, post_view::PostView}; use lemmy_db_views_actor::{ community_moderator_view::CommunityModeratorView, community_view::CommunityView, @@ -117,85 +107,3 @@ impl PerformCrud for GetPost { }) } } - -#[async_trait::async_trait(?Send)] -impl PerformCrud for GetPosts { - type Response = GetPostsResponse; - - #[tracing::instrument(skip(context, _websocket_id))] - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetPosts = self; - let local_user_view = - get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret()) - .await?; - - check_private_instance(&local_user_view, context.pool()).await?; - - let person_id = local_user_view.to_owned().map(|l| l.person.id); - - let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw); - let show_bot_accounts = local_user_view - .as_ref() - .map(|t| t.local_user.show_bot_accounts); - let show_read_posts = local_user_view - .as_ref() - .map(|t| t.local_user.show_read_posts); - - let sort: Option = from_opt_str_to_opt_enum(&data.sort); - let listing_type: Option = from_opt_str_to_opt_enum(&data.type_); - - let page = data.page; - let limit = data.limit; - let community_id = data.community_id; - let community_actor_id = if let Some(name) = &data.community_name { - resolve_actor_identifier::(name, context) - .await - .ok() - .map(|c| c.actor_id) - } else { - None - }; - let saved_only = data.saved_only; - - let mut posts = blocking(context.pool(), move |conn| { - PostQueryBuilder::create(conn) - .listing_type(listing_type) - .sort(sort) - .show_nsfw(show_nsfw) - .show_bot_accounts(show_bot_accounts) - .show_read_posts(show_read_posts) - .community_id(community_id) - .community_actor_id(community_actor_id) - .saved_only(saved_only) - .my_person_id(person_id) - .page(page) - .limit(limit) - .list() - }) - .await? - .map_err(|e| LemmyError::from_error_message(e, "couldnt_get_posts"))?; - - // Blank out deleted or removed info for non-logged in users - if person_id.is_none() { - for pv in posts - .iter_mut() - .filter(|p| p.post.deleted || p.post.removed) - { - pv.post = pv.to_owned().post.blank_out_deleted_or_removed_info(); - } - - for pv in posts - .iter_mut() - .filter(|p| p.community.deleted || p.community.removed) - { - pv.community = pv.to_owned().community.blank_out_deleted_or_removed_info(); - } - } - - Ok(GetPostsResponse { posts }) - } -} diff --git a/src/api_routes.rs b/src/api_routes.rs index 7a3feb5c..757e40a1 100644 --- a/src/api_routes.rs +++ b/src/api_routes.rs @@ -195,7 +195,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { ) .route( "/password_change", - web::post().to(route_post::), + web::post().to(route_post::), ) // mark_all_as_read feels off being in this section as well .route(