# Build your own Discord bot

The [SentinelShare bot](/developers/discord-bot) covers checking, filing and lifting bans, and already [warns your staff](/developers/discord-bot#3-staff-alerts-and-new-ban-posts) when a banned player joins. If your community wants something of its own, such as adding these checks to a bot you already run, call the admin panel API from your bot.

Both examples below do two things:

1. **On member join:** look up the new member's Discord ID and post in your staff channel if they have an active ban anywhere on the network.
2. **`/check member`:** let staff check anyone on demand. Only members with Timeout Members permission see the command.

You need a community API key ([how to get one](/developers/admin-panel-api#get-a-key)). Keep it in an environment variable on the machine that runs the bot.

## Set up the Discord application

1. In the [Discord Developer Portal](https://discord.com/developers/applications), create an application and add a bot.
2. Under **Bot → Privileged Gateway Intents**, turn on **Server Members Intent**. Without it your bot never sees members join.
3. Invite it with the `bot` and `applications.commands` scopes and the **Send Messages** and **Embed Links** permissions.
4. Right-click your staff channel and your server and **Copy ID** (turn on Developer Mode in Discord's Advanced settings first).

Environment variables used below:

```bash
DISCORD_TOKEN=your-bot-token
SENTINEL_KEY=ssk_your_key_here
GUILD_ID=your-server-id
STAFF_CHANNEL_ID=your-staff-channel-id
```

## discord.js (Node.js 18+)

`npm install discord.js`, then `node bot.mjs`.

```js
// bot.mjs: SentinelShare checks for your Discord
import {
  Client, EmbedBuilder, Events, GatewayIntentBits, MessageFlags,
  PermissionFlagsBits, SlashCommandBuilder,
} from "discord.js";

const API = "https://sentinelshare.com/api/v1/integration";

async function lookup(params) {
  const url = new URL(API + "/lookup");
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
  const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SENTINEL_KEY}` } });
  if (!res.ok) throw new Error(`SentinelShare lookup failed: HTTP ${res.status}`);
  return res.json();
}

function banEmbed(user, result) {
  const active = result.bans.filter((b) => b.status === "active");
  return new EmbedBuilder()
    .setTitle(`${user.username}: ${active.length} active SentinelShare ban(s)`)
    .setURL(result.url)
    .setColor(0xeb6262)
    .addFields(
      active.slice(0, 5).map((b) => ({
        name: `${b.type} · ${b.verification.state.replaceAll("_", " ").toLowerCase()}`,
        value: `${b.filed_by.community}: ${b.reason ?? "No reason given"}`.slice(0, 1024),
      })),
    );
}

const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] });

client.once(Events.ClientReady, async (c) => {
  const check = new SlashCommandBuilder()
    .setName("check")
    .setDescription("Check a member on SentinelShare")
    .addUserOption((o) => o.setName("member").setDescription("Who to check").setRequired(true))
    .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers);
  await c.application.commands.create(check, process.env.GUILD_ID);
  console.log(`Ready as ${c.user.username}`);
});

// 1. Warn staff when a banned player joins
client.on(Events.GuildMemberAdd, async (member) => {
  try {
    const result = await lookup({ discord_id: member.id });
    if (!result.found || !result.bans.some((b) => b.status === "active")) return;
    const channel = await client.channels.fetch(process.env.STAFF_CHANNEL_ID);
    await channel.send({ content: `${member} just joined.`, embeds: [banEmbed(member.user, result)] });
  } catch (err) {
    console.error("Join check failed:", err);
  }
});

// 2. /check member
client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand() || interaction.commandName !== "check") return;
  await interaction.deferReply({ flags: MessageFlags.Ephemeral });
  const user = interaction.options.getUser("member", true);
  try {
    const result = await lookup({ discord_id: user.id });
    if (!result.found || !result.bans.some((b) => b.status === "active")) {
      await interaction.editReply(`${user.username} has no active bans on SentinelShare.`);
    } else {
      await interaction.editReply({ embeds: [banEmbed(user, result)] });
    }
  } catch {
    await interaction.editReply("SentinelShare didn't answer. Try again in a minute.");
  }
});

client.login(process.env.DISCORD_TOKEN);
```

## discord.py (Python 3.9+)

`pip install discord.py`, then `python bot.py`.

```python
# bot.py: SentinelShare checks for your Discord
import os

import aiohttp
import discord
from discord import app_commands

API = "https://sentinelshare.com/api/v1/integration"
GUILD = discord.Object(id=int(os.environ["GUILD_ID"]))
STAFF_CHANNEL_ID = int(os.environ["STAFF_CHANNEL_ID"])


async def lookup(**params) -> dict:
    headers = {"Authorization": f"Bearer {os.environ['SENTINEL_KEY']}"}
    async with aiohttp.ClientSession(headers=headers) as session:
        async with session.get(f"{API}/lookup", params=params, timeout=aiohttp.ClientTimeout(total=15)) as res:
            res.raise_for_status()
            return await res.json()


def active_bans(result: dict) -> list[dict]:
    return [b for b in result["bans"] if b["status"] == "active"] if result["found"] else []


def ban_embed(user: discord.abc.User, result: dict) -> discord.Embed:
    active = active_bans(result)
    embed = discord.Embed(
        title=f"{user.name}: {len(active)} active SentinelShare ban(s)",
        url=result["url"],
        color=0xEB6262,
    )
    for ban in active[:5]:
        state = ban["verification"]["state"].replace("_", " ").lower()
        embed.add_field(
            name=f"{ban['type']} · {state}",
            value=f"{ban['filed_by']['community']}: {ban['reason'] or 'No reason given'}"[:1024],
            inline=False,
        )
    return embed


class Bot(discord.Client):
    def __init__(self):
        intents = discord.Intents.default()
        intents.members = True  # needs Server Members Intent in the Developer Portal
        super().__init__(intents=intents)
        self.tree = app_commands.CommandTree(self)

    async def setup_hook(self):
        self.tree.copy_global_to(guild=GUILD)
        await self.tree.sync(guild=GUILD)


bot = Bot()


# 1. Warn staff when a banned player joins
@bot.event
async def on_member_join(member: discord.Member):
    try:
        result = await lookup(discord_id=str(member.id))
    except aiohttp.ClientError as exc:
        print("Join check failed:", exc)
        return
    if active_bans(result):
        channel = bot.get_channel(STAFF_CHANNEL_ID)
        await channel.send(f"{member.mention} just joined.", embed=ban_embed(member, result))


# 2. /check member
@bot.tree.command(name="check", description="Check a member on SentinelShare")
@app_commands.default_permissions(moderate_members=True)
async def check(interaction: discord.Interaction, member: discord.User):
    await interaction.response.defer(ephemeral=True)
    try:
        result = await lookup(discord_id=str(member.id))
    except aiohttp.ClientError:
        await interaction.followup.send("SentinelShare didn't answer. Try again in a minute.", ephemeral=True)
        return
    if active_bans(result):
        await interaction.followup.send(embed=ban_embed(member, result), ephemeral=True)
    else:
        await interaction.followup.send(f"{member.name} has no active bans on SentinelShare.", ephemeral=True)


bot.run(os.environ["DISCORD_TOKEN"])
```

## Ideas to take further

- Check by Arma UID too: if your whitelist or verification bot knows a member's Arma UID, pass `arma_uid` as well as `discord_id`.
- Only alert on verified bans: check `ban.verification.state` is `PEER_VERIFIED` or `STAFF_VERIFIED`.
- Post every new network ban to a channel: poll `GET /bans` with a saved cursor every few minutes (see [Node.js and Python](/developers/node-and-python)) and send an embed per new record. The official bot already does this with `/setup notifications`.
- Remember each key allows 300 requests a minute. A busy server's join wave fits easily; don't look up every member of a large server at once.
- To check everyone already in your server, use the official bot's `/bans scan`: it checks 1,000 members per request and saves a report on the website. See [Scan your Discord](/developers/discord-bot#4-scan-your-discord-for-banned-members).
