]> Untitled Git - lemmy.git/blob - crates/api/src/lib.rs
6f4ea8f4dec508fb392b5b09613f9e2e39c8b3ef
[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   post::*,
7   site::*,
8   user::*,
9   websocket::*,
10 };
11 use lemmy_db_queries::{
12   source::{
13     community::{CommunityModerator_, Community_},
14     site::Site_,
15     user::UserSafeSettings_,
16   },
17   Crud,
18   DbPool,
19 };
20 use lemmy_db_schema::source::{
21   community::{Community, CommunityModerator},
22   post::Post,
23   site::Site,
24   user::{UserSafeSettings, User_},
25 };
26 use lemmy_db_views_actor::{
27   community_user_ban_view::CommunityUserBanView,
28   community_view::CommunityView,
29 };
30 use lemmy_utils::{
31   claims::Claims,
32   settings::structs::Settings,
33   ApiError,
34   ConnectionId,
35   LemmyError,
36 };
37 use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
38 use serde::Deserialize;
39 use std::process::Command;
40 use url::Url;
41
42 pub mod comment;
43 pub mod community;
44 pub mod post;
45 pub mod routes;
46 pub mod site;
47 pub mod user;
48 pub mod websocket;
49
50 #[async_trait::async_trait(?Send)]
51 pub trait Perform {
52   type Response: serde::ser::Serialize + Send;
53
54   async fn perform(
55     &self,
56     context: &Data<LemmyContext>,
57     websocket_id: Option<ConnectionId>,
58   ) -> Result<Self::Response, LemmyError>;
59 }
60
61 pub(crate) async fn is_mod_or_admin(
62   pool: &DbPool,
63   user_id: i32,
64   community_id: i32,
65 ) -> Result<(), LemmyError> {
66   let is_mod_or_admin = blocking(pool, move |conn| {
67     CommunityView::is_mod_or_admin(conn, user_id, community_id)
68   })
69   .await?;
70   if !is_mod_or_admin {
71     return Err(ApiError::err("not_a_mod_or_admin").into());
72   }
73   Ok(())
74 }
75 pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> {
76   let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
77   if !user.admin {
78     return Err(ApiError::err("not_an_admin").into());
79   }
80   Ok(())
81 }
82
83 pub(crate) async fn get_post(post_id: i32, pool: &DbPool) -> Result<Post, LemmyError> {
84   match blocking(pool, move |conn| Post::read(conn, post_id)).await? {
85     Ok(post) => Ok(post),
86     Err(_e) => Err(ApiError::err("couldnt_find_post").into()),
87   }
88 }
89
90 pub(crate) async fn get_user_from_jwt(jwt: &str, pool: &DbPool) -> Result<User_, LemmyError> {
91   let claims = match Claims::decode(&jwt) {
92     Ok(claims) => claims.claims,
93     Err(_e) => return Err(ApiError::err("not_logged_in").into()),
94   };
95   let user_id = claims.id;
96   let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
97   // Check for a site ban
98   if user.banned {
99     return Err(ApiError::err("site_ban").into());
100   }
101   Ok(user)
102 }
103
104 pub(crate) async fn get_user_from_jwt_opt(
105   jwt: &Option<String>,
106   pool: &DbPool,
107 ) -> Result<Option<User_>, LemmyError> {
108   match jwt {
109     Some(jwt) => Ok(Some(get_user_from_jwt(jwt, pool).await?)),
110     None => Ok(None),
111   }
112 }
113
114 pub(crate) async fn get_user_safe_settings_from_jwt(
115   jwt: &str,
116   pool: &DbPool,
117 ) -> Result<UserSafeSettings, LemmyError> {
118   let claims = match Claims::decode(&jwt) {
119     Ok(claims) => claims.claims,
120     Err(_e) => return Err(ApiError::err("not_logged_in").into()),
121   };
122   let user_id = claims.id;
123   let user = blocking(pool, move |conn| UserSafeSettings::read(conn, user_id)).await??;
124   // Check for a site ban
125   if user.banned {
126     return Err(ApiError::err("site_ban").into());
127   }
128   Ok(user)
129 }
130
131 pub(crate) async fn get_user_safe_settings_from_jwt_opt(
132   jwt: &Option<String>,
133   pool: &DbPool,
134 ) -> Result<Option<UserSafeSettings>, LemmyError> {
135   match jwt {
136     Some(jwt) => Ok(Some(get_user_safe_settings_from_jwt(jwt, pool).await?)),
137     None => Ok(None),
138   }
139 }
140
141 pub(crate) async fn check_community_ban(
142   user_id: i32,
143   community_id: i32,
144   pool: &DbPool,
145 ) -> Result<(), LemmyError> {
146   let is_banned = move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
147   if blocking(pool, is_banned).await? {
148     Err(ApiError::err("community_ban").into())
149   } else {
150     Ok(())
151   }
152 }
153
154 pub(crate) async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
155   if score == -1 {
156     let site = blocking(pool, move |conn| Site::read_simple(conn)).await??;
157     if !site.enable_downvotes {
158       return Err(ApiError::err("downvotes_disabled").into());
159     }
160   }
161   Ok(())
162 }
163
164 /// Returns a list of communities that the user moderates
165 /// or if a community_id is supplied validates the user is a moderator
166 /// of that community and returns the community id in a vec
167 ///
168 /// * `user_id` - the user id of the moderator
169 /// * `community_id` - optional community id to check for moderator privileges
170 /// * `pool` - the diesel db pool
171 pub(crate) async fn collect_moderated_communities(
172   user_id: i32,
173   community_id: Option<i32>,
174   pool: &DbPool,
175 ) -> Result<Vec<i32>, LemmyError> {
176   if let Some(community_id) = community_id {
177     // if the user provides a community_id, just check for mod/admin privileges
178     is_mod_or_admin(pool, user_id, community_id).await?;
179     Ok(vec![community_id])
180   } else {
181     let ids = blocking(pool, move |conn: &'_ _| {
182       CommunityModerator::get_user_moderated_communities(conn, user_id)
183     })
184     .await??;
185     Ok(ids)
186   }
187 }
188
189 pub(crate) fn check_optional_url(item: &Option<Option<String>>) -> Result<(), LemmyError> {
190   if let Some(Some(item)) = &item {
191     if Url::parse(item).is_err() {
192       return Err(ApiError::err("invalid_url").into());
193     }
194   }
195   Ok(())
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::GetUserDetails => {
249       do_websocket_operation::<GetUserDetails>(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::BanUser => do_websocket_operation::<BanUser>(context, id, op, data).await,
254     UserOperation::GetUserMentions => {
255       do_websocket_operation::<GetUserMentions>(context, id, op, data).await
256     }
257     UserOperation::MarkUserMentionAsRead => {
258       do_websocket_operation::<MarkUserMentionAsRead>(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 #[cfg(test)]
478 mod tests {
479   use crate::captcha_espeak_wav_base64;
480
481   #[test]
482   fn test_espeak() {
483     assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
484   }
485 }