4 collect_moderated_communities,
10 use actix_web::web::Data;
11 use lemmy_apub::{ApubLikeableType, ApubObjectType};
28 use lemmy_structs::{blocking, post::*};
30 apub::{make_apub_endpoint, EndpointType},
31 request::fetch_iframely_and_pictrs_data,
32 utils::{check_slurs, check_slurs_opt, is_valid_post_title},
37 use lemmy_websocket::{
38 messages::{GetPostUsersOnline, JoinPostRoom, SendModRoomMessage, SendPost, SendUserRoomMessage},
42 use std::str::FromStr;
44 #[async_trait::async_trait(?Send)]
45 impl Perform for CreatePost {
46 type Response = PostResponse;
50 context: &Data<LemmyContext>,
51 websocket_id: Option<ConnectionId>,
52 ) -> Result<PostResponse, LemmyError> {
53 let data: &CreatePost = &self;
54 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
56 check_slurs(&data.name)?;
57 check_slurs_opt(&data.body)?;
59 if !is_valid_post_title(&data.name) {
60 return Err(APIError::err("invalid_post_title").into());
63 check_community_ban(user.id, data.community_id, context.pool()).await?;
65 check_optional_url(&Some(data.url.to_owned()))?;
67 // Fetch Iframely and pictrs cached image
68 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
69 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
71 let post_form = PostForm {
72 name: data.name.trim().to_owned(),
73 url: data.url.to_owned(),
74 body: data.body.to_owned(),
75 community_id: data.community_id,
83 embed_title: iframely_title,
84 embed_description: iframely_description,
85 embed_html: iframely_html,
86 thumbnail_url: pictrs_thumbnail,
93 match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
96 let err_type = if e.to_string() == "value too long for type character varying(200)" {
102 return Err(APIError::err(err_type).into());
106 let inserted_post_id = inserted_post.id;
107 let updated_post = match blocking(context.pool(), move |conn| {
109 make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
110 Post::update_ap_id(conn, inserted_post_id, apub_id)
115 Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
118 updated_post.send_create(&user, context).await?;
120 // They like their own post by default
121 let like_form = PostLikeForm {
122 post_id: inserted_post.id,
127 let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
128 if blocking(context.pool(), like).await?.is_err() {
129 return Err(APIError::err("couldnt_like_post").into());
132 updated_post.send_like(&user, context).await?;
135 let inserted_post_id = inserted_post.id;
136 let post_view = match blocking(context.pool(), move |conn| {
137 PostView::read(conn, inserted_post_id, Some(user.id))
142 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
145 let res = PostResponse { post: post_view };
147 context.chat_server().do_send(SendPost {
148 op: UserOperation::CreatePost,
157 #[async_trait::async_trait(?Send)]
158 impl Perform for GetPost {
159 type Response = GetPostResponse;
163 context: &Data<LemmyContext>,
164 _websocket_id: Option<ConnectionId>,
165 ) -> Result<GetPostResponse, LemmyError> {
166 let data: &GetPost = &self;
167 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
168 let user_id = user.map(|u| u.id);
171 let post_view = match blocking(context.pool(), move |conn| {
172 PostView::read(conn, id, user_id)
177 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
181 let comments = blocking(context.pool(), move |conn| {
182 CommentQueryBuilder::create(conn)
190 let community_id = post_view.community_id;
191 let community = blocking(context.pool(), move |conn| {
192 CommunityView::read(conn, community_id, user_id)
196 let community_id = post_view.community_id;
197 let moderators = blocking(context.pool(), move |conn| {
198 CommunityModeratorView::for_community(conn, community_id)
204 .send(GetPostUsersOnline { post_id: data.id })
219 #[async_trait::async_trait(?Send)]
220 impl Perform for GetPosts {
221 type Response = GetPostsResponse;
225 context: &Data<LemmyContext>,
226 _websocket_id: Option<ConnectionId>,
227 ) -> Result<GetPostsResponse, LemmyError> {
228 let data: &GetPosts = &self;
229 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
231 let user_id = match &user {
232 Some(user) => Some(user.id),
236 let show_nsfw = match &user {
237 Some(user) => user.show_nsfw,
241 let type_ = ListingType::from_str(&data.type_)?;
242 let sort = SortType::from_str(&data.sort)?;
244 let page = data.page;
245 let limit = data.limit;
246 let community_id = data.community_id;
247 let community_name = data.community_name.to_owned();
248 let posts = match blocking(context.pool(), move |conn| {
249 PostQueryBuilder::create(conn)
250 .listing_type(&type_)
252 .show_nsfw(show_nsfw)
253 .for_community_id(community_id)
254 .for_community_name(community_name)
263 Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
266 Ok(GetPostsResponse { posts })
270 #[async_trait::async_trait(?Send)]
271 impl Perform for CreatePostLike {
272 type Response = PostResponse;
276 context: &Data<LemmyContext>,
277 websocket_id: Option<ConnectionId>,
278 ) -> Result<PostResponse, LemmyError> {
279 let data: &CreatePostLike = &self;
280 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
282 // Don't do a downvote if site has downvotes disabled
283 if data.score == -1 {
284 let site = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
285 if !site.enable_downvotes {
286 return Err(APIError::err("downvotes_disabled").into());
290 // Check for a community ban
291 let post_id = data.post_id;
292 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
294 check_community_ban(user.id, post.community_id, context.pool()).await?;
296 let like_form = PostLikeForm {
297 post_id: data.post_id,
302 // Remove any likes first
303 let user_id = user.id;
304 blocking(context.pool(), move |conn| {
305 PostLike::remove(conn, user_id, post_id)
309 // Only add the like if the score isnt 0
310 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
312 let like_form2 = like_form.clone();
313 let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
314 if blocking(context.pool(), like).await?.is_err() {
315 return Err(APIError::err("couldnt_like_post").into());
318 if like_form.score == 1 {
319 post.send_like(&user, context).await?;
320 } else if like_form.score == -1 {
321 post.send_dislike(&user, context).await?;
324 post.send_undo_like(&user, context).await?;
327 let post_id = data.post_id;
328 let user_id = user.id;
329 let post_view = match blocking(context.pool(), move |conn| {
330 PostView::read(conn, post_id, Some(user_id))
335 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
338 let res = PostResponse { post: post_view };
340 context.chat_server().do_send(SendPost {
341 op: UserOperation::CreatePostLike,
350 #[async_trait::async_trait(?Send)]
351 impl Perform for EditPost {
352 type Response = PostResponse;
356 context: &Data<LemmyContext>,
357 websocket_id: Option<ConnectionId>,
358 ) -> Result<PostResponse, LemmyError> {
359 let data: &EditPost = &self;
360 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
362 check_slurs(&data.name)?;
363 check_slurs_opt(&data.body)?;
365 if !is_valid_post_title(&data.name) {
366 return Err(APIError::err("invalid_post_title").into());
369 let edit_id = data.edit_id;
370 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
372 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
374 // Verify that only the creator can edit
375 if !Post::is_post_creator(user.id, orig_post.creator_id) {
376 return Err(APIError::err("no_post_edit_allowed").into());
379 // Fetch Iframely and Pictrs cached image
380 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
381 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
383 let post_form = PostForm {
384 name: data.name.trim().to_owned(),
385 url: data.url.to_owned(),
386 body: data.body.to_owned(),
388 creator_id: orig_post.creator_id.to_owned(),
389 community_id: orig_post.community_id,
390 removed: Some(orig_post.removed),
391 deleted: Some(orig_post.deleted),
392 locked: Some(orig_post.locked),
393 stickied: Some(orig_post.stickied),
394 updated: Some(naive_now()),
395 embed_title: iframely_title,
396 embed_description: iframely_description,
397 embed_html: iframely_html,
398 thumbnail_url: pictrs_thumbnail,
399 ap_id: Some(orig_post.ap_id),
400 local: orig_post.local,
404 let edit_id = data.edit_id;
405 let res = blocking(context.pool(), move |conn| {
406 Post::update(conn, edit_id, &post_form)
409 let updated_post: Post = match res {
412 let err_type = if e.to_string() == "value too long for type character varying(200)" {
413 "post_title_too_long"
415 "couldnt_update_post"
418 return Err(APIError::err(err_type).into());
423 updated_post.send_update(&user, context).await?;
425 let edit_id = data.edit_id;
426 let post_view = blocking(context.pool(), move |conn| {
427 PostView::read(conn, edit_id, Some(user.id))
431 let res = PostResponse { post: post_view };
433 context.chat_server().do_send(SendPost {
434 op: UserOperation::EditPost,
443 #[async_trait::async_trait(?Send)]
444 impl Perform for DeletePost {
445 type Response = PostResponse;
449 context: &Data<LemmyContext>,
450 websocket_id: Option<ConnectionId>,
451 ) -> Result<PostResponse, LemmyError> {
452 let data: &DeletePost = &self;
453 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
455 let edit_id = data.edit_id;
456 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
458 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
460 // Verify that only the creator can delete
461 if !Post::is_post_creator(user.id, orig_post.creator_id) {
462 return Err(APIError::err("no_post_edit_allowed").into());
466 let edit_id = data.edit_id;
467 let deleted = data.deleted;
468 let updated_post = blocking(context.pool(), move |conn| {
469 Post::update_deleted(conn, edit_id, deleted)
475 updated_post.send_delete(&user, context).await?;
477 updated_post.send_undo_delete(&user, context).await?;
481 let edit_id = data.edit_id;
482 let post_view = blocking(context.pool(), move |conn| {
483 PostView::read(conn, edit_id, Some(user.id))
487 let res = PostResponse { post: post_view };
489 context.chat_server().do_send(SendPost {
490 op: UserOperation::DeletePost,
499 #[async_trait::async_trait(?Send)]
500 impl Perform for RemovePost {
501 type Response = PostResponse;
505 context: &Data<LemmyContext>,
506 websocket_id: Option<ConnectionId>,
507 ) -> Result<PostResponse, LemmyError> {
508 let data: &RemovePost = &self;
509 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
511 let edit_id = data.edit_id;
512 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
514 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
516 // Verify that only the mods can remove
517 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
520 let edit_id = data.edit_id;
521 let removed = data.removed;
522 let updated_post = blocking(context.pool(), move |conn| {
523 Post::update_removed(conn, edit_id, removed)
528 let form = ModRemovePostForm {
529 mod_user_id: user.id,
530 post_id: data.edit_id,
531 removed: Some(removed),
532 reason: data.reason.to_owned(),
534 blocking(context.pool(), move |conn| {
535 ModRemovePost::create(conn, &form)
541 updated_post.send_remove(&user, context).await?;
543 updated_post.send_undo_remove(&user, context).await?;
547 let edit_id = data.edit_id;
548 let user_id = user.id;
549 let post_view = blocking(context.pool(), move |conn| {
550 PostView::read(conn, edit_id, Some(user_id))
554 let res = PostResponse { post: post_view };
556 context.chat_server().do_send(SendPost {
557 op: UserOperation::RemovePost,
566 #[async_trait::async_trait(?Send)]
567 impl Perform for LockPost {
568 type Response = PostResponse;
572 context: &Data<LemmyContext>,
573 websocket_id: Option<ConnectionId>,
574 ) -> Result<PostResponse, LemmyError> {
575 let data: &LockPost = &self;
576 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
578 let edit_id = data.edit_id;
579 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
581 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
583 // Verify that only the mods can lock
584 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
587 let edit_id = data.edit_id;
588 let locked = data.locked;
589 let updated_post = blocking(context.pool(), move |conn| {
590 Post::update_locked(conn, edit_id, locked)
595 let form = ModLockPostForm {
596 mod_user_id: user.id,
597 post_id: data.edit_id,
598 locked: Some(locked),
600 blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
603 updated_post.send_update(&user, context).await?;
606 let edit_id = data.edit_id;
607 let post_view = blocking(context.pool(), move |conn| {
608 PostView::read(conn, edit_id, Some(user.id))
612 let res = PostResponse { post: post_view };
614 context.chat_server().do_send(SendPost {
615 op: UserOperation::LockPost,
624 #[async_trait::async_trait(?Send)]
625 impl Perform for StickyPost {
626 type Response = PostResponse;
630 context: &Data<LemmyContext>,
631 websocket_id: Option<ConnectionId>,
632 ) -> Result<PostResponse, LemmyError> {
633 let data: &StickyPost = &self;
634 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
636 let edit_id = data.edit_id;
637 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
639 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
641 // Verify that only the mods can sticky
642 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
645 let edit_id = data.edit_id;
646 let stickied = data.stickied;
647 let updated_post = blocking(context.pool(), move |conn| {
648 Post::update_stickied(conn, edit_id, stickied)
653 let form = ModStickyPostForm {
654 mod_user_id: user.id,
655 post_id: data.edit_id,
656 stickied: Some(stickied),
658 blocking(context.pool(), move |conn| {
659 ModStickyPost::create(conn, &form)
664 // TODO stickied should pry work like locked for ease of use
665 updated_post.send_update(&user, context).await?;
668 let edit_id = data.edit_id;
669 let post_view = blocking(context.pool(), move |conn| {
670 PostView::read(conn, edit_id, Some(user.id))
674 let res = PostResponse { post: post_view };
676 context.chat_server().do_send(SendPost {
677 op: UserOperation::StickyPost,
686 #[async_trait::async_trait(?Send)]
687 impl Perform for SavePost {
688 type Response = PostResponse;
692 context: &Data<LemmyContext>,
693 _websocket_id: Option<ConnectionId>,
694 ) -> Result<PostResponse, LemmyError> {
695 let data: &SavePost = &self;
696 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
698 let post_saved_form = PostSavedForm {
699 post_id: data.post_id,
704 let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
705 if blocking(context.pool(), save).await?.is_err() {
706 return Err(APIError::err("couldnt_save_post").into());
709 let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
710 if blocking(context.pool(), unsave).await?.is_err() {
711 return Err(APIError::err("couldnt_save_post").into());
715 let post_id = data.post_id;
716 let user_id = user.id;
717 let post_view = blocking(context.pool(), move |conn| {
718 PostView::read(conn, post_id, Some(user_id))
722 Ok(PostResponse { post: post_view })
726 #[async_trait::async_trait(?Send)]
727 impl Perform for PostJoin {
728 type Response = PostJoinResponse;
732 context: &Data<LemmyContext>,
733 websocket_id: Option<ConnectionId>,
734 ) -> Result<PostJoinResponse, LemmyError> {
735 let data: &PostJoin = &self;
737 if let Some(ws_id) = websocket_id {
738 context.chat_server().do_send(JoinPostRoom {
739 post_id: data.post_id,
744 Ok(PostJoinResponse { joined: true })
748 /// Creates a post report and notifies the moderators of the community
749 #[async_trait::async_trait(?Send)]
750 impl Perform for CreatePostReport {
751 type Response = CreatePostReportResponse;
755 context: &Data<LemmyContext>,
756 websocket_id: Option<ConnectionId>,
757 ) -> Result<CreatePostReportResponse, LemmyError> {
758 let data: &CreatePostReport = &self;
759 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
761 // check size of report and check for whitespace
762 let reason = data.reason.trim();
763 if reason.is_empty() {
764 return Err(APIError::err("report_reason_required").into());
766 if reason.len() > 1000 {
767 return Err(APIError::err("report_too_long").into());
770 let user_id = user.id;
771 let post_id = data.post_id;
772 let post = blocking(context.pool(), move |conn| {
773 PostView::read(&conn, post_id, None)
777 check_community_ban(user_id, post.community_id, context.pool()).await?;
779 let report_form = PostReportForm {
782 original_post_name: post.name,
783 original_post_url: post.url,
784 original_post_body: post.body,
785 reason: data.reason.to_owned(),
788 let report = match blocking(context.pool(), move |conn| {
789 PostReport::report(conn, &report_form)
793 Ok(report) => report,
794 Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
797 let res = CreatePostReportResponse { success: true };
799 context.chat_server().do_send(SendUserRoomMessage {
800 op: UserOperation::CreatePostReport,
801 response: res.clone(),
802 recipient_id: user.id,
806 context.chat_server().do_send(SendModRoomMessage {
807 op: UserOperation::CreatePostReport,
809 community_id: post.community_id,
817 /// Resolves or unresolves a post report and notifies the moderators of the community
818 #[async_trait::async_trait(?Send)]
819 impl Perform for ResolvePostReport {
820 type Response = ResolvePostReportResponse;
824 context: &Data<LemmyContext>,
825 websocket_id: Option<ConnectionId>,
826 ) -> Result<ResolvePostReportResponse, LemmyError> {
827 let data: &ResolvePostReport = &self;
828 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
830 let report_id = data.report_id;
831 let report = blocking(context.pool(), move |conn| {
832 PostReportView::read(&conn, report_id)
836 let user_id = user.id;
837 is_mod_or_admin(context.pool(), user_id, report.community_id).await?;
839 let resolved = data.resolved;
840 let resolve_fun = move |conn: &'_ _| {
842 PostReport::resolve(conn, report_id, user_id)
844 PostReport::unresolve(conn, report_id, user_id)
848 let res = ResolvePostReportResponse {
853 if blocking(context.pool(), resolve_fun).await?.is_err() {
854 return Err(APIError::err("couldnt_resolve_report").into());
857 context.chat_server().do_send(SendModRoomMessage {
858 op: UserOperation::ResolvePostReport,
859 response: res.clone(),
860 community_id: report.community_id,
868 /// Lists post reports for a community if an id is supplied
869 /// or returns all post reports for communities a user moderates
870 #[async_trait::async_trait(?Send)]
871 impl Perform for ListPostReports {
872 type Response = ListPostReportsResponse;
876 context: &Data<LemmyContext>,
877 websocket_id: Option<ConnectionId>,
878 ) -> Result<ListPostReportsResponse, LemmyError> {
879 let data: &ListPostReports = &self;
880 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
882 let user_id = user.id;
883 let community_id = data.community;
885 collect_moderated_communities(user_id, community_id, context.pool()).await?;
887 let page = data.page;
888 let limit = data.limit;
889 let posts = blocking(context.pool(), move |conn| {
890 PostReportQueryBuilder::create(conn)
891 .community_ids(community_ids)
898 let res = ListPostReportsResponse { posts };
900 context.chat_server().do_send(SendUserRoomMessage {
901 op: UserOperation::ListPostReports,
902 response: res.clone(),
903 recipient_id: user.id,