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