# 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](/developers/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`.

```js
// 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:

```js
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`.

```python
# 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:

```python
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_uids` and `player.discord_ids` as columns so you can query them, and the whole record as JSON for everything else.
- Save `next_cursor` after every page, not just at the end, so a crash mid-sync doesn't start over.
