TG almost works

This commit is contained in:
AB from home.homenet
2025-10-19 04:13:36 +03:00
parent 42c8016d9c
commit d972f10f83
31 changed files with 3302 additions and 427 deletions

View File

@@ -2,7 +2,7 @@ use teloxide::{Bot, prelude::*};
use tokio::sync::oneshot;
use crate::database::DatabaseManager;
use super::handlers;
use super::handlers::{self, Command};
/// Run the bot polling loop
pub async fn run_polling(
@@ -12,14 +12,21 @@ pub async fn run_polling(
) {
tracing::info!("Starting Telegram bot polling...");
let handler = Update::filter_message()
let handler = dptree::entry()
.branch(
dptree::entry()
.filter_command::<handlers::Command>()
.endpoint(handlers::handle_command)
Update::filter_message()
.branch(
dptree::entry()
.filter_command::<Command>()
.endpoint(handlers::handle_command)
)
.branch(
dptree::endpoint(handlers::handle_message)
)
)
.branch(
dptree::endpoint(handlers::handle_message)
Update::filter_callback_query()
.endpoint(handlers::handle_callback_query)
);
let mut dispatcher = Dispatcher::builder(bot.clone(), handler)

View File

@@ -1,371 +0,0 @@
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,723 @@
use teloxide::{prelude::*, types::{InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery}};
use uuid::Uuid;
use crate::database::DatabaseManager;
use crate::database::repository::{UserRepository, UserRequestRepository};
use crate::database::entities::user_request::RequestStatus;
use super::super::localization::{LocalizationService, Language};
use super::types::get_selected_servers;
/// Handle admin requests edit (show list of recent requests)
pub async fn handle_admin_requests_edit(
bot: Bot,
q: &CallbackQuery,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let admin_telegram_id = q.from.id.0 as i64;
let lang = Language::English; // Default admin language
let l10n = LocalizationService::new();
let chat_id = q.message.as_ref().and_then(|m| {
match m {
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
_ => None,
}
}).ok_or("No chat ID")?;
let user_repo = UserRepository::new(db.connection());
let request_repo = UserRequestRepository::new(db.connection().clone());
// Check if user is admin
if !user_repo.is_telegram_id_admin(admin_telegram_id).await.unwrap_or(false) {
bot.answer_callback_query(q.id.clone())
.text(l10n.get(lang, "unauthorized"))
.await?;
return Ok(());
}
// Get recent requests (last 10)
let recent_requests = request_repo.find_recent(10).await.unwrap_or_default();
if recent_requests.is_empty() {
// Edit message to show no requests
if let Some(msg) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
bot.edit_message_text(chat_id, regular_msg.id, l10n.get(lang.clone(), "no_pending_requests"))
.reply_markup(InlineKeyboardMarkup::new(vec![
vec![InlineKeyboardButton::callback(l10n.get(lang, "back"), "back")],
]))
.await?;
}
}
bot.answer_callback_query(q.id.clone()).await?;
return Ok(());
}
// Build message with request list
let mut message_lines = vec!["📋 <b>Recent Access Requests</b>\n".to_string()];
let mut keyboard_buttons = vec![];
for request in &recent_requests {
let status_emoji = match request.status.as_str() {
"pending" => "",
"approved" => "",
"declined" => "",
_ => ""
};
let username = request.telegram_username.as_deref().unwrap_or("unknown");
let processed_info = if let Some(processed_by_id) = request.processed_by_user_id {
if let Ok(Some(admin)) = user_repo.get_by_id(processed_by_id).await {
format!(" by {}", admin.name)
} else {
String::new()
}
} else {
String::new()
};
let button_text = format!("{} {} @{}{}", status_emoji, request.get_full_name(), username, processed_info);
keyboard_buttons.push(vec![
InlineKeyboardButton::callback(button_text, format!("view_request:{}", request.id))
]);
}
// Add back button
keyboard_buttons.push(vec![
InlineKeyboardButton::callback(l10n.get(lang, "back"), "back")
]);
let message = message_lines.join("\n");
// Edit the existing message instead of sending a new one
if let Some(msg) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
bot.edit_message_text(chat_id, regular_msg.id, message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(InlineKeyboardMarkup::new(keyboard_buttons))
.await?;
}
}
bot.answer_callback_query(q.id.clone()).await?;
Ok(())
}
/// Handle approve request
pub async fn handle_approve_request(
bot: Bot,
q: &CallbackQuery,
request_id: &str,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let admin_telegram_id = q.from.id.0 as i64;
let lang = Language::English; // Default admin language
let l10n = LocalizationService::new();
let _chat_id = q.message.as_ref().and_then(|m| {
match m {
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
_ => None,
}
}).ok_or("No chat ID")?;
let user_repo = UserRepository::new(db.connection());
let request_repo = UserRequestRepository::new(db.connection().clone());
// Get admin user
let admin = user_repo.get_by_telegram_id(admin_telegram_id).await
.unwrap_or(None)
.ok_or(l10n.get(lang.clone(), "admin_not_found"))?;
// Parse request ID
let request_id = Uuid::parse_str(request_id).map_err(|_| l10n.get(lang.clone(), "invalid_request_id"))?;
// Get the request
let request = request_repo.find_by_id(request_id).await
.unwrap_or(None)
.ok_or(l10n.get(lang.clone(), "request_not_found"))?;
// Check if request is already processed
if request.status != "pending" {
bot.answer_callback_query(q.id.clone())
.text("This request has already been processed")
.await?;
return Ok(());
}
// Create user account
let username = request.telegram_username.as_deref().unwrap_or("Unknown");
let dto = crate::database::entities::user::CreateUserDto {
name: request.get_full_name(),
comment: Some(format!("Telegram user: @{}", username)),
telegram_id: Some(request.telegram_id),
is_telegram_admin: false,
};
match user_repo.create(dto).await {
Ok(new_user) => {
// Approve the request
request_repo.approve(
request_id,
Some(format!("Approved by {}", admin.name)),
admin.id
).await?;
// Update the callback message to show approval status and server selection
if let Some(message) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(msg) = message {
if let Some(text) = msg.text() {
let updated_text = format!("{}\n\n✅ <b>APPROVED</b> by {}\n\n📋 Select servers to grant access:", text, admin.name);
let request_id_compact = request_id.to_string().replace("-", "");
let callback_data = format!("s:{}", request_id_compact);
tracing::info!("Callback data length: {} bytes, data: '{}'", callback_data.len(), callback_data);
let server_selection_keyboard = InlineKeyboardMarkup::new(vec![
vec![InlineKeyboardButton::callback("🖥️ Select Servers", callback_data)],
vec![InlineKeyboardButton::callback("📋 All Requests", "back_to_requests")],
]);
let _ = bot.edit_message_text(msg.chat.id, msg.id, updated_text)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(server_selection_keyboard)
.await;
}
}
}
// Notify the user using their saved language preference
let user_lang = Language::from_telegram_code(Some(&request.get_language()));
let user_message = l10n.format(user_lang, "request_approved_notification", &[("user_id", &new_user.id.to_string())]);
bot.send_message(ChatId(request.telegram_id), user_message)
.parse_mode(teloxide::types::ParseMode::Html)
.await?;
bot.answer_callback_query(q.id.clone())
.text(l10n.get(lang, "request_approved_admin"))
.await?;
}
Err(e) => {
tracing::error!("Failed to create user during approval: {}", e);
bot.answer_callback_query(q.id.clone())
.text(l10n.format(lang.clone(), "user_creation_failed", &[("error", &e.to_string())]))
.await?;
}
}
Ok(())
}
/// Handle decline request
pub async fn handle_decline_request(
bot: Bot,
q: &CallbackQuery,
request_id: &str,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let admin_telegram_id = q.from.id.0 as i64;
let lang = Language::English; // Default admin language
let l10n = LocalizationService::new();
let user_repo = UserRepository::new(db.connection());
let request_repo = UserRequestRepository::new(db.connection().clone());
// Get admin user
let admin = user_repo.get_by_telegram_id(admin_telegram_id).await
.unwrap_or(None)
.ok_or(l10n.get(lang.clone(), "admin_not_found"))?;
// Parse request ID
let request_id = Uuid::parse_str(request_id).map_err(|_| l10n.get(lang.clone(), "invalid_request_id"))?;
// Get the request
let request = request_repo.find_by_id(request_id).await
.unwrap_or(None)
.ok_or(l10n.get(lang.clone(), "request_not_found"))?;
// Check if request is already processed
if request.status != "pending" {
bot.answer_callback_query(q.id.clone())
.text("This request has already been processed")
.await?;
return Ok(());
}
// Decline the request
request_repo.decline(
request_id,
Some(format!("Declined by {}", admin.name)),
admin.id
).await?;
// Update the callback message to show decline status
if let Some(message) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(msg) = message {
if let Some(text) = msg.text() {
let updated_text = format!("{}\n\n❌ <b>DECLINED</b> by {}", text, admin.name);
let back_keyboard = InlineKeyboardMarkup::new(vec![
vec![InlineKeyboardButton::callback("📋 All Requests", "back_to_requests")],
]);
let _ = bot.edit_message_text(msg.chat.id, msg.id, updated_text)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(back_keyboard)
.await;
}
}
}
// Notify the user using their saved language preference
let user_lang = Language::from_telegram_code(Some(&request.get_language()));
let user_message = l10n.get(user_lang, "request_declined_notification");
bot.send_message(ChatId(request.telegram_id), user_message)
.parse_mode(teloxide::types::ParseMode::Html)
.await?;
bot.answer_callback_query(q.id.clone())
.text(l10n.get(lang, "request_declined_admin"))
.await?;
Ok(())
}
/// Handle view request details
pub async fn handle_view_request(
bot: Bot,
q: &CallbackQuery,
request_id: &str,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let lang = Language::English; // Default admin language
let l10n = LocalizationService::new();
let chat_id = q.message.as_ref().and_then(|m| {
match m {
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
_ => None,
}
}).ok_or("No chat ID")?;
let request_repo = UserRequestRepository::new(db.connection().clone());
let user_repo = UserRepository::new(db.connection());
// Parse request ID
let request_id = Uuid::parse_str(request_id).map_err(|_| l10n.get(lang.clone(), "invalid_request_id"))?;
// Get the request
let request = request_repo.find_by_id(request_id).await
.unwrap_or(None)
.ok_or(l10n.get(lang.clone(), "request_not_found"))?;
// Get processed by admin info
let processed_by = if let Some(processed_by_id) = request.processed_by_user_id {
if let Ok(Some(admin)) = user_repo.get_by_id(processed_by_id).await {
format!("\n👤 Processed by: {}", admin.name)
} else {
String::new()
}
} else {
String::new()
};
let processed_at = if let Some(processed_at) = request.processed_at {
format!("\n⏰ Processed at: {}", processed_at.format("%Y-%m-%d %H:%M UTC"))
} else {
String::new()
};
let status_emoji = match request.status.as_str() {
"pending" => "",
"approved" => "",
"declined" => "",
_ => ""
};
let message = format!(
"📋 <b>Access Request Details</b>\n\n\
👤 Name: {}\n\
🆔 Telegram: {}\n\
🌍 Language: {}\n\
📅 Requested: {}\n\
{} Status: <b>{}</b>{}{}\n\n\
💬 Message: {}",
request.get_full_name(),
request.get_telegram_link(),
request.get_language().to_uppercase(),
request.created_at.format("%Y-%m-%d %H:%M UTC"),
status_emoji,
request.status.to_uppercase(),
processed_by,
processed_at,
request.request_message.as_deref().unwrap_or("No message")
);
// Create keyboard based on request status
let keyboard = if request.status == "pending" {
InlineKeyboardMarkup::new(vec![
vec![
InlineKeyboardButton::callback(l10n.get(lang.clone(), "approve"), format!("approve:{}", request.id)),
InlineKeyboardButton::callback(l10n.get(lang.clone(), "decline"), format!("decline:{}", request.id)),
],
vec![
InlineKeyboardButton::callback(l10n.get(lang.clone(), "back"), "back_to_requests"),
InlineKeyboardButton::callback("🏠 Menu", "back"),
],
])
} else {
InlineKeyboardMarkup::new(vec![
vec![
InlineKeyboardButton::callback(l10n.get(lang.clone(), "back"), "back_to_requests"),
InlineKeyboardButton::callback("🏠 Menu", "back"),
],
])
};
// Edit the existing message instead of sending a new one
if let Some(msg) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
bot.edit_message_text(chat_id, regular_msg.id, message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(keyboard)
.await?;
}
}
bot.answer_callback_query(q.id.clone()).await?;
Ok(())
}
/// Handle /stats command (admin only)
pub async fn handle_stats(
bot: Bot,
chat_id: ChatId,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let lang = Language::English; // Default admin language
let l10n = LocalizationService::new();
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 request_repo = UserRequestRepository::new(db.connection().clone());
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 pending_requests = request_repo.count_by_status(RequestStatus::Pending).await.unwrap_or(0);
let message = l10n.format(lang, "statistics", &[
("users", &user_count.to_string()),
("servers", &server_count.to_string()),
("inbounds", &inbound_count.to_string()),
("pending", &pending_requests.to_string())
]);
bot.send_message(chat_id, message)
.parse_mode(teloxide::types::ParseMode::Html)
.await?;
Ok(())
}
/// Handle /broadcast command (admin only)
pub 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(_) => failed_count += 1,
}
}
}
let lang = Language::English; // Default admin language
let l10n = LocalizationService::new();
let result_message = l10n.format(lang, "broadcast_complete", &[
("sent", &sent_count.to_string()),
("failed", &failed_count.to_string())
]);
bot.send_message(chat_id, result_message).await?;
Ok(())
}
/// Handle server selection after approval
pub async fn handle_select_server_access(
bot: Bot,
q: &CallbackQuery,
request_id: &str,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let lang = Language::English; // Default admin language
let _l10n = LocalizationService::new();
let chat_id = q.message.as_ref().and_then(|m| {
match m {
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
_ => None,
}
}).ok_or("No chat ID")?;
let server_repo = crate::database::repository::ServerRepository::new(db.connection());
// Get all active servers
let servers = server_repo.find_all().await.unwrap_or_default();
if servers.is_empty() {
bot.answer_callback_query(q.id.clone())
.text("No servers available")
.await?;
return Ok(());
}
// Initialize selected servers for this request (empty initially)
{
let mut selected = get_selected_servers().lock().unwrap();
selected.insert(request_id.to_string(), Vec::new());
}
// Build keyboard with server toggle buttons
let mut keyboard_buttons = vec![];
let selected_servers = {
let selected = get_selected_servers().lock().unwrap();
selected.get(request_id).cloned().unwrap_or_default()
};
for server in &servers {
let is_selected = selected_servers.contains(&server.id.to_string());
let button_text = if is_selected {
format!("{}", server.name)
} else {
format!("{}", server.name)
};
keyboard_buttons.push(vec![
InlineKeyboardButton::callback(
button_text,
format!("t:{}:{}", request_id.to_string().replace("-", ""), server.id.to_string().replace("-", ""))
)
]);
}
// Add apply and back buttons
keyboard_buttons.push(vec![
InlineKeyboardButton::callback("✅ Apply Selected", format!("a:{}", request_id.to_string().replace("-", ""))),
InlineKeyboardButton::callback("🔙 Back", "back_to_requests"),
]);
let message = format!("🖥️ <b>Select Servers for Access</b>\n\nChoose which servers to grant access to the approved user:");
// Edit the existing message
if let Some(msg) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
bot.edit_message_text(chat_id, regular_msg.id, message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(InlineKeyboardMarkup::new(keyboard_buttons))
.await?;
}
}
bot.answer_callback_query(q.id.clone()).await?;
Ok(())
}
/// Handle toggling server selection
pub async fn handle_toggle_server(
bot: Bot,
q: &CallbackQuery,
request_id: &str,
server_id: &str,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let chat_id = q.message.as_ref().and_then(|m| {
match m {
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
_ => None,
}
}).ok_or("No chat ID")?;
// Toggle server selection
{
let mut selected = get_selected_servers().lock().unwrap();
let server_list = selected.entry(request_id.to_string()).or_insert_with(Vec::new);
if let Some(pos) = server_list.iter().position(|x| x == server_id) {
server_list.remove(pos);
} else {
server_list.push(server_id.to_string());
}
}
// Rebuild the keyboard with updated selection
let server_repo = crate::database::repository::ServerRepository::new(db.connection());
let servers = server_repo.find_all().await.unwrap_or_default();
let mut keyboard_buttons = vec![];
let selected_servers = {
let selected = get_selected_servers().lock().unwrap();
selected.get(request_id).cloned().unwrap_or_default()
};
for server in &servers {
let is_selected = selected_servers.contains(&server.id.to_string());
let button_text = if is_selected {
format!("{}", server.name)
} else {
format!("{}", server.name)
};
keyboard_buttons.push(vec![
InlineKeyboardButton::callback(
button_text,
format!("t:{}:{}", request_id.to_string().replace("-", ""), server.id.to_string().replace("-", ""))
)
]);
}
// Add apply and back buttons
keyboard_buttons.push(vec![
InlineKeyboardButton::callback("✅ Apply Selected", format!("a:{}", request_id.to_string().replace("-", ""))),
InlineKeyboardButton::callback("🔙 Back", "back_to_requests"),
]);
let selected_count = selected_servers.len();
let message = format!("🖥️ <b>Select Servers for Access</b>\n\nChoose which servers to grant access to the approved user:\n\n📊 Selected: {} server{}",
selected_count, if selected_count == 1 { "" } else { "s" });
// Edit the existing message
if let Some(msg) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
bot.edit_message_text(chat_id, regular_msg.id, message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(InlineKeyboardMarkup::new(keyboard_buttons))
.await?;
}
}
bot.answer_callback_query(q.id.clone()).await?;
Ok(())
}
/// Handle applying server access
pub async fn handle_apply_server_access(
bot: Bot,
q: &CallbackQuery,
request_id: &str,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let lang = Language::English; // Default admin language
let _l10n = LocalizationService::new();
let chat_id = q.message.as_ref().and_then(|m| {
match m {
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
_ => None,
}
}).ok_or("No chat ID")?;
// Get selected servers
let selected_server_ids = {
let selected = get_selected_servers().lock().unwrap();
selected.get(request_id).cloned().unwrap_or_default()
};
if selected_server_ids.is_empty() {
bot.answer_callback_query(q.id.clone())
.text("No servers selected")
.await?;
return Ok(());
}
let request_repo = UserRequestRepository::new(db.connection().clone());
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().clone());
let inbound_users_repo = crate::database::repository::InboundUsersRepository::new(db.connection().clone());
// Parse request ID and get request
let request_uuid = Uuid::parse_str(request_id).map_err(|_| "Invalid request ID")?;
let request = request_repo.find_by_id(request_uuid).await
.unwrap_or(None)
.ok_or("Request not found")?;
// Get user
let user = user_repo.get_by_telegram_id(request.telegram_id).await
.unwrap_or(None)
.ok_or("User not found")?;
let mut granted_servers = Vec::new();
let mut total_inbounds = 0;
// Grant access to all inbounds on selected servers
for server_id_str in &selected_server_ids {
if let Ok(server_id) = Uuid::parse_str(server_id_str) {
// Get server info
if let Ok(Some(server)) = server_repo.find_by_id(server_id).await {
granted_servers.push(server.name.clone());
// Get all inbounds for this server
if let Ok(inbounds) = inbound_repo.find_by_server_id(server_id).await {
for inbound in inbounds {
// Check if user already has access to this inbound
if !inbound_users_repo.user_has_access_to_inbound(user.id, inbound.id).await.unwrap_or(false) {
// Create inbound user access
let dto = crate::database::entities::inbound_users::CreateInboundUserDto {
user_id: user.id,
server_inbound_id: inbound.id,
level: Some(0),
};
if let Ok(_) = inbound_users_repo.create(dto).await {
total_inbounds += 1;
}
}
}
}
}
}
}
// Clean up selected servers storage
{
let mut selected = get_selected_servers().lock().unwrap();
selected.remove(request_id);
}
// Update message with success
let message = format!(
"✅ <b>Server Access Granted</b>\n\nUser: {}\nServers: {}\nTotal inbounds: {}\n\n✅ Access has been successfully granted!",
user.name,
granted_servers.join(", "),
total_inbounds
);
let back_keyboard = InlineKeyboardMarkup::new(vec![
vec![InlineKeyboardButton::callback("📋 All Requests", "back_to_requests")],
]);
// Edit the existing message
if let Some(msg) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
bot.edit_message_text(chat_id, regular_msg.id, message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(back_keyboard)
.await?;
}
}
bot.answer_callback_query(q.id.clone())
.text(format!("Access granted to {} servers", granted_servers.len()))
.await?;
Ok(())
}

View File

@@ -0,0 +1,153 @@
pub mod admin;
pub mod user;
pub mod types;
// Re-export main handler functions for easier access
pub use admin::*;
pub use user::*;
pub use types::*;
use teloxide::{prelude::*, types::CallbackQuery};
use crate::database::DatabaseManager;
/// Handle bot commands
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.ok_or("No user info")?;
let telegram_id = from.id.0 as i64;
let user_repo = crate::database::repository::UserRepository::new(db.connection());
match cmd {
Command::Start => {
handle_start(bot, chat_id, telegram_id, from, &user_repo, &db).await?;
}
Command::Requests => {
// Check if user is admin
if user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
// Create a fake callback query for admin requests
// This is a workaround since the admin_requests function expects a callback query
// In practice, we could refactor this to not need a callback query
tracing::info!("Admin {} requested to view requests", telegram_id);
let message = "📋 Use the inline keyboard to view recent requests.";
let keyboard = teloxide::types::InlineKeyboardMarkup::new(vec![
vec![teloxide::types::InlineKeyboardButton::callback("📋 Recent Requests", "admin_requests")],
]);
bot.send_message(chat_id, message)
.reply_markup(keyboard)
.await?;
} else {
let lang = get_user_language(from);
let l10n = super::localization::LocalizationService::new();
bot.send_message(chat_id, l10n.get(lang, "unauthorized")).await?;
}
}
Command::Stats => {
// Check if user is admin
if user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
handle_stats(bot, chat_id, &db).await?;
} else {
let lang = get_user_language(from);
let l10n = super::localization::LocalizationService::new();
bot.send_message(chat_id, l10n.get(lang, "unauthorized")).await?;
}
}
Command::Broadcast { message } => {
// Check if user is admin
if user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
handle_broadcast(bot, chat_id, message, &user_repo).await?;
} else {
let lang = get_user_language(from);
let l10n = super::localization::LocalizationService::new();
bot.send_message(chat_id, l10n.get(lang, "unauthorized")).await?;
}
}
}
Ok(())
}
/// Handle regular messages (fallback)
pub async fn handle_message(
bot: Bot,
msg: Message,
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 user info")?;
let telegram_id = from.id.0 as i64;
let user_repo = crate::database::repository::UserRepository::new(db.connection());
// For non-command messages, just show the start menu
handle_start(bot, chat_id, telegram_id, from, &user_repo, &db).await?;
Ok(())
}
/// Handle callback queries from inline keyboards
pub async fn handle_callback_query(
bot: Bot,
q: CallbackQuery,
db: DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Some(data) = &q.data {
if let Some(callback_data) = CallbackData::parse(data) {
match callback_data {
CallbackData::RequestAccess => {
handle_request_access(bot, &q, &db).await?;
}
CallbackData::MyConfigs => {
handle_my_configs_edit(bot, &q, &db).await?;
}
CallbackData::Support => {
handle_support(bot, &q).await?;
}
CallbackData::AdminRequests => {
handle_admin_requests_edit(bot, &q, &db).await?;
}
CallbackData::ApproveRequest(request_id) => {
handle_approve_request(bot, &q, &request_id, &db).await?;
}
CallbackData::DeclineRequest(request_id) => {
handle_decline_request(bot, &q, &request_id, &db).await?;
}
CallbackData::ViewRequest(request_id) => {
handle_view_request(bot, &q, &request_id, &db).await?;
}
CallbackData::ShowServerConfigs(encoded_server_name) => {
handle_show_server_configs(bot, &q, &encoded_server_name, &db).await?;
}
CallbackData::SelectServerAccess(request_id) => {
handle_select_server_access(bot, &q, &request_id, &db).await?;
}
CallbackData::ToggleServer(request_id, server_id) => {
handle_toggle_server(bot, &q, &request_id, &server_id, &db).await?;
}
CallbackData::ApplyServerAccess(request_id) => {
handle_apply_server_access(bot, &q, &request_id, &db).await?;
}
CallbackData::Back => {
// Back to main menu - edit the existing message
handle_start_edit(bot, &q, &db).await?;
}
CallbackData::BackToConfigs => {
handle_my_configs_edit(bot, &q, &db).await?;
}
CallbackData::BackToRequests => {
handle_admin_requests_edit(bot, &q, &db).await?;
}
}
} else {
tracing::warn!("Unknown callback data: {}", data);
bot.answer_callback_query(q.id.clone()).await?;
}
}
Ok(())
}

View File

@@ -0,0 +1,137 @@
use teloxide::utils::command::BotCommands;
use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup, User};
use super::super::localization::{LocalizationService, Language};
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
/// Available bot commands - keeping only admin commands
#[derive(BotCommands, Clone)]
#[command(rename_rule = "lowercase", description = "Admin commands:")]
pub enum Command {
#[command(description = "Start the bot")]
Start,
#[command(description = "[Admin] Manage user requests")]
Requests,
#[command(description = "[Admin] Show statistics")]
Stats,
#[command(description = "[Admin] Broadcast message", parse_with = "split")]
Broadcast { message: String },
}
/// Callback data for inline keyboard buttons
#[derive(Debug, Clone)]
pub enum CallbackData {
RequestAccess,
MyConfigs,
Support,
AdminRequests,
ApproveRequest(String), // request_id
DeclineRequest(String), // request_id
ViewRequest(String), // request_id
ShowServerConfigs(String), // server_name encoded
Back,
BackToConfigs, // Back to configs list from server view
BackToRequests, // Back to requests list from request view
SelectServerAccess(String), // request_id - show server selection after approval
ToggleServer(String, String), // request_id, server_id - toggle server selection
ApplyServerAccess(String), // request_id - apply selected servers
}
impl CallbackData {
pub fn parse(data: &str) -> Option<Self> {
match data {
"request_access" => Some(CallbackData::RequestAccess),
"my_configs" => Some(CallbackData::MyConfigs),
"support" => Some(CallbackData::Support),
"admin_requests" => Some(CallbackData::AdminRequests),
"back" => Some(CallbackData::Back),
"back_to_configs" => Some(CallbackData::BackToConfigs),
"back_to_requests" => Some(CallbackData::BackToRequests),
_ => {
if let Some(id) = data.strip_prefix("approve:") {
Some(CallbackData::ApproveRequest(id.to_string()))
} else if let Some(id) = data.strip_prefix("decline:") {
Some(CallbackData::DeclineRequest(id.to_string()))
} else if let Some(id) = data.strip_prefix("view_request:") {
Some(CallbackData::ViewRequest(id.to_string()))
} else if let Some(server_name) = data.strip_prefix("server_configs:") {
Some(CallbackData::ShowServerConfigs(server_name.to_string()))
} else if let Some(id) = data.strip_prefix("s:") {
restore_uuid(id).map(CallbackData::SelectServerAccess)
} else if let Some(rest) = data.strip_prefix("t:") {
let parts: Vec<&str> = rest.split(':').collect();
if parts.len() == 2 {
if let (Some(request_id), Some(server_id)) = (restore_uuid(parts[0]), restore_uuid(parts[1])) {
Some(CallbackData::ToggleServer(request_id, server_id))
} else {
None
}
} else {
None
}
} else if let Some(id) = data.strip_prefix("a:") {
restore_uuid(id).map(CallbackData::ApplyServerAccess)
} else {
None
}
}
}
}
}
// Global storage for selected servers per request
static SELECTED_SERVERS: OnceLock<Arc<Mutex<HashMap<String, Vec<String>>>>> = OnceLock::new();
pub fn get_selected_servers() -> &'static Arc<Mutex<HashMap<String, Vec<String>>>> {
SELECTED_SERVERS.get_or_init(|| Arc::new(Mutex::new(HashMap::new())))
}
/// Helper function to get user language from Telegram user data
pub fn get_user_language(user: &User) -> Language {
Language::from_telegram_code(user.language_code.as_deref())
}
/// Main keyboard for registered users
pub fn get_main_keyboard(is_admin: bool, lang: Language) -> InlineKeyboardMarkup {
let l10n = LocalizationService::new();
let mut keyboard = vec![
vec![InlineKeyboardButton::callback(l10n.get(lang.clone(), "my_configs"), "my_configs")],
vec![InlineKeyboardButton::callback(l10n.get(lang.clone(), "support"), "support")],
];
if is_admin {
keyboard.push(vec![InlineKeyboardButton::callback(l10n.get(lang, "user_requests"), "admin_requests")]);
}
InlineKeyboardMarkup::new(keyboard)
}
/// Keyboard for new users
pub fn get_new_user_keyboard(lang: Language) -> InlineKeyboardMarkup {
let l10n = LocalizationService::new();
InlineKeyboardMarkup::new(vec![
vec![InlineKeyboardButton::callback(l10n.get(lang, "get_vpn_access"), "request_access")],
])
}
/// Restore UUID from compact format (without dashes)
fn restore_uuid(compact: &str) -> Option<String> {
if compact.len() != 32 {
return None;
}
// Insert dashes at proper positions for UUID format
let uuid_str = format!(
"{}-{}-{}-{}-{}",
&compact[0..8],
&compact[8..12],
&compact[12..16],
&compact[16..20],
&compact[20..32]
);
Some(uuid_str)
}

View File

@@ -0,0 +1,669 @@
use teloxide::{prelude::*, types::{InlineKeyboardButton, InlineKeyboardMarkup}};
use base64::{Engine, engine::general_purpose};
use crate::database::DatabaseManager;
use crate::database::repository::{UserRepository, UserRequestRepository};
use crate::database::entities::user_request::{CreateUserRequestDto, RequestStatus};
use super::super::localization::{LocalizationService, Language};
use super::types::{get_user_language, get_main_keyboard, get_new_user_keyboard};
/// Handle start command and main menu
pub async fn handle_start(
bot: Bot,
chat_id: ChatId,
telegram_id: i64,
from: &teloxide::types::User,
user_repo: &UserRepository,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
handle_start_impl(bot, chat_id, telegram_id, from, user_repo, db, None, None).await
}
/// Handle start with message editing support
pub async fn handle_start_edit(
bot: Bot,
q: &CallbackQuery,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let from = &q.from;
let telegram_id = from.id.0 as i64;
let user_repo = UserRepository::new(db.connection());
if let Some(msg) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
let chat_id = regular_msg.chat.id;
handle_start_impl(
bot.clone(),
chat_id,
telegram_id,
from,
&user_repo,
db,
Some(regular_msg.id),
Some(q.id.clone())
).await?;
}
}
Ok(())
}
/// Internal implementation of handle_start with optional message editing
async fn handle_start_impl(
bot: Bot,
chat_id: ChatId,
telegram_id: i64,
from: &teloxide::types::User,
user_repo: &UserRepository,
db: &DatabaseManager,
edit_message_id: Option<teloxide::types::MessageId>,
callback_query_id: Option<String>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let lang = get_user_language(from);
let l10n = LocalizationService::new();
// Check if user exists in our database
match user_repo.get_by_telegram_id(telegram_id).await {
Ok(Some(user)) => {
// Check if user is admin
let is_admin = user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false);
// Check if user has any pending requests
let request_repo = UserRequestRepository::new(db.connection().clone());
// Check for existing requests
if let Ok(existing_requests) = request_repo.find_by_telegram_id(telegram_id).await {
if let Some(latest_request) = existing_requests.into_iter()
.filter(|r| r.status == "pending" || r.status == "approved" || r.status == "declined")
.max_by_key(|r| r.created_at) {
match latest_request.status.as_str() {
"pending" => {
let message = l10n.format(lang.clone(), "request_pending", &[
("status", "⏳ pending"),
("date", &latest_request.created_at.format("%Y-%m-%d %H:%M UTC").to_string())
]);
let keyboard = get_new_user_keyboard(lang);
if let Some(msg_id) = edit_message_id {
bot.edit_message_text(chat_id, msg_id, message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(keyboard)
.await?;
if let Some(cb_id) = callback_query_id {
bot.answer_callback_query(cb_id).await?;
}
} else {
bot.send_message(chat_id, message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(keyboard)
.await?;
}
return Ok(());
}
"declined" => {
let message = l10n.format(lang.clone(), "request_pending", &[
("status", &l10n.get(lang.clone(), "request_declined_status")),
("date", &latest_request.created_at.format("%Y-%m-%d %H:%M UTC").to_string())
]);
let keyboard = get_new_user_keyboard(lang);
if let Some(msg_id) = edit_message_id {
bot.edit_message_text(chat_id, msg_id, message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(keyboard)
.await?;
if let Some(cb_id) = callback_query_id {
bot.answer_callback_query(cb_id).await?;
}
} else {
bot.send_message(chat_id, message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(keyboard)
.await?;
}
return Ok(());
}
_ => {} // approved - continue with normal flow
}
}
}
// Existing user - show main menu
let message = l10n.format(lang.clone(), "welcome_back", &[("name", &user.name)]);
let keyboard = get_main_keyboard(is_admin, lang);
if let Some(msg_id) = edit_message_id {
bot.edit_message_text(chat_id, msg_id, message)
.reply_markup(keyboard)
.await?;
if let Some(cb_id) = callback_query_id {
bot.answer_callback_query(cb_id).await?;
}
} else {
bot.send_message(chat_id, message)
.reply_markup(keyboard)
.await?;
}
}
Ok(None) => {
// New user - show access request
let username = from.username.as_deref().unwrap_or("Unknown");
let message = l10n.format(lang.clone(), "welcome_new_user", &[("username", username)]);
let keyboard = get_new_user_keyboard(lang);
if let Some(msg_id) = edit_message_id {
bot.edit_message_text(chat_id, msg_id, message)
.reply_markup(keyboard)
.await?;
if let Some(cb_id) = callback_query_id {
bot.answer_callback_query(cb_id).await?;
}
} else {
bot.send_message(chat_id, message)
.reply_markup(keyboard)
.await?;
}
}
Err(e) => {
tracing::error!("Database error: {}", e);
bot.send_message(chat_id, "Database error occurred").await?;
}
}
Ok(())
}
/// Handle access request
pub async fn handle_request_access(
bot: Bot,
q: &CallbackQuery,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let from = &q.from;
let lang = get_user_language(from);
let l10n = LocalizationService::new();
let telegram_id = from.id.0 as i64;
let chat_id = q.message.as_ref().and_then(|m| {
match m {
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
_ => None,
}
}).ok_or("No chat ID")?;
let user_repo = UserRepository::new(db.connection());
let request_repo = UserRequestRepository::new(db.connection().clone());
// Check if user already exists
if let Some(_) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
bot.answer_callback_query(q.id.clone())
.text(l10n.get(lang, "already_approved"))
.await?;
return Ok(());
}
// Check for existing requests
if let Ok(existing_requests) = request_repo.find_by_telegram_id(telegram_id).await {
if let Some(latest_request) = existing_requests.iter()
.filter(|r| r.status == "pending")
.max_by_key(|r| r.created_at) {
// Show pending status in the message instead of just an alert
let message = l10n.format(lang.clone(), "request_pending", &[
("status", "⏳ pending"),
("date", &latest_request.created_at.format("%Y-%m-%d %H:%M UTC").to_string())
]);
if let Some(message_ref) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(msg) = message_ref {
let _ = bot.edit_message_text(chat_id, msg.id, message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(InlineKeyboardMarkup::new(vec![
vec![InlineKeyboardButton::callback(l10n.get(lang, "back"), "back")],
]))
.await;
}
}
bot.answer_callback_query(q.id.clone()).await?;
return Ok(());
}
// Check for declined requests - allow new request after decline
let _has_declined = existing_requests.iter()
.any(|r| r.status == "declined");
}
// Create new access request
let dto = CreateUserRequestDto {
telegram_id,
telegram_first_name: Some(from.first_name.clone()),
telegram_last_name: from.last_name.clone(),
telegram_username: from.username.clone(),
request_message: Some("Access request via Telegram bot".to_string()),
language: lang.code().to_string(),
};
match request_repo.create(dto).await {
Ok(request) => {
// Edit message to show success
if let Some(message) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(msg) = message {
let _ = bot.edit_message_text(chat_id, msg.id, l10n.get(lang.clone(), "request_submitted"))
.reply_markup(InlineKeyboardMarkup::new(vec![
vec![InlineKeyboardButton::callback(l10n.get(lang, "back"), "back")],
]))
.await;
}
}
// Notify admins
notify_admins_new_request(&bot, &request, db).await?;
bot.answer_callback_query(q.id.clone()).await?;
}
Err(e) => {
tracing::error!("Failed to create request: {}", e);
bot.answer_callback_query(q.id.clone())
.text(l10n.format(lang, "request_submit_failed", &[("error", &e.to_string())]))
.await?;
}
}
Ok(())
}
/// Handle my configs with message editing
pub async fn handle_my_configs_edit(
bot: Bot,
q: &CallbackQuery,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let from = &q.from;
let lang = get_user_language(from);
let l10n = LocalizationService::new();
let telegram_id = from.id.0 as i64;
let chat_id = q.message.as_ref().and_then(|m| {
match m {
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
_ => None,
}
}).ok_or("No chat ID")?;
let user_repo = UserRepository::new(db.connection());
let inbound_users_repo = crate::database::repository::InboundUsersRepository::new(db.connection().clone());
let uri_service = crate::services::UriGeneratorService::new();
if let Some(user) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
// Get all active inbound users for this user
let inbound_users = inbound_users_repo.find_by_user_id(user.id).await.unwrap_or_default();
if inbound_users.is_empty() {
// Edit message to show no configs available
if let Some(msg) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
bot.edit_message_text(chat_id, regular_msg.id, l10n.get(lang.clone(), "no_configs_available"))
.reply_markup(InlineKeyboardMarkup::new(vec![
vec![InlineKeyboardButton::callback(l10n.get(lang, "back"), "back")],
]))
.await?;
}
}
bot.answer_callback_query(q.id.clone()).await?;
return Ok(());
}
// Structure to hold config with inbound_id
#[derive(Debug, Clone)]
struct ConfigWithInbound {
client_config: crate::services::uri_generator::ClientConfig,
server_inbound_id: uuid::Uuid,
}
// Group configurations by server name
let mut servers: std::collections::HashMap<String, Vec<ConfigWithInbound>> = std::collections::HashMap::new();
for inbound_user in inbound_users {
if !inbound_user.is_active {
continue;
}
// Get client config data for this specific inbound
if let Ok(Some(config_data)) = inbound_users_repo.get_client_config_data(user.id, inbound_user.server_inbound_id).await {
match uri_service.generate_client_config(user.id, &config_data) {
Ok(client_config) => {
let config_with_inbound = ConfigWithInbound {
client_config: client_config.clone(),
server_inbound_id: inbound_user.server_inbound_id,
};
servers.entry(client_config.server_name.clone())
.or_insert_with(Vec::new)
.push(config_with_inbound);
},
Err(e) => {
tracing::warn!("Failed to generate client config: {}", e);
continue;
}
}
}
}
// Build message with statistics only
let mut message_lines = vec![l10n.get(lang.clone(), "your_configurations")];
// Calculate statistics
let server_count = servers.len();
let total_configs = servers.values().map(|configs| configs.len()).sum::<usize>();
// Count unique protocols
let mut protocols = std::collections::HashSet::new();
for configs in servers.values() {
for config_with_inbound in configs {
protocols.insert(config_with_inbound.client_config.protocol.clone());
}
}
let server_word = match lang {
Language::Russian => {
if server_count == 1 { "сервер" }
else if server_count < 5 { "сервера" }
else { "серверов" }
},
Language::English => {
if server_count == 1 { "server" }
else { "servers" }
}
};
let config_word = match lang {
Language::Russian => {
if total_configs == 1 { "конфигурация" }
else if total_configs < 5 { "конфигурации" }
else { "конфигураций" }
},
Language::English => {
if total_configs == 1 { "configuration" }
else { "configurations" }
}
};
let protocol_word = match lang {
Language::Russian => {
if protocols.len() == 1 { "протокол" }
else if protocols.len() < 5 { "протокола" }
else { "протоколов" }
},
Language::English => {
if protocols.len() == 1 { "protocol" }
else { "protocols" }
}
};
message_lines.push(format!(
"\n📊 {} {}{} {}{} {}",
server_count, server_word,
total_configs, config_word,
protocols.len(), protocol_word
));
// Create keyboard with buttons for each server
let mut keyboard_buttons = vec![];
for (server_name, configs) in servers.iter() {
// Encode server name to avoid issues with special characters
let encoded_server_name = general_purpose::STANDARD.encode(server_name.as_bytes());
let config_count = configs.len();
let config_suffix = match lang {
Language::Russian => {
if config_count == 1 {
""
} else if config_count < 5 {
"а"
} else {
"ов"
}
},
Language::English => {
if config_count == 1 {
""
} else {
"s"
}
}
};
let config_word = match lang {
Language::Russian => "конфиг",
Language::English => "config",
};
keyboard_buttons.push(vec![
InlineKeyboardButton::callback(
format!("🖥️ {} ({} {}{})", server_name, config_count, config_word, config_suffix),
format!("server_configs:{}", encoded_server_name)
)
]);
}
keyboard_buttons.push(vec![
InlineKeyboardButton::callback(l10n.get(lang, "back"), "back")
]);
let message = message_lines.join("\n");
// Edit the existing message instead of sending a new one
if let Some(msg) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
bot.edit_message_text(chat_id, regular_msg.id, message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(InlineKeyboardMarkup::new(keyboard_buttons))
.await?;
}
}
bot.answer_callback_query(q.id.clone()).await?;
}
Ok(())
}
/// Handle show server configs callback
pub async fn handle_show_server_configs(
bot: Bot,
q: &CallbackQuery,
encoded_server_name: &str,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let from = &q.from;
let lang = get_user_language(from);
let l10n = LocalizationService::new();
let telegram_id = from.id.0 as i64;
let chat_id = q.message.as_ref().and_then(|m| {
match m {
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
_ => None,
}
}).ok_or("No chat ID")?;
// Decode server name
let server_name = match general_purpose::STANDARD.decode(encoded_server_name) {
Ok(bytes) => String::from_utf8(bytes).map_err(|_| "Invalid server name encoding")?,
Err(_) => return Ok(()), // Invalid encoding, ignore
};
let user_repo = UserRepository::new(db.connection());
let inbound_users_repo = crate::database::repository::InboundUsersRepository::new(db.connection().clone());
let uri_service = crate::services::UriGeneratorService::new();
// Get user from telegram_id
if let Some(user) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
// Get all active inbound users for this user
let inbound_users = inbound_users_repo.find_by_user_id(user.id).await.unwrap_or_default();
let mut server_configs = Vec::new();
for inbound_user in inbound_users {
if !inbound_user.is_active {
continue;
}
// Get client config data for this specific inbound
if let Ok(Some(config_data)) = inbound_users_repo.get_client_config_data(user.id, inbound_user.server_inbound_id).await {
if config_data.server_name == server_name {
match uri_service.generate_client_config(user.id, &config_data) {
Ok(client_config) => {
server_configs.push(client_config);
},
Err(e) => {
tracing::warn!("Failed to generate client config: {}", e);
continue;
}
}
}
}
}
if server_configs.is_empty() {
bot.answer_callback_query(q.id.clone())
.text(l10n.get(lang, "config_not_found"))
.await?;
return Ok(());
}
// Build message with all configs for this server
let mut message_lines = vec![
l10n.format(lang.clone(), "server_configs_title", &[("server_name", &server_name)])
];
for config in &server_configs {
let protocol_emoji = match config.protocol.as_str() {
"vless" => "🔵",
"vmess" => "🟢",
"trojan" => "🔴",
"shadowsocks" => "🟡",
_ => ""
};
message_lines.push(format!(
"\n{} <b>{} - {}</b> ({})",
protocol_emoji,
config.server_name,
config.template_name,
config.protocol.to_uppercase()
));
message_lines.push(format!("<code>{}</code>", config.uri));
}
// Create back button
let keyboard = InlineKeyboardMarkup::new(vec![
vec![InlineKeyboardButton::callback(l10n.get(lang, "back"), "back_to_configs")],
]);
let message = message_lines.join("\n");
// Edit the existing message instead of sending a new one
if let Some(msg) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
bot.edit_message_text(chat_id, regular_msg.id, message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(keyboard)
.await?;
}
}
bot.answer_callback_query(q.id.clone()).await?;
} else {
bot.answer_callback_query(q.id.clone())
.text(l10n.get(lang, "unauthorized"))
.await?;
}
Ok(())
}
/// Handle support button
pub async fn handle_support(
bot: Bot,
q: &CallbackQuery,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let from = &q.from;
let lang = get_user_language(from);
let l10n = LocalizationService::new();
let chat_id = q.message.as_ref().and_then(|m| {
match m {
teloxide::types::MaybeInaccessibleMessage::Regular(msg) => Some(msg.chat.id),
_ => None,
}
}).ok_or("No chat ID")?;
let keyboard = InlineKeyboardMarkup::new(vec![
vec![InlineKeyboardButton::callback(l10n.get(lang.clone(), "back"), "back")],
]);
// Edit the existing message instead of sending a new one
if let Some(msg) = &q.message {
if let teloxide::types::MaybeInaccessibleMessage::Regular(regular_msg) = msg {
bot.edit_message_text(chat_id, regular_msg.id, l10n.get(lang, "support_info"))
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(keyboard)
.await?;
}
}
bot.answer_callback_query(q.id.clone()).await?;
Ok(())
}
/// Notify admins about new access request
async fn notify_admins_new_request(
bot: &Bot,
request: &crate::database::entities::user_request::Model,
db: &DatabaseManager,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let user_repo = UserRepository::new(db.connection());
// Get all admins
let admins = user_repo.get_telegram_admins().await.unwrap_or_default();
if !admins.is_empty() {
let lang = Language::English; // Default admin language
let l10n = LocalizationService::new();
let message = l10n.format(lang.clone(), "new_access_request", &[
("first_name", &request.telegram_first_name.as_deref().unwrap_or("")),
("last_name", &request.telegram_last_name.as_deref().unwrap_or("")),
("username", &request.telegram_username.as_deref().unwrap_or("unknown")),
]);
let keyboard = InlineKeyboardMarkup::new(vec![
vec![
InlineKeyboardButton::callback(l10n.get(lang.clone(), "approve"), format!("approve:{}", request.id)),
InlineKeyboardButton::callback(l10n.get(lang.clone(), "decline"), format!("decline:{}", request.id)),
],
vec![
InlineKeyboardButton::callback("📋 All Requests", "back_to_requests"),
],
]);
for admin in admins {
if let Some(telegram_id) = admin.telegram_id {
let _ = bot.send_message(ChatId(telegram_id), &message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(keyboard.clone())
.await;
}
}
}
Ok(())
}

View File

@@ -0,0 +1,297 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Language {
Russian,
English,
}
impl Language {
pub fn from_telegram_code(code: Option<&str>) -> Self {
match code {
Some("ru") | Some("by") | Some("kk") | Some("uk") => Self::Russian,
_ => Self::English, // Default to English
}
}
pub fn code(&self) -> &'static str {
match self {
Self::Russian => "ru",
Self::English => "en",
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Translations {
pub welcome_new_user: String,
pub welcome_back: String,
pub request_pending: String,
pub request_approved_status: String,
pub request_declined_status: String,
pub get_vpn_access: String,
pub my_configs: String,
pub support: String,
pub user_requests: String,
pub back: String,
pub approve: String,
pub decline: String,
// Request handling
pub already_pending: String,
pub already_approved: String,
pub already_declined: String,
pub request_submitted: String,
pub request_submit_failed: String,
// Approval/Decline messages
pub request_approved: String,
pub request_declined: String,
pub request_approved_notification: String,
pub request_declined_notification: String,
// Admin messages
pub new_access_request: String,
pub no_pending_requests: String,
pub access_request_details: String,
pub unauthorized: String,
pub request_approved_admin: String,
pub request_declined_admin: String,
pub user_creation_failed: String,
// Support
pub support_info: String,
// Stats
pub statistics: String,
pub total_users: String,
pub total_servers: String,
pub total_inbounds: String,
pub pending_requests: String,
// Broadcast
pub broadcast_complete: String,
pub sent: String,
pub failed: String,
// Configs
pub configs_coming_soon: String,
pub your_configurations: String,
pub no_configs_available: String,
pub config_copy_message: String,
pub config_copied: String,
pub config_not_found: String,
pub server_configs_title: String,
// Errors
pub error_occurred: String,
pub admin_not_found: String,
pub request_not_found: String,
pub invalid_request_id: String,
}
pub struct LocalizationService {
translations: HashMap<Language, Translations>,
}
impl LocalizationService {
pub fn new() -> Self {
let mut translations = HashMap::new();
// Load English translations
translations.insert(Language::English, Self::load_english());
// Load Russian translations
translations.insert(Language::Russian, Self::load_russian());
Self { translations }
}
pub fn get(&self, lang: Language, key: &str) -> String {
let translations = self.translations.get(&lang)
.unwrap_or_else(|| self.translations.get(&Language::English).unwrap());
match key {
"welcome_new_user" => translations.welcome_new_user.clone(),
"welcome_back" => translations.welcome_back.clone(),
"request_pending" => translations.request_pending.clone(),
"request_approved_status" => translations.request_approved_status.clone(),
"request_declined_status" => translations.request_declined_status.clone(),
"get_vpn_access" => translations.get_vpn_access.clone(),
"my_configs" => translations.my_configs.clone(),
"support" => translations.support.clone(),
"user_requests" => translations.user_requests.clone(),
"back" => translations.back.clone(),
"approve" => translations.approve.clone(),
"decline" => translations.decline.clone(),
"already_pending" => translations.already_pending.clone(),
"already_approved" => translations.already_approved.clone(),
"already_declined" => translations.already_declined.clone(),
"request_submitted" => translations.request_submitted.clone(),
"request_submit_failed" => translations.request_submit_failed.clone(),
"request_approved" => translations.request_approved.clone(),
"request_declined" => translations.request_declined.clone(),
"request_approved_notification" => translations.request_approved_notification.clone(),
"request_declined_notification" => translations.request_declined_notification.clone(),
"new_access_request" => translations.new_access_request.clone(),
"no_pending_requests" => translations.no_pending_requests.clone(),
"access_request_details" => translations.access_request_details.clone(),
"unauthorized" => translations.unauthorized.clone(),
"request_approved_admin" => translations.request_approved_admin.clone(),
"request_declined_admin" => translations.request_declined_admin.clone(),
"user_creation_failed" => translations.user_creation_failed.clone(),
"support_info" => translations.support_info.clone(),
"statistics" => translations.statistics.clone(),
"total_users" => translations.total_users.clone(),
"total_servers" => translations.total_servers.clone(),
"total_inbounds" => translations.total_inbounds.clone(),
"pending_requests" => translations.pending_requests.clone(),
"broadcast_complete" => translations.broadcast_complete.clone(),
"sent" => translations.sent.clone(),
"failed" => translations.failed.clone(),
"configs_coming_soon" => translations.configs_coming_soon.clone(),
"your_configurations" => translations.your_configurations.clone(),
"no_configs_available" => translations.no_configs_available.clone(),
"config_copy_message" => translations.config_copy_message.clone(),
"config_copied" => translations.config_copied.clone(),
"config_not_found" => translations.config_not_found.clone(),
"server_configs_title" => translations.server_configs_title.clone(),
"error_occurred" => translations.error_occurred.clone(),
"admin_not_found" => translations.admin_not_found.clone(),
"request_not_found" => translations.request_not_found.clone(),
"invalid_request_id" => translations.invalid_request_id.clone(),
_ => format!("Missing translation: {}", key),
}
}
pub fn format(&self, lang: Language, template: &str, args: &[(&str, &str)]) -> String {
let mut result = self.get(lang, template);
for (placeholder, value) in args {
result = result.replace(&format!("{{{}}}", placeholder), value);
}
result
}
fn load_english() -> Translations {
Translations {
welcome_new_user: "👋 Welcome, {username}!\n\nI'm the OutFleet VPN bot. To get started, you'll need to request access.\n\nClick the button below to submit your access request:".to_string(),
welcome_back: "👋 Welcome back, {name}!\n\nWhat would you like to do?".to_string(),
request_pending: "👋 Hello!\n\nYour access request is currently <b>{status}</b>.\n\nRequest submitted: {date}".to_string(),
request_approved_status: "✅ approved".to_string(),
request_declined_status: "❌ declined".to_string(),
get_vpn_access: "🚀 Get VPN Access".to_string(),
my_configs: "📋 My Configs".to_string(),
support: "💬 Support".to_string(),
user_requests: "👥 User Requests".to_string(),
back: "🔙 Back".to_string(),
approve: "✅ Approve".to_string(),
decline: "❌ Decline".to_string(),
already_pending: "⏳ You already have a pending access request. Please wait for admin review.".to_string(),
already_approved: "✅ Your access request has already been approved. Use /start to access the main menu.".to_string(),
already_declined: "❌ Your previous access request was declined. Please contact administrators if you believe this is a mistake.".to_string(),
request_submitted: "✅ Your access request has been submitted!\n\nAn administrator will review your request soon. You'll receive a notification once it's processed.".to_string(),
request_submit_failed: "❌ Failed to submit request: {error}".to_string(),
request_approved: "✅ Request approved".to_string(),
request_declined: "❌ Request declined".to_string(),
request_approved_notification: "🎉 <b>Your access request has been approved!</b>\n\nWelcome to OutFleet VPN! Your account has been created.\n\nUser ID: <code>{user_id}</code>\n\nYou can now use /start to access the main menu.".to_string(),
request_declined_notification: "❌ Your access request has been declined.\n\nIf you believe this is a mistake, please contact the administrators.".to_string(),
new_access_request: "🔔 <b>New Access Request</b>\n\n👤 Name: {first_name} {last_name}\n🆔 Username: @{username}\n\nUse /requests to review".to_string(),
no_pending_requests: "No pending access requests".to_string(),
access_request_details: "📋 <b>Access Request</b>\n\n👤 Name: {full_name}\n🆔 Telegram: {telegram_link}\n📅 Requested: {date}\n\nMessage: {message}".to_string(),
unauthorized: "❌ You are not authorized to use this command".to_string(),
request_approved_admin: "✅ Request approved".to_string(),
request_declined_admin: "❌ Request declined".to_string(),
user_creation_failed: "❌ Failed to create user account: {error}\n\nPlease try again or contact technical support.".to_string(),
support_info: "💬 <b>Support Information</b>\n\nIf you need help, please contact the administrators.\n\nYou can also check the documentation at:\nhttps://github.com/OutFleet".to_string(),
statistics: "📊 <b>Statistics</b>\n\n👥 Total Users: {users}\n🖥️ Total Servers: {servers}\n📡 Total Inbounds: {inbounds}\n⏳ Pending Requests: {pending}".to_string(),
total_users: "👥 Total Users".to_string(),
total_servers: "🖥️ Total Servers".to_string(),
total_inbounds: "📡 Total Inbounds".to_string(),
pending_requests: "⏳ Pending Requests".to_string(),
broadcast_complete: "✅ Broadcast complete\nSent: {sent}\nFailed: {failed}".to_string(),
sent: "Sent".to_string(),
failed: "Failed".to_string(),
configs_coming_soon: "📋 Your configurations will be shown here (coming soon)".to_string(),
your_configurations: "📋 <b>Your Configurations</b>".to_string(),
no_configs_available: "📋 No configurations available\n\nYou don't have access to any VPN configurations yet. Please contact an administrator to get access.".to_string(),
config_copy_message: "📋 <b>{server_name}</b> - {inbound_tag} ({protocol})\n\nConnection URI:".to_string(),
config_copied: "✅ Configuration copied to clipboard".to_string(),
config_not_found: "❌ Configuration not found".to_string(),
server_configs_title: "🖥️ <b>{server_name}</b> - Connection Links".to_string(),
error_occurred: "An error occurred".to_string(),
admin_not_found: "Admin not found".to_string(),
request_not_found: "Request not found".to_string(),
invalid_request_id: "Invalid request ID".to_string(),
}
}
fn load_russian() -> Translations {
Translations {
welcome_new_user: "👋 Добро пожаловать, {username}!\n\nЯ бот OutFleet VPN. Чтобы начать работу, вам необходимо запросить доступ.\n\nНажмите кнопку ниже, чтобы отправить запрос на доступ:".to_string(),
welcome_back: "👋 С возвращением, {name}!\n\nЧто вы хотите сделать?".to_string(),
request_pending: "👋 Привет!\n\nВаш запрос на доступ в настоящее время <b>{status}</b>.\n\nЗапрос отправлен: {date}".to_string(),
request_approved_status: "✅ одобрен".to_string(),
request_declined_status: "❌ отклонен".to_string(),
get_vpn_access: "🚀 Получить доступ к VPN".to_string(),
my_configs: "📋 Мои конфигурации".to_string(),
support: "💬 Поддержка".to_string(),
user_requests: "👥 Запросы пользователей".to_string(),
back: "🔙 Назад".to_string(),
approve: "✅ Одобрить".to_string(),
decline: "❌ Отклонить".to_string(),
already_pending: "У вас уже есть ожидающий рассмотрения запрос на доступ. Пожалуйста, дождитесь проверки администратором.".to_string(),
already_approved: "✅ Ваш запрос на доступ уже был одобрен. Используйте /start для доступа к главному меню.".to_string(),
already_declined: "❌ Ваш предыдущий запрос на доступ был отклонен. Пожалуйста, свяжитесь с администраторами, если считаете, что это ошибка.".to_string(),
request_submitted: "✅ Ваш запрос на доступ отправлен!\n\nАдминистратор скоро рассмотрит ваш запрос. Вы получите уведомление после обработки.".to_string(),
request_submit_failed: "Не удалось отправить запрос: {error}".to_string(),
request_approved: "✅ Запрос одобрен".to_string(),
request_declined: "❌ Запрос отклонен".to_string(),
request_approved_notification: "🎉 <b>Ваш запрос на доступ одобрен!</b>\n\nДобро пожаловать в OutFleet VPN! Ваш аккаунт создан.\n\nID пользователя: <code>{user_id}</code>\n\nТеперь вы можете использовать /start для доступа к главному меню.".to_string(),
request_declined_notification: "❌ Ваш запрос на доступ отклонен.\n\nЕсли вы считаете, что это ошибка, пожалуйста, свяжитесь с администраторами.".to_string(),
new_access_request: "🔔 <b>Новый запрос на доступ</b>\n\n👤 Имя: {first_name} {last_name}\n🆔 Имя пользователя: @{username}\n\nИспользуйте /requests для просмотра".to_string(),
no_pending_requests: "Нет ожидающих запросов на доступ".to_string(),
access_request_details: "📋 <b>Запрос на доступ</b>\n\n👤 Имя: {full_name}\n🆔 Telegram: {telegram_link}\n📅 Запрошено: {date}\n\nСообщение: {message}".to_string(),
unauthorized: "У вас нет прав для использования этой команды".to_string(),
request_approved_admin: "✅ Запрос одобрен".to_string(),
request_declined_admin: "❌ Запрос отклонен".to_string(),
user_creation_failed: "Не удалось создать аккаунт пользователя: {error}\n\nПожалуйста, попробуйте еще раз или обратитесь в техническую поддержку.".to_string(),
support_info: "💬 <b>Информация о поддержке</b>\n\nЕсли вам нужна помощь, пожалуйста, свяжитесь с администраторами.\n\nВы также можете ознакомиться с документацией по адресу:\nhttps://github.com/OutFleet".to_string(),
statistics: "📊 <b>Статистика</b>\n\n👥 Всего пользователей: {users}\n🖥️ Всего серверов: {servers}\n📡 Всего входящих подключений: {inbounds}\n⏳ Ожидающих запросов: {pending}".to_string(),
total_users: "👥 Всего пользователей".to_string(),
total_servers: "🖥️ Всего серверов".to_string(),
total_inbounds: "📡 Всего входящих подключений".to_string(),
pending_requests: "⏳ Ожидающих запросов".to_string(),
broadcast_complete: "✅ Рассылка завершена\nОтправлено: {sent}\nНе удалось: {failed}".to_string(),
sent: "Отправлено".to_string(),
failed: "Не удалось".to_string(),
configs_coming_soon: "📋 Ваши конфигурации будут показаны здесь (скоро)".to_string(),
your_configurations: "📋 <b>Ваши конфигурации</b>".to_string(),
no_configs_available: "📋 Нет доступных конфигураций\n\nУ вас пока нет доступа к конфигурациям VPN. Пожалуйста, обратитесь к администратору для получения доступа.".to_string(),
config_copy_message: "📋 <b>{server_name}</b> - {inbound_tag} ({protocol})\n\nСсылка для подключения:".to_string(),
config_copied: "✅ Конфигурация скопирована в буфер обмена".to_string(),
config_not_found: "❌ Конфигурация не найдена".to_string(),
server_configs_title: "🖥️ <b>{server_name}</b> - Ссылки для подключения".to_string(),
error_occurred: "Произошла ошибка".to_string(),
admin_not_found: "Администратор не найден".to_string(),
request_not_found: "Запрос не найден".to_string(),
invalid_request_id: "Неверный ID запроса".to_string(),
}
}
}

View File

@@ -11,6 +11,7 @@ use crate::database::entities::telegram_config::Model as TelegramConfig;
pub mod bot;
pub mod handlers;
pub mod error;
pub mod localization;
pub use error::TelegramError;