X-Git-Url: http://these/git/?a=blobdiff_plain;f=crates%2Fapi_crud%2Fsrc%2Fpost%2Fcreate.rs;h=264cdbc829d9cdff851b538562ccc6aedcf826d6;hb=3471f3533cb724b2cf6953d563aadfcc9f66c1d2;hp=b2034a36741aa3c40fe3ffad0192567316e62b16;hpb=bb7750d8ee8616854acdcf28c563d41b360efdce;p=lemmy.git diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index b2034a36..264cdbc8 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -1,131 +1,200 @@ -use crate::PerformCrud; -use actix_web::web::Data; +use activitypub_federation::config::Data; +use actix_web::web::Json; use lemmy_api_common::{ - blocking, - check_community_ban, - get_local_user_view_from_jwt, - mark_post_as_read, - post::*, + build_response::build_post_response, + context::LemmyContext, + post::{CreatePost, PostResponse}, + request::fetch_site_data, + send_activity::{ActivityChannel, SendActivityData}, + utils::{ + check_community_ban, + check_community_deleted_or_removed, + generate_local_apub_endpoint, + honeypot_check, + local_site_to_slur_regex, + local_user_view_from_jwt, + mark_post_as_read, + sanitize_html, + sanitize_html_opt, + EndpointType, + }, }; -use lemmy_apub::{ - activities::post::create::CreatePost as CreateApubPost, - generate_apub_endpoint, - ApubLikeableType, - EndpointType, +use lemmy_db_schema::{ + impls::actor_language::default_post_language, + source::{ + actor_language::CommunityLanguage, + community::Community, + local_site::LocalSite, + post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm}, + }, + traits::{Crud, Likeable}, }; -use lemmy_db_queries::{source::post::Post_, Crud, Likeable}; -use lemmy_db_schema::source::post::*; -use lemmy_db_views::post_view::PostView; +use lemmy_db_views_actor::structs::CommunityView; use lemmy_utils::{ - request::fetch_iframely_and_pictrs_data, - utils::{check_slurs, check_slurs_opt, clean_url_params, is_valid_post_title}, - ApiError, - ConnectionId, - LemmyError, + error::{LemmyError, LemmyErrorExt, LemmyErrorType}, + spawn_try_task, + utils::{ + slurs::{check_slurs, check_slurs_opt}, + validation::{check_url_scheme, clean_url_params, is_valid_body_field, is_valid_post_title}, + }, + SYNCHRONOUS_FEDERATION, }; -use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperationCrud}; - -#[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()); +use tracing::Instrument; +use url::Url; +use webmention::{Webmention, WebmentionError}; + +#[tracing::instrument(skip(context))] +pub async fn create_post( + data: Json, + context: Data, +) -> Result, LemmyError> { + let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?; + let local_site = LocalSite::read(&mut context.pool()).await?; + + let slur_regex = local_site_to_slur_regex(&local_site); + check_slurs(&data.name, &slur_regex)?; + check_slurs_opt(&data.body, &slur_regex)?; + honeypot_check(&data.honeypot)?; + + let data_url = data.url.as_ref(); + let url = data_url.map(clean_url_params).map(Into::into); // TODO no good way to handle a "clear" + + is_valid_post_title(&data.name)?; + is_valid_body_field(&data.body, true)?; + check_url_scheme(&data.url)?; + + check_community_ban( + local_user_view.person.id, + data.community_id, + &mut context.pool(), + ) + .await?; + check_community_deleted_or_removed(data.community_id, &mut context.pool()).await?; + + let community_id = data.community_id; + let community = Community::read(&mut context.pool(), community_id).await?; + if community.posting_restricted_to_mods { + let community_id = data.community_id; + let is_mod = CommunityView::is_mod_or_admin( + &mut context.pool(), + local_user_view.local_user.person_id, + community_id, + ) + .await?; + if !is_mod { + return Err(LemmyErrorType::OnlyModsCanPostInCommunity)?; } + } - 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| clean_url_params(u.to_owned()).into()), - body: data.body.to_owned(), - community_id: data.community_id, - creator_id: local_user_view.person.id, - nsfw: data.nsfw, - embed_title: iframely_title, - embed_description: iframely_description, - embed_html: iframely_html, - thumbnail_url: pictrs_thumbnail.map(|u| u.into()), - ..PostForm::default() - }; - - 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 = 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? - .map_err(|_| ApiError::err("couldnt_create_post"))?; - - CreateApubPost::send(&updated_post, &local_user_view.person, context).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 { - post_id, - person_id, - score: 1, + // Fetch post links and pictrs cached image + let (metadata_res, thumbnail_url) = + fetch_site_data(context.client(), context.settings(), data_url, true).await; + let (embed_title, embed_description, embed_video_url) = metadata_res + .map(|u| (u.title, u.description, u.embed_video_url)) + .unwrap_or_default(); + + let name = sanitize_html(data.name.trim()); + let body = sanitize_html_opt(&data.body); + let embed_title = sanitize_html_opt(&embed_title); + let embed_description = sanitize_html_opt(&embed_description); + + // Only need to check if language is allowed in case user set it explicitly. When using default + // language, it already only returns allowed languages. + CommunityLanguage::is_allowed_community_language( + &mut context.pool(), + data.language_id, + community_id, + ) + .await?; + + // attempt to set default language if none was provided + let language_id = match data.language_id { + Some(lid) => Some(lid), + None => { + default_post_language( + &mut context.pool(), + community_id, + local_user_view.local_user.id, + ) + .await? + } + }; + + let post_form = PostInsertForm::builder() + .name(name) + .url(url) + .body(body) + .community_id(data.community_id) + .creator_id(local_user_view.person.id) + .nsfw(data.nsfw) + .embed_title(embed_title) + .embed_description(embed_description) + .embed_video_url(embed_video_url) + .language_id(language_id) + .thumbnail_url(thumbnail_url) + .build(); + + let inserted_post = Post::create(&mut context.pool(), &post_form) + .await + .with_lemmy_type(LemmyErrorType::CouldntCreatePost)?; + + let inserted_post_id = inserted_post.id; + let protocol_and_hostname = context.settings().get_protocol_and_hostname(); + let apub_id = generate_local_apub_endpoint( + EndpointType::Post, + &inserted_post_id.to_string(), + &protocol_and_hostname, + )?; + let updated_post = Post::update( + &mut context.pool(), + inserted_post_id, + &PostUpdateForm::builder().ap_id(Some(apub_id)).build(), + ) + .await + .with_lemmy_type(LemmyErrorType::CouldntCreatePost)?; + + // 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 { + post_id, + person_id, + score: 1, + }; + + PostLike::like(&mut context.pool(), &like_form) + .await + .with_lemmy_type(LemmyErrorType::CouldntLikePost)?; + + ActivityChannel::submit_activity(SendActivityData::CreatePost(updated_post.clone()), &context) + .await?; + + // Mark the post as read + mark_post_as_read(person_id, post_id, &mut context.pool()).await?; + + if let Some(url) = updated_post.url.clone() { + let task = async move { + let mut webmention = + Webmention::new::(updated_post.ap_id.clone().into(), url.clone().into())?; + webmention.set_checked(true); + match webmention + .send() + .instrument(tracing::info_span!("Sending webmention")) + .await + { + Err(WebmentionError::NoEndpointDiscovered(_)) => Ok(()), + Ok(_) => Ok(()), + Err(e) => Err(e).with_lemmy_type(LemmyErrorType::CouldntSendWebmention), + } }; - - 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()); + if *SYNCHRONOUS_FEDERATION { + task.await?; + } else { + spawn_try_task(task); } + }; - // Mark the post as read - mark_post_as_read(person_id, post_id, context.pool()).await?; - - updated_post - .send_like(&local_user_view.person, context) - .await?; - - // Refetch the view - let inserted_post_id = inserted_post.id; - let post_view = blocking(context.pool(), move |conn| { - PostView::read(conn, inserted_post_id, Some(local_user_view.person.id)) - }) - .await? - .map_err(|_| ApiError::err("couldnt_find_post"))?; - - let res = PostResponse { post_view }; - - context.chat_server().do_send(SendPost { - op: UserOperationCrud::CreatePost, - post: res.clone(), - websocket_id, - }); - - Ok(res) - } + Ok(Json( + build_post_response(&context, community_id, person_id, post_id).await?, + )) }