async-matrix 2.1.0-x86_64-linux → 3.0.0-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/auth_error.rb +20 -0
- data/lib/async/matrix/bad_json_error.rb +20 -0
- data/lib/async/matrix/client.rb +142 -110
- data/lib/async/matrix/config/vivify.rb +107 -0
- data/lib/async/matrix/config.rb +184 -0
- data/lib/async/matrix/content.rb +106 -0
- data/lib/async/matrix/double_puppet_client.rb +1 -1
- data/lib/async/matrix/error.rb +0 -27
- data/lib/async/matrix/{application_service/error_response.rb → error_response.rb} +10 -10
- data/lib/async/matrix/event.rb +197 -0
- 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/schema/registry.rb +367 -340
- data/lib/async/matrix/schema/validation_error.rb +221 -214
- data/lib/async/matrix/schema.rb +161 -161
- data/lib/async/matrix/version.rb +3 -3
- data/lib/async/matrix.rb +0 -7
- metadata +25 -133
- 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
|
@@ -1,359 +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 "async"
|
|
7
|
-
require "async/http/endpoint"
|
|
8
|
-
require "async/websocket/client"
|
|
9
|
-
require "json"
|
|
10
|
-
require "console"
|
|
11
|
-
|
|
12
|
-
module Async
|
|
13
|
-
module Discord
|
|
14
|
-
# WebSocket client for the Discord Gateway (v10).
|
|
15
|
-
#
|
|
16
|
-
# Connects to Discord's WebSocket gateway, authenticates with a bot token,
|
|
17
|
-
# manages heartbeating, and dispatches incoming events to registered handlers.
|
|
18
|
-
#
|
|
19
|
-
# Supports session resumption on reconnect.
|
|
20
|
-
#
|
|
21
|
-
# gateway = Async::Discord::Gateway.new(token: "MTk...", intents: 0x30001)
|
|
22
|
-
# gateway.on("MESSAGE_CREATE") { |data| puts data["content"] }
|
|
23
|
-
# gateway.on("READY") { |data| puts "Connected as #{data["user"]["username"]}" }
|
|
24
|
-
# gateway.run # blocks, runs inside Async reactor
|
|
25
|
-
#
|
|
26
|
-
class Gateway
|
|
27
|
-
GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json"
|
|
28
|
-
|
|
29
|
-
# Discord Gateway opcodes
|
|
30
|
-
DISPATCH = 0
|
|
31
|
-
HEARTBEAT = 1
|
|
32
|
-
IDENTIFY = 2
|
|
33
|
-
PRESENCE_UPDATE = 3
|
|
34
|
-
VOICE_STATE_UPDATE = 4
|
|
35
|
-
RESUME = 6
|
|
36
|
-
RECONNECT = 7
|
|
37
|
-
REQUEST_GUILD_MEMBERS = 8
|
|
38
|
-
INVALID_SESSION = 9
|
|
39
|
-
HELLO = 10
|
|
40
|
-
HEARTBEAT_ACK = 11
|
|
41
|
-
|
|
42
|
-
attr_reader :session_id, :seq
|
|
43
|
-
|
|
44
|
-
def initialize(token:, intents:, gateway_url: GATEWAY_URL)
|
|
45
|
-
@token = token
|
|
46
|
-
@intents = intents
|
|
47
|
-
@gateway_url = gateway_url
|
|
48
|
-
@handlers = Hash.new { |h, k| h[k] = [] }
|
|
49
|
-
@seq = nil
|
|
50
|
-
@session_id = nil
|
|
51
|
-
@resume_url = nil
|
|
52
|
-
@heartbeat_interval = nil
|
|
53
|
-
@heartbeat_acked = true
|
|
54
|
-
@connection = nil
|
|
55
|
-
@running = false
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Register a handler for a Discord event type.
|
|
59
|
-
#
|
|
60
|
-
# gateway.on("MESSAGE_CREATE") { |data| ... }
|
|
61
|
-
# gateway.on("READY") { |data| ... }
|
|
62
|
-
#
|
|
63
|
-
def on(event_type, &block)
|
|
64
|
-
@handlers[event_type] << block
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Connect to the gateway and run the event loop.
|
|
68
|
-
# Blocks until the connection is closed or an unrecoverable error occurs.
|
|
69
|
-
# Must be called inside an Async reactor.
|
|
70
|
-
def run
|
|
71
|
-
@running = true
|
|
72
|
-
|
|
73
|
-
while @running
|
|
74
|
-
begin
|
|
75
|
-
connect_and_run
|
|
76
|
-
rescue => e
|
|
77
|
-
Console.error(self) { "Gateway error: #{e.class}: #{e.message}" }
|
|
78
|
-
break unless @running
|
|
79
|
-
sleep(5)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Gracefully stop the gateway.
|
|
85
|
-
def stop
|
|
86
|
-
@running = false
|
|
87
|
-
@connection&.close
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Send a raw payload to the gateway.
|
|
91
|
-
def send_payload(op:, d: nil)
|
|
92
|
-
return unless @connection
|
|
93
|
-
|
|
94
|
-
payload = {op: op, d: d}
|
|
95
|
-
@connection.write(payload.to_json)
|
|
96
|
-
@connection.flush
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
private
|
|
100
|
-
|
|
101
|
-
def connect_and_run
|
|
102
|
-
url = @resume_url || @gateway_url
|
|
103
|
-
endpoint = Async::HTTP::Endpoint.parse(url)
|
|
104
|
-
|
|
105
|
-
Console.info(self) { "Connecting to #{url}" }
|
|
106
|
-
|
|
107
|
-
Async::WebSocket::Client.connect(endpoint) do |connection|
|
|
108
|
-
@connection = connection
|
|
109
|
-
|
|
110
|
-
# First message should be HELLO
|
|
111
|
-
hello = read_message
|
|
112
|
-
unless hello && hello["op"] == HELLO
|
|
113
|
-
raise GatewayError.new("GATEWAY", "Expected HELLO, got: #{hello&.dig("op")}")
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
@heartbeat_interval = hello["d"]["heartbeat_interval"] / 1000.0
|
|
117
|
-
Console.info(self) { "Heartbeat interval: #{@heartbeat_interval}s" }
|
|
118
|
-
|
|
119
|
-
# Start heartbeat fiber
|
|
120
|
-
heartbeat_task = Async do
|
|
121
|
-
heartbeat_loop
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Identify or resume
|
|
125
|
-
if @session_id && @seq
|
|
126
|
-
send_resume
|
|
127
|
-
else
|
|
128
|
-
send_identify
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# Read events until disconnected
|
|
132
|
-
read_loop
|
|
133
|
-
|
|
134
|
-
ensure
|
|
135
|
-
heartbeat_task&.stop
|
|
136
|
-
@connection = nil
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def read_message
|
|
141
|
-
message = @connection.read
|
|
142
|
-
return nil unless message
|
|
143
|
-
|
|
144
|
-
JSON.parse(message.to_str)
|
|
145
|
-
rescue => e
|
|
146
|
-
Console.error(self) { "Failed to read/parse gateway message: #{e.message}" }
|
|
147
|
-
nil
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def read_loop
|
|
151
|
-
loop do
|
|
152
|
-
payload = read_message
|
|
153
|
-
break unless payload
|
|
154
|
-
|
|
155
|
-
handle_payload(payload)
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def handle_payload(payload)
|
|
160
|
-
op = payload["op"]
|
|
161
|
-
data = payload["d"]
|
|
162
|
-
seq = payload["s"]
|
|
163
|
-
type = payload["t"]
|
|
164
|
-
|
|
165
|
-
@seq = seq if seq
|
|
166
|
-
|
|
167
|
-
case op
|
|
168
|
-
when DISPATCH
|
|
169
|
-
Console.debug(self) { "DISPATCH: #{type}" }
|
|
170
|
-
dispatch(type, data)
|
|
171
|
-
|
|
172
|
-
when HEARTBEAT
|
|
173
|
-
# Server is asking us to heartbeat immediately
|
|
174
|
-
send_heartbeat
|
|
175
|
-
|
|
176
|
-
when RECONNECT
|
|
177
|
-
Console.info(self) { "Server requested reconnect" }
|
|
178
|
-
@connection&.close
|
|
179
|
-
|
|
180
|
-
when INVALID_SESSION
|
|
181
|
-
resumable = data == true
|
|
182
|
-
Console.warn(self) { "Invalid session (resumable=#{resumable})" }
|
|
183
|
-
unless resumable
|
|
184
|
-
@session_id = nil
|
|
185
|
-
@seq = nil
|
|
186
|
-
end
|
|
187
|
-
sleep(rand(1.0..5.0))
|
|
188
|
-
@connection&.close
|
|
189
|
-
|
|
190
|
-
when HEARTBEAT_ACK
|
|
191
|
-
@heartbeat_acked = true
|
|
192
|
-
|
|
193
|
-
else
|
|
194
|
-
Console.debug(self) { "Unknown opcode: #{op}" }
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def dispatch(event_type, data)
|
|
199
|
-
case event_type
|
|
200
|
-
when "READY"
|
|
201
|
-
@session_id = data["session_id"]
|
|
202
|
-
@resume_url = data["resume_gateway_url"]
|
|
203
|
-
Console.info(self) { "Ready: session=#{@session_id}" }
|
|
204
|
-
when "RESUMED"
|
|
205
|
-
Console.info(self) { "Resumed successfully" }
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
@handlers[event_type].each do |handler|
|
|
209
|
-
begin
|
|
210
|
-
handler.call(data)
|
|
211
|
-
rescue => e
|
|
212
|
-
Console.error(self) { "Handler for #{event_type} raised: #{e.class}: #{e.message}" }
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def send_identify
|
|
218
|
-
send_payload(
|
|
219
|
-
op: IDENTIFY,
|
|
220
|
-
d: {
|
|
221
|
-
token: @token,
|
|
222
|
-
intents: @intents,
|
|
223
|
-
properties: {
|
|
224
|
-
os: RUBY_PLATFORM,
|
|
225
|
-
browser: "async-discord",
|
|
226
|
-
device: "async-discord"
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
)
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def send_resume
|
|
233
|
-
Console.info(self) { "Resuming session #{@session_id} at seq #{@seq}" }
|
|
234
|
-
send_payload(
|
|
235
|
-
op: RESUME,
|
|
236
|
-
d: {
|
|
237
|
-
token: @token,
|
|
238
|
-
session_id: @session_id,
|
|
239
|
-
seq: @seq
|
|
240
|
-
}
|
|
241
|
-
)
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
def send_heartbeat
|
|
245
|
-
send_payload(op: HEARTBEAT, d: @seq)
|
|
246
|
-
@heartbeat_acked = false
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def heartbeat_loop
|
|
250
|
-
# Jitter the first heartbeat as per Discord docs
|
|
251
|
-
sleep(@heartbeat_interval * rand)
|
|
252
|
-
send_heartbeat
|
|
253
|
-
|
|
254
|
-
loop do
|
|
255
|
-
sleep(@heartbeat_interval)
|
|
256
|
-
|
|
257
|
-
unless @heartbeat_acked
|
|
258
|
-
Console.warn(self) { "Heartbeat not ACKed, reconnecting" }
|
|
259
|
-
@connection&.close
|
|
260
|
-
break
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
send_heartbeat
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
__END__
|
|
271
|
-
describe "Async::Discord::Gateway" do
|
|
272
|
-
it "initializes with token and intents" do
|
|
273
|
-
gw = Async::Discord::Gateway.new(token: "test_token", intents: 0x30001)
|
|
274
|
-
gw.session_id.should.be.nil
|
|
275
|
-
gw.seq.should.be.nil
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
it "registers event handlers" do
|
|
279
|
-
gw = Async::Discord::Gateway.new(token: "test_token", intents: 0)
|
|
280
|
-
received = []
|
|
281
|
-
gw.on("MESSAGE_CREATE") { |data| received << data }
|
|
282
|
-
gw.on("MESSAGE_CREATE") { |data| received << :second }
|
|
283
|
-
|
|
284
|
-
# Dispatch manually via send to test handler registration
|
|
285
|
-
gw.send(:dispatch, "MESSAGE_CREATE", {"content" => "hello"})
|
|
286
|
-
received.length.should == 2
|
|
287
|
-
received[0]["content"].should == "hello"
|
|
288
|
-
received[1].should == :second
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
it "handles READY event and stores session_id" do
|
|
292
|
-
gw = Async::Discord::Gateway.new(token: "test_token", intents: 0)
|
|
293
|
-
ready_data = {
|
|
294
|
-
"session_id" => "abc123",
|
|
295
|
-
"resume_gateway_url" => "wss://resume.discord.gg",
|
|
296
|
-
"user" => {"username" => "testbot"}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
gw.send(:dispatch, "READY", ready_data)
|
|
300
|
-
gw.session_id.should == "abc123"
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
it "updates seq from dispatch payloads" do
|
|
304
|
-
gw = Async::Discord::Gateway.new(token: "test_token", intents: 0)
|
|
305
|
-
gw.send(:handle_payload, {
|
|
306
|
-
"op" => 0,
|
|
307
|
-
"d" => {"content" => "hi"},
|
|
308
|
-
"s" => 42,
|
|
309
|
-
"t" => "MESSAGE_CREATE"
|
|
310
|
-
})
|
|
311
|
-
gw.seq.should == 42
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
it "clears session on non-resumable INVALID_SESSION" do
|
|
315
|
-
gw = Async::Discord::Gateway.new(token: "test_token", intents: 0)
|
|
316
|
-
# Set a session first
|
|
317
|
-
gw.send(:dispatch, "READY", {
|
|
318
|
-
"session_id" => "sess123",
|
|
319
|
-
"resume_gateway_url" => "wss://resume.discord.gg"
|
|
320
|
-
})
|
|
321
|
-
gw.session_id.should == "sess123"
|
|
322
|
-
|
|
323
|
-
# Simulate non-resumable invalid session (stub connection close)
|
|
324
|
-
gw.instance_variable_set(:@connection, nil)
|
|
325
|
-
gw.send(:handle_payload, {
|
|
326
|
-
"op" => Async::Discord::Gateway::INVALID_SESSION,
|
|
327
|
-
"d" => false,
|
|
328
|
-
"s" => nil,
|
|
329
|
-
"t" => nil
|
|
330
|
-
})
|
|
331
|
-
gw.session_id.should.be.nil
|
|
332
|
-
gw.seq.should.be.nil
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
it "continues dispatching when a handler raises" do
|
|
336
|
-
gw = Async::Discord::Gateway.new(token: "test_token", intents: 0)
|
|
337
|
-
results = []
|
|
338
|
-
gw.on("TEST") { |_| raise "boom" }
|
|
339
|
-
gw.on("TEST") { |data| results << data }
|
|
340
|
-
|
|
341
|
-
gw.send(:dispatch, "TEST", {"value" => 1})
|
|
342
|
-
results.length.should == 1
|
|
343
|
-
results.first["value"].should == 1
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
it "responds to stop" do
|
|
347
|
-
gw = Async::Discord::Gateway.new(token: "test_token", intents: 0)
|
|
348
|
-
gw.should.respond_to :stop
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
it "accepts custom gateway URL" do
|
|
352
|
-
gw = Async::Discord::Gateway.new(
|
|
353
|
-
token: "tok",
|
|
354
|
-
intents: 0,
|
|
355
|
-
gateway_url: "wss://custom.gateway.example.com"
|
|
356
|
-
)
|
|
357
|
-
gw.instance_variable_get(:@gateway_url).should == "wss://custom.gateway.example.com"
|
|
358
|
-
end
|
|
359
|
-
end
|
data/lib/async/discord.rb
DELETED
|
@@ -1,15 +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 "async/http"
|
|
7
|
-
|
|
8
|
-
module Async
|
|
9
|
-
module Discord
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
Dir.glob("#{__dir__}/discord/**/*.rb").sort.each do |path|
|
|
14
|
-
require path
|
|
15
|
-
end
|
|
@@ -1,232 +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
|
-
# DSL wrapper that pairs a Client with event handlers.
|
|
12
|
-
#
|
|
13
|
-
# A Bot produces handler objects that conform to the TransactionHandler's
|
|
14
|
-
# duck-type contract (#event_types, #call). Register a bot on a
|
|
15
|
-
# Server (or TransactionHandler) the same way you would register a plain handler.
|
|
16
|
-
#
|
|
17
|
-
# bot = Bot.new(client) do
|
|
18
|
-
# on "m.room.member" do |event|
|
|
19
|
-
# join_room(event.room_id) if event.content.membership == "invite"
|
|
20
|
-
# end
|
|
21
|
-
#
|
|
22
|
-
# on "m.room.message", msgtype: "m.text", not_from: :self do |event|
|
|
23
|
-
# send_notice event.room_id, "Echo: #{event.content.body}"
|
|
24
|
-
# end
|
|
25
|
-
# end
|
|
26
|
-
#
|
|
27
|
-
# server.register(bot)
|
|
28
|
-
#
|
|
29
|
-
class Bot
|
|
30
|
-
attr_reader :client, :handlers
|
|
31
|
-
|
|
32
|
-
def initialize(client, &block)
|
|
33
|
-
@client = client
|
|
34
|
-
@handlers = []
|
|
35
|
-
|
|
36
|
-
instance_eval(&block) if block
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Register a handler block for one or more event types.
|
|
40
|
-
#
|
|
41
|
-
# Filters (all optional):
|
|
42
|
-
# msgtype: — only dispatch when content.msgtype matches (e.g. "m.text")
|
|
43
|
-
# not_from: — :self skips events sent by this bot's own MXID
|
|
44
|
-
#
|
|
45
|
-
def on(*event_types, msgtype: nil, not_from: nil, &block)
|
|
46
|
-
raise ArgumentError, "on requires at least one event type" if event_types.empty?
|
|
47
|
-
raise ArgumentError, "on requires a block" unless block
|
|
48
|
-
|
|
49
|
-
handler = Handler.new(
|
|
50
|
-
bot: self,
|
|
51
|
-
event_types: event_types.flatten,
|
|
52
|
-
msgtype: msgtype,
|
|
53
|
-
not_from: not_from,
|
|
54
|
-
block: block
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
@handlers << handler
|
|
58
|
-
handler
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Internal handler object produced by the #on DSL method.
|
|
62
|
-
# Conforms to the TransactionHandler duck-type: #event_types, #call.
|
|
63
|
-
class Handler
|
|
64
|
-
attr_reader :event_types
|
|
65
|
-
|
|
66
|
-
def initialize(bot:, event_types:, msgtype:, not_from:, block:)
|
|
67
|
-
@bot = bot
|
|
68
|
-
@event_types = event_types
|
|
69
|
-
@msgtype = msgtype
|
|
70
|
-
@not_from = not_from
|
|
71
|
-
@block = block
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def call(event)
|
|
75
|
-
return if @msgtype && event.content&.msgtype != @msgtype
|
|
76
|
-
return if @not_from == :self && event.sender == @bot.client.config.bot_mxid
|
|
77
|
-
|
|
78
|
-
# Execute the block in a context that has helper methods
|
|
79
|
-
Context.new(@bot.client).execute(event, &@block)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Execution context for handler blocks.
|
|
84
|
-
# Provides helper methods so blocks can call send_notice, join_room, etc.
|
|
85
|
-
# directly without holding a reference to the client.
|
|
86
|
-
class Context
|
|
87
|
-
def initialize(client)
|
|
88
|
-
@client = client
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def execute(event, &block)
|
|
92
|
-
@event = event
|
|
93
|
-
instance_exec(event, &block)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def send_text(room_id, text)
|
|
97
|
-
@client.send_text(room_id, text)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def send_html(room_id, html, plaintext = nil)
|
|
101
|
-
@client.send_html(room_id, html, plaintext)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def send_notice(room_id, text)
|
|
105
|
-
@client.send_notice(room_id, text)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def join_room(room_id)
|
|
109
|
-
@client.join_room(room_id)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def leave_room(room_id)
|
|
113
|
-
@client.leave_room(room_id)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def set_display_name(name, user_id = nil)
|
|
117
|
-
@client.set_display_name(name, user_id)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def client
|
|
121
|
-
@client
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
__END__
|
|
130
|
-
describe "Async::Matrix::ApplicationService::Bot" do
|
|
131
|
-
def make_config
|
|
132
|
-
Async::Matrix::ApplicationService::Config.new({
|
|
133
|
-
"homeserver" => { "address" => "http://localhost:8008", "domain" => "localhost" },
|
|
134
|
-
"appservice" => { "as_token" => "token_secret", "hs_token" => "hs_secret", "bot" => { "username" => "bot" } }
|
|
135
|
-
})
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def make_event(type:, sender: "@user:localhost", msgtype: nil, body: nil, membership: nil, state_key: nil)
|
|
139
|
-
data = {"type" => type, "sender" => sender, "room_id" => "!room:localhost"}
|
|
140
|
-
content = {}
|
|
141
|
-
content["msgtype"] = msgtype if msgtype
|
|
142
|
-
content["body"] = body if body
|
|
143
|
-
content["membership"] = membership if membership
|
|
144
|
-
data["state_key"] = state_key if state_key
|
|
145
|
-
data["content"] = content
|
|
146
|
-
Async::Matrix::ApplicationService::Event.new(data)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
it "registers handlers via the on DSL" do
|
|
150
|
-
config = make_config
|
|
151
|
-
client = Async::Matrix::Client.new(config)
|
|
152
|
-
|
|
153
|
-
bot = Async::Matrix::ApplicationService::Bot.new(client) do
|
|
154
|
-
on "m.room.message" do |event|
|
|
155
|
-
# no-op
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
bot.handlers.length.should == 1
|
|
160
|
-
bot.handlers.first.event_types.should == ["m.room.message"]
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
it "registers multiple event types on a single handler" do
|
|
164
|
-
config = make_config
|
|
165
|
-
client = Async::Matrix::Client.new(config)
|
|
166
|
-
|
|
167
|
-
bot = Async::Matrix::ApplicationService::Bot.new(client) do
|
|
168
|
-
on "m.room.message", "m.room.member" do |event|
|
|
169
|
-
# no-op
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
bot.handlers.first.event_types.should == ["m.room.message", "m.room.member"]
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
it "filters by msgtype" do
|
|
177
|
-
config = make_config
|
|
178
|
-
client = Async::Matrix::Client.new(config)
|
|
179
|
-
received = []
|
|
180
|
-
|
|
181
|
-
bot = Async::Matrix::ApplicationService::Bot.new(client) do
|
|
182
|
-
on "m.room.message", msgtype: "m.text" do |event|
|
|
183
|
-
received << event.content.body
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
handler = bot.handlers.first
|
|
188
|
-
handler.call(make_event(type: "m.room.message", msgtype: "m.text", body: "hello"))
|
|
189
|
-
handler.call(make_event(type: "m.room.message", msgtype: "m.notice", body: "ignored"))
|
|
190
|
-
|
|
191
|
-
received.should == ["hello"]
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
it "filters out self with not_from: :self" do
|
|
195
|
-
config = make_config
|
|
196
|
-
client = Async::Matrix::Client.new(config)
|
|
197
|
-
received = []
|
|
198
|
-
|
|
199
|
-
bot = Async::Matrix::ApplicationService::Bot.new(client) do
|
|
200
|
-
on "m.room.message", not_from: :self do |event|
|
|
201
|
-
received << event.content.body
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
handler = bot.handlers.first
|
|
206
|
-
handler.call(make_event(type: "m.room.message", sender: "@user:localhost", msgtype: "m.text", body: "yes"))
|
|
207
|
-
handler.call(make_event(type: "m.room.message", sender: "@bot:localhost", msgtype: "m.text", body: "no"))
|
|
208
|
-
|
|
209
|
-
received.should == ["yes"]
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
it "raises if on is called without event types" do
|
|
213
|
-
config = make_config
|
|
214
|
-
client = Async::Matrix::Client.new(config)
|
|
215
|
-
|
|
216
|
-
lambda {
|
|
217
|
-
Async::Matrix::ApplicationService::Bot.new(client) do
|
|
218
|
-
on do |event|; end
|
|
219
|
-
end
|
|
220
|
-
}.should.raise(ArgumentError)
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
it "raises if on is called without a block" do
|
|
224
|
-
config = make_config
|
|
225
|
-
client = Async::Matrix::Client.new(config)
|
|
226
|
-
|
|
227
|
-
lambda {
|
|
228
|
-
bot = Async::Matrix::ApplicationService::Bot.new(client)
|
|
229
|
-
bot.on "m.room.message"
|
|
230
|
-
}.should.raise(ArgumentError)
|
|
231
|
-
end
|
|
232
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "analytics.json",
|
|
4
|
-
"description": "Analytics/telemetry settings. Maps to mautrix bridgev2/bridgeconfig.AnalyticsConfig.",
|
|
5
|
-
"type": "object",
|
|
6
|
-
"properties": {
|
|
7
|
-
"token": {
|
|
8
|
-
"type": "string",
|
|
9
|
-
"description": "Analytics token for the telemetry backend."
|
|
10
|
-
},
|
|
11
|
-
"url": {
|
|
12
|
-
"type": "string",
|
|
13
|
-
"description": "URL of the analytics/telemetry endpoint."
|
|
14
|
-
},
|
|
15
|
-
"user_id": {
|
|
16
|
-
"type": "string",
|
|
17
|
-
"description": "User ID to associate with analytics events."
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"additionalProperties": false
|
|
21
|
-
}
|