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