mirror of
https://github.com/flibusta-apps/book_bot.git
synced 2025-12-10 10:20:24 +01:00
Add rust implementation
This commit is contained in:
47
src/bots/approved_bot/mod.rs
Normal file
47
src/bots/approved_bot/mod.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
pub mod modules;
|
||||
pub mod services;
|
||||
mod tools;
|
||||
|
||||
use teloxide::{prelude::*, types::BotCommand};
|
||||
|
||||
use self::modules::{
|
||||
annotations::get_annotations_handler, book::get_book_handler, download::get_download_hander,
|
||||
help::get_help_handler, random::get_random_hander, search::get_search_hanlder,
|
||||
settings::get_settings_handler, support::get_support_handler,
|
||||
update_history::get_update_log_handler,
|
||||
};
|
||||
|
||||
use super::{BotCommands, BotHandler};
|
||||
|
||||
pub fn get_approved_handler() -> (BotHandler, BotCommands) {
|
||||
(
|
||||
dptree::entry()
|
||||
.branch(get_help_handler())
|
||||
.branch(get_settings_handler())
|
||||
.branch(get_support_handler())
|
||||
.branch(get_random_hander())
|
||||
.branch(get_download_hander())
|
||||
.branch(get_annotations_handler())
|
||||
.branch(get_book_handler())
|
||||
.branch(get_update_log_handler())
|
||||
.branch(get_search_hanlder()),
|
||||
Some(vec![
|
||||
BotCommand {
|
||||
command: String::from("random"),
|
||||
description: String::from("Попытать удачу"),
|
||||
},
|
||||
BotCommand {
|
||||
command: String::from("update_log"),
|
||||
description: String::from("Обновления каталога"),
|
||||
},
|
||||
BotCommand {
|
||||
command: String::from("settings"),
|
||||
description: String::from("Настройки"),
|
||||
},
|
||||
BotCommand {
|
||||
command: String::from("support"),
|
||||
description: String::from("Поддержать разработчика"),
|
||||
},
|
||||
]),
|
||||
)
|
||||
}
|
||||
354
src/bots/approved_bot/modules/annotations.rs
Normal file
354
src/bots/approved_bot/modules/annotations.rs
Normal file
@@ -0,0 +1,354 @@
|
||||
use std::{convert::TryInto, str::FromStr};
|
||||
|
||||
use futures::TryStreamExt;
|
||||
use regex::Regex;
|
||||
use teloxide::{dispatching::UpdateFilterExt, dptree, prelude::*, types::*};
|
||||
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||
|
||||
use crate::bots::{
|
||||
approved_bot::{
|
||||
modules::utils::generic_get_pagination_keyboard,
|
||||
services::book_library::{
|
||||
get_author_annotation, get_book_annotation,
|
||||
types::{AuthorAnnotation, BookAnnotation},
|
||||
},
|
||||
tools::filter_callback_query,
|
||||
},
|
||||
BotHandlerInternal,
|
||||
};
|
||||
|
||||
use super::utils::{filter_command, CommandParse, GetPaginationCallbackData};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AnnotationCommand {
|
||||
Book { id: u32 },
|
||||
Author { id: u32 },
|
||||
}
|
||||
|
||||
impl CommandParse<Self> for AnnotationCommand {
|
||||
fn parse(s: &str, bot_name: &str) -> Result<Self, strum::ParseError> {
|
||||
let re = Regex::new(r"^/(?P<an_type>a|b)_an_(?P<id>\d+)$").unwrap();
|
||||
|
||||
let full_bot_name = format!("@{bot_name}");
|
||||
let after_replace = s.replace(&full_bot_name, "");
|
||||
|
||||
let caps = re.captures(&after_replace);
|
||||
let caps = match caps {
|
||||
Some(v) => v,
|
||||
None => return Err(strum::ParseError::VariantNotFound),
|
||||
};
|
||||
|
||||
let annotation_type = &caps["an_type"];
|
||||
let id: u32 = caps["id"].parse().unwrap();
|
||||
|
||||
match annotation_type {
|
||||
"a" => Ok(AnnotationCommand::Author { id }),
|
||||
"b" => Ok(AnnotationCommand::Book { id }),
|
||||
_ => Err(strum::ParseError::VariantNotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AnnotationCallbackData {
|
||||
Book { id: u32, page: u32 },
|
||||
Author { id: u32, page: u32 },
|
||||
}
|
||||
|
||||
impl FromStr for AnnotationCallbackData {
|
||||
type Err = strum::ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let re = Regex::new(r"^(?P<an_type>a|b)_an_(?P<id>\d+)_(?P<page>\d+)$").unwrap();
|
||||
|
||||
let caps = re.captures(s);
|
||||
let caps = match caps {
|
||||
Some(v) => v,
|
||||
None => return Err(strum::ParseError::VariantNotFound),
|
||||
};
|
||||
|
||||
let annotation_type = &caps["an_type"];
|
||||
let id = caps["id"].parse::<u32>().unwrap();
|
||||
let page = caps["page"].parse::<u32>().unwrap();
|
||||
|
||||
match annotation_type {
|
||||
"a" => Ok(AnnotationCallbackData::Author { id, page }),
|
||||
"b" => Ok(AnnotationCallbackData::Book { id, page }),
|
||||
_ => Err(strum::ParseError::VariantNotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for AnnotationCallbackData {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
AnnotationCallbackData::Book { id, page } => format!("b_an_{id}_{page}"),
|
||||
AnnotationCallbackData::Author { id, page } => format!("a_an_{id}_{page}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AnnotationFormat {
|
||||
fn get_file(&self) -> Option<&String>;
|
||||
fn get_text(&self) -> &str;
|
||||
|
||||
fn is_normal_text(&self) -> bool;
|
||||
}
|
||||
|
||||
impl AnnotationFormat for BookAnnotation {
|
||||
fn get_file(&self) -> Option<&String> {
|
||||
self.file.as_ref()
|
||||
}
|
||||
|
||||
fn get_text(&self) -> &str {
|
||||
self.text.as_str()
|
||||
}
|
||||
|
||||
fn is_normal_text(&self) -> bool {
|
||||
self.text.replace('\n', "").replace(' ', "").len() != 0
|
||||
}
|
||||
}
|
||||
|
||||
impl GetPaginationCallbackData for AnnotationCallbackData {
|
||||
fn get_pagination_callback_data(&self, target_page: u32) -> String {
|
||||
match self {
|
||||
AnnotationCallbackData::Book { id, .. } => AnnotationCallbackData::Book {
|
||||
id: id.clone(),
|
||||
page: target_page,
|
||||
},
|
||||
AnnotationCallbackData::Author { id, .. } => AnnotationCallbackData::Author {
|
||||
id: id.clone(),
|
||||
page: target_page,
|
||||
},
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnnotationFormat for AuthorAnnotation {
|
||||
fn get_file(&self) -> Option<&String> {
|
||||
self.file.as_ref()
|
||||
}
|
||||
|
||||
fn get_text(&self) -> &str {
|
||||
self.text.as_str()
|
||||
}
|
||||
|
||||
fn is_normal_text(&self) -> bool {
|
||||
self.text.replace('\n', "").replace(' ', "").len() != 0
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_image(
|
||||
file: &String,
|
||||
) -> Result<reqwest::Response, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let response = reqwest::get(file).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)),
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn send_annotation_handler<T, Fut>(
|
||||
message: Message,
|
||||
bot: AutoSend<Bot>,
|
||||
command: AnnotationCommand,
|
||||
annotation_getter: fn(id: u32) -> Fut,
|
||||
) -> BotHandlerInternal
|
||||
where
|
||||
T: AnnotationFormat,
|
||||
Fut: std::future::Future<Output = Result<T, Box<dyn std::error::Error + Send + Sync>>>,
|
||||
{
|
||||
let id = match command {
|
||||
AnnotationCommand::Book { id } => id,
|
||||
AnnotationCommand::Author { id } => id,
|
||||
};
|
||||
|
||||
let annotation = match annotation_getter(id).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
if annotation.get_file().is_none() && !annotation.is_normal_text() {
|
||||
return match bot
|
||||
.send_message(message.chat.id, "Аннотация недоступна :(")
|
||||
.reply_to_message_id(message.id)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
};
|
||||
};
|
||||
|
||||
if let Some(file) = annotation.get_file() {
|
||||
let image_response = download_image(file).await;
|
||||
|
||||
if let Ok(v) = image_response {
|
||||
let data = v
|
||||
.bytes_stream()
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
.into_async_read()
|
||||
.compat();
|
||||
|
||||
log::info!("{}", file);
|
||||
|
||||
match bot
|
||||
.send_photo(message.chat.id, InputFile::read(data))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) => log::info!("{}", err),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !annotation.is_normal_text() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let chunked_text: Vec<String> = textwrap::wrap(annotation.get_text(), 512)
|
||||
.into_iter()
|
||||
.filter(|text| text.replace('\r', "").len() != 0)
|
||||
.map(|text| text.to_string())
|
||||
.collect();
|
||||
let current_text = chunked_text.get(0).unwrap();
|
||||
|
||||
let callback_data = match command {
|
||||
AnnotationCommand::Book { id } => AnnotationCallbackData::Book { id, page: 1 },
|
||||
AnnotationCommand::Author { id } => AnnotationCallbackData::Author { id, page: 1 },
|
||||
};
|
||||
let keyboard = generic_get_pagination_keyboard(
|
||||
1,
|
||||
chunked_text.len().try_into().unwrap(),
|
||||
callback_data,
|
||||
false,
|
||||
);
|
||||
|
||||
match bot
|
||||
.send_message(message.chat.id, current_text)
|
||||
.reply_markup(keyboard)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn annotation_pagination_handler<T, Fut>(
|
||||
cq: CallbackQuery,
|
||||
bot: AutoSend<Bot>,
|
||||
callback_data: AnnotationCallbackData,
|
||||
annotation_getter: fn(id: u32) -> Fut,
|
||||
) -> BotHandlerInternal
|
||||
where
|
||||
T: AnnotationFormat,
|
||||
Fut: std::future::Future<Output = Result<T, Box<dyn std::error::Error + Send + Sync>>>,
|
||||
{
|
||||
let (id, page) = match callback_data {
|
||||
AnnotationCallbackData::Book { id, page } => (id, page),
|
||||
AnnotationCallbackData::Author { id, page } => (id, page),
|
||||
};
|
||||
|
||||
let annotation = match annotation_getter(id).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let message = match cq.message {
|
||||
Some(v) => v,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let page_index: usize = page.try_into().unwrap();
|
||||
let chunked_text: Vec<String> = textwrap::wrap(annotation.get_text(), 512)
|
||||
.into_iter()
|
||||
.filter(|text| text.replace('\r', "").len() != 0)
|
||||
.map(|text| text.to_string())
|
||||
.collect();
|
||||
let current_text = chunked_text.get(page_index - 1).unwrap();
|
||||
|
||||
let keyboard = generic_get_pagination_keyboard(
|
||||
page,
|
||||
chunked_text.len().try_into().unwrap(),
|
||||
callback_data,
|
||||
false,
|
||||
);
|
||||
|
||||
match bot
|
||||
.edit_message_text(message.chat.id, message.id, current_text)
|
||||
.reply_markup(keyboard)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_annotations_handler() -> crate::bots::BotHandler {
|
||||
dptree::entry()
|
||||
.branch(
|
||||
Update::filter_message()
|
||||
.chain(filter_command::<AnnotationCommand>())
|
||||
.endpoint(
|
||||
|message: Message, bot: AutoSend<Bot>, command: AnnotationCommand| async move {
|
||||
match command {
|
||||
AnnotationCommand::Book { .. } => {
|
||||
send_annotation_handler(message, bot, command, get_book_annotation)
|
||||
.await
|
||||
}
|
||||
AnnotationCommand::Author { .. } => {
|
||||
send_annotation_handler(
|
||||
message,
|
||||
bot,
|
||||
command,
|
||||
get_author_annotation,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
.branch(
|
||||
Update::filter_callback_query()
|
||||
.chain(filter_callback_query::<AnnotationCallbackData>())
|
||||
.endpoint(
|
||||
|cq: CallbackQuery,
|
||||
bot: AutoSend<Bot>,
|
||||
callback_data: AnnotationCallbackData| async move {
|
||||
match callback_data {
|
||||
AnnotationCallbackData::Book { .. } => {
|
||||
annotation_pagination_handler(
|
||||
cq,
|
||||
bot,
|
||||
callback_data,
|
||||
get_book_annotation,
|
||||
)
|
||||
.await
|
||||
}
|
||||
AnnotationCallbackData::Author { .. } => {
|
||||
annotation_pagination_handler(
|
||||
cq,
|
||||
bot,
|
||||
callback_data,
|
||||
get_author_annotation,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
346
src/bots/approved_bot/modules/book.rs
Normal file
346
src/bots/approved_bot/modules/book.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use regex::Regex;
|
||||
use teloxide::{dispatching::UpdateFilterExt, dptree, prelude::*};
|
||||
|
||||
use crate::bots::approved_bot::{
|
||||
services::{
|
||||
book_library::{
|
||||
formaters::Format, get_author_books, get_sequence_books, get_translator_books,
|
||||
types::Page,
|
||||
},
|
||||
user_settings::get_user_or_default_lang_codes,
|
||||
},
|
||||
tools::filter_callback_query,
|
||||
};
|
||||
|
||||
use super::utils::{
|
||||
filter_command, generic_get_pagination_keyboard, CommandParse, GetPaginationCallbackData,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum BookCommand {
|
||||
Author { id: u32 },
|
||||
Translator { id: u32 },
|
||||
Sequence { id: u32 },
|
||||
}
|
||||
|
||||
impl CommandParse<Self> for BookCommand {
|
||||
fn parse(s: &str, bot_name: &str) -> Result<Self, strum::ParseError> {
|
||||
let re = Regex::new(r"^/(?P<an_type>a|t|s)_(?P<id>\d+)$").unwrap();
|
||||
|
||||
let full_bot_name = format!("@{bot_name}");
|
||||
let after_replace = s.replace(&full_bot_name, "");
|
||||
|
||||
let caps = re.captures(&after_replace);
|
||||
let caps = match caps {
|
||||
Some(v) => v,
|
||||
None => return Err(strum::ParseError::VariantNotFound),
|
||||
};
|
||||
|
||||
let annotation_type = &caps["an_type"];
|
||||
let id: u32 = caps["id"].parse().unwrap();
|
||||
|
||||
match annotation_type {
|
||||
"a" => Ok(BookCommand::Author { id }),
|
||||
"t" => Ok(BookCommand::Translator { id }),
|
||||
"s" => Ok(BookCommand::Sequence { id }),
|
||||
_ => Err(strum::ParseError::VariantNotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum BookCallbackData {
|
||||
Author { id: u32, page: u32 },
|
||||
Translator { id: u32, page: u32 },
|
||||
Sequence { id: u32, page: u32 },
|
||||
}
|
||||
|
||||
impl FromStr for BookCallbackData {
|
||||
type Err = strum::ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let re = Regex::new(r"^b(?P<an_type>a|t|s)_(?P<id>\d+)_(?P<page>\d+)$").unwrap();
|
||||
|
||||
let caps = re.captures(s);
|
||||
let caps = match caps {
|
||||
Some(v) => v,
|
||||
None => return Err(strum::ParseError::VariantNotFound),
|
||||
};
|
||||
|
||||
let annotation_type = &caps["an_type"];
|
||||
let id = caps["id"].parse::<u32>().unwrap();
|
||||
let page = caps["page"].parse::<u32>().unwrap();
|
||||
|
||||
match annotation_type {
|
||||
"a" => Ok(BookCallbackData::Author { id, page }),
|
||||
"t" => Ok(BookCallbackData::Translator { id, page }),
|
||||
"s" => Ok(BookCallbackData::Sequence { id, page }),
|
||||
_ => Err(strum::ParseError::VariantNotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for BookCallbackData {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
BookCallbackData::Author { id, page } => format!("ba_{id}_{page}"),
|
||||
BookCallbackData::Translator { id, page } => format!("bt_{id}_{page}"),
|
||||
BookCallbackData::Sequence { id, page } => format!("bs_{id}_{page}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GetPaginationCallbackData for BookCallbackData {
|
||||
fn get_pagination_callback_data(&self, target_page: u32) -> String {
|
||||
match self {
|
||||
BookCallbackData::Author { id, .. } => BookCallbackData::Author {
|
||||
id: id.clone(),
|
||||
page: target_page,
|
||||
},
|
||||
BookCallbackData::Translator { id, .. } => BookCallbackData::Translator {
|
||||
id: id.clone(),
|
||||
page: target_page,
|
||||
},
|
||||
BookCallbackData::Sequence { id, .. } => BookCallbackData::Sequence {
|
||||
id: id.clone(),
|
||||
page: target_page,
|
||||
},
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_book_handler<T, Fut>(
|
||||
message: Message,
|
||||
bot: AutoSend<Bot>,
|
||||
command: BookCommand,
|
||||
books_getter: fn(id: u32, page: u32, allowed_langs: Vec<String>) -> Fut,
|
||||
) -> crate::bots::BotHandlerInternal
|
||||
where
|
||||
T: Format + Clone,
|
||||
Fut: std::future::Future<Output = Result<Page<T>, Box<dyn std::error::Error + Send + Sync>>>,
|
||||
{
|
||||
let id = match command {
|
||||
BookCommand::Author { id } => id,
|
||||
BookCommand::Translator { id } => id,
|
||||
BookCommand::Sequence { id } => id,
|
||||
};
|
||||
|
||||
let chat_id = message.chat.id;
|
||||
let user_id = message.from().map(|from| from.id);
|
||||
|
||||
let user_id = match user_id {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return match bot
|
||||
.send_message(chat_id, "Повторите запрос сначала")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let allowed_langs = get_user_or_default_lang_codes(user_id).await;
|
||||
|
||||
let items_page = match books_getter(id, 1, allowed_langs.clone()).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
match bot
|
||||
.send_message(chat_id, "Ошибка! Попробуйте позже :(")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) => log::error!("{:?}", err),
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
if items_page.total_pages == 0 {
|
||||
match bot.send_message(chat_id, "Книги не найдены!").send().await {
|
||||
Ok(_) => (),
|
||||
Err(err) => return Err(Box::new(err)),
|
||||
};
|
||||
};
|
||||
|
||||
let formated_items = items_page.format_items();
|
||||
let total_pages = items_page.total_pages;
|
||||
|
||||
let footer = format!("\n\nСтраница 1/{total_pages}");
|
||||
let message_text = format!("{formated_items}{footer}");
|
||||
|
||||
let callback_data = match command {
|
||||
BookCommand::Author { id } => BookCallbackData::Author { id, page: 1 },
|
||||
BookCommand::Translator { id } => BookCallbackData::Translator { id, page: 1 },
|
||||
BookCommand::Sequence { id } => BookCallbackData::Sequence { id, page: 1 },
|
||||
};
|
||||
|
||||
let keyboard = generic_get_pagination_keyboard(1, total_pages, callback_data, true);
|
||||
|
||||
match bot
|
||||
.send_message(chat_id, message_text)
|
||||
.reply_markup(keyboard)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_pagination_book_handler<T, Fut>(
|
||||
cq: CallbackQuery,
|
||||
bot: AutoSend<Bot>,
|
||||
callback_data: BookCallbackData,
|
||||
books_getter: fn(id: u32, page: u32, allowed_langs: Vec<String>) -> Fut,
|
||||
) -> crate::bots::BotHandlerInternal
|
||||
where
|
||||
T: Format + Clone,
|
||||
Fut: std::future::Future<Output = Result<Page<T>, Box<dyn std::error::Error + Send + Sync>>>,
|
||||
{
|
||||
let (id, page) = match callback_data {
|
||||
BookCallbackData::Author { id, page } => (id, page),
|
||||
BookCallbackData::Translator { id, page } => (id, page),
|
||||
BookCallbackData::Sequence { id, page } => (id, page),
|
||||
};
|
||||
|
||||
let chat_id = cq.message.as_ref().map(|message| message.chat.id);
|
||||
let user_id = cq
|
||||
.message
|
||||
.as_ref()
|
||||
.map(|message| message.from().map(|from| from.id))
|
||||
.unwrap_or(None);
|
||||
let message_id = cq.message.as_ref().map(|message| message.id);
|
||||
|
||||
let (chat_id, user_id, message_id) = match (chat_id, user_id, message_id) {
|
||||
(Some(chat_id), Some(user_id), Some(message_id)) => (chat_id, user_id, message_id),
|
||||
_ => {
|
||||
return match chat_id {
|
||||
Some(v) => match bot.send_message(v, "Повторите поиск сначала").send().await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
},
|
||||
None => return Ok(()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let allowed_langs = get_user_or_default_lang_codes(user_id).await;
|
||||
|
||||
let mut items_page = match books_getter(id, page, allowed_langs.clone()).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
match bot
|
||||
.send_message(chat_id, "Ошибка! Попробуйте позже :(")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) => log::error!("{:?}", err),
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
if items_page.total_pages == 0 {
|
||||
match bot.send_message(chat_id, "Книги не найдены!").send().await {
|
||||
Ok(_) => (),
|
||||
Err(err) => return Err(Box::new(err)),
|
||||
};
|
||||
};
|
||||
|
||||
if page > items_page.total_pages {
|
||||
items_page = match books_getter(id, items_page.total_pages, allowed_langs.clone()).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
match bot
|
||||
.send_message(chat_id, "Ошибка! Попробуйте позже :(")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) => log::error!("{:?}", err),
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let formated_items = items_page.format_items();
|
||||
|
||||
let total_pages = items_page.total_pages;
|
||||
|
||||
let footer = format!("\n\nСтраница {page}/{total_pages}");
|
||||
let message_text = format!("{formated_items}{footer}");
|
||||
|
||||
let keyboard = generic_get_pagination_keyboard(page, total_pages, callback_data, true);
|
||||
|
||||
match bot
|
||||
.edit_message_text(chat_id, message_id, message_text)
|
||||
.reply_markup(keyboard)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_book_handler() -> crate::bots::BotHandler {
|
||||
dptree::entry()
|
||||
.branch(
|
||||
Update::filter_message()
|
||||
.chain(filter_command::<BookCommand>())
|
||||
.endpoint(
|
||||
|message: Message, bot: AutoSend<Bot>, command: BookCommand| async move {
|
||||
match command {
|
||||
BookCommand::Author { .. } => {
|
||||
send_book_handler(
|
||||
message,
|
||||
bot,
|
||||
command,
|
||||
get_author_books,
|
||||
)
|
||||
.await
|
||||
}
|
||||
BookCommand::Translator { .. } => {
|
||||
send_book_handler(
|
||||
message,
|
||||
bot,
|
||||
command,
|
||||
get_translator_books,
|
||||
)
|
||||
.await
|
||||
}
|
||||
BookCommand::Sequence { .. } => {
|
||||
send_book_handler(
|
||||
message,
|
||||
bot,
|
||||
command,
|
||||
get_sequence_books,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
.branch(
|
||||
Update::filter_callback_query()
|
||||
.chain(filter_callback_query::<BookCallbackData>())
|
||||
.endpoint(|cq: CallbackQuery, bot: AutoSend<Bot>, callback_data: BookCallbackData| async move {
|
||||
match callback_data {
|
||||
BookCallbackData::Author { .. } => send_pagination_book_handler(cq, bot, callback_data, get_author_books).await,
|
||||
BookCallbackData::Translator { .. } => send_pagination_book_handler(cq, bot, callback_data, get_translator_books).await,
|
||||
BookCallbackData::Sequence { .. } => send_pagination_book_handler(cq, bot, callback_data, get_sequence_books).await,
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
153
src/bots/approved_bot/modules/download.rs
Normal file
153
src/bots/approved_bot/modules/download.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use futures::TryStreamExt;
|
||||
use regex::Regex;
|
||||
use teloxide::{dispatching::UpdateFilterExt, dptree, prelude::*, types::*};
|
||||
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||
|
||||
use crate::{
|
||||
bots::{
|
||||
approved_bot::services::book_cache::{
|
||||
clear_book_cache, download_file, get_cached_message,
|
||||
types::{CachedMessage, DownloadFile},
|
||||
},
|
||||
BotHandlerInternal,
|
||||
},
|
||||
bots_manager::BotCache,
|
||||
};
|
||||
|
||||
use super::utils::{filter_command, CommandParse};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DownloadData {
|
||||
pub format: String,
|
||||
pub id: u32,
|
||||
}
|
||||
|
||||
impl CommandParse<Self> for DownloadData {
|
||||
fn parse(s: &str, bot_name: &str) -> Result<Self, strum::ParseError> {
|
||||
let re = Regex::new(r"^/d_(?P<file_format>[a-zA-Z0-9]+)_(?P<book_id>\d+)$").unwrap();
|
||||
|
||||
let full_bot_name = format!("@{bot_name}");
|
||||
let after_replace = s.replace(&full_bot_name, "");
|
||||
|
||||
let caps = re.captures(&after_replace);
|
||||
let caps = match caps {
|
||||
Some(v) => v,
|
||||
None => return Err(strum::ParseError::VariantNotFound),
|
||||
};
|
||||
|
||||
let file_format = &caps["file_format"];
|
||||
let book_id: u32 = caps["book_id"].parse().unwrap();
|
||||
|
||||
Ok(DownloadData {
|
||||
format: file_format.to_string(),
|
||||
id: book_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn _send_cached(
|
||||
message: Message,
|
||||
bot: AutoSend<Bot>,
|
||||
cached_message: CachedMessage,
|
||||
) -> BotHandlerInternal {
|
||||
match bot
|
||||
.copy_message(
|
||||
message.chat.id,
|
||||
Recipient::Id(ChatId(cached_message.chat_id)),
|
||||
cached_message.message_id,
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => todo!(),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_cached_message(
|
||||
message: Message,
|
||||
bot: AutoSend<Bot>,
|
||||
download_data: DownloadData,
|
||||
) -> BotHandlerInternal {
|
||||
let cached_message = get_cached_message(&download_data).await;
|
||||
match cached_message {
|
||||
Ok(v) => match _send_cached(message.clone(), bot.clone(), v).await {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(err) => log::info!("{:?}", err),
|
||||
},
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
match clear_book_cache(&download_data).await {
|
||||
Ok(_) => (),
|
||||
Err(err) => log::error!("{:?}", err),
|
||||
};
|
||||
|
||||
let cached_message = get_cached_message(&download_data).await;
|
||||
match cached_message {
|
||||
Ok(v) => _send_cached(message, bot, v).await,
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_with_download_from_channel(
|
||||
message: Message,
|
||||
bot: AutoSend<Bot>,
|
||||
download_data: DownloadData,
|
||||
) -> BotHandlerInternal {
|
||||
let downloaded_file = match download_file(&download_data).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let DownloadFile {
|
||||
response,
|
||||
filename,
|
||||
caption,
|
||||
} = downloaded_file;
|
||||
|
||||
let data = response
|
||||
.bytes_stream()
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
.into_async_read()
|
||||
.compat();
|
||||
|
||||
let document: InputFile = InputFile::read(data).file_name(filename);
|
||||
|
||||
match bot
|
||||
.send_document(message.chat.id, document)
|
||||
.caption(caption)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_handler(
|
||||
message: Message,
|
||||
bot: AutoSend<Bot>,
|
||||
cache: BotCache,
|
||||
download_data: DownloadData,
|
||||
) -> BotHandlerInternal {
|
||||
match cache {
|
||||
BotCache::Original => send_cached_message(message, bot, download_data).await,
|
||||
BotCache::NoCache => send_with_download_from_channel(message, bot, download_data).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_download_hander() -> crate::bots::BotHandler {
|
||||
dptree::entry().branch(
|
||||
Update::filter_message()
|
||||
.chain(filter_command::<DownloadData>())
|
||||
.endpoint(
|
||||
|message: Message,
|
||||
bot: AutoSend<Bot>,
|
||||
cache: BotCache,
|
||||
download_data: DownloadData| async move {
|
||||
download_handler(message, bot, cache, download_data).await
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
45
src/bots/approved_bot/modules/help.rs
Normal file
45
src/bots/approved_bot/modules/help.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use crate::bots::BotHandlerInternal;
|
||||
|
||||
use teloxide::{prelude::*, utils::command::BotCommands};
|
||||
|
||||
#[derive(BotCommands, Clone)]
|
||||
#[command(rename = "lowercase")]
|
||||
enum HelpCommand {
|
||||
Start,
|
||||
Help,
|
||||
}
|
||||
|
||||
pub async fn help_handler(message: Message, bot: AutoSend<Bot>) -> BotHandlerInternal {
|
||||
let name = message
|
||||
.from()
|
||||
.map(|user| user.first_name.clone())
|
||||
.unwrap_or("пользователь".to_string());
|
||||
|
||||
match bot
|
||||
.send_message(
|
||||
message.chat.id,
|
||||
format!(
|
||||
"
|
||||
Привет, {name}! \n
|
||||
Этот бот поможет тебе загружать книги.\n
|
||||
Настройки языков для поиска /settings.\n
|
||||
"
|
||||
),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_help_handler() -> crate::bots::BotHandler {
|
||||
dptree::entry().branch(
|
||||
Update::filter_message().branch(
|
||||
dptree::entry()
|
||||
.filter_command::<HelpCommand>()
|
||||
.endpoint(|message, bot| async move { help_handler(message, bot).await }),
|
||||
),
|
||||
)
|
||||
}
|
||||
10
src/bots/approved_bot/modules/mod.rs
Normal file
10
src/bots/approved_bot/modules/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
pub mod annotations;
|
||||
pub mod book;
|
||||
pub mod download;
|
||||
pub mod help;
|
||||
pub mod random;
|
||||
pub mod search;
|
||||
pub mod settings;
|
||||
pub mod support;
|
||||
pub mod update_history;
|
||||
pub mod utils;
|
||||
364
src/bots/approved_bot/modules/random.rs
Normal file
364
src/bots/approved_bot/modules/random.rs
Normal file
@@ -0,0 +1,364 @@
|
||||
use strum_macros::{Display, EnumIter};
|
||||
use teloxide::{
|
||||
prelude::*,
|
||||
types::{InlineKeyboardButton, InlineKeyboardMarkup},
|
||||
utils::command::BotCommands,
|
||||
};
|
||||
|
||||
use crate::bots::{
|
||||
approved_bot::{
|
||||
services::{
|
||||
book_library::{self, formaters::Format},
|
||||
user_settings::get_user_or_default_lang_codes,
|
||||
},
|
||||
tools::filter_callback_query,
|
||||
},
|
||||
BotHandlerInternal,
|
||||
};
|
||||
|
||||
#[derive(BotCommands, Clone)]
|
||||
#[command(rename = "lowercase")]
|
||||
enum RandomCommand {
|
||||
Random,
|
||||
}
|
||||
|
||||
#[derive(Clone, Display, EnumIter)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
enum RandomCallbackData {
|
||||
RandomBook,
|
||||
RandomAuthor,
|
||||
RandomSequence,
|
||||
RandomBookByGenreRequest,
|
||||
Genres { index: u32 },
|
||||
RandomBookByGenre { id: u32 },
|
||||
}
|
||||
|
||||
impl std::str::FromStr for RandomCallbackData {
|
||||
type Err = strum::ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let value = s.to_string();
|
||||
|
||||
for callback_data in <RandomCallbackData as strum::IntoEnumIterator>::iter() {
|
||||
match callback_data {
|
||||
RandomCallbackData::Genres { .. }
|
||||
| RandomCallbackData::RandomBookByGenre { .. } => {
|
||||
let callback_prefix = callback_data.to_string();
|
||||
|
||||
if value.starts_with(&callback_prefix) {
|
||||
let data: u32 = value
|
||||
.strip_prefix(&format!("{}_", &callback_prefix).to_string())
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
match callback_data {
|
||||
RandomCallbackData::Genres { .. } => {
|
||||
return Ok(RandomCallbackData::Genres { index: data })
|
||||
}
|
||||
RandomCallbackData::RandomBookByGenre { .. } => {
|
||||
return Ok(RandomCallbackData::RandomBookByGenre { id: data })
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if value == callback_data.to_string() {
|
||||
return Ok(callback_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Err(strum::ParseError::VariantNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
async fn random_handler(message: Message, bot: AutoSend<Bot>) -> crate::bots::BotHandlerInternal {
|
||||
const MESSAGE_TEXT: &str = "Что хотим получить?";
|
||||
|
||||
let keyboard = InlineKeyboardMarkup {
|
||||
inline_keyboard: vec![
|
||||
vec![InlineKeyboardButton {
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
RandomCallbackData::RandomBook.to_string(),
|
||||
),
|
||||
text: String::from("Книгу"),
|
||||
}],
|
||||
vec![InlineKeyboardButton {
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
RandomCallbackData::RandomBookByGenreRequest.to_string(),
|
||||
),
|
||||
text: String::from("Книгу по жанру"),
|
||||
}],
|
||||
vec![InlineKeyboardButton {
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
RandomCallbackData::RandomAuthor.to_string(),
|
||||
),
|
||||
text: String::from("Автора"),
|
||||
}],
|
||||
vec![InlineKeyboardButton {
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
RandomCallbackData::RandomSequence.to_string(),
|
||||
),
|
||||
text: String::from("Серию"),
|
||||
}],
|
||||
],
|
||||
};
|
||||
|
||||
let res = bot
|
||||
.send_message(message.chat.id, MESSAGE_TEXT)
|
||||
.reply_to_message_id(message.id)
|
||||
.reply_markup(keyboard)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_random_item_handler_internal<T>(
|
||||
cq: CallbackQuery,
|
||||
bot: AutoSend<Bot>,
|
||||
item: Result<T, Box<dyn std::error::Error + Send + Sync>>,
|
||||
) -> BotHandlerInternal
|
||||
where
|
||||
T: Format,
|
||||
{
|
||||
match item {
|
||||
Ok(item) => {
|
||||
let item_message = item.format();
|
||||
|
||||
let send_item_handler = tokio::spawn(
|
||||
bot.send_message(cq.from.id, item_message)
|
||||
.reply_markup(InlineKeyboardMarkup {
|
||||
inline_keyboard: vec![vec![InlineKeyboardButton {
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
cq.data.unwrap(),
|
||||
),
|
||||
text: String::from("Повторить?"),
|
||||
}]],
|
||||
})
|
||||
.send(),
|
||||
);
|
||||
|
||||
cq.message.map(|message| async move {
|
||||
bot.edit_message_reply_markup(message.chat.id, message.id)
|
||||
.reply_markup(InlineKeyboardMarkup {
|
||||
inline_keyboard: vec![],
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
});
|
||||
|
||||
match send_item_handler.await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
match bot
|
||||
.send_message(cq.from.id, "Ошибка! Попробуйте позже :(")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(int_error) => return Err(Box::new(int_error)),
|
||||
}
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_random_item_handler<T, Fut>(
|
||||
cq: CallbackQuery,
|
||||
bot: AutoSend<Bot>,
|
||||
item_getter: fn(allowed_langs: Vec<String>) -> Fut,
|
||||
) -> BotHandlerInternal
|
||||
where
|
||||
T: Format,
|
||||
Fut: std::future::Future<Output = Result<T, Box<dyn std::error::Error + Send + Sync>>>,
|
||||
{
|
||||
let allowed_langs = get_user_or_default_lang_codes(cq.from.id).await;
|
||||
|
||||
let item = item_getter(allowed_langs).await;
|
||||
|
||||
get_random_item_handler_internal(cq, bot, item).await
|
||||
}
|
||||
|
||||
async fn get_genre_metas_handler(cq: CallbackQuery, bot: AutoSend<Bot>) -> BotHandlerInternal {
|
||||
let genre_metas = match book_library::get_genre_metas().await {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
match cq.message {
|
||||
Some(message) => {
|
||||
let keyboard = InlineKeyboardMarkup {
|
||||
inline_keyboard: genre_metas
|
||||
.clone()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, genre_meta)| {
|
||||
vec![InlineKeyboardButton {
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(format!(
|
||||
"{}_{index}",
|
||||
RandomCallbackData::Genres {
|
||||
index: index as u32
|
||||
}
|
||||
.to_string()
|
||||
)),
|
||||
text: genre_meta,
|
||||
}]
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
match bot
|
||||
.edit_message_reply_markup(message.chat.id, message.id)
|
||||
.reply_markup(keyboard)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
match bot
|
||||
.send_message(cq.from.id, "Ошибка! Начните заново :(")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_genres_by_meta_handler(
|
||||
cq: CallbackQuery,
|
||||
bot: AutoSend<Bot>,
|
||||
genre_index: u32,
|
||||
) -> BotHandlerInternal {
|
||||
let genre_metas = match book_library::get_genre_metas().await {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let meta = match genre_metas.get(genre_index as usize) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return match bot
|
||||
.send_message(cq.from.id, "Ошибка! Попробуйте позже :(")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let genres = match book_library::get_genres(meta.to_string()).await {
|
||||
Ok(v) => v.items,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let mut buttons: Vec<Vec<InlineKeyboardButton>> = genres
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|genre| {
|
||||
vec![InlineKeyboardButton {
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(format!(
|
||||
"{}_{}",
|
||||
RandomCallbackData::RandomBookByGenre { id: genre.id }.to_string(),
|
||||
genre.id
|
||||
)),
|
||||
text: genre.description,
|
||||
}]
|
||||
})
|
||||
.collect();
|
||||
|
||||
buttons.push(vec![InlineKeyboardButton {
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
RandomCallbackData::RandomBookByGenreRequest.to_string(),
|
||||
),
|
||||
text: "< Назад >".to_string(),
|
||||
}]);
|
||||
|
||||
let keyboard = InlineKeyboardMarkup {
|
||||
inline_keyboard: buttons,
|
||||
};
|
||||
|
||||
match cq.message {
|
||||
Some(message) => {
|
||||
match bot
|
||||
.edit_message_reply_markup(message.chat.id, message.id)
|
||||
.reply_markup(keyboard)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
match bot
|
||||
.send_message(cq.from.id, "Ошибка! Начните заново :(")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_random_book_by_genre(
|
||||
cq: CallbackQuery,
|
||||
bot: AutoSend<Bot>,
|
||||
genre_id: u32,
|
||||
) -> BotHandlerInternal {
|
||||
let allowed_langs = get_user_or_default_lang_codes(cq.from.id).await;
|
||||
|
||||
let item = book_library::get_random_book_by_genre(allowed_langs, Some(genre_id)).await;
|
||||
|
||||
get_random_item_handler_internal(cq, bot, item).await
|
||||
}
|
||||
|
||||
pub fn get_random_hander() -> crate::bots::BotHandler {
|
||||
dptree::entry()
|
||||
.branch(
|
||||
Update::filter_message()
|
||||
.branch(
|
||||
dptree::entry()
|
||||
.filter_command::<RandomCommand>()
|
||||
.endpoint(|message, command, bot| async {
|
||||
match command {
|
||||
RandomCommand::Random => random_handler(message, bot).await,
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
.branch(
|
||||
Update::filter_callback_query()
|
||||
.chain(filter_callback_query::<RandomCallbackData>())
|
||||
.endpoint(|cq: CallbackQuery, callback_data: RandomCallbackData, bot: AutoSend<Bot>| async move {
|
||||
match callback_data {
|
||||
RandomCallbackData::RandomBook => get_random_item_handler(cq, bot, book_library::get_random_book).await,
|
||||
RandomCallbackData::RandomAuthor => get_random_item_handler(cq, bot, book_library::get_random_author).await,
|
||||
RandomCallbackData::RandomSequence => get_random_item_handler(cq, bot, book_library::get_random_sequence).await,
|
||||
RandomCallbackData::RandomBookByGenreRequest => get_genre_metas_handler(cq, bot).await,
|
||||
RandomCallbackData::Genres { index } => get_genres_by_meta_handler(cq, bot, index).await,
|
||||
RandomCallbackData::RandomBookByGenre { id } => get_random_book_by_genre(cq, bot, id).await,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
281
src/bots/approved_bot/modules/search.rs
Normal file
281
src/bots/approved_bot/modules/search.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use regex::Regex;
|
||||
use strum_macros::EnumIter;
|
||||
use teloxide::{
|
||||
prelude::*,
|
||||
types::{InlineKeyboardButton, InlineKeyboardMarkup},
|
||||
};
|
||||
|
||||
use crate::bots::{
|
||||
approved_bot::{
|
||||
services::{
|
||||
book_library::{
|
||||
formaters::Format, search_author, search_book, search_sequence, search_translator,
|
||||
types::Page,
|
||||
},
|
||||
user_settings::get_user_or_default_lang_codes,
|
||||
},
|
||||
tools::filter_callback_query,
|
||||
},
|
||||
BotHandlerInternal,
|
||||
};
|
||||
|
||||
use super::utils::{generic_get_pagination_keyboard, GetPaginationCallbackData};
|
||||
|
||||
#[derive(Clone, EnumIter)]
|
||||
pub enum SearchCallbackData {
|
||||
SearchBook { page: u32 },
|
||||
SearchAuthors { page: u32 },
|
||||
SearchSequences { page: u32 },
|
||||
SearchTranslators { page: u32 },
|
||||
}
|
||||
|
||||
impl ToString for SearchCallbackData {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
SearchCallbackData::SearchBook { page } => format!("sb_{page}"),
|
||||
SearchCallbackData::SearchAuthors { page } => format!("sa_{page}"),
|
||||
SearchCallbackData::SearchSequences { page } => format!("ss_{page}"),
|
||||
SearchCallbackData::SearchTranslators { page } => format!("st_{page}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SearchCallbackData {
|
||||
type Err = strum::ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let re = Regex::new(r"^(?P<search_type>s[a|b|s|t])_(?P<page>\d+)$").unwrap();
|
||||
|
||||
let caps = re.captures(s);
|
||||
let caps = match caps {
|
||||
Some(v) => v,
|
||||
None => return Err(strum::ParseError::VariantNotFound),
|
||||
};
|
||||
|
||||
let search_type = &caps["search_type"];
|
||||
let page: u32 = caps["page"].parse::<u32>().unwrap();
|
||||
|
||||
match search_type {
|
||||
"sb" => Ok(SearchCallbackData::SearchBook { page }),
|
||||
"sa" => Ok(SearchCallbackData::SearchAuthors { page }),
|
||||
"ss" => Ok(SearchCallbackData::SearchSequences { page }),
|
||||
"st" => Ok(SearchCallbackData::SearchTranslators { page }),
|
||||
_ => Err(strum::ParseError::VariantNotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GetPaginationCallbackData for SearchCallbackData {
|
||||
fn get_pagination_callback_data(&self, target_page: u32) -> String {
|
||||
match self {
|
||||
SearchCallbackData::SearchBook { .. } => {
|
||||
SearchCallbackData::SearchBook { page: target_page }
|
||||
}
|
||||
SearchCallbackData::SearchAuthors { .. } => {
|
||||
SearchCallbackData::SearchAuthors { page: target_page }
|
||||
}
|
||||
SearchCallbackData::SearchSequences { .. } => {
|
||||
SearchCallbackData::SearchSequences { page: target_page }
|
||||
}
|
||||
SearchCallbackData::SearchTranslators { .. } => {
|
||||
SearchCallbackData::SearchTranslators { page: target_page }
|
||||
}
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_query(cq: CallbackQuery) -> Option<String> {
|
||||
cq.message
|
||||
.map(|message| {
|
||||
message
|
||||
.reply_to_message()
|
||||
.map(|reply_to_message| {
|
||||
reply_to_message
|
||||
.text()
|
||||
.map(|text| text.replace('/', "").replace('&', "").replace('?', ""))
|
||||
})
|
||||
.unwrap_or(None)
|
||||
})
|
||||
.unwrap_or(None)
|
||||
}
|
||||
|
||||
async fn generic_search_pagination_handler<T, Fut>(
|
||||
cq: CallbackQuery,
|
||||
bot: AutoSend<Bot>,
|
||||
search_data: SearchCallbackData,
|
||||
items_getter: fn(query: String, page: u32, allowed_langs: Vec<String>) -> Fut,
|
||||
) -> BotHandlerInternal
|
||||
where
|
||||
T: Format + Clone,
|
||||
Fut: std::future::Future<Output = Result<Page<T>, Box<dyn std::error::Error + Send + Sync>>>,
|
||||
{
|
||||
let chat_id = cq.message.as_ref().map(|message| message.chat.id);
|
||||
let user_id = cq
|
||||
.message
|
||||
.as_ref()
|
||||
.map(|message| message.from().map(|from| from.id))
|
||||
.unwrap_or(None);
|
||||
let message_id = cq.message.as_ref().map(|message| message.id);
|
||||
let query = get_query(cq);
|
||||
|
||||
let (chat_id, user_id, query, message_id) = match (chat_id, user_id, query, message_id) {
|
||||
(Some(chat_id), Some(user_id), Some(query), Some(message_id)) => {
|
||||
(chat_id, user_id, query, message_id)
|
||||
}
|
||||
_ => {
|
||||
return match chat_id {
|
||||
Some(v) => match bot.send_message(v, "Повторите поиск сначала").send().await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
},
|
||||
None => return Ok(()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let allowed_langs = get_user_or_default_lang_codes(user_id).await;
|
||||
|
||||
let page = match search_data {
|
||||
SearchCallbackData::SearchBook { page } => page,
|
||||
SearchCallbackData::SearchAuthors { page } => page,
|
||||
SearchCallbackData::SearchSequences { page } => page,
|
||||
SearchCallbackData::SearchTranslators { page } => page,
|
||||
};
|
||||
|
||||
let mut items_page = match items_getter(query.clone(), page, allowed_langs.clone()).await {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
match bot
|
||||
.send_message(chat_id, "Ошибка! Попробуйте позже :(")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) => log::error!("{:?}", err),
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
if items_page.total_pages == 0 {
|
||||
let message_text = match search_data {
|
||||
SearchCallbackData::SearchBook { .. } => "Книги не найдены!",
|
||||
SearchCallbackData::SearchAuthors { .. } => "Авторы не найдены!",
|
||||
SearchCallbackData::SearchSequences { .. } => "Серии не найдены!",
|
||||
SearchCallbackData::SearchTranslators { .. } => "Переводчики не найдены!",
|
||||
};
|
||||
|
||||
match bot.send_message(chat_id, message_text).send().await {
|
||||
Ok(_) => (),
|
||||
Err(err) => return Err(Box::new(err)),
|
||||
};
|
||||
};
|
||||
|
||||
if page > items_page.total_pages {
|
||||
items_page = match items_getter(
|
||||
query.clone(),
|
||||
items_page.total_pages,
|
||||
allowed_langs.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
match bot
|
||||
.send_message(chat_id, "Ошибка! Попробуйте позже :(")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) => log::error!("{:?}", err),
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let formated_items = items_page.format_items();
|
||||
|
||||
let total_pages = items_page.total_pages;
|
||||
|
||||
let footer = format!("\n\nСтраница {page}/{total_pages}");
|
||||
let message_text = format!("{formated_items}{footer}");
|
||||
|
||||
let keyboard = generic_get_pagination_keyboard(page, total_pages, search_data, true);
|
||||
|
||||
match bot
|
||||
.edit_message_text(chat_id, message_id, message_text)
|
||||
.reply_markup(keyboard)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn message_handler(message: Message, bot: AutoSend<Bot>) -> BotHandlerInternal {
|
||||
let message_text = "Что ищем?";
|
||||
|
||||
let keyboard = InlineKeyboardMarkup {
|
||||
inline_keyboard: vec![
|
||||
vec![InlineKeyboardButton {
|
||||
text: "Книгу".to_string(),
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
(SearchCallbackData::SearchBook { page: 1 }).to_string(),
|
||||
),
|
||||
}],
|
||||
vec![InlineKeyboardButton {
|
||||
text: "Автора".to_string(),
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
(SearchCallbackData::SearchAuthors { page: 1 }).to_string(),
|
||||
),
|
||||
}],
|
||||
vec![InlineKeyboardButton {
|
||||
text: "Серию".to_string(),
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
(SearchCallbackData::SearchSequences { page: 1 }).to_string(),
|
||||
),
|
||||
}],
|
||||
vec![InlineKeyboardButton {
|
||||
text: "Переводчика".to_string(),
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
(SearchCallbackData::SearchTranslators { page: 1 }).to_string(),
|
||||
),
|
||||
}],
|
||||
],
|
||||
};
|
||||
|
||||
match bot
|
||||
.send_message(message.chat.id, message_text)
|
||||
.reply_to_message_id(message.id)
|
||||
.reply_markup(keyboard)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_search_hanlder() -> crate::bots::BotHandler {
|
||||
dptree::entry().branch(
|
||||
Update::filter_message()
|
||||
.endpoint(|message, bot| async move { message_handler(message, bot).await }),
|
||||
).branch(
|
||||
Update::filter_callback_query()
|
||||
.chain(filter_callback_query::<SearchCallbackData>())
|
||||
.endpoint(|cq: CallbackQuery, callback_data: SearchCallbackData, bot: AutoSend<Bot>| async move {
|
||||
match callback_data {
|
||||
SearchCallbackData::SearchBook { .. } => generic_search_pagination_handler(cq, bot, callback_data, search_book).await,
|
||||
SearchCallbackData::SearchAuthors { .. } => generic_search_pagination_handler(cq, bot, callback_data, search_author).await,
|
||||
SearchCallbackData::SearchSequences { .. } => generic_search_pagination_handler(cq, bot, callback_data, search_sequence).await,
|
||||
SearchCallbackData::SearchTranslators { .. } => generic_search_pagination_handler(cq, bot, callback_data, search_translator).await,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
217
src/bots/approved_bot/modules/settings.rs
Normal file
217
src/bots/approved_bot/modules/settings.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use std::{collections::HashSet, str::FromStr, vec};
|
||||
|
||||
use crate::bots::{
|
||||
approved_bot::{
|
||||
services::user_settings::{
|
||||
create_or_update_user_settings, get_langs, get_user_or_default_lang_codes, Lang,
|
||||
},
|
||||
tools::filter_callback_query,
|
||||
},
|
||||
BotHandlerInternal,
|
||||
};
|
||||
|
||||
use regex::Regex;
|
||||
use teloxide::{
|
||||
prelude::*,
|
||||
types::{InlineKeyboardButton, InlineKeyboardMarkup, Me},
|
||||
utils::command::BotCommands,
|
||||
};
|
||||
|
||||
#[derive(BotCommands, Clone)]
|
||||
#[command(rename = "lowercase")]
|
||||
enum SettingsCommand {
|
||||
Settings,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum SettingsCallbackData {
|
||||
LangSettings,
|
||||
LangOn { code: String },
|
||||
LangOff { code: String },
|
||||
}
|
||||
|
||||
impl FromStr for SettingsCallbackData {
|
||||
type Err = strum::ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s == SettingsCallbackData::LangSettings.to_string().as_str() {
|
||||
return Ok(SettingsCallbackData::LangSettings);
|
||||
}
|
||||
|
||||
let re = Regex::new(r"^lang_(?P<action>(off)|(on))_(?P<code>[a-zA-z]+)$").unwrap();
|
||||
|
||||
let caps = re.captures(s);
|
||||
let caps = match caps {
|
||||
Some(v) => v,
|
||||
None => return Err(strum::ParseError::VariantNotFound),
|
||||
};
|
||||
|
||||
let action = &caps["action"];
|
||||
let code = caps["code"].to_string();
|
||||
|
||||
match action {
|
||||
"on" => Ok(SettingsCallbackData::LangOn { code }),
|
||||
"off" => Ok(SettingsCallbackData::LangOff { code }),
|
||||
_ => Err(strum::ParseError::VariantNotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for SettingsCallbackData {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
SettingsCallbackData::LangSettings => "lang_settings".to_string(),
|
||||
SettingsCallbackData::LangOn { code } => format!("lang_on_{code}"),
|
||||
SettingsCallbackData::LangOff { code } => format!("lang_off_{code}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn settings_handler(message: Message, bot: AutoSend<Bot>) -> BotHandlerInternal {
|
||||
let keyboard = InlineKeyboardMarkup {
|
||||
inline_keyboard: vec![vec![InlineKeyboardButton {
|
||||
text: "Языки".to_string(),
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
SettingsCallbackData::LangSettings.to_string(),
|
||||
),
|
||||
}]],
|
||||
};
|
||||
|
||||
match bot
|
||||
.send_message(message.chat.id, "Настройки")
|
||||
.reply_markup(keyboard)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_lang_keyboard(all_langs: Vec<Lang>, allowed_langs: HashSet<String>) -> InlineKeyboardMarkup {
|
||||
let buttons = all_langs
|
||||
.into_iter()
|
||||
.map(|lang| {
|
||||
let (emoji, callback_data) = match allowed_langs.contains(&lang.code) {
|
||||
true => (
|
||||
"🟢".to_string(),
|
||||
SettingsCallbackData::LangOff { code: lang.code }.to_string(),
|
||||
),
|
||||
false => (
|
||||
"🔴".to_string(),
|
||||
SettingsCallbackData::LangOn { code: lang.code }.to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
vec![InlineKeyboardButton {
|
||||
text: format!("{emoji} {}", lang.label),
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(callback_data),
|
||||
}]
|
||||
})
|
||||
.collect();
|
||||
|
||||
InlineKeyboardMarkup {
|
||||
inline_keyboard: buttons,
|
||||
}
|
||||
}
|
||||
|
||||
async fn settings_callback_handler(
|
||||
cq: CallbackQuery,
|
||||
bot: AutoSend<Bot>,
|
||||
callback_data: SettingsCallbackData,
|
||||
me: Me,
|
||||
) -> BotHandlerInternal {
|
||||
let message = match cq.message {
|
||||
Some(v) => v,
|
||||
None => return Ok(()), // TODO: alert
|
||||
};
|
||||
|
||||
let user = match message.from() {
|
||||
Some(v) => v,
|
||||
None => return Ok(()), // TODO: alert
|
||||
};
|
||||
|
||||
let allowed_langs = get_user_or_default_lang_codes(user.id).await;
|
||||
|
||||
let mut allowed_langs_set: HashSet<String> = HashSet::new();
|
||||
allowed_langs.clone().into_iter().for_each(|v| {
|
||||
allowed_langs_set.insert(v);
|
||||
});
|
||||
|
||||
match callback_data {
|
||||
SettingsCallbackData::LangSettings => (),
|
||||
SettingsCallbackData::LangOn { code } => {
|
||||
allowed_langs_set.insert(code);
|
||||
}
|
||||
SettingsCallbackData::LangOff { code } => {
|
||||
allowed_langs_set.remove(&code);
|
||||
}
|
||||
};
|
||||
|
||||
if allowed_langs_set.len() == 0 {
|
||||
return match bot
|
||||
.answer_callback_query(cq.id)
|
||||
.text("Должен быть активен, хотя бы один язык!")
|
||||
.show_alert(true)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
};
|
||||
}
|
||||
|
||||
match create_or_update_user_settings(
|
||||
user.id,
|
||||
user.last_name.clone().unwrap_or("".to_string()),
|
||||
user.first_name.clone(),
|
||||
user.username.clone().unwrap_or("".to_string()),
|
||||
me.username.clone().unwrap(),
|
||||
allowed_langs_set.clone().into_iter().collect(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(err) => return Err(err), // TODO: err
|
||||
};
|
||||
|
||||
let all_langs = match get_langs().await {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let keyboard = get_lang_keyboard(all_langs, allowed_langs_set);
|
||||
|
||||
match bot
|
||||
.edit_message_reply_markup(message.chat.id, message.id)
|
||||
.reply_markup(keyboard)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_settings_handler() -> crate::bots::BotHandler {
|
||||
dptree::entry()
|
||||
.branch(
|
||||
Update::filter_message().branch(
|
||||
dptree::entry()
|
||||
.filter_command::<SettingsCommand>()
|
||||
.endpoint(|message, bot| async move { settings_handler(message, bot).await }),
|
||||
),
|
||||
)
|
||||
.branch(
|
||||
Update::filter_callback_query()
|
||||
.chain(filter_callback_query::<SettingsCallbackData>())
|
||||
.endpoint(
|
||||
|cq: CallbackQuery,
|
||||
bot: AutoSend<Bot>,
|
||||
callback_data: SettingsCallbackData,
|
||||
me: Me| async move {
|
||||
settings_callback_handler(cq, bot, callback_data, me).await
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
50
src/bots/approved_bot/modules/support.rs
Normal file
50
src/bots/approved_bot/modules/support.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::bots::BotHandlerInternal;
|
||||
|
||||
use teloxide::{
|
||||
prelude::*,
|
||||
types::{InlineKeyboardButton, InlineKeyboardMarkup},
|
||||
utils::command::BotCommands,
|
||||
};
|
||||
|
||||
#[derive(BotCommands, Clone)]
|
||||
#[command(rename = "lowercase")]
|
||||
enum SupportCommand {
|
||||
Support,
|
||||
}
|
||||
|
||||
pub async fn support_command_handler(message: Message, bot: AutoSend<Bot>) -> BotHandlerInternal {
|
||||
const MESSAGE_TEXT: &str = "
|
||||
[Лицензии](https://github.com/flibusta-apps/book_bot/blob/main/LICENSE.md)
|
||||
|
||||
[Исходный код](https://github.com/flibusta-apps)
|
||||
";
|
||||
|
||||
let keyboard = InlineKeyboardMarkup {
|
||||
inline_keyboard: vec![vec![InlineKeyboardButton {
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::Url(
|
||||
url::Url::parse("https://kurbezz.github.io/Kurbezz/").unwrap(),
|
||||
),
|
||||
text: String::from("☕️ Поддержать разработчика"),
|
||||
}]],
|
||||
};
|
||||
|
||||
match bot
|
||||
.send_message(message.chat.id, MESSAGE_TEXT)
|
||||
.parse_mode(teloxide::types::ParseMode::MarkdownV2)
|
||||
.reply_markup(keyboard)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_support_handler() -> crate::bots::BotHandler {
|
||||
dptree::entry().branch(
|
||||
Update::filter_message().branch(
|
||||
dptree::entry().filter_command::<SupportCommand>().endpoint(
|
||||
|message, bot| async move { support_command_handler(message, bot).await },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
227
src/bots/approved_bot/modules/update_history.rs
Normal file
227
src/bots/approved_bot/modules/update_history.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
use chrono::{prelude::*, Duration};
|
||||
use dateparser::parse;
|
||||
|
||||
use std::{str::FromStr, vec};
|
||||
|
||||
use crate::bots::{
|
||||
approved_bot::{services::book_library::get_uploaded_books, tools::filter_callback_query},
|
||||
BotHandlerInternal,
|
||||
};
|
||||
|
||||
use regex::Regex;
|
||||
use teloxide::{
|
||||
prelude::*,
|
||||
types::{InlineKeyboardButton, InlineKeyboardMarkup},
|
||||
utils::command::BotCommands,
|
||||
};
|
||||
|
||||
use super::utils::{generic_get_pagination_keyboard, GetPaginationCallbackData};
|
||||
|
||||
#[derive(BotCommands, Clone)]
|
||||
#[command(rename = "snake_case")]
|
||||
enum UpdateLogCommand {
|
||||
UpdateLog,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct UpdateLogCallbackData {
|
||||
from: Date<Utc>,
|
||||
to: Date<Utc>,
|
||||
page: u32,
|
||||
}
|
||||
|
||||
impl FromStr for UpdateLogCallbackData {
|
||||
type Err = strum::ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let re = Regex::new(
|
||||
r"^update_log_(?P<from>\d{4}-\d{2}-\d{2})_(?P<to>\d{4}-\d{2}-\d{2})_(?P<page>\d+)$",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let caps = re.captures(s);
|
||||
let caps = match caps {
|
||||
Some(v) => v,
|
||||
None => return Err(strum::ParseError::VariantNotFound),
|
||||
};
|
||||
|
||||
let from: Date<Utc> = parse(&caps["from"]).unwrap().date();
|
||||
let to: Date<Utc> = parse(&caps["to"]).unwrap().date();
|
||||
let page: u32 = caps["page"].parse().unwrap();
|
||||
|
||||
Ok(UpdateLogCallbackData { from, to, page })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for UpdateLogCallbackData {
|
||||
fn to_string(&self) -> String {
|
||||
let date_format = "%Y-%m-%d";
|
||||
|
||||
let from = self.from.format(date_format);
|
||||
let to = self.to.format(date_format);
|
||||
let page = self.page;
|
||||
|
||||
format!("update_log_{from}_{to}_{page}")
|
||||
}
|
||||
}
|
||||
|
||||
impl GetPaginationCallbackData for UpdateLogCallbackData {
|
||||
fn get_pagination_callback_data(&self, target_page: u32) -> String {
|
||||
let UpdateLogCallbackData { from, to, .. } = self;
|
||||
UpdateLogCallbackData {
|
||||
from: from.clone(),
|
||||
to: to.clone(),
|
||||
page: target_page,
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_log_command(message: Message, bot: AutoSend<Bot>) -> BotHandlerInternal {
|
||||
let now = Utc::today();
|
||||
let d3 = now - Duration::days(3);
|
||||
let d7 = now - Duration::days(7);
|
||||
let d30 = now - Duration::days(30);
|
||||
|
||||
let keyboard = InlineKeyboardMarkup {
|
||||
inline_keyboard: vec![
|
||||
vec![InlineKeyboardButton {
|
||||
text: "За 3 дня".to_string(),
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
UpdateLogCallbackData {
|
||||
from: d3,
|
||||
to: now,
|
||||
page: 1,
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
}],
|
||||
vec![InlineKeyboardButton {
|
||||
text: "За 7 дней".to_string(),
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
UpdateLogCallbackData {
|
||||
from: d7,
|
||||
to: now,
|
||||
page: 1,
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
}],
|
||||
vec![InlineKeyboardButton {
|
||||
text: "За 30 дней".to_string(),
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(
|
||||
UpdateLogCallbackData {
|
||||
from: d30,
|
||||
to: now,
|
||||
page: 1,
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
}],
|
||||
],
|
||||
};
|
||||
|
||||
match bot
|
||||
.send_message(message.chat.id, "Обновление каталога:")
|
||||
.reply_markup(keyboard)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_log_pagination_handler(
|
||||
cq: CallbackQuery,
|
||||
bot: AutoSend<Bot>,
|
||||
update_callback_data: UpdateLogCallbackData,
|
||||
) -> BotHandlerInternal {
|
||||
let message = match cq.message {
|
||||
Some(v) => v,
|
||||
None => return Ok(()), // TODO: send notification
|
||||
};
|
||||
|
||||
let from = update_callback_data.from.format("%d.%m.%Y");
|
||||
let to = update_callback_data.to.format("%d.%m.%Y");
|
||||
|
||||
let header = format!("Обновление каталога ({from} - {to}):\n\n");
|
||||
|
||||
let mut items_page = match get_uploaded_books(
|
||||
update_callback_data.page,
|
||||
update_callback_data.from.format("%Y-%m-%d").to_string(),
|
||||
update_callback_data.to.format("%Y-%m-%d").to_string(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
if items_page.total_pages == 0 {
|
||||
return match bot
|
||||
.send_message(message.chat.id, "Нет новых книг за этот период.")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
};
|
||||
}
|
||||
|
||||
if update_callback_data.page > items_page.total_pages {
|
||||
items_page = match get_uploaded_books(
|
||||
items_page.total_pages,
|
||||
update_callback_data.from.format("YYYY-MM-DD").to_string(),
|
||||
update_callback_data.to.format("YYYY-MM-DD").to_string(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
}
|
||||
|
||||
let formated_items = items_page.format_items();
|
||||
|
||||
let page = update_callback_data.page;
|
||||
let total_pages = items_page.total_pages;
|
||||
let footer = format!("\n\nСтраница {page}/{total_pages}");
|
||||
|
||||
let message_text = format!("{header}{formated_items}{footer}");
|
||||
|
||||
let keyboard = generic_get_pagination_keyboard(1, total_pages, update_callback_data, true);
|
||||
match bot
|
||||
.edit_message_text(message.chat.id, message.id, message_text)
|
||||
.reply_markup(keyboard)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_update_log_handler() -> crate::bots::BotHandler {
|
||||
dptree::entry()
|
||||
.branch(
|
||||
Update::filter_message().branch(
|
||||
dptree::entry()
|
||||
.filter_command::<UpdateLogCommand>()
|
||||
.endpoint(|message, bot| async move { update_log_command(message, bot).await }),
|
||||
),
|
||||
)
|
||||
.branch(
|
||||
Update::filter_callback_query().branch(
|
||||
dptree::entry()
|
||||
.chain(filter_callback_query::<UpdateLogCallbackData>())
|
||||
.endpoint(
|
||||
|cq: CallbackQuery,
|
||||
bot: AutoSend<Bot>,
|
||||
update_log_data: UpdateLogCallbackData| async move {
|
||||
update_log_pagination_handler(cq, bot, update_log_data).await
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
114
src/bots/approved_bot/modules/utils.rs
Normal file
114
src/bots/approved_bot/modules/utils.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use teloxide::{dptree, prelude::*, types::*};
|
||||
|
||||
pub trait CommandParse<T> {
|
||||
fn parse(s: &str, bot_name: &str) -> Result<T, strum::ParseError>;
|
||||
}
|
||||
|
||||
pub fn filter_command<Output>() -> crate::bots::BotHandler
|
||||
where
|
||||
Output: CommandParse<Output> + Send + Sync + 'static,
|
||||
{
|
||||
dptree::entry().chain(dptree::filter_map(move |message: Message, me: Me| {
|
||||
let bot_name = me.user.username.expect("Bots must have a username");
|
||||
message
|
||||
.text()
|
||||
.and_then(|text| Output::parse(text, &bot_name).ok())
|
||||
}))
|
||||
}
|
||||
|
||||
pub enum PaginationDelta {
|
||||
OneMinus,
|
||||
OnePlus,
|
||||
FiveMinus,
|
||||
FivePlus,
|
||||
}
|
||||
|
||||
pub trait GetPaginationCallbackData {
|
||||
fn get_pagination_callback_data(&self, target_page: u32) -> String;
|
||||
}
|
||||
|
||||
pub fn generic_get_pagination_button<T>(
|
||||
target_page: u32,
|
||||
delta: PaginationDelta,
|
||||
callback_data: &T,
|
||||
) -> InlineKeyboardButton
|
||||
where
|
||||
T: GetPaginationCallbackData,
|
||||
{
|
||||
let text = match delta {
|
||||
PaginationDelta::OneMinus => "<",
|
||||
PaginationDelta::OnePlus => ">",
|
||||
PaginationDelta::FiveMinus => "< 5 <",
|
||||
PaginationDelta::FivePlus => "> 5 >",
|
||||
};
|
||||
|
||||
let callback_data = callback_data.get_pagination_callback_data(target_page);
|
||||
|
||||
InlineKeyboardButton {
|
||||
text: text.to_string(),
|
||||
kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(callback_data),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generic_get_pagination_keyboard<T>(
|
||||
page: u32,
|
||||
total_pages: u32,
|
||||
search_data: T,
|
||||
with_five: bool,
|
||||
) -> InlineKeyboardMarkup
|
||||
where
|
||||
T: GetPaginationCallbackData,
|
||||
{
|
||||
let buttons: Vec<Vec<InlineKeyboardButton>> = {
|
||||
let t_page: i64 = page.into();
|
||||
|
||||
let mut result: Vec<Vec<InlineKeyboardButton>> = vec![];
|
||||
|
||||
let mut one_page_row: Vec<InlineKeyboardButton> = vec![];
|
||||
|
||||
if t_page - 1 > 0 {
|
||||
one_page_row.push(generic_get_pagination_button(
|
||||
page - 1,
|
||||
PaginationDelta::OneMinus,
|
||||
&search_data,
|
||||
))
|
||||
}
|
||||
if t_page + 1 <= total_pages.into() {
|
||||
one_page_row.push(generic_get_pagination_button(
|
||||
page + 1,
|
||||
PaginationDelta::OnePlus,
|
||||
&search_data,
|
||||
))
|
||||
}
|
||||
if one_page_row.len() != 0 {
|
||||
result.push(one_page_row);
|
||||
}
|
||||
|
||||
if with_five {
|
||||
let mut five_page_row: Vec<InlineKeyboardButton> = vec![];
|
||||
if t_page - 5 > 0 {
|
||||
five_page_row.push(generic_get_pagination_button(
|
||||
page - 5,
|
||||
PaginationDelta::FiveMinus,
|
||||
&search_data,
|
||||
))
|
||||
}
|
||||
if t_page + 1 <= total_pages.into() {
|
||||
five_page_row.push(generic_get_pagination_button(
|
||||
page + 5,
|
||||
PaginationDelta::FivePlus,
|
||||
&search_data,
|
||||
))
|
||||
}
|
||||
if five_page_row.len() != 0 {
|
||||
result.push(five_page_row);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
};
|
||||
|
||||
InlineKeyboardMarkup {
|
||||
inline_keyboard: buttons,
|
||||
}
|
||||
}
|
||||
112
src/bots/approved_bot/services/book_cache/mod.rs
Normal file
112
src/bots/approved_bot/services/book_cache/mod.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use crate::{bots::approved_bot::modules::download::DownloadData, config};
|
||||
|
||||
use self::types::{CachedMessage, DownloadFile};
|
||||
|
||||
pub mod types;
|
||||
|
||||
pub async fn get_cached_message(
|
||||
download_data: &DownloadData,
|
||||
) -> Result<CachedMessage, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let DownloadData { format, id } = download_data;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(format!(
|
||||
"{}/api/v1/{id}/{format}",
|
||||
&config::CONFIG.cache_server_url
|
||||
))
|
||||
.header("Authorization", &config::CONFIG.cache_server_api_key)
|
||||
.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::<CachedMessage>().await {
|
||||
Ok(v) => Ok(v),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn clear_book_cache(
|
||||
download_data: &DownloadData,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let DownloadData { format, id } = download_data;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.delete(format!(
|
||||
"{}/api/v1/{id}/{format}",
|
||||
&config::CONFIG.cache_server_url
|
||||
))
|
||||
.header("Authorization", &config::CONFIG.cache_server_api_key)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let response = match response {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Err(Box::new(err)),
|
||||
};
|
||||
|
||||
match response.error_for_status() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => return Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_file(
|
||||
download_data: &DownloadData,
|
||||
) -> Result<DownloadFile, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let DownloadData { format, id } = download_data;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(format!(
|
||||
"{}/api/v1/download/{id}/{format}",
|
||||
&config::CONFIG.cache_server_url
|
||||
))
|
||||
.header("Authorization", &config::CONFIG.cache_server_api_key)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let response = match response {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Err(Box::new(err)),
|
||||
};
|
||||
|
||||
match response.error_for_status() {
|
||||
Ok(response) => {
|
||||
let headers = response.headers();
|
||||
let filename = headers
|
||||
.get("content-disposition")
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace('"', "")
|
||||
.split("filename=")
|
||||
.collect::<Vec<&str>>()
|
||||
.get(1)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let caption = std::str::from_utf8(
|
||||
&base64::decode(headers.get("x-caption-b64").unwrap()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
Ok(DownloadFile {
|
||||
response,
|
||||
filename,
|
||||
caption,
|
||||
})
|
||||
}
|
||||
Err(err) => return Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
13
src/bots/approved_bot/services/book_cache/types.rs
Normal file
13
src/bots/approved_bot/services/book_cache/types.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct CachedMessage {
|
||||
pub message_id: i32,
|
||||
pub chat_id: i64,
|
||||
}
|
||||
|
||||
pub struct DownloadFile {
|
||||
pub response: reqwest::Response,
|
||||
pub filename: String,
|
||||
pub caption: String,
|
||||
}
|
||||
299
src/bots/approved_bot/services/book_library/formaters.rs
Normal file
299
src/bots/approved_bot/services/book_library/formaters.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
use super::types::{Author, AuthorBook, Book, SearchBook, Sequence, Translator, TranslatorBook};
|
||||
|
||||
pub trait Format {
|
||||
fn format(&self) -> String;
|
||||
}
|
||||
|
||||
impl Format for Book {
|
||||
fn format(&self) -> String {
|
||||
let book_title = {
|
||||
let Book { title, lang, .. } = self;
|
||||
format!("📖 {title} | {lang}\n")
|
||||
};
|
||||
|
||||
let pages_count = match self.pages {
|
||||
Some(1) | None => "".to_string(),
|
||||
Some(v) => format!("[ {v}с. ]\n\n"),
|
||||
};
|
||||
|
||||
let annotations = match self.annotation_exists {
|
||||
true => {
|
||||
let Book { id, .. } = self;
|
||||
format!("📝 Аннотация: /b_an_{id}\n\n")
|
||||
}
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let authors = match self.authors.len() != 0 {
|
||||
true => {
|
||||
let formated_authors = self
|
||||
.authors
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|author| author.format_author())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
format!("Авторы:\n{formated_authors}\n\n")
|
||||
}
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let translators = match self.translators.len() != 0 {
|
||||
true => {
|
||||
let formated_translators = self
|
||||
.translators
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|translator| translator.format_translator())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
format!("Переводчики:\n{formated_translators}\n\n")
|
||||
}
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let sequences = match self.sequences.len() != 0 {
|
||||
true => {
|
||||
let formated_sequences: String = self
|
||||
.sequences
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|sequence| sequence.format())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
format!("Серии:\n{formated_sequences}\n\n")
|
||||
}
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let genres = match self.genres.len() != 0 {
|
||||
true => {
|
||||
let formated_genres: String = self
|
||||
.genres
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|genre| genre.format())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
format!("Жанры:\n{formated_genres}\n\n")
|
||||
}
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let links: String = self
|
||||
.available_types
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|a_type| {
|
||||
let Book { id, .. } = self;
|
||||
format!("📥 {a_type}: /d_{a_type}_{id}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
let download_links = format!("Скачать:\n{links}");
|
||||
|
||||
format!("{book_title}{pages_count}{annotations}{authors}{translators}{sequences}{genres}{download_links}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Format for Author {
|
||||
fn format(&self) -> String {
|
||||
let Author {
|
||||
id,
|
||||
last_name,
|
||||
first_name,
|
||||
middle_name,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let title = format!("👤 {last_name} {first_name} {middle_name}\n");
|
||||
let link = format!("/a_{id}\n");
|
||||
let annotation = match self.annotation_exists {
|
||||
true => format!("📝 Аннотация: /a_an_{id}"),
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
format!("{title}{link}{annotation}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Format for Sequence {
|
||||
fn format(&self) -> String {
|
||||
let Sequence { id, name, .. } = self;
|
||||
|
||||
let title = format!("📚 {name}\n");
|
||||
let link = format!("/s_{id}");
|
||||
|
||||
format!("{title}{link}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Format for SearchBook {
|
||||
fn format(&self) -> String {
|
||||
let book_title = {
|
||||
let SearchBook { title, lang, .. } = self;
|
||||
format!("📖 {title} | {lang}\n")
|
||||
};
|
||||
|
||||
let annotations = match self.annotation_exists {
|
||||
true => {
|
||||
let SearchBook { id, .. } = self;
|
||||
format!("📝 Аннотация: /b_an_{id}\n")
|
||||
}
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let authors = match self.authors.len() != 0 {
|
||||
true => {
|
||||
let formated_authors = self
|
||||
.authors
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|author| author.format_author())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
format!("Авторы:\n{formated_authors}\n")
|
||||
}
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let translators = match self.translators.len() != 0 {
|
||||
true => {
|
||||
let formated_translators = self
|
||||
.translators
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|translator| translator.format_translator())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
format!("Переводчики:\n{formated_translators}\n")
|
||||
}
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let links: String = self
|
||||
.available_types
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|a_type| {
|
||||
let SearchBook { id, .. } = self;
|
||||
format!("📥 {a_type}: /d_{a_type}_{id}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
let download_links = format!("Скачать:\n{links}");
|
||||
|
||||
format!("{book_title}{annotations}{authors}{translators}{download_links}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Format for Translator {
|
||||
fn format(&self) -> String {
|
||||
let Translator {
|
||||
id,
|
||||
last_name,
|
||||
first_name,
|
||||
middle_name,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let title = format!("👤 {last_name} {first_name} {middle_name}\n");
|
||||
let link = format!("/t_{id}\n");
|
||||
let annotation = match self.annotation_exists {
|
||||
true => format!("📝 Аннотация: /a_an_{id}"),
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
format!("{title}{link}{annotation}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Format for AuthorBook {
|
||||
fn format(&self) -> String {
|
||||
let book_title = {
|
||||
let AuthorBook { title, lang, .. } = self;
|
||||
format!("📖 {title} | {lang}\n")
|
||||
};
|
||||
|
||||
let annotations = match self.annotation_exists {
|
||||
true => {
|
||||
let AuthorBook { id, .. } = self;
|
||||
format!("📝 Аннотация: /b_an_{id}\n")
|
||||
}
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let translators = match self.translators.len() != 0 {
|
||||
true => {
|
||||
let formated_translators = self
|
||||
.translators
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|translator| translator.format_translator())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
format!("Переводчики:\n{formated_translators}\n")
|
||||
}
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let links: String = self
|
||||
.available_types
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|a_type| {
|
||||
let AuthorBook { id, .. } = self;
|
||||
format!("📥 {a_type}: /d_{a_type}_{id}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
let download_links = format!("Скачать:\n{links}");
|
||||
|
||||
format!("{book_title}{annotations}{translators}{download_links}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Format for TranslatorBook {
|
||||
fn format(&self) -> String {
|
||||
let book_title = {
|
||||
let TranslatorBook { title, lang, .. } = self;
|
||||
format!("📖 {title} | {lang}\n")
|
||||
};
|
||||
|
||||
let annotations = match self.annotation_exists {
|
||||
true => {
|
||||
let TranslatorBook { id, .. } = self;
|
||||
format!("📝 Аннотация: /b_an_{id}\n")
|
||||
}
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let authors = match self.authors.len() != 0 {
|
||||
true => {
|
||||
let formated_authors = self
|
||||
.authors
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|author| author.format_author())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
format!("Авторы:\n{formated_authors}\n")
|
||||
}
|
||||
false => "".to_string(),
|
||||
};
|
||||
|
||||
let links: String = self
|
||||
.available_types
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|a_type| {
|
||||
let TranslatorBook { id, .. } = self;
|
||||
format!("📥 {a_type}: /d_{a_type}_{id}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
let download_links = format!("Скачать:\n{links}");
|
||||
|
||||
format!("{book_title}{annotations}{authors}{download_links}")
|
||||
}
|
||||
}
|
||||
217
src/bots/approved_bot/services/book_library/mod.rs
Normal file
217
src/bots/approved_bot/services/book_library/mod.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
pub mod formaters;
|
||||
pub mod types;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::config;
|
||||
|
||||
fn get_allowed_langs_params(allowed_langs: Vec<String>) -> Vec<(&'static str, String)> {
|
||||
allowed_langs
|
||||
.into_iter()
|
||||
.map(|lang| ("allowed_langs", lang))
|
||||
.collect()
|
||||
}
|
||||
|
||||
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 response = client
|
||||
.get(format!("{}{}", &config::CONFIG.book_server_url, url))
|
||||
.query(¶ms)
|
||||
.header("Authorization", &config::CONFIG.book_server_api_key)
|
||||
.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_random_book_by_genre(
|
||||
allowed_langs: Vec<String>,
|
||||
genre: Option<u32>,
|
||||
) -> Result<types::Book, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut params: Vec<(&str, String)> = get_allowed_langs_params(allowed_langs);
|
||||
|
||||
match genre {
|
||||
Some(v) => params.push(("genre", v.to_string())),
|
||||
None => (),
|
||||
}
|
||||
|
||||
_make_request("/api/v1/books/random", params).await
|
||||
}
|
||||
|
||||
pub async fn get_random_book(
|
||||
allowed_langs: Vec<String>,
|
||||
) -> Result<types::Book, Box<dyn std::error::Error + Send + Sync>> {
|
||||
get_random_book_by_genre(allowed_langs, None).await
|
||||
}
|
||||
|
||||
pub async fn get_random_author(
|
||||
allowed_langs: Vec<String>,
|
||||
) -> Result<types::Author, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let params: Vec<(&str, String)> = get_allowed_langs_params(allowed_langs);
|
||||
|
||||
_make_request("/api/v1/authors/random", params).await
|
||||
}
|
||||
|
||||
pub async fn get_random_sequence(
|
||||
allowed_langs: Vec<String>,
|
||||
) -> Result<types::Sequence, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let params = get_allowed_langs_params(allowed_langs);
|
||||
|
||||
_make_request("/api/v1/sequences/random", params).await
|
||||
}
|
||||
|
||||
pub async fn get_genre_metas() -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
_make_request("/api/v1/genres/metas", vec![]).await
|
||||
}
|
||||
|
||||
pub async fn get_genres(
|
||||
meta: String,
|
||||
) -> Result<types::Page<types::Genre>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let params = vec![("meta", meta)];
|
||||
|
||||
_make_request("/api/v1/genres/", params).await
|
||||
}
|
||||
|
||||
const PAGE_SIZE: &str = "7";
|
||||
|
||||
pub async fn search_book(
|
||||
query: String,
|
||||
page: u32,
|
||||
allowed_langs: Vec<String>,
|
||||
) -> Result<types::Page<types::SearchBook>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut params = get_allowed_langs_params(allowed_langs);
|
||||
|
||||
params.push(("page", page.to_string()));
|
||||
params.push(("size", PAGE_SIZE.to_string()));
|
||||
|
||||
_make_request(format!("/api/v1/books/search/{query}").as_str(), params).await
|
||||
}
|
||||
|
||||
pub async fn search_author(
|
||||
query: String,
|
||||
page: u32,
|
||||
allowed_langs: Vec<String>,
|
||||
) -> Result<types::Page<types::Author>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut params = get_allowed_langs_params(allowed_langs);
|
||||
|
||||
params.push(("page", page.to_string()));
|
||||
params.push(("size", PAGE_SIZE.to_string()));
|
||||
|
||||
_make_request(format!("/api/v1/authors/search/{query}").as_str(), params).await
|
||||
}
|
||||
|
||||
pub async fn search_sequence(
|
||||
query: String,
|
||||
page: u32,
|
||||
allowed_langs: Vec<String>,
|
||||
) -> Result<types::Page<types::Sequence>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut params = get_allowed_langs_params(allowed_langs);
|
||||
|
||||
params.push(("page", page.to_string()));
|
||||
params.push(("size", PAGE_SIZE.to_string()));
|
||||
|
||||
_make_request(format!("/api/v1/sequences/search/{query}").as_str(), params).await
|
||||
}
|
||||
|
||||
pub async fn search_translator(
|
||||
query: String,
|
||||
page: u32,
|
||||
allowed_langs: Vec<String>,
|
||||
) -> Result<types::Page<types::Translator>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut params = get_allowed_langs_params(allowed_langs);
|
||||
|
||||
params.push(("page", page.to_string()));
|
||||
params.push(("size", PAGE_SIZE.to_string()));
|
||||
|
||||
_make_request(
|
||||
format!("/api/v1/translators/search/{query}").as_str(),
|
||||
params,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_book_annotation(
|
||||
id: u32,
|
||||
) -> Result<types::BookAnnotation, Box<dyn std::error::Error + Send + Sync>> {
|
||||
_make_request(format!("/api/v1/books/{id}/annotation").as_str(), vec![]).await
|
||||
}
|
||||
|
||||
pub async fn get_author_annotation(
|
||||
id: u32,
|
||||
) -> Result<types::AuthorAnnotation, Box<dyn std::error::Error + Send + Sync>> {
|
||||
_make_request(format!("/api/v1/authors/{id}/annotation").as_str(), vec![]).await
|
||||
}
|
||||
|
||||
pub async fn get_author_books(
|
||||
id: u32,
|
||||
page: u32,
|
||||
allowed_langs: Vec<String>,
|
||||
) -> Result<types::Page<types::AuthorBook>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut params = get_allowed_langs_params(allowed_langs);
|
||||
|
||||
params.push(("page", page.to_string()));
|
||||
params.push(("size", PAGE_SIZE.to_string()));
|
||||
|
||||
_make_request(format!("/api/v1/authors/{id}/books").as_str(), params).await
|
||||
}
|
||||
|
||||
pub async fn get_translator_books(
|
||||
id: u32,
|
||||
page: u32,
|
||||
allowed_langs: Vec<String>,
|
||||
) -> Result<types::Page<types::TranslatorBook>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut params = get_allowed_langs_params(allowed_langs);
|
||||
|
||||
params.push(("page", page.to_string()));
|
||||
params.push(("size", PAGE_SIZE.to_string()));
|
||||
|
||||
_make_request(format!("/api/v1/translators/{id}/books").as_str(), params).await
|
||||
}
|
||||
|
||||
pub async fn get_sequence_books(
|
||||
id: u32,
|
||||
page: u32,
|
||||
allowed_langs: Vec<String>,
|
||||
) -> Result<types::Page<types::SearchBook>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut params = get_allowed_langs_params(allowed_langs);
|
||||
|
||||
params.push(("page", page.to_string()));
|
||||
params.push(("size", PAGE_SIZE.to_string()));
|
||||
|
||||
_make_request(format!("/api/v1/sequences/{id}/books").as_str(), params).await
|
||||
}
|
||||
|
||||
pub async fn get_uploaded_books(
|
||||
page: u32,
|
||||
uploaded_gte: String,
|
||||
uploaded_lte: String,
|
||||
) -> Result<types::Page<types::SearchBook>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let params = vec![
|
||||
("page", page.to_string()),
|
||||
("size", PAGE_SIZE.to_string()),
|
||||
("uploaded_gte", uploaded_gte),
|
||||
("uploaded_lte", uploaded_lte),
|
||||
("is_deleted", "false".to_string()),
|
||||
];
|
||||
|
||||
_make_request("/api/v1/books/", params).await
|
||||
}
|
||||
182
src/bots/approved_bot/services/book_library/types.rs
Normal file
182
src/bots/approved_bot/services/book_library/types.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::formaters::Format;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct BookAuthor {
|
||||
id: u32,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
middle_name: String,
|
||||
}
|
||||
|
||||
impl BookAuthor {
|
||||
pub fn format_author(&self) -> String {
|
||||
let BookAuthor {
|
||||
id,
|
||||
last_name,
|
||||
first_name,
|
||||
middle_name,
|
||||
} = self;
|
||||
|
||||
format!("👤 {last_name} {first_name} {middle_name} /a_{id}")
|
||||
}
|
||||
|
||||
pub fn format_translator(&self) -> String {
|
||||
let BookAuthor {
|
||||
id,
|
||||
first_name,
|
||||
last_name,
|
||||
middle_name,
|
||||
} = self;
|
||||
|
||||
format!("👤 {last_name} {first_name} {middle_name} /t_{id}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct BookGenre {
|
||||
pub id: u32,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl BookGenre {
|
||||
pub fn format(&self) -> String {
|
||||
format!("🗂 {}", self.description)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Source {
|
||||
// id: u32,
|
||||
// name: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Book {
|
||||
pub id: u32,
|
||||
pub title: String,
|
||||
pub lang: String,
|
||||
// file_type: String,
|
||||
pub available_types: Vec<String>,
|
||||
// uploaded: String,
|
||||
pub annotation_exists: bool,
|
||||
pub authors: Vec<BookAuthor>,
|
||||
pub translators: Vec<BookAuthor>,
|
||||
pub sequences: Vec<Sequence>,
|
||||
pub genres: Vec<BookGenre>,
|
||||
// source: Source,
|
||||
// remote_id: u32,
|
||||
// id_deleted: bool,
|
||||
pub pages: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Author {
|
||||
pub id: u32,
|
||||
pub last_name: String,
|
||||
pub first_name: String,
|
||||
pub middle_name: String,
|
||||
pub annotation_exists: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Translator {
|
||||
pub id: u32,
|
||||
pub last_name: String,
|
||||
pub first_name: String,
|
||||
pub middle_name: String,
|
||||
pub annotation_exists: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Sequence {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Genre {
|
||||
pub id: u32,
|
||||
pub source: Source,
|
||||
pub remote_id: u32,
|
||||
pub code: String,
|
||||
pub description: String,
|
||||
pub meta: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Page<T> {
|
||||
pub items: Vec<T>,
|
||||
pub total: u32,
|
||||
pub page: u32,
|
||||
pub size: u32,
|
||||
pub total_pages: u32,
|
||||
}
|
||||
|
||||
impl<T> Page<T>
|
||||
where
|
||||
T: Format + Clone,
|
||||
{
|
||||
pub fn format_items(&self) -> String {
|
||||
self.items
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|book| book.format())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct SearchBook {
|
||||
pub id: u32,
|
||||
pub title: String,
|
||||
pub lang: String,
|
||||
// file_type: String,
|
||||
pub available_types: Vec<String>,
|
||||
// uploaded: String,
|
||||
pub annotation_exists: bool,
|
||||
pub authors: Vec<BookAuthor>,
|
||||
pub translators: Vec<BookAuthor>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct BookAnnotation {
|
||||
pub id: u32,
|
||||
pub title: String,
|
||||
pub text: String,
|
||||
pub file: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct AuthorAnnotation {
|
||||
pub id: u32,
|
||||
pub title: String,
|
||||
pub text: String,
|
||||
pub file: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct AuthorBook {
|
||||
pub id: u32,
|
||||
pub title: String,
|
||||
pub lang: String,
|
||||
// file_type: String,
|
||||
pub available_types: Vec<String>,
|
||||
// uploaded: String,
|
||||
pub annotation_exists: bool,
|
||||
pub translators: Vec<BookAuthor>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct TranslatorBook {
|
||||
pub id: u32,
|
||||
pub title: String,
|
||||
pub lang: String,
|
||||
// file_type: String,
|
||||
pub available_types: Vec<String>,
|
||||
// uploaded: String,
|
||||
pub annotation_exists: bool,
|
||||
pub authors: Vec<BookAuthor>,
|
||||
}
|
||||
3
src/bots/approved_bot/services/mod.rs
Normal file
3
src/bots/approved_bot/services/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod book_cache;
|
||||
pub mod book_library;
|
||||
pub mod user_settings;
|
||||
126
src/bots/approved_bot/services/user_settings/mod.rs
Normal file
126
src/bots/approved_bot/services/user_settings/mod.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use teloxide::types::UserId;
|
||||
|
||||
use crate::config;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Lang {
|
||||
// pub id: u32,
|
||||
pub label: String,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct UserSettings {
|
||||
pub user_id: u64,
|
||||
pub last_name: String,
|
||||
pub first_name: String,
|
||||
pub username: String,
|
||||
pub source: String,
|
||||
pub allowed_langs: Vec<Lang>,
|
||||
}
|
||||
|
||||
pub async fn get_user_settings(
|
||||
user_id: UserId,
|
||||
) -> Result<UserSettings, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(format!(
|
||||
"{}/users/{}",
|
||||
&config::CONFIG.user_settings_url,
|
||||
user_id
|
||||
))
|
||||
.header("Authorization", &config::CONFIG.user_settings_api_key)
|
||||
.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::<UserSettings>().await {
|
||||
Ok(v) => Ok(v),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_user_or_default_lang_codes(user_id: UserId) -> Vec<String> {
|
||||
let default_lang_codes = vec![String::from("ru"), String::from("be"), String::from("uk")];
|
||||
|
||||
match get_user_settings(user_id).await {
|
||||
Ok(v) => v.allowed_langs.into_iter().map(|lang| lang.code).collect(),
|
||||
Err(_) => default_lang_codes,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_or_update_user_settings(
|
||||
user_id: UserId,
|
||||
last_name: String,
|
||||
first_name: String,
|
||||
username: String,
|
||||
source: String,
|
||||
allowed_langs: Vec<String>,
|
||||
) -> Result<UserSettings, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let body = json!({
|
||||
"user_id": user_id,
|
||||
"last_name": last_name,
|
||||
"first_name": first_name,
|
||||
"username": username,
|
||||
"source": source,
|
||||
"allowed_langs": allowed_langs
|
||||
});
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(format!("{}/users/", &config::CONFIG.user_settings_url))
|
||||
.body(body.to_string())
|
||||
.header("Authorization", &config::CONFIG.user_settings_api_key)
|
||||
.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::<UserSettings>().await {
|
||||
Ok(v) => Ok(v),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_langs() -> Result<Vec<Lang>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(format!("{}/languages/", &config::CONFIG.user_settings_url))
|
||||
.header("Authorization", &config::CONFIG.user_settings_api_key)
|
||||
.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::<Vec<Lang>>().await {
|
||||
Ok(v) => Ok(v),
|
||||
Err(err) => Err(Box::new(err)),
|
||||
}
|
||||
}
|
||||
10
src/bots/approved_bot/tools.rs
Normal file
10
src/bots/approved_bot/tools.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use teloxide::{dptree, types::CallbackQuery};
|
||||
|
||||
pub fn filter_callback_query<T>() -> crate::bots::BotHandler
|
||||
where
|
||||
T: std::str::FromStr + Send + Sync + 'static,
|
||||
{
|
||||
dptree::entry().chain(dptree::filter_map(move |cq: CallbackQuery| {
|
||||
cq.data.and_then(|data| T::from_str(data.as_str()).ok())
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user