cinch 1.1.3 → 2.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
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