]> Untitled Git - lemmy.git/blob - crates/api_crud/src/post/create.rs
Implement webmention support (fixes #1395) (#1671)
[lemmy.git] / crates / api_crud / src / post / create.rs
1 use crate::PerformCrud;
2 use actix_web::web::Data;
3 use lemmy_api_common::{
4   blocking,
5   check_community_ban,
6   get_local_user_view_from_jwt,
7   mark_post_as_read,
8   post::*,
9 };
10 use lemmy_apub::{
11   activities::{
12     post::create_or_update::CreateOrUpdatePost,
13     voting::vote::{Vote, VoteType},
14     CreateOrUpdateType,
15   },
16   fetcher::post_or_comment::PostOrComment,
17   generate_apub_endpoint,
18   EndpointType,
19 };
20 use lemmy_db_queries::{source::post::Post_, Crud, Likeable};
21 use lemmy_db_schema::source::post::*;
22 use lemmy_utils::{
23   request::fetch_site_data,
24   utils::{check_slurs, check_slurs_opt, clean_url_params, is_valid_post_title},
25   ApiError,
26   ConnectionId,
27   LemmyError,
28 };
29 use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperationCrud};
30 use log::warn;
31 use webmention::{Webmention, WebmentionError};
32
33 #[async_trait::async_trait(?Send)]
34 impl PerformCrud for CreatePost {
35   type Response = PostResponse;
36
37   async fn perform(
38     &self,
39     context: &Data<LemmyContext>,
40     websocket_id: Option<ConnectionId>,
41   ) -> Result<PostResponse, LemmyError> {
42     let data: &CreatePost = self;
43     let local_user_view =
44       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
45
46     let slur_regex = &context.settings().slur_regex();
47     check_slurs(&data.name, slur_regex)?;
48     check_slurs_opt(&data.body, slur_regex)?;
49
50     if !is_valid_post_title(&data.name) {
51       return Err(ApiError::err("invalid_post_title").into());
52     }
53
54     check_community_ban(local_user_view.person.id, data.community_id, context.pool()).await?;
55
56     // Fetch post links and pictrs cached image
57     let data_url = data.url.as_ref();
58     let (metadata_res, pictrs_thumbnail) =
59       fetch_site_data(context.client(), &context.settings(), data_url).await;
60     let (embed_title, embed_description, embed_html) = metadata_res
61       .map(|u| (u.title, u.description, u.html))
62       .unwrap_or((None, None, None));
63
64     let post_form = PostForm {
65       name: data.name.trim().to_owned(),
66       url: data_url.map(|u| clean_url_params(u.to_owned()).into()),
67       body: data.body.to_owned(),
68       community_id: data.community_id,
69       creator_id: local_user_view.person.id,
70       nsfw: data.nsfw,
71       embed_title,
72       embed_description,
73       embed_html,
74       thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
75       ..PostForm::default()
76     };
77
78     let inserted_post =
79       match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
80         Ok(post) => post,
81         Err(e) => {
82           let err_type = if e.to_string() == "value too long for type character varying(200)" {
83             "post_title_too_long"
84           } else {
85             "couldnt_create_post"
86           };
87
88           return Err(ApiError::err(err_type).into());
89         }
90       };
91
92     let inserted_post_id = inserted_post.id;
93     let protocol_and_hostname = context.settings().get_protocol_and_hostname();
94     let updated_post = blocking(context.pool(), move |conn| -> Result<Post, LemmyError> {
95       let apub_id = generate_apub_endpoint(
96         EndpointType::Post,
97         &inserted_post_id.to_string(),
98         &protocol_and_hostname,
99       )?;
100       Ok(Post::update_ap_id(conn, inserted_post_id, apub_id)?)
101     })
102     .await?
103     .map_err(|_| ApiError::err("couldnt_create_post"))?;
104
105     CreateOrUpdatePost::send(
106       &updated_post,
107       &local_user_view.person,
108       CreateOrUpdateType::Create,
109       context,
110     )
111     .await?;
112
113     // They like their own post by default
114     let person_id = local_user_view.person.id;
115     let post_id = inserted_post.id;
116     let like_form = PostLikeForm {
117       post_id,
118       person_id,
119       score: 1,
120     };
121
122     let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
123     if blocking(context.pool(), like).await?.is_err() {
124       return Err(ApiError::err("couldnt_like_post").into());
125     }
126
127     // Mark the post as read
128     mark_post_as_read(person_id, post_id, context.pool()).await?;
129
130     if let Some(url) = &updated_post.url {
131       let mut webmention = Webmention::new(
132         updated_post.ap_id.clone().into_inner(),
133         url.clone().into_inner(),
134       )?;
135       webmention.set_checked(true);
136       match webmention.send().await {
137         Ok(_) => {}
138         Err(WebmentionError::NoEndpointDiscovered(_)) => {}
139         Err(e) => warn!("Failed to send webmention: {}", e),
140       }
141     }
142
143     let object = PostOrComment::Post(Box::new(updated_post));
144     Vote::send(
145       &object,
146       &local_user_view.person,
147       inserted_post.community_id,
148       VoteType::Like,
149       context,
150     )
151     .await?;
152
153     send_post_ws_message(
154       inserted_post.id,
155       UserOperationCrud::CreatePost,
156       websocket_id,
157       Some(local_user_view.person.id),
158       context,
159     )
160     .await
161   }
162 }