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,200 @@
|
|
|
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 "json"
|
|
7
|
+
|
|
8
|
+
require_relative "error"
|
|
9
|
+
|
|
10
|
+
module Protocol
|
|
11
|
+
module Matrix
|
|
12
|
+
# Canonical JSON: "the shortest UTF-8 JSON encoding with dictionary keys
|
|
13
|
+
# lexicographically sorted by Unicode codepoint."
|
|
14
|
+
#
|
|
15
|
+
# https://spec.matrix.org/latest/appendices/#canonical-json
|
|
16
|
+
#
|
|
17
|
+
# This is not a formatting preference. A signature is computed over these
|
|
18
|
+
# exact bytes, so any disagreement about whitespace, key order or number
|
|
19
|
+
# formatting produces a signature every other implementation rejects — and
|
|
20
|
+
# the failure appears at the far end, as somebody else's device refusing our
|
|
21
|
+
# keys, with nothing locally to see.
|
|
22
|
+
#
|
|
23
|
+
# The rules, each of which is enforced below:
|
|
24
|
+
#
|
|
25
|
+
# * dictionary keys sorted by Unicode codepoint
|
|
26
|
+
# * no insignificant whitespace (separators are "," and ":")
|
|
27
|
+
# * code-points outside ASCII encoded as UTF-8, NOT as \u escapes
|
|
28
|
+
# * integers only, in [-(2**53)+1, (2**53)-1], no exponents, no decimals
|
|
29
|
+
# * "Float values are not permitted by this encoding"
|
|
30
|
+
# * negative zero MUST NOT appear
|
|
31
|
+
module CanonicalJson
|
|
32
|
+
MAXIMUM_INTEGER = (2**53) - 1
|
|
33
|
+
MINIMUM_INTEGER = -(2**53) + 1
|
|
34
|
+
|
|
35
|
+
# The two keys a signature never covers: "The `unsigned` object and the
|
|
36
|
+
# `signatures` object are not covered by the signature."
|
|
37
|
+
EXCLUDED_FROM_SIGNATURE = %w[signatures unsigned].freeze
|
|
38
|
+
|
|
39
|
+
class Error < Protocol::Matrix::Error; end
|
|
40
|
+
|
|
41
|
+
# @returns [String] the canonical encoding of +value+.
|
|
42
|
+
def self.encode(value)
|
|
43
|
+
JSON.generate(canonicalize(value))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Sort, stringify and check, recursively.
|
|
47
|
+
#
|
|
48
|
+
# Ruby's String#<=> compares bytes, and UTF-8 byte order is codepoint
|
|
49
|
+
# order, so a plain sort IS the codepoint sort the spec asks for. That
|
|
50
|
+
# equivalence is the whole reason this is three lines rather than a
|
|
51
|
+
# custom comparator.
|
|
52
|
+
#
|
|
53
|
+
# Keys are stringified BEFORE sorting, not read back out of the hash
|
|
54
|
+
# afterwards: `h[k] || h[k.to_sym]` would mishandle a stored `false`.
|
|
55
|
+
def self.canonicalize(value)
|
|
56
|
+
case value
|
|
57
|
+
when Hash
|
|
58
|
+
value.to_h { |key, nested| [key.to_s, canonicalize(nested)] }.sort.to_h
|
|
59
|
+
when Array
|
|
60
|
+
value.map { |nested| canonicalize(nested) }
|
|
61
|
+
when Float
|
|
62
|
+
raise Error, "float values are not permitted by canonical JSON: #{value.inspect}"
|
|
63
|
+
when Integer
|
|
64
|
+
check_integer(value)
|
|
65
|
+
when Symbol
|
|
66
|
+
value.to_s
|
|
67
|
+
else
|
|
68
|
+
value
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.check_integer(value)
|
|
73
|
+
if value > MAXIMUM_INTEGER || value < MINIMUM_INTEGER
|
|
74
|
+
raise Error,
|
|
75
|
+
"integer out of canonical JSON range [#{MINIMUM_INTEGER}, #{MAXIMUM_INTEGER}]: #{value}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
value
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The object as it is signed: everything except the two keys a signature
|
|
82
|
+
# does not cover. Returns a copy; the original is untouched, because the
|
|
83
|
+
# caller still needs its signatures.
|
|
84
|
+
def self.signable(object)
|
|
85
|
+
object.reject { |key, _| EXCLUDED_FROM_SIGNATURE.include?(key.to_s) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The exact bytes a signature is computed over.
|
|
89
|
+
def self.signable_bytes(object)
|
|
90
|
+
encode(signable(object))
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
__END__
|
|
97
|
+
describe "Protocol::Matrix::CanonicalJson" do
|
|
98
|
+
def encode(value) = Protocol::Matrix::CanonicalJson.encode(value)
|
|
99
|
+
|
|
100
|
+
# The worked examples from https://spec.matrix.org/latest/appendices/#canonical-json
|
|
101
|
+
it "matches the spec's empty object example" do
|
|
102
|
+
encode({}).should == "{}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "matches the spec's key ordering example" do
|
|
106
|
+
encode({"one" => 1, "two" => "Two"}).should == '{"one":1,"two":"Two"}'
|
|
107
|
+
encode({"b" => "2", "a" => "1"}).should == '{"a":"1","b":"2"}'
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "matches the spec's nested example" do
|
|
111
|
+
encode({"auth" => {"success" => true, "mxid" => "@john.doe:example.com"}})
|
|
112
|
+
.should == '{"auth":{"mxid":"@john.doe:example.com","success":true}}'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# "Encode code-points outside of ASCII as UTF-8 rather than \u escapes."
|
|
116
|
+
it "emits non-ASCII as UTF-8 rather than escapes" do
|
|
117
|
+
encode({"a" => "日本語"}).should == '{"a":"日本語"}'
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "keeps an escape the JSON grammar requires" do
|
|
121
|
+
encode({"a" => "quote\"and\\slash"}).should == '{"a":"quote\"and\\\\slash"}'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Keys sort by Unicode codepoint, which for UTF-8 is byte order.
|
|
125
|
+
it "sorts keys by codepoint, not by locale or length" do
|
|
126
|
+
encode({"Z" => 1, "a" => 2, "A" => 3, "z" => 4})
|
|
127
|
+
.should == '{"A":3,"Z":1,"a":2,"z":4}'
|
|
128
|
+
encode({"é" => 1, "z" => 2}).should == '{"z":2,"é":1}'
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "sorts nested objects too" do
|
|
132
|
+
encode({"b" => {"d" => 1, "c" => 2}, "a" => 3})
|
|
133
|
+
.should == '{"a":3,"b":{"c":2,"d":1}}'
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Arrays are ordered data: their order is the sender's, not ours to sort.
|
|
137
|
+
it "leaves array order alone" do
|
|
138
|
+
encode({"a" => [3, 1, 2]}).should == '{"a":[3,1,2]}'
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "has no insignificant whitespace anywhere" do
|
|
142
|
+
encoded = encode({"a" => {"b" => [1, 2]}, "c" => "d"})
|
|
143
|
+
|
|
144
|
+
encoded.should == '{"a":{"b":[1,2]},"c":"d"}'
|
|
145
|
+
encoded.include?(" ").should == false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "stringifies symbol keys and values" do
|
|
149
|
+
encode({a: :b}).should == '{"a":"b"}'
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "encodes null, true and false" do
|
|
153
|
+
encode({"a" => nil, "b" => true, "c" => false}).should == '{"a":null,"b":true,"c":false}'
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# "Float values are not permitted by this encoding."
|
|
157
|
+
it "refuses floats, including ones that look like integers" do
|
|
158
|
+
lambda { encode({"a" => 1.5}) }.should.raise(Protocol::Matrix::CanonicalJson::Error)
|
|
159
|
+
lambda { encode({"a" => 1.0}) }.should.raise(Protocol::Matrix::CanonicalJson::Error)
|
|
160
|
+
lambda { encode({"a" => [1.0]}) }.should.raise(Protocol::Matrix::CanonicalJson::Error)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# "Numbers in the JSON must be integers in the range [-(2**53)+1, (2**53)-1]"
|
|
164
|
+
it "accepts the integer range boundaries" do
|
|
165
|
+
encode({"a" => (2**53) - 1}).should == '{"a":9007199254740991}'
|
|
166
|
+
encode({"a" => -(2**53) + 1}).should == '{"a":-9007199254740991}'
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "refuses integers outside the range" do
|
|
170
|
+
lambda { encode({"a" => 2**53}) }.should.raise(Protocol::Matrix::CanonicalJson::Error)
|
|
171
|
+
lambda { encode({"a" => -(2**53)}) }.should.raise(Protocol::Matrix::CanonicalJson::Error)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# ── What a signature covers ───────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
# "The `unsigned` object and the `signatures` object are not covered by the
|
|
177
|
+
# signature."
|
|
178
|
+
it "excludes signatures and unsigned from the signable form" do
|
|
179
|
+
object = {
|
|
180
|
+
"a" => 1,
|
|
181
|
+
"signatures" => {"@alice:example.com" => {"ed25519:DEV" => "sig"}},
|
|
182
|
+
"unsigned" => {"age" => 100},
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
Protocol::Matrix::CanonicalJson.signable(object).should == {"a" => 1}
|
|
186
|
+
Protocol::Matrix::CanonicalJson.signable_bytes(object).should == '{"a":1}'
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it "leaves the original object untouched" do
|
|
190
|
+
object = {"a" => 1, "signatures" => {"x" => "y"}}
|
|
191
|
+
Protocol::Matrix::CanonicalJson.signable(object)
|
|
192
|
+
|
|
193
|
+
object.key?("signatures").should == true
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it "excludes them whether the keys are strings or symbols" do
|
|
197
|
+
Protocol::Matrix::CanonicalJson.signable({:a => 1, :signatures => {}, :unsigned => {}})
|
|
198
|
+
.should == {:a => 1}
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the Apache License, Version 2.0.
|
|
4
|
+
# Copyright, 2026, by General Intelligence Systems.
|
|
5
|
+
|
|
6
|
+
module Protocol
|
|
7
|
+
module Matrix
|
|
8
|
+
# Wraps the content object of a Matrix event, providing typed access to
|
|
9
|
+
# schema-defined properties via method_missing.
|
|
10
|
+
#
|
|
11
|
+
# Common fields like `msgtype`, `body`, and `membership` have dedicated
|
|
12
|
+
# accessors for convenience. Any other field defined in the event's schema
|
|
13
|
+
# (or present in the raw hash) is accessible as a method call:
|
|
14
|
+
#
|
|
15
|
+
# content.msgtype # => "m.text"
|
|
16
|
+
# content.body # => "hello"
|
|
17
|
+
# content.membership # => "join"
|
|
18
|
+
# content.avatar_url # => "mxc://example.org/abc"
|
|
19
|
+
# content["custom"] # => direct hash access for non-standard fields
|
|
20
|
+
#
|
|
21
|
+
class Content
|
|
22
|
+
attr_reader :msgtype, :body, :membership
|
|
23
|
+
|
|
24
|
+
def initialize(data)
|
|
25
|
+
@data = data
|
|
26
|
+
@msgtype = data["msgtype"]
|
|
27
|
+
@body = data["body"]
|
|
28
|
+
@membership = data["membership"]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h = @data
|
|
32
|
+
def to_s = @body.to_s
|
|
33
|
+
def to_str = to_s
|
|
34
|
+
|
|
35
|
+
# Direct hash access for any content field.
|
|
36
|
+
def [](key) = @data[key.to_s]
|
|
37
|
+
|
|
38
|
+
# Dynamic access to any content field present in the raw data.
|
|
39
|
+
def method_missing(name, *args)
|
|
40
|
+
key = name.to_s
|
|
41
|
+
|
|
42
|
+
if @data.key?(key)
|
|
43
|
+
@data[key]
|
|
44
|
+
else
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def respond_to_missing?(name, include_private = false)
|
|
50
|
+
@data.key?(name.to_s) || super
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
__END__
|
|
57
|
+
describe "Protocol::Matrix::Content" do
|
|
58
|
+
it "parses msgtype, body, and membership" do
|
|
59
|
+
content = Protocol::Matrix::Content.new({
|
|
60
|
+
"msgtype" => "m.text",
|
|
61
|
+
"body" => "hello",
|
|
62
|
+
"membership" => "join"
|
|
63
|
+
})
|
|
64
|
+
content.msgtype.should == "m.text"
|
|
65
|
+
content.body.should == "hello"
|
|
66
|
+
content.membership.should == "join"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "handles missing fields gracefully" do
|
|
70
|
+
content = Protocol::Matrix::Content.new({})
|
|
71
|
+
content.msgtype.should.be.nil
|
|
72
|
+
content.body.should.be.nil
|
|
73
|
+
content.membership.should.be.nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "provides hash access via []" do
|
|
77
|
+
content = Protocol::Matrix::Content.new({"custom_field" => "value"})
|
|
78
|
+
content["custom_field"].should == "value"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "provides dynamic access via method_missing" do
|
|
82
|
+
content = Protocol::Matrix::Content.new({
|
|
83
|
+
"avatar_url" => "mxc://example.org/abc",
|
|
84
|
+
"displayname" => "Alice"
|
|
85
|
+
})
|
|
86
|
+
content.avatar_url.should == "mxc://example.org/abc"
|
|
87
|
+
content.displayname.should == "Alice"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "returns nil for unknown fields via method_missing" do
|
|
91
|
+
content = Protocol::Matrix::Content.new({})
|
|
92
|
+
content.nonexistent.should.be.nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "returns the raw hash via to_h" do
|
|
96
|
+
data = {"msgtype" => "m.text", "body" => "hi"}
|
|
97
|
+
content = Protocol::Matrix::Content.new(data)
|
|
98
|
+
content.to_h.should == data
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "responds to keys present in the data" do
|
|
102
|
+
content = Protocol::Matrix::Content.new({"avatar_url" => "mxc://x/y"})
|
|
103
|
+
content.respond_to?(:avatar_url).should == true
|
|
104
|
+
content.respond_to?(:nonexistent).should == false
|
|
105
|
+
end
|
|
106
|
+
end
|