cinch 1.0.2 → 1.1.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 (46) hide show
  1. data/README.md +25 -44
  2. data/examples/basic/autovoice.rb +1 -1
  3. data/examples/basic/join_part.rb +0 -4
  4. data/examples/plugins/autovoice.rb +2 -5
  5. data/examples/plugins/google.rb +1 -2
  6. data/examples/plugins/hooks.rb +36 -0
  7. data/examples/plugins/lambdas.rb +35 -0
  8. data/examples/plugins/last_nick.rb +24 -0
  9. data/examples/plugins/multiple_matches.rb +1 -10
  10. data/examples/plugins/own_events.rb +37 -0
  11. data/examples/plugins/timer.rb +22 -0
  12. data/examples/plugins/url_shorten.rb +1 -1
  13. data/lib/cinch.rb +50 -1
  14. data/lib/cinch/ban.rb +5 -2
  15. data/lib/cinch/bot.rb +360 -193
  16. data/lib/cinch/cache_manager.rb +15 -0
  17. data/lib/cinch/callback.rb +6 -0
  18. data/lib/cinch/channel.rb +150 -96
  19. data/lib/cinch/channel_manager.rb +26 -0
  20. data/lib/cinch/constants.rb +6 -4
  21. data/lib/cinch/exceptions.rb +9 -0
  22. data/lib/cinch/irc.rb +197 -82
  23. data/lib/cinch/logger/formatted_logger.rb +8 -8
  24. data/lib/cinch/logger/zcbot_logger.rb +37 -0
  25. data/lib/cinch/mask.rb +17 -3
  26. data/lib/cinch/message.rb +14 -7
  27. data/lib/cinch/message_queue.rb +8 -4
  28. data/lib/cinch/mode_parser.rb +56 -0
  29. data/lib/cinch/pattern.rb +45 -0
  30. data/lib/cinch/plugin.rb +129 -34
  31. data/lib/cinch/rubyext/string.rb +4 -4
  32. data/lib/cinch/syncable.rb +8 -0
  33. data/lib/cinch/user.rb +68 -13
  34. data/lib/cinch/user_manager.rb +60 -0
  35. metadata +17 -35
  36. data/Rakefile +0 -66
  37. data/lib/cinch/PLANNED +0 -4
  38. data/spec/bot_spec.rb +0 -5
  39. data/spec/channel_spec.rb +0 -5
  40. data/spec/cinch_spec.rb +0 -5
  41. data/spec/irc_spec.rb +0 -5
  42. data/spec/message_spec.rb +0 -5
  43. data/spec/plugin_spec.rb +0 -5
  44. data/spec/spec.opts +0 -2
  45. data/spec/spec_helper.rb +0 -8
  46. data/spec/user_spec.rb +0 -5
@@ -1,9 +1,12 @@
1
+ require "timeout"
2
+ require "net/protocol"
3
+
1
4
  module Cinch
2
5
  class IRC
3
6
  # @return [ISupport]
4
7
  attr_reader :isupport
5
- def initialize(bot, config)
6
- @bot, @config = bot, config
8
+ def initialize(bot)
9
+ @bot = bot
7
10
  @isupport = ISupport.new
8
11
  end
9
12
 
@@ -16,15 +19,39 @@ module Cinch
16
19
  @whois_updates = Hash.new {|h, k| h[k] = {}}
17
20
  @in_lists = Set.new
18
21
 
19
- tcp_socket = TCPSocket.open(@config.server, @config.port)
22
+ tcp_socket = nil
23
+ begin
24
+ Timeout::timeout(@bot.config.timeouts.connect) do
25
+ tcp_socket = TCPSocket.new(@bot.config.server, @bot.config.port, @bot.config.local_host)
26
+ end
27
+ rescue Timeout::Error
28
+ @bot.logger.debug("Timed out while connecting")
29
+ return
30
+ rescue => e
31
+ @bot.logger.log_exception(e)
32
+ return
33
+ end
34
+
35
+ if @bot.config.ssl == true || @bot.config.ssl == false
36
+ @bot.logger.debug "Deprecation warning: Beginning from version 1.1.0, @config.ssl should be a set of options, not a boolean value!"
37
+ end
20
38
 
21
- if @config.ssl
39
+ if @bot.config.ssl == true || (@bot.config.ssl.is_a?(OpenStruct) && @bot.config.ssl.use)
22
40
  require 'openssl'
23
41
 
24
42
  ssl_context = OpenSSL::SSL::SSLContext.new
25
- ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
26
43
 
27
- @bot.logger.debug "Using SSL with #{@config.server}:#{@config.port}"
44
+ if @bot.config.ssl.is_a?(OpenStruct)
45
+ if @bot.config.ssl.client_cert
46
+ ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(@bot.config.ssl.client_cert))
47
+ ssl_context.key = OpenSSL::PKey::RSA.new(File.read(@bot.config.ssl.client_cert))
48
+ end
49
+ ssl_context.ca_path = @bot.config.ssl.ca_path
50
+ ssl_context.verify_mode = @bot.config.ssl.verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
51
+ else
52
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
53
+ end
54
+ @bot.logger.debug "Using SSL with #{@bot.config.server}:#{@bot.config.port}"
28
55
 
29
56
  @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
30
57
  @socket.sync = true
@@ -33,32 +60,56 @@ module Cinch
33
60
  @socket = tcp_socket
34
61
  end
35
62
 
63
+ @socket = Net::BufferedIO.new(@socket)
64
+ @socket.read_timeout = @bot.config.timeouts.read
65
+
36
66
  @queue = MessageQueue.new(@socket, @bot)
37
- message "PASS #{@config.password}" if @config.password
38
- message "NICK #{@config.nick}"
39
- message "USER #{@config.nick} 0 * :#{@config.realname}"
67
+ message "PASS #{@bot.config.password}" if @bot.config.password
68
+ message "NICK #{@bot.generate_next_nick}"
69
+ message "USER #{@bot.config.nick} 0 * :#{@bot.config.realname}"
40
70
 
41
- Thread.new do
71
+ reading_thread = Thread.new do
42
72
  begin
43
- while line = @socket.gets
73
+ while line = @socket.readline
44
74
  begin
45
- line.force_encoding(@bot.config.encoding).encode!({:invalid => :replace, :undef => :replace})
75
+ line = Cinch.encode_incoming(line, @bot.config.encoding)
46
76
  parse line
47
77
  rescue => e
48
78
  @bot.logger.log_exception(e)
49
79
  end
50
80
  end
81
+ rescue Timeout::Error
82
+ @bot.logger.debug "Connection timed out."
83
+ rescue EOFError
84
+ @bot.logger.debug "Lost connection."
51
85
  rescue => e
52
86
  @bot.logger.log_exception(e)
53
87
  end
54
88
 
89
+ @socket.close
55
90
  @bot.dispatch(:disconnect)
91
+ @bot.handler_threads.each { |t| t.join(10); t.kill }
56
92
  end
57
- begin
58
- @queue.process!
59
- rescue => e
60
- @bot.logger.log_exception(e)
93
+
94
+ @sending_thread = Thread.new do
95
+ begin
96
+ @queue.process!
97
+ rescue => e
98
+ @bot.logger.log_exception(e)
99
+ end
100
+ end
101
+
102
+ ping_thread = Thread.new do
103
+ while true
104
+ sleep @bot.config.ping_interval
105
+ message("PING 0") # PING requires a single argument. In our
106
+ # case the value doesn't matter though.
107
+ end
61
108
  end
109
+
110
+ reading_thread.join
111
+ @sending_thread.kill
112
+ ping_thread.kill
62
113
  end
63
114
 
64
115
  # @api private
@@ -66,40 +117,41 @@ module Cinch
66
117
  def parse(input)
67
118
  @bot.logger.log(input, :incoming) if @bot.config.verbose
68
119
  msg = Message.new(input, @bot)
69
- events = []
120
+ events = [[:catchall]]
70
121
 
71
122
  if ("001".."004").include? msg.command
72
123
  @registration << msg.command
73
124
  if registered?
74
- events << :connect
125
+ events << [:connect]
126
+ @bot.last_connection_was_successful = true
75
127
  end
76
128
  elsif ["PRIVMSG", "NOTICE"].include?(msg.command)
77
- events << :ctcp if msg.ctcp?
129
+ events << [:ctcp] if msg.ctcp?
78
130
  if msg.channel?
79
- events << :channel
131
+ events << [:channel]
80
132
  else
81
- events << :private
133
+ events << [:private]
82
134
  end
83
135
 
84
136
  if msg.command == "PRIVMSG"
85
- events << :message
137
+ events << [:message]
86
138
  else
87
- events << :notice
139
+ events << [:notice]
88
140
  end
89
141
  else
90
142
  meth = "on_#{msg.command.downcase}"
91
- __send__(meth, msg) if respond_to?(meth, true)
143
+ __send__(meth, msg, events) if respond_to?(meth, true)
92
144
 
93
145
  if msg.error?
94
- events << :error
146
+ events << [:error]
95
147
  else
96
- events << msg.command.downcase.to_sym
148
+ events << [msg.command.downcase.to_sym]
97
149
  end
98
150
  end
99
151
 
100
- msg.instance_variable_set(:@events, events)
101
- events.each do |event|
102
- @bot.dispatch(event, msg)
152
+ msg.instance_variable_set(:@events, events.map(&:first))
153
+ events.each do |event, *args|
154
+ @bot.dispatch(event, msg, *args)
103
155
  end
104
156
  end
105
157
 
@@ -115,7 +167,7 @@ module Cinch
115
167
  end
116
168
 
117
169
  private
118
- def on_join(msg)
170
+ def on_join(msg, events)
119
171
  if msg.user == @bot
120
172
  @bot.channels << msg.channel
121
173
  msg.channel.sync_modes
@@ -123,64 +175,125 @@ module Cinch
123
175
  msg.channel.add_user(msg.user)
124
176
  end
125
177
 
126
- def on_kick(msg)
127
- target = User.find_ensured(msg.params[1], @bot)
178
+ def on_kick(msg, events)
179
+ target = @bot.user_manager.find_ensured(msg.params[1])
128
180
  if target == @bot
129
181
  @bot.channels.delete(msg.channel)
130
182
  end
131
183
  msg.channel.remove_user(target)
132
184
  end
133
185
 
134
- def on_kill(msg)
135
- user = User.find_ensured(msg.params[1], @bot)
136
- Channel.all.each do |channel|
186
+ def on_kill(msg, events)
187
+ user = @bot.user_manager.find_ensured(msg.params[1])
188
+ @bot.channel_manager.each do |channel|
137
189
  channel.remove_user(user)
138
190
  end
139
- msg.user.unsync_all
140
- end
191
+ user.unsync_all
192
+ @bot.user_manager.delete(user)
193
+ end
194
+
195
+ def on_mode(msg, events)
196
+ if msg.channel?
197
+ add_and_remove = @bot.irc.isupport["CHANMODES"]["A"] + @bot.irc.isupport["CHANMODES"]["B"] + @bot.irc.isupport["PREFIX"].keys
198
+
199
+ param_modes = {:add => @bot.irc.isupport["CHANMODES"]["C"] + add_and_remove,
200
+ :remove => add_and_remove}
201
+
202
+ modes = ModeParser.parse_modes(msg.params[1], msg.params[2..-1], param_modes)
203
+ modes.each do |direction, mode, param|
204
+ if @bot.irc.isupport["PREFIX"].keys.include?(mode)
205
+ target = @bot.User(param)
206
+ # (un)set a user-mode
207
+ if direction == :add
208
+ msg.channel.users[target] << mode unless msg.channel.users[@bot.User(param)].include?(mode)
209
+ else
210
+ msg.channel.users[target].delete mode
211
+ end
141
212
 
142
- def on_mode(msg)
143
- msg.channel.sync_modes if msg.channel?
213
+ user_events = {
214
+ "o" => "op",
215
+ "v" => "voice",
216
+ "h" => "halfop"
217
+ }
218
+ if user_events.has_key?(mode)
219
+ event = (direction == :add ? "" : "de") + user_events[mode]
220
+ events << [event.to_sym, target]
221
+ end
222
+ elsif @bot.irc.isupport["CHANMODES"]["A"].include?(mode)
223
+ case mode
224
+ when "b"
225
+ mask = param
226
+ ban = Ban.new(mask, msg.user, Time.now)
227
+
228
+ if direction == :add
229
+ msg.channel.bans_unsynced << ban
230
+ events << [:ban, ban]
231
+ else
232
+ msg.channel.bans_unsynced.delete_if {|b| b.mask == ban.mask}.first
233
+ events << [:unban, ban]
234
+ end
235
+ else
236
+ raise UnsupportedFeature, mode
237
+ end
238
+ else
239
+ # channel options
240
+ if direction == :add
241
+ msg.channel.modes_unsynced[mode] = param
242
+ else
243
+ msg.channel.modes_unsynced.delete(mode)
244
+ end
245
+ end
246
+ end
247
+
248
+ events << [:mode_change, modes]
249
+ end
144
250
  end
145
251
 
146
- def on_nick(msg)
252
+ def on_nick(msg, events)
147
253
  if msg.user == @bot
148
254
  @bot.config.nick = msg.params.last
149
255
  end
150
256
 
151
- msg.user.instance_variable_set(:@nick, msg.params.last)
257
+ msg.user.update_nick(msg.params.last)
152
258
  end
153
259
 
154
- def on_part(msg)
260
+ def on_part(msg, events)
155
261
  msg.channel.remove_user(msg.user)
156
262
  if msg.user == @bot
157
263
  @bot.channels.delete(msg.channel)
158
264
  end
159
265
  end
160
266
 
161
- def on_ping(msg)
267
+ def on_ping(msg, events)
162
268
  message "PONG :#{msg.params.first}"
163
269
  end
164
270
 
165
- def on_topic(msg)
271
+ def on_topic(msg, events)
166
272
  msg.channel.sync(:topic, msg.params[1])
167
273
  end
168
274
 
169
- def on_quit(msg)
170
- Channel.all.each do |channel|
275
+ def on_quit(msg, events)
276
+ @bot.channel_manager.each do |channel|
171
277
  channel.remove_user(msg.user)
172
278
  end
173
279
  msg.user.unsync_all
280
+ @bot.user_manager.delete(msg.user)
174
281
  end
175
282
 
176
- def on_005(msg)
283
+ def on_005(msg, events)
177
284
  # ISUPPORT
178
285
  @isupport.parse(*msg.params[1..-2].map {|v| v.split(" ")}.flatten)
179
286
  end
180
287
 
181
- def on_311(msg)
288
+ def on_307(msg, events)
289
+ # RPL_WHOISREGNICK
290
+ user = @bot.user_manager.find_ensured(msg.params[1])
291
+ @whois_updates[user].merge!({:authname => user.nick})
292
+ end
293
+
294
+ def on_311(msg, events)
182
295
  # RPL_WHOISUSER
183
- user = User.find_ensured(msg.params[1], @bot)
296
+ user = @bot.user_manager.find_ensured(msg.params[1])
184
297
  @whois_updates[user].merge!({
185
298
  :user => msg.params[2],
186
299
  :host => msg.params[3],
@@ -188,18 +301,18 @@ module Cinch
188
301
  })
189
302
  end
190
303
 
191
- def on_317(msg)
304
+ def on_317(msg, events)
192
305
  # RPL_WHOISIDLE
193
- user = User.find_ensured(msg.params[1], @bot)
306
+ user = @bot.user_manager.find_ensured(msg.params[1])
194
307
  @whois_updates[user].merge!({
195
308
  :idle => msg.params[2].to_i,
196
309
  :signed_on_at => Time.at(msg.params[3].to_i),
197
310
  })
198
311
  end
199
312
 
200
- def on_318(msg)
313
+ def on_318(msg, events)
201
314
  # RPL_ENDOFWHOIS
202
- user = User.find_ensured(msg.params[1], @bot)
315
+ user = @bot.user_manager.find_ensured(msg.params[1])
203
316
 
204
317
  if @whois_updates[user].empty? && !user.attr(:unknown?, true, true)
205
318
  user.end_of_whois(nil)
@@ -209,14 +322,14 @@ module Cinch
209
322
  end
210
323
  end
211
324
 
212
- def on_319(msg)
325
+ def on_319(msg, events)
213
326
  # RPL_WHOISCHANNELS
214
- user = User.find_ensured(msg.params[1], @bot)
215
- channels = msg.params[2].scan(/#{@isupport["CHANTYPES"].join}[^ ]+/o).map {|c| Channel.find_ensured(c, @bot) }
327
+ user = @bot.user_manager.find_ensured(msg.params[1])
328
+ channels = msg.params[2].scan(/#{@isupport["CHANTYPES"].join}[^ ]+/o).map {|c| @bot.channel_manager.find_ensured(c) }
216
329
  user.sync(:channels, channels, true)
217
330
  end
218
331
 
219
- def on_324(msg)
332
+ def on_324(msg, events)
220
333
  # RPL_CHANNELMODEIS
221
334
 
222
335
  modes = {}
@@ -232,24 +345,24 @@ module Cinch
232
345
  msg.channel.sync(:modes, modes, false)
233
346
  end
234
347
 
235
- def on_330(msg)
348
+ def on_330(msg, events)
236
349
  # RPL_WHOISACCOUNT
237
- user = User.find_ensured(msg.params[1], @bot)
350
+ user = @bot.user_manager.find_ensured(msg.params[1])
238
351
  authname = msg.params[2]
239
352
  @whois_updates[user].merge!({:authname => authname})
240
353
  end
241
354
 
242
- def on_331(msg)
355
+ def on_331(msg, events)
243
356
  # RPL_NOTOPIC
244
357
  msg.channel.sync(:topic, "")
245
358
  end
246
359
 
247
- def on_332(msg)
360
+ def on_332(msg, events)
248
361
  # RPL_TOPIC
249
362
  msg.channel.sync(:topic, msg.params[2])
250
363
  end
251
364
 
252
- def on_353(msg)
365
+ def on_353(msg, events)
253
366
  # RPL_NAMEREPLY
254
367
  unless @in_lists.include?(:names)
255
368
  msg.channel.clear_users
@@ -257,25 +370,26 @@ module Cinch
257
370
  @in_lists << :names
258
371
 
259
372
  msg.params[3].split(" ").each do |user|
260
- if @isupport["PREFIX"].values.include?(user[0..0])
261
- prefix = user[0..0]
262
- nick = user[1..-1]
373
+ m = user.match(/^([#{@isupport["PREFIX"].values.join}]+)/)
374
+ if m
375
+ prefixes = m[1].split.map {|s| @isupport["PREFIX"].key(s)}
376
+ nick = user[prefixes.size..-1]
263
377
  else
264
378
  nick = user
265
- prefix = nil
379
+ prefixes = []
266
380
  end
267
- user = User.find_ensured(nick, @bot)
268
- msg.channel.add_user(user, prefix)
381
+ user = @bot.user_manager.find_ensured(nick)
382
+ msg.channel.add_user(user, prefixes)
269
383
  end
270
384
  end
271
385
 
272
- def on_366(msg)
386
+ def on_366(msg, events)
273
387
  # RPL_ENDOFNAMES
274
388
  @in_lists.delete :names
275
389
  msg.channel.mark_as_synced(:users)
276
390
  end
277
391
 
278
- def on_367(msg)
392
+ def on_367(msg, events)
279
393
  # RPL_BANLIST
280
394
  unless @in_lists.include?(:bans)
281
395
  msg.channel.bans_unsynced.clear
@@ -283,14 +397,14 @@ module Cinch
283
397
  @in_lists << :bans
284
398
 
285
399
  mask = msg.params[2]
286
- by = User.find_ensured(msg.params[3].split("!").first, @bot)
400
+ by = @bot.user_manager.find_ensured(msg.params[3].split("!").first)
287
401
  at = Time.at(msg.params[4].to_i)
288
402
 
289
403
  ban = Ban.new(mask, by, at)
290
404
  msg.channel.bans_unsynced << ban
291
405
  end
292
406
 
293
- def on_368(msg)
407
+ def on_368(msg, events)
294
408
  # RPL_ENDOFBANLIST
295
409
  if @in_lists.include?(:bans)
296
410
  @in_lists.delete :bans
@@ -302,14 +416,15 @@ module Cinch
302
416
  msg.channel.mark_as_synced(:bans)
303
417
  end
304
418
 
305
- def on_396(msg)
419
+ def on_396(msg, events)
420
+ # RPL_HOSTHIDDEN
306
421
  # note: designed for freenode
307
- User.find_ensured(msg.params[0], @bot).sync(:host, msg.params[1], true)
422
+ @bot.user_manager.find_ensured(msg.params[0]).sync(:host, msg.params[1], true)
308
423
  end
309
424
 
310
- def on_401(msg)
425
+ def on_401(msg, events)
311
426
  # ERR_NOSUCHNICK
312
- user = User.find_ensured(msg.params[1], @bot)
427
+ user = @bot.user_manager.find_ensured(msg.params[1])
313
428
  user.sync(:unknown?, true, true)
314
429
  if @whois_updates.key?(user)
315
430
  user.end_of_whois(nil, true)
@@ -317,23 +432,23 @@ module Cinch
317
432
  end
318
433
  end
319
434
 
320
- def on_402(msg)
435
+ def on_402(msg, events)
321
436
  # ERR_NOSUCHSERVER
322
437
 
323
- if user = User.find(msg.params[1]) # not _ensured, we only want a user that already exists
438
+ if user = @bot.user_manager.find(msg.params[1]) # not _ensured, we only want a user that already exists
324
439
  user.end_of_whois(nil, true)
325
440
  @whois_updates.delete user
326
441
  # TODO freenode specific, test on other IRCd
327
442
  end
328
443
  end
329
444
 
330
- def on_433(msg)
445
+ def on_433(msg, events)
331
446
  # ERR_NICKNAMEINUSE
332
- @bot.nick = msg.params[1] + "_"
447
+ @bot.nick = @bot.generate_next_nick(msg.params[1])
333
448
  end
334
449
 
335
- def on_671(msg)
336
- user = User.find_ensured(msg.params[1], @bot)
450
+ def on_671(msg, events)
451
+ user = @bot.user_manager.find_ensured(msg.params[1])
337
452
  @whois_updates[user].merge!({:secure? => true})
338
453
  end
339
454
  end