A community-specific plugin for Inquisitor to award community points based on trivia participation
Find a file
rb14060 63ebaed5f5
docs: add changelog for 0.4.0-rc.4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDQPDBPWAPjU572y6jcuWF
2026-11-01 00:27:10 -04:00
.planning docs: defer hostmask payout exclusion to M002 2026-10-29 22:56:04 -04:00
db feat(S01): plugin scaffold and DB foundation 2026-06-03 19:05:22 -04:00
.gitignore chore: migrate planning from GSD Pi (.gsd/) to GSD core (.planning/) 2026-10-28 23:15:28 -04:00
__init__.py chore: bump version number 2026-11-01 00:25:03 -04:00
CHANGELOG.md docs: add changelog for 0.4.0-rc.4 2026-11-01 00:27:10 -04:00
config.py feat(S01): plugin scaffold and DB foundation 2026-06-03 19:05:22 -04:00
dispatch.py feat: add kudos.api.extraFields for static form fields 2026-11-01 00:24:46 -04:00
payout.py feat: raise multiplier rate limit to 5000 and cap multiplier payouts at 2000000 2026-10-29 15:10:44 -04:00
plugin.py fix(05.2-04): compute rollover cooldown on the DB UTC clock 2026-10-29 14:06:41 -04:00
README.md feat: add kudos.api.extraFields for static form fields 2026-11-01 00:24:46 -04:00
settings.py feat: add kudos.api.extraFields for static form fields 2026-11-01 00:24:46 -04:00
test.py feat: add kudos.api.extraFields for static form fields 2026-11-01 00:24:46 -04:00
UAT.md feat: add kudos.api.extraFields for static form fields 2026-11-01 00:24:46 -04:00

Kudos

Community bonus payout plugin for Inquisitor, a Limnoria IRC trivia bot.

Kudos rewards trivia players with bonus points at the end of each season (or any operator-defined period) based on three criteria: being new to the channel, meeting an activity threshold, and earning points since the last baseline. Payouts can be recorded locally in the database or dispatched to an external API.

Features

  • Three stacking bonus types -- players can earn all three simultaneously:
    • New Player -- fixed bonus for users who joined the channel since the last baseline
    • Activity -- fixed bonus for users who answered enough questions correctly
    • Multiplier -- percentage of alltime point gains since last baseline
  • API dispatch with configurable form field names, cookie/CSRF auth, and per-player batching
  • DB-only mode for recording payouts without dispatching to an external API
  • Payout eligibility gates -- users with MANAGE+ permission are automatically excluded; per-channel exclusion list for additional nicks
  • Retry command to re-dispatch failed payouts, by the 8-character rollover ID, with progressive backoff on transient errors and session-expiry detection that tells the operator which credentials to update
  • Per-channel configuration with three-tier setting hierarchy (channel > network > global > defaults)
  • Settings validation -- config set rejects bad values before writing, and config reset removes a channel override (or clears a bad stored value)
  • Sensitive value masking -- API credentials never appear in IRC output
  • PM enforcement -- sensitive settings can only be changed via private message
  • Double-rollover protection -- 60-second cooldown between rollovers, measured on the database clock in UTC (overridable with --force)
  • MasterLogger integration -- all actions logged to Inquisitor's log channels
  • Standalone deployment -- installs and removes without modifying Inquisitor source; owns its own migration runner and schema version table

Requirements

  • Inquisitor (must be loaded before Kudos)
  • Python 3.10+
  • MariaDB/MySQL (shared with Inquisitor)
  • requests library (pip install requests)

Installation

  1. Clone or copy this directory into your Limnoria plugins directory alongside Inquisitor:

    plugins/
      Inquisitor/
      Kudos/
    
  2. Install the requests dependency in your Limnoria virtualenv:

    source ~/limnoria/bin/activate
    pip install requests
    
  3. Load the plugin (Inquisitor must be loaded first):

    @load Kudos
    

    Kudos automatically runs its database migrations on load, creating the kudos_baselines and kudos_payout_records tables with its own kudos_schema_version tracking.

Commands

Every command needs Inquisitor MANAGE permission. Which channel it is checked on depends on the command:

  • kudos init, kudos rollover and kudos config check MANAGE on the channel the command acts on: the <channel> argument, or the current channel when you leave it out. The channel you type the command in is not checked, so MANAGE on #a does not allow kudos rollover #b.
  • With MANAGE on the target channel you can send these commands from another channel or by PM. Sensitive config settings can only be set by PM (see the sensitive settings paragraph under kudos config below).
  • kudos retry <rollover_id> checks MANAGE on the channel the rollover belongs to (the channel stored with its payout records), wherever you type it. A malformed, unknown or ambiguous ID is answered before the permission check. Run retry from the network the rollover was made on: from any other network it is refused with That rollover belongs to another network; run retry from that network.
  • A refused command gets Permission denied.. The reply names no channel, and refusals are not written to the log channels.
  • If the permission check cannot run because the Inquisitor plugin is not loaded, every command refuses with Kudos requires the Inquisitor plugin to be loaded (permission check unavailable). There is no owner bypass. If the check itself fails, the command refuses with Permission check failed; try again later. and the error goes only to the bot's local log.

kudos init <channel>

Snapshots current alltime scores for all users in the channel. This establishes the baseline for the next rollover.

Only needed once per channel: each rollover advances the baseline itself. Re-run it only to deliberately discard deltas accrued since the last rollover.

<admin> kudos init #trivia
<bot> Baseline set for #trivia: 42 users captured.

kudos rollover [--force] <channel>

Calculates bonuses based on changes since the last baseline and either records them in the database or dispatches them via API, depending on the kudos.mode setting.

On completion the rollover advances the baseline to the current scores, in the same transaction that writes the payout records. Every bonus is a delta against the latest baseline, so this is what stops the next rollover re-awarding the same bonuses.

<admin> kudos rollover #trivia
<bot> Rollover a1b2c3d4 for #trivia: 5 dispatched (350 points, 3 users, baseline advanced)

If some payouts failed or were aborted the reply lists them too, e.g. Rollover a1b2c3d4 for #trivia: 3 dispatched, 2 failed (350 points, 3 users, baseline advanced).

DB-only mode:

<admin> kudos rollover #trivia
<bot> Rollover a1b2c3d4 complete for #trivia: 5 payouts totaling 350 points across 3 users (DB-only mode). Baseline advanced.

Session expired mid-batch: if the API answers 401/403, or redirects somewhere other than the configured endpoint (typically a login page), the batch stops and the reply tells you what to do:

<admin> kudos rollover #trivia
<bot> Rollover a1b2c3d4 for #trivia: session expired/auth failed after 3 dispatched. 12 payouts marked failed. Update kudos.api.cookies (and kudos.api.csrfValue) via PM, then: kudos retry a1b2c3d4

The specific cause (HTTP 401, HTTP 403, 302 → /login.php, 3xx without Location) is deliberately not in the reply. It is written to the log channel at warn level and stored in the failing payout's error_message. Log lines carry the full 36-character rollover UUID; replies show only the first 8 characters.

The --force flag overrides the 60-second double-rollover cooldown. Put it before the channel: kudos rollover --force #trivia.

<admin> kudos rollover #trivia
<bot> Last rollover for #trivia was 23s ago (cooldown 60s). Use --force to override.

The elapsed time is measured on the database clock in UTC, so the bot host's timezone does not matter. Only a rollover that recorded payouts starts the cooldown; one that reported no qualifying payouts does not.

Unexpected errors

If kudos rollover, kudos retry or kudos config fails with an error nobody anticipated, the bot still replies, with a sentence containing "failed unexpectedly":

<bot> Rollover for #trivia failed unexpectedly. Details were logged.
<bot> Retry a1b2c3d4 failed unexpectedly. Details were logged.
<bot> Config command failed unexpectedly. Details were logged.

It writes one warn line to the log channel that names only the error type (... unexpected KeyError (traceback in the bot log)), never credentials or the values you typed. The full traceback goes to the bot's own log. A rollover that fails after its payouts were already recorded says so and gives the retry command:

<bot> Rollover a1b2c3d4 for #trivia failed unexpectedly after its payouts were recorded. Details were logged; check them, then: kudos retry a1b2c3d4

kudos config <channel> <list|get|set|reset> [args...]

Manage per-channel Kudos settings.

<admin> kudos config #trivia list
<bot> kudos.activity.amount = 50 | kudos.activity.enabled = True | kudos.api.cookies = ******** | ...

<admin> kudos config #trivia get kudos.newPlayer.amount
<bot> kudos.newPlayer.amount = 100

<admin> kudos config #trivia set kudos.newPlayer.amount 200
<bot> Applied: kudos.newPlayer.amount: 100 -> 200

set validates the value before writing it. A rejected value is reported with the reason and nothing is stored (see the validation table under Settings):

<admin> kudos config #trivia set kudos.newPlayer.amount 5000000
<bot> Invalid value for kudos.newPlayer.amount: must be between 0 and 2000000

reset removes the channel override, so the key falls back to the network, global or default value:

<admin> kudos config #trivia reset kudos.newPlayer.amount
<bot> Reset kudos.newPlayer.amount: 200 -> 100 (now inherited from network/global/default)

<admin> kudos config #trivia reset kudos.newPlayer.amount
<bot> kudos.newPlayer.amount has no channel override to reset (effective value: 100).

Usage: kudos config <channel> reset <key>. Unlike set, reset is allowed in-channel even for sensitive keys, because it reveals nothing.

Sensitive settings (kudos.api.url, kudos.api.cookies, kudos.api.csrfValue) are masked with ******** in all output and can only be set via PM:

(in channel)
<admin> kudos config #trivia set kudos.api.cookies session=abc
<bot> kudos.api.cookies is a sensitive setting. Send this command via PM to set it.

(in PM)
<admin> kudos config #trivia set kudos.api.cookies session=abc
<bot> Applied: kudos.api.cookies: ******** -> ********

Config-change log lines mask sensitive values exactly like replies, e.g. Settings changed by <prefix>: kudos.api.cookies: ******** -> ********. A reset writes Settings reset by <prefix>: ... in the same form.

The INVALID marker

If a stored value is bad (for example one written by hand into the database), Kudos ignores it, falls back to the next scope or the default, and flags the key in config list and config get:

<admin> kudos config #trivia get kudos.newPlayer.amount
<bot> kudos.newPlayer.amount = 100 [INVALID value ignored at channel scope; fix with: kudos config #trivia reset kudos.newPlayer.amount]

A channel-scope value is fixed with reset. Every command that reads an ignored value, rollover included, also writes one warn line for it to the log channel, with the value masked like replies:

Settings for #trivia: ignored invalid channel-scope value for kudos.newPlayer.amount: abc

kudos retry <rollover_id>

Re-dispatches undelivered payouts from a previous rollover — both failed records and any left pending by a dispatch that was interrupted part-way through the batch.

The argument is the full rollover ID or a unique prefix of at least 8 characters; the 8-character ID shown in the rollover reply works as is.

Not available for channels in DB-only mode, where records are a ledger rather than a dispatch queue.

<admin> kudos retry a1b2c3d4
<bot> Retry a1b2c3d4: 3 dispatched

If some payouts fail again, the reply adds the count: Retry a1b2c3d4: 2 dispatched, 1 still failed. Other replies:

<bot> No undelivered payouts for rollover a1b2c3d4.
<bot> Rollover ID must be at least 8 characters (use the ID shown in the rollover reply).
<bot> Several rollovers start with a1b2c3d4; give more characters.
<bot> No rollover found with ID a1b2c3d4.

If the session has expired again, retry gives the same credential guidance as rollover (Retry a1b2c3d4 for #trivia: session expired/auth failed after 0 dispatched. 3 payouts marked failed. Update kudos.api.cookies (and kudos.api.csrfValue) via PM, then: kudos retry a1b2c3d4).

Settings

All settings are prefixed with kudos. and stored in the shared Inquisitor settings table.

Validation

kudos config <channel> set checks the value against these rules before writing anything; a rejected value is not stored.

Setting Accepted values
bool settings (kudos.enabled, kudos.excludeManagers, kudos.newPlayer.enabled, kudos.activity.enabled, kudos.multiplier.enabled, kudos.api.batchEnabled) true, false, yes, no, on, off, 1, 0
kudos.newPlayer.amount whole number, 0-2000000
kudos.activity.threshold whole number, 1-10000
kudos.activity.amount whole number, 0-2000000
kudos.multiplier.rate number, 0.0-5000.0, finite
kudos.mode api or db
kudos.api.url http:// or https:// URL with a host
kudos.api.reasonTemplate only the four placeholders below; literal braces are doubled ({{ and }})
kudos.api.extraFields empty, or URL-encoded name=value pairs joined by &; names non-empty and unique
Free-text keys (kudos.excludedUsers, kudos.api.cookies, kudos.api.csrfField, kudos.api.csrfValue, kudos.api.fieldUsername, kudos.api.fieldAmount, kudos.api.fieldReason) unchecked

Rejection messages never repeat the value you typed, because some settings are credentials. A bad value that is already stored (for example inserted by hand) is ignored on read and flagged with the INVALID marker; see kudos config above.

General

Setting Type Default Description
kudos.enabled bool false Enable Kudos for this channel
kudos.mode str api Payout mode: api (dispatch to external service) or db (record only)
kudos.excludeManagers bool true Exclude users with MANAGE or higher permission from payouts
kudos.excludedUsers str (empty) Comma-separated list of nicks excluded from payouts

New Player Bonus

Setting Type Default Description
kudos.newPlayer.enabled bool true Enable new-player welcome bonus
kudos.newPlayer.amount int 100 Fixed bonus amount for new players

A "new player" is anyone whose channel-scoped user record was created after the last baseline timestamp and who has at least one correct answer in the cycle.

A new player with zero correct answers in their first cycle will never receive the welcome bonus: the rollover advances the baseline, so the next rollover's baseline is newer than their account and they no longer count as new.

The same applies once to a legacy baseline: a baseline recorded before correct-answer snapshots existed. A rollover from a legacy baseline has no answer counts, so it pays no welcome bonus to anyone, and the new players it skips never receive it later. This happens only once, because that rollover writes a current-format baseline.

Activity Bonus

Setting Type Default Description
kudos.activity.enabled bool true Enable activity threshold bonus
kudos.activity.threshold int 10 Minimum correct answers since baseline to qualify
kudos.activity.amount int 50 Fixed bonus amount for active players

Multiplier Bonus

Setting Type Default Description
kudos.multiplier.enabled bool true Enable point multiplier bonus
kudos.multiplier.rate float 0.1 Bonus points per trivia point earned since the baseline (e.g., 0.1 = 10%, 100 = 100 bonus points per trivia point); 0.0-5000.0. Each multiplier payout is capped at 2000000

API Configuration

Setting Type Default Sensitive Description
kudos.api.url str (empty) Yes API endpoint URL
kudos.api.cookies str (empty) Yes Cookie header for authentication
kudos.api.csrfField str (empty) No CSRF token form field name
kudos.api.csrfValue str (empty) Yes CSRF token value
kudos.api.fieldUsername str username No Form field name for username
kudos.api.fieldAmount str amount No Form field name for amount
kudos.api.fieldReason str reason No Form field name for reason
kudos.api.reasonTemplate str Kudos: {payout_types} for {username} No Reason template (see below)
kudos.api.extraFields str (empty) No Static form fields sent with every request, e.g. action=send or action=send&mode=fast (see API Dispatch)
kudos.api.batchEnabled bool true No Combine all bonus types per player into a single API call

Reason Template Placeholders

The kudos.api.reasonTemplate supports these placeholders:

  • {username} -- player's IRC nick
  • {amount} -- total payout amount
  • {date_iso} -- ISO 8601 timestamp
  • {payout_types} -- comma-separated list of bonus types (e.g., new_player,activity)

Payout Eligibility

Two gates control who receives payouts during rollover:

  1. Manager exclusion (kudos.excludeManagers, default: true): Users with MANAGE or higher Inquisitor permission are automatically excluded. This prevents operators who run kudos rollover from paying themselves.

  2. Excluded users list (kudos.excludedUsers): A comma-separated list of nicks that are excluded from payouts regardless of permission level. Case-insensitive.

kudos config #trivia set kudos.excludedUsers BotNick, AltAccount, Sponsor

Both gates are applied before any bonus calculation. Excluded users do not appear in any payout records.

Setting Hierarchy

Settings resolve in order: channel-specific > network-level > global > code defaults. This allows setting defaults across all channels while overriding per-channel where needed.

Database Schema

Kudos creates two tables and tracks its own schema version separately from Inquisitor:

kudos_baselines

Column Type Description
id INT (PK) Auto-increment ID
network VARCHAR(100) IRC network
channel VARCHAR(100) IRC channel
baseline_timestamp TIMESTAMP When the baseline was created
baseline_data JSON Snapshot: {"v": 2, "pts": {user_id: alltime_points}, "ca": {user_id: correct_answers}}
created_by VARCHAR(255) Nick of the operator who ran init or rollover
created_at TIMESTAMP Row creation time

kudos_payout_records

Column Type Description
id INT (PK) Auto-increment ID
rollover_id VARCHAR(36) UUID grouping all payouts from one rollover
network VARCHAR(100) IRC network
channel VARCHAR(100) IRC channel
user_id INT Inquisitor user ID
username VARCHAR(50) Player nick at time of payout
bonus_type ENUM new_player, activity, or multiplier
amount INT Bonus points awarded
status ENUM pending, success, or failed
error_message TEXT Error details (if failed)
dispatched_at TIMESTAMP When dispatch was attempted
created_at TIMESTAMP Row creation time

API Dispatch

When kudos.mode is api, rollover sends form-encoded POST requests to the configured endpoint:

POST {kudos.api.url}
Content-Type: application/x-www-form-urlencoded
Cookie: {kudos.api.cookies}

{extraFields}&{fieldUsername}=player1&{fieldAmount}=150&{fieldReason}=Kudos:+new_player,activity+for+player1&{csrfField}={csrfValue}

Extra fields: kudos.api.extraFields adds fixed fields to every request, for endpoints that need something like action=send. Write it URL-encoded (note=hello%20there). A name that clashes with the username, amount, reason or CSRF field is ignored: those always carry the real payout values. The value is shown unmasked in kudos config, so don't put credentials in it; use kudos.api.cookies or the CSRF pair for those.

Batching: When kudos.api.batchEnabled is true (default), all bonus types for a single player are combined into one API call with the amounts summed.

Redirects are not followed. A redirect back to the kudos.api.url host and path counts as success. Any other redirect, or a 3xx with no Location, means the session expired (typically a bounce to a login page) and is treated like 401/403.

Timeouts: 5 s to connect and 15 s to read, per request.

Error handling:

  • Auth failure (401/403, or a session-expiry redirect): Entire batch is aborted immediately. The failing payout is marked failed with the cause, the remaining payouts are marked failed with the reason "Aborted: auth failure on prior request", and the reply names the credentials to update and the kudos retry command.
  • Transient errors (connection errors, timeouts, 429 and 5xx): retried up to 3 times with 2s, 5s and 10s backoff. If every attempt fails, the payout is marked failed and the batch continues.
  • Other 4xx and SSL errors: fail immediately with no retry; the payout is marked failed and the batch continues.

Worst case: a payout that fails every attempt takes about 97 seconds (4 attempts of up to 20 s each, plus 17 s of backoff). There is no outage cap: during an outage every payout gets its full retry budget, and the command blocks until the batch finishes.

Double-pay risk: a read timeout is retried like any transient error, but the request may already have been applied by the API. The API has no idempotency key, so a retried read timeout can pay a player twice. Reconcile against the API's own records after an outage.

Undelivered payouts (failed or stranded pending) can be retried with kudos retry <rollover_id> (the 8-character ID or a unique prefix). The baseline has already advanced by then, so retrying replays the stored records rather than recomputing them.

Typical Workflow

  1. Enable Kudos for a channel:

    kudos config #trivia set kudos.enabled true
    
  2. Configure API (via PM for sensitive values):

    kudos config #trivia set kudos.api.url https://example.com/api/points
    kudos config #trivia set kudos.api.cookies session=abc123
    
  3. Set baseline at the start of a season:

    kudos init #trivia
    
  4. Run rollover at the end of the season:

    kudos rollover #trivia
    
  5. Retry failures if any payouts failed:

    kudos retry a1b2c3d4
    

The rollover advances the baseline itself, so the next season starts measuring from the moment of the last rollover — no second kudos init required.

Running Tests

Tests use Limnoria's supybot-test runner, in a durable virtualenv kept outside the repo.

One-time setup:

python3 -m venv ~/.venvs/kudos-test
~/.venvs/kudos-test/bin/pip install limnoria requests rapidfuzz inflect
mkdir -p ~/.venvs/kudos-test/run
ln -s /home/debian/Inquisitor/Kudos ~/.venvs/kudos-test/run/Kudos

Every run:

cd ~/.venvs/kudos-test/run && PYTHONPATH=/home/debian/Inquisitor ~/.venvs/kudos-test/bin/supybot-test --disable-multiprocessing Kudos

Expected output ends with "Ran 232 tests in …" and "OK".

Notes:

  • PYTHONPATH is needed because the tests import Inquisitor.*.
  • supybot-test loads the plugin from the current directory, hence the run directory and the Kudos symlink. This also keeps conf/logs litter out of the repo.
  • It cannot select a single TestCase, and the whole Kudos suite takes about 5 seconds.
  • No MariaDB server or driver is needed.
  • To test a git worktree, point a symlink named Kudos at the worktree instead.
  • Adding Inquisitor as a second plugin argument also runs Inquisitor's own suite.

Architecture

Kudos/
  __init__.py          # Limnoria plugin loader
  config.py            # Limnoria config registry (minimal)
  plugin.py            # Command handlers (init, rollover, config, retry)
  payout.py            # Payout calculation engine and baseline management
  dispatch.py          # API dispatch with batching and auth-failure abort
  settings.py          # Settings definitions, metadata, and manager
  test.py              # 232 unit tests
  db/
    __init__.py
    migrations/
      __init__.py
      001_initial.py   # Creates kudos_baselines and kudos_payout_records
      runner.py        # KudosMigrationRunner (own schema_version table)

Kudos is fully standalone: it imports Inquisitor's ConnectionPool, Permission, and MasterLogger singletons but never modifies Inquisitor's source code, settings, or schema. All Kudos settings live in Kudos/settings.py with the kudos. prefix.

License

Copyright (c) 2024, rb14060. All rights reserved.