Skip to content

Commit

Permalink
added proof-of-concept of PartyLine plugins and behaviour (cinch mods)
Browse files Browse the repository at this point in the history
  • Loading branch information
blowback committed Aug 19, 2012
1 parent e47e9f0 commit e8e2450
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 8 deletions.
112 changes: 112 additions & 0 deletions gertrude/plugins/partyline.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env ruby

$:.unshift File.expand_path('../../../lib', __FILE__)
$:.unshift File.expand_path('../../lib', __FILE__)
require 'cinch'

require './partyline_test'

require 'delegate'

# The PartyLine plugin acts as a proxy for other plugins; you can register
# plugins with an instance of PartyLine exactly as you would with an instance
# of Bot.
# The crucial difference is that plugins registered with PartyLine have their
# handlers called sequentially, rather than in parallel. If any handler returns
# a TRUE value, the remaining candidate handlers are not called.
#
# This allows the same event to be handled by multiple plugins, on a
# first-come-first-served basis.
#
# For example, imagine there are three plugins which can respond to the channel
# message "what is X?" where X is some string. PluginA does a simple lookup of X in
# a hash in RAM, PluginB looks up X in a local database, and PluginC sends the string
# X via HTTP to some remote lookup service. PartyLine allows us to try PluginA, then
# if it can't handle the request try PluginB. And if that can't deal with it either
# we finally resort to PluginC. In this way, we don't attempt the more computationally
# expensive solutions until we have exhausted the simpler possibilities.
#
# With a vanilla cinch bot/plugin approach, all three plugins would execute in parallel.
# Also, if they all had a match for X, you'd get three responses, which is quite confusing.

# Plugins want to register themselves with bot.handlers, but PartyLine wants to manage
# its own independent set of plugins. The BotDelegate delegates all methods to @bot,
# with the exception of the @handlers attribute, and the @plugins attribute.

# WITHOUT PartyLine (PartyLineTestA, PartyLineTestB, PartyLineTestC all regular cinch plugins):
#
# [16:01] <blowbacH> gertrude, what is cheese
# [16:01] <gertrude> PartyLineTestB has no idea what cheese is either
# [16:01] <gertrude> finally, PartyLineTestC has zero clue about cheese
# [16:01] <gertrude> PartyLineTestA has no idea what cheese is

# WITH PartyLine (PartyLineTest[ABC] are subordinate plugins)
#
# [16:20] <blowbacH> gertrude, what is cheese
# [16:20] <gertrude> PartyLineTestB has no idea what cheese is either
# [16:20] <blowbacH> gertrude, what is cheese
# [16:20] <gertrude> PartyLineTestA has no idea what cheese is
# [16:20] <blowbacH> gertrude, what is cheese
# [16:20] <gertrude> PartyLineTestB has no idea what cheese is either
# [16:20] <blowbacH> gertrude, what is cheese
# [16:20] <gertrude> PartyLineTestA has no idea what cheese is
# [16:20] <blowbacH> gertrude, what is cheese
# [16:20] <gertrude> finally, PartyLineTestC has zero clue about cheese

class BotDelegate < Delegator
attr_accessor :handlers
attr_reader :plugins

def initialize(bot)
super
@bot = bot
@plugins = Cinch::PluginList.new(self) # manage our own plugins
@handlers = Cinch::HandlerList.new # manage our own handler list
@handlers.synchronous = true # our handlers are SYNCHRONOUS
end

def __getobj__; @bot; end
def __setobj__(bot); @bot = bot; end
end

class PartyLine

include Cinch::Plugin

set :prefix, %r{\A(?:!|gertrude,\s*)}
set :suffix, %r{\??\Z}

match %r{what\s+is\s+(\S+)}, react_on: :message, method: :what

def initialize(*args)
super
@bot_delegate = BotDelegate.new(@bot)

@bot_delegate.plugins.register_plugins( [ PartyLineTestA, PartyLineTestB, PartyLineTestC ] )
end

def what(m, x)
# unfortunately, plugins don't know what the event list is for a
# given invocation, so we're hardwiring all our child plugins to
# respond to :message here
@bot_delegate.handlers.dispatch(:message, m, x)
end

end

if __FILE__ == $0
bot = Cinch::Bot.new do
configure do |c|
c.nicks = [ 'gertrude', 'gert', 'gertie', 'gerters', 'ermintrude']
c.server = "irc.z.je"
c.channels = ["#gertrude"]
c.plugins.plugins = [PartyLine]
end
end

bot.start
end




81 changes: 81 additions & 0 deletions gertrude/plugins/partyline_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env ruby

$:.unshift File.expand_path('../../../lib', __FILE__)
$:.unshift File.expand_path('../../lib', __FILE__)
require 'cinch'


class PartyLineTestA
include Cinch::Plugin

set :prefix, %r{\A(?:!|gertrude,\s*)}
set :suffix, %r{\??\Z}

match %r{what\s+is\s+(\S+)}, react_on: :message, method: :what

def what(m, x)
if ret = [true, false].sample
m.reply("#{self.class} has no idea what #{x} is")
end
ret
end
end






class PartyLineTestB
include Cinch::Plugin

set :prefix, %r{\A(?:!|gertrude,\s*)}
set :suffix, %r{\??\Z}

match %r{what\s+is\s+(\S+)}, react_on: :message, method: :what

def what(m, x)
if ret = [true, false].sample
m.reply("#{self.class} has no idea what #{x} is either")
end
ret
end
end







class PartyLineTestC
include Cinch::Plugin

set :prefix, %r{\A(?:!|gertrude,\s*)}
set :suffix, %r{\??\Z}

match %r{what\s+is\s+(\S+)}, react_on: :message, method: :what

def what(m, x)
if ret = [true, false].sample
m.reply("finally, #{self.class} has zero clue about #{x}")
end
ret
end
end

# Illustrates that PartyLineTest[ABC] are all proper plugin citizens, they can
# be executed directly as cinch plugins without using the PartyLine proxy.
if __FILE__ == $0
bot = Cinch::Bot.new do
configure do |c|
c.nicks = [ 'gertrude', 'gert', 'gertie', 'gerters', 'ermintrude']
c.server = "irc.z.je"
c.channels = ["#gertrude"]
c.plugins.plugins = [PartyLineTestA, PartyLineTestB, PartyLineTestC]
end
end

bot.start
end

16 changes: 13 additions & 3 deletions lib/cinch/handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,14 @@ def stop
# @param [Message] message Message that caused the invocation
# @param [Array] captures Capture groups of the pattern that are
# being passed as arguments
# @return [void]
def call(message, captures, arguments)
# @param [Boolean] sync If TRUE, wait for return value from called block
# @return [Boolean] the value returned from the block if sync was TRUE.
# or FALSE otherwise.
def call(message, captures, arguments, sync=false)
bargs = captures + arguments
ret = false

@thread_group.add Thread.new {
thr = Thread.new {
@bot.loggers.debug "[New thread] For #{self}: #{Thread.current} -- #{@thread_group.list.size} in total."

begin
Expand All @@ -93,6 +96,13 @@ def call(message, captures, arguments)
@bot.loggers.debug "[Thread done] For #{self}: #{Thread.current} -- #{@thread_group.list.size - 1} remaining."
end
}
@thread_group.add thr

# If we're synchronous, wait for the return value from the block, which
# will become our own return value. The convention is that blocks only
# return TRUE if they have successfully and completely handled the event.
ret = thr.value if sync
ret
end

# @return [String]
Expand Down
6 changes: 5 additions & 1 deletion lib/cinch/handler_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ module Cinch
class HandlerList
include Enumerable

attr_accessor :synchronous

def initialize
@handlers = Hash.new {|h,k| h[k] = []}
@mutex = Mutex.new
@synchronous = false
end

def register(handler)
Expand Down Expand Up @@ -66,7 +69,8 @@ def dispatch(event, msg = nil, *arguments)
captures = []
end

handler.call(msg, captures, arguments)
# a synchronous handler can return TRUE; if it does stop processing immediately
return if handler.call(msg, captures, arguments, @synchronous)
end
end
end
Expand Down
14 changes: 10 additions & 4 deletions lib/cinch/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -315,11 +315,13 @@ def __register_listeners
@bot.loggers.debug "[plugin] #{self.class.plugin_name}: Registering listener for type `#{listener.event}`"
new_handler = Handler.new(@bot, listener.event, Pattern.new(nil, //, nil)) do |message, *args|
if self.class.call_hooks(:pre, :listen_to, self, [message])
__send__(listener.method, message, *args)
ret = __send__(listener.method, message, *args)
self.class.call_hooks(:post, :listen_to, self, [message])
else
@bot.loggers.debug "[plugin] #{self.class.plugin_name}: Dropping message due to hook"
ret = false
end
ret
end

@handlers << new_handler
Expand All @@ -333,11 +335,13 @@ def __register_ctcps
@bot.loggers.debug "[plugin] #{self.class.plugin_name}: Registering CTCP `#{ctcp}`"
new_handler = Handler.new(@bot, :ctcp, Pattern.generate(:ctcp, ctcp)) do |message, *args|
if self.class.call_hooks(:pre, :ctcp, self, [message])
__send__("ctcp_#{ctcp.downcase}", message, *args)
ret = __send__("ctcp_#{ctcp.downcase}", message, *args)
self.class.call_hooks(:post, :ctcp, self, [message])
else
@bot.loggers.debug "[plugin] #{self.class.plugin_name}: Dropping message due to hook"
ret = false
end
ret
end

@handlers << new_handler
Expand Down Expand Up @@ -379,11 +383,13 @@ def __register_matchers
args = []
end
if self.class.call_hooks(:pre, :match, self, [message])
method.call(message, *args)
ret = method.call(message, *args)
self.class.call_hooks(:post, :match, self, [message])
else
@bot.loggers.debug "[plugin] #{self.class.plugin_name}: Dropping message due to hook"
ret = false
end
ret
end
@handlers << new_handler
@bot.handlers.register(new_handler)
Expand All @@ -398,7 +404,7 @@ def __register_help
@bot.loggers.debug "[plugin] #{self.class.plugin_name}: Registering help message"
help_pattern = Pattern.new(prefix, "help #{self.class.plugin_name}", suffix)
new_handler = Handler.new(@bot, :message, help_pattern) do |message|
message.reply(self.class.help)
ret = message.reply(self.class.help)
end

@handlers << new_handler
Expand Down

0 comments on commit e8e2450

Please sign in to comment.