- Python 100%
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NDQPDBPWAPjU572y6jcuWF |
||
|---|---|---|
| .planning | ||
| db | ||
| .gitignore | ||
| __init__.py | ||
| CHANGELOG.md | ||
| config.py | ||
| dispatch.py | ||
| payout.py | ||
| plugin.py | ||
| README.md | ||
| settings.py | ||
| test.py | ||
| UAT.md | ||
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 setrejects bad values before writing, andconfig resetremoves 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)
requestslibrary (pip install requests)
Installation
-
Clone or copy this directory into your Limnoria plugins directory alongside Inquisitor:
plugins/ Inquisitor/ Kudos/ -
Install the
requestsdependency in your Limnoria virtualenv:source ~/limnoria/bin/activate pip install requests -
Load the plugin (Inquisitor must be loaded first):
@load KudosKudos automatically runs its database migrations on load, creating the
kudos_baselinesandkudos_payout_recordstables with its ownkudos_schema_versiontracking.
Commands
Every command needs Inquisitor MANAGE permission. Which channel it is checked on depends on the command:
kudos init,kudos rolloverandkudos configcheck 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#adoes not allowkudos 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 configbelow). 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 withThat 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 withPermission 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:
-
Manager exclusion (
kudos.excludeManagers, default:true): Users with MANAGE or higher Inquisitor permission are automatically excluded. This prevents operators who runkudos rolloverfrom paying themselves. -
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 retrycommand. - 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
-
Enable Kudos for a channel:
kudos config #trivia set kudos.enabled true -
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 -
Set baseline at the start of a season:
kudos init #trivia -
Run rollover at the end of the season:
kudos rollover #trivia -
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:
PYTHONPATHis needed because the tests importInquisitor.*.supybot-testloads the plugin from the current directory, hence the run directory and theKudossymlink. 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
Kudosat the worktree instead. - Adding
Inquisitoras 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.