refactor api

This commit is contained in:
41666 2022-11-27 18:18:41 -05:00
parent a8c4bc5756
commit 01471342b0
18 changed files with 873 additions and 1595 deletions

1343
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,17 @@
// GENERATED CODE -- Do not edit. Run `cargo run --bin codegen` to regenerate. // GENERATED CODE -- Do not edit. Run `cargo run --bin codegen` to regenerate.
use once_cell::sync::Lazy; use lazy_static::lazy_static;
use std::collections::HashMap; use std::collections::HashMap;
static VEHICLE_TO_NAME: Lazy<HashMap<&str, &str>> = Lazy::new(|| { lazy_static! {
HashMap::from([ static ref VEHICLE_TO_NAME: HashMap<&'static str, &'static str> = HashMap::from([
{% for vehicle in vehicles %}("{{ vehicle.vehicle_id }}", "{{ vehicle.name.en }}"),{% endfor %} {% for vehicle in vehicles %}("{{ vehicle.vehicle_id }}", "{{ vehicle.name.en }}"),{% endfor %}
]) ]);
});
static ref LOADOUT_TO_CLASS: HashMap<&'static str, &'static str> = HashMap::from([
{% for class in classes %}("{{ class.loadout_id }}", "{{ class.code_name }}"),{% endfor %}
]);
}
pub fn vehicle_to_name(vehicle_id: &str) -> String { pub fn vehicle_to_name(vehicle_id: &str) -> String {
match VEHICLE_TO_NAME.get(&vehicle_id) { match VEHICLE_TO_NAME.get(&vehicle_id) {
@ -16,12 +20,6 @@ pub fn vehicle_to_name(vehicle_id: &str) -> String {
} }
} }
static LOADOUT_TO_CLASS: Lazy<HashMap<&str, &str>> = Lazy::new(|| {
HashMap::from([
{% for class in classes %}("{{ class.loadout_id }}", "{{ class.code_name }}"),{% endfor %}
])
});
pub fn loadout_to_class(loadout_id: &str) -> String { pub fn loadout_to_class(loadout_id: &str) -> String {
match LOADOUT_TO_CLASS.get(&loadout_id) { match LOADOUT_TO_CLASS.get(&loadout_id) {
Some(name) => name.to_string(), Some(name) => name.to_string(),

View file

@ -6,10 +6,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
rocket = { version = "0.5.0-rc.2", features = ["json"] } redis = { version = "0.22.1", features = ["aio", "r2d2", "tokio-comp"] }
rocket_db_pools = { version = "0.1.0-rc.2", features = [ "deadpool_redis" ] }
serde_json = "1.0.88" serde_json = "1.0.88"
serde = "1.0.147" serde = "1.0.147"
juniper = "0.15.10" async-graphql = { version = "4.0.16", features = ["apollo_tracing"] }
juniper_rocket = "0.8.2" async-graphql-axum = "4.0.16"
once_cell = "1.16.0" axum = "0.6.0"
tokio = { version = "1.22.0", features = ["full"] }
tower-http = { version = "0.3.4", features = ["cors"] }
lazy_static = "1.4.0"

View file

@ -0,0 +1,39 @@
use crate::util::zcount;
use async_graphql::{Context, Object};
use redis::aio::MultiplexedConnection;
pub struct Classes {
world_id: String,
}
impl Classes {
pub fn new(world_id: String) -> Self {
Self { world_id }
}
async fn by_class<'ctx>(&self, ctx: &Context<'ctx>, class_name: &str) -> u32 {
let con = ctx.data::<MultiplexedConnection>().unwrap().to_owned();
zcount(con, format!("c:{}/{}", self.world_id, class_name)).await
}
}
#[Object]
impl Classes {
async fn infiltrator<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_class(ctx, "infiltrator").await
}
async fn light_assault<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_class(ctx, "light_assault").await
}
async fn combat_medic<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_class(ctx, "combat_medic").await
}
async fn engineer<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_class(ctx, "engineer").await
}
async fn heavy_assault<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_class(ctx, "heavy_assault").await
}
async fn max<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_class(ctx, "max").await
}
}

View file

@ -1,22 +0,0 @@
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;
use rocket::{Request, Response};
pub struct CORS;
#[rocket::async_trait]
impl Fairing for CORS {
fn info(&self) -> Info {
Info {
name: "Add CORS headers to responses",
kind: Kind::Response,
}
}
async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
response.set_header(Header::new("Access-Control-Allow-Methods", "GET, POST"));
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
response.set_header(Header::new("Access-Control-Allow-Credentials", "false"));
}
}

View file

@ -1,123 +0,0 @@
use crate::redispool::RedisPool;
use self::types::{Health, World};
use juniper::{graphql_object, FieldResult};
use rocket::response::content::RawHtml;
pub mod types;
pub struct Query;
#[graphql_object(context = Context)]
impl Query {
fn world(id: String) -> FieldResult<World> {
Ok(World {
world_id: id.clone(),
})
}
fn allWorlds() -> FieldResult<Vec<World>> {
Ok(vec![
World {
world_id: "1".to_string(),
},
World {
world_id: "10".to_string(),
},
World {
world_id: "13".to_string(),
},
World {
world_id: "17".to_string(),
},
World {
world_id: "19".to_string(),
},
World {
world_id: "40".to_string(),
},
World {
world_id: "1000".to_string(),
},
World {
world_id: "2000".to_string(),
},
])
}
fn worldByName(name: String) -> FieldResult<World> {
let id = match name.to_lowercase().as_str() {
"connery" => "1",
"miller" => "10",
"cobalt" => "13",
"emerald" => "17",
"jaeger" => "19",
"soltech" => "40",
"genudine" => "1000",
"ceres" => "2000",
_ => "-1",
};
Ok(World {
world_id: id.to_string(),
})
}
fn health() -> FieldResult<Health> {
Ok(Health {})
}
}
pub struct Context {
con: RedisPool,
}
impl juniper::Context for Context {}
#[get("/graphiql")]
pub fn graphiql() -> RawHtml<String> {
juniper_rocket::graphiql_source("/graphql", None)
}
#[get("/")]
pub fn playground() -> RawHtml<String> {
juniper_rocket::playground_source("/graphql", None)
}
#[get("/playground")]
pub fn playground2() -> RawHtml<String> {
juniper_rocket::playground_source("/graphql", None)
}
#[post("/", data = "<query>")]
pub async fn post_graphql(
query: juniper_rocket::GraphQLRequest,
schema: &rocket::State<Schema>,
con: &RedisPool,
) -> juniper_rocket::GraphQLResponse {
query.execute(&*schema, &Context { con: con.clone() }).await
}
#[get("/?<query..>")]
pub async fn get_graphql(
query: juniper_rocket::GraphQLRequest,
schema: &rocket::State<Schema>,
con: &RedisPool,
) -> juniper_rocket::GraphQLResponse {
query.execute(&*schema, &Context { con: con.clone() }).await
}
pub type Schema = juniper::RootNode<
'static,
Query,
juniper::EmptyMutation<Context>,
juniper::EmptySubscription<Context>,
>;
pub fn schema() -> Schema {
Schema::new(
Query,
juniper::EmptyMutation::<Context>::new(),
juniper::EmptySubscription::<Context>::new(),
)
}

View file

@ -1,361 +0,0 @@
use juniper::graphql_object;
use once_cell::sync::Lazy;
use rocket_db_pools::deadpool_redis::redis::cmd;
use std::{
collections::HashMap,
ops::Sub,
time::{Duration, SystemTime},
};
static WORLD_ID_TO_NAME: Lazy<HashMap<&str, &str>> = Lazy::new(|| {
HashMap::from([
("1", "Connery"),
("10", "Miller"),
("13", "Cobalt"),
("17", "Emerald"),
("19", "Jaeger"),
("40", "SolTech"),
("1000", "Genudine"),
("2000", "Ceres"),
])
});
#[derive(Clone, Debug)]
pub struct World {
pub world_id: String,
}
#[graphql_object(context = super::Context)]
impl World {
pub fn id(&self) -> juniper::ID {
juniper::ID::from(self.world_id.clone())
}
pub fn name(&self) -> String {
WORLD_ID_TO_NAME
.get(&self.world_id.to_string().as_str())
.unwrap_or(&"Unknown")
.to_string()
}
pub async fn population(&self, context: &mut super::Context) -> i32 {
let mut con = (*context).con.get().await.unwrap();
let id = self.world_id.to_string();
let filter_timestamp = SystemTime::now()
.sub(Duration::from_secs(60 * 15))
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let pop: u32 = cmd("ZCOUNT")
.arg(format!("wp:{}", id))
.arg(filter_timestamp)
.arg("+inf")
.query_async(&mut con)
.await
.unwrap();
pop as i32
}
pub async fn faction_population(&self) -> FactionPopulation {
FactionPopulation {
world_id: juniper::ID::from(self.world_id.clone()),
}
}
pub async fn vehicles(&self) -> Vehicles {
Vehicles {
world_id: juniper::ID::from(self.world_id.clone()),
}
}
pub async fn classes(&self) -> Classes {
Classes {
world_id: juniper::ID::from(self.world_id.clone()),
}
}
}
pub struct FactionPopulation {
world_id: juniper::ID,
}
impl FactionPopulation {
async fn by_faction(&self, context: &super::Context, world_id: String, faction: i32) -> i32 {
let mut con = (*context).con.get().await.unwrap();
let filter_timestamp = SystemTime::now()
.sub(Duration::from_secs(60 * 15))
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
cmd("ZCOUNT")
.arg(format!("wp:{}/{}", world_id, faction))
.arg(filter_timestamp)
.arg("+inf")
.query_async(&mut con)
.await
.unwrap()
}
}
#[graphql_object(context = super::Context)]
#[graphql(description = "The population of each faction on a world")]
impl FactionPopulation {
async fn vs(&self, context: &super::Context) -> i32 {
self.by_faction(context, self.world_id.to_string(), 1).await
}
async fn nc(&self, context: &super::Context) -> i32 {
self.by_faction(context, self.world_id.to_string(), 2).await
}
async fn tr(&self, context: &super::Context) -> i32 {
self.by_faction(context, self.world_id.to_string(), 3).await
}
async fn ns(&self, context: &super::Context) -> i32 {
self.by_faction(context, self.world_id.to_string(), 4).await
}
}
pub struct Vehicles {
world_id: juniper::ID,
}
impl Vehicles {
async fn get_vehicle(
&self,
context: &super::Context,
world_id: String,
vehicle_name: &str,
) -> i32 {
let mut con = (*context).con.get().await.unwrap();
let filter_timestamp = SystemTime::now()
.sub(Duration::from_secs(60 * 15))
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
cmd("ZCOUNT")
.arg(format!("v:{}/{}", world_id, vehicle_name))
.arg(filter_timestamp)
.arg("+inf")
.query_async(&mut con)
.await
.unwrap()
}
}
#[graphql_object(context = super::Context)]
#[graphql(description = "The count of active vehicles on a world")]
impl Vehicles {
// Transporters
async fn flash(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "flash")
.await
}
async fn sunderer(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "sunderer")
.await
}
async fn ant(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "ant")
.await
}
async fn harasser(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "harasser")
.await
}
async fn javelin(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "javelin")
.await
}
// Tanks
async fn lightning(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "lightning")
.await
}
async fn prowler(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "prowler")
.await
}
async fn vanguard(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "vanguard")
.await
}
async fn magrider(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "magrider")
.await
}
async fn chimera(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "chimera")
.await
}
// Air
async fn mosquito(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "mosquito")
.await
}
async fn liberator(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "liberator")
.await
}
async fn galaxy(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "galaxy")
.await
}
async fn valkyrie(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "valkyrie")
.await
}
async fn reaver(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "reaver")
.await
}
async fn scythe(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "scythe")
.await
}
async fn dervish(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "dervish")
.await
}
}
pub struct Classes {
pub world_id: juniper::ID,
}
impl Classes {
async fn get_class(&self, context: &super::Context, world_id: String, class_name: &str) -> i32 {
let mut con = (*context).con.get().await.unwrap();
let filter_timestamp = SystemTime::now()
.sub(Duration::from_secs(60 * 15))
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
cmd("ZCOUNT")
.arg(format!("c:{}/{}", world_id, class_name))
.arg(filter_timestamp)
.arg("+inf")
.query_async(&mut con)
.await
.unwrap()
}
}
#[graphql_object(context = super::Context)]
#[graphql(description = "The count of active classes on a world")]
impl Classes {
async fn infiltrator(&self, context: &super::Context) -> i32 {
self.get_class(context, self.world_id.to_string(), "infiltrator")
.await
}
async fn light_assault(&self, context: &super::Context) -> i32 {
self.get_class(context, self.world_id.to_string(), "light_assault")
.await
}
async fn combat_medic(&self, context: &super::Context) -> i32 {
self.get_class(context, self.world_id.to_string(), "combat_medic")
.await
}
async fn engineer(&self, context: &super::Context) -> i32 {
self.get_class(context, self.world_id.to_string(), "engineer")
.await
}
async fn heavy_assault(&self, context: &super::Context) -> i32 {
self.get_class(context, self.world_id.to_string(), "heavy_assault")
.await
}
async fn max(&self, context: &super::Context) -> i32 {
self.get_class(context, self.world_id.to_string(), "max")
.await
}
}
#[derive(juniper::GraphQLEnum)]
enum WebsocketState {
#[graphql(
description = "Using Nanite Systems manifold. This is the best possible running state."
)]
Primary,
#[graphql(
description = "Using backup Daybreak Games manifold. This means the primary socket hasn't recieved events for at least 60 seconds."
)]
Backup,
#[graphql(description = "Both event processors are down. This is bad.")]
Down,
}
#[derive(juniper::GraphQLEnum)]
enum UpDown {
#[graphql(description = "Checks have passed.")]
Up,
#[graphql(description = "Checks have failed. This is bad.")]
Down,
}
pub struct Health {}
impl Health {
async fn get_heartbeat(context: &super::Context, pair: &str) -> WebsocketState {
let mut con = (*context).con.get().await.unwrap();
let res: Result<i32, _> = cmd("GET")
.arg(format!("heartbeat:{}:primary", pair))
.query_async(&mut *con)
.await;
match res {
Ok(_) => WebsocketState::Primary,
Err(_) => {
let res: Result<i32, _> = cmd("GET")
.arg(format!("heartbeat:{}:backup", pair))
.query_async(&mut con)
.await;
match res {
Ok(_) => WebsocketState::Backup,
Err(_) => WebsocketState::Down,
}
}
}
}
}
#[graphql_object(context = super::Context)]
#[graphql(description = "Saerro's self-checks. Down is universally bad.")]
impl Health {
#[graphql(description = "Checks PC event processors for its running state.")]
async fn pc(context: &super::Context) -> WebsocketState {
Health::get_heartbeat(context, "pc").await
}
#[graphql(description = "Checks PS4 US event processors for its running state.")]
async fn ps4us(context: &super::Context) -> WebsocketState {
Health::get_heartbeat(context, "ps4us").await
}
#[graphql(description = "Checks PS4 EU event processors for its running state.")]
async fn ps4eu(context: &super::Context) -> WebsocketState {
Health::get_heartbeat(context, "ps4eu").await
}
#[graphql(description = "Is our datastore working?")]
async fn redis(context: &super::Context) -> UpDown {
let mut con = (*context).con.get().await.unwrap();
let res: Result<String, _> = cmd("PING").query_async(&mut con).await;
match res {
Ok(_) => UpDown::Up,
Err(_) => UpDown::Down,
}
}
}

View file

@ -0,0 +1,74 @@
use async_graphql::{Enum, Object};
use axum::{http::StatusCode, response::IntoResponse, Extension, Json};
use redis::pipe;
pub async fn get_health(
Extension(mut redis): Extension<redis::aio::MultiplexedConnection>,
) -> impl IntoResponse {
let (ping, pc, ps4us, ps4eu): (String, bool, bool, bool) = pipe()
.cmd("PING")
.get("heartbeat:pc")
.get("heartbeat:ps4us")
.get("heartbeat:ps4eu")
.query_async(&mut redis)
.await
.unwrap_or_default();
if ping != "PONG" {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
"status": "error",
"message": "Redis is not responding",
})),
);
}
(
StatusCode::OK,
Json(json!({
"status": if ping == "PONG" && pc && ps4us && ps4eu { "ok" } else { "degraded" },
"redis": ping == "PONG",
"pc": if pc { "primary" } else { "backup/down" },
"ps4us": if ps4us { "primary" } else { "backup/down" },
"ps4eu": if ps4eu { "primary" } else { "backup/down" },
})),
)
}
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum UpDown {
Up,
Down,
}
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
enum WebsocketState {
Primary,
Backup,
Down,
}
pub struct Health {}
#[Object]
impl Health {
async fn redis(&self) -> UpDown {
UpDown::Up
}
#[graphql(name = "pc")]
async fn pc(&self) -> WebsocketState {
WebsocketState::Primary
}
#[graphql(name = "ps4us")]
async fn ps4us(&self) -> WebsocketState {
WebsocketState::Primary
}
#[graphql(name = "ps4eu")]
async fn ps4eu(&self) -> WebsocketState {
WebsocketState::Primary
}
}

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<title>404 - Saerro Listening Post</title>
<meta charset="utf-8" />
<style>
body {
font-family: monospace;
background-color: #010101;
color: #e0e0e0;
font-size: 1.25rem;
line-height: 1.6;
}
a {
color: #cead42;
text-decoration: none;
}
</style>
<h1>404 Not Found</h1>
<p>
[<a href="/">home</a>] [<a href="/graphql/playground">graphql playground</a>]
</p>

View file

@ -1,91 +1,102 @@
pub mod cors; mod classes;
pub mod graphql; mod health;
pub mod redispool; mod query;
mod util;
mod vehicles;
mod world;
use redispool::RedisPool; use async_graphql::{
use rocket::fairing::AdHoc; extensions::ApolloTracing,
use rocket::response::content::RawHtml; http::{playground_source, GraphQLPlaygroundConfig},
use rocket::response::status; EmptyMutation, EmptySubscription, Request, Response, Schema,
use rocket::{error, Build, Rocket}; };
use rocket_db_pools::deadpool_redis::redis::{cmd, pipe}; use axum::{
use rocket_db_pools::{Connection, Database}; extract::Query,
http::Method,
response::{Html, IntoResponse, Redirect},
routing::{get, post},
Extension, Json, Router,
};
use std::net::SocketAddr;
use tower_http::cors::{Any, CorsLayer};
#[macro_use]
extern crate rocket;
#[macro_use] #[macro_use]
extern crate serde_json; extern crate serde_json;
#[get("/")] async fn index() -> Html<&'static str> {
async fn index() -> RawHtml<String> { Html(include_str!("html/index.html"))
RawHtml(include_str!("html/index.html").to_string())
} }
#[get("/health")] async fn handle_404() -> Html<&'static str> {
async fn health( Html(include_str!("html/404.html"))
mut con: Connection<RedisPool>, }
) -> Result<serde_json::Value, status::Custom<serde_json::Value>> {
let (ping, pc, ps4us, ps4eu): (String, bool, bool, bool) = pipe()
.cmd("PING")
.get("heartbeat:pc")
.get("heartbeat:ps4us")
.get("heartbeat:ps4eu")
.query_async(&mut *con)
.await
.unwrap_or_default();
if ping != "PONG" { async fn graphql_handler_post(
return Err(status::Custom( Extension(schema): Extension<Schema<query::Query, EmptyMutation, EmptySubscription>>,
rocket::http::Status::ServiceUnavailable, Json(query): Json<Request>,
json!({ ) -> Json<Response> {
"status": "error", Json(schema.execute(query).await)
"message": "Redis is not responding", }
}),
)); async fn graphql_handler_get(
Extension(schema): Extension<Schema<query::Query, EmptyMutation, EmptySubscription>>,
query: Query<Request>,
) -> axum::response::Response {
match query.operation_name {
Some(_) => Json(schema.execute(query.0).await).into_response(),
None => Redirect::to("/graphql/playground").into_response(),
} }
}
Ok(json!({ async fn graphql_playground() -> impl IntoResponse {
"status": if ping == "PONG" && pc && ps4us && ps4eu { "ok" } else { "degraded" }, Html(playground_source(GraphQLPlaygroundConfig::new("/graphql")))
"redis": ping == "PONG",
"pc": if pc { "primary" } else { "backup/down" },
"ps4us": if ps4us { "primary" } else { "backup/down" },
"ps4eu": if ps4eu { "primary" } else { "backup/down" },
}))
} }
#[launch] #[tokio::main]
fn rocket() -> Rocket<Build> { async fn main() {
let figment = rocket::Config::figment().merge(( let redis_url = format!(
"databases.redis.url",
format!(
"redis://{}:{}", "redis://{}:{}",
std::env::var("REDIS_HOST").unwrap_or("localhost".to_string()), std::env::var("REDIS_HOST").unwrap_or("localhost".to_string()),
std::env::var("REDIS_PORT").unwrap_or("6379".to_string()), std::env::var("REDIS_PORT").unwrap_or("6379".to_string()),
), );
));
rocket::build() let redis = redis::Client::open(redis_url)
.configure(figment) .unwrap()
.attach(cors::CORS) .get_multiplexed_tokio_connection()
.attach(RedisPool::init()) .await
.attach(AdHoc::on_ignite("Redis Check", |rocket| async move { .unwrap();
if let Some(pool) = RedisPool::fetch(&rocket) {
let mut con = pool.get().await.unwrap(); let schema = Schema::build(query::Query, EmptyMutation, EmptySubscription)
let _: () = cmd("PING").query_async(&mut con).await.unwrap(); .data(redis.clone())
} else { .extension(ApolloTracing)
error!("Redis connection failed"); .finish();
}
rocket let app = Router::new()
})) .route("/", get(index))
.manage(graphql::schema()) .route("/health", get(health::get_health))
.mount("/", routes![index, health,]) .route(
.mount(
"/graphql", "/graphql",
routes![ post(graphql_handler_post).get(graphql_handler_get),
graphql::graphiql,
graphql::playground,
graphql::playground2,
graphql::get_graphql,
graphql::post_graphql
],
) )
.route("/graphql/playground", get(graphql_playground))
.fallback(handle_404)
.layer(Extension(redis))
.layer(Extension(schema))
.layer(CorsLayer::new().allow_origin(Any).allow_methods([
Method::GET,
Method::POST,
Method::OPTIONS,
]));
let port: u16 = std::env::var("PORT")
.unwrap_or("8000".to_string())
.parse()
.unwrap();
let addr = SocketAddr::from(([127, 0, 0, 1], port));
println!("Listening on http://{}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
} }

24
services/api/src/query.rs Normal file
View file

@ -0,0 +1,24 @@
use crate::health::Health;
use crate::world::World;
use async_graphql::Object;
pub struct Query;
#[Object]
impl Query {
async fn world(&self, id: String) -> World {
World { id: id.clone() }
}
async fn world_by_name(&self, name: String) -> World {
World::from_name(name)
}
async fn all_worlds(&self) -> Vec<World> {
World::all_worlds()
}
async fn health(&self) -> Health {
Health {}
}
}

View file

@ -1,5 +0,0 @@
use rocket_db_pools::{deadpool_redis, Database};
#[derive(Database, Clone)]
#[database("redis")]
pub struct RedisPool(deadpool_redis::Pool);

17
services/api/src/util.rs Normal file
View file

@ -0,0 +1,17 @@
use redis::{aio::MultiplexedConnection, AsyncCommands, FromRedisValue};
use std::{
ops::Sub,
time::{Duration, SystemTime},
};
pub async fn zcount<RV: FromRedisValue>(mut con: MultiplexedConnection, key: String) -> RV {
let filter_timestamp = SystemTime::now()
.sub(Duration::from_secs(60 * 15))
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
con.zcount::<String, u64, &'static str, RV>(key, filter_timestamp, "+inf")
.await
.unwrap()
}

View file

@ -0,0 +1,76 @@
use crate::util::zcount;
use async_graphql::{Context, Object};
use redis::aio::MultiplexedConnection;
pub struct Vehicles {
world_id: String,
}
impl Vehicles {
pub fn new(world_id: String) -> Self {
Self { world_id }
}
async fn by_vehicle<'ctx>(&self, ctx: &Context<'ctx>, vehicle_name: &str) -> u32 {
let con = ctx.data::<MultiplexedConnection>().unwrap().to_owned();
zcount(con, format!("v:{}/{}", self.world_id, vehicle_name)).await
}
}
#[Object]
impl Vehicles {
async fn flash<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "flash").await
}
async fn sunderer<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "sunderer").await
}
async fn ant<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "ant").await
}
async fn harasser<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "harasser").await
}
async fn javelin<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "javelin").await
}
// Tanks
async fn lightning<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "lightning").await
}
async fn prowler<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "prowler").await
}
async fn vanguard<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "vanguard").await
}
async fn magrider<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "magrider").await
}
async fn chimera<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "chimera").await
}
// Air
async fn mosquito<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "mosquito").await
}
async fn liberator<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "liberator").await
}
async fn galaxy<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "galaxy").await
}
async fn valkyrie<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "valkyrie").await
}
async fn reaver<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "reaver").await
}
async fn scythe<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "scythe").await
}
async fn dervish<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_vehicle(ctx, "dervish").await
}
}

112
services/api/src/world.rs Normal file
View file

@ -0,0 +1,112 @@
use crate::{classes::Classes, util::zcount, vehicles::Vehicles};
use async_graphql::{Context, Object};
use lazy_static::lazy_static;
use redis::aio::MultiplexedConnection;
use std::collections::HashMap;
lazy_static! {
static ref WORLD_NAME_TO_ID: HashMap<&'static str, &'static str> = HashMap::from([
("connery", "1"),
("miller", "10"),
("cobalt", "13"),
("emerald", "17"),
("jaeger", "19"),
("soltech", "40"),
("genudine", "1000"),
("ceres", "2000"),
]);
static ref WORLD_ID_TO_NAME: HashMap<&'static str, &'static str> = HashMap::from([
("1", "Connery"),
("10", "Miller"),
("13", "Cobalt"),
("17", "Emerald"),
("19", "Jaeger"),
("40", "SolTech"),
("1000", "Genudine"),
("2000", "Ceres"),
]);
}
pub struct World {
pub id: String,
}
impl World {
pub fn from_name(name: String) -> World {
let id = WORLD_NAME_TO_ID
.get(name.to_lowercase().as_str())
.unwrap_or(&"-1");
World { id: id.to_string() }
}
pub fn all_worlds() -> Vec<World> {
WORLD_ID_TO_NAME
.keys()
.map(|id| World { id: id.to_string() })
.collect()
}
}
#[Object]
impl World {
async fn id(&self) -> &str {
&self.id
}
async fn name(&self) -> String {
WORLD_ID_TO_NAME
.get(self.id.as_str())
.unwrap_or(&"Unknown")
.to_string()
}
async fn population<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
let con = ctx.data::<MultiplexedConnection>().unwrap().to_owned();
zcount(con, format!("wp:{}", self.id)).await
}
async fn faction_population(&self) -> FactionPopulation {
FactionPopulation {
world_id: self.id.clone(),
}
}
async fn vehicles(&self) -> Vehicles {
Vehicles::new(self.id.clone())
}
async fn classes(&self) -> Classes {
Classes::new(self.id.clone())
}
}
struct FactionPopulation {
world_id: String,
}
impl FactionPopulation {
async fn by_faction<'ctx>(&self, ctx: &Context<'ctx>, faction: u8) -> u32 {
let con = ctx.data::<MultiplexedConnection>().unwrap().to_owned();
zcount(con, format!("wp:{}/{}", self.world_id, faction)).await
}
}
#[Object]
impl FactionPopulation {
async fn vs<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_faction(ctx, 1).await
}
async fn nc<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_faction(ctx, 2).await
}
async fn tr<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_faction(ctx, 3).await
}
async fn ns<'ctx>(&self, ctx: &Context<'ctx>) -> u32 {
self.by_faction(ctx, 4).await
}
}

View file

@ -7,8 +7,7 @@ edition = "2021"
[dependencies] [dependencies]
redis = { version = "0.22.1", default_features = false, features = ["r2d2"] } redis = { version = "0.22.1", default_features = false, features = ["r2d2"] }
redis_ts = "0.4.2" lazy_static = "1.4.0"
once_cell = "1.16.0"
tokio-tungstenite = { version = "0.17.2", features=["native-tls"] } tokio-tungstenite = { version = "0.17.2", features=["native-tls"] }
serde_json = "1.0.88" serde_json = "1.0.88"
serde = { version = "1.0.147", features = ["derive"] } serde = { version = "1.0.147", features = ["derive"] }

View file

@ -1,6 +1,6 @@
use futures::{pin_mut, FutureExt}; use futures::{pin_mut, FutureExt};
use futures_util::StreamExt; use futures_util::StreamExt;
use once_cell::sync::Lazy; use lazy_static::lazy_static;
use redis::Commands; use redis::Commands;
use serde::Deserialize; use serde::Deserialize;
use serde_json::json; use serde_json::json;
@ -9,14 +9,17 @@ use tokio_tungstenite::{connect_async, tungstenite::Message};
mod translators; mod translators;
pub static REDIS_CLIENT: Lazy<redis::Client> = Lazy::new(|| { lazy_static! {
redis::Client::open(std::env::var("REDIS_ADDR").unwrap_or("redis://localhost:6379".to_string())) static ref REDIS_CLIENT: redis::Client = redis::Client::open(format!(
.unwrap() "redis://{}:{}",
}); std::env::var("REDIS_HOST").unwrap_or("localhost".to_string()),
std::env::var("REDIS_PORT").unwrap_or("6379".to_string()),
static PAIR: Lazy<String> = Lazy::new(|| env::var("PAIR").unwrap_or_default()); ))
static ROLE: Lazy<String> = Lazy::new(|| env::var("ROLE").unwrap_or("primary".to_string())); .unwrap();
static WS_ADDR: Lazy<String> = Lazy::new(|| env::var("WS_ADDR").unwrap_or_default()); static ref PAIR: String = env::var("PAIR").unwrap_or_default();
static ref ROLE: String = env::var("ROLE").unwrap_or("primary".to_string());
static ref WS_ADDR: String = env::var("WS_ADDR").unwrap_or_default();
}
async fn send_init(tx: futures::channel::mpsc::UnboundedSender<Message>) { async fn send_init(tx: futures::channel::mpsc::UnboundedSender<Message>) {
let worlds_raw = env::var("WORLDS").unwrap_or_default(); let worlds_raw = env::var("WORLDS").unwrap_or_default();

View file

@ -1,10 +1,10 @@
// GENERATED CODE -- Do not edit. Run `cargo run --bin codegen` to regenerate. // GENERATED CODE -- Do not edit. Run `cargo run --bin codegen` to regenerate.
use once_cell::sync::Lazy; use lazy_static::lazy_static;
use std::collections::HashMap; use std::collections::HashMap;
static VEHICLE_TO_NAME: Lazy<HashMap<&str, &str>> = Lazy::new(|| { lazy_static! {
HashMap::from([ static ref VEHICLE_TO_NAME: HashMap<&'static str, &'static str> = HashMap::from([
("1", "flash"), ("1", "flash"),
("2", "sunderer"), ("2", "sunderer"),
("3", "lightning"), ("3", "lightning"),
@ -48,18 +48,8 @@ static VEHICLE_TO_NAME: Lazy<HashMap<&str, &str>> = Lazy::new(|| {
("2136", "dervish"), ("2136", "dervish"),
("2137", "chimera"), ("2137", "chimera"),
("2142", "corsair"), ("2142", "corsair"),
]) ]);
}); static ref LOADOUT_TO_CLASS: HashMap<&'static str, &'static str> = HashMap::from([
pub fn vehicle_to_name(vehicle_id: &str) -> String {
match VEHICLE_TO_NAME.get(&vehicle_id) {
Some(name) => name.to_string(),
None => "unknown".to_string(),
}
}
static LOADOUT_TO_CLASS: Lazy<HashMap<&str, &str>> = Lazy::new(|| {
HashMap::from([
("1", "infiltrator"), ("1", "infiltrator"),
("3", "light_assault"), ("3", "light_assault"),
("4", "combat_medic"), ("4", "combat_medic"),
@ -84,8 +74,15 @@ static LOADOUT_TO_CLASS: Lazy<HashMap<&str, &str>> = Lazy::new(|| {
("31", "engineer"), ("31", "engineer"),
("32", "heavy_assault"), ("32", "heavy_assault"),
("45", "max"), ("45", "max"),
]) ]);
}); }
pub fn vehicle_to_name(vehicle_id: &str) -> String {
match VEHICLE_TO_NAME.get(&vehicle_id) {
Some(name) => name.to_string(),
None => "unknown".to_string(),
}
}
pub fn loadout_to_class(loadout_id: &str) -> String { pub fn loadout_to_class(loadout_id: &str) -> String {
match LOADOUT_TO_CLASS.get(&loadout_id) { match LOADOUT_TO_CLASS.get(&loadout_id) {