feat!: add telegram auth and async refactor

- Removed "/schedule/update-download-url" endpoint, this mechanism was replaced by Yandex Cloud FaaS. Ура :)
- Improved schedule caching mechanism.
- Added Telegram WebApp authentication support.
- Reworked endpoints responses and errors mechanism.
- Refactored application state management.
- Make synchronous database operations, middlewares and extractors to asynchronous.
- Made user password field optional to support multiple auth methods.
- Renamed users table column "version" to "android_version" and made it nullable.
This commit is contained in:
2025-06-08 01:29:21 +04:00
parent 6a106a366c
commit e64011ba16
66 changed files with 1842 additions and 1243 deletions

View File

@@ -1,14 +1,12 @@
use crate::xls_downloader::interface::{FetchError, FetchOk, FetchResult, XLSDownloader};
use chrono::{DateTime, Utc};
use std::env;
use std::sync::Arc;
pub struct BasicXlsDownloader {
pub url: Option<String>,
user_agent: String,
}
async fn fetch_specified(url: &String, user_agent: &String, head: bool) -> FetchResult {
async fn fetch_specified(url: &str, head: bool) -> FetchResult {
let client = reqwest::Client::new();
let response = if head {
@@ -16,59 +14,47 @@ async fn fetch_specified(url: &String, user_agent: &String, head: bool) -> Fetch
} else {
client.get(url)
}
.header("User-Agent", user_agent.clone())
.header("User-Agent", ua_generator::ua::spoof_chrome_ua())
.send()
.await;
.await
.map_err(|e| FetchError::unknown(Arc::new(e)))?;
match response {
Ok(r) => {
if r.status().as_u16() != 200 {
return Err(FetchError::BadStatusCode(r.status().as_u16()));
}
let headers = r.headers();
let content_type = headers.get("Content-Type");
let etag = headers.get("etag");
let last_modified = headers.get("last-modified");
let date = headers.get("date");
if content_type.is_none() {
Err(FetchError::BadHeaders("Content-Type".to_string()))
} else if etag.is_none() {
Err(FetchError::BadHeaders("ETag".to_string()))
} else if last_modified.is_none() {
Err(FetchError::BadHeaders("Last-Modified".to_string()))
} else if date.is_none() {
Err(FetchError::BadHeaders("Date".to_string()))
} else if content_type.unwrap() != "application/vnd.ms-excel" {
Err(FetchError::BadContentType(
content_type.unwrap().to_str().unwrap().to_string(),
))
} else {
let etag = etag.unwrap().to_str().unwrap().to_string();
let last_modified =
DateTime::parse_from_rfc2822(&last_modified.unwrap().to_str().unwrap())
.unwrap()
.with_timezone(&Utc);
Ok(if head {
FetchOk::head(etag, last_modified)
} else {
FetchOk::get(etag, last_modified, r.bytes().await.unwrap().to_vec())
})
}
}
Err(error) => Err(FetchError::Unknown(Arc::new(error))),
if response.status().as_u16() != 200 {
return Err(FetchError::bad_status_code(response.status().as_u16()));
}
let headers = response.headers();
let content_type = headers
.get("Content-Type")
.ok_or(FetchError::bad_headers("Content-Type"))?;
if !headers.contains_key("etag") {
return Err(FetchError::bad_headers("etag"));
}
let last_modified = headers
.get("last-modified")
.ok_or(FetchError::bad_headers("last-modified"))?;
if content_type != "application/vnd.ms-excel" {
return Err(FetchError::bad_content_type(content_type.to_str().unwrap()));
}
let last_modified = DateTime::parse_from_rfc2822(&last_modified.to_str().unwrap())
.unwrap()
.with_timezone(&Utc);
Ok(if head {
FetchOk::head(last_modified)
} else {
FetchOk::get(last_modified, response.bytes().await.unwrap().to_vec())
})
}
impl BasicXlsDownloader {
pub fn new() -> Self {
BasicXlsDownloader {
url: None,
user_agent: env::var("REQWEST_USER_AGENT").expect("USER_AGENT must be set"),
}
BasicXlsDownloader { url: None }
}
}
@@ -77,15 +63,15 @@ impl XLSDownloader for BasicXlsDownloader {
if self.url.is_none() {
Err(FetchError::NoUrlProvided)
} else {
fetch_specified(self.url.as_ref().unwrap(), &self.user_agent, head).await
fetch_specified(&*self.url.as_ref().unwrap(), head).await
}
}
async fn set_url(&mut self, url: String) -> FetchResult {
let result = fetch_specified(&url, &self.user_agent, true).await;
async fn set_url(&mut self, url: &str) -> FetchResult {
let result = fetch_specified(url, true).await;
if let Ok(_) = result {
self.url = Some(url);
self.url = Some(url.to_string());
}
result
@@ -94,17 +80,16 @@ impl XLSDownloader for BasicXlsDownloader {
#[cfg(test)]
mod tests {
use crate::xls_downloader::basic_impl::{fetch_specified, BasicXlsDownloader};
use crate::xls_downloader::basic_impl::{BasicXlsDownloader, fetch_specified};
use crate::xls_downloader::interface::{FetchError, XLSDownloader};
#[tokio::test]
async fn bad_url() {
let url = "bad_url".to_string();
let user_agent = String::new();
let url = "bad_url";
let results = [
fetch_specified(&url, &user_agent, true).await,
fetch_specified(&url, &user_agent, false).await,
fetch_specified(url, true).await,
fetch_specified(url, false).await,
];
assert!(results[0].is_err());
@@ -113,18 +98,17 @@ mod tests {
#[tokio::test]
async fn bad_status_code() {
let url = "https://www.google.com/not-found".to_string();
let user_agent = String::new();
let url = "https://www.google.com/not-found";
let results = [
fetch_specified(&url, &user_agent, true).await,
fetch_specified(&url, &user_agent, false).await,
fetch_specified(url, true).await,
fetch_specified(url, false).await,
];
assert!(results[0].is_err());
assert!(results[1].is_err());
let expected_error = FetchError::BadStatusCode(404);
let expected_error = FetchError::BadStatusCode { status_code: 404 };
assert_eq!(*results[0].as_ref().err().unwrap(), expected_error);
assert_eq!(*results[1].as_ref().err().unwrap(), expected_error);
@@ -132,18 +116,19 @@ mod tests {
#[tokio::test]
async fn bad_headers() {
let url = "https://www.google.com/favicon.ico".to_string();
let user_agent = String::new();
let url = "https://www.google.com/favicon.ico";
let results = [
fetch_specified(&url, &user_agent, true).await,
fetch_specified(&url, &user_agent, false).await,
fetch_specified(url, true).await,
fetch_specified(url, false).await,
];
assert!(results[0].is_err());
assert!(results[1].is_err());
let expected_error = FetchError::BadHeaders("ETag".to_string());
let expected_error = FetchError::BadHeaders {
expected_header: "ETag".to_string(),
};
assert_eq!(*results[0].as_ref().err().unwrap(), expected_error);
assert_eq!(*results[1].as_ref().err().unwrap(), expected_error);
@@ -151,12 +136,11 @@ mod tests {
#[tokio::test]
async fn bad_content_type() {
let url = "https://s3.aero-storage.ldragol.ru/679e5d1145a6ad00843ad3f1/67ddb59fd46303008396ac96%2Fexample.txt".to_string();
let user_agent = String::new();
let url = "https://s3.aero-storage.ldragol.ru/679e5d1145a6ad00843ad3f1/67ddb59fd46303008396ac96%2Fexample.txt";
let results = [
fetch_specified(&url, &user_agent, true).await,
fetch_specified(&url, &user_agent, false).await,
fetch_specified(url, true).await,
fetch_specified(url, false).await,
];
assert!(results[0].is_err());
@@ -165,12 +149,11 @@ mod tests {
#[tokio::test]
async fn ok() {
let url = "https://s3.aero-storage.ldragol.ru/679e5d1145a6ad00843ad3f1/67ddb5fad46303008396ac97%2Fschedule.xls".to_string();
let user_agent = String::new();
let url = "https://s3.aero-storage.ldragol.ru/679e5d1145a6ad00843ad3f1/67ddb5fad46303008396ac97%2Fschedule.xls";
let results = [
fetch_specified(&url, &user_agent, true).await,
fetch_specified(&url, &user_agent, false).await,
fetch_specified(url, true).await,
fetch_specified(url, false).await,
];
assert!(results[0].is_ok());
@@ -179,7 +162,7 @@ mod tests {
#[tokio::test]
async fn downloader_set_ok() {
let url = "https://s3.aero-storage.ldragol.ru/679e5d1145a6ad00843ad3f1/67ddb5fad46303008396ac97%2Fschedule.xls".to_string();
let url = "https://s3.aero-storage.ldragol.ru/679e5d1145a6ad00843ad3f1/67ddb5fad46303008396ac97%2Fschedule.xls";
let mut downloader = BasicXlsDownloader::new();
@@ -188,7 +171,7 @@ mod tests {
#[tokio::test]
async fn downloader_set_err() {
let url = "bad_url".to_string();
let url = "bad_url";
let mut downloader = BasicXlsDownloader::new();
@@ -197,7 +180,7 @@ mod tests {
#[tokio::test]
async fn downloader_ok() {
let url = "https://s3.aero-storage.ldragol.ru/679e5d1145a6ad00843ad3f1/67ddb5fad46303008396ac97%2Fschedule.xls".to_string();
let url = "https://s3.aero-storage.ldragol.ru/679e5d1145a6ad00843ad3f1/67ddb5fad46303008396ac97%2Fschedule.xls";
let mut downloader = BasicXlsDownloader::new();