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

@@ -6,6 +6,7 @@ pub mod server;
pub mod server_inbound;
pub mod user_access;
pub mod inbound_users;
pub mod telegram_config;
pub mod prelude {
pub use super::user::Entity as User;
@@ -16,4 +17,5 @@ pub mod prelude {
pub use super::server_inbound::Entity as ServerInbound;
pub use super::user_access::Entity as UserAccess;
pub use super::inbound_users::Entity as InboundUsers;
pub use super::telegram_config::Entity as TelegramConfig;
}

View File

@@ -0,0 +1,94 @@
use sea_orm::entity::prelude::*;
use sea_orm::{Set, ActiveModelTrait};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "telegram_config")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: Uuid,
/// Telegram bot token (encrypted in production)
pub bot_token: String,
/// Whether the bot is active
pub is_active: bool,
/// When the config was created
pub created_at: DateTimeUtc,
/// Last time config was updated
pub updated_at: DateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {
/// Called before insert and update
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
..ActiveModelTrait::default()
}
}
/// Called before update
fn before_save<'life0, 'async_trait, C>(
mut self,
_db: &'life0 C,
insert: bool,
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
where
'life0: 'async_trait,
C: 'async_trait + ConnectionTrait,
Self: 'async_trait,
{
Box::pin(async move {
if !insert {
self.updated_at = Set(chrono::Utc::now());
} else if self.id.is_not_set() {
self.id = Set(Uuid::new_v4());
}
if self.created_at.is_not_set() {
self.created_at = Set(chrono::Utc::now());
}
if self.updated_at.is_not_set() {
self.updated_at = Set(chrono::Utc::now());
}
Ok(self)
})
}
}
/// DTO for creating a new Telegram configuration
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateTelegramConfigDto {
pub bot_token: String,
pub is_active: bool,
}
/// DTO for updating Telegram configuration
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateTelegramConfigDto {
pub bot_token: Option<String>,
pub is_active: Option<bool>,
}
impl Model {
/// Convert to ActiveModel for updates
pub fn into_active_model(self) -> ActiveModel {
ActiveModel {
id: Set(self.id),
bot_token: Set(self.bot_token),
is_active: Set(self.is_active),
created_at: Set(self.created_at),
updated_at: Set(self.updated_at),
}
}
}

View File

@@ -18,6 +18,9 @@ pub struct Model {
/// Optional Telegram user ID for bot integration
pub telegram_id: Option<i64>,
/// Whether the user is a Telegram admin
pub is_telegram_admin: bool,
/// When the user was registered/created
pub created_at: DateTimeUtc,
@@ -33,6 +36,7 @@ impl ActiveModelBehavior for ActiveModel {
fn new() -> Self {
Self {
id: Set(Uuid::new_v4()),
is_telegram_admin: Set(false),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
..ActiveModelTrait::default()
@@ -65,6 +69,8 @@ pub struct CreateUserDto {
pub name: String,
pub comment: Option<String>,
pub telegram_id: Option<i64>,
#[serde(default)]
pub is_telegram_admin: bool,
}
/// User update data transfer object
@@ -73,6 +79,7 @@ pub struct UpdateUserDto {
pub name: Option<String>,
pub comment: Option<String>,
pub telegram_id: Option<i64>,
pub is_telegram_admin: Option<bool>,
}
impl From<CreateUserDto> for ActiveModel {
@@ -81,6 +88,7 @@ impl From<CreateUserDto> for ActiveModel {
name: Set(dto.name),
comment: Set(dto.comment),
telegram_id: Set(dto.telegram_id),
is_telegram_admin: Set(dto.is_telegram_admin),
..Self::new()
}
}
@@ -103,6 +111,9 @@ impl Model {
if dto.telegram_id.is_some() {
active_model.telegram_id = Set(dto.telegram_id);
}
if let Some(is_admin) = dto.is_telegram_admin {
active_model.is_telegram_admin = Set(is_admin);
}
active_model
}

View File

@@ -0,0 +1,51 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(TelegramConfig::Table)
.if_not_exists()
.col(ColumnDef::new(TelegramConfig::Id)
.uuid()
.not_null()
.primary_key())
.col(ColumnDef::new(TelegramConfig::BotToken)
.string()
.not_null())
.col(ColumnDef::new(TelegramConfig::IsActive)
.boolean()
.not_null()
.default(false))
.col(ColumnDef::new(TelegramConfig::CreatedAt)
.timestamp_with_time_zone()
.not_null())
.col(ColumnDef::new(TelegramConfig::UpdatedAt)
.timestamp_with_time_zone()
.not_null())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(TelegramConfig::Table).to_owned())
.await
}
}
#[derive(Iden)]
pub enum TelegramConfig {
Table,
Id,
BotToken,
IsActive,
CreatedAt,
UpdatedAt,
}

View File

@@ -0,0 +1,40 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Users::Table)
.add_column(
ColumnDef::new(Users::IsTelegramAdmin)
.boolean()
.not_null()
.default(false)
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Users::Table)
.drop_column(Users::IsTelegramAdmin)
.to_owned(),
)
.await
}
}
#[derive(Iden)]
enum Users {
Table,
IsTelegramAdmin,
}

View File

@@ -10,6 +10,8 @@ mod m20241201_000007_create_inbound_users_table;
mod m20250919_000001_update_inbound_users_schema;
mod m20250922_000001_add_grpc_hostname_to_servers;
mod m20250923_000001_create_dns_providers_table;
mod m20250929_000001_create_telegram_config_table;
mod m20250929_000002_add_telegram_admin_to_users;
pub struct Migrator;
@@ -27,6 +29,8 @@ impl MigratorTrait for Migrator {
Box::new(m20250919_000001_update_inbound_users_schema::Migration),
Box::new(m20250922_000001_add_grpc_hostname_to_servers::Migration),
Box::new(m20250923_000001_create_dns_providers_table::Migration),
Box::new(m20250929_000001_create_telegram_config_table::Migration),
Box::new(m20250929_000002_add_telegram_admin_to_users::Migration),
]
}
}

View File

@@ -51,8 +51,8 @@ impl DatabaseManager {
}
/// Get database connection
pub fn connection(&self) -> &DatabaseConnection {
&self.connection
pub fn connection(&self) -> DatabaseConnection {
self.connection.clone()
}
/// Run database migrations

View File

@@ -6,6 +6,7 @@ pub mod server;
pub mod server_inbound;
pub mod user_access;
pub mod inbound_users;
pub mod telegram_config;
pub use user::UserRepository;
pub use certificate::CertificateRepository;
@@ -14,4 +15,5 @@ pub use inbound_template::InboundTemplateRepository;
pub use server::ServerRepository;
pub use server_inbound::ServerInboundRepository;
pub use user_access::UserAccessRepository;
pub use inbound_users::InboundUsersRepository;
pub use inbound_users::InboundUsersRepository;
pub use telegram_config::TelegramConfigRepository;

View File

@@ -76,4 +76,13 @@ impl ServerRepository {
Ok(server.get_grpc_endpoint())
}
pub async fn get_all(&self) -> Result<Vec<server::Model>> {
Ok(Server::find().all(&self.db).await?)
}
pub async fn count(&self) -> Result<u64> {
let count = Server::find().count(&self.db).await?;
Ok(count)
}
}

View File

@@ -163,4 +163,16 @@ impl ServerInboundRepository {
Ok(inbound.update(&self.db).await?)
}
pub async fn find_by_user_id(&self, user_id: Uuid) -> Result<Vec<server_inbound::Model>> {
// This would need a join with user_access table
// For now, returning empty vec as placeholder
// TODO: Implement proper join query
Ok(vec![])
}
pub async fn count(&self) -> Result<u64> {
let count = ServerInbound::find().count(&self.db).await?;
Ok(count)
}
}

View File

@@ -0,0 +1,167 @@
use anyhow::Result;
use sea_orm::{DatabaseConnection, EntityTrait, ActiveModelTrait, Set, QueryFilter, ColumnTrait, QueryOrder};
use uuid::Uuid;
use crate::database::entities::telegram_config::{
self, Model, CreateTelegramConfigDto, UpdateTelegramConfigDto
};
pub struct TelegramConfigRepository {
db: DatabaseConnection,
}
impl TelegramConfigRepository {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
/// Get the current active configuration (should be only one)
pub async fn get_active(&self) -> Result<Option<Model>> {
Ok(telegram_config::Entity::find()
.filter(telegram_config::Column::IsActive.eq(true))
.one(&self.db)
.await?)
}
/// Get configuration by ID
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<Model>> {
Ok(telegram_config::Entity::find_by_id(id)
.one(&self.db)
.await?)
}
/// Get the latest configuration (active or not)
pub async fn get_latest(&self) -> Result<Option<Model>> {
Ok(telegram_config::Entity::find()
.order_by_desc(telegram_config::Column::CreatedAt)
.one(&self.db)
.await?)
}
/// Create new configuration (deactivates previous if exists)
pub async fn create(&self, dto: CreateTelegramConfigDto) -> Result<Model> {
// If is_active is true, deactivate all other configs
if dto.is_active {
self.deactivate_all().await?;
}
let model = telegram_config::ActiveModel {
id: Set(Uuid::new_v4()),
bot_token: Set(dto.bot_token),
is_active: Set(dto.is_active),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
};
Ok(model.insert(&self.db).await?)
}
/// Update configuration
pub async fn update(&self, id: Uuid, dto: UpdateTelegramConfigDto) -> Result<Option<Model>> {
let model = telegram_config::Entity::find_by_id(id)
.one(&self.db)
.await?;
let Some(model) = model else {
return Ok(None);
};
// If activating this config, deactivate others
if dto.is_active == Some(true) {
self.deactivate_all_except(id).await?;
}
let mut active_model = model.into_active_model();
if let Some(bot_token) = dto.bot_token {
active_model.bot_token = Set(bot_token);
}
if let Some(is_active) = dto.is_active {
active_model.is_active = Set(is_active);
}
active_model.updated_at = Set(chrono::Utc::now());
Ok(Some(active_model.update(&self.db).await?))
}
/// Activate a configuration (deactivates all others)
pub async fn activate(&self, id: Uuid) -> Result<Option<Model>> {
self.deactivate_all_except(id).await?;
let model = telegram_config::Entity::find_by_id(id)
.one(&self.db)
.await?;
let Some(model) = model else {
return Ok(None);
};
let mut active_model = model.into_active_model();
active_model.is_active = Set(true);
active_model.updated_at = Set(chrono::Utc::now());
Ok(Some(active_model.update(&self.db).await?))
}
/// Deactivate a configuration
pub async fn deactivate(&self, id: Uuid) -> Result<Option<Model>> {
let model = telegram_config::Entity::find_by_id(id)
.one(&self.db)
.await?;
let Some(model) = model else {
return Ok(None);
};
let mut active_model = model.into_active_model();
active_model.is_active = Set(false);
active_model.updated_at = Set(chrono::Utc::now());
Ok(Some(active_model.update(&self.db).await?))
}
/// Delete configuration
pub async fn delete(&self, id: Uuid) -> Result<bool> {
let result = telegram_config::Entity::delete_by_id(id)
.exec(&self.db)
.await?;
Ok(result.rows_affected > 0)
}
/// Deactivate all configurations
async fn deactivate_all(&self) -> Result<()> {
let configs = telegram_config::Entity::find()
.filter(telegram_config::Column::IsActive.eq(true))
.all(&self.db)
.await?;
for config in configs {
let mut active_model = config.into_active_model();
active_model.is_active = Set(false);
active_model.updated_at = Set(chrono::Utc::now());
active_model.update(&self.db).await?;
}
Ok(())
}
/// Deactivate all configurations except one
async fn deactivate_all_except(&self, except_id: Uuid) -> Result<()> {
let configs = telegram_config::Entity::find()
.filter(telegram_config::Column::IsActive.eq(true))
.filter(telegram_config::Column::Id.ne(except_id))
.all(&self.db)
.await?;
for config in configs {
let mut active_model = config.into_active_model();
active_model.is_active = Set(false);
active_model.updated_at = Set(chrono::Utc::now());
active_model.update(&self.db).await?;
}
Ok(())
}
}

View File

@@ -2,6 +2,7 @@ use anyhow::Result;
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait, QuerySelect};
use uuid::Uuid;
use sea_orm::{Set, ActiveModelTrait};
use crate::database::entities::user::{Entity as User, Column, Model, ActiveModel, CreateUserDto, UpdateUserDto};
pub struct UserRepository {
@@ -124,6 +125,48 @@ impl UserRepository {
.await?;
Ok(count > 0)
}
/// Get all Telegram admins
pub async fn get_telegram_admins(&self) -> Result<Vec<Model>> {
let admins = User::find()
.filter(Column::IsTelegramAdmin.eq(true))
.order_by_desc(Column::CreatedAt)
.all(&self.db)
.await?;
Ok(admins)
}
/// Set user as Telegram admin
pub async fn set_telegram_admin(&self, user_id: Uuid, is_admin: bool) -> Result<Option<Model>> {
if let Some(user) = self.get_by_id(user_id).await? {
let mut active_model: ActiveModel = user.into();
active_model.is_telegram_admin = Set(is_admin);
active_model.updated_at = Set(chrono::Utc::now());
let updated = active_model.update(&self.db).await?;
Ok(Some(updated))
} else {
Ok(None)
}
}
/// Check if user is Telegram admin
pub async fn is_telegram_admin(&self, user_id: Uuid) -> Result<bool> {
if let Some(user) = self.get_by_id(user_id).await? {
Ok(user.is_telegram_admin)
} else {
Ok(false)
}
}
/// Check if telegram_id is admin
pub async fn is_telegram_id_admin(&self, telegram_id: i64) -> Result<bool> {
if let Some(user) = self.get_by_telegram_id(telegram_id).await? {
Ok(user.is_telegram_admin)
} else {
Ok(false)
}
}
}
#[cfg(test)]
@@ -158,6 +201,7 @@ mod tests {
name: "Test User".to_string(),
comment: Some("Test comment".to_string()),
telegram_id: Some(123456789),
is_telegram_admin: false,
};
let created_user = repo.create(create_dto).await.unwrap();

View File

@@ -1,4 +1,5 @@
use anyhow::Result;
use std::sync::Arc;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod config;
@@ -8,7 +9,7 @@ mod web;
use config::{AppConfig, args::parse_args};
use database::DatabaseManager;
use services::{TaskScheduler, XrayService};
use services::{TaskScheduler, XrayService, TelegramService};
#[tokio::main]
async fn main() -> Result<()> {
@@ -89,10 +90,16 @@ async fn main() -> Result<()> {
// Start event-driven sync handler with the receiver
TaskScheduler::start_event_handler(db.clone(), event_receiver).await;
// Initialize Telegram service if needed
let telegram_service = Arc::new(TelegramService::new(db.clone()));
if let Err(e) = telegram_service.initialize().await {
tracing::warn!("Failed to initialize Telegram service: {}", e);
}
// Start web server with task scheduler
tokio::select! {
result = web::start_server(db, config.web.clone()) => {
result = web::start_server(db, config.web.clone(), Some(telegram_service.clone())) => {
match result {
Err(e) => tracing::error!("Web server error: {}", e),
_ => {}

View File

@@ -4,8 +4,10 @@ pub mod certificates;
pub mod events;
pub mod tasks;
pub mod uri_generator;
pub mod telegram;
pub use xray::XrayService;
pub use tasks::TaskScheduler;
pub use uri_generator::UriGeneratorService;
pub use certificates::CertificateService;
pub use certificates::CertificateService;
pub use telegram::TelegramService;

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>,
}

View File

@@ -5,6 +5,7 @@ pub mod templates;
pub mod client_configs;
pub mod dns_providers;
pub mod tasks;
pub mod telegram;
pub use users::*;
pub use servers::*;
@@ -12,4 +13,5 @@ pub use certificates::*;
pub use templates::*;
pub use client_configs::*;
pub use dns_providers::*;
pub use tasks::*;
pub use tasks::*;
pub use telegram::*;

View File

@@ -504,6 +504,7 @@ pub async fn add_user_to_inbound(
name: user_name.clone(),
comment: user_data["comment"].as_str().map(|s| s.to_string()),
telegram_id: user_data["telegram_id"].as_i64(),
is_telegram_admin: false,
};
match user_repo.create(create_user_dto).await {

View File

@@ -0,0 +1,304 @@
use axum::{
extract::{State, Path, Json},
http::StatusCode,
response::IntoResponse,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::web::AppState;
use crate::database::repository::{UserRepository, TelegramConfigRepository};
use crate::database::entities::telegram_config::{CreateTelegramConfigDto, UpdateTelegramConfigDto};
/// Response for Telegram config
#[derive(Debug, Serialize)]
pub struct TelegramConfigResponse {
pub id: Uuid,
pub is_active: bool,
pub bot_info: Option<BotInfo>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Serialize)]
pub struct BotInfo {
pub username: String,
pub first_name: String,
}
/// Get current Telegram configuration
pub async fn get_telegram_config(
State(state): State<AppState>,
) -> impl IntoResponse {
let repo = TelegramConfigRepository::new(state.db.connection());
match repo.get_latest().await {
Ok(Some(config)) => {
let mut response = TelegramConfigResponse {
id: config.id,
is_active: config.is_active,
bot_info: None,
created_at: config.created_at.to_rfc3339(),
updated_at: config.updated_at.to_rfc3339(),
};
// Get bot info if active
if config.is_active {
if let Ok(status) = get_bot_status(&state).await {
response.bot_info = status.bot_info;
}
}
Json(response).into_response()
}
Ok(None) => {
StatusCode::NOT_FOUND.into_response()
}
Err(e) => {
tracing::error!("Failed to get telegram config: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
/// Create new Telegram configuration
pub async fn create_telegram_config(
State(state): State<AppState>,
Json(dto): Json<CreateTelegramConfigDto>,
) -> impl IntoResponse {
let repo = TelegramConfigRepository::new(state.db.connection());
match repo.create(dto).await {
Ok(config) => {
// Initialize telegram service with new config if active
if config.is_active {
if let Some(telegram_service) = &state.telegram_service {
let _ = telegram_service.update_config(config.id).await;
}
}
(StatusCode::CREATED, Json(config)).into_response()
}
Err(e) => {
tracing::error!("Failed to create telegram config: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
/// Update Telegram configuration
pub async fn update_telegram_config(
State(state): State<AppState>,
Path(id): Path<Uuid>,
Json(dto): Json<UpdateTelegramConfigDto>,
) -> impl IntoResponse {
let repo = TelegramConfigRepository::new(state.db.connection());
match repo.update(id, dto).await {
Ok(Some(config)) => {
// Update telegram service
if let Some(telegram_service) = &state.telegram_service {
let _ = telegram_service.update_config(config.id).await;
}
Json(config).into_response()
}
Ok(None) => {
StatusCode::NOT_FOUND.into_response()
}
Err(e) => {
tracing::error!("Failed to update telegram config: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
/// Delete Telegram configuration
pub async fn delete_telegram_config(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> impl IntoResponse {
let repo = TelegramConfigRepository::new(state.db.connection());
// Stop bot if this config is active
if let Ok(Some(config)) = repo.find_by_id(id).await {
if config.is_active {
if let Some(telegram_service) = &state.telegram_service {
let _ = telegram_service.stop().await;
}
}
}
match repo.delete(id).await {
Ok(true) => StatusCode::NO_CONTENT.into_response(),
Ok(false) => StatusCode::NOT_FOUND.into_response(),
Err(e) => {
tracing::error!("Failed to delete telegram config: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
/// Get Telegram bot status
#[derive(Debug, Serialize)]
pub struct BotStatusResponse {
pub is_running: bool,
pub bot_info: Option<BotInfo>,
}
async fn get_bot_status(state: &AppState) -> Result<BotStatusResponse, String> {
if let Some(telegram_service) = &state.telegram_service {
let status = telegram_service.get_status().await;
let bot_info = if status.is_running {
// In production, you would get this from the bot API
Some(BotInfo {
username: "bot".to_string(),
first_name: "Bot".to_string(),
})
} else {
None
};
Ok(BotStatusResponse {
is_running: status.is_running,
bot_info,
})
} else {
Ok(BotStatusResponse {
is_running: false,
bot_info: None,
})
}
}
pub async fn get_telegram_status(
State(state): State<AppState>,
) -> impl IntoResponse {
match get_bot_status(&state).await {
Ok(status) => Json(status).into_response(),
Err(e) => {
tracing::error!("Failed to get bot status: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
/// Get list of Telegram admins
#[derive(Debug, Serialize)]
pub struct TelegramAdmin {
pub user_id: Uuid,
pub name: String,
pub telegram_id: Option<i64>,
}
pub async fn get_telegram_admins(
State(state): State<AppState>,
) -> impl IntoResponse {
let repo = UserRepository::new(state.db.connection());
match repo.get_telegram_admins().await {
Ok(admins) => {
let response: Vec<TelegramAdmin> = admins
.into_iter()
.map(|u| TelegramAdmin {
user_id: u.id,
name: u.name,
telegram_id: u.telegram_id,
})
.collect();
Json(response).into_response()
}
Err(e) => {
tracing::error!("Failed to get telegram admins: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
/// Add Telegram admin
pub async fn add_telegram_admin(
State(state): State<AppState>,
Path(user_id): Path<Uuid>,
) -> impl IntoResponse {
let repo = UserRepository::new(state.db.connection());
match repo.set_telegram_admin(user_id, true).await {
Ok(Some(user)) => {
// Notify via Telegram if bot is running
if let Some(telegram_service) = &state.telegram_service {
if let Some(telegram_id) = user.telegram_id {
let _ = telegram_service.send_message(
telegram_id,
"✅ You have been granted admin privileges!".to_string()
).await;
}
}
Json(user).into_response()
}
Ok(None) => {
StatusCode::NOT_FOUND.into_response()
}
Err(e) => {
tracing::error!("Failed to add telegram admin: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
/// Remove Telegram admin
pub async fn remove_telegram_admin(
State(state): State<AppState>,
Path(user_id): Path<Uuid>,
) -> impl IntoResponse {
let repo = UserRepository::new(state.db.connection());
match repo.set_telegram_admin(user_id, false).await {
Ok(Some(user)) => {
// Notify via Telegram if bot is running
if let Some(telegram_service) = &state.telegram_service {
if let Some(telegram_id) = user.telegram_id {
let _ = telegram_service.send_message(
telegram_id,
"❌ Your admin privileges have been revoked.".to_string()
).await;
}
}
Json(user).into_response()
}
Ok(None) => {
StatusCode::NOT_FOUND.into_response()
}
Err(e) => {
tracing::error!("Failed to remove telegram admin: {}", e);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
/// Send test message
#[derive(Debug, Deserialize)]
pub struct SendMessageRequest {
pub chat_id: i64,
pub text: String,
}
pub async fn send_test_message(
State(state): State<AppState>,
Json(req): Json<SendMessageRequest>,
) -> impl IntoResponse {
if let Some(telegram_service) = &state.telegram_service {
match telegram_service.send_message(req.chat_id, req.text).await {
Ok(_) => StatusCode::OK.into_response(),
Err(e) => {
tracing::error!("Failed to send test message: {}", e);
(StatusCode::BAD_REQUEST, e.to_string()).into_response()
}
}
} else {
StatusCode::SERVICE_UNAVAILABLE.into_response()
}
}

View File

@@ -13,9 +13,10 @@ use tower_http::cors::CorsLayer;
use tower_http::services::ServeDir;
use tracing::info;
use std::sync::Arc;
use crate::config::WebConfig;
use crate::database::DatabaseManager;
use crate::services::XrayService;
use crate::services::{XrayService, TelegramService};
pub mod handlers;
pub mod routes;
@@ -29,16 +30,18 @@ pub struct AppState {
#[allow(dead_code)]
pub config: WebConfig,
pub xray_service: XrayService,
pub telegram_service: Option<Arc<TelegramService>>,
}
/// Start the web server
pub async fn start_server(db: DatabaseManager, config: WebConfig) -> Result<()> {
pub async fn start_server(db: DatabaseManager, config: WebConfig, telegram_service: Option<Arc<TelegramService>>) -> Result<()> {
let xray_service = XrayService::new();
let app_state = AppState {
db,
config: config.clone(),
xray_service,
telegram_service,
};
// Serve static files

View File

@@ -16,6 +16,7 @@ pub fn api_routes() -> Router<AppState> {
.nest("/templates", servers::template_routes())
.nest("/dns-providers", dns_provider_routes())
.nest("/tasks", task_routes())
.nest("/telegram", telegram_routes())
}
/// User management routes
@@ -46,4 +47,21 @@ fn task_routes() -> Router<AppState> {
Router::new()
.route("/", get(handlers::get_tasks_status))
.route("/:id/trigger", post(handlers::trigger_task))
}
/// Telegram bot management routes
fn telegram_routes() -> Router<AppState> {
Router::new()
.route("/config", get(handlers::get_telegram_config)
.post(handlers::create_telegram_config))
.route("/config/:id",
get(handlers::get_telegram_config)
.put(handlers::update_telegram_config)
.delete(handlers::delete_telegram_config))
.route("/status", get(handlers::get_telegram_status))
.route("/admins", get(handlers::get_telegram_admins))
.route("/admins/:user_id",
post(handlers::add_telegram_admin)
.delete(handlers::remove_telegram_admin))
.route("/send", post(handlers::send_test_message))
}