Node.js and Python
Copy-paste examples for the two things most panels need: a sync loop that keeps a local copy of every ban, and a lookup when a player joins. They use only built-in libraries plus requests for Python. Swap the saveBan / save_ban functions for your own database.
See Admin panel API for every field and error.
Node.js (18 or newer)
Set SENTINEL_KEY in the environment, then run with node sentinel-sync.mjs.
// sentinel-sync.mjs: keeps a local copy of every SentinelShare ban
import { readFile, writeFile } from "node:fs/promises";
const API = "https://sentinelshare.com/api/v1/integration";
const HEADERS = { Authorization: `Bearer ${process.env.SENTINEL_KEY}` };
const CURSOR_FILE = "./sentinel-cursor.txt";
async function api(path, params = {}) {
const url = new URL(API + path);
for (const [k, v] of Object.entries(params)) if (v) url.searchParams.set(k, v);
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(url, { headers: HEADERS });
if (res.status === 429) {
const wait = Number(res.headers.get("retry-after") ?? 10);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
if (!res.ok) throw new Error(`SentinelShare ${path}: HTTP ${res.status} ${await res.text()}`);
return res.json();
}
throw new Error("SentinelShare: still rate limited");
}
async function saveBan(ban) {
// Replace with an upsert into your database, keyed by ban.id
console.log(ban.status, ban.type, ban.player.alias, ban.verification.state);
}
export async function syncOnce() {
let cursor = await readFile(CURSOR_FILE, "utf8").catch(() => null);
let page;
do {
page = await api("/bans", { since: cursor, limit: 200 });
for (const ban of page.items) await saveBan(ban);
if (page.next_cursor) {
cursor = page.next_cursor;
await writeFile(CURSOR_FILE, cursor);
}
} while (page.has_more);
}
export async function lookup(identifiers) {
// identifiers: { arma_uid?, discord_id?, ip?, name? }
return api("/lookup", identifiers);
}
// Sync now, then every five minutes
await syncOnce();
setInterval(() => syncOnce().catch(console.error), 5 * 60 * 1000);A join check with the same helper:
const result = await lookup({ arma_uid: "f91f26e6-aea4-4822-b26b-c9135f3c3b66" });
const active = result.found ? result.bans.filter((b) => b.status === "active") : [];
const verified = active.filter((b) => ["PEER_VERIFIED", "STAFF_VERIFIED"].includes(b.verification.state));
if (verified.length) console.log(`Verified network ban: ${result.url}`);
else if (active.length) console.log(`Unverified network ban, flag for staff: ${result.url}`);Python (3.9 or newer)
pip install requests, set SENTINEL_KEY, then python sentinel_sync.py.
# sentinel_sync.py: keeps a local copy of every SentinelShare ban
import os
import time
from pathlib import Path
import requests
API = "https://sentinelshare.com/api/v1/integration"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['SENTINEL_KEY']}"
CURSOR_FILE = Path("sentinel-cursor.txt")
def api(path: str, **params) -> dict:
params = {k: v for k, v in params.items() if v}
for _ in range(5):
res = SESSION.get(API + path, params=params, timeout=15)
if res.status_code == 429:
time.sleep(int(res.headers.get("Retry-After", 10)))
continue
res.raise_for_status()
return res.json()
raise RuntimeError("SentinelShare: still rate limited")
def save_ban(ban: dict) -> None:
# Replace with an upsert into your database, keyed by ban["id"]
print(ban["status"], ban["type"], ban["player"]["alias"], ban["verification"]["state"])
def sync_once() -> None:
cursor = CURSOR_FILE.read_text().strip() if CURSOR_FILE.exists() else None
while True:
page = api("/bans", since=cursor, limit=200)
for ban in page["items"]:
save_ban(ban)
if page["next_cursor"]:
cursor = page["next_cursor"]
CURSOR_FILE.write_text(cursor)
if not page["has_more"]:
break
def lookup(**identifiers) -> dict:
"""Pass any of arma_uid, discord_id, ip, name."""
return api("/lookup", **identifiers)
if __name__ == "__main__":
while True:
try:
sync_once()
except requests.RequestException as exc:
print("sync failed:", exc)
time.sleep(300)A join check:
result = lookup(arma_uid="f91f26e6-aea4-4822-b26b-c9135f3c3b66")
active = [b for b in result["bans"] if b["status"] == "active"] if result["found"] else []
verified = [b for b in active if b["verification"]["state"] in ("PEER_VERIFIED", "STAFF_VERIFIED")]
if verified:
print("Verified network ban:", result["url"])
elif active:
print("Unverified network ban, flag for staff:", result["url"])Storing bans
Whatever database you use, the pattern is the same:
- One row per ban, keyed by
id. Insert or replace on every record you receive. - Keep
status,type,verification.state,player.arma_uidsandplayer.discord_idsas columns so you can query them, and the whole record as JSON for everything else. - Save
next_cursorafter every page, not just at the end, so a crash mid-sync doesn't start over.