This commit is contained in:
Sense T
2023-12-24 18:48:40 +08:00
commit 8ddd89cb51
26 changed files with 4574 additions and 0 deletions

167
src/commands.rs Normal file
View File

@@ -0,0 +1,167 @@
use std::{collections::HashMap, error::Error};
use rand::{rngs::OsRng, Rng};
use sea_orm::DbErr;
use strfmt::Format;
use teloxide::{
payloads::{SendMessage, SendMessageSetters},
prelude::Bot,
requests::{JsonRequest, Requester},
types::{Message, ParseMode},
utils::{command::BotCommands, markdown::escape},
};
use wd_log::log_error_ln;
use crate::{
config::Args,
db_controller::Controller,
messages::{
BOT_ABOUT, BOT_TEXT_HANGED, BOT_TEXT_IS_CHANNEL, BOT_TEXT_NO_TARGET, BOT_TEXT_TOP_GLOBAL,
BOT_TEXT_TOP_GROUP, BOT_TEXT_TOP_NONE, BOT_TEXT_TOP_TEMPLATE, BOT_TEXT_TOP_TITLE,
},
};
#[derive(BotCommands, PartialEq, Debug, Clone)]
#[command(rename_rule = "lowercase", description = "帮助:")]
pub enum Commands {
#[command(description = "显示帮助信息")]
Help,
#[command(description = "关于本 Bot")]
About,
#[command(description = "排行榜")]
Top,
#[command(description = "吊丫起来!")]
HangIt,
}
impl Default for Commands {
fn default() -> Self {
Commands::Help
}
}
#[derive(Debug, Clone)]
pub struct CommandHandler {
pub controller: Controller,
}
impl CommandHandler {
pub async fn new(config: &Args) -> Result<Self, DbErr> {
Ok(Self {
controller: Controller::new(config.database_uri.to_owned()).await?,
})
}
pub async fn init(&self) -> Result<(), DbErr> {
self.controller.migrate().await
}
async fn send_text_reply(
&self,
bot: &Bot,
message: &Message,
text: String,
) -> JsonRequest<SendMessage> {
bot.send_message(message.chat.id, text)
.reply_to_message_id(message.id)
.parse_mode(ParseMode::MarkdownV2)
}
pub async fn help_handler(
&self,
bot: &Bot,
message: &Message,
) -> Result<JsonRequest<SendMessage>, Box<dyn Error + Send + Sync>> {
Ok(self
.send_text_reply(bot, message, Commands::descriptions().to_string())
.await)
}
pub async fn about_handler(
&self,
bot: &Bot,
message: &Message,
) -> Result<JsonRequest<SendMessage>, Box<dyn Error + Send + Sync>> {
Ok(self
.send_text_reply(bot, message, BOT_ABOUT.to_string())
.await)
}
pub async fn hangit_handler(
&self,
bot: &Bot,
message: &Message,
) -> Result<JsonRequest<SendMessage>, Box<dyn Error + Send + Sync>> {
if let Some(reply) = message.reply_to_message() {
if let Some(user) = reply.from() {
let mut vars = HashMap::new();
let index = OsRng.gen::<usize>() % BOT_TEXT_HANGED.len();
let text = BOT_TEXT_HANGED[index];
vars.insert("name".to_string(), user.first_name.as_str());
let _ = self
.controller
.hangit(&user.full_name(), message.chat.id)
.await;
Ok(self
.send_text_reply(bot, reply, escape(&text.format(&vars).unwrap()))
.await)
} else {
Ok(self
.send_text_reply(bot, message, BOT_TEXT_IS_CHANNEL.to_string())
.await)
}
} else {
Ok(self
.send_text_reply(bot, message, BOT_TEXT_NO_TARGET.to_string())
.await)
}
}
pub async fn top_handler(
&self,
bot: &Bot,
message: &Message,
) -> Result<JsonRequest<SendMessage>, Box<dyn Error + Send + Sync>> {
let chat_id = message.chat.id;
let scope = match chat_id.is_group() {
true => BOT_TEXT_TOP_GROUP,
false => BOT_TEXT_TOP_GLOBAL,
};
let mut index = 1;
let mut text = format!("{}-{}\n\n", BOT_TEXT_TOP_TITLE, scope);
let results = match self.controller.top(chat_id).await {
Ok(r) => match r {
Some(result) => result,
None => {
return Ok(self
.send_text_reply(bot, message, BOT_TEXT_TOP_NONE.to_string())
.await)
}
},
Err(error) => {
log_error_ln!("{}", error);
return Err(Box::new(error));
}
};
for result in results {
let mut vars = HashMap::new();
vars.insert("name".to_string(), result.name);
vars.insert("count".to_string(), result.counts.to_string());
let record = BOT_TEXT_TOP_TEMPLATE.format(&vars).unwrap();
text = format!("{}{} {}\n", text, index, record);
index += 1;
}
Ok(self.send_text_reply(bot, message, text).await)
}
}

24
src/config.rs Normal file
View File

@@ -0,0 +1,24 @@
use clap::Parser;
const DEFAULT_DATABASE: &'static str = "sqlite:///hangitbot.db";
const DEFAULT_API_URL: &'static str = "https://api.telegram.org";
#[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,
/// Api Server URL
#[clap(long, value_parser, env = "API_URL", default_value=DEFAULT_API_URL)]
pub api_url: String,
}

90
src/db_controller.rs Normal file
View File

@@ -0,0 +1,90 @@
use migration::{Migrator, MigratorTrait};
use models::prelude::*;
use sea_orm::{
ActiveModelTrait, ColumnTrait, Database, DatabaseConnection, DbErr, EntityTrait,
PaginatorTrait, QueryFilter, QueryOrder, Set, TransactionTrait,
};
use teloxide::types::ChatId;
use wd_log::{log_info_ln, log_warn_ln};
#[derive(Debug, Clone)]
pub struct Controller {
db: DatabaseConnection,
}
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(())
}
}
/// hang someone
pub async fn hangit(&self, name: &String, group_id: ChatId) -> Result<(), DbErr> {
let transcation = self.db.begin().await?;
match Stats::find()
.filter(StatsColumn::GroupId.eq(group_id.0))
.filter(StatsColumn::Name.eq(name))
.one(&transcation)
.await?
{
Some(one) => {
let counts = one.counts;
let mut one_active: StatsActiveModel = one.into();
one_active.counts = Set(counts + 1);
one_active.save(&transcation).await?;
}
None => {
StatsActiveModel {
group_id: Set(group_id.0),
name: Set(name.to_string()),
counts: Set(1),
..Default::default()
}
.save(&transcation)
.await?;
}
}
transcation.commit().await
}
/// stats
pub async fn top(&self, group_id: ChatId) -> Result<Option<Vec<StatsModel>>, DbErr> {
const LIMIT: u64 = 10;
let transcation = self.db.begin().await?;
let query = match group_id.is_group() {
true => {
Stats::find()
.filter(StatsColumn::GroupId.eq(group_id.0))
.order_by_desc(StatsColumn::Counts)
.paginate(&transcation, LIMIT)
.fetch()
.await?
}
false => {
Stats::find()
.order_by_desc(StatsColumn::Counts)
.paginate(&transcation, LIMIT)
.fetch()
.await?
}
};
Ok(Some(query))
}
}

73
src/main.rs Normal file
View File

@@ -0,0 +1,73 @@
mod commands;
mod config;
mod db_controller;
mod messages;
use clap::Parser;
use commands::{CommandHandler, Commands};
use config::Args;
use teloxide::{
prelude::*,
requests::{Request, Requester},
utils::command::BotCommands,
};
use wd_log::{
log_debug_ln, log_error_ln, log_info_ln, log_panic, set_level, set_prefix, DEBUG, INFO,
};
#[tokio::main]
async fn main() {
let args = Args::parse();
set_prefix("hangitboot");
if args.debug {
set_level(DEBUG);
log_debug_ln!("{:?}", args);
} else {
set_level(INFO);
}
let command_handler = match CommandHandler::new(&args).await {
Err(err) => log_panic!("{}", err),
Ok(c) => c,
};
command_handler.init().await.unwrap();
let bot = Bot::new(args.tgbot_token.to_owned())
.set_api_url(reqwest::Url::parse(&args.api_url.as_str()).unwrap());
match bot.get_me().send().await {
Ok(result) => log_info_ln!(
"connect succeed: id={}, botname=\"{}\"",
result.id,
result.username()
),
Err(error) => log_panic!("{}", error),
}
register_commands(&bot).await;
Commands::repl(bot, move |bot: Bot, message: Message, cmd: Commands| {
let command_handler = command_handler.clone();
async move {
let _ = match cmd {
Commands::Help => command_handler.clone().help_handler(&bot, &message).await,
Commands::About => command_handler.clone().about_handler(&bot, &message).await,
Commands::Top => command_handler.clone().top_handler(&bot, &message).await,
Commands::HangIt => command_handler.clone().hangit_handler(&bot, &message).await,
};
Ok(())
}
})
.await;
}
async fn register_commands(bot: &Bot) {
if let Err(error) = bot.set_my_commands(Commands::bot_commands()).send().await {
log_error_ln!("{:?}", error);
} else {
log_info_ln!("commands registered")
}
}

27
src/messages.rs Normal file
View File

@@ -0,0 +1,27 @@
pub const BOT_ABOUT: &'static str =
"*Hang it bot*\n\nHang your boss up!\n[Github](https://github.com/senseab/saysthbot-reborn) @ssthbot";
pub const BOT_TEXT_NO_TARGET: &'static str = "请在回复某一条信息时使用该指令";
pub const BOT_TEXT_IS_CHANNEL: &'static str = "这不是个人,吊不起来";
pub const BOT_TEXT_TOP_TITLE: &'static str = "**吊人排行榜**";
pub const BOT_TEXT_TOP_GROUP: &'static str = "群内";
pub const BOT_TEXT_TOP_GLOBAL: &'static str = "全球";
pub const BOT_TEXT_TOP_NONE: &'static str = "无人上榜";
pub const BOT_TEXT_TOP_TEMPLATE: &'static str = "{name} 被吊了 {count} 次";
const BOT_TEXT_HANGED_1: &'static str = "{name} 被吊路灯了,绳子是昨天他卖出去的……";
const BOT_TEXT_HANGED_2: &'static str = "因为 {name} 太过逆天,我们把 TA 吊在了路灯上……";
const BOT_TEXT_HANGED_3: &'static str = "{name} 吊在了路灯上TA 兴风作浪的时代结束了……";
const BOT_TEXT_HANGED_4: &'static str = "吊在路灯上的 {name} 正在接受大家的鄙视……";
const BOT_TEXT_HANGED_5: &'static str = "对 {name} 来说,绳命来得快去得也快,只有路灯是永恒的……";
const BOT_TEXT_HANGED_6: &'static str =
"被套上麻袋的 {name} 在经历了一顿胖揍之后,最后还是成了路灯的挂件……";
pub const BOT_TEXT_HANGED: [&str; 6] = [
BOT_TEXT_HANGED_1,
BOT_TEXT_HANGED_2,
BOT_TEXT_HANGED_3,
BOT_TEXT_HANGED_4,
BOT_TEXT_HANGED_5,
BOT_TEXT_HANGED_6,
];

91
src/telegram_bot.rs Normal file
View File

@@ -0,0 +1,91 @@
use sea_orm::DbErr;
use teloxide::{
prelude::*,
requests::{Request, Requester},
types::{Message, UpdateKind},
utils::command::BotCommands,
RequestError,
};
use wd_log::{log_debug_ln, log_error_ln, log_info_ln, log_panic};
use crate::{
commands::{CommandHandler, Commands},
config::Args,
};
pub struct BotServer {
bot: Bot,
pub command_handler: CommandHandler,
}
impl BotServer {
/// Create new bot
pub async fn new(config: &Args) -> Result<Self, DbErr> {
Ok(Self {
bot: Bot::new(config.tgbot_token.to_owned())
.set_api_url(reqwest::Url::parse(&config.api_url.as_str()).unwrap()),
command_handler: CommandHandler::new(config).await?,
})
}
fn default_error_hander(&self, error: &RequestError) {
log_error_ln!("{:?}", error);
}
async fn register_commands(&self) {
if let Err(error) = self
.bot
.set_my_commands(Commands::bot_commands())
.send()
.await
{
self.default_error_hander(&error);
} else {
log_info_ln!("commands registered")
}
}
async fn default_update_handler(&self, update_kind: &UpdateKind) {
log_debug_ln!("non-supported kind {:?}", update_kind);
}
fn default_error_handler(&self, error: &RequestError) {
log_error_ln!("{:?}", error);
}
/// 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;
Commands::repl(
self.bot.to_owned(),
|bot: Bot, msg: Message, cmd: Commands| async move {},
)
.await;
}
/*
fn answer_generator(&self) -> impl Future<
Output = fn (Bot, Message, Commands) -> ResponseResult<()>> {
fn (bot: Bot, msg: Message, cmd: Commands) -> ResponseResult<()> {
async {
match cmd {
Commands::Help => Ok(self.command_handler.help_handler(&bot, &msg).await),
Commands::About => Ok(self.command_handler.about_handler(&bot, &msg).await),
Commands::Top => Ok(self.command_handler.top_handler(&bot, &msg).await),
Commands::HangIt => Ok(self.command_handler.hangit_handler(&bot, &msg).await),
}
}
}
}
*/
}