]> Untitled Git - lemmy.git/blob - crates/api/src/lib.rs
Use URL type in most outstanding struct fields (#1468)
[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) async fn build_federated_instances(
190   pool: &DbPool,
191 ) -> Result<Option<FederatedInstances>, LemmyError> {
192   if Settings::get().federation().enabled {
193     let distinct_communities = blocking(pool, move |conn| {
194       Community::distinct_federated_communities(conn)
195     })
196     .await??;
197
198     let allowed = Settings::get().get_allowed_instances();
199     let blocked = Settings::get().get_blocked_instances();
200
201     let mut linked = distinct_communities
202       .iter()
203       .map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string()))
204       .collect::<Result<Vec<String>, LemmyError>>()?;
205
206     if let Some(allowed) = allowed.as_ref() {
207       linked.extend_from_slice(allowed);
208     }
209
210     if let Some(blocked) = blocked.as_ref() {
211       linked.retain(|a| !blocked.contains(a) && !a.eq(&Settings::get().hostname()));
212     }
213
214     // Sort and remove dupes
215     linked.sort_unstable();
216     linked.dedup();
217
218     Ok(Some(FederatedInstances {
219       linked,
220       allowed,
221       blocked,
222     }))
223   } else {
224     Ok(None)
225   }
226 }
227
228 pub async fn match_websocket_operation(
229   context: LemmyContext,
230   id: ConnectionId,
231   op: UserOperation,
232   data: &str,
233 ) -> Result<String, LemmyError> {
234   match op {
235     // User ops
236     UserOperation::Login => do_websocket_operation::<Login>(context, id, op, data).await,
237     UserOperation::Register => do_websocket_operation::<Register>(context, id, op, data).await,
238     UserOperation::GetCaptcha => do_websocket_operation::<GetCaptcha>(context, id, op, data).await,
239     UserOperation::GetUserDetails => {
240       do_websocket_operation::<GetUserDetails>(context, id, op, data).await
241     }
242     UserOperation::GetReplies => do_websocket_operation::<GetReplies>(context, id, op, data).await,
243     UserOperation::AddAdmin => do_websocket_operation::<AddAdmin>(context, id, op, data).await,
244     UserOperation::BanUser => do_websocket_operation::<BanUser>(context, id, op, data).await,
245     UserOperation::GetUserMentions => {
246       do_websocket_operation::<GetUserMentions>(context, id, op, data).await
247     }
248     UserOperation::MarkUserMentionAsRead => {
249       do_websocket_operation::<MarkUserMentionAsRead>(context, id, op, data).await
250     }
251     UserOperation::MarkAllAsRead => {
252       do_websocket_operation::<MarkAllAsRead>(context, id, op, data).await
253     }
254     UserOperation::DeleteAccount => {
255       do_websocket_operation::<DeleteAccount>(context, id, op, data).await
256     }
257     UserOperation::PasswordReset => {
258       do_websocket_operation::<PasswordReset>(context, id, op, data).await
259     }
260     UserOperation::PasswordChange => {
261       do_websocket_operation::<PasswordChange>(context, id, op, data).await
262     }
263     UserOperation::UserJoin => do_websocket_operation::<UserJoin>(context, id, op, data).await,
264     UserOperation::PostJoin => do_websocket_operation::<PostJoin>(context, id, op, data).await,
265     UserOperation::CommunityJoin => {
266       do_websocket_operation::<CommunityJoin>(context, id, op, data).await
267     }
268     UserOperation::ModJoin => do_websocket_operation::<ModJoin>(context, id, op, data).await,
269     UserOperation::SaveUserSettings => {
270       do_websocket_operation::<SaveUserSettings>(context, id, op, data).await
271     }
272     UserOperation::GetReportCount => {
273       do_websocket_operation::<GetReportCount>(context, id, op, data).await
274     }
275
276     // Private Message ops
277     UserOperation::CreatePrivateMessage => {
278       do_websocket_operation::<CreatePrivateMessage>(context, id, op, data).await
279     }
280     UserOperation::EditPrivateMessage => {
281       do_websocket_operation::<EditPrivateMessage>(context, id, op, data).await
282     }
283     UserOperation::DeletePrivateMessage => {
284       do_websocket_operation::<DeletePrivateMessage>(context, id, op, data).await
285     }
286     UserOperation::MarkPrivateMessageAsRead => {
287       do_websocket_operation::<MarkPrivateMessageAsRead>(context, id, op, data).await
288     }
289     UserOperation::GetPrivateMessages => {
290       do_websocket_operation::<GetPrivateMessages>(context, id, op, data).await
291     }
292
293     // Site ops
294     UserOperation::GetModlog => do_websocket_operation::<GetModlog>(context, id, op, data).await,
295     UserOperation::CreateSite => do_websocket_operation::<CreateSite>(context, id, op, data).await,
296     UserOperation::EditSite => do_websocket_operation::<EditSite>(context, id, op, data).await,
297     UserOperation::GetSite => do_websocket_operation::<GetSite>(context, id, op, data).await,
298     UserOperation::GetSiteConfig => {
299       do_websocket_operation::<GetSiteConfig>(context, id, op, data).await
300     }
301     UserOperation::SaveSiteConfig => {
302       do_websocket_operation::<SaveSiteConfig>(context, id, op, data).await
303     }
304     UserOperation::Search => do_websocket_operation::<Search>(context, id, op, data).await,
305     UserOperation::TransferCommunity => {
306       do_websocket_operation::<TransferCommunity>(context, id, op, data).await
307     }
308     UserOperation::TransferSite => {
309       do_websocket_operation::<TransferSite>(context, id, op, data).await
310     }
311
312     // Community ops
313     UserOperation::GetCommunity => {
314       do_websocket_operation::<GetCommunity>(context, id, op, data).await
315     }
316     UserOperation::ListCommunities => {
317       do_websocket_operation::<ListCommunities>(context, id, op, data).await
318     }
319     UserOperation::CreateCommunity => {
320       do_websocket_operation::<CreateCommunity>(context, id, op, data).await
321     }
322     UserOperation::EditCommunity => {
323       do_websocket_operation::<EditCommunity>(context, id, op, data).await
324     }
325     UserOperation::DeleteCommunity => {
326       do_websocket_operation::<DeleteCommunity>(context, id, op, data).await
327     }
328     UserOperation::RemoveCommunity => {
329       do_websocket_operation::<RemoveCommunity>(context, id, op, data).await
330     }
331     UserOperation::FollowCommunity => {
332       do_websocket_operation::<FollowCommunity>(context, id, op, data).await
333     }
334     UserOperation::GetFollowedCommunities => {
335       do_websocket_operation::<GetFollowedCommunities>(context, id, op, data).await
336     }
337     UserOperation::BanFromCommunity => {
338       do_websocket_operation::<BanFromCommunity>(context, id, op, data).await
339     }
340     UserOperation::AddModToCommunity => {
341       do_websocket_operation::<AddModToCommunity>(context, id, op, data).await
342     }
343
344     // Post ops
345     UserOperation::CreatePost => do_websocket_operation::<CreatePost>(context, id, op, data).await,
346     UserOperation::GetPost => do_websocket_operation::<GetPost>(context, id, op, data).await,
347     UserOperation::GetPosts => do_websocket_operation::<GetPosts>(context, id, op, data).await,
348     UserOperation::EditPost => do_websocket_operation::<EditPost>(context, id, op, data).await,
349     UserOperation::DeletePost => do_websocket_operation::<DeletePost>(context, id, op, data).await,
350     UserOperation::RemovePost => do_websocket_operation::<RemovePost>(context, id, op, data).await,
351     UserOperation::LockPost => do_websocket_operation::<LockPost>(context, id, op, data).await,
352     UserOperation::StickyPost => do_websocket_operation::<StickyPost>(context, id, op, data).await,
353     UserOperation::CreatePostLike => {
354       do_websocket_operation::<CreatePostLike>(context, id, op, data).await
355     }
356     UserOperation::SavePost => do_websocket_operation::<SavePost>(context, id, op, data).await,
357     UserOperation::CreatePostReport => {
358       do_websocket_operation::<CreatePostReport>(context, id, op, data).await
359     }
360     UserOperation::ListPostReports => {
361       do_websocket_operation::<ListPostReports>(context, id, op, data).await
362     }
363     UserOperation::ResolvePostReport => {
364       do_websocket_operation::<ResolvePostReport>(context, id, op, data).await
365     }
366
367     // Comment ops
368     UserOperation::CreateComment => {
369       do_websocket_operation::<CreateComment>(context, id, op, data).await
370     }
371     UserOperation::EditComment => {
372       do_websocket_operation::<EditComment>(context, id, op, data).await
373     }
374     UserOperation::DeleteComment => {
375       do_websocket_operation::<DeleteComment>(context, id, op, data).await
376     }
377     UserOperation::RemoveComment => {
378       do_websocket_operation::<RemoveComment>(context, id, op, data).await
379     }
380     UserOperation::MarkCommentAsRead => {
381       do_websocket_operation::<MarkCommentAsRead>(context, id, op, data).await
382     }
383     UserOperation::SaveComment => {
384       do_websocket_operation::<SaveComment>(context, id, op, data).await
385     }
386     UserOperation::GetComments => {
387       do_websocket_operation::<GetComments>(context, id, op, data).await
388     }
389     UserOperation::CreateCommentLike => {
390       do_websocket_operation::<CreateCommentLike>(context, id, op, data).await
391     }
392     UserOperation::CreateCommentReport => {
393       do_websocket_operation::<CreateCommentReport>(context, id, op, data).await
394     }
395     UserOperation::ListCommentReports => {
396       do_websocket_operation::<ListCommentReports>(context, id, op, data).await
397     }
398     UserOperation::ResolveCommentReport => {
399       do_websocket_operation::<ResolveCommentReport>(context, id, op, data).await
400     }
401   }
402 }
403
404 async fn do_websocket_operation<'a, 'b, Data>(
405   context: LemmyContext,
406   id: ConnectionId,
407   op: UserOperation,
408   data: &str,
409 ) -> Result<String, LemmyError>
410 where
411   for<'de> Data: Deserialize<'de> + 'a,
412   Data: Perform,
413 {
414   let parsed_data: Data = serde_json::from_str(&data)?;
415   let res = parsed_data
416     .perform(&web::Data::new(context), Some(id))
417     .await?;
418   serialize_websocket_message(&op, &res)
419 }
420
421 pub(crate) fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
422   let mut built_text = String::new();
423
424   // Building proper speech text for espeak
425   for mut c in captcha.chars() {
426     let new_str = if c.is_alphabetic() {
427       if c.is_lowercase() {
428         c.make_ascii_uppercase();
429         format!("lower case {} ... ", c)
430       } else {
431         c.make_ascii_uppercase();
432         format!("capital {} ... ", c)
433       }
434     } else {
435       format!("{} ...", c)
436     };
437
438     built_text.push_str(&new_str);
439   }
440
441   espeak_wav_base64(&built_text)
442 }
443
444 pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
445   // Make a temp file path
446   let uuid = uuid::Uuid::new_v4().to_string();
447   let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid);
448
449   // Write the wav file
450   Command::new("espeak")
451     .arg("-w")
452     .arg(&file_path)
453     .arg(text)
454     .status()?;
455
456   // Read the wav file bytes
457   let bytes = std::fs::read(&file_path)?;
458
459   // Delete the file
460   std::fs::remove_file(file_path)?;
461
462   // Convert to base64
463   let base64 = base64::encode(bytes);
464
465   Ok(base64)
466 }
467
468 #[cfg(test)]
469 mod tests {
470   use crate::captcha_espeak_wav_base64;
471
472   #[test]
473   fn test_espeak() {
474     assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
475   }
476 }