diff --git a/Cargo.lock b/Cargo.lock index 6591e89..f575839 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,7 @@ version = 4 name = "ServerSeekerV2-Rust" version = "0.1.0" dependencies = [ + "anyhow", "futures-core", "futures-util", "indicatif", @@ -38,6 +39,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + [[package]] name = "atoi" version = "2.0.0" diff --git a/Cargo.toml b/Cargo.toml index a5149fb..0f7b260 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ sqlx = { version = "0.8.3", features = ["postgres", "runtime-tokio"] } indicatif = { version = "0.17.11" } futures-core = "0.3.31" futures-util = "0.3.31" +anyhow = "1.0.98" #anyhow = "1.0.97" #futures = "0.3.31" diff --git a/src/database.rs b/src/database.rs index 49849fb..38149b0 100644 --- a/src/database.rs +++ b/src/database.rs @@ -24,13 +24,23 @@ pub async fn fetch_count(pool: &Pool) -> i64 { .get(0) } -pub async fn update(server: Server, conn: &PgPool) -> Result<(), Error> { +pub async fn update(server: Server, conn: &PgPool, host: &(String, u16)) -> anyhow::Result<()> { + let mut transaction = conn.begin().await?; let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("system time is before the unix epoch") .as_secs() as i32; - let mut transaction = conn.begin().await?; - let address: &str = server.address.as_str(); + + let (address, port) = host; + // Can't be used in the database yet + let server_type = server.get_type(); + + let players = server + .players + .ok_or(anyhow::Error::msg("Players object is missing!"))?; + let forge_data = server + .forge_data + .ok_or(anyhow::Error::msg("ForgeData object is missing!"))?; // Update server sqlx::query( @@ -47,30 +57,33 @@ pub async fn update(server: Server, conn: &PgPool) -> Result<(), Error> { WHERE address = $10 AND port = $11", ) - .bind(server.version) - .bind(server.protocol) - .bind(server.icon) - .bind(server.motd) + .bind(server.version.name) + .bind(server.version.protocol) + .bind(server.favicon) + .bind(server.description) .bind(server.prevents_reports) .bind(server.enforces_secure_chat) .bind(timestamp) - .bind(server.online_players) - .bind(server.max_players) + .bind(players.online) + .bind(players.max) .bind(address) - .bind(server.port) + .bind(*port as i64) .execute(&mut *transaction) .await?; - // Upsert players - for player in server.players { + // Update players + for player in players + .sample + .ok_or(anyhow::Error::msg("Players sample is missing!"))? + { sqlx::query("INSERT INTO playerhistory (address, port, playeruuid, playername, firstseen, lastseen) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (address, port, playeruuid) DO UPDATE SET lastseen = EXCLUDED.lastseen, playername = EXCLUDED.playername") .bind(address) - .bind(server.port) - .bind(player.uuid) - .bind(player.username) + .bind(*port as i64) + .bind(player.id) + .bind(player.name) .bind(timestamp) .bind(timestamp) .execute(&mut *transaction) @@ -78,17 +91,20 @@ pub async fn update(server: Server, conn: &PgPool) -> Result<(), Error> { } // Update mods - for mods in server.mods { + for mods in forge_data.mods { sqlx::query("INSERT INTO mods (address, port, modid, modmarker) VALUES ($1, $2, $3, $4) ON CONFLICT (address, port, modid) DO NOTHING") .bind(address) - .bind(server.port) - .bind(mods.mod_id) - .bind(mods.mod_name) + .bind(*port as i64) + .bind(mods.id) + .bind(mods.marker) .bind(timestamp) .bind(timestamp) .execute(&mut *transaction) .await?; } - transaction.commit().await + transaction + .commit() + .await + .map_err(|_| anyhow::Error::msg("Failed to commit transaction!")) } diff --git a/src/main.rs b/src/main.rs index 00f9176..bde3452 100644 --- a/src/main.rs +++ b/src/main.rs @@ -144,8 +144,8 @@ enum RunError { async fn run(host: (String, u16), state: Arc) -> Result<(), RunError> { let results = ping::ping_server(&host).await?; - let response = response::parse_response(results, &host)?; - database::update(response, &state.pool).await?; + let response = response::parse_response(results)?; + let _ = database::update(response, &state.pool, &host).await; state.progress_bar.inc(1); Ok(()) diff --git a/src/response.rs b/src/response.rs index 152c961..440d950 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,104 +1,112 @@ -use serde_json::Value; -use std::str::FromStr; +use serde::Deserialize; + +#[derive(Debug)] +pub enum ServerType { + JAVA, + NEOFORGE, + LEXFORGE, + PAPER, + SPIGOT, + BUKKIT, + PURPUR, + FOLIA, + PUFFERFISH, + VELOCITY, + LEAVES, + WATERFALL, + BUNGEECORD, +} #[allow(dead_code)] -#[derive(Debug)] +#[derive(Debug, Deserialize)] pub struct Server { - pub address: String, - pub port: i32, - pub version: Option, - pub protocol: Option, - pub motd: Option, - pub icon: Option, + pub version: Version, + pub favicon: Option, + pub description: Option, + #[serde(rename = "preventsChatReports")] pub prevents_reports: Option, + #[serde(rename = "enforcesSecureChat")] pub enforces_secure_chat: Option, - pub online_players: Option, - pub max_players: Option, - pub software: Option, - pub mods: Vec, - pub players: Vec, + pub players: Option, + #[serde(rename = "isModded")] + pub modded: Option, + #[serde(rename = "forgeData")] + pub forge_data: Option, } #[allow(dead_code)] -#[derive(Debug)] +#[derive(Debug, Deserialize)] +pub struct Version { + pub name: String, + pub protocol: i32, +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub enum Description { + Plain(String), + Object { text: String }, +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub struct Players { + pub max: i32, + pub online: i32, + pub sample: Option>, +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] pub struct Player { - pub username: Option, - // TODO! Replace this with a UUID type - pub uuid: Option, + pub id: String, + pub name: String, } #[allow(dead_code)] -#[derive(Debug)] +#[derive(Debug, Deserialize)] +pub struct ForgeData { + pub mods: Vec, +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] pub struct Mod { - pub mod_id: Option, - pub mod_name: Option, + #[serde(rename = "modId")] + pub id: String, + #[serde(rename = "modmarker")] + pub marker: String, } -pub fn parse_response(response: String, host: &(String, u16)) -> Result { - let json = Value::from_str(response.as_str())?; +impl Server { + pub fn get_type(&self) -> ServerType { + // Neoforge sends a "isModded" field + if self.modded.is_some() { + return ServerType::NEOFORGE; + } - let mut version: Option = None; - let mut protocol: Option = None; - let mut online_players: Option = None; - let mut max_players: Option = None; - let mut players = vec![]; - let mut mods = vec![]; + // Forge sends a "forgeData" object + if self.forge_data.is_some() { + return ServerType::LEXFORGE; + } - let icon = json["icon"].as_str().map(String::from); - let prevents_reports = json["preventsChatReports"].as_bool(); - let enforces_secure_chat = json["enforcesChat"].as_bool(); - - // Version object - if let Some(value) = json.get("version") { - version = value["name"].as_str().map(String::from); - - if let Some(p) = value["protocol"].as_i64() { - protocol = Some(p as i32) + match self.version.name.split(' ').nth(0) { + Some("Paper") => ServerType::PAPER, + Some("Spigot") => ServerType::SPIGOT, + Some("Bukkit") => ServerType::BUKKIT, + Some("Purpur") => ServerType::PURPUR, + Some("Folia") => ServerType::FOLIA, + Some("Pufferfish") => ServerType::PUFFERFISH, + Some("Velocity") => ServerType::VELOCITY, + Some("Leaves") => ServerType::LEAVES, + Some("Waterfall") => ServerType::WATERFALL, + Some("Bungeecord") => ServerType::BUNGEECORD, + Some(_) => ServerType::JAVA, + None => ServerType::JAVA, } } - - // Players object - if let Some(value) = json.get("players") { - if let Some(online) = value["online"].as_i64() { - online_players = Some(online as i32); - } - - if let Some(max) = value["max"].as_i64() { - max_players = Some(max as i32) - } - - if let Some(player_sample) = value["sample"].as_array() { - player_sample.iter().for_each(|player| { - players.push(Player { - username: player["name"].as_str().map(String::from), - uuid: player["id"].as_str().map(String::from), - }) - }) - } - } - - if let Some(mods_array) = json["mods"].as_array() { - mods_array.iter().for_each(|m| { - mods.push(Mod { - mod_name: m["modid"].as_str().map(String::from), - mod_id: m["version"].as_str().map(String::from), - }) - }) - } - - Ok(Server { - address: host.0.to_string(), - port: host.1 as i32, - version, - protocol, - motd: None, - icon, - prevents_reports, - enforces_secure_chat, - online_players, - max_players, - software: None, - mods, - players, - }) +} + +pub fn parse_response(response: String) -> Result { + Ok(serde_json::from_str(&response)?) }