Merge pull request 'Response parsing rewrite' (#3) from response-rewrite into main

Reviewed-on: https://git.funtimes909.xyz/ServerSeekerV2/ServerSeekerV2-Rust/pulls/3
This commit is contained in:
Amy committed 2025-04-18 15:19:22 +12:00
commit 9d3f0e5ce9
5 files changed
+138 -106

No files matched your search

Generated
+7
View File
@@ -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"
+1
View File
@@ -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"
+36 -20
View File
@@ -24,13 +24,23 @@ pub async fn fetch_count(pool: &Pool<Postgres>) -> 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!"))
}
+2 -2
View File
@@ -144,8 +144,8 @@ enum RunError {
async fn run(host: (String, u16), state: Arc<State>) -> 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(())
+92 -84
View File
@@ -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<String>,
pub protocol: Option<i32>,
pub motd: Option<String>,
pub icon: Option<String>,
pub version: Version,
pub favicon: Option<String>,
pub description: Option<String>,
#[serde(rename = "preventsChatReports")]
pub prevents_reports: Option<bool>,
#[serde(rename = "enforcesSecureChat")]
pub enforces_secure_chat: Option<bool>,
pub online_players: Option<i32>,
pub max_players: Option<i32>,
pub software: Option<String>,
pub mods: Vec<Mod>,
pub players: Vec<Player>,
pub players: Option<Players>,
#[serde(rename = "isModded")]
pub modded: Option<bool>,
#[serde(rename = "forgeData")]
pub forge_data: Option<ForgeData>,
}
#[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<Vec<Player>>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
pub struct Player {
pub username: Option<String>,
// TODO! Replace this with a UUID type
pub uuid: Option<String>,
pub id: String,
pub name: String,
}
#[allow(dead_code)]
#[derive(Debug)]
#[derive(Debug, Deserialize)]
pub struct ForgeData {
pub mods: Vec<Mod>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
pub struct Mod {
pub mod_id: Option<String>,
pub mod_name: Option<String>,
#[serde(rename = "modId")]
pub id: String,
#[serde(rename = "modmarker")]
pub marker: String,
}
pub fn parse_response(response: String, host: &(String, u16)) -> Result<Server, serde_json::Error> {
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<String> = None;
let mut protocol: Option<i32> = None;
let mut online_players: Option<i32> = None;
let mut max_players: Option<i32> = 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<Server, serde_json::Error> {
Ok(serde_json::from_str(&response)?)
}