2 api::{check_community_ban, get_user_from_jwt, get_user_from_jwt_opt, is_mod_or_admin, Perform},
3 apub::{ApubLikeableType, ApubObjectType},
4 fetch_iframely_and_pictrs_data,
6 messages::{GetPostUsersOnline, JoinCommunityRoom, JoinPostRoom, SendPost},
11 use actix_web::web::Data;
12 use lemmy_api_structs::{blocking, post::*};
28 apub::{make_apub_endpoint, EndpointType},
29 utils::{check_slurs, check_slurs_opt, is_valid_post_title},
34 use std::str::FromStr;
37 #[async_trait::async_trait(?Send)]
38 impl Perform for CreatePost {
39 type Response = PostResponse;
43 context: &Data<LemmyContext>,
44 websocket_id: Option<ConnectionId>,
45 ) -> Result<PostResponse, LemmyError> {
46 let data: &CreatePost = &self;
47 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
49 check_slurs(&data.name)?;
50 check_slurs_opt(&data.body)?;
52 if !is_valid_post_title(&data.name) {
53 return Err(APIError::err("invalid_post_title").into());
56 check_community_ban(user.id, data.community_id, context.pool()).await?;
58 if let Some(url) = data.url.as_ref() {
59 match Url::parse(url) {
61 Err(_e) => return Err(APIError::err("invalid_url").into()),
65 // Fetch Iframely and pictrs cached image
66 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
67 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
69 let post_form = PostForm {
70 name: data.name.trim().to_owned(),
71 url: data.url.to_owned(),
72 body: data.body.to_owned(),
73 community_id: data.community_id,
81 embed_title: iframely_title,
82 embed_description: iframely_description,
83 embed_html: iframely_html,
84 thumbnail_url: pictrs_thumbnail,
91 match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
94 let err_type = if e.to_string() == "value too long for type character varying(200)" {
100 return Err(APIError::err(err_type).into());
104 let inserted_post_id = inserted_post.id;
105 let updated_post = match blocking(context.pool(), move |conn| {
107 make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
108 Post::update_ap_id(conn, inserted_post_id, apub_id)
113 Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
116 updated_post.send_create(&user, context).await?;
118 // They like their own post by default
119 let like_form = PostLikeForm {
120 post_id: inserted_post.id,
125 let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
126 if blocking(context.pool(), like).await?.is_err() {
127 return Err(APIError::err("couldnt_like_post").into());
130 updated_post.send_like(&user, context).await?;
133 let inserted_post_id = inserted_post.id;
134 let post_view = match blocking(context.pool(), move |conn| {
135 PostView::read(conn, inserted_post_id, Some(user.id))
140 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
143 let res = PostResponse { post: post_view };
145 context.chat_server().do_send(SendPost {
146 op: UserOperation::CreatePost,
155 #[async_trait::async_trait(?Send)]
156 impl Perform for GetPost {
157 type Response = GetPostResponse;
161 context: &Data<LemmyContext>,
162 websocket_id: Option<ConnectionId>,
163 ) -> Result<GetPostResponse, LemmyError> {
164 let data: &GetPost = &self;
165 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
166 let user_id = user.map(|u| u.id);
169 let post_view = match blocking(context.pool(), move |conn| {
170 PostView::read(conn, id, user_id)
175 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
179 let comments = blocking(context.pool(), move |conn| {
180 CommentQueryBuilder::create(conn)
188 let community_id = post_view.community_id;
189 let community = blocking(context.pool(), move |conn| {
190 CommunityView::read(conn, community_id, user_id)
194 let community_id = post_view.community_id;
195 let moderators = blocking(context.pool(), move |conn| {
196 CommunityModeratorView::for_community(conn, community_id)
200 if let Some(id) = websocket_id {
201 context.chat_server().do_send(JoinPostRoom {
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)
257 .show_nsfw(show_nsfw)
258 .for_community_id(community_id)
259 .for_community_name(community_name)
268 Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
271 if let Some(id) = websocket_id {
272 // You don't need to join the specific community room, bc this is already handled by
274 if data.community_id.is_none() {
275 // 0 is the "all" community
276 context.chat_server().do_send(JoinCommunityRoom {
283 Ok(GetPostsResponse { posts })
287 #[async_trait::async_trait(?Send)]
288 impl Perform for CreatePostLike {
289 type Response = PostResponse;
293 context: &Data<LemmyContext>,
294 websocket_id: Option<ConnectionId>,
295 ) -> Result<PostResponse, LemmyError> {
296 let data: &CreatePostLike = &self;
297 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
299 // Don't do a downvote if site has downvotes disabled
300 if data.score == -1 {
301 let site = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
302 if !site.enable_downvotes {
303 return Err(APIError::err("downvotes_disabled").into());
307 // Check for a community ban
308 let post_id = data.post_id;
309 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
311 check_community_ban(user.id, post.community_id, context.pool()).await?;
313 let like_form = PostLikeForm {
314 post_id: data.post_id,
319 // Remove any likes first
320 let user_id = user.id;
321 blocking(context.pool(), move |conn| {
322 PostLike::remove(conn, user_id, post_id)
326 // Only add the like if the score isnt 0
327 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
329 let like_form2 = like_form.clone();
330 let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
331 if blocking(context.pool(), like).await?.is_err() {
332 return Err(APIError::err("couldnt_like_post").into());
335 if like_form.score == 1 {
336 post.send_like(&user, context).await?;
337 } else if like_form.score == -1 {
338 post.send_dislike(&user, context).await?;
341 post.send_undo_like(&user, context).await?;
344 let post_id = data.post_id;
345 let user_id = user.id;
346 let post_view = match blocking(context.pool(), move |conn| {
347 PostView::read(conn, post_id, Some(user_id))
352 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
355 let res = PostResponse { post: post_view };
357 context.chat_server().do_send(SendPost {
358 op: UserOperation::CreatePostLike,
367 #[async_trait::async_trait(?Send)]
368 impl Perform for EditPost {
369 type Response = PostResponse;
373 context: &Data<LemmyContext>,
374 websocket_id: Option<ConnectionId>,
375 ) -> Result<PostResponse, LemmyError> {
376 let data: &EditPost = &self;
377 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
379 check_slurs(&data.name)?;
380 check_slurs_opt(&data.body)?;
382 if !is_valid_post_title(&data.name) {
383 return Err(APIError::err("invalid_post_title").into());
386 let edit_id = data.edit_id;
387 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
389 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
391 // Verify that only the creator can edit
392 if !Post::is_post_creator(user.id, orig_post.creator_id) {
393 return Err(APIError::err("no_post_edit_allowed").into());
396 // Fetch Iframely and Pictrs cached image
397 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
398 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
400 let post_form = PostForm {
401 name: data.name.trim().to_owned(),
402 url: data.url.to_owned(),
403 body: data.body.to_owned(),
405 creator_id: orig_post.creator_id.to_owned(),
406 community_id: orig_post.community_id,
407 removed: Some(orig_post.removed),
408 deleted: Some(orig_post.deleted),
409 locked: Some(orig_post.locked),
410 stickied: Some(orig_post.stickied),
411 updated: Some(naive_now()),
412 embed_title: iframely_title,
413 embed_description: iframely_description,
414 embed_html: iframely_html,
415 thumbnail_url: pictrs_thumbnail,
416 ap_id: Some(orig_post.ap_id),
417 local: orig_post.local,
421 let edit_id = data.edit_id;
422 let res = blocking(context.pool(), move |conn| {
423 Post::update(conn, edit_id, &post_form)
426 let updated_post: Post = match res {
429 let err_type = if e.to_string() == "value too long for type character varying(200)" {
430 "post_title_too_long"
432 "couldnt_update_post"
435 return Err(APIError::err(err_type).into());
440 updated_post.send_update(&user, context).await?;
442 let edit_id = data.edit_id;
443 let post_view = blocking(context.pool(), move |conn| {
444 PostView::read(conn, edit_id, Some(user.id))
448 let res = PostResponse { post: post_view };
450 context.chat_server().do_send(SendPost {
451 op: UserOperation::EditPost,
460 #[async_trait::async_trait(?Send)]
461 impl Perform for DeletePost {
462 type Response = PostResponse;
466 context: &Data<LemmyContext>,
467 websocket_id: Option<ConnectionId>,
468 ) -> Result<PostResponse, LemmyError> {
469 let data: &DeletePost = &self;
470 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
472 let edit_id = data.edit_id;
473 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
475 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
477 // Verify that only the creator can delete
478 if !Post::is_post_creator(user.id, orig_post.creator_id) {
479 return Err(APIError::err("no_post_edit_allowed").into());
483 let edit_id = data.edit_id;
484 let deleted = data.deleted;
485 let updated_post = blocking(context.pool(), move |conn| {
486 Post::update_deleted(conn, edit_id, deleted)
492 updated_post.send_delete(&user, context).await?;
494 updated_post.send_undo_delete(&user, context).await?;
498 let edit_id = data.edit_id;
499 let post_view = blocking(context.pool(), move |conn| {
500 PostView::read(conn, edit_id, Some(user.id))
504 let res = PostResponse { post: post_view };
506 context.chat_server().do_send(SendPost {
507 op: UserOperation::DeletePost,
516 #[async_trait::async_trait(?Send)]
517 impl Perform for RemovePost {
518 type Response = PostResponse;
522 context: &Data<LemmyContext>,
523 websocket_id: Option<ConnectionId>,
524 ) -> Result<PostResponse, LemmyError> {
525 let data: &RemovePost = &self;
526 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
528 let edit_id = data.edit_id;
529 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
531 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
533 // Verify that only the mods can remove
534 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
537 let edit_id = data.edit_id;
538 let removed = data.removed;
539 let updated_post = blocking(context.pool(), move |conn| {
540 Post::update_removed(conn, edit_id, removed)
545 let form = ModRemovePostForm {
546 mod_user_id: user.id,
547 post_id: data.edit_id,
548 removed: Some(removed),
549 reason: data.reason.to_owned(),
551 blocking(context.pool(), move |conn| {
552 ModRemovePost::create(conn, &form)
558 updated_post.send_remove(&user, context).await?;
560 updated_post.send_undo_remove(&user, context).await?;
564 let edit_id = data.edit_id;
565 let user_id = user.id;
566 let post_view = blocking(context.pool(), move |conn| {
567 PostView::read(conn, edit_id, Some(user_id))
571 let res = PostResponse { post: post_view };
573 context.chat_server().do_send(SendPost {
574 op: UserOperation::RemovePost,
583 #[async_trait::async_trait(?Send)]
584 impl Perform for LockPost {
585 type Response = PostResponse;
589 context: &Data<LemmyContext>,
590 websocket_id: Option<ConnectionId>,
591 ) -> Result<PostResponse, LemmyError> {
592 let data: &LockPost = &self;
593 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
595 let edit_id = data.edit_id;
596 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
598 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
600 // Verify that only the mods can lock
601 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
604 let edit_id = data.edit_id;
605 let locked = data.locked;
606 let updated_post = blocking(context.pool(), move |conn| {
607 Post::update_locked(conn, edit_id, locked)
612 let form = ModLockPostForm {
613 mod_user_id: user.id,
614 post_id: data.edit_id,
615 locked: Some(locked),
617 blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
620 updated_post.send_update(&user, context).await?;
623 let edit_id = data.edit_id;
624 let post_view = blocking(context.pool(), move |conn| {
625 PostView::read(conn, edit_id, Some(user.id))
629 let res = PostResponse { post: post_view };
631 context.chat_server().do_send(SendPost {
632 op: UserOperation::LockPost,
641 #[async_trait::async_trait(?Send)]
642 impl Perform for StickyPost {
643 type Response = PostResponse;
647 context: &Data<LemmyContext>,
648 websocket_id: Option<ConnectionId>,
649 ) -> Result<PostResponse, LemmyError> {
650 let data: &StickyPost = &self;
651 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
653 let edit_id = data.edit_id;
654 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
656 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
658 // Verify that only the mods can sticky
659 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
662 let edit_id = data.edit_id;
663 let stickied = data.stickied;
664 let updated_post = blocking(context.pool(), move |conn| {
665 Post::update_stickied(conn, edit_id, stickied)
670 let form = ModStickyPostForm {
671 mod_user_id: user.id,
672 post_id: data.edit_id,
673 stickied: Some(stickied),
675 blocking(context.pool(), move |conn| {
676 ModStickyPost::create(conn, &form)
681 // TODO stickied should pry work like locked for ease of use
682 updated_post.send_update(&user, context).await?;
685 let edit_id = data.edit_id;
686 let post_view = blocking(context.pool(), move |conn| {
687 PostView::read(conn, edit_id, Some(user.id))
691 let res = PostResponse { post: post_view };
693 context.chat_server().do_send(SendPost {
694 op: UserOperation::StickyPost,
703 #[async_trait::async_trait(?Send)]
704 impl Perform for SavePost {
705 type Response = PostResponse;
709 context: &Data<LemmyContext>,
710 _websocket_id: Option<ConnectionId>,
711 ) -> Result<PostResponse, LemmyError> {
712 let data: &SavePost = &self;
713 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
715 let post_saved_form = PostSavedForm {
716 post_id: data.post_id,
721 let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
722 if blocking(context.pool(), save).await?.is_err() {
723 return Err(APIError::err("couldnt_save_post").into());
726 let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
727 if blocking(context.pool(), unsave).await?.is_err() {
728 return Err(APIError::err("couldnt_save_post").into());
732 let post_id = data.post_id;
733 let user_id = user.id;
734 let post_view = blocking(context.pool(), move |conn| {
735 PostView::read(conn, post_id, Some(user_id))
739 Ok(PostResponse { post: post_view })