mirror of
https://github.com/flibusta-apps/telegram_files_cache_server.git
synced 2026-03-03 23:20:48 +01:00
Rewrite to rust
This commit is contained in:
BIN
src/services/.DS_Store
vendored
Normal file
BIN
src/services/.DS_Store
vendored
Normal file
Binary file not shown.
49
src/services/book_library/mod.rs
Normal file
49
src/services/book_library/mod.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
pub mod types;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::config::CONFIG;
|
||||
|
||||
async fn _make_request<T>(
|
||||
url: &str,
|
||||
params: Vec<(&str, String)>,
|
||||
) -> Result<T, Box<dyn std::error::Error + Send + Sync>>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let formated_url = format!("{}{}", CONFIG.library_url, url);
|
||||
|
||||
let response = client
|
||||
.get(formated_url)
|
||||
.query(¶ms)
|
||||
.header("Authorization", CONFIG.library_api_key.clone())
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let response = match response {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Err(Box::new(err)),
|
||||
};
|
||||
|
||||
let response = match response.error_for_status() {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Err(Box::new(err)),
|
||||
};
|
||||
|
||||
match response.json::<T>().await {
|
||||
Ok(v) => Ok(v),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_sources() -> Result<types::Source, Box<dyn std::error::Error + Send + Sync>> {
|
||||
_make_request("/api/v1/sources", vec![]).await
|
||||
}
|
||||
|
||||
pub async fn get_book(
|
||||
book_id: i32,
|
||||
) -> Result<types::BookWithRemote, Box<dyn std::error::Error + Send + Sync>> {
|
||||
_make_request(format!("/api/v1/books/{book_id}").as_str(), vec![]).await
|
||||
}
|
||||
108
src/services/book_library/types.rs
Normal file
108
src/services/book_library/types.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Source {
|
||||
// id: u32,
|
||||
// name: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct BookAuthor {
|
||||
pub id: u32,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub middle_name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Book {
|
||||
pub id: u32,
|
||||
pub title: String,
|
||||
pub lang: String,
|
||||
pub file_type: String,
|
||||
pub uploaded: String,
|
||||
pub authors: Vec<BookAuthor>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct BookWithRemote {
|
||||
pub id: u32,
|
||||
pub remote_id: u32,
|
||||
pub title: String,
|
||||
pub lang: String,
|
||||
pub file_type: String,
|
||||
pub uploaded: String,
|
||||
pub authors: Vec<BookAuthor>,
|
||||
}
|
||||
|
||||
impl BookWithRemote {
|
||||
pub fn from_book(book: Book, remote_id: u32) -> Self {
|
||||
Self {
|
||||
id: book.id,
|
||||
remote_id,
|
||||
title: book.title,
|
||||
lang: book.lang,
|
||||
file_type: book.file_type,
|
||||
uploaded: book.uploaded,
|
||||
authors: book.authors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl BookAuthor {
|
||||
pub fn get_caption(self) -> String {
|
||||
let mut parts: Vec<String> = vec![];
|
||||
|
||||
if !self.last_name.is_empty() {
|
||||
parts.push(self.last_name);
|
||||
}
|
||||
|
||||
if !self.first_name.is_empty() {
|
||||
parts.push(self.first_name);
|
||||
}
|
||||
|
||||
if !self.middle_name.is_empty() {
|
||||
parts.push(self.middle_name);
|
||||
}
|
||||
|
||||
let joined_parts = parts.join(" ");
|
||||
|
||||
format!("👤 {joined_parts}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl BookWithRemote {
|
||||
pub fn get_caption(self) -> String {
|
||||
let BookWithRemote {
|
||||
title,
|
||||
authors,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let caption_title = format!("📖 {title}");
|
||||
|
||||
let author_captions: Vec<String> = authors
|
||||
.into_iter()
|
||||
.map(|a| a.get_caption())
|
||||
.collect();
|
||||
|
||||
let mut author_parts: Vec<String> = vec![];
|
||||
let mut author_parts_len = 3;
|
||||
|
||||
for author_caption in author_captions {
|
||||
if caption_title.len() + author_parts_len + author_caption.len() + 1 <= 1024 {
|
||||
author_parts_len = author_caption.len() + 1;
|
||||
author_parts.push(author_caption);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let caption_authors = author_parts.join("\n");
|
||||
|
||||
format!("{caption_title}\n\n{caption_authors}")
|
||||
}
|
||||
}
|
||||
19
src/services/download_utils.rs
Normal file
19
src/services/download_utils.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use futures::TryStreamExt;
|
||||
use reqwest::Response;
|
||||
use tokio::io::AsyncRead;
|
||||
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||
|
||||
|
||||
pub struct DownloadResult {
|
||||
pub response: Response,
|
||||
pub filename: String,
|
||||
pub filename_ascii: String,
|
||||
pub caption: String,
|
||||
}
|
||||
|
||||
pub fn get_response_async_read(it: Response) -> impl AsyncRead {
|
||||
it.bytes_stream()
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
.into_async_read()
|
||||
.compat()
|
||||
}
|
||||
57
src/services/downloader/mod.rs
Normal file
57
src/services/downloader/mod.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use reqwest::Response;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::CONFIG;
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct FilenameData {
|
||||
pub filename: String,
|
||||
pub filename_ascii: String
|
||||
}
|
||||
|
||||
|
||||
pub async fn download_from_downloader(
|
||||
remote_id: u32,
|
||||
object_id: i32,
|
||||
object_type: String
|
||||
) -> Result<Response, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let url = format!(
|
||||
"{}/download/{remote_id}/{object_id}/{object_type}",
|
||||
CONFIG.downloader_url
|
||||
);
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.get(url)
|
||||
.header("Authorization", &CONFIG.downloader_api_key)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
|
||||
pub async fn get_filename(
|
||||
object_id: i32,
|
||||
object_type: String
|
||||
) -> Result<FilenameData, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let url = format!(
|
||||
"{}/filename/{object_id}/{object_type}",
|
||||
CONFIG.downloader_url
|
||||
);
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.get(url)
|
||||
.header("Authorization", &CONFIG.downloader_api_key)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
match response.json::<FilenameData>().await {
|
||||
Ok(v) => Ok(v),
|
||||
Err(err) => {
|
||||
Err(Box::new(err))
|
||||
},
|
||||
}
|
||||
}
|
||||
124
src/services/mod.rs
Normal file
124
src/services/mod.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
pub mod book_library;
|
||||
pub mod download_utils;
|
||||
pub mod telegram_files;
|
||||
pub mod downloader;
|
||||
|
||||
use tracing::log;
|
||||
|
||||
use crate::{prisma::cached_file, views::Database};
|
||||
|
||||
use self::{download_utils::DownloadResult, telegram_files::{download_from_telegram_files, UploadData, upload_to_telegram_files}, downloader::{get_filename, FilenameData, download_from_downloader}, book_library::get_book};
|
||||
|
||||
|
||||
pub async fn get_cached_file_or_cache(
|
||||
object_id: i32,
|
||||
object_type: String,
|
||||
db: Database
|
||||
) -> Option<cached_file::Data> {
|
||||
let cached_file = db.cached_file()
|
||||
.find_unique(cached_file::object_id_object_type(object_id, object_type.clone()))
|
||||
.exec()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match cached_file {
|
||||
Some(cached_file) => Some(cached_file),
|
||||
None => cache_file(object_id, object_type, db).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn cache_file(
|
||||
object_id: i32,
|
||||
object_type: String,
|
||||
db: Database
|
||||
) -> Option<cached_file::Data> {
|
||||
let book = match get_book(object_id).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
log::error!("{:?}", err);
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
let downloader_result = match download_from_downloader(
|
||||
book.remote_id,
|
||||
object_id,
|
||||
object_type.clone()
|
||||
).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
log::error!("{:?}", err);
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
let UploadData { chat_id, message_id } = match upload_to_telegram_files(
|
||||
downloader_result,
|
||||
book.get_caption()
|
||||
).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
log::error!("{:?}", err);
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
Some(
|
||||
db
|
||||
.cached_file()
|
||||
.create(
|
||||
object_id,
|
||||
object_type,
|
||||
message_id,
|
||||
chat_id,
|
||||
vec![]
|
||||
)
|
||||
.exec()
|
||||
.await
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
pub async fn download_from_cache(
|
||||
cached_data: cached_file::Data,
|
||||
) -> Option<DownloadResult> {
|
||||
let response_task = tokio::task::spawn(download_from_telegram_files(cached_data.message_id, cached_data.chat_id));
|
||||
let filename_task = tokio::task::spawn(get_filename(cached_data.object_id, cached_data.object_type.clone()));
|
||||
let book_task = tokio::task::spawn(get_book(cached_data.object_id));
|
||||
|
||||
let response = match response_task.await.unwrap() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
log::error!("{:?}", err);
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
let filename_data = match filename_task.await.unwrap() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
log::error!("{:?}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let book = match book_task.await.unwrap() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
log::error!("{:?}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let FilenameData {filename, filename_ascii} = filename_data;
|
||||
let caption = book.get_caption();
|
||||
|
||||
Some(DownloadResult {
|
||||
response,
|
||||
filename,
|
||||
filename_ascii,
|
||||
caption
|
||||
})
|
||||
}
|
||||
87
src/services/telegram_files/mod.rs
Normal file
87
src/services/telegram_files/mod.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use reqwest::{Response, multipart::{Form, Part}, header};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::CONFIG;
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UploadData {
|
||||
pub chat_id: i64,
|
||||
pub message_id: i64
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UploadResult {
|
||||
pub backend: String,
|
||||
pub data: UploadData
|
||||
}
|
||||
|
||||
|
||||
pub async fn download_from_telegram_files(
|
||||
message_id: i64,
|
||||
chat_id: i64
|
||||
) -> Result<Response, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let url = format!(
|
||||
"{}/api/v1/files/download_by_message/{chat_id}/{message_id}",
|
||||
CONFIG.files_url
|
||||
);
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.get(url)
|
||||
.header("Authorization", CONFIG.library_api_key.clone())
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
|
||||
pub async fn upload_to_telegram_files(
|
||||
data_response: Response,
|
||||
caption: String
|
||||
) -> Result<UploadData, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let url = format!(
|
||||
"{}/api/v1/files/upload/",
|
||||
CONFIG.files_url
|
||||
);
|
||||
|
||||
let headers = data_response.headers();
|
||||
|
||||
let file_size = headers
|
||||
.get(header::CONTENT_LENGTH)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let filename = headers
|
||||
.get("x-filename-b64-ascii")
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let part = Part::stream(data_response)
|
||||
.file_name(filename);
|
||||
|
||||
let form = Form::new()
|
||||
.text("caption", caption)
|
||||
.text("file_size", file_size)
|
||||
.part("file", part);
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(url)
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
match response.json::<UploadResult>().await {
|
||||
Ok(v) => Ok(v.data),
|
||||
Err(err) => {
|
||||
Err(Box::new(err))
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user