HOW-IRC(7) Miscellaneous Information Manual HOW-IRC(7) NAME How I Relay Chat – in code DESCRIPTION I've been writing a lot of IRC software lately (SEE ALSO), and developed some nice code patterns that I've been reusing. Here they are. Parsing I use fixed size buffers almost everywhere, so it's necessary to know IRC's size limits. A traditional IRC message is a maximum of 512 bytes, but the IRCv3 message-tags spec adds (unreasonably, in my opinion) 8191 bytes for tags. IRC messages also have a maximum of 15 command parameters. enum { MessageCap = 8191 + 512 }; enum { ParamCap = 15 }; If I'm using tags, I'll use X macros to declare the set I care about. X macros are a way of maintaining parallel arrays, or in this case an enum and an array. #define ENUM_TAG \ X("msgid", TagMsgid) \ X("time", TagTime) enum Tag { #define X(name, id) id, ENUM_TAG #undef X TagCap, }; static const char *TagNames[TagCap] = { #define X(name, id) [id] = name, ENUM_TAG #undef X }; The TagNames array is used by the parsing function to assign tag values into the message structure, which looks like this: struct Message { char *tags[TagCap]; char *nick; char *user; char *host; char *cmd; char *params[ParamCap]; }; I'm a fan of using strsep(3) for simple parsing. Although it modifies its input (replacing delimiters with NUL terminators), since the raw message is in a static buffer, it is ideal for so-called zero-copy parsing. I'm not going to include the whole parsing function here, but I will at least include the part that many get wrong, which is dealing with the colon-prefixed trailing parameter: msg.cmd = strsep(&line, " "); for (int i = 0; line && i < ParamCap; ++i) { if (line[0] == ':') { msg.params[i] = &line[1]; break; } msg.params[i] = strsep(&line, " "); } Handling To handle IRC commands and replies I add handler functions to a big array. I usually have some form of helper as well to check the number of expected parameters. typedef void HandlerFn(struct Message *msg); static const struct Handler { const char *cmd; HandlerFn *fn; } Handlers[] = { { "001", handleReplyWelcome }, { "PING", handlePing }, { "PRIVMSG", handlePrivmsg }, }; Since I keep these arrays sorted anyway, I started using the standard bsearch(3) function, but a basic for loop probably works just as well. I do wish I could compile-time assert that the array really is sorted, though. static int compar(const void *cmd, const void *_handler) { const struct Handler *handler = _handler; return strcmp(cmd, handler->cmd); } void handle(struct Message msg) { if (!msg.cmd) return; const struct Handler *handler = bsearch( msg.cmd, Handlers, ARRAY_LEN(Handlers), sizeof(*handler), compar ); if (handler) handler->fn(&msg); } Capabilities For IRCv3 capabilties I use X macros again, this time with another handy macro for declaring bit flag enums. #define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit #define ENUM_CAP \ X("message-tags", CapMessageTags) \ X("sasl", CapSASL) \ X("server-time", CapServerTime) enum Cap { #define X(name, id) BIT(id), ENUM_CAP #undef X }; static const char *CapNames[] = { #define X(name, id) [id##Bit] = name, ENUM_CAP #undef X }; The BIT() macro declares, for example, CapSASL as the bit flag and CapSASLBit as the corresponding index. The enum Cap is used as a set, for example checking if SASL is enabled with ‘caps & CapSASL’. These patterns are serving my IRC software well, and my IRC projects are serving me well. It is immensely satisfying to be (near) constantly using software that I wrote myself and am happy with, regardless of how niche it may be. SEE ALSO IRC bouncer: https://git.causal.agency/pounce/about IRC logger: https://git.causal.agency/litterbox/about IRC client: https://git.causal.agency/catgirl/about AUTHORS June Bug Causal Agency March 8, 2020 Causal Agency