vurpo

Building on Matrix

So I discovered Matrix, and I like programming. What’s next? Writing a client for Matrix, obviously! Follow me into the realm of the Matrix Client-Server API…

Writing a Matrix client, as it turns out, is simple. There are libraries/SDKs for writing clients already made for many languages. This is where I started my journey. Now, since the language on that list that I am the most familiar with is Python, my SDK of choice became matrix-python-sdk. Using this SDK is easy as pie:

from matrix_client.client import MatrixClient

client = MatrixClient("https://vurpo.fi")
client.login_with_password("username","password)

…or to continue using an existing session instead of logging in:

client = MatrixClient(
  "https://vurpo.fi",
  token="ACCESS_TOKEN_HERE",
  user_id="@username:vurpo.fi")

…and after the initial sync has completed, you can now interact with the Matrix homeserver using matrix-python-sdk’s simple Python API!

My goal was to convert my vurpobot (Telegram status bot for Turku Hacklab) into a Matrix bot. This means listening for messages, and if the message is a hacklab command (i.e. the message starts with “!hacklab”), gather the relevant data from the sensors, and answer with a nicely formatted statement stating the current sensor data.

To get the rooms that you are currently joined to, call client.get_rooms(), which returns a dict of room IDs (opaque identifiers for Matrix rooms) to Room objects. Then to listen for all events in a specific room, you call room.addListener(listener), where listener is a function that takes a room object and an Event (dictionary) as its parameters. This function is where the logic to detect commands and respond to them goes. Responding to messages is easy, since a message can be sent directly to the room through the room parameter passed as an argument.

def onEvent(self, room, event):
  if event['type'] == "m.room.message"
    and event['content']['msgtype'] == "m.text":
    if room.room_id == self.errorReportRoom: #admin commands
      leaveCommand = re.match(
        "!leave (.+)",
        event['content']['body'])
      if leaveCommand:
        self.client.get_rooms()[leaveCommand.group(1)].leave()
    for handler in self.commandMap:
      if event['content']['body'].startswith(handler.command):
        self.client.api._send(
          "PUT",
          "/rooms/{}/typing/{}"
            .format(room.room_id, self.client.user_id),
          {'typing': True, 'timeout': 30000})
        try:
          room.send_text(
            handler.handleCommand(event['content']['body']))
        except KeyboardInterrupt:
          raise
        except:
          self.reportMainloopError()
          room.send_notice(
            "Command failed! The error has been reported to {}."
              .format(self.ownerID))
        self.client.api._send(
          "PUT", 
          "/rooms/{}/typing/{}"
            .format(room.room_id, self.client.user_id),
          {'typing': False, 'timeout': 30000})
        return

This is, as they say, where the magic happens. Obviously, as you can see from the code, there’s a lot more going on outside this snippet (such as handling invites into rooms, and setting up the connection and the owner’s admin room), but this function is what actually handles the messages. First it handles the one hard-coded admin command (which is there just to remove the bot from rooms where I’m not an admin), then it reads the message, generates the appropriate response if the message is a command, and sends the response back into the room.

The command handler pattern is a simple pattern I built to make a very simple, kind of flexible, bot architechture back when I wrote the original Telegram bot. The hacklab command handler just reads the HTTP APIs for the sensors, creates a nice-looking response message, and returns it.

The entire code can be found at https://gitlab.com/vurpo/vurpobot-matrix. The bot lives at @hackbot:vurpo.fi, if you want you can go test it out by saying “!hacklab”!

This is a very simple introduction to client programming for Matrix, and there are a lot of features of Matrix that I didn’t even mention in this article. I will be going much more in-depth into Matrix client-side programming in the next article, where I write a “full” Matrix client from scratch using the REST API… see you then!


Creative Commons License
All code on this site is licensed under the MIT License
(except if otherwise noted)