cinch 1.0.2 → 1.1.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 (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