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