mcinch 2.4.1

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