Стек: teloxide + sqlx + axum + tokio-cron-scheduler. Вся логика перенесена: /start, /help, /settings, выбор частоты, cron-рассылка цитат, admin API. Совместимость с существующей БД сохранена (camelCase колонки). Старый TypeScript-код удалён. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
147 lines
5.3 KiB
Rust
147 lines
5.3 KiB
Rust
use std::sync::Arc;
|
||
|
||
use teloxide::dispatching::UpdateHandler;
|
||
use teloxide::prelude::*;
|
||
use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, KeyboardMarkup};
|
||
use teloxide::utils::command::BotCommands;
|
||
|
||
use crate::app_state::AppState;
|
||
use crate::db;
|
||
|
||
type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||
|
||
#[derive(BotCommands, Clone)]
|
||
#[command(rename_rule = "lowercase")]
|
||
pub enum Command {
|
||
#[command(description = "Запуск бота")]
|
||
Start,
|
||
#[command(description = "Справка")]
|
||
Help,
|
||
#[command(description = "Настройки частоты цитат")]
|
||
Settings,
|
||
}
|
||
|
||
/// Постоянная клавиатура с кнопкой "⚙️ Настройки"
|
||
fn settings_keyboard() -> KeyboardMarkup {
|
||
KeyboardMarkup::new(vec![vec![KeyboardButton::new("⚙️ Настройки")]]).resize_keyboard()
|
||
}
|
||
|
||
/// Inline-клавиатура выбора частоты
|
||
fn frequency_keyboard() -> InlineKeyboardMarkup {
|
||
InlineKeyboardMarkup::new(vec![
|
||
vec![
|
||
InlineKeyboardButton::callback("1 час", "frequency_1"),
|
||
InlineKeyboardButton::callback("3 часа", "frequency_3"),
|
||
],
|
||
vec![
|
||
InlineKeyboardButton::callback("5 часов", "frequency_5"),
|
||
InlineKeyboardButton::callback("7 часов", "frequency_7"),
|
||
],
|
||
vec![
|
||
InlineKeyboardButton::callback("9 часов", "frequency_9"),
|
||
InlineKeyboardButton::callback("12 часов", "frequency_12"),
|
||
],
|
||
])
|
||
}
|
||
|
||
/// Обработка команд /start, /help, /settings
|
||
async fn command_handler(
|
||
bot: Bot,
|
||
msg: Message,
|
||
cmd: Command,
|
||
state: Arc<AppState>,
|
||
) -> HandlerResult {
|
||
match cmd {
|
||
Command::Start => {
|
||
if let Some(user) = msg.from {
|
||
let fio = if let Some(ref last) = user.last_name {
|
||
format!("{} {}", user.first_name, last)
|
||
} else {
|
||
user.first_name.clone()
|
||
};
|
||
db::upsert_user(
|
||
&state.pool,
|
||
user.id.0 as i64,
|
||
Some(&fio),
|
||
user.username.as_deref(),
|
||
)
|
||
.await;
|
||
}
|
||
bot.send_message(
|
||
msg.chat.id,
|
||
"Приветствую тебя, мой дорогой друг. Я бот, который будет писать тебе мотивирующие цитаты. Сейчас цитаты буду приходит один раз в час, в настройках можно изменить это время.",
|
||
)
|
||
.reply_markup(settings_keyboard())
|
||
.await?;
|
||
}
|
||
Command::Help => {
|
||
bot.send_message(
|
||
msg.chat.id,
|
||
"Я буду присылать тебе мотивирующие цитаты. Используй меню для настроек.",
|
||
)
|
||
.reply_markup(settings_keyboard())
|
||
.await?;
|
||
}
|
||
Command::Settings => {
|
||
bot.send_message(msg.chat.id, "Выберите частоту получения цитат:")
|
||
.reply_markup(frequency_keyboard())
|
||
.await?;
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Обработка текстового сообщения "⚙️ Настройки" от reply-кнопки
|
||
async fn text_message_handler(bot: Bot, msg: Message) -> HandlerResult {
|
||
if let Some(text) = msg.text() {
|
||
if text == "⚙️ Настройки" {
|
||
bot.send_message(msg.chat.id, "Выберите частоту получения цитат:")
|
||
.reply_markup(frequency_keyboard())
|
||
.await?;
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Обработка callback-запроса frequency_N
|
||
async fn callback_handler(bot: Bot, q: CallbackQuery, state: Arc<AppState>) -> HandlerResult {
|
||
if let Some(ref data) = q.data {
|
||
if let Some(hours_str) = data.strip_prefix("frequency_") {
|
||
if let Ok(hours) = hours_str.parse::<i32>() {
|
||
let telegram_id = q.from.id.0 as i64;
|
||
db::update_frequency(&state.pool, telegram_id, hours).await;
|
||
|
||
bot.answer_callback_query(&q.id).await?;
|
||
|
||
if let Some(msg) = q.regular_message() {
|
||
bot.edit_message_text(
|
||
msg.chat.id,
|
||
msg.id,
|
||
format!(
|
||
"Отлично! Теперь я буду присылать цитаты каждые {} ч.",
|
||
hours
|
||
),
|
||
)
|
||
.await?;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Собрать dptree-обработчик для Dispatcher
|
||
pub fn build_handler() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||
dptree::entry()
|
||
.branch(
|
||
Update::filter_message()
|
||
.branch(
|
||
dptree::entry()
|
||
.filter_command::<Command>()
|
||
.endpoint(command_handler),
|
||
)
|
||
.branch(dptree::endpoint(text_message_handler)),
|
||
)
|
||
.branch(Update::filter_callback_query().endpoint(callback_handler))
|
||
}
|