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