RABot is the official RetroAchievements Discord bot, built with Bun runtime, TypeScript, Discord.js v14, and Drizzle ORM with SQLite. The bot is transitioning from legacy prefix commands (!) to modern slash commands (/) while maintaining backward compatibility.
# Initial setup
bun install
cp .env.example .env # Configure environment variables
bun run db:generate # Generate database migrations
bun run db:migrate # Apply migrations
bun run db:seed # Seed default teams (RACheats)
# Development
bun run dev # Run with hot reload (--watch)
bun run tsc # TypeScript type checking (via tsgo)
bun run format # Format code with oxfmt
bun run format:check # Check formatting without writing
bun run lint # Run oxlint
bun run lint:fix # Auto-fix linting issues (oxlint)
bun run test # Run all tests (vitest)
bun run test:watch # Run tests in watch mode (vitest)
bun run verify # Run format check, lint, type checking, and tests
# Deployment
bun run deploy-commands # Deploy slash commands to Discord (required after adding/modifying slash commands)
bun run start # Production mode
# Database management
bun run db:studio # Open Drizzle Studio GUIThe bot supports both legacy prefix commands and modern slash commands during the migration period:
-
Legacy Commands (
src/commands/*.command.ts)- Use the
Commandinterface - Accessed via prefix (default:
!) - Show migration notices encouraging slash command use
- Example:
!gan 14402
- Use the
-
Slash Commands (
src/slash-commands/*.command.ts)- Use the
SlashCommandinterface - Built with Discord.js SlashCommandBuilder
- Support autocomplete, better validation, ephemeral responses
- Example:
/gan game-id:14402
- Use the
When users use legacy commands that have slash equivalents:
- A temporary migration notice appears (15 seconds)
- The legacy command still executes
- Configured via
legacyNameproperty in slash commands
- Drizzle ORM with SQLite (
bun:sqlite) - Schema defined in
src/database/schema.ts - Services pattern for database operations (
src/services/*.service.ts) - Tables:
teams,team_members,polls,poll_votes,uwc_polls,uwc_poll_results
- Legacy commands auto-loaded from
src/commands/ - Slash commands auto-loaded from
src/slash-commands/ - Slash commands must be deployed via
bun run deploy-commands
- TeamService: Manages teams and members, supports both ID and name lookups
- PollService: Handles poll creation and voting
- UwcPollService: Tracks UWC polls, stores results, enables searching by achievement/game
- UwcHistoryService: Retrieves and formats previous UWC poll history for auto-detection
- AutoPublishService: Automatically publishes messages in configured announcement channels
Required in .env:
DISCORD_TOKEN: Bot token (required)DISCORD_APPLICATION_ID: Bot application ID (required)RA_WEB_API_KEY: RetroAchievements Web API key (required)LEGACY_COMMAND_PREFIX: Prefix for legacy commands (default:!)RA_CONNECT_API_KEY: RetroAchievements Connect API key (future use)YOUTUBE_API_KEY: For longplay searches in gan commands (optional, but recommended)MAIN_GUILD_ID: Discord guild ID for the main RetroAchievements server (optional, but recommended)WORKSHOP_GUILD_ID: Discord guild ID for the RetroAchievements Workshop server (optional, but recommended)CHEAT_INVESTIGATION_CATEGORY_ID: Category ID for RACheats team restrictionsUWC_VOTING_TAG_ID: Forum tag ID for active UWC polls (optional)UWC_VOTE_CONCLUDED_TAG_ID: Forum tag ID for completed UWC polls (optional)UWC_FORUM_CHANNEL_ID: Forum channel ID for UWC auto-detection feature (optional)AUTO_PUBLISH_CHANNEL_IDS: Comma-separated list of announcement channel IDs to auto-publish from (optional)NODE_ENV: Set to "production" in production (default: "development")LOG_LEVEL: Logging level - trace, debug, info, warn, error, fatal (default: "debug" in dev, "info" in prod)
Note: The bot will validate required environment variables on startup and exit with an error if any are missing.
- Use
MessageFlags.Ephemeralinstead ofephemeral: true - Autocomplete handlers in main interaction event
- Proper intent configuration for message content access
The bot automatically provides context when new UWC (Unwelcome Concept) reports are created:
- Monitors threads in the configured forum channel (
UWC_FORUM_CHANNEL_ID) - Detects threads matching pattern:
12345: Achievement Title (Game Name) - Queries database for previous UWC polls for the same achievement ID
- Posts an automated message with links to up to 5 previous discussions
- Shows poll dates, outcomes (Approved/Denied/Active/No Action), and vote results
- Uses efficient database queries instead of Discord API calls for performance
The bot can automatically publish messages in Discord announcement channels:
- Configure channel IDs via
AUTO_PUBLISH_CHANNEL_IDSenvironment variable - Bot requires "Manage Messages" permission in announcement channels
- Automatically publishes non-bot messages that aren't already crossposted
- Handles rate limits and permission errors gracefully
- Logs all publishing activities for monitoring
- Slash commands preferred for new features
- Create in
src/slash-commands/[name].command.ts - Export default with
SlashCommandinterface - Set
legacyNameif replacing a prefix command - Add guild restrictions using
requireGuild()utility if needed - Run
bun run deploy-commandsafter adding - Update README.md - Add the new command to both the slash commands and legacy commands lists (if applicable)
Use the requireGuild() utility for server-restricted commands:
import { requireGuild } from "../utils/guild-restrictions";
import { WORKSHOP_GUILD_ID } from "../config/constants";
async execute(interaction, _client) {
if (!(await requireGuild(interaction, WORKSHOP_GUILD_ID))) {
return;
}
// Command logic here...
}- Provides consistent, low-cognitive-load guild restrictions
- Automatically sends ephemeral error responses
- Use
MAIN_GUILD_IDorWORKSHOP_GUILD_IDconstants
- Teams stored by ID, accessed by name in commands
- Autocomplete support for team selection
- Special restrictions for certain teams (e.g., RACheats)
- Team commands restricted to Workshop server only
- Use
@retroachievements/apipackage - Build authorization with
buildAuthorization() - Handle game IDs and URLs in gan commands
- Memory parsing utility for achievement logic analysis (mem command)
The bot uses Pino for structured logging with the following features:
trace: Most detailed loggingdebug: Detailed information for debugginginfo: General informational messageswarn: Warning messageserror: Error messagesfatal: Fatal errors that cause the bot to exit
-
Basic Logger (
src/utils/logger.ts)logger.info(),logger.error(), etc. for standard logginglogError()- Log errors with contextlogCommandExecution()- Log command executionslogMigrationNotice()- Log migration noticeslogDatabaseQuery()- Log database operationslogApiCall()- Log external API calls
-
Error Tracking (
src/utils/error-tracker.ts)ErrorTracker.trackMessageError()- Track errors from message commandsErrorTracker.trackInteractionError()- Track errors from slash commandsErrorTracker.formatUserError()- Format errors for user display with error IDs
-
Command Analytics (
src/utils/command-analytics.ts)- Automatic tracking of all command executions
- Execution time measurement
- Success/failure tracking
- Per-user and per-guild statistics
- Access statistics via
CommandAnalytics.getStatistics()
- Always use structured logging with context objects
- Include user ID, guild ID, and command name in error logs
- Use appropriate log levels (don't use
infofor debugging) - Error IDs help users report issues
- The bot automatically deploys via Forge when changes are merged to main
- Runs under a process supervisor on the production server
- No manual PM2 configuration needed
- SQLite WAL mode is enabled by default for better concurrent access
- WAL mode allows concurrent reads during writes, ideal for Discord bot usage patterns
- The bot handles SIGTERM and SIGINT signals for graceful shutdown
- Discord client connections are properly closed before exit
- Uncaught exceptions and promise rejections trigger graceful shutdown
Tests use Vitest as the test runner (configured in vitest.config.ts). All tests run in both local development and CI environments.
- Always use Bun commands (
bun run,bun install) not npm/yarn/pnpm - Deploy slash commands after changes (
bun deploy-commands) - Check channel type before accessing properties like
topicorparentId - Use proper null checks for Discord.js properties
- Remember to handle both team IDs and names in TeamService methods
- Use
requireGuild()for server restrictions instead of manual guild ID checks - Always write tests for new utilities and commands (follow existing test patterns)
- The bot validates required environment variables on startup - check logs if it exits immediately