diff --git a/Cargo.lock b/Cargo.lock index c3aeeda..27e5eba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -822,7 +822,7 @@ dependencies = [ [[package]] name = "kon" -version = "0.1.12" +version = "0.1.13" dependencies = [ "cargo_toml", "gamedig", diff --git a/Cargo.toml b/Cargo.toml index e3fb396..18f91a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kon" -version = "0.1.12" +version = "0.1.13" rust-version = "1.74" edition = "2021" diff --git a/src/commands/gameserver.rs b/src/commands/gameserver.rs index 23ab6f5..437a69f 100644 --- a/src/commands/gameserver.rs +++ b/src/commands/gameserver.rs @@ -5,9 +5,15 @@ use crate::{ }; use serenity::{ + futures::{ + stream::iter, + future::ready, + Stream, + StreamExt + }, all::Mentionable, builder::CreateActionRow, - builder::CreateEmbed + builder::CreateEmbed, }; use poise::{ CreateReply, @@ -46,10 +52,10 @@ pub async fn add( } let action_row = CreateActionRow::Buttons(vec![ - serenity_prelude::CreateButton::new("confirm") + serenity_prelude::CreateButton::new("add-confirm") .style(ButtonStyle::Success) .label("Yes"), - serenity_prelude::CreateButton::new("cancel") + serenity_prelude::CreateButton::new("add-cancel") .style(ButtonStyle::Danger) .label("No") ]); @@ -76,7 +82,7 @@ pub async fn add( .timeout(std::time::Duration::from_secs(30)) .await { - if collector.data.custom_id == "confirm" { + if collector.data.custom_id == "add-confirm" { let result = Gameservers::add_server( ctx.guild_id().unwrap().into(), server_name.as_str(), @@ -93,6 +99,7 @@ pub async fn add( ctx, serenity_prelude::EditMessage::new() .content("*Confirmed, added the server to database*") + .embeds(Vec::new()) .components(Vec::new()) ).await?; }, @@ -101,17 +108,19 @@ pub async fn add( ctx, serenity_prelude::EditMessage::new() .content(format!("*Error adding server to database: {:?}*", y)) + .embeds(Vec::new()) .components(Vec::new()) ).await?; } } - } else if collector.data.custom_id == "cancel" { + } else if collector.data.custom_id == "add-cancel" { let mut msg = collector.message.clone(); msg.edit( ctx, serenity_prelude::EditMessage::new() .content("*Command cancelled*") + .embeds(Vec::new()) .components(Vec::new()) ).await?; } @@ -122,16 +131,102 @@ pub async fn add( /// Remove a game server from the database #[poise::command(slash_command)] -pub async fn remove(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> { - ctx.send(CreateReply::default().content("Yet to be implemented.")).await?; +pub async fn remove( + ctx: poise::Context<'_, (), Error>, + #[description = "Server name"] #[autocomplete = "ac_server_name"] server_name: String +) -> Result<(), Error> { + let reply = CreateReply::default() + .embed(CreateEmbed::new() + .title("Are you sure you want to remove this server?") + .description(format!("**Server name:** `{}`", server_name)) + .color(EMBED_COLOR) + ) + .components(vec![ + CreateActionRow::Buttons(vec![ + serenity_prelude::CreateButton::new("delete-confirm") + .style(ButtonStyle::Success) + .label("Yes"), + serenity_prelude::CreateButton::new("delete-cancel") + .style(ButtonStyle::Danger) + .label("No") + ]) + ]); + + 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)) + .await + { + if collector.data.custom_id == "delete-confirm" { + let result = Gameservers::remove_server(ctx.guild_id().unwrap().into(), server_name.as_str()).await; + + let mut msg = collector.message.clone(); + + match result { + Ok(_) => { + msg.edit( + ctx, + serenity_prelude::EditMessage::new() + .content("*Confirmed, removed the server from database*") + .embeds(Vec::new()) + .components(Vec::new()) + ).await?; + }, + Err(y) => { + msg.edit( + ctx, + serenity_prelude::EditMessage::new() + .content(format!("*Error removing server from database: {:?}*", y)) + .embeds(Vec::new()) + .components(Vec::new()) + ).await?; + } + } + } else if collector.data.custom_id == "delete-cancel" { + let mut msg = collector.message.clone(); + + msg.edit( + ctx, + serenity_prelude::EditMessage::new() + .content("*Command cancelled*") + .embeds(Vec::new()) + .components(Vec::new()) + ).await?; + } + } Ok(()) } /// Update a game server in the database #[poise::command(slash_command)] -pub async fn update(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> { - ctx.send(CreateReply::default().content("Yet to be implemented.")).await?; +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 = "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(), + &ip_address + ).await; + + match result { + Ok(_) => { + ctx.send(CreateReply::default().content("Updated the server in database.")).await?; + }, + Err(y) => { + ctx.send(CreateReply::default().content(format!("Error updating the server in database: {:?}", y))).await?; + } + } Ok(()) } @@ -158,3 +253,22 @@ pub async fn list(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> { Ok(()) } + +pub async fn ac_server_name<'a>( + ctx: poise::Context<'_, (), Error>, + partial: &'a str +) -> impl Stream + 'a { + let result = Gameservers::get_server_names(ctx.guild_id().unwrap().into()).await; + + let names = match result { + Ok(names_vector) => names_vector, + Err(y) => { + println!("Error retrieving server names: {:?}", y); + Vec::new() + } + }; + + iter(names) + .filter(move |server_name| ready(server_name.starts_with(partial))) + .map(|server_name| server_name.to_string()) +} diff --git a/src/commands/status.rs b/src/commands/status.rs index db98d71..7f281d1 100644 --- a/src/commands/status.rs +++ b/src/commands/status.rs @@ -1,15 +1,20 @@ use crate::{ - models::mpservers::MPServers, + Error, EMBED_COLOR, - Error + models::gameservers::Gameservers, + commands::gameserver::ac_server_name }; use gamedig::protocols::{ valve::{ - Engine, GatheringSettings, Response + Engine, + Response, + GatheringSettings }, - types::TimeoutSettings, - valve + valve, + minecraft, + minecraft::RequestSettings, + types::TimeoutSettings }; use std::{ str::FromStr, @@ -22,18 +27,21 @@ use reqwest::{ Client, header::USER_AGENT }; +use tokio::{ + net::lookup_host, + join +}; use poise::CreateReply; use serenity::builder::CreateEmbed; use once_cell::sync::Lazy; use cargo_toml::Manifest; use serde_json::Value; -use tokio::join; static PMS_BASE: Lazy = Lazy::new(|| var("WG_PMS").expect("Expected a \"WG_PMS\" in the envvar but none was found") ); -fn query_server() -> Result { +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); @@ -62,6 +70,35 @@ fn query_server() -> Result { Ok(response?) } +async fn query_gameserver(ip_address: &str) -> Result> { + println!("Querying {}", ip_address); + + 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)) + } +} + async fn pms_serverstatus(url: &str) -> Result, Error> { let bot_version = Manifest::from_path("Cargo.toml").unwrap().package.unwrap().version.unwrap(); @@ -77,7 +114,7 @@ async fn pms_serverstatus(url: &str) -> Result, Error> { } /// Query the server statuses -#[poise::command(slash_command, subcommands("ats", "wg", "fs"), subcommand_required)] +#[poise::command(slash_command, subcommands("ats", "wg", "mc"), subcommand_required)] pub async fn status(_: poise::Context<'_, (), Error>) -> Result<(), Error> { Ok(()) } @@ -86,7 +123,7 @@ pub async fn status(_: poise::Context<'_, (), Error>) -> Result<(), Error> { #[poise::command(slash_command)] pub async fn ats(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> { let embed = CreateEmbed::new().color(EMBED_COLOR); - match query_server() { + match query_ats_server() { Ok(response) => { ctx.send(CreateReply::default() .embed(embed @@ -138,17 +175,38 @@ pub async fn wg(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> { Ok(()) } -/// Retrieve the data from Farming Simulator 22 server +/// Retrieve the server data from given Minecraft Java server #[poise::command(slash_command, guild_only)] -pub async fn fs(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> { - // let embed = CreateEmbed::new().color(EMBED_COLOR); - let server = MPServers::get_server_ip(ctx.guild_id().unwrap().into(), "testserver").await?; - let ip = server.0; - let md5 = server.1; +pub async fn mc( + 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; - ctx.send(CreateReply::default().content(format!("IP: {}\nMD5: {}", ip, md5))).await?; - - // ctx.send(CreateReply::default().content("This command is not yet implemented")).await?; + match server { + Ok(data) => { + let name = &data[0]; + let game = &data[1]; + let ip = &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?; + }, + Err(why) => { + ctx.send(CreateReply::default().content(format!("Error retrieving the server data: {:?}", why))).await?; + } + } + Ok(()) } diff --git a/src/models/gameservers.rs b/src/models/gameservers.rs index a0f60a3..9998f5f 100644 --- a/src/models/gameservers.rs +++ b/src/models/gameservers.rs @@ -8,7 +8,6 @@ pub struct Gameservers { pub ip_address: String } -#[allow(dead_code)] impl Gameservers { pub async fn list_servers(guild_id: u64) -> Result, tokio_postgres::Error> { let client = DatabaseController::new().await?.client; @@ -74,7 +73,8 @@ impl Gameservers { Ok(()) } - pub async fn update_name(guild_id: u64, server_name: &str, new_name: &str) -> Result<(), tokio_postgres::Error> { + // 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 @@ -83,5 +83,37 @@ impl Gameservers { ", &[&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(" + SELECT server_name FROM gameservers + WHERE guild_owner = $1 + ", &[&(guild_id as i64)]).await?; + + let mut servers = Vec::new(); + for row in rows { + servers.push(row.get("server_name")); + } + + Ok(servers) + } + + pub async fn get_server_data(guild_id: u64, server_name: &str) -> Result, tokio_postgres::Error> { + let client = DatabaseController::new().await?.client; + let rows = client.query(" + SELECT * FROM gameservers + WHERE guild_owner = $1 AND server_name = $2 + ", &[&(guild_id as i64), &server_name]).await?; + + let mut server = Vec::new(); + for row in rows { + server.push(row.get("server_name")); + server.push(row.get("game_name")); + server.push(row.get("ip_address")) + } + + Ok(server) } } diff --git a/src/models/mod.rs b/src/models/mod.rs index f46172e..ca6994a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,2 +1 @@ -pub mod mpservers; pub mod gameservers; diff --git a/src/models/mpservers.rs b/src/models/mpservers.rs deleted file mode 100644 index 0a7ca1f..0000000 --- a/src/models/mpservers.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::controllers::database::DatabaseController; - -// #[derive(Debug)] -pub struct MPServers { - pub server_name: String, - pub guild_owner: i64, - pub is_active: bool, - pub ip_address: String, - pub md5_code: String -} - -#[allow(dead_code)] -impl MPServers { - pub async fn get_servers(guild_id: u64) -> Result { - let client = DatabaseController::new().await?.client; - let row = client.query_one(" - SELECT * FROM mpservers - WHERE guild_owner = $1 - ", &[&(guild_id as i64)]).await?; - - Ok(Self { - server_name: row.get("server_name"), - guild_owner: row.get("guild_owner"), - is_active: row.get("is_active"), - ip_address: row.get("ip_address"), - md5_code: row.get("md5_code") - }) - } - - pub async fn get_server_ip(guild_id: u64, server_name: &str) -> Result<(String, String), tokio_postgres::Error> { - let client = DatabaseController::new().await?.client; - let row = client.query_one(" - SELECT ip_address, md5_code FROM mpservers - WHERE guild_owner = $1 AND server_name = $2 - ", &[&(guild_id as i64), &server_name]).await?; - - Ok((row.get("ip_address"), row.get("md5_code"))) - } -}