Vine/src/command/send.rs
Alexander Medvedev f2653e5747 first commit
2026-09-07 21:16:31 +02:00

362 lines
12 KiB
Rust

use std::net::SocketAddr;
use pumpkin_command::CommandSource;
use pumpkin_command::argument_builder::{ArgumentBuilder, argument, command};
use pumpkin_command::argument_types::core::string::StringArgumentType;
use pumpkin_command::context::command_context::CommandContext;
use pumpkin_command::dispatcher::CommandDispatcher as RawCommandDispatcher;
use pumpkin_command::node::{CommandExecutor, CommandExecutorResult};
use pumpkin_command::suggestion::provider::{SuggestionProvider, SuggestionProviderResult};
use pumpkin_command::suggestion::suggestions::SuggestionsBuilder;
use pumpkin_util::text::TextComponent;
use super::{ProxyCommandSource, command_error};
use crate::session::PlayerAction;
/// Parses a host:port address string
pub fn parse_address(addr: &str) -> Option<(String, u16)> {
if let Ok(socket_addr) = addr.parse::<SocketAddr>() {
return Some((socket_addr.ip().to_string(), socket_addr.port()));
}
if let Some((host, port_str)) = addr.rsplit_once(':')
&& let Ok(port) = port_str.parse::<u16>()
{
return Some((host.to_string(), port));
}
None
}
struct TargetSuggestionProvider;
impl SuggestionProvider<ProxyCommandSource> for TargetSuggestionProvider {
fn suggest(
&self,
context: &CommandContext<ProxyCommandSource>,
builder: SuggestionsBuilder,
) -> SuggestionProviderResult {
let mut builder = builder;
let remaining = builder.remaining_lowercase().to_string();
if "all".starts_with(&remaining) {
builder = builder.suggest("all");
}
for session in context.source.session_manager.list_sessions() {
if session
.username
.to_ascii_lowercase()
.starts_with(&remaining)
{
builder = builder.suggest(session.username);
}
}
builder.build()
}
}
struct ServerSuggestionProvider;
impl SuggestionProvider<ProxyCommandSource> for ServerSuggestionProvider {
fn suggest(
&self,
context: &CommandContext<ProxyCommandSource>,
builder: SuggestionsBuilder,
) -> SuggestionProviderResult {
let mut builder = builder;
let remaining = builder.remaining_lowercase().to_string();
for server in context.source.config.servers.keys() {
if server.to_ascii_lowercase().starts_with(&remaining) {
builder = builder.suggest(server.as_str());
}
}
builder.build()
}
}
struct SendCommandExecutor;
impl CommandExecutor<ProxyCommandSource> for SendCommandExecutor {
fn execute(&self, context: &CommandContext<ProxyCommandSource>) -> CommandExecutorResult {
if !context.source.has_permission("vine.command.send") {
return Err(command_error(
"You do not have permission to execute this command (required: vine.command.send).",
));
}
let target = StringArgumentType::get(context, "target")?;
let server_name = StringArgumentType::get(context, "server")?;
let canonical_server = match context
.source
.config
.servers
.keys()
.find(|k| k.eq_ignore_ascii_case(server_name))
{
Some(name) => name.clone(),
None => {
let mut available: Vec<&String> = context.source.config.servers.keys().collect();
available.sort();
return Err(command_error(format!(
"Unknown backend server '{}'. Available servers: [{}]",
server_name,
available
.into_iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
)));
}
};
if target.eq_ignore_ascii_case("all") {
let sessions = context.source.session_manager.list_sessions();
if sessions.is_empty() {
return Err(command_error("No players currently online to send."));
}
let count = sessions.len();
for session in &sessions {
let _ = session.action_tx.send(PlayerAction::Connect {
server_name: canonical_server.clone(),
});
}
let msg = format!("Sent {} player(s) to '{}'", count, canonical_server);
context.source.send_message(TextComponent::text(msg));
Ok(count as i32)
} else {
let session = context
.source
.session_manager
.get_session(target)
.ok_or_else(|| command_error(format!("Player '{}' is not online.", target)))?;
let _ = session.action_tx.send(PlayerAction::Connect {
server_name: canonical_server.clone(),
});
let msg = format!("Sent '{}' to '{}'", session.username, canonical_server);
context.source.send_message(TextComponent::text(msg));
Ok(1)
}
}
}
pub fn register(dispatcher: &mut RawCommandDispatcher<ProxyCommandSource>) {
dispatcher.register(
command(
"send",
"Sends a player or all players to another backend server",
)
.then(
argument("target", StringArgumentType::SingleWord)
.suggests(TargetSuggestionProvider)
.then(
argument("server", StringArgumentType::SingleWord)
.suggests(ServerSuggestionProvider)
.executes(SendCommandExecutor),
),
),
);
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use tokio::sync::mpsc;
use uuid::Uuid;
use crate::command::{CommandDispatcher, CommandSender};
use crate::config::{BackendConfig, Config, ForwardingMode};
use crate::session::{PlayerSession, SessionManager};
#[test]
fn test_parse_address() {
assert_eq!(
parse_address("127.0.0.1:25565"),
Some(("127.0.0.1".to_string(), 25565))
);
assert_eq!(
parse_address("mc.example.com:25577"),
Some(("mc.example.com".to_string(), 25577))
);
assert_eq!(parse_address("invalid"), None);
}
#[tokio::test]
async fn test_send_command_execution() {
let mut config = Config::default();
config.servers.insert(
"survival".to_string(),
BackendConfig {
address: "127.0.0.1:25567".to_string(),
forwarding: ForwardingMode::None,
secret: None,
motd_line1: None,
motd_line2: None,
icon_path: None,
},
);
let session_manager = Arc::new(SessionManager::new());
let (tx, mut rx) = mpsc::unbounded_channel();
session_manager.register(PlayerSession {
username: "Steve".to_string(),
uuid: Uuid::new_v4(),
current_server: "lobby".to_string(),
action_tx: tx,
});
let source = ProxyCommandSource {
sender: CommandSender::Console,
config: Arc::new(config),
session_manager,
};
let dispatcher = CommandDispatcher::new();
let res = dispatcher.execute_input("send Steve survival", &source);
assert!(res.is_ok());
let action = rx.recv().await;
assert_eq!(
action,
Some(PlayerAction::Connect {
server_name: "survival".to_string(),
})
);
// Player not in admin list is rejected
let (non_admin_tx, _) = mpsc::unbounded_channel();
let non_admin_source = ProxyCommandSource {
sender: CommandSender::Player {
username: "RegularPlayer".to_string(),
action_tx: non_admin_tx,
},
config: Arc::new(Config::default()),
session_manager: Arc::new(SessionManager::new()),
};
let res_denied = dispatcher.execute_input("send Steve survival", &non_admin_source);
assert!(res_denied.is_err());
assert!(
res_denied
.unwrap_err()
.message
.to_pretty_console()
.contains("permission")
);
}
#[test]
fn test_send_command_unknown_server() {
let config = Config::default();
let session_manager = Arc::new(SessionManager::new());
let source = ProxyCommandSource {
sender: CommandSender::Console,
config: Arc::new(config),
session_manager,
};
let dispatcher = CommandDispatcher::new();
let res = dispatcher.execute_input("send Steve nonexistent", &source);
assert!(res.is_err());
assert!(
res.unwrap_err()
.message
.to_pretty_console()
.contains("Unknown backend server")
);
}
#[test]
fn test_send_command_offline_player() {
let mut config = Config::default();
config.servers.insert(
"survival".to_string(),
BackendConfig {
address: "127.0.0.1:25567".to_string(),
forwarding: ForwardingMode::None,
secret: None,
motd_line1: None,
motd_line2: None,
icon_path: None,
},
);
let session_manager = Arc::new(SessionManager::new());
let source = ProxyCommandSource {
sender: CommandSender::Console,
config: Arc::new(config),
session_manager,
};
let dispatcher = CommandDispatcher::new();
let res = dispatcher.execute_input("send Alex survival", &source);
assert!(res.is_err());
assert!(
res.unwrap_err()
.message
.to_pretty_console()
.contains("Player 'Alex' is not online")
);
}
#[test]
fn test_send_command_suggestions() {
let mut config = Config::default();
config.servers.insert(
"survival".to_string(),
BackendConfig {
address: "127.0.0.1:25567".to_string(),
forwarding: ForwardingMode::None,
secret: None,
motd_line1: None,
motd_line2: None,
icon_path: None,
},
);
config.servers.insert(
"lobby".to_string(),
BackendConfig {
address: "127.0.0.1:25565".to_string(),
forwarding: ForwardingMode::None,
secret: None,
motd_line1: None,
motd_line2: None,
icon_path: None,
},
);
let session_manager = Arc::new(SessionManager::new());
let (tx, _rx) = mpsc::unbounded_channel();
session_manager.register(PlayerSession {
username: "Steve".to_string(),
uuid: Uuid::new_v4(),
current_server: "lobby".to_string(),
action_tx: tx,
});
let source = ProxyCommandSource {
sender: CommandSender::Console,
config: Arc::new(config),
session_manager,
};
let dispatcher = CommandDispatcher::new();
let target_suggestions = dispatcher.suggest("send ", &source);
let names: Vec<&str> = target_suggestions
.iter()
.map(|s| s.suggestion.as_str())
.collect();
assert!(names.contains(&"all"));
assert!(names.contains(&"Steve"));
let server_suggestions = dispatcher.suggest("send Steve ", &source);
let servers: Vec<&str> = server_suggestions
.iter()
.map(|s| s.suggestion.as_str())
.collect();
assert!(servers.contains(&"survival"));
assert!(servers.contains(&"lobby"));
}
}