]> Untitled Git - lemmy.git/blob - lemmy_api/src/post.rs
Fix nginx config for local federation setup (#104)
[lemmy.git] / lemmy_api / src / post.rs
1 use crate::{
2   check_community_ban,
3   fetch_iframely_and_pictrs_data,
4   get_user_from_jwt,
5   get_user_from_jwt_opt,
6   is_mod_or_admin,
7   Perform,
8 };
9 use actix_web::web::Data;
10 use lemmy_apub::{ApubLikeableType, ApubObjectType};
11 use lemmy_db::{
12   comment_view::*,
13   community_view::*,
14   moderator::*,
15   naive_now,
16   post::*,
17   post_view::*,
18   site_view::*,
19   Crud,
20   Likeable,
21   ListingType,
22   Saveable,
23   SortType,
24 };
25 use lemmy_structs::{blocking, post::*};
26 use lemmy_utils::{
27   apub::{make_apub_endpoint, EndpointType},
28   utils::{check_slurs, check_slurs_opt, is_valid_post_title},
29   APIError,
30   ConnectionId,
31   LemmyError,
32 };
33 use lemmy_websocket::{
34   messages::{GetPostUsersOnline, JoinPostRoom, SendPost},
35   LemmyContext,
36   UserOperation,
37 };
38 use std::str::FromStr;
39 use url::Url;
40
41 #[async_trait::async_trait(?Send)]
42 impl Perform for CreatePost {
43   type Response = PostResponse;
44
45   async fn perform(
46     &self,
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?;
52
53     check_slurs(&data.name)?;
54     check_slurs_opt(&data.body)?;
55
56     if !is_valid_post_title(&data.name) {
57       return Err(APIError::err("invalid_post_title").into());
58     }
59
60     check_community_ban(user.id, data.community_id, context.pool()).await?;
61
62     if let Some(url) = data.url.as_ref() {
63       match Url::parse(url) {
64         Ok(_t) => (),
65         Err(_e) => return Err(APIError::err("invalid_url").into()),
66       }
67     }
68
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;
72
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,
78       creator_id: user.id,
79       removed: None,
80       deleted: None,
81       nsfw: data.nsfw,
82       locked: None,
83       stickied: None,
84       updated: None,
85       embed_title: iframely_title,
86       embed_description: iframely_description,
87       embed_html: iframely_html,
88       thumbnail_url: pictrs_thumbnail,
89       ap_id: None,
90       local: true,
91       published: None,
92     };
93
94     let inserted_post =
95       match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
96         Ok(post) => post,
97         Err(e) => {
98           let err_type = if e.to_string() == "value too long for type character varying(200)" {
99             "post_title_too_long"
100           } else {
101             "couldnt_create_post"
102           };
103
104           return Err(APIError::err(err_type).into());
105         }
106       };
107
108     let inserted_post_id = inserted_post.id;
109     let updated_post = match blocking(context.pool(), move |conn| {
110       let apub_id =
111         make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
112       Post::update_ap_id(conn, inserted_post_id, apub_id)
113     })
114     .await?
115     {
116       Ok(post) => post,
117       Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
118     };
119
120     updated_post.send_create(&user, context).await?;
121
122     // They like their own post by default
123     let like_form = PostLikeForm {
124       post_id: inserted_post.id,
125       user_id: user.id,
126       score: 1,
127     };
128
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());
132     }
133
134     updated_post.send_like(&user, context).await?;
135
136     // Refetch the view
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))
140     })
141     .await?
142     {
143       Ok(post) => post,
144       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
145     };
146
147     let res = PostResponse { post: post_view };
148
149     context.chat_server().do_send(SendPost {
150       op: UserOperation::CreatePost,
151       post: res.clone(),
152       websocket_id,
153     });
154
155     Ok(res)
156   }
157 }
158
159 #[async_trait::async_trait(?Send)]
160 impl Perform for GetPost {
161   type Response = GetPostResponse;
162
163   async fn perform(
164     &self,
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);
171
172     let id = data.id;
173     let post_view = match blocking(context.pool(), move |conn| {
174       PostView::read(conn, id, user_id)
175     })
176     .await?
177     {
178       Ok(post) => post,
179       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
180     };
181
182     let id = data.id;
183     let comments = blocking(context.pool(), move |conn| {
184       CommentQueryBuilder::create(conn)
185         .for_post_id(id)
186         .my_user_id(user_id)
187         .limit(9999)
188         .list()
189     })
190     .await??;
191
192     let community_id = post_view.community_id;
193     let community = blocking(context.pool(), move |conn| {
194       CommunityView::read(conn, community_id, user_id)
195     })
196     .await??;
197
198     let community_id = post_view.community_id;
199     let moderators = blocking(context.pool(), move |conn| {
200       CommunityModeratorView::for_community(conn, community_id)
201     })
202     .await??;
203
204     let online = context
205       .chat_server()
206       .send(GetPostUsersOnline { post_id: data.id })
207       .await
208       .unwrap_or(1);
209
210     // Return the jwt
211     Ok(GetPostResponse {
212       post: post_view,
213       comments,
214       community,
215       moderators,
216       online,
217     })
218   }
219 }
220
221 #[async_trait::async_trait(?Send)]
222 impl Perform for GetPosts {
223   type Response = GetPostsResponse;
224
225   async fn perform(
226     &self,
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?;
232
233     let user_id = match &user {
234       Some(user) => Some(user.id),
235       None => None,
236     };
237
238     let show_nsfw = match &user {
239       Some(user) => user.show_nsfw,
240       None => false,
241     };
242
243     let type_ = ListingType::from_str(&data.type_)?;
244     let sort = SortType::from_str(&data.sort)?;
245
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)
252         .listing_type(type_)
253         .sort(&sort)
254         .show_nsfw(show_nsfw)
255         .for_community_id(community_id)
256         .for_community_name(community_name)
257         .my_user_id(user_id)
258         .page(page)
259         .limit(limit)
260         .list()
261     })
262     .await?
263     {
264       Ok(posts) => posts,
265       Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
266     };
267
268     Ok(GetPostsResponse { posts })
269   }
270 }
271
272 #[async_trait::async_trait(?Send)]
273 impl Perform for CreatePostLike {
274   type Response = PostResponse;
275
276   async fn perform(
277     &self,
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?;
283
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());
289       }
290     }
291
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??;
295
296     check_community_ban(user.id, post.community_id, context.pool()).await?;
297
298     let like_form = PostLikeForm {
299       post_id: data.post_id,
300       user_id: user.id,
301       score: data.score,
302     };
303
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)
308     })
309     .await??;
310
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);
313     if do_add {
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());
318       }
319
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?;
324       }
325     } else {
326       post.send_undo_like(&user, context).await?;
327     }
328
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))
333     })
334     .await?
335     {
336       Ok(post) => post,
337       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
338     };
339
340     let res = PostResponse { post: post_view };
341
342     context.chat_server().do_send(SendPost {
343       op: UserOperation::CreatePostLike,
344       post: res.clone(),
345       websocket_id,
346     });
347
348     Ok(res)
349   }
350 }
351
352 #[async_trait::async_trait(?Send)]
353 impl Perform for EditPost {
354   type Response = PostResponse;
355
356   async fn perform(
357     &self,
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?;
363
364     check_slurs(&data.name)?;
365     check_slurs_opt(&data.body)?;
366
367     if !is_valid_post_title(&data.name) {
368       return Err(APIError::err("invalid_post_title").into());
369     }
370
371     let edit_id = data.edit_id;
372     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
373
374     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
375
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());
379     }
380
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;
384
385     let post_form = PostForm {
386       name: data.name.trim().to_owned(),
387       url: data.url.to_owned(),
388       body: data.body.to_owned(),
389       nsfw: data.nsfw,
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,
403       published: None,
404     };
405
406     let edit_id = data.edit_id;
407     let res = blocking(context.pool(), move |conn| {
408       Post::update(conn, edit_id, &post_form)
409     })
410     .await?;
411     let updated_post: Post = match res {
412       Ok(post) => post,
413       Err(e) => {
414         let err_type = if e.to_string() == "value too long for type character varying(200)" {
415           "post_title_too_long"
416         } else {
417           "couldnt_update_post"
418         };
419
420         return Err(APIError::err(err_type).into());
421       }
422     };
423
424     // Send apub update
425     updated_post.send_update(&user, context).await?;
426
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))
430     })
431     .await??;
432
433     let res = PostResponse { post: post_view };
434
435     context.chat_server().do_send(SendPost {
436       op: UserOperation::EditPost,
437       post: res.clone(),
438       websocket_id,
439     });
440
441     Ok(res)
442   }
443 }
444
445 #[async_trait::async_trait(?Send)]
446 impl Perform for DeletePost {
447   type Response = PostResponse;
448
449   async fn perform(
450     &self,
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?;
456
457     let edit_id = data.edit_id;
458     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
459
460     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
461
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());
465     }
466
467     // Update the post
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)
472     })
473     .await??;
474
475     // apub updates
476     if deleted {
477       updated_post.send_delete(&user, context).await?;
478     } else {
479       updated_post.send_undo_delete(&user, context).await?;
480     }
481
482     // Refetch the post
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))
486     })
487     .await??;
488
489     let res = PostResponse { post: post_view };
490
491     context.chat_server().do_send(SendPost {
492       op: UserOperation::DeletePost,
493       post: res.clone(),
494       websocket_id,
495     });
496
497     Ok(res)
498   }
499 }
500
501 #[async_trait::async_trait(?Send)]
502 impl Perform for RemovePost {
503   type Response = PostResponse;
504
505   async fn perform(
506     &self,
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?;
512
513     let edit_id = data.edit_id;
514     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
515
516     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
517
518     // Verify that only the mods can remove
519     is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
520
521     // Update the post
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)
526     })
527     .await??;
528
529     // Mod tables
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(),
535     };
536     blocking(context.pool(), move |conn| {
537       ModRemovePost::create(conn, &form)
538     })
539     .await??;
540
541     // apub updates
542     if removed {
543       updated_post.send_remove(&user, context).await?;
544     } else {
545       updated_post.send_undo_remove(&user, context).await?;
546     }
547
548     // Refetch the post
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))
553     })
554     .await??;
555
556     let res = PostResponse { post: post_view };
557
558     context.chat_server().do_send(SendPost {
559       op: UserOperation::RemovePost,
560       post: res.clone(),
561       websocket_id,
562     });
563
564     Ok(res)
565   }
566 }
567
568 #[async_trait::async_trait(?Send)]
569 impl Perform for LockPost {
570   type Response = PostResponse;
571
572   async fn perform(
573     &self,
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?;
579
580     let edit_id = data.edit_id;
581     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
582
583     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
584
585     // Verify that only the mods can lock
586     is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
587
588     // Update the post
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)
593     })
594     .await??;
595
596     // Mod tables
597     let form = ModLockPostForm {
598       mod_user_id: user.id,
599       post_id: data.edit_id,
600       locked: Some(locked),
601     };
602     blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
603
604     // apub updates
605     updated_post.send_update(&user, context).await?;
606
607     // Refetch the post
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))
611     })
612     .await??;
613
614     let res = PostResponse { post: post_view };
615
616     context.chat_server().do_send(SendPost {
617       op: UserOperation::LockPost,
618       post: res.clone(),
619       websocket_id,
620     });
621
622     Ok(res)
623   }
624 }
625
626 #[async_trait::async_trait(?Send)]
627 impl Perform for StickyPost {
628   type Response = PostResponse;
629
630   async fn perform(
631     &self,
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?;
637
638     let edit_id = data.edit_id;
639     let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
640
641     check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
642
643     // Verify that only the mods can sticky
644     is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
645
646     // Update the post
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)
651     })
652     .await??;
653
654     // Mod tables
655     let form = ModStickyPostForm {
656       mod_user_id: user.id,
657       post_id: data.edit_id,
658       stickied: Some(stickied),
659     };
660     blocking(context.pool(), move |conn| {
661       ModStickyPost::create(conn, &form)
662     })
663     .await??;
664
665     // Apub updates
666     // TODO stickied should pry work like locked for ease of use
667     updated_post.send_update(&user, context).await?;
668
669     // Refetch the post
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))
673     })
674     .await??;
675
676     let res = PostResponse { post: post_view };
677
678     context.chat_server().do_send(SendPost {
679       op: UserOperation::StickyPost,
680       post: res.clone(),
681       websocket_id,
682     });
683
684     Ok(res)
685   }
686 }
687
688 #[async_trait::async_trait(?Send)]
689 impl Perform for SavePost {
690   type Response = PostResponse;
691
692   async fn perform(
693     &self,
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?;
699
700     let post_saved_form = PostSavedForm {
701       post_id: data.post_id,
702       user_id: user.id,
703     };
704
705     if data.save {
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());
709       }
710     } else {
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());
714       }
715     }
716
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))
721     })
722     .await??;
723
724     Ok(PostResponse { post: post_view })
725   }
726 }
727
728 #[async_trait::async_trait(?Send)]
729 impl Perform for PostJoin {
730   type Response = PostJoinResponse;
731
732   async fn perform(
733     &self,
734     context: &Data<LemmyContext>,
735     websocket_id: Option<ConnectionId>,
736   ) -> Result<PostJoinResponse, LemmyError> {
737     let data: &PostJoin = &self;
738
739     if let Some(ws_id) = websocket_id {
740       context.chat_server().do_send(JoinPostRoom {
741         post_id: data.post_id,
742         id: ws_id,
743       });
744     }
745
746     Ok(PostJoinResponse { joined: true })
747   }
748 }