move out bot handlers to separate module
This commit is contained in:
parent
e63187dcd3
commit
866a028de1
468
src/handlers/admin.rs
Normal file
468
src/handlers/admin.rs
Normal file
@ -0,0 +1,468 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
use log::info;
|
||||||
|
use std::time::Duration;
|
||||||
|
use teloxide::dispatching::dialogue::serializer::Json;
|
||||||
|
use teloxide::prelude::*;
|
||||||
|
use teloxide::sugar::request::RequestReplyExt;
|
||||||
|
use teloxide::types::{MediaKind, MessageId, MessageKind, ParseMode};
|
||||||
|
use teloxide::utils::render::RenderMessageTextHelper;
|
||||||
|
use teloxide::{dptree, types::Update};
|
||||||
|
|
||||||
|
use crate::admin::{admin_command_handler, AdminCommands};
|
||||||
|
use crate::bot_handler::BotHandler;
|
||||||
|
use crate::db::message_forward::MessageForward;
|
||||||
|
use crate::db::{CallDB, DB};
|
||||||
|
use crate::mongodb_storage::MongodbStorage;
|
||||||
|
use crate::{BotDialogue, BotError, BotResult, CallbackStore, State};
|
||||||
|
|
||||||
|
pub fn admin_handler() -> BotHandler {
|
||||||
|
dptree::entry()
|
||||||
|
.branch(
|
||||||
|
Update::filter_callback_query()
|
||||||
|
.filter_async(async |q: CallbackQuery, mut db: DB| {
|
||||||
|
let tguser = q.from.clone();
|
||||||
|
let user = db
|
||||||
|
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
||||||
|
.await;
|
||||||
|
user.map(|u| u.is_admin).unwrap_or(false)
|
||||||
|
})
|
||||||
|
.enter_dialogue::<CallbackQuery, MongodbStorage<Json>, State>()
|
||||||
|
.branch(dptree::case![State::EditButton].endpoint(button_edit_callback)),
|
||||||
|
)
|
||||||
|
.branch(command_handler())
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
.filter_async(async |msg: Message, mut db: DB| {
|
||||||
|
let tguser = match msg.from.clone() {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return false, // do nothing, cause its not usecase of function
|
||||||
|
};
|
||||||
|
let user = db
|
||||||
|
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
||||||
|
.await;
|
||||||
|
user.map(|u| u.is_admin).unwrap_or(false)
|
||||||
|
})
|
||||||
|
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
.filter(|msg: Message| {
|
||||||
|
msg.text().unwrap_or("").to_lowercase().as_str() == "edit"
|
||||||
|
})
|
||||||
|
.endpoint(edit_msg_cmd_handler),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
.filter(|msg: Message| msg.reply_to_message().is_some())
|
||||||
|
.filter(|state: State| matches!(state, State::Start))
|
||||||
|
.endpoint(support_reply_handler),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
dptree::case![State::Edit {
|
||||||
|
literal,
|
||||||
|
variant,
|
||||||
|
lang,
|
||||||
|
is_caption_set
|
||||||
|
}]
|
||||||
|
.endpoint(edit_msg_handler),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
||||||
|
.branch(dptree::case![State::MessageForwardReply].endpoint(user_reply_to_support)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn button_edit_callback(
|
||||||
|
bot: Bot,
|
||||||
|
mut db: DB,
|
||||||
|
dialogue: BotDialogue,
|
||||||
|
q: CallbackQuery,
|
||||||
|
) -> BotResult<()> {
|
||||||
|
bot.answer_callback_query(&q.id).await?;
|
||||||
|
|
||||||
|
let id = match q.data {
|
||||||
|
Some(id) => id,
|
||||||
|
None => {
|
||||||
|
bot.send_message(q.from.id, "Not compatible callback to edit text on")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let ci = match CallbackStore::get(&mut db, &id).await? {
|
||||||
|
Some(ci) => ci,
|
||||||
|
None => {
|
||||||
|
bot.send_message(
|
||||||
|
q.from.id,
|
||||||
|
"Can't get button information. Maybe created not by this bot or message too old",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let literal = match ci.literal {
|
||||||
|
Some(l) => l,
|
||||||
|
None => {
|
||||||
|
bot.send_message(
|
||||||
|
q.from.id,
|
||||||
|
"This button is not editable (probably text is generated)",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let lang = "ru".to_string();
|
||||||
|
dialogue
|
||||||
|
.update(State::Edit {
|
||||||
|
literal,
|
||||||
|
variant: None,
|
||||||
|
lang,
|
||||||
|
is_caption_set: false,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
bot.send_message(q.from.id, "Send text of button").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_handler() -> BotHandler {
|
||||||
|
Update::filter_message()
|
||||||
|
.filter_async(async |msg: Message, mut db: DB| {
|
||||||
|
let tguser = match msg.from.clone() {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return false, // do nothing, cause its not usecase of function
|
||||||
|
};
|
||||||
|
let user = db
|
||||||
|
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
||||||
|
.await;
|
||||||
|
user.map(|u| u.is_admin).unwrap_or(false)
|
||||||
|
})
|
||||||
|
.filter_command::<AdminCommands>()
|
||||||
|
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
||||||
|
.endpoint(admin_command_handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn edit_msg_cmd_handler(
|
||||||
|
bot: Bot,
|
||||||
|
mut db: DB,
|
||||||
|
dialogue: BotDialogue,
|
||||||
|
msg: Message,
|
||||||
|
) -> BotResult<()> {
|
||||||
|
match msg.reply_to_message() {
|
||||||
|
Some(replied) => {
|
||||||
|
let msgid = replied.id;
|
||||||
|
// look for message in db and set text
|
||||||
|
let literal = match db.get_message_literal(msg.chat.id.0, msgid.0).await? {
|
||||||
|
Some(l) => l,
|
||||||
|
None => {
|
||||||
|
bot.send_message(msg.chat.id, "No such message found to edit. Look if you replying bot's message and this message is supposed to be editable").await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// TODO: language selector will be implemented in future 😈
|
||||||
|
let lang = "ru".to_string();
|
||||||
|
dialogue
|
||||||
|
.update(State::Edit {
|
||||||
|
literal,
|
||||||
|
variant: None,
|
||||||
|
lang,
|
||||||
|
is_caption_set: false,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
bot.send_message(
|
||||||
|
msg.chat.id,
|
||||||
|
"Ok, now you have to send message text (formatting supported)\n\
|
||||||
|
<b>Notice:</b> if this message supposed to replace message (tg shows them as edited) \
|
||||||
|
or be raplaced, do NOT send message with multiple media, only single photo, video etc. \
|
||||||
|
To get more information about why, see in /why_media_group",
|
||||||
|
).parse_mode(ParseMode::Html)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
bot.send_message(msg.chat.id, "You have to reply to message to edit it")
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn support_reply_handler(
|
||||||
|
bot: Bot,
|
||||||
|
mut db: DB,
|
||||||
|
msg: Message,
|
||||||
|
state_mgr: std::sync::Arc<MongodbStorage<Json>>,
|
||||||
|
) -> BotResult<()> {
|
||||||
|
use teloxide::utils::render::Renderer;
|
||||||
|
|
||||||
|
let rm = match msg.reply_to_message() {
|
||||||
|
Some(rm) => rm,
|
||||||
|
None => {
|
||||||
|
return Err(BotError::BotLogicError(
|
||||||
|
"support_reply_handler should not be called when no message is replied".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (chat_id, message_id) = (rm.chat.id.0, rm.id.0);
|
||||||
|
let mf = match MessageForward::get(&mut db, chat_id, message_id).await? {
|
||||||
|
Some(mf) => mf,
|
||||||
|
None => {
|
||||||
|
bot.send_message(msg.chat.id, "No forwarded message found for your reply")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = match msg.kind {
|
||||||
|
MessageKind::Common(message_common) => match message_common.media_kind {
|
||||||
|
MediaKind::Text(media_text) => {
|
||||||
|
Renderer::new(&media_text.text, &media_text.entities).as_html()
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
bot.send_message(msg.chat.id, "Only text messages currently supported!")
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// can't hapen because we already have check for reply
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let msg = bot
|
||||||
|
.send_message(ChatId(mf.source_chat_id), text)
|
||||||
|
.parse_mode(ParseMode::Html);
|
||||||
|
let msg = match mf.reply {
|
||||||
|
false => msg,
|
||||||
|
true => msg.reply_to(MessageId(mf.source_message_id)),
|
||||||
|
};
|
||||||
|
msg.await?;
|
||||||
|
|
||||||
|
let user_dialogue = BotDialogue::new(state_mgr, ChatId(mf.source_chat_id));
|
||||||
|
user_dialogue.update(State::MessageForwardReply).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn edit_msg_handler(
|
||||||
|
bot: Bot,
|
||||||
|
mut db: DB,
|
||||||
|
dialogue: BotDialogue,
|
||||||
|
(literal, variant, lang, is_caption_set): (String, Option<String>, String, bool),
|
||||||
|
msg: Message,
|
||||||
|
) -> BotResult<()> {
|
||||||
|
use teloxide::utils::render::Renderer;
|
||||||
|
|
||||||
|
let chat_id = msg.chat.id;
|
||||||
|
info!("Type: {:#?}", msg.kind);
|
||||||
|
let msg = if let MessageKind::Common(msg) = msg.kind {
|
||||||
|
msg
|
||||||
|
} else {
|
||||||
|
info!("Not a Common, somehow");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(variant) = variant {
|
||||||
|
if let MediaKind::Text(text) = msg.media_kind {
|
||||||
|
let html_text = Renderer::new(&text.text, &text.entities).as_html();
|
||||||
|
|
||||||
|
db.set_literal_alternative(&literal, &variant, &html_text)
|
||||||
|
.await?;
|
||||||
|
bot.send_message(chat_id, "Updated text of variant!")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
dialogue.exit().await?;
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
bot.send_message(
|
||||||
|
chat_id,
|
||||||
|
"On variants only text alternating supported. Try to send text only",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match msg.media_kind {
|
||||||
|
MediaKind::Text(text) => {
|
||||||
|
db.drop_media(&literal).await?;
|
||||||
|
if is_caption_set {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let html_text = Renderer::new(&text.text, &text.entities).as_html();
|
||||||
|
db.set_literal(&literal, &html_text).await?;
|
||||||
|
bot.send_message(chat_id, "Updated text of message!")
|
||||||
|
.await?;
|
||||||
|
dialogue.exit().await?;
|
||||||
|
}
|
||||||
|
MediaKind::Photo(photo) => {
|
||||||
|
let group = photo.media_group_id;
|
||||||
|
if let Some(group) = group.clone() {
|
||||||
|
db.drop_media_except(&literal, &group).await?;
|
||||||
|
} else {
|
||||||
|
db.drop_media(&literal).await?;
|
||||||
|
}
|
||||||
|
let file_id = photo.photo[0].file.id.clone();
|
||||||
|
db.add_media(&literal, "photo", &file_id, group.as_deref())
|
||||||
|
.await?;
|
||||||
|
match photo.caption {
|
||||||
|
Some(text) => {
|
||||||
|
let html_text = Renderer::new(&text, &photo.caption_entities).as_html();
|
||||||
|
db.set_literal(&literal, &html_text).await?;
|
||||||
|
bot.send_message(chat_id, "Updated photo caption!").await?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// if it is a first message in group,
|
||||||
|
// or just a photo without caption (unwrap_or case),
|
||||||
|
// set text empty
|
||||||
|
if !db
|
||||||
|
.is_media_group_exists(group.as_deref().unwrap_or(""))
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
db.set_literal(&literal, "").await?;
|
||||||
|
bot.send_message(chat_id, "Set photo without caption")
|
||||||
|
.await?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Some workaround because Telegram's group system
|
||||||
|
// is not easily and obviously handled with this
|
||||||
|
// code architecture, but probably there is a solution.
|
||||||
|
//
|
||||||
|
// So, this code will just wait for all media group
|
||||||
|
// updates to be processed
|
||||||
|
dialogue
|
||||||
|
.update(State::Edit {
|
||||||
|
literal,
|
||||||
|
variant: None,
|
||||||
|
lang,
|
||||||
|
is_caption_set: true,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
dialogue.exit().await.unwrap_or(());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
MediaKind::Video(video) => {
|
||||||
|
let group = video.media_group_id;
|
||||||
|
if let Some(group) = group.clone() {
|
||||||
|
db.drop_media_except(&literal, &group).await?;
|
||||||
|
} else {
|
||||||
|
db.drop_media(&literal).await?;
|
||||||
|
}
|
||||||
|
let file_id = video.video.file.id;
|
||||||
|
db.add_media(&literal, "video", &file_id, group.as_deref())
|
||||||
|
.await?;
|
||||||
|
match video.caption {
|
||||||
|
Some(text) => {
|
||||||
|
let html_text = Renderer::new(&text, &video.caption_entities).as_html();
|
||||||
|
db.set_literal(&literal, &html_text).await?;
|
||||||
|
bot.send_message(chat_id, "Updated video caption!").await?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// if it is a first message in group,
|
||||||
|
// or just a video without caption (unwrap_or case),
|
||||||
|
// set text empty
|
||||||
|
if !db
|
||||||
|
.is_media_group_exists(group.as_deref().unwrap_or(""))
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
db.set_literal(&literal, "").await?;
|
||||||
|
bot.send_message(chat_id, "Set video without caption")
|
||||||
|
.await?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Some workaround because Telegram's group system
|
||||||
|
// is not easily and obviously handled with this
|
||||||
|
// code architecture, but probably there is a solution.
|
||||||
|
//
|
||||||
|
// So, this code will just wait for all media group
|
||||||
|
// updates to be processed
|
||||||
|
dialogue
|
||||||
|
.update(State::Edit {
|
||||||
|
literal,
|
||||||
|
variant: None,
|
||||||
|
lang,
|
||||||
|
is_caption_set: true,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
dialogue.exit().await.unwrap_or(());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
bot.send_message(chat_id, "this type of message is not supported yet")
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn user_reply_to_support(bot: Bot, mut db: DB, msg: Message) -> BotResult<()> {
|
||||||
|
let (source_chat_id, source_message_id) = (msg.chat.id.0, msg.id.0);
|
||||||
|
let text = match msg.html_text() {
|
||||||
|
Some(text) => text,
|
||||||
|
// TODO: come up with better idea than just ignoring (say something to user)
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
let scid =
|
||||||
|
db.get_literal_value("support_chat_id")
|
||||||
|
.await?
|
||||||
|
.ok_or(BotError::AdminMisconfiguration(
|
||||||
|
"support_chat_id is not set".to_string(),
|
||||||
|
))?;
|
||||||
|
let support_chat_id = match scid.parse::<i64>() {
|
||||||
|
Ok(cid) => cid,
|
||||||
|
Err(parseerr) => {
|
||||||
|
return Err(BotError::BotLogicError(format!(
|
||||||
|
"source_chat_id, got: {scid}, expected: i64, err: {parseerr}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let user = msg.from.ok_or(BotError::BotLogicError(
|
||||||
|
"Unable to get user somehow:/".to_string(),
|
||||||
|
))?;
|
||||||
|
let parts = [
|
||||||
|
Some(user.first_name),
|
||||||
|
user.last_name,
|
||||||
|
user.username.map(|un| format!("(@{un})")),
|
||||||
|
];
|
||||||
|
#[allow(unstable_name_collisions)]
|
||||||
|
let userformat: String = parts
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.intersperse(" ".to_string())
|
||||||
|
.collect();
|
||||||
|
let msgtext = format!("From: {userformat}\nMessage:\n{text}");
|
||||||
|
|
||||||
|
// TODO: fix bug: parse mode's purpose is to display user-formated text in right way,
|
||||||
|
// but there is a bug: user can inject html code with his first/last/user name
|
||||||
|
// it's not harmful, only visible to support, but still need a fix
|
||||||
|
let sentmsg = bot
|
||||||
|
.send_message(ChatId(support_chat_id), msgtext)
|
||||||
|
.parse_mode(ParseMode::Html)
|
||||||
|
.await?;
|
||||||
|
MessageForward::new(
|
||||||
|
sentmsg.chat.id.0,
|
||||||
|
sentmsg.id.0,
|
||||||
|
source_chat_id,
|
||||||
|
source_message_id,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.store(&mut db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
1
src/handlers/mod.rs
Normal file
1
src/handlers/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod admin;
|
||||||
469
src/main.rs
469
src/main.rs
@ -4,6 +4,7 @@ pub mod bot_manager;
|
|||||||
pub mod botscript;
|
pub mod botscript;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
pub mod handlers;
|
||||||
pub mod message_answerer;
|
pub mod message_answerer;
|
||||||
pub mod mongodb_storage;
|
pub mod mongodb_storage;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
@ -222,61 +223,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
})
|
})
|
||||||
.endpoint(botscript_command_handler),
|
.endpoint(botscript_command_handler),
|
||||||
)
|
)
|
||||||
.branch(
|
// .branch(
|
||||||
Update::filter_callback_query()
|
// Update::filter_message()
|
||||||
.filter_async(async |q: CallbackQuery, mut db: DB| {
|
// .enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
||||||
let tguser = q.from.clone();
|
// .branch(dptree::case![State::MessageForwardReply].endpoint(user_reply_to_support)),
|
||||||
let user = db
|
// )
|
||||||
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
|
||||||
.await;
|
|
||||||
user.map(|u| u.is_admin).unwrap_or(false)
|
|
||||||
})
|
|
||||||
.enter_dialogue::<CallbackQuery, MongodbStorage<Json>, State>()
|
|
||||||
.branch(dptree::case![State::EditButton].endpoint(button_edit_callback)),
|
|
||||||
)
|
|
||||||
.branch(Update::filter_callback_query().endpoint(callback_handler))
|
|
||||||
.branch(command_handler(config))
|
|
||||||
.branch(
|
|
||||||
Update::filter_message()
|
|
||||||
.filter_async(async |msg: Message, mut db: DB| {
|
|
||||||
let tguser = match msg.from.clone() {
|
|
||||||
Some(user) => user,
|
|
||||||
None => return false, // do nothing, cause its not usecase of function
|
|
||||||
};
|
|
||||||
let user = db
|
|
||||||
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
|
||||||
.await;
|
|
||||||
user.map(|u| u.is_admin).unwrap_or(false)
|
|
||||||
})
|
|
||||||
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
|
||||||
.branch(
|
|
||||||
Update::filter_message()
|
|
||||||
.filter(|msg: Message| {
|
|
||||||
msg.text().unwrap_or("").to_lowercase().as_str() == "edit"
|
|
||||||
})
|
|
||||||
.endpoint(edit_msg_cmd_handler),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
Update::filter_message()
|
|
||||||
.filter(|msg: Message| msg.reply_to_message().is_some())
|
|
||||||
.filter(|state: State| matches!(state, State::Start))
|
|
||||||
.endpoint(support_reply_handler),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
dptree::case![State::Edit {
|
|
||||||
literal,
|
|
||||||
variant,
|
|
||||||
lang,
|
|
||||||
is_caption_set
|
|
||||||
}]
|
|
||||||
.endpoint(edit_msg_handler),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
Update::filter_message()
|
|
||||||
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
|
||||||
.branch(dptree::case![State::MessageForwardReply].endpoint(user_reply_to_support)),
|
|
||||||
)
|
|
||||||
.branch(Update::filter_message().endpoint(echo));
|
.branch(Update::filter_message().endpoint(echo));
|
||||||
|
|
||||||
Dispatcher::builder(bc.bot, handler)
|
Dispatcher::builder(bc.bot, handler)
|
||||||
@ -323,178 +274,6 @@ async fn botscript_command_handler(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn user_reply_to_support(bot: Bot, mut db: DB, msg: Message) -> BotResult<()> {
|
|
||||||
let (source_chat_id, source_message_id) = (msg.chat.id.0, msg.id.0);
|
|
||||||
let text = match msg.html_text() {
|
|
||||||
Some(text) => text,
|
|
||||||
// TODO: come up with better idea than just ignoring (say something to user)
|
|
||||||
None => return Ok(()),
|
|
||||||
};
|
|
||||||
let scid =
|
|
||||||
db.get_literal_value("support_chat_id")
|
|
||||||
.await?
|
|
||||||
.ok_or(BotError::AdminMisconfiguration(
|
|
||||||
"support_chat_id is not set".to_string(),
|
|
||||||
))?;
|
|
||||||
let support_chat_id = match scid.parse::<i64>() {
|
|
||||||
Ok(cid) => cid,
|
|
||||||
Err(parseerr) => {
|
|
||||||
return Err(BotError::BotLogicError(format!(
|
|
||||||
"source_chat_id, got: {scid}, expected: i64, err: {parseerr}"
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let user = msg.from.ok_or(BotError::BotLogicError(
|
|
||||||
"Unable to get user somehow:/".to_string(),
|
|
||||||
))?;
|
|
||||||
let parts = [
|
|
||||||
Some(user.first_name),
|
|
||||||
user.last_name,
|
|
||||||
user.username.map(|un| format!("(@{un})")),
|
|
||||||
];
|
|
||||||
#[allow(unstable_name_collisions)]
|
|
||||||
let userformat: String = parts
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.intersperse(" ".to_string())
|
|
||||||
.collect();
|
|
||||||
let msgtext = format!("From: {userformat}\nMessage:\n{text}");
|
|
||||||
|
|
||||||
// TODO: fix bug: parse mode's purpose is to display user-formated text in right way,
|
|
||||||
// but there is a bug: user can inject html code with his first/last/user name
|
|
||||||
// it's not harmful, only visible to support, but still need a fix
|
|
||||||
let sentmsg = bot
|
|
||||||
.send_message(ChatId(support_chat_id), msgtext)
|
|
||||||
.parse_mode(ParseMode::Html)
|
|
||||||
.await?;
|
|
||||||
MessageForward::new(
|
|
||||||
sentmsg.chat.id.0,
|
|
||||||
sentmsg.id.0,
|
|
||||||
source_chat_id,
|
|
||||||
source_message_id,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.store(&mut db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn support_reply_handler(
|
|
||||||
bot: Bot,
|
|
||||||
mut db: DB,
|
|
||||||
msg: Message,
|
|
||||||
state_mgr: std::sync::Arc<MongodbStorage<Json>>,
|
|
||||||
) -> BotResult<()> {
|
|
||||||
use teloxide::utils::render::Renderer;
|
|
||||||
|
|
||||||
let rm = match msg.reply_to_message() {
|
|
||||||
Some(rm) => rm,
|
|
||||||
None => {
|
|
||||||
return Err(BotError::BotLogicError(
|
|
||||||
"support_reply_handler should not be called when no message is replied".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let (chat_id, message_id) = (rm.chat.id.0, rm.id.0);
|
|
||||||
let mf = match MessageForward::get(&mut db, chat_id, message_id).await? {
|
|
||||||
Some(mf) => mf,
|
|
||||||
None => {
|
|
||||||
bot.send_message(msg.chat.id, "No forwarded message found for your reply")
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let text = match msg.kind {
|
|
||||||
MessageKind::Common(message_common) => match message_common.media_kind {
|
|
||||||
MediaKind::Text(media_text) => {
|
|
||||||
Renderer::new(&media_text.text, &media_text.entities).as_html()
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
bot.send_message(msg.chat.id, "Only text messages currently supported!")
|
|
||||||
.await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// can't hapen because we already have check for reply
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let msg = bot
|
|
||||||
.send_message(ChatId(mf.source_chat_id), text)
|
|
||||||
.parse_mode(ParseMode::Html);
|
|
||||||
let msg = match mf.reply {
|
|
||||||
false => msg,
|
|
||||||
true => msg.reply_to(MessageId(mf.source_message_id)),
|
|
||||||
};
|
|
||||||
msg.await?;
|
|
||||||
|
|
||||||
let user_dialogue = BotDialogue::new(state_mgr, ChatId(mf.source_chat_id));
|
|
||||||
user_dialogue.update(State::MessageForwardReply).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn button_edit_callback(
|
|
||||||
bot: Bot,
|
|
||||||
mut db: DB,
|
|
||||||
dialogue: BotDialogue,
|
|
||||||
q: CallbackQuery,
|
|
||||||
) -> BotResult<()> {
|
|
||||||
bot.answer_callback_query(&q.id).await?;
|
|
||||||
|
|
||||||
let id = match q.data {
|
|
||||||
Some(id) => id,
|
|
||||||
None => {
|
|
||||||
bot.send_message(q.from.id, "Not compatible callback to edit text on")
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let ci = match CallbackStore::get(&mut db, &id).await? {
|
|
||||||
Some(ci) => ci,
|
|
||||||
None => {
|
|
||||||
bot.send_message(
|
|
||||||
q.from.id,
|
|
||||||
"Can't get button information. Maybe created not by this bot or message too old",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let literal = match ci.literal {
|
|
||||||
Some(l) => l,
|
|
||||||
None => {
|
|
||||||
bot.send_message(
|
|
||||||
q.from.id,
|
|
||||||
"This button is not editable (probably text is generated)",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let lang = "ru".to_string();
|
|
||||||
dialogue
|
|
||||||
.update(State::Edit {
|
|
||||||
literal,
|
|
||||||
variant: None,
|
|
||||||
lang,
|
|
||||||
is_caption_set: false,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
bot.send_message(q.from.id, "Send text of button").await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn callback_handler(bot: Bot, mut db: DB, q: CallbackQuery) -> BotResult<()> {
|
async fn callback_handler(bot: Bot, mut db: DB, q: CallbackQuery) -> BotResult<()> {
|
||||||
bot.answer_callback_query(&q.id).await?;
|
bot.answer_callback_query(&q.id).await?;
|
||||||
|
|
||||||
@ -689,242 +468,6 @@ async fn notify_admin(text: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn edit_msg_cmd_handler(
|
|
||||||
bot: Bot,
|
|
||||||
mut db: DB,
|
|
||||||
dialogue: BotDialogue,
|
|
||||||
msg: Message,
|
|
||||||
) -> BotResult<()> {
|
|
||||||
match msg.reply_to_message() {
|
|
||||||
Some(replied) => {
|
|
||||||
let msgid = replied.id;
|
|
||||||
// look for message in db and set text
|
|
||||||
let literal = match db.get_message_literal(msg.chat.id.0, msgid.0).await? {
|
|
||||||
Some(l) => l,
|
|
||||||
None => {
|
|
||||||
bot.send_message(msg.chat.id, "No such message found to edit. Look if you replying bot's message and this message is supposed to be editable").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// TODO: language selector will be implemented in future 😈
|
|
||||||
let lang = "ru".to_string();
|
|
||||||
dialogue
|
|
||||||
.update(State::Edit {
|
|
||||||
literal,
|
|
||||||
variant: None,
|
|
||||||
lang,
|
|
||||||
is_caption_set: false,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
bot.send_message(
|
|
||||||
msg.chat.id,
|
|
||||||
"Ok, now you have to send message text (formatting supported)\n\
|
|
||||||
<b>Notice:</b> if this message supposed to replace message (tg shows them as edited) \
|
|
||||||
or be raplaced, do NOT send message with multiple media, only single photo, video etc. \
|
|
||||||
To get more information about why, see in /why_media_group",
|
|
||||||
).parse_mode(ParseMode::Html)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
bot.send_message(msg.chat.id, "You have to reply to message to edit it")
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn edit_msg_handler(
|
|
||||||
bot: Bot,
|
|
||||||
mut db: DB,
|
|
||||||
dialogue: BotDialogue,
|
|
||||||
(literal, variant, lang, is_caption_set): (String, Option<String>, String, bool),
|
|
||||||
msg: Message,
|
|
||||||
) -> BotResult<()> {
|
|
||||||
use teloxide::utils::render::Renderer;
|
|
||||||
|
|
||||||
let chat_id = msg.chat.id;
|
|
||||||
info!("Type: {:#?}", msg.kind);
|
|
||||||
let msg = if let MessageKind::Common(msg) = msg.kind {
|
|
||||||
msg
|
|
||||||
} else {
|
|
||||||
info!("Not a Common, somehow");
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(variant) = variant {
|
|
||||||
if let MediaKind::Text(text) = msg.media_kind {
|
|
||||||
let html_text = Renderer::new(&text.text, &text.entities).as_html();
|
|
||||||
|
|
||||||
db.set_literal_alternative(&literal, &variant, &html_text)
|
|
||||||
.await?;
|
|
||||||
bot.send_message(chat_id, "Updated text of variant!")
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
dialogue.exit().await?;
|
|
||||||
return Ok(());
|
|
||||||
} else {
|
|
||||||
bot.send_message(
|
|
||||||
chat_id,
|
|
||||||
"On variants only text alternating supported. Try to send text only",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match msg.media_kind {
|
|
||||||
MediaKind::Text(text) => {
|
|
||||||
db.drop_media(&literal).await?;
|
|
||||||
if is_caption_set {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let html_text = Renderer::new(&text.text, &text.entities).as_html();
|
|
||||||
db.set_literal(&literal, &html_text).await?;
|
|
||||||
bot.send_message(chat_id, "Updated text of message!")
|
|
||||||
.await?;
|
|
||||||
dialogue.exit().await?;
|
|
||||||
}
|
|
||||||
MediaKind::Photo(photo) => {
|
|
||||||
let group = photo.media_group_id;
|
|
||||||
if let Some(group) = group.clone() {
|
|
||||||
db.drop_media_except(&literal, &group).await?;
|
|
||||||
} else {
|
|
||||||
db.drop_media(&literal).await?;
|
|
||||||
}
|
|
||||||
let file_id = photo.photo[0].file.id.clone();
|
|
||||||
db.add_media(&literal, "photo", &file_id, group.as_deref())
|
|
||||||
.await?;
|
|
||||||
match photo.caption {
|
|
||||||
Some(text) => {
|
|
||||||
let html_text = Renderer::new(&text, &photo.caption_entities).as_html();
|
|
||||||
db.set_literal(&literal, &html_text).await?;
|
|
||||||
bot.send_message(chat_id, "Updated photo caption!").await?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// if it is a first message in group,
|
|
||||||
// or just a photo without caption (unwrap_or case),
|
|
||||||
// set text empty
|
|
||||||
if !db
|
|
||||||
.is_media_group_exists(group.as_deref().unwrap_or(""))
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
db.set_literal(&literal, "").await?;
|
|
||||||
bot.send_message(chat_id, "Set photo without caption")
|
|
||||||
.await?;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Some workaround because Telegram's group system
|
|
||||||
// is not easily and obviously handled with this
|
|
||||||
// code architecture, but probably there is a solution.
|
|
||||||
//
|
|
||||||
// So, this code will just wait for all media group
|
|
||||||
// updates to be processed
|
|
||||||
dialogue
|
|
||||||
.update(State::Edit {
|
|
||||||
literal,
|
|
||||||
variant: None,
|
|
||||||
lang,
|
|
||||||
is_caption_set: true,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
tokio::spawn(async move {
|
|
||||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
||||||
dialogue.exit().await.unwrap_or(());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
MediaKind::Video(video) => {
|
|
||||||
let group = video.media_group_id;
|
|
||||||
if let Some(group) = group.clone() {
|
|
||||||
db.drop_media_except(&literal, &group).await?;
|
|
||||||
} else {
|
|
||||||
db.drop_media(&literal).await?;
|
|
||||||
}
|
|
||||||
let file_id = video.video.file.id;
|
|
||||||
db.add_media(&literal, "video", &file_id, group.as_deref())
|
|
||||||
.await?;
|
|
||||||
match video.caption {
|
|
||||||
Some(text) => {
|
|
||||||
let html_text = Renderer::new(&text, &video.caption_entities).as_html();
|
|
||||||
db.set_literal(&literal, &html_text).await?;
|
|
||||||
bot.send_message(chat_id, "Updated video caption!").await?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// if it is a first message in group,
|
|
||||||
// or just a video without caption (unwrap_or case),
|
|
||||||
// set text empty
|
|
||||||
if !db
|
|
||||||
.is_media_group_exists(group.as_deref().unwrap_or(""))
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
db.set_literal(&literal, "").await?;
|
|
||||||
bot.send_message(chat_id, "Set video without caption")
|
|
||||||
.await?;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Some workaround because Telegram's group system
|
|
||||||
// is not easily and obviously handled with this
|
|
||||||
// code architecture, but probably there is a solution.
|
|
||||||
//
|
|
||||||
// So, this code will just wait for all media group
|
|
||||||
// updates to be processed
|
|
||||||
dialogue
|
|
||||||
.update(State::Edit {
|
|
||||||
literal,
|
|
||||||
variant: None,
|
|
||||||
lang,
|
|
||||||
is_caption_set: true,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
tokio::spawn(async move {
|
|
||||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
||||||
dialogue.exit().await.unwrap_or(());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
bot.send_message(chat_id, "this type of message is not supported yet")
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_handler(
|
|
||||||
config: Config,
|
|
||||||
) -> Handler<'static, DependencyMap, BotResult<()>, teloxide::dispatching::DpHandlerDescription> {
|
|
||||||
Update::filter_message()
|
|
||||||
.branch(
|
|
||||||
dptree::entry()
|
|
||||||
.filter_command::<UserCommands>()
|
|
||||||
.endpoint(user_command_handler),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
dptree::entry()
|
|
||||||
.filter_command::<SecretCommands>()
|
|
||||||
.map(move || config.admin_password.clone())
|
|
||||||
.endpoint(secret_command_handler),
|
|
||||||
)
|
|
||||||
.branch(
|
|
||||||
dptree::entry()
|
|
||||||
.filter_async(async |msg: Message, mut db: DB| {
|
|
||||||
let tguser = match msg.from.clone() {
|
|
||||||
Some(user) => user,
|
|
||||||
None => return false, // do nothing, cause its not usecase of function
|
|
||||||
};
|
|
||||||
let user = db
|
|
||||||
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
|
||||||
.await;
|
|
||||||
user.map(|u| u.is_admin).unwrap_or(false)
|
|
||||||
})
|
|
||||||
.filter_command::<AdminCommands>()
|
|
||||||
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
|
||||||
.endpoint(admin_command_handler),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn user_command_handler(
|
async fn user_command_handler(
|
||||||
mut db: DB,
|
mut db: DB,
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user