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