========================
== Experimental Emacs ==
========================
Corwin's Emacs Blog


ComicChat

I stumbled upon ComicChat recently and have been having a great deal of fun learning node.js while messing around with it.

So far I've added:

  • UI/UX

    • inline linkification

    • mIRC color, bold, italics, and underline formatting

    • force wrapping for longer phrases

    • ability to break panels based on total character count (patched upstream)

    • rotating backgrounds (patched upstream)

  • IRC relay (relay.js)

    • ability to join multiple channels (patched upstream)

  • Custom IRC relay (relay+.js)

    • ability to command join/part via PM to bot

  • Discord relay (discord-comicchat on Sourcehut)

    • /join - "slash command" to join discord text channels

  • "Creative Controller", avatars customization, etc. (eventually)

In this post I'll start by giving some tips on getting starting running ComicChat. I'll also describe some additional tools I created or found while learning to with it/node.js and, finally, describe some of my ideas for what else could be possible with this cool project.

I'm leading with the "head-first" in this post, but I'll cover the design of ComicChat more generally (including describing some further ideas I have) toward the end of the post.

Using ComicChat

In the next example, we'll see how I typically start server.js. This is the program which creates the "service" (always running process) to receive, store, and play-back text messages for the "client" to transform into a web-comic.

  nohup node.js /var/bots/comicchat/server/server.js >/var/bots/comicchat/log/server.log 2>&1 &

We feed the call to node via nohup, which will keep the process running even in case our shell session is interrupted. We also redirect standard output (>) as well as standard error outputs (2>&1) to a log file, overwriting it if it exists. Starting others (e.g. relay.js) is identical % file names.

A complete deployment includes server.js, one or more of relay.js, relay+.js, or discord-relay.js (given you want messages sent in from IRC/discord), and serving the files in the client folder via a web-server (I'm using apache2).

You can see an example here: http://dungeon-mode.com/toon/#dungeon

The (Original) Programs

Originally developed by Microsoft's David Kurlander in 1996, my work is based on a 2013 reimplementation in node.js by Ng Guoyou. I started looking at the code while experimenting with installing and configuring it and was at once taken by the direct minimal-but-readable style. Let's look each of Ng's programs before turning what I've created and am thinking of.

server/Server.js

This is the main ("ComicChat service") program. While it's running it maintains key data memory. The program doesn't have a confutation file, instead, defining important settings at the top of each program.

var wsServerPort = args.port || 8042;
var historySize = args.historySize || 1000;
var clients = [];
var rooms = {};

The server port must be exposed to the internet for web-socket connections. You can change this port but must also change client/js/client.js to match.

The historySize variable stores a number of text messages to keep in memory for each channel. (Pedantic: In fact, messages are truncated after adding new messages, so the functional max size of the history is historySize + 1 while processing a new message to a channel with max messages kept already.)

The clients list stores the connections ("listeners") to the server. The value for each element in this list is a node WebSocket connection used to send data back to that client.

These track, for each text channel that the server instance is tracking, a number of items including a history of messages. This is perhaps best explained by a look at the initRoom function.

function initRoom (name) {
  // Init room if doesn't exist
  rooms[name] = rooms[name] || {};
  rooms[name].history = rooms[name].history || [];
  rooms[name].clients = rooms[name].clients || [];
}

The name and history properties are hopefully self-evident. The client list stores, once again, node WebSocket connections. Each connection mentioned in a given channel's list revives a push of each new message sent to that channel, allowing the web-comic to update in Somewhat Real-Timeβ„’, as messages are sent from IRC and/or Discord.

We call the initRoom function when the server "joins" a channel. Membership in the servers rooms list controls whether that server will accept messages targeted to that room.

Server Join vs Relay Join

It's worth noting, nothing discussed so far is to do with any "bot" or IRC or discord network/channel/server/connection: each such must track for itself what channels it should be present it, and keep that current with the server by sending messages to it over the Socket/WebSocket connection mentioned earlier.

Interprocess Messaging

Pograms pass instructions around to control each other, and function as a coordinated system. For example if a bot (relay+, discord-comicchat) receives a command to join a channel from a user, in addition to joining in the chat network, the relay/bot must send a message to the server, allowing it to initialize the channel, etc. Meanwhile, any bot would use this same functionality to pass messages text back from the room to the server, and thus to clients/listeners.

Because it has several always/often running parts, ComicChat needs a means for communication and coordination between them. The WebSocket provides the technical transportation component. For the rest we craft custom JSON message, according to a certain design, and take care to send/use those messages consistently throughout all the programs in application. Taken together we have a simple Protocol.

From ComicChat's Readme:

Connect to the WebSocket server and start pushing JSON. Subject to change.

Send
    {
        type: 'join',
        room: 'room'
    }
  • `history`, `join`, `part` require `room`

  • `message` requires `room` and `text`, `spoof: true` optional for relays

Receive
  • `history` β€” `type`, `history` (an array of messages for the requested room)

  • `message` β€” `type`, `room`, `time`, `text`, `author`

More Server Message Examples

White this may seem terse to the point of cryptic, in fact it provides for a quite functional API ("Application Programing Interface").

A example join message is above. A part message is similar:

  {
    type: 'part',
    room: 'room'
  }

The message object, as mentioned must include type, room, and text. A minmal example is:

  {
    type: 'message',
    message: 'Hello, ComicChat',
    room: 'room'
  }

When a message also provides spoof (with a truthy value), we can additionally pass an explicit author. (In fact, this is the usual case, as when relaying from IRC or Discord. The alternate case, when we assume the author, supports text messages originating from the web-client. We'll come back to this when we discuss client/js/ui.js.)

Here's a message example providing an author, "someone":

  {
    type: 'message',
    message: 'Hello again, ComicChat',
    author: "someone",
    spoof: true,
    room: 'room'
  }

relay/relay.js

Relay provide a means of "feeding" messages into the ComicChat server from some other source. In the case of relay.js (in the relay folder of the ComicChat project), provides a bare-bones IRC bot (automated client for an IRC network) able to join a predefined set of channels and passes the messages it sees in them to the ComicChat server. There aer no interactive commands (e.g. via public/private message with the bot via the chat network).

As with the server component, relay is configured within the program. Relay has a more substantial set of "options":

var config = {
  cchat: {
    nick: 'petroglyph',
    room: '##freenode',
    host: 'dungeon-mode.com',  // hidoi.moebros.org
    port: 8042,
    roomLink:  'http://dungeon-mode.com/toon/#dungeon'
  },
  irc: {
    nick: 'petroglyph',
    user: 'comic',
    real: 'relay',
    channels: [ '#dungeon' ],
    host: 'irc.libera.chat',
    port: 6697,
    ssl: true
  }
};

The first group of options ccchat coordinates with configuration given in/to the server, described above, and with your (my) web-server configuration discussed below.

Most important here are host, which is the host or IP (localhost should work, here) where we'll connect to the ComicChat server, and port, which should match what you have in server.js.

In the IRC block we configure our simple IRC client/bot with the information it will need to connect the network and join channels.

The nick used on IRC can be different from config.cchat.nick, the latter of which is used for messages sent to the sever (so, for the comic) from the bot. The bot makes brief "hard-coded" messages channel on the ComicChat server as well as an entry message each time it joins an IRC channel. The IRC nick on the other hand (config.irc.nick) is used to establish a name for the bot on the IRC network. (IRC servers don't repeat our own messages back to us.)