💾 Archived View for gemini.abiscuola.com › gemlog › 2024 › 04 › 21 › having-fun-building-irc-bridges… captured on 2024-05-10 at 10:39:31. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

Having fun building IRC bridges

With the announcement of the first supported "transport" for IRCTk, I think it's a nice idea to put some more words on the experience building such a software.

Fossil transport announcement.

A small recap: IRCTk, the IRC client I'm working on with some other people is, in essence, a GUI front-end to whatever program that can either connect to a real IRC server, or that can speak a minimal amount of the IRC protocol to act like a server.

The main advantage of this kind of design, is the possibility to reuse existing programs without the need to reinvent the wheel for things like managing connections or TLS. As mentioned in one of the examples, "connecting" IRCTk to Libera.chat with TLS, is achieved, on OpenBSD, with the simple use of the nc(1) command:

$ nc -c irc.libera.chat 6697

Given that IRCTk is completely written in TCL, it means that it works on any platform where TCL is supported. Running it on Linux, simply means finding an equivalent program like OpenBSD's nc(1), or writing a comparable one. In many modern programming languages, this is a really simple thing to do, given IRC is a line-based protocol.

Read a line from a socket, write the line on standard output and viceversa and, you are done.

But what about making a different protocol speak IRC?

In that case, three things are required:

Of course, a bridge may not be limited to chat protocols. You may want to implement something for the, so-called "ChatOps", or you may want to create a bridge to speak whatever API for any remote service that comes to your mind. In the end, if you can map things to IRC, even adding to your bridge custom commands, you can do it.

The Fossil chat

The Fossil chat was strictly designed to be used from a web browser. IMHO, the Fossil developers did a good job at it, given that the chat has enough nice features to be useful, including markdown formatting and file transfer capabilities.

The chat implementation speaks a simple JSON-over-HTTP protocol, where, the web version, fetches new messages using a simple "hanging GET" from the browser. The messages are relayed to the client wrapped in a JSON object, including the message timestamp, file attachment name and size and the message body itself as an HTML snippet that, in the end, is rendered by the client's browser in the chat window. Following calls to the "poll" endpoint, passes the ID of the last received message, in this way, the Fossil server knows what messages are missing on the client. If no messages are ready to be delivered, the request will hang until a timeout occurs, to be then tried again.

Another feature of this design, is that the Fossil chat has history capabilities, where a client can request the "last n messages".

Luckily, the fossil server has an option to relay "raw" text messages, without any particular interpretation. This made our life much easier, while implementing the bridge.

The Fossil Chat.

Creating the C code to talk to the chat, meant handling:

Here comes the first problem: It's not possible to handle sending messages and receiving them in the same thread, given that the receiving part is designed to hang waiting for a response.

It means that, at least, a multi-threaded approach is required.

Talking to the client

Implementing the a minimal server-side IRC, turned out to be surprisingly easy. Considering that IRCTk is strictly event-driven, not many commands are required. In the case of this specific bridge, just a handful of things needed to be written:

In the end, the majority of the code takes care of parsing the message IRCv3 tags (if any), the message command and it's text part.

How to split the message body parts was, purposefully, left to the main code to handle.

How everything works.

The program is structured with two processes, where one listen on messages coming from the IRC client, while the other listen on the HTTP endpoint to receive messages from the fossil chat.

Initially, the main process waits for a PASS + NICK couple of messages, those are standard in IRC and are used by the bridge as authentication details to the remote Fossil chat instance. This simplifies things a lot, given that the connection details can be managed directly from the GUI while configuring the connection and, in the end, the details are remembered by IRCTk for the following sessions.

Once the bridge receives the authentication details, it tries to login to the remote chat. If the login is successful, the authentication cookie is saved in memory to be re-used later and the "welcome" message is sent to the client.

But, how can the client "know" what channel to join? Here IRC shines: a JOIN command is sent back to the client for the authenticated user. This causes IRCTk to "open" a new channel in the GUI, a NAMESREPLY for the authenticated user is sent, in order for it to be listed among the users in the new channel, the TOPIC is set and, at the end, any message from the chat history is posted by the bridge.

One issue we faced, is that the Fossil chat has no concept of "online users", as such, this is simulated by the bridge issuing NAMES for every unknown user it sees a message from and, once the bootstrap is done, by issuing JOINs for any unknown user that sends a message. This maps nicely by showing a "x has joined" when the first message of the session for a new user is received.

At this point in time, one process is forked. The new process handles the polling from the Fossil chat repository, while the parent listens for messages coming from the client. If the program dies for whatever reason, IRCTk will recognized that the pipe is closed and will issue a "reconnection", by running the same program again with the same configuration details. This means that, in the bridge, there is no logic to handle such events, simplifying the codebase a lot. If a problem arises, the bridge will just "fail fast", leaving re-establishing the session to the client running it. Bonus point: the processes are completely sandboxed. They don't have access to the filesystem and the amount of system calls available is very limited as well.

Given that the two processes shares the same data structures, we were able to issue just one login for the whole program, sharing the various handlers after the fact. Using processes instead of threads, saved the code from the need to handle any particular synchronization, where things like libcurl is notoriuos to have issues. The only potential problem is with standard input and standard output, where it's possible to have lines commingled together. However, we think it will never be a problem in practice, given that the chat is to be used by very small teams (IRC is still out there for those of us with the need for more scale).

Right now, the bridge does not handle file attachments, but we plan to support it, given that the code to download the files is already written. Possibly, a custom FILESEND IRC command could be implemented to transfer files from the local system and, potentially, downloading the files locally by sending a file path (maybe stored in /tmp), to the client via a PRIVMSG. However, it's not a priority.

Conclusions

Out of the fact that we would like to use this bridge effectively, it will be a testbed for future enhancements for IRCTk. Things like text formatting via the IRCv3 tags and other such nice features, may be successfully tested here.

IRC is a fun protocol for a single (or a small group) to hack on. It's accessible, easy to parse and extensible. We do not know how many other transports we will create, but the code for this bridge is nicely splitted and reusable.

Fossil bridge code.