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.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/.standard.yml +3 -0
  3. data/CHANGELOG.md +298 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/Gemfile +5 -0
  6. data/LICENSE.txt +23 -0
  7. data/README.md +195 -0
  8. data/Rakefile +14 -0
  9. data/docs/bot_options.md +454 -0
  10. data/docs/changes.md +541 -0
  11. data/docs/common_mistakes.md +60 -0
  12. data/docs/common_tasks.md +57 -0
  13. data/docs/encodings.md +69 -0
  14. data/docs/events.md +273 -0
  15. data/docs/getting_started.md +184 -0
  16. data/docs/logging.md +90 -0
  17. data/docs/migrating.md +267 -0
  18. data/docs/plugins.md +4 -0
  19. data/docs/readme.md +20 -0
  20. data/examples/basic/autovoice.rb +32 -0
  21. data/examples/basic/google.rb +35 -0
  22. data/examples/basic/hello.rb +14 -0
  23. data/examples/basic/join_part.rb +35 -0
  24. data/examples/basic/memo.rb +39 -0
  25. data/examples/basic/msg.rb +15 -0
  26. data/examples/basic/seen.rb +37 -0
  27. data/examples/basic/urban_dict.rb +36 -0
  28. data/examples/basic/url_shorten.rb +36 -0
  29. data/examples/plugins/autovoice.rb +37 -0
  30. data/examples/plugins/custom_prefix.rb +22 -0
  31. data/examples/plugins/dice_roll.rb +38 -0
  32. data/examples/plugins/google.rb +36 -0
  33. data/examples/plugins/hello.rb +21 -0
  34. data/examples/plugins/hooks.rb +34 -0
  35. data/examples/plugins/join_part.rb +41 -0
  36. data/examples/plugins/lambdas.rb +35 -0
  37. data/examples/plugins/last_nick.rb +24 -0
  38. data/examples/plugins/msg.rb +21 -0
  39. data/examples/plugins/multiple_matches.rb +32 -0
  40. data/examples/plugins/own_events.rb +37 -0
  41. data/examples/plugins/seen.rb +44 -0
  42. data/examples/plugins/timer.rb +22 -0
  43. data/examples/plugins/url_shorten.rb +34 -0
  44. data/ircinch.gemspec +43 -0
  45. data/lib/cinch/ban.rb +53 -0
  46. data/lib/cinch/bot.rb +476 -0
  47. data/lib/cinch/cached_list.rb +21 -0
  48. data/lib/cinch/callback.rb +22 -0
  49. data/lib/cinch/channel.rb +465 -0
  50. data/lib/cinch/channel_list.rb +31 -0
  51. data/lib/cinch/configuration/bot.rb +50 -0
  52. data/lib/cinch/configuration/dcc.rb +18 -0
  53. data/lib/cinch/configuration/plugins.rb +43 -0
  54. data/lib/cinch/configuration/sasl.rb +21 -0
  55. data/lib/cinch/configuration/ssl.rb +21 -0
  56. data/lib/cinch/configuration/timeouts.rb +16 -0
  57. data/lib/cinch/configuration.rb +75 -0
  58. data/lib/cinch/constants.rb +535 -0
  59. data/lib/cinch/dcc/dccable_object.rb +39 -0
  60. data/lib/cinch/dcc/incoming/send.rb +149 -0
  61. data/lib/cinch/dcc/incoming.rb +3 -0
  62. data/lib/cinch/dcc/outgoing/send.rb +123 -0
  63. data/lib/cinch/dcc/outgoing.rb +3 -0
  64. data/lib/cinch/dcc.rb +14 -0
  65. data/lib/cinch/exceptions.rb +48 -0
  66. data/lib/cinch/formatting.rb +127 -0
  67. data/lib/cinch/handler.rb +120 -0
  68. data/lib/cinch/handler_list.rb +92 -0
  69. data/lib/cinch/helpers.rb +230 -0
  70. data/lib/cinch/i_support.rb +100 -0
  71. data/lib/cinch/irc.rb +924 -0
  72. data/lib/cinch/log_filter.rb +23 -0
  73. data/lib/cinch/logger/formatted_logger.rb +100 -0
  74. data/lib/cinch/logger/zcbot_logger.rb +26 -0
  75. data/lib/cinch/logger.rb +171 -0
  76. data/lib/cinch/logger_list.rb +88 -0
  77. data/lib/cinch/mask.rb +69 -0
  78. data/lib/cinch/message.rb +397 -0
  79. data/lib/cinch/message_queue.rb +104 -0
  80. data/lib/cinch/mode_parser.rb +78 -0
  81. data/lib/cinch/network.rb +106 -0
  82. data/lib/cinch/open_ended_queue.rb +26 -0
  83. data/lib/cinch/pattern.rb +66 -0
  84. data/lib/cinch/plugin.rb +517 -0
  85. data/lib/cinch/plugin_list.rb +40 -0
  86. data/lib/cinch/rubyext/float.rb +5 -0
  87. data/lib/cinch/rubyext/module.rb +28 -0
  88. data/lib/cinch/rubyext/string.rb +35 -0
  89. data/lib/cinch/sasl/dh_blowfish.rb +73 -0
  90. data/lib/cinch/sasl/diffie_hellman.rb +50 -0
  91. data/lib/cinch/sasl/mechanism.rb +8 -0
  92. data/lib/cinch/sasl/plain.rb +29 -0
  93. data/lib/cinch/sasl.rb +36 -0
  94. data/lib/cinch/syncable.rb +83 -0
  95. data/lib/cinch/target.rb +199 -0
  96. data/lib/cinch/timer.rb +147 -0
  97. data/lib/cinch/user.rb +489 -0
  98. data/lib/cinch/user_list.rb +89 -0
  99. data/lib/cinch/utilities/deprecation.rb +18 -0
  100. data/lib/cinch/utilities/encoding.rb +39 -0
  101. data/lib/cinch/utilities/kernel.rb +15 -0
  102. data/lib/cinch/version.rb +6 -0
  103. data/lib/cinch.rb +7 -0
  104. data/lib/ircinch.rb +7 -0
  105. 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