cinch 0.3.5 → 1.0.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 (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