mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-24 17:29:08 +00:00
Added telegram
This commit is contained in:
39
src/services/telegram/bot.rs
Normal file
39
src/services/telegram/bot.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use teloxide::{Bot, prelude::*};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::database::DatabaseManager;
|
||||
use super::handlers;
|
||||
|
||||
/// Run the bot polling loop
|
||||
pub async fn run_polling(
|
||||
bot: Bot,
|
||||
db: DatabaseManager,
|
||||
mut shutdown_rx: oneshot::Receiver<()>,
|
||||
) {
|
||||
tracing::info!("Starting Telegram bot polling...");
|
||||
|
||||
let handler = Update::filter_message()
|
||||
.branch(
|
||||
dptree::entry()
|
||||
.filter_command::<handlers::Command>()
|
||||
.endpoint(handlers::handle_command)
|
||||
)
|
||||
.branch(
|
||||
dptree::endpoint(handlers::handle_message)
|
||||
);
|
||||
|
||||
let mut dispatcher = Dispatcher::builder(bot.clone(), handler)
|
||||
.dependencies(dptree::deps![db])
|
||||
.enable_ctrlc_handler()
|
||||
.build();
|
||||
|
||||
// Run dispatcher with shutdown signal
|
||||
tokio::select! {
|
||||
_ = dispatcher.dispatch() => {
|
||||
tracing::info!("Telegram bot polling stopped");
|
||||
}
|
||||
_ = shutdown_rx => {
|
||||
tracing::info!("Telegram bot received shutdown signal");
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/services/telegram/error.rs
Normal file
46
src/services/telegram/error.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TelegramError {
|
||||
#[error("Bot is not configured")]
|
||||
NotConfigured,
|
||||
|
||||
#[error("Bot is not running")]
|
||||
NotRunning,
|
||||
|
||||
#[error("Invalid bot token")]
|
||||
InvalidToken,
|
||||
|
||||
#[error("User not found")]
|
||||
UserNotFound,
|
||||
|
||||
#[error("User is not authorized")]
|
||||
Unauthorized,
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
Database(String),
|
||||
|
||||
#[error("Telegram API error: {0}")]
|
||||
TelegramApi(String),
|
||||
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<teloxide::RequestError> for TelegramError {
|
||||
fn from(err: teloxide::RequestError) -> Self {
|
||||
Self::TelegramApi(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sea_orm::DbErr> for TelegramError {
|
||||
fn from(err: sea_orm::DbErr) -> Self {
|
||||
Self::Database(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for TelegramError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
Self::Other(err.to_string())
|
||||
}
|
||||
}
|
||||
371
src/services/telegram/handlers.rs
Normal file
371
src/services/telegram/handlers.rs
Normal file
@@ -0,0 +1,371 @@
|
||||
use teloxide::{prelude::*, utils::command::BotCommands};
|
||||
use teloxide::types::Me;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::database::repository::UserRepository;
|
||||
use crate::database::entities::user::CreateUserDto;
|
||||
|
||||
/// Available bot commands
|
||||
#[derive(BotCommands, Clone)]
|
||||
#[command(rename_rule = "lowercase", description = "Available commands:")]
|
||||
pub enum Command {
|
||||
#[command(description = "Start the bot and register")]
|
||||
Start,
|
||||
#[command(description = "Show help message")]
|
||||
Help,
|
||||
#[command(description = "Show your status")]
|
||||
Status,
|
||||
#[command(description = "List available configurations")]
|
||||
Configs,
|
||||
// Admin commands
|
||||
#[command(description = "[Admin] List all users")]
|
||||
Users,
|
||||
#[command(description = "[Admin] List all servers")]
|
||||
Servers,
|
||||
#[command(description = "[Admin] Show statistics")]
|
||||
Stats,
|
||||
#[command(description = "[Admin] Broadcast message", parse_with = "split")]
|
||||
Broadcast { message: String },
|
||||
}
|
||||
|
||||
/// Handle command messages
|
||||
pub async fn handle_command(
|
||||
bot: Bot,
|
||||
msg: Message,
|
||||
cmd: Command,
|
||||
db: DatabaseManager,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let chat_id = msg.chat.id;
|
||||
let from = msg.from.as_ref().ok_or("No sender info")?;
|
||||
let telegram_id = from.id.0 as i64;
|
||||
|
||||
let user_repo = UserRepository::new(db.connection());
|
||||
|
||||
match cmd {
|
||||
Command::Start => {
|
||||
handle_start(bot, chat_id, telegram_id, from, &user_repo).await?;
|
||||
}
|
||||
Command::Help => {
|
||||
bot.send_message(chat_id, Command::descriptions().to_string()).await?;
|
||||
}
|
||||
Command::Status => {
|
||||
handle_status(bot, chat_id, telegram_id, &user_repo, &db).await?;
|
||||
}
|
||||
Command::Configs => {
|
||||
handle_configs(bot, chat_id, telegram_id, &user_repo, &db).await?;
|
||||
}
|
||||
// Admin commands
|
||||
Command::Users => {
|
||||
if !user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
|
||||
bot.send_message(chat_id, "❌ You are not authorized to use this command").await?;
|
||||
return Ok(());
|
||||
}
|
||||
handle_users(bot, chat_id, &user_repo).await?;
|
||||
}
|
||||
Command::Servers => {
|
||||
if !user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
|
||||
bot.send_message(chat_id, "❌ You are not authorized to use this command").await?;
|
||||
return Ok(());
|
||||
}
|
||||
handle_servers(bot, chat_id, &db).await?;
|
||||
}
|
||||
Command::Stats => {
|
||||
if !user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
|
||||
bot.send_message(chat_id, "❌ You are not authorized to use this command").await?;
|
||||
return Ok(());
|
||||
}
|
||||
handle_stats(bot, chat_id, &db).await?;
|
||||
}
|
||||
Command::Broadcast { message } => {
|
||||
if !user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
|
||||
bot.send_message(chat_id, "❌ You are not authorized to use this command").await?;
|
||||
return Ok(());
|
||||
}
|
||||
handle_broadcast(bot, chat_id, message, &user_repo).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle regular text messages
|
||||
pub async fn handle_message(
|
||||
bot: Bot,
|
||||
msg: Message,
|
||||
db: DatabaseManager,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if let Some(text) = msg.text() {
|
||||
if !text.starts_with('/') {
|
||||
bot.send_message(
|
||||
msg.chat.id,
|
||||
"Please use /help to see available commands"
|
||||
).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /start command
|
||||
async fn handle_start(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
telegram_id: i64,
|
||||
from: &teloxide::types::User,
|
||||
user_repo: &UserRepository,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Check if user already exists
|
||||
if let Some(user) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
|
||||
let message = format!(
|
||||
"👋 Welcome back, {}!\n\n\
|
||||
You are already registered.\n\
|
||||
Use /help to see available commands.",
|
||||
user.name
|
||||
);
|
||||
bot.send_message(chat_id, message).await?;
|
||||
} else {
|
||||
// Create new user
|
||||
let username = from.username.as_deref().unwrap_or("Unknown");
|
||||
let full_name = format!(
|
||||
"{} {}",
|
||||
from.first_name,
|
||||
from.last_name.as_deref().unwrap_or("")
|
||||
).trim().to_string();
|
||||
|
||||
let dto = CreateUserDto {
|
||||
name: if !full_name.is_empty() { full_name } else { username.to_string() },
|
||||
comment: Some(format!("Telegram user: @{}", username)),
|
||||
telegram_id: Some(telegram_id),
|
||||
is_telegram_admin: false,
|
||||
};
|
||||
|
||||
match user_repo.create(dto).await {
|
||||
Ok(user) => {
|
||||
let message = format!(
|
||||
"✅ Registration successful!\n\n\
|
||||
Name: {}\n\
|
||||
User ID: {}\n\n\
|
||||
Use /help to see available commands.",
|
||||
user.name, user.id
|
||||
);
|
||||
bot.send_message(chat_id, message).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
format!("❌ Registration failed: {}", e)
|
||||
).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /status command
|
||||
async fn handle_status(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
telegram_id: i64,
|
||||
user_repo: &UserRepository,
|
||||
db: &DatabaseManager,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if let Some(user) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
|
||||
let server_inbound_repo = crate::database::repository::ServerInboundRepository::new(db.connection());
|
||||
let configs = server_inbound_repo.find_by_user_id(user.id).await.unwrap_or_default();
|
||||
|
||||
let admin_status = if user.is_telegram_admin { "Admin" } else { "User" };
|
||||
|
||||
let message = format!(
|
||||
"📊 Your Status\n\n\
|
||||
Name: {}\n\
|
||||
User ID: {}\n\
|
||||
Role: {}\n\
|
||||
Active Configs: {}\n\
|
||||
Registered: {}",
|
||||
user.name,
|
||||
user.id,
|
||||
admin_status,
|
||||
configs.len(),
|
||||
user.created_at.format("%Y-%m-%d %H:%M UTC")
|
||||
);
|
||||
|
||||
bot.send_message(chat_id, message).await?;
|
||||
} else {
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
"❌ You are not registered. Use /start to register."
|
||||
).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /configs command
|
||||
async fn handle_configs(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
telegram_id: i64,
|
||||
user_repo: &UserRepository,
|
||||
db: &DatabaseManager,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if let Some(user) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
|
||||
let server_inbound_repo = crate::database::repository::ServerInboundRepository::new(db.connection());
|
||||
let configs = server_inbound_repo.find_by_user_id(user.id).await.unwrap_or_default();
|
||||
|
||||
if configs.is_empty() {
|
||||
bot.send_message(chat_id, "You don't have any configurations yet.").await?;
|
||||
} else {
|
||||
let mut message = String::from("📋 Your Configurations:\n\n");
|
||||
|
||||
for (i, config) in configs.iter().enumerate() {
|
||||
message.push_str(&format!(
|
||||
"{}. {} (Port: {})\n",
|
||||
i + 1,
|
||||
config.tag,
|
||||
config.port_override.unwrap_or(0)
|
||||
));
|
||||
}
|
||||
|
||||
bot.send_message(chat_id, message).await?;
|
||||
}
|
||||
} else {
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
"❌ You are not registered. Use /start to register."
|
||||
).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /users command (admin only)
|
||||
async fn handle_users(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
user_repo: &UserRepository,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let users = user_repo.get_all(1, 100).await.unwrap_or_default();
|
||||
|
||||
if users.is_empty() {
|
||||
bot.send_message(chat_id, "No users found.").await?;
|
||||
} else {
|
||||
let mut message = String::from("👥 Users:\n\n");
|
||||
|
||||
for (i, user) in users.iter().enumerate() {
|
||||
let telegram_status = if user.telegram_id.is_some() { "✅" } else { "❌" };
|
||||
let admin_status = if user.is_telegram_admin { " (Admin)" } else { "" };
|
||||
|
||||
message.push_str(&format!(
|
||||
"{}. {} {} {}{}\n",
|
||||
i + 1,
|
||||
user.name,
|
||||
telegram_status,
|
||||
user.id,
|
||||
admin_status
|
||||
));
|
||||
}
|
||||
|
||||
bot.send_message(chat_id, message).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /servers command (admin only)
|
||||
async fn handle_servers(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
db: &DatabaseManager,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let server_repo = crate::database::repository::ServerRepository::new(db.connection());
|
||||
let servers = server_repo.get_all().await.unwrap_or_default();
|
||||
|
||||
if servers.is_empty() {
|
||||
bot.send_message(chat_id, "No servers found.").await?;
|
||||
} else {
|
||||
let mut message = String::from("🖥️ Servers:\n\n");
|
||||
|
||||
for (i, server) in servers.iter().enumerate() {
|
||||
let status = if server.status == "active" { "✅" } else { "❌" };
|
||||
|
||||
message.push_str(&format!(
|
||||
"{}. {} {} - {}\n",
|
||||
i + 1,
|
||||
status,
|
||||
server.name,
|
||||
server.hostname
|
||||
));
|
||||
}
|
||||
|
||||
bot.send_message(chat_id, message).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /stats command (admin only)
|
||||
async fn handle_stats(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
db: &DatabaseManager,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let user_repo = UserRepository::new(db.connection());
|
||||
let server_repo = crate::database::repository::ServerRepository::new(db.connection());
|
||||
let inbound_repo = crate::database::repository::ServerInboundRepository::new(db.connection());
|
||||
|
||||
let user_count = user_repo.count().await.unwrap_or(0);
|
||||
let server_count = server_repo.count().await.unwrap_or(0);
|
||||
let inbound_count = inbound_repo.count().await.unwrap_or(0);
|
||||
|
||||
let message = format!(
|
||||
"📊 Statistics\n\n\
|
||||
Total Users: {}\n\
|
||||
Total Servers: {}\n\
|
||||
Total Inbounds: {}",
|
||||
user_count,
|
||||
server_count,
|
||||
inbound_count
|
||||
);
|
||||
|
||||
bot.send_message(chat_id, message).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /broadcast command (admin only)
|
||||
async fn handle_broadcast(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
message: String,
|
||||
user_repo: &UserRepository,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let users = user_repo.get_all(1, 1000).await.unwrap_or_default();
|
||||
let mut sent_count = 0;
|
||||
let mut failed_count = 0;
|
||||
|
||||
for user in users {
|
||||
if let Some(telegram_id) = user.telegram_id {
|
||||
match bot.send_message(ChatId(telegram_id), &message).await {
|
||||
Ok(_) => sent_count += 1,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to send broadcast to {}: {}", telegram_id, e);
|
||||
failed_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
format!(
|
||||
"✅ Broadcast complete\n\
|
||||
Sent: {}\n\
|
||||
Failed: {}",
|
||||
sent_count, failed_count
|
||||
)
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
175
src/services/telegram/mod.rs
Normal file
175
src/services/telegram/mod.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use teloxide::{Bot, prelude::*};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::database::repository::TelegramConfigRepository;
|
||||
use crate::database::entities::telegram_config::Model as TelegramConfig;
|
||||
|
||||
pub mod bot;
|
||||
pub mod handlers;
|
||||
pub mod error;
|
||||
|
||||
pub use error::TelegramError;
|
||||
|
||||
/// Main Telegram service that manages the bot lifecycle
|
||||
pub struct TelegramService {
|
||||
db: DatabaseManager,
|
||||
bot: Arc<RwLock<Option<Bot>>>,
|
||||
config: Arc<RwLock<Option<TelegramConfig>>>,
|
||||
shutdown_signal: Arc<RwLock<Option<tokio::sync::oneshot::Sender<()>>>>,
|
||||
}
|
||||
|
||||
impl TelegramService {
|
||||
/// Create a new Telegram service
|
||||
pub fn new(db: DatabaseManager) -> Self {
|
||||
Self {
|
||||
db,
|
||||
bot: Arc::new(RwLock::new(None)),
|
||||
config: Arc::new(RwLock::new(None)),
|
||||
shutdown_signal: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize and start the bot if active configuration exists
|
||||
pub async fn initialize(&self) -> Result<()> {
|
||||
let repo = TelegramConfigRepository::new(self.db.connection());
|
||||
|
||||
// Get active configuration
|
||||
if let Some(config) = repo.get_active().await? {
|
||||
self.start_with_config(config).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start bot with specific configuration
|
||||
pub async fn start_with_config(&self, config: TelegramConfig) -> Result<()> {
|
||||
// Stop existing bot if running
|
||||
self.stop().await?;
|
||||
|
||||
// Create new bot instance
|
||||
let bot = Bot::new(&config.bot_token);
|
||||
|
||||
// Verify token by calling getMe
|
||||
match bot.get_me().await {
|
||||
Ok(me) => {
|
||||
let username = me.user.username.unwrap_or_default();
|
||||
tracing::info!("Telegram bot started: @{}", username);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(anyhow::anyhow!("Invalid bot token: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// Store bot and config
|
||||
*self.bot.write().await = Some(bot.clone());
|
||||
*self.config.write().await = Some(config.clone());
|
||||
|
||||
// Start polling in background
|
||||
if config.is_active {
|
||||
self.start_polling(bot).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start polling for updates
|
||||
async fn start_polling(&self, bot: Bot) -> Result<()> {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
*self.shutdown_signal.write().await = Some(tx);
|
||||
|
||||
let db = self.db.clone();
|
||||
|
||||
// Spawn polling task
|
||||
tokio::spawn(async move {
|
||||
bot::run_polling(bot, db, rx).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop the bot
|
||||
pub async fn stop(&self) -> Result<()> {
|
||||
// Send shutdown signal if polling is running
|
||||
if let Some(tx) = self.shutdown_signal.write().await.take() {
|
||||
let _ = tx.send(()); // Ignore error if receiver is already dropped
|
||||
}
|
||||
|
||||
// Clear bot and config
|
||||
*self.bot.write().await = None;
|
||||
*self.config.write().await = None;
|
||||
|
||||
tracing::info!("Telegram bot stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update configuration and restart if needed
|
||||
pub async fn update_config(&self, config_id: Uuid) -> Result<()> {
|
||||
let repo = TelegramConfigRepository::new(self.db.connection());
|
||||
|
||||
if let Some(config) = repo.find_by_id(config_id).await? {
|
||||
if config.is_active {
|
||||
self.start_with_config(config).await?;
|
||||
} else {
|
||||
self.stop().await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get current bot status
|
||||
pub async fn get_status(&self) -> BotStatus {
|
||||
let bot_guard = self.bot.read().await;
|
||||
let config_guard = self.config.read().await;
|
||||
|
||||
BotStatus {
|
||||
is_running: bot_guard.is_some(),
|
||||
config: config_guard.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send message to user
|
||||
pub async fn send_message(&self, chat_id: i64, text: String) -> Result<()> {
|
||||
let bot_guard = self.bot.read().await;
|
||||
|
||||
if let Some(bot) = bot_guard.as_ref() {
|
||||
bot.send_message(ChatId(chat_id), text).await?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Bot is not running"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Send message to all admins
|
||||
pub async fn broadcast_to_admins(&self, text: String) -> Result<()> {
|
||||
let bot_guard = self.bot.read().await;
|
||||
|
||||
if let Some(bot) = bot_guard.as_ref() {
|
||||
let user_repo = crate::database::repository::UserRepository::new(self.db.connection());
|
||||
let admins = user_repo.get_telegram_admins().await?;
|
||||
|
||||
for admin in admins {
|
||||
if let Some(telegram_id) = admin.telegram_id {
|
||||
if let Err(e) = bot.send_message(ChatId(telegram_id), text.clone()).await {
|
||||
tracing::warn!("Failed to send message to admin {}: {}", telegram_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Bot is not running"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bot status information
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct BotStatus {
|
||||
pub is_running: bool,
|
||||
pub config: Option<TelegramConfig>,
|
||||
}
|
||||
Reference in New Issue
Block a user