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.
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.
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.
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.
@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.
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.
@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
.
# {{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
…