commit
667c6fb879
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
/Cargo.lock
|
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "rust-tbot"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
teloxide = { version = "0.13", features = ["macros"] }
|
||||||
|
log = "0.4"
|
||||||
|
env_logger = "0.9"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
clap = { version = "4.1", features = ["derive"] }
|
||||||
|
dirs = "4.0"
|
||||||
|
|
@ -0,0 +1,191 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use dirs::home_dir;
|
||||||
|
use log::{info}; // We'll call `info!()` for our logs
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use teloxide::dispatching::HandlerExt;
|
||||||
|
use teloxide::prelude::*;
|
||||||
|
use teloxide::types::{Message, UserId};
|
||||||
|
|
||||||
|
// Derive macro for commands:
|
||||||
|
use teloxide::macros::BotCommands as BotCommandsDerive;
|
||||||
|
// Trait for command descriptions():
|
||||||
|
use teloxide::utils::command::BotCommands as BotCommandsTrait;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(version, about)]
|
||||||
|
struct Cli {
|
||||||
|
/// Path to a YAML config file (defaults to ~/.config/rust-tbot.yaml)
|
||||||
|
#[arg(short = 'f', long = "file", value_name = "FILE")]
|
||||||
|
config_file: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Our YAML config format
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct BotConfig {
|
||||||
|
bot_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The commands we support.
|
||||||
|
#[derive(BotCommandsDerive, Clone)]
|
||||||
|
#[command(rename_rule = "lowercase", description = "These commands are supported:")]
|
||||||
|
enum Command {
|
||||||
|
/// Show your own user/chat ID
|
||||||
|
#[command(description = "Show your own user/chat ID.")]
|
||||||
|
Start,
|
||||||
|
/// Show help text
|
||||||
|
#[command(description = "Show help.")]
|
||||||
|
Help,
|
||||||
|
// Fallback for an unknown command
|
||||||
|
/// Unknown command
|
||||||
|
#[command(description = "Unknown command.")]
|
||||||
|
Unknown(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Initialize logging to stderr by default. If you want strictly stdout, see below.
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
// 1) Parse CLI flags
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
// 2) Determine config path
|
||||||
|
let config_path = match cli.config_file {
|
||||||
|
Some(path) => path,
|
||||||
|
None => default_config_path().ok_or("Couldn't determine default config path")?,
|
||||||
|
};
|
||||||
|
info!("Using config file: {:?}", config_path);
|
||||||
|
|
||||||
|
// 3) Read the config
|
||||||
|
let config = read_config(&config_path)?;
|
||||||
|
|
||||||
|
// 4) Create the bot
|
||||||
|
let bot = Bot::new(config.bot_token);
|
||||||
|
|
||||||
|
// 5) Build a dispatcher with two branches:
|
||||||
|
// A: new chat members,
|
||||||
|
// B: recognized commands (/start, /help, etc.)
|
||||||
|
let handler = dptree::entry()
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
.filter(|msg: Message| msg.new_chat_members().is_some())
|
||||||
|
.endpoint(handle_new_chat_members),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
// parse the text into Command
|
||||||
|
.filter_command::<Command>()
|
||||||
|
.endpoint(handle_commands),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6) Launch the dispatcher
|
||||||
|
Dispatcher::builder(bot, handler)
|
||||||
|
.enable_ctrlc_handler()
|
||||||
|
.build()
|
||||||
|
.dispatch()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect when the bot is added to a group, say the group ID, then leave.
|
||||||
|
async fn handle_new_chat_members(bot: Bot, msg: Message) -> Result<(), teloxide::RequestError> {
|
||||||
|
let me = bot.get_me().await?;
|
||||||
|
let chat_id = msg.chat.id;
|
||||||
|
|
||||||
|
if let Some(new_members) = msg.new_chat_members() {
|
||||||
|
for user in new_members {
|
||||||
|
if user.id == me.id {
|
||||||
|
// BOT was just added to a group
|
||||||
|
let output_text = format!("Group Chat ID is: {chat_id}");
|
||||||
|
bot.send_message(chat_id, &output_text).await?;
|
||||||
|
|
||||||
|
// Log the event to stdout/stderr
|
||||||
|
// There's no specific "user ID" or "username" in this scenario
|
||||||
|
// because the "actor" is the admin who added the bot,
|
||||||
|
// but Telegram doesn't always provide that in new_chat_members.
|
||||||
|
// We'll just log that we joined a group, posted the chat ID, then left.
|
||||||
|
info!(
|
||||||
|
"Bot added to group (chatID: {chat_id}). Output: \"{output_text}\". Leaving group now..."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Leave the group
|
||||||
|
bot.leave_chat(chat_id).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle commands in private chat
|
||||||
|
async fn handle_commands(
|
||||||
|
bot: Bot,
|
||||||
|
msg: Message,
|
||||||
|
cmd: Command,
|
||||||
|
) -> Result<(), teloxide::RequestError> {
|
||||||
|
let chat_id = msg.chat.id;
|
||||||
|
let (user_id, username) = extract_user_info(&msg);
|
||||||
|
|
||||||
|
match cmd {
|
||||||
|
Command::Start => {
|
||||||
|
if msg.chat.is_private() {
|
||||||
|
let text_out = format!("Your chat/user ID is: {chat_id}");
|
||||||
|
bot.send_message(chat_id, &text_out).await?;
|
||||||
|
|
||||||
|
// Log: username, chatID, userID, command, output
|
||||||
|
info!(
|
||||||
|
"User \"{username}\" (chatID: {chat_id}, userID: {user_id}) ran /start. \
|
||||||
|
Output => \"{text_out}\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Help => {
|
||||||
|
// The macro-generated `descriptions()` implements Display, so we `.to_string()`
|
||||||
|
let text_out = Command::descriptions().to_string();
|
||||||
|
bot.send_message(chat_id, &text_out).await?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"User \"{username}\" (chatID: {chat_id}, userID: {user_id}) ran /help. \
|
||||||
|
Output => \"{text_out}\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Command::Unknown(unknown) => {
|
||||||
|
// By default we do not respond, but we can log it
|
||||||
|
info!(
|
||||||
|
"User \"{username}\" (chatID: {chat_id}, userID: {user_id}) \
|
||||||
|
ran unknown command: /{unknown}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract user info from Message to log: user ID and username (if any)
|
||||||
|
fn extract_user_info(msg: &Message) -> (UserId, String) {
|
||||||
|
let user = msg.from.as_ref();
|
||||||
|
let user_id = user.map(|u| u.id).unwrap_or_else(|| UserId(0));
|
||||||
|
let username = user
|
||||||
|
.and_then(|u| u.username.clone())
|
||||||
|
.unwrap_or_else(|| "<no username>".to_string());
|
||||||
|
|
||||||
|
(user_id, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the YAML config from the given path.
|
||||||
|
fn read_config(path: &PathBuf) -> Result<BotConfig, Box<dyn std::error::Error>> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
let config: BotConfig = serde_yaml::from_reader(reader)?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default config path: `~/.config/rust-tbot.yaml`
|
||||||
|
fn default_config_path() -> Option<PathBuf> {
|
||||||
|
let home = home_dir()?;
|
||||||
|
Some(home.join(".config").join("rust-tbot.yaml"))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in new issue