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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +33 -58
  3. data/lib/async/matrix/3.3/async_matrix_e2ee.so +0 -0
  4. data/lib/async/matrix/3.4/async_matrix_e2ee.so +0 -0
  5. data/lib/async/matrix/4.0/async_matrix_e2ee.so +0 -0
  6. data/lib/async/matrix/api/chain.rb +120 -94
  7. data/lib/async/matrix/api/concat.rb +28 -22
  8. data/lib/async/matrix/api/path_tree.rb +71 -44
  9. data/lib/async/matrix/api.rb +6 -1
  10. data/lib/async/matrix/app_service_client.rb +213 -0
  11. data/lib/async/matrix/auth_error.rb +20 -0
  12. data/lib/async/matrix/bad_json_error.rb +20 -0
  13. data/lib/async/matrix/client/encryption.rb +413 -0
  14. data/lib/async/matrix/client/rooms.rb +478 -0
  15. data/lib/async/matrix/client/sync.rb +508 -0
  16. data/lib/async/matrix/client.rb +217 -113
  17. data/lib/async/matrix/config/vivify.rb +107 -0
  18. data/lib/async/matrix/config.rb +184 -0
  19. data/lib/async/matrix/device_store.rb +1621 -0
  20. data/lib/async/matrix/double_puppet_client.rb +1 -1
  21. data/lib/async/matrix/e2ee/pickle_key.rb +182 -0
  22. data/lib/async/matrix/error.rb +9 -36
  23. data/lib/async/matrix/{application_service/error_response.rb → error_response.rb} +10 -10
  24. data/lib/async/matrix/homeserver_error.rb +20 -0
  25. data/lib/async/matrix/invalid_endpoint_error.rb +20 -0
  26. data/lib/async/matrix/media_client.rb +46 -35
  27. data/lib/async/matrix/not_found_error.rb +20 -0
  28. data/lib/async/matrix/notifier.rb +7 -3
  29. data/lib/async/matrix/response_too_large_error.rb +20 -0
  30. data/lib/async/matrix/version.rb +1 -1
  31. data/lib/async/matrix.rb +16 -7
  32. data/lib/protocol/matrix/canonical_json.rb +200 -0
  33. data/lib/protocol/matrix/content.rb +106 -0
  34. data/lib/protocol/matrix/encrypted_message.rb +1018 -0
  35. data/lib/protocol/matrix/error.rb +49 -0
  36. data/lib/protocol/matrix/event.rb +208 -0
  37. data/lib/protocol/matrix/key_backup.rb +347 -0
  38. data/lib/protocol/matrix/keys.rb +381 -0
  39. data/lib/protocol/matrix/message_batch.rb +482 -0
  40. data/lib/protocol/matrix/schema/registry.rb +381 -0
  41. data/lib/protocol/matrix/schema/validation_error.rb +234 -0
  42. data/lib/protocol/matrix/schema.rb +170 -0
  43. data/lib/protocol/matrix/secret_storage.rb +535 -0
  44. data/lib/protocol/matrix/signing.rb +278 -0
  45. data/lib/protocol/matrix.rb +25 -0
  46. metadata +43 -136
  47. data/lib/async/discord/api/path_tree.rb +0 -127
  48. data/lib/async/discord/api.rb +0 -151
  49. data/lib/async/discord/client.rb +0 -283
  50. data/lib/async/discord/error.rb +0 -84
  51. data/lib/async/discord/gateway.rb +0 -359
  52. data/lib/async/discord.rb +0 -15
  53. data/lib/async/matrix/application_service/bot.rb +0 -232
  54. data/lib/async/matrix/application_service/config/schema/analytics.json +0 -21
  55. data/lib/async/matrix/application_service/config/schema/appservice.json +0 -82
  56. data/lib/async/matrix/application_service/config/schema/backfill.json +0 -91
  57. data/lib/async/matrix/application_service/config/schema/bridge.json +0 -209
  58. data/lib/async/matrix/application_service/config/schema/config.json +0 -61
  59. data/lib/async/matrix/application_service/config/schema/database.json +0 -38
  60. data/lib/async/matrix/application_service/config/schema/direct_media.json +0 -35
  61. data/lib/async/matrix/application_service/config/schema/double_puppet.json +0 -24
  62. data/lib/async/matrix/application_service/config/schema/encryption.json +0 -164
  63. data/lib/async/matrix/application_service/config/schema/homeserver.json +0 -58
  64. data/lib/async/matrix/application_service/config/schema/logging.json +0 -50
  65. data/lib/async/matrix/application_service/config/schema/management_room_texts.json +0 -25
  66. data/lib/async/matrix/application_service/config/schema/matrix.json +0 -45
  67. data/lib/async/matrix/application_service/config/schema/permissions.json +0 -54
  68. data/lib/async/matrix/application_service/config/schema/provisioning.json +0 -23
  69. data/lib/async/matrix/application_service/config/schema/public_media.json +0 -39
  70. data/lib/async/matrix/application_service/config/schema/relay.json +0 -43
  71. data/lib/async/matrix/application_service/config/vivify.rb +0 -109
  72. data/lib/async/matrix/application_service/config.rb +0 -225
  73. data/lib/async/matrix/application_service/dispatcher.rb +0 -185
  74. data/lib/async/matrix/application_service/event.rb +0 -285
  75. data/lib/async/matrix/application_service/server.rb +0 -430
  76. data/lib/async/matrix/application_service/transaction.rb +0 -66
  77. data/lib/async/matrix/application_service/transaction_handler.rb +0 -185
  78. data/lib/async/matrix/application_service/transaction_store.rb +0 -80
  79. data/lib/async/matrix/bridge/discord/db/connection.rb +0 -141
  80. data/lib/async/matrix/bridge/discord/db/file.rb +0 -118
  81. data/lib/async/matrix/bridge/discord/db/guild.rb +0 -120
  82. data/lib/async/matrix/bridge/discord/db/message.rb +0 -160
  83. data/lib/async/matrix/bridge/discord/db/migrations/001_create_users.rb +0 -14
  84. data/lib/async/matrix/bridge/discord/db/migrations/002_create_guilds.rb +0 -14
  85. data/lib/async/matrix/bridge/discord/db/migrations/003_create_portals.rb +0 -23
  86. data/lib/async/matrix/bridge/discord/db/migrations/004_create_puppets.rb +0 -19
  87. data/lib/async/matrix/bridge/discord/db/migrations/005_create_messages.rb +0 -20
  88. data/lib/async/matrix/bridge/discord/db/migrations/006_create_reactions.rb +0 -19
  89. data/lib/async/matrix/bridge/discord/db/migrations/007_create_files.rb +0 -18
  90. data/lib/async/matrix/bridge/discord/db/portal.rb +0 -150
  91. data/lib/async/matrix/bridge/discord/db/puppet.rb +0 -128
  92. data/lib/async/matrix/bridge/discord/db/reaction.rb +0 -165
  93. data/lib/async/matrix/bridge/discord/db/schema.rb +0 -18
  94. data/lib/async/matrix/bridge/discord/db/user.rb +0 -112
  95. data/lib/async/matrix/bridge/discord/db.rb +0 -138
  96. data/lib/async/matrix/schema/registry.rb +0 -354
  97. data/lib/async/matrix/schema/validation_error.rb +0 -225
  98. 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