Add rust implementation

This commit is contained in:
2022-09-14 18:19:02 +03:00
parent 40da6d8b56
commit d23a3c3ab1
61 changed files with 5596 additions and 3820 deletions

View 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("Поддержать разработчика"),
},
]),
)
}

View 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
}
}
},
),
)
}

View 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,
}
}),
)
}

View 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
},
),
)
}

View 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 }),
),
)
}

View 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;

View 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,
}
})
)
}

View 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,
}
})
)
}

View 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
},
),
)
}

View 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 },
),
),
)
}

View 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
},
),
),
)
}

View 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,
}
}

View 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)),
}
}

View 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,
}

View 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}")
}
}

View 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(&params)
.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
}

View 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>,
}

View File

@@ -0,0 +1,3 @@
pub mod book_cache;
pub mod book_library;
pub mod user_settings;

View 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)),
}
}

View 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())
}))
}