Switch to fetching instances from group

Rather than having configuration file based instance list. Having both
is an option but the trend is generally using group instances for events.
This commit is contained in:
Gabriel Simmer 2023-10-21 19:25:06 +01:00
parent 3ac0d13780
commit d4cffaea0a
Signed by: arch
SSH key fingerprint: SHA256:m3OEcdtrnBpMX+2BDGh/byv3hrCekCLzDYMdvGEKPPQ
2 changed files with 50 additions and 46 deletions

View file

@ -13,20 +13,11 @@ Not a lot of metrics are collected at the moment. Just the ones I care about.
Configuration is done with a `config.toml` file with the following format:
```toml
instances = [
{ name = "optional instance name"
, url = "https://vrchat.com/home/launch?worldId=wrld_123&instanceId=123"
},
{ name = "another instance"
, instance = "234"
, world = "wrld_234"
}
]
group = "group_7f23c663-e3c5-408f-bfb3-02b164f9e921"
vrcdn = "vrcdn_stream_name"
```
Instances can either be the URL or instance and world ID seperated out, the later taking priority over the former.
Instances will be retrieved from the group.
Runs on port `6534`, on all interfaces. May be configurable in the future.

View file

@ -1,4 +1,3 @@
use std::collections::HashMap;
use std::error::Error;
use std::{env, fmt, fs};
@ -10,11 +9,10 @@ use axum::{
routing::get,
Router,
};
use lazy_static::lazy_static;
use maud::html;
use maud::Markup;
use prometheus::{register_gauge_vec, Encoder, GaugeVec, TextEncoder};
use lazy_static::lazy_static;
use reqwest::header::USER_AGENT;
use serde::Deserialize;
use url::Url;
@ -68,21 +66,21 @@ impl fmt::Display for WsError {
#[derive(Clone, Debug, Deserialize)]
struct Config {
vrcdn: Option<String>,
instances: Option<Vec<VrcInstance>>,
group: Option<String>,
vrchat_token: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
struct VrcInstance {
/// Full invite URL. Takes precedence over instance and world.
url: Option<String>,
/// Instance ID.
instance: Option<String>,
/// World ID.
world: Option<String>,
/// Raw location
location: Option<String>,
/// Custom name for the instance.
#[serde(skip)]
name: Option<String>
name: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
@ -101,6 +99,11 @@ struct VrCdnRegion {
total: f64,
}
#[derive(Clone, Debug, Deserialize)]
struct VrcGroupInstance {
location: String,
}
#[tokio::main]
async fn main() -> Result<(), ()> {
let content = fs::read_to_string("config.toml").unwrap();
@ -111,7 +114,7 @@ async fn main() -> Result<(), ()> {
.route("/", get(homepage))
.route("/metrics", get(metrics_handler))
.with_state(config);
let _ = axum::Server::bind(&"0.0.0.0:6534".parse().unwrap())
.serve(app.into_make_service())
.await;
@ -143,45 +146,52 @@ async fn metrics(config: Config) -> Result<Vec<u8>, WsError> {
let encoder = TextEncoder::new();
let client = reqwest::Client::new();
let auth_cookie = format!("auth={}", &config.vrchat_token.unwrap());
let mut instances: Vec<VrcInstance> = vec![];
// Do work.
for instance in config.instances.ok_or(WsError::Custom("".to_owned()))? {
let (instance_id, world_id): (String, String) = if instance.url.is_none() {
(
instance.instance.ok_or(WsError::Custom("".to_owned()))?,
instance.world.ok_or(WsError::Custom("".to_owned()))?,
)
} else {
let url = Url::parse(&instance.url.unwrap_or("".to_owned()))?;
let hash_query: HashMap<_, _> = url.query_pairs().into_owned().collect();
(
hash_query
.get("instanceId")
.ok_or(WsError::Custom("".to_owned()))?
.to_string(),
hash_query
.get("worldId")
.ok_or(WsError::Custom("".to_owned()))?
.to_string(),
)
};
// Check if we can fetch instances from a group if set.
if config.group.is_some() {
let api_url = format!(
"https://api.vrchat.cloud/api/1/instances/{0}:{1}",
world_id, instance_id
"https://api.vrchat.cloud/api/1/groups/{}/instances",
config.group.unwrap()
);
let url = Url::parse(&api_url).unwrap();
let req = client
.get(url)
.header(
USER_AGENT,
"vr-event-tracker(git.gmem.ca/arch/vr-event-tracker)",
)
.header("Cookie", &auth_cookie)
.send()
.await?;
let data: Vec<VrcGroupInstance> = req.json().await?;
instances = data.into_iter().map(|f| {
let spl: Vec<&str> = f.location.split(":").collect();
VrcInstance{ instance: Some(spl[0].to_owned()), world: Some(spl[1].to_owned()), location: Some(f.location), name: None }
}).collect();
}
for instance in instances {
let api_url = format!(
"https://api.vrchat.cloud/api/1/instances/{}",
&instance.location.clone().unwrap()
);
let url = Url::parse(&api_url).unwrap();
let req = client
.get(url)
.header(USER_AGENT, "vr-event-tracker(git.gmem.ca/arch/vr-event-tracker)")
.header(
USER_AGENT,
"vr-event-tracker(git.gmem.ca/arch/vr-event-tracker)",
)
.header("Cookie", &auth_cookie)
.send()
.await?;
let data: VrcInstanceData = req.json().await?;
let name = instance.name.unwrap_or(instance_id.clone());
let name = instance.name.unwrap_or(instance.location.unwrap());
PLAYER_COUNT
.with_label_values(&[&world_id, &instance_id, &name])
.with_label_values(&[&instance.world.unwrap(), &instance.instance.unwrap(), &name])
.set(data.user_count.unwrap_or(0 as f64));
}
@ -191,7 +201,10 @@ async fn metrics(config: Config) -> Result<Vec<u8>, WsError> {
);
let req = client
.get(vrcdn_url)
.header(USER_AGENT, "vr-event-tracker(git.gmem.ca/arch/vr-event-tracker)")
.header(
USER_AGENT,
"vr-event-tracker(git.gmem.ca/arch/vr-event-tracker)",
)
.send()
.await
.unwrap();