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