Dev (#1)
* init * bot framework done * here and ready for orm * might use sea-orm * orm done * use teloxide * ready to go? * 需要完成命令部分 * 需要完成:list_handler() * 查询用户名应当以@开头 * use rustls to avoid segfault? * postgresql ready * inline query done * list_handler * flattern code * test needed * ready to build * some bugs * almost done * ready to take off Co-authored-by: senset <dummy@dummy.d>
This commit is contained in:
21
src/callback_commands.rs
Normal file
21
src/callback_commands.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use teloxide::utils::command::BotCommands;
|
||||
|
||||
#[derive(PartialEq, Debug, BotCommands)]
|
||||
#[command(rename = "lowercase", prefix = "!")]
|
||||
pub enum CallbackCommands {
|
||||
#[command(description = "internal command page", parse_with = "split")]
|
||||
Page {
|
||||
msg_id: i32,
|
||||
username: String,
|
||||
page: usize,
|
||||
},
|
||||
|
||||
#[command(description = "default dummy command")]
|
||||
Default,
|
||||
}
|
||||
|
||||
impl Default for CallbackCommands {
|
||||
fn default() -> Self {
|
||||
CallbackCommands::Default
|
||||
}
|
||||
}
|
330
src/commands.rs
Normal file
330
src/commands.rs
Normal file
@@ -0,0 +1,330 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use strfmt::Format;
|
||||
use teloxide::{
|
||||
prelude::*,
|
||||
types::{InlineKeyboardButton, InlineKeyboardMarkup},
|
||||
types::{InlineKeyboardButtonKind, ReplyMarkup},
|
||||
utils::command::{BotCommands, ParseError},
|
||||
};
|
||||
use wd_log::log_debug_ln;
|
||||
|
||||
use crate::{
|
||||
db_controller::PaginatedRecordData,
|
||||
messages::{
|
||||
BOT_ABOUT, BOT_BUTTON_END, BOT_BUTTON_HEAD, BOT_BUTTON_NEXT, BOT_BUTTON_PREV, BOT_HELP,
|
||||
BOT_TEXT_DELETED, BOT_TEXT_LOADING, BOT_TEXT_MUTE_STATUS, BOT_TEXT_STATUS_OFF,
|
||||
BOT_TEXT_STATUS_ON, BOT_TEXT_WELCOME,
|
||||
},
|
||||
telegram_bot::BotServer,
|
||||
};
|
||||
|
||||
#[derive(BotCommands, PartialEq, Debug)]
|
||||
#[command(rename = "lowercase")]
|
||||
pub enum Commands {
|
||||
#[command(description = "显示帮助信息")]
|
||||
Help,
|
||||
|
||||
#[command(description = "关于本 Bot")]
|
||||
About,
|
||||
|
||||
#[command(description = "关闭提醒")]
|
||||
Mute,
|
||||
|
||||
#[command(description = "开启提醒")]
|
||||
Unmute,
|
||||
|
||||
#[command(description = "列出已记录的内容", parse_with = "list_command_parser")]
|
||||
List { username: String },
|
||||
|
||||
#[command(description = "删除记录")]
|
||||
Del { id: i64 },
|
||||
|
||||
#[command(description = "注册")]
|
||||
Start,
|
||||
}
|
||||
|
||||
impl Default for Commands {
|
||||
fn default() -> Self {
|
||||
Commands::Help
|
||||
}
|
||||
}
|
||||
|
||||
fn list_command_parser(input: String) -> Result<(String,), ParseError> {
|
||||
log_debug_ln!(
|
||||
"list_command_parse = \"{}\", is empty = {}",
|
||||
input,
|
||||
input.trim().is_empty()
|
||||
);
|
||||
|
||||
let output: String;
|
||||
|
||||
if input.trim().is_empty() {
|
||||
output = "me".to_string();
|
||||
} else {
|
||||
output = input
|
||||
}
|
||||
|
||||
Ok((output,))
|
||||
}
|
||||
|
||||
pub struct CommandHandler {}
|
||||
|
||||
impl CommandHandler {
|
||||
pub async fn about_handler(bot_s: &BotServer, message: &Message) {
|
||||
bot_s.send_text_reply(message, BOT_ABOUT).await;
|
||||
}
|
||||
|
||||
pub async fn help_handler(bot_s: &BotServer, message: &Message) {
|
||||
bot_s.send_text_reply(message, BOT_HELP).await;
|
||||
}
|
||||
|
||||
pub async fn notify_handler(bot_s: &BotServer, message: &Message, enabled: bool) {
|
||||
let user = match message.from() {
|
||||
Some(user) => user,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if user.is_bot {
|
||||
if let Err(error) = bot_s
|
||||
.controller
|
||||
.set_user_notify(&user.id.0.try_into().unwrap(), enabled)
|
||||
.await
|
||||
{
|
||||
bot_s.controller.err_handler(error);
|
||||
}
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert(
|
||||
"status".to_string(),
|
||||
match enabled {
|
||||
true => BOT_TEXT_STATUS_ON,
|
||||
false => BOT_TEXT_STATUS_OFF,
|
||||
},
|
||||
);
|
||||
bot_s
|
||||
.send_text_reply(message, &BOT_TEXT_MUTE_STATUS.format(&vars).unwrap())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn setup_handler(bot_s: &BotServer, message: &Message) {
|
||||
let user = match message.from() {
|
||||
Some(user) => user,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if user.is_bot {
|
||||
return;
|
||||
}
|
||||
|
||||
let user_id: i64 = user.id.0.try_into().unwrap();
|
||||
let username = match user.username.to_owned() {
|
||||
Some(username) => format!("@{}", username),
|
||||
None => user.first_name.to_owned(),
|
||||
};
|
||||
if let Err(error) = bot_s.controller.register_user(&user_id, &username).await {
|
||||
bot_s.controller.err_handler(error);
|
||||
}
|
||||
bot_s.send_text_reply(message, BOT_TEXT_WELCOME).await;
|
||||
}
|
||||
|
||||
pub async fn del_handler(bot_s: &BotServer, message: &Message, id: i64) {
|
||||
let user = match message.from() {
|
||||
Some(user) => user,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if user.is_bot {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(error) = bot_s
|
||||
.controller
|
||||
.del_record(id, user.id.0.try_into().unwrap())
|
||||
.await
|
||||
{
|
||||
bot_s.controller.err_handler(error);
|
||||
}
|
||||
|
||||
bot_s.send_text_reply(message, BOT_TEXT_DELETED).await;
|
||||
}
|
||||
|
||||
pub async fn list_handler(bot_s: &BotServer, message: &Message, username: &str, page: usize) {
|
||||
let user = match message.from() {
|
||||
Some(user) => user,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if user.is_bot {
|
||||
return;
|
||||
}
|
||||
|
||||
let msg_id = match bot_s.send_text_reply(message, BOT_TEXT_LOADING).await {
|
||||
Some(id) => id,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let (msg, markup) = match Self::record_msg_genrator(bot_s, message, username, page).await {
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
};
|
||||
|
||||
bot_s
|
||||
.edit_text_reply_with_inline_key(message, msg_id, msg.as_str(), markup)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn record_msg_genrator(
|
||||
bot_s: &BotServer,
|
||||
message: &Message,
|
||||
username: &str,
|
||||
page: usize,
|
||||
) -> Option<(String, ReplyMarkup)> {
|
||||
let someone = match bot_s.controller.get_user_by_username(username).await {
|
||||
Ok(someone) => someone,
|
||||
Err(error) => {
|
||||
bot_s.controller.err_handler(error);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let someone = match someone {
|
||||
Some(someone) => someone,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let data = match bot_s
|
||||
.controller
|
||||
.get_records_by_userid_with_pagination(someone.id, page)
|
||||
.await
|
||||
{
|
||||
Ok(data) => data,
|
||||
Err(error) => {
|
||||
bot_s.controller.err_handler(error);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let paginated_record_data = match data {
|
||||
Some(d) => d,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
Some((
|
||||
Self::generate_text_record_msg(&paginated_record_data, page),
|
||||
Self::generate_inline_keyboard(
|
||||
page,
|
||||
paginated_record_data.pages_count,
|
||||
username,
|
||||
message,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn generate_inline_keyboard(
|
||||
page: usize,
|
||||
pages_count: usize,
|
||||
username: &str,
|
||||
message: &Message,
|
||||
) -> ReplyMarkup {
|
||||
let inline_keyboards = match page {
|
||||
page if page == 0 && pages_count > 1 => vec![
|
||||
InlineKeyboardButton {
|
||||
text: BOT_BUTTON_NEXT.to_string(),
|
||||
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||
"!page {} {} {}",
|
||||
message.id,
|
||||
username,
|
||||
page + 1
|
||||
)),
|
||||
},
|
||||
InlineKeyboardButton {
|
||||
text: BOT_BUTTON_END.to_string(),
|
||||
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||
"!page {} {} {}",
|
||||
message.id,
|
||||
username,
|
||||
pages_count - 1
|
||||
)),
|
||||
},
|
||||
],
|
||||
page if page == 0 && pages_count <= 1 => vec![],
|
||||
page if page >= pages_count - 1 => vec![
|
||||
InlineKeyboardButton {
|
||||
text: BOT_BUTTON_HEAD.to_string(),
|
||||
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||
"!page {} {} {}",
|
||||
message.id, username, 0
|
||||
)),
|
||||
},
|
||||
InlineKeyboardButton {
|
||||
text: BOT_BUTTON_PREV.to_string(),
|
||||
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||
"!page {} {} {}",
|
||||
message.id,
|
||||
username,
|
||||
page - 1
|
||||
)),
|
||||
},
|
||||
],
|
||||
_ => vec![
|
||||
InlineKeyboardButton {
|
||||
text: BOT_BUTTON_HEAD.to_string(),
|
||||
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||
"!page {} {} {}",
|
||||
message.id, username, 0
|
||||
)),
|
||||
},
|
||||
InlineKeyboardButton {
|
||||
text: BOT_BUTTON_PREV.to_string(),
|
||||
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||
"!page {} {} {}",
|
||||
message.id,
|
||||
username,
|
||||
page - 1
|
||||
)),
|
||||
},
|
||||
InlineKeyboardButton {
|
||||
text: BOT_BUTTON_NEXT.to_string(),
|
||||
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||
"!page {} {} {}",
|
||||
message.id,
|
||||
username,
|
||||
page + 1
|
||||
)),
|
||||
},
|
||||
InlineKeyboardButton {
|
||||
text: BOT_BUTTON_END.to_string(),
|
||||
kind: InlineKeyboardButtonKind::CallbackData(format!(
|
||||
"!page {} {} {}",
|
||||
message.id,
|
||||
username,
|
||||
pages_count - 1
|
||||
)),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
ReplyMarkup::InlineKeyboard(InlineKeyboardMarkup {
|
||||
inline_keyboard: vec![inline_keyboards],
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_text_record_msg(
|
||||
paginated_record_data: &PaginatedRecordData,
|
||||
page: usize,
|
||||
) -> String {
|
||||
let mut msg = String::from("```");
|
||||
for (message, _) in paginated_record_data.current_data.iter() {
|
||||
msg = format!("{}\n{}\t\t\t\t{}", msg, message.id, message.message);
|
||||
}
|
||||
msg = format!(
|
||||
"{}\n```\n{}/{}",
|
||||
msg,
|
||||
page + 1,
|
||||
paginated_record_data.pages_count
|
||||
);
|
||||
|
||||
msg
|
||||
}
|
||||
}
|
19
src/config.rs
Normal file
19
src/config.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use clap::Parser;
|
||||
|
||||
const DEFAULT_DATABASE: &'static str = "sqlite:///saysthbot.db";
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
/// Enable debug mode
|
||||
#[clap(short = 'D', long, value_parser, default_value_t = false)]
|
||||
pub debug: bool,
|
||||
|
||||
/// Telegram bot token
|
||||
#[clap(short, long, value_parser, env = "TGBOT_TOKEN")]
|
||||
pub tgbot_token: String,
|
||||
|
||||
/// Database URI
|
||||
#[clap(short, long, value_parser, env = "DATABASE_URI", default_value=DEFAULT_DATABASE)]
|
||||
pub database_uri: String,
|
||||
}
|
190
src/db_controller.rs
Normal file
190
src/db_controller.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use models::prelude::*;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, Database, DatabaseConnection, DatabaseTransaction, DbErr,
|
||||
EntityTrait, PaginatorTrait, QueryFilter, Set, TransactionTrait,
|
||||
};
|
||||
use wd_log::{log_error_ln, log_info_ln, log_warn_ln};
|
||||
|
||||
const PAGE_SIZE: usize = 25;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Controller {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
pub struct PaginatedRecordData {
|
||||
pub items_count: usize,
|
||||
pub pages_count: usize,
|
||||
pub current_data: Vec<(RecordModel, Option<UserModel>)>,
|
||||
}
|
||||
|
||||
impl Controller {
|
||||
/// Create controller
|
||||
pub async fn new(config: String) -> Result<Self, DbErr> {
|
||||
Ok(Self {
|
||||
db: Database::connect(config).await?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Do migrate
|
||||
pub async fn migrate(&self) -> Result<(), DbErr> {
|
||||
if let Err(err) = Migrator::install(&self.db).await {
|
||||
log_warn_ln!("{}", err)
|
||||
}
|
||||
if let Err(err) = Migrator::up(&self.db, None).await {
|
||||
Err(err)
|
||||
} else {
|
||||
log_info_ln!("database initialized.");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// register user when `/start` command called.
|
||||
pub async fn register_user(&self, user_id: &i64, username: &String) -> Result<(), DbErr> {
|
||||
let transaction = self.db.begin().await?;
|
||||
self.setup_user(user_id, username, &transaction).await?;
|
||||
transaction.commit().await
|
||||
}
|
||||
|
||||
/// update user notify when `/mute` or `/unmute` command called.
|
||||
pub async fn set_user_notify(&self, user_id: &i64, notify: bool) -> Result<(), DbErr> {
|
||||
let transaction = self.db.begin().await?;
|
||||
if let Some(user) = self.get_user(user_id, &transaction).await? {
|
||||
let mut user_active: UserActiveModel = user.into();
|
||||
user_active.notify = Set(notify);
|
||||
user_active.save(&transaction).await?;
|
||||
}
|
||||
transaction.commit().await
|
||||
}
|
||||
|
||||
pub async fn get_user_notify(&self, user_id: &i64) -> Result<bool, DbErr> {
|
||||
let transaction = self.db.begin().await?;
|
||||
if let Some(user) = self.get_user(&user_id, &transaction).await? {
|
||||
Ok(user.notify)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
async fn setup_user(
|
||||
&self,
|
||||
user_id: &i64,
|
||||
username: &String,
|
||||
transaction: &DatabaseTransaction,
|
||||
) -> Result<UserActiveModel, DbErr> {
|
||||
match self.get_user(user_id, &transaction).await? {
|
||||
Some(user) => {
|
||||
let mut user_active: UserActiveModel = user.into();
|
||||
user_active.username = Set(Some(username.to_string()));
|
||||
user_active.save(transaction).await
|
||||
}
|
||||
None => {
|
||||
UserActiveModel {
|
||||
tg_uid: Set(user_id.to_owned()),
|
||||
username: Set(Some(username.to_string())),
|
||||
notify: Set(true),
|
||||
..Default::default()
|
||||
}
|
||||
.save(transaction)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_user(
|
||||
&self,
|
||||
user_id: &i64,
|
||||
transaction: &DatabaseTransaction,
|
||||
) -> Result<Option<UserModel>, DbErr> {
|
||||
User::find()
|
||||
.filter(UserColumn::Id.eq(user_id.to_owned()))
|
||||
.one(transaction)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_user_by_username(&self, username: &str) -> Result<Option<UserModel>, DbErr> {
|
||||
let transaction = self.db.begin().await?;
|
||||
User::find()
|
||||
.filter(UserColumn::Username.eq(username.to_owned()))
|
||||
.one(&transaction)
|
||||
.await
|
||||
}
|
||||
|
||||
/// get records when inline query called.
|
||||
pub async fn get_records_by_keywords(
|
||||
&self,
|
||||
key_word: &String,
|
||||
) -> Result<PaginatedRecordData, DbErr> {
|
||||
let pagination = Record::find()
|
||||
.find_also_related(User)
|
||||
.filter(RecordColumn::Message.contains(key_word.as_str()))
|
||||
.paginate(&self.db, PAGE_SIZE * 2); // 50 records seems ok.
|
||||
Ok(PaginatedRecordData {
|
||||
items_count: pagination.num_items().await?,
|
||||
pages_count: pagination.num_pages().await?,
|
||||
current_data: pagination.fetch().await?,
|
||||
})
|
||||
}
|
||||
|
||||
/// get records when `/list` command called or inline button request.
|
||||
pub async fn get_records_by_userid_with_pagination(
|
||||
&self,
|
||||
user_id: i64,
|
||||
page: usize,
|
||||
) -> Result<Option<PaginatedRecordData>, DbErr> {
|
||||
let transaction = self.db.begin().await?;
|
||||
if let Some(user) = self.get_user(&user_id, &transaction).await? {
|
||||
let pagination = Record::find()
|
||||
.find_also_related(User)
|
||||
.filter(RecordColumn::UserId.eq(user.id))
|
||||
.paginate(&transaction, PAGE_SIZE);
|
||||
Ok(Some(PaginatedRecordData {
|
||||
current_data: pagination.fetch_page(page).await?,
|
||||
items_count: pagination.num_items().await?,
|
||||
pages_count: pagination.num_pages().await?,
|
||||
}))
|
||||
} else {
|
||||
log_error_ln!("cannot find user tg_uid={}", user_id);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// add record forward a message to bot.
|
||||
pub async fn add_record(
|
||||
&self,
|
||||
user_id: i64,
|
||||
username: &String,
|
||||
text: String,
|
||||
) -> Result<(), DbErr> {
|
||||
let transaction = self.db.begin().await?;
|
||||
let user = self.setup_user(&user_id, &username, &transaction).await?;
|
||||
RecordActiveModel {
|
||||
message: Set(text),
|
||||
user_id: user.id,
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&transaction)
|
||||
.await?;
|
||||
transaction.commit().await
|
||||
}
|
||||
|
||||
/// del record when `/delete` command called.
|
||||
pub async fn del_record(&self, id: i64, user_id: i64) -> Result<(), DbErr> {
|
||||
let transaction = self.db.begin().await?;
|
||||
if let Some(user) = self.get_user(&user_id, &transaction).await? {
|
||||
RecordActiveModel {
|
||||
id: Set(id),
|
||||
user_id: Set(user.id),
|
||||
..Default::default()
|
||||
}
|
||||
.delete(&transaction)
|
||||
.await?;
|
||||
}
|
||||
transaction.commit().await
|
||||
}
|
||||
|
||||
pub fn err_handler(&self, error: DbErr) {
|
||||
log_error_ln!("{}", error);
|
||||
}
|
||||
}
|
36
src/main.rs
Normal file
36
src/main.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
mod callback_commands;
|
||||
mod commands;
|
||||
mod config;
|
||||
mod db_controller;
|
||||
mod messages;
|
||||
mod telegram_bot;
|
||||
|
||||
use clap::Parser;
|
||||
use config::Args;
|
||||
use telegram_bot::BotServer;
|
||||
use wd_log::{log_debug_ln, log_panic, set_level, set_prefix, DEBUG, INFO};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
set_prefix("saysthbot");
|
||||
|
||||
if args.debug {
|
||||
set_level(DEBUG);
|
||||
log_debug_ln!("{:?}", args);
|
||||
} else {
|
||||
set_level(INFO);
|
||||
}
|
||||
|
||||
let bot = match BotServer::new(args).await {
|
||||
Ok(bot) => bot,
|
||||
Err(err) => log_panic!("{}", err),
|
||||
};
|
||||
|
||||
if let Err(err) = bot.init().await {
|
||||
log_panic!("{}", err);
|
||||
}
|
||||
|
||||
bot.run().await;
|
||||
}
|
21
src/messages.rs
Normal file
21
src/messages.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
pub const BOT_TEXT_MESSAGE_ONLY: &'static str = "仅支持文本信息";
|
||||
pub const BOT_TEXT_FORWARDED_ONLY: &'static str = "仅支持转发信息";
|
||||
pub const BOT_TEXT_USER_ONLY: &'static str = "仅支持用户信息";
|
||||
pub const BOT_TEXT_NO_BOT: &'static str = "不支持 bot 消息";
|
||||
pub const BOT_TEXT_NOTED: &'static str = "✅ `{data}` 已记录";
|
||||
pub const BOT_TEXT_NOTICE: &'static str = "[{username}](tg://user?id={user_id}) 转发了你的 `{data}`\n\n\t你可以使用 /list 命令查看自己或者他人被记录的信息\n\t你可以使用 /del 命令删除某条自己的信息\n\t你也可以使用 /mute 或者 /unmute 命令开启或者关闭提醒";
|
||||
pub const BOT_TEXT_WELCOME: &'static str =
|
||||
"✅ 注册成功!如果有别人记录了你的消息,这里会有提醒,可使用 /mute 命令关闭提醒";
|
||||
pub const BOT_HELP: &'static str = "*帮助*\n\n\t/list `[@username]` 列出已记录的内容\n\t/del `id` 删除对应id的记录,只能删除自己的\n\t/mute 关闭提醒\n\t/unmute 开启提醒";
|
||||
pub const BOT_ABOUT: &'static str =
|
||||
"Say something bot \\- Reborn\n\n[Github](https://github.com/senseab/saysthbot-reborn) @ssthbot";
|
||||
pub const BOT_TEXT_MUTE_STATUS: &'static str = "提醒状态:{status}";
|
||||
pub const BOT_TEXT_STATUS_ON: &'static str = "✅ 开启";
|
||||
pub const BOT_TEXT_STATUS_OFF: &'static str = "❎ 关闭";
|
||||
pub const BOT_TEXT_DELETED: &'static str = "已删除";
|
||||
pub const BOT_TEXT_SHOULD_START_WITH_AT: &'static str = "用户名应当以 `@` 开头";
|
||||
pub const BOT_BUTTON_HEAD: &'static str = "⏮ 首页";
|
||||
pub const BOT_BUTTON_END: &'static str = "末页 ⏭";
|
||||
pub const BOT_BUTTON_PREV: &'static str = "⏪ 上一页";
|
||||
pub const BOT_BUTTON_NEXT: &'static str = "下一页 ⏩";
|
||||
pub const BOT_TEXT_LOADING: &'static str = "⌛️ 载入中……";
|
433
src/telegram_bot.rs
Normal file
433
src/telegram_bot.rs
Normal file
@@ -0,0 +1,433 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::callback_commands::CallbackCommands;
|
||||
use crate::db_controller::Controller;
|
||||
use crate::messages::*;
|
||||
use crate::{commands::CommandHandler, commands::Commands, config::Args};
|
||||
use migration::DbErr;
|
||||
use strfmt::Format;
|
||||
|
||||
use teloxide::utils::command::BotCommands;
|
||||
use teloxide::{
|
||||
prelude::*, types::ForwardedFrom, types::InlineQueryResult, types::InlineQueryResultArticle,
|
||||
types::InputMessageContent, types::InputMessageContentText, types::ParseMode,
|
||||
types::ReplyMarkup, types::UpdateKind, RequestError,
|
||||
};
|
||||
use wd_log::{log_debug_ln, log_error_ln, log_info_ln, log_panic, log_warn_ln};
|
||||
|
||||
pub struct BotServer {
|
||||
pub controller: Controller,
|
||||
bot: Bot,
|
||||
}
|
||||
|
||||
impl BotServer {
|
||||
/// Create new bot
|
||||
pub async fn new(config: Args) -> Result<Self, DbErr> {
|
||||
Ok(Self {
|
||||
bot: Bot::new(config.tgbot_token),
|
||||
controller: Controller::new(config.database_uri).await?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn init(&self) -> Result<(), DbErr> {
|
||||
self.controller.migrate().await
|
||||
}
|
||||
|
||||
/// Run the bot
|
||||
pub async fn run(&self) {
|
||||
match self.bot.get_me().send().await {
|
||||
Ok(result) => log_info_ln!(
|
||||
"connect succeed: id={}, botname=\"{}\"",
|
||||
result.id,
|
||||
result.username()
|
||||
),
|
||||
Err(error) => log_panic!("{}", error),
|
||||
}
|
||||
|
||||
self.register_commands().await;
|
||||
|
||||
let mut offset_id = 0;
|
||||
|
||||
loop {
|
||||
let updates = match self.bot.get_updates().offset(offset_id).send().await {
|
||||
Ok(it) => it,
|
||||
_ => continue,
|
||||
};
|
||||
for update in updates {
|
||||
self.update_handler(&update).await;
|
||||
offset_id = update.id + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn register_commands(&self) {
|
||||
if let Err(error) = self
|
||||
.bot
|
||||
.set_my_commands(Commands::bot_commands())
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
self.default_error_handler(&error);
|
||||
} else {
|
||||
log_info_ln!("commands registered")
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_handler(&self, update: &Update) {
|
||||
match &update.kind {
|
||||
UpdateKind::Message(ref message) => self.message_handler(message).await,
|
||||
UpdateKind::InlineQuery(inline_query) => self.inline_query_hander(inline_query).await,
|
||||
UpdateKind::CallbackQuery(callback) => self.callback_handler(callback).await,
|
||||
kind => self.default_update_hander(&kind).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn default_update_hander(&self, update_kind: &UpdateKind) {
|
||||
log_debug_ln!("non-supported kind {:?}", update_kind);
|
||||
}
|
||||
|
||||
async fn callback_handler(&self, callback: &CallbackQuery) {
|
||||
log_debug_ln!("callback={:#?}", callback);
|
||||
|
||||
let message = match &callback.message {
|
||||
Some(msg) => msg,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let text = match &callback.data {
|
||||
Some(text) => text,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let bot_username = match self.bot.get_me().send().await {
|
||||
Ok(result) => result.username.to_owned(),
|
||||
Err(error) => {
|
||||
self.default_error_handler(&error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let bot_username = match bot_username {
|
||||
Some(b) => b,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let commands = match CallbackCommands::parse(text, bot_username) {
|
||||
Ok(c) => c,
|
||||
Err(error) => {
|
||||
log_warn_ln!("{}", error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match commands {
|
||||
CallbackCommands::Page {
|
||||
msg_id: _,
|
||||
username,
|
||||
page,
|
||||
} => {
|
||||
let (msg, keyboard) = match CommandHandler::record_msg_genrator(
|
||||
self,
|
||||
message,
|
||||
username.as_str(),
|
||||
page,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
};
|
||||
|
||||
self.edit_text_reply_with_inline_key(message, message.id, msg.as_str(), keyboard)
|
||||
.await;
|
||||
|
||||
match self.bot.answer_callback_query(&callback.id).send().await {
|
||||
Ok(_) => (),
|
||||
Err(error) => self.default_error_handler(&error),
|
||||
}
|
||||
}
|
||||
CallbackCommands::Default => return,
|
||||
}
|
||||
}
|
||||
|
||||
async fn inline_query_hander(&self, inline_query: &InlineQuery) {
|
||||
let results = match self
|
||||
.controller
|
||||
.get_records_by_keywords(&inline_query.query)
|
||||
.await
|
||||
{
|
||||
Ok(results) => results,
|
||||
Err(error) => {
|
||||
self.controller.err_handler(error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut r: Vec<InlineQueryResult> = vec![];
|
||||
for (record, o_user) in results.current_data.iter() {
|
||||
let user = match o_user {
|
||||
Some(user) => user,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let username = match &user.username {
|
||||
Some(username) => username,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
r.push(InlineQueryResult::Article(InlineQueryResultArticle {
|
||||
id: record.id.to_string(),
|
||||
title: record.message.to_owned(),
|
||||
input_message_content: InputMessageContent::Text(InputMessageContentText {
|
||||
message_text: format!(
|
||||
"*{}*: {}",
|
||||
username.trim_start_matches("@"),
|
||||
record.message
|
||||
),
|
||||
parse_mode: Some(ParseMode::MarkdownV2),
|
||||
entities: None,
|
||||
disable_web_page_preview: Some(true),
|
||||
}),
|
||||
reply_markup: None,
|
||||
url: None,
|
||||
hide_url: None,
|
||||
description: Some(format!("By: {}", username)),
|
||||
thumb_url: None,
|
||||
thumb_width: None,
|
||||
thumb_height: None,
|
||||
}));
|
||||
}
|
||||
|
||||
if let Err(error) = self
|
||||
.bot
|
||||
.answer_inline_query(&inline_query.id, r.into_iter())
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
self.default_error_handler(&error);
|
||||
}
|
||||
}
|
||||
|
||||
async fn message_handler(&self, message: &Message) {
|
||||
if let Some(data) = &message.text() {
|
||||
self.text_message_heandler(message, data).await
|
||||
} else {
|
||||
self.default_message_handler(message).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn text_message_heandler(&self, message: &Message, data: &str) {
|
||||
let forward = match message.forward() {
|
||||
Some(forward) => forward,
|
||||
None => {
|
||||
if data.starts_with("/") {
|
||||
self.command_hanler(message).await;
|
||||
} else {
|
||||
self.send_text_reply(message, BOT_TEXT_FORWARDED_ONLY).await;
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match &forward.from {
|
||||
ForwardedFrom::User(user) if !user.is_bot => {
|
||||
let username = match &user.username {
|
||||
Some(username) => format!("@{}", username),
|
||||
None => user.first_name.to_owned(),
|
||||
};
|
||||
|
||||
if let Err(err) = self
|
||||
.controller
|
||||
.add_record(user.id.0.try_into().unwrap(), &username, data.to_string())
|
||||
.await
|
||||
{
|
||||
log_error_ln!("{}", err);
|
||||
return;
|
||||
}
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("data".to_string(), data);
|
||||
|
||||
self.send_text_reply(message, &BOT_TEXT_NOTED.format(&vars).unwrap())
|
||||
.await;
|
||||
|
||||
let from = match message.from() {
|
||||
Some(from) => from,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if from.id == user.id {
|
||||
return;
|
||||
}
|
||||
|
||||
if match self
|
||||
.controller
|
||||
.get_user_notify(&user.id.0.try_into().unwrap())
|
||||
.await
|
||||
{
|
||||
Ok(notify) => notify,
|
||||
Err(error) => {
|
||||
log_error_ln!("{}", error);
|
||||
return;
|
||||
}
|
||||
} {
|
||||
let mut vars = HashMap::new();
|
||||
let user_id = user.id.to_string();
|
||||
let data = data.to_string();
|
||||
|
||||
vars.insert("username".to_string(), &from.first_name);
|
||||
vars.insert("user_id".to_string(), &user_id);
|
||||
vars.insert("data".to_string(), &data);
|
||||
|
||||
match self
|
||||
.bot
|
||||
.send_message(user.id, &BOT_TEXT_NOTICE.format(&vars).unwrap())
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
log_debug_ln!("message sent {:?}", result)
|
||||
}
|
||||
Err(err) => self.default_error_handler(&err),
|
||||
}
|
||||
}
|
||||
}
|
||||
ForwardedFrom::User(_) => {
|
||||
self.send_text_reply(message, BOT_TEXT_NO_BOT).await;
|
||||
}
|
||||
_ => {
|
||||
self.send_text_message(message, BOT_TEXT_USER_ONLY).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn command_hanler(&self, message: &Message) {
|
||||
let msg = match message.text() {
|
||||
Some(msg) => msg,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let bot_username = match self.bot.get_me().send().await {
|
||||
Ok(result) => result.username.to_owned(),
|
||||
Err(error) => {
|
||||
self.default_error_handler(&error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let bot_username = match bot_username {
|
||||
Some(b) => b,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let commands = match Commands::parse(msg, bot_username) {
|
||||
Ok(c) => c,
|
||||
Err(error) => {
|
||||
log_warn_ln!("{}", error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match commands {
|
||||
Commands::Help => CommandHandler::help_handler(&self, message).await,
|
||||
Commands::About => CommandHandler::about_handler(&self, message).await,
|
||||
Commands::Mute => CommandHandler::notify_handler(&self, message, true).await,
|
||||
Commands::Unmute => CommandHandler::notify_handler(&self, message, false).await,
|
||||
Commands::List { mut username } => {
|
||||
if username == "me" {
|
||||
if let Some(from) = message.from() {
|
||||
if let Some(_username) = &from.username {
|
||||
username = format!("@{}", _username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if username.starts_with("@") {
|
||||
// always start from page=0
|
||||
CommandHandler::list_handler(&self, message, &username, 0).await;
|
||||
} else {
|
||||
self.send_text_reply(message, BOT_TEXT_SHOULD_START_WITH_AT)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Commands::Del { id } => CommandHandler::del_handler(&self, message, id).await,
|
||||
Commands::Start => CommandHandler::setup_handler(&self, message).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_error_handler(&self, error: &RequestError) {
|
||||
log_error_ln!("{:?}", error);
|
||||
}
|
||||
|
||||
async fn default_message_handler(&self, message: &Message) {
|
||||
log_debug_ln!(
|
||||
"non-spported message {:?} from `{:?}`",
|
||||
message.kind,
|
||||
message.from()
|
||||
);
|
||||
self.send_text_reply(message, BOT_TEXT_MESSAGE_ONLY).await;
|
||||
}
|
||||
|
||||
pub async fn send_text_message(&self, message: &Message, text: &str) -> Option<i32> {
|
||||
match &self
|
||||
.bot
|
||||
.send_message(message.chat.id, text)
|
||||
.parse_mode(ParseMode::MarkdownV2)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
log_debug_ln!("message sent {:?}", result);
|
||||
Some(result.id)
|
||||
}
|
||||
Err(error) => {
|
||||
self.default_error_handler(error);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_text_reply(&self, message: &Message, text: &str) -> Option<i32> {
|
||||
match &self
|
||||
.bot
|
||||
.send_message(message.chat.id, text)
|
||||
.reply_to_message_id(message.id)
|
||||
.parse_mode(ParseMode::MarkdownV2)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
log_debug_ln!("reply sent {:?}", result);
|
||||
Some(result.id)
|
||||
}
|
||||
Err(error) => {
|
||||
self.default_error_handler(error);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn edit_text_reply_with_inline_key(
|
||||
&self,
|
||||
message: &Message,
|
||||
msg_id: i32,
|
||||
text: &str,
|
||||
keyboard: ReplyMarkup,
|
||||
) {
|
||||
let keyboard = match keyboard {
|
||||
ReplyMarkup::InlineKeyboard(keyboard) => keyboard,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match &self
|
||||
.bot
|
||||
.edit_message_text(message.chat.id, msg_id, text)
|
||||
.reply_markup(keyboard)
|
||||
.parse_mode(ParseMode::MarkdownV2)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(result) => log_debug_ln!("reply sent {:?}", result),
|
||||
Err(error) => self.default_error_handler(error),
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user