diff --git a/Cargo.lock b/Cargo.lock index b525d65a6..004b31d04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1746,7 +1746,7 @@ dependencies = [ "http-signature-normalization-actix", "itertools", "lazy_static", - "lemmy_api_structs", + "lemmy_api_common", "lemmy_apub", "lemmy_db_queries", "lemmy_db_schema", @@ -1771,7 +1771,7 @@ dependencies = [ ] [[package]] -name = "lemmy_api_structs" +name = "lemmy_api_common" version = "0.1.0" dependencies = [ "actix-web", @@ -1789,6 +1789,51 @@ dependencies = [ "url", ] +[[package]] +name = "lemmy_api_crud" +version = "0.1.0" +dependencies = [ + "actix", + "actix-rt", + "actix-web", + "anyhow", + "async-trait", + "awc", + "background-jobs", + "base64 0.13.0", + "bcrypt", + "captcha", + "chrono", + "diesel", + "futures", + "http", + "http-signature-normalization-actix", + "itertools", + "lazy_static", + "lemmy_api_common", + "lemmy_apub", + "lemmy_db_queries", + "lemmy_db_schema", + "lemmy_db_views", + "lemmy_db_views_actor", + "lemmy_db_views_moderator", + "lemmy_utils", + "lemmy_websocket", + "log", + "openssl", + "rand 0.8.3", + "reqwest", + "serde", + "serde_json", + "sha2", + "strum", + "strum_macros", + "thiserror", + "tokio 0.3.7", + "url", + "uuid", +] + [[package]] name = "lemmy_apub" version = "0.1.0" @@ -1813,7 +1858,7 @@ dependencies = [ "http-signature-normalization-reqwest", "itertools", "lazy_static", - "lemmy_api_structs", + "lemmy_api_common", "lemmy_db_queries", "lemmy_db_schema", "lemmy_db_views", @@ -1916,7 +1961,7 @@ dependencies = [ "chrono", "diesel", "lazy_static", - "lemmy_api_structs", + "lemmy_api_common", "lemmy_db_queries", "lemmy_db_schema", "lemmy_db_views", @@ -1948,7 +1993,8 @@ dependencies = [ "env_logger", "http-signature-normalization-actix", "lemmy_api", - "lemmy_api_structs", + "lemmy_api_common", + "lemmy_api_crud", "lemmy_apub", "lemmy_db_queries", "lemmy_db_schema", @@ -2013,7 +2059,7 @@ dependencies = [ "background-jobs", "chrono", "diesel", - "lemmy_api_structs", + "lemmy_api_common", "lemmy_db_queries", "lemmy_db_schema", "lemmy_utils", diff --git a/Cargo.toml b/Cargo.toml index e7d92fdf1..4f338f9b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ debug = 0 [workspace] members = [ "crates/api", + "crates/api_crud", + "crates/api_common", "crates/apub", "crates/utils", "crates/db_queries", @@ -19,13 +21,13 @@ members = [ "crates/db_views", "crates/db_views_actor", "crates/db_views_actor", - "crates/api_structs", "crates/websocket", "crates/routes" ] [dependencies] lemmy_api = { path = "./crates/api" } +lemmy_api_crud = { path = "./crates/api_crud" } lemmy_apub = { path = "./crates/apub" } lemmy_utils = { path = "./crates/utils" } lemmy_db_schema = { path = "./crates/db_schema" } @@ -33,7 +35,7 @@ lemmy_db_queries = { path = "./crates/db_queries" } lemmy_db_views = { path = "./crates/db_views" } lemmy_db_views_moderator = { path = "./crates/db_views_moderator" } lemmy_db_views_actor = { path = "./crates/db_views_actor" } -lemmy_api_structs = { path = "crates/api_structs" } +lemmy_api_common = { path = "crates/api_common" } lemmy_websocket = { path = "./crates/websocket" } lemmy_routes = { path = "./crates/routes" } diesel = "1.4.5" diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index ea3cd625c..1aaa0528e 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -16,7 +16,7 @@ lemmy_db_schema = { path = "../db_schema" } lemmy_db_views = { path = "../db_views" } lemmy_db_views_moderator = { path = "../db_views_moderator" } lemmy_db_views_actor = { path = "../db_views_actor" } -lemmy_api_structs = { path = "../api_structs" } +lemmy_api_common = { path = "../api_common" } lemmy_websocket = { path = "../websocket" } diesel = "1.4.5" bcrypt = "0.9.0" diff --git a/crates/api/src/comment.rs b/crates/api/src/comment.rs index bcee72b09..2237204f4 100644 --- a/crates/api/src/comment.rs +++ b/crates/api/src/comment.rs @@ -1,472 +1,18 @@ -use crate::{ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, check_community_ban, check_downvotes_enabled, - collect_moderated_communities, + comment::*, get_local_user_view_from_jwt, - get_local_user_view_from_jwt_opt, - get_post, - is_mod_or_admin, - Perform, }; -use actix_web::web::Data; -use lemmy_api_structs::{blocking, comment::*, send_local_notifs}; -use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType}; -use lemmy_db_queries::{ - source::comment::Comment_, - Crud, - Likeable, - ListingType, - Reportable, - Saveable, - SortType, -}; -use lemmy_db_schema::{ - source::{comment::*, comment_report::*, moderator::*}, - LocalUserId, -}; -use lemmy_db_views::{ - comment_report_view::{CommentReportQueryBuilder, CommentReportView}, - comment_view::{CommentQueryBuilder, CommentView}, - local_user_view::LocalUserView, -}; -use lemmy_utils::{ - utils::{remove_slurs, scrape_text_for_mentions}, - ApiError, - ConnectionId, - LemmyError, -}; -use lemmy_websocket::{ - messages::{SendComment, SendModRoomMessage, SendUserRoomMessage}, - LemmyContext, - UserOperation, -}; -use std::str::FromStr; - -#[async_trait::async_trait(?Send)] -impl Perform for CreateComment { - type Response = CommentResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &CreateComment = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - let content_slurs_removed = remove_slurs(&data.content.to_owned()); - - // Check for a community ban - let post_id = data.post_id; - let post = get_post(post_id, context.pool()).await?; - - check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?; - - // Check if post is locked, no new comments - if post.locked { - return Err(ApiError::err("locked").into()); - } - - // If there's a parent_id, check to make sure that comment is in that post - if let Some(parent_id) = data.parent_id { - // Make sure the parent comment exists - let parent = - match blocking(context.pool(), move |conn| Comment::read(&conn, parent_id)).await? { - Ok(comment) => comment, - Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()), - }; - if parent.post_id != post_id { - return Err(ApiError::err("couldnt_create_comment").into()); - } - } - - let comment_form = CommentForm { - content: content_slurs_removed, - parent_id: data.parent_id.to_owned(), - post_id: data.post_id, - creator_id: local_user_view.person.id, - removed: None, - deleted: None, - read: None, - published: None, - updated: None, - ap_id: None, - local: true, - }; - - // Create the comment - let comment_form2 = comment_form.clone(); - let inserted_comment = match blocking(context.pool(), move |conn| { - Comment::create(&conn, &comment_form2) - }) - .await? - { - Ok(comment) => comment, - Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()), - }; - - // Necessary to update the ap_id - let inserted_comment_id = inserted_comment.id; - let updated_comment: Comment = - match blocking(context.pool(), move |conn| -> Result { - let apub_id = - generate_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string())?; - Ok(Comment::update_ap_id(&conn, inserted_comment_id, apub_id)?) - }) - .await? - { - Ok(comment) => comment, - Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()), - }; - - updated_comment - .send_create(&local_user_view.person, context) - .await?; - - // Scan the comment for user mentions, add those rows - let post_id = post.id; - let mentions = scrape_text_for_mentions(&comment_form.content); - let recipient_ids = send_local_notifs( - mentions, - updated_comment.clone(), - local_user_view.person.clone(), - post, - context.pool(), - true, - ) - .await?; - - // You like your own comment by default - let like_form = CommentLikeForm { - comment_id: inserted_comment.id, - post_id, - person_id: local_user_view.person.id, - score: 1, - }; - - let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form); - if blocking(context.pool(), like).await?.is_err() { - return Err(ApiError::err("couldnt_like_comment").into()); - } - - updated_comment - .send_like(&local_user_view.person, context) - .await?; - - let person_id = local_user_view.person.id; - let mut comment_view = blocking(context.pool(), move |conn| { - CommentView::read(&conn, inserted_comment.id, Some(person_id)) - }) - .await??; - - // If its a comment to yourself, mark it as read - let comment_id = comment_view.comment.id; - if local_user_view.person.id == comment_view.get_recipient_id() { - match blocking(context.pool(), move |conn| { - Comment::update_read(conn, comment_id, true) - }) - .await? - { - Ok(comment) => comment, - Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()), - }; - comment_view.comment.read = true; - } - - let mut res = CommentResponse { - comment_view, - recipient_ids, - form_id: data.form_id.to_owned(), - }; - - context.chat_server().do_send(SendComment { - op: UserOperation::CreateComment, - comment: res.clone(), - websocket_id, - }); - - res.recipient_ids = Vec::new(); // Necessary to avoid doubles - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for EditComment { - type Response = CommentResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &EditComment = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - let comment_id = data.comment_id; - let orig_comment = blocking(context.pool(), move |conn| { - CommentView::read(&conn, comment_id, None) - }) - .await??; - - check_community_ban( - local_user_view.person.id, - orig_comment.community.id, - context.pool(), - ) - .await?; - - // Verify that only the creator can edit - if local_user_view.person.id != orig_comment.creator.id { - return Err(ApiError::err("no_comment_edit_allowed").into()); - } - - // Do the update - let content_slurs_removed = remove_slurs(&data.content.to_owned()); - let comment_id = data.comment_id; - let updated_comment = match blocking(context.pool(), move |conn| { - Comment::update_content(conn, comment_id, &content_slurs_removed) - }) - .await? - { - Ok(comment) => comment, - Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()), - }; - - // Send the apub update - updated_comment - .send_update(&local_user_view.person, context) - .await?; - - // Do the mentions / recipients - let updated_comment_content = updated_comment.content.to_owned(); - let mentions = scrape_text_for_mentions(&updated_comment_content); - let recipient_ids = send_local_notifs( - mentions, - updated_comment, - local_user_view.person.clone(), - orig_comment.post, - context.pool(), - false, - ) - .await?; - - 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, - form_id: data.form_id.to_owned(), - }; - - context.chat_server().do_send(SendComment { - op: UserOperation::EditComment, - comment: res.clone(), - websocket_id, - }); - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for DeleteComment { - type Response = CommentResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &DeleteComment = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - let comment_id = data.comment_id; - let orig_comment = blocking(context.pool(), move |conn| { - CommentView::read(&conn, comment_id, None) - }) - .await??; - - check_community_ban( - local_user_view.person.id, - orig_comment.community.id, - context.pool(), - ) - .await?; - - // Verify that only the creator can delete - if local_user_view.person.id != orig_comment.creator.id { - return Err(ApiError::err("no_comment_edit_allowed").into()); - } - - // Do the delete - let deleted = data.deleted; - let updated_comment = match blocking(context.pool(), move |conn| { - Comment::update_deleted(conn, comment_id, deleted) - }) - .await? - { - Ok(comment) => comment, - Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()), - }; - - // Send the apub message - if deleted { - updated_comment - .send_delete(&local_user_view.person, context) - .await?; - } else { - updated_comment - .send_undo_delete(&local_user_view.person, context) - .await?; - } - - // 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??; - - // Build the recipients - let comment_view_2 = comment_view.clone(); - let mentions = vec![]; - let recipient_ids = send_local_notifs( - mentions, - updated_comment, - local_user_view.person.clone(), - comment_view_2.post, - context.pool(), - false, - ) - .await?; - - let res = CommentResponse { - comment_view, - recipient_ids, - form_id: None, // TODO a comment delete might clear forms? - }; - - context.chat_server().do_send(SendComment { - op: UserOperation::DeleteComment, - comment: res.clone(), - websocket_id, - }); - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for RemoveComment { - type Response = CommentResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &RemoveComment = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - let comment_id = data.comment_id; - let orig_comment = blocking(context.pool(), move |conn| { - CommentView::read(&conn, comment_id, None) - }) - .await??; - - check_community_ban( - local_user_view.person.id, - orig_comment.community.id, - context.pool(), - ) - .await?; - - // Verify that only a mod or admin can remove - is_mod_or_admin( - context.pool(), - local_user_view.person.id, - orig_comment.community.id, - ) - .await?; - - // Do the remove - let removed = data.removed; - let updated_comment = match blocking(context.pool(), move |conn| { - Comment::update_removed(conn, comment_id, removed) - }) - .await? - { - Ok(comment) => comment, - Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()), - }; - - // Mod tables - let form = ModRemoveCommentForm { - mod_person_id: local_user_view.person.id, - comment_id: data.comment_id, - removed: Some(removed), - reason: data.reason.to_owned(), - }; - blocking(context.pool(), move |conn| { - ModRemoveComment::create(conn, &form) - }) - .await??; - - // Send the apub message - if removed { - updated_comment - .send_remove(&local_user_view.person, context) - .await?; - } else { - updated_comment - .send_undo_remove(&local_user_view.person, context) - .await?; - } - - // 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??; - - // Build the recipients - let comment_view_2 = comment_view.clone(); - - let mentions = vec![]; - let recipient_ids = send_local_notifs( - mentions, - updated_comment, - local_user_view.person.clone(), - comment_view_2.post, - context.pool(), - false, - ) - .await?; - - let res = CommentResponse { - comment_view, - recipient_ids, - form_id: None, // TODO maybe this might clear other forms - }; - - context.chat_server().do_send(SendComment { - op: UserOperation::RemoveComment, - comment: res.clone(), - websocket_id, - }); - - Ok(res) - } -} +use lemmy_apub::ApubLikeableType; +use lemmy_db_queries::{source::comment::Comment_, Likeable, Saveable}; +use lemmy_db_schema::{source::comment::*, LocalUserId}; +use lemmy_db_views::{comment_view::CommentView, local_user_view::LocalUserView}; +use lemmy_utils::{ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation}; #[async_trait::async_trait(?Send)] impl Perform for MarkCommentAsRead { @@ -671,208 +217,3 @@ impl Perform for CreateCommentLike { Ok(res) } } - -#[async_trait::async_trait(?Send)] -impl Perform for GetComments { - type Response = GetCommentsResponse; - - 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, context.pool()).await?; - let person_id = local_user_view.map(|u| u.person.id); - - let type_ = ListingType::from_str(&data.type_)?; - let sort = SortType::from_str(&data.sort)?; - - let community_id = data.community_id; - let community_name = data.community_name.to_owned(); - let saved_only = data.saved_only; - let page = data.page; - let limit = data.limit; - let comments = blocking(context.pool(), move |conn| { - CommentQueryBuilder::create(conn) - .listing_type(type_) - .sort(&sort) - .saved_only(saved_only) - .community_id(community_id) - .community_name(community_name) - .my_person_id(person_id) - .page(page) - .limit(limit) - .list() - }) - .await?; - let comments = match comments { - Ok(comments) => comments, - Err(_) => return Err(ApiError::err("couldnt_get_comments").into()), - }; - - Ok(GetCommentsResponse { comments }) - } -} - -/// Creates a comment report and notifies the moderators of the community -#[async_trait::async_trait(?Send)] -impl Perform for CreateCommentReport { - type Response = CreateCommentReportResponse; - - 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()).await?; - - // check size of report and check for whitespace - let reason = data.reason.trim(); - if reason.is_empty() { - return Err(ApiError::err("report_reason_required").into()); - } - if reason.chars().count() > 1000 { - return Err(ApiError::err("report_too_long").into()); - } - - 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 = match blocking(context.pool(), move |conn| { - CommentReport::report(conn, &report_form) - }) - .await? - { - Ok(report) => report, - Err(_e) => return Err(ApiError::err("couldnt_create_report").into()), - }; - - let res = CreateCommentReportResponse { success: true }; - - context.chat_server().do_send(SendUserRoomMessage { - op: UserOperation::CreateCommentReport, - response: res.clone(), - local_recipient_id: local_user_view.local_user.id, - websocket_id, - }); - - context.chat_server().do_send(SendModRoomMessage { - op: UserOperation::CreateCommentReport, - response: report, - community_id: comment_view.community.id, - websocket_id, - }); - - 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 = ResolveCommentReportResponse; - - 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()).await?; - - let report_id = data.report_id; - let report = blocking(context.pool(), move |conn| { - CommentReportView::read(&conn, report_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) - } - }; - - if blocking(context.pool(), resolve_fun).await?.is_err() { - return Err(ApiError::err("couldnt_resolve_report").into()); - }; - - let report_id = data.report_id; - let res = ResolveCommentReportResponse { - report_id, - resolved, - }; - - 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; - - 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()).await?; - - let person_id = local_user_view.person.id; - let community_id = data.community; - let community_ids = - collect_moderated_communities(person_id, community_id, context.pool()).await?; - - let page = data.page; - let limit = data.limit; - let comments = blocking(context.pool(), move |conn| { - CommentReportQueryBuilder::create(conn) - .community_ids(community_ids) - .page(page) - .limit(limit) - .list() - }) - .await??; - - let res = ListCommentReportsResponse { comments }; - - context.chat_server().do_send(SendUserRoomMessage { - op: UserOperation::ListCommentReports, - response: res.clone(), - local_recipient_id: local_user_view.local_user.id, - websocket_id, - }); - - Ok(res) - } -} diff --git a/crates/api/src/comment_report.rs b/crates/api/src/comment_report.rs new file mode 100644 index 000000000..9cd504b5e --- /dev/null +++ b/crates/api/src/comment_report.rs @@ -0,0 +1,184 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_community_ban, + collect_moderated_communities, + comment::*, + get_local_user_view_from_jwt, + is_mod_or_admin, +}; +use lemmy_db_queries::Reportable; +use lemmy_db_schema::source::comment_report::*; +use lemmy_db_views::{ + comment_report_view::{CommentReportQueryBuilder, CommentReportView}, + comment_view::CommentView, +}; +use lemmy_utils::{ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{ + messages::{SendModRoomMessage, SendUserRoomMessage}, + LemmyContext, + UserOperation, +}; + +/// Creates a comment report and notifies the moderators of the community +#[async_trait::async_trait(?Send)] +impl Perform for CreateCommentReport { + type Response = CreateCommentReportResponse; + + 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()).await?; + + // check size of report and check for whitespace + let reason = data.reason.trim(); + if reason.is_empty() { + return Err(ApiError::err("report_reason_required").into()); + } + if reason.chars().count() > 1000 { + return Err(ApiError::err("report_too_long").into()); + } + + 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 = match blocking(context.pool(), move |conn| { + CommentReport::report(conn, &report_form) + }) + .await? + { + Ok(report) => report, + Err(_e) => return Err(ApiError::err("couldnt_create_report").into()), + }; + + let res = CreateCommentReportResponse { success: true }; + + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperation::CreateCommentReport, + response: res.clone(), + local_recipient_id: local_user_view.local_user.id, + websocket_id, + }); + + context.chat_server().do_send(SendModRoomMessage { + op: UserOperation::CreateCommentReport, + response: report, + community_id: comment_view.community.id, + websocket_id, + }); + + 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 = ResolveCommentReportResponse; + + 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()).await?; + + let report_id = data.report_id; + let report = blocking(context.pool(), move |conn| { + CommentReportView::read(&conn, report_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) + } + }; + + if blocking(context.pool(), resolve_fun).await?.is_err() { + return Err(ApiError::err("couldnt_resolve_report").into()); + }; + + let report_id = data.report_id; + let res = ResolveCommentReportResponse { + report_id, + resolved, + }; + + 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; + + 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()).await?; + + let person_id = local_user_view.person.id; + let community_id = data.community; + let community_ids = + collect_moderated_communities(person_id, community_id, context.pool()).await?; + + let page = data.page; + let limit = data.limit; + let comments = blocking(context.pool(), move |conn| { + CommentReportQueryBuilder::create(conn) + .community_ids(community_ids) + .page(page) + .limit(limit) + .list() + }) + .await??; + + let res = ListCommentReportsResponse { comments }; + + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperation::ListCommentReports, + response: res.clone(), + local_recipient_id: local_user_view.local_user.id, + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api/src/community.rs b/crates/api/src/community.rs index f7e8e23ce..1ee2a9a76 100644 --- a/crates/api/src/community.rs +++ b/crates/api/src/community.rs @@ -1,483 +1,41 @@ -use crate::{ - check_community_ban, - get_local_user_view_from_jwt, - get_local_user_view_from_jwt_opt, - is_admin, - is_mod_or_admin, - Perform, -}; +use crate::Perform; use actix_web::web::Data; use anyhow::Context; -use lemmy_api_structs::{blocking, community::*}; -use lemmy_apub::{ - generate_apub_endpoint, - generate_followers_url, - generate_inbox_url, - generate_shared_inbox_url, - ActorType, - CommunityType, - EndpointType, - UserType, +use lemmy_api_common::{ + blocking, + check_community_ban, + community::*, + get_local_user_view_from_jwt, + is_mod_or_admin, }; +use lemmy_apub::{ActorType, CommunityType, UserType}; use lemmy_db_queries::{ - diesel_option_overwrite_to_url, source::{ comment::Comment_, community::{CommunityModerator_, Community_}, post::Post_, }, - ApubObject, Bannable, Crud, Followable, Joinable, - ListingType, - SortType, }; -use lemmy_db_schema::{ - naive_now, - source::{comment::Comment, community::*, moderator::*, person::Person, post::Post, site::*}, - PersonId, +use lemmy_db_schema::source::{ + comment::Comment, + community::*, + moderator::*, + person::Person, + post::Post, + site::*, }; use lemmy_db_views::comment_view::CommentQueryBuilder; use lemmy_db_views_actor::{ - community_follower_view::CommunityFollowerView, community_moderator_view::CommunityModeratorView, - community_view::{CommunityQueryBuilder, CommunityView}, + community_view::CommunityView, person_view::PersonViewSafe, }; -use lemmy_utils::{ - apub::generate_actor_keypair, - location_info, - utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix}, - ApiError, - ConnectionId, - LemmyError, -}; -use lemmy_websocket::{ - messages::{GetCommunityUsersOnline, SendCommunityRoomMessage}, - LemmyContext, - UserOperation, -}; -use std::str::FromStr; - -#[async_trait::async_trait(?Send)] -impl Perform for GetCommunity { - type Response = GetCommunityResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetCommunity = &self; - let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?; - let person_id = local_user_view.map(|u| u.person.id); - - let community_id = match data.id { - Some(id) => id, - None => { - let name = data.name.to_owned().unwrap_or_else(|| "main".to_string()); - match blocking(context.pool(), move |conn| { - Community::read_from_name(conn, &name) - }) - .await? - { - Ok(community) => community, - Err(_e) => return Err(ApiError::err("couldnt_find_community").into()), - } - .id - } - }; - - let community_view = match blocking(context.pool(), move |conn| { - CommunityView::read(conn, community_id, person_id) - }) - .await? - { - Ok(community) => community, - Err(_e) => return Err(ApiError::err("couldnt_find_community").into()), - }; - - let moderators: Vec = match blocking(context.pool(), move |conn| { - CommunityModeratorView::for_community(conn, community_id) - }) - .await? - { - Ok(moderators) => moderators, - Err(_e) => return Err(ApiError::err("couldnt_find_community").into()), - }; - - let online = context - .chat_server() - .send(GetCommunityUsersOnline { community_id }) - .await - .unwrap_or(1); - - let res = GetCommunityResponse { - community_view, - moderators, - online, - }; - - // Return the jwt - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for CreateCommunity { - type Response = CommunityResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &CreateCommunity = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - check_slurs(&data.name)?; - check_slurs(&data.title)?; - check_slurs_opt(&data.description)?; - - if !is_valid_community_name(&data.name) { - return Err(ApiError::err("invalid_community_name").into()); - } - - // Double check for duplicate community actor_ids - let community_actor_id = generate_apub_endpoint(EndpointType::Community, &data.name)?; - let actor_id_cloned = community_actor_id.to_owned(); - let community_dupe = blocking(context.pool(), move |conn| { - Community::read_from_apub_id(conn, &actor_id_cloned) - }) - .await?; - if community_dupe.is_ok() { - return Err(ApiError::err("community_already_exists").into()); - } - - // Check to make sure the icon and banners are urls - let icon = diesel_option_overwrite_to_url(&data.icon)?; - let banner = diesel_option_overwrite_to_url(&data.banner)?; - - // When you create a community, make sure the user becomes a moderator and a follower - let keypair = generate_actor_keypair()?; - - let community_form = CommunityForm { - name: data.name.to_owned(), - title: data.title.to_owned(), - description: data.description.to_owned(), - icon, - banner, - creator_id: local_user_view.person.id, - removed: None, - deleted: None, - nsfw: data.nsfw, - updated: None, - actor_id: Some(community_actor_id.to_owned()), - local: true, - private_key: Some(keypair.private_key), - public_key: Some(keypair.public_key), - last_refreshed_at: None, - published: None, - followers_url: Some(generate_followers_url(&community_actor_id)?), - inbox_url: Some(generate_inbox_url(&community_actor_id)?), - shared_inbox_url: Some(Some(generate_shared_inbox_url(&community_actor_id)?)), - }; - - let inserted_community = match blocking(context.pool(), move |conn| { - Community::create(conn, &community_form) - }) - .await? - { - Ok(community) => community, - Err(_e) => return Err(ApiError::err("community_already_exists").into()), - }; - - // The community creator becomes a moderator - let community_moderator_form = CommunityModeratorForm { - community_id: inserted_community.id, - person_id: local_user_view.person.id, - }; - - let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); - if blocking(context.pool(), join).await?.is_err() { - return Err(ApiError::err("community_moderator_already_exists").into()); - } - - // Follow your own community - let community_follower_form = CommunityFollowerForm { - community_id: inserted_community.id, - person_id: local_user_view.person.id, - pending: false, - }; - - let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); - if blocking(context.pool(), follow).await?.is_err() { - return Err(ApiError::err("community_follower_already_exists").into()); - } - - let person_id = local_user_view.person.id; - let community_view = blocking(context.pool(), move |conn| { - CommunityView::read(conn, inserted_community.id, Some(person_id)) - }) - .await??; - - Ok(CommunityResponse { community_view }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for EditCommunity { - type Response = CommunityResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &EditCommunity = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - check_slurs(&data.title)?; - check_slurs_opt(&data.description)?; - - // Verify its a mod (only mods can edit it) - let community_id = data.community_id; - let mods: Vec = blocking(context.pool(), move |conn| { - CommunityModeratorView::for_community(conn, community_id) - .map(|v| v.into_iter().map(|m| m.moderator.id).collect()) - }) - .await??; - if !mods.contains(&local_user_view.person.id) { - return Err(ApiError::err("not_a_moderator").into()); - } - - let community_id = data.community_id; - let read_community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - - let icon = diesel_option_overwrite_to_url(&data.icon)?; - let banner = diesel_option_overwrite_to_url(&data.banner)?; - - let community_form = CommunityForm { - name: read_community.name, - title: data.title.to_owned(), - description: data.description.to_owned(), - icon, - banner, - creator_id: read_community.creator_id, - removed: Some(read_community.removed), - deleted: Some(read_community.deleted), - nsfw: data.nsfw, - updated: Some(naive_now()), - actor_id: Some(read_community.actor_id), - local: read_community.local, - private_key: read_community.private_key, - public_key: read_community.public_key, - last_refreshed_at: None, - published: None, - followers_url: None, - inbox_url: None, - shared_inbox_url: None, - }; - - let community_id = data.community_id; - match blocking(context.pool(), move |conn| { - Community::update(conn, community_id, &community_form) - }) - .await? - { - Ok(community) => community, - Err(_e) => return Err(ApiError::err("couldnt_update_community").into()), - }; - - // TODO there needs to be some kind of an apub update - // process for communities and users - - 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??; - - let res = CommunityResponse { community_view }; - - send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity); - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for DeleteCommunity { - type Response = CommunityResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &DeleteCommunity = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - // Verify its the creator (only a creator can delete the community) - let community_id = data.community_id; - let read_community = blocking(context.pool(), move |conn| { - Community::read(conn, community_id) - }) - .await??; - if read_community.creator_id != local_user_view.person.id { - return Err(ApiError::err("no_community_edit_allowed").into()); - } - - // Do the delete - let community_id = data.community_id; - let deleted = data.deleted; - let updated_community = match blocking(context.pool(), move |conn| { - Community::update_deleted(conn, community_id, deleted) - }) - .await? - { - Ok(community) => community, - Err(_e) => return Err(ApiError::err("couldnt_update_community").into()), - }; - - // Send apub messages - if deleted { - updated_community.send_delete(context).await?; - } else { - updated_community.send_undo_delete(context).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??; - - let res = CommunityResponse { community_view }; - - send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity); - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for RemoveCommunity { - type Response = CommunityResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &RemoveCommunity = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - // Verify its an admin (only an admin can remove a community) - is_admin(&local_user_view)?; - - // Do the remove - let community_id = data.community_id; - let removed = data.removed; - let updated_community = match blocking(context.pool(), move |conn| { - Community::update_removed(conn, community_id, removed) - }) - .await? - { - Ok(community) => community, - Err(_e) => return Err(ApiError::err("couldnt_update_community").into()), - }; - - // Mod tables - let expires = match data.expires { - Some(time) => Some(naive_from_unix(time)), - None => None, - }; - let form = ModRemoveCommunityForm { - mod_person_id: local_user_view.person.id, - community_id: data.community_id, - removed: Some(removed), - reason: data.reason.to_owned(), - expires, - }; - blocking(context.pool(), move |conn| { - ModRemoveCommunity::create(conn, &form) - }) - .await??; - - // Apub messages - if removed { - updated_community.send_remove(context).await?; - } else { - updated_community.send_undo_remove(context).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??; - - let res = CommunityResponse { community_view }; - - send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity); - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for ListCommunities { - type Response = ListCommunitiesResponse; - - 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, context.pool()).await?; - - let person_id = match &local_user_view { - Some(uv) => Some(uv.person.id), - None => None, - }; - - // Don't show NSFW by default - let show_nsfw = match &local_user_view { - Some(uv) => uv.local_user.show_nsfw, - None => false, - }; - - let type_ = ListingType::from_str(&data.type_)?; - let sort = SortType::from_str(&data.sort)?; - - let page = data.page; - let limit = data.limit; - let communities = blocking(context.pool(), move |conn| { - CommunityQueryBuilder::create(conn) - .listing_type(&type_) - .sort(&sort) - .show_nsfw(show_nsfw) - .my_person_id(person_id) - .page(page) - .limit(limit) - .list() - }) - .await??; - - // Return the jwt - Ok(ListCommunitiesResponse { communities }) - } -} +use lemmy_utils::{location_info, utils::naive_from_unix, ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation}; #[async_trait::async_trait(?Send)] impl Perform for FollowCommunity { @@ -553,33 +111,6 @@ impl Perform for FollowCommunity { } } -#[async_trait::async_trait(?Send)] -impl Perform for GetFollowedCommunities { - type Response = GetFollowedCommunitiesResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetFollowedCommunities = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - let person_id = local_user_view.person.id; - let communities = match blocking(context.pool(), move |conn| { - CommunityFollowerView::for_person(conn, person_id) - }) - .await? - { - Ok(communities) => communities, - _ => return Err(ApiError::err("system_err_login").into()), - }; - - // Return the jwt - Ok(GetFollowedCommunitiesResponse { communities }) - } -} - #[async_trait::async_trait(?Send)] impl Perform for BanFromCommunity { type Response = BanFromCommunityResponse; @@ -907,21 +438,3 @@ impl Perform for TransferCommunity { }) } } - -fn send_community_websocket( - res: &CommunityResponse, - context: &Data, - websocket_id: Option, - op: UserOperation, -) { - // Strip out the person id and subscribed when sending to others - let mut res_sent = res.clone(); - res_sent.community_view.subscribed = false; - - context.chat_server().do_send(SendCommunityRoomMessage { - op, - response: res_sent, - community_id: res.community_view.community.id, - websocket_id, - }); -} diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 529a13cfd..277498f1f 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -1,56 +1,20 @@ use actix_web::{web, web::Data}; -use lemmy_api_structs::{ - blocking, - comment::*, - community::*, - person::*, - post::*, - site::*, - websocket::*, -}; -use lemmy_db_queries::{ - source::{ - community::{CommunityModerator_, Community_}, - site::Site_, - }, - Crud, - DbPool, -}; -use lemmy_db_schema::{ - source::{ - community::{Community, CommunityModerator}, - post::Post, - site::Site, - }, - CommunityId, - LocalUserId, - PersonId, - PostId, -}; -use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView}; -use lemmy_db_views_actor::{ - community_person_ban_view::CommunityPersonBanView, - community_view::CommunityView, -}; -use lemmy_utils::{ - claims::Claims, - settings::structs::Settings, - ApiError, - ConnectionId, - LemmyError, -}; +use lemmy_api_common::{comment::*, community::*, person::*, post::*, site::*, websocket::*}; +use lemmy_utils::{ConnectionId, LemmyError}; use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation}; use serde::Deserialize; use std::{env, process::Command}; -use url::Url; -pub mod comment; -pub mod community; -pub mod local_user; -pub mod post; +mod comment; +mod comment_report; +mod community; +mod local_user; +mod post; +mod post_report; +mod private_message; pub mod routes; -pub mod site; -pub mod websocket; +mod site; +mod websocket; #[async_trait::async_trait(?Send)] pub trait Perform { @@ -63,221 +27,35 @@ pub trait Perform { ) -> Result; } -pub(crate) async fn is_mod_or_admin( - pool: &DbPool, - person_id: PersonId, - community_id: CommunityId, -) -> Result<(), LemmyError> { - let is_mod_or_admin = blocking(pool, move |conn| { - CommunityView::is_mod_or_admin(conn, person_id, community_id) - }) - .await?; - if !is_mod_or_admin { - return Err(ApiError::err("not_a_mod_or_admin").into()); - } - Ok(()) -} - -pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> { - if !local_user_view.local_user.admin { - return Err(ApiError::err("not_an_admin").into()); - } - Ok(()) -} - -pub(crate) async fn get_post(post_id: PostId, pool: &DbPool) -> Result { - match blocking(pool, move |conn| Post::read(conn, post_id)).await? { - Ok(post) => Ok(post), - Err(_e) => Err(ApiError::err("couldnt_find_post").into()), - } -} - -pub(crate) async fn get_local_user_view_from_jwt( - jwt: &str, - pool: &DbPool, -) -> Result { - let claims = match Claims::decode(&jwt) { - Ok(claims) => claims.claims, - Err(_e) => return Err(ApiError::err("not_logged_in").into()), - }; - let local_user_id = LocalUserId(claims.sub); - let local_user_view = - blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??; - // Check for a site ban - if local_user_view.person.banned { - return Err(ApiError::err("site_ban").into()); - } - - check_validator_time(&local_user_view.local_user.validator_time, &claims)?; - - Ok(local_user_view) -} - -/// Checks if user's token was issued before user's password reset. -pub(crate) fn check_validator_time( - validator_time: &chrono::NaiveDateTime, - claims: &Claims, -) -> Result<(), LemmyError> { - let user_validation_time = validator_time.timestamp(); - if user_validation_time > claims.iat { - Err(ApiError::err("not_logged_in").into()) - } else { - Ok(()) - } -} - -pub(crate) async fn get_local_user_view_from_jwt_opt( - jwt: &Option, - pool: &DbPool, -) -> Result, LemmyError> { - match jwt { - Some(jwt) => Ok(Some(get_local_user_view_from_jwt(jwt, pool).await?)), - None => Ok(None), - } -} - -pub(crate) async fn get_local_user_settings_view_from_jwt( - jwt: &str, - pool: &DbPool, -) -> Result { - let claims = match Claims::decode(&jwt) { - Ok(claims) => claims.claims, - Err(_e) => return Err(ApiError::err("not_logged_in").into()), - }; - let local_user_id = LocalUserId(claims.sub); - let local_user_view = blocking(pool, move |conn| { - LocalUserSettingsView::read(conn, local_user_id) - }) - .await??; - // Check for a site ban - if local_user_view.person.banned { - return Err(ApiError::err("site_ban").into()); - } - - check_validator_time(&local_user_view.local_user.validator_time, &claims)?; - - Ok(local_user_view) -} - -pub(crate) async fn get_local_user_settings_view_from_jwt_opt( - jwt: &Option, - pool: &DbPool, -) -> Result, LemmyError> { - match jwt { - Some(jwt) => Ok(Some( - get_local_user_settings_view_from_jwt(jwt, pool).await?, - )), - None => Ok(None), - } -} - -pub(crate) async fn check_community_ban( - person_id: PersonId, - community_id: CommunityId, - pool: &DbPool, -) -> Result<(), LemmyError> { - let is_banned = - move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok(); - if blocking(pool, is_banned).await? { - Err(ApiError::err("community_ban").into()) - } else { - Ok(()) - } -} - -pub(crate) async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> { - if score == -1 { - let site = blocking(pool, move |conn| Site::read_simple(conn)).await??; - if !site.enable_downvotes { - return Err(ApiError::err("downvotes_disabled").into()); - } - } - Ok(()) -} - -/// Returns a list of communities that the user moderates -/// or if a community_id is supplied validates the user is a moderator -/// of that community and returns the community id in a vec -/// -/// * `person_id` - the person id of the moderator -/// * `community_id` - optional community id to check for moderator privileges -/// * `pool` - the diesel db pool -pub(crate) async fn collect_moderated_communities( - person_id: PersonId, - community_id: Option, - pool: &DbPool, -) -> Result, LemmyError> { - if let Some(community_id) = community_id { - // if the user provides a community_id, just check for mod/admin privileges - is_mod_or_admin(pool, person_id, community_id).await?; - Ok(vec![community_id]) - } else { - let ids = blocking(pool, move |conn: &'_ _| { - CommunityModerator::get_person_moderated_communities(conn, person_id) - }) - .await??; - Ok(ids) - } -} - -pub(crate) async fn build_federated_instances( - pool: &DbPool, -) -> Result, LemmyError> { - if Settings::get().federation().enabled { - let distinct_communities = blocking(pool, move |conn| { - Community::distinct_federated_communities(conn) - }) - .await??; - - let allowed = Settings::get().get_allowed_instances(); - let blocked = Settings::get().get_blocked_instances(); - - let mut linked = distinct_communities - .iter() - .map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string())) - .collect::, LemmyError>>()?; - - if let Some(allowed) = allowed.as_ref() { - linked.extend_from_slice(allowed); - } - - if let Some(blocked) = blocked.as_ref() { - linked.retain(|a| !blocked.contains(a) && !a.eq(&Settings::get().hostname())); - } - - // Sort and remove dupes - linked.sort_unstable(); - linked.dedup(); - - Ok(Some(FederatedInstances { - linked, - allowed, - blocked, - })) - } else { - Ok(None) - } -} - pub async fn match_websocket_operation( context: LemmyContext, id: ConnectionId, op: UserOperation, data: &str, ) -> Result { + //TODO: handle commented out actions in crud crate + match op { // User ops - UserOperation::Login => do_websocket_operation::(context, id, op, data).await, - UserOperation::Register => do_websocket_operation::(context, id, op, data).await, + UserOperation::Login => { + //do_websocket_operation::(context, id, op, data).await + todo!() + } + UserOperation::Register => { + //do_websocket_operation::(context, id, op, data).await + todo!() + } UserOperation::GetCaptcha => do_websocket_operation::(context, id, op, data).await, UserOperation::GetPersonDetails => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::GetReplies => do_websocket_operation::(context, id, op, data).await, UserOperation::AddAdmin => do_websocket_operation::(context, id, op, data).await, UserOperation::BanPerson => do_websocket_operation::(context, id, op, data).await, UserOperation::GetPersonMentions => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::MarkPersonMentionAsRead => { do_websocket_operation::(context, id, op, data).await @@ -286,7 +64,8 @@ pub async fn match_websocket_operation( do_websocket_operation::(context, id, op, data).await } UserOperation::DeleteAccount => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::PasswordReset => { do_websocket_operation::(context, id, op, data).await @@ -309,26 +88,39 @@ pub async fn match_websocket_operation( // Private Message ops UserOperation::CreatePrivateMessage => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::EditPrivateMessage => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::DeletePrivateMessage => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::MarkPrivateMessageAsRead => { do_websocket_operation::(context, id, op, data).await } UserOperation::GetPrivateMessages => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } // Site ops UserOperation::GetModlog => do_websocket_operation::(context, id, op, data).await, - UserOperation::CreateSite => do_websocket_operation::(context, id, op, data).await, - UserOperation::EditSite => do_websocket_operation::(context, id, op, data).await, - UserOperation::GetSite => do_websocket_operation::(context, id, op, data).await, + UserOperation::CreateSite => { + //do_websocket_operation::(context, id, op, data).await + todo!() + } + UserOperation::EditSite => { + //do_websocket_operation::(context, id, op, data).await + todo!() + } + UserOperation::GetSite => { + //do_websocket_operation::(context, id, op, data).await + todo!() + } UserOperation::GetSiteConfig => { do_websocket_operation::(context, id, op, data).await } @@ -345,22 +137,28 @@ pub async fn match_websocket_operation( // Community ops UserOperation::GetCommunity => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::ListCommunities => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::CreateCommunity => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::EditCommunity => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::DeleteCommunity => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::RemoveCommunity => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::FollowCommunity => { do_websocket_operation::(context, id, op, data).await @@ -376,12 +174,30 @@ pub async fn match_websocket_operation( } // Post ops - UserOperation::CreatePost => do_websocket_operation::(context, id, op, data).await, - UserOperation::GetPost => do_websocket_operation::(context, id, op, data).await, - UserOperation::GetPosts => do_websocket_operation::(context, id, op, data).await, - UserOperation::EditPost => do_websocket_operation::(context, id, op, data).await, - UserOperation::DeletePost => do_websocket_operation::(context, id, op, data).await, - UserOperation::RemovePost => do_websocket_operation::(context, id, op, data).await, + UserOperation::CreatePost => { + //do_websocket_operation::(context, id, op, data).await + todo!() + } + UserOperation::GetPost => { + //do_websocket_operation::(context, id, op, data).await + todo!() + } + UserOperation::GetPosts => { + //do_websocket_operation::(context, id, op, data).await + todo!() + } + UserOperation::EditPost => { + //do_websocket_operation::(context, id, op, data).await + todo!() + } + UserOperation::DeletePost => { + //do_websocket_operation::(context, id, op, data).await + todo!() + } + UserOperation::RemovePost => { + //do_websocket_operation::(context, id, op, data).await + todo!() + } UserOperation::LockPost => do_websocket_operation::(context, id, op, data).await, UserOperation::StickyPost => do_websocket_operation::(context, id, op, data).await, UserOperation::CreatePostLike => { @@ -400,16 +216,20 @@ pub async fn match_websocket_operation( // Comment ops UserOperation::CreateComment => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::EditComment => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::DeleteComment => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::RemoveComment => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::MarkCommentAsRead => { do_websocket_operation::(context, id, op, data).await @@ -418,7 +238,8 @@ pub async fn match_websocket_operation( do_websocket_operation::(context, id, op, data).await } UserOperation::GetComments => { - do_websocket_operation::(context, id, op, data).await + //do_websocket_operation::(context, id, op, data).await + todo!() } UserOperation::CreateCommentLike => { do_websocket_operation::(context, id, op, data).await @@ -503,18 +324,10 @@ pub(crate) fn espeak_wav_base64(text: &str) -> Result { Ok(base64) } -/// Checks the password length -pub(crate) fn password_length_check(pass: &str) -> Result<(), LemmyError> { - if pass.len() > 60 { - Err(ApiError::err("invalid_password").into()) - } else { - Ok(()) - } -} - #[cfg(test)] mod tests { use crate::{captcha_espeak_wav_base64, check_validator_time}; + use lemmy_api_common::check_validator_time; use lemmy_db_queries::{establish_unpooled_connection, source::local_user::LocalUser_, Crud}; use lemmy_db_schema::source::{ local_user::{LocalUser, LocalUserForm}, diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs index 6aa8b26c9..aacb7d0bf 100644 --- a/crates/api/src/local_user.rs +++ b/crates/api/src/local_user.rs @@ -1,25 +1,17 @@ -use crate::{ - captcha_espeak_wav_base64, - collect_moderated_communities, - get_local_user_view_from_jwt, - get_local_user_view_from_jwt_opt, - is_admin, - password_length_check, - Perform, -}; +use crate::{captcha_espeak_wav_base64, Perform}; use actix_web::web::Data; use anyhow::Context; use bcrypt::verify; use captcha::{gen, Difficulty}; use chrono::Duration; -use lemmy_api_structs::{blocking, person::*, send_email_to_user}; -use lemmy_apub::{ - generate_apub_endpoint, - generate_followers_url, - generate_inbox_url, - generate_shared_inbox_url, - ApubObjectType, - EndpointType, +use lemmy_api_common::{ + blocking, + collect_moderated_communities, + community::{GetFollowedCommunities, GetFollowedCommunitiesResponse}, + get_local_user_view_from_jwt, + is_admin, + password_length_check, + person::*, }; use lemmy_db_queries::{ diesel_option_overwrite, @@ -33,12 +25,8 @@ use lemmy_db_queries::{ person_mention::PersonMention_, post::Post_, private_message::PrivateMessage_, - site::Site_, }, Crud, - Followable, - Joinable, - ListingType, SortType, }; use lemmy_db_schema::{ @@ -52,45 +40,33 @@ use lemmy_db_schema::{ person::*, person_mention::*, post::Post, - private_message::*, + private_message::PrivateMessage, site::*, }, - CommunityId, }; use lemmy_db_views::{ comment_report_view::CommentReportView, comment_view::CommentQueryBuilder, local_user_view::LocalUserView, post_report_view::PostReportView, - post_view::PostQueryBuilder, - private_message_view::{PrivateMessageQueryBuilder, PrivateMessageView}, }; use lemmy_db_views_actor::{ community_follower_view::CommunityFollowerView, - community_moderator_view::CommunityModeratorView, person_mention_view::{PersonMentionQueryBuilder, PersonMentionView}, person_view::PersonViewSafe, }; use lemmy_utils::{ - apub::generate_actor_keypair, claims::Claims, email::send_email, location_info, settings::structs::Settings, - utils::{ - check_slurs, - generate_random_string, - is_valid_preferred_username, - is_valid_username, - naive_from_unix, - remove_slurs, - }, + utils::{generate_random_string, is_valid_preferred_username, naive_from_unix}, ApiError, ConnectionId, LemmyError, }; use lemmy_websocket::{ - messages::{CaptchaItem, CheckCaptcha, SendAllMessage, SendUserRoomMessage}, + messages::{CaptchaItem, SendAllMessage, SendUserRoomMessage}, LemmyContext, UserOperation, }; @@ -135,212 +111,6 @@ impl Perform for Login { } } -#[async_trait::async_trait(?Send)] -impl Perform for Register { - type Response = LoginResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &Register = &self; - - // Make sure site has open registration - if let Ok(site) = blocking(context.pool(), move |conn| Site::read_simple(conn)).await? { - if !site.open_registration { - return Err(ApiError::err("registration_closed").into()); - } - } - - password_length_check(&data.password)?; - - // Make sure passwords match - if data.password != data.password_verify { - return Err(ApiError::err("passwords_dont_match").into()); - } - - // Check if there are admins. False if admins exist - let no_admins = blocking(context.pool(), move |conn| { - PersonViewSafe::admins(conn).map(|a| a.is_empty()) - }) - .await??; - - // If its not the admin, check the captcha - if !no_admins && Settings::get().captcha().enabled { - let check = context - .chat_server() - .send(CheckCaptcha { - uuid: data - .captcha_uuid - .to_owned() - .unwrap_or_else(|| "".to_string()), - answer: data - .captcha_answer - .to_owned() - .unwrap_or_else(|| "".to_string()), - }) - .await?; - if !check { - return Err(ApiError::err("captcha_incorrect").into()); - } - } - - check_slurs(&data.username)?; - - let actor_keypair = generate_actor_keypair()?; - if !is_valid_username(&data.username) { - return Err(ApiError::err("invalid_username").into()); - } - let actor_id = generate_apub_endpoint(EndpointType::Person, &data.username)?; - - // We have to create both a person, and local_user - - // Register the new person - let person_form = PersonForm { - name: data.username.to_owned(), - avatar: None, - banner: None, - preferred_username: None, - published: None, - updated: None, - banned: None, - deleted: None, - actor_id: Some(actor_id.clone()), - bio: None, - local: Some(true), - private_key: Some(Some(actor_keypair.private_key)), - public_key: Some(Some(actor_keypair.public_key)), - last_refreshed_at: None, - inbox_url: Some(generate_inbox_url(&actor_id)?), - shared_inbox_url: Some(Some(generate_shared_inbox_url(&actor_id)?)), - }; - - // insert the person - let inserted_person = match blocking(context.pool(), move |conn| { - Person::create(conn, &person_form) - }) - .await? - { - Ok(u) => u, - Err(_) => { - return Err(ApiError::err("user_already_exists").into()); - } - }; - - // Create the local user - let local_user_form = LocalUserForm { - person_id: inserted_person.id, - email: Some(data.email.to_owned()), - matrix_user_id: None, - password_encrypted: data.password.to_owned(), - admin: Some(no_admins), - show_nsfw: Some(data.show_nsfw), - theme: Some("browser".into()), - default_sort_type: Some(SortType::Active as i16), - default_listing_type: Some(ListingType::Subscribed as i16), - lang: Some("browser".into()), - show_avatars: Some(true), - send_notifications_to_email: Some(false), - }; - - let inserted_local_user = match blocking(context.pool(), move |conn| { - LocalUser::register(conn, &local_user_form) - }) - .await? - { - Ok(lu) => lu, - 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" - }; - - // If the local user creation errored, then delete that person - blocking(context.pool(), move |conn| { - Person::delete(&conn, inserted_person.id) - }) - .await??; - - return Err(ApiError::err(err_type).into()); - } - }; - - let main_community_keypair = generate_actor_keypair()?; - - // Create the main community if it doesn't exist - let main_community = match blocking(context.pool(), move |conn| { - Community::read(conn, CommunityId(2)) - }) - .await? - { - Ok(c) => c, - Err(_e) => { - let default_community_name = "main"; - let actor_id = generate_apub_endpoint(EndpointType::Community, default_community_name)?; - let community_form = CommunityForm { - name: default_community_name.to_string(), - title: "The Default Community".to_string(), - description: Some("The Default Community".to_string()), - nsfw: false, - creator_id: inserted_person.id, - removed: None, - deleted: None, - updated: None, - actor_id: Some(actor_id.to_owned()), - local: true, - private_key: Some(main_community_keypair.private_key), - public_key: Some(main_community_keypair.public_key), - last_refreshed_at: None, - published: None, - icon: None, - banner: None, - followers_url: Some(generate_followers_url(&actor_id)?), - inbox_url: Some(generate_inbox_url(&actor_id)?), - shared_inbox_url: Some(Some(generate_shared_inbox_url(&actor_id)?)), - }; - blocking(context.pool(), move |conn| { - Community::create(conn, &community_form) - }) - .await?? - } - }; - - // Sign them up for main community no matter what - let community_follower_form = CommunityFollowerForm { - community_id: main_community.id, - person_id: inserted_person.id, - pending: false, - }; - - let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); - if blocking(context.pool(), follow).await?.is_err() { - return Err(ApiError::err("community_follower_already_exists").into()); - }; - - // If its an admin, add them as a mod and follower to main - if no_admins { - let community_moderator_form = CommunityModeratorForm { - community_id: main_community.id, - person_id: inserted_person.id, - }; - - let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); - if blocking(context.pool(), join).await?.is_err() { - return Err(ApiError::err("community_moderator_already_exists").into()); - } - } - - // Return the jwt - Ok(LoginResponse { - jwt: Claims::jwt(inserted_local_user.id.0)?, - }) - } -} - #[async_trait::async_trait(?Send)] impl Perform for GetCaptcha { type Response = GetCaptchaResponse; @@ -531,114 +301,6 @@ impl Perform for SaveUserSettings { } } -#[async_trait::async_trait(?Send)] -impl Perform for GetPersonDetails { - type Response = GetPersonDetailsResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetPersonDetails = &self; - let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?; - - let show_nsfw = match &local_user_view { - Some(uv) => uv.local_user.show_nsfw, - None => false, - }; - - let sort = SortType::from_str(&data.sort)?; - - let username = data - .username - .to_owned() - .unwrap_or_else(|| "admin".to_string()); - let person_details_id = match data.person_id { - Some(id) => id, - None => { - let person = blocking(context.pool(), move |conn| { - Person::find_by_name(conn, &username) - }) - .await?; - match person { - Ok(p) => p.id, - Err(_e) => return Err(ApiError::err("couldnt_find_that_username_or_email").into()), - } - } - }; - - let person_id = local_user_view.map(|uv| uv.person.id); - - // You don't need to return settings for the user, since this comes back with GetSite - // `my_user` - let person_view = blocking(context.pool(), move |conn| { - PersonViewSafe::read(conn, person_details_id) - }) - .await??; - - let page = data.page; - let limit = data.limit; - let saved_only = data.saved_only; - let community_id = data.community_id; - - let (posts, comments) = blocking(context.pool(), move |conn| { - let mut posts_query = PostQueryBuilder::create(conn) - .sort(&sort) - .show_nsfw(show_nsfw) - .saved_only(saved_only) - .community_id(community_id) - .my_person_id(person_id) - .page(page) - .limit(limit); - - let mut comments_query = CommentQueryBuilder::create(conn) - .my_person_id(person_id) - .sort(&sort) - .saved_only(saved_only) - .community_id(community_id) - .page(page) - .limit(limit); - - // If its saved only, you don't care what creator it was - // Or, if its not saved, then you only want it for that specific creator - if !saved_only { - posts_query = posts_query.creator_id(person_details_id); - comments_query = comments_query.creator_id(person_details_id); - } - - let posts = posts_query.list()?; - let comments = comments_query.list()?; - - Ok((posts, comments)) as Result<_, LemmyError> - }) - .await??; - - let mut follows = vec![]; - if let Some(pid) = person_id { - if pid == person_details_id { - follows = blocking(context.pool(), move |conn| { - CommunityFollowerView::for_person(conn, person_details_id) - }) - .await??; - } - }; - let moderates = blocking(context.pool(), move |conn| { - CommunityModeratorView::for_person(conn, person_details_id) - }) - .await??; - - // Return the jwt - Ok(GetPersonDetailsResponse { - person_view, - follows, - moderates, - comments, - posts, - }) - } -} - #[async_trait::async_trait(?Send)] impl Perform for AddAdmin { type Response = AddAdminResponse; @@ -947,52 +609,6 @@ impl Perform for MarkAllAsRead { } } -#[async_trait::async_trait(?Send)] -impl Perform for DeleteAccount { - type Response = LoginResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &DeleteAccount = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - // Verify the password - let valid: bool = verify( - &data.password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); - if !valid { - return Err(ApiError::err("password_incorrect").into()); - } - - // Comments - let person_id = local_user_view.person.id; - let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, person_id); - if blocking(context.pool(), permadelete).await?.is_err() { - return Err(ApiError::err("couldnt_update_comment").into()); - } - - // Posts - let permadelete = move |conn: &'_ _| Post::permadelete_for_creator(conn, person_id); - if blocking(context.pool(), permadelete).await?.is_err() { - return Err(ApiError::err("couldnt_update_post").into()); - } - - blocking(context.pool(), move |conn| { - Person::delete_account(conn, person_id) - }) - .await??; - - Ok(LoginResponse { - jwt: data.auth.to_owned(), - }) - } -} - #[async_trait::async_trait(?Send)] impl Perform for PasswordReset { type Response = PasswordResetResponse; @@ -1084,344 +700,6 @@ impl Perform for PasswordChange { } } -#[async_trait::async_trait(?Send)] -impl Perform for CreatePrivateMessage { - type Response = PrivateMessageResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &CreatePrivateMessage = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - let content_slurs_removed = remove_slurs(&data.content.to_owned()); - - let private_message_form = PrivateMessageForm { - content: content_slurs_removed.to_owned(), - creator_id: local_user_view.person.id, - recipient_id: data.recipient_id, - deleted: None, - read: None, - updated: None, - ap_id: None, - local: true, - published: None, - }; - - let inserted_private_message = match blocking(context.pool(), move |conn| { - PrivateMessage::create(conn, &private_message_form) - }) - .await? - { - Ok(private_message) => private_message, - Err(_e) => { - return Err(ApiError::err("couldnt_create_private_message").into()); - } - }; - - let inserted_private_message_id = inserted_private_message.id; - let updated_private_message = match blocking( - context.pool(), - move |conn| -> Result { - let apub_id = generate_apub_endpoint( - EndpointType::PrivateMessage, - &inserted_private_message_id.to_string(), - )?; - Ok(PrivateMessage::update_ap_id( - &conn, - inserted_private_message_id, - apub_id, - )?) - }, - ) - .await? - { - Ok(private_message) => private_message, - Err(_e) => return Err(ApiError::err("couldnt_create_private_message").into()), - }; - - updated_private_message - .send_create(&local_user_view.person, context) - .await?; - - let private_message_view = blocking(context.pool(), move |conn| { - PrivateMessageView::read(conn, inserted_private_message.id) - }) - .await??; - - let res = PrivateMessageResponse { - private_message_view, - }; - - // Send notifications to the local recipient, if one exists - let recipient_id = data.recipient_id; - if let Ok(local_recipient) = blocking(context.pool(), move |conn| { - LocalUserView::read_person(conn, recipient_id) - }) - .await? - { - send_email_to_user( - &local_recipient, - "Private Message from", - "Private Message", - &content_slurs_removed, - ); - - let local_recipient_id = local_recipient.local_user.id; - context.chat_server().do_send(SendUserRoomMessage { - op: UserOperation::CreatePrivateMessage, - response: res.clone(), - local_recipient_id, - websocket_id, - }); - } - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for EditPrivateMessage { - type Response = PrivateMessageResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &EditPrivateMessage = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - // Checking permissions - let private_message_id = data.private_message_id; - let orig_private_message = blocking(context.pool(), move |conn| { - PrivateMessage::read(conn, private_message_id) - }) - .await??; - if local_user_view.person.id != orig_private_message.creator_id { - return Err(ApiError::err("no_private_message_edit_allowed").into()); - } - - // Doing the update - let content_slurs_removed = remove_slurs(&data.content); - let private_message_id = data.private_message_id; - let updated_private_message = match blocking(context.pool(), move |conn| { - PrivateMessage::update_content(conn, private_message_id, &content_slurs_removed) - }) - .await? - { - Ok(private_message) => private_message, - Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()), - }; - - // Send the apub update - updated_private_message - .send_update(&local_user_view.person, context) - .await?; - - let private_message_id = data.private_message_id; - let private_message_view = blocking(context.pool(), move |conn| { - PrivateMessageView::read(conn, private_message_id) - }) - .await??; - - let res = PrivateMessageResponse { - private_message_view, - }; - - // Send notifications to the local recipient, if one exists - let recipient_id = orig_private_message.recipient_id; - if let Ok(local_recipient) = blocking(context.pool(), move |conn| { - LocalUserView::read_person(conn, recipient_id) - }) - .await? - { - let local_recipient_id = local_recipient.local_user.id; - context.chat_server().do_send(SendUserRoomMessage { - op: UserOperation::EditPrivateMessage, - response: res.clone(), - local_recipient_id, - websocket_id, - }); - } - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for DeletePrivateMessage { - type Response = PrivateMessageResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &DeletePrivateMessage = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - // Checking permissions - let private_message_id = data.private_message_id; - let orig_private_message = blocking(context.pool(), move |conn| { - PrivateMessage::read(conn, private_message_id) - }) - .await??; - if local_user_view.person.id != orig_private_message.creator_id { - return Err(ApiError::err("no_private_message_edit_allowed").into()); - } - - // Doing the update - let private_message_id = data.private_message_id; - let deleted = data.deleted; - let updated_private_message = match blocking(context.pool(), move |conn| { - PrivateMessage::update_deleted(conn, private_message_id, deleted) - }) - .await? - { - Ok(private_message) => private_message, - Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()), - }; - - // Send the apub update - if data.deleted { - updated_private_message - .send_delete(&local_user_view.person, context) - .await?; - } else { - updated_private_message - .send_undo_delete(&local_user_view.person, context) - .await?; - } - - let private_message_id = data.private_message_id; - let private_message_view = blocking(context.pool(), move |conn| { - PrivateMessageView::read(conn, private_message_id) - }) - .await??; - - let res = PrivateMessageResponse { - private_message_view, - }; - - // Send notifications to the local recipient, if one exists - let recipient_id = orig_private_message.recipient_id; - if let Ok(local_recipient) = blocking(context.pool(), move |conn| { - LocalUserView::read_person(conn, recipient_id) - }) - .await? - { - let local_recipient_id = local_recipient.local_user.id; - context.chat_server().do_send(SendUserRoomMessage { - op: UserOperation::DeletePrivateMessage, - response: res.clone(), - local_recipient_id, - websocket_id, - }); - } - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for MarkPrivateMessageAsRead { - type Response = PrivateMessageResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &MarkPrivateMessageAsRead = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - // Checking permissions - let private_message_id = data.private_message_id; - let orig_private_message = blocking(context.pool(), move |conn| { - PrivateMessage::read(conn, private_message_id) - }) - .await??; - if local_user_view.person.id != orig_private_message.recipient_id { - return Err(ApiError::err("couldnt_update_private_message").into()); - } - - // Doing the update - let private_message_id = data.private_message_id; - let read = data.read; - match blocking(context.pool(), move |conn| { - PrivateMessage::update_read(conn, private_message_id, read) - }) - .await? - { - Ok(private_message) => private_message, - Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()), - }; - - // No need to send an apub update - let private_message_id = data.private_message_id; - let private_message_view = blocking(context.pool(), move |conn| { - PrivateMessageView::read(conn, private_message_id) - }) - .await??; - - let res = PrivateMessageResponse { - private_message_view, - }; - - // Send notifications to the local recipient, if one exists - let recipient_id = orig_private_message.recipient_id; - if let Ok(local_recipient) = blocking(context.pool(), move |conn| { - LocalUserView::read_person(conn, recipient_id) - }) - .await? - { - let local_recipient_id = local_recipient.local_user.id; - context.chat_server().do_send(SendUserRoomMessage { - op: UserOperation::MarkPrivateMessageAsRead, - response: res.clone(), - local_recipient_id, - websocket_id, - }); - } - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for GetPrivateMessages { - type Response = PrivateMessagesResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetPrivateMessages = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - let person_id = local_user_view.person.id; - - let page = data.page; - let limit = data.limit; - let unread_only = data.unread_only; - let messages = blocking(context.pool(), move |conn| { - PrivateMessageQueryBuilder::create(&conn, person_id) - .page(page) - .limit(limit) - .unread_only(unread_only) - .list() - }) - .await??; - - Ok(PrivateMessagesResponse { - private_messages: messages, - }) - } -} - #[async_trait::async_trait(?Send)] impl Perform for GetReportCount { type Response = GetReportCountResponse; @@ -1477,3 +755,30 @@ impl Perform for GetReportCount { Ok(res) } } + +#[async_trait::async_trait(?Send)] +impl Perform for GetFollowedCommunities { + type Response = GetFollowedCommunitiesResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &GetFollowedCommunities = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + let person_id = local_user_view.person.id; + let communities = match blocking(context.pool(), move |conn| { + CommunityFollowerView::for_person(conn, person_id) + }) + .await? + { + Ok(communities) => communities, + _ => return Err(ApiError::err("system_err_login").into()), + }; + + // Return the jwt + Ok(GetFollowedCommunitiesResponse { communities }) + } +} diff --git a/crates/api/src/post.rs b/crates/api/src/post.rs index bbc3e04bb..be39cf53c 100644 --- a/crates/api/src/post.rs +++ b/crates/api/src/post.rs @@ -1,289 +1,19 @@ -use crate::{ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, check_community_ban, check_downvotes_enabled, - collect_moderated_communities, get_local_user_view_from_jwt, - get_local_user_view_from_jwt_opt, is_mod_or_admin, - Perform, + post::*, }; -use actix_web::web::Data; -use lemmy_api_structs::{blocking, post::*}; -use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType}; -use lemmy_db_queries::{ - source::post::Post_, - Crud, - Likeable, - ListingType, - Reportable, - Saveable, - SortType, -}; -use lemmy_db_schema::{ - naive_now, - source::{ - moderator::*, - post::*, - post_report::{PostReport, PostReportForm}, - }, -}; -use lemmy_db_views::{ - comment_view::CommentQueryBuilder, - post_report_view::{PostReportQueryBuilder, PostReportView}, - post_view::{PostQueryBuilder, PostView}, -}; -use lemmy_db_views_actor::{ - community_moderator_view::CommunityModeratorView, - community_view::CommunityView, -}; -use lemmy_utils::{ - request::fetch_iframely_and_pictrs_data, - utils::{check_slurs, check_slurs_opt, is_valid_post_title}, - ApiError, - ConnectionId, - LemmyError, -}; -use lemmy_websocket::{ - messages::{GetPostUsersOnline, SendModRoomMessage, SendPost, SendUserRoomMessage}, - LemmyContext, - UserOperation, -}; -use std::str::FromStr; - -#[async_trait::async_trait(?Send)] -impl Perform for CreatePost { - type Response = PostResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &CreatePost = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - check_slurs(&data.name)?; - check_slurs_opt(&data.body)?; - - if !is_valid_post_title(&data.name) { - return Err(ApiError::err("invalid_post_title").into()); - } - - check_community_ban(local_user_view.person.id, data.community_id, context.pool()).await?; - - // Fetch Iframely and pictrs cached image - let data_url = data.url.as_ref(); - let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = - fetch_iframely_and_pictrs_data(context.client(), data_url).await; - - let post_form = PostForm { - name: data.name.trim().to_owned(), - url: data_url.map(|u| u.to_owned().into()), - body: data.body.to_owned(), - community_id: data.community_id, - creator_id: local_user_view.person.id, - removed: None, - deleted: None, - nsfw: data.nsfw, - locked: None, - stickied: None, - updated: None, - embed_title: iframely_title, - embed_description: iframely_description, - embed_html: iframely_html, - thumbnail_url: pictrs_thumbnail.map(|u| u.into()), - ap_id: None, - local: true, - published: None, - }; - - let inserted_post = - match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? { - Ok(post) => post, - Err(e) => { - let err_type = if e.to_string() == "value too long for type character varying(200)" { - "post_title_too_long" - } else { - "couldnt_create_post" - }; - - return Err(ApiError::err(err_type).into()); - } - }; - - let inserted_post_id = inserted_post.id; - let updated_post = match blocking(context.pool(), move |conn| -> Result { - let apub_id = generate_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string())?; - Ok(Post::update_ap_id(conn, inserted_post_id, apub_id)?) - }) - .await? - { - Ok(post) => post, - Err(_e) => return Err(ApiError::err("couldnt_create_post").into()), - }; - - updated_post - .send_create(&local_user_view.person, context) - .await?; - - // They like their own post by default - let like_form = PostLikeForm { - post_id: inserted_post.id, - person_id: local_user_view.person.id, - score: 1, - }; - - let like = move |conn: &'_ _| PostLike::like(conn, &like_form); - if blocking(context.pool(), like).await?.is_err() { - return Err(ApiError::err("couldnt_like_post").into()); - } - - updated_post - .send_like(&local_user_view.person, context) - .await?; - - // Refetch the view - let inserted_post_id = inserted_post.id; - let post_view = match blocking(context.pool(), move |conn| { - PostView::read(conn, inserted_post_id, Some(local_user_view.person.id)) - }) - .await? - { - Ok(post) => post, - Err(_e) => return Err(ApiError::err("couldnt_find_post").into()), - }; - - let res = PostResponse { post_view }; - - context.chat_server().do_send(SendPost { - op: UserOperation::CreatePost, - post: res.clone(), - websocket_id, - }); - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for GetPost { - type Response = GetPostResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &GetPost = &self; - let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?; - let person_id = local_user_view.map(|u| u.person.id); - - let id = data.id; - let post_view = match blocking(context.pool(), move |conn| { - PostView::read(conn, id, person_id) - }) - .await? - { - Ok(post) => post, - Err(_e) => return Err(ApiError::err("couldnt_find_post").into()), - }; - - let id = data.id; - let comments = blocking(context.pool(), move |conn| { - CommentQueryBuilder::create(conn) - .my_person_id(person_id) - .post_id(id) - .limit(9999) - .list() - }) - .await??; - - let community_id = post_view.community.id; - let moderators = blocking(context.pool(), move |conn| { - CommunityModeratorView::for_community(conn, community_id) - }) - .await??; - - // Necessary for the sidebar - let community_view = match blocking(context.pool(), move |conn| { - CommunityView::read(conn, community_id, person_id) - }) - .await? - { - Ok(community) => community, - Err(_e) => return Err(ApiError::err("couldnt_find_community").into()), - }; - - let online = context - .chat_server() - .send(GetPostUsersOnline { post_id: data.id }) - .await - .unwrap_or(1); - - // Return the jwt - Ok(GetPostResponse { - post_view, - community_view, - comments, - moderators, - online, - }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for GetPosts { - type Response = GetPostsResponse; - - 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, context.pool()).await?; - - let person_id = match &local_user_view { - Some(uv) => Some(uv.person.id), - None => None, - }; - - let show_nsfw = match &local_user_view { - Some(uv) => uv.local_user.show_nsfw, - None => false, - }; - - let type_ = ListingType::from_str(&data.type_)?; - let sort = SortType::from_str(&data.sort)?; - - let page = data.page; - let limit = data.limit; - let community_id = data.community_id; - let community_name = data.community_name.to_owned(); - let saved_only = data.saved_only; - - let posts = match blocking(context.pool(), move |conn| { - PostQueryBuilder::create(conn) - .listing_type(&type_) - .sort(&sort) - .show_nsfw(show_nsfw) - .community_id(community_id) - .community_name(community_name) - .saved_only(saved_only) - .my_person_id(person_id) - .page(page) - .limit(limit) - .list() - }) - .await? - { - Ok(posts) => posts, - Err(_e) => return Err(ApiError::err("couldnt_get_posts").into()), - }; - - Ok(GetPostsResponse { posts }) - } -} +use lemmy_apub::{ApubLikeableType, ApubObjectType}; +use lemmy_db_queries::{source::post::Post_, Crud, Likeable, Saveable}; +use lemmy_db_schema::source::{moderator::*, post::*}; +use lemmy_db_views::post_view::PostView; +use lemmy_utils::{ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation}; #[async_trait::async_trait(?Send)] impl Perform for CreatePostLike { @@ -362,253 +92,6 @@ impl Perform for CreatePostLike { } } -#[async_trait::async_trait(?Send)] -impl Perform for EditPost { - type Response = PostResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &EditPost = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - check_slurs(&data.name)?; - check_slurs_opt(&data.body)?; - - if !is_valid_post_title(&data.name) { - return Err(ApiError::err("invalid_post_title").into()); - } - - 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?; - - // Verify that only the creator can edit - if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) { - return Err(ApiError::err("no_post_edit_allowed").into()); - } - - // Fetch Iframely and Pictrs cached image - let data_url = data.url.as_ref(); - let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = - fetch_iframely_and_pictrs_data(context.client(), data_url).await; - - let post_form = PostForm { - name: data.name.trim().to_owned(), - url: data_url.map(|u| u.to_owned().into()), - body: data.body.to_owned(), - nsfw: data.nsfw, - creator_id: orig_post.creator_id.to_owned(), - community_id: orig_post.community_id, - removed: Some(orig_post.removed), - deleted: Some(orig_post.deleted), - locked: Some(orig_post.locked), - stickied: Some(orig_post.stickied), - updated: Some(naive_now()), - embed_title: iframely_title, - embed_description: iframely_description, - embed_html: iframely_html, - thumbnail_url: pictrs_thumbnail.map(|u| u.into()), - ap_id: Some(orig_post.ap_id), - local: orig_post.local, - published: None, - }; - - let post_id = data.post_id; - let res = blocking(context.pool(), move |conn| { - Post::update(conn, post_id, &post_form) - }) - .await?; - let updated_post: Post = match res { - Ok(post) => post, - Err(e) => { - let err_type = if e.to_string() == "value too long for type character varying(200)" { - "post_title_too_long" - } else { - "couldnt_update_post" - }; - - return Err(ApiError::err(err_type).into()); - } - }; - - // Send apub update - updated_post - .send_update(&local_user_view.person, context) - .await?; - - let post_id = data.post_id; - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, Some(local_user_view.person.id)) - }) - .await??; - - let res = PostResponse { post_view }; - - context.chat_server().do_send(SendPost { - op: UserOperation::EditPost, - post: res.clone(), - websocket_id, - }); - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for DeletePost { - type Response = PostResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &DeletePost = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).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?; - - // Verify that only the creator can delete - if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) { - return Err(ApiError::err("no_post_edit_allowed").into()); - } - - // Update the post - let post_id = data.post_id; - let deleted = data.deleted; - let updated_post = blocking(context.pool(), move |conn| { - Post::update_deleted(conn, post_id, deleted) - }) - .await??; - - // apub updates - if deleted { - updated_post - .send_delete(&local_user_view.person, context) - .await?; - } else { - updated_post - .send_undo_delete(&local_user_view.person, context) - .await?; - } - - // Refetch the post - let post_id = data.post_id; - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, post_id, Some(local_user_view.person.id)) - }) - .await??; - - let res = PostResponse { post_view }; - - context.chat_server().do_send(SendPost { - op: UserOperation::DeletePost, - post: res.clone(), - websocket_id, - }); - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for RemovePost { - type Response = PostResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &RemovePost = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).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?; - - // Verify that only the mods can remove - 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 removed = data.removed; - let updated_post = blocking(context.pool(), move |conn| { - Post::update_removed(conn, post_id, removed) - }) - .await??; - - // Mod tables - let form = ModRemovePostForm { - mod_person_id: local_user_view.person.id, - post_id: data.post_id, - removed: Some(removed), - reason: data.reason.to_owned(), - }; - blocking(context.pool(), move |conn| { - ModRemovePost::create(conn, &form) - }) - .await??; - - // apub updates - if removed { - updated_post - .send_remove(&local_user_view.person, context) - .await?; - } else { - updated_post - .send_undo_remove(&local_user_view.person, context) - .await?; - } - - // Refetch the 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??; - - let res = PostResponse { post_view }; - - context.chat_server().do_send(SendPost { - op: UserOperation::RemovePost, - post: res.clone(), - websocket_id, - }); - - Ok(res) - } -} - #[async_trait::async_trait(?Send)] impl Perform for LockPost { type Response = PostResponse; @@ -792,166 +275,3 @@ impl Perform for SavePost { Ok(PostResponse { post_view }) } } - -/// Creates a post report and notifies the moderators of the community -#[async_trait::async_trait(?Send)] -impl Perform for CreatePostReport { - type Response = CreatePostReportResponse; - - 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()).await?; - - // check size of report and check for whitespace - let reason = data.reason.trim(); - if reason.is_empty() { - return Err(ApiError::err("report_reason_required").into()); - } - if reason.chars().count() > 1000 { - return Err(ApiError::err("report_too_long").into()); - } - - 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 = match blocking(context.pool(), move |conn| { - PostReport::report(conn, &report_form) - }) - .await? - { - Ok(report) => report, - Err(_e) => return Err(ApiError::err("couldnt_create_report").into()), - }; - - let res = CreatePostReportResponse { success: true }; - - context.chat_server().do_send(SendUserRoomMessage { - op: UserOperation::CreatePostReport, - response: res.clone(), - local_recipient_id: local_user_view.local_user.id, - websocket_id, - }); - - context.chat_server().do_send(SendModRoomMessage { - op: UserOperation::CreatePostReport, - response: report, - community_id: post_view.community.id, - websocket_id, - }); - - 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 = ResolvePostReportResponse; - - 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()).await?; - - let report_id = data.report_id; - let report = blocking(context.pool(), move |conn| { - PostReportView::read(&conn, report_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) - } - }; - - let res = ResolvePostReportResponse { - report_id, - resolved: true, - }; - - if blocking(context.pool(), resolve_fun).await?.is_err() { - return Err(ApiError::err("couldnt_resolve_report").into()); - }; - - 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; - - 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()).await?; - - let person_id = local_user_view.person.id; - let community_id = data.community; - let community_ids = - collect_moderated_communities(person_id, community_id, context.pool()).await?; - - let page = data.page; - let limit = data.limit; - let posts = blocking(context.pool(), move |conn| { - PostReportQueryBuilder::create(conn) - .community_ids(community_ids) - .page(page) - .limit(limit) - .list() - }) - .await??; - - let res = ListPostReportsResponse { posts }; - - context.chat_server().do_send(SendUserRoomMessage { - op: UserOperation::ListPostReports, - response: res.clone(), - local_recipient_id: local_user_view.local_user.id, - websocket_id, - }); - - Ok(res) - } -} diff --git a/crates/api/src/post_report.rs b/crates/api/src/post_report.rs new file mode 100644 index 000000000..c81ee2aa2 --- /dev/null +++ b/crates/api/src/post_report.rs @@ -0,0 +1,192 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_community_ban, + collect_moderated_communities, + get_local_user_view_from_jwt, + is_mod_or_admin, + post::{ + CreatePostReport, + CreatePostReportResponse, + ListPostReports, + ListPostReportsResponse, + ResolvePostReport, + ResolvePostReportResponse, + }, +}; +use lemmy_db_queries::Reportable; +use lemmy_db_schema::source::post_report::{PostReport, PostReportForm}; +use lemmy_db_views::{ + post_report_view::{PostReportQueryBuilder, PostReportView}, + post_view::PostView, +}; +use lemmy_utils::{ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{ + messages::{SendModRoomMessage, SendUserRoomMessage}, + LemmyContext, + UserOperation, +}; + +/// Creates a post report and notifies the moderators of the community +#[async_trait::async_trait(?Send)] +impl Perform for CreatePostReport { + type Response = CreatePostReportResponse; + + 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()).await?; + + // check size of report and check for whitespace + let reason = data.reason.trim(); + if reason.is_empty() { + return Err(ApiError::err("report_reason_required").into()); + } + if reason.chars().count() > 1000 { + return Err(ApiError::err("report_too_long").into()); + } + + 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 = match blocking(context.pool(), move |conn| { + PostReport::report(conn, &report_form) + }) + .await? + { + Ok(report) => report, + Err(_e) => return Err(ApiError::err("couldnt_create_report").into()), + }; + + let res = CreatePostReportResponse { success: true }; + + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperation::CreatePostReport, + response: res.clone(), + local_recipient_id: local_user_view.local_user.id, + websocket_id, + }); + + context.chat_server().do_send(SendModRoomMessage { + op: UserOperation::CreatePostReport, + response: report, + community_id: post_view.community.id, + websocket_id, + }); + + 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 = ResolvePostReportResponse; + + 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()).await?; + + let report_id = data.report_id; + let report = blocking(context.pool(), move |conn| { + PostReportView::read(&conn, report_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) + } + }; + + let res = ResolvePostReportResponse { + report_id, + resolved: true, + }; + + if blocking(context.pool(), resolve_fun).await?.is_err() { + return Err(ApiError::err("couldnt_resolve_report").into()); + }; + + 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; + + 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()).await?; + + let person_id = local_user_view.person.id; + let community_id = data.community; + let community_ids = + collect_moderated_communities(person_id, community_id, context.pool()).await?; + + let page = data.page; + let limit = data.limit; + let posts = blocking(context.pool(), move |conn| { + PostReportQueryBuilder::create(conn) + .community_ids(community_ids) + .page(page) + .limit(limit) + .list() + }) + .await??; + + let res = ListPostReportsResponse { posts }; + + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperation::ListPostReports, + response: res.clone(), + local_recipient_id: local_user_view.local_user.id, + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api/src/private_message.rs b/crates/api/src/private_message.rs new file mode 100644 index 000000000..5420084d6 --- /dev/null +++ b/crates/api/src/private_message.rs @@ -0,0 +1,77 @@ +use crate::Perform; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + person::{MarkPrivateMessageAsRead, PrivateMessageResponse}, +}; +use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud}; +use lemmy_db_schema::source::private_message::PrivateMessage; +use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView}; +use lemmy_utils::{ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl Perform for MarkPrivateMessageAsRead { + type Response = PrivateMessageResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &MarkPrivateMessageAsRead = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + // Checking permissions + let private_message_id = data.private_message_id; + let orig_private_message = blocking(context.pool(), move |conn| { + PrivateMessage::read(conn, private_message_id) + }) + .await??; + if local_user_view.person.id != orig_private_message.recipient_id { + return Err(ApiError::err("couldnt_update_private_message").into()); + } + + // Doing the update + let private_message_id = data.private_message_id; + let read = data.read; + match blocking(context.pool(), move |conn| { + PrivateMessage::update_read(conn, private_message_id, read) + }) + .await? + { + Ok(private_message) => private_message, + Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()), + }; + + // No need to send an apub update + let private_message_id = data.private_message_id; + let private_message_view = blocking(context.pool(), move |conn| { + PrivateMessageView::read(conn, private_message_id) + }) + .await??; + + let res = PrivateMessageResponse { + private_message_view, + }; + + // Send notifications to the local recipient, if one exists + let recipient_id = orig_private_message.recipient_id; + if let Ok(local_recipient) = blocking(context.pool(), move |conn| { + LocalUserView::read_person(conn, recipient_id) + }) + .await? + { + let local_recipient_id = local_recipient.local_user.id; + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperation::MarkPrivateMessageAsRead, + response: res.clone(), + local_recipient_id, + websocket_id, + }); + } + + Ok(res) + } +} diff --git a/crates/api/src/routes.rs b/crates/api/src/routes.rs index 6fc46ca4c..cdc9e736b 100644 --- a/crates/api/src/routes.rs +++ b/crates/api/src/routes.rs @@ -1,6 +1,6 @@ use crate::Perform; use actix_web::{error::ErrorBadRequest, *}; -use lemmy_api_structs::{comment::*, community::*, person::*, post::*, site::*, websocket::*}; +use lemmy_api_common::{comment::*, community::*, person::*, post::*, site::*, websocket::*}; use lemmy_utils::rate_limit::RateLimit; use lemmy_websocket::{routes::chat_route, LemmyContext}; use serde::Deserialize; @@ -14,10 +14,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .service( web::scope("/site") .wrap(rate_limit.message()) - .route("", web::get().to(route_get::)) // Admin Actions - .route("", web::post().to(route_post::)) - .route("", web::put().to(route_post::)) .route("/transfer", web::post().to(route_post::)) .route("/config", web::get().to(route_get::)) .route("/config", web::put().to(route_post::)), @@ -33,22 +30,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .route(web::get().to(route_get::)), ) // Community - .service( - web::resource("/community") - .guard(guard::Post()) - .wrap(rate_limit.register()) - .route(web::post().to(route_post::)), - ) .service( web::scope("/community") .wrap(rate_limit.message()) - .route("", web::get().to(route_get::)) - .route("", web::put().to(route_post::)) - .route("/list", web::get().to(route_get::)) .route("/follow", web::post().to(route_post::)) - .route("/delete", web::post().to(route_post::)) - // Mod Actions - .route("/remove", web::post().to(route_post::)) .route("/transfer", web::post().to(route_post::)) .route("/ban_user", web::post().to(route_post::)) .route("/mod", web::post().to(route_post::)) @@ -56,23 +41,11 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .route("/mod/join", web::post().to(route_post::)), ) // Post - .service( - // Handle POST to /post separately to add the post() rate limitter - web::resource("/post") - .guard(guard::Post()) - .wrap(rate_limit.post()) - .route(web::post().to(route_post::)), - ) .service( web::scope("/post") .wrap(rate_limit.message()) - .route("", web::get().to(route_get::)) - .route("", web::put().to(route_post::)) - .route("/delete", web::post().to(route_post::)) - .route("/remove", web::post().to(route_post::)) .route("/lock", web::post().to(route_post::)) .route("/sticky", web::post().to(route_post::)) - .route("/list", web::get().to(route_get::)) .route("/like", web::post().to(route_post::)) .route("/save", web::put().to(route_post::)) .route("/join", web::post().to(route_post::)) @@ -87,17 +60,12 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .service( web::scope("/comment") .wrap(rate_limit.message()) - .route("", web::post().to(route_post::)) - .route("", web::put().to(route_post::)) - .route("/delete", web::post().to(route_post::)) - .route("/remove", web::post().to(route_post::)) .route( "/mark_as_read", web::post().to(route_post::), ) .route("/like", web::post().to(route_post::)) .route("/save", web::put().to(route_post::)) - .route("/list", web::get().to(route_get::)) .route("/report", web::post().to(route_post::)) .route( "/report/resolve", @@ -112,32 +80,15 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { .service( web::scope("/private_message") .wrap(rate_limit.message()) - .route("/list", web::get().to(route_get::)) - .route("", web::post().to(route_post::)) - .route("", web::put().to(route_post::)) - .route( - "/delete", - web::post().to(route_post::), - ) .route( "/mark_as_read", web::post().to(route_post::), ), ) - // User - .service( - // Account action, I don't like that it's in /user maybe /accounts - // Handle /user/register separately to add the register() rate limitter - web::resource("/user/register") - .guard(guard::Post()) - .wrap(rate_limit.register()) - .route(web::post().to(route_post::)), - ) // User actions .service( web::scope("/user") .wrap(rate_limit.message()) - .route("", web::get().to(route_get::)) .route("/mention", web::get().to(route_get::)) .route( "/mention/mark_as_read", @@ -154,10 +105,6 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { // Account actions. I don't like that they're in /user maybe /accounts .route("/login", web::post().to(route_post::)) .route("/get_captcha", web::get().to(route_get::)) - .route( - "/delete_account", - web::post().to(route_post::), - ) .route( "/password_reset", web::post().to(route_post::), diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs index 4d561ab21..91402da1a 100644 --- a/crates/api/src/site.rs +++ b/crates/api/src/site.rs @@ -1,30 +1,18 @@ -use crate::{ +use crate::Perform; +use actix_web::web::Data; +use anyhow::Context; +use lemmy_api_common::{ + blocking, build_federated_instances, get_local_user_settings_view_from_jwt, - get_local_user_settings_view_from_jwt_opt, get_local_user_view_from_jwt, get_local_user_view_from_jwt_opt, is_admin, - Perform, + site::*, }; -use actix_web::web::Data; -use anyhow::Context; -use lemmy_api_structs::{blocking, person::Register, site::*}; use lemmy_apub::fetcher::search::search_by_apub_id; -use lemmy_db_queries::{ - diesel_option_overwrite_to_url, - source::site::Site_, - Crud, - SearchType, - SortType, -}; -use lemmy_db_schema::{ - naive_now, - source::{ - moderator::*, - site::{Site, *}, - }, -}; +use lemmy_db_queries::{source::site::Site_, Crud, SearchType, SortType}; +use lemmy_db_schema::source::{moderator::*, site::Site}; use lemmy_db_views::{ comment_view::CommentQueryBuilder, post_view::PostQueryBuilder, @@ -48,18 +36,13 @@ use lemmy_db_views_moderator::{ use lemmy_utils::{ location_info, settings::structs::Settings, - utils::{check_slurs, check_slurs_opt}, version, ApiError, ConnectionId, LemmyError, }; -use lemmy_websocket::{ - messages::{GetUsersOnline, SendAllMessage}, - LemmyContext, - UserOperation, -}; -use log::{debug, info}; +use lemmy_websocket::LemmyContext; +use log::debug; use std::str::FromStr; #[async_trait::async_trait(?Send)] @@ -136,189 +119,6 @@ impl Perform for GetModlog { } } -#[async_trait::async_trait(?Send)] -impl Perform for CreateSite { - type Response = SiteResponse; - - async fn perform( - &self, - context: &Data, - _websocket_id: Option, - ) -> Result { - let data: &CreateSite = &self; - - let read_site = move |conn: &'_ _| Site::read_simple(conn); - if blocking(context.pool(), read_site).await?.is_ok() { - return Err(ApiError::err("site_already_exists").into()); - }; - - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - check_slurs(&data.name)?; - check_slurs_opt(&data.description)?; - - // Make sure user is an admin - is_admin(&local_user_view)?; - - let site_form = SiteForm { - name: data.name.to_owned(), - description: data.description.to_owned(), - icon: Some(data.icon.to_owned().map(|url| url.into())), - banner: Some(data.banner.to_owned().map(|url| url.into())), - creator_id: local_user_view.person.id, - enable_downvotes: data.enable_downvotes, - open_registration: data.open_registration, - enable_nsfw: data.enable_nsfw, - updated: None, - }; - - let create_site = move |conn: &'_ _| Site::create(conn, &site_form); - if blocking(context.pool(), create_site).await?.is_err() { - return Err(ApiError::err("site_already_exists").into()); - } - - let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??; - - Ok(SiteResponse { site_view }) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for EditSite { - type Response = SiteResponse; - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &EditSite = &self; - let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; - - check_slurs(&data.name)?; - check_slurs_opt(&data.description)?; - - // Make sure user is an admin - is_admin(&local_user_view)?; - - let found_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??; - - let icon = diesel_option_overwrite_to_url(&data.icon)?; - let banner = diesel_option_overwrite_to_url(&data.banner)?; - - let site_form = SiteForm { - name: data.name.to_owned(), - description: data.description.to_owned(), - icon, - banner, - creator_id: found_site.creator_id, - updated: Some(naive_now()), - enable_downvotes: data.enable_downvotes, - open_registration: data.open_registration, - enable_nsfw: data.enable_nsfw, - }; - - let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form); - if blocking(context.pool(), update_site).await?.is_err() { - return Err(ApiError::err("couldnt_update_site").into()); - } - - let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??; - - let res = SiteResponse { site_view }; - - context.chat_server().do_send(SendAllMessage { - op: UserOperation::EditSite, - response: res.clone(), - websocket_id, - }); - - Ok(res) - } -} - -#[async_trait::async_trait(?Send)] -impl Perform for GetSite { - type Response = GetSiteResponse; - - async fn perform( - &self, - context: &Data, - websocket_id: Option, - ) -> Result { - let data: &GetSite = &self; - - let site_view = match blocking(context.pool(), move |conn| SiteView::read(conn)).await? { - Ok(site_view) => Some(site_view), - // If the site isn't created yet, check the setup - Err(_) => { - if let Some(setup) = Settings::get().setup().as_ref() { - let register = Register { - username: setup.admin_username.to_owned(), - email: setup.admin_email.to_owned(), - password: setup.admin_password.to_owned(), - password_verify: setup.admin_password.to_owned(), - show_nsfw: true, - captcha_uuid: None, - captcha_answer: None, - }; - let login_response = register.perform(context, websocket_id).await?; - info!("Admin {} created", setup.admin_username); - - let create_site = CreateSite { - name: setup.site_name.to_owned(), - description: None, - icon: None, - banner: None, - enable_downvotes: true, - open_registration: true, - enable_nsfw: true, - auth: login_response.jwt, - }; - create_site.perform(context, websocket_id).await?; - info!("Site {} created", setup.site_name); - Some(blocking(context.pool(), move |conn| SiteView::read(conn)).await??) - } else { - None - } - } - }; - - let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??; - - // Make sure the site creator is the top admin - if let Some(site_view) = site_view.to_owned() { - let site_creator_id = site_view.creator.id; - // TODO investigate why this is sometimes coming back null - // Maybe user_.admin isn't being set to true? - if let Some(creator_index) = admins.iter().position(|r| r.person.id == site_creator_id) { - let creator_person = admins.remove(creator_index); - admins.insert(0, creator_person); - } - } - - let banned = blocking(context.pool(), move |conn| PersonViewSafe::banned(conn)).await??; - - let online = context - .chat_server() - .send(GetUsersOnline) - .await - .unwrap_or(1); - - let my_user = get_local_user_settings_view_from_jwt_opt(&data.auth, context.pool()).await?; - let federated_instances = build_federated_instances(context.pool()).await?; - - Ok(GetSiteResponse { - site_view, - admins, - banned, - online, - version: version::VERSION.to_string(), - my_user, - federated_instances, - }) - } -} - #[async_trait::async_trait(?Send)] impl Perform for Search { type Response = SearchResponse; diff --git a/crates/api/src/websocket.rs b/crates/api/src/websocket.rs index ae5ba8940..683b4c4d4 100644 --- a/crates/api/src/websocket.rs +++ b/crates/api/src/websocket.rs @@ -1,6 +1,6 @@ -use crate::{get_local_user_view_from_jwt, Perform}; +use crate::Perform; use actix_web::web::Data; -use lemmy_api_structs::websocket::*; +use lemmy_api_common::{get_local_user_view_from_jwt, websocket::*}; use lemmy_utils::{ConnectionId, LemmyError}; use lemmy_websocket::{ messages::{JoinCommunityRoom, JoinModRoom, JoinPostRoom, JoinUserRoom}, diff --git a/crates/api_structs/Cargo.toml b/crates/api_common/Cargo.toml similarity index 92% rename from crates/api_structs/Cargo.toml rename to crates/api_common/Cargo.toml index 242383e6f..f870a2668 100644 --- a/crates/api_structs/Cargo.toml +++ b/crates/api_common/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "lemmy_api_structs" +name = "lemmy_api_common" version = "0.1.0" edition = "2018" [lib] -name = "lemmy_api_structs" +name = "lemmy_api_common" path = "src/lib.rs" doctest = false diff --git a/crates/api_structs/src/comment.rs b/crates/api_common/src/comment.rs similarity index 100% rename from crates/api_structs/src/comment.rs rename to crates/api_common/src/comment.rs diff --git a/crates/api_structs/src/community.rs b/crates/api_common/src/community.rs similarity index 100% rename from crates/api_structs/src/community.rs rename to crates/api_common/src/community.rs diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs new file mode 100644 index 000000000..2337f10ba --- /dev/null +++ b/crates/api_common/src/lib.rs @@ -0,0 +1,420 @@ +pub mod comment; +pub mod community; +pub mod person; +pub mod post; +pub mod site; +pub mod websocket; + +use crate::site::FederatedInstances; +use diesel::PgConnection; +use lemmy_db_queries::{ + source::{ + community::{CommunityModerator_, Community_}, + site::Site_, + }, + Crud, + DbPool, +}; +use lemmy_db_schema::{ + source::{ + comment::Comment, + community::{Community, CommunityModerator}, + person::Person, + person_mention::{PersonMention, PersonMentionForm}, + post::Post, + site::Site, + }, + CommunityId, + LocalUserId, + PersonId, + PostId, +}; +use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView}; +use lemmy_db_views_actor::{ + community_person_ban_view::CommunityPersonBanView, + community_view::CommunityView, +}; +use lemmy_utils::{ + claims::Claims, + email::send_email, + settings::structs::Settings, + utils::MentionData, + ApiError, + LemmyError, +}; +use log::error; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Serialize, Deserialize, Debug)] +pub struct WebFingerLink { + pub rel: Option, + #[serde(rename(serialize = "type", deserialize = "type"))] + pub type_: Option, + pub href: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub template: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct WebFingerResponse { + pub subject: String, + pub aliases: Vec, + pub links: Vec, +} + +pub async fn blocking(pool: &DbPool, f: F) -> Result +where + F: FnOnce(&diesel::PgConnection) -> T + Send + 'static, + T: Send + 'static, +{ + let pool = pool.clone(); + let res = actix_web::web::block(move || { + let conn = pool.get()?; + let res = (f)(&conn); + Ok(res) as Result<_, LemmyError> + }) + .await?; + + Ok(res) +} + +pub async fn send_local_notifs( + mentions: Vec, + comment: Comment, + person: Person, + post: Post, + pool: &DbPool, + do_send_email: bool, +) -> Result, LemmyError> { + let ids = blocking(pool, move |conn| { + do_send_local_notifs(conn, &mentions, &comment, &person, &post, do_send_email) + }) + .await?; + + Ok(ids) +} + +fn do_send_local_notifs( + conn: &PgConnection, + mentions: &[MentionData], + comment: &Comment, + person: &Person, + post: &Post, + do_send_email: bool, +) -> Vec { + let mut recipient_ids = Vec::new(); + + // Send the local mentions + for mention in mentions + .iter() + .filter(|m| m.is_local() && m.name.ne(&person.name)) + .collect::>() + { + if let Ok(mention_user_view) = LocalUserView::read_from_name(&conn, &mention.name) { + // TODO + // At some point, make it so you can't tag the parent creator either + // This can cause two notifications, one for reply and the other for mention + recipient_ids.push(mention_user_view.local_user.id); + + let user_mention_form = PersonMentionForm { + recipient_id: mention_user_view.person.id, + comment_id: comment.id, + read: None, + }; + + // Allow this to fail softly, since comment edits might re-update or replace it + // Let the uniqueness handle this fail + PersonMention::create(&conn, &user_mention_form).ok(); + + // Send an email to those local users that have notifications on + if do_send_email { + send_email_to_user( + &mention_user_view, + "Mentioned by", + "Person Mention", + &comment.content, + ) + } + } + } + + // Send notifs to the parent commenter / poster + match comment.parent_id { + Some(parent_id) => { + if let Ok(parent_comment) = Comment::read(&conn, parent_id) { + // Don't send a notif to yourself + if parent_comment.creator_id != person.id { + // Get the parent commenter local_user + if let Ok(parent_user_view) = LocalUserView::read_person(&conn, parent_comment.creator_id) + { + recipient_ids.push(parent_user_view.local_user.id); + + if do_send_email { + send_email_to_user( + &parent_user_view, + "Reply from", + "Comment Reply", + &comment.content, + ) + } + } + } + } + } + // Its a post + None => { + if post.creator_id != person.id { + if let Ok(parent_user_view) = LocalUserView::read_person(&conn, post.creator_id) { + recipient_ids.push(parent_user_view.local_user.id); + + if do_send_email { + send_email_to_user( + &parent_user_view, + "Reply from", + "Post Reply", + &comment.content, + ) + } + } + } + } + }; + recipient_ids +} + +pub fn send_email_to_user( + local_user_view: &LocalUserView, + subject_text: &str, + body_text: &str, + comment_content: &str, +) { + if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email { + return; + } + + if let Some(user_email) = &local_user_view.local_user.email { + let subject = &format!( + "{} - {} {}", + subject_text, + Settings::get().hostname(), + local_user_view.person.name, + ); + let html = &format!( + "

{}


{} - {}

inbox", + body_text, + local_user_view.person.name, + comment_content, + Settings::get().get_protocol_and_hostname() + ); + match send_email(subject, &user_email, &local_user_view.person.name, html) { + Ok(_o) => _o, + Err(e) => error!("{}", e), + }; + } +} + +pub async fn is_mod_or_admin( + pool: &DbPool, + person_id: PersonId, + community_id: CommunityId, +) -> Result<(), LemmyError> { + let is_mod_or_admin = blocking(pool, move |conn| { + CommunityView::is_mod_or_admin(conn, person_id, community_id) + }) + .await?; + if !is_mod_or_admin { + return Err(ApiError::err("not_a_mod_or_admin").into()); + } + Ok(()) +} + +pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> { + if !local_user_view.local_user.admin { + return Err(ApiError::err("not_an_admin").into()); + } + Ok(()) +} + +pub async fn get_post(post_id: PostId, pool: &DbPool) -> Result { + match blocking(pool, move |conn| Post::read(conn, post_id)).await? { + Ok(post) => Ok(post), + Err(_e) => Err(ApiError::err("couldnt_find_post").into()), + } +} + +pub async fn get_local_user_view_from_jwt( + jwt: &str, + pool: &DbPool, +) -> Result { + let claims = match Claims::decode(&jwt) { + Ok(claims) => claims.claims, + Err(_e) => return Err(ApiError::err("not_logged_in").into()), + }; + let local_user_id = LocalUserId(claims.sub); + let local_user_view = + blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??; + // Check for a site ban + if local_user_view.person.banned { + return Err(ApiError::err("site_ban").into()); + } + + check_validator_time(&local_user_view.local_user.validator_time, &claims)?; + + Ok(local_user_view) +} + +/// Checks if user's token was issued before user's password reset. +pub fn check_validator_time( + validator_time: &chrono::NaiveDateTime, + claims: &Claims, +) -> Result<(), LemmyError> { + let user_validation_time = validator_time.timestamp(); + if user_validation_time > claims.iat { + Err(ApiError::err("not_logged_in").into()) + } else { + Ok(()) + } +} + +pub async fn get_local_user_view_from_jwt_opt( + jwt: &Option, + pool: &DbPool, +) -> Result, LemmyError> { + match jwt { + Some(jwt) => Ok(Some(get_local_user_view_from_jwt(jwt, pool).await?)), + None => Ok(None), + } +} + +pub async fn get_local_user_settings_view_from_jwt( + jwt: &str, + pool: &DbPool, +) -> Result { + let claims = match Claims::decode(&jwt) { + Ok(claims) => claims.claims, + Err(_e) => return Err(ApiError::err("not_logged_in").into()), + }; + let local_user_id = LocalUserId(claims.sub); + let local_user_view = blocking(pool, move |conn| { + LocalUserSettingsView::read(conn, local_user_id) + }) + .await??; + // Check for a site ban + if local_user_view.person.banned { + return Err(ApiError::err("site_ban").into()); + } + + check_validator_time(&local_user_view.local_user.validator_time, &claims)?; + + Ok(local_user_view) +} + +pub async fn get_local_user_settings_view_from_jwt_opt( + jwt: &Option, + pool: &DbPool, +) -> Result, LemmyError> { + match jwt { + Some(jwt) => Ok(Some( + get_local_user_settings_view_from_jwt(jwt, pool).await?, + )), + None => Ok(None), + } +} + +pub async fn check_community_ban( + person_id: PersonId, + community_id: CommunityId, + pool: &DbPool, +) -> Result<(), LemmyError> { + let is_banned = + move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + Err(ApiError::err("community_ban").into()) + } else { + Ok(()) + } +} + +pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> { + if score == -1 { + let site = blocking(pool, move |conn| Site::read_simple(conn)).await??; + if !site.enable_downvotes { + return Err(ApiError::err("downvotes_disabled").into()); + } + } + Ok(()) +} + +/// Returns a list of communities that the user moderates +/// or if a community_id is supplied validates the user is a moderator +/// of that community and returns the community id in a vec +/// +/// * `person_id` - the person id of the moderator +/// * `community_id` - optional community id to check for moderator privileges +/// * `pool` - the diesel db pool +pub async fn collect_moderated_communities( + person_id: PersonId, + community_id: Option, + pool: &DbPool, +) -> Result, LemmyError> { + if let Some(community_id) = community_id { + // if the user provides a community_id, just check for mod/admin privileges + is_mod_or_admin(pool, person_id, community_id).await?; + Ok(vec![community_id]) + } else { + let ids = blocking(pool, move |conn: &'_ _| { + CommunityModerator::get_person_moderated_communities(conn, person_id) + }) + .await??; + Ok(ids) + } +} + +pub async fn build_federated_instances( + pool: &DbPool, +) -> Result, LemmyError> { + if Settings::get().federation().enabled { + let distinct_communities = blocking(pool, move |conn| { + Community::distinct_federated_communities(conn) + }) + .await??; + + let allowed = Settings::get().get_allowed_instances(); + let blocked = Settings::get().get_blocked_instances(); + + let mut linked = distinct_communities + .iter() + .map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string())) + .collect::, LemmyError>>()?; + + if let Some(allowed) = allowed.as_ref() { + linked.extend_from_slice(allowed); + } + + if let Some(blocked) = blocked.as_ref() { + linked.retain(|a| !blocked.contains(a) && !a.eq(&Settings::get().hostname())); + } + + // Sort and remove dupes + linked.sort_unstable(); + linked.dedup(); + + Ok(Some(FederatedInstances { + linked, + allowed, + blocked, + })) + } else { + Ok(None) + } +} + +/// Checks the password length +pub fn password_length_check(pass: &str) -> Result<(), LemmyError> { + if pass.len() > 60 { + Err(ApiError::err("invalid_password").into()) + } else { + Ok(()) + } +} diff --git a/crates/api_structs/src/person.rs b/crates/api_common/src/person.rs similarity index 100% rename from crates/api_structs/src/person.rs rename to crates/api_common/src/person.rs diff --git a/crates/api_structs/src/post.rs b/crates/api_common/src/post.rs similarity index 100% rename from crates/api_structs/src/post.rs rename to crates/api_common/src/post.rs diff --git a/crates/api_structs/src/site.rs b/crates/api_common/src/site.rs similarity index 100% rename from crates/api_structs/src/site.rs rename to crates/api_common/src/site.rs diff --git a/crates/api_structs/src/websocket.rs b/crates/api_common/src/websocket.rs similarity index 100% rename from crates/api_structs/src/websocket.rs rename to crates/api_common/src/websocket.rs diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml new file mode 100644 index 000000000..f7fde4a68 --- /dev/null +++ b/crates/api_crud/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "lemmy_api_crud" +version = "0.1.0" +edition = "2018" + +[dependencies] +lemmy_apub = { path = "../apub" } +lemmy_utils = { path = "../utils" } +lemmy_db_queries = { path = "../db_queries" } +lemmy_db_schema = { path = "../db_schema" } +lemmy_db_views = { path = "../db_views" } +lemmy_db_views_moderator = { path = "../db_views_moderator" } +lemmy_db_views_actor = { path = "../db_views_actor" } +lemmy_api_common = { path = "../api_common" } +lemmy_websocket = { path = "../websocket" } +diesel = "1.4.5" +bcrypt = "0.9.0" +chrono = { version = "0.4.19", features = ["serde"] } +serde_json = { version = "1.0.61", features = ["preserve_order"] } +serde = { version = "1.0.123", features = ["derive"] } +actix = "0.10.0" +actix-web = { version = "3.3.2", default-features = false } +actix-rt = { version = "1.1.1", default-features = false } +awc = { version = "2.0.3", default-features = false } +log = "0.4.14" +rand = "0.8.3" +strum = "0.20.0" +strum_macros = "0.20.1" +lazy_static = "1.4.0" +url = { version = "2.2.1", features = ["serde"] } +openssl = "0.10.32" +http = "0.2.3" +http-signature-normalization-actix = { version = "0.4.1", default-features = false, features = ["sha-2"] } +base64 = "0.13.0" +tokio = "0.3.6" +futures = "0.3.12" +itertools = "0.10.0" +uuid = { version = "0.8.2", features = ["serde", "v4"] } +sha2 = "0.9.3" +async-trait = "0.1.42" +captcha = "0.0.8" +anyhow = "1.0.38" +thiserror = "1.0.23" +background-jobs = "0.8.0" +reqwest = { version = "0.10.10", features = ["json"] } diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs new file mode 100644 index 000000000..74ef27f15 --- /dev/null +++ b/crates/api_crud/src/comment/create.rs @@ -0,0 +1,170 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_community_ban, + comment::*, + get_local_user_view_from_jwt, + get_post, + send_local_notifs, +}; +use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType}; +use lemmy_db_queries::{source::comment::Comment_, Crud, Likeable}; +use lemmy_db_schema::source::comment::*; +use lemmy_db_views::comment_view::CommentView; +use lemmy_utils::{ + utils::{remove_slurs, scrape_text_for_mentions}, + ApiError, + ConnectionId, + LemmyError, +}; +use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for CreateComment { + type Response = CommentResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &CreateComment = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + let content_slurs_removed = remove_slurs(&data.content.to_owned()); + + // Check for a community ban + let post_id = data.post_id; + let post = get_post(post_id, context.pool()).await?; + + check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?; + + // Check if post is locked, no new comments + if post.locked { + return Err(ApiError::err("locked").into()); + } + + // If there's a parent_id, check to make sure that comment is in that post + if let Some(parent_id) = data.parent_id { + // Make sure the parent comment exists + let parent = + match blocking(context.pool(), move |conn| Comment::read(&conn, parent_id)).await? { + Ok(comment) => comment, + Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()), + }; + if parent.post_id != post_id { + return Err(ApiError::err("couldnt_create_comment").into()); + } + } + + let comment_form = CommentForm { + content: content_slurs_removed, + parent_id: data.parent_id.to_owned(), + post_id: data.post_id, + creator_id: local_user_view.person.id, + removed: None, + deleted: None, + read: None, + published: None, + updated: None, + ap_id: None, + local: true, + }; + + // Create the comment + let comment_form2 = comment_form.clone(); + let inserted_comment = match blocking(context.pool(), move |conn| { + Comment::create(&conn, &comment_form2) + }) + .await? + { + Ok(comment) => comment, + Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()), + }; + + // Necessary to update the ap_id + let inserted_comment_id = inserted_comment.id; + let updated_comment: Comment = + match blocking(context.pool(), move |conn| -> Result { + let apub_id = + generate_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string())?; + Ok(Comment::update_ap_id(&conn, inserted_comment_id, apub_id)?) + }) + .await? + { + Ok(comment) => comment, + Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()), + }; + + updated_comment + .send_create(&local_user_view.person, context) + .await?; + + // Scan the comment for user mentions, add those rows + let post_id = post.id; + let mentions = scrape_text_for_mentions(&comment_form.content); + let recipient_ids = send_local_notifs( + mentions, + updated_comment.clone(), + local_user_view.person.clone(), + post, + context.pool(), + true, + ) + .await?; + + // You like your own comment by default + let like_form = CommentLikeForm { + comment_id: inserted_comment.id, + post_id, + person_id: local_user_view.person.id, + score: 1, + }; + + let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form); + if blocking(context.pool(), like).await?.is_err() { + return Err(ApiError::err("couldnt_like_comment").into()); + } + + updated_comment + .send_like(&local_user_view.person, context) + .await?; + + let person_id = local_user_view.person.id; + let mut comment_view = blocking(context.pool(), move |conn| { + CommentView::read(&conn, inserted_comment.id, Some(person_id)) + }) + .await??; + + // If its a comment to yourself, mark it as read + let comment_id = comment_view.comment.id; + if local_user_view.person.id == comment_view.get_recipient_id() { + match blocking(context.pool(), move |conn| { + Comment::update_read(conn, comment_id, true) + }) + .await? + { + Ok(comment) => comment, + Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()), + }; + comment_view.comment.read = true; + } + + let mut res = CommentResponse { + comment_view, + recipient_ids, + form_id: data.form_id.to_owned(), + }; + + context.chat_server().do_send(SendComment { + op: UserOperation::CreateComment, + comment: res.clone(), + websocket_id, + }); + + res.recipient_ids = Vec::new(); // Necessary to avoid doubles + + Ok(res) + } +} diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs new file mode 100644 index 000000000..1980106b3 --- /dev/null +++ b/crates/api_crud/src/comment/delete.rs @@ -0,0 +1,210 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_community_ban, + comment::*, + get_local_user_view_from_jwt, + is_mod_or_admin, + send_local_notifs, +}; +use lemmy_apub::ApubObjectType; +use lemmy_db_queries::{source::comment::Comment_, Crud}; +use lemmy_db_schema::source::{comment::*, moderator::*}; +use lemmy_db_views::comment_view::CommentView; +use lemmy_utils::{ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for DeleteComment { + type Response = CommentResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &DeleteComment = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + let comment_id = data.comment_id; + let orig_comment = blocking(context.pool(), move |conn| { + CommentView::read(&conn, comment_id, None) + }) + .await??; + + check_community_ban( + local_user_view.person.id, + orig_comment.community.id, + context.pool(), + ) + .await?; + + // Verify that only the creator can delete + if local_user_view.person.id != orig_comment.creator.id { + return Err(ApiError::err("no_comment_edit_allowed").into()); + } + + // Do the delete + let deleted = data.deleted; + let updated_comment = match blocking(context.pool(), move |conn| { + Comment::update_deleted(conn, comment_id, deleted) + }) + .await? + { + Ok(comment) => comment, + Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()), + }; + + // Send the apub message + if deleted { + updated_comment + .send_delete(&local_user_view.person, context) + .await?; + } else { + updated_comment + .send_undo_delete(&local_user_view.person, context) + .await?; + } + + // 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??; + + // Build the recipients + let comment_view_2 = comment_view.clone(); + let mentions = vec![]; + let recipient_ids = send_local_notifs( + mentions, + updated_comment, + local_user_view.person.clone(), + comment_view_2.post, + context.pool(), + false, + ) + .await?; + + let res = CommentResponse { + comment_view, + recipient_ids, + form_id: None, // TODO a comment delete might clear forms? + }; + + context.chat_server().do_send(SendComment { + op: UserOperation::DeleteComment, + comment: res.clone(), + websocket_id, + }); + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl PerformCrud for RemoveComment { + type Response = CommentResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &RemoveComment = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + let comment_id = data.comment_id; + let orig_comment = blocking(context.pool(), move |conn| { + CommentView::read(&conn, comment_id, None) + }) + .await??; + + check_community_ban( + local_user_view.person.id, + orig_comment.community.id, + context.pool(), + ) + .await?; + + // Verify that only a mod or admin can remove + is_mod_or_admin( + context.pool(), + local_user_view.person.id, + orig_comment.community.id, + ) + .await?; + + // Do the remove + let removed = data.removed; + let updated_comment = match blocking(context.pool(), move |conn| { + Comment::update_removed(conn, comment_id, removed) + }) + .await? + { + Ok(comment) => comment, + Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()), + }; + + // Mod tables + let form = ModRemoveCommentForm { + mod_person_id: local_user_view.person.id, + comment_id: data.comment_id, + removed: Some(removed), + reason: data.reason.to_owned(), + }; + blocking(context.pool(), move |conn| { + ModRemoveComment::create(conn, &form) + }) + .await??; + + // Send the apub message + if removed { + updated_comment + .send_remove(&local_user_view.person, context) + .await?; + } else { + updated_comment + .send_undo_remove(&local_user_view.person, context) + .await?; + } + + // 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??; + + // Build the recipients + let comment_view_2 = comment_view.clone(); + + let mentions = vec![]; + let recipient_ids = send_local_notifs( + mentions, + updated_comment, + local_user_view.person.clone(), + comment_view_2.post, + context.pool(), + false, + ) + .await?; + + let res = CommentResponse { + comment_view, + recipient_ids, + form_id: None, // TODO maybe this might clear other forms + }; + + context.chat_server().do_send(SendComment { + op: UserOperation::RemoveComment, + comment: res.clone(), + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api_crud/src/comment/mod.rs b/crates/api_crud/src/comment/mod.rs new file mode 100644 index 000000000..716832376 --- /dev/null +++ b/crates/api_crud/src/comment/mod.rs @@ -0,0 +1,4 @@ +mod create; +mod delete; +mod read; +mod update; diff --git a/crates/api_crud/src/comment/read.rs b/crates/api_crud/src/comment/read.rs new file mode 100644 index 000000000..a7ad3aca2 --- /dev/null +++ b/crates/api_crud/src/comment/read.rs @@ -0,0 +1,51 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{blocking, comment::*, get_local_user_view_from_jwt_opt}; +use lemmy_db_queries::{ListingType, SortType}; +use lemmy_db_views::comment_view::CommentQueryBuilder; +use lemmy_utils::{ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; +use std::str::FromStr; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for GetComments { + type Response = GetCommentsResponse; + + 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, context.pool()).await?; + let person_id = local_user_view.map(|u| u.person.id); + + let type_ = ListingType::from_str(&data.type_)?; + let sort = SortType::from_str(&data.sort)?; + + let community_id = data.community_id; + let community_name = data.community_name.to_owned(); + let saved_only = data.saved_only; + let page = data.page; + let limit = data.limit; + let comments = blocking(context.pool(), move |conn| { + CommentQueryBuilder::create(conn) + .listing_type(type_) + .sort(&sort) + .saved_only(saved_only) + .community_id(community_id) + .community_name(community_name) + .my_person_id(person_id) + .page(page) + .limit(limit) + .list() + }) + .await?; + let comments = match comments { + Ok(comments) => comments, + Err(_) => return Err(ApiError::err("couldnt_get_comments").into()), + }; + + Ok(GetCommentsResponse { comments }) + } +} diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs new file mode 100644 index 000000000..46d99e936 --- /dev/null +++ b/crates/api_crud/src/comment/update.rs @@ -0,0 +1,103 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_community_ban, + comment::*, + get_local_user_view_from_jwt, + send_local_notifs, +}; +use lemmy_apub::ApubObjectType; +use lemmy_db_queries::source::comment::Comment_; +use lemmy_db_schema::source::comment::*; +use lemmy_db_views::comment_view::CommentView; +use lemmy_utils::{ + utils::{remove_slurs, scrape_text_for_mentions}, + ApiError, + ConnectionId, + LemmyError, +}; +use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for EditComment { + type Response = CommentResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &EditComment = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + let comment_id = data.comment_id; + let orig_comment = blocking(context.pool(), move |conn| { + CommentView::read(&conn, comment_id, None) + }) + .await??; + + check_community_ban( + local_user_view.person.id, + orig_comment.community.id, + context.pool(), + ) + .await?; + + // Verify that only the creator can edit + if local_user_view.person.id != orig_comment.creator.id { + return Err(ApiError::err("no_comment_edit_allowed").into()); + } + + // Do the update + let content_slurs_removed = remove_slurs(&data.content.to_owned()); + let comment_id = data.comment_id; + let updated_comment = match blocking(context.pool(), move |conn| { + Comment::update_content(conn, comment_id, &content_slurs_removed) + }) + .await? + { + Ok(comment) => comment, + Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()), + }; + + // Send the apub update + updated_comment + .send_update(&local_user_view.person, context) + .await?; + + // Do the mentions / recipients + let updated_comment_content = updated_comment.content.to_owned(); + let mentions = scrape_text_for_mentions(&updated_comment_content); + let recipient_ids = send_local_notifs( + mentions, + updated_comment, + local_user_view.person.clone(), + orig_comment.post, + context.pool(), + false, + ) + .await?; + + 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, + form_id: data.form_id.to_owned(), + }; + + context.chat_server().do_send(SendComment { + op: UserOperation::EditComment, + comment: res.clone(), + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api_crud/src/community/create.rs b/crates/api_crud/src/community/create.rs new file mode 100644 index 000000000..104975b70 --- /dev/null +++ b/crates/api_crud/src/community/create.rs @@ -0,0 +1,134 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + community::{CommunityResponse, CreateCommunity}, + get_local_user_view_from_jwt, +}; +use lemmy_apub::{ + generate_apub_endpoint, + generate_followers_url, + generate_inbox_url, + generate_shared_inbox_url, + EndpointType, +}; +use lemmy_db_queries::{diesel_option_overwrite_to_url, ApubObject, Crud, Followable, Joinable}; +use lemmy_db_schema::source::community::{ + Community, + CommunityFollower, + CommunityFollowerForm, + CommunityForm, + CommunityModerator, + CommunityModeratorForm, +}; +use lemmy_db_views_actor::community_view::CommunityView; +use lemmy_utils::{ + apub::generate_actor_keypair, + utils::{check_slurs, check_slurs_opt, is_valid_community_name}, + ApiError, + ConnectionId, + LemmyError, +}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for CreateCommunity { + type Response = CommunityResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &CreateCommunity = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + check_slurs(&data.name)?; + check_slurs(&data.title)?; + check_slurs_opt(&data.description)?; + + if !is_valid_community_name(&data.name) { + return Err(ApiError::err("invalid_community_name").into()); + } + + // Double check for duplicate community actor_ids + let community_actor_id = generate_apub_endpoint(EndpointType::Community, &data.name)?; + let actor_id_cloned = community_actor_id.to_owned(); + let community_dupe = blocking(context.pool(), move |conn| { + Community::read_from_apub_id(conn, &actor_id_cloned) + }) + .await?; + if community_dupe.is_ok() { + return Err(ApiError::err("community_already_exists").into()); + } + + // Check to make sure the icon and banners are urls + let icon = diesel_option_overwrite_to_url(&data.icon)?; + let banner = diesel_option_overwrite_to_url(&data.banner)?; + + // When you create a community, make sure the user becomes a moderator and a follower + let keypair = generate_actor_keypair()?; + + let community_form = CommunityForm { + name: data.name.to_owned(), + title: data.title.to_owned(), + description: data.description.to_owned(), + icon, + banner, + creator_id: local_user_view.person.id, + removed: None, + deleted: None, + nsfw: data.nsfw, + updated: None, + actor_id: Some(community_actor_id.to_owned()), + local: true, + private_key: Some(keypair.private_key), + public_key: Some(keypair.public_key), + last_refreshed_at: None, + published: None, + followers_url: Some(generate_followers_url(&community_actor_id)?), + inbox_url: Some(generate_inbox_url(&community_actor_id)?), + shared_inbox_url: Some(Some(generate_shared_inbox_url(&community_actor_id)?)), + }; + + let inserted_community = match blocking(context.pool(), move |conn| { + Community::create(conn, &community_form) + }) + .await? + { + Ok(community) => community, + Err(_e) => return Err(ApiError::err("community_already_exists").into()), + }; + + // The community creator becomes a moderator + let community_moderator_form = CommunityModeratorForm { + community_id: inserted_community.id, + person_id: local_user_view.person.id, + }; + + let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); + if blocking(context.pool(), join).await?.is_err() { + return Err(ApiError::err("community_moderator_already_exists").into()); + } + + // Follow your own community + let community_follower_form = CommunityFollowerForm { + community_id: inserted_community.id, + person_id: local_user_view.person.id, + pending: false, + }; + + let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); + if blocking(context.pool(), follow).await?.is_err() { + return Err(ApiError::err("community_follower_already_exists").into()); + } + + let person_id = local_user_view.person.id; + let community_view = blocking(context.pool(), move |conn| { + CommunityView::read(conn, inserted_community.id, Some(person_id)) + }) + .await??; + + Ok(CommunityResponse { community_view }) + } +} diff --git a/crates/api_crud/src/community/delete.rs b/crates/api_crud/src/community/delete.rs new file mode 100644 index 000000000..e59ccd6bc --- /dev/null +++ b/crates/api_crud/src/community/delete.rs @@ -0,0 +1,134 @@ +use crate::{community::send_community_websocket, PerformCrud}; +use actix_web::web::Data; +use lemmy_api_common::{blocking, community::*, get_local_user_view_from_jwt, is_admin}; +use lemmy_apub::CommunityType; +use lemmy_db_queries::{source::community::Community_, Crud}; +use lemmy_db_schema::source::{ + community::*, + moderator::{ModRemoveCommunity, ModRemoveCommunityForm}, +}; +use lemmy_db_views_actor::community_view::CommunityView; +use lemmy_utils::{utils::naive_from_unix, ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for DeleteCommunity { + type Response = CommunityResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &DeleteCommunity = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + // Verify its the creator (only a creator can delete the community) + let community_id = data.community_id; + let read_community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + if read_community.creator_id != local_user_view.person.id { + return Err(ApiError::err("no_community_edit_allowed").into()); + } + + // Do the delete + let community_id = data.community_id; + let deleted = data.deleted; + let updated_community = match blocking(context.pool(), move |conn| { + Community::update_deleted(conn, community_id, deleted) + }) + .await? + { + Ok(community) => community, + Err(_e) => return Err(ApiError::err("couldnt_update_community").into()), + }; + + // Send apub messages + if deleted { + updated_community.send_delete(context).await?; + } else { + updated_community.send_undo_delete(context).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??; + + let res = CommunityResponse { community_view }; + + send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity); + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl PerformCrud for RemoveCommunity { + type Response = CommunityResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &RemoveCommunity = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + // Verify its an admin (only an admin can remove a community) + is_admin(&local_user_view)?; + + // Do the remove + let community_id = data.community_id; + let removed = data.removed; + let updated_community = match blocking(context.pool(), move |conn| { + Community::update_removed(conn, community_id, removed) + }) + .await? + { + Ok(community) => community, + Err(_e) => return Err(ApiError::err("couldnt_update_community").into()), + }; + + // Mod tables + let expires = match data.expires { + Some(time) => Some(naive_from_unix(time)), + None => None, + }; + let form = ModRemoveCommunityForm { + mod_person_id: local_user_view.person.id, + community_id: data.community_id, + removed: Some(removed), + reason: data.reason.to_owned(), + expires, + }; + blocking(context.pool(), move |conn| { + ModRemoveCommunity::create(conn, &form) + }) + .await??; + + // Apub messages + if removed { + updated_community.send_remove(context).await?; + } else { + updated_community.send_undo_remove(context).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??; + + let res = CommunityResponse { community_view }; + + send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity); + + Ok(res) + } +} diff --git a/crates/api_crud/src/community/mod.rs b/crates/api_crud/src/community/mod.rs new file mode 100644 index 000000000..874aba9a1 --- /dev/null +++ b/crates/api_crud/src/community/mod.rs @@ -0,0 +1,27 @@ +use actix_web::web::Data; +use lemmy_api_common::community::CommunityResponse; +use lemmy_utils::ConnectionId; +use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation}; + +mod create; +mod delete; +mod read; +mod update; + +pub(in crate::community) fn send_community_websocket( + res: &CommunityResponse, + context: &Data, + websocket_id: Option, + op: UserOperation, +) { + // Strip out the person id and subscribed when sending to others + let mut res_sent = res.clone(); + res_sent.community_view.subscribed = false; + + context.chat_server().do_send(SendCommunityRoomMessage { + op, + response: res_sent, + community_id: res.community_view.community.id, + websocket_id, + }); +} diff --git a/crates/api_crud/src/community/read.rs b/crates/api_crud/src/community/read.rs new file mode 100644 index 000000000..af83a7749 --- /dev/null +++ b/crates/api_crud/src/community/read.rs @@ -0,0 +1,121 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{blocking, community::*, get_local_user_view_from_jwt_opt}; +use lemmy_db_queries::{source::community::Community_, ListingType, SortType}; +use lemmy_db_schema::source::community::*; +use lemmy_db_views_actor::{ + community_moderator_view::CommunityModeratorView, + community_view::{CommunityQueryBuilder, CommunityView}, +}; +use lemmy_utils::{ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::GetCommunityUsersOnline, LemmyContext}; +use std::str::FromStr; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for GetCommunity { + type Response = GetCommunityResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &GetCommunity = &self; + let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?; + let person_id = local_user_view.map(|u| u.person.id); + + let community_id = match data.id { + Some(id) => id, + None => { + let name = data.name.to_owned().unwrap_or_else(|| "main".to_string()); + match blocking(context.pool(), move |conn| { + Community::read_from_name(conn, &name) + }) + .await? + { + Ok(community) => community, + Err(_e) => return Err(ApiError::err("couldnt_find_community").into()), + } + .id + } + }; + + let community_view = match blocking(context.pool(), move |conn| { + CommunityView::read(conn, community_id, person_id) + }) + .await? + { + Ok(community) => community, + Err(_e) => return Err(ApiError::err("couldnt_find_community").into()), + }; + + let moderators: Vec = match blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(conn, community_id) + }) + .await? + { + Ok(moderators) => moderators, + Err(_e) => return Err(ApiError::err("couldnt_find_community").into()), + }; + + let online = context + .chat_server() + .send(GetCommunityUsersOnline { community_id }) + .await + .unwrap_or(1); + + let res = GetCommunityResponse { + community_view, + moderators, + online, + }; + + // Return the jwt + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl PerformCrud for ListCommunities { + type Response = ListCommunitiesResponse; + + 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, context.pool()).await?; + + let person_id = match &local_user_view { + Some(uv) => Some(uv.person.id), + None => None, + }; + + // Don't show NSFW by default + let show_nsfw = match &local_user_view { + Some(uv) => uv.local_user.show_nsfw, + None => false, + }; + + let type_ = ListingType::from_str(&data.type_)?; + let sort = SortType::from_str(&data.sort)?; + + let page = data.page; + let limit = data.limit; + let communities = blocking(context.pool(), move |conn| { + CommunityQueryBuilder::create(conn) + .listing_type(&type_) + .sort(&sort) + .show_nsfw(show_nsfw) + .my_person_id(person_id) + .page(page) + .limit(limit) + .list() + }) + .await??; + + // Return the jwt + Ok(ListCommunitiesResponse { communities }) + } +} diff --git a/crates/api_crud/src/community/update.rs b/crates/api_crud/src/community/update.rs new file mode 100644 index 000000000..d7fa3061e --- /dev/null +++ b/crates/api_crud/src/community/update.rs @@ -0,0 +1,109 @@ +use crate::{community::send_community_websocket, PerformCrud}; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + community::{CommunityResponse, EditCommunity}, + get_local_user_view_from_jwt, +}; +use lemmy_db_queries::{diesel_option_overwrite_to_url, Crud}; +use lemmy_db_schema::{ + naive_now, + source::community::{Community, CommunityForm}, + PersonId, +}; +use lemmy_db_views_actor::{ + community_moderator_view::CommunityModeratorView, + community_view::CommunityView, +}; +use lemmy_utils::{ + utils::{check_slurs, check_slurs_opt}, + ApiError, + ConnectionId, + LemmyError, +}; +use lemmy_websocket::{LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for EditCommunity { + type Response = CommunityResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &EditCommunity = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + check_slurs(&data.title)?; + check_slurs_opt(&data.description)?; + + // Verify its a mod (only mods can edit it) + let community_id = data.community_id; + let mods: Vec = blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(conn, community_id) + .map(|v| v.into_iter().map(|m| m.moderator.id).collect()) + }) + .await??; + if !mods.contains(&local_user_view.person.id) { + return Err(ApiError::err("not_a_moderator").into()); + } + + let community_id = data.community_id; + let read_community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + + let icon = diesel_option_overwrite_to_url(&data.icon)?; + let banner = diesel_option_overwrite_to_url(&data.banner)?; + + let community_form = CommunityForm { + name: read_community.name, + title: data.title.to_owned(), + description: data.description.to_owned(), + icon, + banner, + creator_id: read_community.creator_id, + removed: Some(read_community.removed), + deleted: Some(read_community.deleted), + nsfw: data.nsfw, + updated: Some(naive_now()), + actor_id: Some(read_community.actor_id), + local: read_community.local, + private_key: read_community.private_key, + public_key: read_community.public_key, + last_refreshed_at: None, + published: None, + followers_url: None, + inbox_url: None, + shared_inbox_url: None, + }; + + let community_id = data.community_id; + match blocking(context.pool(), move |conn| { + Community::update(conn, community_id, &community_form) + }) + .await? + { + Ok(community) => community, + Err(_e) => return Err(ApiError::err("couldnt_update_community").into()), + }; + + // TODO there needs to be some kind of an apub update + // process for communities and users + + 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??; + + let res = CommunityResponse { community_view }; + + send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity); + + Ok(res) + } +} diff --git a/crates/api_crud/src/lib.rs b/crates/api_crud/src/lib.rs new file mode 100644 index 000000000..77a900dd4 --- /dev/null +++ b/crates/api_crud/src/lib.rs @@ -0,0 +1,22 @@ +use actix_web::web::Data; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +mod comment; +mod community; +mod post; +mod private_message; +pub mod routes; +mod site; +mod user; + +#[async_trait::async_trait(?Send)] +pub trait PerformCrud { + type Response: serde::ser::Serialize + Send; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result; +} diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs new file mode 100644 index 000000000..231a891d4 --- /dev/null +++ b/crates/api_crud/src/post/create.rs @@ -0,0 +1,130 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{blocking, check_community_ban, get_local_user_view_from_jwt, post::*}; +use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType}; +use lemmy_db_queries::{source::post::Post_, Crud, Likeable}; +use lemmy_db_schema::source::post::*; +use lemmy_db_views::post_view::PostView; +use lemmy_utils::{ + request::fetch_iframely_and_pictrs_data, + utils::{check_slurs, check_slurs_opt, is_valid_post_title}, + ApiError, + ConnectionId, + LemmyError, +}; +use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for CreatePost { + type Response = PostResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &CreatePost = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + check_slurs(&data.name)?; + check_slurs_opt(&data.body)?; + + if !is_valid_post_title(&data.name) { + return Err(ApiError::err("invalid_post_title").into()); + } + + check_community_ban(local_user_view.person.id, data.community_id, context.pool()).await?; + + // Fetch Iframely and pictrs cached image + let data_url = data.url.as_ref(); + let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = + fetch_iframely_and_pictrs_data(context.client(), data_url).await; + + let post_form = PostForm { + name: data.name.trim().to_owned(), + url: data_url.map(|u| u.to_owned().into()), + body: data.body.to_owned(), + community_id: data.community_id, + creator_id: local_user_view.person.id, + removed: None, + deleted: None, + nsfw: data.nsfw, + locked: None, + stickied: None, + updated: None, + embed_title: iframely_title, + embed_description: iframely_description, + embed_html: iframely_html, + thumbnail_url: pictrs_thumbnail.map(|u| u.into()), + ap_id: None, + local: true, + published: None, + }; + + let inserted_post = + match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? { + Ok(post) => post, + Err(e) => { + let err_type = if e.to_string() == "value too long for type character varying(200)" { + "post_title_too_long" + } else { + "couldnt_create_post" + }; + + return Err(ApiError::err(err_type).into()); + } + }; + + let inserted_post_id = inserted_post.id; + let updated_post = match blocking(context.pool(), move |conn| -> Result { + let apub_id = generate_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string())?; + Ok(Post::update_ap_id(conn, inserted_post_id, apub_id)?) + }) + .await? + { + Ok(post) => post, + Err(_e) => return Err(ApiError::err("couldnt_create_post").into()), + }; + + updated_post + .send_create(&local_user_view.person, context) + .await?; + + // They like their own post by default + let like_form = PostLikeForm { + post_id: inserted_post.id, + person_id: local_user_view.person.id, + score: 1, + }; + + let like = move |conn: &'_ _| PostLike::like(conn, &like_form); + if blocking(context.pool(), like).await?.is_err() { + return Err(ApiError::err("couldnt_like_post").into()); + } + + updated_post + .send_like(&local_user_view.person, context) + .await?; + + // Refetch the view + let inserted_post_id = inserted_post.id; + let post_view = match blocking(context.pool(), move |conn| { + PostView::read(conn, inserted_post_id, Some(local_user_view.person.id)) + }) + .await? + { + Ok(post) => post, + Err(_e) => return Err(ApiError::err("couldnt_find_post").into()), + }; + + let res = PostResponse { post_view }; + + context.chat_server().do_send(SendPost { + op: UserOperation::CreatePost, + post: res.clone(), + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api_crud/src/post/delete.rs b/crates/api_crud/src/post/delete.rs new file mode 100644 index 000000000..ca25f3b11 --- /dev/null +++ b/crates/api_crud/src/post/delete.rs @@ -0,0 +1,161 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + check_community_ban, + get_local_user_view_from_jwt, + is_mod_or_admin, + post::*, +}; +use lemmy_apub::ApubObjectType; +use lemmy_db_queries::{source::post::Post_, Crud}; +use lemmy_db_schema::source::{moderator::*, post::*}; +use lemmy_db_views::post_view::PostView; +use lemmy_utils::{ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for DeletePost { + type Response = PostResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &DeletePost = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).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?; + + // Verify that only the creator can delete + if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) { + return Err(ApiError::err("no_post_edit_allowed").into()); + } + + // Update the post + let post_id = data.post_id; + let deleted = data.deleted; + let updated_post = blocking(context.pool(), move |conn| { + Post::update_deleted(conn, post_id, deleted) + }) + .await??; + + // apub updates + if deleted { + updated_post + .send_delete(&local_user_view.person, context) + .await?; + } else { + updated_post + .send_undo_delete(&local_user_view.person, context) + .await?; + } + + // Refetch the post + let post_id = data.post_id; + let post_view = blocking(context.pool(), move |conn| { + PostView::read(conn, post_id, Some(local_user_view.person.id)) + }) + .await??; + + let res = PostResponse { post_view }; + + context.chat_server().do_send(SendPost { + op: UserOperation::DeletePost, + post: res.clone(), + websocket_id, + }); + + Ok(res) + } +} + +#[async_trait::async_trait(?Send)] +impl PerformCrud for RemovePost { + type Response = PostResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &RemovePost = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).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?; + + // Verify that only the mods can remove + 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 removed = data.removed; + let updated_post = blocking(context.pool(), move |conn| { + Post::update_removed(conn, post_id, removed) + }) + .await??; + + // Mod tables + let form = ModRemovePostForm { + mod_person_id: local_user_view.person.id, + post_id: data.post_id, + removed: Some(removed), + reason: data.reason.to_owned(), + }; + blocking(context.pool(), move |conn| { + ModRemovePost::create(conn, &form) + }) + .await??; + + // apub updates + if removed { + updated_post + .send_remove(&local_user_view.person, context) + .await?; + } else { + updated_post + .send_undo_remove(&local_user_view.person, context) + .await?; + } + + // Refetch the 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??; + + let res = PostResponse { post_view }; + + context.chat_server().do_send(SendPost { + op: UserOperation::RemovePost, + post: res.clone(), + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api_crud/src/post/mod.rs b/crates/api_crud/src/post/mod.rs new file mode 100644 index 000000000..716832376 --- /dev/null +++ b/crates/api_crud/src/post/mod.rs @@ -0,0 +1,4 @@ +mod create; +mod delete; +mod read; +mod update; diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs new file mode 100644 index 000000000..1b173418b --- /dev/null +++ b/crates/api_crud/src/post/read.rs @@ -0,0 +1,135 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, post::*}; +use lemmy_db_queries::{ListingType, SortType}; +use lemmy_db_views::{ + comment_view::CommentQueryBuilder, + post_view::{PostQueryBuilder, PostView}, +}; +use lemmy_db_views_actor::{ + community_moderator_view::CommunityModeratorView, + community_view::CommunityView, +}; +use lemmy_utils::{ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::GetPostUsersOnline, LemmyContext}; +use std::str::FromStr; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for GetPost { + type Response = GetPostResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &GetPost = &self; + let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?; + let person_id = local_user_view.map(|u| u.person.id); + + let id = data.id; + let post_view = match blocking(context.pool(), move |conn| { + PostView::read(conn, id, person_id) + }) + .await? + { + Ok(post) => post, + Err(_e) => return Err(ApiError::err("couldnt_find_post").into()), + }; + + let id = data.id; + let comments = blocking(context.pool(), move |conn| { + CommentQueryBuilder::create(conn) + .my_person_id(person_id) + .post_id(id) + .limit(9999) + .list() + }) + .await??; + + let community_id = post_view.community.id; + let moderators = blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(conn, community_id) + }) + .await??; + + // Necessary for the sidebar + let community_view = match blocking(context.pool(), move |conn| { + CommunityView::read(conn, community_id, person_id) + }) + .await? + { + Ok(community) => community, + Err(_e) => return Err(ApiError::err("couldnt_find_community").into()), + }; + + let online = context + .chat_server() + .send(GetPostUsersOnline { post_id: data.id }) + .await + .unwrap_or(1); + + // Return the jwt + Ok(GetPostResponse { + post_view, + community_view, + comments, + moderators, + online, + }) + } +} + +#[async_trait::async_trait(?Send)] +impl PerformCrud for GetPosts { + type Response = GetPostsResponse; + + 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, context.pool()).await?; + + let person_id = match &local_user_view { + Some(uv) => Some(uv.person.id), + None => None, + }; + + let show_nsfw = match &local_user_view { + Some(uv) => uv.local_user.show_nsfw, + None => false, + }; + + let type_ = ListingType::from_str(&data.type_)?; + let sort = SortType::from_str(&data.sort)?; + + let page = data.page; + let limit = data.limit; + let community_id = data.community_id; + let community_name = data.community_name.to_owned(); + let saved_only = data.saved_only; + + let posts = match blocking(context.pool(), move |conn| { + PostQueryBuilder::create(conn) + .listing_type(&type_) + .sort(&sort) + .show_nsfw(show_nsfw) + .community_id(community_id) + .community_name(community_name) + .saved_only(saved_only) + .my_person_id(person_id) + .page(page) + .limit(limit) + .list() + }) + .await? + { + Ok(posts) => posts, + Err(_e) => return Err(ApiError::err("couldnt_get_posts").into()), + }; + + Ok(GetPostsResponse { posts }) + } +} diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs new file mode 100644 index 000000000..c03bddf82 --- /dev/null +++ b/crates/api_crud/src/post/update.rs @@ -0,0 +1,116 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{blocking, check_community_ban, get_local_user_view_from_jwt, post::*}; +use lemmy_apub::ApubObjectType; +use lemmy_db_queries::{source::post::Post_, Crud}; +use lemmy_db_schema::{naive_now, source::post::*}; +use lemmy_db_views::post_view::PostView; +use lemmy_utils::{ + request::fetch_iframely_and_pictrs_data, + utils::{check_slurs, check_slurs_opt, is_valid_post_title}, + ApiError, + ConnectionId, + LemmyError, +}; +use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for EditPost { + type Response = PostResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &EditPost = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + check_slurs(&data.name)?; + check_slurs_opt(&data.body)?; + + if !is_valid_post_title(&data.name) { + return Err(ApiError::err("invalid_post_title").into()); + } + + 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?; + + // Verify that only the creator can edit + if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) { + return Err(ApiError::err("no_post_edit_allowed").into()); + } + + // Fetch Iframely and Pictrs cached image + let data_url = data.url.as_ref(); + let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = + fetch_iframely_and_pictrs_data(context.client(), data_url).await; + + let post_form = PostForm { + name: data.name.trim().to_owned(), + url: data_url.map(|u| u.to_owned().into()), + body: data.body.to_owned(), + nsfw: data.nsfw, + creator_id: orig_post.creator_id.to_owned(), + community_id: orig_post.community_id, + removed: Some(orig_post.removed), + deleted: Some(orig_post.deleted), + locked: Some(orig_post.locked), + stickied: Some(orig_post.stickied), + updated: Some(naive_now()), + embed_title: iframely_title, + embed_description: iframely_description, + embed_html: iframely_html, + thumbnail_url: pictrs_thumbnail.map(|u| u.into()), + ap_id: Some(orig_post.ap_id), + local: orig_post.local, + published: None, + }; + + let post_id = data.post_id; + let res = blocking(context.pool(), move |conn| { + Post::update(conn, post_id, &post_form) + }) + .await?; + let updated_post: Post = match res { + Ok(post) => post, + Err(e) => { + let err_type = if e.to_string() == "value too long for type character varying(200)" { + "post_title_too_long" + } else { + "couldnt_update_post" + }; + + return Err(ApiError::err(err_type).into()); + } + }; + + // Send apub update + updated_post + .send_update(&local_user_view.person, context) + .await?; + + let post_id = data.post_id; + let post_view = blocking(context.pool(), move |conn| { + PostView::read(conn, post_id, Some(local_user_view.person.id)) + }) + .await??; + + let res = PostResponse { post_view }; + + context.chat_server().do_send(SendPost { + op: UserOperation::EditPost, + post: res.clone(), + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs new file mode 100644 index 000000000..9654d22fd --- /dev/null +++ b/crates/api_crud/src/private_message/create.rs @@ -0,0 +1,112 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + person::{CreatePrivateMessage, PrivateMessageResponse}, + send_email_to_user, +}; +use lemmy_apub::{generate_apub_endpoint, ApubObjectType, EndpointType}; +use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud}; +use lemmy_db_schema::source::private_message::{PrivateMessage, PrivateMessageForm}; +use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView}; +use lemmy_utils::{utils::remove_slurs, ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for CreatePrivateMessage { + type Response = PrivateMessageResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &CreatePrivateMessage = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + let content_slurs_removed = remove_slurs(&data.content.to_owned()); + + let private_message_form = PrivateMessageForm { + content: content_slurs_removed.to_owned(), + creator_id: local_user_view.person.id, + recipient_id: data.recipient_id, + deleted: None, + read: None, + updated: None, + ap_id: None, + local: true, + published: None, + }; + + let inserted_private_message = match blocking(context.pool(), move |conn| { + PrivateMessage::create(conn, &private_message_form) + }) + .await? + { + Ok(private_message) => private_message, + Err(_e) => { + return Err(ApiError::err("couldnt_create_private_message").into()); + } + }; + + let inserted_private_message_id = inserted_private_message.id; + let updated_private_message = match blocking( + context.pool(), + move |conn| -> Result { + let apub_id = generate_apub_endpoint( + EndpointType::PrivateMessage, + &inserted_private_message_id.to_string(), + )?; + Ok(PrivateMessage::update_ap_id( + &conn, + inserted_private_message_id, + apub_id, + )?) + }, + ) + .await? + { + Ok(private_message) => private_message, + Err(_e) => return Err(ApiError::err("couldnt_create_private_message").into()), + }; + + updated_private_message + .send_create(&local_user_view.person, context) + .await?; + + let private_message_view = blocking(context.pool(), move |conn| { + PrivateMessageView::read(conn, inserted_private_message.id) + }) + .await??; + + let res = PrivateMessageResponse { + private_message_view, + }; + + // Send notifications to the local recipient, if one exists + let recipient_id = data.recipient_id; + if let Ok(local_recipient) = blocking(context.pool(), move |conn| { + LocalUserView::read_person(conn, recipient_id) + }) + .await? + { + send_email_to_user( + &local_recipient, + "Private Message from", + "Private Message", + &content_slurs_removed, + ); + + let local_recipient_id = local_recipient.local_user.id; + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperation::CreatePrivateMessage, + response: res.clone(), + local_recipient_id, + websocket_id, + }); + } + + Ok(res) + } +} diff --git a/crates/api_crud/src/private_message/delete.rs b/crates/api_crud/src/private_message/delete.rs new file mode 100644 index 000000000..120f57aaa --- /dev/null +++ b/crates/api_crud/src/private_message/delete.rs @@ -0,0 +1,88 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + person::{DeletePrivateMessage, PrivateMessageResponse}, +}; +use lemmy_apub::ApubObjectType; +use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud}; +use lemmy_db_schema::source::private_message::PrivateMessage; +use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView}; +use lemmy_utils::{ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for DeletePrivateMessage { + type Response = PrivateMessageResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &DeletePrivateMessage = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + // Checking permissions + let private_message_id = data.private_message_id; + let orig_private_message = blocking(context.pool(), move |conn| { + PrivateMessage::read(conn, private_message_id) + }) + .await??; + if local_user_view.person.id != orig_private_message.creator_id { + return Err(ApiError::err("no_private_message_edit_allowed").into()); + } + + // Doing the update + let private_message_id = data.private_message_id; + let deleted = data.deleted; + let updated_private_message = match blocking(context.pool(), move |conn| { + PrivateMessage::update_deleted(conn, private_message_id, deleted) + }) + .await? + { + Ok(private_message) => private_message, + Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()), + }; + + // Send the apub update + if data.deleted { + updated_private_message + .send_delete(&local_user_view.person, context) + .await?; + } else { + updated_private_message + .send_undo_delete(&local_user_view.person, context) + .await?; + } + + let private_message_id = data.private_message_id; + let private_message_view = blocking(context.pool(), move |conn| { + PrivateMessageView::read(conn, private_message_id) + }) + .await??; + + let res = PrivateMessageResponse { + private_message_view, + }; + + // Send notifications to the local recipient, if one exists + let recipient_id = orig_private_message.recipient_id; + if let Ok(local_recipient) = blocking(context.pool(), move |conn| { + LocalUserView::read_person(conn, recipient_id) + }) + .await? + { + let local_recipient_id = local_recipient.local_user.id; + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperation::DeletePrivateMessage, + response: res.clone(), + local_recipient_id, + websocket_id, + }); + } + + Ok(res) + } +} diff --git a/crates/api_crud/src/private_message/mod.rs b/crates/api_crud/src/private_message/mod.rs new file mode 100644 index 000000000..716832376 --- /dev/null +++ b/crates/api_crud/src/private_message/mod.rs @@ -0,0 +1,4 @@ +mod create; +mod delete; +mod read; +mod update; diff --git a/crates/api_crud/src/private_message/read.rs b/crates/api_crud/src/private_message/read.rs new file mode 100644 index 000000000..79bc85f45 --- /dev/null +++ b/crates/api_crud/src/private_message/read.rs @@ -0,0 +1,41 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + person::{GetPrivateMessages, PrivateMessagesResponse}, +}; +use lemmy_db_views::private_message_view::PrivateMessageQueryBuilder; +use lemmy_utils::{ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for GetPrivateMessages { + type Response = PrivateMessagesResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &GetPrivateMessages = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + let person_id = local_user_view.person.id; + + let page = data.page; + let limit = data.limit; + let unread_only = data.unread_only; + let messages = blocking(context.pool(), move |conn| { + PrivateMessageQueryBuilder::create(&conn, person_id) + .page(page) + .limit(limit) + .unread_only(unread_only) + .list() + }) + .await??; + + Ok(PrivateMessagesResponse { + private_messages: messages, + }) + } +} diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs new file mode 100644 index 000000000..b6baa0365 --- /dev/null +++ b/crates/api_crud/src/private_message/update.rs @@ -0,0 +1,82 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + person::{EditPrivateMessage, PrivateMessageResponse}, +}; +use lemmy_apub::ApubObjectType; +use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud}; +use lemmy_db_schema::source::private_message::PrivateMessage; +use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView}; +use lemmy_utils::{utils::remove_slurs, ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for EditPrivateMessage { + type Response = PrivateMessageResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &EditPrivateMessage = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + // Checking permissions + let private_message_id = data.private_message_id; + let orig_private_message = blocking(context.pool(), move |conn| { + PrivateMessage::read(conn, private_message_id) + }) + .await??; + if local_user_view.person.id != orig_private_message.creator_id { + return Err(ApiError::err("no_private_message_edit_allowed").into()); + } + + // Doing the update + let content_slurs_removed = remove_slurs(&data.content); + let private_message_id = data.private_message_id; + let updated_private_message = match blocking(context.pool(), move |conn| { + PrivateMessage::update_content(conn, private_message_id, &content_slurs_removed) + }) + .await? + { + Ok(private_message) => private_message, + Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()), + }; + + // Send the apub update + updated_private_message + .send_update(&local_user_view.person, context) + .await?; + + let private_message_id = data.private_message_id; + let private_message_view = blocking(context.pool(), move |conn| { + PrivateMessageView::read(conn, private_message_id) + }) + .await??; + + let res = PrivateMessageResponse { + private_message_view, + }; + + // Send notifications to the local recipient, if one exists + let recipient_id = orig_private_message.recipient_id; + if let Ok(local_recipient) = blocking(context.pool(), move |conn| { + LocalUserView::read_person(conn, recipient_id) + }) + .await? + { + let local_recipient_id = local_recipient.local_user.id; + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperation::EditPrivateMessage, + response: res.clone(), + local_recipient_id, + websocket_id, + }); + } + + Ok(res) + } +} diff --git a/crates/api_crud/src/routes.rs b/crates/api_crud/src/routes.rs new file mode 100644 index 000000000..774268b69 --- /dev/null +++ b/crates/api_crud/src/routes.rs @@ -0,0 +1,133 @@ +use crate::PerformCrud; +use actix_web::{error::ErrorBadRequest, *}; +use lemmy_api_common::{comment::*, community::*, person::*, post::*, site::*}; +use lemmy_utils::rate_limit::RateLimit; +use lemmy_websocket::LemmyContext; +use serde::Deserialize; + +pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { + cfg + .service( + web::scope("/api/v2") + // Site + .service( + web::scope("/site") + .wrap(rate_limit.message()) + .route("", web::get().to(route_get::)) + // Admin Actions + .route("", web::post().to(route_post::)) + .route("", web::put().to(route_post::)), + ) + // Community + .service( + web::resource("/community") + .guard(guard::Post()) + .wrap(rate_limit.register()) + .route(web::post().to(route_post::)), + ) + .service( + web::scope("/community") + .wrap(rate_limit.message()) + .route("", web::get().to(route_get::)) + .route("", web::put().to(route_post::)) + .route("/list", web::get().to(route_get::)) + .route("/delete", web::post().to(route_post::)) + // Mod Actions + .route("/remove", web::post().to(route_post::)), + ) + // Post + .service( + // Handle POST to /post separately to add the post() rate limitter + web::resource("/post") + .guard(guard::Post()) + .wrap(rate_limit.post()) + .route(web::post().to(route_post::)), + ) + .service( + web::scope("/post") + .wrap(rate_limit.message()) + .route("", web::get().to(route_get::)) + .route("", web::put().to(route_post::)) + .route("/delete", web::post().to(route_post::)) + .route("/remove", web::post().to(route_post::)) + .route("/list", web::get().to(route_get::)), + ) + // Comment + .service( + web::scope("/comment") + .wrap(rate_limit.message()) + .route("", web::post().to(route_post::)) + .route("", web::put().to(route_post::)) + .route("/delete", web::post().to(route_post::)) + .route("/remove", web::post().to(route_post::)) + .route("/list", web::get().to(route_get::)), + ), + ) + // Private Message + .service( + web::scope("/private_message") + .wrap(rate_limit.message()) + .route("/list", web::get().to(route_get::)) + .route("", web::post().to(route_post::)) + .route("", web::put().to(route_post::)) + .route( + "/delete", + web::post().to(route_post::), + ), + ) + // User + .service( + // Account action, I don't like that it's in /user maybe /accounts + // Handle /user/register separately to add the register() rate limitter + web::resource("/user/register") + .guard(guard::Post()) + .wrap(rate_limit.register()) + .route(web::post().to(route_post::)), + ) + // User actions + .service( + web::scope("/user") + .wrap(rate_limit.message()) + .route("", web::get().to(route_get::)) + .route( + "/delete_account", + web::post().to(route_post::), + ), + ); +} + +async fn perform( + data: Request, + context: web::Data, +) -> Result +where + Request: PerformCrud, + Request: Send + 'static, +{ + let res = data + .perform(&context, None) + .await + .map(|json| HttpResponse::Ok().json(json)) + .map_err(ErrorBadRequest)?; + Ok(res) +} + +async fn route_get<'a, Data>( + data: web::Query, + context: web::Data, +) -> Result +where + Data: Deserialize<'a> + Send + 'static + PerformCrud, +{ + perform::(data.0, context).await +} + +async fn route_post<'a, Data>( + data: web::Json, + context: web::Data, +) -> Result +where + Data: Deserialize<'a> + Send + 'static + PerformCrud, +{ + perform::(data.0, context).await +} diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs new file mode 100644 index 000000000..855e41c1f --- /dev/null +++ b/crates/api_crud/src/site/create.rs @@ -0,0 +1,60 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{blocking, get_local_user_view_from_jwt, is_admin, site::*}; +use lemmy_db_queries::{source::site::Site_, Crud}; +use lemmy_db_schema::source::site::{Site, *}; +use lemmy_db_views::site_view::SiteView; +use lemmy_utils::{ + utils::{check_slurs, check_slurs_opt}, + ApiError, + ConnectionId, + LemmyError, +}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for CreateSite { + type Response = SiteResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &CreateSite = &self; + + let read_site = move |conn: &'_ _| Site::read_simple(conn); + if blocking(context.pool(), read_site).await?.is_ok() { + return Err(ApiError::err("site_already_exists").into()); + }; + + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + check_slurs(&data.name)?; + check_slurs_opt(&data.description)?; + + // Make sure user is an admin + is_admin(&local_user_view)?; + + let site_form = SiteForm { + name: data.name.to_owned(), + description: data.description.to_owned(), + icon: Some(data.icon.to_owned().map(|url| url.into())), + banner: Some(data.banner.to_owned().map(|url| url.into())), + creator_id: local_user_view.person.id, + enable_downvotes: data.enable_downvotes, + open_registration: data.open_registration, + enable_nsfw: data.enable_nsfw, + updated: None, + }; + + let create_site = move |conn: &'_ _| Site::create(conn, &site_form); + if blocking(context.pool(), create_site).await?.is_err() { + return Err(ApiError::err("site_already_exists").into()); + } + + let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??; + + Ok(SiteResponse { site_view }) + } +} diff --git a/crates/api_crud/src/site/mod.rs b/crates/api_crud/src/site/mod.rs new file mode 100644 index 000000000..845da0491 --- /dev/null +++ b/crates/api_crud/src/site/mod.rs @@ -0,0 +1,3 @@ +mod create; +mod read; +mod update; diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs new file mode 100644 index 000000000..270665195 --- /dev/null +++ b/crates/api_crud/src/site/read.rs @@ -0,0 +1,97 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + build_federated_instances, + get_local_user_settings_view_from_jwt_opt, + person::Register, + site::*, +}; +use lemmy_db_views::site_view::SiteView; +use lemmy_db_views_actor::person_view::PersonViewSafe; +use lemmy_utils::{settings::structs::Settings, version, ConnectionId, LemmyError}; +use lemmy_websocket::{messages::GetUsersOnline, LemmyContext}; +use log::info; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for GetSite { + type Response = GetSiteResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &GetSite = &self; + + let site_view = match blocking(context.pool(), move |conn| SiteView::read(conn)).await? { + Ok(site_view) => Some(site_view), + // If the site isn't created yet, check the setup + Err(_) => { + if let Some(setup) = Settings::get().setup().as_ref() { + let register = Register { + username: setup.admin_username.to_owned(), + email: setup.admin_email.to_owned(), + password: setup.admin_password.to_owned(), + password_verify: setup.admin_password.to_owned(), + show_nsfw: true, + captcha_uuid: None, + captcha_answer: None, + }; + let login_response = register.perform(context, websocket_id).await?; + info!("Admin {} created", setup.admin_username); + + let create_site = CreateSite { + name: setup.site_name.to_owned(), + description: None, + icon: None, + banner: None, + enable_downvotes: true, + open_registration: true, + enable_nsfw: true, + auth: login_response.jwt, + }; + create_site.perform(context, websocket_id).await?; + info!("Site {} created", setup.site_name); + Some(blocking(context.pool(), move |conn| SiteView::read(conn)).await??) + } else { + None + } + } + }; + + let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??; + + // Make sure the site creator is the top admin + if let Some(site_view) = site_view.to_owned() { + let site_creator_id = site_view.creator.id; + // TODO investigate why this is sometimes coming back null + // Maybe user_.admin isn't being set to true? + if let Some(creator_index) = admins.iter().position(|r| r.person.id == site_creator_id) { + let creator_person = admins.remove(creator_index); + admins.insert(0, creator_person); + } + } + + let banned = blocking(context.pool(), move |conn| PersonViewSafe::banned(conn)).await??; + + let online = context + .chat_server() + .send(GetUsersOnline) + .await + .unwrap_or(1); + + let my_user = get_local_user_settings_view_from_jwt_opt(&data.auth, context.pool()).await?; + let federated_instances = build_federated_instances(context.pool()).await?; + + Ok(GetSiteResponse { + site_view, + admins, + banned, + online, + version: version::VERSION.to_string(), + my_user, + federated_instances, + }) + } +} diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs new file mode 100644 index 000000000..3a4f50729 --- /dev/null +++ b/crates/api_crud/src/site/update.rs @@ -0,0 +1,74 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + is_admin, + site::{EditSite, SiteResponse}, +}; +use lemmy_db_queries::{diesel_option_overwrite_to_url, source::site::Site_, Crud}; +use lemmy_db_schema::{ + naive_now, + source::site::{Site, SiteForm}, +}; +use lemmy_db_views::site_view::SiteView; +use lemmy_utils::{ + utils::{check_slurs, check_slurs_opt}, + ApiError, + ConnectionId, + LemmyError, +}; +use lemmy_websocket::{messages::SendAllMessage, LemmyContext, UserOperation}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for EditSite { + type Response = SiteResponse; + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &EditSite = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + check_slurs(&data.name)?; + check_slurs_opt(&data.description)?; + + // Make sure user is an admin + is_admin(&local_user_view)?; + + let found_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??; + + let icon = diesel_option_overwrite_to_url(&data.icon)?; + let banner = diesel_option_overwrite_to_url(&data.banner)?; + + let site_form = SiteForm { + name: data.name.to_owned(), + description: data.description.to_owned(), + icon, + banner, + creator_id: found_site.creator_id, + updated: Some(naive_now()), + enable_downvotes: data.enable_downvotes, + open_registration: data.open_registration, + enable_nsfw: data.enable_nsfw, + }; + + let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form); + if blocking(context.pool(), update_site).await?.is_err() { + return Err(ApiError::err("couldnt_update_site").into()); + } + + let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??; + + let res = SiteResponse { site_view }; + + context.chat_server().do_send(SendAllMessage { + op: UserOperation::EditSite, + response: res.clone(), + websocket_id, + }); + + Ok(res) + } +} diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs new file mode 100644 index 000000000..81c7f5d22 --- /dev/null +++ b/crates/api_crud/src/user/create.rs @@ -0,0 +1,244 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{blocking, password_length_check, person::*}; +use lemmy_apub::{ + generate_apub_endpoint, + generate_followers_url, + generate_inbox_url, + generate_shared_inbox_url, + EndpointType, +}; +use lemmy_db_queries::{ + source::{local_user::LocalUser_, site::Site_}, + Crud, + Followable, + Joinable, + ListingType, + SortType, +}; +use lemmy_db_schema::{ + source::{ + community::*, + local_user::{LocalUser, LocalUserForm}, + person::*, + site::*, + }, + CommunityId, +}; +use lemmy_db_views_actor::person_view::PersonViewSafe; +use lemmy_utils::{ + apub::generate_actor_keypair, + claims::Claims, + settings::structs::Settings, + utils::{check_slurs, is_valid_username}, + ApiError, + ConnectionId, + LemmyError, +}; +use lemmy_websocket::{messages::CheckCaptcha, LemmyContext}; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for Register { + type Response = LoginResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &Register = &self; + + // Make sure site has open registration + if let Ok(site) = blocking(context.pool(), move |conn| Site::read_simple(conn)).await? { + if !site.open_registration { + return Err(ApiError::err("registration_closed").into()); + } + } + + password_length_check(&data.password)?; + + // Make sure passwords match + if data.password != data.password_verify { + return Err(ApiError::err("passwords_dont_match").into()); + } + + // Check if there are admins. False if admins exist + let no_admins = blocking(context.pool(), move |conn| { + PersonViewSafe::admins(conn).map(|a| a.is_empty()) + }) + .await??; + + // If its not the admin, check the captcha + if !no_admins && Settings::get().captcha().enabled { + let check = context + .chat_server() + .send(CheckCaptcha { + uuid: data + .captcha_uuid + .to_owned() + .unwrap_or_else(|| "".to_string()), + answer: data + .captcha_answer + .to_owned() + .unwrap_or_else(|| "".to_string()), + }) + .await?; + if !check { + return Err(ApiError::err("captcha_incorrect").into()); + } + } + + check_slurs(&data.username)?; + + let actor_keypair = generate_actor_keypair()?; + if !is_valid_username(&data.username) { + return Err(ApiError::err("invalid_username").into()); + } + let actor_id = generate_apub_endpoint(EndpointType::Person, &data.username)?; + + // We have to create both a person, and local_user + + // Register the new person + let person_form = PersonForm { + name: data.username.to_owned(), + avatar: None, + banner: None, + preferred_username: None, + published: None, + updated: None, + banned: None, + deleted: None, + actor_id: Some(actor_id.clone()), + bio: None, + local: Some(true), + private_key: Some(Some(actor_keypair.private_key)), + public_key: Some(Some(actor_keypair.public_key)), + last_refreshed_at: None, + inbox_url: Some(generate_inbox_url(&actor_id)?), + shared_inbox_url: Some(Some(generate_shared_inbox_url(&actor_id)?)), + }; + + // insert the person + let inserted_person = match blocking(context.pool(), move |conn| { + Person::create(conn, &person_form) + }) + .await? + { + Ok(u) => u, + Err(_) => { + return Err(ApiError::err("user_already_exists").into()); + } + }; + + // Create the local user + let local_user_form = LocalUserForm { + person_id: inserted_person.id, + email: Some(data.email.to_owned()), + matrix_user_id: None, + password_encrypted: data.password.to_owned(), + admin: Some(no_admins), + show_nsfw: Some(data.show_nsfw), + theme: Some("browser".into()), + default_sort_type: Some(SortType::Active as i16), + default_listing_type: Some(ListingType::Subscribed as i16), + lang: Some("browser".into()), + show_avatars: Some(true), + send_notifications_to_email: Some(false), + }; + + let inserted_local_user = match blocking(context.pool(), move |conn| { + LocalUser::register(conn, &local_user_form) + }) + .await? + { + Ok(lu) => lu, + 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" + }; + + // If the local user creation errored, then delete that person + blocking(context.pool(), move |conn| { + Person::delete(&conn, inserted_person.id) + }) + .await??; + + return Err(ApiError::err(err_type).into()); + } + }; + + let main_community_keypair = generate_actor_keypair()?; + + // Create the main community if it doesn't exist + let main_community = match blocking(context.pool(), move |conn| { + Community::read(conn, CommunityId(2)) + }) + .await? + { + Ok(c) => c, + Err(_e) => { + let default_community_name = "main"; + let actor_id = generate_apub_endpoint(EndpointType::Community, default_community_name)?; + let community_form = CommunityForm { + name: default_community_name.to_string(), + title: "The Default Community".to_string(), + description: Some("The Default Community".to_string()), + nsfw: false, + creator_id: inserted_person.id, + removed: None, + deleted: None, + updated: None, + actor_id: Some(actor_id.to_owned()), + local: true, + private_key: Some(main_community_keypair.private_key), + public_key: Some(main_community_keypair.public_key), + last_refreshed_at: None, + published: None, + icon: None, + banner: None, + followers_url: Some(generate_followers_url(&actor_id)?), + inbox_url: Some(generate_inbox_url(&actor_id)?), + shared_inbox_url: Some(Some(generate_shared_inbox_url(&actor_id)?)), + }; + blocking(context.pool(), move |conn| { + Community::create(conn, &community_form) + }) + .await?? + } + }; + + // Sign them up for main community no matter what + let community_follower_form = CommunityFollowerForm { + community_id: main_community.id, + person_id: inserted_person.id, + pending: false, + }; + + let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); + if blocking(context.pool(), follow).await?.is_err() { + return Err(ApiError::err("community_follower_already_exists").into()); + }; + + // If its an admin, add them as a mod and follower to main + if no_admins { + let community_moderator_form = CommunityModeratorForm { + community_id: main_community.id, + person_id: inserted_person.id, + }; + + let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); + if blocking(context.pool(), join).await?.is_err() { + return Err(ApiError::err("community_moderator_already_exists").into()); + } + } + + // Return the jwt + Ok(LoginResponse { + jwt: Claims::jwt(inserted_local_user.id.0)?, + }) + } +} diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs new file mode 100644 index 000000000..ca88830c3 --- /dev/null +++ b/crates/api_crud/src/user/delete.rs @@ -0,0 +1,54 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use bcrypt::verify; +use lemmy_api_common::{blocking, get_local_user_view_from_jwt, person::*}; +use lemmy_db_queries::source::{comment::Comment_, person::Person_, post::Post_}; +use lemmy_db_schema::source::{comment::Comment, person::*, post::Post}; +use lemmy_utils::{ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for DeleteAccount { + type Response = LoginResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &DeleteAccount = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + // Verify the password + let valid: bool = verify( + &data.password, + &local_user_view.local_user.password_encrypted, + ) + .unwrap_or(false); + if !valid { + return Err(ApiError::err("password_incorrect").into()); + } + + // Comments + let person_id = local_user_view.person.id; + let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, person_id); + if blocking(context.pool(), permadelete).await?.is_err() { + return Err(ApiError::err("couldnt_update_comment").into()); + } + + // Posts + let permadelete = move |conn: &'_ _| Post::permadelete_for_creator(conn, person_id); + if blocking(context.pool(), permadelete).await?.is_err() { + return Err(ApiError::err("couldnt_update_post").into()); + } + + blocking(context.pool(), move |conn| { + Person::delete_account(conn, person_id) + }) + .await??; + + Ok(LoginResponse { + jwt: data.auth.to_owned(), + }) + } +} diff --git a/crates/api_crud/src/user/mod.rs b/crates/api_crud/src/user/mod.rs new file mode 100644 index 000000000..843072411 --- /dev/null +++ b/crates/api_crud/src/user/mod.rs @@ -0,0 +1,3 @@ +mod create; +mod delete; +mod read; diff --git a/crates/api_crud/src/user/read.rs b/crates/api_crud/src/user/read.rs new file mode 100644 index 000000000..3136e100f --- /dev/null +++ b/crates/api_crud/src/user/read.rs @@ -0,0 +1,122 @@ +use crate::PerformCrud; +use actix_web::web::Data; +use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, person::*}; +use lemmy_db_queries::{source::person::Person_, SortType}; +use lemmy_db_schema::source::person::*; +use lemmy_db_views::{comment_view::CommentQueryBuilder, post_view::PostQueryBuilder}; +use lemmy_db_views_actor::{ + community_follower_view::CommunityFollowerView, + community_moderator_view::CommunityModeratorView, + person_view::PersonViewSafe, +}; +use lemmy_utils::{ApiError, ConnectionId, LemmyError}; +use lemmy_websocket::LemmyContext; +use std::str::FromStr; + +#[async_trait::async_trait(?Send)] +impl PerformCrud for GetPersonDetails { + type Response = GetPersonDetailsResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &GetPersonDetails = &self; + let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?; + + let show_nsfw = match &local_user_view { + Some(uv) => uv.local_user.show_nsfw, + None => false, + }; + + let sort = SortType::from_str(&data.sort)?; + + let username = data + .username + .to_owned() + .unwrap_or_else(|| "admin".to_string()); + let person_details_id = match data.person_id { + Some(id) => id, + None => { + let person = blocking(context.pool(), move |conn| { + Person::find_by_name(conn, &username) + }) + .await?; + match person { + Ok(p) => p.id, + Err(_e) => return Err(ApiError::err("couldnt_find_that_username_or_email").into()), + } + } + }; + + let person_id = local_user_view.map(|uv| uv.person.id); + + // You don't need to return settings for the user, since this comes back with GetSite + // `my_user` + let person_view = blocking(context.pool(), move |conn| { + PersonViewSafe::read(conn, person_details_id) + }) + .await??; + + let page = data.page; + let limit = data.limit; + let saved_only = data.saved_only; + let community_id = data.community_id; + + let (posts, comments) = blocking(context.pool(), move |conn| { + let mut posts_query = PostQueryBuilder::create(conn) + .sort(&sort) + .show_nsfw(show_nsfw) + .saved_only(saved_only) + .community_id(community_id) + .my_person_id(person_id) + .page(page) + .limit(limit); + + let mut comments_query = CommentQueryBuilder::create(conn) + .my_person_id(person_id) + .sort(&sort) + .saved_only(saved_only) + .community_id(community_id) + .page(page) + .limit(limit); + + // If its saved only, you don't care what creator it was + // Or, if its not saved, then you only want it for that specific creator + if !saved_only { + posts_query = posts_query.creator_id(person_details_id); + comments_query = comments_query.creator_id(person_details_id); + } + + let posts = posts_query.list()?; + let comments = comments_query.list()?; + + Ok((posts, comments)) as Result<_, LemmyError> + }) + .await??; + + let mut follows = vec![]; + if let Some(pid) = person_id { + if pid == person_details_id { + follows = blocking(context.pool(), move |conn| { + CommunityFollowerView::for_person(conn, person_details_id) + }) + .await??; + } + }; + let moderates = blocking(context.pool(), move |conn| { + CommunityModeratorView::for_person(conn, person_details_id) + }) + .await??; + + // Return the jwt + Ok(GetPersonDetailsResponse { + person_view, + follows, + moderates, + comments, + posts, + }) + } +} diff --git a/crates/api_structs/src/lib.rs b/crates/api_structs/src/lib.rs deleted file mode 100644 index f57d7f2b4..000000000 --- a/crates/api_structs/src/lib.rs +++ /dev/null @@ -1,191 +0,0 @@ -pub mod comment; -pub mod community; -pub mod person; -pub mod post; -pub mod site; -pub mod websocket; - -use diesel::PgConnection; -use lemmy_db_queries::{Crud, DbPool}; -use lemmy_db_schema::{ - source::{ - comment::Comment, - person::Person, - person_mention::{PersonMention, PersonMentionForm}, - post::Post, - }, - LocalUserId, -}; -use lemmy_db_views::local_user_view::LocalUserView; -use lemmy_utils::{email::send_email, settings::structs::Settings, utils::MentionData, LemmyError}; -use log::error; -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Serialize, Deserialize, Debug)] -pub struct WebFingerLink { - pub rel: Option, - #[serde(rename(serialize = "type", deserialize = "type"))] - pub type_: Option, - pub href: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub template: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct WebFingerResponse { - pub subject: String, - pub aliases: Vec, - pub links: Vec, -} - -pub async fn blocking(pool: &DbPool, f: F) -> Result -where - F: FnOnce(&diesel::PgConnection) -> T + Send + 'static, - T: Send + 'static, -{ - let pool = pool.clone(); - let res = actix_web::web::block(move || { - let conn = pool.get()?; - let res = (f)(&conn); - Ok(res) as Result<_, LemmyError> - }) - .await?; - - Ok(res) -} - -pub async fn send_local_notifs( - mentions: Vec, - comment: Comment, - person: Person, - post: Post, - pool: &DbPool, - do_send_email: bool, -) -> Result, LemmyError> { - let ids = blocking(pool, move |conn| { - do_send_local_notifs(conn, &mentions, &comment, &person, &post, do_send_email) - }) - .await?; - - Ok(ids) -} - -fn do_send_local_notifs( - conn: &PgConnection, - mentions: &[MentionData], - comment: &Comment, - person: &Person, - post: &Post, - do_send_email: bool, -) -> Vec { - let mut recipient_ids = Vec::new(); - - // Send the local mentions - for mention in mentions - .iter() - .filter(|m| m.is_local() && m.name.ne(&person.name)) - .collect::>() - { - if let Ok(mention_user_view) = LocalUserView::read_from_name(&conn, &mention.name) { - // TODO - // At some point, make it so you can't tag the parent creator either - // This can cause two notifications, one for reply and the other for mention - recipient_ids.push(mention_user_view.local_user.id); - - let user_mention_form = PersonMentionForm { - recipient_id: mention_user_view.person.id, - comment_id: comment.id, - read: None, - }; - - // Allow this to fail softly, since comment edits might re-update or replace it - // Let the uniqueness handle this fail - PersonMention::create(&conn, &user_mention_form).ok(); - - // Send an email to those local users that have notifications on - if do_send_email { - send_email_to_user( - &mention_user_view, - "Mentioned by", - "Person Mention", - &comment.content, - ) - } - } - } - - // Send notifs to the parent commenter / poster - match comment.parent_id { - Some(parent_id) => { - if let Ok(parent_comment) = Comment::read(&conn, parent_id) { - // Don't send a notif to yourself - if parent_comment.creator_id != person.id { - // Get the parent commenter local_user - if let Ok(parent_user_view) = LocalUserView::read_person(&conn, parent_comment.creator_id) - { - recipient_ids.push(parent_user_view.local_user.id); - - if do_send_email { - send_email_to_user( - &parent_user_view, - "Reply from", - "Comment Reply", - &comment.content, - ) - } - } - } - } - } - // Its a post - None => { - if post.creator_id != person.id { - if let Ok(parent_user_view) = LocalUserView::read_person(&conn, post.creator_id) { - recipient_ids.push(parent_user_view.local_user.id); - - if do_send_email { - send_email_to_user( - &parent_user_view, - "Reply from", - "Post Reply", - &comment.content, - ) - } - } - } - } - }; - recipient_ids -} - -pub fn send_email_to_user( - local_user_view: &LocalUserView, - subject_text: &str, - body_text: &str, - comment_content: &str, -) { - if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email { - return; - } - - if let Some(user_email) = &local_user_view.local_user.email { - let subject = &format!( - "{} - {} {}", - subject_text, - Settings::get().hostname(), - local_user_view.person.name, - ); - let html = &format!( - "

{}


{} - {}

inbox", - body_text, - local_user_view.person.name, - comment_content, - Settings::get().get_protocol_and_hostname() - ); - match send_email(subject, &user_email, &local_user_view.person.name, html) { - Ok(_o) => _o, - Err(e) => error!("{}", e), - }; - } -} diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index acb19db09..7d413f851 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -14,7 +14,7 @@ lemmy_db_queries = { path = "../db_queries" } lemmy_db_schema = { path = "../db_schema" } lemmy_db_views = { path = "../db_views" } lemmy_db_views_actor = { path = "../db_views_actor" } -lemmy_api_structs = { path = "../api_structs" } +lemmy_api_common = { path = "../api_common" } lemmy_websocket = { path = "../websocket" } diesel = "1.4.5" activitystreams = "0.7.0-alpha.11" diff --git a/crates/apub/src/activities/receive/comment.rs b/crates/apub/src/activities/receive/comment.rs index 95b51d648..2575035bd 100644 --- a/crates/apub/src/activities/receive/comment.rs +++ b/crates/apub/src/activities/receive/comment.rs @@ -4,7 +4,7 @@ use activitystreams::{ base::ExtendsExt, }; use anyhow::Context; -use lemmy_api_structs::{blocking, comment::CommentResponse, send_local_notifs}; +use lemmy_api_common::{blocking, comment::CommentResponse, send_local_notifs}; use lemmy_db_queries::{source::comment::Comment_, Crud, Likeable}; use lemmy_db_schema::source::{ comment::{Comment, CommentLike, CommentLikeForm}, diff --git a/crates/apub/src/activities/receive/comment_undo.rs b/crates/apub/src/activities/receive/comment_undo.rs index 22594f338..12a49ee38 100644 --- a/crates/apub/src/activities/receive/comment_undo.rs +++ b/crates/apub/src/activities/receive/comment_undo.rs @@ -1,6 +1,6 @@ use crate::activities::receive::get_actor_as_person; use activitystreams::activity::{Dislike, Like}; -use lemmy_api_structs::{blocking, comment::CommentResponse}; +use lemmy_api_common::{blocking, comment::CommentResponse}; use lemmy_db_queries::{source::comment::Comment_, Likeable}; use lemmy_db_schema::source::comment::{Comment, CommentLike}; use lemmy_db_views::comment_view::CommentView; diff --git a/crates/apub/src/activities/receive/community.rs b/crates/apub/src/activities/receive/community.rs index 48f6b295d..d6dba673d 100644 --- a/crates/apub/src/activities/receive/community.rs +++ b/crates/apub/src/activities/receive/community.rs @@ -1,4 +1,4 @@ -use lemmy_api_structs::{blocking, community::CommunityResponse}; +use lemmy_api_common::{blocking, community::CommunityResponse}; use lemmy_db_queries::source::community::Community_; use lemmy_db_schema::source::community::Community; use lemmy_db_views_actor::community_view::CommunityView; diff --git a/crates/apub/src/activities/receive/post.rs b/crates/apub/src/activities/receive/post.rs index d1c935d56..e490964d1 100644 --- a/crates/apub/src/activities/receive/post.rs +++ b/crates/apub/src/activities/receive/post.rs @@ -10,7 +10,7 @@ use activitystreams::{ prelude::*, }; use anyhow::Context; -use lemmy_api_structs::{blocking, post::PostResponse}; +use lemmy_api_common::{blocking, post::PostResponse}; use lemmy_db_queries::{source::post::Post_, ApubObject, Crud, Likeable}; use lemmy_db_schema::{ source::{ diff --git a/crates/apub/src/activities/receive/post_undo.rs b/crates/apub/src/activities/receive/post_undo.rs index 67cc20df7..589b0d229 100644 --- a/crates/apub/src/activities/receive/post_undo.rs +++ b/crates/apub/src/activities/receive/post_undo.rs @@ -1,6 +1,6 @@ use crate::activities::receive::get_actor_as_person; use activitystreams::activity::{Dislike, Like}; -use lemmy_api_structs::{blocking, post::PostResponse}; +use lemmy_api_common::{blocking, post::PostResponse}; use lemmy_db_queries::{source::post::Post_, Likeable}; use lemmy_db_schema::source::post::{Post, PostLike}; use lemmy_db_views::post_view::PostView; diff --git a/crates/apub/src/activities/receive/private_message.rs b/crates/apub/src/activities/receive/private_message.rs index 04954b718..47067b7a6 100644 --- a/crates/apub/src/activities/receive/private_message.rs +++ b/crates/apub/src/activities/receive/private_message.rs @@ -13,7 +13,7 @@ use activitystreams::{ public, }; use anyhow::{anyhow, Context}; -use lemmy_api_structs::{blocking, person::PrivateMessageResponse}; +use lemmy_api_common::{blocking, person::PrivateMessageResponse}; use lemmy_db_queries::source::private_message::PrivateMessage_; use lemmy_db_schema::source::private_message::PrivateMessage; use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView}; diff --git a/crates/apub/src/activities/send/comment.rs b/crates/apub/src/activities/send/comment.rs index 867e45e9f..76371b997 100644 --- a/crates/apub/src/activities/send/comment.rs +++ b/crates/apub/src/activities/send/comment.rs @@ -26,7 +26,7 @@ use activitystreams::{ }; use anyhow::anyhow; use itertools::Itertools; -use lemmy_api_structs::{blocking, WebFingerResponse}; +use lemmy_api_common::{blocking, WebFingerResponse}; use lemmy_db_queries::{Crud, DbPool}; use lemmy_db_schema::source::{comment::Comment, community::Community, person::Person, post::Post}; use lemmy_utils::{ diff --git a/crates/apub/src/activities/send/community.rs b/crates/apub/src/activities/send/community.rs index 80f0a42c8..f31eb260a 100644 --- a/crates/apub/src/activities/send/community.rs +++ b/crates/apub/src/activities/send/community.rs @@ -28,7 +28,7 @@ use activitystreams::{ }; use anyhow::Context; use itertools::Itertools; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::DbPool; use lemmy_db_schema::source::{community::Community, person::Person}; use lemmy_db_views_actor::community_follower_view::CommunityFollowerView; diff --git a/crates/apub/src/activities/send/person.rs b/crates/apub/src/activities/send/person.rs index 9560c2fbc..c034f5935 100644 --- a/crates/apub/src/activities/send/person.rs +++ b/crates/apub/src/activities/send/person.rs @@ -14,7 +14,7 @@ use activitystreams::{ base::{BaseExt, ExtendsExt}, object::ObjectExt, }; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{ApubObject, Followable}; use lemmy_db_schema::source::{ community::{Community, CommunityFollower, CommunityFollowerForm}, diff --git a/crates/apub/src/activities/send/post.rs b/crates/apub/src/activities/send/post.rs index 4d3bb9d1b..9f8be1e1e 100644 --- a/crates/apub/src/activities/send/post.rs +++ b/crates/apub/src/activities/send/post.rs @@ -21,7 +21,7 @@ use activitystreams::{ prelude::*, public, }; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::Crud; use lemmy_db_schema::source::{community::Community, person::Person, post::Post}; use lemmy_utils::LemmyError; diff --git a/crates/apub/src/activities/send/private_message.rs b/crates/apub/src/activities/send/private_message.rs index 92d818ab9..e5a30585b 100644 --- a/crates/apub/src/activities/send/private_message.rs +++ b/crates/apub/src/activities/send/private_message.rs @@ -16,7 +16,7 @@ use activitystreams::{ }, prelude::*, }; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::Crud; use lemmy_db_schema::source::{person::Person, private_message::PrivateMessage}; use lemmy_utils::LemmyError; diff --git a/crates/apub/src/fetcher/community.rs b/crates/apub/src/fetcher/community.rs index c27116dc3..c657bfad4 100644 --- a/crates/apub/src/fetcher/community.rs +++ b/crates/apub/src/fetcher/community.rs @@ -15,7 +15,7 @@ use activitystreams::{ }; use anyhow::Context; use diesel::result::Error::NotFound; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{source::community::Community_, ApubObject, Joinable}; use lemmy_db_schema::{ source::community::{Community, CommunityModerator, CommunityModeratorForm}, diff --git a/crates/apub/src/fetcher/objects.rs b/crates/apub/src/fetcher/objects.rs index 4ba2a56fa..b8f8bbde4 100644 --- a/crates/apub/src/fetcher/objects.rs +++ b/crates/apub/src/fetcher/objects.rs @@ -1,7 +1,7 @@ use crate::{fetcher::fetch::fetch_remote_object, objects::FromApub, NoteExt, PageExt}; use anyhow::anyhow; use diesel::result::Error::NotFound; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{ApubObject, Crud}; use lemmy_db_schema::source::{comment::Comment, post::Post}; use lemmy_utils::LemmyError; diff --git a/crates/apub/src/fetcher/person.rs b/crates/apub/src/fetcher/person.rs index 3788163b8..81ba731b1 100644 --- a/crates/apub/src/fetcher/person.rs +++ b/crates/apub/src/fetcher/person.rs @@ -5,7 +5,7 @@ use crate::{ }; use anyhow::anyhow; use diesel::result::Error::NotFound; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{source::person::Person_, ApubObject}; use lemmy_db_schema::source::person::Person; use lemmy_utils::LemmyError; diff --git a/crates/apub/src/fetcher/search.rs b/crates/apub/src/fetcher/search.rs index 230442972..d2d00f324 100644 --- a/crates/apub/src/fetcher/search.rs +++ b/crates/apub/src/fetcher/search.rs @@ -15,7 +15,7 @@ use crate::{ }; use activitystreams::base::BaseExt; use anyhow::{anyhow, Context}; -use lemmy_api_structs::{blocking, site::SearchResponse}; +use lemmy_api_common::{blocking, site::SearchResponse}; use lemmy_db_queries::{ source::{ comment::Comment_, diff --git a/crates/apub/src/http/comment.rs b/crates/apub/src/http/comment.rs index 3071445bd..4f63d89d3 100644 --- a/crates/apub/src/http/comment.rs +++ b/crates/apub/src/http/comment.rs @@ -4,7 +4,7 @@ use crate::{ }; use actix_web::{body::Body, web, web::Path, HttpResponse}; use diesel::result::Error::NotFound; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::Crud; use lemmy_db_schema::{source::comment::Comment, CommentId}; use lemmy_utils::LemmyError; diff --git a/crates/apub/src/http/community.rs b/crates/apub/src/http/community.rs index fcf20748a..4d3d8481d 100644 --- a/crates/apub/src/http/community.rs +++ b/crates/apub/src/http/community.rs @@ -11,7 +11,7 @@ use activitystreams::{ url::Url, }; use actix_web::{body::Body, web, HttpResponse}; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::source::{activity::Activity_, community::Community_}; use lemmy_db_schema::source::{activity::Activity, community::Community}; use lemmy_db_views_actor::{ diff --git a/crates/apub/src/http/mod.rs b/crates/apub/src/http/mod.rs index 8702bb5f7..5d0cf71f2 100644 --- a/crates/apub/src/http/mod.rs +++ b/crates/apub/src/http/mod.rs @@ -1,7 +1,7 @@ use crate::APUB_JSON_CONTENT_TYPE; use actix_web::{body::Body, web, HttpResponse}; use http::StatusCode; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::source::activity::Activity_; use lemmy_db_schema::source::activity::Activity; use lemmy_utils::{settings::structs::Settings, LemmyError}; diff --git a/crates/apub/src/http/person.rs b/crates/apub/src/http/person.rs index d523d6412..6a5a9a27b 100644 --- a/crates/apub/src/http/person.rs +++ b/crates/apub/src/http/person.rs @@ -9,7 +9,7 @@ use activitystreams::{ collection::{CollectionExt, OrderedCollection}, }; use actix_web::{body::Body, web, HttpResponse}; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::source::person::Person_; use lemmy_db_schema::source::person::Person; use lemmy_utils::LemmyError; diff --git a/crates/apub/src/http/post.rs b/crates/apub/src/http/post.rs index 03218b68f..324bb7da9 100644 --- a/crates/apub/src/http/post.rs +++ b/crates/apub/src/http/post.rs @@ -4,7 +4,7 @@ use crate::{ }; use actix_web::{body::Body, web, HttpResponse}; use diesel::result::Error::NotFound; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::Crud; use lemmy_db_schema::{source::post::Post, PostId}; use lemmy_utils::LemmyError; diff --git a/crates/apub/src/inbox/community_inbox.rs b/crates/apub/src/inbox/community_inbox.rs index c36d4db11..61123d06f 100644 --- a/crates/apub/src/inbox/community_inbox.rs +++ b/crates/apub/src/inbox/community_inbox.rs @@ -29,7 +29,7 @@ use activitystreams::{ }; use actix_web::{web, HttpRequest, HttpResponse}; use anyhow::{anyhow, Context}; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{source::community::Community_, ApubObject, DbPool, Followable}; use lemmy_db_schema::{ source::{ diff --git a/crates/apub/src/inbox/mod.rs b/crates/apub/src/inbox/mod.rs index 87d6d1824..72b00c334 100644 --- a/crates/apub/src/inbox/mod.rs +++ b/crates/apub/src/inbox/mod.rs @@ -12,7 +12,7 @@ use activitystreams::{ }; use actix_web::HttpRequest; use anyhow::{anyhow, Context}; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{ source::{activity::Activity_, community::Community_}, ApubObject, diff --git a/crates/apub/src/inbox/person_inbox.rs b/crates/apub/src/inbox/person_inbox.rs index 38e4167a7..99c0f18f5 100644 --- a/crates/apub/src/inbox/person_inbox.rs +++ b/crates/apub/src/inbox/person_inbox.rs @@ -49,7 +49,7 @@ use activitystreams::{ use actix_web::{web, HttpRequest, HttpResponse}; use anyhow::{anyhow, Context}; use diesel::NotFound; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{source::person::Person_, ApubObject, Followable}; use lemmy_db_schema::source::{ community::{Community, CommunityFollower}, diff --git a/crates/apub/src/inbox/receive_for_community.rs b/crates/apub/src/inbox/receive_for_community.rs index 2a077e11d..5fe2bdf7a 100644 --- a/crates/apub/src/inbox/receive_for_community.rs +++ b/crates/apub/src/inbox/receive_for_community.rs @@ -61,7 +61,7 @@ use activitystreams::{ }; use anyhow::{anyhow, Context}; use diesel::result::Error::NotFound; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{source::community::CommunityModerator_, ApubObject, Crud, Joinable}; use lemmy_db_schema::{ source::{ diff --git a/crates/apub/src/inbox/shared_inbox.rs b/crates/apub/src/inbox/shared_inbox.rs index 633388a56..710f34b44 100644 --- a/crates/apub/src/inbox/shared_inbox.rs +++ b/crates/apub/src/inbox/shared_inbox.rs @@ -15,7 +15,7 @@ use crate::{ use activitystreams::{activity::ActorAndObject, prelude::*}; use actix_web::{web, HttpRequest, HttpResponse}; use anyhow::Context; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{ApubObject, DbPool}; use lemmy_db_schema::source::community::Community; use lemmy_utils::{location_info, LemmyError}; diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index 74d4cbef3..6fa937438 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -24,7 +24,7 @@ use activitystreams::{ use activitystreams_ext::{Ext1, Ext2}; use anyhow::{anyhow, Context}; use diesel::NotFound; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{source::activity::Activity_, ApubObject, DbPool}; use lemmy_db_schema::{ source::{ diff --git a/crates/apub/src/objects/comment.rs b/crates/apub/src/objects/comment.rs index bd6c1a332..39f586839 100644 --- a/crates/apub/src/objects/comment.rs +++ b/crates/apub/src/objects/comment.rs @@ -21,7 +21,7 @@ use activitystreams::{ public, }; use anyhow::{anyhow, Context}; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{Crud, DbPool}; use lemmy_db_schema::{ source::{ diff --git a/crates/apub/src/objects/community.rs b/crates/apub/src/objects/community.rs index d7e42c4ae..ad1b7d613 100644 --- a/crates/apub/src/objects/community.rs +++ b/crates/apub/src/objects/community.rs @@ -23,7 +23,7 @@ use activitystreams::{ }; use activitystreams_ext::Ext2; use anyhow::Context; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::DbPool; use lemmy_db_schema::{ naive_now, diff --git a/crates/apub/src/objects/mod.rs b/crates/apub/src/objects/mod.rs index 77e9ffbfd..08822abfd 100644 --- a/crates/apub/src/objects/mod.rs +++ b/crates/apub/src/objects/mod.rs @@ -13,7 +13,7 @@ use activitystreams::{ use anyhow::{anyhow, Context}; use chrono::NaiveDateTime; use diesel::result::Error::NotFound; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{ApubObject, Crud, DbPool}; use lemmy_db_schema::{source::community::Community, CommunityId, DbUrl}; use lemmy_utils::{ diff --git a/crates/apub/src/objects/person.rs b/crates/apub/src/objects/person.rs index 87227dd14..c0dee8c19 100644 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@ -18,7 +18,7 @@ use activitystreams::{ }; use activitystreams_ext::Ext1; use anyhow::Context; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{ApubObject, DbPool}; use lemmy_db_schema::{ naive_now, diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index f532fcc1d..f26da74e3 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -23,7 +23,7 @@ use activitystreams::{ }; use activitystreams_ext::Ext1; use anyhow::Context; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{Crud, DbPool}; use lemmy_db_schema::{ self, diff --git a/crates/apub/src/objects/private_message.rs b/crates/apub/src/objects/private_message.rs index 6eec9fa8c..7cbb10f49 100644 --- a/crates/apub/src/objects/private_message.rs +++ b/crates/apub/src/objects/private_message.rs @@ -19,7 +19,7 @@ use activitystreams::{ prelude::*, }; use anyhow::Context; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{Crud, DbPool}; use lemmy_db_schema::source::{ person::Person, diff --git a/crates/routes/Cargo.toml b/crates/routes/Cargo.toml index b6b464c33..181d4c501 100644 --- a/crates/routes/Cargo.toml +++ b/crates/routes/Cargo.toml @@ -13,7 +13,7 @@ lemmy_db_queries = { path = "../db_queries" } lemmy_db_views = { path = "../db_views" } lemmy_db_views_actor = { path = "../db_views_actor" } lemmy_db_schema = { path = "../db_schema" } -lemmy_api_structs = { path = "../api_structs" } +lemmy_api_common = { path = "../api_common" } diesel = "1.4.5" actix = "0.10.0" actix-web = { version = "3.3.2", default-features = false, features = ["rustls"] } diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 6fc370ed3..9181a129b 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -2,7 +2,7 @@ use actix_web::{error::ErrorBadRequest, *}; use anyhow::anyhow; use chrono::{DateTime, NaiveDateTime, Utc}; use diesel::PgConnection; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_queries::{ source::{community::Community_, person::Person_}, Crud, diff --git a/crates/routes/src/nodeinfo.rs b/crates/routes/src/nodeinfo.rs index 1333279cc..d06f60928 100644 --- a/crates/routes/src/nodeinfo.rs +++ b/crates/routes/src/nodeinfo.rs @@ -1,6 +1,6 @@ use actix_web::{body::Body, error::ErrorBadRequest, *}; use anyhow::anyhow; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_db_views::site_view::SiteView; use lemmy_utils::{settings::structs::Settings, version, LemmyError}; use lemmy_websocket::LemmyContext; diff --git a/crates/routes/src/webfinger.rs b/crates/routes/src/webfinger.rs index 8ab2a5b6a..82c993daf 100644 --- a/crates/routes/src/webfinger.rs +++ b/crates/routes/src/webfinger.rs @@ -1,6 +1,6 @@ use actix_web::{error::ErrorBadRequest, web::Query, *}; use anyhow::anyhow; -use lemmy_api_structs::{blocking, WebFingerLink, WebFingerResponse}; +use lemmy_api_common::{blocking, WebFingerLink, WebFingerResponse}; use lemmy_db_queries::source::{community::Community_, person::Person_}; use lemmy_db_schema::source::{community::Community, person::Person}; use lemmy_utils::{ diff --git a/crates/websocket/Cargo.toml b/crates/websocket/Cargo.toml index b957e9446..e550d2b5f 100644 --- a/crates/websocket/Cargo.toml +++ b/crates/websocket/Cargo.toml @@ -10,7 +10,7 @@ doctest = false [dependencies] lemmy_utils = { path = "../utils" } -lemmy_api_structs = { path = "../api_structs" } +lemmy_api_common = { path = "../api_common" } lemmy_db_queries = { path = "../db_queries" } lemmy_db_schema = { path = "../db_schema" } reqwest = { version = "0.10.10", features = ["json"] } diff --git a/crates/websocket/src/chat_server.rs b/crates/websocket/src/chat_server.rs index c9016a250..f1c936d6d 100644 --- a/crates/websocket/src/chat_server.rs +++ b/crates/websocket/src/chat_server.rs @@ -6,7 +6,7 @@ use diesel::{ r2d2::{ConnectionManager, Pool}, PgConnection, }; -use lemmy_api_structs::{comment::*, post::*}; +use lemmy_api_common::{comment::*, post::*}; use lemmy_db_schema::{CommunityId, LocalUserId, PostId}; use lemmy_utils::{ location_info, diff --git a/crates/websocket/src/messages.rs b/crates/websocket/src/messages.rs index a1d4396bb..31ca755f1 100644 --- a/crates/websocket/src/messages.rs +++ b/crates/websocket/src/messages.rs @@ -1,6 +1,6 @@ use crate::UserOperation; use actix::{prelude::*, Recipient}; -use lemmy_api_structs::{comment::CommentResponse, post::PostResponse}; +use lemmy_api_common::{comment::CommentResponse, post::PostResponse}; use lemmy_db_schema::{CommunityId, LocalUserId, PostId}; use lemmy_utils::{ConnectionId, IpAddr}; use serde::{Deserialize, Serialize}; diff --git a/src/main.rs b/src/main.rs index fa110b51e..3cdab3ec7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use diesel::{ PgConnection, }; use lemmy_api::match_websocket_operation; -use lemmy_api_structs::blocking; +use lemmy_api_common::blocking; use lemmy_apub::activity_queue::create_activity_queue; use lemmy_db_queries::get_database_url_from_env; use lemmy_routes::{feeds, images, nodeinfo, webfinger}; @@ -88,6 +88,7 @@ async fn main() -> Result<(), LemmyError> { .wrap(middleware::Logger::default()) .data(context) // The routes + .configure(|cfg| lemmy_api_crud::routes::config(cfg, &rate_limiter)) .configure(|cfg| lemmy_api::routes::config(cfg, &rate_limiter)) .configure(lemmy_apub::routes::config) .configure(feeds::config)