From d9905824eba7ad962fa384d793ad293f0377cde8 Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:50:20 -0500 Subject: [PATCH] Add some communities sub scopes --- crates/api/src/community/add_mod.rs | 14 ++-- crates/api/src/community/ban.rs | 15 ++-- crates/api/src/community/transfer.rs | 10 +-- crates/api_crud/src/post/create.rs | 121 +++++++++++++++++++++++++-- crates/apub/src/api/list_posts.rs | 88 ++++++++++++++++++- src/api_routes_v4.rs | 27 ++++-- 6 files changed, 239 insertions(+), 36 deletions(-) diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs index f781ab4a3..5f31f472b 100644 --- a/crates/api/src/community/add_mod.rs +++ b/crates/api/src/community/add_mod.rs @@ -7,7 +7,7 @@ use lemmy_api_common::{ utils::check_community_mod_action, }; use lemmy_db_schema::{ - newtypes::CommunityId, + newtypes::{CommunityId, PersonId}, source::{ community::{Community, CommunityModerator, CommunityModeratorForm}, local_user::LocalUser, @@ -24,9 +24,9 @@ pub async fn add_mod_to_community( data: Json, context: Data, local_user_view: LocalUserView, - path: Path, + path: Path<(CommunityId, PersonId)>, ) -> LemmyResult> { - let community_id = path.into_inner(); + let (community_id, person_id) = path.into_inner(); let community = Community::read(&mut context.pool(), community_id).await?; // Verify that only mods or admins can add mod check_community_mod_action( @@ -43,7 +43,7 @@ pub async fn add_mod_to_community( &mut context.pool(), community_id, local_user_view.person.id, - vec![data.person_id], + vec![person_id], ) .await?; } @@ -63,7 +63,7 @@ pub async fn add_mod_to_community( // Update in local database let community_moderator_form = CommunityModeratorForm { community_id, - person_id: data.person_id, + person_id, }; if data.added { CommunityModerator::join(&mut context.pool(), &community_moderator_form) @@ -78,7 +78,7 @@ pub async fn add_mod_to_community( // Mod tables let form = ModAddCommunityForm { mod_person_id: local_user_view.person.id, - other_person_id: data.person_id, + other_person_id: person_id, community_id, removed: Some(!data.added), }; @@ -93,7 +93,7 @@ pub async fn add_mod_to_community( SendActivityData::AddModToCommunity { moderator: local_user_view.person, community_id, - target: data.person_id, + target: person_id, added: data.added, }, &context, diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index 65ea21ba2..1355fbdce 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -9,7 +9,7 @@ use lemmy_api_common::{ }, }; use lemmy_db_schema::{ - newtypes::CommunityId, + newtypes::{CommunityId, PersonId}, source::{ community::{ Community, CommunityFollower, CommunityFollowerForm, CommunityPersonBan, @@ -32,10 +32,9 @@ pub async fn ban_from_community( data: Json, context: Data, local_user_view: LocalUserView, - path: Path, + path: Path<(CommunityId, PersonId)>, ) -> LemmyResult> { - let community_id = path.into_inner(); - let banned_person_id = data.person_id; + let (community_id, banned_person_id) = path.into_inner(); let expires = check_expire_time(data.expires)?; let community = Community::read(&mut context.pool(), community_id).await?; @@ -52,7 +51,7 @@ pub async fn ban_from_community( &mut context.pool(), community_id, local_user_view.person.id, - vec![data.person_id], + vec![banned_person_id], ) .await?; @@ -62,7 +61,7 @@ pub async fn ban_from_community( let community_user_ban_form = CommunityPersonBanForm { community_id, - person_id: data.person_id, + person_id: banned_person_id, expires: Some(expires), }; @@ -99,7 +98,7 @@ pub async fn ban_from_community( // Mod tables let form = ModBanFromCommunityForm { mod_person_id: local_user_view.person.id, - other_person_id: data.person_id, + other_person_id: banned_person_id, community_id, reason: data.reason.clone(), banned: Some(data.ban), @@ -108,7 +107,7 @@ pub async fn ban_from_community( ModBanFromCommunity::create(&mut context.pool(), &form).await?; - let person_view = PersonView::read(&mut context.pool(), data.person_id, false).await?; + let person_view = PersonView::read(&mut context.pool(), banned_person_id, false).await?; ActivityChannel::submit_activity( SendActivityData::BanFromCommunity { diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs index cb7916857..e597abf0b 100644 --- a/crates/api/src/community/transfer.rs +++ b/crates/api/src/community/transfer.rs @@ -6,7 +6,7 @@ use lemmy_api_common::{ utils::{check_community_user_action, is_admin, is_top_mod}, }; use lemmy_db_schema::{ - newtypes::CommunityId, + newtypes::{CommunityId, PersonId}, source::{ community::{Community, CommunityModerator, CommunityModeratorForm}, mod_log::moderator::{ModTransferCommunity, ModTransferCommunityForm}, @@ -27,9 +27,9 @@ pub async fn transfer_community( data: Json, context: Data, local_user_view: LocalUserView, - path: Path, + path: Path<(CommunityId, PersonId)>, ) -> LemmyResult> { - let community_id = path.into_inner(); + let (community_id, person_id) = path.into_inner(); let community = Community::read(&mut context.pool(), community_id).await?; let mut community_mods = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; @@ -46,7 +46,7 @@ pub async fn transfer_community( // Add the transferee to the top let creator_index = community_mods .iter() - .position(|r| r.moderator.id == data.person_id) + .position(|r| r.moderator.id == person_id) .context(location_info!())?; let creator_person = community_mods.remove(creator_index); community_mods.insert(0, creator_person); @@ -70,7 +70,7 @@ pub async fn transfer_community( // Mod tables let form = ModTransferCommunityForm { mod_person_id: local_user_view.person.id, - other_person_id: data.person_id, + other_person_id: person_id, community_id, }; diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 948a7617e..76d007e03 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -1,6 +1,6 @@ use super::convert_published_time; use activitypub_federation::config::Data; -use actix_web::web::Json; +use actix_web::web::{Json, Path}; use lemmy_api_common::{ build_response::build_post_response, context::LemmyContext, @@ -8,15 +8,13 @@ use lemmy_api_common::{ request::generate_post_link_metadata, send_activity::SendActivityData, utils::{ - check_community_user_action, - get_url_blocklist, - honeypot_check, - local_site_to_slur_regex, + check_community_user_action, get_url_blocklist, honeypot_check, local_site_to_slur_regex, process_markdown_opt, }, }; use lemmy_db_schema::{ impls::actor_language::validate_post_language, + newtypes::CommunityId, source::{ community::Community, local_site::LocalSite, @@ -34,10 +32,7 @@ use lemmy_utils::{ utils::{ slurs::check_slurs, validation::{ - is_url_blocked, - is_valid_alt_text_field, - is_valid_body_field, - is_valid_post_title, + is_url_blocked, is_valid_alt_text_field, is_valid_body_field, is_valid_post_title, is_valid_url, }, }, @@ -46,6 +41,114 @@ use tracing::Instrument; use url::Url; use webmention::{Webmention, WebmentionError}; +#[tracing::instrument(skip(context))] +pub async fn create_post_for_community( + data: Json, + context: Data, + local_user_view: LocalUserView, + path: Path, +) -> LemmyResult> { + let community_id = path.into_inner(); + let local_site = LocalSite::read(&mut context.pool()).await?; + + honeypot_check(&data.honeypot)?; + + let slur_regex = local_site_to_slur_regex(&local_site); + check_slurs(&data.name, &slur_regex)?; + let url_blocklist = get_url_blocklist(&context).await?; + + let body = process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context).await?; + let url = diesel_url_create(data.url.as_deref())?; + let custom_thumbnail = diesel_url_create(data.custom_thumbnail.as_deref())?; + + is_valid_post_title(&data.name)?; + + if let Some(url) = &url { + is_url_blocked(url, &url_blocklist)?; + is_valid_url(url)?; + } + + if let Some(custom_thumbnail) = &custom_thumbnail { + is_valid_url(custom_thumbnail)?; + } + + if let Some(alt_text) = &data.alt_text { + is_valid_alt_text_field(alt_text)?; + } + + if let Some(body) = &body { + is_valid_body_field(body, true)?; + } + + let community = Community::read(&mut context.pool(), community_id).await?; + check_community_user_action(&local_user_view.person, &community, &mut context.pool()).await?; + + if community.posting_restricted_to_mods { + CommunityModeratorView::check_is_community_moderator( + &mut context.pool(), + community_id, + local_user_view.local_user.person_id, + ) + .await?; + } + + let language_id = validate_post_language( + &mut context.pool(), + data.language_id, + community_id, + local_user_view.local_user.id, + ) + .await?; + + let scheduled_publish_time = + convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?; + let post_form = PostInsertForm { + url: url.map(Into::into), + body, + alt_text: data.alt_text.clone(), + nsfw: data.nsfw, + language_id: Some(language_id), + scheduled_publish_time, + ..PostInsertForm::new( + data.name.trim().to_string(), + local_user_view.person.id, + community_id, + ) + }; + + let inserted_post = Post::create(&mut context.pool(), &post_form) + .await + .with_lemmy_type(LemmyErrorType::CouldntCreatePost)?; + + let federate_post = if scheduled_publish_time.is_none() { + send_webmention(inserted_post.clone(), community); + |post| Some(SendActivityData::CreatePost(post)) + } else { + |_| None + }; + generate_post_link_metadata( + inserted_post.clone(), + custom_thumbnail.map(Into::into), + federate_post, + context.reset_request_count(), + ) + .await?; + + // They like their own post by default + let person_id = local_user_view.person.id; + let post_id = inserted_post.id; + let like_form = PostLikeForm::new(post_id, person_id, 1); + + PostLike::like(&mut context.pool(), &like_form) + .await + .with_lemmy_type(LemmyErrorType::CouldntLikePost)?; + + let read_form = PostReadForm::new(post_id, person_id); + PostRead::mark_as_read(&mut context.pool(), &read_form).await?; + + build_post_response(&context, community_id, local_user_view, post_id).await +} + #[tracing::instrument(skip(context))] pub async fn create_post( data: Json, diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index 63e737fdd..a0206aa63 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -4,14 +4,14 @@ use crate::{ objects::community::ApubCommunity, }; use activitypub_federation::config::Data; -use actix_web::web::{Json, Query}; +use actix_web::web::{Json, Path, Query}; use lemmy_api_common::{ context::LemmyContext, post::{GetPosts, GetPostsResponse}, utils::{check_conflicting_like_filters, check_private_instance}, }; use lemmy_db_schema::{ - newtypes::PostId, + newtypes::{CommunityId, PostId}, source::{community::Community, post::PostRead}, }; use lemmy_db_views::{ @@ -20,6 +20,90 @@ use lemmy_db_views::{ }; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; +// API v4 TODO This is copy-pasted from list_posts. Refactor when API stabilizied. +#[tracing::instrument(skip(context))] +pub async fn list_posts_for_community( + data: Query, + context: Data, + local_user_view: Option, + path: Path, +) -> LemmyResult> { + let community_id = Some(path.into_inner()); + let local_site = SiteView::read_local(&mut context.pool()).await?; + + check_private_instance(&local_user_view, &local_site.local_site)?; + + let page = data.page; + let limit = data.limit; + + let saved_only = data.saved_only; + let show_hidden = data.show_hidden; + let show_read = data.show_read; + let show_nsfw = data.show_nsfw; + let no_comments_only = data.no_comments_only; + + let liked_only = data.liked_only; + let disliked_only = data.disliked_only; + check_conflicting_like_filters(liked_only, disliked_only)?; + + let local_user = local_user_view.as_ref().map(|u| &u.local_user); + let listing_type = Some(listing_type_with_default( + data.type_, + local_user, + &local_site.local_site, + community_id, + )); + + let sort = Some(post_sort_type_with_default( + data.sort, + local_user, + &local_site.local_site, + )); + + // parse pagination token + let page_after = if let Some(pa) = &data.page_cursor { + Some(pa.read(&mut context.pool()).await?) + } else { + None + }; + + let posts = PostQuery { + local_user, + listing_type, + sort, + community_id, + saved_only, + liked_only, + disliked_only, + page, + page_after, + limit, + show_hidden, + show_read, + show_nsfw, + no_comments_only, + ..Default::default() + } + .list(&local_site.site, &mut context.pool()) + .await + .with_lemmy_type(LemmyErrorType::CouldntGetPosts)?; + + // If in their user settings (or as part of the API request), auto-mark fetched posts as read + if let Some(local_user) = local_user { + if data + .mark_as_read + .unwrap_or(local_user.auto_mark_fetched_posts_as_read) + { + let post_ids = posts.iter().map(|p| p.post.id).collect::>(); + PostRead::mark_many_as_read(&mut context.pool(), &post_ids, local_user.person_id).await?; + } + } + + // if this page wasn't empty, then there is a next page after the last post on this page + let next_page = posts.last().map(PaginationCursor::after_post); + Ok(Json(GetPostsResponse { posts, next_page })) +} + #[tracing::instrument(skip(context))] pub async fn list_posts( data: Query, diff --git a/src/api_routes_v4.rs b/src/api_routes_v4.rs index 074a564c9..3e634a35f 100644 --- a/src/api_routes_v4.rs +++ b/src/api_routes_v4.rs @@ -93,7 +93,10 @@ use lemmy_api_crud::{ create::create_oauth_provider, delete::delete_oauth_provider, update::update_oauth_provider, }, post::{ - create::create_post, delete::delete_post, read::get_post, remove::remove_post, + create::{create_post, create_post_for_community}, + delete::delete_post, + read::get_post, + remove::remove_post, update::update_post, }, private_message::{ @@ -112,7 +115,7 @@ use lemmy_api_crud::{ }; use lemmy_apub::api::{ list_comments::list_comments, - list_posts::list_posts, + list_posts::{list_posts, list_posts_for_community}, read_community::get_community, read_person::read_person, resolve_object::resolve_object, @@ -161,9 +164,23 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/follow", post().to(follow_community)) // Mod Actions .route("/remove", post().to(remove_community)) - .route("/transfer", post().to(transfer_community)) - .route("/ban_user", post().to(ban_from_community)) - .route("/mod", post().to(add_mod_to_community)) + .service( + scope("/posts") + .route("", get().to(list_posts_for_community)) + .service( + // Handle POST to /post separately to add the post() rate limitter + resource("") + .guard(guard::Post()) + .wrap(rate_limit.post()) + .route(post().to(create_post_for_community)), + ), + ) + .service( + scope("/users/{person_id}") + .route("/transfer-moderation", post().to(transfer_community)) + .route("/ban", post().to(ban_from_community)) + .route("/appoint-mod", post().to(add_mod_to_community)), + ) .service( // TODO: Not sure what to do with these scope("/pending_follows")