Skip to content

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 via ssl.create_default_context()), sends PASS/NICK/USER handshake, and starts the _message_loop() coroutine.
  • Automatic reconnection: _schedule_reconnect() implements exponential backoff (configurable via IRCBridgeConfig.reconnect_delay, reconnect_backoff_factor, max_reconnect_delay, max_reconnect_attempts).
  • Security: Refuses to send passwords over unencrypted connections (SSL required when config.password is 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 asyncio and ssl from 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 irc3 come 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.