]> Untitled Git - lemmy.git/blob - src/api/post.rs
routes.api: fix get_captcha endpoint (#1135)
[lemmy.git] / src / api / post.rs
1 use crate::{
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,
5   websocket::{
6     messages::{GetPostUsersOnline, JoinPostRoom, SendPost},
7     UserOperation,
8   },
9   LemmyContext,
10 };
11 use actix_web::web::Data;
12 use lemmy_api_structs::{blocking, post::*};
13 use lemmy_db::{
14   comment_view::*,
15   community_view::*,
16   moderator::*,
17   naive_now,
18   post::*,
19   post_view::*,
20   site_view::*,
21   Crud,
22   Likeable,
23   ListingType,
24   Saveable,
25   SortType,
26 };
27 use lemmy_utils::{
28   apub::{make_apub_endpoint, EndpointType},
29   utils::{check_slurs, check_slurs_opt, is_valid_post_title},
30   APIError,
31   ConnectionId,
32   LemmyError,
33 };
34 use std::str::FromStr;
35 use url::Url;
36
37 #[async_trait::async_trait(?Send)]
38 impl Perform for CreatePost {
39   type Response = PostResponse;
40
41   async fn perform(
42     &self,
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?;
48
49     check_slurs(&data.name)?;
50     check_slurs_opt(&data.body)?;
51
52     if !is_valid_post_title(&data.name) {
53       return Err(APIError::err("invalid_post_title").into());
54     }
55
56     check_community_ban(user.id, data.community_id, context.pool()).await?;
57
58     if let Some(url) = data.url.as_ref() {
59       match Url::parse(url) {
60         Ok(_t) => (),
61         Err(_e) => return Err(APIError::err("invalid_url").into()),
62       }
63     }
64
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;
68
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,
74       creator_id: user.id,
75       removed: None,
76       deleted: None,
77       nsfw: data.nsfw,
78       locked: None,
79       stickied: None,
80       updated: None,
81       embed_title: iframely_title,
82       embed_description: iframely_description,
83       embed_html: iframely_html,
84       thumbnail_url: pictrs_thumbnail,
85       ap_id: None,
86       local: true,
87       published: None,
88     };
89
90     let inserted_post =
91       match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
92         Ok(post) => post,
93         Err(e) => {
94           let err_type = if e.to_string() == "value too long for type character varying(200)" {
95             "post_title_too_long"
96           } else {
97             "couldnt_create_post"
98           };
99
100           return Err(APIError::err(err_type).into());
101         }
102       };
103
104     let inserted_post_id = inserted_post.id;
105     let updated_post = match blocking(context.pool(), move |conn| {
106       let apub_id =
107         make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
108       Post::update_ap_id(conn, inserted_post_id, apub_id)
109     })
110     .await?
111     {
112       Ok(post) => post,
113       Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
114     };
115
116     updated_post.send_create(&user, context).await?;
117
118     // They like their own post by default
119     let like_form = PostLikeForm {
120       post_id: inserted_post.id,
121       user_id: user.id,
122       score: 1,
123     };
124
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());
128     }
129
130     updated_post.send_like(&user, context).await?;
131
132     // Refetch the view
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))
136     })
137     .await?
138     {
139       Ok(post) => post,
140       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
141     };
142
143     let res = PostResponse { post: post_view };
144
145     context.chat_server().do_send(SendPost {
146       op: UserOperation::CreatePost,
147       post: res.clone(),
148       websocket_id,
149     });
150
151     Ok(res)
152   }
153 }
154
155 #[async_trait::async_trait(?Send)]
156 impl Perform for GetPost {
157   type Response = GetPostResponse;
158
159   async fn perform(
160     &self,
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);
167
168     let id = data.id;
169     let post_view = match blocking(context.pool(), move |conn| {
170       PostView::read(conn, id, user_id)
171     })
172     .await?
173     {
174       Ok(post) => post,
175       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
176     };
177
178     let id = data.id;
179     let comments = blocking(context.pool(), move |conn| {
180       CommentQueryBuilder::create(conn)
181         .for_post_id(id)
182         .my_user_id(user_id)
183         .limit(9999)
184         .list()
185     })
186     .await??;
187
188     let community_id = post_view.community_id;
189     let community = blocking(context.pool(), move |conn| {
190       CommunityView::read(conn, community_id, user_id)
191     })
192     .await??;
193
194     let community_id = post_view.community_id;
195     let moderators = blocking(context.pool(), move |conn| {
196       CommunityModeratorView::for_community(conn, community_id)
197     })
198     .await??;
199
200     let online = context
201       .chat_server()
202       .send(GetPostUsersOnline { post_id: data.id })
203       .await
204       .unwrap_or(1);
205
206     // Return the jwt
207     Ok(GetPostResponse {
208       post: post_view,
209       comments,
210       community,
211       moderators,
212       online,
213     })
214   }
215 }
216
217 #[async_trait::async_trait(?Send)]
218 impl Perform for GetPosts {
219   type Response = GetPostsResponse;
220
221   async fn perform(
222     &self,
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?;
228
229     let user_id = match &user {
230       Some(user) => Some(user.id),
231       None => None,
232     };
233
234     let show_nsfw = match &user {
235       Some(user) => user.show_nsfw,
236       None => false,
237     };
238
239     let type_ = ListingType::from_str(&data.type_)?;
240     let sort = SortType::from_str(&data.sort)?;
241
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_)
249         .sort(&sort)
250         .show_nsfw(show_nsfw)
251         .for_community_id(community_id)
252         .for_community_name(community_name)
253         .my_user_id(user_id)
254         .page(page)
255         .limit(limit)
256         .list()
257     })
258     .await?
259     {
260       Ok(posts) => posts,
261       Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
262     };
263
264     Ok(GetPostsResponse { posts })
265   }
266 }
267
268 #[async_trait::async_trait(?Send)]
269 impl Perform for CreatePostLike {
270   type Response = PostResponse;
271
272   async fn perform(
273     &self,
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?;
279
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());
285       }
286     }
287
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??;
291
292     check_community_ban(user.id, post.community_id, context.pool()).await?;
293
294     let like_form = PostLikeForm {
295       post_id: data.post_id,
296       user_id: user.id,
297       score: data.score,
298     };
299
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)
304     })
305     .await??;
306
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);
309     if do_add {
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());
314       }
315
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?;
320       }
321     } else {
322       post.send_undo_like(&user, context).await?;
323     }
324
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))
329     })
330     .await?
331     {
332       Ok(post) => post,
333       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
334     };
335
336     let res = PostResponse { post: post_view };
337
338     context.chat_server().do_send(SendPost {
339       op: UserOperation::CreatePostLike,
340       post: res.clone(),
341       websocket_id,
342     });
343
344     Ok(res)
345   }
346 }
347
348 #[async_trait::async_trait(?Send)]
349 impl Perform for EditPost {
350   type Response = PostResponse;
351
352   async fn perform(
353     &self,
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?;
359
360     check_slurs(&data.name)?;
361     check_slurs_opt(&data.body)?;
362
363     if !is_valid_post_title(&data.name) {
364       return Err(APIError::err("invalid_post_title").into());
365     }
366
367     let edit_id = data.edit_id;
368     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
369
370     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
371
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());
375     }
376
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;
380
381     let post_form = PostForm {
382       name: data.name.trim().to_owned(),
383       url: data.url.to_owned(),
384       body: data.body.to_owned(),
385       nsfw: data.nsfw,
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,
399       published: None,
400     };
401
402     let edit_id = data.edit_id;
403     let res = blocking(context.pool(), move |conn| {
404       Post::update(conn, edit_id, &post_form)
405     })
406     .await?;
407     let updated_post: Post = match res {
408       Ok(post) => post,
409       Err(e) => {
410         let err_type = if e.to_string() == "value too long for type character varying(200)" {
411           "post_title_too_long"
412         } else {
413           "couldnt_update_post"
414         };
415
416         return Err(APIError::err(err_type).into());
417       }
418     };
419
420     // Send apub update
421     updated_post.send_update(&user, context).await?;
422
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))
426     })
427     .await??;
428
429     let res = PostResponse { post: post_view };
430
431     context.chat_server().do_send(SendPost {
432       op: UserOperation::EditPost,
433       post: res.clone(),
434       websocket_id,
435     });
436
437     Ok(res)
438   }
439 }
440
441 #[async_trait::async_trait(?Send)]
442 impl Perform for DeletePost {
443   type Response = PostResponse;
444
445   async fn perform(
446     &self,
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?;
452
453     let edit_id = data.edit_id;
454     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
455
456     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
457
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());
461     }
462
463     // Update the post
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)
468     })
469     .await??;
470
471     // apub updates
472     if deleted {
473       updated_post.send_delete(&user, context).await?;
474     } else {
475       updated_post.send_undo_delete(&user, context).await?;
476     }
477
478     // Refetch the post
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))
482     })
483     .await??;
484
485     let res = PostResponse { post: post_view };
486
487     context.chat_server().do_send(SendPost {
488       op: UserOperation::DeletePost,
489       post: res.clone(),
490       websocket_id,
491     });
492
493     Ok(res)
494   }
495 }
496
497 #[async_trait::async_trait(?Send)]
498 impl Perform for RemovePost {
499   type Response = PostResponse;
500
501   async fn perform(
502     &self,
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?;
508
509     let edit_id = data.edit_id;
510     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
511
512     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
513
514     // Verify that only the mods can remove
515     is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
516
517     // Update the post
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)
522     })
523     .await??;
524
525     // Mod tables
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(),
531     };
532     blocking(context.pool(), move |conn| {
533       ModRemovePost::create(conn, &form)
534     })
535     .await??;
536
537     // apub updates
538     if removed {
539       updated_post.send_remove(&user, context).await?;
540     } else {
541       updated_post.send_undo_remove(&user, context).await?;
542     }
543
544     // Refetch the post
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))
549     })
550     .await??;
551
552     let res = PostResponse { post: post_view };
553
554     context.chat_server().do_send(SendPost {
555       op: UserOperation::RemovePost,
556       post: res.clone(),
557       websocket_id,
558     });
559
560     Ok(res)
561   }
562 }
563
564 #[async_trait::async_trait(?Send)]
565 impl Perform for LockPost {
566   type Response = PostResponse;
567
568   async fn perform(
569     &self,
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?;
575
576     let edit_id = data.edit_id;
577     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
578
579     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
580
581     // Verify that only the mods can lock
582     is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
583
584     // Update the post
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)
589     })
590     .await??;
591
592     // Mod tables
593     let form = ModLockPostForm {
594       mod_user_id: user.id,
595       post_id: data.edit_id,
596       locked: Some(locked),
597     };
598     blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
599
600     // apub updates
601     updated_post.send_update(&user, context).await?;
602
603     // Refetch the post
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))
607     })
608     .await??;
609
610     let res = PostResponse { post: post_view };
611
612     context.chat_server().do_send(SendPost {
613       op: UserOperation::LockPost,
614       post: res.clone(),
615       websocket_id,
616     });
617
618     Ok(res)
619   }
620 }
621
622 #[async_trait::async_trait(?Send)]
623 impl Perform for StickyPost {
624   type Response = PostResponse;
625
626   async fn perform(
627     &self,
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?;
633
634     let edit_id = data.edit_id;
635     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
636
637     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
638
639     // Verify that only the mods can sticky
640     is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
641
642     // Update the post
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)
647     })
648     .await??;
649
650     // Mod tables
651     let form = ModStickyPostForm {
652       mod_user_id: user.id,
653       post_id: data.edit_id,
654       stickied: Some(stickied),
655     };
656     blocking(context.pool(), move |conn| {
657       ModStickyPost::create(conn, &form)
658     })
659     .await??;
660
661     // Apub updates
662     // TODO stickied should pry work like locked for ease of use
663     updated_post.send_update(&user, context).await?;
664
665     // Refetch the post
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))
669     })
670     .await??;
671
672     let res = PostResponse { post: post_view };
673
674     context.chat_server().do_send(SendPost {
675       op: UserOperation::StickyPost,
676       post: res.clone(),
677       websocket_id,
678     });
679
680     Ok(res)
681   }
682 }
683
684 #[async_trait::async_trait(?Send)]
685 impl Perform for SavePost {
686   type Response = PostResponse;
687
688   async fn perform(
689     &self,
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?;
695
696     let post_saved_form = PostSavedForm {
697       post_id: data.post_id,
698       user_id: user.id,
699     };
700
701     if data.save {
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());
705       }
706     } else {
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());
710       }
711     }
712
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))
717     })
718     .await??;
719
720     Ok(PostResponse { post: post_view })
721   }
722 }
723
724 #[async_trait::async_trait(?Send)]
725 impl Perform for PostJoin {
726   type Response = PostJoinResponse;
727
728   async fn perform(
729     &self,
730     context: &Data<LemmyContext>,
731     websocket_id: Option<ConnectionId>,
732   ) -> Result<PostJoinResponse, LemmyError> {
733     let data: &PostJoin = &self;
734
735     if let Some(ws_id) = websocket_id {
736       context.chat_server().do_send(JoinPostRoom {
737         post_id: data.post_id,
738         id: ws_id,
739       });
740     }
741
742     Ok(PostJoinResponse { joined: true })
743   }
744 }