Three rounds of refinement on the Claude integration — a clean architecture
for output routing, per-nick rate limiting to prevent API abuse, and five
new test cases to lock it all down. The bot is now production-grade on !ai.
“Protego Maxima! The shields are up. The bot is hardened.” 🛡️✨
claudeAI() Output Callback RefactorBefore: _cmd_ai (Partyline) used local *Mediabot::Helpers::botPrivmsg
to monkey-patch the output function. This is fragile, non-reentrant, and
invisible to static analysis tools.
After: claudeAI() now accepts an optional $output_fn CODE reference
as its first argument after $chan. When set, every output line calls
$output_fn->($text) instead of botPrivmsg.
# Signature
claudeAI($self, $message, $nick, $chan, @args) # IRC — botPrivmsg
claudeAI($self, $message, $nick, $chan, $fn, @args) # callback mode
# Dispatcher inside claudeAI
my $_out = sub {
my ($text) = @_;
if ($output_fn) { $output_fn->($text); }
else { botPrivmsg($self, $chan, $text); }
};
_cmd_ai (Partyline) now passes a closure that writes to the stream:
my $output_fn = sub {
my ($text) = @_;
$text =~ s/[\r\n]+$//;
$stream->write("[Claude] $text\r\n");
};
Mediabot::External::claudeAI($bot, undef, $pl_nick, $chan, $output_fn, split(/\s+/, $prompt));
Side effect: usleep($sleep_us) is skipped when $output_fn is set —
no artificial delay in Partyline responses.
Result: _cmd_ai went from 55 lines to 34. No monkey-patching.
!aiMax 5 requests per 60 seconds per nick+channel pair. Sliding window reset on expiry.
<teuk> m ai … (×5 quickly)
<teuk> m ai one more
<mediabotv3> -teuk- Rate limit: please wait 47s before using !ai again.
Implementation:
my $rl = $self->{_claude_ratelimit}{$rl_key} //= { count => 0, window => $now };
if ($now - $rl->{window} >= 60) { $rl->{count} = 0; $rl->{window} = $now; }
if (++$rl->{count} > 5) {
botNotice($self, $nick, "Rate limit: please wait ${wait}s ...");
$self->{metrics}->inc('mediabot_claude_ratelimit_total') if $self->{metrics};
return;
}
Rate limiting is skipped for Partyline sessions (already authenticated
operators) — detected by checking unless ($output_fn).
New Prometheus counter: mediabot_claude_ratelimit_total.
| # | File | What |
|---|---|---|
| 225 | 225_external_claude_history_cmd.t |
!ai history subcommand: accesses _claude_history, uses botNotice, truncates at 120 chars |
| 226 | 226_external_claude_ratelimit.t |
Rate limiter: _claude_ratelimit hash, 60s window, botNotice message, Partyline bypass, Prometheus counter |
| 227 | 227_external_claude_callback.t |
$output_fn callback: CODE ref detection, $_out dispatcher, chunks via $_out, no monkey-patch in _cmd_ai |
| 228 | 228_external_yt_search_meta.t |
!yt search metadata: contentDetails+statistics API call, ISO 8601 parsing, M/K view format |
| 229 | 229_external_claude_prompt_cache.t |
Prompt cache: MD5 key, 60s TTL, 120s eviction, history updated on hit |
All follow the project’s static-analysis pattern — no live server required. Total test suite: 224 cases.
| File | Changes |
|---|---|
External.pm |
R1 $output_fn callback + $_out dispatcher; R2 rate limiter + claude_ratelimit_total |
Partyline.pm |
R1 _cmd_ai rewrite (55→34 lines, no monkey-patch) |
t/cases/225…229 |
5 new test cases |
You must be logged in to reply.