Sending events to a room

Message events

To send a message event, the client makes a request to PUT /rooms/{roomId}/send/{eventType}/{txnId}. The {roomId} and {eventType} parameters are fairly self-explanatory. The {txnId} is called a transaction ID and is a string that is used to identify the request. It must be unique to the device, and is used to ensure that if a request is retried, the event is not sent multiple times, even if the request was received by the server multiple times. For example, a client may make a request, and the server may receive the request, but the server’s response fails to reach the client. The client then retries the request, and the server can see that it has already received that transaction ID, and so it knows the request is a duplicate, rather than the client wanting to send another copy of the same message.

Since several endpoints use a transaction ID like this, we will create a function to generate a transaction ID whenever we need one. To ensure that the transaction ID is unique, we will combine several pieces of information. First, we will keep a counter of the transaction IDs that we have generated so far. We initialize a counter in our Client object’s initialization, and every time we generate a transaction ID, we will increment it.

Client class initialization:
self.txn_count = 0

Using a counter ensures that the transaction ID is unique during one run of the client, but when the client is restarted, the counter will restart at 0. We could store the counter every time we generate a transaction ID, but it is better to not require storage if we don’t need it.

We can also add the current timestamp. Unless the client generates a transaction ID, gets restarted, and generates another transaction ID within the resolution of the timestamp, the transaction ID will not be reused.

For languages that run as processes in the operating system, we can also add in the process ID, which will prevent duplicate transaction IDs if the process is restarted. Alternatively, we could use the timestamp of when the client started, or when our Client object was created, so that we would not get duplication unless the client is restarted multiple times in quick succession.

Client class initialization:
self.pid = os.getpid()
# alternatively: self.start_time = time.time_ns()

We will concatenate these three pieces of information together with a separator to generate our transaction ID.

Client class methods:
def make_txn_id(self) -> str:
    """Generate a unique transaction ID"""
    self.txn_count = self.txn_count + 1
    return f"{self.pid}_{time.time_ns()}_{self.txn_count}"
    # alternatively: f"{self.start_time}_{time.time_ns()}_{self.txn_count}"
Tests

To test this function, we run it twice and ensure that it generates different IDs each time.

test events:
@pytest.mark.asyncio
async def test_txn_id():
    async with client.Client(
        storage={},
        callbacks={},
        base_client_url="https://matrix-client.example.org/_matrix/client/",
    ) as c:
        first = c.make_txn_id()
        second = c.make_txn_id()
        assert first != second

Now we can write our function to send a message. This will be a low-level function, where the caller will be expected to construct the event contents. It return a pair of strings: the event ID of the sent event, and the transaction ID.

Note

Whenever we generate a URL, we must remember to URL-encode path parameters, unless we know that the parameter will only contain URL-safe characters. In this case, since the transaction ID only contains numbers and underscores, it is URL-safe. However, the room ID and event type might not be, so we encode those.

Client class methods:
async def send_event(
    self,
    room_id: str,
    event_type: str,
    event_content: dict[str, typing.Any],
    retry_ms: int = 0,
    txn_id: typing.Optional[str] = None,
) -> typing.Tuple[str, str]:
    """Send an event to a room

    Arguments:

    ``room_id``:
      the ID of the room to send to
    ``event_type``:
      the type of event to send
    ``event_content``:
      the content of the event
    ``retry_ms``:
      how long to retry sending, in milliseconds
    ``txn_id``:
      the transaction ID to use.  If none is specified, one is generated.
    """
    txn_id = txn_id or self.make_txn_id()
    url = self.url(
        f"v3/rooms/{urlquote(room_id, '')}/send/{urlquote(event_type, '')}/{txn_id}"
    )
    resp = await retry(
        retry_ms, self.authenticated, self.http_session.put, url, json=event_content
    )
    async with resp:
        status, resp_body = await check_response(resp)
        schema.ensure_valid(resp_body, {"event_id": str})
        return (resp_body["event_id"], txn_id)
Tests

We test that we hit the right endpoint, and that we retrieve the event ID correctly. Since the transaction ID part of the endpoint is unpredictable, we use a regular expression to match the endpoint.

test events:
@pytest.mark.asyncio
async def test_send_event(mock_aioresponse):
    async with client.Client(
        storage={
            "access_token": "anaccesstoken",
            "user_id": "@alice:example.org",
            "device_id": "ABCDEFG",
        },
        callbacks={},
        base_client_url="https://matrix-client.example.org/_matrix/client/",
    ) as c:
        pattern = re.compile(
            r"^https://matrix-client.example.org/_matrix/client/v3/rooms/!roomid/send/m.room.message/.*$"
        )
        mock_aioresponse.put(
            pattern,
            status=200,
            body='{"event_id":"$event_id"}',
            headers={
                "content-type": "application/json",
            },
        )
        event_id, _txn_id = await c.send_event(
            "!roomid", "m.room.message", '{"body":"Hello World!"}'
        )
        assert event_id == "$event_id"

Important

You may need to take care to ensure that events get sent in order, depending on your programming language and the structure of your application. If your application tries to send two events concurrently, there is no guarantee as to which event gets sent first. Particularly when sending multiple events to the same room, you should wait until one event is sent before beginning to send the next one. This can be done by maintaining a queue of outgoing messages, either a single queue for the entire application, or one queue per room.

You may also need to consider how to handle failures: if one event fails to send, how should the remaining events in the queue be handled? For example, an interactive client may wish to pause sending completely and prompt the user to decide whether they wish to retry or cancel some or all of the events. This may also depend on the error code.

Example: send a message

We can now create a simple script that will send a message in a room. The script takes the room ID and a message as arguments. It relies on having previously logged in with the login script.

For ease of parsing, the script will take the room ID as the first argument. The rest of the arguments will be the message to send, with a space separating the arguments. The script will then send an m.room.message event with a msgtype of m.notice, which, as mentioned earlier in the message events section, is the type used by bots. If the message were to be sent as if by a normal user, then it would be sent with a msgtype of m.text.

examples/send_message.py:
# {{copyright}}

"""Send a message to a room"""

import asyncio
import json
import sys
import typing

from matrixlib import client


{{json file storage}}


if len(sys.argv) < 3 or not sys.argv[1].startswith("!"):
    print(__doc__)
    print()
    print(f"Usage: {sys.argv[0]} <roomid> <message...>")
    exit(1)


async def main():
    msg = " ".join(sys.argv[2:])
    async with client.Client(storage=JsonFileStorage()) as c:
        event_id, _ = await c.send_event(
            sys.argv[1],
            "m.room.message",
            {"body": msg, "msgtype": "m.notice"},
        )
        print(f"Sent message with event ID {event_id}")


asyncio.run(main())

Note than, when specifying the room ID, the exclamation mark is a special character in certain shells and must either be escaped using a backslash, or enclosed within quotes (in some shells, single quotes must be used). Also, the bot user must be in the room, and have permissions to send events. You may want to log in as the bot using a different client and join some rooms.

# python examples/send_message.py '!fMkMNQHvGATNJGrbZO:example.org' Hello world
Sent message with event ID $wiyFMuEbVMwMNyYNnl9uTnrY5ZDHa0vEO6q1wmXAxWo

State events

To send a state event in a room, a different endpoint is used in the general case, and some state events have special endpoints.

Todo