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.
@@ -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