]> Untitled Git - lemmy.git/blob - crates/api/src/lib.rs
Moving ChangePassword to its own API action. Fixes #1471
[lemmy.git] / crates / api / src / lib.rs
1 use actix_web::{web, web::Data};
2 use lemmy_api_common::{comment::*, community::*, person::*, post::*, site::*, websocket::*};
3 use lemmy_utils::{ConnectionId, LemmyError};
4 use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
5 use serde::Deserialize;
6 use std::{env, process::Command};
7
8 mod comment;
9 mod comment_report;
10 mod community;
11 mod local_user;
12 mod post;
13 mod post_report;
14 mod private_message;
15 mod site;
16 mod websocket;
17
18 #[async_trait::async_trait(?Send)]
19 pub trait Perform {
20   type Response: serde::ser::Serialize + Send;
21
22   async fn perform(
23     &self,
24     context: &Data<LemmyContext>,
25     websocket_id: Option<ConnectionId>,
26   ) -> Result<Self::Response, LemmyError>;
27 }
28
29 pub async fn match_websocket_operation(
30   context: LemmyContext,
31   id: ConnectionId,
32   op: UserOperation,
33   data: &str,
34 ) -> Result<String, LemmyError> {
35   match op {
36     // User ops
37     UserOperation::Login => do_websocket_operation::<Login>(context, id, op, data).await,
38     UserOperation::GetCaptcha => do_websocket_operation::<GetCaptcha>(context, id, op, data).await,
39     UserOperation::GetReplies => do_websocket_operation::<GetReplies>(context, id, op, data).await,
40     UserOperation::AddAdmin => do_websocket_operation::<AddAdmin>(context, id, op, data).await,
41     UserOperation::BanPerson => do_websocket_operation::<BanPerson>(context, id, op, data).await,
42     UserOperation::GetPersonMentions => {
43       do_websocket_operation::<GetPersonMentions>(context, id, op, data).await
44     }
45     UserOperation::MarkPersonMentionAsRead => {
46       do_websocket_operation::<MarkPersonMentionAsRead>(context, id, op, data).await
47     }
48     UserOperation::MarkAllAsRead => {
49       do_websocket_operation::<MarkAllAsRead>(context, id, op, data).await
50     }
51     UserOperation::PasswordReset => {
52       do_websocket_operation::<PasswordReset>(context, id, op, data).await
53     }
54     UserOperation::PasswordChange => {
55       do_websocket_operation::<PasswordChange>(context, id, op, data).await
56     }
57     UserOperation::UserJoin => do_websocket_operation::<UserJoin>(context, id, op, data).await,
58     UserOperation::PostJoin => do_websocket_operation::<PostJoin>(context, id, op, data).await,
59     UserOperation::CommunityJoin => {
60       do_websocket_operation::<CommunityJoin>(context, id, op, data).await
61     }
62     UserOperation::ModJoin => do_websocket_operation::<ModJoin>(context, id, op, data).await,
63     UserOperation::SaveUserSettings => {
64       do_websocket_operation::<SaveUserSettings>(context, id, op, data).await
65     }
66     UserOperation::ChangePassword => {
67       do_websocket_operation::<ChangePassword>(context, id, op, data).await
68     }
69     UserOperation::GetReportCount => {
70       do_websocket_operation::<GetReportCount>(context, id, op, data).await
71     }
72
73     // Private Message ops
74     UserOperation::MarkPrivateMessageAsRead => {
75       do_websocket_operation::<MarkPrivateMessageAsRead>(context, id, op, data).await
76     }
77
78     // Site ops
79     UserOperation::GetModlog => do_websocket_operation::<GetModlog>(context, id, op, data).await,
80     UserOperation::GetSiteConfig => {
81       do_websocket_operation::<GetSiteConfig>(context, id, op, data).await
82     }
83     UserOperation::SaveSiteConfig => {
84       do_websocket_operation::<SaveSiteConfig>(context, id, op, data).await
85     }
86     UserOperation::Search => do_websocket_operation::<Search>(context, id, op, data).await,
87     UserOperation::TransferCommunity => {
88       do_websocket_operation::<TransferCommunity>(context, id, op, data).await
89     }
90     UserOperation::TransferSite => {
91       do_websocket_operation::<TransferSite>(context, id, op, data).await
92     }
93
94     // Community ops
95     UserOperation::FollowCommunity => {
96       do_websocket_operation::<FollowCommunity>(context, id, op, data).await
97     }
98     UserOperation::GetFollowedCommunities => {
99       do_websocket_operation::<GetFollowedCommunities>(context, id, op, data).await
100     }
101     UserOperation::BanFromCommunity => {
102       do_websocket_operation::<BanFromCommunity>(context, id, op, data).await
103     }
104     UserOperation::AddModToCommunity => {
105       do_websocket_operation::<AddModToCommunity>(context, id, op, data).await
106     }
107
108     // Post ops
109     UserOperation::LockPost => do_websocket_operation::<LockPost>(context, id, op, data).await,
110     UserOperation::StickyPost => do_websocket_operation::<StickyPost>(context, id, op, data).await,
111     UserOperation::CreatePostLike => {
112       do_websocket_operation::<CreatePostLike>(context, id, op, data).await
113     }
114     UserOperation::SavePost => do_websocket_operation::<SavePost>(context, id, op, data).await,
115     UserOperation::CreatePostReport => {
116       do_websocket_operation::<CreatePostReport>(context, id, op, data).await
117     }
118     UserOperation::ListPostReports => {
119       do_websocket_operation::<ListPostReports>(context, id, op, data).await
120     }
121     UserOperation::ResolvePostReport => {
122       do_websocket_operation::<ResolvePostReport>(context, id, op, data).await
123     }
124
125     // Comment ops
126     UserOperation::MarkCommentAsRead => {
127       do_websocket_operation::<MarkCommentAsRead>(context, id, op, data).await
128     }
129     UserOperation::SaveComment => {
130       do_websocket_operation::<SaveComment>(context, id, op, data).await
131     }
132     UserOperation::CreateCommentLike => {
133       do_websocket_operation::<CreateCommentLike>(context, id, op, data).await
134     }
135     UserOperation::CreateCommentReport => {
136       do_websocket_operation::<CreateCommentReport>(context, id, op, data).await
137     }
138     UserOperation::ListCommentReports => {
139       do_websocket_operation::<ListCommentReports>(context, id, op, data).await
140     }
141     UserOperation::ResolveCommentReport => {
142       do_websocket_operation::<ResolveCommentReport>(context, id, op, data).await
143     }
144   }
145 }
146
147 async fn do_websocket_operation<'a, 'b, Data>(
148   context: LemmyContext,
149   id: ConnectionId,
150   op: UserOperation,
151   data: &str,
152 ) -> Result<String, LemmyError>
153 where
154   for<'de> Data: Deserialize<'de> + 'a,
155   Data: Perform,
156 {
157   let parsed_data: Data = serde_json::from_str(&data)?;
158   let res = parsed_data
159     .perform(&web::Data::new(context), Some(id))
160     .await?;
161   serialize_websocket_message(&op, &res)
162 }
163
164 pub(crate) fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
165   let mut built_text = String::new();
166
167   // Building proper speech text for espeak
168   for mut c in captcha.chars() {
169     let new_str = if c.is_alphabetic() {
170       if c.is_lowercase() {
171         c.make_ascii_uppercase();
172         format!("lower case {} ... ", c)
173       } else {
174         c.make_ascii_uppercase();
175         format!("capital {} ... ", c)
176       }
177     } else {
178       format!("{} ...", c)
179     };
180
181     built_text.push_str(&new_str);
182   }
183
184   espeak_wav_base64(&built_text)
185 }
186
187 pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
188   // Make a temp file path
189   let uuid = uuid::Uuid::new_v4().to_string();
190   let file_path = format!(
191     "{}/lemmy_espeak_{}.wav",
192     env::temp_dir().to_string_lossy(),
193     &uuid
194   );
195
196   // Write the wav file
197   Command::new("espeak")
198     .arg("-w")
199     .arg(&file_path)
200     .arg(text)
201     .status()?;
202
203   // Read the wav file bytes
204   let bytes = std::fs::read(&file_path)?;
205
206   // Delete the file
207   std::fs::remove_file(file_path)?;
208
209   // Convert to base64
210   let base64 = base64::encode(bytes);
211
212   Ok(base64)
213 }
214
215 #[cfg(test)]
216 mod tests {
217   use crate::captcha_espeak_wav_base64;
218   use lemmy_api_common::check_validator_time;
219   use lemmy_db_queries::{establish_unpooled_connection, source::local_user::LocalUser_, Crud};
220   use lemmy_db_schema::source::{
221     local_user::{LocalUser, LocalUserForm},
222     person::{Person, PersonForm},
223   };
224   use lemmy_utils::claims::Claims;
225
226   #[test]
227   fn test_should_not_validate_user_token_after_password_change() {
228     let conn = establish_unpooled_connection();
229
230     let new_person = PersonForm {
231       name: "Gerry9812".into(),
232       ..PersonForm::default()
233     };
234
235     let inserted_person = Person::create(&conn, &new_person).unwrap();
236
237     let local_user_form = LocalUserForm {
238       person_id: inserted_person.id,
239       password_encrypted: "123456".to_string(),
240       ..LocalUserForm::default()
241     };
242
243     let inserted_local_user = LocalUser::create(&conn, &local_user_form).unwrap();
244
245     let jwt = Claims::jwt(inserted_local_user.id.0).unwrap();
246     let claims = Claims::decode(&jwt).unwrap().claims;
247     let check = check_validator_time(&inserted_local_user.validator_time, &claims);
248     assert!(check.is_ok());
249
250     // The check should fail, since the validator time is now newer than the jwt issue time
251     let updated_local_user =
252       LocalUser::update_password(&conn, inserted_local_user.id, &"password111").unwrap();
253     let check_after = check_validator_time(&updated_local_user.validator_time, &claims);
254     assert!(check_after.is_err());
255
256     let num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
257     assert_eq!(1, num_deleted);
258   }
259
260   #[test]
261   fn test_espeak() {
262     assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
263   }
264 }