Royal PB Bot

Royal PB Bot

A Discord slash-command bot for tracking clan personal bests (PBs) in Old School RuneScape. Members submit kill screenshots, staff approve them in a review queue, and the bot maintains a navigable leaderboard channel, with optional blert.io verification for supported bosses, record-break pings, monthly digests, and an admin toolset.

Python 3.11+ discord.py Google Sheets backend Single file No database / web server / build step
SetupGoogle, Discord, .env, first run Member commands/pb submit, top, history, lookup Admin commands/pbadmin toolset Sheets schemaTabs and columns TroubleshootingCommon problems and fixes

What it does

Requirements

Python dependencies

discord.py>=2.4
gspread>=6.0
openpyxl>=3.1
python-dotenv>=1.0
aiohttp            # ships with discord.py, listed for clarity

Install:

pip install discord.py gspread openpyxl python-dotenv

Setup

Google service account + sheet

  1. In Google Cloud Console, create a project and enable the Google Sheets API.
  2. Create a service account and download its JSON key. Save it next to pbbot.py as google-key.json (the filename is configurable; see Configuration file).
  3. Create a Google Sheet. Copy its ID from the URL: https://docs.google.com/spreadsheets/d/<SHEET_ID>/edit.
  4. Share the sheet with the service account's email (found in the JSON under client_email) as an Editor.
  5. The bot creates the config, bosses, categories, and submissions tabs on first run. You do not need to pre-build them. The submissions_archive tab is created on first /pbadmin prune.

Discord application + invite

  1. Create an application at the Discord Developer Portal. Add a Bot user and copy its token.
  2. Under OAuth2 → URL Generator, select scopes bot and applications.commands. Give the bot at minimum these permissions:
    • View Channels, Send Messages, Embed Links, Attach Files, Read Message History
    • Manage Messages (for leaderboard refresh: deletes + re-sends)
    • Use External Emojis (optional, for medal emojis)
  3. Open the generated URL and invite the bot to your clan server.
  4. Note the guild ID of your clan server (Developer Mode → right-click server → Copy ID). Setting DEV_GUILD_ID makes slash commands sync instantly during setup.

Configuration file

Create a .env file next to pbbot.py:

DISCORD_TOKEN=<bot token>
GOOGLE_KEYFILE=google-key.json
SHEET_ID=<your sheet id>
DEV_GUILD_ID=<your clan guild id>   # optional, for instant slash sync

DEV_GUILD_ID is optional. If set, slash commands sync to that guild on startup (instant). If unset, they sync globally (can take up to an hour to propagate). Either way commands work in every server the bot is in.

First run

python pbbot.py

On startup the bot:

  1. Opens the Google Sheet and creates the four base tabs if missing, seeds the boss/category list from the built-in seed, and seeds the Hallowed Sepulchre meme banner URL into the config tab. This is safe to run on every restart: it only adds missing bosses/categories, never removes anything (removals are explicit, via /pbadmin boss remove).
  2. Registers persistent views (the Approve/Reject buttons on old review messages and the Select menu on the old leaderboard detail message keep working after a restart).
  3. Syncs slash commands (guild if DEV_GUILD_ID is set, else global).
  4. Starts the background loops: cache warmup every ~45s and the monthly digest checker every 15 minutes.

You'll see five print() lines on startup. That is the bot's only logging. There is no log file.

Post-startup checklist

After the bot is online and you see the startup lines, run these once:

  1. /pbadmin set-channel submit <your submit channel>: binds the member submit channel.
  2. /pbadmin set-channel review <your staff review channel>: binds the staff review queue.
  3. /pbadmin set-channel announce <your announcements channel>: binds the approval announcements channel.
  4. /pbadmin set-channel leaderboard <your leaderboard channel>: binds and immediately posts the Master Board + detail message + Hallowed Sepulchre meme banner.
  5. /pbadmin set-role <your staff role>: binds the staff role (used for review-queue gating and record-break pings).
  6. Restrict the /pbadmin command group to your staff role in Server Settings → Integrations → Royal PB Bot.
  7. Pin the how-to message in the submit channel manually: copy the text from Submit channel pinned message below, post it, and right-click → Pin.
  8. (Optional) /pbadmin record-ping <role> <days>: set the role to ping when a record standing longer than days falls.

Commands

There are two command groups: /pb (everyone) and /pbadmin (admins). The admin group also has subgroups /pbadmin boss and /pbadmin category.

Member commands: /pb

/pb submit

boss         Boss (autocomplete)                          required
category     Category (autocomplete, after boss)          required
time         Kill time e.g. 2:31.40, or a level e.g. 167  required
proof        Screenshot of the kill (image attachment)    required
teammate_2 … teammate_8   Teammates (you are #1 automatically)
blert_url    blert.io recording link (blert-supported bosses only)
notes        Anything staff should know

Must be run in the submit channel. Gives an ephemeral receipt (only you see it; the channel stays clean), posts the screenshot to the review queue with Approve/Reject buttons. The bot does not DM you on decision: watch the announcements channel for approved records (an admin will tell you directly if you need to re-submit). A 5-minute edit window lets you resubmit to replace a pending submission (the old review message is deleted). A duplicate guard blocks re-submitting the exact same boss+category+time+roster after approval. The submit channel's pinned how-to message (pinned by an admin) at the top explains the fields; see below for the text to pin.

Submit channel pinned message

The bot does not create or manage a pinned message in the submit channel. An admin posts the text below and pins it manually (right-click the message → Pin). This keeps the instructions at the top of the channel so members landing there always see how to submit. Copy the block verbatim:

**How to submit a personal best**

Use the `/pb submit` command in this channel. You'll get a private (ephemeral) reply confirming it's in the staff review queue — the channel itself stays clean.

**Fields:**
• **boss** — start typing and pick from the list (e.g. `Theatre of Blood`)
• **category** — pick after choosing the boss (e.g. `Solo`, `Duo`)
• **time** — kill time for time-based categories. Formats: `2:31.40`, `1:02:31`, `5m19s`, `90s`. Use a plain integer for level-based categories (e.g. `167`)
• **proof** — screenshot of the kill (required, image only)
• **teammate_2 … teammate_8** — for raids; you're #1 automatically
• **blert_url** — blert.io recording link, only for blert-supported challenges
• **notes** — anything staff should know

The bot does not DM you on decision — watch the announcements channel for approved records (an admin will tell you directly if you need to re-submit).

Tips: re-submitting within 5 minutes replaces your pending submission (handy for typos). An identical time + roster to an existing approved record is refused as a duplicate.

/pb top

boss   Boss (autocomplete)   required

Posts a public top-5 ranked embed for that boss (visible to the whole channel). Medal emojis on spots 1–3 (🏆 🥈 🥉).

/pb history

boss   Boss (autocomplete)   required

Posts a public record progression for that boss with relative timestamps ("2 days ago", "1 hour ago"). The current champion is crowned with 🏆.

/pb lookup

player   Discord user (autocomplete shows current members)   required

Posts a public embed of that player's best approved record per category, with their clan-wide rank. Footer shows their Discord user ID. Categories where they are #1 get 🏆.

Admin commands: /pbadmin

All /pbadmin commands are ephemeral (only the caller sees the response). By default anyone with the bot's default permissions can run them; restrict the /pbadmin group in Server Settings → Integrations → Royal PB Bot to your staff role after setup.

Setup

CommandWhat it does
/pbadmin set-channel kind channelBind a channel. kind is one of submit (member-facing, clean, no per-submission log), review (staff queue), announce (approved records), leaderboard (navigable top-3 channel). Setting the leaderboard channel immediately posts the Master Board + detail message + Hallowed Sepulchre meme banner. The submit channel does not get a bot-managed pinned message; an admin pins the how-to message manually (see Submit channel pinned message).
/pbadmin set-role roleBind the staff role (used for review-queue gating and record-break pings).
/pbadmin record-ping role daysSet the role to ping when a record standing longer than days falls. Omit role to disable the ping.
/pbadmin showPrint the current channel bindings, staff role, and record-ping config.
/pbadmin statusBot health: cache warmth + age, boss/category/submission row counts, channel config, uptime.

Records management

CommandWhat it does
/pbadmin revert submission_id reasonReverts an approved submission. Posts an announcement, removes the entry from the leaderboard (or updates if a new #1 takes over), and marks the row reverted.
/pbadmin prune [include_reverted] [dry_run]Archives rejected (and optionally reverted) rows from submissions to submissions_archive, then deletes them from the active tab. dry_run=True previews the count without touching the sheet. Idempotent: re-running after a partial failure does not duplicate (it dedups against existing archive IDs).
/pbadmin refreshDeletes and re-sends the leaderboard Master Board + detail message. The meme banner survives (it is snapshotted). Use after manual sheet edits to force a re-render.
/pbadmin migrateRe-runs the seed sync (adds any new built-in bosses/categories, no removals). Run it after a bot update that adds a boss to the seed list, then /pbadmin refresh to re-render.
/pbadmin exportUploads the entire sheet (all tabs) as an .xlsx attachment. One Excel sheet per Google Sheets tab.

Boss management: /pbadmin boss

CommandWhat it does
/pbadmin boss add name [aliases] [blert_supported]Add a boss to the pickers. aliases is a comma-separated list for autocomplete matching. Set blert_supported=True if blert.io can verify runs.
/pbadmin boss edit boss [name] [aliases] [blert_supported] [image_url]Edit fields of an existing boss. image_url shows on the leaderboard detail page for that boss.
/pbadmin boss remove bossSoft-delete: removes from pickers, keeps history. (Hard removal is never done; past submissions still reference the boss row.)
/pbadmin boss listLists all active bosses with their blert support status.

Category management: /pbadmin category

CommandWhat it does
/pbadmin category add boss name [size] [record_type]Add a category (e.g. Solo, Duo, Highest Delve Level). size is 1–8 (for team-size categories). record_type is time (default, stores ms) or level (stores a level number, renders as "Level N").
/pbadmin category remove boss nameSoft-delete a category. History intact.
/pbadmin category list bossLists active categories for a boss, sorted by size then name.

Google Sheets schema

The bot owns four tabs (plus a fifth created on demand). All tabs are created on first run; you do not need to pre-build them. You can browse and edit the sheet directly at any time. The bot caches reads for up to 60 seconds, so in-sheet edits appear within a minute or after /pbadmin refresh.

config: key/value pairs

keyvalueset by
submit_channel_idDiscord channel ID/pbadmin set-channel
review_channel_idDiscord channel ID/pbadmin set-channel
announce_channel_idDiscord channel ID/pbadmin set-channel
leaderboard_channel_idDiscord channel ID/pbadmin set-channel
staff_role_idDiscord role ID/pbadmin set-role
record_ping_role_idDiscord role ID/pbadmin record-ping
record_ping_min_daysinteger (e.g. 30)/pbadmin record-ping
last_digest_monthYYYY-MMthe bot, after each monthly digest
sepulchre_meme_titletextseeded on first run; editable in-sheet afterward
sepulchre_meme_credittextseeded on first run; editable in-sheet afterward
sepulchre_meme_banner_urlimage URLseeded on first run (empty by default); editable in-sheet afterward

bosses

columnmeaning
idauto-incremented integer
nameboss name
aliasescomma-separated autocomplete aliases
blert_supported1 if blert.io can verify, else 0
group_namereserved (currently unused)
active1 = shown in pickers, 0 = soft-deleted
image_urloptional image shown on the leaderboard detail page

categories

columnmeaning
idauto-incremented integer
boss_idforeign key to bosses.id
namecategory name (e.g. Solo, Duo, Highest Delve Level)
sizeteam size 1–8, or blank for non-size categories
active1 / 0
record_typetime (stores ms) or level (stores a level integer)

submissions

columnmeaning
idauto-incremented
created_atISO timestamp (UTC)
statuspending, approved, rejected, or reverted
boss_id, category_idforeign keys
mskill time in ms (for time categories) or the level integer (for level categories)
submitter_idDiscord user ID of the submitter
participantsJSON list of Discord user IDs (submitter first)
rsnsRSNs resolved from blert.io (if a blert URL was supplied)
review_msg_idDiscord message ID of the review message
feed_msg_idkept for historical rows only; no longer written (the submit channel no longer carries per-submission status lines)
blert_url, blert_status, blert_detailblert.io verification result
reviewer_id, review_reason, reviewed_atreview metadata
notesfree-text submitter notes
proof_urlsubmit-time proof image URL (fallback)
announce_urljump URL of the announcements message (used as the leaderboard's [proof] link)

submissions_archive

Same columns as submissions. Created on first /pbadmin prune. Rows moved here are deleted from submissions. The archive append is idempotent: re-running prune after a partial failure skips rows whose ID is already in the archive.

How the leaderboard works

The leaderboard channel contains three things, posted by /pbadmin set-channel leaderboard (and re-posted by /pbadmin refresh):

  1. The Master Board: an overview embed plus Prev/Next navigation buttons. The overview lists the currently selected boss group (one of 17 pre-defined groups, e.g. [Raids] Theatre of Blood, [DT2] Desert Treasure 2 (all variants), [Misc] …). Navigation cycles through the 17 groups.
  2. The detail message: a Select menu. Anyone can pick a boss from the dropdown for an ephemeral ranked view (only they see it; the shared detail message does not change). This is the main way members browse rankings.
  3. The Hallowed Sepulchre meme banner: a fixed embed with an image and a fake "records" entry. The meme's title, credit line, and banner image URL all live in the config tab (sepulchre_meme_title, sepulchre_meme_credit, sepulchre_meme_banner_url); editing any of those rows updates the meme with no restart. The shipped defaults are generic and non-identifying (no Discord user IDs or personal handles). An admin pastes a clan-specific credit (e.g. 🏆 - <@&ROLE_ID> to ping a role) and a durable image URL on first run. The banner URL is intentionally empty by default (signed Discord CDN URLs expire within ~24 h); until an admin sets it, the meme renders without an image.

The Master Board and the detail message are persistent: they survive bot restarts. The bot reuses the existing messages on restart (does not re-send) unless /pbadmin refresh is run.

Spot formatting: 🏆 on rank 1, 🥈 on rank 2, 🥉 on rank 3. Times render as M:SS.cc (e.g. 2:31.40); levels render as Level N. The [proof] link in each spot points to the announcements message (rich context) when available, falling back to the bare proof image URL.

blert.io verification

Some bosses are flagged as blert-supported in the built-in seed (ToB, ToB HM, TFA, Doom of Mokhaiotl, Fortis Colosseum, The Inferno). When a submitter supplies a blert_url for one of these bosses, the bot:

  1. Fetches the blert.io recording metadata.
  2. Verifies the boss and participants match what was submitted.
  3. Stores the result in blert_status (verified, mismatch, unreachable) and blert_detail on the submission row.
  4. Shows the verification status on the review embed so staff can see it before approving.
Blert verification is advisory. Staff can still approve a mismatched or unreachable submission; it just makes the review more informed. The bot makes exactly one HTTP call to blert.io per submission, only when a URL is supplied, and only for blert-supported bosses.

Background jobs

Two background loops start with the bot:

Both loops log failures to stdout and continue; a transient sheet error does not crash them.

Troubleshooting

Slash commands don't appear in Discord
If DEV_GUILD_ID is set, commands sync to that guild instantly. If it is unset, global sync can take up to an hour. To force a re-sync, restart the bot. Check that the bot was invited with the applications.commands scope.
Unknown interaction on autocomplete
This means the bot couldn't respond within Discord's 3-second window. The cache warmup and prune paths are already off the event loop, so this should not happen in normal use. If it does, make sure the Google Sheet isn't slow to respond (check the Google Cloud project's Sheets API quotas) and that the host isn't CPU-starved.
Leaderboard didn't update after I edited the sheet
The bot caches reads for 60s and re-warms every 45s. Wait a minute, or run /pbadmin refresh to force a re-render.
A rejected submission is still showing on the leaderboard
Rejected submissions never reach the leaderboard (only approved rows do). If you see one, it was approved and later reverted. Run /pbadmin refresh to re-render, or check the row's status in the sheet.
/pbadmin prune duplicated rows in the archive
The prune now dedups against existing archive IDs, so this won't happen on new runs. To clean up existing duplicates, open submissions_archive in the sheet and delete the extra rows by hand (sort by id, delete the lower created_at duplicates).
The meme banner image is broken
Edit the sepulchre_meme_banner_url row in the config tab to a durable image URL (imgur, direct host, or a re-signed Discord CDN link; note signed Discord CDN URLs expire within ~24 h), then run /pbadmin refresh. No restart needed. To rebrand the meme text itself, edit sepulchre_meme_title and sepulchre_meme_credit in the same tab.
Bot can't read the sheet
Check that the service account email (in google-key.json under client_email) was shared on the sheet as an Editor, and that the Sheets API is enabled in the Google Cloud project. The SHEET_ID in .env must match the sheet's ID in its URL.
Bot crashed and won't start
Check the five print() lines on startup; they are the only logging. Common causes: missing .env, wrong SHEET_ID, service account not shared on the sheet, or a pbbot.py syntax error (run python -m py_compile pbbot.py to check).