Skip to content

Instantly share code, notes, and snippets.

@leafstorm
Created February 18, 2013 03:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leafstorm/4975030 to your computer and use it in GitHub Desktop.
Save leafstorm/4975030 to your computer and use it in GitHub Desktop.
LeafStorm's Relay Chat - my idea for an IRC-like chat system. This does not constitute a spec.

LeafStorm's Relay Chat

LRC is a chat protocol based on edn - Clojure's extension of S-expressions. The idea is to have a format that is easily typable and telnettable, but also clearly and consistently structured.

High-Level Overview

LRC is based on exchanging messages. Messages can be exchanged between clients and servers, or servers and servers, bidirectionally. Each message is an edn element - either a list, or a tagged #lrc/req or #lrc/res element.

Normally, the transport protocol is "open a TCP connection, send an element and then a newline", but you could easily come up with alternate transports (like WebSockets, text messages, or even IRC) so long as it can transport entire edn elements.

Message Classes

The core of a message is an edn list, the first element of which is the message type (for example, joined or nick-changed). Each message type belongs to a message class, which describes the overall behavior of the message.

The most common message type is an event. This is sent by the server to one or more clients to notify them of a change in server state. Examples of events are joined (a user joins a channel), chatmesg (a user posts a message in a channel), nick-changed (a user changes their nick), etc.

The other most common message type is an action. This is sent by the client to the server, and is intended to change some sort of state (for example: display a message, join a room, etc.).

A query message can be sent client-to-server, or server-to-client. Usually, their names end in ?, and represent a request for information about the current server state, not an intent to actually change the state.

Info messages are the response to query messages. Like the difference between an action and a query, the difference between an event and an info is that an info message does not immediately reflect a state change, but merely informs the client/server of the current state.

Demos

> (conn-state?)
< (conn-state-is :waiting)
> (conn-params?)
< (conn-params-are {:email :required, :name :required,
                    :password :required})
> (conn-language?)
< (conn-language-is :en)
> (server-info?)
< (server-info {:version "LRCd 0.1"})

This is right after the user connects to the server. It demonstrates the common pattern that "query" messages end with ?, and info messages end with either -is, -are, or -info.

> (connect "leafstorm" {:email "leafstormrush@gmail.com"
                        :name "Matthew Frazier"
                        :password "turtles"})
< (input-error :bad-credentials "That is not a valid password.")
> (connect "leafstorm" {:email "leafstormrush@gmail.com"
                        :name "Matthew Frazier"
                        :password "turtlesturtles"})

When connecting the user requests a nickname, and provides some additional information. The server can use this information for user authentication, nick registration, or whatever. Servers have a choice of what parameters to request, and what happens to the provided parameters, but the semantics of all the standard ones will be defined by the spec.

Also, the format of *-error messages is a keyword for the error type, followed by a human-readable message. Note that in this example, linked requests are not used for simplicity. However, it's possible to send a message (action or query) with a request wrapper:

> #lrc/req [1 (connect "leafstorm" {...})]

Then, all events, infos, and statuses generated from that request will be wrapped in:

< #lrc/res [1 (input-error :bad-credentials ...)]

The client picks the number (and is allowed to reuse them if it has some way of disambiguating) but it must be an integer.

< (welcome [613 "leafstorm"])
> (conn-identity?)
< (conn-identity-is [613 "leafstorm"])
> (conn-state?)
< (conn-state-is :live)

Besides a nickname, each user is uniquely identified by an ID vector. (This can be an actual vector of strings and integers, or just an integer.) Even if users change their nick, their ID vector stays the same. If an ID vector is reused after the associated connection ends, it must be the same person. (This guarantee holds until the server restarts.)

(So, for example, if the system supports user registration, it could issue an ID vector that applies to all connections from a specific user.)

> (channels?)
< (channels-are #{"ncsulug" "turtles"})
> (user-info? "IsharaComix")
< (user-info [404 "IsharaComix"] {:name "Barry Peddycord" ...})

Unlike IRC, channels aren't required to have a sigil attached. (They can, if it makes you feel comfortable.) They are described by the regular expression [a-z0-9#][a-z0-9?!#/+-]+.

Nicknames follow the format [a-zA-Z0-9_-]+. Commands that describe users can be provided either a nickname or an ID vector. Each nickname will only have one ID vector associated with it at a time, and therefore one connection.

> (join "ncsulug")
* (joined "ncsulug" "leafstorm")
< (users-in-channel-are "ncsulug" #{
    [613 "leafstorm"] [404 "IsharaComix"] [420 "spvensko"]
    [512 "mamarley"] [2 "LOUDBOT"]
  })

When a user does something like join a channel, the resulting event message is sent even to the person who did the joining. This serves to confirm that the server actually processed the event. ("Direct result" events like this will be wrapped in an #lrc/res if the action message is wrapped in a #lrc/req.)

Also, this users-in-channel-are is an example of an info message being sent without an explicit query. The server can send info messages whenever it wants to synchronize the server state with the client state.

> (send-chatmesg "ncsulug" "Hi, guys!")
* (chatmesg "ncsulug" [614 "leafstorm"]
            "Hi, guys!")
* (chatmesg "ncsulug" [404 "IsharaComix"]
            "Hey, what's up leafstorm?")
> (send-chatmesg "ncsulug" "Not much. We should have a Google hangout.")
* (chatmesg "ncsulug" [614 "leafstorm"]
            "Not much. We should have a Google hangout.")
* (chatmesg "ncsulug" [512 "mamarley"]
            #lrc/action "shudders.")
* (chatmesg "ncsulug" [512 "mamarley"]
            "That didn't work out so well the last time.")
* (chatmesg "ncsulug" [404 "IsharaComix"]
            "What are you talking about? It went great!")
* (chatmesg "ncsulug" [404 "IsharaComix"]
            "ISN'T THAT RIGHT LOUDBOT?")
* (chatmesg "ncsulug" [2 "LOUDBOT"]
            "BAG OF DUCKS")

Please ignore the unrealistic dialogue and focus on the message syntax.

If a message is just a string, it's treated as a plain old message. However, other things can be sent as messages. For example, #lrc/action is the equivalent of the CTCP \001ACTION\001. Servers may decide to transform or filter such messages, but in general, clients are expected to interpret messages that may consist of arbitrary data. (Even if "interpret" consists of displaying, "* mamarley sent some crap I didn't understand.")

* (joined "ncsulug" [614 "spvensko_"])
* (parted "ncsulug" [420 "spvensko"])
* (nick-changed 614 "spvensko_" "spvensko")

Note that once a person connects, their ID vector can't change, even if their nick does, as the whole purpose of an ID vector is to allow a connection to be consistently targeted, even if they change their name like Gus from Psych. If spvensko had waited until his existing connection timed out before reconnecting, it would be perfectly OK for the server to reassign him ID vector 420 (assuming it could verify that the client actually was spvensko, or a reasonable duplicate).

> (send-privmesg "IsharaComix" "Also, my debit card came in the mail.")
< (privmesg [614 "leafstorm"] [404 "IsharaComix"]
            "Also, my debit card came in the mail.")
> (send-privmesg "IsharaComix" "The number is hunter2.")
< (privmesg [614 "leafstorm"] [404 "IsharaComix"]
            "The number is hunter2.")
< (privmesg [404 "IsharaComix"] [614 "leafstorm"]
            "That's not a number! :-P")

Private messages, like all user-related actions and queries, can be targeted either by nickname or ID vector.

> (client-info? 404 [:version])
< (client-info 404 {:version "LeafChat 0.0.1"})

This is an example of a user being targeted by ID vector. client-info? queries are passed directly to the client to be answered. From IsharaComix's client's point of view, the exchange would look like:

< #lrc/req [48531 (send-client-info? [614 "leafstorm"] [:version])]
> #lrc/res [48531 (send-client-info [614 "leafstorm"]
                  {:version "LeafChat 0.0.1"})]

If IsharaComix's client thought it was none of my business what version it was, it could send:

> #lrc/res [48531 (deny-client-info 614)]

Which would tell my client:

< (access-error :info-denied "IsharaComix rejected your request.")

Ping is another server-to-client query of this type:

> #lrc/req [13812 (ping?)]
< #lrc/res [13812 (pong)]

Finally, to disconnect, the client sends:

> (disconnect)
< (disconnecting [614 "leafstorm"] "Goodbye!")

Whenever the server sends a disconnecting message, it can disconnect the TCP connection instantly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment