]> Untitled Git - lemmy.git/blob - server/src/api/post.rs
Merge branch 'master' into federation_merge_from_master_2
[lemmy.git] / server / src / api / post.rs
1 use crate::{
2   api::{APIError, Oper, Perform},
3   apub::{ApubLikeableType, ApubObjectType},
4   db::{
5     comment_view::*,
6     community_view::*,
7     moderator::*,
8     post::*,
9     post_view::*,
10     site::*,
11     site_view::*,
12     user::*,
13     user_view::*,
14     Crud,
15     Likeable,
16     ListingType,
17     Saveable,
18     SortType,
19   },
20   fetch_iframely_and_pictrs_data,
21   naive_now,
22   slur_check,
23   slurs_vec_to_str,
24   websocket::{
25     server::{JoinCommunityRoom, JoinPostRoom, SendPost},
26     UserOperation,
27     WebsocketInfo,
28   },
29 };
30 use diesel::{
31   r2d2::{ConnectionManager, Pool},
32   PgConnection,
33 };
34 use failure::Error;
35 use serde::{Deserialize, Serialize};
36 use std::str::FromStr;
37
38 #[derive(Serialize, Deserialize, Debug)]
39 pub struct CreatePost {
40   name: String,
41   url: Option<String>,
42   body: Option<String>,
43   nsfw: bool,
44   pub community_id: i32,
45   auth: String,
46 }
47
48 #[derive(Serialize, Deserialize, Clone)]
49 pub struct PostResponse {
50   pub post: PostView,
51 }
52
53 #[derive(Serialize, Deserialize)]
54 pub struct GetPost {
55   pub id: i32,
56   auth: Option<String>,
57 }
58
59 #[derive(Serialize, Deserialize)]
60 pub struct GetPostResponse {
61   post: PostView,
62   comments: Vec<CommentView>,
63   community: CommunityView,
64   moderators: Vec<CommunityModeratorView>,
65   admins: Vec<UserView>,
66   pub online: usize,
67 }
68
69 #[derive(Serialize, Deserialize, Debug)]
70 pub struct GetPosts {
71   type_: String,
72   sort: String,
73   page: Option<i64>,
74   limit: Option<i64>,
75   pub community_id: Option<i32>,
76   auth: Option<String>,
77 }
78
79 #[derive(Serialize, Deserialize, Debug)]
80 pub struct GetPostsResponse {
81   pub posts: Vec<PostView>,
82 }
83
84 #[derive(Serialize, Deserialize)]
85 pub struct CreatePostLike {
86   post_id: i32,
87   score: i16,
88   auth: String,
89 }
90
91 #[derive(Serialize, Deserialize)]
92 pub struct EditPost {
93   pub edit_id: i32,
94   creator_id: i32,
95   community_id: i32,
96   name: String,
97   url: Option<String>,
98   body: Option<String>,
99   removed: Option<bool>,
100   deleted: Option<bool>,
101   nsfw: bool,
102   locked: Option<bool>,
103   stickied: Option<bool>,
104   reason: Option<String>,
105   auth: String,
106 }
107
108 #[derive(Serialize, Deserialize)]
109 pub struct SavePost {
110   post_id: i32,
111   save: bool,
112   auth: String,
113 }
114
115 impl Perform for Oper<CreatePost> {
116   type Response = PostResponse;
117
118   fn perform(
119     &self,
120     pool: Pool<ConnectionManager<PgConnection>>,
121     websocket_info: Option<WebsocketInfo>,
122   ) -> Result<PostResponse, Error> {
123     let data: &CreatePost = &self.data;
124
125     let claims = match Claims::decode(&data.auth) {
126       Ok(claims) => claims.claims,
127       Err(_e) => return Err(APIError::err("not_logged_in").into()),
128     };
129
130     if let Err(slurs) = slur_check(&data.name) {
131       return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
132     }
133
134     if let Some(body) = &data.body {
135       if let Err(slurs) = slur_check(body) {
136         return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
137       }
138     }
139
140     let user_id = claims.id;
141
142     let conn = pool.get()?;
143
144     // Check for a community ban
145     if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
146       return Err(APIError::err("community_ban").into());
147     }
148
149     // Check for a site ban
150     let user = User_::read(&conn, user_id)?;
151     if user.banned {
152       return Err(APIError::err("site_ban").into());
153     }
154
155     // Fetch Iframely and pictrs cached image
156     let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
157       fetch_iframely_and_pictrs_data(data.url.to_owned());
158
159     let post_form = PostForm {
160       name: data.name.to_owned(),
161       url: data.url.to_owned(),
162       body: data.body.to_owned(),
163       community_id: data.community_id,
164       creator_id: user_id,
165       removed: None,
166       deleted: None,
167       nsfw: data.nsfw,
168       locked: None,
169       stickied: None,
170       updated: None,
171       embed_title: iframely_title,
172       embed_description: iframely_description,
173       embed_html: iframely_html,
174       thumbnail_url: pictrs_thumbnail,
175       ap_id: "changeme".into(),
176       local: true,
177       published: None,
178     };
179
180     let inserted_post = match Post::create(&conn, &post_form) {
181       Ok(post) => post,
182       Err(e) => {
183         let err_type = if e.to_string() == "value too long for type character varying(200)" {
184           "post_title_too_long"
185         } else {
186           "couldnt_create_post"
187         };
188
189         return Err(APIError::err(err_type).into());
190       }
191     };
192
193     let updated_post = match Post::update_ap_id(&conn, inserted_post.id) {
194       Ok(post) => post,
195       Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
196     };
197
198     updated_post.send_create(&user, &conn)?;
199
200     // They like their own post by default
201     let like_form = PostLikeForm {
202       post_id: inserted_post.id,
203       user_id,
204       score: 1,
205     };
206
207     let _inserted_like = match PostLike::like(&conn, &like_form) {
208       Ok(like) => like,
209       Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
210     };
211
212     updated_post.send_like(&user, &conn)?;
213
214     // Refetch the view
215     let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
216       Ok(post) => post,
217       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
218     };
219
220     let res = PostResponse { post: post_view };
221
222     if let Some(ws) = websocket_info {
223       ws.chatserver.do_send(SendPost {
224         op: UserOperation::CreatePost,
225         post: res.clone(),
226         my_id: ws.id,
227       });
228     }
229
230     Ok(res)
231   }
232 }
233
234 impl Perform for Oper<GetPost> {
235   type Response = GetPostResponse;
236
237   fn perform(
238     &self,
239     pool: Pool<ConnectionManager<PgConnection>>,
240     websocket_info: Option<WebsocketInfo>,
241   ) -> Result<GetPostResponse, Error> {
242     let data: &GetPost = &self.data;
243
244     let user_id: Option<i32> = match &data.auth {
245       Some(auth) => match Claims::decode(&auth) {
246         Ok(claims) => {
247           let user_id = claims.claims.id;
248           Some(user_id)
249         }
250         Err(_e) => None,
251       },
252       None => None,
253     };
254
255     let conn = pool.get()?;
256
257     let post_view = match PostView::read(&conn, data.id, user_id) {
258       Ok(post) => post,
259       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
260     };
261
262     let comments = CommentQueryBuilder::create(&conn)
263       .for_post_id(data.id)
264       .my_user_id(user_id)
265       .limit(9999)
266       .list()?;
267
268     let community = CommunityView::read(&conn, post_view.community_id, user_id)?;
269
270     let moderators = CommunityModeratorView::for_community(&conn, post_view.community_id)?;
271
272     let site_creator_id = Site::read(&conn, 1)?.creator_id;
273     let mut admins = UserView::admins(&conn)?;
274     let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
275     let creator_user = admins.remove(creator_index);
276     admins.insert(0, creator_user);
277
278     let online = if let Some(ws) = websocket_info {
279       if let Some(id) = ws.id {
280         ws.chatserver.do_send(JoinPostRoom {
281           post_id: data.id,
282           id,
283         });
284       }
285
286       // TODO
287       1
288     // let fut = async {
289     //   ws.chatserver.send(GetPostUsersOnline {post_id: data.id}).await.unwrap()
290     // };
291     // Runtime::new().unwrap().block_on(fut)
292     } else {
293       0
294     };
295
296     // Return the jwt
297     Ok(GetPostResponse {
298       post: post_view,
299       comments,
300       community,
301       moderators,
302       admins,
303       online,
304     })
305   }
306 }
307
308 impl Perform for Oper<GetPosts> {
309   type Response = GetPostsResponse;
310
311   fn perform(
312     &self,
313     pool: Pool<ConnectionManager<PgConnection>>,
314     websocket_info: Option<WebsocketInfo>,
315   ) -> Result<GetPostsResponse, Error> {
316     let data: &GetPosts = &self.data;
317
318     let user_claims: Option<Claims> = match &data.auth {
319       Some(auth) => match Claims::decode(&auth) {
320         Ok(claims) => Some(claims.claims),
321         Err(_e) => None,
322       },
323       None => None,
324     };
325
326     let user_id = match &user_claims {
327       Some(claims) => Some(claims.id),
328       None => None,
329     };
330
331     let show_nsfw = match &user_claims {
332       Some(claims) => claims.show_nsfw,
333       None => false,
334     };
335
336     let type_ = ListingType::from_str(&data.type_)?;
337     let sort = SortType::from_str(&data.sort)?;
338
339     let conn = pool.get()?;
340
341     let posts = match PostQueryBuilder::create(&conn)
342       .listing_type(type_)
343       .sort(&sort)
344       .show_nsfw(show_nsfw)
345       .for_community_id(data.community_id)
346       .my_user_id(user_id)
347       .page(data.page)
348       .limit(data.limit)
349       .list()
350     {
351       Ok(posts) => posts,
352       Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
353     };
354
355     if let Some(ws) = websocket_info {
356       // You don't need to join the specific community room, bc this is already handled by
357       // GetCommunity
358       if data.community_id.is_none() {
359         if let Some(id) = ws.id {
360           // 0 is the "all" community
361           ws.chatserver.do_send(JoinCommunityRoom {
362             community_id: 0,
363             id,
364           });
365         }
366       }
367     }
368
369     Ok(GetPostsResponse { posts })
370   }
371 }
372
373 impl Perform for Oper<CreatePostLike> {
374   type Response = PostResponse;
375
376   fn perform(
377     &self,
378     pool: Pool<ConnectionManager<PgConnection>>,
379     websocket_info: Option<WebsocketInfo>,
380   ) -> Result<PostResponse, Error> {
381     let data: &CreatePostLike = &self.data;
382
383     let claims = match Claims::decode(&data.auth) {
384       Ok(claims) => claims.claims,
385       Err(_e) => return Err(APIError::err("not_logged_in").into()),
386     };
387
388     let user_id = claims.id;
389
390     let conn = pool.get()?;
391
392     // Don't do a downvote if site has downvotes disabled
393     if data.score == -1 {
394       let site = SiteView::read(&conn)?;
395       if !site.enable_downvotes {
396         return Err(APIError::err("downvotes_disabled").into());
397       }
398     }
399
400     // Check for a community ban
401     let post = Post::read(&conn, data.post_id)?;
402     if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
403       return Err(APIError::err("community_ban").into());
404     }
405
406     // Check for a site ban
407     let user = User_::read(&conn, user_id)?;
408     if user.banned {
409       return Err(APIError::err("site_ban").into());
410     }
411
412     let like_form = PostLikeForm {
413       post_id: data.post_id,
414       user_id,
415       score: data.score,
416     };
417
418     // Remove any likes first
419     PostLike::remove(&conn, &like_form)?;
420
421     // Only add the like if the score isnt 0
422     let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
423     if do_add {
424       let _inserted_like = match PostLike::like(&conn, &like_form) {
425         Ok(like) => like,
426         Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
427       };
428
429       if like_form.score == 1 {
430         post.send_like(&user, &conn)?;
431       } else if like_form.score == -1 {
432         post.send_dislike(&user, &conn)?;
433       }
434     } else {
435       post.send_undo_like(&user, &conn)?;
436     }
437
438     let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
439       Ok(post) => post,
440       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
441     };
442
443     let res = PostResponse { post: post_view };
444
445     if let Some(ws) = websocket_info {
446       ws.chatserver.do_send(SendPost {
447         op: UserOperation::CreatePostLike,
448         post: res.clone(),
449         my_id: ws.id,
450       });
451     }
452
453     Ok(res)
454   }
455 }
456
457 impl Perform for Oper<EditPost> {
458   type Response = PostResponse;
459
460   fn perform(
461     &self,
462     pool: Pool<ConnectionManager<PgConnection>>,
463     websocket_info: Option<WebsocketInfo>,
464   ) -> Result<PostResponse, Error> {
465     let data: &EditPost = &self.data;
466
467     if let Err(slurs) = slur_check(&data.name) {
468       return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
469     }
470
471     if let Some(body) = &data.body {
472       if let Err(slurs) = slur_check(body) {
473         return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
474       }
475     }
476
477     let claims = match Claims::decode(&data.auth) {
478       Ok(claims) => claims.claims,
479       Err(_e) => return Err(APIError::err("not_logged_in").into()),
480     };
481
482     let user_id = claims.id;
483
484     let conn = pool.get()?;
485
486     // Verify its the creator or a mod or admin
487     let mut editors: Vec<i32> = vec![data.creator_id];
488     editors.append(
489       &mut CommunityModeratorView::for_community(&conn, data.community_id)?
490         .into_iter()
491         .map(|m| m.user_id)
492         .collect(),
493     );
494     editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
495     if !editors.contains(&user_id) {
496       return Err(APIError::err("no_post_edit_allowed").into());
497     }
498
499     // Check for a community ban
500     if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
501       return Err(APIError::err("community_ban").into());
502     }
503
504     // Check for a site ban
505     let user = User_::read(&conn, user_id)?;
506     if user.banned {
507       return Err(APIError::err("site_ban").into());
508     }
509
510     // Fetch Iframely and Pictrs cached image
511     let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
512       fetch_iframely_and_pictrs_data(data.url.to_owned());
513
514     let read_post = Post::read(&conn, data.edit_id)?;
515
516     let post_form = PostForm {
517       name: data.name.to_owned(),
518       url: data.url.to_owned(),
519       body: data.body.to_owned(),
520       creator_id: data.creator_id.to_owned(),
521       community_id: data.community_id,
522       removed: data.removed.to_owned(),
523       deleted: data.deleted.to_owned(),
524       nsfw: data.nsfw,
525       locked: data.locked.to_owned(),
526       stickied: data.stickied.to_owned(),
527       updated: Some(naive_now()),
528       embed_title: iframely_title,
529       embed_description: iframely_description,
530       embed_html: iframely_html,
531       thumbnail_url: pictrs_thumbnail,
532       ap_id: read_post.ap_id,
533       local: read_post.local,
534       published: None,
535     };
536
537     let updated_post = match Post::update(&conn, data.edit_id, &post_form) {
538       Ok(post) => post,
539       Err(e) => {
540         let err_type = if e.to_string() == "value too long for type character varying(200)" {
541           "post_title_too_long"
542         } else {
543           "couldnt_update_post"
544         };
545
546         return Err(APIError::err(err_type).into());
547       }
548     };
549
550     // Mod tables
551     if let Some(removed) = data.removed.to_owned() {
552       let form = ModRemovePostForm {
553         mod_user_id: user_id,
554         post_id: data.edit_id,
555         removed: Some(removed),
556         reason: data.reason.to_owned(),
557       };
558       ModRemovePost::create(&conn, &form)?;
559     }
560
561     if let Some(locked) = data.locked.to_owned() {
562       let form = ModLockPostForm {
563         mod_user_id: user_id,
564         post_id: data.edit_id,
565         locked: Some(locked),
566       };
567       ModLockPost::create(&conn, &form)?;
568     }
569
570     if let Some(stickied) = data.stickied.to_owned() {
571       let form = ModStickyPostForm {
572         mod_user_id: user_id,
573         post_id: data.edit_id,
574         stickied: Some(stickied),
575       };
576       ModStickyPost::create(&conn, &form)?;
577     }
578
579     if let Some(deleted) = data.deleted.to_owned() {
580       if deleted {
581         updated_post.send_delete(&user, &conn)?;
582       } else {
583         updated_post.send_undo_delete(&user, &conn)?;
584       }
585     } else if let Some(removed) = data.removed.to_owned() {
586       if removed {
587         updated_post.send_remove(&user, &conn)?;
588       } else {
589         updated_post.send_undo_remove(&user, &conn)?;
590       }
591     } else {
592       updated_post.send_update(&user, &conn)?;
593     }
594
595     let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
596
597     let res = PostResponse { post: post_view };
598
599     if let Some(ws) = websocket_info {
600       ws.chatserver.do_send(SendPost {
601         op: UserOperation::EditPost,
602         post: res.clone(),
603         my_id: ws.id,
604       });
605     }
606
607     Ok(res)
608   }
609 }
610
611 impl Perform for Oper<SavePost> {
612   type Response = PostResponse;
613
614   fn perform(
615     &self,
616     pool: Pool<ConnectionManager<PgConnection>>,
617     _websocket_info: Option<WebsocketInfo>,
618   ) -> Result<PostResponse, Error> {
619     let data: &SavePost = &self.data;
620
621     let claims = match Claims::decode(&data.auth) {
622       Ok(claims) => claims.claims,
623       Err(_e) => return Err(APIError::err("not_logged_in").into()),
624     };
625
626     let user_id = claims.id;
627
628     let post_saved_form = PostSavedForm {
629       post_id: data.post_id,
630       user_id,
631     };
632
633     let conn = pool.get()?;
634
635     if data.save {
636       match PostSaved::save(&conn, &post_saved_form) {
637         Ok(post) => post,
638         Err(_e) => return Err(APIError::err("couldnt_save_post").into()),
639       };
640     } else {
641       match PostSaved::unsave(&conn, &post_saved_form) {
642         Ok(post) => post,
643         Err(_e) => return Err(APIError::err("couldnt_save_post").into()),
644       };
645     }
646
647     let post_view = PostView::read(&conn, data.post_id, Some(user_id))?;
648
649     Ok(PostResponse { post: post_view })
650   }
651 }