# source

A multipurpose Discord bot (antinuke, economy/gambling, roleplay, moderation).
Lives in this repo for convenience only - it has no relationship to the
rotten.network website, backend, or database. Its SQLite file
(`data/bot.db`) is separate from `backend/data/`.

```
bot/
├── src/
│   ├── index.js              # entry point, logs in, wires up handlers
│   ├── deploy-commands.js    # registers slash commands with Discord
│   ├── config.js             # env var loading
│   ├── handlers/             # auto-loaders for commands/ and events/
│   ├── commands/
│   │   ├── antinuke/         # /antinuke setup, whitelist, punishment config
│   │   ├── economy/          # /balance /daily /coinflip /slots
│   │   ├── roleplay/         # /roleplay hug|slap|kiss|pat|poke|highfive
│   │   ├── moderation/       # /ban /kick /warn /warnings
│   │   ├── utility/          # /ping /help
│   │   └── vc/                # ,vcstats ,vcleaderboard ,vcglobal (prefix commands)
│   ├── events/                # ready, interactionCreate, messageCreate, antinuke + logging listeners
│   ├── services/              # permissions, antinuke, economy, logs, guildAccess, invites, vcTime, globalBans, dashboardUsers
│   └── db/                    # better-sqlite3 connection + schema.sql
├── dashboard/                  # separate Express app - web admin panel (see below)
│   ├── server.js
│   ├── oauth.js               # Discord OAuth2 (identify scope)
│   ├── session.js             # signed cookie session, same pattern as backend/src/session.js
│   └── views.js               # server-rendered HTML, no build step
└── data/                       # bot.db (gitignored) - shared by the bot process and the dashboard
```

## Setup

1. `cd bot`
2. `cp .env.example .env` and fill in `DISCORD_TOKEN`, `DISCORD_CLIENT_ID`,
   and optionally `DISCORD_GUILD_ID` (for instant command registration while
   developing), `OWNER_IDS` (comma-separated, exempt from antinuke),
   `JOIN_LOG_CHANNEL_ID` (see Guild whitelist below), and `PREFIX`
   (defaults to `,`).
3. In the [Discord Developer Portal](https://discord.com/developers/applications) →
   your app → Bot, enable the **Server Members Intent** and
   **Message Content Intent** - both are privileged and required for the
   logging features below (member joins/updates, message content) and for
   prefix commands to read message text at all.
4. `npm install`
5. `npm run deploy` - registers slash commands with Discord.
6. `npm start` - or `npm run dev` for auto-restart on file changes.

## Adding a command

Two shapes, both auto-loaded from `src/commands/<category>/`:

- **Slash**: `{ data: SlashCommandBuilder, execute(interaction) }` - run
  `npm run deploy` again after adding one to register it with Discord.
- **Prefix**: `{ name, aliases?, description, syntax, example, execute(message, args, client) }`
  - picked up on next boot, no separate registration step. Use
  `embeds.commandHelpEmbed({ client, name, description, syntax, example })`
  for a usage card matching the reference bot's "Command: x / description /
  Syntax + Example" layout when args are missing or wrong.

## Permission model

Two separate walls, deliberately not both tied to Discord's own permission
system:

- **Moderation** (ban/kick/warn, when added) - gated by the actual Discord
  permission for that action (`Ban Members`, `Kick Members`, etc.), via each
  command's `setDefaultMemberPermissions`. Ordinary Discord permission wall.
- **Antinuke / admin** - gated by a bot-local rank in `services/permissions.js`,
  independent of Discord's Administrator permission. This means a hijacked
  "Administrator" role can't touch antinuke config - only three ranks can:
  - **Owner** - the single user ID in `OWNER_ID` (`.env`). That's you. The
    only rank that can appoint or remove **Whitelist Admins**, or manage the
    guild whitelist below.
  - **Server owner** - auto-detected via `guild.ownerId`, no manual setup
    per server. Whoever actually owns a Discord server gets the same
    antinuke/log-config trust as a whitelist admin in that server, and is
    automatically exempt from antinuke punishment there.
  - **Whitelist Admin** - appointed per-guild by the owner
    (`permissions.addWhitelistAdmin`) for anyone else you want trusted with
    antinuke config who isn't the server owner.

  All three can change antinuke settings (`antinuke.setSettings`) and
  grant/revoke the separate antinuke-exemption whitelist
  (`antinuke.addWhitelist` / `removeWhitelist`) - the list of users antinuke
  will never punish even if they trip it.

Every antinuke/log service function that mutates state takes the full
`guild` object (not just its id - needed for owner auto-detection) plus
`actorId`, and returns `{ ok: false, reason }` if the caller isn't
authorized. The wall lives in the service layer, not just in command
permission flags, so it holds regardless of what commands get built on top
of it later.

## Antinuke

Listens for channel deletion, role deletion, mass bans, and webhook creation.
If the executor isn't the owner or on the exemption whitelist, it gets
punished per the configured action (strip all roles, or ban) and the action
is logged to the configured log channel. Off by default.

Requires the bot to have `Ban Members`, `Manage Roles` (above the roles it
strips), and `View Audit Log` permissions.

## Guild whitelist (which servers the bot can be in)

Access is whitelist-only, controlled solely by the owner
(`services/guildAccess.js`). On `guildCreate` (the bot joining a server), it
checks `guild_access`; if the guild isn't listed, it logs the attempt -
server name/id, owner, member count, created date - to
`JOIN_LOG_CHANNEL_ID` and immediately leaves. Whitelisted joins get logged
the same way but the bot stays. There's no command yet to manage the
whitelist - call `guildAccess.addGuild(ownerId, guildId)` directly (e.g. from
a REPL or a future `/whitelist-server` command) until one's built.

## Logging

Per-guild log channels are configured via `services/logs.js`
(`logs.setLogChannel(guildId, actorId, type, channelId)` - gated to the
owner/whitelist admins, same as antinuke). No slash command wires this up
yet; it's the same pattern as the guild whitelist above. Log types:

| Type | Channel example | Covers |
|------|------------------|--------|
| `mod` | `mod-log` | Bans, unbans, kicks, timeouts - always shows executor + target, whether done via a command or manually in Discord |
| `members` | `members-log` | Joins (+ inviter, when resolvable), leaves, avatar/banner/username changes |
| `msg` | `msg-log` | Edits (before/after + jump link), deletes (who deleted + message owner) |
| `role` | `role-logs` | Role create/update (permission diff)/delete + who did it |
| `channel` | `channel-logs` | Channel create/update/delete + who did it |
| `invite` | `invite-logs` | Invite create/delete; join events show which invite (and inviter) was used |
| `vc` | `vc-logs` | Voice join/leave/move, mute/deafen (self + server), stream start/stop |
| `server` | `server-logs` | Server name/icon/banner/splash/description changes |

`integration-logs` and `vanity` are intentionally not implemented.

Every log embed carries the bot's avatar as the author icon (via
`utils/embeds.js`'s `baseEmbed`) except lookup-style commands like
`/avatar`, `/userinfo`, `/banner` (not built yet) which should pass
`basic: true` to skip it, since those are about the subject's image/info,
not the bot's.

Invite-use attribution (`services/invites.js`) is in-memory only - it's
reprimed from Discord on `ready` and on joining a new guild, so a bot
restart briefly loses the ability to attribute the very next join until it
re-syncs (happens automatically, just noting the gap).

## VC time tracking

`services/vcTime.js` tracks every voice session as a row in `vc_sessions`
(guild, user, start, end, duration) - totals, leaderboards, and longest
sessions are all derived from that table with `SUM`/`ORDER BY`, no separate
counter to keep in sync. A session starts when `voiceStateUpdate` sees a
user go from no channel to a channel, and ends the same way in reverse;
moving between channels doesn't reset it. In-progress sessions count toward
stats too (added on top of the DB sum at query time), so `,vcstats` mid-call
still reflects live time. Like invites, active sessions are in-memory only -
a bot restart loses whatever session was in progress (already-closed
sessions are safe in the DB), so `ready` re-opens sessions for anyone
currently in voice when it restarts.

Commands (prefix, `src/commands/vc/`):

- `,vcstats (user)` - total VC time in this server + up to 3 longest
  sessions. Defaults to yourself.
- `,vcleaderboard` - top 10 by VC time in this server.
- `,vcglobal (user)` - no argument: top 10 leaderboard summed across every
  server the bot is in; with a user: that person's VC time summed across
  every server.

## Web dashboard

A standalone Express app (`dashboard/server.js`), not part of the bot's
gateway process - it only needs the SQLite file (`data/bot.db`, same one
the bot writes to) and the bot token for REST calls. Run it separately:
`npm run dashboard` (or `npm run dashboard:dev`).

**Login** (`/login`, `/callback`) is Discord OAuth2 with the `identify`
scope - the same app as `DISCORD_CLIENT_ID`/`DISCORD_TOKEN`, just needs its
**Client Secret** added as `DISCORD_CLIENT_SECRET`, and the callback URL
(`DASHBOARD_REDIRECT_URI`) registered on that app's OAuth2 page in the
Developer Portal. *Anyone* can log in - this is intentional, mirrors the
main site's `logins`/`admin.php` pattern: every login records the account +
IP (`dashboard_logins`, `services/dashboardUsers.js`), purely so the owner
can spot alt accounts sharing an IP before issuing a global ban.

**`/admin`** is gated to `OWNER_ID` only (`permissions.isOwner`) - everyone
else gets a 403. It lists every account that's ever logged in, their known
IPs, and a per-row **Global ban** / **Lift global ban** form. IPs are
real in the HTML but rendered with `filter: blur(...)` by default
(`dashboard/views.js`) and sharp on `:hover` - a glance at the table shows
nothing, hovering a specific cell reveals it.

**Global ban** (`services/globalBans.js`) is owner-only and does two
things: bans the user via REST in every guild listed in `guild_access`
*right now*, and records it in `global_bans` so `guildMemberAdd` auto-bans
them again if they try to evade by rejoining or joining a different
whitelisted server later. "Lift global ban" reverses both.
