From c81e682de03c7ade709d6cd82953302abcc709fa Mon Sep 17 00:00:00 2001 From: toast Date: Mon, 5 Feb 2024 22:49:04 +1100 Subject: [PATCH] I am very satisfied with the results. --- Cargo.lock | 11 ++- Cargo.toml | 3 +- src/commands/gameserver.rs | 42 +++----- src/commands/status.rs | 185 +++++++++++++----------------------- src/controllers/database.rs | 12 --- src/models/gameservers.rs | 26 +---- 6 files changed, 94 insertions(+), 185 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa7fabc..4602ff7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -822,13 +822,14 @@ dependencies = [ [[package]] name = "kon" -version = "0.1.13" +version = "0.1.14" dependencies = [ "cargo_toml", "gamedig", "once_cell", "poise", "reqwest", + "serde", "serde_json", "serenity", "sysinfo", @@ -1520,18 +1521,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.194" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.194" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d6c267b..cf19a79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kon" -version = "0.1.13" +version = "0.1.14" rust-version = "1.74" edition = "2021" @@ -12,6 +12,7 @@ gamedig = "0.4.1" once_cell = "1.19.0" poise = "0.6.1" reqwest = { version = "0.11.24", features = ["json"] } +serde = "1.0.196" serde_json = "1.0.113" serenity = "0.12.0" sysinfo = "0.30.5" diff --git a/src/commands/gameserver.rs b/src/commands/gameserver.rs index 437a69f..2ead341 100644 --- a/src/commands/gameserver.rs +++ b/src/commands/gameserver.rs @@ -11,16 +11,22 @@ use serenity::{ Stream, StreamExt }, - all::Mentionable, builder::CreateActionRow, builder::CreateEmbed, }; use poise::{ CreateReply, serenity_prelude, - serenity_prelude::ButtonStyle + serenity_prelude::ButtonStyle, + ChoiceParameter }; +#[derive(Debug, poise::ChoiceParameter)] +enum GameNames { + #[name = "Minecraft"] + Minecraft +} + /// Manage the game servers for this guild #[poise::command(slash_command, subcommands("add", "remove", "update", "list"), subcommand_required, guild_only)] pub async fn gameserver(_: poise::Context<'_, (), Error>) -> Result<(), Error> { @@ -32,25 +38,9 @@ pub async fn gameserver(_: poise::Context<'_, (), Error>) -> Result<(), Error> { pub async fn add( ctx: poise::Context<'_, (), Error>, #[description = "Server name as shown in-game or friendly name"] server_name: String, - #[description = "Which game is this server running?"] game_name: String, - #[channel_types("Text")] #[description = "Which channel should this server be restricted to?"] guild_channel: serenity_prelude::GuildChannel, + #[description = "Which game is this server running?"] game_name: GameNames, #[description = "IP address/domain of the server (Include the port if it has one, e.g 127.0.0.1:8080)"] ip_address: String ) -> Result<(), Error> { - let unsupported_games_list = [ - "ATS", - "ETS2", - "Euro Truck Simulator 2", - "American Truck Simulator", - ]; - if unsupported_games_list.contains(&game_name.as_str()) { - ctx.send(CreateReply::default() - .ephemeral(true) - .content(format!("Sorry, `{}` is not supported yet due to database design.", game_name)) - ).await?; - - return Ok(()); - } - let action_row = CreateActionRow::Buttons(vec![ serenity_prelude::CreateButton::new("add-confirm") .style(ButtonStyle::Success) @@ -66,9 +56,8 @@ pub async fn add( .description(format!(" **Server name:** `{}` **Game name:** `{}` - **Channel:** {} **IP Address:** `{}` - ", server_name, game_name, guild_channel.mention(), ip_address)) + ", server_name, game_name.name(), ip_address)) .color(EMBED_COLOR) ) .components(vec![action_row]); @@ -76,7 +65,6 @@ pub async fn add( ctx.send(reply).await?; while let Some(collector) = serenity_prelude::ComponentInteractionCollector::new(ctx) - .channel_id(ctx.channel_id()) .guild_id(ctx.guild_id().unwrap()) .author_id(ctx.author().id) .timeout(std::time::Duration::from_secs(30)) @@ -86,8 +74,7 @@ pub async fn add( let result = Gameservers::add_server( ctx.guild_id().unwrap().into(), server_name.as_str(), - game_name.as_str(), - guild_channel.id.into(), + game_name.name(), ip_address.as_str() ).await; @@ -155,7 +142,6 @@ pub async fn remove( ctx.send(reply).await?; while let Some(collector) = serenity_prelude::ComponentInteractionCollector::new(ctx) - .channel_id(ctx.channel_id()) .guild_id(ctx.guild_id().unwrap()) .author_id(ctx.author().id) .timeout(std::time::Duration::from_secs(30)) @@ -207,15 +193,13 @@ pub async fn remove( pub async fn update( ctx: poise::Context<'_, (), Error>, #[description = "Server name"] #[autocomplete = "ac_server_name"] server_name: String, - #[description = "Game name"] game_name: String, - #[description = "Channel"] #[channel_types("Text")] guild_channel: serenity_prelude::GuildChannel, + #[description = "Game name"] game_name: GameNames, #[description = "IP address"] ip_address: String ) -> Result<(), Error> { let result = Gameservers::update_server( ctx.guild_id().unwrap().into(), &server_name, - &game_name, - guild_channel.id.into(), + &game_name.name(), &ip_address ).await; diff --git a/src/commands/status.rs b/src/commands/status.rs index 7f281d1..539dadd 100644 --- a/src/commands/status.rs +++ b/src/commands/status.rs @@ -5,21 +5,7 @@ use crate::{ commands::gameserver::ac_server_name }; -use gamedig::protocols::{ - valve::{ - Engine, - Response, - GatheringSettings - }, - valve, - minecraft, - minecraft::RequestSettings, - types::TimeoutSettings -}; use std::{ - str::FromStr, - net::SocketAddr, - time::Duration, collections::HashMap, env::var }; @@ -27,76 +13,35 @@ use reqwest::{ Client, header::USER_AGENT }; -use tokio::{ - net::lookup_host, - join -}; +use tokio::join; use poise::CreateReply; use serenity::builder::CreateEmbed; use once_cell::sync::Lazy; use cargo_toml::Manifest; +use serde::Deserialize; use serde_json::Value; static PMS_BASE: Lazy = Lazy::new(|| var("WG_PMS").expect("Expected a \"WG_PMS\" in the envvar but none was found") ); -fn query_ats_server() -> Result { - let server_ip = var("ATS_SERVER_IP").expect("Expected a \"ATS_SERVER_IP\" in the envvar but none was found"); - let addr = SocketAddr::from_str(&server_ip).unwrap(); - let engine = Engine::Source(None); - let gather_settings = GatheringSettings { - players: true, - rules: false, - check_app_id: false - }; - - let read_timeout = Duration::from_secs(2); - let write_timeout = Duration::from_secs(2); - let retries = 1; - let timeout_settings = TimeoutSettings::new( - Some(read_timeout), - Some(write_timeout), - retries - ).unwrap(); - - let response = valve::query( - &addr, - engine, - Some(gather_settings), - Some(timeout_settings) - ); - - Ok(response?) +#[derive(Deserialize)] +struct MinecraftQueryData { + motd: MinecraftMotd, + players: MinecraftPlayers, + version: String, + online: bool } -async fn query_gameserver(ip_address: &str) -> Result> { - println!("Querying {}", ip_address); +#[derive(Deserialize)] +struct MinecraftMotd { + clean: Vec +} - let full_address = if ip_address.contains(':') { - String::from(ip_address) - } else { - format!("{}:25565", ip_address) - }; - - let addr = match SocketAddr::from_str(&full_address) { - Ok(addr) => addr, - Err(_) => { - let mut addrs = lookup_host(&full_address).await?; - addrs.next().ok_or("Address lookup failed")? - } - }; - - let response = minecraft::query_java(&addr, None, Some(RequestSettings { - hostname: addr.to_string(), - protocol_version: -1 - })); - println!("{:?}", response); - - match response { - Ok(response) => Ok(response), - Err(why) => Err(Box::new(why)) - } +#[derive(Deserialize)] +struct MinecraftPlayers { + online: i32, + max: i32 } async fn pms_serverstatus(url: &str) -> Result, Error> { @@ -113,30 +58,26 @@ async fn pms_serverstatus(url: &str) -> Result, Error> { Ok(servers) } -/// Query the server statuses -#[poise::command(slash_command, subcommands("ats", "wg", "mc"), subcommand_required)] -pub async fn status(_: poise::Context<'_, (), Error>) -> Result<(), Error> { - Ok(()) +async fn gs_query_minecraft(server_ip: &str) -> Result { + let bot_version = Manifest::from_path("Cargo.toml").unwrap().package.unwrap().version.unwrap(); + + let client = Client::new(); + let req = client.get(format!("https://api.mcsrvstat.us/2/{}", server_ip)) + .header(USER_AGENT, format!("Kon/{}/Rust", bot_version)) + .send() + .await?; + + if req.status().is_success() { + let data: MinecraftQueryData = req.json().await?; + Ok(data) + } else { + return Err(Error::from("Failed to query the server.")); + } } -/// Retrieve the server status from ATS -#[poise::command(slash_command)] -pub async fn ats(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> { - let embed = CreateEmbed::new().color(EMBED_COLOR); - match query_ats_server() { - Ok(response) => { - ctx.send(CreateReply::default() - .embed(embed - .title("American Truck Simulator Server Status") - .fields(vec![ - ("Name", format!("{}", response.info.name), true), - ("Players", format!("{}/{}", response.info.players_online, response.info.players_maximum), true) - ]) - )).await?; - } - Err(why) => println!("Error querying the server: {:?}", why) - } - +/// Query the server statuses +#[poise::command(slash_command, subcommands("wg", "gs"), subcommand_required)] +pub async fn status(_: poise::Context<'_, (), Error>) -> Result<(), Error> { Ok(()) } @@ -175,38 +116,48 @@ pub async fn wg(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> { Ok(()) } -/// Retrieve the server data from given Minecraft Java server +/// Retrieve the given server data from gameservers DB #[poise::command(slash_command, guild_only)] -pub async fn mc( +pub async fn gs( ctx: poise::Context<'_, (), Error>, #[description = "Server name"] #[autocomplete = "ac_server_name"] server_name: String ) -> Result<(), Error> { - let server = Gameservers::get_server_data(ctx.guild_id().unwrap().into(), &server_name).await; + let server_data = Gameservers::get_server_data(ctx.guild_id().unwrap().into(), &server_name).await?; - match server { - Ok(data) => { - let name = &data[0]; - let game = &data[1]; - let ip = &data[2]; + // Extract values from a Vec above + let game_name = &server_data[1]; + let ip_address = &server_data[2]; - let query_result = query_gameserver(ip).await?; - ctx.send(CreateReply::default() - .embed(CreateEmbed::new() - .title(format!("{} Server Status", name)) - .fields(vec![ - ("Game", format!("{}", game), true), - ("Players", format!("{}/{}", query_result.players_online, query_result.players_maximum), true), - ("Version", format!("{}", query_result.game_version), true) - ]) - .color(EMBED_COLOR) - ) - ).await?; - // ctx.send(CreateReply::default().content("aaa")).await?; + match game_name.as_str() { + "Minecraft" => { + let result = gs_query_minecraft(ip_address).await?; + let embed = CreateEmbed::new().color(EMBED_COLOR); + + if result.online { + let mut embed_fields = Vec::new(); + embed_fields.push(("Server IP".to_owned(), ip_address.to_owned(), true)); + embed_fields.push((format!("\u{200b}"), format!("\u{200b}"), true)); + embed_fields.push(("MOTD".to_owned(), format!("{}", result.motd.clean[0]), true)); + embed_fields.push(("Players".to_owned(), format!("**{}**/**{}**", result.players.online, result.players.max), true)); + embed_fields.push(("Version".to_owned(), result.version, true)); + + ctx.send(CreateReply::default() + .embed(embed + .title(server_name) + .fields(embed_fields) + ) + ).await?; + } else { + ctx.send(CreateReply::default() + .content(format!("Server **{}** (`{}`) is currently offline.", server_name, ip_address)) + ).await?; + } }, - Err(why) => { - ctx.send(CreateReply::default().content(format!("Error retrieving the server data: {:?}", why))).await?; + _ => { + ctx.send(CreateReply::default().content("Game not supported yet.")).await?; + return Ok(()); } } - + Ok(()) } diff --git a/src/controllers/database.rs b/src/controllers/database.rs index 5063148..38c0d33 100644 --- a/src/controllers/database.rs +++ b/src/controllers/database.rs @@ -20,24 +20,12 @@ impl DatabaseController { } }); - // MPServers - client.batch_execute(" - CREATE TABLE IF NOT EXISTS mpservers ( - server_name VARCHAR(255) NOT NULL PRIMARY KEY, - guild_owner BIGINT NOT NULL, - is_active BOOLEAN NOT NULL, - ip_address VARCHAR(255) NOT NULL, - md5_code VARCHAR(255) NOT NULL - ); - ").await?; - // Gameservers client.batch_execute(" CREATE TABLE IF NOT EXISTS gameservers ( server_name VARCHAR(255) NOT NULL PRIMARY KEY, game_name VARCHAR(255) NOT NULL, guild_owner BIGINT NOT NULL, - guild_channel BIGINT NOT NULL, ip_address VARCHAR(255) NOT NULL ); ").await?; diff --git a/src/models/gameservers.rs b/src/models/gameservers.rs index 9998f5f..df92fa6 100644 --- a/src/models/gameservers.rs +++ b/src/models/gameservers.rs @@ -4,7 +4,6 @@ pub struct Gameservers { pub server_name: String, pub game_name: String, pub guild_owner: i64, - pub guild_channel: i64, pub ip_address: String } @@ -22,7 +21,6 @@ impl Gameservers { server_name: row.get("server_name"), game_name: row.get("game_name"), guild_owner: row.get("guild_owner"), - guild_channel: row.get("guild_channel"), ip_address: row.get("ip_address") }); } @@ -34,14 +32,13 @@ impl Gameservers { guild_id: u64, server_name: &str, game_name: &str, - guild_channel: u64, ip_address: &str ) -> Result<(), tokio_postgres::Error> { let client = DatabaseController::new().await?.client; client.execute(" - INSERT INTO gameservers (server_name, game_name, guild_owner, guild_channel, ip_address) + INSERT INTO gameservers (server_name, game_name, guild_owner, ip_address) VALUES ($1, $2, $3, $4, $5) - ", &[&server_name, &game_name, &(guild_id as i64), &(guild_channel as i64), &ip_address]).await?; + ", &[&server_name, &game_name, &(guild_id as i64), &ip_address]).await?; Ok(()) } @@ -60,31 +57,18 @@ impl Gameservers { guild_id: u64, server_name: &str, game_name: &str, - guild_channel: u64, ip_address: &str ) -> Result<(), tokio_postgres::Error> { let client = DatabaseController::new().await?.client; client.execute(" UPDATE gameservers - SET game_name = $1, guild_channel = $2, ip_address = $3 - WHERE guild_owner = $4 AND server_name = $5 - ", &[&game_name, &(guild_channel as i64), &ip_address, &(guild_id as i64), &server_name]).await?; + SET game_name = $1, ip_address = $2 + WHERE guild_owner = $3 AND server_name = $4 + ", &[&game_name, &ip_address, &(guild_id as i64), &server_name]).await?; Ok(()) } - // To be added at some point. Not sure if it's needed. - /* pub async fn update_name(guild_id: u64, server_name: &str, new_name: &str) -> Result<(), tokio_postgres::Error> { - let client = DatabaseController::new().await?.client; - client.execute(" - UPDATE gameservers - SET server_name = $1 - WHERE guild_owner = $2 AND server_name = $3 - ", &[&new_name, &(guild_id as i64), &server_name]).await?; - - Ok(()) - } */ - pub async fn get_server_names(guild_id: u64) -> Result, tokio_postgres::Error> { let client = DatabaseController::new().await?.client; let rows = client.query("