cinch 1.1.3 → 2.0.0.pre.1

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 (83) hide show
  1. data/LICENSE +1 -0
  2. data/README.md +3 -3
  3. data/docs/bot_options.md +435 -0
  4. data/docs/changes.md +440 -0
  5. data/docs/common_mistakes.md +35 -0
  6. data/docs/common_tasks.md +47 -0
  7. data/docs/encodings.md +67 -0
  8. data/docs/events.md +272 -0
  9. data/docs/logging.md +5 -0
  10. data/docs/migrating.md +267 -0
  11. data/docs/readme.md +18 -0
  12. data/examples/plugins/custom_prefix.rb +1 -1
  13. data/examples/plugins/dice_roll.rb +38 -0
  14. data/examples/plugins/lambdas.rb +1 -1
  15. data/examples/plugins/memo.rb +16 -10
  16. data/examples/plugins/url_shorten.rb +1 -0
  17. data/lib/cinch.rb +5 -60
  18. data/lib/cinch/ban.rb +13 -7
  19. data/lib/cinch/bot.rb +228 -403
  20. data/lib/cinch/{cache_manager.rb → cached_list.rb} +5 -1
  21. data/lib/cinch/callback.rb +3 -0
  22. data/lib/cinch/channel.rb +119 -195
  23. data/lib/cinch/{channel_manager.rb → channel_list.rb} +6 -3
  24. data/lib/cinch/configuration.rb +73 -0
  25. data/lib/cinch/configuration/bot.rb +47 -0
  26. data/lib/cinch/configuration/dcc.rb +16 -0
  27. data/lib/cinch/configuration/plugins.rb +41 -0
  28. data/lib/cinch/configuration/sasl.rb +17 -0
  29. data/lib/cinch/configuration/ssl.rb +19 -0
  30. data/lib/cinch/configuration/storage.rb +37 -0
  31. data/lib/cinch/configuration/timeouts.rb +14 -0
  32. data/lib/cinch/constants.rb +531 -369
  33. data/lib/cinch/dcc.rb +12 -0
  34. data/lib/cinch/dcc/dccable_object.rb +37 -0
  35. data/lib/cinch/dcc/incoming.rb +1 -0
  36. data/lib/cinch/dcc/incoming/send.rb +131 -0
  37. data/lib/cinch/dcc/outgoing.rb +1 -0
  38. data/lib/cinch/dcc/outgoing/send.rb +115 -0
  39. data/lib/cinch/exceptions.rb +8 -1
  40. data/lib/cinch/formatting.rb +106 -0
  41. data/lib/cinch/handler.rb +104 -0
  42. data/lib/cinch/handler_list.rb +86 -0
  43. data/lib/cinch/helpers.rb +167 -10
  44. data/lib/cinch/irc.rb +525 -110
  45. data/lib/cinch/isupport.rb +11 -9
  46. data/lib/cinch/logger.rb +168 -0
  47. data/lib/cinch/logger/formatted_logger.rb +72 -55
  48. data/lib/cinch/logger/zcbot_logger.rb +9 -24
  49. data/lib/cinch/logger_list.rb +62 -0
  50. data/lib/cinch/mask.rb +19 -10
  51. data/lib/cinch/message.rb +94 -28
  52. data/lib/cinch/message_queue.rb +70 -28
  53. data/lib/cinch/mode_parser.rb +6 -1
  54. data/lib/cinch/network.rb +104 -0
  55. data/lib/cinch/{rubyext/queue.rb → open_ended_queue.rb} +8 -1
  56. data/lib/cinch/pattern.rb +24 -4
  57. data/lib/cinch/plugin.rb +352 -177
  58. data/lib/cinch/plugin_list.rb +35 -0
  59. data/lib/cinch/rubyext/float.rb +3 -0
  60. data/lib/cinch/rubyext/module.rb +7 -0
  61. data/lib/cinch/rubyext/string.rb +9 -0
  62. data/lib/cinch/sasl.rb +34 -0
  63. data/lib/cinch/sasl/dh_blowfish.rb +71 -0
  64. data/lib/cinch/sasl/diffie_hellman.rb +47 -0
  65. data/lib/cinch/sasl/mechanism.rb +6 -0
  66. data/lib/cinch/sasl/plain.rb +26 -0
  67. data/lib/cinch/storage.rb +62 -0
  68. data/lib/cinch/storage/null.rb +12 -0
  69. data/lib/cinch/storage/yaml.rb +96 -0
  70. data/lib/cinch/syncable.rb +13 -1
  71. data/lib/cinch/target.rb +144 -0
  72. data/lib/cinch/timer.rb +145 -0
  73. data/lib/cinch/user.rb +169 -225
  74. data/lib/cinch/{user_manager.rb → user_list.rb} +7 -2
  75. data/lib/cinch/utilities/deprecation.rb +12 -0
  76. data/lib/cinch/utilities/encoding.rb +54 -0
  77. data/lib/cinch/utilities/kernel.rb +13 -0
  78. data/lib/cinch/utilities/string.rb +13 -0
  79. data/lib/cinch/version.rb +4 -0
  80. metadata +88 -47
  81. data/lib/cinch/logger/logger.rb +0 -44
  82. data/lib/cinch/logger/null_logger.rb +0 -18
  83. data/lib/cinch/rubyext/infinity.rb +0 -1
@@ -0,0 +1,86 @@
1
+ require "thread"
2
+ require "set"
3
+ require "cinch/cached_list"
4
+
5
+ module Cinch
6
+ # @since 2.0.0
7
+ class HandlerList
8
+ include Enumerable
9
+
10
+ def initialize
11
+ @handlers = Hash.new {|h,k| h[k] = []}
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ def register(handler)
16
+ @mutex.synchronize do
17
+ handler.bot.loggers.debug "[on handler] Registering handler with pattern `#{handler.pattern.inspect}`, reacting on `#{handler.event}`"
18
+ @handlers[handler.event] << handler
19
+ end
20
+ end
21
+
22
+ # @param [Handler, Array<Handler>] *handlers The handlers to unregister
23
+ # @return [void]
24
+ # @see Handler#unregister
25
+ def unregister(*handlers)
26
+ @mutex.synchronize do
27
+ handlers.each do |handler|
28
+ @handlers[handler.event].delete(handler)
29
+ end
30
+ end
31
+ end
32
+
33
+ # @api private
34
+ # @return [Array<Handler>]
35
+ def find(type, msg = nil)
36
+ if handlers = @handlers[type]
37
+ if msg.nil?
38
+ return handlers
39
+ end
40
+
41
+ handlers = handlers.select { |handler|
42
+ msg.match(handler.pattern.to_r(msg), type)
43
+ }.group_by {|handler| handler.group}
44
+
45
+ handlers.values_at(*(handlers.keys - [nil])).map(&:first) + (handlers[nil] || [])
46
+ end
47
+ end
48
+
49
+ # @param [Symbol] event The event type
50
+ # @param [Message, nil] msg The message which is responsible for
51
+ # and attached to the event, or nil.
52
+ # @param [Array] *arguments A list of additional arguments to pass
53
+ # to event handlers
54
+ # @return [void]
55
+ def dispatch(event, msg = nil, *arguments)
56
+ if handlers = find(event, msg)
57
+ already_run = Set.new
58
+ handlers.each do |handler|
59
+ next if already_run.include?(handler.block)
60
+ already_run << handler.block
61
+ # calling Message#match multiple times is not a problem
62
+ # because we cache the result
63
+ if msg
64
+ captures = msg.match(handler.pattern.to_r(msg), event).captures
65
+ else
66
+ captures = []
67
+ end
68
+
69
+ handler.call(msg, captures, arguments)
70
+ end
71
+ end
72
+ end
73
+
74
+ # @yield [handler] Yields all registered handlers
75
+ # @yieldparam [Handler] handler
76
+ # @return [void]
77
+ def each(&block)
78
+ @handlers.values.flatten.each(&block)
79
+ end
80
+
81
+ # @api private
82
+ def stop_all
83
+ each { |h| h.stop }
84
+ end
85
+ end
86
+ end
@@ -1,21 +1,178 @@
1
+ # TODO @since for all methods
1
2
  module Cinch
3
+ # The Helpers module contains a number of methods whose purpose is
4
+ # to make writing plugins easier by hiding parts of the API. The
5
+ # {#Channel} helper, for example, provides an easier way for turning
6
+ # a String object into a {Channel} object than directly using
7
+ # {ChannelList}: Compare `Channel("#some_channel")` with
8
+ # `bot.channel_list.find_ensured("#some_channel")`.
9
+ #
10
+ # The Helpers module automatically gets included in all plugins.
2
11
  module Helpers
12
+ # @group Type casts
13
+
14
+ # Helper method for turning a String into a {Target} object.
15
+ #
16
+ # @param [String] target a target name
17
+ # @return [Target] a {Target} object
18
+ # @example
19
+ # on :message, /^message (.+)$/ do |m, target|
20
+ # Target(target).send "hi!"
21
+ # end
22
+ def Target(target)
23
+ return target if target.is_a?(Target)
24
+ Target.new(target, bot)
25
+ end
26
+
3
27
  # Helper method for turning a String into a {Channel} object.
4
28
  #
5
- # @param (see Bot#Channel)
6
- # @return (see Bot#Channel)
7
- # @example (see Bot#Channel)
8
- def Channel(*args)
9
- @bot.Channel(*args)
29
+ # @param [String] channel a channel name
30
+ # @return [Channel] a {Channel} object
31
+ # @example
32
+ # on :message, /^please join (#.+)$/ do |m, target|
33
+ # Channel(target).join
34
+ # end
35
+ def Channel(channel)
36
+ return channel if channel.is_a?(Channel)
37
+ bot.channel_list.find_ensured(channel)
10
38
  end
11
39
 
12
40
  # Helper method for turning a String into an {User} object.
13
41
  #
14
- # @param (see Bot#User)
15
- # @return (see Bot#User)
16
- # @example (see Bot#User)
17
- def User(*args)
18
- @bot.User(*args)
42
+ # @param [String] user a user's nickname
43
+ # @return [User] an {User} object
44
+ # @example
45
+ # on :message, /^tell me everything about (.+)$/ do |m, target|
46
+ # user = User(target)
47
+ # m.reply "%s is named %s and connects from %s" % [user.nick, user.name, user.host]
48
+ # end
49
+ def User(user)
50
+ return user if user.is_a?(User)
51
+ if user == bot.nick
52
+ bot
53
+ else
54
+ bot.user_list.find_ensured(user)
55
+ end
56
+ end
57
+
58
+ # @example Used as a class method in a plugin
59
+ # timer 5, method: :some_method
60
+ # def some_method
61
+ # Channel("#cinch-bots").send(Time.now.to_s)
62
+ # end
63
+ #
64
+ # @example Used as an instance method in a plugin
65
+ # match "start timer"
66
+ # def execute(m)
67
+ # Timer(5) { puts "timer fired" }
68
+ # end
69
+ #
70
+ # @example Used as an instance method in a traditional `on` handler
71
+ # on :message, "start timer" do
72
+ # Timer(5) { puts "timer fired" }
73
+ # end
74
+ #
75
+ # @param [Number] interval Interval in seconds
76
+ # @param [Proc] block A proc to execute
77
+ # @option options [Symbol] :method (:timer) Method to call (only
78
+ # if no proc is provided)
79
+ # @option options [Boolean] :threaded (true) Call method in a
80
+ # thread?
81
+ # @option options [Number] :shots (Float::INFINITY) How often
82
+ # should the timer fire?
83
+ # @option options [Boolean] :start_automatically (true) If true,
84
+ # the timer will automatically start after the bot finished
85
+ # connecting.
86
+ # @option options [Boolean] :stop_automaticall (true) If true, the
87
+ # timer will automatically stop when the bot disconnects.
88
+ # @return [Timer]
89
+ # @since 2.0.0
90
+ def Timer(interval, options = {}, &block)
91
+ options = {:method => :timer, :threaded => true, :interval => interval}.merge(options)
92
+ block ||= self.method(options[:method])
93
+ timer = Cinch::Timer.new(bot, options, &block)
94
+ timer.start
95
+
96
+ timer
97
+ end
98
+
99
+ # @endgroup
100
+
101
+ # @group Logging
102
+
103
+ # Use this method to automatically log exceptions to the loggers.
104
+ #
105
+ # @example
106
+ # def my_method
107
+ # rescue_exception do
108
+ # something_that_might_raise()
109
+ # end
110
+ # end
111
+ #
112
+ # @return [void]
113
+ # @since 2.0.0
114
+ def rescue_exception
115
+ begin
116
+ yield
117
+ rescue => e
118
+ bot.loggers.exception(e)
119
+ end
120
+ end
121
+
122
+ # (see Logger#log)
123
+ def log(messages, event = :debug, level = event)
124
+ @bot.loggers.log(messages, event, level)
125
+ end
126
+
127
+ # (see Logger#debug)
128
+ def debug(message)
129
+ log(message, :debug)
130
+ end
131
+
132
+ # (see Logger#error)
133
+ def error(message)
134
+ log(message, :error)
135
+ end
136
+
137
+ # (see Logger#fatal)
138
+ def fatal(message)
139
+ log(message, :fatal)
19
140
  end
141
+
142
+ # (see Logger#info)
143
+ def info(message)
144
+ log(message, :info)
145
+ end
146
+
147
+ # (see Logger#warn)
148
+ def warn(message)
149
+ log(message, :warn)
150
+ end
151
+
152
+ # (see Logger#incoming)
153
+ def incoming(message)
154
+ log(message, :incoming, :log)
155
+ end
156
+
157
+ # (see Logger#outgoing)
158
+ def outgoing(message)
159
+ log(message, :outgoing, :log)
160
+ end
161
+
162
+ # (see Logger#exception)
163
+ def exception(e)
164
+ log(e.message, :exception, :error)
165
+ end
166
+ # @endgroup
167
+
168
+ # @group Formatting
169
+
170
+ # (see Formatting.format)
171
+ def Format(*args)
172
+ Formatting.format(*args)
173
+ end
174
+ alias_method :Color, :Format
175
+
176
+ # @endgroup
20
177
  end
21
178
  end
@@ -1,121 +1,227 @@
1
1
  require "timeout"
2
2
  require "net/protocol"
3
+ require "cinch/network"
3
4
 
4
5
  module Cinch
6
+ # This class manages the connection to the IRC server. That includes
7
+ # processing incoming and outgoing messages, creating Ruby objects
8
+ # and invoking plugins.
5
9
  class IRC
10
+ include Helpers
11
+
6
12
  # @return [ISupport]
7
13
  attr_reader :isupport
14
+
15
+ # @return [Bot]
16
+ attr_reader :bot
17
+
18
+ # @return [Network] The detected network
19
+ attr_reader :network
20
+
8
21
  def initialize(bot)
9
22
  @bot = bot
10
23
  @isupport = ISupport.new
11
24
  end
12
25
 
13
- # Establish a connection.
14
- #
15
- # @return [void]
16
- def connect
17
- @registration = []
26
+ # @return [TCPSocket]
27
+ # @api private
28
+ # @since 2.0.0
29
+ def socket
30
+ @socket.io
31
+ end
18
32
 
33
+ # @api private
34
+ # @return [void]
35
+ # @since 2.0.0
36
+ def setup
37
+ @registration = []
38
+ @network = Network.new(:unknown, :unknown)
19
39
  @whois_updates = Hash.new {|h, k| h[k] = {}}
20
40
  @in_lists = Set.new
41
+ end
21
42
 
43
+ # @api private
44
+ # @return [Boolean] True if the connection could be established
45
+ def connect
22
46
  tcp_socket = nil
47
+
23
48
  begin
24
49
  Timeout::timeout(@bot.config.timeouts.connect) do
25
50
  tcp_socket = TCPSocket.new(@bot.config.server, @bot.config.port, @bot.config.local_host)
26
51
  end
27
52
  rescue Timeout::Error
28
- @bot.logger.debug("Timed out while connecting")
29
- return
53
+ @bot.loggers.warn("Timed out while connecting")
54
+ return false
55
+ rescue SocketError => e
56
+ @bot.loggers.warn("Could not connect to the IRC server. Please check your network: #{e.message}")
57
+ return false
30
58
  rescue => e
31
- @bot.logger.log_exception(e)
32
- return
59
+ @bot.loggers.exception(e)
60
+ return false
33
61
  end
34
62
 
35
- if @bot.config.ssl == true || @bot.config.ssl == false
36
- @bot.logger.debug "Deprecation warning: Beginning from version 1.1.0, @config.ssl should be a set of options, not a boolean value!"
63
+ if @bot.config.ssl.use
64
+ setup_ssl(tcp_socket)
65
+ else
66
+ @socket = tcp_socket
37
67
  end
38
68
 
39
- if @bot.config.ssl == true || (@bot.config.ssl.is_a?(OpenStruct) && @bot.config.ssl.use)
40
- require 'openssl'
69
+ @socket = Net::BufferedIO.new(@socket)
70
+ @socket.read_timeout = @bot.config.timeouts.read
71
+ @queue = MessageQueue.new(@socket, @bot)
41
72
 
42
- ssl_context = OpenSSL::SSL::SSLContext.new
73
+ return true
74
+ end
43
75
 
44
- if @bot.config.ssl.is_a?(OpenStruct)
45
- if @bot.config.ssl.client_cert
46
- ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(@bot.config.ssl.client_cert))
47
- ssl_context.key = OpenSSL::PKey::RSA.new(File.read(@bot.config.ssl.client_cert))
48
- end
49
- ssl_context.ca_path = @bot.config.ssl.ca_path
50
- ssl_context.verify_mode = @bot.config.ssl.verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
51
- else
52
- ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
76
+ # @api private
77
+ # @return [void]
78
+ # @since 2.0.0
79
+ def setup_ssl(socket)
80
+ # require openssl in this method so the bot doesn't break for
81
+ # people who don't have SSL but don't want to use SSL anyway.
82
+ require 'openssl'
83
+
84
+ ssl_context = OpenSSL::SSL::SSLContext.new
85
+
86
+ if @bot.config.ssl.is_a?(Configuration::SSL)
87
+ if @bot.config.ssl.client_cert
88
+ ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(@bot.config.ssl.client_cert))
89
+ ssl_context.key = OpenSSL::PKey::RSA.new(File.read(@bot.config.ssl.client_cert))
53
90
  end
54
- @bot.logger.debug "Using SSL with #{@bot.config.server}:#{@bot.config.port}"
55
91
 
56
- @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
57
- @socket.sync = true
58
- @socket.connect
92
+ ssl_context.ca_path = @bot.config.ssl.ca_path
93
+ ssl_context.verify_mode = @bot.config.ssl.verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
59
94
  else
60
- @socket = tcp_socket
95
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
61
96
  end
97
+ @bot.loggers.info "Using SSL with #{@bot.config.server}:#{@bot.config.port}"
62
98
 
63
- @socket = Net::BufferedIO.new(@socket)
64
- @socket.read_timeout = @bot.config.timeouts.read
99
+ @socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
100
+ @socket.sync = true
101
+ @socket.connect
102
+ end
65
103
 
66
- @queue = MessageQueue.new(@socket, @bot)
67
- message "PASS #{@bot.config.password}" if @bot.config.password
68
- message "NICK #{@bot.generate_next_nick}"
69
- message "USER #{@bot.config.user} 0 * :#{@bot.config.realname}"
104
+ # @api private
105
+ # @return [void]
106
+ # @since 2.0.0
107
+ def send_cap_ls
108
+ send "CAP LS"
109
+ end
110
+
111
+ # @api private
112
+ # @return [void]
113
+ # @since 2.0.0
114
+ def send_cap_req
115
+ send "CAP REQ :" + ([:"away-notify", :"multi-prefix", :sasl] & @network.capabilities).join(" ")
116
+ end
117
+
118
+ # @since 2.0.0
119
+ # @api private
120
+ # @return [void]
121
+ def send_cap_end
122
+ send "CAP END"
123
+ end
124
+
125
+ # @api private
126
+ # @return [void]
127
+ # @since 2.0.0
128
+ def send_login
129
+ send "PASS #{@bot.config.password}" if @bot.config.password
130
+ send "NICK #{@bot.generate_next_nick!}"
131
+ send "USER #{@bot.config.user} 0 * :#{@bot.config.realname}"
132
+ end
70
133
 
71
- reading_thread = Thread.new do
134
+ # @api private
135
+ # @return [Thread] the reading thread
136
+ # @since 2.0.0
137
+ def start_reading_thread
138
+ Thread.new do
72
139
  begin
73
140
  while line = @socket.readline
74
- begin
75
- line = Cinch.encode_incoming(line, @bot.config.encoding)
141
+ rescue_exception do
142
+ line = Cinch::Utilities::Encoding.encode_incoming(line, @bot.config.encoding)
76
143
  parse line
77
- rescue => e
78
- @bot.logger.log_exception(e)
79
144
  end
80
145
  end
81
146
  rescue Timeout::Error
82
- @bot.logger.debug "Connection timed out."
147
+ @bot.loggers.warn "Connection timed out."
83
148
  rescue EOFError
84
- @bot.logger.debug "Lost connection."
149
+ @bot.loggers.warn "Lost connection."
85
150
  rescue => e
86
- @bot.logger.log_exception(e)
151
+ @bot.loggers.exception(e)
87
152
  end
88
153
 
89
154
  @socket.close
90
- @bot.dispatch(:disconnect)
91
- @bot.handler_threads.each { |t| t.join(10); t.kill }
155
+ @bot.handlers.dispatch(:disconnect)
156
+ # FIXME won't we kill all :disconnect handlers here? prolly
157
+ # not, as they have 10 seconds to finish. that should be
158
+ # plenty of time
159
+ @bot.handlers.stop_all
92
160
  end
161
+ end
93
162
 
94
- @sending_thread = Thread.new do
95
- begin
163
+ # @api private
164
+ # @return [Thread] the sending thread
165
+ # @since 2.0.0
166
+ def start_sending_thread
167
+ Thread.new do
168
+ rescue_exception do
96
169
  @queue.process!
97
- rescue => e
98
- @bot.logger.log_exception(e)
99
170
  end
100
171
  end
172
+ end
101
173
 
102
- ping_thread = Thread.new do
174
+ # @api private
175
+ # @return [Thread] The ping thread.
176
+ # @since 2.0.0
177
+ def start_ping_thread
178
+ Thread.new do
103
179
  while true
104
180
  sleep @bot.config.ping_interval
105
- message("PING 0") # PING requires a single argument. In our
106
- # case the value doesn't matter though.
181
+ # PING requires a single argument. In our case the value
182
+ # doesn't matter though.
183
+ send("PING 0")
107
184
  end
108
185
  end
186
+ end
109
187
 
110
- reading_thread.join
111
- @sending_thread.kill
112
- ping_thread.kill
188
+ # @since 2.0.0
189
+ def send_sasl
190
+ if @bot.config.sasl.username && @sasl_current_method = @sasl_remaining_methods.pop
191
+ @bot.loggers.info "[SASL] Trying to authenticate with #{@sasl_current_method.mechanism_name}"
192
+ send "AUTHENTICATE #{@sasl_current_method.mechanism_name}"
193
+ else
194
+ send_cap_end
195
+ end
196
+ end
197
+
198
+ # Establish a connection.
199
+ #
200
+ # @return [void]
201
+ # @since 2.0.0
202
+ def start
203
+ setup
204
+ if connect
205
+ @sasl_remaining_methods = [SASL::Plain, SASL::DH_Blowfish]
206
+ send_cap_ls
207
+ send_login
208
+
209
+ reading_thread = start_reading_thread
210
+ sending_thread = start_sending_thread
211
+ ping_thread = start_ping_thread
212
+
213
+ reading_thread.join
214
+ sending_thread.kill
215
+ ping_thread.kill
216
+ end
113
217
  end
114
218
 
115
219
  # @api private
116
220
  # @return [void]
117
221
  def parse(input)
118
- @bot.logger.log(input, :incoming) if @bot.config.verbose
222
+ return if input.chomp.empty?
223
+ @bot.loggers.incoming(input)
224
+
119
225
  msg = Message.new(input, @bot)
120
226
  events = [[:catchall]]
121
227
 
@@ -125,7 +231,9 @@ module Cinch
125
231
  events << [:connect]
126
232
  @bot.last_connection_was_successful = true
127
233
  end
128
- elsif ["PRIVMSG", "NOTICE"].include?(msg.command)
234
+ end
235
+
236
+ if ["PRIVMSG", "NOTICE"].include?(msg.command)
129
237
  events << [:ctcp] if msg.ctcp?
130
238
  if msg.channel?
131
239
  events << [:channel]
@@ -134,24 +242,28 @@ module Cinch
134
242
  end
135
243
 
136
244
  if msg.command == "PRIVMSG"
137
- events << [:message] << [:privmsg]
245
+ events << [:message]
138
246
  else
139
247
  events << [:notice]
140
248
  end
141
- else
142
- meth = "on_#{msg.command.downcase}"
143
- __send__(meth, msg, events) if respond_to?(meth, true)
144
249
 
145
- if msg.error?
146
- events << [:error]
147
- else
148
- events << [msg.command.downcase.to_sym]
250
+ if msg.action?
251
+ events << [:action]
149
252
  end
150
253
  end
151
254
 
152
- msg.instance_variable_set(:@events, events.map(&:first))
255
+ meth = "on_#{msg.command.downcase}"
256
+ __send__(meth, msg, events) if respond_to?(meth, true)
257
+
258
+ if msg.error?
259
+ events << [:error]
260
+ else
261
+ events << [msg.command.downcase.to_sym]
262
+ end
263
+
264
+ msg.events = events.map(&:first)
153
265
  events.each do |event, *args|
154
- @bot.dispatch(event, msg, *args)
266
+ @bot.handlers.dispatch(event, msg, *args)
155
267
  end
156
268
  end
157
269
 
@@ -160,52 +272,173 @@ module Cinch
160
272
  (("001".."004").to_a - @registration).empty?
161
273
  end
162
274
 
163
- # Send a message.
275
+ # Send a message to the server.
276
+ # @param [String] msg
164
277
  # @return [void]
165
- def message(msg)
278
+ def send(msg)
166
279
  @queue.queue(msg)
167
280
  end
168
281
 
169
282
  private
283
+ def set_leaving_user(message, user, events)
284
+ events << [:leaving, user]
285
+ end
286
+
287
+ # @since 2.0.0
288
+ def detect_network(msg, event)
289
+ old_network = @network
290
+ new_network = nil
291
+ new_ircd = nil
292
+ case event
293
+ when "002"
294
+ if msg.params.last =~ /^Your host is .+?, running version (.+)$/
295
+ case $1
296
+ when /\+snircd\(/
297
+ new_ircd = :snircd
298
+ when /^u[\d\.]+$/
299
+ new_ircd = :ircu
300
+ when /^(.+?)-?\d+/
301
+ new_ircd = $1.downcase.to_sym
302
+ end
303
+ elsif msg.params.last == "Your host is jtvchat"
304
+ new_network = :jtv
305
+ new_ircd = :jtv
306
+ end
307
+ when "005"
308
+ case @isupport["NETWORK"]
309
+ when "NGameTV"
310
+ new_network = :ngametv
311
+ new_ircd = :ngametv
312
+ when nil
313
+ else
314
+ new_network = @isupport["NETWORK"].downcase.to_sym
315
+ end
316
+ end
317
+
318
+ new_network ||= old_network.name
319
+ new_ircd ||= old_network.ircd
320
+
321
+ if old_network.unknown_ircd? && new_ircd != :unknown
322
+ @bot.loggers.info "Detected IRCd: #{new_ircd}"
323
+ end
324
+ if !old_network.unknown_ircd? && new_ircd != old_network.ircd
325
+ @bot.loggers.info "Detected different IRCd: #{old_network.ircd} -> #{new_ircd}"
326
+ end
327
+ if old_network.unknown_network? && new_network != :unknown
328
+ @bot.loggers.info "Detected network: #{new_network}"
329
+ end
330
+ if !old_network.unknown_network? && new_network != old_network.name
331
+ @bot.loggers.info "Detected different network: #{old_network.name} -> #{new_network}"
332
+ end
333
+
334
+ @network.name = new_network
335
+ @network.ircd = new_ircd
336
+ end
337
+
338
+ def process_ban_mode(msg, events, param, direction)
339
+ mask = param
340
+ ban = Ban.new(mask, msg.user, Time.now)
341
+
342
+ if direction == :add
343
+ msg.channel.bans_unsynced << ban
344
+ events << [:ban, ban]
345
+ else
346
+ msg.channel.bans_unsynced.delete_if {|b| b.mask == ban.mask}.first
347
+ events << [:unban, ban]
348
+ end
349
+ end
350
+
351
+ def process_owner_mode(msg, events, param, direction)
352
+ owner = User(param)
353
+ if direction == :add
354
+ msg.channel.owners_unsynced << owner unless msg.channel.owners_unsynced.include?(owner)
355
+ events << [:owner, owner]
356
+ else
357
+ msg.channel.owners_unsynced.delete(owner)
358
+ events << [:deowner, owner]
359
+ end
360
+ end
361
+
362
+ # @since 2.0.0
363
+ def on_away(msg, events)
364
+ if msg.message.to_s.empty?
365
+ # unaway
366
+ m.user.sync(:away, nil, true)
367
+ events << [:unaway]
368
+ else
369
+ # away
370
+ m.user.sync(:away, msg.message, true)
371
+ events << [:away]
372
+ end
373
+ end
374
+
375
+ # @since 2.0.0
376
+ def on_cap(msg, events)
377
+ case msg.params[1]
378
+ when "LS"
379
+ @network.capabilities.concat msg.message.split(" ").map(&:to_sym)
380
+ send_cap_req
381
+ when "ACK"
382
+ if @network.capabilities.include?(:sasl)
383
+ send_sasl
384
+ else
385
+ send_cap_end
386
+ end
387
+ when "NACK"
388
+ send_cap_end
389
+ end
390
+ end
391
+
170
392
  def on_join(msg, events)
171
393
  if msg.user == @bot
172
394
  @bot.channels << msg.channel
173
395
  msg.channel.sync_modes
174
396
  end
175
397
  msg.channel.add_user(msg.user)
398
+ msg.user.online = true
176
399
  end
177
400
 
178
401
  def on_kick(msg, events)
179
- target = @bot.user_manager.find_ensured(msg.params[1])
402
+ target = User(msg.params[1])
180
403
  if target == @bot
181
404
  @bot.channels.delete(msg.channel)
182
405
  end
183
406
  msg.channel.remove_user(target)
407
+
408
+ set_leaving_user(msg, target, events)
184
409
  end
185
410
 
186
411
  def on_kill(msg, events)
187
- user = @bot.user_manager.find_ensured(msg.params[1])
188
- @bot.channel_manager.each do |channel|
412
+ user = User(msg.params[1])
413
+
414
+ @bot.channel_list.each do |channel|
189
415
  channel.remove_user(user)
190
416
  end
417
+
191
418
  user.unsync_all
192
- @bot.user_manager.delete(user)
419
+ user.online = false
420
+
421
+ set_leaving_user(msg, user, events)
193
422
  end
194
423
 
424
+ # @version 1.1.0
195
425
  def on_mode(msg, events)
196
426
  if msg.channel?
197
427
  add_and_remove = @bot.irc.isupport["CHANMODES"]["A"] + @bot.irc.isupport["CHANMODES"]["B"] + @bot.irc.isupport["PREFIX"].keys
198
428
 
199
- param_modes = {:add => @bot.irc.isupport["CHANMODES"]["C"] + add_and_remove,
200
- :remove => add_and_remove}
429
+ param_modes = {
430
+ :add => @bot.irc.isupport["CHANMODES"]["C"] + add_and_remove,
431
+ :remove => add_and_remove
432
+ }
201
433
 
202
434
  modes = ModeParser.parse_modes(msg.params[1], msg.params[2..-1], param_modes)
203
435
  modes.each do |direction, mode, param|
204
436
  if @bot.irc.isupport["PREFIX"].keys.include?(mode)
205
- target = @bot.User(param)
437
+ target = User(param)
438
+
206
439
  # (un)set a user-mode
207
440
  if direction == :add
208
- msg.channel.users[target] << mode unless msg.channel.users[@bot.User(param)].include?(mode)
441
+ msg.channel.users[target] << mode unless msg.channel.users[User(param)].include?(mode)
209
442
  else
210
443
  msg.channel.users[target].delete mode
211
444
  end
@@ -222,23 +455,16 @@ module Cinch
222
455
  elsif @bot.irc.isupport["CHANMODES"]["A"].include?(mode)
223
456
  case mode
224
457
  when "b"
225
- mask = param
226
- ban = Ban.new(mask, msg.user, Time.now)
227
-
228
- if direction == :add
229
- msg.channel.bans_unsynced << ban
230
- events << [:ban, ban]
231
- else
232
- msg.channel.bans_unsynced.delete_if {|b| b.mask == ban.mask}.first
233
- events << [:unban, ban]
234
- end
458
+ process_ban_mode(msg, events, param, direction)
459
+ when "q"
460
+ process_owner_mode(msg, events, param, direction) if @network.owner_list_mode
235
461
  else
236
- raise UnsupportedFeature, mode
462
+ raise Exceptions::UnsupportedMode, mode
237
463
  end
238
464
  else
239
465
  # channel options
240
466
  if direction == :add
241
- msg.channel.modes_unsynced[mode] = param
467
+ msg.channel.modes_unsynced[mode] = param.nil? ? true : param
242
468
  else
243
469
  msg.channel.modes_unsynced.delete(mode)
244
470
  end
@@ -246,26 +472,40 @@ module Cinch
246
472
  end
247
473
 
248
474
  events << [:mode_change, modes]
475
+ elsif msg.params.first == bot.nick
476
+ modes = ModeParser.parse_modes(msg.params[1], msg.params[2..-1])
477
+ modes.each do |direction, mode, _|
478
+ if direction == :add
479
+ @bot.modes << mode
480
+ else
481
+ @bot.modes.delete(mode)
482
+ end
483
+ end
249
484
  end
250
485
  end
251
486
 
252
487
  def on_nick(msg, events)
253
488
  if msg.user == @bot
254
- @bot.config.nick = msg.params.last
489
+ @bot.set_nick msg.params.last
255
490
  end
256
491
 
257
492
  msg.user.update_nick(msg.params.last)
493
+ msg.user.online = true
258
494
  end
259
495
 
260
496
  def on_part(msg, events)
261
497
  msg.channel.remove_user(msg.user)
498
+ msg.user.channels_unsynced.delete msg.channel
499
+
262
500
  if msg.user == @bot
263
501
  @bot.channels.delete(msg.channel)
264
502
  end
503
+
504
+ set_leaving_user(msg, msg.user, events)
265
505
  end
266
506
 
267
507
  def on_ping(msg, events)
268
- message "PONG :#{msg.params.first}"
508
+ send "PONG :#{msg.params.first}"
269
509
  end
270
510
 
271
511
  def on_topic(msg, events)
@@ -273,27 +513,87 @@ module Cinch
273
513
  end
274
514
 
275
515
  def on_quit(msg, events)
276
- @bot.channel_manager.each do |channel|
516
+ @bot.channel_list.each do |channel|
277
517
  channel.remove_user(msg.user)
278
518
  end
279
519
  msg.user.unsync_all
280
- @bot.user_manager.delete(msg.user)
520
+ msg.user.online = true
521
+
522
+ set_leaving_user(msg, msg.user, events)
523
+
524
+ if msg.message.downcase == "excess flood" && msg.user == @bot
525
+ @bot.warn ["Looks like your bot has been kicked because of excess flood.",
526
+ "If you haven't modified the throttling options manually, please file a bug report at https://github.com/cinchrb/cinch/issues and include the following information:",
527
+ "- Server: #{@bot.config.server}",
528
+ "- Messages per second: #{@bot.config.messages_per_second}",
529
+ "- Server queue size: #{@bot.config.server_queue_size}"]
530
+ end
531
+ end
532
+
533
+ # @since 2.0.0
534
+ def on_privmsg(msg, events)
535
+ if msg.user
536
+ msg.user.online = true
537
+ end
538
+
539
+
540
+ if msg.message =~ /^\001DCC SEND (\S+) (\d+) (\d+)(?: (\d+))?\001$/
541
+ process_dcc_send($1, $2, $3, $4, msg, events)
542
+ end
543
+ end
544
+
545
+ # @since 2.0.0
546
+ def process_dcc_send(filename, ip, port, size, m, events)
547
+ ip = ip.to_i
548
+ ip = [24, 16, 8, 0].collect {|b| (ip >> b) & 255}.join('.')
549
+ port = port.to_i
550
+ size = size.to_i
551
+
552
+ @bot.loggers.info "DCC: Incoming DCC SEND: File name: %s - Size: %dB - IP: %s - Port: %d" % [filename, size, ip, port]
553
+
554
+ dcc = DCC::Incoming::Send.new(user: m.user, filename: filename, size: size, ip: ip, port: port)
555
+ events << [:dcc_send, dcc]
556
+ end
557
+
558
+ # @since 2.0.0
559
+ def on_001(msg, events)
560
+ # Ensure that we know our real, possibly truncated or otherwise
561
+ # modified nick.
562
+ @bot.set_nick msg.params.first
563
+ end
564
+
565
+ # @since 2.0.0
566
+ def on_002(msg, events)
567
+ detect_network(msg, "002")
281
568
  end
282
569
 
283
570
  def on_005(msg, events)
284
571
  # ISUPPORT
285
572
  @isupport.parse(*msg.params[1..-2].map {|v| v.split(" ")}.flatten)
573
+ detect_network(msg, "005")
574
+ end
575
+
576
+ # @since 2.0.0
577
+ def on_301(msg, events)
578
+ # RPL_AWAY
579
+ user = User(msg.params.first)
580
+ away = msg.message
581
+
582
+ if @whois_updates[user]
583
+ @whois_updates[user].merge!({:away => away})
584
+ end
286
585
  end
287
586
 
587
+ # @since 1.1.0
288
588
  def on_307(msg, events)
289
589
  # RPL_WHOISREGNICK
290
- user = @bot.user_manager.find_ensured(msg.params[1])
590
+ user = User(msg.params[1])
291
591
  @whois_updates[user].merge!({:authname => user.nick})
292
592
  end
293
593
 
294
594
  def on_311(msg, events)
295
595
  # RPL_WHOISUSER
296
- user = @bot.user_manager.find_ensured(msg.params[1])
596
+ user = User(msg.params[1])
297
597
  @whois_updates[user].merge!({
298
598
  :user => msg.params[2],
299
599
  :host => msg.params[3],
@@ -303,7 +603,7 @@ module Cinch
303
603
 
304
604
  def on_317(msg, events)
305
605
  # RPL_WHOISIDLE
306
- user = @bot.user_manager.find_ensured(msg.params[1])
606
+ user = User(msg.params[1])
307
607
  @whois_updates[user].merge!({
308
608
  :idle => msg.params[2].to_i,
309
609
  :signed_on_at => Time.at(msg.params[3].to_i),
@@ -312,7 +612,7 @@ module Cinch
312
612
 
313
613
  def on_318(msg, events)
314
614
  # RPL_ENDOFWHOIS
315
- user = @bot.user_manager.find_ensured(msg.params[1])
615
+ user = User(msg.params[1])
316
616
 
317
617
  if @whois_updates[user].empty? && !user.attr(:unknown?, true, true)
318
618
  user.end_of_whois(nil)
@@ -324,16 +624,16 @@ module Cinch
324
624
 
325
625
  def on_319(msg, events)
326
626
  # RPL_WHOISCHANNELS
327
- user = @bot.user_manager.find_ensured(msg.params[1])
328
- channels = msg.params[2].scan(/#{@isupport["CHANTYPES"].join}[^ ]+/o).map {|c| @bot.channel_manager.find_ensured(c) }
627
+ user = User(msg.params[1])
628
+ channels = msg.params[2].scan(/#{@isupport["CHANTYPES"].join}[^ ]+/o).map {|c| Channel(c) }
329
629
  user.sync(:channels, channels, true)
330
630
  end
331
631
 
332
632
  def on_324(msg, events)
333
633
  # RPL_CHANNELMODEIS
334
-
335
- modes = {}
634
+ modes = {}
336
635
  arguments = msg.params[3..-1]
636
+
337
637
  msg.params[2][1..-1].split("").each do |mode|
338
638
  if (@isupport["CHANMODES"]["B"] + @isupport["CHANMODES"]["C"]).include?(mode)
339
639
  modes[mode] = arguments.shift
@@ -347,8 +647,9 @@ module Cinch
347
647
 
348
648
  def on_330(msg, events)
349
649
  # RPL_WHOISACCOUNT
350
- user = @bot.user_manager.find_ensured(msg.params[1])
650
+ user = User(msg.params[1])
351
651
  authname = msg.params[2]
652
+
352
653
  @whois_updates[user].merge!({:authname => authname})
353
654
  end
354
655
 
@@ -362,6 +663,35 @@ module Cinch
362
663
  msg.channel.sync(:topic, msg.params[2])
363
664
  end
364
665
 
666
+ def on_352(msg, events)
667
+ # RPL_WHOREPLY
668
+ # "<channel> <user> <host> <server> <nick> <H|G>[*][@|+] :<hopcount> <real name>"
669
+ _, channel, user, host, server, nick, flags, hopsrealname = msg.params
670
+ hops, realname = hopsrealname.split(" ", 2)
671
+ channel = Channel(channel)
672
+ user_object = User(nick)
673
+ user_object.sync(:user, user, true)
674
+ user_object.sync(:host, host, true)
675
+ end
676
+
677
+ def on_354(msg, events)
678
+ # RPL_WHOSPCRPL
679
+ # We are using the following format: %acfhnru
680
+
681
+ # _ user host nick f account realame
682
+ # :leguin.freenode.net 354 dominikh_ ~a ip-88-152-125-117.unitymediagroup.de dominikh_ H 0 :d
683
+ # :leguin.freenode.net 354 dominikh_ ~FiXato fixato.net FiXato H FiXato :FiXato, using WeeChat -- More? See: http://twitter
684
+ # :leguin.freenode.net 354 dominikh_ ~dominikh cinch/developer/dominikh dominikh H DominikH :dominikh
685
+ # :leguin.freenode.net 354 dominikh_ ~oddmunds s21-04214.dsl.no.powertech.net oddmunds H 0 :oddmunds
686
+
687
+ _, channel, user, host, nick, _, account, realname = msg.params
688
+ channel = Channel(channel)
689
+ user_object = User(nick)
690
+ user_object.sync(:user, user, true)
691
+ user_object.sync(:host, host, true)
692
+ user_object.sync(:authname, account == "0" ? nil : account, true)
693
+ end
694
+
365
695
  def on_353(msg, events)
366
696
  # RPL_NAMEREPLY
367
697
  unless @in_lists.include?(:names)
@@ -372,14 +702,16 @@ module Cinch
372
702
  msg.params[3].split(" ").each do |user|
373
703
  m = user.match(/^([#{@isupport["PREFIX"].values.join}]+)/)
374
704
  if m
375
- prefixes = m[1].split.map {|s| @isupport["PREFIX"].key(s)}
705
+ prefixes = m[1].split("").map {|s| @isupport["PREFIX"].key(s)}
376
706
  nick = user[prefixes.size..-1]
377
707
  else
378
708
  nick = user
379
709
  prefixes = []
380
710
  end
381
- user = @bot.user_manager.find_ensured(nick)
711
+ user = User(nick)
712
+ user.online = true
382
713
  msg.channel.add_user(user, prefixes)
714
+ user.channels_unsynced << msg.channel unless user.channels_unsynced.include?(msg.channel)
383
715
  end
384
716
  end
385
717
 
@@ -389,6 +721,7 @@ module Cinch
389
721
  msg.channel.mark_as_synced(:users)
390
722
  end
391
723
 
724
+ # @version 2.0.0
392
725
  def on_367(msg, events)
393
726
  # RPL_BANLIST
394
727
  unless @in_lists.include?(:bans)
@@ -397,9 +730,19 @@ module Cinch
397
730
  @in_lists << :bans
398
731
 
399
732
  mask = msg.params[2]
400
- by = @bot.user_manager.find_ensured(msg.params[3].split("!").first)
401
- at = Time.at(msg.params[4].to_i)
733
+ if @network.jtv?
734
+ # on the justin tv network, ban "masks" only consist of the
735
+ # nick/username
736
+ mask = "%s!%s@%s" % [mask, mask, mask + ".irc.justin.tv"]
737
+ end
738
+
739
+ if msg.params[3]
740
+ by = User(msg.params[3].split("!").first)
741
+ else
742
+ by = nil
743
+ end
402
744
 
745
+ at = Time.at(msg.params[4].to_i)
403
746
  ban = Ban.new(mask, by, at)
404
747
  msg.channel.bans_unsynced << ban
405
748
  end
@@ -416,16 +759,40 @@ module Cinch
416
759
  msg.channel.mark_as_synced(:bans)
417
760
  end
418
761
 
762
+ def on_386(msg, events)
763
+ # RPL_QLIST
764
+ unless @in_lists.include?(:owners)
765
+ msg.channel.owners_unsynced.clear
766
+ end
767
+ @in_lists << :owners
768
+
769
+ owner = User(msg.params[2])
770
+ msg.channel.owners_unsynced << owner
771
+ end
772
+
773
+ def on_387(msg, events)
774
+ # RPL_ENDOFQLIST
775
+ if @in_lists.include?(:owners)
776
+ @in_lists.delete :owners
777
+ else
778
+ #we never received an owner, yet an end of list -> no owners
779
+ msg.channel.owners_unsynced.clear
780
+ end
781
+
782
+ msg.channel.mark_as_synced(:owners)
783
+ end
784
+
419
785
  def on_396(msg, events)
420
786
  # RPL_HOSTHIDDEN
421
787
  # note: designed for freenode
422
- @bot.user_manager.find_ensured(msg.params[0]).sync(:host, msg.params[1], true)
788
+ User(msg.params[0]).sync(:host, msg.params[1], true)
423
789
  end
424
790
 
425
791
  def on_401(msg, events)
426
792
  # ERR_NOSUCHNICK
427
- user = @bot.user_manager.find_ensured(msg.params[1])
793
+ user = User(msg.params[1])
428
794
  user.sync(:unknown?, true, true)
795
+ msg.user.online = false
429
796
  if @whois_updates.key?(user)
430
797
  user.end_of_whois(nil, true)
431
798
  @whois_updates.delete user
@@ -435,7 +802,7 @@ module Cinch
435
802
  def on_402(msg, events)
436
803
  # ERR_NOSUCHSERVER
437
804
 
438
- if user = @bot.user_manager.find(msg.params[1]) # not _ensured, we only want a user that already exists
805
+ if user = @bot.user_list.find(msg.params[1]) # not _ensured, we only want a user that already exists
439
806
  user.end_of_whois(nil, true)
440
807
  @whois_updates.delete user
441
808
  # TODO freenode specific, test on other IRCd
@@ -444,12 +811,60 @@ module Cinch
444
811
 
445
812
  def on_433(msg, events)
446
813
  # ERR_NICKNAMEINUSE
447
- @bot.nick = @bot.generate_next_nick(msg.params[1])
814
+ @bot.nick = @bot.generate_next_nick!(msg.params[1])
448
815
  end
449
816
 
450
817
  def on_671(msg, events)
451
- user = @bot.user_manager.find_ensured(msg.params[1])
818
+ user = User(msg.params[1])
452
819
  @whois_updates[user].merge!({:secure? => true})
453
820
  end
821
+
822
+ # @since 2.0.0
823
+ def on_730(msg, events)
824
+ # RPL_MONONLINE
825
+ msg.params.last.split(",").each do |mask|
826
+ user = User(Mask.new(mask).nick)
827
+ # User is responsible for emitting an event
828
+ user.online = true
829
+ end
830
+ end
831
+
832
+ # @since 2.0.0
833
+ def on_731(msg, events)
834
+ # RPL_MONOFFLINE
835
+ msg.params.last.split(",").each do |nick|
836
+ user = User(nick)
837
+ # User is responsible for emitting an event
838
+ user.online = false
839
+ end
840
+ end
841
+
842
+ # @since 2.0.0
843
+ def on_734(msg, events)
844
+ # ERR_MONLISTFULL
845
+ user = User(msg.params[2])
846
+ user.monitored = false
847
+ end
848
+
849
+ # @since 2.0.0
850
+ def on_903(msg, events)
851
+ # SASL authentication successful
852
+ @bot.loggers.info "[SASL] SASL authentication with #{@sasl_current_method.mechanism_name} successful"
853
+ send_cap_end
854
+ end
855
+
856
+ # @since 2.0.0
857
+ def on_904(msg, events)
858
+ # SASL authentication failed
859
+ @bot.loggers.info "[SASL] SASL authentication with #{@sasl_current_method.mechanism_name} failed"
860
+ send_sasl
861
+ end
862
+
863
+ # @since 2.0.0
864
+ def on_authenticate(msg, events)
865
+ send "AUTHENTICATE " + @sasl_current_method.generate(@bot.config.sasl.username,
866
+ @bot.config.sasl.password,
867
+ msg.params.last)
868
+ end
454
869
  end
455
870
  end