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