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; andcontent
, 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; andorigin_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).
# {{copyright}}
"""Classes that represent Matrix events"""
import typing
from . import schema
{{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,
}
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
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
# {{copyright}}
import json
import pytest
import re
from matrixlib import client
from matrixlib import events
from matrixlib import schema
{{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 msgtype
s, 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 ofleave
, 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 asm.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
, andm.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
, andm.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.
"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.