Skip to content
Snippets Groups Projects
Verified Commit f3cb20e4 authored by Alexander Olofsson's avatar Alexander Olofsson
Browse files

bot: Add support for handling arbitrary events

parent b165170c
Branches
Tags
No related merge requests found
# frozen_string_literal: true # frozen_string_literal: true
require 'matrix_sdk/bot/request'
require 'shellwords' require 'shellwords'
module MatrixSdk::Bot module MatrixSdk::Bot
class Base class Base
extend MatrixSdk::Extensions extend MatrixSdk::Extensions
RequestHandler = Struct.new('RequestHandler', :command, :proc, :data) do RequestHandler = Struct.new('RequestHandler', :command, :type, :proc, :data) do
def command?
type == :command
end
def event?
type == :event
end
def arity def arity
arity = self.proc.parameters.count { |t, _| %i[opt req].include? t } arity = self.proc.parameters.count { |t, _| %i[opt req].include? t }
arity = -arity if self.proc.parameters.any? { |t, _| t.to_s.include? 'rest' } arity = -arity if self.proc.parameters.any? { |t, _| t.to_s.include? 'rest' }
...@@ -32,7 +39,7 @@ module MatrixSdk::Bot ...@@ -32,7 +39,7 @@ module MatrixSdk::Bot
MatrixSdk::Client.new_for_domain hs_url, **params MatrixSdk::Client.new_for_domain hs_url, **params
end end
@client.on_event.add_handler('m.room.message') { |ev| _handle_event(ev) } @client.on_event.add_handler { |ev| _handle_event(ev) }
@client.on_invite_event.add_handler { |ev| client.join_room(ev[:room_id]) if settings.accept_invites? } @client.on_invite_event.add_handler { |ev| client.join_room(ev[:room_id]) if settings.accept_invites? }
end end
...@@ -59,6 +66,14 @@ module MatrixSdk::Bot ...@@ -59,6 +66,14 @@ module MatrixSdk::Bot
self.class.command(command, **params, &block) self.class.command(command, **params, &block)
end end
# Register an event during runtime
#
# @param event [String] The event to register
# @see Base.event for full parameter information
def register_event(event, **params, &block)
self.class.event(event, **params, &block)
end
# Removes a registered command during runtime # Removes a registered command during runtime
# #
# @param command [String] The command to remove # @param command [String] The command to remove
...@@ -67,6 +82,14 @@ module MatrixSdk::Bot ...@@ -67,6 +82,14 @@ module MatrixSdk::Bot
self.class.remove_command(command) self.class.remove_command(command)
end end
# Removes a registered event during runtime
#
# @param event [String] The event to remove
# @see Base.remove_event
def unregister_event(command)
self.class.remove_event(command)
end
# Gets the handler for a command # Gets the handler for a command
# #
# @param command [String] The command to retrieve # @param command [String] The command to retrieve
...@@ -76,6 +99,15 @@ module MatrixSdk::Bot ...@@ -76,6 +99,15 @@ module MatrixSdk::Bot
self.class.get_command(command, **params) self.class.get_command(command, **params)
end end
# Gets the handler for an event
#
# @param event [String] The event to retrieve
# @return [RequestHandler] The registered event handler
# @see Base.get_event
def get_event(event, **params)
self.class.get_event(event, **params)
end
# Checks for the existence of a command # Checks for the existence of a command
# #
# @param command [String] The command to check # @param command [String] The command to check
...@@ -84,6 +116,14 @@ module MatrixSdk::Bot ...@@ -84,6 +116,14 @@ module MatrixSdk::Bot
self.class.command?(command, **params) self.class.command?(command, **params)
end end
# Checks for the existence of a handled event
#
# @param event [String] The event to check
# @see Base.event?
def event?(event, **params)
self.class.event?(event, **params)
end
# Access settings defined with Base.set # Access settings defined with Base.set
def settings def settings
self.class.settings self.class.settings
...@@ -128,9 +168,9 @@ module MatrixSdk::Bot ...@@ -128,9 +168,9 @@ module MatrixSdk::Bot
@client_handler = nil @client_handler = nil
end end
def all_handlers def all_handlers(type: :command)
parent = superclass&.all_handlers if superclass.respond_to? :all_handlers parent = superclass&.all_handlers(type: type) if superclass.respond_to? :all_handlers
(parent || {}).merge(@handlers).compact (parent || {}).merge(@handlers.select { |_, h| h.type == type }).compact
end end
def set(option, value = (not_set = true), ignore_setter = false, &block) # rubocop:disable Style/OptionalBooleanParameter def set(option, value = (not_set = true), ignore_setter = false, &block) # rubocop:disable Style/OptionalBooleanParameter
...@@ -181,10 +221,6 @@ module MatrixSdk::Bot ...@@ -181,10 +221,6 @@ module MatrixSdk::Bot
opts.each { |key| set(key, false) } opts.each { |key| set(key, false) }
end end
def add_handler(command, **data, &block)
@handlers[command] = RequestHandler.new command.to_s.downcase, block, data.compact
end
# Register a bot command # Register a bot command
# #
# @note Due to the way blocks are handled, required parameters won't block execution. # @note Due to the way blocks are handled, required parameters won't block execution.
...@@ -214,6 +250,7 @@ module MatrixSdk::Bot ...@@ -214,6 +250,7 @@ module MatrixSdk::Bot
add_handler( add_handler(
command.to_s.downcase, command.to_s.downcase,
type: :command,
args: args, args: args,
desc: desc, desc: desc,
notes: notes, notes: notes,
...@@ -222,33 +259,70 @@ module MatrixSdk::Bot ...@@ -222,33 +259,70 @@ module MatrixSdk::Bot
) )
end end
def event(event, only: nil, **_params, &block)
logger.debug "Registering event #{event}"
add_handler(
event.to_s,
type: :event,
only: [only].flatten.compact,
&block
)
end
# Registers a block to be run when configuring the client, before starting the sync
def client(&block)
@client_handler = block
end
def command?(command, ignore_inherited: false) def command?(command, ignore_inherited: false)
return @handlers.key? command if ignore_inherited return @handlers[command]&.command? if ignore_inherited
all_handlers.key? command all_handlers[command]&.command? || false
end
def event?(event, ignore_inherited: false)
return @handlers[event]&.event? if ignore_inherited
all_handlers(type: :event)[event]&.event? || false
end end
def get_command(command, ignore_inherited: false) def get_command(command, ignore_inherited: false)
if ignore_inherited if ignore_inherited && @handlers[command]&.command?
@handlers[command] @handlers[command]
else elsif !ignore_inherited && all_handlers[command]&.command?
all_handlers[command] all_handlers[command]
end end
end end
def get_event(event, ignore_inherited: false)
if ignore_inherited && @handlers[event]&.event?
@handlers[event]
elsif !ignore_inherited && all_handlers(type: :event)[event]&.event?
all_handlers(type: :event)[event]
end
end
# Removes a registered command from the bot # Removes a registered command from the bot
# #
# @note This will only affect local commands, not ones inherited # @note This will only affect local commands, not ones inherited
# @param command [String] The command to remove # @param command [String] The command to remove
def remove_command(command) def remove_command(command)
return false unless @handlers.key? command return false unless @handlers[command]&.command?
@handers.delete command @handers.delete command
true true
end end
def client(&block) # Removes a registered event from the bot
@client_handler = block #
# @note This will only affect local event, not ones inherited
# @param event [String] The event to remove
def remove_event(event)
return false unless @handlers[event]&.event?
@handers.delete event
true
end end
# Stops any running instance of the bot # Stops any running instance of the bot
...@@ -311,6 +385,10 @@ module MatrixSdk::Bot ...@@ -311,6 +385,10 @@ module MatrixSdk::Bot
private private
def add_handler(command, type:, **data, &block)
@handlers[command] = RequestHandler.new command.to_s.downcase, type, block, data.compact
end
def start_bot(bot_settings, &block) def start_bot(bot_settings, &block)
cl = if homeserver =~ %r{^https?://} cl = if homeserver =~ %r{^https?://}
MatrixSdk::Client.new homeserver MatrixSdk::Client.new homeserver
...@@ -331,8 +409,8 @@ module MatrixSdk::Bot ...@@ -331,8 +409,8 @@ module MatrixSdk::Bot
set :active_bot, bot set :active_bot, bot
block&.call bot
@client_handler&.call bot.client @client_handler&.call bot.client
block&.call bot
if settings.sync_token? if settings.sync_token?
bot.client.instance_variable_set(:@next_batch, settings.sync_token) bot.client.instance_variable_set(:@next_batch, settings.sync_token)
...@@ -424,6 +502,36 @@ module MatrixSdk::Bot ...@@ -424,6 +502,36 @@ module MatrixSdk::Bot
@event = pre_event @event = pre_event
end end
def event_allowed?(event)
pre_event = @event
return false unless event? event[:type]
handler = get_event(event[:type])
return true if (handler.data[:only] || []).empty?
# Avoid modifying input data for a checking method
@event = MatrixSdk::Response.new(client.api, event.dup)
return false if [handler.data[:only]].flatten.compact.any? do |only|
if only.is_a? Proc
instance_exec(&only)
else
case only.to_s.downcase.to_sym
when :dm
!room.dm?(members_only: true)
when :admin
!sender_admin?
when :mod
!sender_moderator?
end
end
end
true
ensure
@event = pre_event
end
# #
# Helpers for handling events # Helpers for handling events
# #
...@@ -475,6 +583,31 @@ module MatrixSdk::Bot ...@@ -475,6 +583,31 @@ module MatrixSdk::Bot
return if settings.ignore_own? && client.mxid == event[:sender] return if settings.ignore_own? && client.mxid == event[:sender]
logger.debug "Received event #{event}" logger.debug "Received event #{event}"
return _handle_message(event) if event[:type] == 'm.room.message'
return unless event?(event[:type])
handler = get_event(event[:type])
return unless event_allowed? event
@event = MatrixSdk::Response.new(client.api, event)
logger.debug "Handling command #{handler.command}"
instance_exec(&handler.proc)
# Argument errors are likely to be a "friendly" error, so don't direct the user to the log
rescue ArgumentError => e
logger.error "#{e.class} when handling #{event[:type]}: #{e}\n#{e.backtrace[0, 10].join("\n")}"
room.send_notice("Failed to handle event of type #{event[:type]} - #{e}.")
rescue StandardError => e
puts e, e.backtrace if settings.respond_to?(:testing?) && settings.testing?
logger.error "#{e.class} when handling #{event[:type]}: #{e}\n#{e.backtrace[0, 10].join("\n")}"
room.send_notice("Failed to handle event of type #{event[:type]} - #{e}.\nMore information is available in the bot logs")
ensure
@event = nil
end
def _handle_message(event)
return if in_event?
return if settings.ignore_own? && client.mxid == event[:sender]
type = event[:content][:msgtype] type = event[:content][:msgtype]
return unless settings.allowed_types.include? type return unless settings.allowed_types.include? type
......
...@@ -21,6 +21,10 @@ class BotTest < Test::Unit::TestCase ...@@ -21,6 +21,10 @@ class BotTest < Test::Unit::TestCase
command :test_event do command :test_event do
bot.send :test_event, event.event_id bot.send :test_event, event.event_id
end end
event 'dev.ananace.ruby-sdk.TestEvent' do
bot.send :test_state_event
end
end end
def setup def setup
...@@ -76,6 +80,7 @@ class BotTest < Test::Unit::TestCase ...@@ -76,6 +80,7 @@ class BotTest < Test::Unit::TestCase
@room.stubs(:dm?).returns(false) @room.stubs(:dm?).returns(false)
ev = { ev = {
type: 'm.room.message',
sender: '@bob:example.com', sender: '@bob:example.com',
room_id: @id, room_id: @id,
content: { content: {
...@@ -88,6 +93,7 @@ class BotTest < Test::Unit::TestCase ...@@ -88,6 +93,7 @@ class BotTest < Test::Unit::TestCase
@bot.send :_handle_event, ev @bot.send :_handle_event, ev
ev = { ev = {
type: 'm.room.message',
sender: '@bob:example.com', sender: '@bob:example.com',
room_id: @id, room_id: @id,
content: { content: {
...@@ -103,6 +109,7 @@ class BotTest < Test::Unit::TestCase ...@@ -103,6 +109,7 @@ class BotTest < Test::Unit::TestCase
@bot.expects(:test_arr_executed).never @bot.expects(:test_arr_executed).never
ev = { ev = {
type: 'm.room.message',
sender: '@bob:example.com', sender: '@bob:example.com',
room_id: @id, room_id: @id,
content: { content: {
...@@ -118,6 +125,7 @@ class BotTest < Test::Unit::TestCase ...@@ -118,6 +125,7 @@ class BotTest < Test::Unit::TestCase
@bot.expects(:test_arr_executed).once @bot.expects(:test_arr_executed).once
ev = { ev = {
type: 'm.room.message',
sender: '@bob:example.com', sender: '@bob:example.com',
room_id: @id, room_id: @id,
content: { content: {
...@@ -130,6 +138,7 @@ class BotTest < Test::Unit::TestCase ...@@ -130,6 +138,7 @@ class BotTest < Test::Unit::TestCase
@bot.send :_handle_event, ev @bot.send :_handle_event, ev
ev = { ev = {
type: 'm.room.message',
sender: '@bob:example.com', sender: '@bob:example.com',
room_id: @id, room_id: @id,
content: { content: {
...@@ -155,6 +164,7 @@ class BotTest < Test::Unit::TestCase ...@@ -155,6 +164,7 @@ class BotTest < Test::Unit::TestCase
@bot.expects(:test_executed).never @bot.expects(:test_executed).never
ev = { ev = {
type: 'm.room.message',
sender: '@alice:example.com', sender: '@alice:example.com',
room_id: @id, room_id: @id,
content: { content: {
...@@ -176,6 +186,7 @@ class BotTest < Test::Unit::TestCase ...@@ -176,6 +186,7 @@ class BotTest < Test::Unit::TestCase
@bot.expects(:test_event).with('$someevent').once @bot.expects(:test_event).with('$someevent').once
ev = { ev = {
type: 'm.room.message',
sender: '@bob:example.com', sender: '@bob:example.com',
room_id: @id, room_id: @id,
event_id: '$someevent', event_id: '$someevent',
...@@ -187,6 +198,20 @@ class BotTest < Test::Unit::TestCase ...@@ -187,6 +198,20 @@ class BotTest < Test::Unit::TestCase
assert @bot.command_allowed? 'test_event', ev assert @bot.command_allowed? 'test_event', ev
@bot.send :_handle_event, ev @bot.send :_handle_event, ev
@bot.expects(:test_state_event).once
ev = {
type: 'dev.ananace.ruby-sdk.TestEvent',
sender: '@bob:example.com',
room_id: @id,
content: {
hello: 'world'
}
}
assert @bot.event_allowed? ev
@bot.send :_handle_event, ev
end end
def test_builtin_help def test_builtin_help
...@@ -202,6 +227,7 @@ class BotTest < Test::Unit::TestCase ...@@ -202,6 +227,7 @@ class BotTest < Test::Unit::TestCase
MSG MSG
@bot.send :_handle_event, { @bot.send :_handle_event, {
type: 'm.room.message',
sender: '@bob:example.com', sender: '@bob:example.com',
room_id: @id, room_id: @id,
content: { content: {
...@@ -218,6 +244,7 @@ class BotTest < Test::Unit::TestCase ...@@ -218,6 +244,7 @@ class BotTest < Test::Unit::TestCase
MSG MSG
@bot.send :_handle_event, { @bot.send :_handle_event, {
type: 'm.room.message',
sender: '@bob:example.com', sender: '@bob:example.com',
room_id: @id, room_id: @id,
content: { content: {
...@@ -239,6 +266,7 @@ class BotTest < Test::Unit::TestCase ...@@ -239,6 +266,7 @@ class BotTest < Test::Unit::TestCase
MSG MSG
@bot.send :_handle_event, { @bot.send :_handle_event, {
type: 'm.room.message',
sender: '@bob:example.com', sender: '@bob:example.com',
room_id: @id, room_id: @id,
content: { content: {
...@@ -255,6 +283,7 @@ class BotTest < Test::Unit::TestCase ...@@ -255,6 +283,7 @@ class BotTest < Test::Unit::TestCase
MSG MSG
@bot.send :_handle_event, { @bot.send :_handle_event, {
type: 'm.room.message',
sender: '@bob:example.com', sender: '@bob:example.com',
room_id: @id, room_id: @id,
content: { content: {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment