ADR-005: Custom Asyncio IRC Client¶
Status¶
Accepted
Date¶
2024-03-01
Context¶
MAID supports bridging game channels to external services, including IRC.
The IRC bridge (IRCBridge in packages/maid-engine/src/maid_engine/bridges/irc_bridge.py)
must connect to IRC servers, join channels, relay messages bidirectionally between
the game and IRC, and handle disconnections gracefully.
Several existing Python IRC libraries exist: irc3 (asyncio-based, Venusian
decorators), pydle (asyncio-based, PEP 3156), and irc (synchronous,
callback-based). Each was evaluated against MAID's requirements.
Decision¶
Implement a custom IRC client using raw asyncio.StreamReader and
asyncio.StreamWriter rather than depending on an existing IRC library.
The implementation in IRCBridge (approximately 700 lines) includes:
- RFC 1459 message parsing via
_parse_irc_message(): Extracts prefix, command, and parameters from raw IRC lines, handling edge cases like malformed messages with missing commands. - Connection lifecycle:
connect()establishes a TCP connection (with optional TLS viassl.create_default_context()), sends PASS/NICK/USER handshake, and starts the_message_loop()coroutine. - Automatic reconnection:
_schedule_reconnect()implements exponential backoff (configurable viaIRCBridgeConfig.reconnect_delay,reconnect_backoff_factor,max_reconnect_delay,max_reconnect_attempts). - Security: Refuses to send passwords over unencrypted connections
(SSL required when
config.passwordis set), supports certificate verification toggle for testing with self-signed certs. - Rate limiting:
_wait_for_rate_limit()enforces minimum delay between outgoing messages to avoid flood disconnects. - Nick collision handling: Appends underscores on
433 ERR_NICKNAMEINUSE, with a maximum retry count (_max_nick_retries = 5). - Message splitting:
_split_message()breaks long messages to fit within IRC's approximately 512-byte line limit, handling multi-byte UTF-8 characters. - Kick auto-rejoin:
_delayed_rejoin()re-joins channels after being kicked, with tracked tasks that are cleaned up on disconnect.
The bridge implements the ExternalBridge protocol defined in
packages/maid-engine/src/maid_engine/bridges/protocol.py, alongside the
DiscordBridge and RSSFeedManager.
Consequences¶
Positive¶
- Full control: The IRC protocol is simple enough (text-based, line-delimited) that a custom implementation is straightforward. We have complete control over reconnection strategy, error handling, and message formatting.
- No extra dependencies: No additional PyPI packages are needed for IRC support.
The implementation uses only
asyncioandsslfrom the standard library. - Async-native: The client integrates directly with MAID's asyncio event loop. There is no adapter layer or threading bridge between the IRC library and the game engine.
- Security by design: The SSL-required-for-passwords check was trivial to implement because we control the entire connection flow.
Negative¶
- Maintenance burden: IRC has many edge cases (CTCP, DCC, server-specific extensions). The custom client handles only the subset needed for bridging (PRIVMSG, JOIN, KICK, PING/PONG, numeric replies). Supporting additional features requires manual implementation.
- No IRC specification coverage: A full IRC library would handle SASL authentication, IRCv3 capabilities negotiation, and channel modes. The custom client does not support these.
- Testing complexity: The client must be tested against mock IRC servers.
Libraries like
irc3come with their own test infrastructure.
Alternatives Considered¶
irc3 Library¶
irc3 provides an asyncio-based IRC client with decorator-based event handling.
Rejected because it uses Venusian for decorator scanning (an unusual dependency),
its plugin architecture conflicts with MAID's content pack system, and it pulls
in several transitive dependencies.
pydle Library¶
pydle is a modern asyncio IRC library. Rejected because it was unmaintained
at the time of evaluation, and its class-based connection model would require
wrapping to fit the ExternalBridge protocol.
irc Library¶
The irc library is synchronous and callback-based. Rejected because it would
require running in a separate thread with an adapter to bridge between synchronous
callbacks and MAID's async event loop.