masover-net-irc 0.9.3.1

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.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 unwwwired.net
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,25 @@
1
+ # Net::IRC
2
+
3
+ A ruby implementation of the IRC client protocol.
4
+
5
+ ## Installation
6
+
7
+ Run the following if you haven't already:
8
+
9
+ $ gem sources -a http://gems.github.com
10
+
11
+ Install the gem(s):
12
+
13
+ $ sudo gem install -r sbfaulkner-net-irc
14
+
15
+ ## Example
16
+
17
+ The gem installs the nicl command-line client (pronounced "nickle", as in worth
18
+ about a...). This client is strictly intended as a sample usage of and testbed
19
+ for Net::IRC, and is not meant as an example of how to write a command-line
20
+ client.
21
+
22
+ ## Legal
23
+
24
+ **Author:** S. Brent Faulkner <brentf@unwwwired.net>
25
+ **License:** Copyright &copy; 2008 unwwwired.net, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ Gem::manage_gems
3
+ require 'rake/gempackagetask'
4
+
5
+ load File.join(File.dirname(__FILE__),'net-irc.gemspec')
6
+
7
+ Rake::GemPackageTask.new(SPEC) do |pkg|
8
+ end
9
+
10
+ task :default => :package
data/bin/nicl ADDED
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/ruby
2
+ #
3
+ # nicl Copyright © 2007-2008 unwwwired.net
4
+ # Created by: S. Brent Faulkner (brentf@unwwwired.net) 2007-08-29
5
+ #
6
+
7
+ require 'rubygems'
8
+ require 'net/irc'
9
+
10
+ module Ansi
11
+ RESET = 0
12
+ BOLD = 1
13
+ DIM = 2
14
+ UNDERSCORE = 4
15
+ BLINK = 5
16
+ REVERSE = 7
17
+ HIDDEN = 8
18
+
19
+ BLACK = 0
20
+ RED = 1
21
+ GREEN = 2
22
+ YELLOW = 3
23
+ BLUE = 4
24
+ MAGENTA = 5
25
+ CYAN = 6
26
+ WHITE = 7
27
+
28
+ def esc(*attrs)
29
+ "\033[#{attrs.join(';')}m"
30
+ end
31
+
32
+ def fg(colour)
33
+ 30 + colour
34
+ end
35
+
36
+ def bg(colour)
37
+ 40 + colour
38
+ end
39
+
40
+ def highlight(text, *attrs)
41
+ "#{esc(*attrs)}#{text}#{esc(RESET)}"
42
+ end
43
+ end
44
+
45
+ include Ansi
46
+
47
+ begin
48
+ require 'termios'
49
+ # real implementation for toggling echo
50
+ def echo(on = true)
51
+ oldt = Termios.tcgetattr(STDIN)
52
+ newt = oldt.dup
53
+ newt.lflag &= ~Termios::ECHO
54
+ Termios.tcsetattr(STDIN, Termios::TCSANOW, newt)
55
+
56
+ # if no block is provided, return the original echo setting
57
+ return (oldt.lflag & Termios::ECHO) == Termios::ECHO unless block_given?
58
+
59
+ # otherwise yield to the block and restore the original echo setting
60
+ ret = yield
61
+ Termios.tcsetattr(STDIN, Termios::TCSANOW, oldt)
62
+ ret
63
+ end
64
+ rescue LoadError
65
+ # minimal stub in case Termios is not installed
66
+ def echo(on = true)
67
+ return true unless block_given?
68
+ yield
69
+ end
70
+ end
71
+
72
+ def prompt(text, hidden = false)
73
+ echo(false) do
74
+ print text
75
+ line = STDIN.readline.chomp
76
+ print "\n"
77
+ line
78
+ end
79
+ end
80
+
81
+ # TODO: a bit backwards... should probably be "Net::IRC.logger = logger"
82
+ logger = Net::IRC.logger
83
+
84
+ Net::IRC.logger.level = Logger::DEBUG
85
+ Net::IRC.logger.datetime_format = "%Y/%m/%d %H:%M:%S"
86
+
87
+ Thread.abort_on_exception = true
88
+
89
+ server = ARGV[0] || "irc.freenode.net"
90
+ port = ARGV[1] && ARGV[1].to_i || 6667
91
+ user = ARGV[2] || prompt('User: ')
92
+ full_name = ARGV[3] || ARGV[2] || prompt('Full name: ')
93
+ password = ARGV[4] || prompt('Password: ', true)
94
+
95
+ Net::IRC.start user, password, full_name, server, port do |irc|
96
+ Thread.new do
97
+ irc.each do |message|
98
+ case message
99
+ # TODO: required = VERSION, PING, CLIENTINFO, ACTION
100
+ # TODO: handle internally... probably true for most CTCP requests
101
+ when Net::IRC::CTCPVersion
102
+ irc.ctcp_version(message.source, "net-irc simple-client", Net::IRC::VERSION, PLATFORM, "http://www.github.com/sbfaulkner/net-irc")
103
+
104
+ when Net::IRC::CTCPAction
105
+ puts "#{highlight(message.source, BOLD, fg(YELLOW))} #{highlight(message.target, BOLD, fg(GREEN))}: #{highlight(message.text, BOLD)}"
106
+
107
+ when Net::IRC::CTCPPing
108
+ irc.ctcp_ping(message.source, message.arg)
109
+
110
+ when Net::IRC::CTCPTime
111
+ irc.ctcp_time(message.source)
112
+
113
+ when Net::IRC::CTCP
114
+ puts highlight("Unhandled CTCP REQUEST: #{message.class} (#{message.keyword})", BOLD, fg(RED))
115
+
116
+ when Net::IRC::Join
117
+ puts "#{highlight(message.prefix.nickname, BOLD, fg(YELLOW))} joined #{highlight(message.channels.first, BOLD, fg(GREEN))}."
118
+
119
+ when Net::IRC::Part
120
+ if message.text && ! message.text.empty?
121
+ puts "#{highlight(message.prefix.nickname, BOLD, fg(YELLOW))} has left #{highlight(message.channels.first, BOLD, fg(GREEN))} (#{message.text})."
122
+ else
123
+ puts "#{highlight(message.prefix.nickname, BOLD, fg(YELLOW))} has left #{highlight(message.channels.first, BOLD, fg(GREEN))}."
124
+ end
125
+
126
+ when Net::IRC::Mode
127
+ # TODO: handle internally
128
+ puts highlight("#{message.channel} mode changed #{message.modes}", fg(BLUE))
129
+
130
+ when Net::IRC::Quit
131
+ if message.text && ! message.text.empty?
132
+ puts "#{highlight(message.prefix.nickname, BOLD, fg(YELLOW))} has quit (#{message.text})."
133
+ else
134
+ puts "#{highlight(message.prefix.nickname, BOLD, fg(YELLOW))} has quit."
135
+ end
136
+
137
+ when Net::IRC::Notice
138
+ puts highlight(message.text, fg(CYAN))
139
+
140
+ when Net::IRC::Privmsg
141
+ puts "#{highlight(message.prefix.nickname, BOLD, fg(YELLOW))} #{highlight(message.target, BOLD, fg(GREEN))}: #{highlight(message.text, BOLD)}"
142
+
143
+ when Net::IRC::Nick
144
+ puts "#{highlight(message.prefix.nickname, BOLD)} is now #{highlight(message.nickname, BOLD, fg(YELLOW))}"
145
+
146
+ when Net::IRC::ErrNicknameinuse
147
+ irc.nick message.nickname.sub(/\d*$/) { |n| n.to_i + 1 }
148
+
149
+ when Net::IRC::ErrNeedreggednick
150
+ irc.privmsg('nickserv', 'help')
151
+
152
+ when Net::IRC::RplLoggedin, Net::IRC::RplLoggedout
153
+ puts highlight(message.text, fg(GREEN))
154
+
155
+ when Net::IRC::Error
156
+ puts highlight("Unhandled ERROR: #{message.class} (#{message.command})", BOLD, fg(RED))
157
+
158
+ when Net::IRC::RplWelcome, Net::IRC::RplYourhost, Net::IRC::RplCreated
159
+ puts message.text
160
+
161
+ when Net::IRC::RplLuserclient, Net::IRC::RplLuserme, Net::IRC::RplLocalusers, Net::IRC::RplGlobalusers, Net::IRC::RplStatsconn
162
+ puts highlight(message.text, fg(BLUE))
163
+
164
+ when Net::IRC::RplLuserop, Net::IRC::RplLuserchannels
165
+ puts highlight("#{message.count} #{message.text}", fg(BLUE))
166
+
167
+ when Net::IRC::RplIsupport
168
+ # TODO: handle internally... parse into capabilities collection
169
+
170
+ when Net::IRC::RplMyinfo
171
+ when Net::IRC::RplMotdstart
172
+
173
+ when Net::IRC::RplTopic
174
+ # TODO: handle internally
175
+ puts "#{highlight(message.channel, BOLD, fg(GREEN))}: #{message.text}"
176
+
177
+ when Net::IRC::RplTopicwhotime
178
+ # TODO: handle internally
179
+ puts "#{highlight(message.channel, BOLD, fg(GREEN))}: #{message.nickname} #{message.time.strftime("%Y/%m/%d %H:%M:%S")}"
180
+
181
+ when Net::IRC::RplNamreply
182
+ # TODO: handle internally
183
+ puts "#{highlight(message.channel, BOLD, fg(GREEN))}: #{message.names.join(', ')}"
184
+
185
+ when Net::IRC::RplEndofnames
186
+ # TODO: handle internally
187
+
188
+ when Net::IRC::RplMotd
189
+ puts message.text.sub(/^- /,'')
190
+
191
+ when Net::IRC::RplEndofmotd
192
+ puts ""
193
+
194
+ when Net::IRC::Reply
195
+ puts highlight("Unhandled REPLY: #{message.class} (#{message.command})", BOLD, fg(RED))
196
+
197
+ when Net::IRC::Message
198
+ puts highlight("Unhandled MESSAGE: #{message.class} (#{message.command})", BOLD, fg(RED))
199
+
200
+ else
201
+ raise IOError, "unknown class #{message.class}"
202
+
203
+ end
204
+ end
205
+ end
206
+
207
+ while line = STDIN.readline
208
+ scanner = StringScanner.new(line.chomp)
209
+ if command = scanner.scan(/\/(\S+)\s*/) && scanner[1]
210
+ case command.upcase
211
+ when 'JOIN'
212
+ # TODO: validate arguments... support for password... etc.
213
+ irc.join scanner.rest
214
+
215
+ when 'MSG'
216
+ # TODO: validate arguments... support for password... etc.
217
+ scanner.scan(/(\S+)\s+(.*)/)
218
+ irc.privmsg(scanner[1], scanner[2])
219
+ when 'PART'
220
+ # TODO: validate arguments... support for password... etc.
221
+ irc.part scanner.rest
222
+
223
+ when 'QUIT'
224
+ break
225
+ else
226
+ puts highlight("Unknown COMMAND: #{command}", BOLD, fg(RED))
227
+ end
228
+ elsif scanner.scan(/(\S+)\s+(.*)/)
229
+ irc.privmsg(scanner[1], scanner[2])
230
+ else
231
+ # TODO: error? need a concept of a current room
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,14 @@
1
+ ---
2
+ 14: RPL_YOURCOOKIE
3
+ 220: RPL_STATSPLINE
4
+ 224: RPL_STATSFLINE
5
+ 225: RPL_STATSDLINE
6
+ 246: RPL_STATSULINE
7
+ 247: RPL_STATSXLINE
8
+ 249: RPL_STATSDEBUG
9
+ 265: RPL_LOCALUSERS
10
+ 266: RPL_GLOBALUSERS
11
+ 385: RPL_NOTOPERANYMORE
12
+ 479: ERR_BADCHANNAME
13
+ 484: ERR_DESYNC
14
+ 503: ERR_GHOSTEDCLIENT
@@ -0,0 +1,3 @@
1
+ ---
2
+ 901: RPL_LOGGEDIN
3
+ 902: RPL_LOGGEDOUT
data/lib/net/irc.rb ADDED
@@ -0,0 +1,825 @@
1
+ require 'net/protocol'
2
+ require 'strscan'
3
+ require 'yaml'
4
+ require 'logger'
5
+
6
+ module Net
7
+ class IRC < Protocol
8
+ include Enumerable
9
+
10
+ class << self
11
+ def logger
12
+ @logger ||= Logger.new('net-irc.log')
13
+ end
14
+
15
+ def logger=(logger)
16
+ @logger = logger
17
+ end
18
+ end
19
+
20
+ USER_MODE_DEFAULT = 0
21
+ USER_MODE_RECEIVE_WALLOPS = 4
22
+ USER_MODE_INVISIBLE = 8
23
+
24
+ PORT_DEFAULT = 6667
25
+
26
+ VERSION = "0.9.3"
27
+
28
+ class CTCP
29
+ attr_accessor :source, :target, :keyword, :parameters
30
+
31
+ CTCP_REGEX = /\001(.*?)\001/
32
+
33
+ def initialize(keyword, *parameters)
34
+ @source = nil
35
+ @keyword = keyword
36
+ @parameters = parameters
37
+ end
38
+
39
+ def to_s
40
+ str = "\001#{keyword}"
41
+ str << parameters.collect { |p| " #{p}"}.join
42
+ str << "\001"
43
+ end
44
+
45
+ class << self
46
+ def parse(text)
47
+ [
48
+ text.gsub(CTCP_REGEX, ''),
49
+ text.scan(CTCP_REGEX).flatten.collect do |message|
50
+ parameters = message.split(' ')
51
+ case keyword = parameters.shift
52
+ when 'VERSION'
53
+ CTCPVersion.new(*parameters)
54
+ when 'PING'
55
+ CTCPPing.new(*parameters)
56
+ when 'CLIENTINFO'
57
+ CTCPClientinfo.new(*parameters)
58
+ when 'ACTION'
59
+ CTCPAction.new(*parameters)
60
+ when 'FINGER'
61
+ CTCPFinger.new(*parameters)
62
+ when 'TIME'
63
+ CTCPTime.new(*parameters)
64
+ when 'DCC'
65
+ CTCPDcc.new(*parameters)
66
+ when 'ERRMSG'
67
+ CTCPErrmsg.new(*parameters)
68
+ when 'PLAY'
69
+ CTCPPlay.new(*parameters)
70
+ else
71
+ CTCP.new(keyword, *parameters)
72
+ end
73
+ end
74
+ ]
75
+ end
76
+ end
77
+ end
78
+
79
+ class CTCPVersion < CTCP
80
+ def initialize(*parameters)
81
+ super('VERSION', parameters)
82
+ end
83
+ end
84
+
85
+ class CTCPPing < CTCP
86
+ attr_accessor :arg
87
+
88
+ def initialize(arg = nil)
89
+ if @arg = arg
90
+ super('PING', arg)
91
+ else
92
+ super('PING')
93
+ end
94
+ end
95
+ end
96
+
97
+ class CTCPClientinfo < CTCP
98
+ def initialize(*keywords)
99
+ super('CLIENTINFO', *keywords)
100
+ end
101
+ end
102
+
103
+ class CTCPAction < CTCP
104
+ attr_accessor :text
105
+
106
+ def initialize(*parameters)
107
+ @text = parameters.join(' ')
108
+
109
+ super('ACTION', *parameters)
110
+ end
111
+ end
112
+
113
+ class CTCPFinger < CTCP
114
+ def initialize(text = nil)
115
+ super('FINGER', text)
116
+ end
117
+ end
118
+
119
+ class CTCPTime < CTCP
120
+ attr_accessor :time
121
+ def initialize(*parameters)
122
+ @time = parameters.join(' ')
123
+
124
+ super('TIME', *parameters)
125
+ end
126
+ end
127
+
128
+ class CTCPDcc < CTCP
129
+ def initialize(type, protocol, ip, port, *args)
130
+ super('DCC', type, protocol, ip, port, *args)
131
+ end
132
+ end
133
+
134
+ class CTCPErrmsg < CTCP
135
+ def initialize(keyword, text = nil)
136
+ super('ERRMSG', keyword, text)
137
+ end
138
+ end
139
+
140
+ class CTCPPlay < CTCP
141
+ def initialize(filename, mime_type)
142
+ super('PLAY', filename, mime_type)
143
+ end
144
+ end
145
+
146
+ class Message
147
+ attr_reader :prefix
148
+ attr_accessor :command, :parameters
149
+
150
+ COMMAND_MAPS = %w(rfc1459 rfc2812 isupport hybrid ircu hyperion)
151
+
152
+ def initialize(*args)
153
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 2)" if args.size < 2
154
+
155
+ # puts ">>>>> args=#{args.inspect}"
156
+
157
+ @prefix, @command, *parameters = args
158
+ # puts ">>>>> @prefix=#{@prefix.inspect}, command=#{@command.inspect}, parameters=#{parameters.inspect}"
159
+ @parameters = Array(parameters)
160
+ end
161
+
162
+ class Prefix
163
+ attr_accessor :prefix
164
+
165
+ PREFIX_REGEX = /^([^!@]+)(?:(?:!([^@]+))?@(.+))?/
166
+
167
+ def initialize(prefix)
168
+ @prefix = prefix
169
+ @matches = prefix.match(PREFIX_REGEX)
170
+ end
171
+
172
+ def server
173
+ @prefix
174
+ end
175
+
176
+ def nickname
177
+ @matches[1]
178
+ end
179
+
180
+ def user
181
+ @matches[2]
182
+ end
183
+
184
+ def host
185
+ @matches[3]
186
+ end
187
+
188
+ def to_s
189
+ @prefix
190
+ end
191
+ end
192
+
193
+ def prefix=(value)
194
+ @prefix = value && Prefix.new(value)
195
+ end
196
+
197
+ def prefix?
198
+ @prefix
199
+ end
200
+
201
+ def to_s
202
+ # puts ">>>>> prefix=#{prefix.inspect}, command=#{command.inspect}, parameters=#{parameters.inspect}"
203
+ str = prefix ? ":#{prefix} " : ""
204
+ str << command
205
+ if ! parameters.empty?
206
+ parameters[0..-2].each do |param|
207
+ str << " #{param}"
208
+ end
209
+ if parameters.last =~ /^:| /
210
+ str << " :#{parameters.last}"
211
+ else
212
+ str << " #{parameters.last}"
213
+ end
214
+ end
215
+ str
216
+ end
217
+
218
+ def write(socket)
219
+ line = to_s
220
+ IRC.logger.debug ">>>>> #{line.inspect}"
221
+ socket.writeline(line)
222
+ end
223
+
224
+ class << self
225
+ def parse(line)
226
+ scanner = StringScanner.new(line)
227
+
228
+ prefix = scanner.scan(/:([^ ]+) /) && scanner[1]
229
+ command = scanner.scan(/[[:alpha:]]+|\d{3}/)
230
+ params = []
231
+ 14.times do
232
+ break if ! scanner.scan(/ ([^ :][^ ]*)/)
233
+ params << scanner[1]
234
+ end
235
+ params << scanner[1] if scanner.scan(/ :(.+)/)
236
+
237
+ message = nil
238
+ command_name = command.to_i > 0 ? command_for_number(command.to_i) : command
239
+
240
+ if command_name
241
+ message_type = "#{command_name.downcase.split('_').collect { |w| w.capitalize }.join}"
242
+ if Net::IRC.const_defined?(message_type)
243
+ message_type = Net::IRC.const_get(message_type)
244
+ message = message_type.new(*params)
245
+ message.prefix = prefix
246
+ end
247
+ end
248
+
249
+ message ||= Message.new(prefix, command_name || command, *params)
250
+ end
251
+
252
+ def command_for_number(number)
253
+ @command_map ||= COMMAND_MAPS.inject({}) { |merged,map| merged.merge!(YAML.load_file("#{File.dirname(__FILE__)}/#{map}.yml")) }
254
+ @command_map[number]
255
+ end
256
+ end
257
+ end
258
+
259
+ class Reply < Message
260
+ attr_accessor :text
261
+
262
+ def initialize(prefix, command, *args)
263
+ args.pop unless @text = args.last
264
+ super(nil, command, *args)
265
+ end
266
+ end
267
+
268
+ class ReplyWithTarget < Reply
269
+ attr_accessor :target
270
+
271
+ def initialize(prefix, command, target, *args)
272
+ @target = target
273
+ super(prefix, command, @target, *args)
274
+ end
275
+ end
276
+
277
+ # 001 <target> :Welcome to the Internet Relay Network <nick>!<user>@<host>
278
+ class RplWelcome < ReplyWithTarget
279
+ def initialize(target, text)
280
+ super(nil, 'RPL_WELCOME', target, text)
281
+ end
282
+ end
283
+
284
+ # 002 <target> :Your host is <servername>, running version <ver>
285
+ class RplYourhost < Reply
286
+ def initialize(target, text)
287
+ super(nil, 'RPL_YOURHOST', target, text)
288
+ end
289
+ end
290
+
291
+ # 003 <target> :This server was created <date>
292
+ class RplCreated < ReplyWithTarget
293
+ def initialize(target, text)
294
+ super(nil, 'RPL_CREATED', target, text)
295
+ end
296
+ end
297
+
298
+ # 004 <target> <servername> <version> <available user modes> <available channel modes>
299
+ class RplMyinfo < ReplyWithTarget
300
+ attr_accessor :servername, :version, :available_user_modes, :available_channel_modes
301
+
302
+ def initialize(target, servername, version, available_user_modes, available_channel_modes)
303
+ @servername = servername
304
+ @version = version
305
+ @available_user_modes = available_user_modes
306
+ @available_channel_modes = available_channel_modes
307
+
308
+ super(nil, 'RPL_MYINFO', target, servername, version, available_user_modes, available_channel_modes, nil)
309
+ end
310
+ end
311
+
312
+ # 005 <target> ( [ "-" ] <parameter> ) | ( <parameter> "=" [ <value> ] ) *( ( [ "-" ] <parameter> ) | ( <parameter> "=" [ <value> ] ) ) :are supported by this server
313
+ class RplIsupport < ReplyWithTarget
314
+ class Parameter
315
+ PARAMETER_REGEX = /^(-)?([[:alnum:]]{1,20})(?:=(.*))?/
316
+
317
+ def initialize(param)
318
+ @param = param
319
+ @matches = param.match(PARAMETER_REGEX)
320
+ end
321
+
322
+ def name
323
+ @matches[2]
324
+ end
325
+
326
+ def value
327
+ @matches[3] || @matches[1].nil?
328
+ end
329
+ end
330
+
331
+ def initialize(target, *args)
332
+ raise ArgumentError, "wrong number of arguments (#{1 + args.size} for 3)" if args.size < 2
333
+
334
+ @parameters = args[0..-2].collect { |p| Parameter.new(p) }
335
+
336
+ super(nil, 'RPL_ISUPPORT', target, *args)
337
+ end
338
+ end
339
+
340
+ # 250 <target> :<text>
341
+ class RplStatsconn < ReplyWithTarget
342
+ def initialize(target, text)
343
+ super(nil, 'RPL_LSTATSCONN', target, text)
344
+ end
345
+ end
346
+
347
+ # 251 <target> :<text>
348
+ class RplLuserclient < ReplyWithTarget
349
+ def initialize(target, text)
350
+ super(nil, 'RPL_LUSERCLIENT', target, text)
351
+ end
352
+ end
353
+
354
+ class ReplyWithCount < ReplyWithTarget
355
+ attr_accessor :count
356
+
357
+ def initialize(prefix, command, target, count, text)
358
+ @count = count
359
+ super(prefix, command, target, count, text)
360
+ end
361
+ end
362
+
363
+ # 252 <target> <count> :<text>
364
+ class RplLuserop < ReplyWithCount
365
+ def initialize(target, count, text)
366
+ super(nil, 'RPL_LUSEROP', target, count, text)
367
+ end
368
+ end
369
+
370
+ # 254 <target> <count> :<text>
371
+ class RplLuserchannels < ReplyWithCount
372
+ def initialize(target, count, text)
373
+ super(nil, 'RPL_LUSERCHANNELS', target, count, text)
374
+ end
375
+ end
376
+
377
+ # 255 <target> :<text>
378
+ class RplLuserme < ReplyWithTarget
379
+ def initialize(target, text)
380
+ super(nil, 'RPL_LUSERME', target, text)
381
+ end
382
+ end
383
+
384
+ # 265 <target> :<text>
385
+ class RplLocalusers < ReplyWithTarget
386
+ def initialize(target, text)
387
+ super(nil, 'RPL_LOCALUSERS', target, text)
388
+ end
389
+ end
390
+
391
+ # 266 <target> :<text>
392
+ class RplGlobalusers < ReplyWithTarget
393
+ def initialize(target, text)
394
+ super(nil, 'RPL_GLOBALUSERS', target, text)
395
+ end
396
+ end
397
+
398
+ class ReplyWithChannel < ReplyWithTarget
399
+ attr_accessor :channel
400
+
401
+ def initialize(prefix, command, target, channel, *args)
402
+ @channel = channel
403
+ super(prefix, command, target, @channel, *args)
404
+ end
405
+ end
406
+
407
+ # 332 <target> <target> <channel> :<text>
408
+ class RplTopic < ReplyWithChannel
409
+ def initialize(target, channel, text)
410
+ super(nil, 'RPL_TOPIC', target, channel, text)
411
+ end
412
+ end
413
+
414
+ # 333 <target> <channel> <nickname> <time>
415
+ class RplTopicwhotime < ReplyWithChannel
416
+ attr_accessor :nickname, :time
417
+
418
+ def initialize(target, channel, nickname, time)
419
+ @nickname = nickname
420
+ @time = Time.at(time.to_i)
421
+ super(nil, 'RPL_TOPICWHOTIME', target, channel, nickname, time, nil)
422
+ end
423
+ end
424
+
425
+ # 353 <target> ( "=" | "*" | "@" ) <channel> :[ "@" | "+" ] <nick> *( " " [ "@" / "+" ] <nick> )
426
+ class RplNamreply < Reply
427
+ attr_accessor :channel_type, :channel, :names
428
+
429
+ def initialize(target, channel_type, channel, names)
430
+ @channel_type = channel_type
431
+ @channel = channel
432
+ @names = names.split(' ')
433
+ super(nil, 'RPL_NAMREPLY', target, @channel_type, @channel, names, nil)
434
+ end
435
+ end
436
+
437
+ # 366 <target> <channel> :End of /NAMES list
438
+ class RplEndofnames < ReplyWithChannel
439
+ def initialize(target, channel, text)
440
+ super(nil, 'RPL_ENDOFNAMES', target, channel, text)
441
+ end
442
+ end
443
+
444
+ # 372 <target> :- <text>
445
+ class RplMotd < Reply
446
+ def initialize(target, text)
447
+ super(nil, 'RPL_MOTD', target, text)
448
+ end
449
+ end
450
+
451
+ # 375 <target> :- <server> Message of the day -
452
+ class RplMotdstart < Reply
453
+ def initialize(target, text)
454
+ super(nil, 'RPL_MOTDSTART', target, text)
455
+ end
456
+ end
457
+
458
+ # 376 <target> :End of MOTD command.
459
+ class RplEndofmotd < Reply
460
+ def initialize(target, text)
461
+ super(nil, 'RPL_ENDOFMOTD', target, text)
462
+ end
463
+ end
464
+
465
+ class Error < ReplyWithTarget
466
+ end
467
+
468
+ # 422 <target> <nickname> :Nickname is already in use.
469
+ class ErrNicknameinuse < Error
470
+ attr_accessor :nickname
471
+
472
+ def initialize(target, nickname, text)
473
+ @nickname = nickname
474
+
475
+ super(nil, 'ERR_NICKNAMEINUSE', target, @nickname, text)
476
+ end
477
+ end
478
+
479
+ # 477 <target> <channel> :<text>
480
+ class ErrNeedreggednick < Error
481
+ attr_accessor :channel
482
+
483
+ def initialize(target, channel, text)
484
+ @channel = channel
485
+
486
+ super(nil, 'ERR_NEEDREGGEDNICK', target, channel, text)
487
+ end
488
+ end
489
+
490
+ # 901 <target> <id> <username> <hostname> :You are now logged in. (id <id>, username <username>, hostname <hostname>)
491
+ class ReplyWithRegistryParameters < ReplyWithTarget
492
+ attr_accessor :id, :username, :hostname
493
+
494
+ def initialize(prefix, command, target, id, username, hostname, text)
495
+ @id = id
496
+ @username = username
497
+ @hostname = hostname
498
+
499
+ super(prefix, command, target, id, username, hostname, text)
500
+ end
501
+ end
502
+
503
+ # 901 <target> <id> <username> <hostname> :You are now logged in. (id <id>, username <username>, hostname <hostname>)
504
+ class RplLoggedin < ReplyWithRegistryParameters
505
+ def initialize(target, id, username, hostname, text)
506
+ super(nil, 'RPL_LOGGED_IN', target, id, username, hostname, text)
507
+ end
508
+ end
509
+
510
+ # 902 <target> <id> <username> <hostname> :You are now logged out. (id <id>, username <username>, hostname <hostname>)
511
+ class RplLoggedout < ReplyWithRegistryParameters
512
+ def initialize(target, id, username, hostname, text)
513
+ super(nil, 'RPL_LOGGED_OUT', target, id, username, hostname, text)
514
+ end
515
+ end
516
+
517
+
518
+ # JOIN ( <channel> *( "," <channel> ) [ <key> *( "," <key> ) ] )
519
+ # / "0"
520
+ class Join < Message
521
+ attr_accessor :channels, :keys
522
+
523
+ def initialize(channels, keys = nil)
524
+ @channels = channels.split(',')
525
+ @keys = keys && keys.split(',')
526
+
527
+ if keys
528
+ super(nil, 'JOIN', channels, keys)
529
+ else
530
+ super(nil, 'JOIN', channels)
531
+ end
532
+ end
533
+ end
534
+
535
+ # MODE <channel> *( ( "-" / "+" ) *<modes> *<modeparams> )
536
+ class Mode < Message
537
+ attr_accessor :channel, :modes
538
+
539
+ def initialize(channel, *parameters)
540
+ @channel = channel
541
+ @modes = parameters.join(' ')
542
+
543
+ super(nil, 'MODE', channel, *parameters)
544
+ end
545
+ end
546
+
547
+ # NICK <nickname>
548
+ class Nick < Message
549
+ attr_accessor :nickname
550
+
551
+ def initialize(nickname)
552
+ @nickname = nickname
553
+
554
+ super(nil, 'NICK', @nickname)
555
+ end
556
+ end
557
+
558
+ # NOTICE <target> <text>
559
+ class Notice < Message
560
+ attr_accessor :target, :text, :ctcp
561
+
562
+ def initialize(target, text)
563
+ @target = target
564
+ @text, @ctcp = CTCP.parse(text)
565
+
566
+ super(nil, 'NOTICE', @target, text)
567
+ end
568
+ end
569
+
570
+ # PART <channel> *( "," <channel> ) [ <text> ]
571
+ class Part < Message
572
+ attr_accessor :channels, :text
573
+
574
+ def initialize(channels, message = nil)
575
+ @channels = channels.split(',')
576
+
577
+ if message
578
+ super(nil, 'PART', channels, message)
579
+ else
580
+ super(nil, 'PART', channels)
581
+ end
582
+ end
583
+ end
584
+
585
+ # PASS <password>
586
+ class Pass < Message
587
+ attr_accessor :password
588
+
589
+ def initialize(password)
590
+ @password = password
591
+
592
+ super(nil, 'PASS', @password)
593
+ end
594
+ end
595
+
596
+ # PING <server> [ <target> ]
597
+ class Ping < Message
598
+ attr_accessor :server, :target
599
+
600
+ def initialize(server, target = nil)
601
+ @server = server
602
+
603
+ if @target = target
604
+ super(nil, 'PING', @server, @target)
605
+ else
606
+ super(nil, 'PING', @server)
607
+ end
608
+ end
609
+ end
610
+
611
+ # PONG <server> [ <target> ]
612
+ class Pong < Message
613
+ attr_accessor :server, :target
614
+
615
+ def initialize(server, target = nil)
616
+ @server = server
617
+
618
+ if @target = target
619
+ super(nil, 'PONG', @server, @target)
620
+ else
621
+ super(nil, 'PONG', @server)
622
+ end
623
+ end
624
+ end
625
+
626
+ # PRIVMSG <target> <text>
627
+ class Privmsg < Message
628
+ attr_accessor :target, :text, :ctcp
629
+
630
+ def initialize(target, text)
631
+ @target = target
632
+ @text, @ctcp = CTCP.parse(text)
633
+
634
+ super(nil, 'PRIVMSG', @target, text)
635
+ end
636
+ end
637
+
638
+ # QUIT [ <text> ]
639
+ class Quit < Message
640
+ attr_accessor :text
641
+
642
+ def initialize(text = nil)
643
+ if @text = text
644
+ super(nil, 'QUIT', @text)
645
+ else
646
+ super(nil, 'QUIT')
647
+ end
648
+ end
649
+ end
650
+
651
+ # USER <user> <mode> <unused> <realname>
652
+ class User < Message
653
+ attr_accessor :user, :realname, :mode
654
+
655
+ def initialize(*args)
656
+ # puts ">>>>> User#initialize(#{args.inspect})"
657
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 2)" if args.size < 2
658
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 4)" if args.size > 4
659
+
660
+ @user = args.shift
661
+
662
+ # treat mode and "unused" as optional for convenience
663
+ @mode = args.size > 1 && args.shift || USER_MODE_DEFAULT
664
+
665
+ args.shift if args.size > 1
666
+
667
+ @realname = args.shift
668
+
669
+ # puts ">>>>> @user=#{@user.inspect}, @mode=#{@mode.inspect}, unused=#{unused.inspect}, @realname=#{@realname.inspect}"
670
+ super(nil, 'USER', @user, @mode, '*', @realname)
671
+ # puts ">>>>> prefix=#{prefix.inspect}, command=#{command.inspect}, parameters=#{parameters.inspect}"
672
+ end
673
+ end
674
+
675
+ class << self
676
+ def start(user, password, realname, address, port = nil, &block)
677
+ new(address, port).start(user, password, realname, &block)
678
+ end
679
+ end
680
+
681
+ def initialize(address, port = nil)
682
+ @address = address
683
+ @port = port || PORT_DEFAULT
684
+ @started = false
685
+ @socket = nil
686
+ end
687
+
688
+ def started?
689
+ @started
690
+ end
691
+
692
+ def start(user, password, realname, nickname = nil)
693
+ raise IOError, 'IRC session already started' if started?
694
+
695
+ if block_given?
696
+ begin
697
+ do_start(user, password, realname, nickname)
698
+ return yield(self)
699
+ ensure
700
+ do_finish
701
+ end
702
+ else
703
+ do_start(user, password, realname, nickname)
704
+ return self
705
+ end
706
+ end
707
+
708
+ def finish
709
+ raise IOError, 'IRC session not yet started' if ! started?
710
+ end
711
+
712
+ def each
713
+ while line = @socket.readline
714
+ IRC.logger.debug "<<<<< #{line.inspect}"
715
+
716
+ message = Message.parse(line.chomp)
717
+
718
+ if message.respond_to? :ctcp
719
+ message.ctcp.each do |ctcp|
720
+ ctcp.source = message.prefix.nickname
721
+ ctcp.target = message.target
722
+
723
+ yield ctcp
724
+ end
725
+ next if message.text.empty?
726
+ end
727
+
728
+ case message
729
+ when Net::IRC::Ping
730
+ pong message.server
731
+ else
732
+ yield message
733
+ end
734
+ end
735
+ rescue IOError
736
+ raise if started?
737
+ end
738
+
739
+ def ctcp(target, text)
740
+ privmsg(target, "\001#{text}\001")
741
+ end
742
+
743
+ def ctcp_version(target, *parameters)
744
+ notice(target, CTCPVersion.new(*parameters).to_s)
745
+ end
746
+
747
+ def ctcp_ping(target, arg = nil)
748
+ notice(target, CTCPPing.new(arg).to_s)
749
+ end
750
+
751
+ def ctcp_time(target, time = nil)
752
+ time ||= Time.now
753
+ differential = '%.2d%.2d' % (time.utc_offset / 60).divmod(60)
754
+ notice(target, CTCPTime.new(time.strftime("%a, %d %b %Y %H:%M #{differential}")).to_s)
755
+ end
756
+
757
+ def join(channels = nil)
758
+ case channels
759
+ when NilClass
760
+ Join.new('0')
761
+ when Hash
762
+ Join.new(channels.keys.join(','), channels.values.join(','))
763
+ when Array
764
+ Join.new(channels.join(','))
765
+ else
766
+ Join.new(channels.to_s)
767
+ end.write(@socket)
768
+ end
769
+
770
+ def nick(nickname)
771
+ Nick.new(nickname).write(@socket)
772
+ end
773
+
774
+ def notice(target, text)
775
+ Notice.new(target, text).write(@socket)
776
+ end
777
+
778
+ def part(channels, message = nil)
779
+ if message
780
+ Part.new(Array(channels).join(','), message)
781
+ else
782
+ Part.new(Array(channels).join(','))
783
+ end.write(@socket)
784
+ end
785
+
786
+ def pass(password)
787
+ Pass.new(password).write(@socket)
788
+ end
789
+
790
+ def pong(server, target = nil)
791
+ Pong.new(server, target).write(@socket)
792
+ end
793
+
794
+ def privmsg(target, text)
795
+ Privmsg.new(target, text).write(@socket)
796
+ end
797
+
798
+ def quit(text = nil)
799
+ Quit.new(text).write(@socket)
800
+ end
801
+
802
+ def user(user, realname, mode = nil)
803
+ User.new(user, mode || USER_MODE_DEFAULT, realname).write(@socket)
804
+ end
805
+
806
+ private
807
+ def do_start(user, password, realname, nickname = nil)
808
+ @socket = InternetMessageIO.old_open(@address, @port)
809
+ pass(password) unless password.nil? || password.empty?
810
+ nick(user)
811
+ user(user, realname)
812
+ @started = true
813
+ ensure
814
+ do_finish if ! started?
815
+ end
816
+
817
+ def do_finish
818
+ quit if started?
819
+ ensure
820
+ @started = false
821
+ @socket.close if @socket && ! @socket.closed?
822
+ @socket = nil
823
+ end
824
+ end
825
+ end