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: Configuration is done with a `config.toml` file with the following format:
```toml ```toml
instances = [ group = "group_7f23c663-e3c5-408f-bfb3-02b164f9e921"
{ name = "optional instance name"
, url = "https://vrchat.com/home/launch?worldId=wrld_123&instanceId=123"
},
{ name = "another instance"
, instance = "234"
, world = "wrld_234"
}
]
vrcdn = "vrcdn_stream_name" 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. 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::error::Error;
use std::{env, fmt, fs}; use std::{env, fmt, fs};
@ -10,11 +9,10 @@ use axum::{
routing::get, routing::get,
Router, Router,
}; };
use lazy_static::lazy_static;
use maud::html; use maud::html;
use maud::Markup; use maud::Markup;
use prometheus::{register_gauge_vec, Encoder, GaugeVec, TextEncoder}; use prometheus::{register_gauge_vec, Encoder, GaugeVec, TextEncoder};
use lazy_static::lazy_static;
use reqwest::header::USER_AGENT; use reqwest::header::USER_AGENT;
use serde::Deserialize; use serde::Deserialize;
use url::Url; use url::Url;
@ -68,21 +66,21 @@ impl fmt::Display for WsError {
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
struct Config { struct Config {
vrcdn: Option<String>, vrcdn: Option<String>,
instances: Option<Vec<VrcInstance>>, group: Option<String>,
vrchat_token: Option<String>, vrchat_token: Option<String>,
} }
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
struct VrcInstance { struct VrcInstance {
/// Full invite URL. Takes precedence over instance and world.
url: Option<String>,
/// Instance ID. /// Instance ID.
instance: Option<String>, instance: Option<String>,
/// World ID. /// World ID.
world: Option<String>, world: Option<String>,
/// Raw location
location: Option<String>,
/// Custom name for the instance. /// Custom name for the instance.
#[serde(skip)] #[serde(skip)]
name: Option<String> name: Option<String>,
} }
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
@ -101,6 +99,11 @@ struct VrCdnRegion {
total: f64, total: f64,
} }
#[derive(Clone, Debug, Deserialize)]
struct VrcGroupInstance {
location: String,
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), ()> { async fn main() -> Result<(), ()> {
let content = fs::read_to_string("config.toml").unwrap(); let content = fs::read_to_string("config.toml").unwrap();
@ -111,7 +114,7 @@ async fn main() -> Result<(), ()> {
.route("/", get(homepage)) .route("/", get(homepage))
.route("/metrics", get(metrics_handler)) .route("/metrics", get(metrics_handler))
.with_state(config); .with_state(config);
let _ = axum::Server::bind(&"0.0.0.0:6534".parse().unwrap()) let _ = axum::Server::bind(&"0.0.0.0:6534".parse().unwrap())
.serve(app.into_make_service()) .serve(app.into_make_service())
.await; .await;
@ -143,45 +146,52 @@ async fn metrics(config: Config) -> Result<Vec<u8>, WsError> {
let encoder = TextEncoder::new(); let encoder = TextEncoder::new();
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let auth_cookie = format!("auth={}", &config.vrchat_token.unwrap()); let auth_cookie = format!("auth={}", &config.vrchat_token.unwrap());
let mut instances: Vec<VrcInstance> = vec![];
// Do work. // Check if we can fetch instances from a group if set.
for instance in config.instances.ok_or(WsError::Custom("".to_owned()))? { if config.group.is_some() {
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(),
)
};
let api_url = format!( let api_url = format!(
"https://api.vrchat.cloud/api/1/instances/{0}:{1}", "https://api.vrchat.cloud/api/1/groups/{}/instances",
world_id, instance_id 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 url = Url::parse(&api_url).unwrap();
let req = client let req = client
.get(url) .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) .header("Cookie", &auth_cookie)
.send() .send()
.await?; .await?;
let data: VrcInstanceData = req.json().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 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)); .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 let req = client
.get(vrcdn_url) .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() .send()
.await .await
.unwrap(); .unwrap();