Added usermanagement in TG admin

This commit is contained in:
AB from home.homenet
2025-10-24 18:11:34 +03:00
parent c6892b1a73
commit 78bf75b24e
89 changed files with 4389 additions and 2419 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,15 @@
pub mod admin;
pub mod user;
pub mod types;
pub mod user;
// Re-export main handler functions for easier access
pub use admin::*;
pub use user::*;
pub use types::*;
pub use user::*;
use teloxide::{prelude::*, types::CallbackQuery};
use crate::database::DatabaseManager;
use crate::config::AppConfig;
use crate::database::DatabaseManager;
use teloxide::{prelude::*, types::CallbackQuery};
/// Handle bot commands
pub async fn handle_command(
@@ -30,44 +30,62 @@ pub async fn handle_command(
}
Command::Requests => {
// Check if user is admin
if user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
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")],
]);
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?;
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) {
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?;
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) {
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?;
bot.send_message(chat_id, l10n.get(lang, "unauthorized"))
.await?;
}
}
}
@@ -100,68 +118,120 @@ pub async fn handle_callback_query(
db: DatabaseManager,
app_config: AppConfig,
) -> 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::SubscriptionLink => {
handle_subscription_link(bot, &q, &db, &app_config).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) => {
// The request_id is now the full UUID from the mapping
let short_id = types::generate_short_request_id(&request_id);
handle_select_server_access(bot, &q, &short_id, &db).await?;
}
CallbackData::ToggleServer(request_id, server_id) => {
// Both IDs are now full UUIDs from the mapping
let short_request_id = types::generate_short_request_id(&request_id);
let short_server_id = types::generate_short_server_id(&server_id);
handle_toggle_server(bot, &q, &short_request_id, &short_server_id, &db).await?;
}
CallbackData::ApplyServerAccess(request_id) => {
// The request_id is now the full UUID from the mapping
let short_id = types::generate_short_request_id(&request_id);
handle_apply_server_access(bot, &q, &short_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?;
// Wrap all callback handling in a try-catch to send main menu on any error
let result = async {
if let Some(data) = &q.data {
if let Some(callback_data) = CallbackData::parse(data) {
match callback_data {
CallbackData::RequestAccess => {
handle_request_access(bot.clone(), &q, &db).await?;
}
CallbackData::MyConfigs => {
handle_my_configs_edit(bot.clone(), &q, &db).await?;
}
CallbackData::SubscriptionLink => {
handle_subscription_link(bot.clone(), &q, &db, &app_config).await?;
}
CallbackData::Support => {
handle_support(bot.clone(), &q).await?;
}
CallbackData::AdminRequests => {
handle_admin_requests_edit(bot.clone(), &q, &db).await?;
}
CallbackData::RequestList(page) => {
handle_request_list(bot.clone(), &q, &db, page).await?;
}
CallbackData::ApproveRequest(request_id) => {
handle_approve_request(bot.clone(), &q, &request_id, &db).await?;
}
CallbackData::DeclineRequest(request_id) => {
handle_decline_request(bot.clone(), &q, &request_id, &db).await?;
}
CallbackData::ViewRequest(request_id) => {
handle_view_request(bot.clone(), &q, &request_id, &db).await?;
}
CallbackData::ShowServerConfigs(encoded_server_name) => {
handle_show_server_configs(bot.clone(), &q, &encoded_server_name, &db).await?;
}
CallbackData::SelectServerAccess(request_id) => {
// The request_id is now the full UUID from the mapping
let short_id = types::generate_short_request_id(&request_id);
handle_select_server_access(bot.clone(), &q, &short_id, &db).await?;
}
CallbackData::ToggleServer(request_id, server_id) => {
// Both IDs are now full UUIDs from the mapping
let short_request_id = types::generate_short_request_id(&request_id);
let short_server_id = types::generate_short_server_id(&server_id);
handle_toggle_server(bot.clone(), &q, &short_request_id, &short_server_id, &db).await?;
}
CallbackData::ApplyServerAccess(request_id) => {
// The request_id is now the full UUID from the mapping
let short_id = types::generate_short_request_id(&request_id);
handle_apply_server_access(bot.clone(), &q, &short_id, &db).await?;
}
CallbackData::Back => {
// Back to main menu - edit the existing message
handle_start_edit(bot.clone(), &q, &db).await?;
}
CallbackData::BackToConfigs => {
handle_my_configs_edit(bot.clone(), &q, &db).await?;
}
CallbackData::BackToRequests => {
handle_admin_requests_edit(bot.clone(), &q, &db).await?;
}
CallbackData::ManageUsers => {
handle_manage_users(bot.clone(), &q, &db).await?;
}
CallbackData::UserList(page) => {
handle_user_list(bot.clone(), &q, &db, page).await?;
}
CallbackData::UserDetails(user_id) => {
handle_user_details(bot.clone(), &q, &db, &user_id).await?;
}
CallbackData::UserManageAccess(user_id) => {
handle_user_manage_access(bot.clone(), &q, &db, &user_id).await?;
}
CallbackData::UserToggleServer(user_id, server_id) => {
handle_user_toggle_server(bot.clone(), &q, &db, &user_id, &server_id).await?;
}
CallbackData::UserApplyAccess(user_id) => {
handle_user_apply_access(bot.clone(), &q, &db, &user_id).await?;
}
CallbackData::BackToUsers(page) => {
handle_user_list(bot.clone(), &q, &db, page).await?;
}
CallbackData::BackToMenu => {
handle_start_edit(bot.clone(), &q, &db).await?;
}
}
} else {
tracing::warn!("Unknown callback data: {}", data);
return Err("Invalid callback data".into());
}
}
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
}.await;
// If any error occurred, send main menu and answer callback query
if let Err(e) = result {
tracing::warn!("Error handling callback query '{}': {}", q.data.as_deref().unwrap_or("None"), e);
// Answer the callback query first to remove loading state
let _ = bot.answer_callback_query(q.id.clone()).await;
// Try to send main menu
if let Some(message) = q.message {
let chat_id = message.chat().id;
let from = &q.from;
let telegram_id = from.id.0 as i64;
let user_repo = crate::database::repository::UserRepository::new(db.connection());
// Try to send main menu - if this fails too, just log it
if let Err(menu_error) = handle_start(bot, chat_id, telegram_id, from, &user_repo, &db).await {
tracing::error!("Failed to send main menu after callback error: {}", menu_error);
}
} else {
tracing::warn!("Unknown callback data: {}", data);
bot.answer_callback_query(q.id.clone()).await?;
}
}
Ok(())
}
}

View File

@@ -1,7 +1,7 @@
use teloxide::utils::command::BotCommands;
use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup, User};
use teloxide::utils::command::BotCommands;
use super::super::localization::{LocalizationService, Language};
use super::super::localization::{Language, LocalizationService};
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
@@ -27,16 +27,25 @@ pub enum CallbackData {
SubscriptionLink,
Support,
AdminRequests,
ApproveRequest(String), // request_id
DeclineRequest(String), // request_id
ViewRequest(String), // request_id
RequestList(u32), // page number
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
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
ApplyServerAccess(String), // request_id - apply selected servers
ManageUsers,
UserList(u32), // page number
UserDetails(String), // user_id
UserManageAccess(String), // user_id
UserToggleServer(String, String), // user_id, server_id
UserApplyAccess(String), // user_id
BackToUsers(u32), // page number
BackToMenu,
}
impl CallbackData {
@@ -47,9 +56,11 @@ impl CallbackData {
"subscription_link" => Some(CallbackData::SubscriptionLink),
"support" => Some(CallbackData::Support),
"admin_requests" => Some(CallbackData::AdminRequests),
"manage_users" => Some(CallbackData::ManageUsers),
"back" => Some(CallbackData::Back),
"back_to_configs" => Some(CallbackData::BackToConfigs),
"back_to_requests" => Some(CallbackData::BackToRequests),
"back_to_menu" => Some(CallbackData::BackToMenu),
_ => {
if let Some(id) = data.strip_prefix("approve:") {
Some(CallbackData::ApproveRequest(id.to_string()))
@@ -64,7 +75,9 @@ impl CallbackData {
} 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)) = (get_full_request_id(parts[0]), get_full_server_id(parts[1])) {
if let (Some(request_id), Some(server_id)) =
(get_full_request_id(parts[0]), get_full_server_id(parts[1]))
{
Some(CallbackData::ToggleServer(request_id, server_id))
} else {
None
@@ -74,6 +87,31 @@ impl CallbackData {
}
} else if let Some(short_id) = data.strip_prefix("a:") {
get_full_request_id(short_id).map(CallbackData::ApplyServerAccess)
} else if let Some(page_str) = data.strip_prefix("request_list:") {
page_str.parse::<u32>().ok().map(CallbackData::RequestList)
} else if let Some(page_str) = data.strip_prefix("user_list:") {
page_str.parse::<u32>().ok().map(CallbackData::UserList)
} else if let Some(short_user_id) = data.strip_prefix("user_details:") {
get_full_user_id(short_user_id).map(CallbackData::UserDetails)
} else if let Some(short_user_id) = data.strip_prefix("user_manage:") {
get_full_user_id(short_user_id).map(CallbackData::UserManageAccess)
} else if let Some(rest) = data.strip_prefix("user_toggle:") {
let parts: Vec<&str> = rest.split(':').collect();
if parts.len() == 2 {
if let (Some(user_id), Some(server_id)) =
(get_full_user_id(parts[0]), get_full_server_id(parts[1]))
{
Some(CallbackData::UserToggleServer(user_id, server_id))
} else {
None
}
} else {
None
}
} else if let Some(short_user_id) = data.strip_prefix("user_apply:") {
get_full_user_id(short_user_id).map(CallbackData::UserApplyAccess)
} else if let Some(page_str) = data.strip_prefix("back_users:") {
page_str.parse::<u32>().ok().map(CallbackData::BackToUsers)
} else {
None
}
@@ -93,6 +131,10 @@ static REQUEST_COUNTER: OnceLock<Arc<Mutex<u32>>> = OnceLock::new();
static SERVER_ID_MAP: OnceLock<Arc<Mutex<HashMap<String, String>>>> = OnceLock::new();
static SERVER_COUNTER: OnceLock<Arc<Mutex<u32>>> = OnceLock::new();
// Global storage for user ID mappings (short ID -> full UUID)
static USER_ID_MAP: OnceLock<Arc<Mutex<HashMap<String, String>>>> = OnceLock::new();
static USER_COUNTER: OnceLock<Arc<Mutex<u32>>> = 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())))
}
@@ -113,23 +155,31 @@ pub fn get_server_counter() -> &'static Arc<Mutex<u32>> {
SERVER_COUNTER.get_or_init(|| Arc::new(Mutex::new(0)))
}
pub fn get_user_id_map() -> &'static Arc<Mutex<HashMap<String, String>>> {
USER_ID_MAP.get_or_init(|| Arc::new(Mutex::new(HashMap::new())))
}
pub fn get_user_counter() -> &'static Arc<Mutex<u32>> {
USER_COUNTER.get_or_init(|| Arc::new(Mutex::new(0)))
}
/// Generate a short ID for a request UUID and store the mapping
pub fn generate_short_request_id(request_uuid: &str) -> String {
let mut counter = get_request_counter().lock().unwrap();
let mut map = get_request_id_map().lock().unwrap();
// Check if we already have a short ID for this UUID
for (short_id, uuid) in map.iter() {
if uuid == request_uuid {
return short_id.clone();
}
}
// Generate new short ID
*counter += 1;
let short_id = format!("r{}", counter);
map.insert(short_id.clone(), request_uuid.to_string());
short_id
}
@@ -143,19 +193,19 @@ pub fn get_full_request_id(short_id: &str) -> Option<String> {
pub fn generate_short_server_id(server_uuid: &str) -> String {
let mut counter = get_server_counter().lock().unwrap();
let mut map = get_server_id_map().lock().unwrap();
// Check if we already have a short ID for this UUID
for (short_id, uuid) in map.iter() {
if uuid == server_uuid {
return short_id.clone();
}
}
// Generate new short ID
*counter += 1;
let short_id = format!("s{}", counter);
map.insert(short_id.clone(), server_uuid.to_string());
short_id
}
@@ -165,6 +215,32 @@ pub fn get_full_server_id(short_id: &str) -> Option<String> {
map.get(short_id).cloned()
}
/// Generate a short ID for a user UUID and store the mapping
pub fn generate_short_user_id(user_uuid: &str) -> String {
let mut counter = get_user_counter().lock().unwrap();
let mut map = get_user_id_map().lock().unwrap();
// Check if we already have a short ID for this UUID
for (short_id, uuid) in map.iter() {
if uuid == user_uuid {
return short_id.clone();
}
}
// Generate new short ID
*counter += 1;
let short_id = format!("u{}", counter);
map.insert(short_id.clone(), user_uuid.to_string());
short_id
}
/// Get full user UUID from short ID
pub fn get_full_user_id(short_id: &str) -> Option<String> {
let map = get_user_id_map().lock().unwrap();
map.get(short_id).cloned()
}
/// 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())
@@ -173,27 +249,44 @@ pub fn get_user_language(user: &User) -> Language {
/// 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("🔗 Subscription Link", "subscription_link")],
vec![InlineKeyboardButton::callback(l10n.get(lang.clone(), "my_configs"), "my_configs")],
vec![InlineKeyboardButton::callback(l10n.get(lang.clone(), "support"), "support")],
vec![InlineKeyboardButton::callback(
l10n.get(lang.clone(), "subscription_link"),
"subscription_link",
)],
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")]);
keyboard.push(vec![InlineKeyboardButton::callback(
l10n.get(lang.clone(), "user_requests"),
"admin_requests",
)]);
keyboard.push(vec![InlineKeyboardButton::callback(
l10n.get(lang, "manage_users"),
"manage_users",
)]);
}
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")],
])
InlineKeyboardMarkup::new(vec![vec![InlineKeyboardButton::callback(
l10n.get(lang, "get_vpn_access"),
"request_access",
)]])
}
/// Restore UUID from compact format (without dashes)
@@ -201,7 +294,7 @@ 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!(
"{}-{}-{}-{}-{}",
@@ -211,6 +304,6 @@ fn restore_uuid(compact: &str) -> Option<String> {
&compact[16..20],
&compact[20..32]
);
Some(uuid_str)
}
}

View File

@@ -1,11 +1,14 @@
use teloxide::{prelude::*, types::{InlineKeyboardButton, InlineKeyboardMarkup}};
use base64::{Engine, engine::general_purpose};
use base64::{engine::general_purpose, Engine};
use teloxide::{
prelude::*,
types::{InlineKeyboardButton, InlineKeyboardMarkup},
};
use crate::database::DatabaseManager;
use crate::database::repository::{UserRepository, UserRequestRepository};
use super::super::localization::{Language, LocalizationService};
use super::types::{get_main_keyboard, get_new_user_keyboard, get_user_language};
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};
use crate::database::repository::{UserRepository, UserRequestRepository};
use crate::database::DatabaseManager;
/// Handle start command and main menu
pub async fn handle_start(
@@ -28,23 +31,24 @@ pub async fn handle_start_edit(
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,
bot.clone(),
chat_id,
telegram_id,
from,
&user_repo,
db,
Some(regular_msg.id),
Some(q.id.clone())
).await?;
Some(q.id.clone()),
)
.await?;
}
}
Ok(())
}
@@ -61,37 +65,53 @@ async fn handle_start_impl(
) -> 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);
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) {
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 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?;
}
@@ -104,19 +124,29 @@ async fn handle_start_impl(
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 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?;
}
@@ -132,16 +162,16 @@ async fn handle_start_impl(
}
}
}
// 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?;
}
@@ -156,12 +186,12 @@ async fn handle_start_impl(
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?;
}
@@ -176,7 +206,7 @@ async fn handle_start_impl(
bot.send_message(chat_id, "Database error occurred").await?;
}
}
Ok(())
}
@@ -190,56 +220,73 @@ pub async fn handle_request_access(
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 {
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")?;
})
.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) {
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()
if let Some(latest_request) = existing_requests
.iter()
.filter(|r| r.status == "pending")
.max_by_key(|r| r.created_at) {
.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())
]);
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)
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")],
]))
.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");
let _has_declined = existing_requests.iter().any(|r| r.status == "declined");
}
// Create new access request
let dto = CreateUserRequestDto {
telegram_id,
@@ -249,23 +296,28 @@ pub async fn handle_request_access(
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")],
]))
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) => {
@@ -275,7 +327,7 @@ pub async fn handle_request_access(
.await?;
}
}
Ok(())
}
@@ -289,64 +341,83 @@ pub async fn handle_my_configs_edit(
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 {
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")?;
})
.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 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) {
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 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.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();
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 {
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())
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;
@@ -354,14 +425,14 @@ pub async fn handle_my_configs_edit(
}
}
}
// 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() {
@@ -369,96 +440,122 @@ pub async fn handle_my_configs_edit(
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 { "серверов" }
},
if server_count == 1 {
"сервер"
} else if server_count < 5 {
"сервера"
} else {
"серверов"
}
}
Language::English => {
if server_count == 1 { "server" }
else { "servers" }
if server_count == 1 {
"server"
} else {
"servers"
}
}
};
let config_word = match lang {
Language::Russian => {
if total_configs == 1 { "конфигурация" }
else if total_configs < 5 { "конфигурации" }
else { "конфигураций" }
},
if total_configs == 1 {
"конфигурация"
} else if total_configs < 5 {
"конфигурации"
} else {
"конфигураций"
}
}
Language::English => {
if total_configs == 1 { "configuration" }
else { "configurations" }
if total_configs == 1 {
"configuration"
} else {
"configurations"
}
}
};
let protocol_word = match lang {
Language::Russian => {
if protocols.len() == 1 { "протокол" }
else if protocols.len() < 5 { "протокола" }
else { "протоколов" }
},
if protocols.len() == 1 {
"протокол"
} else if protocols.len() < 5 {
"протокола"
} else {
"протоколов"
}
}
Language::English => {
if protocols.len() == 1 { "protocol" }
else { "protocols" }
if protocols.len() == 1 {
"protocol"
} else {
"protocols"
}
}
};
message_lines.push(format!(
"\n📊 {} {}{} {}{} {}",
server_count, server_word,
total_configs, config_word,
protocols.len(), protocol_word
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 {
"ов"
if config_count == 1 {
""
} else if config_count < 5 {
"а"
} else {
"ов"
}
},
}
Language::English => {
if config_count == 1 {
""
} else {
"s"
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(
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")
]);
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 {
@@ -468,10 +565,10 @@ pub async fn handle_my_configs_edit(
.await?;
}
}
bot.answer_callback_query(q.id.clone()).await?;
}
Ok(())
}
@@ -486,42 +583,55 @@ pub async fn handle_show_server_configs(
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 {
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")?;
})
.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 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) {
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 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 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;
@@ -530,28 +640,30 @@ pub async fn handle_show_server_configs(
}
}
}
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)])
];
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" => "🟢",
"vmess" => "🟢",
"trojan" => "🔴",
"shadowsocks" => "🟡",
_ => ""
_ => "",
};
message_lines.push(format!(
"\n{} <b>{} - {}</b> ({})",
protocol_emoji,
@@ -559,17 +671,18 @@ pub async fn handle_show_server_configs(
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 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 {
@@ -579,14 +692,14 @@ pub async fn handle_show_server_configs(
.await?;
}
}
bot.answer_callback_query(q.id.clone()).await?;
} else {
bot.answer_callback_query(q.id.clone())
.text(l10n.get(lang, "unauthorized"))
.await?;
}
Ok(())
}
@@ -598,17 +711,20 @@ pub async fn handle_support(
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 {
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")],
]);
})
.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 {
@@ -618,9 +734,9 @@ pub async fn handle_support(
.await?;
}
}
bot.answer_callback_query(q.id.clone()).await?;
Ok(())
}
@@ -631,40 +747,61 @@ async fn notify_admins_new_request(
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 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"),
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)
let _ = bot
.send_message(ChatId(telegram_id), &message)
.parse_mode(teloxide::types::ParseMode::Html)
.reply_markup(keyboard.clone())
.await;
}
}
}
Ok(())
}
@@ -685,7 +822,7 @@ pub async fn handle_subscription_link(
if let Ok(Some(user)) = user_repo.get_by_telegram_id(telegram_id).await {
// Generate subscription URL
let subscription_url = format!("{}/sub/{}", app_config.web.base_url, user.id);
let message = match lang {
Language::Russian => {
format!(
@@ -695,7 +832,7 @@ pub async fn handle_subscription_link(
💡 <i>Эта ссылка содержит все ваши конфигурации и автоматически обновляется при изменениях</i>",
subscription_url
)
},
}
Language::English => {
format!(
"🔗 <b>Your Subscription Link</b>\n\n\
@@ -707,9 +844,10 @@ pub async fn handle_subscription_link(
}
};
let keyboard = InlineKeyboardMarkup::new(vec![
vec![InlineKeyboardButton::callback(l10n.get(lang, "back"), "back")],
]);
let keyboard = InlineKeyboardMarkup::new(vec![vec![InlineKeyboardButton::callback(
l10n.get(lang, "back"),
"back",
)]]);
// Edit the existing message
if let Some(msg) = &q.message {
@@ -731,4 +869,4 @@ pub async fn handle_subscription_link(
bot.answer_callback_query(q.id.clone()).await?;
Ok(())
}
}