Added telegram

This commit is contained in:
Ultradesu
2025-10-18 15:49:49 +03:00
parent e4984dd29d
commit 42c8016d9c
26 changed files with 2415 additions and 22 deletions

View File

@@ -0,0 +1,39 @@
use teloxide::{Bot, prelude::*};
use tokio::sync::oneshot;
use crate::database::DatabaseManager;
use super::handlers;
/// Run the bot polling loop
pub async fn run_polling(
bot: Bot,
db: DatabaseManager,
mut shutdown_rx: oneshot::Receiver<()>,
) {
tracing::info!("Starting Telegram bot polling...");
let handler = Update::filter_message()
.branch(
dptree::entry()
.filter_command::<handlers::Command>()
.endpoint(handlers::handle_command)
)
.branch(
dptree::endpoint(handlers::handle_message)
);
let mut dispatcher = Dispatcher::builder(bot.clone(), handler)
.dependencies(dptree::deps![db])
.enable_ctrlc_handler()
.build();
// Run dispatcher with shutdown signal
tokio::select! {
_ = dispatcher.dispatch() => {
tracing::info!("Telegram bot polling stopped");
}
_ = shutdown_rx => {
tracing::info!("Telegram bot received shutdown signal");
}
}
}

View File

@@ -0,0 +1,46 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum TelegramError {
#[error("Bot is not configured")]
NotConfigured,
#[error("Bot is not running")]
NotRunning,
#[error("Invalid bot token")]
InvalidToken,
#[error("User not found")]
UserNotFound,
#[error("User is not authorized")]
Unauthorized,
#[error("Database error: {0}")]
Database(String),
#[error("Telegram API error: {0}")]
TelegramApi(String),
#[error("Other error: {0}")]
Other(String),
}
impl From<teloxide::RequestError> for TelegramError {
fn from(err: teloxide::RequestError) -> Self {
Self::TelegramApi(err.to_string())
}
}
impl From<sea_orm::DbErr> for TelegramError {
fn from(err: sea_orm::DbErr) -> Self {
Self::Database(err.to_string())
}
}
impl From<anyhow::Error> for TelegramError {
fn from(err: anyhow::Error) -> Self {
Self::Other(err.to_string())
}
}

View File

@@ -0,0 +1,371 @@
use teloxide::{prelude::*, utils::command::BotCommands};
use teloxide::types::Me;
use uuid::Uuid;
use crate::database::DatabaseManager;
use crate::database::repository::UserRepository;
use crate::database::entities::user::CreateUserDto;
/// Available bot commands
#[derive(BotCommands, Clone)]
#[command(rename_rule = "lowercase", description = "Available commands:")]
pub enum Command {
#[command(description = "Start the bot and register")]
Start,
#[command(description = "Show help message")]
Help,
#[command(description = "Show your status")]
Status,
#[command(description = "List available configurations")]
Configs,
// Admin commands
#[command(description = "[Admin] List all users")]
Users,
#[command(description = "[Admin] List all servers")]
Servers,
#[command(description = "[Admin] Show statistics")]
Stats,
#[command(description = "[Admin] Broadcast message", parse_with = "split")]
Broadcast { message: String },
}
/// Handle command messages
pub async fn handle_command(
bot: Bot,
msg: Message,
cmd: Command,
db: DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let chat_id = msg.chat.id;
let from = msg.from.as_ref().ok_or("No sender info")?;
let telegram_id = from.id.0 as i64;
let user_repo = UserRepository::new(db.connection());
match cmd {
Command::Start => {
handle_start(bot, chat_id, telegram_id, from, &user_repo).await?;
}
Command::Help => {
bot.send_message(chat_id, Command::descriptions().to_string()).await?;
}
Command::Status => {
handle_status(bot, chat_id, telegram_id, &user_repo, &db).await?;
}
Command::Configs => {
handle_configs(bot, chat_id, telegram_id, &user_repo, &db).await?;
}
// Admin commands
Command::Users => {
if !user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
bot.send_message(chat_id, "❌ You are not authorized to use this command").await?;
return Ok(());
}
handle_users(bot, chat_id, &user_repo).await?;
}
Command::Servers => {
if !user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
bot.send_message(chat_id, "❌ You are not authorized to use this command").await?;
return Ok(());
}
handle_servers(bot, chat_id, &db).await?;
}
Command::Stats => {
if !user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
bot.send_message(chat_id, "❌ You are not authorized to use this command").await?;
return Ok(());
}
handle_stats(bot, chat_id, &db).await?;
}
Command::Broadcast { message } => {
if !user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
bot.send_message(chat_id, "❌ You are not authorized to use this command").await?;
return Ok(());
}
handle_broadcast(bot, chat_id, message, &user_repo).await?;
}
}
Ok(())
}
/// Handle regular text messages
pub async fn handle_message(
bot: Bot,
msg: Message,
db: DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Some(text) = msg.text() {
if !text.starts_with('/') {
bot.send_message(
msg.chat.id,
"Please use /help to see available commands"
).await?;
}
}
Ok(())
}
/// Handle /start command
async fn handle_start(
bot: Bot,
chat_id: ChatId,
telegram_id: i64,
from: &teloxide::types::User,
user_repo: &UserRepository,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Check if user already exists
if let Some(user) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
let message = format!(
"👋 Welcome back, {}!\n\n\
You are already registered.\n\
Use /help to see available commands.",
user.name
);
bot.send_message(chat_id, message).await?;
} else {
// Create new user
let username = from.username.as_deref().unwrap_or("Unknown");
let full_name = format!(
"{} {}",
from.first_name,
from.last_name.as_deref().unwrap_or("")
).trim().to_string();
let dto = CreateUserDto {
name: if !full_name.is_empty() { full_name } else { username.to_string() },
comment: Some(format!("Telegram user: @{}", username)),
telegram_id: Some(telegram_id),
is_telegram_admin: false,
};
match user_repo.create(dto).await {
Ok(user) => {
let message = format!(
"✅ Registration successful!\n\n\
Name: {}\n\
User ID: {}\n\n\
Use /help to see available commands.",
user.name, user.id
);
bot.send_message(chat_id, message).await?;
}
Err(e) => {
bot.send_message(
chat_id,
format!("❌ Registration failed: {}", e)
).await?;
}
}
}
Ok(())
}
/// Handle /status command
async fn handle_status(
bot: Bot,
chat_id: ChatId,
telegram_id: i64,
user_repo: &UserRepository,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Some(user) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
let server_inbound_repo = crate::database::repository::ServerInboundRepository::new(db.connection());
let configs = server_inbound_repo.find_by_user_id(user.id).await.unwrap_or_default();
let admin_status = if user.is_telegram_admin { "Admin" } else { "User" };
let message = format!(
"📊 Your Status\n\n\
Name: {}\n\
User ID: {}\n\
Role: {}\n\
Active Configs: {}\n\
Registered: {}",
user.name,
user.id,
admin_status,
configs.len(),
user.created_at.format("%Y-%m-%d %H:%M UTC")
);
bot.send_message(chat_id, message).await?;
} else {
bot.send_message(
chat_id,
"❌ You are not registered. Use /start to register."
).await?;
}
Ok(())
}
/// Handle /configs command
async fn handle_configs(
bot: Bot,
chat_id: ChatId,
telegram_id: i64,
user_repo: &UserRepository,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Some(user) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
let server_inbound_repo = crate::database::repository::ServerInboundRepository::new(db.connection());
let configs = server_inbound_repo.find_by_user_id(user.id).await.unwrap_or_default();
if configs.is_empty() {
bot.send_message(chat_id, "You don't have any configurations yet.").await?;
} else {
let mut message = String::from("📋 Your Configurations:\n\n");
for (i, config) in configs.iter().enumerate() {
message.push_str(&format!(
"{}. {} (Port: {})\n",
i + 1,
config.tag,
config.port_override.unwrap_or(0)
));
}
bot.send_message(chat_id, message).await?;
}
} else {
bot.send_message(
chat_id,
"❌ You are not registered. Use /start to register."
).await?;
}
Ok(())
}
/// Handle /users command (admin only)
async fn handle_users(
bot: Bot,
chat_id: ChatId,
user_repo: &UserRepository,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let users = user_repo.get_all(1, 100).await.unwrap_or_default();
if users.is_empty() {
bot.send_message(chat_id, "No users found.").await?;
} else {
let mut message = String::from("👥 Users:\n\n");
for (i, user) in users.iter().enumerate() {
let telegram_status = if user.telegram_id.is_some() { "" } else { "" };
let admin_status = if user.is_telegram_admin { " (Admin)" } else { "" };
message.push_str(&format!(
"{}. {} {} {}{}\n",
i + 1,
user.name,
telegram_status,
user.id,
admin_status
));
}
bot.send_message(chat_id, message).await?;
}
Ok(())
}
/// Handle /servers command (admin only)
async fn handle_servers(
bot: Bot,
chat_id: ChatId,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let server_repo = crate::database::repository::ServerRepository::new(db.connection());
let servers = server_repo.get_all().await.unwrap_or_default();
if servers.is_empty() {
bot.send_message(chat_id, "No servers found.").await?;
} else {
let mut message = String::from("🖥️ Servers:\n\n");
for (i, server) in servers.iter().enumerate() {
let status = if server.status == "active" { "" } else { "" };
message.push_str(&format!(
"{}. {} {} - {}\n",
i + 1,
status,
server.name,
server.hostname
));
}
bot.send_message(chat_id, message).await?;
}
Ok(())
}
/// Handle /stats command (admin only)
async fn handle_stats(
bot: Bot,
chat_id: ChatId,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let user_repo = UserRepository::new(db.connection());
let server_repo = crate::database::repository::ServerRepository::new(db.connection());
let inbound_repo = crate::database::repository::ServerInboundRepository::new(db.connection());
let user_count = user_repo.count().await.unwrap_or(0);
let server_count = server_repo.count().await.unwrap_or(0);
let inbound_count = inbound_repo.count().await.unwrap_or(0);
let message = format!(
"📊 Statistics\n\n\
Total Users: {}\n\
Total Servers: {}\n\
Total Inbounds: {}",
user_count,
server_count,
inbound_count
);
bot.send_message(chat_id, message).await?;
Ok(())
}
/// Handle /broadcast command (admin only)
async fn handle_broadcast(
bot: Bot,
chat_id: ChatId,
message: String,
user_repo: &UserRepository,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let users = user_repo.get_all(1, 1000).await.unwrap_or_default();
let mut sent_count = 0;
let mut failed_count = 0;
for user in users {
if let Some(telegram_id) = user.telegram_id {
match bot.send_message(ChatId(telegram_id), &message).await {
Ok(_) => sent_count += 1,
Err(e) => {
tracing::warn!("Failed to send broadcast to {}: {}", telegram_id, e);
failed_count += 1;
}
}
}
}
bot.send_message(
chat_id,
format!(
"✅ Broadcast complete\n\
Sent: {}\n\
Failed: {}",
sent_count, failed_count
)
).await?;
Ok(())
}

View File

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