3 check_downvotes_enabled,
5 collect_moderated_communities,
11 use actix_web::web::Data;
12 use lemmy_apub::{ApubLikeableType, ApubObjectType};
18 post_report::{PostReport, PostReportForm},
21 comment_view::CommentQueryBuilder,
22 community::community_moderator_view::CommunityModeratorView,
23 post_report_view::{PostReportQueryBuilder, PostReportView},
24 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 moderators = blocking(context.pool(), move |conn| {
197 CommunityModeratorView::for_community(conn, community_id)
203 .send(GetPostUsersOnline { post_id: data.id })
217 #[async_trait::async_trait(?Send)]
218 impl Perform for GetPosts {
219 type Response = GetPostsResponse;
223 context: &Data<LemmyContext>,
224 _websocket_id: Option<ConnectionId>,
225 ) -> Result<GetPostsResponse, LemmyError> {
226 let data: &GetPosts = &self;
227 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
229 let user_id = match &user {
230 Some(user) => Some(user.id),
234 let show_nsfw = match &user {
235 Some(user) => user.show_nsfw,
239 let type_ = ListingType::from_str(&data.type_)?;
240 let sort = SortType::from_str(&data.sort)?;
242 let page = data.page;
243 let limit = data.limit;
244 let community_id = data.community_id;
245 let community_name = data.community_name.to_owned();
246 let posts = match blocking(context.pool(), move |conn| {
247 PostQueryBuilder::create(conn)
248 .listing_type(&type_)
250 .show_nsfw(show_nsfw)
251 .community_id(community_id)
252 .community_name(community_name)
261 Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
264 Ok(GetPostsResponse { posts })
268 #[async_trait::async_trait(?Send)]
269 impl Perform for CreatePostLike {
270 type Response = PostResponse;
274 context: &Data<LemmyContext>,
275 websocket_id: Option<ConnectionId>,
276 ) -> Result<PostResponse, LemmyError> {
277 let data: &CreatePostLike = &self;
278 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
280 // Don't do a downvote if site has downvotes disabled
281 check_downvotes_enabled(data.score, context.pool()).await?;
283 // Check for a community ban
284 let post_id = data.post_id;
285 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
287 check_community_ban(user.id, post.community_id, context.pool()).await?;
289 let like_form = PostLikeForm {
290 post_id: data.post_id,
295 // Remove any likes first
296 let user_id = user.id;
297 blocking(context.pool(), move |conn| {
298 PostLike::remove(conn, user_id, post_id)
302 // Only add the like if the score isnt 0
303 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
305 let like_form2 = like_form.clone();
306 let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
307 if blocking(context.pool(), like).await?.is_err() {
308 return Err(APIError::err("couldnt_like_post").into());
311 if like_form.score == 1 {
312 post.send_like(&user, context).await?;
313 } else if like_form.score == -1 {
314 post.send_dislike(&user, context).await?;
317 post.send_undo_like(&user, context).await?;
320 let post_id = data.post_id;
321 let user_id = user.id;
322 let post_view = match blocking(context.pool(), move |conn| {
323 PostView::read(conn, post_id, Some(user_id))
328 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
331 let res = PostResponse { post_view };
333 context.chat_server().do_send(SendPost {
334 op: UserOperation::CreatePostLike,
343 #[async_trait::async_trait(?Send)]
344 impl Perform for EditPost {
345 type Response = PostResponse;
349 context: &Data<LemmyContext>,
350 websocket_id: Option<ConnectionId>,
351 ) -> Result<PostResponse, LemmyError> {
352 let data: &EditPost = &self;
353 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
355 check_slurs(&data.name)?;
356 check_slurs_opt(&data.body)?;
358 if !is_valid_post_title(&data.name) {
359 return Err(APIError::err("invalid_post_title").into());
362 let edit_id = data.edit_id;
363 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
365 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
367 // Verify that only the creator can edit
368 if !Post::is_post_creator(user.id, orig_post.creator_id) {
369 return Err(APIError::err("no_post_edit_allowed").into());
372 // Fetch Iframely and Pictrs cached image
373 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
374 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
376 let post_form = PostForm {
377 name: data.name.trim().to_owned(),
378 url: data.url.to_owned(),
379 body: data.body.to_owned(),
381 creator_id: orig_post.creator_id.to_owned(),
382 community_id: orig_post.community_id,
383 removed: Some(orig_post.removed),
384 deleted: Some(orig_post.deleted),
385 locked: Some(orig_post.locked),
386 stickied: Some(orig_post.stickied),
387 updated: Some(naive_now()),
388 embed_title: iframely_title,
389 embed_description: iframely_description,
390 embed_html: iframely_html,
391 thumbnail_url: pictrs_thumbnail,
392 ap_id: Some(orig_post.ap_id),
393 local: orig_post.local,
397 let edit_id = data.edit_id;
398 let res = blocking(context.pool(), move |conn| {
399 Post::update(conn, edit_id, &post_form)
402 let updated_post: Post = match res {
405 let err_type = if e.to_string() == "value too long for type character varying(200)" {
406 "post_title_too_long"
408 "couldnt_update_post"
411 return Err(APIError::err(err_type).into());
416 updated_post.send_update(&user, context).await?;
418 let edit_id = data.edit_id;
419 let post_view = blocking(context.pool(), move |conn| {
420 PostView::read(conn, edit_id, Some(user.id))
424 let res = PostResponse { post_view };
426 context.chat_server().do_send(SendPost {
427 op: UserOperation::EditPost,
436 #[async_trait::async_trait(?Send)]
437 impl Perform for DeletePost {
438 type Response = PostResponse;
442 context: &Data<LemmyContext>,
443 websocket_id: Option<ConnectionId>,
444 ) -> Result<PostResponse, LemmyError> {
445 let data: &DeletePost = &self;
446 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
448 let edit_id = data.edit_id;
449 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
451 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
453 // Verify that only the creator can delete
454 if !Post::is_post_creator(user.id, orig_post.creator_id) {
455 return Err(APIError::err("no_post_edit_allowed").into());
459 let edit_id = data.edit_id;
460 let deleted = data.deleted;
461 let updated_post = blocking(context.pool(), move |conn| {
462 Post::update_deleted(conn, edit_id, deleted)
468 updated_post.send_delete(&user, context).await?;
470 updated_post.send_undo_delete(&user, context).await?;
474 let edit_id = data.edit_id;
475 let post_view = blocking(context.pool(), move |conn| {
476 PostView::read(conn, edit_id, Some(user.id))
480 let res = PostResponse { post_view };
482 context.chat_server().do_send(SendPost {
483 op: UserOperation::DeletePost,
492 #[async_trait::async_trait(?Send)]
493 impl Perform for RemovePost {
494 type Response = PostResponse;
498 context: &Data<LemmyContext>,
499 websocket_id: Option<ConnectionId>,
500 ) -> Result<PostResponse, LemmyError> {
501 let data: &RemovePost = &self;
502 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
504 let edit_id = data.edit_id;
505 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
507 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
509 // Verify that only the mods can remove
510 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
513 let edit_id = data.edit_id;
514 let removed = data.removed;
515 let updated_post = blocking(context.pool(), move |conn| {
516 Post::update_removed(conn, edit_id, removed)
521 let form = ModRemovePostForm {
522 mod_user_id: user.id,
523 post_id: data.edit_id,
524 removed: Some(removed),
525 reason: data.reason.to_owned(),
527 blocking(context.pool(), move |conn| {
528 ModRemovePost::create(conn, &form)
534 updated_post.send_remove(&user, context).await?;
536 updated_post.send_undo_remove(&user, context).await?;
540 let edit_id = data.edit_id;
541 let user_id = user.id;
542 let post_view = blocking(context.pool(), move |conn| {
543 PostView::read(conn, edit_id, Some(user_id))
547 let res = PostResponse { post_view };
549 context.chat_server().do_send(SendPost {
550 op: UserOperation::RemovePost,
559 #[async_trait::async_trait(?Send)]
560 impl Perform for LockPost {
561 type Response = PostResponse;
565 context: &Data<LemmyContext>,
566 websocket_id: Option<ConnectionId>,
567 ) -> Result<PostResponse, LemmyError> {
568 let data: &LockPost = &self;
569 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
571 let edit_id = data.edit_id;
572 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
574 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
576 // Verify that only the mods can lock
577 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
580 let edit_id = data.edit_id;
581 let locked = data.locked;
582 let updated_post = blocking(context.pool(), move |conn| {
583 Post::update_locked(conn, edit_id, locked)
588 let form = ModLockPostForm {
589 mod_user_id: user.id,
590 post_id: data.edit_id,
591 locked: Some(locked),
593 blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
596 updated_post.send_update(&user, context).await?;
599 let edit_id = data.edit_id;
600 let post_view = blocking(context.pool(), move |conn| {
601 PostView::read(conn, edit_id, Some(user.id))
605 let res = PostResponse { post_view };
607 context.chat_server().do_send(SendPost {
608 op: UserOperation::LockPost,
617 #[async_trait::async_trait(?Send)]
618 impl Perform for StickyPost {
619 type Response = PostResponse;
623 context: &Data<LemmyContext>,
624 websocket_id: Option<ConnectionId>,
625 ) -> Result<PostResponse, LemmyError> {
626 let data: &StickyPost = &self;
627 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
629 let edit_id = data.edit_id;
630 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
632 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
634 // Verify that only the mods can sticky
635 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
638 let edit_id = data.edit_id;
639 let stickied = data.stickied;
640 let updated_post = blocking(context.pool(), move |conn| {
641 Post::update_stickied(conn, edit_id, stickied)
646 let form = ModStickyPostForm {
647 mod_user_id: user.id,
648 post_id: data.edit_id,
649 stickied: Some(stickied),
651 blocking(context.pool(), move |conn| {
652 ModStickyPost::create(conn, &form)
657 // TODO stickied should pry work like locked for ease of use
658 updated_post.send_update(&user, context).await?;
661 let edit_id = data.edit_id;
662 let post_view = blocking(context.pool(), move |conn| {
663 PostView::read(conn, edit_id, Some(user.id))
667 let res = PostResponse { post_view };
669 context.chat_server().do_send(SendPost {
670 op: UserOperation::StickyPost,
679 #[async_trait::async_trait(?Send)]
680 impl Perform for SavePost {
681 type Response = PostResponse;
685 context: &Data<LemmyContext>,
686 _websocket_id: Option<ConnectionId>,
687 ) -> Result<PostResponse, LemmyError> {
688 let data: &SavePost = &self;
689 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
691 let post_saved_form = PostSavedForm {
692 post_id: data.post_id,
697 let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
698 if blocking(context.pool(), save).await?.is_err() {
699 return Err(APIError::err("couldnt_save_post").into());
702 let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
703 if blocking(context.pool(), unsave).await?.is_err() {
704 return Err(APIError::err("couldnt_save_post").into());
708 let post_id = data.post_id;
709 let user_id = user.id;
710 let post_view = blocking(context.pool(), move |conn| {
711 PostView::read(conn, post_id, Some(user_id))
715 Ok(PostResponse { post_view })
719 #[async_trait::async_trait(?Send)]
720 impl Perform for PostJoin {
721 type Response = PostJoinResponse;
725 context: &Data<LemmyContext>,
726 websocket_id: Option<ConnectionId>,
727 ) -> Result<PostJoinResponse, LemmyError> {
728 let data: &PostJoin = &self;
730 if let Some(ws_id) = websocket_id {
731 context.chat_server().do_send(JoinPostRoom {
732 post_id: data.post_id,
737 Ok(PostJoinResponse { joined: true })
741 /// Creates a post report and notifies the moderators of the community
742 #[async_trait::async_trait(?Send)]
743 impl Perform for CreatePostReport {
744 type Response = CreatePostReportResponse;
748 context: &Data<LemmyContext>,
749 websocket_id: Option<ConnectionId>,
750 ) -> Result<CreatePostReportResponse, LemmyError> {
751 let data: &CreatePostReport = &self;
752 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
754 // check size of report and check for whitespace
755 let reason = data.reason.trim();
756 if reason.is_empty() {
757 return Err(APIError::err("report_reason_required").into());
759 if reason.len() > 1000 {
760 return Err(APIError::err("report_too_long").into());
763 let user_id = user.id;
764 let post_id = data.post_id;
765 let post_view = blocking(context.pool(), move |conn| {
766 PostView::read(&conn, post_id, None)
770 check_community_ban(user_id, post_view.community.id, context.pool()).await?;
772 let report_form = PostReportForm {
775 original_post_name: post_view.post.name,
776 original_post_url: post_view.post.url,
777 original_post_body: post_view.post.body,
778 reason: data.reason.to_owned(),
781 let report = match blocking(context.pool(), move |conn| {
782 PostReport::report(conn, &report_form)
786 Ok(report) => report,
787 Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
790 let res = CreatePostReportResponse { success: true };
792 context.chat_server().do_send(SendUserRoomMessage {
793 op: UserOperation::CreatePostReport,
794 response: res.clone(),
795 recipient_id: user.id,
799 context.chat_server().do_send(SendModRoomMessage {
800 op: UserOperation::CreatePostReport,
802 community_id: post_view.community.id,
810 /// Resolves or unresolves a post report and notifies the moderators of the community
811 #[async_trait::async_trait(?Send)]
812 impl Perform for ResolvePostReport {
813 type Response = ResolvePostReportResponse;
817 context: &Data<LemmyContext>,
818 websocket_id: Option<ConnectionId>,
819 ) -> Result<ResolvePostReportResponse, LemmyError> {
820 let data: &ResolvePostReport = &self;
821 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
823 let report_id = data.report_id;
824 let report = blocking(context.pool(), move |conn| {
825 PostReportView::read(&conn, report_id)
829 let user_id = user.id;
830 is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
832 let resolved = data.resolved;
833 let resolve_fun = move |conn: &'_ _| {
835 PostReport::resolve(conn, report_id, user_id)
837 PostReport::unresolve(conn, report_id, user_id)
841 let res = ResolvePostReportResponse {
846 if blocking(context.pool(), resolve_fun).await?.is_err() {
847 return Err(APIError::err("couldnt_resolve_report").into());
850 context.chat_server().do_send(SendModRoomMessage {
851 op: UserOperation::ResolvePostReport,
852 response: res.clone(),
853 community_id: report.community.id,
861 /// Lists post reports for a community if an id is supplied
862 /// or returns all post reports for communities a user moderates
863 #[async_trait::async_trait(?Send)]
864 impl Perform for ListPostReports {
865 type Response = ListPostReportsResponse;
869 context: &Data<LemmyContext>,
870 websocket_id: Option<ConnectionId>,
871 ) -> Result<ListPostReportsResponse, LemmyError> {
872 let data: &ListPostReports = &self;
873 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
875 let user_id = user.id;
876 let community_id = data.community;
878 collect_moderated_communities(user_id, community_id, context.pool()).await?;
880 let page = data.page;
881 let limit = data.limit;
882 let posts = blocking(context.pool(), move |conn| {
883 PostReportQueryBuilder::create(conn)
884 .community_ids(community_ids)
891 let res = ListPostReportsResponse { posts };
893 context.chat_server().do_send(SendUserRoomMessage {
894 op: UserOperation::ListPostReports,
895 response: res.clone(),
896 recipient_id: user.id,