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
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
01aee6da86
commit
68e96e9aad
5 changed files with 202 additions and 129 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()),
|
||||
};
|
||||
|
|
|
@ -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<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"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<World>,
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct 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 {
|
||||
Germany,
|
||||
USA,
|
||||
UnitedKingdom,
|
||||
UK,
|
||||
Australia,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for WorldLocation {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let field = i16::deserialize(deserializer)?;
|
||||
let world = match field {
|
||||
impl From<i8> 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<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 {
|
||||
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<WorldType>,
|
||||
fn get_world_types(c: i32) -> Vec<WorldType> {
|
||||
let mut res: Vec<WorldType> = 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),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
if res.contains(&WorldType::Unknown) {
|
||||
warn!("Unknown World Type: {:?}", raw)
|
||||
}
|
||||
|
||||
if res.len() == 0 {
|
||||
res.push(WorldType::FreeToPlay)
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
async fn read_string(c: &mut Cursor<Bytes>) -> eyre::Result<String> {
|
||||
let mut col: Vec<u8> = 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<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 {
|
||||
|
@ -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<Vec<World>> {
|
||||
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::<Worlds>()
|
||||
.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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue