mcinch 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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