Get the world data directly from the runescape api rather than via the runelite api
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Daan Boerlage 2023-04-09 03:58:25 +02:00
parent 01aee6da86
commit 68e96e9aad
Signed by: daan
GPG key ID: FCE070E1E4956606
5 changed files with 202 additions and 129 deletions

22
Cargo.lock generated
View file

@ -64,7 +64,7 @@ checksum = "349f8ccfd9221ee7d1f3d4b33e1f8319b3a81ed8f61f2ea40b37b859794b4491"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core",
"bitflags", "bitflags 1.3.2",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http",
@ -142,6 +142,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c70beb79cbb5ce9c4f8e20849978f34225931f665bb49efa6982875a4d5facb3"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.12.0" version = "3.12.0"
@ -936,11 +942,13 @@ dependencies = [
[[package]] [[package]]
name = "osrs-prometheus-exporter" name = "osrs-prometheus-exporter"
version = "0.1.0" version = "1.0.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
"axum-tracing-opentelemetry", "axum-tracing-opentelemetry",
"bitflags 2.1.0",
"bytes",
"eyre", "eyre",
"opentelemetry", "opentelemetry",
"reqwest", "reqwest",
@ -1182,7 +1190,7 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
] ]
[[package]] [[package]]
@ -1191,7 +1199,7 @@ version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
] ]
[[package]] [[package]]
@ -1342,7 +1350,7 @@ version = "0.37.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"errno", "errno",
"io-lifetimes", "io-lifetimes",
"libc", "libc",
@ -1757,7 +1765,7 @@ version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"bytes", "bytes",
"futures-core", "futures-core",
"futures-util", "futures-util",
@ -1776,7 +1784,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658" checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"bytes", "bytes",
"futures-core", "futures-core",
"futures-util", "futures-util",

View file

@ -11,9 +11,11 @@ async-trait = "0.1"
# Web framework # Web framework
axum = "0.6.12" axum = "0.6.12"
# Serde # Data
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
bytes = "1.4"
bitflags = "2.1"
# Error handling # Error handling
eyre = "0.6" eyre = "0.6"

View file

@ -1,10 +1,10 @@
use super::convert_into_prom_metrics; 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::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
pub async fn get_worlds() -> impl 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, Ok(r) => r,
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Nope".to_string()), Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Nope".to_string()),
}; };

View file

@ -1,30 +1,6 @@
pub mod player_count; pub mod player_count;
pub mod stats; pub mod stats;
use serde::Deserialize;
pub trait PromMetric { pub trait PromMetric {
fn to_metric_string(self: &Self) -> String; fn to_metric_string(self: &Self) -> String;
} }
#[derive(Deserialize)]
struct GithubTag {
name: String,
}
async fn get_runelite_version() -> eyre::Result<String> {
let resp = crate::transport::http::new()
.get("https://api.github.com/repos/runelite/runelite/tags")
.send()
.await?
.json::<Vec<GithubTag>>()
.await?;
if let Some(latest) = resp.first() {
Ok(latest.name.replace("parent-", ""))
} else {
Err(eyre::eyre!(
"Failed to get github tags for runelite version"
))
}
}

View file

@ -1,37 +1,41 @@
use super::PromMetric; use crate::collectors::PromMetric;
use serde::{Deserialize, Deserializer}; use crate::transport;
use bitflags::bitflags;
use bytes::Bytes;
use serde::Serialize;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::str; use std::io::Cursor;
use tokio::io::AsyncReadExt;
use tracing::{span, warn, Level};
#[derive(Deserialize, Debug)] #[derive(Serialize, Debug)]
pub struct Worlds { pub struct World {
pub worlds: Vec<World>, pub id: u16,
pub types: Vec<WorldType>,
pub address: String,
pub activity: String,
pub location: WorldLocation,
pub players: i16,
} }
#[derive(Debug)] #[derive(Serialize, Debug)]
pub enum WorldLocation { pub enum WorldLocation {
Germany, Germany,
USA, USA,
UnitedKingdom, UK,
Australia, Australia,
Unknown, Unknown,
} }
impl<'de> Deserialize<'de> for WorldLocation { impl From<i8> for WorldLocation {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn from(value: i8) -> Self {
where match value {
D: Deserializer<'de>,
{
let field = i16::deserialize(deserializer)?;
let world = match field {
0 => Self::USA, 0 => Self::USA,
1 => Self::UnitedKingdom, 1 => Self::UK,
3 => Self::Australia, 3 => Self::Australia,
7 => Self::Germany, 7 => Self::Germany,
_ => Self::Unknown, _ => Self::Unknown,
}; }
Ok(world)
} }
} }
@ -40,14 +44,39 @@ impl Display for WorldLocation {
match self { match self {
WorldLocation::Germany => write!(f, "Germany"), WorldLocation::Germany => write!(f, "Germany"),
WorldLocation::USA => write!(f, "USA"), WorldLocation::USA => write!(f, "USA"),
WorldLocation::UnitedKingdom => write!(f, "UK"), WorldLocation::UK => write!(f, "UK"),
WorldLocation::Australia => write!(f, "Australia"), WorldLocation::Australia => write!(f, "Australia"),
WorldLocation::Unknown => write!(f, "Unknown"), 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 { pub enum WorldType {
FreeToPlay, FreeToPlay,
Members, Members,
@ -62,80 +91,134 @@ pub enum WorldType {
Tournament, Tournament,
FreshStartWorld, FreshStartWorld,
Deadman, Deadman,
Beta,
SoulWars,
Minigame,
Seasonal, Seasonal,
Unknown, Unknown,
} }
impl<'de> Deserialize<'de> for WorldType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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 { impl Display for WorldType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
WorldType::FreeToPlay => write!(f, "FreeToPlay"), &WorldType::FreeToPlay => write!(f, "FreeToPlay"),
WorldType::Members => write!(f, "Members"), &WorldType::Members => write!(f, "Members"),
WorldType::PVP => write!(f, "PVP"), &WorldType::PVP => write!(f, "PVP"),
WorldType::Bounty => write!(f, "Bounty"), &WorldType::Bounty => write!(f, "Bounty"),
WorldType::PVPArena => write!(f, "PVPArena"), &WorldType::PVPArena => write!(f, "PVPArena"),
WorldType::SkillTotal => write!(f, "SkillTotal"), &WorldType::SkillTotal => write!(f, "SkillTotal"),
WorldType::QuestSpeedrunning => write!(f, "QuestSpeedrunning"), &WorldType::QuestSpeedrunning => write!(f, "QuestSpeedrunning"),
WorldType::HighRisk => write!(f, "HighRisk"), &WorldType::HighRisk => write!(f, "HighRisk"),
WorldType::LastManStanding => write!(f, "LastManStanding"), &WorldType::LastManStanding => write!(f, "LastManStanding"),
WorldType::NoSaveMode => write!(f, "NoSaveMode"), &WorldType::NoSaveMode => write!(f, "NoSaveMode"),
WorldType::Tournament => write!(f, "Tournament"), &WorldType::Tournament => write!(f, "Tournament"),
WorldType::FreshStartWorld => write!(f, "FreshStartWorld"), &WorldType::FreshStartWorld => write!(f, "FreshStartWorld"),
WorldType::Deadman => write!(f, "Deadman"), &WorldType::Deadman => write!(f, "Deadman"),
WorldType::Seasonal => write!(f, "Seasonal"), &WorldType::Seasonal => write!(f, "Seasonal"),
WorldType::Unknown => write!(f, "Unknown"), &WorldType::Beta => write!(f, "Beta"),
&WorldType::SoulWars => write!(f, "SoulWars"),
&WorldType::Minigame => write!(f, "Minigame"),
&WorldType::Unknown => write!(f, "Unknown"),
} }
} }
} }
#[derive(Deserialize, Debug)] fn get_world_types(c: i32) -> Vec<WorldType> {
pub struct World { let mut res: Vec<WorldType> = vec![];
pub id: i16,
pub address: String, let raw = WorldTypeRaw::from_bits_retain(c);
pub activity: String,
pub location: WorldLocation, for world_type in raw.iter() {
pub players: i16, match world_type {
pub types: Vec<WorldType>, 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 { async fn read_string(c: &mut Cursor<Bytes>) -> eyre::Result<String> {
fn to_metric_string(&self) -> String { let mut col: Vec<u8> = vec![];
format!(
"osrs_world_players{{id=\"{}\",location=\"{}\",isMembers=\"{:?}\",type=\"{}\"}} {}", loop {
self.id, let item = c.read_u8().await?;
self.location, if item == 0 {
self.is_members(), break;
self.world_type(),
self.players
)
} }
col.push(item);
}
Ok(String::from_utf8(col)?)
}
async fn decode(buffer: Bytes) -> eyre::Result<Vec<World>> {
let mut res: Vec<World> = 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<Vec<World>> {
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 { impl World {
@ -143,9 +226,7 @@ impl World {
self.types.contains(&WorldType::Members) self.types.contains(&WorldType::Members)
} }
pub fn world_type(&self) -> WorldType { pub fn world_type(&self) -> WorldType {
if self.types.len() == 0 { if self.types.contains(&WorldType::QuestSpeedrunning) {
WorldType::FreeToPlay
} else if self.types.contains(&WorldType::QuestSpeedrunning) {
WorldType::QuestSpeedrunning WorldType::QuestSpeedrunning
} else if self.types.contains(&WorldType::HighRisk) { } else if self.types.contains(&WorldType::HighRisk) {
WorldType::HighRisk WorldType::HighRisk
@ -167,25 +248,31 @@ impl World {
WorldType::SkillTotal WorldType::SkillTotal
} else if self.types.contains(&WorldType::FreshStartWorld) { } else if self.types.contains(&WorldType::FreshStartWorld) {
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) { } else if self.types.contains(&WorldType::Seasonal) {
WorldType::Seasonal WorldType::Seasonal
} else if self.is_members() { } else if self.is_members() {
WorldType::Members WorldType::Members
} else if self.types.contains(&WorldType::FreeToPlay) {
WorldType::FreeToPlay
} else { } else {
WorldType::Unknown WorldType::Unknown
} }
} }
} }
pub async fn get_player_count() -> eyre::Result<Vec<World>> { impl PromMetric for World {
let runelite_version = super::get_runelite_version().await?; fn to_metric_string(&self) -> String {
let req_url = format!("https://api.runelite.net/{}/worlds.js", runelite_version); format!(
let resp = crate::transport::http::new() "osrs_world_players{{id=\"{}\",location=\"{}\",isMembers=\"{:?}\",type=\"{}\"}} {}",
.get(req_url) self.id,
.send() self.location,
.await? self.is_members(),
.json::<Worlds>() self.world_type(),
.await?; self.players
)
Ok(resp.worlds) }
} }