async-matrix 2.1.0-x86_64-linux → 3.0.1-x86_64-linux
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/README.md +33 -58
- data/lib/async/matrix/3.3/async_matrix_e2ee.so +0 -0
- data/lib/async/matrix/3.4/async_matrix_e2ee.so +0 -0
- data/lib/async/matrix/4.0/async_matrix_e2ee.so +0 -0
- 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 +43 -136
- 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,508 @@
|
|
|
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 "console"
|
|
7
|
+
|
|
8
|
+
require_relative "../../../protocol/matrix/message_batch"
|
|
9
|
+
|
|
10
|
+
module Async
|
|
11
|
+
module Matrix
|
|
12
|
+
class Client
|
|
13
|
+
# A stream of messages from the homeserver, decrypted on the way through.
|
|
14
|
+
#
|
|
15
|
+
# stream = client.sync(store: device_store)
|
|
16
|
+
#
|
|
17
|
+
# stream.each do |message|
|
|
18
|
+
# message.decrypted? # false means we hold no key for it YET
|
|
19
|
+
# message.type # the real type, once decrypted
|
|
20
|
+
# message.content
|
|
21
|
+
# checkpoint(stream.next_batch)
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# WHAT A CONSUMER SEES IS MESSAGES. /sync also carries to-device events,
|
|
25
|
+
# device lists and key counts, and none of that is a message: to-device
|
|
26
|
+
# events are how room keys travel, so they are fed to the store and never
|
|
27
|
+
# yielded. A consumer that had to recognise and route them would be
|
|
28
|
+
# reimplementing this class.
|
|
29
|
+
#
|
|
30
|
+
# ORDER IS THE WHOLE TRICK, and MessageBatch provides it. A batch
|
|
31
|
+
# routinely carries both an `m.room_key` and the timeline event that key
|
|
32
|
+
# unlocks; because the batch yields to-device first and the store absorbs
|
|
33
|
+
# keys as it reads them, that event decrypts on its first pass instead of
|
|
34
|
+
# being stored as a placeholder and repaired later.
|
|
35
|
+
#
|
|
36
|
+
# THE STORE IS PASSED IN, holding this device's keys. It is what makes any
|
|
37
|
+
# room readable, and it is optional only in the sense that a consumer
|
|
38
|
+
# interested solely in unencrypted rooms can omit it and receive encrypted
|
|
39
|
+
# messages undecrypted.
|
|
40
|
+
#
|
|
41
|
+
# NOT RESILIENT, DELIBERATELY. A failed request raises. How to react to a
|
|
42
|
+
# homeserver that is down -- retry, back off, give up, alert -- is policy,
|
|
43
|
+
# and policy belongs to whatever is driving the loop rather than to the
|
|
44
|
+
# thing reading the protocol.
|
|
45
|
+
class Sync
|
|
46
|
+
# Synapse holds a /sync request open until something happens or this
|
|
47
|
+
# elapses, so it is a normal quiet-room duration rather than a failure
|
|
48
|
+
# timeout.
|
|
49
|
+
DEFAULT_TIMEOUT = 30_000
|
|
50
|
+
|
|
51
|
+
def initialize(client, store: nil, since: nil, timeout: DEFAULT_TIMEOUT, filter: nil)
|
|
52
|
+
@client = client
|
|
53
|
+
@store = store
|
|
54
|
+
@next_batch = since
|
|
55
|
+
@timeout = timeout
|
|
56
|
+
@filter = filter
|
|
57
|
+
@one_time_keys_count = {}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
attr_reader :store, :timeout, :filter
|
|
61
|
+
|
|
62
|
+
# The cursor to resume from.
|
|
63
|
+
#
|
|
64
|
+
# COMMIT IT ONLY AFTER THE MESSAGES OF ITS BATCH ARE DURABLE. It advances
|
|
65
|
+
# when a batch is drained, so reading it inside the loop and storing it
|
|
66
|
+
# before handling the message is how a crash loses messages with nothing
|
|
67
|
+
# recording that it happened.
|
|
68
|
+
attr_reader :next_batch
|
|
69
|
+
|
|
70
|
+
# What the server last said it still holds of our one-time keys.
|
|
71
|
+
#
|
|
72
|
+
# Surfaced rather than acted on. Running low means topping up, which is
|
|
73
|
+
# an upload, and deciding when to upload is not this object's business --
|
|
74
|
+
# but a device whose keys run out silently stops being reachable, so the
|
|
75
|
+
# number has to be visible to someone.
|
|
76
|
+
attr_reader :one_time_keys_count
|
|
77
|
+
|
|
78
|
+
# One round trip. Returns the batch, undecrypted.
|
|
79
|
+
#
|
|
80
|
+
# An INITIAL sync -- no cursor -- asks for the full state of every room
|
|
81
|
+
# the account is in, which is as large as a response gets. A filter is
|
|
82
|
+
# the only thing that makes it smaller.
|
|
83
|
+
def poll
|
|
84
|
+
Protocol::Matrix::MessageBatch.from_sync(fetch)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Read one batch, yielding each room message with whatever the store
|
|
88
|
+
# could decrypt.
|
|
89
|
+
#
|
|
90
|
+
# @returns [Integer] how many messages were yielded.
|
|
91
|
+
def read(&block)
|
|
92
|
+
batch = poll
|
|
93
|
+
yielded = 0
|
|
94
|
+
|
|
95
|
+
batch.each do |message, _section|
|
|
96
|
+
if batch.to_device?
|
|
97
|
+
handle_to_device(message)
|
|
98
|
+
else
|
|
99
|
+
yielded += 1
|
|
100
|
+
block.call(decrypt(message))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# AFTER the batch, never during: the cursor says "everything before
|
|
105
|
+
# this has been handed over", and advancing it mid-batch would promise
|
|
106
|
+
# that of messages still in it.
|
|
107
|
+
@next_batch = batch.next_batch
|
|
108
|
+
yielded
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Poll forever, yielding every message. Stops when +stop+ says so, which
|
|
112
|
+
# defaults to never -- a sync stream has no natural end.
|
|
113
|
+
def each(stop: -> { false }, &block)
|
|
114
|
+
unless block
|
|
115
|
+
raise ArgumentError, "Client::Sync#each requires a block; a sync stream is not enumerable"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
until stop.call
|
|
119
|
+
read(&block)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def fetch
|
|
128
|
+
query = {timeout: @timeout}
|
|
129
|
+
|
|
130
|
+
if @next_batch
|
|
131
|
+
query[:since] = @next_batch
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if @filter
|
|
135
|
+
query[:filter] = @filter
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
response = @client.api.sync.get(**query)
|
|
139
|
+
remember_key_counts(response)
|
|
140
|
+
response
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def remember_key_counts(response)
|
|
144
|
+
counts = response["device_one_time_keys_count"]
|
|
145
|
+
|
|
146
|
+
if counts
|
|
147
|
+
@one_time_keys_count = counts
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# To-device messages are plumbing: decrypting one is how a room key
|
|
152
|
+
# reaches the store, and the store absorbs it as it reads. Nothing is
|
|
153
|
+
# yielded.
|
|
154
|
+
#
|
|
155
|
+
# A FAILURE HERE MUST NOT REACH THE LOOP. Anyone sharing a room can
|
|
156
|
+
# send us a to-device message, so a malformed one that propagated
|
|
157
|
+
# would be a remote-controlled crash -- and the loss is recoverable
|
|
158
|
+
# anyway, because a sender who sees us unable to decrypt re-shares the
|
|
159
|
+
# key. Protocol errors are swallowed; anything else is a bug in us and
|
|
160
|
+
# still raises.
|
|
161
|
+
def handle_to_device(message)
|
|
162
|
+
if @store && message.encrypted?
|
|
163
|
+
@store.decrypt(message)
|
|
164
|
+
end
|
|
165
|
+
rescue Protocol::Matrix::Error => error
|
|
166
|
+
Console.warn(self, "Discarding undecryptable to-device message.", error: error)
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# An encrypted message with no key is returned AS IT IS, undecrypted.
|
|
171
|
+
# That is not a failure: it is the commonest state in an encrypted
|
|
172
|
+
# room, the key may still arrive, and a consumer that stores the row
|
|
173
|
+
# now can have it repaired in place later.
|
|
174
|
+
def decrypt(message)
|
|
175
|
+
if @store && message.encrypted?
|
|
176
|
+
@store.decrypt(message)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
message
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
__END__
|
|
187
|
+
describe "Async::Matrix::Client::Sync" do
|
|
188
|
+
# A client faked at the api-chain level, so the specs exercise the real call
|
|
189
|
+
# path (client.api.sync.get) rather than a shortcut around it.
|
|
190
|
+
def fake_client(*responses)
|
|
191
|
+
queried = []
|
|
192
|
+
remaining = responses.dup
|
|
193
|
+
client = Object.new
|
|
194
|
+
client.define_singleton_method(:queried) { queried }
|
|
195
|
+
client.define_singleton_method(:api) do
|
|
196
|
+
chain = Object.new
|
|
197
|
+
chain.define_singleton_method(:sync) do
|
|
198
|
+
endpoint = Object.new
|
|
199
|
+
endpoint.define_singleton_method(:get) do |**query|
|
|
200
|
+
queried << query
|
|
201
|
+
remaining.shift || {"next_batch" => "empty"}
|
|
202
|
+
end
|
|
203
|
+
endpoint
|
|
204
|
+
end
|
|
205
|
+
chain
|
|
206
|
+
end
|
|
207
|
+
client
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# A store that records what it was asked to read and "decrypts" whatever it
|
|
211
|
+
# has a key for.
|
|
212
|
+
def fake_store(readable: [])
|
|
213
|
+
store = Object.new
|
|
214
|
+
seen = []
|
|
215
|
+
# Captured as locals: inside define_singleton_method, self is the store,
|
|
216
|
+
# so the spec's own helpers are out of scope.
|
|
217
|
+
olm = olm_session_double
|
|
218
|
+
megolm = megolm_session_double
|
|
219
|
+
|
|
220
|
+
store.define_singleton_method(:seen) { seen }
|
|
221
|
+
store.define_singleton_method(:decrypt) do |message|
|
|
222
|
+
seen << message
|
|
223
|
+
|
|
224
|
+
if message.olm?
|
|
225
|
+
message.decrypt!(olm, identity_key: message.recipients.first)
|
|
226
|
+
message
|
|
227
|
+
elsif readable.include?(message.session_id)
|
|
228
|
+
message.decrypt!(megolm)
|
|
229
|
+
message
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
store
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def megolm_session_double
|
|
236
|
+
session = Object.new
|
|
237
|
+
session.define_singleton_method(:decrypt) do |_ciphertext|
|
|
238
|
+
[JSON.generate({"type" => "m.room.message", "content" => {"body" => "plain"}}), 0]
|
|
239
|
+
end
|
|
240
|
+
session
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# A real olm payload: the OlmPayload schema's required fields are what make
|
|
244
|
+
# the message attributable, and EncryptedMessage refuses one without them.
|
|
245
|
+
def olm_session_double(type: "m.room_key")
|
|
246
|
+
session = Object.new
|
|
247
|
+
session.define_singleton_method(:decrypt) do |_type, _body|
|
|
248
|
+
JSON.generate({
|
|
249
|
+
"type" => type,
|
|
250
|
+
"content" => {"algorithm" => "m.megolm.v1.aes-sha2"},
|
|
251
|
+
"sender" => "@bob:example.org",
|
|
252
|
+
"recipient" => "@us:example.org",
|
|
253
|
+
"recipient_keys" => {"ed25519" => "ours"},
|
|
254
|
+
"keys" => {"ed25519" => "theirs"},
|
|
255
|
+
})
|
|
256
|
+
end
|
|
257
|
+
session
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def encrypted_timeline(session_id: "session1", event_id: "$enc")
|
|
261
|
+
{
|
|
262
|
+
"type" => "m.room.encrypted",
|
|
263
|
+
"event_id" => event_id,
|
|
264
|
+
"sender" => "@alice:example.org",
|
|
265
|
+
"content" => {
|
|
266
|
+
"algorithm" => "m.megolm.v1.aes-sha2",
|
|
267
|
+
"ciphertext" => "AwgAEnAC",
|
|
268
|
+
"session_id" => session_id,
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def plaintext_timeline(event_id = "$msg")
|
|
274
|
+
{
|
|
275
|
+
"type" => "m.room.message",
|
|
276
|
+
"event_id" => event_id,
|
|
277
|
+
"sender" => "@alice:example.org",
|
|
278
|
+
"content" => {"msgtype" => "m.text", "body" => "hello"},
|
|
279
|
+
}
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def to_device_event
|
|
283
|
+
{
|
|
284
|
+
"type" => "m.room.encrypted",
|
|
285
|
+
"sender" => "@bob:example.org",
|
|
286
|
+
"content" => {
|
|
287
|
+
"algorithm" => "m.olm.v1.curve25519-aes-sha2",
|
|
288
|
+
"sender_key" => "theircurve",
|
|
289
|
+
"ciphertext" => {"ourcurve" => {"type" => 0, "body" => "olmbody"}},
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def response(events: [plaintext_timeline], to_device: [], next_batch: "s2", **extra)
|
|
295
|
+
{
|
|
296
|
+
"next_batch" => next_batch,
|
|
297
|
+
"to_device" => {"events" => to_device},
|
|
298
|
+
"rooms" => {"join" => {"!room:example.org" => {"timeline" => {"events" => events}}}},
|
|
299
|
+
}.merge(extra)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def sync_for(client, **options)
|
|
303
|
+
Async::Matrix::Client::Sync.new(client, **options)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# ── The request ───────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
it "polls with a timeout and no cursor on the first request" do
|
|
309
|
+
client = fake_client(response)
|
|
310
|
+
sync_for(client).read { |_message| nil }
|
|
311
|
+
|
|
312
|
+
client.queried.should == [{timeout: 30_000}]
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
it "sends the cursor it was given" do
|
|
316
|
+
client = fake_client(response)
|
|
317
|
+
sync_for(client, since: "s1").read { |_message| nil }
|
|
318
|
+
|
|
319
|
+
client.queried.should == [{timeout: 30_000, since: "s1"}]
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it "sends a filter when it has one" do
|
|
323
|
+
client = fake_client(response)
|
|
324
|
+
sync_for(client, filter: "2", timeout: 100).read { |_message| nil }
|
|
325
|
+
|
|
326
|
+
client.queried.should == [{timeout: 100, filter: "2"}]
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
it "advances the cursor across polls" do
|
|
330
|
+
client = fake_client(response(next_batch: "s2"), response(next_batch: "s3"))
|
|
331
|
+
sync = sync_for(client)
|
|
332
|
+
|
|
333
|
+
sync.next_batch.should.be.nil
|
|
334
|
+
sync.read { |_message| nil }
|
|
335
|
+
sync.next_batch.should == "s2"
|
|
336
|
+
sync.read { |_message| nil }
|
|
337
|
+
sync.next_batch.should == "s3"
|
|
338
|
+
client.queried.last.should == {timeout: 30_000, since: "s2"}
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# ── What is yielded ───────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
it "yields room messages" do
|
|
344
|
+
sync = sync_for(fake_client(response(events: [plaintext_timeline("$a"), plaintext_timeline("$b")])))
|
|
345
|
+
seen = []
|
|
346
|
+
sync.read { |message| seen << message.event_id }
|
|
347
|
+
|
|
348
|
+
seen.should == ["$a", "$b"]
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# To-device events are how room keys travel. They are not messages, and a
|
|
352
|
+
# consumer that had to recognise them would be reimplementing this class.
|
|
353
|
+
it "never yields a to-device message" do
|
|
354
|
+
sync = sync_for(
|
|
355
|
+
fake_client(response(to_device: [to_device_event], events: [plaintext_timeline])),
|
|
356
|
+
store: fake_store,
|
|
357
|
+
)
|
|
358
|
+
seen = []
|
|
359
|
+
sync.read { |message| seen << message }
|
|
360
|
+
|
|
361
|
+
seen.length.should == 1
|
|
362
|
+
seen.first.type.should == "m.room.message"
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
it "still feeds to-device messages to the store" do
|
|
366
|
+
store = fake_store
|
|
367
|
+
sync = sync_for(fake_client(response(to_device: [to_device_event], events: [])), store: store)
|
|
368
|
+
sync.read { |_message| nil }
|
|
369
|
+
|
|
370
|
+
store.seen.length.should == 1
|
|
371
|
+
store.seen.first.olm?.should == true
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
it "counts what it yielded" do
|
|
375
|
+
sync = sync_for(fake_client(response(events: [plaintext_timeline("$a"), plaintext_timeline("$b")])))
|
|
376
|
+
|
|
377
|
+
sync.read { |_message| nil }.should == 2
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
it "yields nothing for an empty batch" do
|
|
381
|
+
sync = sync_for(fake_client(response(events: [])))
|
|
382
|
+
seen = []
|
|
383
|
+
|
|
384
|
+
sync.read { |message| seen << message }.should == 0
|
|
385
|
+
seen.should == []
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# ── Decryption ────────────────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
it "decrypts a room message with the store's keys" do
|
|
391
|
+
sync = sync_for(
|
|
392
|
+
fake_client(response(events: [encrypted_timeline(session_id: "known")])),
|
|
393
|
+
store: fake_store(readable: ["known"]),
|
|
394
|
+
)
|
|
395
|
+
seen = []
|
|
396
|
+
sync.read { |message| seen << message }
|
|
397
|
+
|
|
398
|
+
seen.first.decrypted?.should == true
|
|
399
|
+
seen.first.type.should == "m.room.message"
|
|
400
|
+
seen.first.content.should == {"body" => "plain"}
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# NOT A FAILURE, and not a reason to drop the message: the key may still
|
|
404
|
+
# arrive, and a consumer that stored this row can have it repaired later.
|
|
405
|
+
it "yields an encrypted message it has no key for, undecrypted" do
|
|
406
|
+
sync = sync_for(
|
|
407
|
+
fake_client(response(events: [encrypted_timeline(session_id: "unknown")])),
|
|
408
|
+
store: fake_store(readable: []),
|
|
409
|
+
)
|
|
410
|
+
seen = []
|
|
411
|
+
sync.read { |message| seen << message }
|
|
412
|
+
|
|
413
|
+
seen.length.should == 1
|
|
414
|
+
seen.first.decrypted?.should == false
|
|
415
|
+
seen.first.encrypted?.should == true
|
|
416
|
+
seen.first.session_id.should == "unknown"
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# A consumer interested only in unencrypted rooms needs no keys at all.
|
|
420
|
+
it "works with no store, leaving encrypted messages alone" do
|
|
421
|
+
sync = sync_for(fake_client(response(events: [encrypted_timeline, plaintext_timeline])))
|
|
422
|
+
seen = []
|
|
423
|
+
sync.read { |message| seen << message }
|
|
424
|
+
|
|
425
|
+
seen.length.should == 2
|
|
426
|
+
seen.first.decrypted?.should == false
|
|
427
|
+
seen.last.type.should == "m.room.message"
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
it "leaves a plaintext message untouched by the store" do
|
|
431
|
+
store = fake_store
|
|
432
|
+
sync = sync_for(fake_client(response(events: [plaintext_timeline])), store: store)
|
|
433
|
+
sync.read { |_message| nil }
|
|
434
|
+
|
|
435
|
+
store.seen.should == []
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Anyone sharing a room can send us one, so a malformed to-device message
|
|
439
|
+
# must not take the loop down with it.
|
|
440
|
+
it "discards an undecryptable to-device message and keeps going" do
|
|
441
|
+
exploding = Object.new
|
|
442
|
+
exploding.define_singleton_method(:decrypt) do |_message|
|
|
443
|
+
raise(Protocol::Matrix::EncryptedMessage::MalformedError, "nonsense")
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
sync = sync_for(
|
|
447
|
+
fake_client(response(to_device: [to_device_event], events: [plaintext_timeline])),
|
|
448
|
+
store: exploding,
|
|
449
|
+
)
|
|
450
|
+
seen = []
|
|
451
|
+
|
|
452
|
+
sync.read { |message| seen << message }.should == 1
|
|
453
|
+
seen.first.type.should == "m.room.message"
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# ── Key counts ────────────────────────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
# A device whose one-time keys run out silently stops being reachable, so
|
|
459
|
+
# the number has to be visible to someone.
|
|
460
|
+
it "remembers what the server says it holds of our one-time keys" do
|
|
461
|
+
sync = sync_for(
|
|
462
|
+
fake_client(response("device_one_time_keys_count" => {"signed_curve25519" => 12})),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
sync.one_time_keys_count.should == {}
|
|
466
|
+
sync.read { |_message| nil }
|
|
467
|
+
sync.one_time_keys_count.should == {"signed_curve25519" => 12}
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
it "keeps the last count when a response omits it" do
|
|
471
|
+
client = fake_client(
|
|
472
|
+
response("device_one_time_keys_count" => {"signed_curve25519" => 12}),
|
|
473
|
+
response,
|
|
474
|
+
)
|
|
475
|
+
sync = sync_for(client)
|
|
476
|
+
sync.read { |_message| nil }
|
|
477
|
+
sync.read { |_message| nil }
|
|
478
|
+
|
|
479
|
+
sync.one_time_keys_count.should == {"signed_curve25519" => 12}
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# ── Looping ───────────────────────────────────────────────────────────────
|
|
483
|
+
|
|
484
|
+
it "polls until told to stop" do
|
|
485
|
+
client = fake_client(response(next_batch: "s2"), response(next_batch: "s3"))
|
|
486
|
+
polls = 0
|
|
487
|
+
sync = sync_for(client)
|
|
488
|
+
|
|
489
|
+
sync.each(stop: -> { polls >= 2 }) { |_message| polls += 1 }
|
|
490
|
+
|
|
491
|
+
polls.should == 2
|
|
492
|
+
client.queried.length.should == 2
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
it "requires a block, because a sync stream is not enumerable" do
|
|
496
|
+
lambda { sync_for(fake_client(response)).each }.should.raise(ArgumentError)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# ── The batch, unprocessed ────────────────────────────────────────────────
|
|
500
|
+
|
|
501
|
+
it "exposes the raw batch for a caller that wants to drive it itself" do
|
|
502
|
+
batch = sync_for(fake_client(response(to_device: [to_device_event]))).poll
|
|
503
|
+
|
|
504
|
+
batch.should.be.kind_of Protocol::Matrix::MessageBatch
|
|
505
|
+
batch.size.should == 2
|
|
506
|
+
batch.next_batch.should == "s2"
|
|
507
|
+
end
|
|
508
|
+
end
|