Events

Communication in Matrix is done primarily by sending event to a room. There are two types of events: state events, which are events about the room, and some of which may affect the operation of the room; and message events. Most communication is done using message events, though state events may be used for certain things. State events and message events are distinguished by the state_key property: state events have a state_key (which in many cases is set to the empty string), while message events do not.

Other properties that events will have are:

  • type, indicating what kind of event it is;

  • sender, giving the ID of the user who sent the event; and

  • content, giving the body of the event as a JSON object.

In addition, events sent to a room (there are also events sent directly to other devices, which will be discussed elsewhere) have a:

  • event_id, giving a globally unique identifier for the event;

  • room_id, giving the ID of the room in which the event was sent; and

  • origin_server_ts, giving the time that the event was sent, according to the originating homeserver, given as milliseconds since the UNIX epoch.

Events may also have an unsigned property, giving extra information that is added by the client’s homeserver (as opposed to the server where the event originated). Within unsigned, there can be an age property, which gives the age of the event, based on the origin_server_ts property, as calculated by the client’s homeserver. Since the clocks on the client’s homeserver and the originating server may be out of sync, the age may not be accurate, and may even be negative. unsigned may have other properties as well, depending on the event; these other properties will be discussed when we discuss the conditions when they will show up.

We create some classes to represent the different types of events. We start with an __init__ function, a function to give the schema for that type of event, and a function to validate an event against that schema. In some situations, we will receive an event without the room_id property as it is implied from the context, so we also implement schema functions that omit the room_id. We also implement a function to compare equality between events, which will be important for our unit tests later on.

With the way the __init__ functions are constructed, this will allow us to create an event from a JSON object by performing, for example, parsed_event = events.RoomEvent(**event) (after event has been validated, of course).

src/matrixlib/events.py:
# {{copyright}}

"""Classes that represent Matrix events"""

import typing

from . import schema


{{events module classes}}
events module classes:
class Event:
    """Base class for Matrix events"""

    def __init__(
        self,
        type: str,
        sender: str,
        content: dict[str, typing.Any],
        unsigned: typing.Optional[dict[str, typing.Any]] = None,
        **kwargs,  # discard any unknown arguments
    ):
        self.type = type
        self.sender = sender
        self.content = content
        self.unsigned = unsigned if unsigned != None else {}

    @staticmethod
    def schema():
        return {
            "type": str,
            "sender": str,
            "content": dict[str, typing.Any],
            "unsigned": schema.Optional(
                {
                    "age": schema.Optional(int),
                    {{event > unsigned schema}}
                }
            ),
        }

    @classmethod
    def is_valid(cls, data):
        # since this is a classmethod, it will call the correct `schema`
        # function for the class that this was called from
        return schema.is_valid(data, cls.schema())

    @classmethod
    def _eq_helper(cls, self, other: typing.Any) -> bool:
        return (
            self.__class__ == other.__class__
            and self.type == other.type
            and self.sender == other.sender
            and self.content == other.content
            and self.unsigned == other.unsigned
        )

    def __eq__(self, other):
        return self.__class__._eq_helper(self, other)

    def to_dict(self) -> dict:
        """Return a dict representation of the event, e.g. for serialization"""
        return {
            "type": self.type,
            "sender": self.sender,
            "content": self.content,
            "unsigned": self.unsigned,
        }
events module classes:
class RoomEvent(Event):
    """Represents an event sent in a room"""

    def __init__(self, event_id: str, room_id: str, origin_server_ts: int, **kwargs):
        Event.__init__(self, **kwargs)
        self.event_id = event_id
        self.room_id = room_id
        self.origin_server_ts = origin_server_ts

    @staticmethod
    def schema():
        return schema.Intersection(
            Event.schema(),
            {
                "event_id": str,
                "room_id": str,
                "origin_server_ts": int,
            },
        )

    @staticmethod
    def schema_without_room_id():
        return schema.Intersection(
            Event.schema(),
            {
                "event_id": str,
                "origin_server_ts": int,
            },
        )

    @classmethod
    def _eq_helper(cls, self, other: typing.Any) -> bool:
        return (
            Event._eq_helper(self, other)
            and self.event_id == other.event_id
            and self.room_id == other.room_id
            and self.origin_server_ts == other.origin_server_ts
        )

    def to_dict(self) -> dict:
        """Return a dict representation of the event, e.g. for serialization"""
        ret = super().to_dict()
        ret.update(
            {
                "event_id": self.event_id,
                "room_id": self.room_id,
                "origin_server_ts": self.origin_server_ts,
            }
        )
        return ret
events module classes:
class StateEvent(RoomEvent):
    """Represents a state room"""

    def __init__(self, state_key: str, **kwargs):
        RoomEvent.__init__(self, **kwargs)
        self.state_key = state_key

    @staticmethod
    def schema():
        return schema.Intersection(
            RoomEvent.schema(),
            {
                "state_key": str,
            },
        )

    @staticmethod
    def schema_without_room_id():
        return schema.Intersection(
            RoomEvent.schema_without_room_id(),
            {
                "state_key": str,
            },
        )

    @classmethod
    def _eq_helper(cls, self, other: typing.Any) -> bool:
        return RoomEvent._eq_helper(self, other) and self.state_key == other.state_key

    def to_dict(self) -> dict:
        """Return a dict representation of the event, e.g. for serialization"""
        ret = super().to_dict()
        ret.update(
            {
                "state_key": self.state_key,
            }
        )
        return ret
Tests
tests/test_events.py:
# {{copyright}}

import json
import pytest
import re

from matrixlib import client
from matrixlib import events
from matrixlib import schema


{{test events}}
test events:
def test_event_parsing():
    event = {
        "sender": "@alice:example.org",
        "type": "m.room.message",
        "event_id": "$an_event_id",
        "room_id": "!a_room_id",
        "content": {
            "body": "Hello world!",
            "msgtype": "m.text",
        },
        "origin_server_ts": 123567890000,
        "unknown_property": "foo",
    }
    assert events.Event.is_valid(event)
    assert events.RoomEvent.is_valid(event)
    assert not events.StateEvent.is_valid(event)
    parsed_event = events.RoomEvent(**event)
    assert parsed_event.sender == event["sender"]
    assert parsed_event.type == event["type"]
    assert parsed_event.event_id == event["event_id"]
    assert parsed_event.room_id == event["room_id"]
    assert parsed_event.content == event["content"]
    assert parsed_event.origin_server_ts == event["origin_server_ts"]
    assert parsed_event.unsigned == {}
    assert parsed_event == events.RoomEvent(**event)
    assert not (parsed_event != events.RoomEvent(**event))
    assert parsed_event != events.Event(**event)

    event_copy = event.copy()
    del event_copy["unknown_property"]
    event_copy["unsigned"] = {}
    assert parsed_event.to_dict() == event_copy

Now let us look into more detail about how the different types of events are used.

Message events

Message events are generally standalone events that describe some sort of activity to be communicated to the room. The most common message event in an instant-messaging context is the m.room.message event, which represents a message — whether text, image, audio, etc. — being sent to the room. The most basic form of this event is a content that contains a body property containing a plain-text message, and a msgtype indicating the type of message (usually m.text):

{
    "type": "m.room.message",
    "sender": "@alice:example.org",
    "content": {
        "body": "Hello world!",
        "msgtype": "m.text"
    },
    "event_id": "$an_event_id",
    "room_id": "!a_room_id",
    "origin_server_ts": 123567890000
}

Bots should send text messages with a msgtype of m.notice, and avoid responding to m.notice messages, to avoid accidental response loops. Images, audio, and videos are sent using other msgtypes, though these involve using the media repository, which will be discussed later. Messages sent with a msgtype of m.emote represent the sender performing an action, and are generally displayed with the name of the sender preceding the message body (e.g. a message sent by Alice with a body of “deploys a Matrix bot” would be rendered as “Alice deploys a Matrix bot”), and can be formatted differently to distinguish it from regular messages.

m.room.message events can also include HTML formatting by including a format parameter set to org.matrix.custom.html in the content, and a formatted_body parameter set to the HTML version of the message. The body parameter should still contain a plain text version of the message, and should try to convey as much of the intent of the HTML version as possible, though there is no standardised way of doing so. Libraries may wish to provide some sort of functionality for easily generating both the plain text and HTML versions of a message. For example, this could be done by using some sort of markup language, such as Markdown.

{
    "type": "m.room.message",
    "sender": "@alice:example.org",
    "content": {
        "body": "*Hello* world!",
        "msgtype": "m.text",
        "format": "org.matrix.custom.html",
        "formatted_body": "<b>Hello</b> world!"
    },
    "event_id": "$an_event_id",
    "room_id": "!a_room_id",
    "origin_server_ts": 123567890000
}

Received messages are untrusted input, and clients should be careful when displaying messages. For example:

  • Messages with HTML formatting must be sanitised: only certain HTML elements and attributes should be allowed, and all elements must be properly closed.

  • Clients should sensibly handle messages that are too large in one or both dimensions.

  • Clients should ensure that Zalgo text in one message does not interfere with other messages.

  • Clients should ensure that Unicode text direction marks in one message do not affect other messages.

Message events can also be sent as different event types other than m.room.message, depending on what they represent. Different functionality will define the event types that they use.

Todo

extensible events

State events

State events are events that are about the room, and some affect how the room operates (such as room permissions), or how clients interact with the room (such as encryption settings). State events have a state key (which is the empty string in many cases, but this should not be confused with not having a state key). State events effectively replace previously-sent state events that have the same event type and state key; a room’s state is the set of the latest events sent for each event type, state key pair.

Some common state events are:

  • m.room.create is the first event sent to the room, and gives some room parameters such as the room creator, and the room version (TODO: link to room version section).

  • m.room.member indicates a user’s membership state in the room and gives the user’s display name and avatar for the room. The state key is the ID of the user that the event refers to; this is the only event type in this list whose state key is not the empty string. A user’s membership state can be: a non-member the room (either if the state event for the user has a membership state of leave, or if there is no state event for the user), a member of the room, invited to the room, knocking to ask permission to join the room, or banned from the room.

  • m.room.power_levels is the main event that controls room permissions. The full details of the room permissions are beyond the scope of this book, and depend on the room version, but the general mechanism is that each user is assigned a power level (defaulting to 0), and event types are assigned power levels (defaulting to 0 for message events and 50 for state events). With some exceptions, the general rule is that if a user’s power level is greater or equal to an event’s required power level, the user will be allowed to send the event. The exceptions to this rule mainly concern event types that affect room permissions, such as m.room.power_levels itself, and involve comparing the contents of the previous state with the new state so that, for example, a user may demote themselves to a lower power level, but cannot promote themselves to a higher power level.

  • m.room.join_rules, m.room.history_visibility, and m.room.server_acl control other specific aspects of the room permissions, namely who is allowed to join, what messages non-members can see, and what homeservers are or are not allowed to participate.

  • m.room.name, m.room.avatar, and m.room.topic give information to be displayed to users, indicating what the room is about.

  • m.room.canonical_alias indicates the aliases used by the room. Room aliases allow users to assign user-friendly names to rooms so that they can be found more easily. Aliases belong to a homeserver and are completely controlled by their homeserver. This event allows room administrators to declare which alias (if any) should be considered the canonical one for the room, as well as which other aliases are recognized by the room administrators.

Certain features may also define their own state events.

When a state event is received and it overwrites the previous state in the room, then there may be a prev_content property within the unsigned property, giving the state content of the state event that was replaced. This way, a client can calculate the room state when going backwards through history.

event > unsigned schema:
"prev_content": schema.Optional(dict[str, typing.Any]),

Clients should be careful to check the state key and not just the event type when processing events. For example, if the client receives an m.room.name event that does not have a state key, or that has a state key that is not the empty string, it should not treat it as a change to the room name.