cinch 0.3.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +192 -0
  3. data/Rakefile +53 -43
  4. data/examples/basic/autovoice.rb +32 -0
  5. data/examples/basic/google.rb +35 -0
  6. data/examples/basic/hello.rb +15 -0
  7. data/examples/basic/join_part.rb +38 -0
  8. data/examples/basic/memo.rb +39 -0
  9. data/examples/basic/msg.rb +16 -0
  10. data/examples/basic/seen.rb +36 -0
  11. data/examples/basic/urban_dict.rb +35 -0
  12. data/examples/basic/url_shorten.rb +35 -0
  13. data/examples/plugins/autovoice.rb +40 -0
  14. data/examples/plugins/custom_prefix.rb +23 -0
  15. data/examples/plugins/google.rb +37 -0
  16. data/examples/plugins/hello.rb +22 -0
  17. data/examples/plugins/join_part.rb +42 -0
  18. data/examples/plugins/memo.rb +50 -0
  19. data/examples/plugins/msg.rb +22 -0
  20. data/examples/plugins/multiple_matches.rb +41 -0
  21. data/examples/plugins/seen.rb +45 -0
  22. data/examples/plugins/urban_dict.rb +30 -0
  23. data/examples/plugins/url_shorten.rb +32 -0
  24. data/lib/cinch.rb +7 -20
  25. data/lib/cinch/ban.rb +41 -0
  26. data/lib/cinch/bot.rb +479 -0
  27. data/lib/cinch/callback.rb +11 -0
  28. data/lib/cinch/channel.rb +419 -0
  29. data/lib/cinch/constants.rb +369 -0
  30. data/lib/cinch/exceptions.rb +25 -0
  31. data/lib/cinch/helpers.rb +21 -0
  32. data/lib/cinch/irc.rb +344 -38
  33. data/lib/cinch/isupport.rb +96 -0
  34. data/lib/cinch/logger/formatted_logger.rb +80 -0
  35. data/lib/cinch/logger/logger.rb +44 -0
  36. data/lib/cinch/logger/null_logger.rb +18 -0
  37. data/lib/cinch/mask.rb +46 -0
  38. data/lib/cinch/message.rb +183 -0
  39. data/lib/cinch/message_queue.rb +62 -0
  40. data/lib/cinch/plugin.rb +205 -0
  41. data/lib/cinch/rubyext/infinity.rb +1 -0
  42. data/lib/cinch/rubyext/module.rb +18 -0
  43. data/lib/cinch/rubyext/queue.rb +19 -0
  44. data/lib/cinch/rubyext/string.rb +24 -0
  45. data/lib/cinch/syncable.rb +55 -0
  46. data/lib/cinch/user.rb +325 -0
  47. data/spec/bot_spec.rb +5 -0
  48. data/spec/channel_spec.rb +5 -0
  49. data/spec/cinch_spec.rb +5 -0
  50. data/spec/irc_spec.rb +5 -0
  51. data/spec/message_spec.rb +5 -0
  52. data/spec/plugin_spec.rb +5 -0
  53. data/spec/{helper.rb → spec_helper.rb} +0 -0
  54. data/spec/user_spec.rb +5 -0
  55. metadata +69 -51
  56. data/README.rdoc +0 -195
  57. data/examples/autovoice.rb +0 -32
  58. data/examples/custom_patterns.rb +0 -19
  59. data/examples/custom_prefix.rb +0 -25
  60. data/examples/google.rb +0 -31
  61. data/examples/hello.rb +0 -13
  62. data/examples/join_part.rb +0 -26
  63. data/examples/memo.rb +0 -40
  64. data/examples/msg.rb +0 -14
  65. data/examples/named-param-types.rb +0 -19
  66. data/examples/seen.rb +0 -41
  67. data/examples/urban_dict.rb +0 -31
  68. data/examples/url_shorten.rb +0 -34
  69. data/lib/cinch/base.rb +0 -368
  70. data/lib/cinch/irc/message.rb +0 -135
  71. data/lib/cinch/irc/parser.rb +0 -141
  72. data/lib/cinch/irc/socket.rb +0 -329
  73. data/lib/cinch/names.rb +0 -54
  74. data/lib/cinch/rules.rb +0 -171
  75. data/spec/base_spec.rb +0 -94
  76. data/spec/irc/helper.rb +0 -8
  77. data/spec/irc/message_spec.rb +0 -61
  78. data/spec/irc/parser_spec.rb +0 -103
  79. data/spec/irc/socket_spec.rb +0 -90
  80. data/spec/names_spec.rb +0 -393
  81. data/spec/options_spec.rb +0 -45
  82. data/spec/rules_spec.rb +0 -109
@@ -0,0 +1,25 @@
1
+ module Cinch
2
+ module Exceptions
3
+ # Generic error. Superclass for all Cinch-specific errors.
4
+ class Generic < ::StandardError
5
+ end
6
+
7
+ class ArgumentTooLong < Generic
8
+ end
9
+
10
+ # Error that is raised when a topic is too long to be set.
11
+ class TopicTooLong < ArgumentTooLong
12
+ end
13
+
14
+ # Error that is raised when a nick is too long to be used.
15
+ class NickTooLong < ArgumentTooLong
16
+ end
17
+
18
+ # Error that is raised when a kick reason is too long.
19
+ class KickReasonTooLong < ArgumentTooLong
20
+ end
21
+
22
+ class UnsupportedFeature < Generic
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ module Cinch
2
+ module Helpers
3
+ # Helper method for turning a String into a {Channel} object.
4
+ #
5
+ # @param (see Bot#Channel)
6
+ # @return (see Bot#Channel)
7
+ # @example (see Bot#Channel)
8
+ def Channel(*args)
9
+ @bot.Channel(*args)
10
+ end
11
+
12
+ # Helper method for turning a String into an {User} object.
13
+ #
14
+ # @param (see Bot#User)
15
+ # @return (see Bot#User)
16
+ # @example (see Bot#User)
17
+ def User(*args)
18
+ @bot.User(*args)
19
+ end
20
+ end
21
+ end
@@ -1,45 +1,351 @@
1
- lib = File.dirname(__FILE__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
1
+ module Cinch
2
+ class IRC
3
+ # @return [ISupport]
4
+ attr_reader :isupport
5
+ def initialize(bot, config)
6
+ @bot, @config = bot, config
7
+ @isupport = ISupport.new
8
+ end
3
9
 
4
- require 'socket'
5
- require 'timeout'
10
+ # Establish a connection.
11
+ #
12
+ # @return [void]
13
+ def connect
14
+ @registration = []
6
15
 
7
- require 'irc/parser'
8
- require 'irc/message'
9
- require 'irc/socket'
16
+ @whois_updates = Hash.new {|h, k| h[k] = {}}
17
+ @in_lists = Set.new
10
18
 
11
- module Cinch
19
+ tcp_socket = TCPSocket.open(@config.server, @config.port)
20
+
21
+ if @config.ssl
22
+ require 'openssl'
23
+
24
+ ssl_context = OpenSSL::SSL::SSLContext.new
25
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
26
+
27
+ @bot.logger.debug "Using SSL with #{@config.server}:#{@config.port}"
28
+
29
+ @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
30
+ @socket.sync = true
31
+ @socket.connect
32
+ else
33
+ @socket = tcp_socket
34
+ end
35
+ @socket.set_encoding(@bot.config.encoding || Encoding.default_external,
36
+ Encoding.default_internal,
37
+ {:invalid => :replace, :undef => :replace})
38
+
39
+ @queue = MessageQueue.new(@socket, @bot)
40
+ message "PASS #{@config.password}" if @config.password
41
+ message "NICK #{@config.nick}"
42
+ message "USER #{@config.nick} 0 * :#{@config.realname}"
43
+
44
+ Thread.new do
45
+ begin
46
+ while line = @socket.gets
47
+ begin
48
+ parse line
49
+ rescue => e
50
+ @bot.logger.log_exception(e)
51
+ end
52
+ end
53
+ rescue => e
54
+ @bot.logger.log_exception(e)
55
+ end
56
+
57
+ @bot.dispatch(:disconnect)
58
+ end
59
+ begin
60
+ @queue.process!
61
+ rescue => e
62
+ @bot.logger.log_exception(e)
63
+ end
64
+ end
65
+
66
+ # @api private
67
+ # @return [void]
68
+ def parse(input)
69
+ @bot.logger.log(input, :incoming) if @bot.config.verbose
70
+ msg = Message.new(input, @bot)
71
+ events = []
72
+
73
+ if ("001".."004").include? msg.command
74
+ @registration << msg.command
75
+ if registered?
76
+ events << :connect
77
+ end
78
+ elsif ["PRIVMSG", "NOTICE"].include?(msg.command)
79
+ events << :ctcp if msg.ctcp?
80
+ if msg.channel?
81
+ events << :channel
82
+ else
83
+ events << :private
84
+ end
85
+
86
+ if msg.command == "PRIVMSG"
87
+ events << :message
88
+ else
89
+ events << :notice
90
+ end
91
+ else
92
+ meth = "on_#{msg.command.downcase}"
93
+ __send__(meth, msg) if respond_to?(meth, true)
94
+
95
+ if msg.error?
96
+ events << :error
97
+ else
98
+ events << msg.command.downcase.to_sym
99
+ end
100
+ end
101
+
102
+ msg.instance_variable_set(:@events, events)
103
+ events.each do |event|
104
+ @bot.dispatch(event, msg)
105
+ end
106
+ end
107
+
108
+ # @return [Boolean] true if we successfully registered yet
109
+ def registered?
110
+ (("001".."004").to_a - @registration).empty?
111
+ end
112
+
113
+ # Send a message.
114
+ # @return [void]
115
+ def message(msg)
116
+ @queue.queue(msg)
117
+ end
118
+
119
+ private
120
+ def on_join(msg)
121
+ if msg.user == @bot
122
+ @bot.channels << msg.channel
123
+ msg.channel.sync_modes
124
+ end
125
+ msg.channel.add_user(msg.user)
126
+ end
127
+
128
+ def on_kick(msg)
129
+ target = User.find_ensured(msg.params[1], @bot)
130
+ if target == @bot
131
+ @bot.channels.delete(msg.channel)
132
+ end
133
+ msg.channel.remove_user(target)
134
+ end
135
+
136
+ def on_kill(msg)
137
+ user = User.find_ensured(msg.params[1], @bot)
138
+ Channel.all.each do |channel|
139
+ channel.remove_user(user)
140
+ end
141
+ user.synced = false
142
+ end
143
+
144
+ def on_mode(msg)
145
+ msg.channel.sync_modes if msg.channel?
146
+ end
147
+
148
+ def on_nick(msg)
149
+ if msg.user == @bot
150
+ @bot.config.nick = msg.params.last
151
+ end
152
+
153
+ msg.user.instance_variable_set(:@nick, msg.params.last)
154
+ end
155
+
156
+ def on_part(msg)
157
+ msg.channel.remove_user(msg.user)
158
+ if msg.user == @bot
159
+ @bot.channels.delete(msg.channel)
160
+ end
161
+ end
162
+
163
+ def on_ping(msg)
164
+ message "PONG :#{msg.params.first}"
165
+ end
166
+
167
+ def on_topic(msg)
168
+ msg.channel.sync(:topic, msg.params[1])
169
+ end
170
+
171
+ def on_quit(msg)
172
+ Channel.all.each do |channel|
173
+ channel.remove_user(msg.user)
174
+ end
175
+ msg.user.synced = false
176
+ end
177
+
178
+ def on_005(msg)
179
+ # ISUPPORT
180
+ @isupport.parse(*msg.params[1..-2].map {|v| v.split(" ")}.flatten)
181
+ end
182
+
183
+ def on_311(msg)
184
+ # RPL_WHOISUSER
185
+ user = User.find_ensured(msg.params[1], @bot)
186
+ @whois_updates[user].merge!({
187
+ :user => msg.params[2],
188
+ :host => msg.params[3],
189
+ :realname => msg.params[5],
190
+ })
191
+ end
192
+
193
+ def on_317(msg)
194
+ # RPL_WHOISIDLE
195
+ user = User.find_ensured(msg.params[1], @bot)
196
+ @whois_updates[user].merge!({
197
+ :idle => msg.params[2].to_i,
198
+ :signed_on_at => Time.at(msg.params[3].to_i),
199
+ })
200
+ end
201
+
202
+ def on_318(msg)
203
+ # RPL_ENDOFWHOIS
204
+ user = User.find_ensured(msg.params[1], @bot)
205
+ user.instance_variable_set(:@in_whois, false)
206
+ if @whois_updates[user].empty? && !user.attr(:unknown?, true, true)
207
+ # for some reason, we did not receive user information. one
208
+ # reason is freenode throttling WHOIS
209
+ Thread.new do
210
+ sleep 2
211
+ user.whois
212
+ end
213
+ else
214
+ {
215
+ :authname => nil,
216
+ :idle => 0,
217
+ :secure? => false,
218
+ }.merge(@whois_updates[user]).each do |attr, value|
219
+ user.sync(attr, value, true)
220
+ end
221
+
222
+ user.sync(:unknown?, false, true)
223
+ user.instance_variable_set(:@synced, true)
224
+ @whois_updates.delete user
225
+ end
226
+ end
227
+
228
+ def on_319(msg)
229
+ # RPL_WHOISCHANNELS
230
+ user = User.find_ensured(msg.params[1], @bot)
231
+ channels = msg.params[2].scan(/#{@isupport["CHANTYPES"].join}[^ ]+/o).map {|c| Channel.find_ensured(c, @bot) }
232
+ user.sync(:channels, channels, true)
233
+ end
234
+
235
+ def on_324(msg)
236
+ # RPL_CHANNELMODEIS
237
+
238
+ modes = {}
239
+ arguments = msg.params[3..-1]
240
+ msg.params[2][1..-1].split("").each do |mode|
241
+ if (@isupport["CHANMODES"]["B"] + @isupport["CHANMODES"]["C"]).include?(mode)
242
+ modes[mode] = arguments.shift
243
+ else
244
+ modes[mode] = true
245
+ end
246
+ end
247
+
248
+ msg.channel.sync(:modes, modes, false)
249
+ end
250
+
251
+ def on_330(msg)
252
+ # RPL_WHOISACCOUNT
253
+ user = User.find_ensured(msg.params[1], @bot)
254
+ authname = msg.params[2]
255
+ @whois_updates[user].merge!({:authname => authname})
256
+ end
257
+
258
+ def on_331(msg)
259
+ # RPL_NOTOPIC
260
+ msg.channel.sync(:topic, "")
261
+ end
262
+
263
+ def on_332(msg)
264
+ # RPL_TOPIC
265
+ msg.channel.sync(:topic, msg.params[2])
266
+ end
267
+
268
+ def on_353(msg)
269
+ # RPL_NAMEREPLY
270
+ unless @in_lists.include?(:names)
271
+ msg.channel.clear_users
272
+ end
273
+ @in_lists << :names
274
+
275
+ msg.params[3].split(" ").each do |user|
276
+ if @isupport["PREFIX"].values.include?(user[0..0])
277
+ prefix = user[0..0]
278
+ nick = user[1..-1]
279
+ else
280
+ nick = user
281
+ prefix = nil
282
+ end
283
+ user = User.find_ensured(nick, @bot)
284
+ msg.channel.add_user(user, prefix)
285
+ end
286
+ end
287
+
288
+ def on_366(msg)
289
+ # RPL_ENDOFNAMES
290
+ @in_lists.delete :names
291
+ msg.channel.mark_as_synced(:users)
292
+ end
293
+
294
+ def on_367(msg)
295
+ # RPL_BANLIST
296
+ unless @in_lists.include?(:bans)
297
+ msg.channel.bans_unsynced.clear
298
+ end
299
+ @in_lists << :bans
300
+
301
+ mask = msg.params[2]
302
+ by = User.find_ensured(msg.params[3].split("!").first, @bot)
303
+ at = Time.at(msg.params[4].to_i)
304
+
305
+ ban = Ban.new(mask, by, at)
306
+ msg.channel.bans_unsynced << ban
307
+ end
308
+
309
+ def on_368(msg)
310
+ # RPL_ENDOFBANLIST
311
+ if @in_lists.include?(:bans)
312
+ @in_lists.delete :bans
313
+ else
314
+ # we never received a ban, yet an end of list => no bans
315
+ msg.channel.bans_unsynced.clear
316
+ end
317
+
318
+ msg.channel.mark_as_synced(:bans)
319
+ end
320
+
321
+ def on_396(msg)
322
+ # note: designed for freenode
323
+ User.find_ensured(msg.params[0], @bot).sync(:host, msg.params[1], true)
324
+ end
325
+
326
+ def on_401(msg)
327
+ # ERR_NOSUCHNICK
328
+ user = User.find_ensured(msg.params[1], @bot)
329
+ user.sync(:unknown?, true, true)
330
+ end
331
+
332
+ def on_402(msg)
333
+ # ERR_NOSUCHSERVER
334
+ if user = User.find(msg.params[1]) # not _ensured, we only want a user that already exists
335
+ user.sync(:unknown?, true, true)
336
+ @whois_updates.delete user
337
+ # TODO freenode specific, test on other IRCd
338
+ end
339
+ end
12
340
 
13
- # == Description
14
- # Cinch::IRC provides tools to interact with an IRC server, this
15
- # includes reading/writing/parsing and building a message response.
16
- #
17
- # You can use these tools through Cinch or include them directly and use
18
- # them on their own.
19
- #
20
- # Each class inside of this module can be used direcly as they contain
21
- # no references to higher level classes inside Cinch
22
- #
23
- # == Example
24
- # require 'cinch/irc'
25
- # require 'pp'
26
- #
27
- # parser = Cinch::IRC::Parser.new
28
- #
29
- # Cinch::IRC::Socket.new('irc.2600.net') do |irc|
30
- # irc.nick "Cinch"
31
- # irc.user "Cinch", 0, '*', "Cinch IRC bot"
32
- #
33
- # while line = irc.read
34
- # message = parser.parse(line)
35
- #
36
- # pp message
37
- # end
38
- # end
39
- #
40
- # == Author
41
- # * Lee Jarvis - ljjarvis@gmail.com
42
- module IRC
341
+ def on_433(msg)
342
+ # ERR_NICKNAMEINUSE
343
+ @bot.nick = msg.params[1] + "_"
344
+ end
43
345
 
346
+ def on_671(msg)
347
+ user = User.find_ensured(msg.params[1], @bot)
348
+ @whois_updates[user].merge!({:secure? => true})
349
+ end
44
350
  end
45
351
  end
@@ -0,0 +1,96 @@
1
+ module Cinch
2
+ class ISupport < Hash
3
+ @@mappings = {
4
+ %w[PREFIX] => lambda {|v|
5
+ modes, prefixes = v.match(/^\((.+)\)(.+)$/)[1..2]
6
+ h = {}
7
+ modes.split("").each_with_index do |c, i|
8
+ h[c] = prefixes[i]
9
+ end
10
+ h
11
+ },
12
+
13
+ %w[CHANTYPES] => lambda {|v| v.split("")},
14
+ %w[CHANMODES] => lambda {|v|
15
+ h = {}
16
+ h["A"], h["B"], h["C"], h["D"] = v.split(",").map {|l| l.split("")}
17
+ h
18
+ },
19
+
20
+ %w[MODES MAXCHANNELS NICKLEN MAXBANS TOPICLEN
21
+ KICKLEN CHANNELLEN CHIDLEN SILENCE AWAYLEN
22
+ MAXTARGETS WATCH] => lambda {|v| v.to_i},
23
+
24
+ %w[CHANLIMIT MAXLIST IDCHAN] => lambda {|v|
25
+ h = {}
26
+ v.split(",").each do |pair|
27
+ args, num = pair.split(":")
28
+ args.split("").each do |arg|
29
+ h[arg] = num.to_i
30
+ end
31
+ end
32
+ h
33
+ },
34
+
35
+ %w[TARGMAX] => lambda {|v|
36
+ h = {}
37
+ v.split(",").each do |pair|
38
+ name, value = pair.split(":")
39
+ h[name] = value.to_i
40
+ end
41
+ h
42
+ },
43
+
44
+ %w[NETWORK] => lambda {|v| v},
45
+ %w[STATUSMSG] => lambda {|v| v.split("")},
46
+ %w[CASEMAPPING] => lambda {|v| v.to_sym},
47
+ %w[ELIST] => lambda {|v| v.split("")},
48
+ # TODO STD
49
+ }
50
+
51
+ def initialize(*args)
52
+ super
53
+ # by setting most numeric values to "Infinity", we let the
54
+ # server truncate messages and lists while at the same time
55
+ # allowing the use of strictness=:strict for servers that don't
56
+ # support ISUPPORT (hopefully none, anyway)
57
+
58
+ self["PREFIX"] = {"o" => "@", "v" => "+"}
59
+ self["CHANTYPES"] = ["#"]
60
+ self["CHANMODES"] = {
61
+ "A" => ["b"],
62
+ "B" => ["k"],
63
+ "C" => ["l"],
64
+ "D" => %w[i m n p s t r]
65
+ }
66
+ self["MODES"] = 1
67
+ self["NICKLEN"] = Infinity
68
+ self["MAXBANS"] = Infinity
69
+ self["TOPICLEN"] = Infinity
70
+ self["KICKLEN"] = Infinity
71
+ self["CHANNELLEN"] = Infinity
72
+ self["CHIDLEN"] = 5
73
+ self["AWAYLEN"] = Infinity
74
+ self["MAXTARGETS"] = 1
75
+ self["MAXCHANNELS"] = Infinity # deprecated
76
+ self["CHANLIMIT"] = {"#" => Infinity}
77
+ self["STATUSMSG"] = ["@", "+"]
78
+ self["CASEMAPPING"] = :rfc1459
79
+ self["ELIST"] = []
80
+ end
81
+
82
+ # @api private
83
+ # @return [void]
84
+ def parse(*options)
85
+ options.each do |option|
86
+ name, value = option.split("=")
87
+ if value
88
+ proc = @@mappings.find {|key, value| key.include?(name)}
89
+ self[name] = (proc && proc[1].call(value)) || value
90
+ else
91
+ self[name] = true
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end