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.
What it does
- Submission flow. A member runs
/pb submitwith a screenshot and a time (or level). The bot posts the screenshot to a staff review channel with Approve / Reject buttons and gives the submitter an ephemeral receipt (only they see it; the submit channel itself stays clean). The bot does not DM users on decision; the result is visible in the review-message footer and, on approval, the announcements channel (an admin tells a submitter directly if they need to re-submit). Submissions can be restricted to a role via/pbadmin set-submitter-role(open to everyone until a role is bound; see Admin commands). The submit channel keeps a single pinned "how to submit" message. An admin pins it manually from the text in this page (see Submit channel pinned message); it is not bot-managed. An edit window (5 minutes) lets a member replace a pending submission without cluttering the queue. - Review flow. Staff approve or reject from the review channel. Approvals go to an announcements channel as a rich embed (image, roster mentions, proof link); rejections open a reason modal and post a red embed. The bot detects first-of-category approvals and labels them.
- Leaderboard. A dedicated channel gets a Master Board (overview + navigation buttons) and a detail message with a Select menu. Anyone can pick a boss from the select for an ephemeral ranked view;
/pb topposts a public top-5;/pb historyshows the record progression;/pb lookupshows a player's best per category with their clan-wide rank. - Record-break pings. When a record that has stood longer than a configurable threshold falls, the bot pings a role (e.g.
@records). - Monthly digest. On the 1st of each month the bot posts a "records set last month" summary to the announcements channel.
- Admin toolset. Configure channels and roles, add/edit/remove bosses and categories, export the whole sheet to
.xlsx, prune rejected/reverted submissions to an archive, force a leaderboard refresh, and re-sync the sheet with the built-in boss seed.
Requirements
- Python 3.11+. Uses modern Python features; older versions will not work.
- A Discord bot application with the
applications.commandsandbotscopes. - A Google Cloud service account with the Sheets API enabled, and a Google Sheet shared with that service account.
- A persistent host (the bot holds a gateway connection; if it restarts, persistent views let old Approve/Reject buttons and the leaderboard Select menu keep working).
Python dependencies
discord.py>=2.4
gspread>=6.0
openpyxl>=3.1
python-dotenv>=1.0
aiohttp # ships with discord.py, listed for clarityInstall:
pip install discord.py gspread openpyxl python-dotenvSetup
Google service account + sheet
- In Google Cloud Console, create a project and enable the Google Sheets API.
- Create a service account and download its JSON key. Save it next to
pbbot.pyasgoogle-key.json(the filename is configurable; see Configuration file). - Create a Google Sheet. Copy its ID from the URL:
https://docs.google.com/spreadsheets/d/<SHEET_ID>/edit. - Share the sheet with the service account's email (found in the JSON under
client_email) as an Editor. - The bot creates the
config,bosses,categories, andsubmissionstabs on first run. You do not need to pre-build them. Thesubmissions_archivetab is created on first/pbadmin prune.
Discord application + invite
- Create an application at the Discord Developer Portal. Add a Bot user and copy its token.
- Under OAuth2 → URL Generator, select scopes
botandapplications.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)
- Open the generated URL and invite the bot to your clan server.
- Note the guild ID of your clan server (Developer Mode → right-click server → Copy ID). Setting
DEV_GUILD_IDmakes 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 syncDEV_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.pyOn startup the bot:
- 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). - 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).
- Syncs slash commands (guild if
DEV_GUILD_IDis set, else global). - 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:
/pbadmin set-channel submit <your submit channel>: binds the member submit channel./pbadmin set-channel review <your staff review channel>: binds the staff review queue./pbadmin set-channel announce <your announcements channel>: binds the approval announcements channel./pbadmin set-channel leaderboard <your leaderboard channel>: binds and immediately posts the Master Board + detail message + Hallowed Sepulchre meme banner./pbadmin set-role <your staff role>: binds the staff role (used for review-queue gating and record-break pings).- (Optional)
/pbadmin set-submitter-role <your member role>: restrict/pb submitto a role. Skip this to leave submissions open to everyone. - Restrict the
/pbadmincommand group to your staff role in Server Settings → Integrations → Royal PB Bot. - 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.
- (Optional)
/pbadmin record-ping <role> <days>: set the role to ping when a record standing longer thandaysfalls.
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 knowMust 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. Submissions are open to everyone by default; an admin can restrict /pb submit to a role with /pbadmin set-submitter-role (staff and server managers can always submit). 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) requiredPosts 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) requiredPosts 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) requiredPosts 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
/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
| Command | What it does |
|---|---|
/pbadmin set-channel kind channel | Bind 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 role | Bind the staff role (used for review-queue gating and record-break pings). |
/pbadmin set-submitter-role role | Restrict /pb submit to a role. Omit role to re-open submissions to everyone. Open to everyone until a role is bound. Staff and server managers can always submit (so they can test the flow). |
/pbadmin record-ping role days | Set the role to ping when a record standing longer than days falls. Omit role to disable the ping. |
/pbadmin show | Print the current channel bindings, staff role, submitter role, and record-ping config. |
/pbadmin status | Bot health: cache warmth + age, boss/category/submission row counts, channel config, uptime. |
Records management
| Command | What it does |
|---|---|
/pbadmin revert submission_id reason | Reverts 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 refresh | Deletes 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 migrate | Re-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 export | Uploads the entire sheet (all tabs) as an .xlsx attachment. One Excel sheet per Google Sheets tab. |
Boss management: /pbadmin boss
| Command | What 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 boss | Soft-delete: removes from pickers, keeps history. (Hard removal is never done; past submissions still reference the boss row.) |
/pbadmin boss list | Lists all active bosses with their blert support status. |
Category management: /pbadmin category
| Command | What 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 name | Soft-delete a category. History intact. |
/pbadmin category list boss | Lists 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
| key | value | set by |
|---|---|---|
submit_channel_id | Discord channel ID | /pbadmin set-channel |
review_channel_id | Discord channel ID | /pbadmin set-channel |
announce_channel_id | Discord channel ID | /pbadmin set-channel |
leaderboard_channel_id | Discord channel ID | /pbadmin set-channel |
staff_role_id | Discord role ID | /pbadmin set-role |
submitter_role_id | Discord role ID | /pbadmin set-submitter-role (blank = open to everyone) |
record_ping_role_id | Discord role ID | /pbadmin record-ping |
record_ping_min_days | integer (e.g. 30) | /pbadmin record-ping |
last_digest_month | YYYY-MM | the bot, after each monthly digest |
sepulchre_meme_title | text | seeded on first run; editable in-sheet afterward |
sepulchre_meme_credit | text | seeded on first run; editable in-sheet afterward |
sepulchre_meme_banner_url | image URL | seeded on first run (empty by default); editable in-sheet afterward |
bosses
| column | meaning |
|---|---|
id | auto-incremented integer |
name | boss name |
aliases | comma-separated autocomplete aliases |
blert_supported | 1 if blert.io can verify, else 0 |
group_name | reserved (currently unused) |
active | 1 = shown in pickers, 0 = soft-deleted |
image_url | optional image shown on the leaderboard detail page |
categories
| column | meaning |
|---|---|
id | auto-incremented integer |
boss_id | foreign key to bosses.id |
name | category name (e.g. Solo, Duo, Highest Delve Level) |
size | team size 1–8, or blank for non-size categories |
active | 1 / 0 |
record_type | time (stores ms) or level (stores a level integer) |
submissions
| column | meaning |
|---|---|
id | auto-incremented |
created_at | ISO timestamp (UTC) |
status | pending, approved, rejected, or reverted |
boss_id, category_id | foreign keys |
ms | kill time in ms (for time categories) or the level integer (for level categories) |
submitter_id | Discord user ID of the submitter |
participants | JSON list of Discord user IDs (submitter first) |
rsns | RSNs resolved from blert.io (if a blert URL was supplied) |
review_msg_id | Discord message ID of the review message |
feed_msg_id | kept for historical rows only; no longer written (the submit channel no longer carries per-submission status lines) |
blert_url, blert_status, blert_detail | blert.io verification result |
reviewer_id, review_reason, reviewed_at | review metadata |
notes | free-text submitter notes |
proof_url | submit-time proof image URL (fallback) |
announce_url | jump 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):
- 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. - 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.
- 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
configtab (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:
- Fetches the blert.io recording metadata.
- Verifies the boss and participants match what was submitted.
- Stores the result in
blert_status(verified,mismatch,unreachable) andblert_detailon the submission row. - Shows the verification status on the review embed so staff can see it before approving.
Background jobs
Two background loops start with the bot:
- Cache warmup: every ~45s, re-reads the sheet tabs so the bot's in-memory cache stays fresh. The first iteration runs immediately so the cache is warm before the first command. This is why autocomplete stays responsive even during a large prune.
- Monthly digest: every 15 minutes, checks whether the calendar month has rolled over since the last digest. If it has, builds a "records set last month" summary and posts it to the announcements channel, then records the month in the config tab.
Both loops log failures to stdout and continue; a transient sheet error does not crash them.
Troubleshooting
Slash commands don't appear in Discord
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
Leaderboard didn't update after I edited the sheet
/pbadmin refresh to force a re-render.
A rejected submission is still showing on the leaderboard
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
submissions_archive in the sheet and delete the extra rows by hand (sort by id, delete the lower created_at duplicates).
Bot can't read the sheet
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
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).