]> Untitled Git - lemmy.git/blob - crates/api/src/post.rs
bbc3e04bb0e9497e7fd4bc74bbacee908042c7b1
[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       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.map(|u| u.into()),
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| -> Result<Post, LemmyError> {
116       let apub_id = generate_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string())?;
117       Ok(Post::update_ap_id(conn, inserted_post_id, apub_id)?)
118     })
119     .await?
120     {
121       Ok(post) => post,
122       Err(_e) => return Err(ApiError::err("couldnt_create_post").into()),
123     };
124
125     updated_post
126       .send_create(&local_user_view.person, context)
127       .await?;
128
129     // They like their own post by default
130     let like_form = PostLikeForm {
131       post_id: inserted_post.id,
132       person_id: local_user_view.person.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
142       .send_like(&local_user_view.person, context)
143       .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(local_user_view.person.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 local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
179     let person_id = local_user_view.map(|u| u.person.id);
180
181     let id = data.id;
182     let post_view = match blocking(context.pool(), move |conn| {
183       PostView::read(conn, id, person_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_person_id(person_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, person_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 local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
245
246     let person_id = match &local_user_view {
247       Some(uv) => Some(uv.person.id),
248       None => None,
249     };
250
251     let show_nsfw = match &local_user_view {
252       Some(uv) => uv.local_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 saved_only = data.saved_only;
264
265     let posts = match blocking(context.pool(), move |conn| {
266       PostQueryBuilder::create(conn)
267         .listing_type(&type_)
268         .sort(&sort)
269         .show_nsfw(show_nsfw)
270         .community_id(community_id)
271         .community_name(community_name)
272         .saved_only(saved_only)
273         .my_person_id(person_id)
274         .page(page)
275         .limit(limit)
276         .list()
277     })
278     .await?
279     {
280       Ok(posts) => posts,
281       Err(_e) => return Err(ApiError::err("couldnt_get_posts").into()),
282     };
283
284     Ok(GetPostsResponse { posts })
285   }
286 }
287
288 #[async_trait::async_trait(?Send)]
289 impl Perform for CreatePostLike {
290   type Response = PostResponse;
291
292   async fn perform(
293     &self,
294     context: &Data<LemmyContext>,
295     websocket_id: Option<ConnectionId>,
296   ) -> Result<PostResponse, LemmyError> {
297     let data: &CreatePostLike = &self;
298     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
299
300     // Don't do a downvote if site has downvotes disabled
301     check_downvotes_enabled(data.score, context.pool()).await?;
302
303     // Check for a community ban
304     let post_id = data.post_id;
305     let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
306
307     check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
308
309     let like_form = PostLikeForm {
310       post_id: data.post_id,
311       person_id: local_user_view.person.id,
312       score: data.score,
313     };
314
315     // Remove any likes first
316     let person_id = local_user_view.person.id;
317     blocking(context.pool(), move |conn| {
318       PostLike::remove(conn, person_id, post_id)
319     })
320     .await??;
321
322     // Only add the like if the score isnt 0
323     let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
324     if do_add {
325       let like_form2 = like_form.clone();
326       let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
327       if blocking(context.pool(), like).await?.is_err() {
328         return Err(ApiError::err("couldnt_like_post").into());
329       }
330
331       if like_form.score == 1 {
332         post.send_like(&local_user_view.person, context).await?;
333       } else if like_form.score == -1 {
334         post.send_dislike(&local_user_view.person, context).await?;
335       }
336     } else {
337       post
338         .send_undo_like(&local_user_view.person, context)
339         .await?;
340     }
341
342     let post_id = data.post_id;
343     let person_id = local_user_view.person.id;
344     let post_view = match blocking(context.pool(), move |conn| {
345       PostView::read(conn, post_id, Some(person_id))
346     })
347     .await?
348     {
349       Ok(post) => post,
350       Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
351     };
352
353     let res = PostResponse { post_view };
354
355     context.chat_server().do_send(SendPost {
356       op: UserOperation::CreatePostLike,
357       post: res.clone(),
358       websocket_id,
359     });
360
361     Ok(res)
362   }
363 }
364
365 #[async_trait::async_trait(?Send)]
366 impl Perform for EditPost {
367   type Response = PostResponse;
368
369   async fn perform(
370     &self,
371     context: &Data<LemmyContext>,
372     websocket_id: Option<ConnectionId>,
373   ) -> Result<PostResponse, LemmyError> {
374     let data: &EditPost = &self;
375     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
376
377     check_slurs(&data.name)?;
378     check_slurs_opt(&data.body)?;
379
380     if !is_valid_post_title(&data.name) {
381       return Err(ApiError::err("invalid_post_title").into());
382     }
383
384     let post_id = data.post_id;
385     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
386
387     check_community_ban(
388       local_user_view.person.id,
389       orig_post.community_id,
390       context.pool(),
391     )
392     .await?;
393
394     // Verify that only the creator can edit
395     if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
396       return Err(ApiError::err("no_post_edit_allowed").into());
397     }
398
399     // Fetch Iframely and Pictrs cached image
400     let data_url = data.url.as_ref();
401     let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
402       fetch_iframely_and_pictrs_data(context.client(), data_url).await;
403
404     let post_form = PostForm {
405       name: data.name.trim().to_owned(),
406       url: data_url.map(|u| u.to_owned().into()),
407       body: data.body.to_owned(),
408       nsfw: data.nsfw,
409       creator_id: orig_post.creator_id.to_owned(),
410       community_id: orig_post.community_id,
411       removed: Some(orig_post.removed),
412       deleted: Some(orig_post.deleted),
413       locked: Some(orig_post.locked),
414       stickied: Some(orig_post.stickied),
415       updated: Some(naive_now()),
416       embed_title: iframely_title,
417       embed_description: iframely_description,
418       embed_html: iframely_html,
419       thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
420       ap_id: Some(orig_post.ap_id),
421       local: orig_post.local,
422       published: None,
423     };
424
425     let post_id = data.post_id;
426     let res = blocking(context.pool(), move |conn| {
427       Post::update(conn, post_id, &post_form)
428     })
429     .await?;
430     let updated_post: Post = match res {
431       Ok(post) => post,
432       Err(e) => {
433         let err_type = if e.to_string() == "value too long for type character varying(200)" {
434           "post_title_too_long"
435         } else {
436           "couldnt_update_post"
437         };
438
439         return Err(ApiError::err(err_type).into());
440       }
441     };
442
443     // Send apub update
444     updated_post
445       .send_update(&local_user_view.person, context)
446       .await?;
447
448     let post_id = data.post_id;
449     let post_view = blocking(context.pool(), move |conn| {
450       PostView::read(conn, post_id, Some(local_user_view.person.id))
451     })
452     .await??;
453
454     let res = PostResponse { post_view };
455
456     context.chat_server().do_send(SendPost {
457       op: UserOperation::EditPost,
458       post: res.clone(),
459       websocket_id,
460     });
461
462     Ok(res)
463   }
464 }
465
466 #[async_trait::async_trait(?Send)]
467 impl Perform for DeletePost {
468   type Response = PostResponse;
469
470   async fn perform(
471     &self,
472     context: &Data<LemmyContext>,
473     websocket_id: Option<ConnectionId>,
474   ) -> Result<PostResponse, LemmyError> {
475     let data: &DeletePost = &self;
476     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
477
478     let post_id = data.post_id;
479     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
480
481     check_community_ban(
482       local_user_view.person.id,
483       orig_post.community_id,
484       context.pool(),
485     )
486     .await?;
487
488     // Verify that only the creator can delete
489     if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
490       return Err(ApiError::err("no_post_edit_allowed").into());
491     }
492
493     // Update the post
494     let post_id = data.post_id;
495     let deleted = data.deleted;
496     let updated_post = blocking(context.pool(), move |conn| {
497       Post::update_deleted(conn, post_id, deleted)
498     })
499     .await??;
500
501     // apub updates
502     if deleted {
503       updated_post
504         .send_delete(&local_user_view.person, context)
505         .await?;
506     } else {
507       updated_post
508         .send_undo_delete(&local_user_view.person, context)
509         .await?;
510     }
511
512     // Refetch the post
513     let post_id = data.post_id;
514     let post_view = blocking(context.pool(), move |conn| {
515       PostView::read(conn, post_id, Some(local_user_view.person.id))
516     })
517     .await??;
518
519     let res = PostResponse { post_view };
520
521     context.chat_server().do_send(SendPost {
522       op: UserOperation::DeletePost,
523       post: res.clone(),
524       websocket_id,
525     });
526
527     Ok(res)
528   }
529 }
530
531 #[async_trait::async_trait(?Send)]
532 impl Perform for RemovePost {
533   type Response = PostResponse;
534
535   async fn perform(
536     &self,
537     context: &Data<LemmyContext>,
538     websocket_id: Option<ConnectionId>,
539   ) -> Result<PostResponse, LemmyError> {
540     let data: &RemovePost = &self;
541     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
542
543     let post_id = data.post_id;
544     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
545
546     check_community_ban(
547       local_user_view.person.id,
548       orig_post.community_id,
549       context.pool(),
550     )
551     .await?;
552
553     // Verify that only the mods can remove
554     is_mod_or_admin(
555       context.pool(),
556       local_user_view.person.id,
557       orig_post.community_id,
558     )
559     .await?;
560
561     // Update the post
562     let post_id = data.post_id;
563     let removed = data.removed;
564     let updated_post = blocking(context.pool(), move |conn| {
565       Post::update_removed(conn, post_id, removed)
566     })
567     .await??;
568
569     // Mod tables
570     let form = ModRemovePostForm {
571       mod_person_id: local_user_view.person.id,
572       post_id: data.post_id,
573       removed: Some(removed),
574       reason: data.reason.to_owned(),
575     };
576     blocking(context.pool(), move |conn| {
577       ModRemovePost::create(conn, &form)
578     })
579     .await??;
580
581     // apub updates
582     if removed {
583       updated_post
584         .send_remove(&local_user_view.person, context)
585         .await?;
586     } else {
587       updated_post
588         .send_undo_remove(&local_user_view.person, context)
589         .await?;
590     }
591
592     // Refetch the post
593     let post_id = data.post_id;
594     let person_id = local_user_view.person.id;
595     let post_view = blocking(context.pool(), move |conn| {
596       PostView::read(conn, post_id, Some(person_id))
597     })
598     .await??;
599
600     let res = PostResponse { post_view };
601
602     context.chat_server().do_send(SendPost {
603       op: UserOperation::RemovePost,
604       post: res.clone(),
605       websocket_id,
606     });
607
608     Ok(res)
609   }
610 }
611
612 #[async_trait::async_trait(?Send)]
613 impl Perform for LockPost {
614   type Response = PostResponse;
615
616   async fn perform(
617     &self,
618     context: &Data<LemmyContext>,
619     websocket_id: Option<ConnectionId>,
620   ) -> Result<PostResponse, LemmyError> {
621     let data: &LockPost = &self;
622     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
623
624     let post_id = data.post_id;
625     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
626
627     check_community_ban(
628       local_user_view.person.id,
629       orig_post.community_id,
630       context.pool(),
631     )
632     .await?;
633
634     // Verify that only the mods can lock
635     is_mod_or_admin(
636       context.pool(),
637       local_user_view.person.id,
638       orig_post.community_id,
639     )
640     .await?;
641
642     // Update the post
643     let post_id = data.post_id;
644     let locked = data.locked;
645     let updated_post = blocking(context.pool(), move |conn| {
646       Post::update_locked(conn, post_id, locked)
647     })
648     .await??;
649
650     // Mod tables
651     let form = ModLockPostForm {
652       mod_person_id: local_user_view.person.id,
653       post_id: data.post_id,
654       locked: Some(locked),
655     };
656     blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
657
658     // apub updates
659     updated_post
660       .send_update(&local_user_view.person, context)
661       .await?;
662
663     // Refetch the post
664     let post_id = data.post_id;
665     let post_view = blocking(context.pool(), move |conn| {
666       PostView::read(conn, post_id, Some(local_user_view.person.id))
667     })
668     .await??;
669
670     let res = PostResponse { post_view };
671
672     context.chat_server().do_send(SendPost {
673       op: UserOperation::LockPost,
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 StickyPost {
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: &StickyPost = &self;
692     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
693
694     let post_id = data.post_id;
695     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
696
697     check_community_ban(
698       local_user_view.person.id,
699       orig_post.community_id,
700       context.pool(),
701     )
702     .await?;
703
704     // Verify that only the mods can sticky
705     is_mod_or_admin(
706       context.pool(),
707       local_user_view.person.id,
708       orig_post.community_id,
709     )
710     .await?;
711
712     // Update the post
713     let post_id = data.post_id;
714     let stickied = data.stickied;
715     let updated_post = blocking(context.pool(), move |conn| {
716       Post::update_stickied(conn, post_id, stickied)
717     })
718     .await??;
719
720     // Mod tables
721     let form = ModStickyPostForm {
722       mod_person_id: local_user_view.person.id,
723       post_id: data.post_id,
724       stickied: Some(stickied),
725     };
726     blocking(context.pool(), move |conn| {
727       ModStickyPost::create(conn, &form)
728     })
729     .await??;
730
731     // Apub updates
732     // TODO stickied should pry work like locked for ease of use
733     updated_post
734       .send_update(&local_user_view.person, context)
735       .await?;
736
737     // Refetch the post
738     let post_id = data.post_id;
739     let post_view = blocking(context.pool(), move |conn| {
740       PostView::read(conn, post_id, Some(local_user_view.person.id))
741     })
742     .await??;
743
744     let res = PostResponse { post_view };
745
746     context.chat_server().do_send(SendPost {
747       op: UserOperation::StickyPost,
748       post: res.clone(),
749       websocket_id,
750     });
751
752     Ok(res)
753   }
754 }
755
756 #[async_trait::async_trait(?Send)]
757 impl Perform for SavePost {
758   type Response = PostResponse;
759
760   async fn perform(
761     &self,
762     context: &Data<LemmyContext>,
763     _websocket_id: Option<ConnectionId>,
764   ) -> Result<PostResponse, LemmyError> {
765     let data: &SavePost = &self;
766     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
767
768     let post_saved_form = PostSavedForm {
769       post_id: data.post_id,
770       person_id: local_user_view.person.id,
771     };
772
773     if data.save {
774       let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
775       if blocking(context.pool(), save).await?.is_err() {
776         return Err(ApiError::err("couldnt_save_post").into());
777       }
778     } else {
779       let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
780       if blocking(context.pool(), unsave).await?.is_err() {
781         return Err(ApiError::err("couldnt_save_post").into());
782       }
783     }
784
785     let post_id = data.post_id;
786     let person_id = local_user_view.person.id;
787     let post_view = blocking(context.pool(), move |conn| {
788       PostView::read(conn, post_id, Some(person_id))
789     })
790     .await??;
791
792     Ok(PostResponse { post_view })
793   }
794 }
795
796 /// Creates a post report and notifies the moderators of the community
797 #[async_trait::async_trait(?Send)]
798 impl Perform for CreatePostReport {
799   type Response = CreatePostReportResponse;
800
801   async fn perform(
802     &self,
803     context: &Data<LemmyContext>,
804     websocket_id: Option<ConnectionId>,
805   ) -> Result<CreatePostReportResponse, LemmyError> {
806     let data: &CreatePostReport = &self;
807     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
808
809     // check size of report and check for whitespace
810     let reason = data.reason.trim();
811     if reason.is_empty() {
812       return Err(ApiError::err("report_reason_required").into());
813     }
814     if reason.chars().count() > 1000 {
815       return Err(ApiError::err("report_too_long").into());
816     }
817
818     let person_id = local_user_view.person.id;
819     let post_id = data.post_id;
820     let post_view = blocking(context.pool(), move |conn| {
821       PostView::read(&conn, post_id, None)
822     })
823     .await??;
824
825     check_community_ban(person_id, post_view.community.id, context.pool()).await?;
826
827     let report_form = PostReportForm {
828       creator_id: person_id,
829       post_id,
830       original_post_name: post_view.post.name,
831       original_post_url: post_view.post.url,
832       original_post_body: post_view.post.body,
833       reason: data.reason.to_owned(),
834     };
835
836     let report = match blocking(context.pool(), move |conn| {
837       PostReport::report(conn, &report_form)
838     })
839     .await?
840     {
841       Ok(report) => report,
842       Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
843     };
844
845     let res = CreatePostReportResponse { success: true };
846
847     context.chat_server().do_send(SendUserRoomMessage {
848       op: UserOperation::CreatePostReport,
849       response: res.clone(),
850       local_recipient_id: local_user_view.local_user.id,
851       websocket_id,
852     });
853
854     context.chat_server().do_send(SendModRoomMessage {
855       op: UserOperation::CreatePostReport,
856       response: report,
857       community_id: post_view.community.id,
858       websocket_id,
859     });
860
861     Ok(res)
862   }
863 }
864
865 /// Resolves or unresolves a post report and notifies the moderators of the community
866 #[async_trait::async_trait(?Send)]
867 impl Perform for ResolvePostReport {
868   type Response = ResolvePostReportResponse;
869
870   async fn perform(
871     &self,
872     context: &Data<LemmyContext>,
873     websocket_id: Option<ConnectionId>,
874   ) -> Result<ResolvePostReportResponse, LemmyError> {
875     let data: &ResolvePostReport = &self;
876     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
877
878     let report_id = data.report_id;
879     let report = blocking(context.pool(), move |conn| {
880       PostReportView::read(&conn, report_id)
881     })
882     .await??;
883
884     let person_id = local_user_view.person.id;
885     is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
886
887     let resolved = data.resolved;
888     let resolve_fun = move |conn: &'_ _| {
889       if resolved {
890         PostReport::resolve(conn, report_id, person_id)
891       } else {
892         PostReport::unresolve(conn, report_id, person_id)
893       }
894     };
895
896     let res = ResolvePostReportResponse {
897       report_id,
898       resolved: true,
899     };
900
901     if blocking(context.pool(), resolve_fun).await?.is_err() {
902       return Err(ApiError::err("couldnt_resolve_report").into());
903     };
904
905     context.chat_server().do_send(SendModRoomMessage {
906       op: UserOperation::ResolvePostReport,
907       response: res.clone(),
908       community_id: report.community.id,
909       websocket_id,
910     });
911
912     Ok(res)
913   }
914 }
915
916 /// Lists post reports for a community if an id is supplied
917 /// or returns all post reports for communities a user moderates
918 #[async_trait::async_trait(?Send)]
919 impl Perform for ListPostReports {
920   type Response = ListPostReportsResponse;
921
922   async fn perform(
923     &self,
924     context: &Data<LemmyContext>,
925     websocket_id: Option<ConnectionId>,
926   ) -> Result<ListPostReportsResponse, LemmyError> {
927     let data: &ListPostReports = &self;
928     let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
929
930     let person_id = local_user_view.person.id;
931     let community_id = data.community;
932     let community_ids =
933       collect_moderated_communities(person_id, community_id, context.pool()).await?;
934
935     let page = data.page;
936     let limit = data.limit;
937     let posts = blocking(context.pool(), move |conn| {
938       PostReportQueryBuilder::create(conn)
939         .community_ids(community_ids)
940         .page(page)
941         .limit(limit)
942         .list()
943     })
944     .await??;
945
946     let res = ListPostReportsResponse { posts };
947
948     context.chat_server().do_send(SendUserRoomMessage {
949       op: UserOperation::ListPostReports,
950       response: res.clone(),
951       local_recipient_id: local_user_view.local_user.id,
952       websocket_id,
953     });
954
955     Ok(res)
956   }
957 }