Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 102 additions & 5 deletions matrix_client/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,7 @@ def send_message_event(self, room_id, event_type, content, txn_id=None,
timestamp (int): Set origin_server_ts (For application services only)
"""
if not txn_id:
txn_id = str(self.txn_id) + str(int(time() * 1000))

self.txn_id = self.txn_id + 1
txn_id = self._make_txn_id()

path = "/rooms/%s/send/%s/%s" % (
quote(room_id), quote(event_type), quote(str(txn_id)),
Expand All @@ -265,9 +263,8 @@ def redact_event(self, room_id, event_id, reason=None, txn_id=None, timestamp=No
timestamp(int): Optional. Set origin_server_ts (For application services only)
"""
if not txn_id:
txn_id = str(self.txn_id) + str(int(time() * 1000))
txn_id = self._make_txn_id()

self.txn_id = self.txn_id + 1
path = '/rooms/%s/redact/%s/%s' % (
room_id, event_id, txn_id
)
Expand Down Expand Up @@ -836,3 +833,103 @@ def delete_devices(self, auth_body, devices):
"devices": devices
}
return self._send("POST", "/delete_devices", content=content)

def upload_keys(self, device_keys=None, one_time_keys=None):
"""Publishes end-to-end encryption keys for the device.

Said device must be the one used when logging in.

Args:
device_keys (dict): Optional. Identity keys for the device. The required
keys are:

| user_id (str): The ID of the user the device belongs to. Must match
the user ID used when logging in.
| device_id (str): The ID of the device these keys belong to. Must match
the device ID used when logging in.
| algorithms (list<str>): The encryption algorithms supported by this
device.
| keys (dict): Public identity keys. Should be formatted as
<algorithm:device_id>: <key>.
| signatures (dict): Signatures for the device key object. Should be
formatted as <user_id>: {<algorithm:device_id>: <key>}

one_time_keys (dict): Optional. One-time public keys. Should be
formatted as <algorithm:key_id>: <key>, the key format being
determined by the algorithm.
"""
content = {}
if device_keys:
content["device_keys"] = device_keys
if one_time_keys:
content["one_time_keys"] = one_time_keys
return self._send("POST", "/keys/upload", content=content)

def query_keys(self, user_devices, timeout=None, token=None):
"""Query HS for public keys by user and optionally device.

Args:
user_devices (dict): The devices whose keys to download. Should be
formatted as <user_id>: [<device_ids>]. No device_ids indicates
all devices for the corresponding user.
timeout (int): Optional. The time (in milliseconds) to wait when
downloading keys from remote servers.
token (str): Optional. If the client is fetching keys as a result of
a device update received in a sync request, this should be the
'since' token of that sync request, or any later sync token.
"""
content = {"device_keys": user_devices}
if timeout:
content["timeout"] = timeout
if token:
content["token"] = token
return self._send("POST", "/keys/query", content=content)

def claim_keys(self, key_request, timeout=None):
"""Claims one-time keys for use in pre-key messages.

Args:
key_request (dict): The keys to be claimed. Format should be
<user_id>: { <device_id>: <algorithm> }.
timeout (int): Optional. The time (in milliseconds) to wait when
downloading keys from remote servers.
"""
content = {"one_time_keys": key_request}
if timeout:
content["timeout"] = timeout
return self._send("POST", "/keys/claim", content=content)

def key_changes(self, from_token, to_token):
"""Gets a list of users who have updated their device identity keys.

Args:
from_token (str): The desired start point of the list. Should be the
next_batch field from a response to an earlier call to /sync.
to_token (str): The desired end point of the list. Should be the next_batch
field from a recent call to /sync - typically the most recent such call.
"""
params = {"from": from_token, "to": to_token}
return self._send("GET", "/keys/changes", query_params=params)

def send_to_device(self, event_type, messages, txn_id=None):
"""Sends send-to-device events to a set of client devices.

Args:
event_type (str): The type of event to send.
messages (dict): The messages to send. Format should be
<user_id>: {<device_id>: <event_content>}.
The device ID may also be '*', meaning all known devices for the user.
txn_id (str): Optional. The transaction ID for this event, will be generated
automatically otherwise.
"""
txn_id = txn_id if txn_id else self._make_txn_id()
return self._send(
"PUT",
"/sendToDevice/{}/{}".format(event_type, txn_id),
content={"messages": messages}
)

def _make_txn_id(self):
txn_id = str(self.txn_id) + str(int(time() * 1000))
self.txn_id += 1
return txn_id
84 changes: 84 additions & 0 deletions test/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,87 @@ def test_delete_devices(self):

with pytest.raises(MatrixRequestError):
self.cli.api.delete_devices(self.auth_body, [self.device_id])


class TestKeysApi:
cli = client.MatrixClient("http://example.com")
user_id = "@alice:matrix.org"
device_id = "JLAFKJWSCS"
one_time_keys = {"curve25519:AAAAAQ": "/qyvZvwjiTxGdGU0RCguDCLeR+nmsb3FfNG3/Ve4vU8"}
device_keys = {
"user_id": "@alice:example.com",
"device_id": "JLAFKJWSCS",
"algorithms": [
"m.olm.curve25519-aes-sha256",
"m.megolm.v1.aes-sha"
],
"keys": {
"curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI",
"ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"
},
"signatures": {
"@alice:example.com": {
"ed25519:JLAFKJWSCS": ("dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2gi"
"MIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA")
}
}
}

@responses.activate
@pytest.mark.parametrize("args", [
{},
{'device_keys': device_keys},
{'one_time_keys': one_time_keys}
])
def test_upload_keys(self, args):
upload_keys_url = "http://example.com/_matrix/client/r0/keys/upload"
responses.add(responses.POST, upload_keys_url, body='{}')
self.cli.api.upload_keys(**args)
req = responses.calls[0].request
assert req.url == upload_keys_url
assert req.method == 'POST'

@responses.activate
def test_query_keys(self):
query_user_keys_url = "http://example.com/_matrix/client/r0/keys/query"
responses.add(responses.POST, query_user_keys_url, body='{}')
self.cli.api.query_keys({self.user_id: self.device_id}, timeout=10)
req = responses.calls[0].request
assert req.url == query_user_keys_url
assert req.method == 'POST'

@responses.activate
def test_claim_keys(self):
claim_keys_url = "http://example.com/_matrix/client/r0/keys/claim"
responses.add(responses.POST, claim_keys_url, body='{}')
self.cli.api.claim_keys({self.user_id: {self.device_id: "algo"}}, timeout=1000)
req = responses.calls[0].request
assert req.url == claim_keys_url
assert req.method == 'POST'

@responses.activate
def test_key_changes(self):
key_changes_url = "http://example.com/_matrix/client/r0/keys/changes"
responses.add(responses.GET, key_changes_url, body='{}')
self.cli.api.key_changes('s72594_4483_1934', 's75689_5632_2435')
req = responses.calls[0].request
assert req.url.split('?')[0] == key_changes_url
assert req.method == 'GET'


class TestSendToDeviceApi:
cli = client.MatrixClient("http://example.com")
user_id = "@alice:matrix.org"
device_id = "JLAFKJWSCS"

@responses.activate
def test_send_to_device(self):
txn_id = self.cli.api._make_txn_id()
send_to_device_url = \
"http://example.com/_matrix/client/r0/sendToDevice/m.new_device/" + txn_id
responses.add(responses.PUT, send_to_device_url, body='{}')
payload = {self.user_id: {self.device_id: {"test": 1}}}
self.cli.api.send_to_device("m.new_device", payload, txn_id)
req = responses.calls[0].request
assert req.url == send_to_device_url
assert req.method == 'PUT'