matrix_sdk 2.5.0 → 2.8.0

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