]> Untitled Git - lemmy.git/blob - server/src/api/post.rs
routes.api: fix get_captcha endpoint (#1135)
[lemmy.git] / server / 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, JoinCommunityRoom, 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     if let Some(id) = websocket_id {
201       context.chat_server().do_send(JoinPostRoom {
202         post_id: data.id,
203         id,
204       });
205     }
206
207     let online = context
208       .chat_server()
209       .send(GetPostUsersOnline { post_id: data.id })
210       .await
211       .unwrap_or(1);
212
213     // Return the jwt
214     Ok(GetPostResponse {
215       post: post_view,
216       comments,
217       community,
218       moderators,
219       online,
220     })
221   }
222 }
223
224 #[async_trait::async_trait(?Send)]
225 impl Perform for GetPosts {
226   type Response = GetPostsResponse;
227
228   async fn perform(
229     &self,
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?;
235
236     let user_id = match &user {
237       Some(user) => Some(user.id),
238       None => None,
239     };
240
241     let show_nsfw = match &user {
242       Some(user) => user.show_nsfw,
243       None => false,
244     };
245
246     let type_ = ListingType::from_str(&data.type_)?;
247     let sort = SortType::from_str(&data.sort)?;
248
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)
255         .listing_type(type_)
256         .sort(&sort)
257         .show_nsfw(show_nsfw)
258         .for_community_id(community_id)
259         .for_community_name(community_name)
260         .my_user_id(user_id)
261         .page(page)
262         .limit(limit)
263         .list()
264     })
265     .await?
266     {
267       Ok(posts) => posts,
268       Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
269     };
270
271     if let Some(id) = websocket_id {
272       // You don't need to join the specific community room, bc this is already handled by
273       // GetCommunity
274       if data.community_id.is_none() {
275         // 0 is the "all" community
276         context.chat_server().do_send(JoinCommunityRoom {
277           community_id: 0,
278           id,
279         });
280       }
281     }
282
283     Ok(GetPostsResponse { posts })
284   }
285 }
286
287 #[async_trait::async_trait(?Send)]
288 impl Perform for CreatePostLike {
289   type Response = PostResponse;
290
291   async fn perform(
292     &self,
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?;
298
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());
304       }
305     }
306
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??;
310
311     check_community_ban(user.id, post.community_id, context.pool()).await?;
312
313     let like_form = PostLikeForm {
314       post_id: data.post_id,
315       user_id: user.id,
316       score: data.score,
317     };
318
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)
323     })
324     .await??;
325
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);
328     if do_add {
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());
333       }
334
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?;
339       }
340     } else {
341       post.send_undo_like(&user, context).await?;
342     }
343
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))
348     })
349     .await?
350     {
351       Ok(post) => post,
352       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
353     };
354
355     let res = PostResponse { post: post_view };
356
357     context.chat_server().do_send(SendPost {
358       op: UserOperation::CreatePostLike,
359       post: res.clone(),
360       websocket_id,
361     });
362
363     Ok(res)
364   }
365 }
366
367 #[async_trait::async_trait(?Send)]
368 impl Perform for EditPost {
369   type Response = PostResponse;
370
371   async fn perform(
372     &self,
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?;
378
379     check_slurs(&data.name)?;
380     check_slurs_opt(&data.body)?;
381
382     if !is_valid_post_title(&data.name) {
383       return Err(APIError::err("invalid_post_title").into());
384     }
385
386     let edit_id = data.edit_id;
387     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
388
389     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
390
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());
394     }
395
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;
399
400     let post_form = PostForm {
401       name: data.name.trim().to_owned(),
402       url: data.url.to_owned(),
403       body: data.body.to_owned(),
404       nsfw: data.nsfw,
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,
418       published: None,
419     };
420
421     let edit_id = data.edit_id;
422     let res = blocking(context.pool(), move |conn| {
423       Post::update(conn, edit_id, &post_form)
424     })
425     .await?;
426     let updated_post: Post = match res {
427       Ok(post) => post,
428       Err(e) => {
429         let err_type = if e.to_string() == "value too long for type character varying(200)" {
430           "post_title_too_long"
431         } else {
432           "couldnt_update_post"
433         };
434
435         return Err(APIError::err(err_type).into());
436       }
437     };
438
439     // Send apub update
440     updated_post.send_update(&user, context).await?;
441
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))
445     })
446     .await??;
447
448     let res = PostResponse { post: post_view };
449
450     context.chat_server().do_send(SendPost {
451       op: UserOperation::EditPost,
452       post: res.clone(),
453       websocket_id,
454     });
455
456     Ok(res)
457   }
458 }
459
460 #[async_trait::async_trait(?Send)]
461 impl Perform for DeletePost {
462   type Response = PostResponse;
463
464   async fn perform(
465     &self,
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?;
471
472     let edit_id = data.edit_id;
473     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
474
475     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
476
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());
480     }
481
482     // Update the post
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)
487     })
488     .await??;
489
490     // apub updates
491     if deleted {
492       updated_post.send_delete(&user, context).await?;
493     } else {
494       updated_post.send_undo_delete(&user, context).await?;
495     }
496
497     // Refetch the post
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))
501     })
502     .await??;
503
504     let res = PostResponse { post: post_view };
505
506     context.chat_server().do_send(SendPost {
507       op: UserOperation::DeletePost,
508       post: res.clone(),
509       websocket_id,
510     });
511
512     Ok(res)
513   }
514 }
515
516 #[async_trait::async_trait(?Send)]
517 impl Perform for RemovePost {
518   type Response = PostResponse;
519
520   async fn perform(
521     &self,
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?;
527
528     let edit_id = data.edit_id;
529     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
530
531     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
532
533     // Verify that only the mods can remove
534     is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
535
536     // Update the post
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)
541     })
542     .await??;
543
544     // Mod tables
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(),
550     };
551     blocking(context.pool(), move |conn| {
552       ModRemovePost::create(conn, &form)
553     })
554     .await??;
555
556     // apub updates
557     if removed {
558       updated_post.send_remove(&user, context).await?;
559     } else {
560       updated_post.send_undo_remove(&user, context).await?;
561     }
562
563     // Refetch the post
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))
568     })
569     .await??;
570
571     let res = PostResponse { post: post_view };
572
573     context.chat_server().do_send(SendPost {
574       op: UserOperation::RemovePost,
575       post: res.clone(),
576       websocket_id,
577     });
578
579     Ok(res)
580   }
581 }
582
583 #[async_trait::async_trait(?Send)]
584 impl Perform for LockPost {
585   type Response = PostResponse;
586
587   async fn perform(
588     &self,
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?;
594
595     let edit_id = data.edit_id;
596     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
597
598     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
599
600     // Verify that only the mods can lock
601     is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
602
603     // Update the post
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)
608     })
609     .await??;
610
611     // Mod tables
612     let form = ModLockPostForm {
613       mod_user_id: user.id,
614       post_id: data.edit_id,
615       locked: Some(locked),
616     };
617     blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
618
619     // apub updates
620     updated_post.send_update(&user, context).await?;
621
622     // Refetch the post
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))
626     })
627     .await??;
628
629     let res = PostResponse { post: post_view };
630
631     context.chat_server().do_send(SendPost {
632       op: UserOperation::LockPost,
633       post: res.clone(),
634       websocket_id,
635     });
636
637     Ok(res)
638   }
639 }
640
641 #[async_trait::async_trait(?Send)]
642 impl Perform for StickyPost {
643   type Response = PostResponse;
644
645   async fn perform(
646     &self,
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?;
652
653     let edit_id = data.edit_id;
654     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
655
656     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
657
658     // Verify that only the mods can sticky
659     is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
660
661     // Update the post
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)
666     })
667     .await??;
668
669     // Mod tables
670     let form = ModStickyPostForm {
671       mod_user_id: user.id,
672       post_id: data.edit_id,
673       stickied: Some(stickied),
674     };
675     blocking(context.pool(), move |conn| {
676       ModStickyPost::create(conn, &form)
677     })
678     .await??;
679
680     // Apub updates
681     // TODO stickied should pry work like locked for ease of use
682     updated_post.send_update(&user, context).await?;
683
684     // Refetch the post
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))
688     })
689     .await??;
690
691     let res = PostResponse { post: post_view };
692
693     context.chat_server().do_send(SendPost {
694       op: UserOperation::StickyPost,
695       post: res.clone(),
696       websocket_id,
697     });
698
699     Ok(res)
700   }
701 }
702
703 #[async_trait::async_trait(?Send)]
704 impl Perform for SavePost {
705   type Response = PostResponse;
706
707   async fn perform(
708     &self,
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?;
714
715     let post_saved_form = PostSavedForm {
716       post_id: data.post_id,
717       user_id: user.id,
718     };
719
720     if data.save {
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());
724       }
725     } else {
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());
729       }
730     }
731
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))
736     })
737     .await??;
738
739     Ok(PostResponse { post: post_view })
740   }
741 }