Forum teuk.org

Context-Driven Dispatch Refactoring: Completing the Command Architecture

in Mediabot · started by TeuK · 2d ago

TeuK · 2d ago

Background

Mediabot v3 has been using Mediabot::Context and Mediabot::Command objects for a while, but their adoption was partial and inconsistent. Two dispatch functions — mbCommandPublic and mbCommandPrivate — were mixing three different coding styles simultaneously: legacy ($self, $message, $sNick, $sChannel, @tArgs) signatures, closures capturing variables from the outer scope, and the new Context-based pattern. Authentication checks were duplicated: once via require_level in Context, and again manually inside individual handlers using checkUserLevel, getNickInfo, and local $auth / $iMatchingUser* variables.

This post documents the refactoring work done to bring the codebase to a consistent state.


What Changed

Mediabot::Context — Completed

Context.pm was a skeleton with accessors but missing the reply and logging methods that Mediabot::Command already referenced. Added:

  • reply($msg) — sends a PRIVMSG to the current channel
  • reply_channel($msg) — explicit alias of reply
  • reply_private($msg) — sends a NOTICE to the calling nick
  • is_private() — true if the command came from a private message
  • channel_obj() — returns the Mediabot::Channel object for the current channel
  • command_obj() — returns the attached Mediabot::Command object
  • log_info($msg), log_error($msg) — convenience logging wrappers

Mediabot::Command — Aligned

require_auth_level was reimplementing auth logic that already existed in Context->require_level. It now delegates directly:

sub require_auth_level {
    my ($self, $level) = @_;
    return $self->context->require_level($level);
}

mbCommandPublic — Cleaned Up

  • lc($sCommand) stored in my $cmd once — no more repeated lc() calls
  • Double exists $command_map{...} lookup reduced to a single if (my $handler = ...)
  • Redundant Q entry removed (already covered by lc normalization)
  • help inline closure extracted to mbHelp_ctx($ctx)
  • botNickTriggered block extracted to mbHandleNickTriggered($ctx, $what)
  • Mediabot::Command object instantiated and attached to Context via $ctx->{command_obj}

mbCommandPrivate — Dual Path Eliminated

The most impactful change. The old implementation maintained two parallel structures:

my %command_table = ( ... );   # handler refs
my %ctx_commands  = map { ... } qw( ... );  # list of which ones take Context

Any new command had to be registered in both — a silent bug waiting to happen. chanlist was in %ctx_commands but missing from %command_table, making it unreachable in private.

The new implementation:

  • Single %command_table with closures, exactly like mbCommandPublic
  • Context built once at entry, passed to all handlers
  • Mediabot::Command attached to Context
  • Legacy handlers (pass, ident, topic, debug, radio) wrapped in thin _ctx shims

Legacy Handler Wrappers

Radio commands and a few others still had the old signature. Rather than rewriting hundreds of lines of logic, thin wrappers were added:

sub playRadio_ctx {
    my ($ctx) = @_;
    playRadio($ctx->bot, $ctx->message, $ctx->nick, $ctx->channel, @{ $ctx->args });
}

Same pattern for rplayRadio_ctx, queueRadio_ctx, nextRadio_ctx, update_ctx, radioPub_ctx, userPass_ctx, userIdent_ctx, userTopicChannel_ctx.

versionCheck — Migrated to Context

# Before
sub versionCheck {
    my ($self, $message, $sChannel, $sNick) = @_;
    botPrivmsg($self, $sChannel, $sMsg);
}

# After
sub versionCheck {
    my ($ctx) = @_;
    $ctx->reply($sMsg);
}

getReplyTarget — Removed

This helper existed to figure out whether to reply to a channel or a nick. It became obsolete once mbCommandPrivate started building its Context with channel => $sNick directly.

channelList_ctx — Cleaned

30 lines of manual auth checking replaced by one line:

return unless $ctx->require_level('Master');

Handler Auth Blocks — Migrated

Seven _ctx handlers were still doing manual authentication: resolving the user object with a get_user_from_message fallback, checking $auth, and calling checkUserLevel. All replaced with $ctx->require_level:

Handler Before After
radioNext_ctx 15 lines + checkUserLevel require_level('Administrator')
mbModUser_ctx 20 lines + $auth check require_level('Master')
setChannelAntiFloodParams_ctx 25 lines + $auth + checkUserLevel require_level('Master')
mbRehash_ctx 80 lines incl. manual autologin attempt require_level('Master')
mp3_ctx 15 lines + checkUserLevel require_level('User')
mbQuotes_ctx checkUserLevel + $auth var has_level('User') (two-tier logic preserved)
spike dispatcher entry botPrivmsg($self, $sChannel, ...) closure $ctx->reply(...)

mbRehash_ctx deserves a special mention: it had 80 lines of manual autologin logic including DB queries, mask matching, and multiple noticeConsoleChan debug calls. This was development scaffolding that had never been cleaned up. require_level handles all of this transparently through Context->user and _ensure_logged_in_state.


What Was Left Intentionally Unchanged

Not everything was migrated, by design:

  • Radio legacy bodies (playRadio, rplayRadio, queueRadio, nextRadio, update) — hundreds of lines of domain logic. Wrapped, not rewritten.
  • checkUserChannelLevel-based handlers (channelSet_ctx, userOpChannel_ctx, etc.) — these use per-channel privilege levels, which are distinct from global require_level. Correct as-is.
  • mbRestart, mbJump — legacy signature, not in the dispatcher, out of scope.
  • mbQuoteAdd, getNickInfoWhois — internal helpers called by other subs, not handlers.

Test Coverage

The live test framework was extended from 13 to 41 tests across 7 files, covering:

  • Routing verification (PRIVMSG canal vs NOTICE nick for each response type)
  • Exact auth messages (“Unknown user”, “Bad password”, “successful”)
  • Authenticated whoami confirming nickname and level in the response
  • chanlist in private (was broken due to P11 — missing from %command_table)
  • users, chaninfo, seen with authenticated context
  • Nick-triggered natural language patterns (deterministic ones only)
  • !die as the last test, confirming the bot’s QUIT is received by the spy

The schema was extended with a second test user mboper (Master level, password-authenticated, hostmask mbspy*!*@*) to enable authenticated command testing without relying on autologin.


Commit

🧹 Hermione cleaned the common room — Context.pm completed, Command.pm aligned,
all _ctx handlers migrated to require_level, radio wrappers extracted,
legacy auth blocks banished

You must be logged in to reply.