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