4 collect_moderated_communities,
10 use actix_web::web::Data;
11 use lemmy_apub::{ApubLikeableType, ApubObjectType};
17 post_report::{PostReport, PostReportForm},
20 comment_view::CommentQueryBuilder,
21 community::{community_moderator_view::CommunityModeratorView, community_view::CommunityView},
22 post_report_view::{PostReportQueryBuilder, PostReportView},
23 post_view::{PostQueryBuilder, PostView},
33 use lemmy_structs::{blocking, post::*};
35 apub::{make_apub_endpoint, EndpointType},
36 request::fetch_iframely_and_pictrs_data,
37 utils::{check_slurs, check_slurs_opt, is_valid_post_title},
42 use lemmy_websocket::{
43 messages::{GetPostUsersOnline, JoinPostRoom, SendModRoomMessage, SendPost, SendUserRoomMessage},
47 use std::str::FromStr;
49 #[async_trait::async_trait(?Send)]
50 impl Perform for CreatePost {
51 type Response = PostResponse;
55 context: &Data<LemmyContext>,
56 websocket_id: Option<ConnectionId>,
57 ) -> Result<PostResponse, LemmyError> {
58 let data: &CreatePost = &self;
59 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
61 check_slurs(&data.name)?;
62 check_slurs_opt(&data.body)?;
64 if !is_valid_post_title(&data.name) {
65 return Err(APIError::err("invalid_post_title").into());
68 check_community_ban(user.id, data.community_id, context.pool()).await?;
70 check_optional_url(&Some(data.url.to_owned()))?;
72 // Fetch Iframely and pictrs cached image
73 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
74 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
76 let post_form = PostForm {
77 name: data.name.trim().to_owned(),
78 url: data.url.to_owned(),
79 body: data.body.to_owned(),
80 community_id: data.community_id,
88 embed_title: iframely_title,
89 embed_description: iframely_description,
90 embed_html: iframely_html,
91 thumbnail_url: pictrs_thumbnail,
98 match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
101 let err_type = if e.to_string() == "value too long for type character varying(200)" {
102 "post_title_too_long"
104 "couldnt_create_post"
107 return Err(APIError::err(err_type).into());
111 let inserted_post_id = inserted_post.id;
112 let updated_post = match blocking(context.pool(), move |conn| {
114 make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
115 Post::update_ap_id(conn, inserted_post_id, apub_id)
120 Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
123 updated_post.send_create(&user, context).await?;
125 // They like their own post by default
126 let like_form = PostLikeForm {
127 post_id: inserted_post.id,
132 let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
133 if blocking(context.pool(), like).await?.is_err() {
134 return Err(APIError::err("couldnt_like_post").into());
137 updated_post.send_like(&user, context).await?;
140 let inserted_post_id = inserted_post.id;
141 let post_view = match blocking(context.pool(), move |conn| {
142 PostView::read(conn, inserted_post_id, Some(user.id))
147 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
150 let res = PostResponse { post_view };
152 context.chat_server().do_send(SendPost {
153 op: UserOperation::CreatePost,
162 #[async_trait::async_trait(?Send)]
163 impl Perform for GetPost {
164 type Response = GetPostResponse;
168 context: &Data<LemmyContext>,
169 _websocket_id: Option<ConnectionId>,
170 ) -> Result<GetPostResponse, LemmyError> {
171 let data: &GetPost = &self;
172 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
173 let user_id = user.map(|u| u.id);
176 let post_view = match blocking(context.pool(), move |conn| {
177 PostView::read(conn, id, user_id)
182 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
186 let comments = blocking(context.pool(), move |conn| {
187 CommentQueryBuilder::create(conn)
195 let community_id = post_view.community.id;
196 let community = blocking(context.pool(), move |conn| {
197 CommunityView::read(conn, community_id, user_id)
201 let community_id = post_view.community.id;
202 let moderators = blocking(context.pool(), move |conn| {
203 CommunityModeratorView::for_community(conn, community_id)
209 .send(GetPostUsersOnline { post_id: data.id })
224 #[async_trait::async_trait(?Send)]
225 impl Perform for GetPosts {
226 type Response = GetPostsResponse;
230 context: &Data<LemmyContext>,
231 _websocket_id: Option<ConnectionId>,
232 ) -> Result<GetPostsResponse, LemmyError> {
233 let data: &GetPosts = &self;
234 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
236 let user_id = match &user {
237 Some(user) => Some(user.id),
241 let show_nsfw = match &user {
242 Some(user) => user.show_nsfw,
246 let type_ = ListingType::from_str(&data.type_)?;
247 let sort = SortType::from_str(&data.sort)?;
249 let page = data.page;
250 let limit = data.limit;
251 let community_id = data.community_id;
252 let community_name = data.community_name.to_owned();
253 let posts = match blocking(context.pool(), move |conn| {
254 PostQueryBuilder::create(conn)
255 .listing_type(&type_)
257 .show_nsfw(show_nsfw)
258 .community_id(community_id)
259 .community_name(community_name)
268 Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
271 Ok(GetPostsResponse { posts })
275 #[async_trait::async_trait(?Send)]
276 impl Perform for CreatePostLike {
277 type Response = PostResponse;
281 context: &Data<LemmyContext>,
282 websocket_id: Option<ConnectionId>,
283 ) -> Result<PostResponse, LemmyError> {
284 let data: &CreatePostLike = &self;
285 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
287 // Don't do a downvote if site has downvotes disabled
288 if data.score == -1 {
289 let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
290 if !site_view.site.enable_downvotes {
291 return Err(APIError::err("downvotes_disabled").into());
295 // Check for a community ban
296 let post_id = data.post_id;
297 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
299 check_community_ban(user.id, post.community_id, context.pool()).await?;
301 let like_form = PostLikeForm {
302 post_id: data.post_id,
307 // Remove any likes first
308 let user_id = user.id;
309 blocking(context.pool(), move |conn| {
310 PostLike::remove(conn, user_id, post_id)
314 // Only add the like if the score isnt 0
315 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
317 let like_form2 = like_form.clone();
318 let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
319 if blocking(context.pool(), like).await?.is_err() {
320 return Err(APIError::err("couldnt_like_post").into());
323 if like_form.score == 1 {
324 post.send_like(&user, context).await?;
325 } else if like_form.score == -1 {
326 post.send_dislike(&user, context).await?;
329 post.send_undo_like(&user, context).await?;
332 let post_id = data.post_id;
333 let user_id = user.id;
334 let post_view = match blocking(context.pool(), move |conn| {
335 PostView::read(conn, post_id, Some(user_id))
340 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
343 let res = PostResponse { post_view };
345 context.chat_server().do_send(SendPost {
346 op: UserOperation::CreatePostLike,
355 #[async_trait::async_trait(?Send)]
356 impl Perform for EditPost {
357 type Response = PostResponse;
361 context: &Data<LemmyContext>,
362 websocket_id: Option<ConnectionId>,
363 ) -> Result<PostResponse, LemmyError> {
364 let data: &EditPost = &self;
365 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
367 check_slurs(&data.name)?;
368 check_slurs_opt(&data.body)?;
370 if !is_valid_post_title(&data.name) {
371 return Err(APIError::err("invalid_post_title").into());
374 let edit_id = data.edit_id;
375 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
377 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
379 // Verify that only the creator can edit
380 if !Post::is_post_creator(user.id, orig_post.creator_id) {
381 return Err(APIError::err("no_post_edit_allowed").into());
384 // Fetch Iframely and Pictrs cached image
385 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
386 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
388 let post_form = PostForm {
389 name: data.name.trim().to_owned(),
390 url: data.url.to_owned(),
391 body: data.body.to_owned(),
393 creator_id: orig_post.creator_id.to_owned(),
394 community_id: orig_post.community_id,
395 removed: Some(orig_post.removed),
396 deleted: Some(orig_post.deleted),
397 locked: Some(orig_post.locked),
398 stickied: Some(orig_post.stickied),
399 updated: Some(naive_now()),
400 embed_title: iframely_title,
401 embed_description: iframely_description,
402 embed_html: iframely_html,
403 thumbnail_url: pictrs_thumbnail,
404 ap_id: Some(orig_post.ap_id),
405 local: orig_post.local,
409 let edit_id = data.edit_id;
410 let res = blocking(context.pool(), move |conn| {
411 Post::update(conn, edit_id, &post_form)
414 let updated_post: Post = match res {
417 let err_type = if e.to_string() == "value too long for type character varying(200)" {
418 "post_title_too_long"
420 "couldnt_update_post"
423 return Err(APIError::err(err_type).into());
428 updated_post.send_update(&user, context).await?;
430 let edit_id = data.edit_id;
431 let post_view = blocking(context.pool(), move |conn| {
432 PostView::read(conn, edit_id, Some(user.id))
436 let res = PostResponse { post_view };
438 context.chat_server().do_send(SendPost {
439 op: UserOperation::EditPost,
448 #[async_trait::async_trait(?Send)]
449 impl Perform for DeletePost {
450 type Response = PostResponse;
454 context: &Data<LemmyContext>,
455 websocket_id: Option<ConnectionId>,
456 ) -> Result<PostResponse, LemmyError> {
457 let data: &DeletePost = &self;
458 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
460 let edit_id = data.edit_id;
461 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
463 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
465 // Verify that only the creator can delete
466 if !Post::is_post_creator(user.id, orig_post.creator_id) {
467 return Err(APIError::err("no_post_edit_allowed").into());
471 let edit_id = data.edit_id;
472 let deleted = data.deleted;
473 let updated_post = blocking(context.pool(), move |conn| {
474 Post::update_deleted(conn, edit_id, deleted)
480 updated_post.send_delete(&user, context).await?;
482 updated_post.send_undo_delete(&user, context).await?;
486 let edit_id = data.edit_id;
487 let post_view = blocking(context.pool(), move |conn| {
488 PostView::read(conn, edit_id, Some(user.id))
492 let res = PostResponse { post_view };
494 context.chat_server().do_send(SendPost {
495 op: UserOperation::DeletePost,
504 #[async_trait::async_trait(?Send)]
505 impl Perform for RemovePost {
506 type Response = PostResponse;
510 context: &Data<LemmyContext>,
511 websocket_id: Option<ConnectionId>,
512 ) -> Result<PostResponse, LemmyError> {
513 let data: &RemovePost = &self;
514 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
516 let edit_id = data.edit_id;
517 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
519 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
521 // Verify that only the mods can remove
522 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
525 let edit_id = data.edit_id;
526 let removed = data.removed;
527 let updated_post = blocking(context.pool(), move |conn| {
528 Post::update_removed(conn, edit_id, removed)
533 let form = ModRemovePostForm {
534 mod_user_id: user.id,
535 post_id: data.edit_id,
536 removed: Some(removed),
537 reason: data.reason.to_owned(),
539 blocking(context.pool(), move |conn| {
540 ModRemovePost::create(conn, &form)
546 updated_post.send_remove(&user, context).await?;
548 updated_post.send_undo_remove(&user, context).await?;
552 let edit_id = data.edit_id;
553 let user_id = user.id;
554 let post_view = blocking(context.pool(), move |conn| {
555 PostView::read(conn, edit_id, Some(user_id))
559 let res = PostResponse { post_view };
561 context.chat_server().do_send(SendPost {
562 op: UserOperation::RemovePost,
571 #[async_trait::async_trait(?Send)]
572 impl Perform for LockPost {
573 type Response = PostResponse;
577 context: &Data<LemmyContext>,
578 websocket_id: Option<ConnectionId>,
579 ) -> Result<PostResponse, LemmyError> {
580 let data: &LockPost = &self;
581 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
583 let edit_id = data.edit_id;
584 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
586 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
588 // Verify that only the mods can lock
589 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
592 let edit_id = data.edit_id;
593 let locked = data.locked;
594 let updated_post = blocking(context.pool(), move |conn| {
595 Post::update_locked(conn, edit_id, locked)
600 let form = ModLockPostForm {
601 mod_user_id: user.id,
602 post_id: data.edit_id,
603 locked: Some(locked),
605 blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
608 updated_post.send_update(&user, context).await?;
611 let edit_id = data.edit_id;
612 let post_view = blocking(context.pool(), move |conn| {
613 PostView::read(conn, edit_id, Some(user.id))
617 let res = PostResponse { post_view };
619 context.chat_server().do_send(SendPost {
620 op: UserOperation::LockPost,
629 #[async_trait::async_trait(?Send)]
630 impl Perform for StickyPost {
631 type Response = PostResponse;
635 context: &Data<LemmyContext>,
636 websocket_id: Option<ConnectionId>,
637 ) -> Result<PostResponse, LemmyError> {
638 let data: &StickyPost = &self;
639 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
641 let edit_id = data.edit_id;
642 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
644 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
646 // Verify that only the mods can sticky
647 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
650 let edit_id = data.edit_id;
651 let stickied = data.stickied;
652 let updated_post = blocking(context.pool(), move |conn| {
653 Post::update_stickied(conn, edit_id, stickied)
658 let form = ModStickyPostForm {
659 mod_user_id: user.id,
660 post_id: data.edit_id,
661 stickied: Some(stickied),
663 blocking(context.pool(), move |conn| {
664 ModStickyPost::create(conn, &form)
669 // TODO stickied should pry work like locked for ease of use
670 updated_post.send_update(&user, context).await?;
673 let edit_id = data.edit_id;
674 let post_view = blocking(context.pool(), move |conn| {
675 PostView::read(conn, edit_id, Some(user.id))
679 let res = PostResponse { post_view };
681 context.chat_server().do_send(SendPost {
682 op: UserOperation::StickyPost,
691 #[async_trait::async_trait(?Send)]
692 impl Perform for SavePost {
693 type Response = PostResponse;
697 context: &Data<LemmyContext>,
698 _websocket_id: Option<ConnectionId>,
699 ) -> Result<PostResponse, LemmyError> {
700 let data: &SavePost = &self;
701 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
703 let post_saved_form = PostSavedForm {
704 post_id: data.post_id,
709 let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
710 if blocking(context.pool(), save).await?.is_err() {
711 return Err(APIError::err("couldnt_save_post").into());
714 let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
715 if blocking(context.pool(), unsave).await?.is_err() {
716 return Err(APIError::err("couldnt_save_post").into());
720 let post_id = data.post_id;
721 let user_id = user.id;
722 let post_view = blocking(context.pool(), move |conn| {
723 PostView::read(conn, post_id, Some(user_id))
727 Ok(PostResponse { post_view })
731 #[async_trait::async_trait(?Send)]
732 impl Perform for PostJoin {
733 type Response = PostJoinResponse;
737 context: &Data<LemmyContext>,
738 websocket_id: Option<ConnectionId>,
739 ) -> Result<PostJoinResponse, LemmyError> {
740 let data: &PostJoin = &self;
742 if let Some(ws_id) = websocket_id {
743 context.chat_server().do_send(JoinPostRoom {
744 post_id: data.post_id,
749 Ok(PostJoinResponse { joined: true })
753 /// Creates a post report and notifies the moderators of the community
754 #[async_trait::async_trait(?Send)]
755 impl Perform for CreatePostReport {
756 type Response = CreatePostReportResponse;
760 context: &Data<LemmyContext>,
761 websocket_id: Option<ConnectionId>,
762 ) -> Result<CreatePostReportResponse, LemmyError> {
763 let data: &CreatePostReport = &self;
764 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
766 // check size of report and check for whitespace
767 let reason = data.reason.trim();
768 if reason.is_empty() {
769 return Err(APIError::err("report_reason_required").into());
771 if reason.len() > 1000 {
772 return Err(APIError::err("report_too_long").into());
775 let user_id = user.id;
776 let post_id = data.post_id;
777 let post_view = blocking(context.pool(), move |conn| {
778 PostView::read(&conn, post_id, None)
782 check_community_ban(user_id, post_view.community.id, context.pool()).await?;
784 let report_form = PostReportForm {
787 original_post_name: post_view.post.name,
788 original_post_url: post_view.post.url,
789 original_post_body: post_view.post.body,
790 reason: data.reason.to_owned(),
793 let report = match blocking(context.pool(), move |conn| {
794 PostReport::report(conn, &report_form)
798 Ok(report) => report,
799 Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
802 let res = CreatePostReportResponse { success: true };
804 context.chat_server().do_send(SendUserRoomMessage {
805 op: UserOperation::CreatePostReport,
806 response: res.clone(),
807 recipient_id: user.id,
811 context.chat_server().do_send(SendModRoomMessage {
812 op: UserOperation::CreatePostReport,
814 community_id: post_view.community.id,
822 /// Resolves or unresolves a post report and notifies the moderators of the community
823 #[async_trait::async_trait(?Send)]
824 impl Perform for ResolvePostReport {
825 type Response = ResolvePostReportResponse;
829 context: &Data<LemmyContext>,
830 websocket_id: Option<ConnectionId>,
831 ) -> Result<ResolvePostReportResponse, LemmyError> {
832 let data: &ResolvePostReport = &self;
833 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
835 let report_id = data.report_id;
836 let report = blocking(context.pool(), move |conn| {
837 PostReportView::read(&conn, report_id)
841 let user_id = user.id;
842 is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
844 let resolved = data.resolved;
845 let resolve_fun = move |conn: &'_ _| {
847 PostReport::resolve(conn, report_id, user_id)
849 PostReport::unresolve(conn, report_id, user_id)
853 let res = ResolvePostReportResponse {
858 if blocking(context.pool(), resolve_fun).await?.is_err() {
859 return Err(APIError::err("couldnt_resolve_report").into());
862 context.chat_server().do_send(SendModRoomMessage {
863 op: UserOperation::ResolvePostReport,
864 response: res.clone(),
865 community_id: report.community.id,
873 /// Lists post reports for a community if an id is supplied
874 /// or returns all post reports for communities a user moderates
875 #[async_trait::async_trait(?Send)]
876 impl Perform for ListPostReports {
877 type Response = ListPostReportsResponse;
881 context: &Data<LemmyContext>,
882 websocket_id: Option<ConnectionId>,
883 ) -> Result<ListPostReportsResponse, LemmyError> {
884 let data: &ListPostReports = &self;
885 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
887 let user_id = user.id;
888 let community_id = data.community;
890 collect_moderated_communities(user_id, community_id, context.pool()).await?;
892 let page = data.page;
893 let limit = data.limit;
894 let posts = blocking(context.pool(), move |conn| {
895 PostReportQueryBuilder::create(conn)
896 .community_ids(community_ids)
903 let res = ListPostReportsResponse { posts };
905 context.chat_server().do_send(SendUserRoomMessage {
906 op: UserOperation::ListPostReports,
907 response: res.clone(),
908 recipient_id: user.id,