1use std::time::{SystemTime, UNIX_EPOCH};
4
5use lazy_static::lazy_static;
6use regex::{Regex, RegexBuilder};
7use reqwest::{
8 Client,
9 header::{
10 HeaderMap, HeaderName, HeaderValue, USER_AGENT,
11 },
12};
13
14use crate::{
15 enums::{Theme, Answer, Language},
16 error::{
17 Result,
18 Error,
19 UpdateInfoError,
20 },
21};
22
23pub mod models;
24pub mod error;
25pub mod enums;
26
27
28lazy_static! {
29 static ref HEADERS: HeaderMap<HeaderValue> = {
30 let mut headers = HeaderMap::new();
31
32 headers.insert(
33 USER_AGENT,
34 HeaderValue::from_static(
35 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) snap Chromium/81.0.4044.92 Chrome/81.0.4044.92 Safari/537.36"
36 ),
37 );
38 headers.insert(
39 HeaderName::from_static("x-requested-with"),
40 HeaderValue::from_static(
41 "XMLHttpRequest"
42 ),
43 );
44 headers
45 };
46}
47
48macro_rules! get_field {
51 ( $field:expr ) => {
52 $field.as_ref()
53 .ok_or(Error::NoDataFound)?
54 .to_string()
55 }
56}
57
58
59#[derive(Debug, Clone)]
61pub struct Akinator {
62 pub language: Language,
64 pub theme: Theme,
68 pub child_mode: bool,
70
71 http_client: Client,
73 timestamp: u64,
76 uri: String,
79 uid: Option<String>,
81 ws_url: Option<String>,
83 session: Option<usize>,
85 frontaddr: Option<String>,
87 signature: Option<usize>,
89 question_filter: Option<String>,
90
91 pub current_question: Option<String>,
93 pub progression: f32,
96 pub step: usize,
99
100 pub first_guess: Option<models::Guess>,
104 pub guesses: Vec<models::Guess>,
108}
109
110impl Akinator {
111 pub fn new() -> Result<Self> {
117 Ok(Self {
118 language: Language::default(),
119 theme: Theme::default(),
120 child_mode: false,
121
122 http_client: Client::builder()
123 .danger_accept_invalid_certs(true)
124 .build()?,
125 timestamp: 0,
126 uri: "https://en.akinator.com".to_string(),
127 uid: None,
128 ws_url: None,
129 session: None,
130 frontaddr: None,
131 signature: None,
132 question_filter: None,
133
134 current_question: None,
135 progression: 0.0,
136 step: 0,
137
138 first_guess: None,
139 guesses: Vec::new(),
140 })
141 }
142
143 #[must_use]
145 pub const fn with_theme(mut self, theme: Theme) -> Self {
146 self.theme = theme;
147 self
148 }
149
150 #[must_use]
152 pub const fn with_language(mut self, language: Language) -> Self {
153 self.language = language;
154 self
155 }
156
157 #[must_use]
159 pub const fn with_child_mode(mut self) -> Self {
160 self.child_mode = true;
161 self
162 }
163
164 #[must_use]
167 #[allow(clippy::needless_pass_by_value)]
168 fn handle_error_response(completion: String) -> Error {
169 match completion.to_uppercase().as_str() {
170 "KO - SERVER DOWN" => Error::ServersDown,
171 "KO - TECHNICAL ERROR" => Error::TechnicalError,
172 "KO - TIMEOUT" => Error::TimeoutError,
173 "KO - ELEM LIST IS EMPTY" | "WARN - NO QUESTION" => Error::NoMoreQuestions,
174 _ => Error::ConnectionError,
175 }
176 }
177
178 async fn find_server(&self) -> Result<String> {
180 lazy_static! {
181 static ref DATA_REGEX: Regex = RegexBuilder::new(
182 r#"\[\{"translated_theme_name":".*","urlWs":"https:\\/\\/srv[0-9]+\.akinator\.com:[0-9]+\\/ws","subject_id":"[0-9]+"\}\]"#
183 )
184 .case_insensitive(true)
185 .multi_line(true)
186 .build()
187 .unwrap();
188 }
189
190 let html = self.http_client.get(&self.uri)
191 .send()
192 .await?
193 .text()
194 .await?;
195
196 let id = (self.theme as usize)
197 .to_string();
198
199 if let Some(mat) = DATA_REGEX.find(html.as_str()) {
200 let json: Vec<models::ServerData> =
201 serde_json::from_str(mat.as_str())?;
202
203 let mat = json
204 .into_iter()
205 .find(|entry| entry.subject_id == id)
206 .ok_or(Error::NoDataFound)?;
207
208 Ok(mat.url_ws)
209 } else {
210 Err(Error::NoDataFound)
211 }
212 }
213
214 async fn find_session_info(&self) -> Result<(String, String)> {
218 lazy_static! {
219 static ref VARS_REGEX: Regex =
220 RegexBuilder::new(r"var uid_ext_session = '(.*)';\n.*var frontaddr = '(.*)';")
221 .case_insensitive(true)
222 .multi_line(true)
223 .build()
224 .unwrap();
225 }
226
227 let html = self.http_client
228 .get("https://en.akinator.com/game")
229 .send()
230 .await?
231 .text()
232 .await?;
233
234 if let Some(mat) = VARS_REGEX.captures(html.as_str()) {
235 let result = (
236 mat.get(1).ok_or(Error::NoDataFound)?
237 .as_str().to_string(),
238 mat.get(2).ok_or(Error::NoDataFound)?
239 .as_str().to_string(),
240 );
241
242 Ok(result)
243 } else {
244 Err(Error::NoDataFound)
245 }
246 }
247
248 #[must_use]
252 #[allow(clippy::needless_pass_by_value)]
253 fn parse_response(html: String) -> String {
254 lazy_static! {
255 static ref RESPONSE_REGEX: Regex =
256 RegexBuilder::new(r"^jQuery\d+_\d+\(")
257 .case_insensitive(true)
258 .multi_line(true)
259 .build()
260 .unwrap();
261 }
262
263 RESPONSE_REGEX
264 .replace(html.as_str(), "")
265 .strip_suffix(')')
266 .unwrap_or(html.as_str())
267 .to_string()
268 }
269
270 fn update_move_info(&mut self, json: models::MoveJson) -> Result<(), UpdateInfoError> {
272 let params = json.parameters
273 .ok_or(UpdateInfoError::MissingData)?;
274
275 self.current_question = Some(
276 params.question
277 );
278
279 self.progression = params.progression
280 .parse::<f32>()?;
281
282 self.step = params.step
283 .parse::<usize>()?;
284
285 Ok(())
286 }
287
288 fn update_start_info(&mut self, json: &models::StartJson) -> Result<(), UpdateInfoError> {
290 let ident = &json.parameters
291 .as_ref()
292 .ok_or(UpdateInfoError::MissingData)?
293 .identification;
294
295 let step_info = &json.parameters
296 .as_ref()
297 .ok_or(UpdateInfoError::MissingData)?
298 .step_information;
299
300 self.session = Some(
301 ident.session
302 .parse::<usize>()?
303 );
304
305 self.signature = Some(
306 ident.signature
307 .parse::<usize>()?
308 );
309
310 self.current_question = Some(
311 step_info.question.clone()
312 );
313
314 self.progression = step_info.progression
315 .parse::<f32>()?;
316
317 self.step = step_info.step
318 .parse::<usize>()?;
319
320 Ok(())
321 }
322
323 pub async fn start(&mut self) -> Result<Option<String>> {
329 self.uri = format!("https://{}.akinator.com", self.language);
330 self.ws_url = Some(self.find_server().await?);
331
332 let (uid, frontaddr) = self.find_session_info().await?;
333 self.uid = Some(uid);
334 self.frontaddr = Some(frontaddr);
335
336 self.timestamp = SystemTime::now()
337 .duration_since(UNIX_EPOCH)?
338 .as_secs();
339
340 let soft_constraint =
341 if self.child_mode {
342 "ETAT='EN'"
343 } else {
344 ""
345 }
346 .to_string();
347
348 self.question_filter = Some(
349 if self.child_mode {
350 "cat=1"
351 } else {
352 ""
353 }
354 .to_string()
355 );
356
357 let params = [
358 (
359 "callback",
360 format!("jQuery331023608747682107778_{}", self.timestamp),
361 ),
362 ("urlApiWs", get_field!(self.ws_url)),
363 ("partner", 1.to_string()),
364 ("childMod", self.child_mode.to_string()),
365 ("player", "website-desktop".to_string()),
366 ("uid_ext_session", get_field!(self.uid)),
367 ("frontaddr", get_field!(self.frontaddr)),
368 ("constraint", "ETAT<>'AV'".to_string()),
369 ("soft_constraint", soft_constraint),
370 (
371 "question_filter",
372 get_field!(self.question_filter),
373 ),
374 ];
375
376 let response = self.http_client
377 .get(format!("{}/new_session", &self.uri))
378 .headers(HEADERS.clone())
379 .query(¶ms)
380 .send()
381 .await?;
382
383 let json_string = Self::parse_response(response.text().await?);
384 let json: models::StartJson =
385 serde_json::from_str(json_string.as_str())?;
386
387 if json.completion.as_str() == "OK" {
388 self.update_start_info(&json)?;
389
390 Ok(self.current_question.clone())
391 } else {
392 Err(Self::handle_error_response(json.completion))
393 }
394 }
395
396 pub async fn answer(&mut self, answer: Answer) -> Result<Option<String>> {
402 let params = [
403 (
404 "callback",
405 format!("jQuery331023608747682107778_{}", self.timestamp),
406 ),
407 ("urlApiWs", get_field!(self.ws_url)),
408 ("childMod", self.child_mode.to_string()),
409 ("session", get_field!(self.session)),
410 ("signature", get_field!(self.signature)),
411 ("frontaddr", get_field!(self.frontaddr)),
412 ("step", self.step.to_string()),
413 ("answer", (answer as u8).to_string()),
414 (
415 "question_filter",
416 get_field!(self.question_filter),
417 ),
418 ];
419
420 let response = self.http_client
421 .get(format!("{}/answer_api", &self.uri))
422 .headers(HEADERS.clone())
423 .query(¶ms)
424 .send()
425 .await?
426 .text()
427 .await?;
428
429 let json_string = Self::parse_response(response);
430 let json: models::MoveJson =
431 serde_json::from_str(json_string.as_str())?;
432
433 if json.completion.as_str() == "OK" {
434 self.update_move_info(json)?;
435
436 Ok(self.current_question.clone())
437 } else {
438 Err(Self::handle_error_response(json.completion))
439 }
440 }
441
442 pub async fn win(&mut self) -> Result<Option<models::Guess>> {
449 let params = [
450 (
451 "callback",
452 format!("jQuery331023608747682107778_{}", self.timestamp),
453 ),
454 ("childMod", self.child_mode.to_string()),
455 ("session", get_field!(self.session)),
456 ("signature", get_field!(self.signature)),
457 ("step", self.step.to_string()),
458 ];
459
460 let response = self.http_client
461 .get(format!("{}/list", get_field!(self.ws_url)))
462 .headers(HEADERS.clone())
463 .query(¶ms)
464 .send()
465 .await?
466 .text()
467 .await?;
468
469 let json_string = Self::parse_response(response);
470 let json: models::WinJson =
471 serde_json::from_str(json_string.as_str())?;
472
473 if json.completion.as_str() == "OK" {
474 let elements = json.parameters
475 .ok_or(UpdateInfoError::MissingData)?
476 .elements;
477
478 self.guesses = elements
479 .into_iter()
480 .map(|e| e.element)
481 .collect::<Vec<models::Guess>>();
482
483 self.first_guess = self.guesses
484 .first()
485 .cloned();
486
487 Ok(self.first_guess.clone())
488 } else {
489 Err(Self::handle_error_response(json.completion))
490 }
491 }
492
493 pub async fn back(&mut self) -> Result<Option<String>> {
500 if self.step == 0 {
501 return Err(Error::CantGoBackAnyFurther);
502 }
503
504 let params = [
505 (
506 "callback",
507 format!("jQuery331023608747682107778_{}", self.timestamp),
508 ),
509 ("childMod", self.child_mode.to_string()),
510 ("session", get_field!(self.session)),
511 ("signature", get_field!(self.signature)),
512 ("step", self.step.to_string()),
513 ("answer", "-1".to_string()),
514 (
515 "question_filter",
516 get_field!(self.question_filter)
517 ),
518 ];
519
520 let response = self.http_client
521 .get(format!("{}/cancel_answer", get_field!(self.ws_url)))
522 .headers(HEADERS.clone())
523 .query(¶ms)
524 .send()
525 .await?
526 .text()
527 .await?;
528
529 let json_string = Self::parse_response(response);
530 let json: models::MoveJson =
531 serde_json::from_str(json_string.as_str())?;
532
533 if json.completion.as_str() == "OK" {
534 self.update_move_info(json)?;
535
536 Ok(self.current_question.clone())
537 } else {
538 Err(Self::handle_error_response(json.completion))
539 }
540 }
541}