sbfaulkner-net-irc 0.9.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 +20 -0
- data/README.markdown +22 -0
- data/Rakefile +10 -0
- data/lib/net/irc.rb +793 -0
- data/lib/net/rfc2812.yml +29 -0
- data/net-irc.gemspec +20 -0
- metadata +59 -0
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,22 @@
|
|
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
|
+
See the examples folder for how to use.
|
18
|
+
|
19
|
+
## Legal
|
20
|
+
|
21
|
+
**Author:** S. Brent Faulkner <brentf@unwwwired.net>
|
22
|
+
**License:** Copyright © 2008 unwwwired.net, released under the MIT license
|
data/Rakefile
ADDED
data/lib/net/irc.rb
ADDED
@@ -0,0 +1,793 @@
|
|
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.1"
|
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)
|
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
|
+
# JOIN ( <channel> *( "," <channel> ) [ <key> *( "," <key> ) ] )
|
491
|
+
# / "0"
|
492
|
+
class Join < Message
|
493
|
+
attr_accessor :channels, :keys
|
494
|
+
|
495
|
+
def initialize(channels, keys = nil)
|
496
|
+
@channels = channels.split(',')
|
497
|
+
@keys = keys && keys.split(',')
|
498
|
+
|
499
|
+
if keys
|
500
|
+
super(nil, 'JOIN', channels, keys)
|
501
|
+
else
|
502
|
+
super(nil, 'JOIN', channels)
|
503
|
+
end
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
# MODE <channel> *( ( "-" / "+" ) *<modes> *<modeparams> )
|
508
|
+
class Mode < Message
|
509
|
+
attr_accessor :channel, :modes
|
510
|
+
|
511
|
+
def initialize(channel, *parameters)
|
512
|
+
@channel = channel
|
513
|
+
@modes = parameters.join(' ')
|
514
|
+
|
515
|
+
super(nil, 'MODE', channel, *parameters)
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
# NICK <nickname>
|
520
|
+
class Nick < Message
|
521
|
+
attr_accessor :nickname
|
522
|
+
|
523
|
+
def initialize(nickname)
|
524
|
+
@nickname = nickname
|
525
|
+
|
526
|
+
super(nil, 'NICK', @nickname)
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
# NOTICE <target> <text>
|
531
|
+
class Notice < Message
|
532
|
+
attr_accessor :target, :text, :ctcp
|
533
|
+
|
534
|
+
def initialize(target, text)
|
535
|
+
@target = target
|
536
|
+
@text, @ctcp = CTCP.parse(text)
|
537
|
+
|
538
|
+
super(nil, 'NOTICE', @target, text)
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
# PART <channel> *( "," <channel> ) [ <text> ]
|
543
|
+
class Part < Message
|
544
|
+
attr_accessor :channels, :text
|
545
|
+
|
546
|
+
def initialize(channels, message = nil)
|
547
|
+
@channels = channels.split(',')
|
548
|
+
|
549
|
+
if message
|
550
|
+
super(nil, 'PART', channels, message)
|
551
|
+
else
|
552
|
+
super(nil, 'PART', channels)
|
553
|
+
end
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
# PASS <password>
|
558
|
+
class Pass < Message
|
559
|
+
attr_accessor :password
|
560
|
+
|
561
|
+
def initialize(password)
|
562
|
+
@password = password
|
563
|
+
|
564
|
+
super(nil, 'PASS', @password)
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
# PING <server> [ <target> ]
|
569
|
+
class Ping < Message
|
570
|
+
attr_accessor :server, :target
|
571
|
+
|
572
|
+
def initialize(server, target = nil)
|
573
|
+
@server = server
|
574
|
+
|
575
|
+
if @target = target
|
576
|
+
super(nil, 'PING', @server, @target)
|
577
|
+
else
|
578
|
+
super(nil, 'PING', @server)
|
579
|
+
end
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
# PONG <server> [ <target> ]
|
584
|
+
class Pong < Message
|
585
|
+
attr_accessor :server, :target
|
586
|
+
|
587
|
+
def initialize(server, target = nil)
|
588
|
+
@server = server
|
589
|
+
|
590
|
+
if @target = target
|
591
|
+
super(nil, 'PONG', @server, @target)
|
592
|
+
else
|
593
|
+
super(nil, 'PONG', @server)
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
# PRIVMSG <target> <text>
|
599
|
+
class Privmsg < Message
|
600
|
+
attr_accessor :target, :text, :ctcp
|
601
|
+
|
602
|
+
def initialize(target, text)
|
603
|
+
@target = target
|
604
|
+
@text, @ctcp = CTCP.parse(text)
|
605
|
+
|
606
|
+
super(nil, 'PRIVMSG', @target, text)
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
# QUIT [ <text> ]
|
611
|
+
class Quit < Message
|
612
|
+
attr_accessor :text
|
613
|
+
|
614
|
+
def initialize(text = nil)
|
615
|
+
if @text = text
|
616
|
+
super(nil, 'QUIT', @text)
|
617
|
+
else
|
618
|
+
super(nil, 'QUIT')
|
619
|
+
end
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
# USER <user> <mode> <unused> <realname>
|
624
|
+
class User < Message
|
625
|
+
attr_accessor :user, :realname, :mode
|
626
|
+
|
627
|
+
def initialize(*args)
|
628
|
+
# puts ">>>>> User#initialize(#{args.inspect})"
|
629
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 2)" if args.size < 2
|
630
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 4)" if args.size > 4
|
631
|
+
|
632
|
+
@user = args.shift
|
633
|
+
|
634
|
+
# treat mode and "unused" as optional for convenience
|
635
|
+
@mode = args.size > 1 && args.shift || USER_MODE_DEFAULT
|
636
|
+
|
637
|
+
args.shift if args.size > 1
|
638
|
+
|
639
|
+
@realname = args.shift
|
640
|
+
|
641
|
+
# puts ">>>>> @user=#{@user.inspect}, @mode=#{@mode.inspect}, unused=#{unused.inspect}, @realname=#{@realname.inspect}"
|
642
|
+
super(nil, 'USER', @user, @mode, '*', @realname)
|
643
|
+
# puts ">>>>> prefix=#{prefix.inspect}, command=#{command.inspect}, parameters=#{parameters.inspect}"
|
644
|
+
end
|
645
|
+
end
|
646
|
+
|
647
|
+
class << self
|
648
|
+
def start(user, realname, address, port = nil, &block)
|
649
|
+
new(address, port).start(user, realname, &block)
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
def initialize(address, port = nil)
|
654
|
+
@address = address
|
655
|
+
@port = port || PORT_DEFAULT
|
656
|
+
@started = false
|
657
|
+
@socket = nil
|
658
|
+
end
|
659
|
+
|
660
|
+
def started?
|
661
|
+
@started
|
662
|
+
end
|
663
|
+
|
664
|
+
def start(user, realname, nickname = nil)
|
665
|
+
raise IOError, 'IRC session already started' if started?
|
666
|
+
|
667
|
+
if block_given?
|
668
|
+
begin
|
669
|
+
do_start(user, realname, nickname)
|
670
|
+
return yield(self)
|
671
|
+
ensure
|
672
|
+
do_finish
|
673
|
+
end
|
674
|
+
else
|
675
|
+
do_start(user, realname, nickname)
|
676
|
+
return self
|
677
|
+
end
|
678
|
+
end
|
679
|
+
|
680
|
+
def finish
|
681
|
+
raise IOError, 'IRC session not yet started' if ! started?
|
682
|
+
end
|
683
|
+
|
684
|
+
def each
|
685
|
+
while line = @socket.readline
|
686
|
+
IRC.logger.debug "<<<<< #{line.inspect}"
|
687
|
+
|
688
|
+
message = Message.parse(line.chomp)
|
689
|
+
|
690
|
+
if message.respond_to? :ctcp
|
691
|
+
message.ctcp.each do |ctcp|
|
692
|
+
ctcp.source = message.prefix.nickname
|
693
|
+
ctcp.target = message.target
|
694
|
+
|
695
|
+
yield ctcp
|
696
|
+
end
|
697
|
+
next if message.text.empty?
|
698
|
+
end
|
699
|
+
|
700
|
+
case message
|
701
|
+
when Net::IRC::Ping
|
702
|
+
pong message.server
|
703
|
+
else
|
704
|
+
yield message
|
705
|
+
end
|
706
|
+
end
|
707
|
+
rescue IOError
|
708
|
+
raise if started?
|
709
|
+
end
|
710
|
+
|
711
|
+
def ctcp(target, text)
|
712
|
+
privmsg(target, "\001#{text}\001")
|
713
|
+
end
|
714
|
+
|
715
|
+
def ctcp_version(target, *parameters)
|
716
|
+
notice(target, CTCPVersion.new(*parameters).to_s)
|
717
|
+
end
|
718
|
+
|
719
|
+
def ctcp_ping(target, arg = nil)
|
720
|
+
notice(target, CTCPPing.new(arg).to_s)
|
721
|
+
end
|
722
|
+
|
723
|
+
def ctcp_time(target, time = nil)
|
724
|
+
time ||= Time.now
|
725
|
+
differential = '%.2d%.2d' % (time.utc_offset / 60).divmod(60)
|
726
|
+
notice(target, CTCPTime.new(time.strftime("%a, %d %b %Y %H:%M #{differential}")).to_s)
|
727
|
+
end
|
728
|
+
|
729
|
+
def join(channels = nil)
|
730
|
+
case channels
|
731
|
+
when NilClass
|
732
|
+
Join.new('0')
|
733
|
+
when Hash
|
734
|
+
Join.new(channels.keys.join(','), channels.values.join(','))
|
735
|
+
when Array
|
736
|
+
Join.new(channels.join(','))
|
737
|
+
else
|
738
|
+
Join.new(channels.to_s)
|
739
|
+
end.write(@socket)
|
740
|
+
end
|
741
|
+
|
742
|
+
def nick(nickname)
|
743
|
+
Nick.new(nickname).write(@socket)
|
744
|
+
end
|
745
|
+
|
746
|
+
def notice(target, text)
|
747
|
+
Notice.new(target, text).write(@socket)
|
748
|
+
end
|
749
|
+
|
750
|
+
def part(channels, message = nil)
|
751
|
+
if message
|
752
|
+
Part.new(Array(channels).join(','), message)
|
753
|
+
else
|
754
|
+
Part.new(Array(channels).join(','))
|
755
|
+
end.write(@socket)
|
756
|
+
end
|
757
|
+
|
758
|
+
def pong(server, target = nil)
|
759
|
+
Pong.new(server, target).write(@socket)
|
760
|
+
end
|
761
|
+
|
762
|
+
def privmsg(target, text)
|
763
|
+
Privmsg.new(target, text).write(@socket)
|
764
|
+
end
|
765
|
+
|
766
|
+
def quit(text = nil)
|
767
|
+
Quit.new(text).write(@socket)
|
768
|
+
end
|
769
|
+
|
770
|
+
def user(user, realname, mode = nil)
|
771
|
+
User.new(user, mode || USER_MODE_DEFAULT, realname).write(@socket)
|
772
|
+
end
|
773
|
+
|
774
|
+
private
|
775
|
+
def do_start(user, realname, nickname = nil)
|
776
|
+
@socket = InternetMessageIO.old_open(@address, @port)
|
777
|
+
# TODO: Pass.new(password).write(@socket)
|
778
|
+
nick(user)
|
779
|
+
user(user, realname)
|
780
|
+
@started = true
|
781
|
+
ensure
|
782
|
+
do_finish if ! started?
|
783
|
+
end
|
784
|
+
|
785
|
+
def do_finish
|
786
|
+
quit if started?
|
787
|
+
ensure
|
788
|
+
@started = false
|
789
|
+
@socket.close if @socket && ! @socket.closed?
|
790
|
+
@socket = nil
|
791
|
+
end
|
792
|
+
end
|
793
|
+
end
|
data/lib/net/rfc2812.yml
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
---
|
2
|
+
1: RPL_WELCOME
|
3
|
+
2: RPL_YOURHOST
|
4
|
+
3: RPL_CREATED
|
5
|
+
4: RPL_MYINFO
|
6
|
+
5: RPL_BOUNCE
|
7
|
+
207: RPL_TRACESERVICE
|
8
|
+
210: RPL_TRACERECONNECT
|
9
|
+
240: RPL_STATSVLINE
|
10
|
+
245: RPL_STATSSLINE
|
11
|
+
246: RPL_STATSPING
|
12
|
+
247: RPL_STATSBLINE
|
13
|
+
250: RPL_STATSDLINE
|
14
|
+
262: RPL_TRACEEND
|
15
|
+
263: RPL_TRYAGAIN
|
16
|
+
325: RPL_UNIQOPIS
|
17
|
+
346: RPL_INVITELIST
|
18
|
+
347: RPL_ENDOFINVITELIST
|
19
|
+
348: RPL_EXCEPTLIST
|
20
|
+
349: RPL_ENDOFEXCEPTLIST
|
21
|
+
383: RPL_YOURESERVICE
|
22
|
+
408: ERR_NOSUCHSERVICE
|
23
|
+
415: ERR_BADMASK
|
24
|
+
437: ERR_UNAVAILRESOURCE
|
25
|
+
476: ERR_BADCHANMASK
|
26
|
+
477: ERR_NOCHANMODES
|
27
|
+
478: ERR_BANLISTFULL
|
28
|
+
484: ERR_RESTRICTED
|
29
|
+
485: ERR_UNIQOPPRIVSNEEDED
|
data/net-irc.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
SPEC = Gem::Specification.new do |s|
|
2
|
+
# identify the gem
|
3
|
+
s.name = "net-irc"
|
4
|
+
s.version = "0.9.1"
|
5
|
+
s.author = "S. Brent Faulkner"
|
6
|
+
s.email = "brentf@unwwwired.net"
|
7
|
+
s.homepage = "http://www.unwwwired.net"
|
8
|
+
# platform of choice
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
# description of gem
|
11
|
+
s.summary = "a ruby implementation of the IRC client protocol"
|
12
|
+
s.files = %w(examples/test.rb lib/net/irc.rb lib/net/rfc2812.yml MIT-LICENSE Rakefile README.markdown net-irc.gemspec)
|
13
|
+
s.require_path = "lib"
|
14
|
+
s.autorequire = "irc"
|
15
|
+
# s.test_file = "test/net-irc.rb"
|
16
|
+
s.has_rdoc = true
|
17
|
+
s.extra_rdoc_files = ["README.markdown"]
|
18
|
+
# s.add_dependency("fastercsv", ">= 1.2.3")
|
19
|
+
# s.executables = ["rsql"]
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sbfaulkner-net-irc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- S. Brent Faulkner
|
8
|
+
autorequire: irc
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-08-13 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: brentf@unwwwired.net
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.markdown
|
24
|
+
files:
|
25
|
+
- examples/test.rb
|
26
|
+
- lib/net/irc.rb
|
27
|
+
- lib/net/rfc2812.yml
|
28
|
+
- MIT-LICENSE
|
29
|
+
- Rakefile
|
30
|
+
- README.markdown
|
31
|
+
- net-irc.gemspec
|
32
|
+
has_rdoc: true
|
33
|
+
homepage: http://www.unwwwired.net
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options: []
|
36
|
+
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
version:
|
51
|
+
requirements: []
|
52
|
+
|
53
|
+
rubyforge_project:
|
54
|
+
rubygems_version: 1.2.0
|
55
|
+
signing_key:
|
56
|
+
specification_version: 2
|
57
|
+
summary: a ruby implementation of the IRC client protocol
|
58
|
+
test_files: []
|
59
|
+
|