cinch 0.3.5 → 1.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.
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