3 fetch_iframely_and_pictrs_data,
9 use actix_web::web::Data;
10 use lemmy_apub::{ApubLikeableType, ApubObjectType};
25 use lemmy_structs::{blocking, post::*};
27 apub::{make_apub_endpoint, EndpointType},
28 utils::{check_slurs, check_slurs_opt, is_valid_post_title},
33 use lemmy_websocket::{
34 messages::{GetPostUsersOnline, JoinPostRoom, SendPost},
38 use std::str::FromStr;
41 #[async_trait::async_trait(?Send)]
42 impl Perform for CreatePost {
43 type Response = PostResponse;
47 context: &Data<LemmyContext>,
48 websocket_id: Option<ConnectionId>,
49 ) -> Result<PostResponse, LemmyError> {
50 let data: &CreatePost = &self;
51 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
53 check_slurs(&data.name)?;
54 check_slurs_opt(&data.body)?;
56 if !is_valid_post_title(&data.name) {
57 return Err(APIError::err("invalid_post_title").into());
60 check_community_ban(user.id, data.community_id, context.pool()).await?;
62 if let Some(url) = data.url.as_ref() {
63 match Url::parse(url) {
65 Err(_e) => return Err(APIError::err("invalid_url").into()),
69 // Fetch Iframely and pictrs cached image
70 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
71 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
73 let post_form = PostForm {
74 name: data.name.trim().to_owned(),
75 url: data.url.to_owned(),
76 body: data.body.to_owned(),
77 community_id: data.community_id,
85 embed_title: iframely_title,
86 embed_description: iframely_description,
87 embed_html: iframely_html,
88 thumbnail_url: pictrs_thumbnail,
95 match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
98 let err_type = if e.to_string() == "value too long for type character varying(200)" {
101 "couldnt_create_post"
104 return Err(APIError::err(err_type).into());
108 let inserted_post_id = inserted_post.id;
109 let updated_post = match blocking(context.pool(), move |conn| {
111 make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
112 Post::update_ap_id(conn, inserted_post_id, apub_id)
117 Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
120 updated_post.send_create(&user, context).await?;
122 // They like their own post by default
123 let like_form = PostLikeForm {
124 post_id: inserted_post.id,
129 let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
130 if blocking(context.pool(), like).await?.is_err() {
131 return Err(APIError::err("couldnt_like_post").into());
134 updated_post.send_like(&user, context).await?;
137 let inserted_post_id = inserted_post.id;
138 let post_view = match blocking(context.pool(), move |conn| {
139 PostView::read(conn, inserted_post_id, Some(user.id))
144 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
147 let res = PostResponse { post: post_view };
149 context.chat_server().do_send(SendPost {
150 op: UserOperation::CreatePost,
159 #[async_trait::async_trait(?Send)]
160 impl Perform for GetPost {
161 type Response = GetPostResponse;
165 context: &Data<LemmyContext>,
166 _websocket_id: Option<ConnectionId>,
167 ) -> Result<GetPostResponse, LemmyError> {
168 let data: &GetPost = &self;
169 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
170 let user_id = user.map(|u| u.id);
173 let post_view = match blocking(context.pool(), move |conn| {
174 PostView::read(conn, id, user_id)
179 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
183 let comments = blocking(context.pool(), move |conn| {
184 CommentQueryBuilder::create(conn)
192 let community_id = post_view.community_id;
193 let community = blocking(context.pool(), move |conn| {
194 CommunityView::read(conn, community_id, user_id)
198 let community_id = post_view.community_id;
199 let moderators = blocking(context.pool(), move |conn| {
200 CommunityModeratorView::for_community(conn, community_id)
206 .send(GetPostUsersOnline { post_id: data.id })
221 #[async_trait::async_trait(?Send)]
222 impl Perform for GetPosts {
223 type Response = GetPostsResponse;
227 context: &Data<LemmyContext>,
228 _websocket_id: Option<ConnectionId>,
229 ) -> Result<GetPostsResponse, LemmyError> {
230 let data: &GetPosts = &self;
231 let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
233 let user_id = match &user {
234 Some(user) => Some(user.id),
238 let show_nsfw = match &user {
239 Some(user) => user.show_nsfw,
243 let type_ = ListingType::from_str(&data.type_)?;
244 let sort = SortType::from_str(&data.sort)?;
246 let page = data.page;
247 let limit = data.limit;
248 let community_id = data.community_id;
249 let community_name = data.community_name.to_owned();
250 let posts = match blocking(context.pool(), move |conn| {
251 PostQueryBuilder::create(conn)
254 .show_nsfw(show_nsfw)
255 .for_community_id(community_id)
256 .for_community_name(community_name)
265 Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
268 Ok(GetPostsResponse { posts })
272 #[async_trait::async_trait(?Send)]
273 impl Perform for CreatePostLike {
274 type Response = PostResponse;
278 context: &Data<LemmyContext>,
279 websocket_id: Option<ConnectionId>,
280 ) -> Result<PostResponse, LemmyError> {
281 let data: &CreatePostLike = &self;
282 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
284 // Don't do a downvote if site has downvotes disabled
285 if data.score == -1 {
286 let site = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
287 if !site.enable_downvotes {
288 return Err(APIError::err("downvotes_disabled").into());
292 // Check for a community ban
293 let post_id = data.post_id;
294 let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
296 check_community_ban(user.id, post.community_id, context.pool()).await?;
298 let like_form = PostLikeForm {
299 post_id: data.post_id,
304 // Remove any likes first
305 let user_id = user.id;
306 blocking(context.pool(), move |conn| {
307 PostLike::remove(conn, user_id, post_id)
311 // Only add the like if the score isnt 0
312 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
314 let like_form2 = like_form.clone();
315 let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
316 if blocking(context.pool(), like).await?.is_err() {
317 return Err(APIError::err("couldnt_like_post").into());
320 if like_form.score == 1 {
321 post.send_like(&user, context).await?;
322 } else if like_form.score == -1 {
323 post.send_dislike(&user, context).await?;
326 post.send_undo_like(&user, context).await?;
329 let post_id = data.post_id;
330 let user_id = user.id;
331 let post_view = match blocking(context.pool(), move |conn| {
332 PostView::read(conn, post_id, Some(user_id))
337 Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
340 let res = PostResponse { post: post_view };
342 context.chat_server().do_send(SendPost {
343 op: UserOperation::CreatePostLike,
352 #[async_trait::async_trait(?Send)]
353 impl Perform for EditPost {
354 type Response = PostResponse;
358 context: &Data<LemmyContext>,
359 websocket_id: Option<ConnectionId>,
360 ) -> Result<PostResponse, LemmyError> {
361 let data: &EditPost = &self;
362 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
364 check_slurs(&data.name)?;
365 check_slurs_opt(&data.body)?;
367 if !is_valid_post_title(&data.name) {
368 return Err(APIError::err("invalid_post_title").into());
371 let edit_id = data.edit_id;
372 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
374 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
376 // Verify that only the creator can edit
377 if !Post::is_post_creator(user.id, orig_post.creator_id) {
378 return Err(APIError::err("no_post_edit_allowed").into());
381 // Fetch Iframely and Pictrs cached image
382 let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
383 fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
385 let post_form = PostForm {
386 name: data.name.trim().to_owned(),
387 url: data.url.to_owned(),
388 body: data.body.to_owned(),
390 creator_id: orig_post.creator_id.to_owned(),
391 community_id: orig_post.community_id,
392 removed: Some(orig_post.removed),
393 deleted: Some(orig_post.deleted),
394 locked: Some(orig_post.locked),
395 stickied: Some(orig_post.stickied),
396 updated: Some(naive_now()),
397 embed_title: iframely_title,
398 embed_description: iframely_description,
399 embed_html: iframely_html,
400 thumbnail_url: pictrs_thumbnail,
401 ap_id: Some(orig_post.ap_id),
402 local: orig_post.local,
406 let edit_id = data.edit_id;
407 let res = blocking(context.pool(), move |conn| {
408 Post::update(conn, edit_id, &post_form)
411 let updated_post: Post = match res {
414 let err_type = if e.to_string() == "value too long for type character varying(200)" {
415 "post_title_too_long"
417 "couldnt_update_post"
420 return Err(APIError::err(err_type).into());
425 updated_post.send_update(&user, context).await?;
427 let edit_id = data.edit_id;
428 let post_view = blocking(context.pool(), move |conn| {
429 PostView::read(conn, edit_id, Some(user.id))
433 let res = PostResponse { post: post_view };
435 context.chat_server().do_send(SendPost {
436 op: UserOperation::EditPost,
445 #[async_trait::async_trait(?Send)]
446 impl Perform for DeletePost {
447 type Response = PostResponse;
451 context: &Data<LemmyContext>,
452 websocket_id: Option<ConnectionId>,
453 ) -> Result<PostResponse, LemmyError> {
454 let data: &DeletePost = &self;
455 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
457 let edit_id = data.edit_id;
458 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
460 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
462 // Verify that only the creator can delete
463 if !Post::is_post_creator(user.id, orig_post.creator_id) {
464 return Err(APIError::err("no_post_edit_allowed").into());
468 let edit_id = data.edit_id;
469 let deleted = data.deleted;
470 let updated_post = blocking(context.pool(), move |conn| {
471 Post::update_deleted(conn, edit_id, deleted)
477 updated_post.send_delete(&user, context).await?;
479 updated_post.send_undo_delete(&user, context).await?;
483 let edit_id = data.edit_id;
484 let post_view = blocking(context.pool(), move |conn| {
485 PostView::read(conn, edit_id, Some(user.id))
489 let res = PostResponse { post: post_view };
491 context.chat_server().do_send(SendPost {
492 op: UserOperation::DeletePost,
501 #[async_trait::async_trait(?Send)]
502 impl Perform for RemovePost {
503 type Response = PostResponse;
507 context: &Data<LemmyContext>,
508 websocket_id: Option<ConnectionId>,
509 ) -> Result<PostResponse, LemmyError> {
510 let data: &RemovePost = &self;
511 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
513 let edit_id = data.edit_id;
514 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
516 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
518 // Verify that only the mods can remove
519 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
522 let edit_id = data.edit_id;
523 let removed = data.removed;
524 let updated_post = blocking(context.pool(), move |conn| {
525 Post::update_removed(conn, edit_id, removed)
530 let form = ModRemovePostForm {
531 mod_user_id: user.id,
532 post_id: data.edit_id,
533 removed: Some(removed),
534 reason: data.reason.to_owned(),
536 blocking(context.pool(), move |conn| {
537 ModRemovePost::create(conn, &form)
543 updated_post.send_remove(&user, context).await?;
545 updated_post.send_undo_remove(&user, context).await?;
549 let edit_id = data.edit_id;
550 let user_id = user.id;
551 let post_view = blocking(context.pool(), move |conn| {
552 PostView::read(conn, edit_id, Some(user_id))
556 let res = PostResponse { post: post_view };
558 context.chat_server().do_send(SendPost {
559 op: UserOperation::RemovePost,
568 #[async_trait::async_trait(?Send)]
569 impl Perform for LockPost {
570 type Response = PostResponse;
574 context: &Data<LemmyContext>,
575 websocket_id: Option<ConnectionId>,
576 ) -> Result<PostResponse, LemmyError> {
577 let data: &LockPost = &self;
578 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
580 let edit_id = data.edit_id;
581 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
583 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
585 // Verify that only the mods can lock
586 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
589 let edit_id = data.edit_id;
590 let locked = data.locked;
591 let updated_post = blocking(context.pool(), move |conn| {
592 Post::update_locked(conn, edit_id, locked)
597 let form = ModLockPostForm {
598 mod_user_id: user.id,
599 post_id: data.edit_id,
600 locked: Some(locked),
602 blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
605 updated_post.send_update(&user, context).await?;
608 let edit_id = data.edit_id;
609 let post_view = blocking(context.pool(), move |conn| {
610 PostView::read(conn, edit_id, Some(user.id))
614 let res = PostResponse { post: post_view };
616 context.chat_server().do_send(SendPost {
617 op: UserOperation::LockPost,
626 #[async_trait::async_trait(?Send)]
627 impl Perform for StickyPost {
628 type Response = PostResponse;
632 context: &Data<LemmyContext>,
633 websocket_id: Option<ConnectionId>,
634 ) -> Result<PostResponse, LemmyError> {
635 let data: &StickyPost = &self;
636 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
638 let edit_id = data.edit_id;
639 let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
641 check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
643 // Verify that only the mods can sticky
644 is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
647 let edit_id = data.edit_id;
648 let stickied = data.stickied;
649 let updated_post = blocking(context.pool(), move |conn| {
650 Post::update_stickied(conn, edit_id, stickied)
655 let form = ModStickyPostForm {
656 mod_user_id: user.id,
657 post_id: data.edit_id,
658 stickied: Some(stickied),
660 blocking(context.pool(), move |conn| {
661 ModStickyPost::create(conn, &form)
666 // TODO stickied should pry work like locked for ease of use
667 updated_post.send_update(&user, context).await?;
670 let edit_id = data.edit_id;
671 let post_view = blocking(context.pool(), move |conn| {
672 PostView::read(conn, edit_id, Some(user.id))
676 let res = PostResponse { post: post_view };
678 context.chat_server().do_send(SendPost {
679 op: UserOperation::StickyPost,
688 #[async_trait::async_trait(?Send)]
689 impl Perform for SavePost {
690 type Response = PostResponse;
694 context: &Data<LemmyContext>,
695 _websocket_id: Option<ConnectionId>,
696 ) -> Result<PostResponse, LemmyError> {
697 let data: &SavePost = &self;
698 let user = get_user_from_jwt(&data.auth, context.pool()).await?;
700 let post_saved_form = PostSavedForm {
701 post_id: data.post_id,
706 let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
707 if blocking(context.pool(), save).await?.is_err() {
708 return Err(APIError::err("couldnt_save_post").into());
711 let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
712 if blocking(context.pool(), unsave).await?.is_err() {
713 return Err(APIError::err("couldnt_save_post").into());
717 let post_id = data.post_id;
718 let user_id = user.id;
719 let post_view = blocking(context.pool(), move |conn| {
720 PostView::read(conn, post_id, Some(user_id))
724 Ok(PostResponse { post: post_view })
728 #[async_trait::async_trait(?Send)]
729 impl Perform for PostJoin {
730 type Response = PostJoinResponse;
734 context: &Data<LemmyContext>,
735 websocket_id: Option<ConnectionId>,
736 ) -> Result<PostJoinResponse, LemmyError> {
737 let data: &PostJoin = &self;
739 if let Some(ws_id) = websocket_id {
740 context.chat_server().do_send(JoinPostRoom {
741 post_id: data.post_id,
746 Ok(PostJoinResponse { joined: true })