From 16772a2b963b06bd9697c1de773c3bbb409fa99c Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Fri, 18 May 2018 17:32:19 +0200 Subject: [PATCH 1/3] add /keys/ endpoints to api along with some helpers --- matrix_client/api.py | 77 ++++++++++++++++++++++++++++++++++++++++++++ test/api_test.py | 66 +++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/matrix_client/api.py b/matrix_client/api.py index 8220b019..87c89d3f 100644 --- a/matrix_client/api.py +++ b/matrix_client/api.py @@ -836,3 +836,80 @@ 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): The encryption algorithms supported by this + device. + | keys (dict): Public identity keys. Should be formatted as + : . + | signatures (dict): Signatures for the device key object. Should be + formatted as : {: } + + one_time_keys (dict): Optional. One-time public keys. Should be + formatted as : , 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 : []. 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 + : { : }. + 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) diff --git a/test/api_test.py b/test/api_test.py index 4b359dfe..26c1434a 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -148,3 +148,69 @@ 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' From 21c4b1a1b860691c4e148c873f8b51f7a17e2841 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Mon, 28 May 2018 09:16:22 +0200 Subject: [PATCH 2/3] add method to generate txn IDs --- matrix_client/api.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/matrix_client/api.py b/matrix_client/api.py index 87c89d3f..3905bbb2 100644 --- a/matrix_client/api.py +++ b/matrix_client/api.py @@ -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)), @@ -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 ) @@ -913,3 +910,8 @@ def key_changes(self, from_token, to_token): """ params = {"from": from_token, "to": to_token} return self._send("GET", "/keys/changes", query_params=params) + + def _make_txn_id(self): + txn_id = str(self.txn_id) + str(int(time() * 1000)) + self.txn_id += 1 + return txn_id From a8dc4dc2679eaa6cabdc8a5f2b2ee1a1ce127902 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Mon, 28 May 2018 09:36:22 +0200 Subject: [PATCH 3/3] add sendToDevice API --- matrix_client/api.py | 18 ++++++++++++++++++ test/api_test.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/matrix_client/api.py b/matrix_client/api.py index 3905bbb2..264861c2 100644 --- a/matrix_client/api.py +++ b/matrix_client/api.py @@ -911,6 +911,24 @@ def key_changes(self, from_token, to_token): 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 + : {: }. + 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 diff --git a/test/api_test.py b/test/api_test.py index 26c1434a..01369662 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -214,3 +214,21 @@ def test_key_changes(self): 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'