async-matrix 2.1.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +8 -8
- data/README.md +33 -58
- data/ext/async_matrix_e2ee/Cargo.toml +7 -1
- data/lib/async/matrix/api/chain.rb +120 -94
- data/lib/async/matrix/api/concat.rb +28 -22
- data/lib/async/matrix/api/path_tree.rb +71 -44
- data/lib/async/matrix/api.rb +6 -1
- data/lib/async/matrix/app_service_client.rb +213 -0
- data/lib/async/matrix/auth_error.rb +20 -0
- data/lib/async/matrix/bad_json_error.rb +20 -0
- data/lib/async/matrix/client/encryption.rb +413 -0
- data/lib/async/matrix/client/rooms.rb +478 -0
- data/lib/async/matrix/client/sync.rb +508 -0
- data/lib/async/matrix/client.rb +217 -113
- data/lib/async/matrix/config/vivify.rb +107 -0
- data/lib/async/matrix/config.rb +184 -0
- data/lib/async/matrix/device_store.rb +1621 -0
- data/lib/async/matrix/double_puppet_client.rb +1 -1
- data/lib/async/matrix/e2ee/pickle_key.rb +182 -0
- data/lib/async/matrix/error.rb +9 -36
- data/lib/async/matrix/{application_service/error_response.rb → error_response.rb} +10 -10
- data/lib/async/matrix/homeserver_error.rb +20 -0
- data/lib/async/matrix/invalid_endpoint_error.rb +20 -0
- data/lib/async/matrix/media_client.rb +46 -35
- data/lib/async/matrix/not_found_error.rb +20 -0
- data/lib/async/matrix/notifier.rb +7 -3
- data/lib/async/matrix/response_too_large_error.rb +20 -0
- data/lib/async/matrix/version.rb +1 -1
- data/lib/async/matrix.rb +16 -7
- data/lib/protocol/matrix/canonical_json.rb +200 -0
- data/lib/protocol/matrix/content.rb +106 -0
- data/lib/protocol/matrix/encrypted_message.rb +1018 -0
- data/lib/protocol/matrix/error.rb +49 -0
- data/lib/protocol/matrix/event.rb +208 -0
- data/lib/protocol/matrix/key_backup.rb +347 -0
- data/lib/protocol/matrix/keys.rb +381 -0
- data/lib/protocol/matrix/message_batch.rb +482 -0
- data/lib/protocol/matrix/schema/registry.rb +381 -0
- data/lib/protocol/matrix/schema/validation_error.rb +234 -0
- data/lib/protocol/matrix/schema.rb +170 -0
- data/lib/protocol/matrix/secret_storage.rb +535 -0
- data/lib/protocol/matrix/signing.rb +278 -0
- data/lib/protocol/matrix.rb +25 -0
- metadata +42 -135
- data/lib/async/discord/api/path_tree.rb +0 -127
- data/lib/async/discord/api.rb +0 -151
- data/lib/async/discord/client.rb +0 -283
- data/lib/async/discord/error.rb +0 -84
- data/lib/async/discord/gateway.rb +0 -359
- data/lib/async/discord.rb +0 -15
- data/lib/async/matrix/application_service/bot.rb +0 -232
- data/lib/async/matrix/application_service/config/schema/analytics.json +0 -21
- data/lib/async/matrix/application_service/config/schema/appservice.json +0 -82
- data/lib/async/matrix/application_service/config/schema/backfill.json +0 -91
- data/lib/async/matrix/application_service/config/schema/bridge.json +0 -209
- data/lib/async/matrix/application_service/config/schema/config.json +0 -61
- data/lib/async/matrix/application_service/config/schema/database.json +0 -38
- data/lib/async/matrix/application_service/config/schema/direct_media.json +0 -35
- data/lib/async/matrix/application_service/config/schema/double_puppet.json +0 -24
- data/lib/async/matrix/application_service/config/schema/encryption.json +0 -164
- data/lib/async/matrix/application_service/config/schema/homeserver.json +0 -58
- data/lib/async/matrix/application_service/config/schema/logging.json +0 -50
- data/lib/async/matrix/application_service/config/schema/management_room_texts.json +0 -25
- data/lib/async/matrix/application_service/config/schema/matrix.json +0 -45
- data/lib/async/matrix/application_service/config/schema/permissions.json +0 -54
- data/lib/async/matrix/application_service/config/schema/provisioning.json +0 -23
- data/lib/async/matrix/application_service/config/schema/public_media.json +0 -39
- data/lib/async/matrix/application_service/config/schema/relay.json +0 -43
- data/lib/async/matrix/application_service/config/vivify.rb +0 -109
- data/lib/async/matrix/application_service/config.rb +0 -225
- data/lib/async/matrix/application_service/dispatcher.rb +0 -185
- data/lib/async/matrix/application_service/event.rb +0 -285
- data/lib/async/matrix/application_service/server.rb +0 -430
- data/lib/async/matrix/application_service/transaction.rb +0 -66
- data/lib/async/matrix/application_service/transaction_handler.rb +0 -185
- data/lib/async/matrix/application_service/transaction_store.rb +0 -80
- data/lib/async/matrix/bridge/discord/db/connection.rb +0 -141
- data/lib/async/matrix/bridge/discord/db/file.rb +0 -118
- data/lib/async/matrix/bridge/discord/db/guild.rb +0 -120
- data/lib/async/matrix/bridge/discord/db/message.rb +0 -160
- data/lib/async/matrix/bridge/discord/db/migrations/001_create_users.rb +0 -14
- data/lib/async/matrix/bridge/discord/db/migrations/002_create_guilds.rb +0 -14
- data/lib/async/matrix/bridge/discord/db/migrations/003_create_portals.rb +0 -23
- data/lib/async/matrix/bridge/discord/db/migrations/004_create_puppets.rb +0 -19
- data/lib/async/matrix/bridge/discord/db/migrations/005_create_messages.rb +0 -20
- data/lib/async/matrix/bridge/discord/db/migrations/006_create_reactions.rb +0 -19
- data/lib/async/matrix/bridge/discord/db/migrations/007_create_files.rb +0 -18
- data/lib/async/matrix/bridge/discord/db/portal.rb +0 -150
- data/lib/async/matrix/bridge/discord/db/puppet.rb +0 -128
- data/lib/async/matrix/bridge/discord/db/reaction.rb +0 -165
- data/lib/async/matrix/bridge/discord/db/schema.rb +0 -18
- data/lib/async/matrix/bridge/discord/db/user.rb +0 -112
- data/lib/async/matrix/bridge/discord/db.rb +0 -138
- data/lib/async/matrix/schema/registry.rb +0 -354
- data/lib/async/matrix/schema/validation_error.rb +0 -225
- data/lib/async/matrix/schema.rb +0 -170
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the Apache License, Version 2.0.
|
|
4
|
+
# Copyright, 2026, by General Intelligence Systems.
|
|
5
|
+
|
|
6
|
+
require "securerandom"
|
|
7
|
+
|
|
8
|
+
module Async
|
|
9
|
+
module Matrix
|
|
10
|
+
class Client
|
|
11
|
+
# The endpoints that make a device exist, be reachable, and be able to
|
|
12
|
+
# share keys. Mixed into Client.
|
|
13
|
+
#
|
|
14
|
+
# ORDER MATTERS, and it is the one thing a reader needs from this file:
|
|
15
|
+
#
|
|
16
|
+
# register_user the account exists (appservices only)
|
|
17
|
+
# create_device the device exists, without /login (MSC4190)
|
|
18
|
+
# upload_keys other devices can now find and reach us
|
|
19
|
+
# query_keys we learn who else is in the room, and their keys
|
|
20
|
+
# claim_keys we take a key to open a session with one of them
|
|
21
|
+
# send_to_device we hand them a room key through that session
|
|
22
|
+
#
|
|
23
|
+
# Until `upload_keys` has run the device is invisible: nobody can claim a
|
|
24
|
+
# one-time key, so nobody can open an Olm session, so nobody can send us a
|
|
25
|
+
# room key, and every message in an encrypted room stays ciphertext. It is
|
|
26
|
+
# a precondition, not an optimisation.
|
|
27
|
+
#
|
|
28
|
+
# Every method routes through #api, so the path is validated against the
|
|
29
|
+
# vendored OpenAPI tree before a request is made -- a typo is an
|
|
30
|
+
# InvalidEndpointError here rather than a 404 from the homeserver.
|
|
31
|
+
module Encryption
|
|
32
|
+
# ── Publishing our own keys ───────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
# POST /keys/upload. Any combination of the three may be sent; a nil is
|
|
35
|
+
# omitted rather than sent as null.
|
|
36
|
+
#
|
|
37
|
+
# @parameter device_keys [Hash] from Protocol::Matrix::Keys.device_keys
|
|
38
|
+
# @parameter one_time_keys [Hash] from Keys.one_time_keys
|
|
39
|
+
# @parameter fallback_keys [Hash] from Keys.fallback_keys
|
|
40
|
+
# @returns [Hash] with `one_time_key_counts`, which is what tells us how
|
|
41
|
+
# many the server now holds.
|
|
42
|
+
def upload_keys(device_keys: nil, one_time_keys: nil, fallback_keys: nil)
|
|
43
|
+
body = {}
|
|
44
|
+
|
|
45
|
+
if device_keys
|
|
46
|
+
body[:device_keys] = device_keys
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
unless one_time_keys.nil? || one_time_keys.empty?
|
|
50
|
+
body[:one_time_keys] = one_time_keys
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless fallback_keys.nil? || fallback_keys.empty?
|
|
54
|
+
body[:fallback_keys] = fallback_keys
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
api.keys.upload.post(body)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# POST /keys/device_signing/upload -- the cross-signing keys.
|
|
61
|
+
#
|
|
62
|
+
# MSC4190 removed the user-interactive auth requirement here for
|
|
63
|
+
# appservices, which is what makes this reachable without a password.
|
|
64
|
+
# It matters because MSC4153 lets senders refuse to share room keys with
|
|
65
|
+
# a device that is not cross-signed.
|
|
66
|
+
def upload_cross_signing_keys(master_key: nil, self_signing_key: nil, user_signing_key: nil)
|
|
67
|
+
body = {}
|
|
68
|
+
|
|
69
|
+
if master_key
|
|
70
|
+
body[:master_key] = master_key
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if self_signing_key
|
|
74
|
+
body[:self_signing_key] = self_signing_key
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if user_signing_key
|
|
78
|
+
body[:user_signing_key] = user_signing_key
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
api.keys.device_signing.upload.post(body)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# POST /keys/signatures/upload -- our signatures over other keys.
|
|
85
|
+
def upload_signatures(signatures)
|
|
86
|
+
api.keys.signatures.upload.post(signatures)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# ── Learning about other devices ──────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
# POST /keys/query. +user_ids+ is who to ask about; an empty device list
|
|
92
|
+
# per user means "all of their devices".
|
|
93
|
+
#
|
|
94
|
+
# @parameter token [String] the `device_lists` sync token, so the server
|
|
95
|
+
# can tell us whether our view is already current.
|
|
96
|
+
def query_keys(user_ids:, token: nil)
|
|
97
|
+
body = {device_keys: user_ids.to_h { |user_id| [user_id, []] }}
|
|
98
|
+
|
|
99
|
+
if token
|
|
100
|
+
body[:token] = token
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
api.keys.query.post(body)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# POST /keys/claim -- take one one-time key per device, to open a
|
|
107
|
+
# session.
|
|
108
|
+
#
|
|
109
|
+
# ONLY FOR DEVICES WE HAVE NO SESSION WITH. Each claim consumes a key
|
|
110
|
+
# from a finite pool, so claiming for a device we can already reach
|
|
111
|
+
# burns one for nothing.
|
|
112
|
+
#
|
|
113
|
+
# @parameter devices [Hash] { "@user:server" => ["DEVICEID", ...] }
|
|
114
|
+
def claim_keys(devices:, algorithm: Protocol::Matrix::Keys::SIGNED_CURVE25519, timeout: nil)
|
|
115
|
+
body = {
|
|
116
|
+
one_time_keys: devices.transform_values { |ids|
|
|
117
|
+
ids.to_h { |device_id| [device_id, algorithm] }
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if timeout
|
|
122
|
+
body[:timeout] = timeout
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
api.keys.claim.post(body)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# ── Talking to devices directly ───────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
# PUT /sendToDevice/{eventType}/{txnId}
|
|
131
|
+
#
|
|
132
|
+
# @parameter messages [Hash] { "@user:server" => { "DEVICEID" => content } }
|
|
133
|
+
#
|
|
134
|
+
# This is how a room key travels. The transaction id makes it
|
|
135
|
+
# idempotent, so a retry after a timeout cannot deliver twice.
|
|
136
|
+
def send_to_device(event_type:, messages:, txn_id: nil)
|
|
137
|
+
api.sendToDevice(event_type, txn_id || SecureRandom.uuid).put({messages: messages})
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# ── Device lifecycle ──────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
# PUT /devices/{deviceId} -- MSC4190.
|
|
143
|
+
#
|
|
144
|
+
# THE REASON THIS EXISTS: appservices used to create devices by calling
|
|
145
|
+
# /login with `m.login.application_service`, and that route is gone on a
|
|
146
|
+
# homeserver fronted by OAuth2 (MAS). This endpoint creates the device
|
|
147
|
+
# with no login at all, answering 201 for a new one and 200 for one that
|
|
148
|
+
# already existed.
|
|
149
|
+
#
|
|
150
|
+
# The device id is OURS to choose and must never change afterwards: it
|
|
151
|
+
# is the identity every room key we hold is bound to.
|
|
152
|
+
def create_device(device_id:, display_name: nil)
|
|
153
|
+
body = {}
|
|
154
|
+
|
|
155
|
+
if display_name
|
|
156
|
+
body[:display_name] = display_name
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
api.devices(device_id).put(body)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# DELETE /devices/{deviceId}. MSC4190 removed the UIA requirement for
|
|
163
|
+
# appservices, which is what makes this callable unattended.
|
|
164
|
+
def delete_device(device_id:)
|
|
165
|
+
api.devices(device_id).delete
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def devices = api.devices.get
|
|
169
|
+
|
|
170
|
+
def device(device_id:) = api.devices(device_id).get
|
|
171
|
+
|
|
172
|
+
# POST /register for an appservice-owned user.
|
|
173
|
+
#
|
|
174
|
+
# `inhibit_login` IS MANDATORY, not tidiness: honouring a login would
|
|
175
|
+
# mean issuing an access token, and under OAuth2 the homeserver no
|
|
176
|
+
# longer owns that -- so without it the call fails with
|
|
177
|
+
# M_APPSERVICE_LOGIN_UNSUPPORTED.
|
|
178
|
+
def register_user(username:)
|
|
179
|
+
api.register.post(
|
|
180
|
+
{
|
|
181
|
+
type: "m.login.application_service",
|
|
182
|
+
username: username,
|
|
183
|
+
inhibit_login: true,
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# ── Key backup ────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
# GET /room_keys/version[/{version}] -- the backup's algorithm and
|
|
191
|
+
# auth_data, which carries the public key it was encrypted to.
|
|
192
|
+
def key_backup_version(version: nil)
|
|
193
|
+
if version
|
|
194
|
+
api.room_keys.version(version).get
|
|
195
|
+
else
|
|
196
|
+
api.room_keys.version.get
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# GET /room_keys/keys -- every backed-up session, as
|
|
201
|
+
# rooms -> sessions -> session_data blobs for
|
|
202
|
+
# Protocol::Matrix::KeyBackup to decrypt.
|
|
203
|
+
def room_keys(version:)
|
|
204
|
+
# Plain keys, not the "?"-prefixed form: that convention exists to
|
|
205
|
+
# separate query params from a BODY on POST/PUT, and a GET has no
|
|
206
|
+
# body -- every kwarg is already a query parameter.
|
|
207
|
+
api.room_keys.keys.get(version: version)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
__END__
|
|
215
|
+
describe "Async::Matrix::Client::Encryption" do
|
|
216
|
+
# A real Client with its transport stubbed, so these specs exercise the
|
|
217
|
+
# actual path construction AND the OpenAPI path-tree validation -- a wrong
|
|
218
|
+
# path fails here rather than as a 404 from a homeserver.
|
|
219
|
+
def recording_client(response = {})
|
|
220
|
+
# The REAL path tree. Api memoises it process-wide, and the Api specs
|
|
221
|
+
# inject a small fixture tree without restoring it (Api.reset! exists for
|
|
222
|
+
# that and goes uncalled), so without this these specs pass or fail
|
|
223
|
+
# according to the order scampi happens to load files in.
|
|
224
|
+
Async::Matrix::Api.reset!
|
|
225
|
+
|
|
226
|
+
config = Async::Matrix::Config.new({
|
|
227
|
+
"homeserver" => {"address" => "http://synapse:8008", "domain" => "example.org"},
|
|
228
|
+
"appservice" => {"as_token" => "as", "hs_token" => "hs", "bot" => {"username" => "bot"}},
|
|
229
|
+
})
|
|
230
|
+
client = Async::Matrix::Client.new(config)
|
|
231
|
+
calls = []
|
|
232
|
+
client.define_singleton_method(:calls) { calls }
|
|
233
|
+
client.define_singleton_method(:request) do |method, path, body = nil, **_options|
|
|
234
|
+
calls << [method, path, body]
|
|
235
|
+
response
|
|
236
|
+
end
|
|
237
|
+
client
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def last(client) = client.calls.last
|
|
241
|
+
|
|
242
|
+
# ── Publishing ────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
it "uploads device keys" do
|
|
245
|
+
client = recording_client
|
|
246
|
+
client.upload_keys(device_keys: {"user_id" => "@bot:example.org"})
|
|
247
|
+
|
|
248
|
+
last(client)[0].should == "POST"
|
|
249
|
+
last(client)[1].should == "/_matrix/client/v3/keys/upload"
|
|
250
|
+
last(client)[2].should == {device_keys: {"user_id" => "@bot:example.org"}}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "uploads all three kinds of key together" do
|
|
254
|
+
client = recording_client
|
|
255
|
+
client.upload_keys(
|
|
256
|
+
device_keys: {"a" => 1},
|
|
257
|
+
one_time_keys: {"signed_curve25519:k" => {"key" => "k"}},
|
|
258
|
+
fallback_keys: {"signed_curve25519:f" => {"key" => "f", "fallback" => true}},
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
last(client)[2].keys.sort.should == [:device_keys, :fallback_keys, :one_time_keys]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# A nil is omitted rather than sent as null -- "May be absent if no new
|
|
265
|
+
# one-time keys are required".
|
|
266
|
+
it "omits what it was not given" do
|
|
267
|
+
client = recording_client
|
|
268
|
+
client.upload_keys(one_time_keys: {})
|
|
269
|
+
|
|
270
|
+
last(client)[2].should == {}
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
it "uploads cross-signing keys" do
|
|
274
|
+
client = recording_client
|
|
275
|
+
client.upload_cross_signing_keys(master_key: {"keys" => {}})
|
|
276
|
+
|
|
277
|
+
last(client)[1].should == "/_matrix/client/v3/keys/device_signing/upload"
|
|
278
|
+
last(client)[2].should == {master_key: {"keys" => {}}}
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
it "uploads signatures" do
|
|
282
|
+
client = recording_client
|
|
283
|
+
client.upload_signatures({"@bot:example.org" => {"ed25519:DEV" => {}}})
|
|
284
|
+
|
|
285
|
+
last(client)[1].should == "/_matrix/client/v3/keys/signatures/upload"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# ── Querying ──────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
# An empty device list per user means "all of their devices".
|
|
291
|
+
it "queries keys for a set of users" do
|
|
292
|
+
client = recording_client
|
|
293
|
+
client.query_keys(user_ids: ["@ada:example.org", "@bob:example.org"])
|
|
294
|
+
|
|
295
|
+
last(client)[1].should == "/_matrix/client/v3/keys/query"
|
|
296
|
+
last(client)[2].should == {
|
|
297
|
+
device_keys: {"@ada:example.org" => [], "@bob:example.org" => []},
|
|
298
|
+
}
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
it "passes the device_lists token when it has one" do
|
|
302
|
+
client = recording_client
|
|
303
|
+
client.query_keys(user_ids: ["@ada:example.org"], token: "s72")
|
|
304
|
+
|
|
305
|
+
last(client)[2][:token].should == "s72"
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it "claims one key per device, naming the algorithm" do
|
|
309
|
+
client = recording_client
|
|
310
|
+
client.claim_keys(devices: {"@ada:example.org" => ["DEV1", "DEV2"]})
|
|
311
|
+
|
|
312
|
+
last(client)[1].should == "/_matrix/client/v3/keys/claim"
|
|
313
|
+
last(client)[2].should == {
|
|
314
|
+
one_time_keys: {
|
|
315
|
+
"@ada:example.org" => {
|
|
316
|
+
"DEV1" => "signed_curve25519",
|
|
317
|
+
"DEV2" => "signed_curve25519",
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# ── To-device ─────────────────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
it "sends to-device messages with a transaction id" do
|
|
326
|
+
client = recording_client
|
|
327
|
+
client.send_to_device(
|
|
328
|
+
event_type: "m.room.encrypted",
|
|
329
|
+
messages: {"@ada:example.org" => {"DEV1" => {"algorithm" => "m.olm.v1.curve25519-aes-sha2"}}},
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
last(client)[0].should == "PUT"
|
|
333
|
+
last(client)[1].should.be.start_with? "/_matrix/client/v3/sendToDevice/m.room.encrypted/"
|
|
334
|
+
last(client)[2].should == {
|
|
335
|
+
messages: {"@ada:example.org" => {"DEV1" => {"algorithm" => "m.olm.v1.curve25519-aes-sha2"}}},
|
|
336
|
+
}
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Idempotence: a retry after a timeout must not deliver twice.
|
|
340
|
+
it "lets the caller supply the transaction id" do
|
|
341
|
+
client = recording_client
|
|
342
|
+
client.send_to_device(event_type: "m.room.encrypted", messages: {}, txn_id: "txn1")
|
|
343
|
+
|
|
344
|
+
last(client)[1].should == "/_matrix/client/v3/sendToDevice/m.room.encrypted/txn1"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
it "generates a different transaction id each time" do
|
|
348
|
+
client = recording_client
|
|
349
|
+
client.send_to_device(event_type: "m.room.encrypted", messages: {})
|
|
350
|
+
client.send_to_device(event_type: "m.room.encrypted", messages: {})
|
|
351
|
+
|
|
352
|
+
client.calls[0][1].should.not == client.calls[1][1]
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# ── Devices ───────────────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
# MSC4190: creates the device with no /login, which is the only way under
|
|
358
|
+
# OAuth2.
|
|
359
|
+
it "creates a device" do
|
|
360
|
+
client = recording_client
|
|
361
|
+
client.create_device(device_id: "ABCDEFGHIJ", display_name: "controller")
|
|
362
|
+
|
|
363
|
+
last(client)[0].should == "PUT"
|
|
364
|
+
last(client)[1].should == "/_matrix/client/v3/devices/ABCDEFGHIJ"
|
|
365
|
+
last(client)[2].should == {display_name: "controller"}
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
it "deletes a device" do
|
|
369
|
+
client = recording_client
|
|
370
|
+
client.delete_device(device_id: "ABCDEFGHIJ")
|
|
371
|
+
|
|
372
|
+
last(client)[0].should == "DELETE"
|
|
373
|
+
last(client)[1].should == "/_matrix/client/v3/devices/ABCDEFGHIJ"
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# inhibit_login is mandatory under OAuth2: honouring a login would mean
|
|
377
|
+
# issuing an access token the homeserver no longer owns.
|
|
378
|
+
it "registers an appservice user without logging it in" do
|
|
379
|
+
client = recording_client
|
|
380
|
+
client.register_user(username: "controller")
|
|
381
|
+
|
|
382
|
+
last(client)[1].should == "/_matrix/client/v3/register"
|
|
383
|
+
last(client)[2].should == {
|
|
384
|
+
type: "m.login.application_service",
|
|
385
|
+
username: "controller",
|
|
386
|
+
inhibit_login: true,
|
|
387
|
+
}
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# ── Key backup ────────────────────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
it "reads the current backup version" do
|
|
393
|
+
client = recording_client
|
|
394
|
+
client.key_backup_version
|
|
395
|
+
|
|
396
|
+
last(client)[0].should == "GET"
|
|
397
|
+
last(client)[1].should == "/_matrix/client/v3/room_keys/version"
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
it "reads a specific backup version" do
|
|
401
|
+
client = recording_client
|
|
402
|
+
client.key_backup_version(version: "3")
|
|
403
|
+
|
|
404
|
+
last(client)[1].should == "/_matrix/client/v3/room_keys/version/3"
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
it "fetches the backed-up keys for a version" do
|
|
408
|
+
client = recording_client
|
|
409
|
+
client.room_keys(version: "3")
|
|
410
|
+
|
|
411
|
+
last(client)[1].should == "/_matrix/client/v3/room_keys/keys?version=3"
|
|
412
|
+
end
|
|
413
|
+
end
|