Administrator Manual
This manual covers everything needed to install, configure, and operate Inquisitor on your own Limnoria instance. It is intended for bot administrators responsible for the full lifecycle of the plugin.
Installation
Prerequisites
- Python 3.10+
- MariaDB 10.5+
- Limnoria (Supybot fork):
pip install limnoria
Install Dependencies
pip install limnoria mariadb rapidfuzz inflect
Clone the Repository
Clone Inquisitor into Limnoria's plugins directory:
cd ~/.local/share/supybot/plugins/
git clone https://git.ptp-trivia.me/Inquisitor/Inquisitor Inquisitor
Load the Plugin
Start your bot and load the plugin:
@load Inquisitor
Database Setup
Create the Database
Run this SQL as a privileged MariaDB user:
CREATE DATABASE inquisitor CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'inquisitor'@'localhost' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON inquisitor.* TO 'inquisitor'@'localhost';
FLUSH PRIVILEGES;
Configure the Database Connection
Set database credentials via Limnoria's config system (in IRC or in supybot.conf):
@config plugins.Inquisitor.database.host localhost
@config plugins.Inquisitor.database.port 3306
@config plugins.Inquisitor.database.user inquisitor
@config plugins.Inquisitor.database.password your_password
@config plugins.Inquisitor.database.database inquisitor
Note
These are the only settings stored in Limnoria's config. All game and network settings live in the database itself, managed via @config.
Automatic Migrations
When Inquisitor first connects to the database, the migration runner executes all pending schema migrations automatically. No manual database setup is needed beyond creating the database and user.
Database Integrity Check
On every plugin load, Inquisitor runs a database integrity check. If the check fails, the plugin raises a RuntimeError and blocks all functionality until the database is repaired. Check Limnoria's log file for details.
Global Setup Wizard
First-Run Setup
After loading the plugin and configuring the database connection, run the global setup command once from your master network:
@setup <network#channel> <note>
Example:
@setup libera#trivia-admin Initial setup by rb14060
This command:
- Marks the database as verified (
database_verified = true) - Sets
libera#trivia-adminas the master log channel (receives all events from all networks) - Makes the caller the first admin with global ADMIN permission across all networks
Warning
The @setup command is protected by a one-time state guard. It can only be run once. If you need to change the master log channel after setup, use @setmasterlog.
Activate Your Network
After global setup, activate each network for trivia:
@activate <question_set_name> [#log-channel]
Example:
@activate General #trivia-log
This command:
- Links the current network to the specified question set
- Optionally sets a per-network log channel (receives events from this network only)
- Creates the network's settings record with global defaults
- Sets network state to
active
Note
@activate is a first-run-only command. After a network has been activated and then deactivated, use @reactivate instead.
Network States
Inquisitor tracks three network states:
| State | Description |
|---|---|
never_activated |
Network has never been set up |
active |
Network is live and operational |
deactivated |
Network was soft-deactivated (data preserved) |
Network Management
Commands
| Command | Permission | Description |
|---|---|---|
@activate <set> [#chan] |
None (first-run guard) | Activate network for trivia |
@deactivate [--hard] |
ADMIN | Soft-deactivate this network |
@reactivate |
ADMIN | Re-enable a deactivated network |
@netstatus [network] |
ADMIN | View network configuration and state |
@listnetworks |
ADMIN | List all networks with their status |
@setquestionset <name> |
MODERATE | Switch the active question set |
Deactivation
@deactivate
Sets the network's setup.archived = true. This is a soft-deactivation:
- All data (questions, scores, settings) is preserved
- Trivia cannot be started on the network while deactivated
- Reactivate at any time with @reactivate
@deactivate --hard
Warning
Hard deactivation removes the network's settings and activation record. This is not reversible without running @activate again and importing data. Use only when permanently retiring a network.
Reactivation
@reactivate
Brings a soft-deactivated network back to active state. If the question set that was active before deactivation has been deleted, Inquisitor falls back to the global default question set with a warning.
Question Set Switching
@setquestionset <question_set_name>
Switches the current network's active question set. This command:
- Checks that no games are currently running on any channel of this network
- Announces the change to the current channel
- Takes effect for the next game started
Note
@setquestionset requires MODERATE permission, not ADMIN, so channel moderators can manage question sets without full admin access.
Permission Management
Overview
Inquisitor uses an internal permission system independent of IRC server modes. Permissions are:
- Per-network: ADMIN on Network A does not grant anything on Network B
- Hierarchical: Higher levels inherit all capabilities of lower levels
- Pattern-based: Can be granted to specific users or wildcard hostmask patterns
Permission Levels
ADMIN
└─ MANAGE
└─ MODERATE
└─ START
└─ PLAY
| Level | Grants |
|---|---|
| PLAY | Answer questions, use @recall, view stats |
| START | Everything in PLAY + start/stop games (@trivia) |
| MODERATE | Everything in START + manage settings, switch question sets, manage pools |
| MANAGE | Everything in MODERATE + add/edit/delete questions and question sets |
| ADMIN | Everything in MANAGE + manage permissions, manage networks, use admin settings |
Commands
| Command | Permission | Description |
|---|---|---|
@grant <nick> <level> |
ADMIN | Grant an explicit permission level |
@revoke <nick> <level> |
ADMIN | Remove an explicit grant or denial |
@deny <nick> <level> |
ADMIN | Block a user's default permissions |
@undeny <nick> <level> |
ADMIN | Remove a denial (same as @revoke for denials) |
@permissions <nick> |
ADMIN | View a user's effective permissions |
@listperms [network] |
ADMIN | List all grants/denials on the network (sent via private notice for long lists) |
@mypermissions |
None | View your own effective permissions |
Granting Permissions
@grant alice MODERATE
If alice is currently in the channel, her hostmask is resolved to *!*@<host> automatically. If she is not in the channel, the grant uses alice!*@*.
You can also grant directly to a hostmask pattern:
@grant *!*@staff.example.com ADMIN
Grants are network-wide — they apply to all channels on the current network.
Revoking and Denying
@revoke alice MODERATE # Remove explicit grant
@deny spammer PLAY # Block default PLAY from a specific user
@revoke spammer PLAY # Remove the denial (restores access via defaults)
Note
@revoke removes both grants and denials. To restore a user who has a denial, use @revoke (not @deny).
Permission Check Order
- Limnoria owner → Automatic ADMIN on all networks
- Explicit grants (checked first — grants beat denials)
- Permission hierarchy from grants
- Explicit denials (only block if no grant exists)
- Default permissions (see Default Permissions)
- Channel mode-based permissions (if
permissions.modeMapEnabled=true) - Final deny
Default Permissions
Default permissions determine what new users can do without any explicit grants:
| Setting | Default | Description |
|---|---|---|
permissions.defaultPlay |
true |
All users can play by default |
permissions.defaultStart |
true |
All users can start/stop games by default |
permissions.defaultModerate |
false |
Moderation requires explicit grant |
permissions.defaultManage |
false |
Question management requires explicit grant |
permissions.defaultAdmin |
false |
Admin access requires explicit grant |
Change defaults for your network:
@config set permissions.defaultStart=false # Require explicit grant to start games
@config set permissions.defaultPlay=false # Require explicit grant to play (private network)
Warning
All permissions.default* settings are admin-only. Changing permissions.defaultModerate=true grants PLAY and START to all users via hierarchy.
Channel Mode-Based Permissions (Optional)
When permissions.modeMapEnabled=true, Inquisitor grants permissions based on IRC channel modes:
| IRC Mode | Default Permission |
|---|---|
| +v (voice) | PLAY |
| +h (halfop) | START |
| +o (op) | MODERATE |
| +a (admin) | MANAGE |
| +q (owner) | ADMIN |
@config set permissions.modeMapEnabled=true
@config set permissions.modeMapOp=ADMIN # Make ops full admins instead
This feature is disabled by default for backwards compatibility. Explicit grants always take precedence over mode-based permissions.
Configuration Reference
Settings are stored in the database and can be scoped per-channel, per-network, or globally.
Hierarchy
Channel override > Network override > Global default > Code default
Use @config list to see all current settings with scope indicators:
- [C] = Channel-specific
- [N] = Network-level
- [G] = Global default
Commands
| Command | Permission | Description |
|---|---|---|
@config get <key> |
PLAY | Show setting value and metadata |
@config list [namespace] |
PLAY | List all settings |
@config set <key=value> |
MODERATE (ADMIN for admin-only settings) | Change a setting |
@config reset <key> |
MODERATE | Reset setting to default |
@config diff [target] |
MODERATE | Compare settings with another context |
@config export [scope] |
MODERATE | Export settings as JSON |
@config import <json> |
MODERATE | Import settings from JSON |
@config set-default <key> <value> |
ADMIN | Change global default for a setting |
@config show-overrides <key> |
ADMIN | List all networks/channels overriding a setting |
@config help [subcommand] |
PLAY | Show help for config commands |
game.* Namespace
| Setting | Type | Default | Range | Admin Only | Description |
|---|---|---|---|---|---|
game.enabled |
bool | true |
— | No | Enable/disable trivia in this channel |
game.hintDelay |
int | 15 |
5–300 | No | Seconds between standard question hints |
game.questionTimeout |
int | 60 |
10–600 | No | Seconds before question times out |
game.maxHints |
int | 3 |
0–10 | No | Maximum hints per question |
game.delayBetweenQuestions |
int | 5 |
1–60 | No | Seconds between questions |
Note
game.hintDelay must be less than game.questionTimeout. The validator enforces this cross-setting constraint.
scoring.* Namespace
| Setting | Type | Default | Range | Admin Only | Description |
|---|---|---|---|---|---|
scoring.minPoints |
int | 10 |
1–1000 | No | Minimum base points for standard/scramble questions |
scoring.maxPoints |
int | 50 |
1–1000 | No | Maximum base points for standard/scramble questions |
scoring.lengthBonus |
bool | true |
— | No | Award bonus for longer answers |
scoring.speedBonus |
bool | true |
— | No | Award bonus for fast answers |
scoring.timezone |
str | UTC |
— | Yes | IANA timezone for period boundaries (e.g., America/New_York) |
scoring.periodOffset |
int | 0 |
0–23 | Yes | Hours after midnight when daily/weekly/monthly periods reset |
scoring.basePoints |
int | 10 |
1–1000 | No | Legacy setting (superseded by min/max range) |
Note
scoring.timezone uses IANA timezone names validated by Python's zoneinfo module. Examples: America/New_York, Europe/London, Asia/Tokyo, UTC. Changing this setting affects when daily/weekly/monthly periods roll over.
kaos.* Namespace
| Setting | Type | Default | Range | Admin Only | Description |
|---|---|---|---|---|---|
kaos.frequency |
int | 10 |
-1–100 | No | KAOS frequency: -1=KAOS-only, 0=disabled, N=1 per N standard questions |
kaos.hintDelay |
int | 20 |
5–300 | No | Seconds between KAOS hints |
kaos.autoRecall |
int | 3 |
0–20 | No | Auto-recall after N KAOS answers (0=disabled) |
kaos.minPoints |
int | 10 |
1–1000 | No | Minimum base points per KAOS answer |
kaos.maxPoints |
int | 50 |
1–1000 | No | Maximum base points per KAOS answer |
kaos.bonusLow |
int | 50 |
0–10000 | Yes | Minimum KAOS completion bonus (BogusTrivia: kbonlo) |
kaos.bonusHigh |
int | 200 |
0–10000 | Yes | Maximum KAOS completion bonus (BogusTrivia: kbonhi) |
kaos.bonusMinAnswers |
int | 0 |
0–100 | Yes | Minimum answers required to award bonus (0 = always, if multi-player) |
scramble.* Namespace
| Setting | Type | Default | Range | Admin Only | Description |
|---|---|---|---|---|---|
scramble.hintDelay |
int | 15 |
5–300 | No | Seconds between unscramble hints |
scramble.questionTimeout |
int | 60 |
10–600 | No | Seconds before scramble question times out |
bonus.* Namespace
| Setting | Type | Default | Range | Admin Only | Description |
|---|---|---|---|---|---|
bonus.enabled |
bool | true |
— | No | Enable streak bonus multiplier |
bonus.interval |
int | 10 |
1–100 | No | Questions between bonus multipliers |
bonus.multiplier |
float | 2.0 |
1.0–10.0 | No | Bonus point multiplier |
autovoice.* Namespace (Admin Only)
| Setting | Type | Default | Range | Admin Only | Description |
|---|---|---|---|---|---|
autovoice.enabled |
bool | false |
— | Yes | Auto-voice top players in channel |
autovoice.topN |
int | 3 |
1–25 | Yes | Number of top players to auto-voice |
autovoice.period |
str | weekly |
— | Yes | Period for auto-voice calculation |
flood.* Namespace (Admin Only)
| Setting | Type | Default | Range | Admin Only | Description |
|---|---|---|---|---|---|
flood.maxPerSecond |
float | 2.0 |
0.1–10.0 | Yes | Max messages per second before flood protection |
flood.muteSeconds |
int | 30 |
5–600 | Yes | Mute duration when flood protection triggers |
permissions.* Namespace (Admin Only)
| Setting | Type | Default | Admin Only | Description |
|---|---|---|---|---|
permissions.defaultPlay |
bool | true |
Yes | Default PLAY permission for users without grants |
permissions.defaultStart |
bool | true |
Yes | Default START permission for users without grants |
permissions.defaultModerate |
bool | false |
Yes | Default MODERATE permission for users without grants |
permissions.defaultManage |
bool | false |
Yes | Default MANAGE permission for users without grants |
permissions.defaultAdmin |
bool | false |
Yes | Default ADMIN permission for users without grants |
permissions.modeMapEnabled |
bool | false |
Yes | Enable automatic permissions based on IRC channel modes |
permissions.modeMapVoice |
str | PLAY |
Yes | Permission level for voiced users (+v); NONE to disable |
permissions.modeMapHalfop |
str | START |
Yes | Permission level for half-ops (+h); NONE to disable |
permissions.modeMapOp |
str | MODERATE |
Yes | Permission level for ops (+o); NONE to disable |
permissions.modeMapAdmin |
str | MANAGE |
Yes | Permission level for admins (+a); NONE to disable |
permissions.modeMapOwner |
str | ADMIN |
Yes | Permission level for owners (+q); NONE to disable |
debug.* Namespace (Admin Only)
| Setting | Type | Default | Admin Only | Description |
|---|---|---|---|---|
debug.verbose |
bool | false |
Yes | Enable verbose debug logging to file. Global scope only — use @config set-default debug.verbose=true |
Logging Configuration
Log Channels
Inquisitor supports two log channels:
- Master log channel (set during
@setup): Receives all events from all networks. Lives on the bot's home network. - Per-network log channel (set via
@activateor@setnetlog): Receives events from that network only.
Change log channels at runtime:
@setmasterlog libera#trivia-admin # Change master log channel
@setnetlog #network-specific-log # Set per-network log channel
@setnetlog none # Disable per-network log channel
Both commands require ADMIN permission.
What Is Logged
| Event | Master Log | Network Log |
|---|---|---|
| Global setup | Yes | — |
| Network activate/deactivate/reactivate | Yes | Yes |
| Question set change | Yes | Yes |
| Game start/stop | Yes | Yes |
| Config changes (with old → new values) | Yes | Yes |
| Permission grant/revoke | Yes | Yes |
Log Format
[NetworkName] Event description
Messages are color-coded by severity: - Green: Informational (game start, question set change) - Yellow: Warnings (pool exhaustion, fallback activation) - Red: Errors and destructive actions (deactivation, hard deactivate)
File Logging
All events are written to Limnoria's log directory at DEBUG level. Enable verbose debug output for operational traces:
@config set-default debug.verbose=true
Note
debug.verbose is a global-scope setting. Use @config set-default rather than @config set. Security-related log calls ([SECURITY]) and error events are always logged regardless of this setting.
Security Overview
SQL Injection Protection
All database queries use parameterized statements with %s placeholders (MariaDB connector syntax). No user input is ever interpolated directly into SQL strings.
An automated regression test (SQLInjectionRegressionTest) scans all Python source files for f-string SQL construction patterns on every test run. Pre-existing safe dynamic patterns (table constants, whitelisted column sets) are annotated with # noqa: SQL-FSTRING.
Files audited: db/connection.py, core/settings_manager.py, core/permissions.py, core/question_manager.py, importers/engine.py, core/game_manager.py, core/score_tracker.py, core/stats_module.py.
Input Validation
All validation logic lives in utils/validators.py.
| Input | Validation | On Failure |
|---|---|---|
| Question text | Strip IRC controls; length 1–400 chars | Error message to user |
| Answer text | Strip IRC controls; length 1–200 chars | Error message to user |
| Alternative answers | Each validated individually | Error message to user |
| Hostmask patterns | Non-empty; no SQL metacharacters (', ;, --, /*, */) |
Suspicious: silent drop + [SECURITY] log entry; empty: error to user |
| Channel names | Starts with #, &, +, or !; no invalid IRC chars; 2–50 chars |
Error message to user |
| Import file paths | Resolved with realpath; must be within allowed data directory |
Error message to user |
| Imported question/answer text | IRC controls stripped before INSERT | Silent strip |
IRC control characters stripped: \x02 (bold), \x03 (color), \x0f (reset), \x16 (reverse), \x1d (italic), \x1e (strikethrough), \x1f (underline).
Rate Limiting
| Command | Cooldown |
|---|---|
| Permission denials (all commands) | 60 seconds per user per command |
@question search |
8 seconds |
@stats |
10 seconds |
@importpreview, @importpointspreview |
60 seconds |
Rate-limited requests are silently dropped and logged with a [RATE LIMIT] prefix.
Note
@importconfirm and @importpointsconfirm are intentionally not rate-limited. The preview commands gate the expensive database operations.
File Path Traversal Prevention
Import commands validate file paths:
- Path is resolved to an absolute real path via
os.path.realpath() - Resolved path must share a common prefix with Limnoria's data directory
- Paths containing
..or symlinks pointing outside the data directory are rejected
Known Limitations
- IRC command injection: Not addressed at plugin level. Limnoria handles command dispatch. Plugin output does not trigger secondary command execution.
- Nick-based lookups:
@stats <nick>looks up by nick in the database. Impersonation via nick change is possible only if the target has never played. - File encoding: Import uses
chardet/charset_normalizerfor encoding detection. Malformed non-UTF-8 files may produce garbled text but will not cause code execution.
See SECURITY.md at the repository root for the complete technical security audit.
Data Import
BogusTrivia Question Import
Via IRC:
@importpreview <file_path> <question_set_name> [default_category]
This starts the preview-confirm workflow:
@importpreview /path/to/questions.ques General— preview what would be imported@importconfirm /path/to/questions.ques General— commit the import (within 5 minutes)
Via CLI tool (recommended for large imports):
python tools/import_questions.py /path/to/questions.ques \
--config /path/to/supybot.conf \
--question-set "General" \
--dry-run
CLI tool flags:
| Flag | Description |
|---|---|
--config <path> |
Read database credentials from supybot.conf |
--question-set <name> |
Override destination question set name (single-file imports) |
--dry-run |
Preview import without writing to database |
--default-category <name> |
Override fallback category for uncategorized questions |
The CLI tool supports wildcards and multiple file arguments:
python tools/import_questions.py /data/*.ques
python tools/import_questions.py general.ques sports.ques movies.ques
Player Point Import
@importpointspreview <file_path>
Imports player scores from a BogusTrivia player data file. Points are accumulated — existing scores are added to, not replaced.
Follow with @importpointsconfirm <file_path> to commit.
Accepted delimiters: space, colon, or equals sign.
Import Safety
- All-or-nothing transactions: Any error during import causes a full rollback — your database is never left in a partial state.
- Checkpoint every 500 questions: Large imports can resume from the last checkpoint if interrupted.
- Duplicate detection: Questions are checked against existing content using fuzzy matching at 85% threshold (token_set_ratio). Add
--forcein CLI or use@question add --forcein IRC to bypass. - Quality checks: Questions with text under 10 chars, empty answers, question equal to answer, or more than 20 alternatives are flagged.
Question Management
Adding Questions via IRC
@question add "Question text" "Answer" --set "Set Name" [--category "Cat"] [--type standard|kaos|scramble] [--alternatives "alt1,alt2"] [--force]
Types: standard (default), kaos, scramble
Requires MANAGE permission.
Question Sets
@questionset add <name> # Create a new question set (MANAGE)
@questionset rename <old> <new> # Rename a question set (MANAGE)
@questionset list # List all sets (no permission required)
@questionset stats <name> # Show statistics for a set (no permission required)
@questionset delete <name> # Delete a set (ADMIN, requires @confirm)
Warning
@questionset delete cascades to all questions in the set and all associated asked-question tracking history. This is not reversible.
Bulk Operations
@question bulkdelete --set "SetName" [--category "Cat"] # Delete all questions in a set/category (ADMIN)
@question bulkmove --from-set "Source" --to-set "Target" # Move all questions to another set (ADMIN)
Both require @confirm within 5 minutes of preview.
Warning
Bulk operations are destructive and require ADMIN permission. Always preview before confirming.
Statistics and Maintenance
Cross-Network Statistics
@crossstats <nick_or_hostmask>
Shows a user's scores across all networks. Requires ADMIN permission.
Database Commands (Bot Owner Only)
@dbstatus # Show database connection status and pool metrics
@dbconnect # Reconnect to database with current Limnoria config
These commands require Limnoria bot owner capability (not Inquisitor ADMIN), because they expose database credentials.
Question Pool Status
@poolstatus # Show asked/total for each pool type (no permission required)
@poolreset # Reset pool history for current question set (MODERATE)
The pool tracks which questions have been asked to prevent repeats. Pools reset automatically when exhausted (silently reshuffled). Use @poolreset to force a reset before exhaustion.
Viewing Network Status
@netstatus [network] # Show current network config, timezone, question set, log channel
@listnetworks # List all networks with their activation state and question set
Troubleshooting
Plugin Won't Load
Check Limnoria's log for a RuntimeError from the database integrity check. Common causes:
- Database credentials not configured (run
@config plugins.Inquisitor.database.*) - MariaDB server not running or unreachable
- MariaDB user lacks sufficient privileges
"Permission denied" errors
Check the user's effective permissions:
@permissions <nick>
@config list permissions
Common causes: hostmask changed on reconnect; permission was granted on a different network; explicit denial blocking default access.
KAOS questions not appearing
Check the kaos.frequency setting:
@config get kaos.frequency
0= KAOS disabled- Positive integer N = 1 KAOS per N standard questions (default: 10)
-1= KAOS-only mode
If the question set has no KAOS-type questions and kaos.frequency=-1, Inquisitor falls back to standard questions with a warning.
Period scores look wrong
Verify the timezone setting:
@config get scoring.timezone
Period boundaries (daily/weekly/monthly) are calculated using the configured IANA timezone. A network in UTC will reset at midnight UTC. Use @config set scoring.timezone=America/New_York (ADMIN) to use a different timezone.
Best Practices
Tip
Run the @setup command from your most reliable network. The master log channel should be on a network the bot is connected to continuously.
Tip
Use explicit ADMIN grants sparingly. Prefer granting MODERATE for game management and MANAGE for question set maintenance. Reserve ADMIN for trusted operators who need to manage networks and permissions.
Tip
Before running destructive operations (bulk delete, question set delete, deactivation), export your configuration with @config export channel and document the question count with @questionset stats <name>.
Tip
For large BogusTrivia migrations (thousands of questions), use the CLI tool tools/import_questions.py with --dry-run first to validate the import, then run without --dry-run. The CLI tool avoids IRC message length limits and timeouts that can interrupt large imports.