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