activematrix 0.0.0
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 +7 -0
- data/CHANGELOG.md +219 -0
- data/LICENSE.txt +21 -0
- data/README.md +82 -0
- data/lib/matrix_sdk/api.rb +451 -0
- data/lib/matrix_sdk/bot/base.rb +847 -0
- data/lib/matrix_sdk/bot/main.rb +79 -0
- data/lib/matrix_sdk/bot.rb +4 -0
- data/lib/matrix_sdk/client.rb +696 -0
- data/lib/matrix_sdk/errors.rb +68 -0
- data/lib/matrix_sdk/mxid.rb +146 -0
- data/lib/matrix_sdk/protocols/as.rb +7 -0
- data/lib/matrix_sdk/protocols/cs.rb +1982 -0
- data/lib/matrix_sdk/protocols/is.rb +35 -0
- data/lib/matrix_sdk/protocols/msc.rb +152 -0
- data/lib/matrix_sdk/protocols/ss.rb +14 -0
- data/lib/matrix_sdk/response.rb +63 -0
- data/lib/matrix_sdk/room.rb +1044 -0
- data/lib/matrix_sdk/rooms/space.rb +79 -0
- data/lib/matrix_sdk/user.rb +168 -0
- data/lib/matrix_sdk/util/account_data_cache.rb +91 -0
- data/lib/matrix_sdk/util/events.rb +111 -0
- data/lib/matrix_sdk/util/extensions.rb +85 -0
- data/lib/matrix_sdk/util/state_event_cache.rb +92 -0
- data/lib/matrix_sdk/util/tinycache.rb +140 -0
- data/lib/matrix_sdk/util/tinycache_adapter.rb +87 -0
- data/lib/matrix_sdk/util/uri.rb +101 -0
- data/lib/matrix_sdk/version.rb +5 -0
- data/lib/matrix_sdk.rb +75 -0
- metadata +172 -0
@@ -0,0 +1,847 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'shellwords'
|
4
|
+
|
5
|
+
module MatrixSdk::Bot
|
6
|
+
class Base
|
7
|
+
extend MatrixSdk::Extensions
|
8
|
+
|
9
|
+
RequestHandler = Struct.new('RequestHandler', :command, :type, :proc, :data) do
|
10
|
+
def command?
|
11
|
+
type == :command
|
12
|
+
end
|
13
|
+
|
14
|
+
def event?
|
15
|
+
type == :event
|
16
|
+
end
|
17
|
+
|
18
|
+
def arity
|
19
|
+
arity = self.proc.parameters.count { |t, _| %i[opt req].include? t }
|
20
|
+
arity = -arity if self.proc.parameters.any? { |t, _| t.to_s.include? 'rest' }
|
21
|
+
arity
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :client, :event
|
26
|
+
attr_writer :logger
|
27
|
+
|
28
|
+
ignore_inspect :client
|
29
|
+
|
30
|
+
def initialize(hs_url, **params)
|
31
|
+
@client = case hs_url
|
32
|
+
when MatrixSdk::Api
|
33
|
+
MatrixSdk::Client.new hs_url
|
34
|
+
when MatrixSdk::Client
|
35
|
+
hs_url
|
36
|
+
when %r{^https?://.*}
|
37
|
+
MatrixSdk::Client.new hs_url, **params
|
38
|
+
else
|
39
|
+
MatrixSdk::Client.new_for_domain hs_url, **params
|
40
|
+
end
|
41
|
+
|
42
|
+
@client.on_event.add_handler { |ev| _handle_event(ev) }
|
43
|
+
@client.on_invite_event.add_handler do |ev|
|
44
|
+
break unless settings.accept_invites?
|
45
|
+
|
46
|
+
logger.info "Received invite to #{ev[:room_id]}, joining."
|
47
|
+
client.join_room(ev[:room_id])
|
48
|
+
end
|
49
|
+
|
50
|
+
@event = nil
|
51
|
+
|
52
|
+
logger.warn 'The bot abstraction is not fully finalized and can be expected to change.'
|
53
|
+
end
|
54
|
+
|
55
|
+
def logger
|
56
|
+
return @logger if instance_variable_defined?(:@logger) && @logger
|
57
|
+
|
58
|
+
self.class.logger
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.logger
|
62
|
+
Logging.logger[self].tap do |l|
|
63
|
+
begin
|
64
|
+
l.level = :debug if MatrixSdk::Bot::PARAMS_CONFIG[:logging]
|
65
|
+
rescue NameError
|
66
|
+
# Not running as instance
|
67
|
+
end
|
68
|
+
l.level = settings.log_level unless settings.logging?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Register a command during runtime
|
73
|
+
#
|
74
|
+
# @param command [String] The command to register
|
75
|
+
# @see Base.command for full parameter information
|
76
|
+
def register_command(command, **params, &block)
|
77
|
+
self.class.command(command, **params, &block)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Register an event during runtime
|
81
|
+
#
|
82
|
+
# @param event [String] The event to register
|
83
|
+
# @see Base.event for full parameter information
|
84
|
+
def register_event(event, **params, &block)
|
85
|
+
self.class.event(event, **params, &block)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Removes a registered command during runtime
|
89
|
+
#
|
90
|
+
# @param command [String] The command to remove
|
91
|
+
# @see Base.remove_command
|
92
|
+
def unregister_command(command)
|
93
|
+
self.class.remove_command(command)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Removes a registered event during runtime
|
97
|
+
#
|
98
|
+
# @param event [String] The event to remove
|
99
|
+
# @see Base.remove_event
|
100
|
+
def unregister_event(command)
|
101
|
+
self.class.remove_event(command)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Gets the handler for a command
|
105
|
+
#
|
106
|
+
# @param command [String] The command to retrieve
|
107
|
+
# @return [RequestHandler] The registered command handler
|
108
|
+
# @see Base.get_command
|
109
|
+
def get_command(command, **params)
|
110
|
+
self.class.get_command(command, **params)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Gets the handler for an event
|
114
|
+
#
|
115
|
+
# @param event [String] The event to retrieve
|
116
|
+
# @return [RequestHandler] The registered event handler
|
117
|
+
# @see Base.get_event
|
118
|
+
def get_event(event, **params)
|
119
|
+
self.class.get_event(event, **params)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Checks for the existence of a command
|
123
|
+
#
|
124
|
+
# @param command [String] The command to check
|
125
|
+
# @see Base.command?
|
126
|
+
def command?(command, **params)
|
127
|
+
self.class.command?(command, **params)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Checks for the existence of a handled event
|
131
|
+
#
|
132
|
+
# @param event [String] The event to check
|
133
|
+
# @see Base.event?
|
134
|
+
def event?(event, **params)
|
135
|
+
self.class.event?(event, **params)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Access settings defined with Base.set
|
139
|
+
def settings
|
140
|
+
self.class.settings
|
141
|
+
end
|
142
|
+
|
143
|
+
# Access settings defined with Base.set
|
144
|
+
def self.settings
|
145
|
+
self
|
146
|
+
end
|
147
|
+
|
148
|
+
class << self
|
149
|
+
attr_reader :handlers
|
150
|
+
|
151
|
+
CALLERS_TO_IGNORE = [
|
152
|
+
/\/matrix_sdk\/.+\.rb$/, # all MatrixSdk code
|
153
|
+
/^\(.*\)$/, # generated code
|
154
|
+
/rubygems\/(custom|core_ext\/kernel)_require\.rb$/, # rubygems require hacks
|
155
|
+
/bundler(\/(?:runtime|inline))?\.rb/, # bundler require hacks
|
156
|
+
/<internal:/ # internal in ruby >= 1.9.2
|
157
|
+
].freeze
|
158
|
+
|
159
|
+
# A filter that should only result in a valid sync token and no other data
|
160
|
+
EMPTY_BOT_FILTER = {
|
161
|
+
account_data: { types: [] },
|
162
|
+
event_fields: [],
|
163
|
+
presence: { types: [] },
|
164
|
+
room: {
|
165
|
+
account_data: { types: [] },
|
166
|
+
ephemeral: { types: [] },
|
167
|
+
state: {
|
168
|
+
types: [],
|
169
|
+
lazy_load_members: true
|
170
|
+
},
|
171
|
+
timeline: {
|
172
|
+
types: []
|
173
|
+
}
|
174
|
+
}
|
175
|
+
}.freeze
|
176
|
+
|
177
|
+
# Reset the bot class, removing any local handlers that have been registered
|
178
|
+
def reset!
|
179
|
+
@handlers = {}
|
180
|
+
@client_handler = nil
|
181
|
+
end
|
182
|
+
|
183
|
+
# Retrieves all registered - including inherited - handlers for the bot
|
184
|
+
#
|
185
|
+
# @param type [:command,:event,:all] Which handler type to return, or :all to return all handlers regardless of type
|
186
|
+
# @return [Array[RequestHandler]] The registered handlers for the bot and parents
|
187
|
+
def all_handlers(type: :command)
|
188
|
+
parent = superclass&.all_handlers(type: type) if superclass.respond_to? :all_handlers
|
189
|
+
(parent || {}).merge(@handlers.select { |_, h| type == :all || h.type == type }).compact
|
190
|
+
end
|
191
|
+
|
192
|
+
# Set a class-wide option for the bot
|
193
|
+
#
|
194
|
+
# @param option [Symbol,Hash] The option/options to set
|
195
|
+
# @param value [Proc,Symbol,Integer,Boolean,Hash,nil] The value to set for the option, should be ignored if option is a Hash
|
196
|
+
# @param ignore_setter [Boolean] Should any existing setter method be ignored during assigning of the option
|
197
|
+
# @yieldreturn The value that the option should return when requested, as an alternative to passing the Proc as value
|
198
|
+
def set(option, value = (not_set = true), ignore_setter = false, &block) # rubocop:disable Style/OptionalBooleanParameter
|
199
|
+
raise ArgumentError if block && !not_set
|
200
|
+
|
201
|
+
if block
|
202
|
+
value = block
|
203
|
+
not_set = false
|
204
|
+
end
|
205
|
+
|
206
|
+
if not_set
|
207
|
+
raise ArgumentError unless option.respond_to?(:each)
|
208
|
+
|
209
|
+
option.each { |k, v| set(k, v) }
|
210
|
+
return self
|
211
|
+
end
|
212
|
+
|
213
|
+
return send("#{option}=", value) if respond_to?("#{option}=") && !ignore_setter
|
214
|
+
|
215
|
+
setter = proc { |val| set option, val, true }
|
216
|
+
getter = proc { value }
|
217
|
+
|
218
|
+
case value
|
219
|
+
when Proc
|
220
|
+
getter = value
|
221
|
+
when Symbol, Integer, FalseClass, TrueClass, NilClass
|
222
|
+
getter = value.inspect
|
223
|
+
when Hash
|
224
|
+
setter = proc do |val|
|
225
|
+
val = value.merge val if val.is_a? Hash
|
226
|
+
set option, val, true
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
define_singleton("#{option}=", setter)
|
231
|
+
define_singleton(option, getter)
|
232
|
+
define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?"
|
233
|
+
self
|
234
|
+
end
|
235
|
+
|
236
|
+
# Same as calling `set :option, true` for each of the given options.
|
237
|
+
#
|
238
|
+
# @param opts [Array[Symbol]] The options to set to true
|
239
|
+
def enable(*opts)
|
240
|
+
opts.each { |key| set(key, true) }
|
241
|
+
end
|
242
|
+
|
243
|
+
# Same as calling `set :option, false` for each of the given options.
|
244
|
+
#
|
245
|
+
# @param opts [Array[Symbol]] The options to set to false
|
246
|
+
def disable(*opts)
|
247
|
+
opts.each { |key| set(key, false) }
|
248
|
+
end
|
249
|
+
|
250
|
+
# Register a bot command
|
251
|
+
#
|
252
|
+
# @note Due to the way blocks are handled, required parameters won't block execution.
|
253
|
+
# If your command requires all parameters to be valid, you will need to check for nil yourself.
|
254
|
+
#
|
255
|
+
# @note Execution will be performed with a MatrixSdk::Bot::Request object as self.
|
256
|
+
# To access the bot instance, use MatrixSdk::Bot::Request#bot
|
257
|
+
#
|
258
|
+
# @param command [String] The command to register, will be routed based on the prefix and bot NameError
|
259
|
+
# @param desc [String] A human-readable description for the command
|
260
|
+
# @param only [Symbol,Proc,Array[Symbol,Proc]] What limitations does this command have?
|
261
|
+
# Can use :DM, :Admin, :Mod
|
262
|
+
# @option params
|
263
|
+
def command(command, desc: nil, notes: nil, only: nil, **params, &block)
|
264
|
+
args = params[:args] || convert_to_lambda(&block).parameters.map do |type, name|
|
265
|
+
case type
|
266
|
+
when :req
|
267
|
+
name.to_s.upcase
|
268
|
+
when :opt
|
269
|
+
"[#{name.to_s.upcase}]"
|
270
|
+
when :rest
|
271
|
+
"[#{name.to_s.upcase}...]"
|
272
|
+
end
|
273
|
+
end.compact.join(' ')
|
274
|
+
|
275
|
+
logger.debug "Registering command #{command} with args #{args}"
|
276
|
+
|
277
|
+
add_handler(
|
278
|
+
command.to_s.downcase,
|
279
|
+
type: :command,
|
280
|
+
args: args,
|
281
|
+
desc: desc,
|
282
|
+
notes: notes,
|
283
|
+
only: [only].flatten.compact,
|
284
|
+
&block
|
285
|
+
)
|
286
|
+
end
|
287
|
+
|
288
|
+
# Register a Matrix event
|
289
|
+
#
|
290
|
+
# @note Currently it's only possible to register one handler per event type
|
291
|
+
#
|
292
|
+
# @param event [String] The ID for the event to register
|
293
|
+
# @param only [Symbol,Proc,Array[Symbol,Proc]] The limitations to when the event should be handled
|
294
|
+
# @option params
|
295
|
+
def event(event, only: nil, **_params, &block)
|
296
|
+
logger.debug "Registering event #{event}"
|
297
|
+
|
298
|
+
add_handler(
|
299
|
+
event.to_s,
|
300
|
+
type: :event,
|
301
|
+
only: [only].flatten.compact,
|
302
|
+
&block
|
303
|
+
)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Registers a block to be run when configuring the client, before starting the sync
|
307
|
+
def client(&block)
|
308
|
+
@client_handler = block
|
309
|
+
end
|
310
|
+
|
311
|
+
# Check if a command is registered
|
312
|
+
#
|
313
|
+
# @param command [String] The command to check
|
314
|
+
# @param ignore_inherited [Booleen] Should the check ignore any inherited commands and only check local registrations
|
315
|
+
def command?(command, ignore_inherited: false)
|
316
|
+
return @handlers[command.to_s.downcase]&.command? if ignore_inherited
|
317
|
+
|
318
|
+
all_handlers[command.to_s.downcase]&.command? || false
|
319
|
+
end
|
320
|
+
|
321
|
+
# Check if an event is registered
|
322
|
+
#
|
323
|
+
# @param event [String] The event type to check
|
324
|
+
# @param ignore_inherited [Booleen] Should the check ignore any inherited events and only check local registrations
|
325
|
+
def event?(event, ignore_inherited: false)
|
326
|
+
return @handlers[event]&.event? if ignore_inherited
|
327
|
+
|
328
|
+
all_handlers(type: :event)[event]&.event? || false
|
329
|
+
end
|
330
|
+
|
331
|
+
# Retrieves the RequestHandler for a given command
|
332
|
+
#
|
333
|
+
# @param command [String] The command to retrieve
|
334
|
+
# @param ignore_inherited [Booleen] Should the retrieval ignore any inherited commands and only check local registrations
|
335
|
+
# @return [RequestHandler,nil] The registered handler for the command if any
|
336
|
+
def get_command(command, ignore_inherited: false)
|
337
|
+
if ignore_inherited && @handlers[command]&.command?
|
338
|
+
@handlers[command]
|
339
|
+
elsif !ignore_inherited && all_handlers[command]&.command?
|
340
|
+
all_handlers[command]
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# Retrieves the RequestHandler for a given event
|
345
|
+
#
|
346
|
+
# @param event [String] The event type to retrieve
|
347
|
+
# @param ignore_inherited [Booleen] Should the retrieval ignore any inherited events and only check local registrations
|
348
|
+
# @return [RequestHandler,nil] The registered handler for the event if any
|
349
|
+
def get_event(event, ignore_inherited: false)
|
350
|
+
if ignore_inherited && @handlers[event]&.event?
|
351
|
+
@handlers[event]
|
352
|
+
elsif !ignore_inherited && all_handlers(type: :event)[event]&.event?
|
353
|
+
all_handlers(type: :event)[event]
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
# Removes a registered command from the bot
|
358
|
+
#
|
359
|
+
# @note This will only affect local commands, not ones inherited
|
360
|
+
# @param command [String] The command to remove
|
361
|
+
def remove_command(command)
|
362
|
+
return false unless @handlers[command]&.command?
|
363
|
+
|
364
|
+
@handers.delete command
|
365
|
+
true
|
366
|
+
end
|
367
|
+
|
368
|
+
# Removes a registered event from the bot
|
369
|
+
#
|
370
|
+
# @note This will only affect local event, not ones inherited
|
371
|
+
# @param event [String] The event to remove
|
372
|
+
def remove_event(event)
|
373
|
+
return false unless @handlers[event]&.event?
|
374
|
+
|
375
|
+
@handers.delete event
|
376
|
+
true
|
377
|
+
end
|
378
|
+
|
379
|
+
# Stops any running instance of the bot
|
380
|
+
def quit!
|
381
|
+
return unless running?
|
382
|
+
|
383
|
+
active_bot.logger.info "Stopping #{settings.bot_name}..."
|
384
|
+
|
385
|
+
if settings.store_sync_token
|
386
|
+
begin
|
387
|
+
active_bot.client.api.set_account_data(
|
388
|
+
active_bot.client.mxid, "dev.ananace.ruby-sdk.#{settings.bot_name}",
|
389
|
+
{ sync_token: active_bot.client.sync_token }
|
390
|
+
)
|
391
|
+
rescue StandardError => e
|
392
|
+
active_bot.logger.error "Failed to save sync token, #{e.class}: #{e}"
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
active_bot.client.logout if login?
|
397
|
+
|
398
|
+
active_bot.client.api.stop_inflight
|
399
|
+
active_bot.client.stop_listener_thread
|
400
|
+
|
401
|
+
set :active_bot, nil
|
402
|
+
end
|
403
|
+
|
404
|
+
# Starts the bot up
|
405
|
+
#
|
406
|
+
# @param options [Hash] Settings to apply using Base.set
|
407
|
+
def run!(options = {}, &block)
|
408
|
+
return if running?
|
409
|
+
|
410
|
+
set options
|
411
|
+
|
412
|
+
bot_settings = settings.respond_to?(:bot_settings) ? settings.bot_settings : {}
|
413
|
+
bot_settings.merge!(
|
414
|
+
threadsafe: settings.threadsafe,
|
415
|
+
client_cache: settings.client_cache,
|
416
|
+
sync_filter: settings.sync_filter
|
417
|
+
)
|
418
|
+
|
419
|
+
bot_settings[:auth] = if settings.access_token?
|
420
|
+
{ access_token: settings.access_token }
|
421
|
+
else
|
422
|
+
{ username: settings.username, password: settings.password }
|
423
|
+
end
|
424
|
+
|
425
|
+
begin
|
426
|
+
start_bot(bot_settings, &block)
|
427
|
+
ensure
|
428
|
+
quit!
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
# Check whether the self-hosted server is running or not.
|
433
|
+
def running?
|
434
|
+
active_bot?
|
435
|
+
end
|
436
|
+
|
437
|
+
private
|
438
|
+
|
439
|
+
def add_handler(command, type:, **data, &block)
|
440
|
+
@handlers[command] = RequestHandler.new command.to_s.downcase, type, block, data.compact
|
441
|
+
end
|
442
|
+
|
443
|
+
def start_bot(bot_settings, &block)
|
444
|
+
cl = if homeserver =~ %r{^https?://}
|
445
|
+
MatrixSdk::Client.new homeserver
|
446
|
+
else
|
447
|
+
MatrixSdk::Client.new_for_domain homeserver
|
448
|
+
end
|
449
|
+
|
450
|
+
auth = bot_settings.delete :auth
|
451
|
+
bot = new cl, **bot_settings
|
452
|
+
bot.logger.level = settings.log_level
|
453
|
+
bot.logger.info "Starting #{settings.bot_name}..."
|
454
|
+
|
455
|
+
if settings.login?
|
456
|
+
bot.client.login auth[:username], auth[:password], no_sync: true
|
457
|
+
else
|
458
|
+
bot.client.access_token = auth[:access_token]
|
459
|
+
end
|
460
|
+
|
461
|
+
set :active_bot, bot
|
462
|
+
|
463
|
+
if @client_handler
|
464
|
+
case @client_handler.arity
|
465
|
+
when 0
|
466
|
+
bot.client.instance_exec(&@client_handler)
|
467
|
+
else
|
468
|
+
@client_handler.call(bot.client)
|
469
|
+
end
|
470
|
+
end
|
471
|
+
block&.call bot
|
472
|
+
|
473
|
+
if settings.sync_token?
|
474
|
+
bot.client.instance_variable_set(:@next_batch, settings.sync_token)
|
475
|
+
elsif settings.store_sync_token?
|
476
|
+
begin
|
477
|
+
data = bot.client.api.get_account_data(bot.client.mxid, "dev.ananace.ruby-sdk.#{bot_name}")
|
478
|
+
bot.client.sync_token = data[:sync_token]
|
479
|
+
rescue MatrixSdk::MatrixNotFoundError
|
480
|
+
# Valid
|
481
|
+
rescue StandardError => e
|
482
|
+
bot.logger.error "Failed to restore old sync token, #{e.class}: #{e}"
|
483
|
+
end
|
484
|
+
else
|
485
|
+
bot.client.sync(filter: EMPTY_BOT_FILTER)
|
486
|
+
end
|
487
|
+
|
488
|
+
bot.client.start_listener_thread
|
489
|
+
|
490
|
+
bot.client.instance_variable_get(:@sync_thread).join
|
491
|
+
rescue Interrupt
|
492
|
+
# Happens when killed
|
493
|
+
rescue StandardError => e
|
494
|
+
logger.fatal "Failed to start #{settings.bot_name} - #{e.class}: #{e}"
|
495
|
+
raise
|
496
|
+
end
|
497
|
+
|
498
|
+
def define_singleton(name, content = Proc.new)
|
499
|
+
singleton_class.class_eval do
|
500
|
+
undef_method(name) if method_defined? name
|
501
|
+
content.is_a?(String) ? class_eval("def #{name}() #{content}; end", __FILE__, __LINE__) : define_method(name, &content)
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
# Helper to convert a proc to a non-callable lambda
|
506
|
+
#
|
507
|
+
# This method is only used to get a correct parameter list, the resulting lambda is invalid and can't be used to actually execute a call
|
508
|
+
def convert_to_lambda(this: nil, &block)
|
509
|
+
return block if block.lambda?
|
510
|
+
|
511
|
+
this ||= Object.new
|
512
|
+
this.define_singleton_method(:_, &block)
|
513
|
+
this.method(:_).to_proc
|
514
|
+
end
|
515
|
+
|
516
|
+
def cleaned_caller(keep = 3)
|
517
|
+
caller(1)
|
518
|
+
.map! { |line| line.split(/:(?=\d|in )/, 3)[0, keep] }
|
519
|
+
.reject { |file, *_| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
|
520
|
+
end
|
521
|
+
|
522
|
+
def caller_files
|
523
|
+
cleaned_caller(1).flatten
|
524
|
+
end
|
525
|
+
|
526
|
+
def inherited(subclass)
|
527
|
+
subclass.reset!
|
528
|
+
subclass.set :app_file, caller_files.first unless subclass.app_file?
|
529
|
+
super
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
def command_allowed?(command, event)
|
534
|
+
pre_event = @event
|
535
|
+
|
536
|
+
return false unless command? command
|
537
|
+
|
538
|
+
handler = get_command(command)
|
539
|
+
return true if (handler.data[:only] || []).empty?
|
540
|
+
|
541
|
+
# Avoid modifying input data for a checking method
|
542
|
+
@event = MatrixSdk::Response.new(client.api, event.dup)
|
543
|
+
return false if [handler.data[:only]].flatten.compact.any? do |only|
|
544
|
+
if only.is_a? Proc
|
545
|
+
!instance_exec(&only)
|
546
|
+
else
|
547
|
+
case only.to_s.downcase.to_sym
|
548
|
+
when :dm
|
549
|
+
!room.dm?(members_only: true)
|
550
|
+
when :admin
|
551
|
+
!sender_admin?
|
552
|
+
when :mod
|
553
|
+
!sender_moderator?
|
554
|
+
end
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
true
|
559
|
+
ensure
|
560
|
+
@event = pre_event
|
561
|
+
end
|
562
|
+
|
563
|
+
def event_allowed?(event)
|
564
|
+
pre_event = @event
|
565
|
+
|
566
|
+
return false unless event? event[:type]
|
567
|
+
|
568
|
+
handler = get_event(event[:type])
|
569
|
+
return true if (handler.data[:only] || []).empty?
|
570
|
+
|
571
|
+
# Avoid modifying input data for a checking method
|
572
|
+
@event = MatrixSdk::Response.new(client.api, event.dup)
|
573
|
+
return false if [handler.data[:only]].flatten.compact.any? do |only|
|
574
|
+
if only.is_a? Proc
|
575
|
+
instance_exec(&only)
|
576
|
+
else
|
577
|
+
case only.to_s.downcase.to_sym
|
578
|
+
when :dm
|
579
|
+
!room.dm?(members_only: true)
|
580
|
+
when :admin
|
581
|
+
!sender_admin?
|
582
|
+
when :mod
|
583
|
+
!sender_moderator?
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
true
|
589
|
+
ensure
|
590
|
+
@event = pre_event
|
591
|
+
end
|
592
|
+
|
593
|
+
#
|
594
|
+
# Helpers for handling events
|
595
|
+
#
|
596
|
+
|
597
|
+
def in_event?
|
598
|
+
!@event.nil?
|
599
|
+
end
|
600
|
+
|
601
|
+
def bot
|
602
|
+
self
|
603
|
+
end
|
604
|
+
|
605
|
+
def room
|
606
|
+
client.ensure_room(event[:room_id]) if in_event?
|
607
|
+
end
|
608
|
+
|
609
|
+
def sender
|
610
|
+
client.get_user(event[:sender]) if in_event?
|
611
|
+
end
|
612
|
+
|
613
|
+
# Helpers for checking power levels
|
614
|
+
def sender_admin?
|
615
|
+
sender&.admin? room
|
616
|
+
end
|
617
|
+
|
618
|
+
def sender_moderator?
|
619
|
+
sender&.moderator? room
|
620
|
+
end
|
621
|
+
|
622
|
+
#
|
623
|
+
# Helpers
|
624
|
+
#
|
625
|
+
|
626
|
+
def expanded_prefix
|
627
|
+
return "#{settings.command_prefix}#{settings.bot_name} " if settings.bot_name?
|
628
|
+
|
629
|
+
settings.command_prefix
|
630
|
+
end
|
631
|
+
|
632
|
+
private
|
633
|
+
|
634
|
+
#
|
635
|
+
# Event handling
|
636
|
+
#
|
637
|
+
|
638
|
+
# TODO: Add handling results - Ok, NoSuchCommand, NotAllowed, etc
|
639
|
+
def _handle_event(event)
|
640
|
+
return if in_event?
|
641
|
+
return if settings.ignore_own? && client.mxid == event[:sender]
|
642
|
+
|
643
|
+
event = event.data if event.is_a? MatrixSdk::MatrixEvent
|
644
|
+
|
645
|
+
logger.debug "Received event #{event}"
|
646
|
+
return _handle_message(event) if event[:type] == 'm.room.message'
|
647
|
+
return unless event?(event[:type])
|
648
|
+
|
649
|
+
handler = get_event(event[:type])
|
650
|
+
return unless event_allowed? event
|
651
|
+
|
652
|
+
logger.info "Handling event #{event[:sender]}/#{event[:room_id]} => #{event[:type]}"
|
653
|
+
|
654
|
+
@event = MatrixSdk::Response.new(client.api, event)
|
655
|
+
instance_exec(&handler.proc)
|
656
|
+
# Argument errors are likely to be a "friendly" error, so don't direct the user to the log
|
657
|
+
rescue ArgumentError => e
|
658
|
+
logger.error "#{e.class} when handling #{event[:type]}: #{e}\n#{e.backtrace[0, 10].join("\n")}"
|
659
|
+
room.send_notice("Failed to handle event of type #{event[:type]} - #{e}.")
|
660
|
+
rescue StandardError => e
|
661
|
+
puts e, e.backtrace if settings.respond_to?(:testing?) && settings.testing?
|
662
|
+
logger.error "#{e.class} when handling #{event[:type]}: #{e}\n#{e.backtrace[0, 10].join("\n")}"
|
663
|
+
room.send_notice("Failed to handle event of type #{event[:type]} - #{e}.\nMore information is available in the bot logs")
|
664
|
+
ensure
|
665
|
+
@event = nil
|
666
|
+
end
|
667
|
+
|
668
|
+
def _handle_message(event)
|
669
|
+
return if in_event?
|
670
|
+
return if settings.ignore_own? && client.mxid == event[:sender]
|
671
|
+
|
672
|
+
type = event[:content][:msgtype]
|
673
|
+
return unless settings.allowed_types.include? type
|
674
|
+
|
675
|
+
message = event[:content][:body].dup
|
676
|
+
|
677
|
+
room = client.ensure_room(event[:room_id])
|
678
|
+
if room.dm?(members_only: true)
|
679
|
+
unless message.start_with? settings.command_prefix
|
680
|
+
prefix = expanded_prefix || settings.command_prefix
|
681
|
+
message.prepend prefix unless message.start_with? prefix
|
682
|
+
end
|
683
|
+
else
|
684
|
+
return if settings.require_fullname? && !message.start_with?(expanded_prefix)
|
685
|
+
return unless message.start_with? settings.command_prefix
|
686
|
+
end
|
687
|
+
|
688
|
+
if message.start_with?(expanded_prefix)
|
689
|
+
message.sub!(expanded_prefix, '')
|
690
|
+
else
|
691
|
+
message.sub!(settings.command_prefix, '')
|
692
|
+
end
|
693
|
+
|
694
|
+
parts = message.shellsplit
|
695
|
+
command = parts.shift.downcase
|
696
|
+
|
697
|
+
message.sub!(command, '')
|
698
|
+
message.lstrip!
|
699
|
+
|
700
|
+
handler = get_command(command)
|
701
|
+
return unless handler
|
702
|
+
return unless command_allowed?(command, event)
|
703
|
+
|
704
|
+
logger.info "Handling command #{event[:sender]}/#{event[:room_id]}: #{settings.command_prefix}#{command}"
|
705
|
+
|
706
|
+
@event = MatrixSdk::Response.new(client.api, event)
|
707
|
+
arity = handler.arity
|
708
|
+
case arity
|
709
|
+
when 0
|
710
|
+
instance_exec(&handler.proc)
|
711
|
+
when 1
|
712
|
+
message = message.sub("#{settings.command_prefix}#{command}", '').lstrip
|
713
|
+
message = nil if message.empty?
|
714
|
+
|
715
|
+
# TODO: What's the most correct way to handle messages with quotes?
|
716
|
+
# XXX Currently all quotes are kept
|
717
|
+
|
718
|
+
instance_exec(message, &handler.proc)
|
719
|
+
else
|
720
|
+
instance_exec(*parts, &handler.proc)
|
721
|
+
end
|
722
|
+
# Argument errors are likely to be a "friendly" error, so don't direct the user to the log
|
723
|
+
rescue ArgumentError => e
|
724
|
+
logger.error "#{e.class} when handling #{settings.command_prefix}#{command}: #{e}\n#{e.backtrace[0, 10].join("\n")}"
|
725
|
+
room.send_notice("Failed to handle #{command} - #{e}.")
|
726
|
+
rescue StandardError => e
|
727
|
+
puts e, e.backtrace if settings.respond_to?(:testing?) && settings.testing?
|
728
|
+
logger.error "#{e.class} when handling #{settings.command_prefix}#{command}: #{e}\n#{e.backtrace[0, 10].join("\n")}"
|
729
|
+
room.send_notice("Failed to handle #{command} - #{e}.\nMore information is available in the bot logs")
|
730
|
+
ensure
|
731
|
+
@event = nil
|
732
|
+
end
|
733
|
+
|
734
|
+
#
|
735
|
+
# Default configuration
|
736
|
+
#
|
737
|
+
|
738
|
+
reset!
|
739
|
+
|
740
|
+
## Bot configuration
|
741
|
+
# Should the bot automatically accept invites
|
742
|
+
set :accept_invites, true
|
743
|
+
# What character should commands be prefixed with
|
744
|
+
set :command_prefix, '!'
|
745
|
+
# What's the name of the bot - used for non 1:1 rooms and sync-token storage
|
746
|
+
set(:bot_name) { File.basename $PROGRAM_NAME, '.*' }
|
747
|
+
# Which msgtypes should the bot listen for when handling commands
|
748
|
+
set :allowed_types, %w[m.text]
|
749
|
+
# Should the bot ignore its own events
|
750
|
+
set :ignore_own, true
|
751
|
+
# Should the bot require full-name commands in non-DM rooms?
|
752
|
+
set :require_fullname, false
|
753
|
+
# Sets a text to display before the usage information in the built-in help command
|
754
|
+
set :help_preamble, nil
|
755
|
+
|
756
|
+
## Sync token handling
|
757
|
+
# Token specified by the user
|
758
|
+
set :sync_token, nil
|
759
|
+
# Token automatically stored in an account_data key
|
760
|
+
set :store_sync_token, false
|
761
|
+
|
762
|
+
# Homeserver, either domain or URL
|
763
|
+
set :homeserver, 'matrix.org'
|
764
|
+
# Which level of thread safety should be used
|
765
|
+
set :threadsafe, :multithread
|
766
|
+
|
767
|
+
## User authorization
|
768
|
+
# Existing access token
|
769
|
+
set :access_token, nil
|
770
|
+
# Username for a per-instance login
|
771
|
+
set :username, nil
|
772
|
+
# Password for a per-instance login
|
773
|
+
set :password, nil
|
774
|
+
|
775
|
+
# Helper to check if a login is requested
|
776
|
+
set(:login) { username? && password? }
|
777
|
+
|
778
|
+
## Client abstraction configuration
|
779
|
+
# What level of caching is wanted - most bots won't need full client-level caches
|
780
|
+
set :client_cache, :some
|
781
|
+
# The default sync filter, should be modified to limit to what the bot uses
|
782
|
+
set :sync_filter, {
|
783
|
+
room: {
|
784
|
+
timeline: {
|
785
|
+
limit: 20
|
786
|
+
},
|
787
|
+
state: {
|
788
|
+
lazy_load_members: true
|
789
|
+
}
|
790
|
+
}
|
791
|
+
}
|
792
|
+
|
793
|
+
## Logging configuration
|
794
|
+
# Should logging be enabled? (Will always log fatal errors)
|
795
|
+
set :logging, false
|
796
|
+
# What level of logging should the bot use
|
797
|
+
set :log_level, :info
|
798
|
+
|
799
|
+
## Internal configuration values
|
800
|
+
set :app_file, nil
|
801
|
+
set :active_bot, nil
|
802
|
+
|
803
|
+
#
|
804
|
+
# Default commands
|
805
|
+
#
|
806
|
+
|
807
|
+
# Displays an usage information text, listing all available commands as well as their arguments
|
808
|
+
command(
|
809
|
+
:help,
|
810
|
+
desc: 'Shows this help text',
|
811
|
+
notes: <<~NOTES
|
812
|
+
For commands that take multiple arguments, you will need to use quotes around spaces
|
813
|
+
E.g. !login "my username" "this is not a real password"
|
814
|
+
NOTES
|
815
|
+
) do |command = nil|
|
816
|
+
logger.debug "Handling request for built-in help for #{sender}" if command.nil?
|
817
|
+
logger.debug "Handling request for built-in help for #{sender} on #{command.inspect}" unless command.nil?
|
818
|
+
|
819
|
+
commands = self.class.all_handlers
|
820
|
+
commands.select! { |c, _| c.include? command } if command
|
821
|
+
commands.select! { |c, _| command_allowed? c, event }
|
822
|
+
|
823
|
+
commands = commands.map do |_cmd, handler|
|
824
|
+
info = handler.data[:args]
|
825
|
+
info += " - #{handler.data[:desc]}" if handler.data[:desc]
|
826
|
+
info += "\n #{handler.data[:notes].split("\n").join("\n ")}" if !command.nil? && handler.data[:notes]
|
827
|
+
info = nil if info.empty?
|
828
|
+
|
829
|
+
[
|
830
|
+
room.dm? ? "#{settings.command_prefix}#{handler.command}" : "#{expanded_prefix}#{handler.command}",
|
831
|
+
info
|
832
|
+
].compact
|
833
|
+
end
|
834
|
+
|
835
|
+
commands = commands.map { |*args| args.join(' ') }.join("\n")
|
836
|
+
if command
|
837
|
+
if commands.empty?
|
838
|
+
room.send_notice("No information available on #{command}")
|
839
|
+
else
|
840
|
+
room.send_notice("Help for #{command};\n#{commands}")
|
841
|
+
end
|
842
|
+
else
|
843
|
+
room.send_notice("#{settings.help_preamble? ? "#{settings.help_preamble}\n\n" : ''}Usage:\n\n#{commands}")
|
844
|
+
end
|
845
|
+
end
|
846
|
+
end
|
847
|
+
end
|