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