cinch 0.3.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +192 -0
  3. data/Rakefile +53 -43
  4. data/examples/basic/autovoice.rb +32 -0
  5. data/examples/basic/google.rb +35 -0
  6. data/examples/basic/hello.rb +15 -0
  7. data/examples/basic/join_part.rb +38 -0
  8. data/examples/basic/memo.rb +39 -0
  9. data/examples/basic/msg.rb +16 -0
  10. data/examples/basic/seen.rb +36 -0
  11. data/examples/basic/urban_dict.rb +35 -0
  12. data/examples/basic/url_shorten.rb +35 -0
  13. data/examples/plugins/autovoice.rb +40 -0
  14. data/examples/plugins/custom_prefix.rb +23 -0
  15. data/examples/plugins/google.rb +37 -0
  16. data/examples/plugins/hello.rb +22 -0
  17. data/examples/plugins/join_part.rb +42 -0
  18. data/examples/plugins/memo.rb +50 -0
  19. data/examples/plugins/msg.rb +22 -0
  20. data/examples/plugins/multiple_matches.rb +41 -0
  21. data/examples/plugins/seen.rb +45 -0
  22. data/examples/plugins/urban_dict.rb +30 -0
  23. data/examples/plugins/url_shorten.rb +32 -0
  24. data/lib/cinch.rb +7 -20
  25. data/lib/cinch/ban.rb +41 -0
  26. data/lib/cinch/bot.rb +479 -0
  27. data/lib/cinch/callback.rb +11 -0
  28. data/lib/cinch/channel.rb +419 -0
  29. data/lib/cinch/constants.rb +369 -0
  30. data/lib/cinch/exceptions.rb +25 -0
  31. data/lib/cinch/helpers.rb +21 -0
  32. data/lib/cinch/irc.rb +344 -38
  33. data/lib/cinch/isupport.rb +96 -0
  34. data/lib/cinch/logger/formatted_logger.rb +80 -0
  35. data/lib/cinch/logger/logger.rb +44 -0
  36. data/lib/cinch/logger/null_logger.rb +18 -0
  37. data/lib/cinch/mask.rb +46 -0
  38. data/lib/cinch/message.rb +183 -0
  39. data/lib/cinch/message_queue.rb +62 -0
  40. data/lib/cinch/plugin.rb +205 -0
  41. data/lib/cinch/rubyext/infinity.rb +1 -0
  42. data/lib/cinch/rubyext/module.rb +18 -0
  43. data/lib/cinch/rubyext/queue.rb +19 -0
  44. data/lib/cinch/rubyext/string.rb +24 -0
  45. data/lib/cinch/syncable.rb +55 -0
  46. data/lib/cinch/user.rb +325 -0
  47. data/spec/bot_spec.rb +5 -0
  48. data/spec/channel_spec.rb +5 -0
  49. data/spec/cinch_spec.rb +5 -0
  50. data/spec/irc_spec.rb +5 -0
  51. data/spec/message_spec.rb +5 -0
  52. data/spec/plugin_spec.rb +5 -0
  53. data/spec/{helper.rb → spec_helper.rb} +0 -0
  54. data/spec/user_spec.rb +5 -0
  55. metadata +69 -51
  56. data/README.rdoc +0 -195
  57. data/examples/autovoice.rb +0 -32
  58. data/examples/custom_patterns.rb +0 -19
  59. data/examples/custom_prefix.rb +0 -25
  60. data/examples/google.rb +0 -31
  61. data/examples/hello.rb +0 -13
  62. data/examples/join_part.rb +0 -26
  63. data/examples/memo.rb +0 -40
  64. data/examples/msg.rb +0 -14
  65. data/examples/named-param-types.rb +0 -19
  66. data/examples/seen.rb +0 -41
  67. data/examples/urban_dict.rb +0 -31
  68. data/examples/url_shorten.rb +0 -34
  69. data/lib/cinch/base.rb +0 -368
  70. data/lib/cinch/irc/message.rb +0 -135
  71. data/lib/cinch/irc/parser.rb +0 -141
  72. data/lib/cinch/irc/socket.rb +0 -329
  73. data/lib/cinch/names.rb +0 -54
  74. data/lib/cinch/rules.rb +0 -171
  75. data/spec/base_spec.rb +0 -94
  76. data/spec/irc/helper.rb +0 -8
  77. data/spec/irc/message_spec.rb +0 -61
  78. data/spec/irc/parser_spec.rb +0 -103
  79. data/spec/irc/socket_spec.rb +0 -90
  80. data/spec/names_spec.rb +0 -393
  81. data/spec/options_spec.rb +0 -45
  82. data/spec/rules_spec.rb +0 -109
@@ -0,0 +1,45 @@
1
+ require 'cinch'
2
+
3
+ class Seen
4
+ class SeenStruct < Struct.new(:who, :where, :what, :time)
5
+ def to_s
6
+ "[#{time.asctime}] #{who} was seen in #{where} saying #{what}"
7
+ end
8
+ end
9
+
10
+ include Cinch::Plugin
11
+ listen_to :channel
12
+ match /seen (.+)/
13
+
14
+ def initialize(*args)
15
+ super
16
+ @users = {}
17
+ end
18
+
19
+ def listen(m)
20
+ @users[m.user.nick] = SeenStruct.new(m.user, m.channel, m.message, Time.now)
21
+ end
22
+
23
+ def execute(m, nick)
24
+ if nick == @bot.nick
25
+ m.reply "That's me!"
26
+ elsif nick == m.user.nick
27
+ m.reply "That's you!"
28
+ elsif @users.key?(nick)
29
+ m.reply @users[nick].to_s
30
+ else
31
+ m.reply "I haven't seen #{nick}"
32
+ end
33
+ end
34
+ end
35
+
36
+ bot = Cinch::Bot.new do
37
+ configure do |c|
38
+ c.server = 'irc.freenode.org'
39
+ c.channels = ["#cinch-bots"]
40
+ c.plugins.plugins = [Seen]
41
+ end
42
+ end
43
+
44
+ bot.start
45
+
@@ -0,0 +1,30 @@
1
+ require 'cinch'
2
+ require 'open-uri'
3
+ require 'nokogiri'
4
+ require 'cgi'
5
+
6
+ class UrbanDictionary
7
+ include Cinch::Plugin
8
+
9
+ match /urban (.+)/
10
+ def lookup(word)
11
+ url = "http://www.urbandictionary.com/define.php?term=#{CGI.escape(word)}"
12
+ CGI.unescape_html Nokogiri::HTML(open(url)).at("div.definition").text.gsub(/\s+/, ' ') rescue nil
13
+ end
14
+
15
+ def execute(m, word)
16
+ m.reply(lookup(word) || "No results found", true)
17
+ end
18
+ end
19
+
20
+ bot = Cinch::Bot.new do
21
+ configure do |c|
22
+ c.server = "irc.freenode.net"
23
+ c.nick = "MrCinch"
24
+ c.channels = ["#cinch-bots"]
25
+ c.plugins.plugins = [UrbanDictionary]
26
+ end
27
+ end
28
+
29
+ bot.start
30
+
@@ -0,0 +1,32 @@
1
+ require 'open-uri'
2
+ require 'cinch'
3
+
4
+ class TinyURL
5
+ include Cinch::Plugin
6
+
7
+ listen_to :channel
8
+
9
+ def shorten(url)
10
+ url = open("http://tinyurl.com/api-create.php?url=#{URI.escape(url)}").read
11
+ url == "Error" ? nil : url
12
+ rescue OpenURI::HTTPError
13
+ nil
14
+ end
15
+
16
+ def listen(m)
17
+ urls = URI.extract(m.message, "http")
18
+ short_urls = urls.map { |url| shorten(url) }.compact
19
+ unless short_urls.empty?
20
+ m.reply short_urls.join(", ")
21
+ end
22
+ end
23
+ end
24
+
25
+ bot = Cinch::Bot.new do
26
+ configure do |c|
27
+ c.server = "irc.freenode.org"
28
+ c.channels = ["#cinch"]
29
+ end
30
+ end
31
+
32
+ bot.start
@@ -1,25 +1,12 @@
1
- dir = File.dirname(__FILE__)
2
- $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include? dir
3
-
4
- require 'ostruct'
5
- require 'optparse'
6
-
7
- require 'cinch/irc'
8
- require 'cinch/rules'
9
- require 'cinch/base'
10
- require 'cinch/names'
1
+ require 'cinch/bot'
11
2
 
12
3
  module Cinch
13
- VERSION = '0.3.5'
14
-
15
- class << self
4
+ VERSION = '1.0.0'
16
5
 
17
- # Setup bot options and return a new Cinch::Base instance
18
- def setup(ops={}, &blk)
19
- Cinch::Base.new(ops, &blk)
20
- end
21
- alias_method :configure, :setup
6
+ # @return [String]
7
+ # @todo Handle mIRC color codes more gracefully.
8
+ # @api private
9
+ def self.filter_string(string)
10
+ string.gsub(/[\x00-\x1f]/, '')
22
11
  end
23
-
24
12
  end
25
-
@@ -0,0 +1,41 @@
1
+ require "cinch/mask"
2
+ module Cinch
3
+ class Ban
4
+ # @return [Mask, String]
5
+ attr_reader :mask
6
+
7
+ # @return [String]
8
+ attr_reader :by
9
+
10
+ # @return [Time]
11
+ attr_reader :created_at
12
+
13
+ # @return [Boolean]
14
+ attr_reader :extended
15
+
16
+ def initialize(mask, by, at)
17
+ @by, @created_at = by, at
18
+ if mask =~ /^\$/
19
+ @extended = true
20
+ @mask = mask
21
+ else
22
+ @extended = false
23
+ @mask = Mask.new(mask)
24
+ end
25
+ end
26
+
27
+ # @return [Boolean] true if the ban matches `user`
28
+ # @raise [Exceptions::UnsupportedFeature] Cinch does not support
29
+ # Freenode's extended bans
30
+ def match(user)
31
+ raise UnsupportedFeature, "extended bans (freenode) are not supported yet" if @extended
32
+ @mask =~ user
33
+ end
34
+ alias_method :=~, :match
35
+
36
+ # @return [String]
37
+ def to_s
38
+ @mask.to_s
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,479 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'socket'
3
+ require "thread"
4
+ require "ostruct"
5
+ require "cinch/rubyext/module"
6
+ require "cinch/rubyext/queue"
7
+ require "cinch/rubyext/string"
8
+ require "cinch/rubyext/infinity"
9
+
10
+ require "cinch/exceptions"
11
+
12
+ require "cinch/helpers"
13
+ require "cinch/logger/logger"
14
+ require "cinch/logger/null_logger"
15
+ require "cinch/logger/formatted_logger"
16
+ require "cinch/syncable"
17
+ require "cinch/message"
18
+ require "cinch/message_queue"
19
+ require "cinch/irc"
20
+ require "cinch/channel"
21
+ require "cinch/user"
22
+ require "cinch/constants"
23
+ require "cinch/callback"
24
+ require "cinch/ban"
25
+ require "cinch/mask"
26
+ require "cinch/isupport"
27
+ require "cinch/plugin"
28
+
29
+ module Cinch
30
+
31
+ class Bot
32
+ # @return [Config]
33
+ attr_accessor :config
34
+ # @return [IRC]
35
+ attr_accessor :irc
36
+ # @return [Logger]
37
+ attr_accessor :logger
38
+ # @return [Array<Channel>] All channels the bot currently is in
39
+ attr_reader :channels
40
+ # @return [String]
41
+ attr_reader :host
42
+ # @return [Mask]
43
+ attr_reader :mask
44
+ # @return [String]
45
+ attr_reader :user
46
+ # @return [String]
47
+ attr_reader :realname
48
+ # @return [Time]
49
+ attr_reader :signed_on_at
50
+
51
+ # Helper method for turning a String into a {Channel} object.
52
+ #
53
+ # @param [String] channel a channel name
54
+ # @return [Channel] a {Channel} object
55
+ # @example
56
+ # on :message, /^please join (#.+)$/ do |target|
57
+ # Channel(target).join
58
+ # end
59
+ def Channel(channel)
60
+ return channel if channel.is_a?(Channel)
61
+ Channel.find_ensured(channel, self)
62
+ end
63
+
64
+ # Helper method for turning a String into an {User} object.
65
+ #
66
+ # @param [String] user a user's nickname
67
+ # @return [User] an {User} object
68
+ # @example
69
+ # on :message, /^tell me everything about (.+)$/ do |target|
70
+ # user = User(target)
71
+ # reply "%s is named %s and connects from %s" % [user.nick, user.name, user.host]
72
+ # end
73
+ def User(user)
74
+ return user if user.is_a?(User)
75
+ User.find_ensured(user, self)
76
+ end
77
+
78
+ # @return [void]
79
+ # @see Logger#debug
80
+ def debug(msg)
81
+ @logger.debug(msg)
82
+ end
83
+
84
+ # @return [Boolean]
85
+ def strict?
86
+ @config.strictness == :strict
87
+ end
88
+
89
+ # @yield
90
+ def initialize(&b)
91
+ @logger = Logger::FormattedLogger.new($stderr)
92
+ @events = {}
93
+ @config = OpenStruct.new({
94
+ :server => "localhost",
95
+ :port => 6667,
96
+ :ssl => false,
97
+ :password => nil,
98
+ :nick => "cinch",
99
+ :realname => "cinch",
100
+ :verbose => true,
101
+ :messages_per_second => 0.5,
102
+ :server_queue_size => 10,
103
+ :strictness => :forgiving,
104
+ :message_split_start => '... ',
105
+ :message_split_end => ' ...',
106
+ :max_messages => nil,
107
+ :plugins => OpenStruct.new({
108
+ :plugins => [],
109
+ :prefix => "!",
110
+ :options => Hash.new {|h,k| h[k] = {}},
111
+ }),
112
+ :channels => [],
113
+ :encoding => nil,
114
+ })
115
+
116
+ @semaphores_mutex = Mutex.new
117
+ @semaphores = Hash.new { |h,k| h[k] = Mutex.new }
118
+ @plugins = []
119
+ @callback = Callback.new(self)
120
+ @channels = []
121
+
122
+ on :connect do
123
+ bot.config.channels.each do |channel|
124
+ bot.join channel
125
+ end
126
+ end
127
+
128
+ instance_eval(&b) if block_given?
129
+ end
130
+
131
+ # This method is used to set a bot's options. It indeed does
132
+ # nothing else but yielding {Bot#config}, but it makes for a nice DSL.
133
+ #
134
+ # @yieldparam [Struct] config the bot's config
135
+ # @return [void]
136
+ def configure(&block)
137
+ @callback.instance_exec(@config, &block)
138
+ end
139
+
140
+ # Since Cinch uses threads, all handlers can be run
141
+ # simultaneously, even the same handler multiple times. This also
142
+ # means, that your code has to be thread-safe. Most of the time,
143
+ # this is not a problem, but if you are accessing stored data, you
144
+ # will most likely have to synchronize access to it. Instead of
145
+ # managing all mutexes yourself, Cinch provides a synchronize
146
+ # method, which takes a name and block.
147
+ #
148
+ # Synchronize blocks with the same name share the same mutex,
149
+ # which means that only one of them will be executed at a time.
150
+ #
151
+ # @param [String, Symbol] name a name for the synchronize block.
152
+ # @return [void]
153
+ # @yield
154
+ #
155
+ # @example
156
+ # configure do |c|
157
+ # …
158
+ # @i = 0
159
+ # end
160
+ #
161
+ # on :channel, /^start counting!/ do
162
+ # synchronize(:my_counter) do
163
+ # 10.times do
164
+ # val = @i
165
+ # # at this point, another thread might've incremented :i already.
166
+ # # this thread wouldn't know about it, though.
167
+ # @i = val + 1
168
+ # end
169
+ # end
170
+ # end
171
+ def synchronize(name, &block)
172
+ # Must run the default block +/ fetch in a thread safe way in order to
173
+ # ensure we always get the same mutex for a given name.
174
+ semaphore = @semaphores_mutex.synchronize { @semaphores[name] }
175
+ semaphore.synchronize(&block)
176
+ end
177
+
178
+ # Registers a handler.
179
+ #
180
+ # @param [String, Symbol, Integer] event the event to match. Available
181
+ # events are all IRC commands in lowercase as symbols, all numeric
182
+ # replies, and the following:
183
+ #
184
+ # - :channel (a channel message)
185
+ # - :private (a private message)
186
+ # - :message (both channel and private messages)
187
+ # - :error (handling errors, use a numeric error code as `match`)
188
+ # - :ctcp (ctcp requests, use a ctcp command as `match`)
189
+ #
190
+ # @param [Regexp, String, Integer] match every message of the
191
+ # right event will be checked against this argument and the event
192
+ # will only be called if it matches
193
+ #
194
+ # @yieldparam [String] *args each capture group of the regex will
195
+ # be one argument to the block. It is optional to accept them,
196
+ # though
197
+ #
198
+ # @return [void]
199
+ def on(event, regexps = [], *args, &block)
200
+ regexps = [*regexps]
201
+ regexps = [//] if regexps.empty?
202
+
203
+ event = event.to_sym
204
+
205
+ regexps.map! do |regexp|
206
+ case regexp
207
+ when String, Integer
208
+ if event == :ctcp
209
+ /^#{Regexp.escape(regexp.to_s)}(?:$| .+)/
210
+ else
211
+ /^#{Regexp.escape(regexp.to_s)}$/
212
+ end
213
+ else
214
+ regexp
215
+ end
216
+ end
217
+ (@events[event] ||= []) << [regexps, args, block]
218
+ end
219
+
220
+ # Define helper methods in the context of the bot.
221
+ #
222
+ # @yield Expects a block containing method definitions
223
+ # @return [void]
224
+ def helpers(&b)
225
+ Callback.class_eval(&b)
226
+ end
227
+
228
+ # Stop execution of the current {#on} handler.
229
+ #
230
+ # @return [void]
231
+ def halt
232
+ throw :halt
233
+ end
234
+
235
+ # Sends a raw message to the server.
236
+ #
237
+ # @param [String] command The message to send.
238
+ # @return [void]
239
+ # @see IRC#message
240
+ def raw(command)
241
+ @irc.message(command)
242
+ end
243
+
244
+ # Sends a PRIVMSG to a recipient (a channel or user).
245
+ # You should be using {Channel#send} and {User#send} instead.
246
+ #
247
+ # @param [String] recipient the recipient
248
+ # @param [String] text the message to send
249
+ # @return [void]
250
+ # @see Channel#send
251
+ # @see User#send
252
+ # @see #safe_msg
253
+ def msg(recipient, text)
254
+ text = text.to_s
255
+ split_start = @config.message_split_start || ""
256
+ split_end = @config.message_split_end || ""
257
+
258
+ text.split(/\r\n|\r|\n/).each do |line|
259
+ # 498 = 510 - length(":" . " PRIVMSG " . " :");
260
+ maxlength = 498 - self.mask.to_s.length - recipient.to_s.length
261
+ maxlength_without_end = maxlength - split_end.bytesize
262
+
263
+ if line.bytesize > maxlength
264
+ splitted = []
265
+
266
+ while line.bytesize > maxlength_without_end
267
+ pos = line.rindex(/\s/, maxlength_without_end)
268
+ r = pos || maxlength_without_end
269
+ splitted << line.slice!(0, r) + split_end.tr(" ", "\u00A0")
270
+ line = split_start.tr(" ", "\u00A0") + line.lstrip
271
+ end
272
+
273
+ splitted << line
274
+ splitted[0, (@config.max_messages || splitted.size)].each do |string|
275
+ string.tr!("\u00A0", " ") # clean string from any non-breaking spaces
276
+ raw("PRIVMSG #{recipient} :#{string}")
277
+ end
278
+ else
279
+ raw("PRIVMSG #{recipient} :#{line}")
280
+ end
281
+ end
282
+ end
283
+ alias_method :privmsg, :msg
284
+ alias_method :send, :msg
285
+
286
+ # Like {#msg}, but remove any non-printable characters from
287
+ # `text`. The purpose of this method is to send text of untrusted
288
+ # sources, like other users or feeds.
289
+ #
290
+ # Note: this will **break** any mIRC color codes embedded in the
291
+ # string.
292
+ #
293
+ # @return (see #msg)
294
+ # @param (see #msg)
295
+ # @see #msg
296
+ # @see User#safe_send
297
+ # @see Channel#safe_send
298
+ # @todo Handle mIRC color codes more gracefully.
299
+ def safe_msg(recipient, text)
300
+ msg(recipient, Cinch.filter_string(text))
301
+ end
302
+ alias_method :safe_privmsg, :safe_msg
303
+ alias_method :safe_send, :safe_msg
304
+
305
+ # Invoke an action (/me) in/to a recipient (a channel or user).
306
+ # You should be using {Channel#action} and {User#action} instead.
307
+ #
308
+ # @param [String] recipient the recipient
309
+ # @param [String] text the message to send
310
+ # @return [void]
311
+ # @see Channel#action
312
+ # @see User#action
313
+ # @see #safe_action
314
+ def action(recipient, text)
315
+ raw("PRIVMSG #{recipient} :\001ACTION #{text}\001")
316
+ end
317
+
318
+ # Like {#action}, but remove any non-printable characters from
319
+ # `text`. The purpose of this method is to send text from
320
+ # untrusted sources, like other users or feeds.
321
+ #
322
+ # Note: this will **break** any mIRC color codes embedded in the
323
+ # string.
324
+ #
325
+ # @param (see #action)
326
+ # @return (see #action)
327
+ # @see #action
328
+ # @see Channel#safe_action
329
+ # @see User#safe_action
330
+ # @todo Handle mIRC color codes more gracefully.
331
+ def safe_action(recipient, text)
332
+ action(recipient, Cinch.filter_string(text))
333
+ end
334
+
335
+ # Join a channel.
336
+ #
337
+ # @param [String, Channel] channel either the name of a channel or a {Channel} object
338
+ # @param [String] key optionally the key of the channel
339
+ # @return [void]
340
+ # @see Channel#join
341
+ def join(channel, key = nil)
342
+ Channel(channel).join(key)
343
+ end
344
+
345
+ # Part a channel.
346
+ #
347
+ # @param [String, Channel] channel either the name of a channel or a {Channel} object
348
+ # @param [String] reason an optional reason/part message
349
+ # @return [void]
350
+ # @see Channel#part
351
+ def part(channel, reason = nil)
352
+ Channel(channel).part(reason)
353
+ end
354
+
355
+ # @return [String]
356
+ attr_accessor :nick
357
+ def nick
358
+ @config.nick
359
+ end
360
+
361
+ def secure?
362
+ @config[:ssl]
363
+ end
364
+
365
+ def unknown?
366
+ false
367
+ end
368
+
369
+ [:host, :mask, :user, :realname, :signed_on_at, :secure?].each do |attr|
370
+ define_method(attr) do
371
+ User(nick).__send__(attr)
372
+ end
373
+ end
374
+
375
+ # Sets the bot's nick.
376
+ #
377
+ # @param [String] new_nick
378
+ # @raise [Exceptions::NickTooLong]
379
+ def nick=(new_nick)
380
+ if new_nick.size > @irc.isupport["NICKLEN"] && strict?
381
+ raise Exceptions::NickTooLong, new_nick
382
+ end
383
+ @config.nick = new_nick
384
+ raw "NICK #{new_nick}"
385
+ end
386
+
387
+ # Disconnects from the server.
388
+ #
389
+ # @return [void]
390
+ def quit(message = nil)
391
+ command = message ? "QUIT :#{message}" : "QUIT"
392
+ raw command
393
+ end
394
+
395
+ # Connects the bot to a server.
396
+ #
397
+ # @param [Boolean] plugins Automatically register plugins from
398
+ # `@config.plugins.plugins`?
399
+ # @return [void]
400
+ def start(plugins = true)
401
+ register_plugins if plugins
402
+ @logger.debug "Connecting to #{@config.server}:#{@config.port}"
403
+ @irc = IRC.new(self, @config)
404
+ @irc.connect
405
+ end
406
+
407
+ # Register all plugins from `@config.plugins.plugins`.
408
+ #
409
+ # @return [void]
410
+ def register_plugins
411
+ @config.plugins.plugins.each do |plugin|
412
+ register_plugin(plugin)
413
+ end
414
+ end
415
+
416
+ # Registers a plugin.
417
+ #
418
+ # @param [Class<Plugin>] plugin The plugin class to register
419
+ # @return [void]
420
+ def register_plugin(plugin)
421
+ @plugins << plugin.new(self)
422
+ end
423
+
424
+ # @api private
425
+ # @return [void]
426
+ def dispatch(event, msg = nil)
427
+ if handlers = find(event, msg)
428
+ handlers.each do |handler|
429
+ regexps, args, block = *handler
430
+ # calling Message#match multiple times is not a problem
431
+ # because we cache the result
432
+ if msg
433
+ regexp = regexps.find { |rx| msg.match(rx, event) }
434
+ captures = msg.match(regexp, event).captures
435
+ else
436
+ captures = []
437
+ end
438
+
439
+ invoke(block, args, msg, captures)
440
+ end
441
+ end
442
+ end
443
+
444
+ private
445
+ def find(type, msg = nil)
446
+ if events = @events[type]
447
+ if msg.nil?
448
+ return events
449
+ end
450
+
451
+ events.select { |regexps|
452
+ regexps.first.any? { |regexp|
453
+ msg.match(regexp, type)
454
+ }
455
+ }
456
+ end
457
+ end
458
+
459
+ def invoke(block, args, msg, match)
460
+ # -1 splat arg, send everything
461
+ # 0 no args, send nothing
462
+ # 1 defined number of args, send only those
463
+ bargs = case block.arity <=> 0
464
+ when -1; match
465
+ when 0; []
466
+ when 1; match[0..block.arity-1 - args.size]
467
+ end
468
+ Thread.new do
469
+ begin
470
+ catch(:halt) do
471
+ @callback.instance_exec(msg, *args, *bargs, &block)
472
+ end
473
+ rescue => e
474
+ @logger.log_exception(e)
475
+ end
476
+ end
477
+ end
478
+ end
479
+ end