mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-26 10:09:08 +00:00
Added telegram
This commit is contained in:
@@ -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;
|
||||
}
|
||||
94
src/database/entities/telegram_config.rs
Normal file
94
src/database/entities/telegram_config.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
167
src/database/repository/telegram_config.rs
Normal file
167
src/database/repository/telegram_config.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
11
src/main.rs
11
src/main.rs
@@ -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),
|
||||
_ => {}
|
||||
|
||||
@@ -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;
|
||||
39
src/services/telegram/bot.rs
Normal file
39
src/services/telegram/bot.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/services/telegram/error.rs
Normal file
46
src/services/telegram/error.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
371
src/services/telegram/handlers.rs
Normal file
371
src/services/telegram/handlers.rs
Normal 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(())
|
||||
}
|
||||
175
src/services/telegram/mod.rs
Normal file
175
src/services/telegram/mod.rs
Normal 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>,
|
||||
}
|
||||
@@ -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::*;
|
||||
@@ -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 {
|
||||
|
||||
304
src/web/handlers/telegram.rs
Normal file
304
src/web/handlers/telegram.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user