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