]> Untitled Git - lemmy.git/blob - lemmy_api/src/post.rs
remove timing files added by accident
[lemmy.git] / lemmy_api / src / post.rs
1 use crate::{
2   check_community_ban,
3   check_optional_url,
4   collect_moderated_communities,
5   get_user_from_jwt,
6   get_user_from_jwt_opt,
7   is_mod_or_admin,
8   Perform,
9 };
10 use actix_web::web::Data;
11 use lemmy_apub::{ApubLikeableType, ApubObjectType};
12 use lemmy_db::{
13   source::post::Post_,
14   views::{
15     comment_view::CommentQueryBuilder,
16     community::{community_moderator_view::CommunityModeratorView, community_view::CommunityView},
17     post_report_view::{PostReportQueryBuilder, PostReportView},
18     post_view::{PostQueryBuilder, PostView},
19     site_view::SiteView,
20   },
21   Crud,
22   Likeable,
23   ListingType,
24   Reportable,
25   Saveable,
26   SortType,
27 };
28 use lemmy_db_schema::{
29   naive_now,
30   source::{
31     moderator::*,
32     post::*,
33     post_report::{PostReport, PostReportForm},
34   },
35 };
36 use lemmy_structs::{blocking, post::*};
37 use lemmy_utils::{
38   apub::{make_apub_endpoint, EndpointType},
39   request::fetch_iframely_and_pictrs_data,
40   utils::{check_slurs, check_slurs_opt, is_valid_post_title},
41   APIError,
42   ConnectionId,
43   LemmyError,
44 };
45 use lemmy_websocket::{
46   messages::{GetPostUsersOnline, JoinPostRoom, SendModRoomMessage, SendPost, SendUserRoomMessage},
47   LemmyContext,
48   UserOperation,
49 };
50 use std::str::FromStr;
51
52 #[async_trait::async_trait(?Send)]
53 impl Perform for CreatePost {
54   type Response = PostResponse;
55
56   async fn perform(
57     &self,
58     context: &Data<LemmyContext>,
59     websocket_id: Option<ConnectionId>,
60   ) -> Result<PostResponse, LemmyError> {
61     let data: &CreatePost = &self;
62     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
63
64     check_slurs(&data.name)?;
65     check_slurs_opt(&data.body)?;
66
67     if !is_valid_post_title(&data.name) {
68       return Err(APIError::err("invalid_post_title").into());
69     }
70
71     check_community_ban(user.id, data.community_id, context.pool()).await?;
72
73     check_optional_url(&Some(data.url.to_owned()))?;
74
75     // Fetch Iframely and pictrs cached image
76     let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
77       fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
78
79     let post_form = PostForm {
80       name: data.name.trim().to_owned(),
81       url: data.url.to_owned(),
82       body: data.body.to_owned(),
83       community_id: data.community_id,
84       creator_id: user.id,
85       removed: None,
86       deleted: None,
87       nsfw: data.nsfw,
88       locked: None,
89       stickied: None,
90       updated: None,
91       embed_title: iframely_title,
92       embed_description: iframely_description,
93       embed_html: iframely_html,
94       thumbnail_url: pictrs_thumbnail,
95       ap_id: None,
96       local: true,
97       published: None,
98     };
99
100     let inserted_post =
101       match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
102         Ok(post) => post,
103         Err(e) => {
104           let err_type = if e.to_string() == "value too long for type character varying(200)" {
105             "post_title_too_long"
106           } else {
107             "couldnt_create_post"
108           };
109
110           return Err(APIError::err(err_type).into());
111         }
112       };
113
114     let inserted_post_id = inserted_post.id;
115     let updated_post = match blocking(context.pool(), move |conn| {
116       let apub_id =
117         make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
118       Post::update_ap_id(conn, inserted_post_id, apub_id)
119     })
120     .await?
121     {
122       Ok(post) => post,
123       Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
124     };
125
126     updated_post.send_create(&user, context).await?;
127
128     // They like their own post by default
129     let like_form = PostLikeForm {
130       post_id: inserted_post.id,
131       user_id: user.id,
132       score: 1,
133     };
134
135     let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
136     if blocking(context.pool(), like).await?.is_err() {
137       return Err(APIError::err("couldnt_like_post").into());
138     }
139
140     updated_post.send_like(&user, context).await?;
141
142     // Refetch the view
143     let inserted_post_id = inserted_post.id;
144     let post_view = match blocking(context.pool(), move |conn| {
145       PostView::read(conn, inserted_post_id, Some(user.id))
146     })
147     .await?
148     {
149       Ok(post) => post,
150       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
151     };
152
153     let res = PostResponse { post_view };
154
155     context.chat_server().do_send(SendPost {
156       op: UserOperation::CreatePost,
157       post: res.clone(),
158       websocket_id,
159     });
160
161     Ok(res)
162   }
163 }
164
165 #[async_trait::async_trait(?Send)]
166 impl Perform for GetPost {
167   type Response = GetPostResponse;
168
169   async fn perform(
170     &self,
171     context: &Data<LemmyContext>,
172     _websocket_id: Option<ConnectionId>,
173   ) -> Result<GetPostResponse, LemmyError> {
174     let data: &GetPost = &self;
175     let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
176     let user_id = user.map(|u| u.id);
177
178     let id = data.id;
179     let post_view = match blocking(context.pool(), move |conn| {
180       PostView::read(conn, id, user_id)
181     })
182     .await?
183     {
184       Ok(post) => post,
185       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
186     };
187
188     let id = data.id;
189     let comments = blocking(context.pool(), move |conn| {
190       CommentQueryBuilder::create(conn)
191         .my_user_id(user_id)
192         .post_id(id)
193         .limit(9999)
194         .list()
195     })
196     .await??;
197
198     let community_id = post_view.community.id;
199     let community = blocking(context.pool(), move |conn| {
200       CommunityView::read(conn, community_id, user_id)
201     })
202     .await??;
203
204     let community_id = post_view.community.id;
205     let moderators = blocking(context.pool(), move |conn| {
206       CommunityModeratorView::for_community(conn, community_id)
207     })
208     .await??;
209
210     let online = context
211       .chat_server()
212       .send(GetPostUsersOnline { post_id: data.id })
213       .await
214       .unwrap_or(1);
215
216     // Return the jwt
217     Ok(GetPostResponse {
218       post_view,
219       comments,
220       community,
221       moderators,
222       online,
223     })
224   }
225 }
226
227 #[async_trait::async_trait(?Send)]
228 impl Perform for GetPosts {
229   type Response = GetPostsResponse;
230
231   async fn perform(
232     &self,
233     context: &Data<LemmyContext>,
234     _websocket_id: Option<ConnectionId>,
235   ) -> Result<GetPostsResponse, LemmyError> {
236     let data: &GetPosts = &self;
237     let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
238
239     let user_id = match &user {
240       Some(user) => Some(user.id),
241       None => None,
242     };
243
244     let show_nsfw = match &user {
245       Some(user) => user.show_nsfw,
246       None => false,
247     };
248
249     let type_ = ListingType::from_str(&data.type_)?;
250     let sort = SortType::from_str(&data.sort)?;
251
252     let page = data.page;
253     let limit = data.limit;
254     let community_id = data.community_id;
255     let community_name = data.community_name.to_owned();
256     let posts = match blocking(context.pool(), move |conn| {
257       PostQueryBuilder::create(conn)
258         .listing_type(&type_)
259         .sort(&sort)
260         .show_nsfw(show_nsfw)
261         .community_id(community_id)
262         .community_name(community_name)
263         .my_user_id(user_id)
264         .page(page)
265         .limit(limit)
266         .list()
267     })
268     .await?
269     {
270       Ok(posts) => posts,
271       Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
272     };
273
274     Ok(GetPostsResponse { posts })
275   }
276 }
277
278 #[async_trait::async_trait(?Send)]
279 impl Perform for CreatePostLike {
280   type Response = PostResponse;
281
282   async fn perform(
283     &self,
284     context: &Data<LemmyContext>,
285     websocket_id: Option<ConnectionId>,
286   ) -> Result<PostResponse, LemmyError> {
287     let data: &CreatePostLike = &self;
288     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
289
290     // Don't do a downvote if site has downvotes disabled
291     if data.score == -1 {
292       let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
293       if !site_view.site.enable_downvotes {
294         return Err(APIError::err("downvotes_disabled").into());
295       }
296     }
297
298     // Check for a community ban
299     let post_id = data.post_id;
300     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
301
302     check_community_ban(user.id, post.community_id, context.pool()).await?;
303
304     let like_form = PostLikeForm {
305       post_id: data.post_id,
306       user_id: user.id,
307       score: data.score,
308     };
309
310     // Remove any likes first
311     let user_id = user.id;
312     blocking(context.pool(), move |conn| {
313       PostLike::remove(conn, user_id, post_id)
314     })
315     .await??;
316
317     // Only add the like if the score isnt 0
318     let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
319     if do_add {
320       let like_form2 = like_form.clone();
321       let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
322       if blocking(context.pool(), like).await?.is_err() {
323         return Err(APIError::err("couldnt_like_post").into());
324       }
325
326       if like_form.score == 1 {
327         post.send_like(&user, context).await?;
328       } else if like_form.score == -1 {
329         post.send_dislike(&user, context).await?;
330       }
331     } else {
332       post.send_undo_like(&user, context).await?;
333     }
334
335     let post_id = data.post_id;
336     let user_id = user.id;
337     let post_view = match blocking(context.pool(), move |conn| {
338       PostView::read(conn, post_id, Some(user_id))
339     })
340     .await?
341     {
342       Ok(post) => post,
343       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
344     };
345
346     let res = PostResponse { post_view };
347
348     context.chat_server().do_send(SendPost {
349       op: UserOperation::CreatePostLike,
350       post: res.clone(),
351       websocket_id,
352     });
353
354     Ok(res)
355   }
356 }
357
358 #[async_trait::async_trait(?Send)]
359 impl Perform for EditPost {
360   type Response = PostResponse;
361
362   async fn perform(
363     &self,
364     context: &Data<LemmyContext>,
365     websocket_id: Option<ConnectionId>,
366   ) -> Result<PostResponse, LemmyError> {
367     let data: &EditPost = &self;
368     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
369
370     check_slurs(&data.name)?;
371     check_slurs_opt(&data.body)?;
372
373     if !is_valid_post_title(&data.name) {
374       return Err(APIError::err("invalid_post_title").into());
375     }
376
377     let edit_id = data.edit_id;
378     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
379
380     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
381
382     // Verify that only the creator can edit
383     if !Post::is_post_creator(user.id, orig_post.creator_id) {
384       return Err(APIError::err("no_post_edit_allowed").into());
385     }
386
387     // Fetch Iframely and Pictrs cached image
388     let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
389       fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
390
391     let post_form = PostForm {
392       name: data.name.trim().to_owned(),
393       url: data.url.to_owned(),
394       body: data.body.to_owned(),
395       nsfw: data.nsfw,
396       creator_id: orig_post.creator_id.to_owned(),
397       community_id: orig_post.community_id,
398       removed: Some(orig_post.removed),
399       deleted: Some(orig_post.deleted),
400       locked: Some(orig_post.locked),
401       stickied: Some(orig_post.stickied),
402       updated: Some(naive_now()),
403       embed_title: iframely_title,
404       embed_description: iframely_description,
405       embed_html: iframely_html,
406       thumbnail_url: pictrs_thumbnail,
407       ap_id: Some(orig_post.ap_id),
408       local: orig_post.local,
409       published: None,
410     };
411
412     let edit_id = data.edit_id;
413     let res = blocking(context.pool(), move |conn| {
414       Post::update(conn, edit_id, &post_form)
415     })
416     .await?;
417     let updated_post: Post = match res {
418       Ok(post) => post,
419       Err(e) => {
420         let err_type = if e.to_string() == "value too long for type character varying(200)" {
421           "post_title_too_long"
422         } else {
423           "couldnt_update_post"
424         };
425
426         return Err(APIError::err(err_type).into());
427       }
428     };
429
430     // Send apub update
431     updated_post.send_update(&user, context).await?;
432
433     let edit_id = data.edit_id;
434     let post_view = blocking(context.pool(), move |conn| {
435       PostView::read(conn, edit_id, Some(user.id))
436     })
437     .await??;
438
439     let res = PostResponse { post_view };
440
441     context.chat_server().do_send(SendPost {
442       op: UserOperation::EditPost,
443       post: res.clone(),
444       websocket_id,
445     });
446
447     Ok(res)
448   }
449 }
450
451 #[async_trait::async_trait(?Send)]
452 impl Perform for DeletePost {
453   type Response = PostResponse;
454
455   async fn perform(
456     &self,
457     context: &Data<LemmyContext>,
458     websocket_id: Option<ConnectionId>,
459   ) -> Result<PostResponse, LemmyError> {
460     let data: &DeletePost = &self;
461     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
462
463     let edit_id = data.edit_id;
464     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
465
466     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
467
468     // Verify that only the creator can delete
469     if !Post::is_post_creator(user.id, orig_post.creator_id) {
470       return Err(APIError::err("no_post_edit_allowed").into());
471     }
472
473     // Update the post
474     let edit_id = data.edit_id;
475     let deleted = data.deleted;
476     let updated_post = blocking(context.pool(), move |conn| {
477       Post::update_deleted(conn, edit_id, deleted)
478     })
479     .await??;
480
481     // apub updates
482     if deleted {
483       updated_post.send_delete(&user, context).await?;
484     } else {
485       updated_post.send_undo_delete(&user, context).await?;
486     }
487
488     // Refetch the post
489     let edit_id = data.edit_id;
490     let post_view = blocking(context.pool(), move |conn| {
491       PostView::read(conn, edit_id, Some(user.id))
492     })
493     .await??;
494
495     let res = PostResponse { post_view };
496
497     context.chat_server().do_send(SendPost {
498       op: UserOperation::DeletePost,
499       post: res.clone(),
500       websocket_id,
501     });
502
503     Ok(res)
504   }
505 }
506
507 #[async_trait::async_trait(?Send)]
508 impl Perform for RemovePost {
509   type Response = PostResponse;
510
511   async fn perform(
512     &self,
513     context: &Data<LemmyContext>,
514     websocket_id: Option<ConnectionId>,
515   ) -> Result<PostResponse, LemmyError> {
516     let data: &RemovePost = &self;
517     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
518
519     let edit_id = data.edit_id;
520     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
521
522     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
523
524     // Verify that only the mods can remove
525     is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
526
527     // Update the post
528     let edit_id = data.edit_id;
529     let removed = data.removed;
530     let updated_post = blocking(context.pool(), move |conn| {
531       Post::update_removed(conn, edit_id, removed)
532     })
533     .await??;
534
535     // Mod tables
536     let form = ModRemovePostForm {
537       mod_user_id: user.id,
538       post_id: data.edit_id,
539       removed: Some(removed),
540       reason: data.reason.to_owned(),
541     };
542     blocking(context.pool(), move |conn| {
543       ModRemovePost::create(conn, &form)
544     })
545     .await??;
546
547     // apub updates
548     if removed {
549       updated_post.send_remove(&user, context).await?;
550     } else {
551       updated_post.send_undo_remove(&user, context).await?;
552     }
553
554     // Refetch the post
555     let edit_id = data.edit_id;
556     let user_id = user.id;
557     let post_view = blocking(context.pool(), move |conn| {
558       PostView::read(conn, edit_id, Some(user_id))
559     })
560     .await??;
561
562     let res = PostResponse { post_view };
563
564     context.chat_server().do_send(SendPost {
565       op: UserOperation::RemovePost,
566       post: res.clone(),
567       websocket_id,
568     });
569
570     Ok(res)
571   }
572 }
573
574 #[async_trait::async_trait(?Send)]
575 impl Perform for LockPost {
576   type Response = PostResponse;
577
578   async fn perform(
579     &self,
580     context: &Data<LemmyContext>,
581     websocket_id: Option<ConnectionId>,
582   ) -> Result<PostResponse, LemmyError> {
583     let data: &LockPost = &self;
584     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
585
586     let edit_id = data.edit_id;
587     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
588
589     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
590
591     // Verify that only the mods can lock
592     is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
593
594     // Update the post
595     let edit_id = data.edit_id;
596     let locked = data.locked;
597     let updated_post = blocking(context.pool(), move |conn| {
598       Post::update_locked(conn, edit_id, locked)
599     })
600     .await??;
601
602     // Mod tables
603     let form = ModLockPostForm {
604       mod_user_id: user.id,
605       post_id: data.edit_id,
606       locked: Some(locked),
607     };
608     blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
609
610     // apub updates
611     updated_post.send_update(&user, context).await?;
612
613     // Refetch the post
614     let edit_id = data.edit_id;
615     let post_view = blocking(context.pool(), move |conn| {
616       PostView::read(conn, edit_id, Some(user.id))
617     })
618     .await??;
619
620     let res = PostResponse { post_view };
621
622     context.chat_server().do_send(SendPost {
623       op: UserOperation::LockPost,
624       post: res.clone(),
625       websocket_id,
626     });
627
628     Ok(res)
629   }
630 }
631
632 #[async_trait::async_trait(?Send)]
633 impl Perform for StickyPost {
634   type Response = PostResponse;
635
636   async fn perform(
637     &self,
638     context: &Data<LemmyContext>,
639     websocket_id: Option<ConnectionId>,
640   ) -> Result<PostResponse, LemmyError> {
641     let data: &StickyPost = &self;
642     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
643
644     let edit_id = data.edit_id;
645     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
646
647     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
648
649     // Verify that only the mods can sticky
650     is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
651
652     // Update the post
653     let edit_id = data.edit_id;
654     let stickied = data.stickied;
655     let updated_post = blocking(context.pool(), move |conn| {
656       Post::update_stickied(conn, edit_id, stickied)
657     })
658     .await??;
659
660     // Mod tables
661     let form = ModStickyPostForm {
662       mod_user_id: user.id,
663       post_id: data.edit_id,
664       stickied: Some(stickied),
665     };
666     blocking(context.pool(), move |conn| {
667       ModStickyPost::create(conn, &form)
668     })
669     .await??;
670
671     // Apub updates
672     // TODO stickied should pry work like locked for ease of use
673     updated_post.send_update(&user, context).await?;
674
675     // Refetch the post
676     let edit_id = data.edit_id;
677     let post_view = blocking(context.pool(), move |conn| {
678       PostView::read(conn, edit_id, Some(user.id))
679     })
680     .await??;
681
682     let res = PostResponse { post_view };
683
684     context.chat_server().do_send(SendPost {
685       op: UserOperation::StickyPost,
686       post: res.clone(),
687       websocket_id,
688     });
689
690     Ok(res)
691   }
692 }
693
694 #[async_trait::async_trait(?Send)]
695 impl Perform for SavePost {
696   type Response = PostResponse;
697
698   async fn perform(
699     &self,
700     context: &Data<LemmyContext>,
701     _websocket_id: Option<ConnectionId>,
702   ) -> Result<PostResponse, LemmyError> {
703     let data: &SavePost = &self;
704     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
705
706     let post_saved_form = PostSavedForm {
707       post_id: data.post_id,
708       user_id: user.id,
709     };
710
711     if data.save {
712       let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
713       if blocking(context.pool(), save).await?.is_err() {
714         return Err(APIError::err("couldnt_save_post").into());
715       }
716     } else {
717       let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
718       if blocking(context.pool(), unsave).await?.is_err() {
719         return Err(APIError::err("couldnt_save_post").into());
720       }
721     }
722
723     let post_id = data.post_id;
724     let user_id = user.id;
725     let post_view = blocking(context.pool(), move |conn| {
726       PostView::read(conn, post_id, Some(user_id))
727     })
728     .await??;
729
730     Ok(PostResponse { post_view })
731   }
732 }
733
734 #[async_trait::async_trait(?Send)]
735 impl Perform for PostJoin {
736   type Response = PostJoinResponse;
737
738   async fn perform(
739     &self,
740     context: &Data<LemmyContext>,
741     websocket_id: Option<ConnectionId>,
742   ) -> Result<PostJoinResponse, LemmyError> {
743     let data: &PostJoin = &self;
744
745     if let Some(ws_id) = websocket_id {
746       context.chat_server().do_send(JoinPostRoom {
747         post_id: data.post_id,
748         id: ws_id,
749       });
750     }
751
752     Ok(PostJoinResponse { joined: true })
753   }
754 }
755
756 /// Creates a post report and notifies the moderators of the community
757 #[async_trait::async_trait(?Send)]
758 impl Perform for CreatePostReport {
759   type Response = CreatePostReportResponse;
760
761   async fn perform(
762     &self,
763     context: &Data<LemmyContext>,
764     websocket_id: Option<ConnectionId>,
765   ) -> Result<CreatePostReportResponse, LemmyError> {
766     let data: &CreatePostReport = &self;
767     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
768
769     // check size of report and check for whitespace
770     let reason = data.reason.trim();
771     if reason.is_empty() {
772       return Err(APIError::err("report_reason_required").into());
773     }
774     if reason.len() > 1000 {
775       return Err(APIError::err("report_too_long").into());
776     }
777
778     let user_id = user.id;
779     let post_id = data.post_id;
780     let post_view = blocking(context.pool(), move |conn| {
781       PostView::read(&conn, post_id, None)
782     })
783     .await??;
784
785     check_community_ban(user_id, post_view.community.id, context.pool()).await?;
786
787     let report_form = PostReportForm {
788       creator_id: user_id,
789       post_id,
790       original_post_name: post_view.post.name,
791       original_post_url: post_view.post.url,
792       original_post_body: post_view.post.body,
793       reason: data.reason.to_owned(),
794     };
795
796     let report = match blocking(context.pool(), move |conn| {
797       PostReport::report(conn, &report_form)
798     })
799     .await?
800     {
801       Ok(report) => report,
802       Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
803     };
804
805     let res = CreatePostReportResponse { success: true };
806
807     context.chat_server().do_send(SendUserRoomMessage {
808       op: UserOperation::CreatePostReport,
809       response: res.clone(),
810       recipient_id: user.id,
811       websocket_id,
812     });
813
814     context.chat_server().do_send(SendModRoomMessage {
815       op: UserOperation::CreatePostReport,
816       response: report,
817       community_id: post_view.community.id,
818       websocket_id,
819     });
820
821     Ok(res)
822   }
823 }
824
825 /// Resolves or unresolves a post report and notifies the moderators of the community
826 #[async_trait::async_trait(?Send)]
827 impl Perform for ResolvePostReport {
828   type Response = ResolvePostReportResponse;
829
830   async fn perform(
831     &self,
832     context: &Data<LemmyContext>,
833     websocket_id: Option<ConnectionId>,
834   ) -> Result<ResolvePostReportResponse, LemmyError> {
835     let data: &ResolvePostReport = &self;
836     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
837
838     let report_id = data.report_id;
839     let report = blocking(context.pool(), move |conn| {
840       PostReportView::read(&conn, report_id)
841     })
842     .await??;
843
844     let user_id = user.id;
845     is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
846
847     let resolved = data.resolved;
848     let resolve_fun = move |conn: &'_ _| {
849       if resolved {
850         PostReport::resolve(conn, report_id, user_id)
851       } else {
852         PostReport::unresolve(conn, report_id, user_id)
853       }
854     };
855
856     let res = ResolvePostReportResponse {
857       report_id,
858       resolved: true,
859     };
860
861     if blocking(context.pool(), resolve_fun).await?.is_err() {
862       return Err(APIError::err("couldnt_resolve_report").into());
863     };
864
865     context.chat_server().do_send(SendModRoomMessage {
866       op: UserOperation::ResolvePostReport,
867       response: res.clone(),
868       community_id: report.community.id,
869       websocket_id,
870     });
871
872     Ok(res)
873   }
874 }
875
876 /// Lists post reports for a community if an id is supplied
877 /// or returns all post reports for communities a user moderates
878 #[async_trait::async_trait(?Send)]
879 impl Perform for ListPostReports {
880   type Response = ListPostReportsResponse;
881
882   async fn perform(
883     &self,
884     context: &Data<LemmyContext>,
885     websocket_id: Option<ConnectionId>,
886   ) -> Result<ListPostReportsResponse, LemmyError> {
887     let data: &ListPostReports = &self;
888     let user = get_user_from_jwt(&data.auth, context.pool()).await?;
889
890     let user_id = user.id;
891     let community_id = data.community;
892     let community_ids =
893       collect_moderated_communities(user_id, community_id, context.pool()).await?;
894
895     let page = data.page;
896     let limit = data.limit;
897     let posts = blocking(context.pool(), move |conn| {
898       PostReportQueryBuilder::create(conn)
899         .community_ids(community_ids)
900         .page(page)
901         .limit(limit)
902         .list()
903     })
904     .await??;
905
906     let res = ListPostReportsResponse { posts };
907
908     context.chat_server().do_send(SendUserRoomMessage {
909       op: UserOperation::ListPostReports,
910       response: res.clone(),
911       recipient_id: user.id,
912       websocket_id,
913     });
914
915     Ok(res)
916   }
917 }