async-matrix 2.1.0-aarch64-linux → 3.0.1-aarch64-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
|
@@ -1,225 +0,0 @@
|
|
|
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 "forwardable"
|
|
7
|
-
require "json_schemer"
|
|
8
|
-
require "pathname"
|
|
9
|
-
require "yaml"
|
|
10
|
-
require_relative "config/vivify"
|
|
11
|
-
|
|
12
|
-
module Async
|
|
13
|
-
module Matrix
|
|
14
|
-
module ApplicationService
|
|
15
|
-
# Loads, validates, and provides dot-notation access to a mautrix
|
|
16
|
-
# bridgev2-compatible YAML configuration file.
|
|
17
|
-
#
|
|
18
|
-
# The full schema mirrors the Go structs defined in
|
|
19
|
-
# mautrix/go v0.27.0 bridgev2/bridgeconfig — split across multiple
|
|
20
|
-
# JSON Schema files under config/schema/ and composed at runtime via $ref.
|
|
21
|
-
#
|
|
22
|
-
# Validated config data is exposed through Vivify-enhanced hashes,
|
|
23
|
-
# giving natural dot-notation access to every nested field:
|
|
24
|
-
#
|
|
25
|
-
# config = Config.load("bridge.yml")
|
|
26
|
-
# config.homeserver.address # => "http://synapse:8008"
|
|
27
|
-
# config.homeserver.domain # => "localhost"
|
|
28
|
-
# config.appservice.as_token # => "secret..."
|
|
29
|
-
# config.appservice.bot.username # => "bot"
|
|
30
|
-
# config.bot_mxid # => "@bot:localhost"
|
|
31
|
-
#
|
|
32
|
-
class Config
|
|
33
|
-
extend Forwardable
|
|
34
|
-
|
|
35
|
-
SCHEMA_DIR = Pathname.new(__dir__).join("config", "schema").freeze
|
|
36
|
-
|
|
37
|
-
# Delegate every top-level config section to the vivified data hash.
|
|
38
|
-
def_delegators :@data,
|
|
39
|
-
:network, :bridge, :database, :homeserver, :appservice,
|
|
40
|
-
:matrix, :analytics, :provisioning, :public_media, :direct_media,
|
|
41
|
-
:backfill, :double_puppet, :encryption, :logging,
|
|
42
|
-
:management_room_texts, :env_config_prefix
|
|
43
|
-
|
|
44
|
-
def initialize(data)
|
|
45
|
-
self.class.validate!(data)
|
|
46
|
-
@data = Vivify.deep_vivify(data)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Load and validate a YAML config file from disk.
|
|
50
|
-
def self.load(path)
|
|
51
|
-
raise Async::Matrix::NotFoundError.new(
|
|
52
|
-
"M_NOT_FOUND", "Config not found: #{path}"
|
|
53
|
-
) unless File.exist?(path)
|
|
54
|
-
|
|
55
|
-
data = YAML.safe_load_file(path, permitted_classes: [Symbol])
|
|
56
|
-
new(data)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Convenience: derive the bot's full Matrix ID from
|
|
60
|
-
# appservice.bot.username + homeserver.domain.
|
|
61
|
-
def bot_mxid
|
|
62
|
-
"@#{appservice.bot.username}:#{homeserver.domain}"
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# ------------------------------------------------------------------
|
|
66
|
-
# Schema loading & validation
|
|
67
|
-
# ------------------------------------------------------------------
|
|
68
|
-
|
|
69
|
-
def self.schema
|
|
70
|
-
@schema ||= JSONSchemer.schema(
|
|
71
|
-
SCHEMA_DIR.join("config.json"),
|
|
72
|
-
insert_property_defaults: true
|
|
73
|
-
)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def self.validate!(data)
|
|
77
|
-
errors = schema.validate(data).to_a
|
|
78
|
-
return if errors.empty?
|
|
79
|
-
|
|
80
|
-
messages = errors.map { |e| e["error"] }.join("; ")
|
|
81
|
-
raise Async::Matrix::BadJsonError.new(
|
|
82
|
-
"M_BAD_JSON", "Config validation failed: #{messages}"
|
|
83
|
-
)
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
__END__
|
|
91
|
-
require "tempfile"
|
|
92
|
-
|
|
93
|
-
describe "Async::Matrix::ApplicationService::Config" do
|
|
94
|
-
def minimal_data
|
|
95
|
-
{
|
|
96
|
-
"homeserver" => {
|
|
97
|
-
"address" => "http://localhost:8008",
|
|
98
|
-
"domain" => "localhost"
|
|
99
|
-
},
|
|
100
|
-
"appservice" => {
|
|
101
|
-
"as_token" => "as_secret_token_value",
|
|
102
|
-
"hs_token" => "hs_secret_token_value",
|
|
103
|
-
"bot" => { "username" => "bot" }
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
it "parses a minimal valid config" do
|
|
109
|
-
config = Async::Matrix::ApplicationService::Config.new(minimal_data)
|
|
110
|
-
config.homeserver.address.should == "http://localhost:8008"
|
|
111
|
-
config.homeserver.domain.should == "localhost"
|
|
112
|
-
config.appservice.as_token.should == "as_secret_token_value"
|
|
113
|
-
config.appservice.hs_token.should == "hs_secret_token_value"
|
|
114
|
-
config.appservice.bot.username.should == "bot"
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
it "derives bot_mxid from appservice.bot.username and homeserver.domain" do
|
|
118
|
-
config = Async::Matrix::ApplicationService::Config.new(minimal_data)
|
|
119
|
-
config.bot_mxid.should == "@bot:localhost"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
it "inserts property defaults from the schema" do
|
|
123
|
-
config = Async::Matrix::ApplicationService::Config.new(minimal_data)
|
|
124
|
-
config.appservice.hostname.should == "0.0.0.0"
|
|
125
|
-
config.appservice.port.should == 29318
|
|
126
|
-
config.homeserver.software.should == "standard"
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
it "provides dot-notation access to deeply nested fields" do
|
|
130
|
-
data = minimal_data.merge(
|
|
131
|
-
"encryption" => {
|
|
132
|
-
"allow" => true,
|
|
133
|
-
"rotation" => { "messages" => 200 }
|
|
134
|
-
}
|
|
135
|
-
)
|
|
136
|
-
config = Async::Matrix::ApplicationService::Config.new(data)
|
|
137
|
-
config.encryption.allow.should == true
|
|
138
|
-
config.encryption.rotation.messages.should == 200
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
it "autovivifies missing optional sections as empty hashes" do
|
|
142
|
-
config = Async::Matrix::ApplicationService::Config.new(minimal_data)
|
|
143
|
-
config.analytics.should.be.kind_of Hash
|
|
144
|
-
config.analytics.should.be.empty?
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
it "raises BadJsonError when homeserver.address is missing" do
|
|
148
|
-
data = {
|
|
149
|
-
"homeserver" => { "domain" => "localhost" },
|
|
150
|
-
"appservice" => { "as_token" => "a", "hs_token" => "b" }
|
|
151
|
-
}
|
|
152
|
-
lambda {
|
|
153
|
-
Async::Matrix::ApplicationService::Config.new(data)
|
|
154
|
-
}.should.raise(Async::Matrix::BadJsonError)
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
it "raises BadJsonError when appservice.as_token is empty" do
|
|
158
|
-
data = {
|
|
159
|
-
"homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
|
|
160
|
-
"appservice" => { "as_token" => "", "hs_token" => "b" }
|
|
161
|
-
}
|
|
162
|
-
lambda {
|
|
163
|
-
Async::Matrix::ApplicationService::Config.new(data)
|
|
164
|
-
}.should.raise(Async::Matrix::BadJsonError)
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
it "raises BadJsonError for unknown top-level keys" do
|
|
168
|
-
data = minimal_data.merge("bogus" => "nope")
|
|
169
|
-
lambda {
|
|
170
|
-
Async::Matrix::ApplicationService::Config.new(data)
|
|
171
|
-
}.should.raise(Async::Matrix::BadJsonError)
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
it "validates enum constraints (homeserver.software)" do
|
|
175
|
-
data = minimal_data
|
|
176
|
-
data["homeserver"]["software"] = "invalid_value"
|
|
177
|
-
lambda {
|
|
178
|
-
Async::Matrix::ApplicationService::Config.new(data)
|
|
179
|
-
}.should.raise(Async::Matrix::BadJsonError)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
it "loads from a YAML file" do
|
|
183
|
-
file = Tempfile.new(["config", ".yml"])
|
|
184
|
-
file.write(<<~YAML)
|
|
185
|
-
homeserver:
|
|
186
|
-
address: "http://localhost:8008"
|
|
187
|
-
domain: "localhost"
|
|
188
|
-
appservice:
|
|
189
|
-
as_token: "as123_secret"
|
|
190
|
-
hs_token: "hs456_secret"
|
|
191
|
-
bot:
|
|
192
|
-
username: "testbot"
|
|
193
|
-
YAML
|
|
194
|
-
file.close
|
|
195
|
-
|
|
196
|
-
config = Async::Matrix::ApplicationService::Config.load(file.path)
|
|
197
|
-
config.homeserver.address.should == "http://localhost:8008"
|
|
198
|
-
config.bot_mxid.should == "@testbot:localhost"
|
|
199
|
-
ensure
|
|
200
|
-
file.unlink
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
it "raises NotFoundError for missing file" do
|
|
204
|
-
lambda {
|
|
205
|
-
Async::Matrix::ApplicationService::Config.load("/nonexistent/path.yml")
|
|
206
|
-
}.should.raise(Async::Matrix::NotFoundError)
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
it "accepts the full bridge config with permissions as string presets" do
|
|
210
|
-
data = minimal_data.merge(
|
|
211
|
-
"bridge" => {
|
|
212
|
-
"command_prefix" => "!signal",
|
|
213
|
-
"permissions" => {
|
|
214
|
-
"*" => "relay",
|
|
215
|
-
"localhost" => "user",
|
|
216
|
-
"@admin:localhost" => "admin"
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
)
|
|
220
|
-
config = Async::Matrix::ApplicationService::Config.new(data)
|
|
221
|
-
config.bridge.command_prefix.should == "!signal"
|
|
222
|
-
config.bridge.permissions[:"*"].should == "relay"
|
|
223
|
-
config.bridge.permissions[:"@admin:localhost"].should == "admin"
|
|
224
|
-
end
|
|
225
|
-
end
|
|
@@ -1,185 +0,0 @@
|
|
|
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
|
-
module Async
|
|
9
|
-
module Matrix
|
|
10
|
-
module ApplicationService
|
|
11
|
-
# Routes incoming Matrix events to registered handler objects.
|
|
12
|
-
#
|
|
13
|
-
# Each handler declares the event types it handles via `#event_types`.
|
|
14
|
-
# When an event arrives, the dispatcher finds all matching handlers
|
|
15
|
-
# and calls them. Errors in one handler do not prevent others from running.
|
|
16
|
-
class Dispatcher
|
|
17
|
-
def initialize(txn_store: TransactionStore.new)
|
|
18
|
-
@handlers = Hash.new { |h, k| h[k] = [] }
|
|
19
|
-
@txn_store = txn_store
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Register a Bot (responds to #handlers) or a plain handler (responds
|
|
23
|
-
# to #event_types and #call). Bots are expanded into their handlers.
|
|
24
|
-
def register(handler)
|
|
25
|
-
if handler.respond_to?(:handlers)
|
|
26
|
-
handler.handlers.each { |h| register(h) }
|
|
27
|
-
else
|
|
28
|
-
handler.event_types.each do |type|
|
|
29
|
-
@handlers[type] << handler
|
|
30
|
-
Console.info(self) { "Registered #{handler.class.name} for #{type}" }
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Idempotently process a homeserver transaction. Duplicate transaction
|
|
36
|
-
# IDs (already seen) are skipped. Returns :processed or :duplicate.
|
|
37
|
-
#
|
|
38
|
-
# The idempotency store lives here rather than in the HTTP layer because
|
|
39
|
-
# the Grape server is stateless across requests — the dispatcher is the
|
|
40
|
-
# stable, long-lived object.
|
|
41
|
-
def receive_transaction(txn_id, body)
|
|
42
|
-
if @txn_store.seen?(txn_id)
|
|
43
|
-
Console.debug(self) { "Duplicate transaction #{txn_id} — skipping" }
|
|
44
|
-
:duplicate
|
|
45
|
-
else
|
|
46
|
-
dispatch_transaction(body)
|
|
47
|
-
@txn_store.mark_seen(txn_id)
|
|
48
|
-
:processed
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def dispatch_transaction(body)
|
|
53
|
-
Transaction.new(body).then do |txn|
|
|
54
|
-
txn.events.each { |event| dispatch(event) }
|
|
55
|
-
txn.ephemeral.each { |event| dispatch(event) }
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def dispatch(event)
|
|
60
|
-
type = event.type
|
|
61
|
-
handlers = @handlers[type]
|
|
62
|
-
|
|
63
|
-
if handlers.empty?
|
|
64
|
-
Console.debug(self) { "No handler for event type: #{type}" }
|
|
65
|
-
else
|
|
66
|
-
handlers.each do |handler|
|
|
67
|
-
begin
|
|
68
|
-
handler.call(event)
|
|
69
|
-
rescue => e
|
|
70
|
-
Console.error(self) { "Handler #{handler.class.name} raised #{e.class}: #{e.message}" }
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def handler_count
|
|
77
|
-
@handlers.values.flatten.size
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
__END__
|
|
85
|
-
describe "Async::Matrix::ApplicationService::Dispatcher" do
|
|
86
|
-
it "registers and dispatches to handlers" do
|
|
87
|
-
received = []
|
|
88
|
-
handler = Object.new
|
|
89
|
-
handler.define_singleton_method(:event_types) { ["m.room.message"] }
|
|
90
|
-
handler.define_singleton_method(:call) { |event| received << event }
|
|
91
|
-
|
|
92
|
-
dispatcher = Async::Matrix::ApplicationService::Dispatcher.new
|
|
93
|
-
dispatcher.register(handler)
|
|
94
|
-
dispatcher.handler_count.should == 1
|
|
95
|
-
|
|
96
|
-
event = Async::Matrix::ApplicationService::Event.new({
|
|
97
|
-
"type" => "m.room.message",
|
|
98
|
-
"content" => {"body" => "hi"}
|
|
99
|
-
})
|
|
100
|
-
dispatcher.dispatch(event)
|
|
101
|
-
received.length.should == 1
|
|
102
|
-
received.first.content.body.should == "hi"
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
it "ignores events with no matching handler" do
|
|
106
|
-
dispatcher = Async::Matrix::ApplicationService::Dispatcher.new
|
|
107
|
-
event = Async::Matrix::ApplicationService::Event.new({"type" => "m.unknown"})
|
|
108
|
-
lambda { dispatcher.dispatch(event) }.should.not.raise
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
it "continues dispatching when a handler raises" do
|
|
112
|
-
results = []
|
|
113
|
-
bad_handler = Object.new
|
|
114
|
-
bad_handler.define_singleton_method(:event_types) { ["m.room.message"] }
|
|
115
|
-
bad_handler.define_singleton_method(:call) { |_| raise "boom" }
|
|
116
|
-
|
|
117
|
-
good_handler = Object.new
|
|
118
|
-
good_handler.define_singleton_method(:event_types) { ["m.room.message"] }
|
|
119
|
-
good_handler.define_singleton_method(:call) { |e| results << e }
|
|
120
|
-
|
|
121
|
-
dispatcher = Async::Matrix::ApplicationService::Dispatcher.new
|
|
122
|
-
dispatcher.register(bad_handler)
|
|
123
|
-
dispatcher.register(good_handler)
|
|
124
|
-
|
|
125
|
-
event = Async::Matrix::ApplicationService::Event.new({
|
|
126
|
-
"type" => "m.room.message",
|
|
127
|
-
"content" => {"body" => "test"}
|
|
128
|
-
})
|
|
129
|
-
dispatcher.dispatch(event)
|
|
130
|
-
results.length.should == 1
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
it "dispatches a full transaction" do
|
|
134
|
-
received = []
|
|
135
|
-
handler = Object.new
|
|
136
|
-
handler.define_singleton_method(:event_types) { ["m.room.message"] }
|
|
137
|
-
handler.define_singleton_method(:call) { |e| received << e }
|
|
138
|
-
|
|
139
|
-
dispatcher = Async::Matrix::ApplicationService::Dispatcher.new
|
|
140
|
-
dispatcher.register(handler)
|
|
141
|
-
|
|
142
|
-
dispatcher.dispatch_transaction({
|
|
143
|
-
"events" => [
|
|
144
|
-
{"type" => "m.room.message", "content" => {"body" => "one"}},
|
|
145
|
-
{"type" => "m.room.message", "content" => {"body" => "two"}}
|
|
146
|
-
]
|
|
147
|
-
})
|
|
148
|
-
received.length.should == 2
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
it "registers a bot (object responding to #handlers)" do
|
|
152
|
-
received = []
|
|
153
|
-
handler = Object.new
|
|
154
|
-
handler.define_singleton_method(:event_types) { ["m.room.message"] }
|
|
155
|
-
handler.define_singleton_method(:call) { |e| received << e }
|
|
156
|
-
|
|
157
|
-
bot = Object.new
|
|
158
|
-
bot.define_singleton_method(:handlers) { [handler] }
|
|
159
|
-
|
|
160
|
-
dispatcher = Async::Matrix::ApplicationService::Dispatcher.new
|
|
161
|
-
dispatcher.register(bot)
|
|
162
|
-
dispatcher.handler_count.should == 1
|
|
163
|
-
|
|
164
|
-
dispatcher.dispatch_transaction({
|
|
165
|
-
"events" => [{"type" => "m.room.message", "content" => {"body" => "hi"}}]
|
|
166
|
-
})
|
|
167
|
-
received.length.should == 1
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
it "processes a transaction idempotently via receive_transaction" do
|
|
171
|
-
received = []
|
|
172
|
-
handler = Object.new
|
|
173
|
-
handler.define_singleton_method(:event_types) { ["m.room.message"] }
|
|
174
|
-
handler.define_singleton_method(:call) { |e| received << e }
|
|
175
|
-
|
|
176
|
-
dispatcher = Async::Matrix::ApplicationService::Dispatcher.new
|
|
177
|
-
dispatcher.register(handler)
|
|
178
|
-
|
|
179
|
-
body = {"events" => [{"type" => "m.room.message", "content" => {"body" => "hi"}}]}
|
|
180
|
-
|
|
181
|
-
dispatcher.receive_transaction("txn1", body).should == :processed
|
|
182
|
-
dispatcher.receive_transaction("txn1", body).should == :duplicate
|
|
183
|
-
received.length.should == 1
|
|
184
|
-
end
|
|
185
|
-
end
|
|
@@ -1,285 +0,0 @@
|
|
|
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 Async
|
|
7
|
-
module Matrix
|
|
8
|
-
module ApplicationService
|
|
9
|
-
# Wraps the content object of a Matrix event, providing typed access to
|
|
10
|
-
# schema-defined properties via method_missing.
|
|
11
|
-
#
|
|
12
|
-
# Common fields like `msgtype`, `body`, and `membership` have dedicated
|
|
13
|
-
# accessors for convenience. Any other field defined in the event's schema
|
|
14
|
-
# (or present in the raw hash) is accessible as a method call:
|
|
15
|
-
#
|
|
16
|
-
# content.msgtype # => "m.text"
|
|
17
|
-
# content.body # => "hello"
|
|
18
|
-
# content.membership # => "join"
|
|
19
|
-
# content.avatar_url # => "mxc://example.org/abc"
|
|
20
|
-
# content["custom"] # => direct hash access for non-standard fields
|
|
21
|
-
#
|
|
22
|
-
class Content
|
|
23
|
-
attr_reader :msgtype, :body, :membership
|
|
24
|
-
|
|
25
|
-
def initialize(data)
|
|
26
|
-
@data = data
|
|
27
|
-
@msgtype = data["msgtype"]
|
|
28
|
-
@body = data["body"]
|
|
29
|
-
@membership = data["membership"]
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def to_h = @data
|
|
33
|
-
def to_s = @body.to_s
|
|
34
|
-
def to_str = to_s
|
|
35
|
-
|
|
36
|
-
# Direct hash access for any content field.
|
|
37
|
-
def [](key) = @data[key.to_s]
|
|
38
|
-
|
|
39
|
-
# Dynamic access to any content field present in the raw data.
|
|
40
|
-
def method_missing(name, *args)
|
|
41
|
-
key = name.to_s
|
|
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
|
-
|
|
54
|
-
# Represents a Matrix event received via the Application Service API.
|
|
55
|
-
#
|
|
56
|
-
# Provides typed accessors for all envelope fields plus schema-driven
|
|
57
|
-
# validation using the official Matrix spec YAML schemas.
|
|
58
|
-
#
|
|
59
|
-
# event = Event.new(raw_hash)
|
|
60
|
-
# event.type # => "m.room.message"
|
|
61
|
-
# event.sender # => "@alice:example.org"
|
|
62
|
-
# event.content.body # => "hello"
|
|
63
|
-
# event.valid? # => true
|
|
64
|
-
# event.valid! # => true (or raises Schema::ValidationError)
|
|
65
|
-
#
|
|
66
|
-
class Event
|
|
67
|
-
attr_reader :type, :sender, :room_id, :state_key, :content,
|
|
68
|
-
:event_id, :origin_server_ts, :unsigned, :raw
|
|
69
|
-
|
|
70
|
-
def initialize(data)
|
|
71
|
-
@raw = data
|
|
72
|
-
@type = data["type"]
|
|
73
|
-
@sender = data["sender"]
|
|
74
|
-
@room_id = data["room_id"]
|
|
75
|
-
@state_key = data["state_key"]
|
|
76
|
-
@event_id = data["event_id"]
|
|
77
|
-
@origin_server_ts = data["origin_server_ts"]
|
|
78
|
-
@unsigned = data["unsigned"]
|
|
79
|
-
@content = Content.new(data["content"] || {})
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# The JSONSchemer::Schema for this event's type, or nil if unknown.
|
|
83
|
-
def schema = Schema[@type]
|
|
84
|
-
|
|
85
|
-
# Validate this event against its schema.
|
|
86
|
-
# Returns true if valid or if no schema exists (lenient).
|
|
87
|
-
def valid? = Schema.valid?(@raw)
|
|
88
|
-
|
|
89
|
-
# Validate this event against its schema.
|
|
90
|
-
# Raises Schema::ValidationError with detailed errors on failure.
|
|
91
|
-
# Returns true if valid or if no schema exists.
|
|
92
|
-
def valid!
|
|
93
|
-
errors = Schema.validate(@raw)
|
|
94
|
-
unless errors.empty?
|
|
95
|
-
raise Schema::ValidationError.new(
|
|
96
|
-
errors,
|
|
97
|
-
event_type: @type,
|
|
98
|
-
event_id: @event_id
|
|
99
|
-
)
|
|
100
|
-
end
|
|
101
|
-
true
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Content property names defined by the schema for this event type.
|
|
105
|
-
# @return [Array<String>]
|
|
106
|
-
def content_properties = Schema.content_properties(@type)
|
|
107
|
-
|
|
108
|
-
# Is this a state event? (has a state_key)
|
|
109
|
-
def state_event? = !@state_key.nil?
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
__END__
|
|
116
|
-
describe "Async::Matrix::ApplicationService::Content" do
|
|
117
|
-
it "parses msgtype, body, and membership" do
|
|
118
|
-
content = Async::Matrix::ApplicationService::Content.new({
|
|
119
|
-
"msgtype" => "m.text",
|
|
120
|
-
"body" => "hello",
|
|
121
|
-
"membership" => "join"
|
|
122
|
-
})
|
|
123
|
-
content.msgtype.should == "m.text"
|
|
124
|
-
content.body.should == "hello"
|
|
125
|
-
content.membership.should == "join"
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
it "handles missing fields gracefully" do
|
|
129
|
-
content = Async::Matrix::ApplicationService::Content.new({})
|
|
130
|
-
content.msgtype.should.be.nil
|
|
131
|
-
content.body.should.be.nil
|
|
132
|
-
content.membership.should.be.nil
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
it "provides hash access via []" do
|
|
136
|
-
content = Async::Matrix::ApplicationService::Content.new({"custom_field" => "value"})
|
|
137
|
-
content["custom_field"].should == "value"
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
it "provides dynamic access via method_missing" do
|
|
141
|
-
content = Async::Matrix::ApplicationService::Content.new({
|
|
142
|
-
"avatar_url" => "mxc://example.org/abc",
|
|
143
|
-
"displayname" => "Alice"
|
|
144
|
-
})
|
|
145
|
-
content.avatar_url.should == "mxc://example.org/abc"
|
|
146
|
-
content.displayname.should == "Alice"
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
it "returns nil for unknown fields via method_missing" do
|
|
150
|
-
content = Async::Matrix::ApplicationService::Content.new({})
|
|
151
|
-
content.nonexistent.should.be.nil
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
it "returns the raw hash via to_h" do
|
|
155
|
-
data = {"msgtype" => "m.text", "body" => "hi"}
|
|
156
|
-
content = Async::Matrix::ApplicationService::Content.new(data)
|
|
157
|
-
content.to_h.should == data
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
it "responds to keys present in the data" do
|
|
161
|
-
content = Async::Matrix::ApplicationService::Content.new({"avatar_url" => "mxc://x/y"})
|
|
162
|
-
content.respond_to?(:avatar_url).should == true
|
|
163
|
-
content.respond_to?(:nonexistent).should == false
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
describe "Async::Matrix::ApplicationService::Event" do
|
|
168
|
-
it "parses all event fields" do
|
|
169
|
-
event = Async::Matrix::ApplicationService::Event.new({
|
|
170
|
-
"type" => "m.room.message",
|
|
171
|
-
"sender" => "@alice:example.com",
|
|
172
|
-
"room_id" => "!abc:example.com",
|
|
173
|
-
"state_key" => "",
|
|
174
|
-
"event_id" => "$evt1",
|
|
175
|
-
"origin_server_ts" => 1234567890,
|
|
176
|
-
"unsigned" => {"age" => 1000},
|
|
177
|
-
"content" => {"msgtype" => "m.text", "body" => "hi"}
|
|
178
|
-
})
|
|
179
|
-
event.type.should == "m.room.message"
|
|
180
|
-
event.sender.should == "@alice:example.com"
|
|
181
|
-
event.room_id.should == "!abc:example.com"
|
|
182
|
-
event.state_key.should == ""
|
|
183
|
-
event.event_id.should == "$evt1"
|
|
184
|
-
event.origin_server_ts.should == 1234567890
|
|
185
|
-
event.unsigned.should == {"age" => 1000}
|
|
186
|
-
event.content.should.be.kind_of Async::Matrix::ApplicationService::Content
|
|
187
|
-
event.content.body.should == "hi"
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
it "defaults content to empty Content when missing" do
|
|
191
|
-
event = Async::Matrix::ApplicationService::Event.new({"type" => "m.room.message"})
|
|
192
|
-
event.content.should.be.kind_of Async::Matrix::ApplicationService::Content
|
|
193
|
-
event.content.body.should.be.nil
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
it "exposes the raw hash" do
|
|
197
|
-
data = {"type" => "m.room.message", "content" => {"body" => "hi", "msgtype" => "m.text"}}
|
|
198
|
-
event = Async::Matrix::ApplicationService::Event.new(data)
|
|
199
|
-
event.raw.should.equal data
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
it "detects state events" do
|
|
203
|
-
state = Async::Matrix::ApplicationService::Event.new({"type" => "m.room.member", "state_key" => "@a:b"})
|
|
204
|
-
state.state_event?.should == true
|
|
205
|
-
|
|
206
|
-
msg = Async::Matrix::ApplicationService::Event.new({"type" => "m.room.message"})
|
|
207
|
-
msg.state_event?.should == false
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
it "returns the schema for known event types" do
|
|
211
|
-
event = Async::Matrix::ApplicationService::Event.new({"type" => "m.room.message", "content" => {}})
|
|
212
|
-
event.schema.should.not.be.nil
|
|
213
|
-
event.schema.should.be.kind_of JSONSchemer::Schema
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
it "returns nil schema for unknown event types" do
|
|
217
|
-
event = Async::Matrix::ApplicationService::Event.new({"type" => "m.custom.event", "content" => {}})
|
|
218
|
-
event.schema.should.be.nil
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
it "validates a correct event" do
|
|
222
|
-
event = Async::Matrix::ApplicationService::Event.new({
|
|
223
|
-
"type" => "m.room.message",
|
|
224
|
-
"content" => {"msgtype" => "m.text", "body" => "hello"},
|
|
225
|
-
"event_id" => "$abc123",
|
|
226
|
-
"sender" => "@alice:example.org",
|
|
227
|
-
"origin_server_ts" => 1234567890,
|
|
228
|
-
"room_id" => "!room:example.org"
|
|
229
|
-
})
|
|
230
|
-
event.valid?.should == true
|
|
231
|
-
event.valid!.should == true
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
it "rejects an invalid event" do
|
|
235
|
-
event = Async::Matrix::ApplicationService::Event.new({
|
|
236
|
-
"type" => "m.room.message",
|
|
237
|
-
"content" => {"msgtype" => "m.text"},
|
|
238
|
-
"event_id" => "$abc123",
|
|
239
|
-
"sender" => "@alice:example.org",
|
|
240
|
-
"origin_server_ts" => 1234567890,
|
|
241
|
-
"room_id" => "!room:example.org"
|
|
242
|
-
})
|
|
243
|
-
event.valid?.should == false
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
it "raises ValidationError from valid!" do
|
|
247
|
-
event = Async::Matrix::ApplicationService::Event.new({
|
|
248
|
-
"type" => "m.room.member",
|
|
249
|
-
"content" => {"membership" => "invalid_state"},
|
|
250
|
-
"state_key" => "@alice:example.org",
|
|
251
|
-
"event_id" => "$abc123",
|
|
252
|
-
"sender" => "@alice:example.org",
|
|
253
|
-
"origin_server_ts" => 1234567890,
|
|
254
|
-
"room_id" => "!room:example.org"
|
|
255
|
-
})
|
|
256
|
-
begin
|
|
257
|
-
event.valid!
|
|
258
|
-
raise "should have raised"
|
|
259
|
-
rescue Async::Matrix::Schema::ValidationError => e
|
|
260
|
-
e.message.should.include "m.room.member"
|
|
261
|
-
e.message.should.include "$abc123"
|
|
262
|
-
e.errors.should.not.be.empty
|
|
263
|
-
end
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
it "is lenient with unknown event types" do
|
|
267
|
-
event = Async::Matrix::ApplicationService::Event.new({
|
|
268
|
-
"type" => "com.custom.event",
|
|
269
|
-
"content" => {"anything" => "goes"},
|
|
270
|
-
"event_id" => "$x",
|
|
271
|
-
"sender" => "@a:b"
|
|
272
|
-
})
|
|
273
|
-
event.valid?.should == true
|
|
274
|
-
event.valid!.should == true
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
it "returns content properties for known types" do
|
|
278
|
-
event = Async::Matrix::ApplicationService::Event.new({
|
|
279
|
-
"type" => "m.room.member",
|
|
280
|
-
"content" => {"membership" => "join"}
|
|
281
|
-
})
|
|
282
|
-
props = event.content_properties
|
|
283
|
-
props.should.include "membership"
|
|
284
|
-
end
|
|
285
|
-
end
|