grinch 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +1 -0
  3. data/LICENSE +22 -0
  4. data/README.md +180 -0
  5. data/docs/bot_options.md +454 -0
  6. data/docs/changes.md +541 -0
  7. data/docs/common_mistakes.md +60 -0
  8. data/docs/common_tasks.md +57 -0
  9. data/docs/encodings.md +69 -0
  10. data/docs/events.md +273 -0
  11. data/docs/getting_started.md +184 -0
  12. data/docs/logging.md +90 -0
  13. data/docs/migrating.md +267 -0
  14. data/docs/plugins.md +4 -0
  15. data/docs/readme.md +20 -0
  16. data/examples/basic/autovoice.rb +32 -0
  17. data/examples/basic/google.rb +35 -0
  18. data/examples/basic/hello.rb +15 -0
  19. data/examples/basic/join_part.rb +34 -0
  20. data/examples/basic/memo.rb +39 -0
  21. data/examples/basic/msg.rb +16 -0
  22. data/examples/basic/seen.rb +36 -0
  23. data/examples/basic/urban_dict.rb +35 -0
  24. data/examples/basic/url_shorten.rb +35 -0
  25. data/examples/plugins/autovoice.rb +37 -0
  26. data/examples/plugins/custom_prefix.rb +23 -0
  27. data/examples/plugins/dice_roll.rb +38 -0
  28. data/examples/plugins/google.rb +36 -0
  29. data/examples/plugins/hello.rb +22 -0
  30. data/examples/plugins/hooks.rb +36 -0
  31. data/examples/plugins/join_part.rb +42 -0
  32. data/examples/plugins/lambdas.rb +35 -0
  33. data/examples/plugins/last_nick.rb +24 -0
  34. data/examples/plugins/msg.rb +22 -0
  35. data/examples/plugins/multiple_matches.rb +32 -0
  36. data/examples/plugins/own_events.rb +37 -0
  37. data/examples/plugins/seen.rb +45 -0
  38. data/examples/plugins/timer.rb +22 -0
  39. data/examples/plugins/url_shorten.rb +33 -0
  40. data/lib/cinch.rb +5 -0
  41. data/lib/cinch/ban.rb +50 -0
  42. data/lib/cinch/bot.rb +479 -0
  43. data/lib/cinch/cached_list.rb +19 -0
  44. data/lib/cinch/callback.rb +20 -0
  45. data/lib/cinch/channel.rb +463 -0
  46. data/lib/cinch/channel_list.rb +29 -0
  47. data/lib/cinch/configuration.rb +73 -0
  48. data/lib/cinch/configuration/bot.rb +48 -0
  49. data/lib/cinch/configuration/dcc.rb +16 -0
  50. data/lib/cinch/configuration/plugins.rb +41 -0
  51. data/lib/cinch/configuration/sasl.rb +19 -0
  52. data/lib/cinch/configuration/ssl.rb +19 -0
  53. data/lib/cinch/configuration/timeouts.rb +14 -0
  54. data/lib/cinch/constants.rb +533 -0
  55. data/lib/cinch/dcc.rb +12 -0
  56. data/lib/cinch/dcc/dccable_object.rb +37 -0
  57. data/lib/cinch/dcc/incoming.rb +1 -0
  58. data/lib/cinch/dcc/incoming/send.rb +147 -0
  59. data/lib/cinch/dcc/outgoing.rb +1 -0
  60. data/lib/cinch/dcc/outgoing/send.rb +122 -0
  61. data/lib/cinch/exceptions.rb +46 -0
  62. data/lib/cinch/formatting.rb +125 -0
  63. data/lib/cinch/handler.rb +118 -0
  64. data/lib/cinch/handler_list.rb +90 -0
  65. data/lib/cinch/helpers.rb +231 -0
  66. data/lib/cinch/irc.rb +924 -0
  67. data/lib/cinch/isupport.rb +98 -0
  68. data/lib/cinch/log_filter.rb +21 -0
  69. data/lib/cinch/logger.rb +168 -0
  70. data/lib/cinch/logger/formatted_logger.rb +97 -0
  71. data/lib/cinch/logger/zcbot_logger.rb +22 -0
  72. data/lib/cinch/logger_list.rb +85 -0
  73. data/lib/cinch/mask.rb +69 -0
  74. data/lib/cinch/message.rb +392 -0
  75. data/lib/cinch/message_queue.rb +107 -0
  76. data/lib/cinch/mode_parser.rb +76 -0
  77. data/lib/cinch/network.rb +104 -0
  78. data/lib/cinch/open_ended_queue.rb +26 -0
  79. data/lib/cinch/pattern.rb +65 -0
  80. data/lib/cinch/plugin.rb +515 -0
  81. data/lib/cinch/plugin_list.rb +38 -0
  82. data/lib/cinch/rubyext/float.rb +3 -0
  83. data/lib/cinch/rubyext/module.rb +26 -0
  84. data/lib/cinch/rubyext/string.rb +33 -0
  85. data/lib/cinch/sasl.rb +34 -0
  86. data/lib/cinch/sasl/dh_blowfish.rb +71 -0
  87. data/lib/cinch/sasl/diffie_hellman.rb +47 -0
  88. data/lib/cinch/sasl/mechanism.rb +6 -0
  89. data/lib/cinch/sasl/plain.rb +26 -0
  90. data/lib/cinch/syncable.rb +83 -0
  91. data/lib/cinch/target.rb +199 -0
  92. data/lib/cinch/timer.rb +145 -0
  93. data/lib/cinch/user.rb +488 -0
  94. data/lib/cinch/user_list.rb +87 -0
  95. data/lib/cinch/utilities/deprecation.rb +16 -0
  96. data/lib/cinch/utilities/encoding.rb +37 -0
  97. data/lib/cinch/utilities/kernel.rb +13 -0
  98. data/lib/cinch/version.rb +4 -0
  99. metadata +140 -0
@@ -0,0 +1,118 @@
1
+ module Cinch
2
+ # @since 2.0.0
3
+ class Handler
4
+ # @return [Bot]
5
+ attr_reader :bot
6
+
7
+ # @return [Symbol]
8
+ attr_reader :event
9
+
10
+ # @return [Pattern]
11
+ attr_reader :pattern
12
+
13
+ # @return [Array]
14
+ attr_reader :args
15
+
16
+ # @return [Proc]
17
+ attr_reader :block
18
+
19
+ # @return [Symbol]
20
+ attr_reader :group
21
+
22
+ # @return [Boolean]
23
+ attr_reader :strip_colors
24
+
25
+ # @return [ThreadGroup]
26
+ # @api private
27
+ attr_reader :thread_group
28
+
29
+ # @param [Bot] bot
30
+ # @param [Symbol] event
31
+ # @param [Pattern] pattern
32
+ # @param [Hash] options
33
+ # @option options [Symbol] :group (nil) Match group the h belongs
34
+ # to.
35
+ # @option options [Boolean] :execute_in_callback (false) Whether
36
+ # to execute the handler in an instance of {Callback}
37
+ # @option options [Boolean] :strip_colors (false) Strip colors
38
+ # from message before attemping match
39
+ # @option options [Array] :args ([]) Additional arguments to pass
40
+ # to the block
41
+ def initialize(bot, event, pattern, options = {}, &block)
42
+ options = {
43
+ :group => nil,
44
+ :execute_in_callback => false,
45
+ :strip_colors => false,
46
+ :args => []
47
+ }.merge(options)
48
+ @bot = bot
49
+ @event = event
50
+ @pattern = pattern
51
+ @group = options[:group]
52
+ @execute_in_callback = options[:execute_in_callback]
53
+ @strip_colors = options[:strip_colors]
54
+ @args = options[:args]
55
+ @block = block
56
+
57
+ @thread_group = ThreadGroup.new
58
+ end
59
+
60
+ # Unregisters the handler.
61
+ #
62
+ # @return [void]
63
+ def unregister
64
+ @bot.handlers.unregister(self)
65
+ end
66
+
67
+ # Stops execution of the handler. This means stopping and killing
68
+ # all associated threads.
69
+ #
70
+ # @return [void]
71
+ def stop
72
+ @bot.loggers.debug "[Stopping handler] Stopping all threads of handler #{self}: #{@thread_group.list.size} threads..."
73
+ @thread_group.list.each do |thread|
74
+ Thread.new do
75
+ @bot.loggers.debug "[Ending thread] Waiting 10 seconds for #{thread} to finish..."
76
+ thread.join(10)
77
+ @bot.loggers.debug "[Killing thread] Killing #{thread}"
78
+ thread.kill
79
+ end
80
+ end
81
+ end
82
+
83
+ # Executes the handler.
84
+ #
85
+ # @param [Message] message Message that caused the invocation
86
+ # @param [Array] captures Capture groups of the pattern that are
87
+ # being passed as arguments
88
+ # @return [Thread]
89
+ def call(message, captures, arguments)
90
+ bargs = captures + arguments
91
+
92
+ thread = Thread.new {
93
+ @bot.loggers.debug "[New thread] For #{self}: #{Thread.current} -- #{@thread_group.list.size} in total."
94
+
95
+ begin
96
+ if @execute_in_callback
97
+ @bot.callback.instance_exec(message, *@args, *bargs, &@block)
98
+ else
99
+ @block.call(message, *@args, *bargs)
100
+ end
101
+ rescue => e
102
+ @bot.loggers.exception(e)
103
+ ensure
104
+ @bot.loggers.debug "[Thread done] For #{self}: #{Thread.current} -- #{@thread_group.list.size - 1} remaining."
105
+ end
106
+ }
107
+
108
+ @thread_group.add(thread)
109
+ thread
110
+ end
111
+
112
+ # @return [String]
113
+ def to_s
114
+ # TODO maybe add the number of running threads to the output?
115
+ "#<Cinch::Handler @event=#{@event.inspect} pattern=#{@pattern.inspect}>"
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,90 @@
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, handler.strip_colors)
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 [Array<Thread>]
55
+ def dispatch(event, msg = nil, *arguments)
56
+ threads = []
57
+
58
+ if handlers = find(event, msg)
59
+ already_run = Set.new
60
+ handlers.each do |handler|
61
+ next if already_run.include?(handler.block)
62
+ already_run << handler.block
63
+ # calling Message#match multiple times is not a problem
64
+ # because we cache the result
65
+ if msg
66
+ captures = msg.match(handler.pattern.to_r(msg), event, handler.strip_colors).captures
67
+ else
68
+ captures = []
69
+ end
70
+
71
+ threads << handler.call(msg, captures, arguments)
72
+ end
73
+ end
74
+
75
+ threads
76
+ end
77
+
78
+ # @yield [handler] Yields all registered handlers
79
+ # @yieldparam [Handler] handler
80
+ # @return [void]
81
+ def each(&block)
82
+ @handlers.values.flatten.each(&block)
83
+ end
84
+
85
+ # @api private
86
+ def stop_all
87
+ each { |h| h.stop }
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,231 @@
1
+ # -*- coding: utf-8 -*-
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.
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
+ # @since 2.0.0
23
+ def Target(target)
24
+ return target if target.is_a?(Target)
25
+ Target.new(target, bot)
26
+ end
27
+
28
+ # Helper method for turning a String into a {Channel} object.
29
+ #
30
+ # @param [String] channel a channel name
31
+ # @return [Channel] a {Channel} object
32
+ # @example
33
+ # on :message, /^please join (#.+)$/ do |m, target|
34
+ # Channel(target).join
35
+ # end
36
+ # @since 1.0.0
37
+ def Channel(channel)
38
+ return channel if channel.is_a?(Channel)
39
+ bot.channel_list.find_ensured(channel)
40
+ end
41
+
42
+ # Helper method for turning a String into an {User} object.
43
+ #
44
+ # @param [String] user a user's nickname
45
+ # @return [User] an {User} object
46
+ # @example
47
+ # on :message, /^tell me everything about (.+)$/ do |m, target|
48
+ # user = User(target)
49
+ # m.reply "%s is named %s and connects from %s" % [user.nick, user.name, user.host]
50
+ # end
51
+ # @since 1.0.0
52
+ def User(user)
53
+ return user if user.is_a?(User)
54
+ if user == bot.nick
55
+ bot
56
+ else
57
+ bot.user_list.find_ensured(user)
58
+ end
59
+ end
60
+
61
+ # @example Used as a class method in a plugin
62
+ # timer 5, method: :some_method
63
+ # def some_method
64
+ # Channel("#cinch-bots").send(Time.now.to_s)
65
+ # end
66
+ #
67
+ # @example Used as an instance method in a plugin
68
+ # match "start timer"
69
+ # def execute(m)
70
+ # Timer(5) { puts "timer fired" }
71
+ # end
72
+ #
73
+ # @example Used as an instance method in a traditional `on` handler
74
+ # on :message, "start timer" do
75
+ # Timer(5) { puts "timer fired" }
76
+ # end
77
+ #
78
+ # @param [Numeric] interval Interval in seconds
79
+ # @param [Proc] block A proc to execute
80
+ # @option options [Symbol] :method (:timer) Method to call (only
81
+ # if no proc is provided)
82
+ # @option options [Boolean] :threaded (true) Call method in a
83
+ # thread?
84
+ # @option options [Integer] :shots (Float::INFINITY) How often
85
+ # should the timer fire?
86
+ # @option options [Boolean] :start_automatically (true) If true,
87
+ # the timer will automatically start after the bot finished
88
+ # connecting.
89
+ # @option options [Boolean] :stop_automaticall (true) If true, the
90
+ # timer will automatically stop when the bot disconnects.
91
+ # @return [Timer]
92
+ # @since 2.0.0
93
+ def Timer(interval, options = {}, &block)
94
+ options = {:method => :timer, :threaded => true, :interval => interval}.merge(options)
95
+ block ||= self.method(options[:method])
96
+ timer = Cinch::Timer.new(bot, options, &block)
97
+ timer.start
98
+
99
+ if self.respond_to?(:timers)
100
+ timers << timer
101
+ end
102
+
103
+ timer
104
+ end
105
+
106
+ # @endgroup
107
+
108
+ # @group Logging
109
+
110
+ # Use this method to automatically log exceptions to the loggers.
111
+ #
112
+ # @example
113
+ # def my_method
114
+ # rescue_exception do
115
+ # something_that_might_raise()
116
+ # end
117
+ # end
118
+ #
119
+ # @return [void]
120
+ # @since 2.0.0
121
+ def rescue_exception
122
+ begin
123
+ yield
124
+ rescue => e
125
+ bot.loggers.exception(e)
126
+ end
127
+ end
128
+
129
+ # (see Logger#log)
130
+ def log(messages, event = :debug, level = event)
131
+ if self.is_a?(Cinch::Plugin)
132
+ messages = Array(messages).map {|m|
133
+ "[#{self.class}] " + m
134
+ }
135
+ end
136
+ @bot.loggers.log(messages, event, level)
137
+ end
138
+
139
+ # (see Logger#debug)
140
+ def debug(message)
141
+ log(message, :debug)
142
+ end
143
+
144
+ # (see Logger#error)
145
+ def error(message)
146
+ log(message, :error)
147
+ end
148
+
149
+ # (see Logger#fatal)
150
+ def fatal(message)
151
+ log(message, :fatal)
152
+ end
153
+
154
+ # (see Logger#info)
155
+ def info(message)
156
+ log(message, :info)
157
+ end
158
+
159
+ # (see Logger#warn)
160
+ def warn(message)
161
+ log(message, :warn)
162
+ end
163
+
164
+ # (see Logger#incoming)
165
+ def incoming(message)
166
+ log(message, :incoming, :log)
167
+ end
168
+
169
+ # (see Logger#outgoing)
170
+ def outgoing(message)
171
+ log(message, :outgoing, :log)
172
+ end
173
+
174
+ # (see Logger#exception)
175
+ def exception(e)
176
+ log(e.message, :exception, :error)
177
+ end
178
+ # @endgroup
179
+
180
+ # @group Formatting
181
+
182
+ # (see Formatting.format)
183
+ def Format(*settings, string)
184
+ Formatting.format(*settings, string)
185
+ end
186
+ alias_method :Color, :Format # deprecated
187
+ undef_method(:Color) # yardoc hack
188
+
189
+ def Color(*args)
190
+ Cinch::Utilities::Deprecation.print_deprecation("2.2.0", "Helpers.Color", "Helpers.Format")
191
+ Format(*args)
192
+ end
193
+
194
+ # (see .sanitize)
195
+ def Sanitize(string)
196
+ Cinch::Helpers.sanitize(string)
197
+ end
198
+
199
+ # Deletes all characters in the ranges 0–8, 10–31 as well as the
200
+ # character 127, that is all non-printable characters and
201
+ # newlines.
202
+ #
203
+ # This method is useful for filtering text from external sources
204
+ # before sending it to IRC.
205
+ #
206
+ # Note that this method does not gracefully handle mIRC color
207
+ # codes, because it will leave the numeric arguments behind. If
208
+ # your text comes from IRC, you may want to filter it through
209
+ # {#Unformat} first. If you want to send sanitized input that
210
+ # includes your own formatting, first use this method, then add
211
+ # your formatting.
212
+ #
213
+ # There exist methods for sending messages that automatically
214
+ # call this method, namely {Target#safe_msg},
215
+ # {Target#safe_notice}, and {Target#safe_action}.
216
+ #
217
+ # @param [String] string The string to filter
218
+ # @return [String] The filtered string
219
+ # @since 2.2.0
220
+ def self.sanitize(string)
221
+ string.gsub(/[\x00-\x08\x0a-\x1f\x7f]/, '')
222
+ end
223
+
224
+ # (see Formatting.unformat)
225
+ def Unformat(string)
226
+ Formatting.unformat(string)
227
+ end
228
+
229
+ # @endgroup
230
+ end
231
+ end
data/lib/cinch/irc.rb ADDED
@@ -0,0 +1,924 @@
1
+ require "timeout"
2
+ require "net/protocol"
3
+ require "cinch/network"
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.
9
+ class IRC
10
+ include Helpers
11
+
12
+ # @return [ISupport]
13
+ attr_reader :isupport
14
+
15
+ # @return [Bot]
16
+ attr_reader :bot
17
+
18
+ # @return [Network] The detected network
19
+ attr_reader :network
20
+
21
+ def initialize(bot)
22
+ @bot = bot
23
+ @isupport = ISupport.new
24
+ end
25
+
26
+ # @return [TCPSocket]
27
+ # @api private
28
+ # @since 2.0.0
29
+ def socket
30
+ @socket.io
31
+ end
32
+
33
+ # @api private
34
+ # @return [void]
35
+ # @since 2.0.0
36
+ def setup
37
+ @registration = []
38
+ @network = Network.new(:unknown, :unknown)
39
+ @whois_updates = {}
40
+ @in_lists = Set.new
41
+ end
42
+
43
+ # @api private
44
+ # @return [Boolean] True if the connection could be established
45
+ def connect
46
+ tcp_socket = nil
47
+
48
+ begin
49
+ Timeout::timeout(@bot.config.timeouts.connect) do
50
+ tcp_socket = TCPSocket.new(@bot.config.server, @bot.config.port, @bot.config.local_host)
51
+ end
52
+ rescue Timeout::Error
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
58
+ rescue => e
59
+ @bot.loggers.exception(e)
60
+ return false
61
+ end
62
+
63
+ if @bot.config.ssl.use
64
+ setup_ssl(tcp_socket)
65
+ else
66
+ @socket = tcp_socket
67
+ end
68
+
69
+ @socket = Net::BufferedIO.new(@socket)
70
+ @socket.read_timeout = @bot.config.timeouts.read
71
+ @queue = MessageQueue.new(@socket, @bot)
72
+
73
+ return true
74
+ end
75
+
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))
90
+ end
91
+
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
94
+ else
95
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
96
+ end
97
+ @bot.loggers.info "Using SSL with #{@bot.config.server}:#{@bot.config.port}"
98
+
99
+ @socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
100
+ @socket.sync = true
101
+ @socket.connect
102
+ end
103
+
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
+ caps = [:"away-notify", :"multi-prefix", :sasl, :"twitch.tv/tags"] & @network.capabilities
116
+
117
+ # InspIRCd doesn't respond to empty REQs, so send an END in that
118
+ # case.
119
+ if caps.size > 0
120
+ send "CAP REQ :" + caps.join(" ")
121
+ else
122
+ send_cap_end
123
+ end
124
+ end
125
+
126
+ # @since 2.0.0
127
+ # @api private
128
+ # @return [void]
129
+ def send_cap_end
130
+ send "CAP END"
131
+ end
132
+
133
+ # @api private
134
+ # @return [void]
135
+ # @since 2.0.0
136
+ def send_login
137
+ send "PASS #{@bot.config.password}" if @bot.config.password
138
+ send "NICK #{@bot.generate_next_nick!}"
139
+ send "USER #{@bot.config.user} 0 * :#{@bot.config.realname}"
140
+ end
141
+
142
+ # @api private
143
+ # @return [Thread] the reading thread
144
+ # @since 2.0.0
145
+ def start_reading_thread
146
+ Thread.new do
147
+ begin
148
+ while line = @socket.readline
149
+ rescue_exception do
150
+ line = Cinch::Utilities::Encoding.encode_incoming(line, @bot.config.encoding)
151
+ parse line
152
+ end
153
+ end
154
+ rescue Timeout::Error
155
+ @bot.loggers.warn "Connection timed out."
156
+ rescue EOFError
157
+ @bot.loggers.warn "Lost connection."
158
+ rescue => e
159
+ @bot.loggers.exception(e)
160
+ end
161
+
162
+ @socket.close
163
+ @bot.handlers.dispatch(:disconnect)
164
+ # FIXME won't we kill all :disconnect handlers here? prolly
165
+ # not, as they have 10 seconds to finish. that should be
166
+ # plenty of time
167
+ @bot.handlers.stop_all
168
+ end
169
+ end
170
+
171
+ # @api private
172
+ # @return [Thread] the sending thread
173
+ # @since 2.0.0
174
+ def start_sending_thread
175
+ Thread.new do
176
+ rescue_exception do
177
+ @queue.process!
178
+ end
179
+ end
180
+ end
181
+
182
+ # @api private
183
+ # @return [Thread] The ping thread.
184
+ # @since 2.0.0
185
+ def start_ping_thread
186
+ Thread.new do
187
+ while true
188
+ sleep @bot.config.ping_interval
189
+ # PING requires a single argument. In our case the value
190
+ # doesn't matter though.
191
+ send("PING 0")
192
+ end
193
+ end
194
+ end
195
+
196
+ # @since 2.0.0
197
+ def send_sasl
198
+ if @bot.config.sasl.username && @sasl_current_method = @sasl_remaining_methods.pop
199
+ @bot.loggers.info "[SASL] Trying to authenticate with #{@sasl_current_method.mechanism_name}"
200
+ send "AUTHENTICATE #{@sasl_current_method.mechanism_name}"
201
+ else
202
+ send_cap_end
203
+ end
204
+ end
205
+
206
+ # Establish a connection.
207
+ #
208
+ # @return [void]
209
+ # @since 2.0.0
210
+ def start
211
+ setup
212
+ if connect
213
+ @sasl_remaining_methods = @bot.config.sasl.mechanisms.reverse
214
+ send_cap_ls
215
+ send_login
216
+
217
+ reading_thread = start_reading_thread
218
+ sending_thread = start_sending_thread
219
+ ping_thread = start_ping_thread
220
+
221
+ reading_thread.join
222
+ sending_thread.kill
223
+ ping_thread.kill
224
+ end
225
+ end
226
+
227
+ # @api private
228
+ # @return [void]
229
+ def parse(input)
230
+ return if input.chomp.empty?
231
+ @bot.loggers.incoming(input)
232
+
233
+ msg = Message.new(input, @bot)
234
+ events = [[:catchall]]
235
+
236
+ if ["001", "002", "003", "004", "422"].include?(msg.command)
237
+ @registration << msg.command
238
+ if registered?
239
+ events << [:connect]
240
+ @bot.last_connection_was_successful = true
241
+ on_connect(msg, events)
242
+ end
243
+ end
244
+
245
+ if ["PRIVMSG", "NOTICE"].include?(msg.command)
246
+ events << [:ctcp] if msg.ctcp?
247
+ if msg.channel?
248
+ events << [:channel]
249
+ else
250
+ events << [:private]
251
+ end
252
+
253
+ if msg.command == "PRIVMSG"
254
+ events << [:message]
255
+ end
256
+
257
+ if msg.action?
258
+ events << [:action]
259
+ end
260
+ end
261
+
262
+ meth = "on_#{msg.command.downcase}"
263
+ __send__(meth, msg, events) if respond_to?(meth, true)
264
+
265
+ if msg.error?
266
+ events << [:error]
267
+ end
268
+
269
+ events << [msg.command.downcase.to_sym]
270
+
271
+ msg.events = events.map(&:first)
272
+ events.each do |event, *args|
273
+ @bot.handlers.dispatch(event, msg, *args)
274
+ end
275
+ end
276
+
277
+ # @return [Boolean] true if we successfully registered yet
278
+ def registered?
279
+ (("001".."004").to_a - @registration).empty? || @registration.include?("422")
280
+ end
281
+
282
+ # Send a message to the server.
283
+ # @param [String] msg
284
+ # @return [void]
285
+ def send(msg)
286
+ @queue.queue(msg)
287
+ end
288
+
289
+ private
290
+ def set_leaving_user(message, user, events)
291
+ events << [:leaving, user]
292
+ end
293
+
294
+ # @since 2.0.0
295
+ def detect_network(msg, event)
296
+ old_network = @network
297
+ new_network = nil
298
+ new_ircd = nil
299
+ case event
300
+ when "002"
301
+ if msg.params.last =~ /^Your host is .+?, running version (.+)$/
302
+ case $1
303
+ when /\+snircd\(/
304
+ new_ircd = :snircd
305
+ when /^u[\d\.]+$/
306
+ new_ircd = :ircu
307
+ when /^(.+?)-?\d+/
308
+ new_ircd = $1.downcase.to_sym
309
+ end
310
+ elsif msg.params.last == "Your host is jtvchat"
311
+ new_network = :jtv
312
+ new_ircd = :jtv
313
+ end
314
+ when "004"
315
+ if msg.params == %w{irc.tinyspeck.com IRC-SLACK gateway}
316
+ new_network = :slack
317
+ new_ircd = :slack
318
+ end
319
+ when "005"
320
+ case @isupport["NETWORK"]
321
+ when "NGameTV"
322
+ new_network = :ngametv
323
+ new_ircd = :ngametv
324
+ when nil
325
+ else
326
+ new_network = @isupport["NETWORK"].downcase.to_sym
327
+ end
328
+ end
329
+
330
+ new_network ||= old_network.name
331
+ new_ircd ||= old_network.ircd
332
+
333
+ if old_network.unknown_ircd? && new_ircd != :unknown
334
+ @bot.loggers.info "Detected IRCd: #{new_ircd}"
335
+ end
336
+ if !old_network.unknown_ircd? && new_ircd != old_network.ircd
337
+ @bot.loggers.info "Detected different IRCd: #{old_network.ircd} -> #{new_ircd}"
338
+ end
339
+ if old_network.unknown_network? && new_network != :unknown
340
+ @bot.loggers.info "Detected network: #{new_network}"
341
+ end
342
+ if !old_network.unknown_network? && new_network != old_network.name
343
+ @bot.loggers.info "Detected different network: #{old_network.name} -> #{new_network}"
344
+ end
345
+
346
+ @network.name = new_network
347
+ @network.ircd = new_ircd
348
+ end
349
+
350
+ def process_ban_mode(msg, events, param, direction)
351
+ mask = param
352
+ ban = Ban.new(mask, msg.user, Time.now)
353
+
354
+ if direction == :add
355
+ msg.channel.bans_unsynced << ban
356
+ events << [:ban, ban]
357
+ else
358
+ msg.channel.bans_unsynced.delete_if {|b| b.mask == ban.mask}
359
+ events << [:unban, ban]
360
+ end
361
+ end
362
+
363
+ def process_owner_mode(msg, events, param, direction)
364
+ owner = User(param)
365
+ if direction == :add
366
+ msg.channel.owners_unsynced << owner unless msg.channel.owners_unsynced.include?(owner)
367
+ events << [:owner, owner]
368
+ else
369
+ msg.channel.owners_unsynced.delete(owner)
370
+ events << [:deowner, owner]
371
+ end
372
+ end
373
+
374
+ def update_whois(user, data)
375
+ @whois_updates[user] ||= {}
376
+ @whois_updates[user].merge!(data)
377
+ end
378
+
379
+ # @since 2.0.0
380
+ def on_away(msg, events)
381
+ if msg.message.to_s.empty?
382
+ # unaway
383
+ msg.user.sync(:away, nil, true)
384
+ events << [:unaway]
385
+ else
386
+ # away
387
+ msg.user.sync(:away, msg.message, true)
388
+ events << [:away]
389
+ end
390
+ end
391
+
392
+ # @since 2.0.0
393
+ def on_cap(msg, events)
394
+ case msg.params[1]
395
+ when "LS"
396
+ @network.capabilities.concat msg.message.split(" ").map(&:to_sym)
397
+ send_cap_req
398
+ when "ACK"
399
+ if @network.capabilities.include?(:sasl)
400
+ send_sasl
401
+ else
402
+ send_cap_end
403
+ end
404
+ when "NAK"
405
+ send_cap_end
406
+ end
407
+ end
408
+
409
+ # @since 2.0.0
410
+ def on_connect(msg, events)
411
+ @bot.modes = @bot.config.modes
412
+ end
413
+
414
+ def on_join(msg, events)
415
+ if msg.user == @bot
416
+ @bot.channels << msg.channel
417
+ msg.channel.sync_modes
418
+ end
419
+ msg.channel.add_user(msg.user)
420
+ msg.user.online = true
421
+ end
422
+
423
+ def on_kick(msg, events)
424
+ target = User(msg.params[1])
425
+ if target == @bot
426
+ @bot.channels.delete(msg.channel)
427
+ end
428
+ msg.channel.remove_user(target)
429
+
430
+ set_leaving_user(msg, target, events)
431
+ end
432
+
433
+ def on_kill(msg, events)
434
+ user = User(msg.params[1])
435
+
436
+ @bot.channel_list.each do |channel|
437
+ channel.remove_user(user)
438
+ end
439
+
440
+ user.unsync_all
441
+ user.online = false
442
+
443
+ set_leaving_user(msg, user, events)
444
+ end
445
+
446
+ # @version 1.1.0
447
+ def on_mode(msg, events)
448
+ if msg.channel?
449
+ parse_channel_modes(msg, events)
450
+ return
451
+ end
452
+ if msg.params.first == bot.nick
453
+ parse_bot_modes(msg)
454
+ end
455
+ end
456
+
457
+ def parse_channel_modes(msg, events)
458
+ add_and_remove = @bot.irc.isupport["CHANMODES"]["A"] + @bot.irc.isupport["CHANMODES"]["B"] + @bot.irc.isupport["PREFIX"].keys
459
+
460
+ param_modes = {
461
+ :add => @bot.irc.isupport["CHANMODES"]["C"] + add_and_remove,
462
+ :remove => add_and_remove
463
+ }
464
+
465
+
466
+ modes, err = ModeParser.parse_modes(msg.params[1], msg.params[2..-1], param_modes)
467
+ if err != nil
468
+ if @network.ircd != :slack || !err.is_a?(ModeParser::TooManyParametersError)
469
+ raise Exceptions::InvalidModeString, err
470
+ end
471
+ end
472
+ modes.each do |direction, mode, param|
473
+ if @bot.irc.isupport["PREFIX"].keys.include?(mode)
474
+ target = User(param)
475
+
476
+ # (un)set a user-mode
477
+ if direction == :add
478
+ msg.channel.users[target] << mode unless msg.channel.users[target].include?(mode)
479
+ else
480
+ msg.channel.users[target].delete mode
481
+ end
482
+
483
+ user_events = {
484
+ "o" => "op",
485
+ "v" => "voice",
486
+ "h" => "halfop"
487
+ }
488
+ if user_events.has_key?(mode)
489
+ event = (direction == :add ? "" : "de") + user_events[mode]
490
+ events << [event.to_sym, target]
491
+ end
492
+ elsif @bot.irc.isupport["CHANMODES"]["A"].include?(mode)
493
+ case mode
494
+ when "b"
495
+ process_ban_mode(msg, events, param, direction)
496
+ when "q"
497
+ process_owner_mode(msg, events, param, direction) if @network.owner_list_mode
498
+ else
499
+ raise Exceptions::UnsupportedMode, mode
500
+ end
501
+ else
502
+ # channel options
503
+ if direction == :add
504
+ msg.channel.modes_unsynced[mode] = param.nil? ? true : param
505
+ else
506
+ msg.channel.modes_unsynced.delete(mode)
507
+ end
508
+ end
509
+ end
510
+
511
+ events << [:mode_change, modes]
512
+ end
513
+
514
+ def parse_bot_modes(msg)
515
+ modes, err = ModeParser.parse_modes(msg.params[1], msg.params[2..-1])
516
+ if err != nil
517
+ raise Exceptions::InvalidModeString, err
518
+ end
519
+ modes.each do |direction, mode, _|
520
+ if direction == :add
521
+ @bot.modes << mode unless @bot.modes.include?(mode)
522
+ else
523
+ @bot.modes.delete(mode)
524
+ end
525
+ end
526
+ end
527
+
528
+ def on_nick(msg, events)
529
+ if msg.user == @bot
530
+ # @bot.set_nick msg.params.last
531
+ target = @bot
532
+ else
533
+ target = msg.user
534
+ end
535
+
536
+ target.update_nick(msg.params.last)
537
+ target.online = true
538
+ end
539
+
540
+ def on_part(msg, events)
541
+ msg.channel.remove_user(msg.user)
542
+ msg.user.channels_unsynced.delete msg.channel
543
+
544
+ if msg.user == @bot
545
+ @bot.channels.delete(msg.channel)
546
+ end
547
+
548
+ set_leaving_user(msg, msg.user, events)
549
+ end
550
+
551
+ def on_ping(msg, events)
552
+ send "PONG :#{msg.params.first}"
553
+ end
554
+
555
+ def on_topic(msg, events)
556
+ msg.channel.sync(:topic, msg.params[1])
557
+ end
558
+
559
+ def on_quit(msg, events)
560
+ @bot.channel_list.each do |channel|
561
+ channel.remove_user(msg.user)
562
+ end
563
+ msg.user.unsync_all
564
+ msg.user.online = false
565
+
566
+ set_leaving_user(msg, msg.user, events)
567
+
568
+ if msg.message.downcase == "excess flood" && msg.user == @bot
569
+ @bot.warn ["Looks like your bot has been kicked because of excess flood.",
570
+ "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:",
571
+ "- Server: #{@bot.config.server}",
572
+ "- Messages per second: #{@bot.config.messages_per_second}",
573
+ "- Server queue size: #{@bot.config.server_queue_size}"]
574
+ end
575
+ end
576
+
577
+ # @since 2.0.0
578
+ def on_privmsg(msg, events)
579
+ if msg.user
580
+ msg.user.online = true
581
+ end
582
+
583
+ if msg.message =~ /^\001DCC SEND (?:"([^"]+)"|(\S+)) (\S+) (\d+)(?: (\d+))?\001$/
584
+ process_dcc_send($1 || $2, $3, $4, $5, msg, events)
585
+ end
586
+ end
587
+
588
+ # @since 2.0.0
589
+ def process_dcc_send(filename, ip, port, size, m, events)
590
+ if ip =~ /^\d+$/
591
+ # If ip is a single integer, assume it's a specification
592
+ # compliant IPv4 address in network byte order. If it's any
593
+ # other string, assume that it's a valid IPv4 or IPv6 address.
594
+ # If it's not valid, let someone higher up the chain notice
595
+ # that.
596
+ ip = ip.to_i
597
+ ip = [24, 16, 8, 0].collect {|b| (ip >> b) & 255}.join('.')
598
+ end
599
+
600
+ port = port.to_i
601
+ size = size.to_i
602
+
603
+ @bot.loggers.info "DCC: Incoming DCC SEND: File name: %s - Size: %dB - IP: %s - Port: %d" % [filename, size, ip, port]
604
+
605
+ dcc = DCC::Incoming::Send.new(user: m.user, filename: filename, size: size, ip: ip, port: port)
606
+ events << [:dcc_send, dcc]
607
+ end
608
+
609
+ # @since 2.0.0
610
+ def on_001(msg, events)
611
+ # Ensure that we know our real, possibly truncated or otherwise
612
+ # modified nick.
613
+ @bot.set_nick msg.params.first
614
+ end
615
+
616
+ # @since 2.0.0
617
+ def on_002(msg, events)
618
+ detect_network(msg, "002")
619
+ end
620
+
621
+ # @since 2.2.6
622
+ def on_004(msg, events)
623
+ detect_network(msg, "004")
624
+ end
625
+
626
+ def on_005(msg, events)
627
+ # ISUPPORT
628
+ @isupport.parse(*msg.params[1..-2].map {|v| v.split(" ")}.flatten)
629
+ detect_network(msg, "005")
630
+ end
631
+
632
+ # @since 2.0.0
633
+ def on_301(msg, events)
634
+ # RPL_AWAY
635
+ user = User(msg.params[1])
636
+ away = msg.params.last
637
+
638
+ if @whois_updates[user]
639
+ update_whois(user, {:away => away})
640
+ end
641
+ end
642
+
643
+ # @since 1.1.0
644
+ def on_307(msg, events)
645
+ # RPL_WHOISREGNICK
646
+ user = User(msg.params[1])
647
+ update_whois(user, {:registered => true})
648
+ end
649
+
650
+ def on_311(msg, events)
651
+ # RPL_WHOISUSER
652
+ user = User(msg.params[1])
653
+ update_whois(user, {
654
+ :user => msg.params[2],
655
+ :host => msg.params[3],
656
+ :realname => msg.params[5],
657
+ })
658
+ end
659
+
660
+ def on_313(msg, events)
661
+ # RPL_WHOISOPERATOR
662
+ user = User(msg.params[1])
663
+ update_whois(user, {:oper? => true})
664
+ end
665
+
666
+ def on_317(msg, events)
667
+ # RPL_WHOISIDLE
668
+ user = User(msg.params[1])
669
+ update_whois(user, {
670
+ :idle => msg.params[2].to_i,
671
+ :signed_on_at => Time.at(msg.params[3].to_i),
672
+ })
673
+ end
674
+
675
+ def on_318(msg, events)
676
+ # RPL_ENDOFWHOIS
677
+ user = User(msg.params[1])
678
+ user.end_of_whois(@whois_updates[user])
679
+ @whois_updates.delete user
680
+ end
681
+
682
+ def on_319(msg, events)
683
+ # RPL_WHOISCHANNELS
684
+ user = User(msg.params[1])
685
+ channels = msg.params[2].scan(/[#{@isupport["CHANTYPES"].join}][^ ]+/o).map {|c| Channel(c) }
686
+ update_whois(user, {:channels => channels})
687
+ end
688
+
689
+ def on_324(msg, events)
690
+ # RPL_CHANNELMODEIS
691
+ modes = {}
692
+ arguments = msg.params[3..-1]
693
+
694
+ msg.params[2][1..-1].split("").each do |mode|
695
+ if (@isupport["CHANMODES"]["B"] + @isupport["CHANMODES"]["C"]).include?(mode)
696
+ modes[mode] = arguments.shift
697
+ else
698
+ modes[mode] = true
699
+ end
700
+ end
701
+
702
+ msg.channel.sync(:modes, modes, false)
703
+ end
704
+
705
+ def on_330(msg, events)
706
+ # RPL_WHOISACCOUNT
707
+ user = User(msg.params[1])
708
+ authname = msg.params[2]
709
+ update_whois(user, {:authname => authname})
710
+ end
711
+
712
+ def on_331(msg, events)
713
+ # RPL_NOTOPIC
714
+ msg.channel.sync(:topic, "")
715
+ end
716
+
717
+ def on_332(msg, events)
718
+ # RPL_TOPIC
719
+ msg.channel.sync(:topic, msg.params[2])
720
+ end
721
+
722
+ def on_352(msg, events)
723
+ # RPL_WHOREPLY
724
+ # "<channel> <user> <host> <server> <nick> <H|G>[*][@|+] :<hopcount> <real name>"
725
+ _, channel, user, host, _, nick, _, hopsrealname = msg.params
726
+ _, realname = hopsrealname.split(" ", 2)
727
+ channel = Channel(channel)
728
+ user_object = User(nick)
729
+ user_object.sync(:user, user, true)
730
+ user_object.sync(:host, host, true)
731
+ user_object.sync(:realname, realname, true)
732
+ end
733
+
734
+ def on_354(msg, events)
735
+ # RPL_WHOSPCRPL
736
+ # We are using the following format: %acfhnru
737
+
738
+ # _ user host nick f account realame
739
+ # :leguin.freenode.net 354 dominikh_ ~a ip-88-152-125-117.unitymediagroup.de dominikh_ H 0 :d
740
+ # :leguin.freenode.net 354 dominikh_ ~FiXato fixato.net FiXato H FiXato :FiXato, using WeeChat -- More? See: http://twitter
741
+ # :leguin.freenode.net 354 dominikh_ ~dominikh cinch/developer/dominikh dominikh H DominikH :dominikh
742
+ # :leguin.freenode.net 354 dominikh_ ~oddmunds s21-04214.dsl.no.powertech.net oddmunds H 0 :oddmunds
743
+
744
+ _, channel, user, host, nick, _, account, realname = msg.params
745
+ channel = Channel(channel)
746
+ user_object = User(nick)
747
+ user_object.sync(:user, user, true)
748
+ user_object.sync(:host, host, true)
749
+ user_object.sync(:realname, realname, true)
750
+ user_object.sync(:authname, account == "0" ? nil : account, true)
751
+ end
752
+
753
+ def on_353(msg, events)
754
+ # RPL_NAMEREPLY
755
+ unless @in_lists.include?(:names)
756
+ msg.channel.clear_users
757
+ end
758
+ @in_lists << :names
759
+
760
+ msg.params[3].split(" ").each do |user|
761
+ m = user.match(/^([#{@isupport["PREFIX"].values.join}]+)/)
762
+ if m
763
+ prefixes = m[1].split("").map {|s| @isupport["PREFIX"].key(s)}
764
+ nick = user[prefixes.size..-1]
765
+ else
766
+ nick = user
767
+ prefixes = []
768
+ end
769
+ user = User(nick)
770
+ user.online = true
771
+ msg.channel.add_user(user, prefixes)
772
+ user.channels_unsynced << msg.channel unless user.channels_unsynced.include?(msg.channel)
773
+ end
774
+ end
775
+
776
+ def on_366(msg, events)
777
+ # RPL_ENDOFNAMES
778
+ @in_lists.delete :names
779
+ msg.channel.mark_as_synced(:users)
780
+ end
781
+
782
+ # @version 2.0.0
783
+ def on_367(msg, events)
784
+ # RPL_BANLIST
785
+ unless @in_lists.include?(:bans)
786
+ msg.channel.bans_unsynced.clear
787
+ end
788
+ @in_lists << :bans
789
+
790
+ mask = msg.params[2]
791
+ if @network.jtv?
792
+ # on the justin tv network, ban "masks" only consist of the
793
+ # nick/username
794
+ mask = "%s!%s@%s" % [mask, mask, mask + ".irc.justin.tv"]
795
+ end
796
+
797
+ if msg.params[3]
798
+ by = User(msg.params[3].split("!").first)
799
+ else
800
+ by = nil
801
+ end
802
+
803
+ at = Time.at(msg.params[4].to_i)
804
+ ban = Ban.new(mask, by, at)
805
+ msg.channel.bans_unsynced << ban
806
+ end
807
+
808
+ def on_368(msg, events)
809
+ # RPL_ENDOFBANLIST
810
+ if @in_lists.include?(:bans)
811
+ @in_lists.delete :bans
812
+ else
813
+ # we never received a ban, yet an end of list => no bans
814
+ msg.channel.bans_unsynced.clear
815
+ end
816
+
817
+ msg.channel.mark_as_synced(:bans)
818
+ end
819
+
820
+ def on_386(msg, events)
821
+ # RPL_QLIST
822
+ unless @in_lists.include?(:owners)
823
+ msg.channel.owners_unsynced.clear
824
+ end
825
+ @in_lists << :owners
826
+
827
+ owner = User(msg.params[2])
828
+ msg.channel.owners_unsynced << owner
829
+ end
830
+
831
+ def on_387(msg, events)
832
+ # RPL_ENDOFQLIST
833
+ if @in_lists.include?(:owners)
834
+ @in_lists.delete :owners
835
+ else
836
+ #we never received an owner, yet an end of list -> no owners
837
+ msg.channel.owners_unsynced.clear
838
+ end
839
+
840
+ msg.channel.mark_as_synced(:owners)
841
+ end
842
+
843
+ def on_396(msg, events)
844
+ # RPL_HOSTHIDDEN
845
+ # note: designed for freenode
846
+ User(msg.params[0]).sync(:host, msg.params[1], true)
847
+ end
848
+
849
+ def on_401(msg, events)
850
+ # ERR_NOSUCHNICK
851
+ if user = @bot.user_list.find(msg.params[1])
852
+ update_whois(user, {:unknown? => true})
853
+ end
854
+ end
855
+
856
+ def on_402(msg, events)
857
+ # ERR_NOSUCHSERVER
858
+
859
+ if user = @bot.user_list.find(msg.params[1]) # not _ensured, we only want a user that already exists
860
+ user.end_of_whois({:unknown? => true})
861
+ @whois_updates.delete user
862
+ # TODO freenode specific, test on other IRCd
863
+ end
864
+ end
865
+
866
+ def on_433(msg, events)
867
+ # ERR_NICKNAMEINUSE
868
+ @bot.nick = @bot.generate_next_nick!(msg.params[1])
869
+ end
870
+
871
+ def on_671(msg, events)
872
+ user = User(msg.params[1])
873
+ update_whois(user, {:secure? => true})
874
+ end
875
+
876
+ # @since 2.0.0
877
+ def on_730(msg, events)
878
+ # RPL_MONONLINE
879
+ msg.params.last.split(",").each do |mask|
880
+ user = User(Mask.new(mask).nick)
881
+ # User is responsible for emitting an event
882
+ user.online = true
883
+ end
884
+ end
885
+
886
+ # @since 2.0.0
887
+ def on_731(msg, events)
888
+ # RPL_MONOFFLINE
889
+ msg.params.last.split(",").each do |nick|
890
+ user = User(nick)
891
+ # User is responsible for emitting an event
892
+ user.online = false
893
+ end
894
+ end
895
+
896
+ # @since 2.0.0
897
+ def on_734(msg, events)
898
+ # ERR_MONLISTFULL
899
+ user = User(msg.params[2])
900
+ user.monitored = false
901
+ end
902
+
903
+ # @since 2.0.0
904
+ def on_903(msg, events)
905
+ # SASL authentication successful
906
+ @bot.loggers.info "[SASL] SASL authentication with #{@sasl_current_method.mechanism_name} successful"
907
+ send_cap_end
908
+ end
909
+
910
+ # @since 2.0.0
911
+ def on_904(msg, events)
912
+ # SASL authentication failed
913
+ @bot.loggers.info "[SASL] SASL authentication with #{@sasl_current_method.mechanism_name} failed"
914
+ send_sasl
915
+ end
916
+
917
+ # @since 2.0.0
918
+ def on_authenticate(msg, events)
919
+ send "AUTHENTICATE " + @sasl_current_method.generate(@bot.config.sasl.username,
920
+ @bot.config.sasl.password,
921
+ msg.params.last)
922
+ end
923
+ end
924
+ end