The wand was raised. Seven bugs disarmed, four quality improvements landed, and the codebase emerges cleaner than ever. No new features this time β just the quiet, necessary work of making everything that already exists work correctly.
βExpelliarmus Maxima! Every bug yields. Every edge case surrenders.β π°π
.bcast: Wrong Session HashModule: Partyline.pm β _cmd_bcast
_cmd_bcast was reading $self->{sessions}{$id} for the operatorβs level.
Auth data is stored in $self->{users}{$id}. The level resolved to
undef // 0 β which meant the <= 1 check always passed, and
any authenticated Partyline user could broadcast to all channels,
regardless of actual level.
# Before β always 0, check always passes
my $session = $self->{sessions}{$id} // {};
my $level = $session->{level} // 0;
# After β correct hash, safe default
my $session = $self->{users}{$id} // {};
my $level = $session->{level} // 99;
The log line also showed undef for the operator nick β fixed implicitly
by reading from the correct hash.
!define: HTML Entities Not DecodedModule: UserCommands.pm β mbDefine_ctx
Wiktionary definitions strip HTML tags correctly (<[^>]+>), but the
content also contains HTML entities like &, ', <. These
were passed through raw to IRC.
# Before
serendipity: An unsought & unexpected, yet fortunate discovery...
# After
serendipity: An unsought & unexpected, yet fortunate discovery...
Fix: added HTML::Entities::decode_entities($first_def) after the tag
strip β consistent with how mbTrivia_ctx already handled its Wiktionary
content.
checkTriviaAnswer: No eval GuardModule: UserCommands.pm β checkTriviaAnswer
The regex lc($text) =~ /\Q$trivia->{answer}\E/ had no eval {} wrapper
and no defined check on $trivia->{answer}. While \Q...\E prevents
ReDoS, an undef answer could still emit a Perl warning on every public
message during an active trivia.
# After
return unless defined $trivia->{answer};
my $matched = eval {
lc($text) eq $trivia->{answer}
|| lc($text) =~ /\Q$trivia->{answer}\E/
};
return unless $matched;
!streak: Float Comparison on ->daysModule: UserCommands.pm β mbStreak_ctx
Time::Pieceβs subtraction operator returns a Time::Seconds object.
Calling ->days on it yields a float, not an integer. The comparison
($d1 - $d2)->days == 1 could silently fail on some platforms.
# Before β fragile float comparison
last unless ($d1 - $d2)->days == 1;
# After β safe rounding
last unless int(($d1 - $d2)->days + 0.5) == 1; # B4/fix
!active: No Output LimitModule: UserCommands.pm β mbActive_ctx
!active 24h on a busy channel could return 50+ nicks in a single
JOIN on CHANNEL_LOG. The resulting IRC line would exceed 512 bytes
and be silently truncated by the server.
Two-part fix:
-- SQL cap
ORDER BY cl.nick LIMIT 30
# Output truncation
if (length($list) > 350) { $list = substr($list, 0, 347) . '...'; }
!alias: Cache Never Loaded at BootModule: UserCommands.pm β mbAlias_ctx
mbAlias_ctx maintains an in-memory _alias_cache hash written on
every !alias set or !alias del. But on bot restart, the cache starts
empty β aliases stored in BOT_ALIAS were never loaded back from DB.
Fix: lazy-load on first !alias call via a _alias_cache_loaded flag:
unless ($self->{_alias_cache_loaded}) {
my $sth = $self->{dbh}->prepare('SELECT alias, command FROM BOT_ALIAS');
if ($sth && $sth->execute()) {
while (my $r = $sth->fetchrow_hashref) {
$self->{_alias_cache}{ $r->{alias} } = $r->{command};
}
$sth->finish;
}
$self->{_alias_cache_loaded} = 1;
}
!poll: No Automatic ExpiryModule: UserCommands.pm β mbPoll_ctx / mbVote_ctx
A poll stayed active indefinitely if !pollstop was forgotten. On the
next !vote, the bot would still accept votes on a days-old poll.
Fix: deadline = time() + 300 (5 minutes) set at poll creation.
mbVote_ctx now checks the deadline before accepting a vote:
if ($poll && $poll->{active} && time() > ($poll->{deadline} // 0)) {
$poll->{active} = 0;
botPrivmsg($self, $channel, 'Poll expired. Use !pollresult to see results.');
return;
}
!calc: Inner Subs Redefined on Every CallModule: DBCommands.pm β mbCalc_ctx
sub round, sub deg2rad, and sub rad2deg are defined inside the
eval block that runs on every !calc call. Perl emits a
Subroutine ... redefined warning on each invocation after the first,
flooding the logs.
Fix: one line, zero risk.
no warnings 'redefine';
sub round { int($_[0] + 0.5 * ($_[0] >= 0 ? 1 : -1)) }
sub deg2rad { $_[0] * 3.14159265358979 / 180 }
sub rad2deg { $_[0] * 180 / 3.14159265358979 }
processKarma: No Per-Message LimitModule: UserCommands.pm β processKarma
A single message like a++ b++ c++ d++ e++ f++ would trigger 6 DB
updates and 6 bot responses β a trivial flood vector.
Fix: cap at 3 karma changes per message.
my $karma_hits = 0;
while ($text =~ /([^\s+\-]{2,32})(\+\+|--)/g) {
last if ++$karma_hits > 3;
...
}
!stats Karma Block: Direct dbh AccessModule: UserCommands.pm β mbStats_ctx (S7 karma block)
The karma lookup added in the previous session used $self->{dbh}
directly. If the DB connection had dropped between the main query and
this block, the karma queries would fail silently. Fixed to use
ensure_connected:
my $dbh_k = eval { $self->{db}->ensure_connected } // $self->{dbh};
!note: time() as Note IDModule: UserCommands.pm β mbNote_ctx
Notes used id => time() β two notes added in the same second would
have the same display ID. Cosmetic only (notes are stored in an array,
not a DB), but confusing for the user.
# Before
push @notes, { id => time(), text => $text };
# After
my $note_id = scalar(@notes) + 1;
push @notes, { id => $note_id, text => $text };
| # | What |
|---|---|
| S2 | logBot() added to !karma, !poll, !vote, !pollstop, !trivia β all now traceable in logs |
| S3 | deliverReminders now calls ensure_connected before any DB query |
| S6 | New Scheduler task reminder_purge β runs daily, deletes delivered/cancelled reminders older than 7 days |
| S7 | !stats <nick> now shows karma inline: teuk (Owner): 1247 msgs (12.4%) | karma +14 | first seen: ... |
| File | Changes |
|---|---|
UserCommands.pm |
B3, B4, B5, B6, B7, C2, C3, C4, S2, S3, S7 |
DBCommands.pm |
C1 |
Partyline.pm |
B1 |
mediabot.pl |
S6 (Scheduler task) |
You must be logged in to reply.