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