]> Untitled Git - lemmy.git/blob - crates/api_crud/src/post/create.rs
458fdb24827d5e1d0711e63a3b7d72a367d2c653
[lemmy.git] / crates / api_crud / src / post / create.rs
1 use activitypub_federation::config::Data;
2 use actix_web::web::Json;
3 use lemmy_api_common::{
4   build_response::build_post_response,
5   context::LemmyContext,
6   post::{CreatePost, PostResponse},
7   request::fetch_site_data,
8   send_activity::{ActivityChannel, SendActivityData},
9   utils::{
10     check_community_ban,
11     check_community_deleted_or_removed,
12     generate_local_apub_endpoint,
13     honeypot_check,
14     local_site_to_slur_regex,
15     local_user_view_from_jwt,
16     mark_post_as_read,
17     EndpointType,
18   },
19 };
20 use lemmy_db_schema::{
21   impls::actor_language::default_post_language,
22   source::{
23     actor_language::CommunityLanguage,
24     community::Community,
25     local_site::LocalSite,
26     post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm},
27   },
28   traits::{Crud, Likeable},
29 };
30 use lemmy_db_views_actor::structs::CommunityView;
31 use lemmy_utils::{
32   error::{LemmyError, LemmyErrorExt, LemmyErrorType},
33   spawn_try_task,
34   utils::{
35     slurs::{check_slurs, check_slurs_opt},
36     validation::{check_url_scheme, clean_url_params, is_valid_body_field, is_valid_post_title},
37   },
38   SYNCHRONOUS_FEDERATION,
39 };
40 use tracing::Instrument;
41 use url::Url;
42 use webmention::{Webmention, WebmentionError};
43
44 #[tracing::instrument(skip(context))]
45 pub async fn create_post(
46   data: Json<CreatePost>,
47   context: Data<LemmyContext>,
48 ) -> Result<Json<PostResponse>, LemmyError> {
49   let local_user_view = local_user_view_from_jwt(&data.auth, &context).await?;
50   let local_site = LocalSite::read(&mut context.pool()).await?;
51
52   let slur_regex = local_site_to_slur_regex(&local_site);
53   check_slurs(&data.name, &slur_regex)?;
54   check_slurs_opt(&data.body, &slur_regex)?;
55   honeypot_check(&data.honeypot)?;
56
57   let data_url = data.url.as_ref();
58   let url = data_url.map(clean_url_params).map(Into::into); // TODO no good way to handle a "clear"
59
60   is_valid_post_title(&data.name)?;
61   is_valid_body_field(&data.body, true)?;
62   check_url_scheme(&data.url)?;
63
64   check_community_ban(
65     local_user_view.person.id,
66     data.community_id,
67     &mut context.pool(),
68   )
69   .await?;
70   check_community_deleted_or_removed(data.community_id, &mut context.pool()).await?;
71
72   let community_id = data.community_id;
73   let community = Community::read(&mut context.pool(), community_id).await?;
74   if community.posting_restricted_to_mods {
75     let community_id = data.community_id;
76     let is_mod = CommunityView::is_mod_or_admin(
77       &mut context.pool(),
78       local_user_view.local_user.person_id,
79       community_id,
80     )
81     .await?;
82     if !is_mod {
83       return Err(LemmyErrorType::OnlyModsCanPostInCommunity)?;
84     }
85   }
86
87   // Fetch post links and pictrs cached image
88   let (metadata_res, thumbnail_url) =
89     fetch_site_data(context.client(), context.settings(), data_url, true).await;
90   let (embed_title, embed_description, embed_video_url) = metadata_res
91     .map(|u| (u.title, u.description, u.embed_video_url))
92     .unwrap_or_default();
93
94   // Only need to check if language is allowed in case user set it explicitly. When using default
95   // language, it already only returns allowed languages.
96   CommunityLanguage::is_allowed_community_language(
97     &mut context.pool(),
98     data.language_id,
99     community_id,
100   )
101   .await?;
102
103   // attempt to set default language if none was provided
104   let language_id = match data.language_id {
105     Some(lid) => Some(lid),
106     None => {
107       default_post_language(
108         &mut context.pool(),
109         community_id,
110         local_user_view.local_user.id,
111       )
112       .await?
113     }
114   };
115
116   let post_form = PostInsertForm::builder()
117     .name(data.name.trim().to_owned())
118     .url(url)
119     .body(data.body.clone())
120     .community_id(data.community_id)
121     .creator_id(local_user_view.person.id)
122     .nsfw(data.nsfw)
123     .embed_title(embed_title)
124     .embed_description(embed_description)
125     .embed_video_url(embed_video_url)
126     .language_id(language_id)
127     .thumbnail_url(thumbnail_url)
128     .build();
129
130   let inserted_post = Post::create(&mut context.pool(), &post_form)
131     .await
132     .with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
133
134   let inserted_post_id = inserted_post.id;
135   let protocol_and_hostname = context.settings().get_protocol_and_hostname();
136   let apub_id = generate_local_apub_endpoint(
137     EndpointType::Post,
138     &inserted_post_id.to_string(),
139     &protocol_and_hostname,
140   )?;
141   let updated_post = Post::update(
142     &mut context.pool(),
143     inserted_post_id,
144     &PostUpdateForm::builder().ap_id(Some(apub_id)).build(),
145   )
146   .await
147   .with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
148
149   // They like their own post by default
150   let person_id = local_user_view.person.id;
151   let post_id = inserted_post.id;
152   let like_form = PostLikeForm {
153     post_id,
154     person_id,
155     score: 1,
156   };
157
158   PostLike::like(&mut context.pool(), &like_form)
159     .await
160     .with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
161
162   ActivityChannel::submit_activity(SendActivityData::CreatePost(updated_post.clone()), &context)
163     .await?;
164
165   // Mark the post as read
166   mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
167
168   if let Some(url) = updated_post.url.clone() {
169     let task = async move {
170       let mut webmention =
171         Webmention::new::<Url>(updated_post.ap_id.clone().into(), url.clone().into())?;
172       webmention.set_checked(true);
173       match webmention
174         .send()
175         .instrument(tracing::info_span!("Sending webmention"))
176         .await
177       {
178         Err(WebmentionError::NoEndpointDiscovered(_)) => Ok(()),
179         Ok(_) => Ok(()),
180         Err(e) => Err(e).with_lemmy_type(LemmyErrorType::CouldntSendWebmention),
181       }
182     };
183     if *SYNCHRONOUS_FEDERATION {
184       task.await?;
185     } else {
186       spawn_try_task(task);
187     }
188   };
189
190   Ok(Json(
191     build_post_response(&context, community_id, person_id, post_id).await?,
192   ))
193 }