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
|
@@ -22,7 +22,13 @@ module Async
|
|
|
22
22
|
class PathTree
|
|
23
23
|
SCHEMA_DIR = Pathname.new(File.expand_path("../../../../data/matrix-spec/api/client-server", __dir__))
|
|
24
24
|
|
|
25
|
-
Node = Struct.new(
|
|
25
|
+
Node = Struct.new(
|
|
26
|
+
:children,
|
|
27
|
+
:wildcard,
|
|
28
|
+
:methods,
|
|
29
|
+
:operation_ids,
|
|
30
|
+
keyword_init: true,
|
|
31
|
+
) do
|
|
26
32
|
def initialize(**)
|
|
27
33
|
super
|
|
28
34
|
self.children ||= {}
|
|
@@ -39,34 +45,46 @@ module Async
|
|
|
39
45
|
|
|
40
46
|
# Load all OpenAPI schemas from data/ and build the tree.
|
|
41
47
|
def self.load(schema_dir: SCHEMA_DIR)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
new.tap do |tree|
|
|
49
|
+
Pathname.glob(schema_dir / "*.yaml").each do |path|
|
|
50
|
+
tree.load_schema(path)
|
|
51
|
+
end
|
|
45
52
|
end
|
|
46
|
-
tree
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
# Parse a single OpenAPI YAML file and insert its paths into the tree.
|
|
50
56
|
def load_schema(path)
|
|
51
57
|
doc = YAML.safe_load(File.read(path), permitted_classes: [Symbol], aliases: true)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
if doc.is_a?(Hash)
|
|
59
|
+
base_path = extract_base_path(doc)
|
|
60
|
+
paths = doc["paths"]
|
|
61
|
+
if paths.is_a?(Hash)
|
|
62
|
+
paths.each do |path_template, methods_hash|
|
|
63
|
+
unless methods_hash.is_a?(Hash)
|
|
64
|
+
next
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Build full path: basePath + path_template
|
|
68
|
+
full_path = "#{base_path}#{path_template.strip}"
|
|
69
|
+
segments = full_path.split("/").reject(&:empty?)
|
|
70
|
+
|
|
71
|
+
methods_hash.each do |method, operation|
|
|
72
|
+
unless %w[get post put delete patch head].include?(method)
|
|
73
|
+
next
|
|
74
|
+
end
|
|
75
|
+
if operation.is_a?(Hash)
|
|
76
|
+
operation_id = operation["operationId"]
|
|
77
|
+
else
|
|
78
|
+
operation_id = nil
|
|
79
|
+
end
|
|
80
|
+
insert(segments, method, operation_id)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
else
|
|
84
|
+
nil
|
|
69
85
|
end
|
|
86
|
+
else
|
|
87
|
+
nil
|
|
70
88
|
end
|
|
71
89
|
end
|
|
72
90
|
|
|
@@ -83,25 +101,30 @@ module Async
|
|
|
83
101
|
node = node.children[segment]
|
|
84
102
|
end
|
|
85
103
|
end
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
unless node.methods.include?(method.downcase)
|
|
105
|
+
node.methods << method.downcase
|
|
106
|
+
end
|
|
107
|
+
if operation_id
|
|
108
|
+
node.operation_ids[method.downcase] = operation_id
|
|
109
|
+
end
|
|
88
110
|
end
|
|
89
111
|
|
|
90
112
|
# Match a concrete path (array of segments) against the trie.
|
|
91
113
|
# Returns a result hash.
|
|
92
114
|
def match(segments, method = nil)
|
|
93
|
-
node = @root
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return {valid: false, methods: [], operation_id: nil}
|
|
115
|
+
node = segments.inject(@root) do |current, segment|
|
|
116
|
+
if current.nil?
|
|
117
|
+
nil
|
|
118
|
+
elsif current.children.key?(segment)
|
|
119
|
+
current.children[segment]
|
|
120
|
+
elsif current.wildcard
|
|
121
|
+
current.wildcard
|
|
101
122
|
end
|
|
102
123
|
end
|
|
103
124
|
|
|
104
|
-
if
|
|
125
|
+
if node.nil?
|
|
126
|
+
{valid: false, methods: [], operation_id: nil}
|
|
127
|
+
elsif method
|
|
105
128
|
method_down = method.downcase
|
|
106
129
|
valid = node.methods.include?(method_down)
|
|
107
130
|
{valid: valid, methods: node.methods, operation_id: node.operation_ids[method_down]}
|
|
@@ -112,17 +135,21 @@ module Async
|
|
|
112
135
|
|
|
113
136
|
private
|
|
114
137
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
def extract_base_path(doc)
|
|
139
|
+
servers = doc["servers"]
|
|
140
|
+
if servers.is_a?(Array) && servers.first.is_a?(Hash)
|
|
141
|
+
server = servers.first
|
|
142
|
+
variables = server["variables"] || {}
|
|
143
|
+
base_path_var = variables["basePath"]
|
|
144
|
+
if base_path_var.is_a?(Hash)
|
|
145
|
+
base_path_var["default"] || ""
|
|
146
|
+
else
|
|
147
|
+
""
|
|
148
|
+
end
|
|
149
|
+
else
|
|
150
|
+
""
|
|
151
|
+
end
|
|
152
|
+
end
|
|
126
153
|
end
|
|
127
154
|
end
|
|
128
155
|
end
|
data/lib/async/matrix/api.rb
CHANGED
|
@@ -0,0 +1,213 @@
|
|
|
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_relative "client"
|
|
7
|
+
|
|
8
|
+
module Async
|
|
9
|
+
module Matrix
|
|
10
|
+
# A Client that acts AS one of the appservice's users, on one of that user's
|
|
11
|
+
# devices.
|
|
12
|
+
#
|
|
13
|
+
# client = AppServiceClient.new(config, user_id: "@ada:example.org", device_id: "ABCDEFGHIJ")
|
|
14
|
+
# client.upload_keys(device_keys: ...) # uploaded for Ada's device, not the bot's
|
|
15
|
+
#
|
|
16
|
+
# This is double puppeting, and it is the officially intended mechanism
|
|
17
|
+
# rather than a trick. Identity assertion -- `?user_id=` -- has been in the
|
|
18
|
+
# application service spec for years; [MSC4326] added `?device_id=` beside
|
|
19
|
+
# it and is MERGED, so the plain parameter names below are stable spec, not
|
|
20
|
+
# the unstable `org.matrix.msc3202.device_id` an older implementation would
|
|
21
|
+
# have sent.
|
|
22
|
+
#
|
|
23
|
+
# WHY NOT A TOKEN. Bridges used to get a per-user access token by calling
|
|
24
|
+
# /login with `m.login.application_service` ([MSC2778]), and that route is
|
|
25
|
+
# gone on a homeserver fronted by OAuth2 -- it answers
|
|
26
|
+
# M_APPSERVICE_LOGIN_UNSUPPORTED. [MSC4190], which is also merged, replaced
|
|
27
|
+
# it: an appservice creates devices directly (`PUT /devices/{deviceId}`) and
|
|
28
|
+
# then acts as them with these two parameters. No token is ever issued, so
|
|
29
|
+
# none can expire or need refreshing.
|
|
30
|
+
#
|
|
31
|
+
# IT STILL WORKS BEHIND MAS. Synapse checks an as_token against its own
|
|
32
|
+
# appservice registry before it introspects anything at the authentication
|
|
33
|
+
# service, so masquerading is unaffected by next-generation auth.
|
|
34
|
+
#
|
|
35
|
+
# THE USER MUST BE IN THE REGISTRATION'S NAMESPACE. The homeserver refuses
|
|
36
|
+
# otherwise -- which is why a double-puppet registration claims a wide user
|
|
37
|
+
# namespace non-exclusively rather than naming individuals.
|
|
38
|
+
#
|
|
39
|
+
# [MSC4326]: https://github.com/matrix-org/matrix-spec-proposals/pull/4326
|
|
40
|
+
# [MSC4190]: https://github.com/matrix-org/matrix-spec-proposals/pull/4190
|
|
41
|
+
# [MSC2778]: https://github.com/matrix-org/matrix-spec-proposals/pull/2778
|
|
42
|
+
class AppServiceClient < Client
|
|
43
|
+
# @parameter user_id [String] the user to act as. Must match the
|
|
44
|
+
# registration's user namespace.
|
|
45
|
+
# @parameter device_id [String] the device to act as.
|
|
46
|
+
#
|
|
47
|
+
# WITHOUT IT THE REQUEST HAS NO DEVICE, and an appservice request with
|
|
48
|
+
# no device cannot upload one-time keys, claim keys or send to-device
|
|
49
|
+
# messages -- every call encryption is made of. It is optional only
|
|
50
|
+
# because the device has to be CREATED before it can be acted as, and
|
|
51
|
+
# that one call is made without it.
|
|
52
|
+
def initialize(config, user_id:, device_id: nil, **options)
|
|
53
|
+
super(config, **options)
|
|
54
|
+
|
|
55
|
+
@user_id = user_id
|
|
56
|
+
@device_id = device_id
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
attr_reader :user_id, :device_id
|
|
60
|
+
|
|
61
|
+
# NAMED ON EVERY REQUEST, not only the ones that obviously need it. An
|
|
62
|
+
# appservice request that omits them is not an error: it silently acts as
|
|
63
|
+
# the registration's sender_localpart, with no device, which is far worse
|
|
64
|
+
# than a failure because it succeeds.
|
|
65
|
+
def default_query
|
|
66
|
+
{user_id: @user_id}.tap do |query|
|
|
67
|
+
if @device_id
|
|
68
|
+
query[:device_id] = @device_id
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A sibling client for another user or device, sharing this one's config
|
|
74
|
+
# and retry policy.
|
|
75
|
+
#
|
|
76
|
+
# One appservice commonly acts for many users, and the alternative --
|
|
77
|
+
# mutating user_id on a single client -- races itself the moment two
|
|
78
|
+
# fibers use it, which is exactly what an async bridge does.
|
|
79
|
+
def as(user_id:, device_id: nil)
|
|
80
|
+
self.class.new(@config, user_id: user_id, device_id: device_id)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The same user, now on a device: what you call once the device exists.
|
|
84
|
+
def with_device(device_id)
|
|
85
|
+
as(user_id: @user_id, device_id: device_id)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
__END__
|
|
92
|
+
describe "Async::Matrix::AppServiceClient" do
|
|
93
|
+
def config
|
|
94
|
+
Async::Matrix::Config.new({
|
|
95
|
+
"homeserver" => {"address" => "http://synapse:8008", "domain" => "example.org"},
|
|
96
|
+
"appservice" => {"as_token" => "as", "hs_token" => "hs", "bot" => {"username" => "bot"}},
|
|
97
|
+
})
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def recording_client(user_id: "@ada:example.org", device_id: "ABCDEFGHIJ")
|
|
101
|
+
Async::Matrix::Api.reset!
|
|
102
|
+
|
|
103
|
+
client = Async::Matrix::AppServiceClient.new(config, user_id: user_id, device_id: device_id)
|
|
104
|
+
calls = []
|
|
105
|
+
client.define_singleton_method(:calls) { calls }
|
|
106
|
+
client.define_singleton_method(:internet) do
|
|
107
|
+
internet = Object.new
|
|
108
|
+
internet.define_singleton_method(:call) do |method, url, _headers, _body|
|
|
109
|
+
calls << [method, url]
|
|
110
|
+
|
|
111
|
+
# Shaped like what Client#read_limited consumes: a body that yields
|
|
112
|
+
# chunks and reports its own length.
|
|
113
|
+
body = Object.new
|
|
114
|
+
body.define_singleton_method(:length) { 2 }
|
|
115
|
+
body.define_singleton_method(:each) { |&block| block.call("{}") }
|
|
116
|
+
body.define_singleton_method(:close) { nil }
|
|
117
|
+
|
|
118
|
+
response = Object.new
|
|
119
|
+
response.define_singleton_method(:status) { 200 }
|
|
120
|
+
response.define_singleton_method(:body) { body }
|
|
121
|
+
response.define_singleton_method(:close) { nil }
|
|
122
|
+
response
|
|
123
|
+
end
|
|
124
|
+
internet
|
|
125
|
+
end
|
|
126
|
+
client
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def last_url(client) = client.calls.last[1]
|
|
130
|
+
|
|
131
|
+
it "asserts the user and device on every request" do
|
|
132
|
+
client = recording_client
|
|
133
|
+
client.whoami
|
|
134
|
+
|
|
135
|
+
last_url(client).should ==
|
|
136
|
+
"http://synapse:8008/_matrix/client/v3/account/whoami" \
|
|
137
|
+
"?user_id=%40ada%3Aexample.org&device_id=ABCDEFGHIJ"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Not only on the calls that obviously need it: an appservice request that
|
|
141
|
+
# omits them silently acts as the sender_localpart, with no device.
|
|
142
|
+
it "asserts them through the api chain too" do
|
|
143
|
+
client = recording_client
|
|
144
|
+
client.joined_members(room_id: "!ops:example.org")
|
|
145
|
+
|
|
146
|
+
last_url(client).should.be.include? "user_id=%40ada%3Aexample.org"
|
|
147
|
+
last_url(client).should.be.include? "device_id=ABCDEFGHIJ"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# The chain appends its own query first, so this is the case that catches a
|
|
151
|
+
# naive "?" concatenation.
|
|
152
|
+
it "appends to a path that already carries a query" do
|
|
153
|
+
client = recording_client
|
|
154
|
+
client.messages(room_id: "!ops:example.org", limit: 10)
|
|
155
|
+
|
|
156
|
+
last_url(client).should.be.include? "?dir=b&limit=10&"
|
|
157
|
+
last_url(client).should.be.include? "user_id=%40ada%3Aexample.org"
|
|
158
|
+
last_url(client).count("?").should == 1
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The device has to be created before it can be acted as, and that one call
|
|
162
|
+
# is made without a device.
|
|
163
|
+
it "omits the device when it has none" do
|
|
164
|
+
client = recording_client(device_id: nil)
|
|
165
|
+
client.whoami
|
|
166
|
+
|
|
167
|
+
last_url(client).should.be.include? "user_id=%40ada%3Aexample.org"
|
|
168
|
+
last_url(client).should.not.be.include? "device_id"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Overwriting it would mean acting as somebody other than the caller asked
|
|
172
|
+
# for, which is worse than a failure.
|
|
173
|
+
it "lets a caller's own parameter win" do
|
|
174
|
+
client = recording_client
|
|
175
|
+
client.get("/_matrix/client/v3/account/whoami?user_id=%40bob%3Aexample.org")
|
|
176
|
+
|
|
177
|
+
last_url(client).should.be.include? "user_id=%40bob%3Aexample.org"
|
|
178
|
+
last_url(client).scan("user_id=").length.should == 1
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it "encodes the punctuation in a user id" do
|
|
182
|
+
client = recording_client(user_id: "@ada:example.org")
|
|
183
|
+
client.whoami
|
|
184
|
+
|
|
185
|
+
last_url(client).should.be.include? "%40ada%3Aexample.org"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# One appservice acts for many users; mutating a shared client would race
|
|
189
|
+
# itself the moment two fibers used it.
|
|
190
|
+
it "makes a sibling client for another user" do
|
|
191
|
+
client = recording_client
|
|
192
|
+
other = client.as(user_id: "@bob:example.org", device_id: "OTHERDEV")
|
|
193
|
+
|
|
194
|
+
other.user_id.should == "@bob:example.org"
|
|
195
|
+
other.device_id.should == "OTHERDEV"
|
|
196
|
+
client.user_id.should == "@ada:example.org"
|
|
197
|
+
other.config.should.be == client.config
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "makes a client for the same user on a device" do
|
|
201
|
+
client = recording_client(device_id: nil)
|
|
202
|
+
with_device = client.with_device("NEWDEVICE")
|
|
203
|
+
|
|
204
|
+
with_device.user_id.should == "@ada:example.org"
|
|
205
|
+
with_device.device_id.should == "NEWDEVICE"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# An ordinary client asserts nothing: a user's own token already says who
|
|
209
|
+
# the request is for.
|
|
210
|
+
it "is the only client that asserts anything" do
|
|
211
|
+
Async::Matrix::Client.new(config).default_query.should == {}
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the Apache License, Version 2.0.
|
|
4
|
+
# Copyright, 2026, by General Intelligence Systems.
|
|
5
|
+
|
|
6
|
+
# Async::Matrix::Error is this class's superclass, so it has to exist before
|
|
7
|
+
# this file finishes loading — the glob in async/matrix.rb sorts
|
|
8
|
+
# alphabetically and would otherwise get here first.
|
|
9
|
+
require_relative "error"
|
|
10
|
+
|
|
11
|
+
module Async
|
|
12
|
+
module Matrix
|
|
13
|
+
class AuthError < Error; end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
__END__
|
|
18
|
+
it "AuthError inherits from Error" do
|
|
19
|
+
Async::Matrix::AuthError.new("M_FORBIDDEN", "denied").should.be.kind_of Async::Matrix::Error
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the Apache License, Version 2.0.
|
|
4
|
+
# Copyright, 2026, by General Intelligence Systems.
|
|
5
|
+
|
|
6
|
+
# Async::Matrix::Error is this class's superclass, so it has to exist before
|
|
7
|
+
# this file finishes loading — the glob in async/matrix.rb sorts
|
|
8
|
+
# alphabetically and would otherwise get here first.
|
|
9
|
+
require_relative "error"
|
|
10
|
+
|
|
11
|
+
module Async
|
|
12
|
+
module Matrix
|
|
13
|
+
class BadJsonError < Error; end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
__END__
|
|
18
|
+
it "BadJsonError inherits from Error" do
|
|
19
|
+
Async::Matrix::BadJsonError.new("M_BAD_JSON", "invalid").should.be.kind_of Async::Matrix::Error
|
|
20
|
+
end
|