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,
7 use actix_web::web::Data;
25 websocket::{GetPostUsersOnline, JoinPostRoom, SendPost, UserOperation},
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)
202 .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)
250 .show_nsfw(show_nsfw)
251 .for_community_id(community_id)
252 .for_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 if data.score == -1 {
282 let site = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
283 if !site.enable_downvotes {
284 return Err(APIError::err("downvotes_disabled").into());
288 // Check for a community ban
289 let post_id = data.post_id;
290 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
292 check_community_ban(user.id, post.community_id, context.pool()).await?;
294 let like_form = PostLikeForm {
295 post_id: data.post_id,
300 // Remove any likes first
301 let user_id = user.id;
302 blocking(context.pool(), move |conn| {
303 PostLike::remove(conn, user_id, post_id)
307 // Only add the like if the score isnt 0
308 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
310 let like_form2 = like_form.clone();
311 let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
312 if blocking(context.pool(), like).await?.is_err() {
313 return Err(APIError::err("couldnt_like_post").into());
316 if like_form.score == 1 {
317 post.send_like(&user, context).await?;
318 } else if like_form.score == -1 {
319 post.send_dislike(&user, context).await?;
322 post.send_undo_like(&user, context).await?;
325 let post_id = data.post_id;
326 let user_id = user.id;
327 let post_view = match blocking(context.pool(), move |conn| {
328 PostView::read(conn, post_id, Some(user_id))
333 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
336 let res = PostResponse { post: post_view };
338 context.chat_server().do_send(SendPost {
339 op: UserOperation::CreatePostLike,
348 #[async_trait::async_trait(?Send)]
349 impl Perform for EditPost {
350 type Response = PostResponse;
354 context: &Data<LemmyContext>,
355 websocket_id: Option<ConnectionId>,
356 ) -> Result<PostResponse, LemmyError> {
357 let data: &EditPost = &self;
358 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
360 check_slurs(&data.name)?;
361 check_slurs_opt(&data.body)?;
363 if !is_valid_post_title(&data.name) {
364 return Err(APIError::err("invalid_post_title").into());
367 let edit_id = data.edit_id;
368 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
370 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
372 // Verify that only the creator can edit
373 if !Post::is_post_creator(user.id, orig_post.creator_id) {
374 return Err(APIError::err("no_post_edit_allowed").into());
377 // Fetch Iframely and Pictrs cached image
378 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
379 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
381 let post_form = PostForm {
382 name: data.name.trim().to_owned(),
383 url: data.url.to_owned(),
384 body: data.body.to_owned(),
386 creator_id: orig_post.creator_id.to_owned(),
387 community_id: orig_post.community_id,
388 removed: Some(orig_post.removed),
389 deleted: Some(orig_post.deleted),
390 locked: Some(orig_post.locked),
391 stickied: Some(orig_post.stickied),
392 updated: Some(naive_now()),
393 embed_title: iframely_title,
394 embed_description: iframely_description,
395 embed_html: iframely_html,
396 thumbnail_url: pictrs_thumbnail,
397 ap_id: Some(orig_post.ap_id),
398 local: orig_post.local,
402 let edit_id = data.edit_id;
403 let res = blocking(context.pool(), move |conn| {
404 Post::update(conn, edit_id, &post_form)
407 let updated_post: Post = match res {
410 let err_type = if e.to_string() == "value too long for type character varying(200)" {
411 "post_title_too_long"
413 "couldnt_update_post"
416 return Err(APIError::err(err_type).into());
421 updated_post.send_update(&user, context).await?;
423 let edit_id = data.edit_id;
424 let post_view = blocking(context.pool(), move |conn| {
425 PostView::read(conn, edit_id, Some(user.id))
429 let res = PostResponse { post: post_view };
431 context.chat_server().do_send(SendPost {
432 op: UserOperation::EditPost,
441 #[async_trait::async_trait(?Send)]
442 impl Perform for DeletePost {
443 type Response = PostResponse;
447 context: &Data<LemmyContext>,
448 websocket_id: Option<ConnectionId>,
449 ) -> Result<PostResponse, LemmyError> {
450 let data: &DeletePost = &self;
451 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
453 let edit_id = data.edit_id;
454 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
456 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
458 // Verify that only the creator can delete
459 if !Post::is_post_creator(user.id, orig_post.creator_id) {
460 return Err(APIError::err("no_post_edit_allowed").into());
464 let edit_id = data.edit_id;
465 let deleted = data.deleted;
466 let updated_post = blocking(context.pool(), move |conn| {
467 Post::update_deleted(conn, edit_id, deleted)
473 updated_post.send_delete(&user, context).await?;
475 updated_post.send_undo_delete(&user, context).await?;
479 let edit_id = data.edit_id;
480 let post_view = blocking(context.pool(), move |conn| {
481 PostView::read(conn, edit_id, Some(user.id))
485 let res = PostResponse { post: post_view };
487 context.chat_server().do_send(SendPost {
488 op: UserOperation::DeletePost,
497 #[async_trait::async_trait(?Send)]
498 impl Perform for RemovePost {
499 type Response = PostResponse;
503 context: &Data<LemmyContext>,
504 websocket_id: Option<ConnectionId>,
505 ) -> Result<PostResponse, LemmyError> {
506 let data: &RemovePost = &self;
507 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
509 let edit_id = data.edit_id;
510 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
512 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
514 // Verify that only the mods can remove
515 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
518 let edit_id = data.edit_id;
519 let removed = data.removed;
520 let updated_post = blocking(context.pool(), move |conn| {
521 Post::update_removed(conn, edit_id, removed)
526 let form = ModRemovePostForm {
527 mod_user_id: user.id,
528 post_id: data.edit_id,
529 removed: Some(removed),
530 reason: data.reason.to_owned(),
532 blocking(context.pool(), move |conn| {
533 ModRemovePost::create(conn, &form)
539 updated_post.send_remove(&user, context).await?;
541 updated_post.send_undo_remove(&user, context).await?;
545 let edit_id = data.edit_id;
546 let user_id = user.id;
547 let post_view = blocking(context.pool(), move |conn| {
548 PostView::read(conn, edit_id, Some(user_id))
552 let res = PostResponse { post: post_view };
554 context.chat_server().do_send(SendPost {
555 op: UserOperation::RemovePost,
564 #[async_trait::async_trait(?Send)]
565 impl Perform for LockPost {
566 type Response = PostResponse;
570 context: &Data<LemmyContext>,
571 websocket_id: Option<ConnectionId>,
572 ) -> Result<PostResponse, LemmyError> {
573 let data: &LockPost = &self;
574 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
576 let edit_id = data.edit_id;
577 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
579 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
581 // Verify that only the mods can lock
582 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
585 let edit_id = data.edit_id;
586 let locked = data.locked;
587 let updated_post = blocking(context.pool(), move |conn| {
588 Post::update_locked(conn, edit_id, locked)
593 let form = ModLockPostForm {
594 mod_user_id: user.id,
595 post_id: data.edit_id,
596 locked: Some(locked),
598 blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
601 updated_post.send_update(&user, context).await?;
604 let edit_id = data.edit_id;
605 let post_view = blocking(context.pool(), move |conn| {
606 PostView::read(conn, edit_id, Some(user.id))
610 let res = PostResponse { post: post_view };
612 context.chat_server().do_send(SendPost {
613 op: UserOperation::LockPost,
622 #[async_trait::async_trait(?Send)]
623 impl Perform for StickyPost {
624 type Response = PostResponse;
628 context: &Data<LemmyContext>,
629 websocket_id: Option<ConnectionId>,
630 ) -> Result<PostResponse, LemmyError> {
631 let data: &StickyPost = &self;
632 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
634 let edit_id = data.edit_id;
635 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
637 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
639 // Verify that only the mods can sticky
640 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
643 let edit_id = data.edit_id;
644 let stickied = data.stickied;
645 let updated_post = blocking(context.pool(), move |conn| {
646 Post::update_stickied(conn, edit_id, stickied)
651 let form = ModStickyPostForm {
652 mod_user_id: user.id,
653 post_id: data.edit_id,
654 stickied: Some(stickied),
656 blocking(context.pool(), move |conn| {
657 ModStickyPost::create(conn, &form)
662 // TODO stickied should pry work like locked for ease of use
663 updated_post.send_update(&user, context).await?;
666 let edit_id = data.edit_id;
667 let post_view = blocking(context.pool(), move |conn| {
668 PostView::read(conn, edit_id, Some(user.id))
672 let res = PostResponse { post: post_view };
674 context.chat_server().do_send(SendPost {
675 op: UserOperation::StickyPost,
684 #[async_trait::async_trait(?Send)]
685 impl Perform for SavePost {
686 type Response = PostResponse;
690 context: &Data<LemmyContext>,
691 _websocket_id: Option<ConnectionId>,
692 ) -> Result<PostResponse, LemmyError> {
693 let data: &SavePost = &self;
694 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
696 let post_saved_form = PostSavedForm {
697 post_id: data.post_id,
702 let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
703 if blocking(context.pool(), save).await?.is_err() {
704 return Err(APIError::err("couldnt_save_post").into());
707 let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
708 if blocking(context.pool(), unsave).await?.is_err() {
709 return Err(APIError::err("couldnt_save_post").into());
713 let post_id = data.post_id;
714 let user_id = user.id;
715 let post_view = blocking(context.pool(), move |conn| {
716 PostView::read(conn, post_id, Some(user_id))
720 Ok(PostResponse { post: post_view })
724 #[async_trait::async_trait(?Send)]
725 impl Perform for PostJoin {
726 type Response = PostJoinResponse;
730 context: &Data<LemmyContext>,
731 websocket_id: Option<ConnectionId>,
732 ) -> Result<PostJoinResponse, LemmyError> {
733 let data: &PostJoin = &self;
735 if let Some(ws_id) = websocket_id {
736 context.chat_server().do_send(JoinPostRoom {
737 post_id: data.post_id,
742 Ok(PostJoinResponse { joined: true })