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