mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-26 10:09:08 +00:00
TG almost works
This commit is contained in:
723
src/services/telegram/handlers/admin.rs
Normal file
723
src/services/telegram/handlers/admin.rs
Normal 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(())
|
||||
}
|
||||
153
src/services/telegram/handlers/mod.rs
Normal file
153
src/services/telegram/handlers/mod.rs
Normal 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(())
|
||||
}
|
||||
137
src/services/telegram/handlers/types.rs
Normal file
137
src/services/telegram/handlers/types.rs
Normal 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)
|
||||
}
|
||||
669
src/services/telegram/handlers/user.rs
Normal file
669
src/services/telegram/handlers/user.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user