diff --git a/Cargo.lock b/Cargo.lock index 2b25af3..304d333 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,7 +64,7 @@ checksum = "349f8ccfd9221ee7d1f3d4b33e1f8319b3a81ed8f61f2ea40b37b859794b4491" dependencies = [ "async-trait", "axum-core", - "bitflags", + "bitflags 1.3.2", "bytes", "futures-util", "http", @@ -142,6 +142,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c70beb79cbb5ce9c4f8e20849978f34225931f665bb49efa6982875a4d5facb3" + [[package]] name = "bumpalo" version = "3.12.0" @@ -936,11 +942,13 @@ dependencies = [ [[package]] name = "osrs-prometheus-exporter" -version = "0.1.0" +version = "1.0.0" dependencies = [ "async-trait", "axum", "axum-tracing-opentelemetry", + "bitflags 2.1.0", + "bytes", "eyre", "opentelemetry", "reqwest", @@ -1182,7 +1190,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1191,7 +1199,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1342,7 +1350,7 @@ version = "0.37.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", @@ -1757,7 +1765,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ - "bitflags", + "bitflags 1.3.2", "bytes", "futures-core", "futures-util", @@ -1776,7 +1784,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658" dependencies = [ - "bitflags", + "bitflags 1.3.2", "bytes", "futures-core", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index f8ae19b..cd525c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,11 @@ async-trait = "0.1" # Web framework axum = "0.6.12" -# Serde +# Data serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +bytes = "1.4" +bitflags = "2.1" # Error handling eyre = "0.6" diff --git a/src/api/endpoints/worlds.rs b/src/api/endpoints/worlds.rs index 9b2d131..031e03d 100644 --- a/src/api/endpoints/worlds.rs +++ b/src/api/endpoints/worlds.rs @@ -1,10 +1,10 @@ use super::convert_into_prom_metrics; -use crate::collectors::player_count::get_player_count; +use crate::collectors::player_count; use axum::http::StatusCode; use axum::response::IntoResponse; pub async fn get_worlds() -> impl IntoResponse { - let resp = match get_player_count().await { + let resp = match player_count::get().await { Ok(r) => r, Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Nope".to_string()), }; diff --git a/src/collectors/mod.rs b/src/collectors/mod.rs index b52f733..eb51d3e 100644 --- a/src/collectors/mod.rs +++ b/src/collectors/mod.rs @@ -1,30 +1,6 @@ pub mod player_count; pub mod stats; -use serde::Deserialize; - pub trait PromMetric { fn to_metric_string(self: &Self) -> String; } - -#[derive(Deserialize)] -struct GithubTag { - name: String, -} - -async fn get_runelite_version() -> eyre::Result { - let resp = crate::transport::http::new() - .get("https://api.github.com/repos/runelite/runelite/tags") - .send() - .await? - .json::>() - .await?; - - if let Some(latest) = resp.first() { - Ok(latest.name.replace("parent-", "")) - } else { - Err(eyre::eyre!( - "Failed to get github tags for runelite version" - )) - } -} diff --git a/src/collectors/player_count.rs b/src/collectors/player_count.rs index 6b5bfef..0c1e21d 100644 --- a/src/collectors/player_count.rs +++ b/src/collectors/player_count.rs @@ -1,37 +1,41 @@ -use super::PromMetric; -use serde::{Deserialize, Deserializer}; +use crate::collectors::PromMetric; +use crate::transport; +use bitflags::bitflags; +use bytes::Bytes; +use serde::Serialize; use std::fmt::{Display, Formatter}; -use std::str; +use std::io::Cursor; +use tokio::io::AsyncReadExt; +use tracing::{span, warn, Level}; -#[derive(Deserialize, Debug)] -pub struct Worlds { - pub worlds: Vec, +#[derive(Serialize, Debug)] +pub struct World { + pub id: u16, + pub types: Vec, + pub address: String, + pub activity: String, + pub location: WorldLocation, + pub players: i16, } -#[derive(Debug)] +#[derive(Serialize, Debug)] pub enum WorldLocation { Germany, USA, - UnitedKingdom, + UK, Australia, Unknown, } -impl<'de> Deserialize<'de> for WorldLocation { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let field = i16::deserialize(deserializer)?; - let world = match field { +impl From for WorldLocation { + fn from(value: i8) -> Self { + match value { 0 => Self::USA, - 1 => Self::UnitedKingdom, + 1 => Self::UK, 3 => Self::Australia, 7 => Self::Germany, _ => Self::Unknown, - }; - - Ok(world) + } } } @@ -40,14 +44,39 @@ impl Display for WorldLocation { match self { WorldLocation::Germany => write!(f, "Germany"), WorldLocation::USA => write!(f, "USA"), - WorldLocation::UnitedKingdom => write!(f, "UK"), + WorldLocation::UK => write!(f, "UK"), WorldLocation::Australia => write!(f, "Australia"), WorldLocation::Unknown => write!(f, "Unknown"), } } } -#[derive(Debug, PartialEq)] +bitflags! { + /// Bit representations of world types + /// + /// This is based on a best guess basis... + #[derive(Debug, PartialEq, Eq)] + pub struct WorldTypeRaw: i32 { + const Members = 1; + const PVP = 1 << 2; + const Bounty = 1 << 5; + const PVPArena = 1 << 6; + const SkillTotal = 1 << 7; + const QuestSpeedrunning = 1 << 8; + const HighRisk = 1 << 10; + const LastManStanding = 1 << 14; + const SoulWars = 1 << 22; + const Beta = 1 << 23; + const NoSaveMode = 1 << 25; + const Tournament = 1 << 26; + const FreshStartWorld = 1 << 27; + const Minigame = 1 << 28; // Not sure about this one... + const Deadman = 1 << 29; + const Seasonal = 1 << 30; + } +} + +#[derive(Serialize, Debug, PartialEq)] pub enum WorldType { FreeToPlay, Members, @@ -62,80 +91,134 @@ pub enum WorldType { Tournament, FreshStartWorld, Deadman, + Beta, + SoulWars, + Minigame, Seasonal, Unknown, } -impl<'de> Deserialize<'de> for WorldType { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let field = String::deserialize(deserializer)?; - let res = match field.as_str() { - "MEMBERS" => Self::Members, - "PVP" => Self::PVP, - "BOUNTY" => Self::Bounty, - "PVP_ARENA" => Self::PVPArena, - "SKILL_TOTAL" => Self::SkillTotal, - "QUEST_SPEEDRUNNING" => Self::QuestSpeedrunning, - "HIGH_RISK" => Self::HighRisk, - "LAST_MAN_STANDING" => Self::LastManStanding, - "NOSAVE_MODE" => Self::NoSaveMode, - "TOURNAMENT" => Self::Tournament, - "FRESH_START_WORLD" => Self::FreshStartWorld, - "DEADMAN" => Self::Deadman, - "SEASONAL" => Self::Seasonal, - _ => Self::Unknown, - }; - - Ok(res) - } -} - impl Display for WorldType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - WorldType::FreeToPlay => write!(f, "FreeToPlay"), - WorldType::Members => write!(f, "Members"), - WorldType::PVP => write!(f, "PVP"), - WorldType::Bounty => write!(f, "Bounty"), - WorldType::PVPArena => write!(f, "PVPArena"), - WorldType::SkillTotal => write!(f, "SkillTotal"), - WorldType::QuestSpeedrunning => write!(f, "QuestSpeedrunning"), - WorldType::HighRisk => write!(f, "HighRisk"), - WorldType::LastManStanding => write!(f, "LastManStanding"), - WorldType::NoSaveMode => write!(f, "NoSaveMode"), - WorldType::Tournament => write!(f, "Tournament"), - WorldType::FreshStartWorld => write!(f, "FreshStartWorld"), - WorldType::Deadman => write!(f, "Deadman"), - WorldType::Seasonal => write!(f, "Seasonal"), - WorldType::Unknown => write!(f, "Unknown"), + &WorldType::FreeToPlay => write!(f, "FreeToPlay"), + &WorldType::Members => write!(f, "Members"), + &WorldType::PVP => write!(f, "PVP"), + &WorldType::Bounty => write!(f, "Bounty"), + &WorldType::PVPArena => write!(f, "PVPArena"), + &WorldType::SkillTotal => write!(f, "SkillTotal"), + &WorldType::QuestSpeedrunning => write!(f, "QuestSpeedrunning"), + &WorldType::HighRisk => write!(f, "HighRisk"), + &WorldType::LastManStanding => write!(f, "LastManStanding"), + &WorldType::NoSaveMode => write!(f, "NoSaveMode"), + &WorldType::Tournament => write!(f, "Tournament"), + &WorldType::FreshStartWorld => write!(f, "FreshStartWorld"), + &WorldType::Deadman => write!(f, "Deadman"), + &WorldType::Seasonal => write!(f, "Seasonal"), + &WorldType::Beta => write!(f, "Beta"), + &WorldType::SoulWars => write!(f, "SoulWars"), + &WorldType::Minigame => write!(f, "Minigame"), + &WorldType::Unknown => write!(f, "Unknown"), } } } -#[derive(Deserialize, Debug)] -pub struct World { - pub id: i16, - pub address: String, - pub activity: String, - pub location: WorldLocation, - pub players: i16, - pub types: Vec, +fn get_world_types(c: i32) -> Vec { + let mut res: Vec = vec![]; + + let raw = WorldTypeRaw::from_bits_retain(c); + + for world_type in raw.iter() { + match world_type { + WorldTypeRaw::Members => res.push(WorldType::Members), + WorldTypeRaw::PVP => res.push(WorldType::PVP), + WorldTypeRaw::Bounty => res.push(WorldType::Bounty), + WorldTypeRaw::PVPArena => res.push(WorldType::PVPArena), + WorldTypeRaw::SkillTotal => res.push(WorldType::SkillTotal), + WorldTypeRaw::QuestSpeedrunning => res.push(WorldType::QuestSpeedrunning), + WorldTypeRaw::HighRisk => res.push(WorldType::HighRisk), + WorldTypeRaw::LastManStanding => res.push(WorldType::LastManStanding), + WorldTypeRaw::NoSaveMode => res.push(WorldType::NoSaveMode), + WorldTypeRaw::Tournament => res.push(WorldType::Tournament), + WorldTypeRaw::FreshStartWorld => res.push(WorldType::FreshStartWorld), + WorldTypeRaw::Beta => res.push(WorldType::Beta), + WorldTypeRaw::Deadman => res.push(WorldType::Deadman), + WorldTypeRaw::Seasonal => res.push(WorldType::Seasonal), + WorldTypeRaw::Minigame => res.push(WorldType::Minigame), + WorldTypeRaw::SoulWars => res.push(WorldType::SoulWars), + _ => res.push(WorldType::Unknown), + } + } + + if res.contains(&WorldType::Unknown) { + warn!("Unknown World Type: {:?}", raw) + } + + if res.len() == 0 { + res.push(WorldType::FreeToPlay) + } + + res } -impl PromMetric for World { - fn to_metric_string(&self) -> String { - format!( - "osrs_world_players{{id=\"{}\",location=\"{}\",isMembers=\"{:?}\",type=\"{}\"}} {}", - self.id, - self.location, - self.is_members(), - self.world_type(), - self.players - ) +async fn read_string(c: &mut Cursor) -> eyre::Result { + let mut col: Vec = vec![]; + + loop { + let item = c.read_u8().await?; + if item == 0 { + break; + } + + col.push(item); } + + Ok(String::from_utf8(col)?) +} + +async fn decode(buffer: Bytes) -> eyre::Result> { + let mut res: Vec = vec![]; + + let mut c = Cursor::new(buffer); + + let _buffer_size = c.read_i32().await? + 4; + let len = c.read_i16().await?; + + for _ in 0..len { + let id = c.read_u16().await?; + let w = World { + id, + types: get_world_types(c.read_i32().await?), + address: read_string(&mut c).await?, + activity: read_string(&mut c).await?, + location: WorldLocation::from(c.read_i8().await?), + players: c.read_i16().await?, + }; + + res.push(w); + } + + Ok(res) +} + +pub async fn get() -> eyre::Result> { + let buffer = transport::http::new() + .get("https://www.runescape.com/g=oldscape/slr.ws?order=LPWM") + .send() + .await + .unwrap() + .bytes() + .await + .unwrap(); + + let parsing_span = span!(Level::INFO, "Parsing world data"); + let parsing_span_run = parsing_span.enter(); + + let res = decode(buffer).await.unwrap(); + + drop(parsing_span_run); + + Ok(res) } impl World { @@ -143,9 +226,7 @@ impl World { self.types.contains(&WorldType::Members) } pub fn world_type(&self) -> WorldType { - if self.types.len() == 0 { - WorldType::FreeToPlay - } else if self.types.contains(&WorldType::QuestSpeedrunning) { + if self.types.contains(&WorldType::QuestSpeedrunning) { WorldType::QuestSpeedrunning } else if self.types.contains(&WorldType::HighRisk) { WorldType::HighRisk @@ -167,25 +248,31 @@ impl World { WorldType::SkillTotal } else if self.types.contains(&WorldType::FreshStartWorld) { WorldType::FreshStartWorld + } else if self.types.contains(&WorldType::Minigame) { + WorldType::Minigame + } else if self.types.contains(&WorldType::SoulWars) { + WorldType::SoulWars } else if self.types.contains(&WorldType::Seasonal) { WorldType::Seasonal } else if self.is_members() { WorldType::Members + } else if self.types.contains(&WorldType::FreeToPlay) { + WorldType::FreeToPlay } else { WorldType::Unknown } } } -pub async fn get_player_count() -> eyre::Result> { - let runelite_version = super::get_runelite_version().await?; - let req_url = format!("https://api.runelite.net/{}/worlds.js", runelite_version); - let resp = crate::transport::http::new() - .get(req_url) - .send() - .await? - .json::() - .await?; - - Ok(resp.worlds) +impl PromMetric for World { + fn to_metric_string(&self) -> String { + format!( + "osrs_world_players{{id=\"{}\",location=\"{}\",isMembers=\"{:?}\",type=\"{}\"}} {}", + self.id, + self.location, + self.is_members(), + self.world_type(), + self.players + ) + } }