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