rust rewrite

This commit is contained in:
41666 2023-05-30 01:40:00 -04:00
parent 4ded6085b7
commit 7ef2fe757c
33 changed files with 2310 additions and 4269 deletions

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
target
.git

23
.github/workflows/ci.yaml vendored Normal file
View file

@ -0,0 +1,23 @@
name: "CI"
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Log in to the Container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- run: docker buildx create --use --driver=docker-container
- run: |
TAG_LATEST_IF_MASTER=$(if [ "$GITHUB_REF_NAME" = "main" ]; then echo "-t ghcr.io/${{ github.repository }}/agg-population:latest"; else echo ""; fi)
docker buildx build . \
-t ghcr.io/${{ github.repository }}/agg-population:${{ github.sha }} $TAG_LATEST_IF_MASTER \
--push \
--cache-to type=gha,scope=$GITHUB_REF_NAME-agg-population \
--cache-from type=gha,scope=$GITHUB_REF_NAME-agg-population

2
.gitignore vendored
View file

@ -1 +1 @@
node_modules
/target

View file

@ -1,3 +1,3 @@
{
"editor.tabSize": 2
"editor.tabSize": 4
}

1788
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

20
Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "agg-population"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.96"
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
sled = "0.34"
reqwest = { version = "0.11", features = ["rustls-tls-webpki-roots", "rustls", "json"] }
openssl = { version = "0.10", features = ["vendored"] }
bincode = "1.3"
chrono = { version = "0.4", features = ["serde"] }
tower-http = { version = "0.4", features = ["trace"] }

21
Dockerfile Normal file
View file

@ -0,0 +1,21 @@
FROM rust:1.69.0-bullseye as rust-base
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl clang
ARG MOLD_VERSION=1.11.0
RUN curl -sSL https://github.com/rui314/mold/releases/download/v${MOLD_VERSION}/mold-${MOLD_VERSION}-x86_64-linux.tar.gz | tar xzv && \
mv mold-${MOLD_VERSION}-x86_64-linux/bin/mold /mold && \
rm -rf mold-${MOLD_VERSION}-x86_64-linux
ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=clang
ENV RUSTFLAGS="-C link-arg=-fuse-ld=/mold"
FROM rust-base as builder
COPY . .
ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=clang
ENV RUSTFLAGS="-C link-arg=-fuse-ld=/mold"
RUN cargo build --release --bin agg-population
FROM debian:bullseye-slim as runtime
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/agg-population /app
ENTRYPOINT ["/app"]

View file

@ -1,31 +0,0 @@
# https://agg.ps2.live/population
A Planetside 2 population aggreggation API between
- https://ps2.fisu.pw
- https://wt.honu.pw
- https://saerro.ps2.live
- https://voidwell.com
Has some jank filters and will average between all 4 sources with as much data as can be used.
If you have a population API to add to this aggreggator, open an issue with your API details. The more sources, the merrier, but we usually need to make some performance considerations.
This API is hosted on Cloudflare Workers, and makes use of aggressive caching with a 3 minute TTL.
Need help? Talk to us on the [Planetside Community Developers Discord](https://discord.gg/yVzGEg3RKV) in `#ps2live`
## API
- [/$worldID](https://agg.ps2.live/population/1) - Single world
- [/all](https://agg.ps2.live/population/all) - All worlds at once
- [~/flags](https://agg.ps2.live/population~/flags) - Feature flags that help limit useless requests and add speed.
- [~/health](https://agg.ps2.live/population~/health) - Tests reachability
## Developing
```
npm install
npm start
```

View file

@ -1,31 +0,0 @@
const socketio = require("socket.io-client");
const client = socketio("https://planetside-2-api.herokuapp.com", {
transports: ["websocket"],
extraHeaders: {
Origin: "https://ps2.nice.kiwi",
},
});
client.on("connect", (e) => {
console.log("Connected to server", { e });
client.emit("worlds-update-request");
});
client.on("worlds-update", (e) => {
console.log(e);
});
client.on("disconnect", () => {
console.log("Disconnected from server");
});
client.on("error", (e) => {
console.log({ error: e });
});
client.on("connect_error", (e) => {
console.log({
connectError: e,
});
});

View file

@ -1,443 +0,0 @@
{
"name": "kiwitest",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"devDependencies": {
"socket.io-client": "2.x",
"ws": "^8.11.0"
}
},
"node_modules/after": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
"integrity": "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==",
"dev": true
},
"node_modules/arraybuffer.slice": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
"integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
"dev": true
},
"node_modules/backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
"integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==",
"dev": true
},
"node_modules/base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==",
"dev": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/blob": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
"integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==",
"dev": true
},
"node_modules/component-bind": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
"integrity": "sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==",
"dev": true
},
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
"dev": true
},
"node_modules/component-inherit": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
"integrity": "sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==",
"dev": true
},
"node_modules/debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/engine.io-client": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.3.tgz",
"integrity": "sha512-qsgyc/CEhJ6cgMUwxRRtOndGVhIu5hpL5tR4umSpmX/MvkFoIxUTM7oFMDQumHNzlNLwSVy6qhstFPoWTf7dOw==",
"dev": true,
"dependencies": {
"component-emitter": "~1.3.0",
"component-inherit": "0.0.3",
"debug": "~3.1.0",
"engine.io-parser": "~2.2.0",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
"ws": "~7.4.2",
"xmlhttprequest-ssl": "~1.6.2",
"yeast": "0.1.2"
}
},
"node_modules/engine.io-client/node_modules/ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"dev": true,
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz",
"integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==",
"dev": true,
"dependencies": {
"after": "0.8.2",
"arraybuffer.slice": "~0.0.7",
"base64-arraybuffer": "0.1.4",
"blob": "0.0.5",
"has-binary2": "~1.0.2"
}
},
"node_modules/has-binary2": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
"integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
"dev": true,
"dependencies": {
"isarray": "2.0.1"
}
},
"node_modules/has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
"integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==",
"dev": true
},
"node_modules/indexof": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
"integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==",
"dev": true
},
"node_modules/isarray": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==",
"dev": true
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
"node_modules/parseqs": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
"integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==",
"dev": true
},
"node_modules/parseuri": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==",
"dev": true
},
"node_modules/socket.io-client": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.5.0.tgz",
"integrity": "sha512-lOO9clmdgssDykiOmVQQitwBAF3I6mYcQAo7hQ7AM6Ny5X7fp8hIJ3HcQs3Rjz4SoggoxA1OgrQyY8EgTbcPYw==",
"dev": true,
"dependencies": {
"backo2": "1.0.2",
"component-bind": "1.0.0",
"component-emitter": "~1.3.0",
"debug": "~3.1.0",
"engine.io-client": "~3.5.0",
"has-binary2": "~1.0.2",
"indexof": "0.0.1",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
"socket.io-parser": "~3.3.0",
"to-array": "0.1.4"
}
},
"node_modules/socket.io-parser": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.3.tgz",
"integrity": "sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==",
"dev": true,
"dependencies": {
"component-emitter": "~1.3.0",
"debug": "~3.1.0",
"isarray": "2.0.1"
}
},
"node_modules/to-array": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
"integrity": "sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==",
"dev": true
},
"node_modules/ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"dev": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz",
"integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==",
"dev": true
}
},
"dependencies": {
"after": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
"integrity": "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==",
"dev": true
},
"arraybuffer.slice": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
"integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
"dev": true
},
"backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
"integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==",
"dev": true
},
"base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==",
"dev": true
},
"blob": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
"integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==",
"dev": true
},
"component-bind": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
"integrity": "sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==",
"dev": true
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
"dev": true
},
"component-inherit": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
"integrity": "sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==",
"dev": true
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
},
"engine.io-client": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.3.tgz",
"integrity": "sha512-qsgyc/CEhJ6cgMUwxRRtOndGVhIu5hpL5tR4umSpmX/MvkFoIxUTM7oFMDQumHNzlNLwSVy6qhstFPoWTf7dOw==",
"dev": true,
"requires": {
"component-emitter": "~1.3.0",
"component-inherit": "0.0.3",
"debug": "~3.1.0",
"engine.io-parser": "~2.2.0",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
"ws": "~7.4.2",
"xmlhttprequest-ssl": "~1.6.2",
"yeast": "0.1.2"
},
"dependencies": {
"ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"dev": true,
"requires": {}
}
}
},
"engine.io-parser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz",
"integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==",
"dev": true,
"requires": {
"after": "0.8.2",
"arraybuffer.slice": "~0.0.7",
"base64-arraybuffer": "0.1.4",
"blob": "0.0.5",
"has-binary2": "~1.0.2"
}
},
"has-binary2": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
"integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
"dev": true,
"requires": {
"isarray": "2.0.1"
}
},
"has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
"integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==",
"dev": true
},
"indexof": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
"integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==",
"dev": true
},
"isarray": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==",
"dev": true
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
"parseqs": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
"integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==",
"dev": true
},
"parseuri": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==",
"dev": true
},
"socket.io-client": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.5.0.tgz",
"integrity": "sha512-lOO9clmdgssDykiOmVQQitwBAF3I6mYcQAo7hQ7AM6Ny5X7fp8hIJ3HcQs3Rjz4SoggoxA1OgrQyY8EgTbcPYw==",
"dev": true,
"requires": {
"backo2": "1.0.2",
"component-bind": "1.0.0",
"component-emitter": "~1.3.0",
"debug": "~3.1.0",
"engine.io-client": "~3.5.0",
"has-binary2": "~1.0.2",
"indexof": "0.0.1",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
"socket.io-parser": "~3.3.0",
"to-array": "0.1.4"
}
},
"socket.io-parser": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.3.tgz",
"integrity": "sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==",
"dev": true,
"requires": {
"component-emitter": "~1.3.0",
"debug": "~3.1.0",
"isarray": "2.0.1"
}
},
"to-array": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
"integrity": "sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==",
"dev": true
},
"ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"dev": true,
"requires": {}
},
"xmlhttprequest-ssl": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz",
"integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==",
"dev": true
},
"yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==",
"dev": true
}
}
}

View file

@ -1,6 +0,0 @@
{
"devDependencies": {
"socket.io-client": "2.x",
"ws": "^8.11.0"
}
}

View file

@ -1,22 +0,0 @@
const WebSocket = require("ws");
const wsc = new WebSocket(
"wss://planetside-2-api.herokuapp.com/socket.io/?EIO=3&transport=websocket",
{
origin: "https://ps2.nice.kiwi",
}
);
wsc.on("open", () => {
wsc.send(`42["worlds-update-request"]`);
});
wsc.on("message", (e) => {
const messageRaw = e.toString();
if (messageRaw.startsWith("42")) {
const [event, message] = JSON.parse(messageRaw.slice(2));
console.log({ message });
wsc.close();
}
});

2610
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,16 +0,0 @@
{
"name": "agg-population",
"version": "0.0.0",
"devDependencies": {
"@cloudflare/workers-types": "^4.20221111.1",
"itty-router": "^3.0.8",
"typescript": "^4.9.4",
"wrangler": "^2.6.2"
},
"private": true,
"scripts": {
"start": "wrangler dev --local",
"deploy": "wrangler publish",
"wrangler": "wrangler"
}
}

View file

@ -1,36 +0,0 @@
export class Cache {
private cache: Map<string, any> = new Map();
constructor(public kv: KVNamespace, public disableCache: boolean = false) {}
async get<T>(id: string): Promise<T | null> {
if (this.disableCache) {
return null;
}
// console.log("cache get", id);
let item = this.cache.get(id);
if (!item) {
// console.log("remote cache get", id);
item = await this.kv.get<T>(id, "json");
if (item) {
// console.log("local cache miss, remote cache hit", id);
this.cache.set(id, item);
}
}
return item;
}
async put<T>(id: string, world: T, ttl: number = 60 * 3): Promise<T> {
if (this.disableCache) {
return world;
}
this.cache.set(id, world);
await this.kv.put(id, JSON.stringify(world), {
expirationTtl: ttl,
});
return world;
}
}

View file

@ -1,7 +0,0 @@
export const noData = () =>
new Response(
JSON.stringify({
error: "No data available",
}),
{ status: 404 }
);

View file

@ -1,145 +0,0 @@
import { Cache } from "./cache";
import { fisuFetchWorld } from "./sources/fisu";
import { honuFetchWorld } from "./sources/honu";
import { saerroFetchWorld } from "./sources/saerro";
import { sanctuaryFetchWorld } from "./sources/sanctuary";
import { voidwellFetchWorld } from "./sources/voidwell";
import { DebugPayload, Flags, OnePayload, ServiceResponse } from "./types";
const avgOf = (arr: number[]) =>
Math.floor(arr.reduce((a, b) => a + b, 0) / arr.length);
const flatMapBy = (arr: any[], key: string) =>
arr.reduce((a, b) => [...a, b[key]], []);
const defaultServiceResponse: ServiceResponse<number, null> = {
population: {
total: -1,
nc: -1,
tr: -1,
vs: -1,
},
raw: null,
cachedAt: new Date(),
};
type World = {
world: OnePayload | null;
debug: DebugPayload;
};
export const getWorld = async (id: string, cache: Cache, flags: Flags) => {
const cached = await cache.get<World>(id);
if (cached) {
return cached;
}
const [saerro, fisu, honu, voidwell, sanctuary] = await Promise.all([
!flags.disableSaerro
? saerroFetchWorld(id, cache).catch((e) => {
console.error("SAERRO ERROR:", e);
return defaultServiceResponse;
})
: defaultServiceResponse,
!flags.disableFisu
? fisuFetchWorld(id, cache, flags.fisuUsePS4EU).catch(
() => defaultServiceResponse
)
: defaultServiceResponse,
!flags.disableHonu
? honuFetchWorld(id, cache).catch(() => defaultServiceResponse)
: defaultServiceResponse,
!flags.disableVoidwell
? voidwellFetchWorld(id, cache, flags.voidwellUsePS4).catch(
() => defaultServiceResponse
)
: defaultServiceResponse,
!flags.disableSanctuary
? sanctuaryFetchWorld(id, cache).catch(() => defaultServiceResponse)
: defaultServiceResponse,
]);
const debug: DebugPayload = {
raw: {
saerro: saerro.raw,
fisu: fisu.raw,
honu: honu.raw,
voidwell: voidwell.raw,
sanctuary: sanctuary.raw,
},
timings: {
saerro: saerro?.timings || null,
fisu: fisu?.timings || null,
honu: honu?.timings || null,
voidwell: voidwell?.timings || null,
sanctuary: sanctuary?.timings || null,
},
lastFetchTimes: {
saerro: saerro.cachedAt,
fisu: fisu.cachedAt,
honu: honu.cachedAt,
voidwell: voidwell.cachedAt,
sanctuary: sanctuary.cachedAt,
},
};
const totalPopulations = [
saerro.population.total,
fisu.population.total,
honu.population.total,
voidwell.population.total,
sanctuary.population.total,
].filter((x) => x > 0);
if (totalPopulations.length === 0) {
return await cache.put<World>(id, {
world:
id !== "19"
? null
: {
// Jaeger gets a special case, we assume it's always up, but empty.
id: 19,
average: 0,
factions: {
nc: 0,
tr: 0,
vs: 0,
},
services: {
saerro: 0,
fisu: 0,
honu: 0,
voidwell: 0,
sanctuary: 0,
},
},
debug,
});
}
const factionPopulations = [
saerro.population,
fisu.population,
honu.population,
sanctuary.population,
].filter((x) => x.total > 0);
const payload: OnePayload = {
id: Number(id),
average: avgOf(totalPopulations),
factions: {
nc: avgOf(flatMapBy(factionPopulations, "nc")),
tr: avgOf(flatMapBy(factionPopulations, "tr")),
vs: avgOf(flatMapBy(factionPopulations, "vs")),
},
services: {
saerro: saerro.population.total,
fisu: fisu.population.total,
honu: honu.population.total,
voidwell: voidwell.population.total,
sanctuary: sanctuary.population.total,
},
};
return await cache.put(id, { world: payload, debug });
};

97
src/handlers.rs Normal file
View file

@ -0,0 +1,97 @@
use crate::{
sources::{fisu, honu, saerro, sanctuary, voidwell},
types::{AllResponse, Population, Response},
};
use axum::{
extract::{Path, State},
Json,
};
use tokio::task::JoinSet;
pub async fn get_one_world(State(db): State<sled::Db>, Path(world): Path<i32>) -> Json<Response> {
Json(get_world(db, world).await)
}
pub async fn get_all_worlds(State(db): State<sled::Db>) -> Json<AllResponse> {
let mut set = JoinSet::new();
let mut worlds = vec![Response::default(); 8];
for world in vec![1, 10, 13, 17, 19, 40, 1000, 2000] {
set.spawn(get_world(db.clone(), world));
}
let mut i = 0;
while let Some(response) = set.join_next().await {
worlds[i] = response.unwrap_or_default();
i += 1;
}
Json(AllResponse { worlds })
}
async fn get_world(db: sled::Db, world: i32) -> Response {
if let Ok(data) = world_from_cache(db.clone(), world) {
return data;
}
let mut response = Response::default();
response.id = world;
let mut populations: Vec<Population> = Vec::new();
let mut set = JoinSet::new();
set.spawn(async move { ("saerro", saerro(world).await) });
set.spawn(async move { ("honu", honu(world).await) });
set.spawn(async move { ("fisu", fisu(world).await) });
set.spawn(async move { ("voidwell", voidwell(world).await) });
set.spawn(async move { ("sanctuary", sanctuary(world).await) });
while let Some(data) = set.join_next().await {
let (service, population) = data.unwrap_or(("failed", Ok(Population::default())));
if service == "failed" || population.is_err() {
continue;
}
let population = population.unwrap();
populations.push(population);
response.services.insert(service.to_string(), population);
}
if (populations.len() as i32) == 0 {
return response;
}
response.average = populations.iter().map(|p| p.total).sum::<i32>() / populations.len() as i32;
response.factions.nc = populations.iter().map(|p| p.nc).sum::<i32>() / populations.len() as i32;
response.factions.tr = populations.iter().map(|p| p.tr).sum::<i32>() / populations.len() as i32;
response.factions.vs = populations.iter().map(|p| p.vs).sum::<i32>() / populations.len() as i32;
response.cached_at = chrono::Utc::now();
world_to_cache(db, world, &response);
response
}
fn world_from_cache(db: sled::Db, world: i32) -> Result<Response, ()> {
let key = format!("world:{}", world);
let value = match db.get(key) {
Ok(Some(value)) => value,
_ => return Err(()),
};
match bincode::deserialize::<Response>(&value) {
Ok(response) => {
if response.cached_at + chrono::Duration::minutes(3) < chrono::Utc::now() {
return Err(());
}
Ok(response)
}
_ => Err(()),
}
}
fn world_to_cache(db: sled::Db, world: i32, response: &Response) {
let key = format!("world:{}", world);
let value = bincode::serialize(response).unwrap();
db.insert(key, value).unwrap();
}

View file

@ -1,77 +0,0 @@
import { IRequest } from "itty-router";
import { noData } from "./errors";
import { DebugPayload, Flags, OnePayload } from "./types";
import { Cache } from "./cache";
import { getWorld } from "./fetcher";
export const handleOne = async (
{ params: { id }, query: { debug: debugParam } }: IRequest,
_1: unknown,
_2: unknown,
Cache: Cache,
flags: Flags
) => {
const { world, debug } = await getWorld(id, Cache, flags);
if (world === null) {
return noData();
}
let output: OnePayload | (OnePayload & DebugPayload) = world;
if (debugParam) {
output = { ...output, ...debug };
}
return new Response(JSON.stringify(output), {
headers: {
"content-type": "application/json",
},
});
};
export const handleAll = async (
{ query: { debug } }: IRequest,
_2: unknown,
_3: unknown,
cache: Cache,
flags: Flags
): Promise<Response> => {
const cached = await cache.get(`all${debug ? ".debug" : ""}`);
if (cached) {
return new Response(JSON.stringify(cached), {
headers: {
"content-type": "application/json",
},
});
}
const worlds = ["1", "10", "13", "17", "19", "40", "1000", "2000"];
const worldTasks = [];
for (const world of worlds) {
worldTasks.push(getWorld(world, cache, flags));
}
await worldTasks[0]; // Force the first one to cache for the rest
const worldData = await Promise.all(worldTasks);
if (debug === "1") {
return new Response(JSON.stringify(worldData), {
headers: {
"content-type": "application/json",
},
});
}
const worldPayloads = worldData.map((x: any) => x.world || x);
await cache.put(`all${debug ? ".debug" : ""}`, worldPayloads);
return new Response(JSON.stringify(worldPayloads), {
headers: {
"content-type": "application/json",
},
});
};

54
src/html/index.html Normal file
View file

@ -0,0 +1,54 @@
<!DOCTYPE html>
<meta charset="utf-8" />
<title>Aggregate PlanetSide 2 Population API</title>
<meta name="description" content="Multi-source population API" />
<style>
body {
font-family: sans-serif;
margin: 0;
padding: 0;
background-color: black;
color: white;
}
main {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
flex-direction: column;
font-weight: lighter;
text-align: center;
}
.big-header {
font-size: 2rem;
}
.api-list {
margin-top: 2rem;
}
.api-item {
display: block;
background-color: #334;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
margin: 0.5rem;
text-decoration: none;
color: white;
font-weight: bold;
}
</style>
<main>
<div>
<b>tl;dr:</b><br />
<span class="big-header"
>( fisu + honu + saerro + sanctuary + voidwell ) / 5</span
>
</div>
<div class="api-list">
<a class="api-item" href="/worlds/1">GET /worlds/{worldID} ▶️</a>
<a class="api-item" href="/worlds/all">GET /worlds/all ▶️</a>
</div>
<p>Results are cached for 3 minutes. Requesting faster is a dumb idea.</p>
</main>

View file

@ -1,91 +0,0 @@
import { Route, Router, RouterType } from "itty-router";
import { handleAll, handleOne } from "./handlers";
import { Env, Flags } from "./types";
import { Cache } from "./cache";
import { index } from "./landing";
interface BasicRouter extends RouterType {
all: Route;
get: Route;
}
const router = <BasicRouter>Router();
router
.get<BasicRouter>(
"/",
() =>
new Response(null, { status: 303, headers: { location: "/population/" } })
)
.get<BasicRouter>("/population/", index)
.get<BasicRouter>("/population/all", handleAll)
.get<BasicRouter>("/population/:id", handleOne)
.get<BasicRouter>(
"/population~/flags",
(_1, _2, _3, cache: Cache, flags: Flags) => {
return new Response(
JSON.stringify({
...flags,
disableCache: cache.disableCache,
}),
{
headers: { "content-type": "application/json" },
}
);
}
)
.get<BasicRouter>("/population~/health", async () => {
const [saerro, voidwell, honu, fisu] = await Promise.all([
fetch("https://saerro.ps2.live/health").then((r) => r.status === 200),
fetch("https://voidwell.com/").then((r) => r.status === 200),
fetch("https://wt.honu.pw/api/health").then((r) => r.status === 200),
fetch("https://ps2.fisu.pw").then((r) => r.status === 200),
]);
return new Response(
JSON.stringify({
saerro,
voidwell,
honu,
fisu,
}),
{
headers: { "content-type": "application/json" },
status: saerro || voidwell || honu || fisu ? 200 : 502,
}
);
})
.all<BasicRouter>("*", () => {
return new Response("Not found", {
headers: { "content-type": "text/plain" },
});
});
export default {
fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {
const worldCache = new Cache(env.CACHE, env.DISABLE_CACHE === "1");
const flags: Flags = {
disableFisu: env.DISABLE_FISU === "1",
disableHonu: env.DISABLE_HONU === "1",
disableSaerro: env.DISABLE_SAERRO === "1",
disableVoidwell: env.DISABLE_VOIDWELL === "1",
voidwellUsePS4: env.VOIDWELL_USE_PS4 === "1",
fisuUsePS4EU: true, // env.FISU_USE_PS4EU === "1",
};
const start = Date.now();
return router
.handle(request as any, env, ctx, worldCache, flags)
.then((response) => {
response.headers.set("access-control-allow-origin", "*");
response.headers.set(
"access-control-allow-method",
"GET, HEAD, OPTIONS"
);
response.headers.set("x-timing", `${Date.now() - start}ms`);
return response;
});
},
};

View file

@ -1,106 +0,0 @@
export const index = (): Response => {
const body = `Aggregate Planetside 2 World Population
GitHub: https://github.com/genudine/agg-population
Production: https://agg.ps2.live/population
Need help with this data?
## Methodology
This service aggregates the population data from the following sources:
- https://saerro.ps2.live/
- https://ps2.fisu.pw/
- https://wt.honu.pw/
- https://voidwell.com/ (caveat: no factions, non-standard counting method)
## Routes
GET /:id - Get one world by ID
{
"id": 17,
"average": 285,
"factions": {
"nc": 91,
"tr": 92,
"vs": 91
},
"services": {
"saerro": 282,
"fisu": 271,
"honu": 292,
"voidwell": 298
}
}
Query Parameters:
?debug=1 - Adds these fields to the response:
{
/// ... other fields
"raw": {
"saerro": { ... },
"fisu": { ... },
"honu": { ... },
"voidwell": { ... }
},
"lastFetchTimes": {
"saerro": "2020-10-10T00:00:00.000Z",
"fisu": "2020-10-10T00:00:00.000Z",
"honu": "2020-10-10T00:00:00.000Z",
"voidwell": "2020-10-10T00:00:00.000Z"
}
}
GET /all - Get all worlds
[
{
"id": 17,
"average": 285,
"factions": {
"nc": 91,
"tr": 92,
"vs": 91
},
"services": {
"saerro": 282,
"fisu": 271,
"honu": 292,
"voidwell": 298
}
},
{
"id": 1,
"average": 83,
"factions": {
"nc": 30,
"tr": 15,
"vs": 29
},
"services": {
"saerro": 95,
"fisu": 48,
"honu": 91,
"voidwell": 99
}
}
]
-- This also has a debug query parameter, but it's extremely verbose. It's good for debugging extreme async issues with the platform.
GET ~/flags - Get the current feature flags. These wiggle knobs that affect request timings, caching, and other things.
GET ~/health - Gets health of this and upstream services.
## Caching and usage limits
This service cached on a world basis for 3 minutes. Debug data is cached alongside world data too.`;
return new Response(body, {
headers: {
"content-type": "text/plain",
},
});
};

35
src/main.rs Normal file
View file

@ -0,0 +1,35 @@
use crate::handlers::{get_all_worlds, get_one_world};
use axum::{response::Html, routing::get, Router};
use std::net::SocketAddr;
use tower_http::trace::TraceLayer;
mod handlers;
mod sources;
mod types;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter("tower_http=trace")
.init();
let db = sled::open("/tmp/agg-population").expect("open");
let app = Router::new()
.route("/", get(root))
.route("/worlds/all", get(get_all_worlds))
.route("/worlds/:world", get(get_one_world))
.layer(TraceLayer::new_for_http())
.with_state(db);
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
tracing::debug!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn root() -> Html<&'static str> {
Html(include_str!("./html/index.html"))
}

227
src/sources.rs Normal file
View file

@ -0,0 +1,227 @@
use crate::types::Population;
pub async fn saerro(world: i32) -> Result<Population, ()> {
#[derive(serde::Deserialize)]
struct World {
pub population: Population,
}
#[derive(serde::Deserialize)]
struct Data {
pub world: World,
}
#[derive(serde::Deserialize)]
struct Response {
pub data: Data,
}
let url = format!(
"https://saerro.ps2.live/graphql?query={{ world(by: {{id: {}}}) {{ population {{ nc tr vs }} }}}}",
world
);
let response = reqwest::get(url)
.await
.unwrap()
.json::<Response>()
.await
.unwrap();
Ok(Population {
nc: response.data.world.population.nc,
tr: response.data.world.population.tr,
vs: response.data.world.population.vs,
total: response.data.world.population.total(),
})
}
pub async fn honu(world: i32) -> Result<Population, ()> {
#[derive(serde::Deserialize)]
struct Response {
pub nc: i32,
pub tr: i32,
pub vs: i32,
pub ns_vs: i32,
pub ns_nc: i32,
pub ns_tr: i32,
}
let url = format!("https://wt.honu.pw/api/population/{}", world);
let response = reqwest::get(url)
.await
.unwrap()
.json::<Response>()
.await
.unwrap();
Ok(Population {
nc: response.nc + response.ns_nc,
tr: response.tr + response.ns_tr,
vs: response.vs + response.ns_vs,
total: response.nc
+ response.tr
+ response.vs
+ response.ns_nc
+ response.ns_tr
+ response.ns_vs,
})
}
pub async fn fisu(world: i32) -> Result<Population, ()> {
#[derive(serde::Deserialize)]
struct Root {
pub result: Vec<Result>,
}
#[derive(serde::Deserialize)]
struct Result {
pub vs: i32,
pub nc: i32,
pub tr: i32,
pub ns: i32,
}
let subdomain = match world {
1000 => "ps4us.ps2",
2000 => "ps4eu.ps2",
_ => "ps2",
};
let url = format!(
"https://{}.fisu.pw/api/population/?world={}",
subdomain, world
);
let response = reqwest::get(url)
.await
.unwrap()
.json::<Root>()
.await
.unwrap();
Ok(Population {
nc: response.result[0].nc + response.result[0].ns,
tr: response.result[0].tr + response.result[0].ns,
vs: response.result[0].vs + response.result[0].ns,
total: response.result[0].nc
+ response.result[0].tr
+ response.result[0].vs
+ response.result[0].ns,
})
}
pub async fn voidwell(world: i32) -> Result<Population, ()> {
if world == 1000 || world == 2000 {
return Err(());
}
#[derive(serde::Deserialize)]
struct Root {
#[serde(rename = "zoneStates")]
pub zone_states: Vec<ZoneState>,
}
#[derive(serde::Deserialize)]
struct ZoneState {
pub population: VoidwellPopulation,
}
#[derive(serde::Deserialize)]
struct VoidwellPopulation {
pub vs: i32,
pub nc: i32,
pub tr: i32,
pub ns: i32,
}
let platform = match world {
1000 => "ps4us",
2000 => "ps4eu",
_ => "pc",
};
let url = format!(
"https://api.voidwell.com/ps2/worldstate/{}?platform={}",
world, platform
);
let response = reqwest::get(url)
.await
.unwrap()
.json::<Root>()
.await
.unwrap();
Ok(Population {
nc: response
.zone_states
.iter()
.map(|zone| zone.population.nc)
.sum(),
tr: response
.zone_states
.iter()
.map(|zone| zone.population.tr)
.sum(),
vs: response
.zone_states
.iter()
.map(|zone| zone.population.vs)
.sum(),
total: response
.zone_states
.iter()
.map(|zone| {
zone.population.nc + zone.population.tr + zone.population.vs + zone.population.ns
})
.sum(),
})
}
pub async fn sanctuary(world: i32) -> Result<Population, ()> {
// No PS4 nor Jaeger
if world == 1000 || world == 2000 || world == 19 {
return Err(());
}
#[derive(serde::Deserialize)]
struct Root {
pub world_population_list: Vec<World>,
}
#[derive(serde::Deserialize)]
struct World {
pub population: SanctuaryPopulation,
}
#[derive(serde::Deserialize)]
struct SanctuaryPopulation {
#[serde(rename = "VS")]
pub vs: i32,
#[serde(rename = "NC")]
pub nc: i32,
#[serde(rename = "TR")]
pub tr: i32,
#[serde(rename = "NSO")]
pub nso: i32,
}
let url = format!(
"https://census.lithafalcon.cc/get/ps2/world_population?c:censusJSON=false&world_id={}",
world
);
let response = reqwest::get(url)
.await
.unwrap()
.json::<Root>()
.await
.unwrap();
Ok(Population {
nc: response.world_population_list[0].population.nc,
tr: response.world_population_list[0].population.tr,
vs: response.world_population_list[0].population.vs,
total: response.world_population_list[0].population.nc
+ response.world_population_list[0].population.tr
+ response.world_population_list[0].population.vs
+ response.world_population_list[0].population.nso,
})
}

View file

@ -1,110 +0,0 @@
import { Cache } from "../cache";
import { ServiceResponse } from "../types";
interface FisuResponse {
result: Record<
string,
{
worldId: number;
vs: number;
nc: number;
tr: number;
ns: number;
}[]
>;
}
const fisuFetchAllWorlds = async (
cache: Cache,
usePS4EU: boolean
): Promise<FisuResponse> => {
const cached = await cache.get<FisuResponse>("fisu");
if (cached) {
// console.log("FISU data cached", cached);
return cached;
}
const [pc, ps4us, ps4eu] = await Promise.all([
fetch(`https://ps2.fisu.pw/api/population/?world=1,10,13,17,19,40`)
.then((res) => res.json<FisuResponse>())
.catch((e) => {
console.error("FISU PC ERROR", e);
return { result: {} } as FisuResponse;
}),
fetch(`https://ps4us.ps2.fisu.pw/api/population/?world=1000`)
.then((res) => res.json<FisuResponse>())
.catch((e) => {
console.error("FISU PS4US ERROR", e);
return { result: {} } as FisuResponse;
}),
usePS4EU
? fetch(`https://ps4eu.ps2.fisu.pw/api/population/?world=2000`)
.then((res) => res.json<FisuResponse>())
.catch((e) => {
console.error("FISU PS4EU ERROR", e);
return { result: {} } as FisuResponse;
})
: ({ result: {} } as FisuResponse),
]).catch((e) => {
console.error("FISU ERROR", e);
return [{ result: {} }, { result: {} }, { result: {} }] as FisuResponse[];
});
// console.log("FISU data fetched", JSON.stringify({ pc, ps4us, ps4eu }));
const response: FisuResponse = {
result: {
...pc.result,
"1000": [ps4us.result[0] as any],
"2000": [ps4eu.result[0] as any],
},
};
return await cache.put("fisu", response);
};
export const fisuFetchWorld = async (
worldID: string,
cache: Cache,
usePS4EU: boolean
): Promise<ServiceResponse<number, any>> => {
if (!usePS4EU && worldID === "2000") {
return {
population: {
total: -1,
nc: -1,
tr: -1,
vs: -1,
},
raw: null,
cachedAt: new Date(0),
};
}
const start = Date.now();
const data: FisuResponse = await fisuFetchAllWorlds(cache, usePS4EU);
const end = Date.now();
const world = data.result[worldID];
if (!world) {
console.error(`fisu: World ${worldID} not found`);
throw new Error(`fisu: World ${worldID} not found`);
}
const { nc, tr, vs, ns } = world[0];
return {
raw: world[0],
population: {
total: vs + nc + tr + ns,
nc,
tr,
vs,
},
cachedAt: new Date(),
timings: {
enter: start,
exit: end,
upstream: end - start,
},
};
};

View file

@ -1,60 +0,0 @@
import { Cache } from "../cache";
import { ServiceResponse } from "../types";
type HonuResponse = {
worldID: number;
timestamp: string;
cachedUntil: string;
total: number;
nc: number;
tr: number;
vs: number;
ns_vs: number;
ns_tr: number;
ns_nc: number;
nsOther: number;
}[];
const honuFetchAllWorlds = async (cache: Cache): Promise<HonuResponse> => {
const cached = await cache.get<HonuResponse>("honu");
if (cached) {
return cached;
}
const req = await fetch(
`https://wt.honu.pw/api/population/multiple?worldID=1&worldID=10&worldID=13&worldID=17&worldID=19&worldID=40&worldID=1000&worldID=2000`
);
return await cache.put("honu", await req.json<HonuResponse>());
};
export const honuFetchWorld = async (
worldID: string,
cache: Cache
): Promise<ServiceResponse<number, any>> => {
const start = Date.now();
const resp = await honuFetchAllWorlds(cache);
const end = Date.now();
const data = resp.find((w) => w.worldID === Number(worldID));
if (!data) {
throw new Error(`honu: World ${worldID} not found`);
}
return {
population: {
total: data.total,
nc: data.nc + data.ns_nc,
tr: data.tr + data.ns_tr,
vs: data.vs + data.ns_vs,
},
raw: data,
cachedAt: new Date(),
timings: {
enter: start,
exit: end,
upstream: end - start,
},
};
};

View file

@ -1,67 +0,0 @@
import { Cache } from "../cache";
import { Population, ServiceResponse } from "../types";
interface SaerroResponse {
data: {
allWorlds: {
id: number;
population: Population<number>;
}[];
};
}
const saerroFetchAllWorlds = async (cache: Cache): Promise<SaerroResponse> => {
const cached = await cache.get<SaerroResponse>("saerro");
if (cached) {
return cached;
}
const req = await fetch(`https://saerro.ps2.live/graphql`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
query: `{
allWorlds {
id
population {
total
nc
tr
vs
}
}
}`,
}),
});
return await cache.put("saerro", await req.json<SaerroResponse>());
};
export const saerroFetchWorld = async (
id: string,
cache: Cache
): Promise<ServiceResponse<number, SaerroResponse["data"]["allWorlds"][1]>> => {
const start = Date.now();
const json: SaerroResponse = await saerroFetchAllWorlds(cache);
const end = Date.now();
const world = json.data.allWorlds.find((w) => w.id === Number(id));
if (!world) {
throw new Error(`saerro: World ${id} not found`);
}
return {
population: world.population,
raw: world,
cachedAt: new Date(),
timings: {
enter: start,
exit: end,
upstream: end - start,
},
};
};

View file

@ -1,84 +0,0 @@
import { Cache } from "../cache";
import { ServiceResponse } from "../types";
// {"world_population_list":[{"world_id":1,"last_updated":1671886187,"total":122,"population":{"VS":49,"NC":45,"TR":28,"NSO":0}},
type SanctuaryResponse = {
world_population_list: {
world_id: number;
last_updated: string;
total: number;
population: {
NC: number;
TR: number;
VS: number;
NSO: number;
};
}[];
};
const sanctuaryFetchAllWorlds = async (
cache: Cache
): Promise<SanctuaryResponse> => {
const cached = await cache.get<SanctuaryResponse>("sanctuary");
if (cached) {
return cached;
}
const req = await fetch(
"https://census.lithafalcon.cc/get/ps2/world_population?c:censusJSON=false"
);
return await cache.put("sanctuary", await req.json<SanctuaryResponse>());
};
export const sanctuaryFetchWorld = async (
worldID: string,
cache: Cache
): Promise<ServiceResponse<number, any>> => {
// No PS4 data nor Jaeger
if (worldID === "1000" || worldID === "2000" || worldID === "19") {
return {
population: {
total: -1,
nc: -1,
tr: -1,
vs: -1,
},
raw: {},
cachedAt: new Date(),
};
}
const start = Date.now();
const resp = await sanctuaryFetchAllWorlds(cache);
const end = Date.now();
const data = resp.world_population_list.find(
(w) => w.world_id === Number(worldID)
);
if (!data) {
throw new Error(`sanctuary: World ${worldID} not found`);
}
if (data.last_updated < (Date.now() / 1000 - 60 * 5).toString()) {
throw new Error(`sanctuary: World ${worldID} is stale`);
}
return {
population: {
total: data.total,
nc: data.population.NC,
tr: data.population.TR,
vs: data.population.VS,
},
raw: data,
cachedAt: new Date(),
timings: {
enter: start,
exit: end,
upstream: end - start,
},
};
};

View file

@ -1,118 +0,0 @@
import { Cache } from "../cache";
import { ServiceResponse } from "../types";
type VoidwellResponse = Array<{
id: number;
name: string;
isOnline: boolean;
onlineCharacters: number;
zoneStates: {
id: number;
name: string;
isTracking: boolean;
lockState: {
state: string;
timestamp: string;
metagameEventId: number;
triggeringFaction: number;
};
population: {
vs: number;
nc: number;
tr: number;
ns: number;
}[];
};
}>;
const voidwellFetchAllWorlds = async (
cache: Cache,
usePS4: boolean
): Promise<VoidwellResponse> => {
const cached = await cache.get<VoidwellResponse>("voidwell");
if (cached) {
return cached;
}
const [pc, ps4us, ps4eu] = await Promise.all([
fetch(`https://api.voidwell.com/ps2/worldstate/?platform=pc`)
.then((res) => res.json<VoidwellResponse>())
.catch((e) => {
console.error("voidwell PC ERROR", e);
return [] as VoidwellResponse;
}),
usePS4
? fetch(`https://api.voidwell.com/ps2/worldstate/?platform=ps4us`)
.then((res) => res.json<VoidwellResponse>())
.catch((e) => {
console.error("voidwell PS4US ERROR", e);
return [] as VoidwellResponse;
})
: [],
usePS4
? fetch(`https://api.voidwell.com/ps2/worldstate/?platform=ps4eu`)
.then((res) => res.json<VoidwellResponse>())
.catch((e) => {
console.error("voidwell PS4EU ERROR", e);
return [] as VoidwellResponse;
})
: [],
]);
// console.log("voidwell data fetched", JSON.stringify({ pc, ps4us, ps4eu }));
const response: VoidwellResponse = [
...pc,
...ps4us,
...ps4eu,
] as VoidwellResponse;
return await cache.put("voidwell", response);
};
// Voidwell is missing Oshur, and since zoneStates are the only way we can get a faction-specific population count,
// we're stuck with not counting faction populations.
export const voidwellFetchWorld = async (
worldID: string,
cache: Cache,
usePS4: boolean
): Promise<ServiceResponse<undefined, VoidwellResponse[0] | null>> => {
if (!usePS4 && (worldID === "1000" || worldID === "2000")) {
// Voidwell doesn't support PS4 well enough.
return {
raw: null,
population: {
total: -1,
nc: undefined,
tr: undefined,
vs: undefined,
},
cachedAt: new Date(0),
};
}
const start = Date.now();
const data = await voidwellFetchAllWorlds(cache, usePS4);
const end = Date.now();
const world = data.find((w) => w.id === Number(worldID));
if (!world) {
throw new Error(`voidwell: World ${worldID} not found`);
}
return {
raw: world,
population: {
total: world.onlineCharacters,
nc: undefined,
tr: undefined,
vs: undefined,
},
cachedAt: new Date(),
timings: {
enter: start,
exit: end,
upstream: end - start,
},
};
};

41
src/types.rs Normal file
View file

@ -0,0 +1,41 @@
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct AllResponse {
pub worlds: Vec<Response>,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct Response {
pub id: i32,
pub average: i32,
pub factions: Factions,
pub services: HashMap<String, Population>,
#[serde(default)]
pub cached_at: DateTime<Utc>,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct Factions {
pub nc: i32,
pub tr: i32,
pub vs: i32,
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, Default)]
pub struct Population {
pub nc: i32,
pub tr: i32,
pub vs: i32,
#[serde(default)]
pub total: i32,
}
impl Population {
pub fn total(&self) -> i32 {
self.nc + self.tr + self.vs
}
}

View file

@ -1,80 +0,0 @@
export interface Population<T extends number | undefined> {
total: number;
nc: T;
tr: T;
vs: T;
}
export interface ServiceResponse<PT extends number | undefined, Raw> {
population: Population<PT>;
raw: Raw;
cachedAt: Date;
timings?: {
enter: number;
upstream: number;
exit: number;
};
}
export interface Env {
CACHE: KVNamespace;
DISABLE_HONU: "1" | undefined;
DISABLE_FISU: "1" | undefined;
DISABLE_SAERRO: "1" | undefined;
DISABLE_VOIDWELL: "1" | undefined;
DISABLE_SANCTUARY: "1" | undefined;
DISABLE_CACHE: "1" | undefined;
VOIDWELL_USE_PS4: "1" | undefined;
FISU_USE_PS4EU: "1" | undefined;
}
export type OnePayload = {
id: number;
average: number;
factions: {
nc: number;
tr: number;
vs: number;
};
services: {
saerro: number | null;
fisu: number | null;
honu: number | null;
voidwell: number | null;
sanctuary: number | null;
};
};
export type DebugPayload = {
raw: {
saerro: any;
fisu: any;
honu: any;
voidwell: any;
sanctuary: any;
};
timings: {
saerro: any;
fisu: any;
honu: any;
voidwell: any;
sanctuary: any;
};
lastFetchTimes: {
saerro?: Date;
fisu?: Date;
honu?: Date;
voidwell?: Date;
sanctuary?: Date;
};
};
export type Flags = {
disableHonu: boolean;
disableFisu: boolean;
disableSaerro: boolean;
disableVoidwell: boolean;
disableSanctuary: boolean;
voidwellUsePS4: boolean;
fisuUsePS4EU: boolean;
};

View file

@ -1,105 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [
"es2021"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"jsx": "react" /* Specify what JSX code is generated. */,
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "es2022" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
"types": [
"@cloudflare/workers-types"
] /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true /* Enable importing .json files */,
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */,
"checkJs": false /* Enable error reporting in type-checked JavaScript files. */,
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
"noEmit": true /* Disable emitting files from a compilation. */,
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
"allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
// "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View file

@ -1,22 +0,0 @@
name = "agg-population"
main = "src/index.ts"
compatibility_date = "2022-12-19"
[route]
pattern = "agg.ps2.live/population*"
zone_name = "ps2.live"
[vars]
DISABLE_HONU = "0"
DISABLE_FISU = "0"
DISABLE_SAERRO = "0"
DISABLE_VOIDWELL = "0"
DISABLE_KIWI = "0"
DISABLE_CACHE = "0"
VOIDWELL_USE_PS4 = "0"
FISU_USE_PS4EU = "1"
[[kv_namespaces]]
binding = "CACHE"
id = "4b3b7f55a14c40cc84824fd0a3f58ff4"
preview_id = "3c0ce37f56be4bd998fc6f36a7a685dc"