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