Permalink
Browse files

added proof-of-concept of PartyLine plugins and behaviour (cinch mods)

  • Loading branch information...
1 parent e47e9f0 commit e8e245058515da7e255b50f2b5e55c98e839ec4d @blowback committed Aug 19, 2012
Showing with 221 additions and 8 deletions.
  1. +112 −0 gertrude/plugins/partyline.rb
  2. +81 −0 gertrude/plugins/partyline_test.rb
  3. +13 −3 lib/cinch/handler.rb
  4. +5 −1 lib/cinch/handler_list.rb
  5. +10 −4 lib/cinch/plugin.rb
@@ -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
+
+
+
+
@@ -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
+
View
@@ -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
@@ -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]
@@ -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)
@@ -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
View
@@ -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
@@ -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
@@ -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)
@@ -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

0 comments on commit e8e2450

Please sign in to comment.