I am very satisfied with the results.
All checks were successful
Build and push container image / build (push) Successful in 10m9s

This commit is contained in:
toast 2024-02-05 22:49:04 +11:00
parent a870fe807a
commit c81e682de0
6 changed files with 94 additions and 185 deletions

11
Cargo.lock generated
View File

@ -822,13 +822,14 @@ dependencies = [
[[package]] [[package]]
name = "kon" name = "kon"
version = "0.1.13" version = "0.1.14"
dependencies = [ dependencies = [
"cargo_toml", "cargo_toml",
"gamedig", "gamedig",
"once_cell", "once_cell",
"poise", "poise",
"reqwest", "reqwest",
"serde",
"serde_json", "serde_json",
"serenity", "serenity",
"sysinfo", "sysinfo",
@ -1520,18 +1521,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.194" version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.194" version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "kon" name = "kon"
version = "0.1.13" version = "0.1.14"
rust-version = "1.74" rust-version = "1.74"
edition = "2021" edition = "2021"
@ -12,6 +12,7 @@ gamedig = "0.4.1"
once_cell = "1.19.0" once_cell = "1.19.0"
poise = "0.6.1" poise = "0.6.1"
reqwest = { version = "0.11.24", features = ["json"] } reqwest = { version = "0.11.24", features = ["json"] }
serde = "1.0.196"
serde_json = "1.0.113" serde_json = "1.0.113"
serenity = "0.12.0" serenity = "0.12.0"
sysinfo = "0.30.5" sysinfo = "0.30.5"

View File

@ -11,16 +11,22 @@ use serenity::{
Stream, Stream,
StreamExt StreamExt
}, },
all::Mentionable,
builder::CreateActionRow, builder::CreateActionRow,
builder::CreateEmbed, builder::CreateEmbed,
}; };
use poise::{ use poise::{
CreateReply, CreateReply,
serenity_prelude, 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 /// Manage the game servers for this guild
#[poise::command(slash_command, subcommands("add", "remove", "update", "list"), subcommand_required, guild_only)] #[poise::command(slash_command, subcommands("add", "remove", "update", "list"), subcommand_required, guild_only)]
pub async fn gameserver(_: poise::Context<'_, (), Error>) -> Result<(), Error> { 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( pub async fn add(
ctx: poise::Context<'_, (), Error>, ctx: poise::Context<'_, (), Error>,
#[description = "Server name as shown in-game or friendly name"] server_name: String, #[description = "Server name as shown in-game or friendly name"] server_name: String,
#[description = "Which game is this server running?"] game_name: String, #[description = "Which game is this server running?"] game_name: GameNames,
#[channel_types("Text")] #[description = "Which channel should this server be restricted to?"] guild_channel: serenity_prelude::GuildChannel,
#[description = "IP address/domain of the server (Include the port if it has one, e.g 127.0.0.1:8080)"] ip_address: String #[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> { ) -> 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![ let action_row = CreateActionRow::Buttons(vec![
serenity_prelude::CreateButton::new("add-confirm") serenity_prelude::CreateButton::new("add-confirm")
.style(ButtonStyle::Success) .style(ButtonStyle::Success)
@ -66,9 +56,8 @@ pub async fn add(
.description(format!(" .description(format!("
**Server name:** `{}` **Server name:** `{}`
**Game name:** `{}` **Game name:** `{}`
**Channel:** {}
**IP Address:** `{}` **IP Address:** `{}`
", server_name, game_name, guild_channel.mention(), ip_address)) ", server_name, game_name.name(), ip_address))
.color(EMBED_COLOR) .color(EMBED_COLOR)
) )
.components(vec![action_row]); .components(vec![action_row]);
@ -76,7 +65,6 @@ pub async fn add(
ctx.send(reply).await?; ctx.send(reply).await?;
while let Some(collector) = serenity_prelude::ComponentInteractionCollector::new(ctx) while let Some(collector) = serenity_prelude::ComponentInteractionCollector::new(ctx)
.channel_id(ctx.channel_id())
.guild_id(ctx.guild_id().unwrap()) .guild_id(ctx.guild_id().unwrap())
.author_id(ctx.author().id) .author_id(ctx.author().id)
.timeout(std::time::Duration::from_secs(30)) .timeout(std::time::Duration::from_secs(30))
@ -86,8 +74,7 @@ pub async fn add(
let result = Gameservers::add_server( let result = Gameservers::add_server(
ctx.guild_id().unwrap().into(), ctx.guild_id().unwrap().into(),
server_name.as_str(), server_name.as_str(),
game_name.as_str(), game_name.name(),
guild_channel.id.into(),
ip_address.as_str() ip_address.as_str()
).await; ).await;
@ -155,7 +142,6 @@ pub async fn remove(
ctx.send(reply).await?; ctx.send(reply).await?;
while let Some(collector) = serenity_prelude::ComponentInteractionCollector::new(ctx) while let Some(collector) = serenity_prelude::ComponentInteractionCollector::new(ctx)
.channel_id(ctx.channel_id())
.guild_id(ctx.guild_id().unwrap()) .guild_id(ctx.guild_id().unwrap())
.author_id(ctx.author().id) .author_id(ctx.author().id)
.timeout(std::time::Duration::from_secs(30)) .timeout(std::time::Duration::from_secs(30))
@ -207,15 +193,13 @@ pub async fn remove(
pub async fn update( pub async fn update(
ctx: poise::Context<'_, (), Error>, ctx: poise::Context<'_, (), Error>,
#[description = "Server name"] #[autocomplete = "ac_server_name"] server_name: String, #[description = "Server name"] #[autocomplete = "ac_server_name"] server_name: String,
#[description = "Game name"] game_name: String, #[description = "Game name"] game_name: GameNames,
#[description = "Channel"] #[channel_types("Text")] guild_channel: serenity_prelude::GuildChannel,
#[description = "IP address"] ip_address: String #[description = "IP address"] ip_address: String
) -> Result<(), Error> { ) -> Result<(), Error> {
let result = Gameservers::update_server( let result = Gameservers::update_server(
ctx.guild_id().unwrap().into(), ctx.guild_id().unwrap().into(),
&server_name, &server_name,
&game_name, &game_name.name(),
guild_channel.id.into(),
&ip_address &ip_address
).await; ).await;

View File

@ -5,21 +5,7 @@ use crate::{
commands::gameserver::ac_server_name commands::gameserver::ac_server_name
}; };
use gamedig::protocols::{
valve::{
Engine,
Response,
GatheringSettings
},
valve,
minecraft,
minecraft::RequestSettings,
types::TimeoutSettings
};
use std::{ use std::{
str::FromStr,
net::SocketAddr,
time::Duration,
collections::HashMap, collections::HashMap,
env::var env::var
}; };
@ -27,76 +13,35 @@ use reqwest::{
Client, Client,
header::USER_AGENT header::USER_AGENT
}; };
use tokio::{ use tokio::join;
net::lookup_host,
join
};
use poise::CreateReply; use poise::CreateReply;
use serenity::builder::CreateEmbed; use serenity::builder::CreateEmbed;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use cargo_toml::Manifest; use cargo_toml::Manifest;
use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
static PMS_BASE: Lazy<String> = Lazy::new(|| static PMS_BASE: Lazy<String> = Lazy::new(||
var("WG_PMS").expect("Expected a \"WG_PMS\" in the envvar but none was found") var("WG_PMS").expect("Expected a \"WG_PMS\" in the envvar but none was found")
); );
fn query_ats_server() -> Result<Response, Error> { #[derive(Deserialize)]
let server_ip = var("ATS_SERVER_IP").expect("Expected a \"ATS_SERVER_IP\" in the envvar but none was found"); struct MinecraftQueryData {
let addr = SocketAddr::from_str(&server_ip).unwrap(); motd: MinecraftMotd,
let engine = Engine::Source(None); players: MinecraftPlayers,
let gather_settings = GatheringSettings { version: String,
players: true, online: bool
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?)
} }
async fn query_gameserver(ip_address: &str) -> Result<minecraft::JavaResponse, Box<dyn std::error::Error + Send + Sync>> { #[derive(Deserialize)]
println!("Querying {}", ip_address); struct MinecraftMotd {
clean: Vec<String>
}
let full_address = if ip_address.contains(':') { #[derive(Deserialize)]
String::from(ip_address) struct MinecraftPlayers {
} else { online: i32,
format!("{}:25565", ip_address) max: i32
};
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<Vec<Value>, Error> { async fn pms_serverstatus(url: &str) -> Result<Vec<Value>, Error> {
@ -113,30 +58,26 @@ async fn pms_serverstatus(url: &str) -> Result<Vec<Value>, Error> {
Ok(servers) Ok(servers)
} }
/// Query the server statuses async fn gs_query_minecraft(server_ip: &str) -> Result<MinecraftQueryData, Error> {
#[poise::command(slash_command, subcommands("ats", "wg", "mc"), subcommand_required)] let bot_version = Manifest::from_path("Cargo.toml").unwrap().package.unwrap().version.unwrap();
pub async fn status(_: poise::Context<'_, (), Error>) -> Result<(), Error> {
Ok(()) 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 /// Query the server statuses
#[poise::command(slash_command)] #[poise::command(slash_command, subcommands("wg", "gs"), subcommand_required)]
pub async fn ats(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> { pub async fn status(_: 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)
}
Ok(()) Ok(())
} }
@ -175,36 +116,46 @@ pub async fn wg(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
Ok(()) Ok(())
} }
/// Retrieve the server data from given Minecraft Java server /// Retrieve the given server data from gameservers DB
#[poise::command(slash_command, guild_only)] #[poise::command(slash_command, guild_only)]
pub async fn mc( pub async fn gs(
ctx: poise::Context<'_, (), Error>, ctx: poise::Context<'_, (), Error>,
#[description = "Server name"] #[autocomplete = "ac_server_name"] server_name: String #[description = "Server name"] #[autocomplete = "ac_server_name"] server_name: String
) -> Result<(), Error> { ) -> 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 { // Extract values from a Vec above
Ok(data) => { let game_name = &server_data[1];
let name = &data[0]; let ip_address = &server_data[2];
let game = &data[1];
let ip = &data[2]; 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));
let query_result = query_gameserver(ip).await?;
ctx.send(CreateReply::default() ctx.send(CreateReply::default()
.embed(CreateEmbed::new() .embed(embed
.title(format!("{} Server Status", name)) .title(server_name)
.fields(vec![ .fields(embed_fields)
("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?; ).await?;
// ctx.send(CreateReply::default().content("aaa")).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(());
} }
} }

View File

@ -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 // Gameservers
client.batch_execute(" client.batch_execute("
CREATE TABLE IF NOT EXISTS gameservers ( CREATE TABLE IF NOT EXISTS gameservers (
server_name VARCHAR(255) NOT NULL PRIMARY KEY, server_name VARCHAR(255) NOT NULL PRIMARY KEY,
game_name VARCHAR(255) NOT NULL, game_name VARCHAR(255) NOT NULL,
guild_owner BIGINT NOT NULL, guild_owner BIGINT NOT NULL,
guild_channel BIGINT NOT NULL,
ip_address VARCHAR(255) NOT NULL ip_address VARCHAR(255) NOT NULL
); );
").await?; ").await?;

View File

@ -4,7 +4,6 @@ pub struct Gameservers {
pub server_name: String, pub server_name: String,
pub game_name: String, pub game_name: String,
pub guild_owner: i64, pub guild_owner: i64,
pub guild_channel: i64,
pub ip_address: String pub ip_address: String
} }
@ -22,7 +21,6 @@ impl Gameservers {
server_name: row.get("server_name"), server_name: row.get("server_name"),
game_name: row.get("game_name"), game_name: row.get("game_name"),
guild_owner: row.get("guild_owner"), guild_owner: row.get("guild_owner"),
guild_channel: row.get("guild_channel"),
ip_address: row.get("ip_address") ip_address: row.get("ip_address")
}); });
} }
@ -34,14 +32,13 @@ impl Gameservers {
guild_id: u64, guild_id: u64,
server_name: &str, server_name: &str,
game_name: &str, game_name: &str,
guild_channel: u64,
ip_address: &str ip_address: &str
) -> Result<(), tokio_postgres::Error> { ) -> Result<(), tokio_postgres::Error> {
let client = DatabaseController::new().await?.client; let client = DatabaseController::new().await?.client;
client.execute(" 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) 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(()) Ok(())
} }
@ -60,31 +57,18 @@ impl Gameservers {
guild_id: u64, guild_id: u64,
server_name: &str, server_name: &str,
game_name: &str, game_name: &str,
guild_channel: u64,
ip_address: &str ip_address: &str
) -> Result<(), tokio_postgres::Error> { ) -> Result<(), tokio_postgres::Error> {
let client = DatabaseController::new().await?.client; let client = DatabaseController::new().await?.client;
client.execute(" client.execute("
UPDATE gameservers UPDATE gameservers
SET game_name = $1, guild_channel = $2, ip_address = $3 SET game_name = $1, ip_address = $2
WHERE guild_owner = $4 AND server_name = $5 WHERE guild_owner = $3 AND server_name = $4
", &[&game_name, &(guild_channel as i64), &ip_address, &(guild_id as i64), &server_name]).await?; ", &[&game_name, &ip_address, &(guild_id as i64), &server_name]).await?;
Ok(()) 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<Vec<String>, tokio_postgres::Error> { pub async fn get_server_names(guild_id: u64) -> Result<Vec<String>, tokio_postgres::Error> {
let client = DatabaseController::new().await?.client; let client = DatabaseController::new().await?.client;
let rows = client.query(" let rows = client.query("