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