]> Untitled Git - lemmy.git/blob - crates/api/src/post.rs
Don't drop error context when adding a message to errors (#1958)
[lemmy.git] / crates / api / src / post.rs
1 use crate::Perform;
2 use actix_web::web::Data;
3 use lemmy_api_common::{
4   blocking,
5   check_community_ban,
6   check_community_deleted_or_removed,
7   check_downvotes_enabled,
8   check_person_block,
9   get_local_user_view_from_jwt,
10   is_mod_or_admin,
11   mark_post_as_read,
12   mark_post_as_unread,
13   post::*,
14 };
15 use lemmy_apub::{
16   fetcher::post_or_comment::PostOrComment,
17   objects::post::ApubPost,
18   protocol::activities::{
19     create_or_update::post::CreateOrUpdatePost,
20     voting::{
21       undo_vote::UndoVote,
22       vote::{Vote, VoteType},
23     },
24     CreateOrUpdateType,
25   },
26 };
27 use lemmy_db_schema::{
28   source::{moderator::*, post::*},
29   traits::{Crud, Likeable, Saveable},
30 };
31 use lemmy_db_views::post_view::PostView;
32 use lemmy_utils::{request::fetch_site_metadata, ConnectionId, LemmyError};
33 use lemmy_websocket::{send::send_post_ws_message, LemmyContext, UserOperation};
34 use std::convert::TryInto;
35
36 #[async_trait::async_trait(?Send)]
37 impl Perform for CreatePostLike {
38   type Response = PostResponse;
39
40   #[tracing::instrument(skip(context, websocket_id))]
41   async fn perform(
42     &self,
43     context: &Data<LemmyContext>,
44     websocket_id: Option<ConnectionId>,
45   ) -> Result<PostResponse, LemmyError> {
46     let data: &CreatePostLike = self;
47     let local_user_view =
48       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
49
50     // Don't do a downvote if site has downvotes disabled
51     check_downvotes_enabled(data.score, context.pool()).await?;
52
53     // Check for a community ban
54     let post_id = data.post_id;
55     let post: ApubPost = blocking(context.pool(), move |conn| Post::read(conn, post_id))
56       .await??
57       .into();
58
59     check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
60     check_community_deleted_or_removed(post.community_id, context.pool()).await?;
61
62     check_person_block(local_user_view.person.id, post.creator_id, context.pool()).await?;
63
64     let like_form = PostLikeForm {
65       post_id: data.post_id,
66       person_id: local_user_view.person.id,
67       score: data.score,
68     };
69
70     // Remove any likes first
71     let person_id = local_user_view.person.id;
72     blocking(context.pool(), move |conn| {
73       PostLike::remove(conn, person_id, post_id)
74     })
75     .await??;
76
77     let community_id = post.community_id;
78     let object = PostOrComment::Post(Box::new(post));
79
80     // Only add the like if the score isnt 0
81     let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
82     if do_add {
83       let like_form2 = like_form.clone();
84       let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
85       blocking(context.pool(), like)
86         .await?
87         .map_err(LemmyError::from)
88         .map_err(|e| e.with_message("couldnt_like_post"))?;
89
90       Vote::send(
91         &object,
92         &local_user_view.person.clone().into(),
93         community_id,
94         like_form.score.try_into()?,
95         context,
96       )
97       .await?;
98     } else {
99       // API doesn't distinguish between Undo/Like and Undo/Dislike
100       UndoVote::send(
101         &object,
102         &local_user_view.person.clone().into(),
103         community_id,
104         VoteType::Like,
105         context,
106       )
107       .await?;
108     }
109
110     // Mark the post as read
111     mark_post_as_read(person_id, post_id, context.pool()).await?;
112
113     send_post_ws_message(
114       data.post_id,
115       UserOperation::CreatePostLike,
116       websocket_id,
117       Some(local_user_view.person.id),
118       context,
119     )
120     .await
121   }
122 }
123
124 #[async_trait::async_trait(?Send)]
125 impl Perform for MarkPostAsRead {
126   type Response = PostResponse;
127
128   #[tracing::instrument(skip(context, _websocket_id))]
129   async fn perform(
130     &self,
131     context: &Data<LemmyContext>,
132     _websocket_id: Option<ConnectionId>,
133   ) -> Result<Self::Response, LemmyError> {
134     let data = self;
135     let local_user_view =
136       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
137
138     let post_id = data.post_id;
139     let person_id = local_user_view.person.id;
140
141     // Mark the post as read / unread
142     if data.read {
143       mark_post_as_read(person_id, post_id, context.pool()).await?;
144     } else {
145       mark_post_as_unread(person_id, post_id, context.pool()).await?;
146     }
147
148     // Fetch it
149     let post_view = blocking(context.pool(), move |conn| {
150       PostView::read(conn, post_id, Some(person_id))
151     })
152     .await??;
153
154     let res = Self::Response { post_view };
155
156     Ok(res)
157   }
158 }
159
160 #[async_trait::async_trait(?Send)]
161 impl Perform for LockPost {
162   type Response = PostResponse;
163
164   #[tracing::instrument(skip(context, websocket_id))]
165   async fn perform(
166     &self,
167     context: &Data<LemmyContext>,
168     websocket_id: Option<ConnectionId>,
169   ) -> Result<PostResponse, LemmyError> {
170     let data: &LockPost = self;
171     let local_user_view =
172       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
173
174     let post_id = data.post_id;
175     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
176
177     check_community_ban(
178       local_user_view.person.id,
179       orig_post.community_id,
180       context.pool(),
181     )
182     .await?;
183     check_community_deleted_or_removed(orig_post.community_id, context.pool()).await?;
184
185     // Verify that only the mods can lock
186     is_mod_or_admin(
187       context.pool(),
188       local_user_view.person.id,
189       orig_post.community_id,
190     )
191     .await?;
192
193     // Update the post
194     let post_id = data.post_id;
195     let locked = data.locked;
196     let updated_post: ApubPost = blocking(context.pool(), move |conn| {
197       Post::update_locked(conn, post_id, locked)
198     })
199     .await??
200     .into();
201
202     // Mod tables
203     let form = ModLockPostForm {
204       mod_person_id: local_user_view.person.id,
205       post_id: data.post_id,
206       locked: Some(locked),
207     };
208     blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
209
210     // apub updates
211     CreateOrUpdatePost::send(
212       updated_post,
213       &local_user_view.person.clone().into(),
214       CreateOrUpdateType::Update,
215       context,
216     )
217     .await?;
218
219     send_post_ws_message(
220       data.post_id,
221       UserOperation::LockPost,
222       websocket_id,
223       Some(local_user_view.person.id),
224       context,
225     )
226     .await
227   }
228 }
229
230 #[async_trait::async_trait(?Send)]
231 impl Perform for StickyPost {
232   type Response = PostResponse;
233
234   #[tracing::instrument(skip(context, websocket_id))]
235   async fn perform(
236     &self,
237     context: &Data<LemmyContext>,
238     websocket_id: Option<ConnectionId>,
239   ) -> Result<PostResponse, LemmyError> {
240     let data: &StickyPost = self;
241     let local_user_view =
242       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
243
244     let post_id = data.post_id;
245     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
246
247     check_community_ban(
248       local_user_view.person.id,
249       orig_post.community_id,
250       context.pool(),
251     )
252     .await?;
253     check_community_deleted_or_removed(orig_post.community_id, context.pool()).await?;
254
255     // Verify that only the mods can sticky
256     is_mod_or_admin(
257       context.pool(),
258       local_user_view.person.id,
259       orig_post.community_id,
260     )
261     .await?;
262
263     // Update the post
264     let post_id = data.post_id;
265     let stickied = data.stickied;
266     let updated_post: ApubPost = blocking(context.pool(), move |conn| {
267       Post::update_stickied(conn, post_id, stickied)
268     })
269     .await??
270     .into();
271
272     // Mod tables
273     let form = ModStickyPostForm {
274       mod_person_id: local_user_view.person.id,
275       post_id: data.post_id,
276       stickied: Some(stickied),
277     };
278     blocking(context.pool(), move |conn| {
279       ModStickyPost::create(conn, &form)
280     })
281     .await??;
282
283     // Apub updates
284     // TODO stickied should pry work like locked for ease of use
285     CreateOrUpdatePost::send(
286       updated_post,
287       &local_user_view.person.clone().into(),
288       CreateOrUpdateType::Update,
289       context,
290     )
291     .await?;
292
293     send_post_ws_message(
294       data.post_id,
295       UserOperation::StickyPost,
296       websocket_id,
297       Some(local_user_view.person.id),
298       context,
299     )
300     .await
301   }
302 }
303
304 #[async_trait::async_trait(?Send)]
305 impl Perform for SavePost {
306   type Response = PostResponse;
307
308   #[tracing::instrument(skip(context, _websocket_id))]
309   async fn perform(
310     &self,
311     context: &Data<LemmyContext>,
312     _websocket_id: Option<ConnectionId>,
313   ) -> Result<PostResponse, LemmyError> {
314     let data: &SavePost = self;
315     let local_user_view =
316       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
317
318     let post_saved_form = PostSavedForm {
319       post_id: data.post_id,
320       person_id: local_user_view.person.id,
321     };
322
323     if data.save {
324       let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
325       blocking(context.pool(), save)
326         .await?
327         .map_err(LemmyError::from)
328         .map_err(|e| e.with_message("couldnt_save_post"))?;
329     } else {
330       let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
331       blocking(context.pool(), unsave)
332         .await?
333         .map_err(LemmyError::from)
334         .map_err(|e| e.with_message("couldnt_save_post"))?;
335     }
336
337     let post_id = data.post_id;
338     let person_id = local_user_view.person.id;
339     let post_view = blocking(context.pool(), move |conn| {
340       PostView::read(conn, post_id, Some(person_id))
341     })
342     .await??;
343
344     // Mark the post as read
345     mark_post_as_read(person_id, post_id, context.pool()).await?;
346
347     Ok(PostResponse { post_view })
348   }
349 }
350
351 #[async_trait::async_trait(?Send)]
352 impl Perform for GetSiteMetadata {
353   type Response = GetSiteMetadataResponse;
354
355   #[tracing::instrument(skip(context, _websocket_id))]
356   async fn perform(
357     &self,
358     context: &Data<LemmyContext>,
359     _websocket_id: Option<ConnectionId>,
360   ) -> Result<GetSiteMetadataResponse, LemmyError> {
361     let data: &Self = self;
362
363     let metadata = fetch_site_metadata(context.client(), &data.url).await?;
364
365     Ok(GetSiteMetadataResponse { metadata })
366   }
367 }