]> Untitled Git - lemmy.git/blob - crates/api/src/lib.rs
Merge branch 'split_user_table' into strictly_type_db_ids
[lemmy.git] / crates / api / src / lib.rs
1 use actix_web::{web, web::Data};
2 use lemmy_api_structs::{
3   blocking,
4   comment::*,
5   community::*,
6   person::*,
7   post::*,
8   site::*,
9   websocket::*,
10 };
11 use lemmy_db_queries::{
12   source::{
13     community::{CommunityModerator_, Community_},
14     site::Site_,
15   },
16   Crud,
17   DbPool,
18 };
19 use lemmy_db_schema::{
20   source::{
21     community::{Community, CommunityModerator},
22     post::Post,
23     site::Site,
24   },
25   CommunityId,
26   LocalUserId,
27   PersonId,
28   PostId,
29 };
30 use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView};
31 use lemmy_db_views_actor::{
32   community_person_ban_view::CommunityPersonBanView,
33   community_view::CommunityView,
34 };
35 use lemmy_utils::{
36   claims::Claims,
37   settings::structs::Settings,
38   ApiError,
39   ConnectionId,
40   LemmyError,
41 };
42 use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
43 use serde::Deserialize;
44 use std::process::Command;
45 use url::Url;
46
47 pub mod comment;
48 pub mod community;
49 pub mod local_user;
50 pub mod post;
51 pub mod routes;
52 pub mod site;
53 pub mod websocket;
54
55 #[async_trait::async_trait(?Send)]
56 pub trait Perform {
57   type Response: serde::ser::Serialize + Send;
58
59   async fn perform(
60     &self,
61     context: &Data<LemmyContext>,
62     websocket_id: Option<ConnectionId>,
63   ) -> Result<Self::Response, LemmyError>;
64 }
65
66 pub(crate) async fn is_mod_or_admin(
67   pool: &DbPool,
68   person_id: PersonId,
69   community_id: CommunityId,
70 ) -> Result<(), LemmyError> {
71   let is_mod_or_admin = blocking(pool, move |conn| {
72     CommunityView::is_mod_or_admin(conn, person_id, community_id)
73   })
74   .await?;
75   if !is_mod_or_admin {
76     return Err(ApiError::err("not_a_mod_or_admin").into());
77   }
78   Ok(())
79 }
80
81 pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> {
82   if !local_user_view.local_user.admin {
83     return Err(ApiError::err("not_an_admin").into());
84   }
85   Ok(())
86 }
87
88 pub(crate) async fn get_post(post_id: PostId, pool: &DbPool) -> Result<Post, LemmyError> {
89   match blocking(pool, move |conn| Post::read(conn, post_id)).await? {
90     Ok(post) => Ok(post),
91     Err(_e) => Err(ApiError::err("couldnt_find_post").into()),
92   }
93 }
94
95 pub(crate) async fn get_local_user_view_from_jwt(
96   jwt: &str,
97   pool: &DbPool,
98 ) -> Result<LocalUserView, LemmyError> {
99   let claims = match Claims::decode(&jwt) {
100     Ok(claims) => claims.claims,
101     Err(_e) => return Err(ApiError::err("not_logged_in").into()),
102   };
103   let local_user_id = LocalUserId(claims.local_user_id);
104   let local_user_view =
105     blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??;
106   // Check for a site ban
107   if local_user_view.person.banned {
108     return Err(ApiError::err("site_ban").into());
109   }
110   Ok(local_user_view)
111 }
112
113 pub(crate) async fn get_local_user_view_from_jwt_opt(
114   jwt: &Option<String>,
115   pool: &DbPool,
116 ) -> Result<Option<LocalUserView>, LemmyError> {
117   match jwt {
118     Some(jwt) => Ok(Some(get_local_user_view_from_jwt(jwt, pool).await?)),
119     None => Ok(None),
120   }
121 }
122
123 pub(crate) async fn get_local_user_settings_view_from_jwt(
124   jwt: &str,
125   pool: &DbPool,
126 ) -> Result<LocalUserSettingsView, LemmyError> {
127   let claims = match Claims::decode(&jwt) {
128     Ok(claims) => claims.claims,
129     Err(_e) => return Err(ApiError::err("not_logged_in").into()),
130   };
131   let local_user_id = LocalUserId(claims.local_user_id);
132   let local_user_view = blocking(pool, move |conn| {
133     LocalUserSettingsView::read(conn, local_user_id)
134   })
135   .await??;
136   // Check for a site ban
137   if local_user_view.person.banned {
138     return Err(ApiError::err("site_ban").into());
139   }
140   Ok(local_user_view)
141 }
142
143 pub(crate) async fn get_local_user_settings_view_from_jwt_opt(
144   jwt: &Option<String>,
145   pool: &DbPool,
146 ) -> Result<Option<LocalUserSettingsView>, LemmyError> {
147   match jwt {
148     Some(jwt) => Ok(Some(
149       get_local_user_settings_view_from_jwt(jwt, pool).await?,
150     )),
151     None => Ok(None),
152   }
153 }
154
155 pub(crate) async fn check_community_ban(
156   person_id: PersonId,
157   community_id: CommunityId,
158   pool: &DbPool,
159 ) -> Result<(), LemmyError> {
160   let is_banned =
161     move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok();
162   if blocking(pool, is_banned).await? {
163     Err(ApiError::err("community_ban").into())
164   } else {
165     Ok(())
166   }
167 }
168
169 pub(crate) async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
170   if score == -1 {
171     let site = blocking(pool, move |conn| Site::read_simple(conn)).await??;
172     if !site.enable_downvotes {
173       return Err(ApiError::err("downvotes_disabled").into());
174     }
175   }
176   Ok(())
177 }
178
179 /// Returns a list of communities that the user moderates
180 /// or if a community_id is supplied validates the user is a moderator
181 /// of that community and returns the community id in a vec
182 ///
183 /// * `person_id` - the person id of the moderator
184 /// * `community_id` - optional community id to check for moderator privileges
185 /// * `pool` - the diesel db pool
186 pub(crate) async fn collect_moderated_communities(
187   person_id: PersonId,
188   community_id: Option<CommunityId>,
189   pool: &DbPool,
190 ) -> Result<Vec<CommunityId>, LemmyError> {
191   if let Some(community_id) = community_id {
192     // if the user provides a community_id, just check for mod/admin privileges
193     is_mod_or_admin(pool, person_id, community_id).await?;
194     Ok(vec![community_id])
195   } else {
196     let ids = blocking(pool, move |conn: &'_ _| {
197       CommunityModerator::get_person_moderated_communities(conn, person_id)
198     })
199     .await??;
200     Ok(ids)
201   }
202 }
203
204 pub(crate) async fn build_federated_instances(
205   pool: &DbPool,
206 ) -> Result<Option<FederatedInstances>, LemmyError> {
207   if Settings::get().federation().enabled {
208     let distinct_communities = blocking(pool, move |conn| {
209       Community::distinct_federated_communities(conn)
210     })
211     .await??;
212
213     let allowed = Settings::get().get_allowed_instances();
214     let blocked = Settings::get().get_blocked_instances();
215
216     let mut linked = distinct_communities
217       .iter()
218       .map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string()))
219       .collect::<Result<Vec<String>, LemmyError>>()?;
220
221     if let Some(allowed) = allowed.as_ref() {
222       linked.extend_from_slice(allowed);
223     }
224
225     if let Some(blocked) = blocked.as_ref() {
226       linked.retain(|a| !blocked.contains(a) && !a.eq(&Settings::get().hostname()));
227     }
228
229     // Sort and remove dupes
230     linked.sort_unstable();
231     linked.dedup();
232
233     Ok(Some(FederatedInstances {
234       linked,
235       allowed,
236       blocked,
237     }))
238   } else {
239     Ok(None)
240   }
241 }
242
243 pub async fn match_websocket_operation(
244   context: LemmyContext,
245   id: ConnectionId,
246   op: UserOperation,
247   data: &str,
248 ) -> Result<String, LemmyError> {
249   match op {
250     // User ops
251     UserOperation::Login => do_websocket_operation::<Login>(context, id, op, data).await,
252     UserOperation::Register => do_websocket_operation::<Register>(context, id, op, data).await,
253     UserOperation::GetCaptcha => do_websocket_operation::<GetCaptcha>(context, id, op, data).await,
254     UserOperation::GetPersonDetails => {
255       do_websocket_operation::<GetPersonDetails>(context, id, op, data).await
256     }
257     UserOperation::GetReplies => do_websocket_operation::<GetReplies>(context, id, op, data).await,
258     UserOperation::AddAdmin => do_websocket_operation::<AddAdmin>(context, id, op, data).await,
259     UserOperation::BanPerson => do_websocket_operation::<BanPerson>(context, id, op, data).await,
260     UserOperation::GetPersonMentions => {
261       do_websocket_operation::<GetPersonMentions>(context, id, op, data).await
262     }
263     UserOperation::MarkPersonMentionAsRead => {
264       do_websocket_operation::<MarkPersonMentionAsRead>(context, id, op, data).await
265     }
266     UserOperation::MarkAllAsRead => {
267       do_websocket_operation::<MarkAllAsRead>(context, id, op, data).await
268     }
269     UserOperation::DeleteAccount => {
270       do_websocket_operation::<DeleteAccount>(context, id, op, data).await
271     }
272     UserOperation::PasswordReset => {
273       do_websocket_operation::<PasswordReset>(context, id, op, data).await
274     }
275     UserOperation::PasswordChange => {
276       do_websocket_operation::<PasswordChange>(context, id, op, data).await
277     }
278     UserOperation::UserJoin => do_websocket_operation::<UserJoin>(context, id, op, data).await,
279     UserOperation::PostJoin => do_websocket_operation::<PostJoin>(context, id, op, data).await,
280     UserOperation::CommunityJoin => {
281       do_websocket_operation::<CommunityJoin>(context, id, op, data).await
282     }
283     UserOperation::ModJoin => do_websocket_operation::<ModJoin>(context, id, op, data).await,
284     UserOperation::SaveUserSettings => {
285       do_websocket_operation::<SaveUserSettings>(context, id, op, data).await
286     }
287     UserOperation::GetReportCount => {
288       do_websocket_operation::<GetReportCount>(context, id, op, data).await
289     }
290
291     // Private Message ops
292     UserOperation::CreatePrivateMessage => {
293       do_websocket_operation::<CreatePrivateMessage>(context, id, op, data).await
294     }
295     UserOperation::EditPrivateMessage => {
296       do_websocket_operation::<EditPrivateMessage>(context, id, op, data).await
297     }
298     UserOperation::DeletePrivateMessage => {
299       do_websocket_operation::<DeletePrivateMessage>(context, id, op, data).await
300     }
301     UserOperation::MarkPrivateMessageAsRead => {
302       do_websocket_operation::<MarkPrivateMessageAsRead>(context, id, op, data).await
303     }
304     UserOperation::GetPrivateMessages => {
305       do_websocket_operation::<GetPrivateMessages>(context, id, op, data).await
306     }
307
308     // Site ops
309     UserOperation::GetModlog => do_websocket_operation::<GetModlog>(context, id, op, data).await,
310     UserOperation::CreateSite => do_websocket_operation::<CreateSite>(context, id, op, data).await,
311     UserOperation::EditSite => do_websocket_operation::<EditSite>(context, id, op, data).await,
312     UserOperation::GetSite => do_websocket_operation::<GetSite>(context, id, op, data).await,
313     UserOperation::GetSiteConfig => {
314       do_websocket_operation::<GetSiteConfig>(context, id, op, data).await
315     }
316     UserOperation::SaveSiteConfig => {
317       do_websocket_operation::<SaveSiteConfig>(context, id, op, data).await
318     }
319     UserOperation::Search => do_websocket_operation::<Search>(context, id, op, data).await,
320     UserOperation::TransferCommunity => {
321       do_websocket_operation::<TransferCommunity>(context, id, op, data).await
322     }
323     UserOperation::TransferSite => {
324       do_websocket_operation::<TransferSite>(context, id, op, data).await
325     }
326
327     // Community ops
328     UserOperation::GetCommunity => {
329       do_websocket_operation::<GetCommunity>(context, id, op, data).await
330     }
331     UserOperation::ListCommunities => {
332       do_websocket_operation::<ListCommunities>(context, id, op, data).await
333     }
334     UserOperation::CreateCommunity => {
335       do_websocket_operation::<CreateCommunity>(context, id, op, data).await
336     }
337     UserOperation::EditCommunity => {
338       do_websocket_operation::<EditCommunity>(context, id, op, data).await
339     }
340     UserOperation::DeleteCommunity => {
341       do_websocket_operation::<DeleteCommunity>(context, id, op, data).await
342     }
343     UserOperation::RemoveCommunity => {
344       do_websocket_operation::<RemoveCommunity>(context, id, op, data).await
345     }
346     UserOperation::FollowCommunity => {
347       do_websocket_operation::<FollowCommunity>(context, id, op, data).await
348     }
349     UserOperation::GetFollowedCommunities => {
350       do_websocket_operation::<GetFollowedCommunities>(context, id, op, data).await
351     }
352     UserOperation::BanFromCommunity => {
353       do_websocket_operation::<BanFromCommunity>(context, id, op, data).await
354     }
355     UserOperation::AddModToCommunity => {
356       do_websocket_operation::<AddModToCommunity>(context, id, op, data).await
357     }
358
359     // Post ops
360     UserOperation::CreatePost => do_websocket_operation::<CreatePost>(context, id, op, data).await,
361     UserOperation::GetPost => do_websocket_operation::<GetPost>(context, id, op, data).await,
362     UserOperation::GetPosts => do_websocket_operation::<GetPosts>(context, id, op, data).await,
363     UserOperation::EditPost => do_websocket_operation::<EditPost>(context, id, op, data).await,
364     UserOperation::DeletePost => do_websocket_operation::<DeletePost>(context, id, op, data).await,
365     UserOperation::RemovePost => do_websocket_operation::<RemovePost>(context, id, op, data).await,
366     UserOperation::LockPost => do_websocket_operation::<LockPost>(context, id, op, data).await,
367     UserOperation::StickyPost => do_websocket_operation::<StickyPost>(context, id, op, data).await,
368     UserOperation::CreatePostLike => {
369       do_websocket_operation::<CreatePostLike>(context, id, op, data).await
370     }
371     UserOperation::SavePost => do_websocket_operation::<SavePost>(context, id, op, data).await,
372     UserOperation::CreatePostReport => {
373       do_websocket_operation::<CreatePostReport>(context, id, op, data).await
374     }
375     UserOperation::ListPostReports => {
376       do_websocket_operation::<ListPostReports>(context, id, op, data).await
377     }
378     UserOperation::ResolvePostReport => {
379       do_websocket_operation::<ResolvePostReport>(context, id, op, data).await
380     }
381
382     // Comment ops
383     UserOperation::CreateComment => {
384       do_websocket_operation::<CreateComment>(context, id, op, data).await
385     }
386     UserOperation::EditComment => {
387       do_websocket_operation::<EditComment>(context, id, op, data).await
388     }
389     UserOperation::DeleteComment => {
390       do_websocket_operation::<DeleteComment>(context, id, op, data).await
391     }
392     UserOperation::RemoveComment => {
393       do_websocket_operation::<RemoveComment>(context, id, op, data).await
394     }
395     UserOperation::MarkCommentAsRead => {
396       do_websocket_operation::<MarkCommentAsRead>(context, id, op, data).await
397     }
398     UserOperation::SaveComment => {
399       do_websocket_operation::<SaveComment>(context, id, op, data).await
400     }
401     UserOperation::GetComments => {
402       do_websocket_operation::<GetComments>(context, id, op, data).await
403     }
404     UserOperation::CreateCommentLike => {
405       do_websocket_operation::<CreateCommentLike>(context, id, op, data).await
406     }
407     UserOperation::CreateCommentReport => {
408       do_websocket_operation::<CreateCommentReport>(context, id, op, data).await
409     }
410     UserOperation::ListCommentReports => {
411       do_websocket_operation::<ListCommentReports>(context, id, op, data).await
412     }
413     UserOperation::ResolveCommentReport => {
414       do_websocket_operation::<ResolveCommentReport>(context, id, op, data).await
415     }
416   }
417 }
418
419 async fn do_websocket_operation<'a, 'b, Data>(
420   context: LemmyContext,
421   id: ConnectionId,
422   op: UserOperation,
423   data: &str,
424 ) -> Result<String, LemmyError>
425 where
426   for<'de> Data: Deserialize<'de> + 'a,
427   Data: Perform,
428 {
429   let parsed_data: Data = serde_json::from_str(&data)?;
430   let res = parsed_data
431     .perform(&web::Data::new(context), Some(id))
432     .await?;
433   serialize_websocket_message(&op, &res)
434 }
435
436 pub(crate) fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
437   let mut built_text = String::new();
438
439   // Building proper speech text for espeak
440   for mut c in captcha.chars() {
441     let new_str = if c.is_alphabetic() {
442       if c.is_lowercase() {
443         c.make_ascii_uppercase();
444         format!("lower case {} ... ", c)
445       } else {
446         c.make_ascii_uppercase();
447         format!("capital {} ... ", c)
448       }
449     } else {
450       format!("{} ...", c)
451     };
452
453     built_text.push_str(&new_str);
454   }
455
456   espeak_wav_base64(&built_text)
457 }
458
459 pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
460   // Make a temp file path
461   let uuid = uuid::Uuid::new_v4().to_string();
462   let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid);
463
464   // Write the wav file
465   Command::new("espeak")
466     .arg("-w")
467     .arg(&file_path)
468     .arg(text)
469     .status()?;
470
471   // Read the wav file bytes
472   let bytes = std::fs::read(&file_path)?;
473
474   // Delete the file
475   std::fs::remove_file(file_path)?;
476
477   // Convert to base64
478   let base64 = base64::encode(bytes);
479
480   Ok(base64)
481 }
482
483 /// Checks the password length
484 pub(crate) fn password_length_check(pass: &str) -> Result<(), LemmyError> {
485   if pass.len() > 60 {
486     Err(ApiError::err("invalid_password").into())
487   } else {
488     Ok(())
489   }
490 }
491
492 #[cfg(test)]
493 mod tests {
494   use crate::captcha_espeak_wav_base64;
495
496   #[test]
497   fn test_espeak() {
498     assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
499   }
500 }