Переписал бота с NestJS/TypeScript на Rust

Стек: teloxide + sqlx + axum + tokio-cron-scheduler.
Вся логика перенесена: /start, /help, /settings, выбор частоты,
cron-рассылка цитат, admin API. Совместимость с существующей БД
сохранена (camelCase колонки). Старый TypeScript-код удалён.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikhail Kilin
2026-02-16 01:55:20 +03:00
parent 0269e62f16
commit b885fd39b9
43 changed files with 4085 additions and 10826 deletions

146
src/bot.rs Normal file
View File

@@ -0,0 +1,146 @@
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))
}