matrix_sdk 2.4.0 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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