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,478 @@
|
|
|
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
|
+
# Room actions: membership, state, events, moderation. Mixed into Client.
|
|
12
|
+
#
|
|
13
|
+
# Every method takes keyword arguments and routes through #api, so the
|
|
14
|
+
# path is checked against the vendored OpenAPI tree before any request is
|
|
15
|
+
# made. Room ids are full of punctuation -- `!id:server`, `@user:server`,
|
|
16
|
+
# `$event` -- and the chain URL-encodes each segment, which is the thing
|
|
17
|
+
# hand-built paths get wrong.
|
|
18
|
+
#
|
|
19
|
+
# NOTHING HERE ENCRYPTS. #send_event sends exactly the content it is
|
|
20
|
+
# given; deciding whether a room is encrypted, and encrypting if so, is
|
|
21
|
+
# the device's job, because only the device holds the keys.
|
|
22
|
+
module Rooms
|
|
23
|
+
# ── Membership ────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
def invite(room_id:, user_id:, reason: nil)
|
|
26
|
+
api.rooms(room_id).invite.post({user_id: user_id, reason: reason}.compact)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def kick(room_id:, user_id:, reason: nil)
|
|
30
|
+
api.rooms(room_id).kick.post({user_id: user_id, reason: reason}.compact)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def ban(room_id:, user_id:, reason: nil)
|
|
34
|
+
api.rooms(room_id).ban.post({user_id: user_id, reason: reason}.compact)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def unban(room_id:, user_id:, reason: nil)
|
|
38
|
+
api.rooms(room_id).unban.post({user_id: user_id, reason: reason}.compact)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Drop a left room from our room list entirely.
|
|
42
|
+
def forget_room(room_id:)
|
|
43
|
+
api.rooms(room_id).forget.post({})
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The room's CURRENT joined members, straight from the homeserver.
|
|
47
|
+
#
|
|
48
|
+
# AUTHORITATIVE IN A WAY A LOCAL PROJECTION IS NOT. A projection is built
|
|
49
|
+
# from the m.room.member events we happened to receive, so a device that
|
|
50
|
+
# joined an existing room knows only whoever its own join batch
|
|
51
|
+
# mentioned. Encrypting to that list means encrypting to a subset of the
|
|
52
|
+
# room, and the rest see an undecryptable message.
|
|
53
|
+
def joined_members(room_id:)
|
|
54
|
+
api.rooms(room_id).joined_members.get
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def joined_rooms = api.joined_rooms.get
|
|
58
|
+
|
|
59
|
+
def create_room(**options)
|
|
60
|
+
api.createRoom.post(options)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# ── Events ────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
# PUT /rooms/{roomId}/send/{eventType}/{txnId}
|
|
66
|
+
#
|
|
67
|
+
# The transaction id is what makes a retry safe: the homeserver
|
|
68
|
+
# deduplicates on it, so a request that timed out after being processed
|
|
69
|
+
# does not post the message twice.
|
|
70
|
+
def send_event(room_id:, event_type:, content:, txn_id: nil)
|
|
71
|
+
api.rooms(room_id).send(event_type, txn_id || SecureRandom.uuid).put(content)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Deleting an event, in Matrix's own vocabulary.
|
|
75
|
+
def redact(room_id:, event_id:, reason: nil, txn_id: nil)
|
|
76
|
+
api.rooms(room_id).redact(event_id, txn_id || SecureRandom.uuid)
|
|
77
|
+
.put({reason: reason}.compact)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Backwards pagination, for "load older messages". `dir: "b"` is
|
|
81
|
+
# backwards from the token given.
|
|
82
|
+
def messages(room_id:, from: nil, to: nil, dir: "b", limit: 50, filter: nil)
|
|
83
|
+
query = {dir: dir, limit: limit}
|
|
84
|
+
|
|
85
|
+
if from
|
|
86
|
+
query[:from] = from
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if to
|
|
90
|
+
query[:to] = to
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if filter
|
|
94
|
+
query[:filter] = filter
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
api.rooms(room_id).messages.get(**query)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def event(room_id:, event_id:)
|
|
101
|
+
api.rooms(room_id).event(event_id).get
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# ── State ─────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
# The empty state key is the common case and still a real path segment,
|
|
107
|
+
# which is why it is sent rather than omitted.
|
|
108
|
+
def send_state(room_id:, event_type:, content:, state_key: "")
|
|
109
|
+
api.rooms(room_id).state(event_type, state_key).put(content)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def get_state(room_id:, event_type:, state_key: "")
|
|
113
|
+
api.rooms(room_id).state(event_type, state_key).get
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def room_state(room_id:)
|
|
117
|
+
api.rooms(room_id).state.get
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def set_room_name(room_id:, name:)
|
|
121
|
+
send_state(room_id: room_id, event_type: "m.room.name", content: {name: name})
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def set_room_topic(room_id:, topic:)
|
|
125
|
+
send_state(room_id: room_id, event_type: "m.room.topic", content: {topic: topic})
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def set_pinned_events(room_id:, event_ids:)
|
|
129
|
+
send_state(
|
|
130
|
+
room_id: room_id,
|
|
131
|
+
event_type: "m.room.pinned_events",
|
|
132
|
+
content: {pinned: event_ids},
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Read-modify-write, because the whole power_levels content is replaced
|
|
137
|
+
# by a state send: writing only the one user would strip every other
|
|
138
|
+
# level in the room.
|
|
139
|
+
def set_power_level(room_id:, user_id:, level:)
|
|
140
|
+
levels = get_state(room_id: room_id, event_type: "m.room.power_levels")
|
|
141
|
+
users = (levels["users"] || {}).merge(user_id => level)
|
|
142
|
+
|
|
143
|
+
send_state(
|
|
144
|
+
room_id: room_id,
|
|
145
|
+
event_type: "m.room.power_levels",
|
|
146
|
+
content: levels.merge("users" => users),
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# ── Ephemeral ─────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
def read_receipt(room_id:, event_id:, receipt_type: "m.read")
|
|
153
|
+
api.rooms(room_id).receipt(receipt_type, event_id).post({})
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def typing(room_id:, user_id:, typing:, timeout: 30_000)
|
|
157
|
+
body = {typing: typing}
|
|
158
|
+
|
|
159
|
+
if typing
|
|
160
|
+
body[:timeout] = timeout
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
api.rooms(room_id).typing(user_id).put(body)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# ── Account data ──────────────────────────────────────────────────────
|
|
167
|
+
#
|
|
168
|
+
# Where 4S secrets and the m.direct map live, among other things.
|
|
169
|
+
|
|
170
|
+
def account_data(user_id:, type:)
|
|
171
|
+
api.user(user_id).account_data(type).get
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def set_account_data(user_id:, type:, content:)
|
|
175
|
+
api.user(user_id).account_data(type).put(content)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def room_account_data(user_id:, room_id:, type:)
|
|
179
|
+
api.user(user_id).rooms(room_id).account_data(type).get
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def set_room_account_data(user_id:, room_id:, type:, content:)
|
|
183
|
+
api.user(user_id).rooms(room_id).account_data(type).put(content)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# ── Profile ───────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
def profile(user_id:) = api.profile(user_id).get
|
|
189
|
+
|
|
190
|
+
def set_avatar_url(user_id:, avatar_url:)
|
|
191
|
+
api.profile(user_id).avatar_url.put({avatar_url: avatar_url})
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
__END__
|
|
199
|
+
describe "Async::Matrix::Client::Rooms" do
|
|
200
|
+
def recording_client(response = {})
|
|
201
|
+
# The REAL path tree. Api memoises it process-wide, and the Api specs
|
|
202
|
+
# inject a small fixture tree without restoring it (Api.reset! exists for
|
|
203
|
+
# that and goes uncalled), so without this these specs pass or fail
|
|
204
|
+
# according to the order scampi happens to load files in.
|
|
205
|
+
Async::Matrix::Api.reset!
|
|
206
|
+
|
|
207
|
+
config = Async::Matrix::Config.new({
|
|
208
|
+
"homeserver" => {"address" => "http://synapse:8008", "domain" => "example.org"},
|
|
209
|
+
"appservice" => {"as_token" => "as", "hs_token" => "hs", "bot" => {"username" => "bot"}},
|
|
210
|
+
})
|
|
211
|
+
client = Async::Matrix::Client.new(config)
|
|
212
|
+
calls = []
|
|
213
|
+
client.define_singleton_method(:calls) { calls }
|
|
214
|
+
client.define_singleton_method(:request) do |method, path, body = nil, **_options|
|
|
215
|
+
calls << [method, path, body]
|
|
216
|
+
response
|
|
217
|
+
end
|
|
218
|
+
client
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def last(client) = client.calls.last
|
|
222
|
+
def room = "!ops:example.org"
|
|
223
|
+
|
|
224
|
+
# ── Encoding ──────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
# Room ids are `!id:server`, users `@user:server`, events `$base64`. Getting
|
|
227
|
+
# this wrong is what hand-built paths do.
|
|
228
|
+
it "url-encodes the punctuation in matrix identifiers" do
|
|
229
|
+
client = recording_client
|
|
230
|
+
client.invite(room_id: room, user_id: "@ada:example.org")
|
|
231
|
+
|
|
232
|
+
last(client)[1].should == "/_matrix/client/v3/rooms/%21ops%3Aexample.org/invite"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "encodes an event id's reserved characters" do
|
|
236
|
+
client = recording_client
|
|
237
|
+
client.event(room_id: room, event_id: "$abc+def/ghi")
|
|
238
|
+
|
|
239
|
+
last(client)[1].should.be.include? "%24abc%2Bdef%2Fghi"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# ── Membership ────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
it "invites, kicks, bans and unbans" do
|
|
245
|
+
client = recording_client
|
|
246
|
+
|
|
247
|
+
client.invite(room_id: room, user_id: "@ada:example.org")
|
|
248
|
+
last(client)[1].should.be.end_with? "/invite"
|
|
249
|
+
last(client)[2].should == {user_id: "@ada:example.org"}
|
|
250
|
+
|
|
251
|
+
client.kick(room_id: room, user_id: "@ada:example.org", reason: "spam")
|
|
252
|
+
last(client)[1].should.be.end_with? "/kick"
|
|
253
|
+
last(client)[2].should == {user_id: "@ada:example.org", reason: "spam"}
|
|
254
|
+
|
|
255
|
+
client.ban(room_id: room, user_id: "@ada:example.org")
|
|
256
|
+
last(client)[1].should.be.end_with? "/ban"
|
|
257
|
+
|
|
258
|
+
client.unban(room_id: room, user_id: "@ada:example.org")
|
|
259
|
+
last(client)[1].should.be.end_with? "/unban"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it "omits a reason it was not given" do
|
|
263
|
+
client = recording_client
|
|
264
|
+
client.kick(room_id: room, user_id: "@ada:example.org")
|
|
265
|
+
|
|
266
|
+
last(client)[2].key?(:reason).should == false
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Both forms, so the action surface is consistent without breaking callers
|
|
270
|
+
# written against the positional signature.
|
|
271
|
+
it "joins and leaves by keyword or positionally" do
|
|
272
|
+
client = recording_client
|
|
273
|
+
|
|
274
|
+
client.join_room(room_id: room)
|
|
275
|
+
last(client)[1].should == "/_matrix/client/v3/join/%21ops%3Aexample.org"
|
|
276
|
+
|
|
277
|
+
client.join_room(room)
|
|
278
|
+
last(client)[1].should == "/_matrix/client/v3/join/%21ops%3Aexample.org"
|
|
279
|
+
|
|
280
|
+
client.leave_room(room_id: room)
|
|
281
|
+
last(client)[1].should.be.end_with? "/leave"
|
|
282
|
+
|
|
283
|
+
client.leave_room(room)
|
|
284
|
+
last(client)[1].should.be.end_with? "/leave"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
it "forgets a room" do
|
|
288
|
+
client = recording_client
|
|
289
|
+
client.forget_room(room_id: room)
|
|
290
|
+
|
|
291
|
+
last(client)[1].should.be.end_with? "/forget"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Authoritative in a way a local projection is not: a device that joined an
|
|
295
|
+
# existing room knows only whoever its own join batch mentioned.
|
|
296
|
+
it "reads the room's joined members from the homeserver" do
|
|
297
|
+
client = recording_client
|
|
298
|
+
client.joined_members(room_id: room)
|
|
299
|
+
|
|
300
|
+
last(client)[0].should == "GET"
|
|
301
|
+
last(client)[1].should.be.end_with? "/joined_members"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
it "creates a room" do
|
|
305
|
+
client = recording_client
|
|
306
|
+
client.create_room(name: "ops", preset: "private_chat", invite: ["@ada:example.org"])
|
|
307
|
+
|
|
308
|
+
last(client)[1].should == "/_matrix/client/v3/createRoom"
|
|
309
|
+
last(client)[2].should == {name: "ops", preset: "private_chat", invite: ["@ada:example.org"]}
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# ── Events ────────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
# The transaction id is what makes a retry safe.
|
|
315
|
+
it "sends an event with a transaction id" do
|
|
316
|
+
client = recording_client
|
|
317
|
+
client.send_event(
|
|
318
|
+
room_id: room, event_type: "m.room.message", content: {msgtype: "m.text", body: "hi"},
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
last(client)[0].should == "PUT"
|
|
322
|
+
last(client)[1].should.be.include? "/send/m.room.message/"
|
|
323
|
+
last(client)[2].should == {msgtype: "m.text", body: "hi"}
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it "accepts a caller's transaction id" do
|
|
327
|
+
client = recording_client
|
|
328
|
+
client.send_event(room_id: room, event_type: "m.room.message", content: {}, txn_id: "t1")
|
|
329
|
+
|
|
330
|
+
last(client)[1].should.be.end_with? "/send/m.room.message/t1"
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# NOTHING HERE ENCRYPTS: the content is sent exactly as given, because only
|
|
334
|
+
# the device holds the keys.
|
|
335
|
+
it "sends encrypted content as-is when handed it" do
|
|
336
|
+
client = recording_client
|
|
337
|
+
client.send_event(
|
|
338
|
+
room_id: room,
|
|
339
|
+
event_type: "m.room.encrypted",
|
|
340
|
+
content: {"algorithm" => "m.megolm.v1.aes-sha2", "ciphertext" => "AwgAEnAC"},
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
last(client)[2]["ciphertext"].should == "AwgAEnAC"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
it "redacts an event" do
|
|
347
|
+
client = recording_client
|
|
348
|
+
client.redact(room_id: room, event_id: "$evt", reason: "mistake", txn_id: "t1")
|
|
349
|
+
|
|
350
|
+
last(client)[1].should.be.end_with? "/redact/%24evt/t1"
|
|
351
|
+
last(client)[2].should == {reason: "mistake"}
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
it "paginates backwards by default" do
|
|
355
|
+
client = recording_client
|
|
356
|
+
client.messages(room_id: room, from: "t42", limit: 20)
|
|
357
|
+
|
|
358
|
+
last(client)[0].should == "GET"
|
|
359
|
+
last(client)[1].should ==
|
|
360
|
+
"/_matrix/client/v3/rooms/%21ops%3Aexample.org/messages?dir=b&limit=20&from=t42"
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
it "omits pagination parameters it was not given" do
|
|
364
|
+
client = recording_client
|
|
365
|
+
client.messages(room_id: room)
|
|
366
|
+
|
|
367
|
+
last(client)[1].should.not.be.include? "from="
|
|
368
|
+
last(client)[1].should.not.be.include? "to="
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# ── State ─────────────────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
# The empty state key is the common case and still a real path segment.
|
|
374
|
+
it "sends and reads state with an empty state key" do
|
|
375
|
+
client = recording_client
|
|
376
|
+
|
|
377
|
+
client.send_state(room_id: room, event_type: "m.room.name", content: {name: "ops"})
|
|
378
|
+
last(client)[1].should.be.end_with? "/state/m.room.name/"
|
|
379
|
+
|
|
380
|
+
client.get_state(room_id: room, event_type: "m.room.name")
|
|
381
|
+
last(client)[0].should == "GET"
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
it "sends state with a state key" do
|
|
385
|
+
client = recording_client
|
|
386
|
+
client.send_state(
|
|
387
|
+
room_id: room, event_type: "m.room.member",
|
|
388
|
+
content: {membership: "join"}, state_key: "@ada:example.org",
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
last(client)[1].should.be.end_with? "/state/m.room.member/%40ada%3Aexample.org"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
it "sets the name, topic and pinned events" do
|
|
395
|
+
client = recording_client
|
|
396
|
+
|
|
397
|
+
client.set_room_name(room_id: room, name: "ops")
|
|
398
|
+
last(client)[2].should == {name: "ops"}
|
|
399
|
+
|
|
400
|
+
client.set_room_topic(room_id: room, topic: "operations")
|
|
401
|
+
last(client)[2].should == {topic: "operations"}
|
|
402
|
+
|
|
403
|
+
client.set_pinned_events(room_id: room, event_ids: ["$a", "$b"])
|
|
404
|
+
last(client)[2].should == {pinned: ["$a", "$b"]}
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Read-modify-write: a state send REPLACES the content, so writing only the
|
|
408
|
+
# one user would strip every other level in the room.
|
|
409
|
+
it "preserves the other power levels when setting one" do
|
|
410
|
+
client = recording_client(
|
|
411
|
+
{"users" => {"@bot:example.org" => 100}, "users_default" => 0, "kick" => 50},
|
|
412
|
+
)
|
|
413
|
+
client.set_power_level(room_id: room, user_id: "@ada:example.org", level: 50)
|
|
414
|
+
|
|
415
|
+
last(client)[2].should == {
|
|
416
|
+
"users" => {"@bot:example.org" => 100, "@ada:example.org" => 50},
|
|
417
|
+
"users_default" => 0,
|
|
418
|
+
"kick" => 50,
|
|
419
|
+
}
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# ── Ephemeral ─────────────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
it "posts a read receipt" do
|
|
425
|
+
client = recording_client
|
|
426
|
+
client.read_receipt(room_id: room, event_id: "$evt")
|
|
427
|
+
|
|
428
|
+
last(client)[0].should == "POST"
|
|
429
|
+
last(client)[1].should.be.end_with? "/receipt/m.read/%24evt"
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
it "sets typing with a timeout, and clears it without one" do
|
|
433
|
+
client = recording_client
|
|
434
|
+
|
|
435
|
+
client.typing(room_id: room, user_id: "@bot:example.org", typing: true)
|
|
436
|
+
last(client)[2].should == {typing: true, timeout: 30_000}
|
|
437
|
+
|
|
438
|
+
client.typing(room_id: room, user_id: "@bot:example.org", typing: false)
|
|
439
|
+
last(client)[2].should == {typing: false}
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# ── Account data ──────────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
# Where 4S secrets and the m.direct map live.
|
|
445
|
+
it "reads and writes account data" do
|
|
446
|
+
client = recording_client
|
|
447
|
+
|
|
448
|
+
client.account_data(user_id: "@bot:example.org", type: "m.secret_storage.default_key")
|
|
449
|
+
last(client)[0].should == "GET"
|
|
450
|
+
last(client)[1].should ==
|
|
451
|
+
"/_matrix/client/v3/user/%40bot%3Aexample.org/account_data/m.secret_storage.default_key"
|
|
452
|
+
|
|
453
|
+
client.set_account_data(
|
|
454
|
+
user_id: "@bot:example.org", type: "m.direct", content: {"@ada:example.org" => [room]},
|
|
455
|
+
)
|
|
456
|
+
last(client)[0].should == "PUT"
|
|
457
|
+
last(client)[2].should == {"@ada:example.org" => [room]}
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
it "reads and writes per-room account data" do
|
|
461
|
+
client = recording_client
|
|
462
|
+
client.room_account_data(user_id: "@bot:example.org", room_id: room, type: "m.tag")
|
|
463
|
+
|
|
464
|
+
last(client)[1].should.be.include? "/rooms/%21ops%3Aexample.org/account_data/m.tag"
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# ── Profile ───────────────────────────────────────────────────────────────
|
|
468
|
+
|
|
469
|
+
it "reads a profile and sets an avatar" do
|
|
470
|
+
client = recording_client
|
|
471
|
+
|
|
472
|
+
client.profile(user_id: "@ada:example.org")
|
|
473
|
+
last(client)[1].should == "/_matrix/client/v3/profile/%40ada%3Aexample.org"
|
|
474
|
+
|
|
475
|
+
client.set_avatar_url(user_id: "@bot:example.org", avatar_url: "mxc://example.org/abc")
|
|
476
|
+
last(client)[2].should == {avatar_url: "mxc://example.org/abc"}
|
|
477
|
+
end
|
|
478
|
+
end
|