grinch 1.0.0

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