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