cinch 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/README.md +25 -44
  2. data/examples/basic/autovoice.rb +1 -1
  3. data/examples/basic/join_part.rb +0 -4
  4. data/examples/plugins/autovoice.rb +2 -5
  5. data/examples/plugins/google.rb +1 -2
  6. data/examples/plugins/hooks.rb +36 -0
  7. data/examples/plugins/lambdas.rb +35 -0
  8. data/examples/plugins/last_nick.rb +24 -0
  9. data/examples/plugins/multiple_matches.rb +1 -10
  10. data/examples/plugins/own_events.rb +37 -0
  11. data/examples/plugins/timer.rb +22 -0
  12. data/examples/plugins/url_shorten.rb +1 -1
  13. data/lib/cinch.rb +50 -1
  14. data/lib/cinch/ban.rb +5 -2
  15. data/lib/cinch/bot.rb +360 -193
  16. data/lib/cinch/cache_manager.rb +15 -0
  17. data/lib/cinch/callback.rb +6 -0
  18. data/lib/cinch/channel.rb +150 -96
  19. data/lib/cinch/channel_manager.rb +26 -0
  20. data/lib/cinch/constants.rb +6 -4
  21. data/lib/cinch/exceptions.rb +9 -0
  22. data/lib/cinch/irc.rb +197 -82
  23. data/lib/cinch/logger/formatted_logger.rb +8 -8
  24. data/lib/cinch/logger/zcbot_logger.rb +37 -0
  25. data/lib/cinch/mask.rb +17 -3
  26. data/lib/cinch/message.rb +14 -7
  27. data/lib/cinch/message_queue.rb +8 -4
  28. data/lib/cinch/mode_parser.rb +56 -0
  29. data/lib/cinch/pattern.rb +45 -0
  30. data/lib/cinch/plugin.rb +129 -34
  31. data/lib/cinch/rubyext/string.rb +4 -4
  32. data/lib/cinch/syncable.rb +8 -0
  33. data/lib/cinch/user.rb +68 -13
  34. data/lib/cinch/user_manager.rb +60 -0
  35. metadata +17 -35
  36. data/Rakefile +0 -66
  37. data/lib/cinch/PLANNED +0 -4
  38. data/spec/bot_spec.rb +0 -5
  39. data/spec/channel_spec.rb +0 -5
  40. data/spec/cinch_spec.rb +0 -5
  41. data/spec/irc_spec.rb +0 -5
  42. data/spec/message_spec.rb +0 -5
  43. data/spec/plugin_spec.rb +0 -5
  44. data/spec/spec.opts +0 -2
  45. data/spec/spec_helper.rb +0 -8
  46. data/spec/user_spec.rb +0 -5
@@ -25,19 +25,24 @@ require "cinch/ban"
25
25
  require "cinch/mask"
26
26
  require "cinch/isupport"
27
27
  require "cinch/plugin"
28
+ require "cinch/pattern"
29
+ require "cinch/mode_parser"
30
+ require "cinch/cache_manager"
31
+ require "cinch/channel_manager"
32
+ require "cinch/user_manager"
28
33
 
29
34
  module Cinch
30
35
 
31
36
  class Bot
32
37
  # @return [Config]
33
- attr_accessor :config
38
+ attr_reader :config
34
39
  # @return [IRC]
35
- attr_accessor :irc
40
+ attr_reader :irc
36
41
  # @return [Logger]
37
42
  attr_accessor :logger
38
43
  # @return [Array<Channel>] All channels the bot currently is in
39
44
  attr_reader :channels
40
- # @return [String]
45
+ # @return [String] the bot's hostname
41
46
  attr_reader :host
42
47
  # @return [Mask]
43
48
  attr_reader :mask
@@ -47,18 +52,34 @@ module Cinch
47
52
  attr_reader :realname
48
53
  # @return [Time]
49
54
  attr_reader :signed_on_at
55
+ # @return [Array<Plugin>] All registered plugins
56
+ attr_reader :plugins
57
+ # @return [Array<Thread>]
58
+ # @api private
59
+ attr_reader :handler_threads
60
+ # @return [Boolean] whether the bot is in the process of disconnecting
61
+ attr_reader :quitting
62
+ # @return [UserManager]
63
+ attr_reader :user_manager
64
+ # @return [ChannelManager]
65
+ attr_reader :channel_manager
66
+ # @return [Boolean]
67
+ # @api private
68
+ attr_accessor :last_connection_was_successful
69
+
70
+ # @group Helper methods
50
71
 
51
72
  # Helper method for turning a String into a {Channel} object.
52
73
  #
53
74
  # @param [String] channel a channel name
54
75
  # @return [Channel] a {Channel} object
55
76
  # @example
56
- # on :message, /^please join (#.+)$/ do |target|
77
+ # on :message, /^please join (#.+)$/ do |m, target|
57
78
  # Channel(target).join
58
79
  # end
59
80
  def Channel(channel)
60
81
  return channel if channel.is_a?(Channel)
61
- Channel.find_ensured(channel, self)
82
+ @channel_manager.find_ensured(channel)
62
83
  end
63
84
 
64
85
  # Helper method for turning a String into an {User} object.
@@ -66,75 +87,21 @@ module Cinch
66
87
  # @param [String] user a user's nickname
67
88
  # @return [User] an {User} object
68
89
  # @example
69
- # on :message, /^tell me everything about (.+)$/ do |target|
90
+ # on :message, /^tell me everything about (.+)$/ do |m, target|
70
91
  # user = User(target)
71
- # reply "%s is named %s and connects from %s" % [user.nick, user.name, user.host]
92
+ # m.reply "%s is named %s and connects from %s" % [user.nick, user.name, user.host]
72
93
  # end
73
94
  def User(user)
74
95
  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 => Encoding.default_external,
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?
96
+ @user_manager.find_ensured(user)
129
97
  end
130
98
 
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.
99
+ # Define helper methods in the context of the bot.
133
100
  #
134
- # @yieldparam [Struct] config the bot's config
101
+ # @yield Expects a block containing method definitions
135
102
  # @return [void]
136
- def configure(&block)
137
- @callback.instance_exec(@config, &block)
103
+ def helpers(&b)
104
+ Callback.class_eval(&b)
138
105
  end
139
106
 
140
107
  # Since Cinch uses threads, all handlers can be run
@@ -175,56 +142,6 @@ module Cinch
175
142
  semaphore.synchronize(&block)
176
143
  end
177
144
 
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
145
  # Stop execution of the current {#on} handler.
229
146
  #
230
147
  # @return [void]
@@ -232,6 +149,9 @@ module Cinch
232
149
  throw :halt
233
150
  end
234
151
 
152
+ # @endgroup
153
+ # @group Sending messages
154
+
235
155
  # Sends a raw message to the server.
236
156
  #
237
157
  # @param [String] command The message to send.
@@ -246,18 +166,20 @@ module Cinch
246
166
  #
247
167
  # @param [String] recipient the recipient
248
168
  # @param [String] text the message to send
169
+ # @param [Boolean] notice Use NOTICE instead of PRIVMSG?
249
170
  # @return [void]
250
171
  # @see Channel#send
251
172
  # @see User#send
252
173
  # @see #safe_msg
253
- def msg(recipient, text)
174
+ def msg(recipient, text, notice = false)
254
175
  text = text.to_s
255
176
  split_start = @config.message_split_start || ""
256
177
  split_end = @config.message_split_end || ""
178
+ command = notice ? "NOTICE" : "PRIVMSG"
257
179
 
258
180
  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
181
+ maxlength = 510 - (":" + " #{command} " + " :").size
182
+ maxlength = maxlength - self.mask.to_s.length - recipient.to_s.length
261
183
  maxlength_without_end = maxlength - split_end.bytesize
262
184
 
263
185
  if line.bytesize > maxlength
@@ -273,16 +195,29 @@ module Cinch
273
195
  splitted << line
274
196
  splitted[0, (@config.max_messages || splitted.size)].each do |string|
275
197
  string.tr!("\u00A0", " ") # clean string from any non-breaking spaces
276
- raw("PRIVMSG #{recipient} :#{string}")
198
+ raw("#{command} #{recipient} :#{string}")
277
199
  end
278
200
  else
279
- raw("PRIVMSG #{recipient} :#{line}")
201
+ raw("#{command} #{recipient} :#{line}")
280
202
  end
281
203
  end
282
204
  end
283
205
  alias_method :privmsg, :msg
284
206
  alias_method :send, :msg
285
207
 
208
+ # Sends a NOTICE to a recipient (a channel or user).
209
+ # You should be using {Channel#notice} and {User#notice} instead.
210
+ #
211
+ # @param [String] recipient the recipient
212
+ # @param [String] text the message to send
213
+ # @return [void]
214
+ # @see Channel#notice
215
+ # @see User#notice
216
+ # @see #safe_notice
217
+ def notice(recipient, text)
218
+ msg(recipient, text, true)
219
+ end
220
+
286
221
  # Like {#msg}, but remove any non-printable characters from
287
222
  # `text`. The purpose of this method is to send text of untrusted
288
223
  # sources, like other users or feeds.
@@ -302,6 +237,19 @@ module Cinch
302
237
  alias_method :safe_privmsg, :safe_msg
303
238
  alias_method :safe_send, :safe_msg
304
239
 
240
+ # Like {#safe_msg} but for notices.
241
+ #
242
+ # @return (see #safe_msg)
243
+ # @param (see #safe_msg)
244
+ # @see #safe_notice
245
+ # @see #notice
246
+ # @see User#safe_notice
247
+ # @see Channel#safe_notice
248
+ # @todo (see #safe_msg)
249
+ def safe_notice(recipient, text)
250
+ msg(recipient, Cinch.filter_string(text), true)
251
+ end
252
+
305
253
  # Invoke an action (/me) in/to a recipient (a channel or user).
306
254
  # You should be using {Channel#action} and {User#action} instead.
307
255
  #
@@ -332,112 +280,336 @@ module Cinch
332
280
  action(recipient, Cinch.filter_string(text))
333
281
  end
334
282
 
335
- # Join a channel.
283
+ # @endgroup
284
+ # @group Events &amp; Plugins
285
+
286
+ # Registers a handler.
287
+ #
288
+ # @param [String, Symbol, Integer] event the event to match. Available
289
+ # events are all IRC commands in lowercase as symbols, all numeric
290
+ # replies, and the following:
291
+ #
292
+ # - :channel (a channel message)
293
+ # - :private (a private message)
294
+ # - :message (both channel and private messages)
295
+ # - :error (handling errors, use a numeric error code as `match`)
296
+ # - :ctcp (ctcp requests, use a ctcp command as `match`)
297
+ #
298
+ # @param [Regexp, String, Integer] match every message of the
299
+ # right event will be checked against this argument and the event
300
+ # will only be called if it matches
301
+ #
302
+ # @yieldparam [String] *args each capture group of the regex will
303
+ # be one argument to the block. It is optional to accept them,
304
+ # though
336
305
  #
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
306
  # @return [void]
340
- # @see Channel#join
341
- def join(channel, key = nil)
342
- Channel(channel).join(key)
307
+ def on(event, regexps = [], *args, &block)
308
+ regexps = [*regexps]
309
+ regexps = [//] if regexps.empty?
310
+
311
+ event = event.to_sym
312
+
313
+ regexps.map! do |regexp|
314
+ pattern = case regexp
315
+ when Pattern
316
+ regexp
317
+ when Regexp
318
+ Pattern.new(nil, regexp, nil)
319
+ else
320
+ if event == :ctcp
321
+ Pattern.new(/^/, /#{Regexp.escape(regexp.to_s)}(?:$| .+)/)
322
+ else
323
+ Pattern.new(/^/, /#{Regexp.escape(regexp.to_s)}/, /$/)
324
+ end
325
+ end
326
+ debug "[on handler] Registering handler with pattern `#{pattern.inspect}`, reacting on `#{event}`"
327
+ pattern
328
+ end
329
+ (@events[event] ||= []) << [regexps, args, block]
343
330
  end
344
331
 
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
332
+ # @param [Symbol] event The event type
333
+ # @param [Message, nil] msg The message which is responsible for
334
+ # and attached to the event, or nil.
335
+ # @param [Array] *arguments A list of additional arguments to pass
336
+ # to event handlers
349
337
  # @return [void]
350
- # @see Channel#part
351
- def part(channel, reason = nil)
352
- Channel(channel).part(reason)
353
- end
338
+ def dispatch(event, msg = nil, *arguments)
339
+ if handlers = find(event, msg)
340
+ handlers.each do |handler|
341
+ regexps, args, block = *handler
342
+ # calling Message#match multiple times is not a problem
343
+ # because we cache the result
344
+ if msg
345
+ regexp = regexps.find { |rx|
346
+ msg.match(rx.to_r(msg), event)
347
+ }
348
+ captures = msg.match(regexp.to_r(msg), event).captures
349
+ else
350
+ captures = []
351
+ end
354
352
 
355
- # @return [String]
356
- attr_accessor :nick
357
- def nick
358
- @config.nick
353
+ invoke(block, args, msg, captures, arguments)
354
+ end
355
+ end
359
356
  end
360
357
 
361
- def secure?
362
- @config[:ssl]
358
+ # Register all plugins from `@config.plugins.plugins`.
359
+ #
360
+ # @return [void]
361
+ def register_plugins
362
+ @config.plugins.plugins.each do |plugin|
363
+ register_plugin(plugin)
364
+ end
363
365
  end
364
366
 
365
- def unknown?
366
- false
367
+ # Registers a plugin.
368
+ #
369
+ # @param [Class<Plugin>] plugin The plugin class to register
370
+ # @return [void]
371
+ def register_plugin(plugin)
372
+ @plugins << plugin.new(self)
367
373
  end
368
374
 
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
375
+ # @endgroup
376
+ # @group Bot Control
374
377
 
375
- # Sets the bot's nick.
378
+ # This method is used to set a bot's options. It indeed does
379
+ # nothing else but yielding {Bot#config}, but it makes for a nice DSL.
376
380
  #
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}"
381
+ # @yieldparam [Struct] config the bot's config
382
+ # @return [void]
383
+ def configure(&block)
384
+ @callback.instance_exec(@config, &block)
385
385
  end
386
386
 
387
387
  # Disconnects from the server.
388
388
  #
389
+ # @param [String] message The quit message to send while quitting
389
390
  # @return [void]
390
391
  def quit(message = nil)
392
+ @quitting = true
391
393
  command = message ? "QUIT :#{message}" : "QUIT"
392
394
  raw command
393
395
  end
394
396
 
397
+ # Connects the bot to a server.
398
+ #
399
+ # @param [Boolean] plugins Automatically register plugins from
400
+ # `@config.plugins.plugins`?
401
+ # @return [void]
395
402
  # Connects the bot to a server.
396
403
  #
397
404
  # @param [Boolean] plugins Automatically register plugins from
398
405
  # `@config.plugins.plugins`?
399
406
  # @return [void]
400
407
  def start(plugins = true)
408
+ @reconnects = 0
401
409
  register_plugins if plugins
402
- @logger.debug "Connecting to #{@config.server}:#{@config.port}"
403
- @irc = IRC.new(self, @config)
404
- @irc.connect
410
+
411
+ begin
412
+ @user_manager.each do |user|
413
+ user.in_whois = false
414
+ user.unsync_all
415
+ end # reset state of all users
416
+
417
+ @channel_manager.each do |channel|
418
+ channel.unsync_all
419
+ end # reset state of all channels
420
+
421
+ @logger.debug "Connecting to #{@config.server}:#{@config.port}"
422
+ @irc = IRC.new(self)
423
+ @irc.connect
424
+
425
+ if @config.reconnect && !@quitting
426
+ # double the delay for each unsuccesful reconnection attempt
427
+ if @last_connection_was_successful
428
+ @reconnects = 0
429
+ @last_connection_was_successful = false
430
+ else
431
+ @reconnects += 1
432
+ end
433
+
434
+ # Sleep for a few seconds before reconnecting to prevent being
435
+ # throttled by the IRC server
436
+ wait = 2**@reconnects
437
+ @logger.debug "Waiting #{wait} seconds before reconnecting"
438
+ sleep wait
439
+ end
440
+ end while @config.reconnect and not @quitting
405
441
  end
406
442
 
407
- # Register all plugins from `@config.plugins.plugins`.
443
+ # @endgroup
444
+ # @group Channel Control
445
+
446
+ # Join a channel.
408
447
  #
448
+ # @param [String, Channel] channel either the name of a channel or a {Channel} object
449
+ # @param [String] key optionally the key of the channel
409
450
  # @return [void]
410
- def register_plugins
411
- @config.plugins.plugins.each do |plugin|
412
- register_plugin(plugin)
413
- end
451
+ # @see Channel#join
452
+ def join(channel, key = nil)
453
+ Channel(channel).join(key)
414
454
  end
415
455
 
416
- # Registers a plugin.
456
+ # Part a channel.
417
457
  #
418
- # @param [Class<Plugin>] plugin The plugin class to register
458
+ # @param [String, Channel] channel either the name of a channel or a {Channel} object
459
+ # @param [String] reason an optional reason/part message
419
460
  # @return [void]
420
- def register_plugin(plugin)
421
- @plugins << plugin.new(self)
461
+ # @see Channel#part
462
+ def part(channel, reason = nil)
463
+ Channel(channel).part(reason)
464
+ end
465
+
466
+ # @endgroup
467
+
468
+ # (see Logger::Logger#debug)
469
+ # @see Logger::Logger#debug
470
+ def debug(msg)
471
+ @logger.debug(msg)
472
+ end
473
+
474
+ # @return [Boolean] True if the bot reports ISUPPORT violations as
475
+ # exceptions.
476
+ def strict?
477
+ @config.strictness == :strict
478
+ end
479
+
480
+ # @yield
481
+ def initialize(&b)
482
+ @logger = Logger::FormattedLogger.new($stderr)
483
+ @events = {}
484
+ @config = OpenStruct.new({
485
+ :server => "localhost",
486
+ :port => 6667,
487
+ :ssl => OpenStruct.new({
488
+ :use => false,
489
+ :verify => false,
490
+ :client_cert => nil,
491
+ :ca_path => "/etc/ssl/certs",
492
+ }),
493
+ :password => nil,
494
+ :nick => "cinch",
495
+ :nicks => nil,
496
+ :realname => "cinch",
497
+ :verbose => true,
498
+ :messages_per_second => 0.5,
499
+ :server_queue_size => 10,
500
+ :strictness => :forgiving,
501
+ :message_split_start => '... ',
502
+ :message_split_end => ' ...',
503
+ :max_messages => nil,
504
+ :plugins => OpenStruct.new({
505
+ :plugins => [],
506
+ :prefix => /^!/,
507
+ :suffix => nil,
508
+ :options => Hash.new {|h,k| h[k] = {}},
509
+ }),
510
+ :channels => [],
511
+ :encoding => :irc,
512
+ :reconnect => true,
513
+ :local_host => nil,
514
+ :timeouts => OpenStruct.new({
515
+ :read => 240,
516
+ :connect => 10,
517
+ }),
518
+ :ping_interval => 120,
519
+ })
520
+
521
+ @semaphores_mutex = Mutex.new
522
+ @semaphores = Hash.new { |h,k| h[k] = Mutex.new }
523
+ @plugins = []
524
+ @callback = Callback.new(self)
525
+ @channels = []
526
+ @handler_threads = []
527
+ @quitting = false
528
+
529
+ @user_manager = UserManager.new(self)
530
+ @channel_manager = ChannelManager.new(self)
531
+
532
+ on :connect do
533
+ bot.config.channels.each do |channel|
534
+ bot.join channel
535
+ end
536
+ end
537
+
538
+ instance_eval(&b) if block_given?
422
539
  end
423
540
 
541
+ # The bot's nickname.
542
+ # @overload nick=(new_nick)
543
+ # @raise [Exceptions::NickTooLong] Raised if the bot is
544
+ # operating in {#strict? strict mode} and the new nickname is
545
+ # too long
546
+ # @return [String]
547
+ # @overload nick
548
+ # @return [String]
549
+ # @return [String]
550
+ attr_accessor :nick
551
+ undef_method "nick"
552
+ undef_method "nick="
553
+ def nick
554
+ @config.nick
555
+ end
556
+
557
+ def nick=(new_nick)
558
+ if new_nick.size > @irc.isupport["NICKLEN"] && strict?
559
+ raise Exceptions::NickTooLong, new_nick
560
+ end
561
+ @config.nick = new_nick
562
+ raw "NICK #{new_nick}"
563
+ end
564
+
565
+ # Try to create a free nick, first by cycling through all
566
+ # available alternatives and then by appending underscores.
567
+ #
568
+ # @param [String] base The base nick to start trying from
424
569
  # @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
570
+ # @return String
571
+ def generate_next_nick(base = nil)
572
+ nicks = @config.nicks || []
573
+
574
+ if base
575
+ # if `base` is not in our list of nicks to try, assume that it's
576
+ # custom and just append an underscore
577
+ if !nicks.include?(base)
578
+ return base + "_"
579
+ else
580
+ # if we have a base, try the next nick or append an
581
+ # underscore if no more nicks are left
582
+ new_index = nicks.index(base) + 1
583
+ if nicks[new_index]
584
+ return nicks[new_index]
435
585
  else
436
- captures = []
586
+ return base + "_"
437
587
  end
438
-
439
- invoke(block, args, msg, captures)
440
588
  end
589
+ else
590
+ # if we have no base, try the first possible nick
591
+ new_nick = @config.nicks ? @config.nicks.first : @config.nick
592
+ end
593
+ end
594
+
595
+ # @return [Boolean] True if the bot is using SSL to connect to the
596
+ # server.
597
+ def secure?
598
+ @config[:ssl] == true || (@config[:ssl].is_a?(Hash) && @config[:ssl][:use])
599
+ end
600
+
601
+ # This method is only provided in order to give Bot and User a
602
+ # common interface.
603
+ #
604
+ # @return [false] Always returns `false`.
605
+ # @see User#unknown? See User#unknown? for the method's real use.
606
+ def unknown?
607
+ false
608
+ end
609
+
610
+ [:host, :mask, :user, :realname, :signed_on_at, :secure?].each do |attr|
611
+ define_method(attr) do
612
+ User(nick).__send__(attr)
441
613
  end
442
614
  end
443
615
 
@@ -450,28 +622,23 @@ module Cinch
450
622
 
451
623
  events.select { |regexps|
452
624
  regexps.first.any? { |regexp|
453
- msg.match(regexp, type)
625
+ msg.match(regexp.to_r(msg), type)
454
626
  }
455
627
  }
456
628
  end
457
629
  end
458
630
 
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
631
+ def invoke(block, args, msg, match, arguments)
632
+ bargs = match + arguments
633
+ @handler_threads << Thread.new do
469
634
  begin
470
635
  catch(:halt) do
471
636
  @callback.instance_exec(msg, *args, *bargs, &block)
472
637
  end
473
638
  rescue => e
474
639
  @logger.log_exception(e)
640
+ ensure
641
+ @handler_threads.delete Thread.current
475
642
  end
476
643
  end
477
644
  end