From 14a6f74aa692bd8db3d88bbf68b6b22c7a5317d1 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Sun, 8 Mar 2020 03:57:03 -0400 Subject: Publish "How I Relay Chat" --- www/text.causal.agency/008-how-irc.7 | 193 +++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 www/text.causal.agency/008-how-irc.7 (limited to 'www/text.causal.agency/008-how-irc.7') diff --git a/www/text.causal.agency/008-how-irc.7 b/www/text.causal.agency/008-how-irc.7 new file mode 100644 index 00000000..aba1bbf9 --- /dev/null +++ b/www/text.causal.agency/008-how-irc.7 @@ -0,0 +1,193 @@ +.Dd March 8, 2020 +.Dt HOW-IRC 7 +.Os "Causal Agency" +. +.Sh NAME +.Nm How I Relay Chat +.Nd in code +. +.Sh DESCRIPTION +I've been writing a lot of IRC software lately +.Pq Sx SEE ALSO , +and developed some nice code patterns +that I've been reusing. +Here they are. +. +.Ss 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. +.Bd -literal -offset indent +enum { MessageCap = 8191 + 512 }; +enum { ParamCap = 15 }; +.Ed +. +.Pp +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. +.Bd -literal -offset indent +#define ENUM_TAG \e + X("msgid", TagMsgid) \e + 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 +}; +.Ed +. +.Pp +The TagNames array is used by the parsing function +to assign tag values into the message structure, +which looks like this: +.Bd -literal -offset indent +struct Message { + char *tags[TagCap]; + char *nick; + char *user; + char *host; + char *cmd; + char *params[ParamCap]; +}; +.Ed +. +.Pp +I'm a fan of using +.Xr 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: +.Bd -literal -offset indent +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, " "); +} +.Ed +. +.Ss 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. +.Bd -literal -offset indent +typedef void HandlerFn(struct Message *msg); + +static const struct Handler { + const char *cmd; + HandlerFn *fn; +} Handlers[] = { + { "001", handleReplyWelcome }, + { "PING", handlePing }, + { "PRIVMSG", handlePrivmsg }, +}; +.Ed +. +.Pp +Since I keep these arrays sorted anyway, +I started using the standard +.Xr 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. +.Bd -literal -offset indent +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); +} +.Ed +. +.Ss Capabilities +For IRCv3 capabilties +I use X macros again, +this time with another handy macro +for declaring bit flag enums. +.Bd -literal -offset indent +#define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit + +#define ENUM_CAP \e + X("message-tags", CapMessageTags) \e + X("sasl", CapSASL) \e + 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 +}; +.Ed +. +.Pp +The +.Fn BIT +macro declares, for example, +.Dv CapSASL +as the bit flag and +.Dv CapSASLBit +as the corresponding index. +The +.Vt "enum Cap" +is used as a set, +for example checking if SASL is enabled with +.Ql caps & CapSASL . +. +.Pp +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. +. +.Sh SEE ALSO +.Bl -item -compact +.It +.Lk https://git.causal.agency/pounce/about "IRC bouncer" +.It +.Lk https://git.causal.agency/litterbox/about "IRC logger" +.It +.Lk https://git.causal.agency/catgirl/about "IRC client" +.El +. +.Sh AUTHORS +.An June Bug Aq Mt june@causal.agency -- cgit 1.4.1