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]]
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",

View File

@ -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"

View File

@ -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;

View File

@ -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<String> = Lazy::new(||
var("WG_PMS").expect("Expected a \"WG_PMS\" in the envvar but none was found")
);
fn query_ats_server() -> Result<Response, Error> {
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<minecraft::JavaResponse, Box<dyn std::error::Error + Send + Sync>> {
println!("Querying {}", ip_address);
#[derive(Deserialize)]
struct MinecraftMotd {
clean: Vec<String>
}
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<Vec<Value>, Error> {
@ -113,30 +58,26 @@ async fn pms_serverstatus(url: &str) -> Result<Vec<Value>, 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<MinecraftQueryData, Error> {
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(())
}

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
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?;

View File

@ -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<Vec<String>, tokio_postgres::Error> {
let client = DatabaseController::new().await?.client;
let rows = client.query("