cinch 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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