]> Untitled Git - lemmy.git/blob - crates/api/src/post.rs
0b77bb95dcf59deb299f1d620506aebec1795553
[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(LemmyError::from)
85         .map_err(|e| e.with_message("couldnt_like_post"))?;
86
87       Vote::send(
88         &object,
89         &local_user_view.person.clone().into(),
90         community_id,
91         like_form.score.try_into()?,
92         context,
93       )
94       .await?;
95     } else {
96       // API doesn't distinguish between Undo/Like and Undo/Dislike
97       UndoVote::send(
98         &object,
99         &local_user_view.person.clone().into(),
100         community_id,
101         VoteType::Like,
102         context,
103       )
104       .await?;
105     }
106
107     // Mark the post as read
108     mark_post_as_read(person_id, post_id, context.pool()).await?;
109
110     send_post_ws_message(
111       data.post_id,
112       UserOperation::CreatePostLike,
113       websocket_id,
114       Some(local_user_view.person.id),
115       context,
116     )
117     .await
118   }
119 }
120
121 #[async_trait::async_trait(?Send)]
122 impl Perform for MarkPostAsRead {
123   type Response = PostResponse;
124
125   #[tracing::instrument(skip(context, _websocket_id))]
126   async fn perform(
127     &self,
128     context: &Data<LemmyContext>,
129     _websocket_id: Option<ConnectionId>,
130   ) -> Result<Self::Response, LemmyError> {
131     let data = self;
132     let local_user_view =
133       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
134
135     let post_id = data.post_id;
136     let person_id = local_user_view.person.id;
137
138     // Mark the post as read / unread
139     if data.read {
140       mark_post_as_read(person_id, post_id, context.pool()).await?;
141     } else {
142       mark_post_as_unread(person_id, post_id, context.pool()).await?;
143     }
144
145     // Fetch it
146     let post_view = blocking(context.pool(), move |conn| {
147       PostView::read(conn, post_id, Some(person_id))
148     })
149     .await??;
150
151     let res = Self::Response { post_view };
152
153     Ok(res)
154   }
155 }
156
157 #[async_trait::async_trait(?Send)]
158 impl Perform for LockPost {
159   type Response = PostResponse;
160
161   #[tracing::instrument(skip(context, websocket_id))]
162   async fn perform(
163     &self,
164     context: &Data<LemmyContext>,
165     websocket_id: Option<ConnectionId>,
166   ) -> Result<PostResponse, LemmyError> {
167     let data: &LockPost = self;
168     let local_user_view =
169       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
170
171     let post_id = data.post_id;
172     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
173
174     check_community_ban(
175       local_user_view.person.id,
176       orig_post.community_id,
177       context.pool(),
178     )
179     .await?;
180     check_community_deleted_or_removed(orig_post.community_id, context.pool()).await?;
181
182     // Verify that only the mods can lock
183     is_mod_or_admin(
184       context.pool(),
185       local_user_view.person.id,
186       orig_post.community_id,
187     )
188     .await?;
189
190     // Update the post
191     let post_id = data.post_id;
192     let locked = data.locked;
193     let updated_post: ApubPost = blocking(context.pool(), move |conn| {
194       Post::update_locked(conn, post_id, locked)
195     })
196     .await??
197     .into();
198
199     // Mod tables
200     let form = ModLockPostForm {
201       mod_person_id: local_user_view.person.id,
202       post_id: data.post_id,
203       locked: Some(locked),
204     };
205     blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
206
207     // apub updates
208     CreateOrUpdatePost::send(
209       updated_post,
210       &local_user_view.person.clone().into(),
211       CreateOrUpdateType::Update,
212       context,
213     )
214     .await?;
215
216     send_post_ws_message(
217       data.post_id,
218       UserOperation::LockPost,
219       websocket_id,
220       Some(local_user_view.person.id),
221       context,
222     )
223     .await
224   }
225 }
226
227 #[async_trait::async_trait(?Send)]
228 impl Perform for StickyPost {
229   type Response = PostResponse;
230
231   #[tracing::instrument(skip(context, websocket_id))]
232   async fn perform(
233     &self,
234     context: &Data<LemmyContext>,
235     websocket_id: Option<ConnectionId>,
236   ) -> Result<PostResponse, LemmyError> {
237     let data: &StickyPost = self;
238     let local_user_view =
239       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
240
241     let post_id = data.post_id;
242     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
243
244     check_community_ban(
245       local_user_view.person.id,
246       orig_post.community_id,
247       context.pool(),
248     )
249     .await?;
250     check_community_deleted_or_removed(orig_post.community_id, context.pool()).await?;
251
252     // Verify that only the mods can sticky
253     is_mod_or_admin(
254       context.pool(),
255       local_user_view.person.id,
256       orig_post.community_id,
257     )
258     .await?;
259
260     // Update the post
261     let post_id = data.post_id;
262     let stickied = data.stickied;
263     let updated_post: ApubPost = blocking(context.pool(), move |conn| {
264       Post::update_stickied(conn, post_id, stickied)
265     })
266     .await??
267     .into();
268
269     // Mod tables
270     let form = ModStickyPostForm {
271       mod_person_id: local_user_view.person.id,
272       post_id: data.post_id,
273       stickied: Some(stickied),
274     };
275     blocking(context.pool(), move |conn| {
276       ModStickyPost::create(conn, &form)
277     })
278     .await??;
279
280     // Apub updates
281     // TODO stickied should pry work like locked for ease of use
282     CreateOrUpdatePost::send(
283       updated_post,
284       &local_user_view.person.clone().into(),
285       CreateOrUpdateType::Update,
286       context,
287     )
288     .await?;
289
290     send_post_ws_message(
291       data.post_id,
292       UserOperation::StickyPost,
293       websocket_id,
294       Some(local_user_view.person.id),
295       context,
296     )
297     .await
298   }
299 }
300
301 #[async_trait::async_trait(?Send)]
302 impl Perform for SavePost {
303   type Response = PostResponse;
304
305   #[tracing::instrument(skip(context, _websocket_id))]
306   async fn perform(
307     &self,
308     context: &Data<LemmyContext>,
309     _websocket_id: Option<ConnectionId>,
310   ) -> Result<PostResponse, LemmyError> {
311     let data: &SavePost = self;
312     let local_user_view =
313       get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
314
315     let post_saved_form = PostSavedForm {
316       post_id: data.post_id,
317       person_id: local_user_view.person.id,
318     };
319
320     if data.save {
321       let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
322       blocking(context.pool(), save)
323         .await?
324         .map_err(LemmyError::from)
325         .map_err(|e| e.with_message("couldnt_save_post"))?;
326     } else {
327       let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
328       blocking(context.pool(), unsave)
329         .await?
330         .map_err(LemmyError::from)
331         .map_err(|e| e.with_message("couldnt_save_post"))?;
332     }
333
334     let post_id = data.post_id;
335     let person_id = local_user_view.person.id;
336     let post_view = blocking(context.pool(), move |conn| {
337       PostView::read(conn, post_id, Some(person_id))
338     })
339     .await??;
340
341     // Mark the post as read
342     mark_post_as_read(person_id, post_id, context.pool()).await?;
343
344     Ok(PostResponse { post_view })
345   }
346 }
347
348 #[async_trait::async_trait(?Send)]
349 impl Perform for GetSiteMetadata {
350   type Response = GetSiteMetadataResponse;
351
352   #[tracing::instrument(skip(context, _websocket_id))]
353   async fn perform(
354     &self,
355     context: &Data<LemmyContext>,
356     _websocket_id: Option<ConnectionId>,
357   ) -> Result<GetSiteMetadataResponse, LemmyError> {
358     let data: &Self = self;
359
360     let metadata = fetch_site_metadata(context.client(), &data.url).await?;
361
362     Ok(GetSiteMetadataResponse { metadata })
363   }
364 }