mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-23 16:59:08 +00:00
191 lines
5.8 KiB
Rust
191 lines
5.8 KiB
Rust
use anyhow::Result;
|
|
use std::sync::Arc;
|
|
use teloxide::{Bot, prelude::*};
|
|
use tokio::sync::RwLock;
|
|
use uuid::Uuid;
|
|
|
|
use crate::database::DatabaseManager;
|
|
use crate::database::repository::TelegramConfigRepository;
|
|
use crate::database::entities::telegram_config::Model as TelegramConfig;
|
|
|
|
pub mod bot;
|
|
pub mod handlers;
|
|
pub mod error;
|
|
pub mod localization;
|
|
|
|
pub use error::TelegramError;
|
|
|
|
/// Main Telegram service that manages the bot lifecycle
|
|
pub struct TelegramService {
|
|
db: DatabaseManager,
|
|
bot: Arc<RwLock<Option<Bot>>>,
|
|
config: Arc<RwLock<Option<TelegramConfig>>>,
|
|
shutdown_signal: Arc<RwLock<Option<tokio::sync::oneshot::Sender<()>>>>,
|
|
}
|
|
|
|
impl TelegramService {
|
|
/// Create a new Telegram service
|
|
pub fn new(db: DatabaseManager) -> Self {
|
|
Self {
|
|
db,
|
|
bot: Arc::new(RwLock::new(None)),
|
|
config: Arc::new(RwLock::new(None)),
|
|
shutdown_signal: Arc::new(RwLock::new(None)),
|
|
}
|
|
}
|
|
|
|
/// Initialize and start the bot if active configuration exists
|
|
pub async fn initialize(&self) -> Result<()> {
|
|
let repo = TelegramConfigRepository::new(self.db.connection());
|
|
|
|
// Get active configuration
|
|
if let Some(config) = repo.get_active().await? {
|
|
self.start_with_config(config).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Start bot with specific configuration
|
|
pub async fn start_with_config(&self, config: TelegramConfig) -> Result<()> {
|
|
// Stop existing bot if running
|
|
self.stop().await?;
|
|
|
|
// Create new bot instance
|
|
let bot = Bot::new(&config.bot_token);
|
|
|
|
// Verify token by calling getMe
|
|
match bot.get_me().await {
|
|
Ok(me) => {
|
|
let username = me.user.username.unwrap_or_default();
|
|
tracing::info!("Telegram bot started: @{}", username);
|
|
}
|
|
Err(e) => {
|
|
return Err(anyhow::anyhow!("Invalid bot token: {}", e));
|
|
}
|
|
}
|
|
|
|
// Store bot and config
|
|
*self.bot.write().await = Some(bot.clone());
|
|
*self.config.write().await = Some(config.clone());
|
|
|
|
// Start polling in background
|
|
if config.is_active {
|
|
self.start_polling(bot).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Start polling for updates
|
|
async fn start_polling(&self, bot: Bot) -> Result<()> {
|
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
*self.shutdown_signal.write().await = Some(tx);
|
|
|
|
let db = self.db.clone();
|
|
|
|
// Spawn polling task
|
|
tokio::spawn(async move {
|
|
bot::run_polling(bot, db, rx).await;
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Stop the bot
|
|
pub async fn stop(&self) -> Result<()> {
|
|
// Send shutdown signal if polling is running
|
|
if let Some(tx) = self.shutdown_signal.write().await.take() {
|
|
let _ = tx.send(()); // Ignore error if receiver is already dropped
|
|
}
|
|
|
|
// Clear bot and config
|
|
*self.bot.write().await = None;
|
|
*self.config.write().await = None;
|
|
|
|
tracing::info!("Telegram bot stopped");
|
|
Ok(())
|
|
}
|
|
|
|
/// Update configuration and restart if needed
|
|
pub async fn update_config(&self, config_id: Uuid) -> Result<()> {
|
|
let repo = TelegramConfigRepository::new(self.db.connection());
|
|
|
|
if let Some(config) = repo.find_by_id(config_id).await? {
|
|
if config.is_active {
|
|
self.start_with_config(config).await?;
|
|
} else {
|
|
self.stop().await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get current bot status
|
|
pub async fn get_status(&self) -> BotStatus {
|
|
let bot_guard = self.bot.read().await;
|
|
let config_guard = self.config.read().await;
|
|
|
|
BotStatus {
|
|
is_running: bot_guard.is_some(),
|
|
config: config_guard.clone(),
|
|
}
|
|
}
|
|
|
|
/// Send message to user
|
|
pub async fn send_message(&self, chat_id: i64, text: String) -> Result<()> {
|
|
let bot_guard = self.bot.read().await;
|
|
|
|
if let Some(bot) = bot_guard.as_ref() {
|
|
bot.send_message(ChatId(chat_id), text).await?;
|
|
Ok(())
|
|
} else {
|
|
Err(anyhow::anyhow!("Bot is not running"))
|
|
}
|
|
}
|
|
|
|
/// Send message to user with inline keyboard
|
|
pub async fn send_message_with_keyboard(&self, chat_id: i64, text: String, keyboard: teloxide::types::InlineKeyboardMarkup) -> Result<()> {
|
|
let bot_guard = self.bot.read().await;
|
|
|
|
if let Some(bot) = bot_guard.as_ref() {
|
|
bot.send_message(ChatId(chat_id), text)
|
|
.parse_mode(teloxide::types::ParseMode::Html)
|
|
.reply_markup(keyboard)
|
|
.await?;
|
|
Ok(())
|
|
} else {
|
|
Err(anyhow::anyhow!("Bot is not running"))
|
|
}
|
|
}
|
|
|
|
/// Send message to all admins
|
|
pub async fn broadcast_to_admins(&self, text: String) -> Result<()> {
|
|
let bot_guard = self.bot.read().await;
|
|
|
|
if let Some(bot) = bot_guard.as_ref() {
|
|
let user_repo = crate::database::repository::UserRepository::new(self.db.connection());
|
|
let admins = user_repo.get_telegram_admins().await?;
|
|
|
|
for admin in admins {
|
|
if let Some(telegram_id) = admin.telegram_id {
|
|
if let Err(e) = bot.send_message(ChatId(telegram_id), text.clone()).await {
|
|
tracing::warn!("Failed to send message to admin {}: {}", telegram_id, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
} else {
|
|
Err(anyhow::anyhow!("Bot is not running"))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Bot status information
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct BotStatus {
|
|
pub is_running: bool,
|
|
pub config: Option<TelegramConfig>,
|
|
} |