Mellium Year in Review
The Mellium project is a collection of libraries and applications in Go relating to XMPP and instant messaging. This post is a copy of a message sent to the xmpp-standards list in response to a call for updates on XMPP related projects from the past year.
But first: Why XMPP?
I recently got asked (and I’m paraphrasing here): “why would you write an XMPP library when trendier technologies are available?” The quick answer is that XMPP is still the most widely used federated instant messaging protocol available on the internet. Despite the demise of XMPP in a few big chat services that users were familiar with (HipChat, Google Talk, Facebook Messenger), it is still widely used in video games (WarFace, Nintendo Switch presence notifications, etc.), in current chat products (Zoom, WhatsApp, Jitsi Meet, etc.), and in industry. XMPP is a core part of internet infrastructure and has a robust ecosystem of both free and commercial products, servers, and clients built around it. We need this infrastructure in Go.
Now, on to the status update.
Last year the main Go module of the Mellium project, mellium.im/xmpp
received a huge number of updates, bug fixes, and API overhauls, many of which
will be released in the upcoming v0.18.0
release.
Some of the highlights include:
- WebSocket support
- The ability to negotiate multiple virtual hosts
- More server side implementations of stream features such as SASL and resource binding
- An implementation of Message styling
- Chat marker support
- Support for XEP-0202: Entity Time and XEP-0082: XMPP Date and Time Profiles
- Integration testing against common servers and clients
- Better fallback behavior when looking up XMPP servers
- Easier sending of IQs without having to think about XML tokens
- Easier APIs for registering handlers against specific IQ payloads
- XMPP URI and IRI parsing support
The full (much longer) list can be found in the changelog.
Message Styling
The change to mellium.im/xmpp
that I’m most proud of in the past year might be
the Message Styling implementation which can be found at
mellium.im/xmpp/styling
.
The actual styling implementation comprises three parts: the Style
type, the
Scan
function, and the Decoder
type.
The Scan
function is a bufio.SplitFunc
that is used to tokenize the byte
stream internally.
It is exported as a convenience to allow new message styling implementations
that behave differently or have other extensions built on top of it.
The Style
type is a uint32
that represents a bitmask of styles such as
“strong and emph” or “pre-formatted text and block quoted”.
It only contains information about the styles, not about metadata related to
the styles or the token stream.
For example, given a style you can know that the text is in a blockquote, but
not the level of nesting (if any) of the blockquote, or you might know that it
is in a strong span, but not where the start and endpoints of that span are.
The package provides the various styles and a handful of masks for various
common combinations as constants for the users convenience.
Finally, the Decoder
type pairs the Scan
function with the Style
type by
scanning for tokens and keeping track of the stream state.
It allows you to get the style of each token popped, and keeps track of metadata
about the styles such as the level of nesting in block quotes or the tag on a
fenced code block.
Here is an example of quickly tokenizing an input stream:
r := strings.NewReader(`The full title is
_Twelfth Night, or What You Will_
but *most* people shorten it.`)
d := styling.NewDecoder(r)
var err error
var tok styling.Token
for {
tok, err = d.Token()
if err != nil {
break
}
fmt.Printf("Token: %q\n", tok.Data)
}
if err != nil && err != io.EOF {
panic(err)
}
// Token: "The full title is\n"
// Token: "_"
// Token: "Twelfth Night, or What You Will"
// Token: "_"
// Token: "\n"
// Token: "but "
// Token: "*"
// Token: "most"
// Token: "*"
// Token: " people shorten it."
A more extensive example for converting styling tokens to HTML can be found in the package examples. For more, see my retrospective post, Message Styling.
Stream Initialization and Virtual Hosts
XMPP servers likely want to serve more than one domain, but prior to the
upcoming v0.18.0 release this was difficult or impossible.
This required changes to the Negotiator
type to allow input and output
streams to be stored on the session, and to the built-in Negotiator
’s
StreamConfig
type which is used for configuring options on the built in
default negotiator to allow the user to select the features in a callback.
Unfortunately, these are breaking changes, but it should be one of the last
major API changes before we can begin thinking about stabilizing the API in
v1.0.0.
The S2S
option was also removed from the StreamConfig
in favor of new
functions for receiving and negotiating a stream which mark the s2s state bit on
the session before beginning negotiation.
This removes a long standing redundancy, and also a bug where the first stream
feature negotiated wouldn’t know that the session was a server-to-server session
because it couldn’t be set until the negotiator function returned for the first
time.
The current list of stream negotiation functions is:
DialClientSession
DialServerSession
DialSession
NewClientSession
NewServerSession
NewSession
ReceiveClientSession
ReceiveServerSession
ReceiveSession
Testing
While it’s not an exciting user-facing feature, one of the most valuable changes
this year has been the addition of integration testing against Prosody,
Ejabberd, McAbber (Loudmouth), and sendxmpp(1)
.
We’ve already caught several issues both in Mellium and in some of the services
we’re testing against thanks to the new tests!
This is all facilitated by the internal/integration
package which provides a
framework for starting an external application, generating certificates, and
passing around file handles.
It works by creating a temporary directory where any config files can be stored,
then launching the process using that temp directory as a working directory.
It also provides generic options for writing files to that directory that can be
used by the child packages, such as prosody
, to write more specific options
that write the prosody config file.
The integration testing packages also provide functionality for running
sub-tests, each one of which will get its own child process with all the
configuration automatically.
For example, to test sending a ping to Prosody and Ejabberd and receiving a ping
on Prosody we might write something like the following:
func TestIntegrationSendPing(t *testing.T) {
prosodyRun := prosody.Test(context.TODO(), t,
integration.Log(),
prosody.ListenC2S(),
)
prosodyRun(integrationSendPing)
prosodyRun(integrationRecvPing)
ejabberdRun := ejabberd.Test(context.TODO(), t,
integration.Log(),
ejabberd.ListenC2S(),
)
ejabberdRun(integrationSendPing)
}
func integrationSendPing(ctx context.Context, t *testing.T, cmd *integration.Cmd) {
…
}
func integrationRecvPing(ctx context.Context, t *testing.T, cmd *integration.Cmd) {
…
}
More complex options for loading Prosody extensions, generating and using certificates and enabling TLS, changing the default username and server host, etc. are also available.
See Also
- The
mellium.im/xmpp
docs and website - Message Styling, a retrospective of XEP-0393: Message Styling
- XMPP in Go, an overview of the Mellium project and its design
- Extensions in Mellium, a guide to designing (not programming) XMPP related Go packages