net-irc 0.0.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/ChangeLog +0 -0
- data/README +92 -0
- data/Rakefile +131 -0
- data/examples/lig.rb +276 -0
- data/examples/tig.rb +446 -0
- data/examples/wig.rb +132 -0
- data/lib/net/irc.rb +844 -0
- metadata +72 -0
data/lib/net/irc.rb
ADDED
@@ -0,0 +1,844 @@
|
|
1
|
+
#!ruby
|
2
|
+
|
3
|
+
require "ostruct"
|
4
|
+
require "socket"
|
5
|
+
require "thread"
|
6
|
+
require "logger"
|
7
|
+
|
8
|
+
module Net; end
|
9
|
+
|
10
|
+
module Net::IRC
|
11
|
+
VERSION = "0.0.1"
|
12
|
+
class IRCException < StandardError; end
|
13
|
+
|
14
|
+
module PATTERN # :nodoc:
|
15
|
+
# letter = %x41-5A / %x61-7A ; A-Z / a-z
|
16
|
+
# digit = %x30-39 ; 0-9
|
17
|
+
# hexdigit = digit / "A" / "B" / "C" / "D" / "E" / "F"
|
18
|
+
# special = %x5B-60 / %x7B-7D
|
19
|
+
# ; "[", "]", "\", "`", "_", "^", "{", "|", "}"
|
20
|
+
LETTER = 'A-Za-z'
|
21
|
+
DIGIT = '\d'
|
22
|
+
HEXDIGIT = "#{DIGIT}A-Fa-f"
|
23
|
+
SPECIAL = '\x5B-\x60\x7B-\x7D'
|
24
|
+
|
25
|
+
# shortname = ( letter / digit ) *( letter / digit / "-" )
|
26
|
+
# *( letter / digit )
|
27
|
+
# ; as specified in RFC 1123 [HNAME]
|
28
|
+
# hostname = shortname *( "." shortname )
|
29
|
+
SHORTNAME = "[#{LETTER}#{DIGIT}](?:[-#{LETTER}#{DIGIT}]*[#{LETTER}#{DIGIT}])?"
|
30
|
+
HOSTNAME = "#{SHORTNAME}(?:\\.#{SHORTNAME})*"
|
31
|
+
|
32
|
+
# servername = hostname
|
33
|
+
SERVERNAME = HOSTNAME
|
34
|
+
|
35
|
+
# nickname = ( letter / special ) *8( letter / digit / special / "-" )
|
36
|
+
#NICKNAME = "[#{LETTER}#{SPECIAL}\\w][-#{LETTER}#{DIGIT}#{SPECIAL}]*"
|
37
|
+
NICKNAME = "\\S+" # for multibytes
|
38
|
+
|
39
|
+
# user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
|
40
|
+
# ; any octet except NUL, CR, LF, " " and "@"
|
41
|
+
USER = '[\x01-\x09\x0B-\x0C\x0E-\x1F\x21-\x3F\x41-\xFF]+'
|
42
|
+
|
43
|
+
# ip4addr = 1*3digit "." 1*3digit "." 1*3digit "." 1*3digit
|
44
|
+
IP4ADDR = "[#{DIGIT}]{1,3}(?:\\.[#{DIGIT}]{1,3}){3}"
|
45
|
+
# ip6addr = 1*hexdigit 7( ":" 1*hexdigit )
|
46
|
+
# ip6addr =/ "0:0:0:0:0:" ( "0" / "FFFF" ) ":" ip4addr
|
47
|
+
IP6ADDR = "(?:[#{HEXDIGIT}]+(?::[#{HEXDIGIT}]+){7}|0:0:0:0:0:(?:0|FFFF):#{IP4ADDR})"
|
48
|
+
# hostaddr = ip4addr / ip6addr
|
49
|
+
HOSTADDR = "(?:#{IP4ADDR}|#{IP6ADDR})"
|
50
|
+
|
51
|
+
# host = hostname / hostaddr
|
52
|
+
HOST = "(?:#{HOSTNAME}|#{HOSTADDR})"
|
53
|
+
|
54
|
+
# prefix = servername / ( nickname [ [ "!" user ] "@" host ] )
|
55
|
+
PREFIX = "(?:#{NICKNAME}(?:(?:!#{USER})?@#{HOST})?|#{SERVERNAME})"
|
56
|
+
|
57
|
+
# nospcrlfcl = %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
|
58
|
+
# ; any octet except NUL, CR, LF, " " and ":"
|
59
|
+
NOSPCRLFCL = '\x01-\x09\x0B-\x0C\x0E-\x1F\x21-\x39\x3B-\xFF'
|
60
|
+
|
61
|
+
# command = 1*letter / 3digit
|
62
|
+
COMMAND = "(?:[#{LETTER}]+|[#{DIGIT}]{3})"
|
63
|
+
|
64
|
+
# SPACE = %x20 ; space character
|
65
|
+
# middle = nospcrlfcl *( ":" / nospcrlfcl )
|
66
|
+
# trailing = *( ":" / " " / nospcrlfcl )
|
67
|
+
# params = *14( SPACE middle ) [ SPACE ":" trailing ]
|
68
|
+
# =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]
|
69
|
+
MIDDLE = "[#{NOSPCRLFCL}][:#{NOSPCRLFCL}]*"
|
70
|
+
TRAILING = "[: #{NOSPCRLFCL}]*"
|
71
|
+
PARAMS = "(?:((?: #{MIDDLE}){0,14})(?: :(#{TRAILING}))?|((?: #{MIDDLE}){14})(?::?)?(#{TRAILING}))"
|
72
|
+
|
73
|
+
# crlf = %x0D %x0A ; "carriage return" "linefeed"
|
74
|
+
# message = [ ":" prefix SPACE ] command [ params ] crlf
|
75
|
+
CRLF = '\x0D\x0A'
|
76
|
+
MESSAGE = "(?::(#{PREFIX}) )?(#{COMMAND})#{PARAMS}\s*#{CRLF}"
|
77
|
+
|
78
|
+
CLIENT_PATTERN = /\A#{NICKNAME}(?:(?:!#{USER})?@#{HOST})\z/on
|
79
|
+
MESSAGE_PATTERN = /\A#{MESSAGE}\z/on
|
80
|
+
end # PATTERN
|
81
|
+
|
82
|
+
module Constants # :nodoc:
|
83
|
+
RPL_WELCOME = '001'
|
84
|
+
RPL_YOURHOST = '002'
|
85
|
+
RPL_CREATED = '003'
|
86
|
+
RPL_MYINFO = '004'
|
87
|
+
RPL_BOUNCE = '005'
|
88
|
+
RPL_USERHOST = '302'
|
89
|
+
RPL_ISON = '303'
|
90
|
+
RPL_AWAY = '301'
|
91
|
+
RPL_UNAWAY = '305'
|
92
|
+
RPL_NOWAWAY = '306'
|
93
|
+
RPL_WHOISUSER = '311'
|
94
|
+
RPL_WHOISSERVER = '312'
|
95
|
+
RPL_WHOISOPERATOR = '313'
|
96
|
+
RPL_WHOISIDLE = '317'
|
97
|
+
RPL_ENDOFWHOIS = '318'
|
98
|
+
RPL_WHOISCHANNELS = '319'
|
99
|
+
RPL_WHOWASUSER = '314'
|
100
|
+
RPL_ENDOFWHOWAS = '369'
|
101
|
+
RPL_LISTSTART = '321'
|
102
|
+
RPL_LIST = '322'
|
103
|
+
RPL_LISTEND = '323'
|
104
|
+
RPL_UNIQOPIS = '325'
|
105
|
+
RPL_CHANNELMODEIS = '324'
|
106
|
+
RPL_NOTOPIC = '331'
|
107
|
+
RPL_TOPIC = '332'
|
108
|
+
RPL_INVITING = '341'
|
109
|
+
RPL_SUMMONING = '342'
|
110
|
+
RPL_INVITELIST = '346'
|
111
|
+
RPL_ENDOFINVITELIST = '347'
|
112
|
+
RPL_EXCEPTLIST = '348'
|
113
|
+
RPL_ENDOFEXCEPTLIST = '349'
|
114
|
+
RPL_VERSION = '351'
|
115
|
+
RPL_WHOREPLY = '352'
|
116
|
+
RPL_ENDOFWHO = '315'
|
117
|
+
RPL_NAMREPLY = '353'
|
118
|
+
RPL_ENDOFNAMES = '366'
|
119
|
+
RPL_LINKS = '364'
|
120
|
+
RPL_ENDOFLINKS = '365'
|
121
|
+
RPL_BANLIST = '367'
|
122
|
+
RPL_ENDOFBANLIST = '368'
|
123
|
+
RPL_INFO = '371'
|
124
|
+
RPL_ENDOFINFO = '374'
|
125
|
+
RPL_MOTDSTART = '375'
|
126
|
+
RPL_MOTD = '372'
|
127
|
+
RPL_ENDOFMOTD = '376'
|
128
|
+
RPL_YOUREOPER = '381'
|
129
|
+
RPL_REHASHING = '382'
|
130
|
+
RPL_YOURESERVICE = '383'
|
131
|
+
RPL_TIM = '391'
|
132
|
+
RPL_ = '392'
|
133
|
+
RPL_USERS = '393'
|
134
|
+
RPL_ENDOFUSERS = '394'
|
135
|
+
RPL_NOUSERS = '395'
|
136
|
+
RPL_TRACELINK = '200'
|
137
|
+
RPL_TRACECONNECTING = '201'
|
138
|
+
RPL_TRACEHANDSHAKE = '202'
|
139
|
+
RPL_TRACEUNKNOWN = '203'
|
140
|
+
RPL_TRACEOPERATOR = '204'
|
141
|
+
RPL_TRACEUSER = '205'
|
142
|
+
RPL_TRACESERVER = '206'
|
143
|
+
RPL_TRACESERVICE = '207'
|
144
|
+
RPL_TRACENEWTYPE = '208'
|
145
|
+
RPL_TRACECLASS = '209'
|
146
|
+
RPL_TRACERECONNECT = '210'
|
147
|
+
RPL_TRACELOG = '261'
|
148
|
+
RPL_TRACEEND = '262'
|
149
|
+
RPL_STATSLINKINFO = '211'
|
150
|
+
RPL_STATSCOMMANDS = '212'
|
151
|
+
RPL_ENDOFSTATS = '219'
|
152
|
+
RPL_STATSUPTIME = '242'
|
153
|
+
RPL_STATSOLINE = '243'
|
154
|
+
RPL_UMODEIS = '221'
|
155
|
+
RPL_SERVLIST = '234'
|
156
|
+
RPL_SERVLISTEND = '235'
|
157
|
+
RPL_LUSERCLIENT = '251'
|
158
|
+
RPL_LUSEROP = '252'
|
159
|
+
RPL_LUSERUNKNOWN = '253'
|
160
|
+
RPL_LUSERCHANNELS = '254'
|
161
|
+
RPL_LUSERME = '255'
|
162
|
+
RPL_ADMINME = '256'
|
163
|
+
RPL_ADMINLOC1 = '257'
|
164
|
+
RPL_ADMINLOC2 = '258'
|
165
|
+
RPL_ADMINEMAIL = '259'
|
166
|
+
RPL_TRYAGAIN = '263'
|
167
|
+
ERR_NOSUCHNICK = '401'
|
168
|
+
ERR_NOSUCHSERVER = '402'
|
169
|
+
ERR_NOSUCHCHANNEL = '403'
|
170
|
+
ERR_CANNOTSENDTOCHAN = '404'
|
171
|
+
ERR_TOOMANYCHANNELS = '405'
|
172
|
+
ERR_WASNOSUCHNICK = '406'
|
173
|
+
ERR_TOOMANYTARGETS = '407'
|
174
|
+
ERR_NOSUCHSERVICE = '408'
|
175
|
+
ERR_NOORIGIN = '409'
|
176
|
+
ERR_NORECIPIENT = '411'
|
177
|
+
ERR_NOTEXTTOSEND = '412'
|
178
|
+
ERR_NOTOPLEVEL = '413'
|
179
|
+
ERR_WILDTOPLEVEL = '414'
|
180
|
+
ERR_BADMASK = '415'
|
181
|
+
ERR_UNKNOWNCOMMAND = '421'
|
182
|
+
ERR_NOMOTD = '422'
|
183
|
+
ERR_NOADMININFO = '423'
|
184
|
+
ERR_FILEERROR = '424'
|
185
|
+
ERR_NONICKNAMEGIVEN = '431'
|
186
|
+
ERR_ERRONEUSNICKNAME = '432'
|
187
|
+
ERR_NICKNAMEINUSE = '433'
|
188
|
+
ERR_NICKCOLLISION = '436'
|
189
|
+
ERR_UNAVAILRESOURCE = '437'
|
190
|
+
ERR_USERNOTINCHANNEL = '441'
|
191
|
+
ERR_NOTONCHANNEL = '442'
|
192
|
+
ERR_USERONCHANNEL = '443'
|
193
|
+
ERR_NOLOGIN = '444'
|
194
|
+
ERR_SUMMONDISABLED = '445'
|
195
|
+
ERR_USERSDISABLED = '446'
|
196
|
+
ERR_NOTREGISTERED = '451'
|
197
|
+
ERR_NEEDMOREPARAMS = '461'
|
198
|
+
ERR_ALREADYREGISTRED = '462'
|
199
|
+
ERR_NOPERMFORHOST = '463'
|
200
|
+
ERR_PASSWDMISMATCH = '464'
|
201
|
+
ERR_YOUREBANNEDCREEP = '465'
|
202
|
+
ERR_YOUWILLBEBANNED = '466'
|
203
|
+
ERR_KEYSE = '467'
|
204
|
+
ERR_CHANNELISFULL = '471'
|
205
|
+
ERR_UNKNOWNMODE = '472'
|
206
|
+
ERR_INVITEONLYCHAN = '473'
|
207
|
+
ERR_BANNEDFROMCHAN = '474'
|
208
|
+
ERR_BADCHANNELKEY = '475'
|
209
|
+
ERR_BADCHANMASK = '476'
|
210
|
+
ERR_NOCHANMODES = '477'
|
211
|
+
ERR_BANLISTFULL = '478'
|
212
|
+
ERR_NOPRIVILEGES = '481'
|
213
|
+
ERR_CHANOPRIVSNEEDED = '482'
|
214
|
+
ERR_CANTKILLSERVER = '483'
|
215
|
+
ERR_RESTRICTED = '484'
|
216
|
+
ERR_UNIQOPPRIVSNEEDED = '485'
|
217
|
+
ERR_NOOPERHOST = '491'
|
218
|
+
ERR_UMODEUNKNOWNFLAG = '501'
|
219
|
+
ERR_USERSDONTMATCH = '502'
|
220
|
+
RPL_SERVICEINFO = '231'
|
221
|
+
RPL_ENDOFSERVICES = '232'
|
222
|
+
RPL_SERVICE = '233'
|
223
|
+
RPL_NONE = '300'
|
224
|
+
RPL_WHOISCHANOP = '316'
|
225
|
+
RPL_KILLDONE = '361'
|
226
|
+
RPL_CLOSING = '362'
|
227
|
+
RPL_CLOSEEND = '363'
|
228
|
+
RPL_INFOSTART = '373'
|
229
|
+
RPL_MYPORTIS = '384'
|
230
|
+
RPL_STATSCLINE = '213'
|
231
|
+
RPL_STATSNLINE = '214'
|
232
|
+
RPL_STATSILINE = '215'
|
233
|
+
RPL_STATSKLINE = '216'
|
234
|
+
RPL_STATSQLINE = '217'
|
235
|
+
RPL_STATSYLINE = '218'
|
236
|
+
RPL_STATSVLINE = '240'
|
237
|
+
RPL_STATSLLINE = '241'
|
238
|
+
RPL_STATSHLINE = '244'
|
239
|
+
RPL_STATSSLINE = '244'
|
240
|
+
RPL_STATSPING = '246'
|
241
|
+
RPL_STATSBLINE = '247'
|
242
|
+
RPL_STATSDLINE = '250'
|
243
|
+
ERR_NOSERVICEHOST = '492'
|
244
|
+
|
245
|
+
PASS = 'PASS'
|
246
|
+
NICK = 'NICK'
|
247
|
+
USER = 'USER'
|
248
|
+
OPER = 'OPER'
|
249
|
+
MODE = 'MODE'
|
250
|
+
SERVICE = 'SERVICE'
|
251
|
+
QUIT = 'QUIT'
|
252
|
+
SQUIT = 'SQUIT'
|
253
|
+
JOIN = 'JOIN'
|
254
|
+
PART = 'PART'
|
255
|
+
TOPIC = 'TOPIC'
|
256
|
+
NAMES = 'NAMES'
|
257
|
+
LIST = 'LIST'
|
258
|
+
INVITE = 'INVITE'
|
259
|
+
KICK = 'KICK'
|
260
|
+
PRIVMSG = 'PRIVMSG'
|
261
|
+
NOTICE = 'NOTICE'
|
262
|
+
MOTD = 'MOTD'
|
263
|
+
LUSERS = 'LUSERS'
|
264
|
+
VERSION = 'VERSION'
|
265
|
+
STATS = 'STATS'
|
266
|
+
LINKS = 'LINKS'
|
267
|
+
TIME = 'TIME'
|
268
|
+
CONNECT = 'CONNECT'
|
269
|
+
TRACE = 'TRACE'
|
270
|
+
ADMIN = 'ADMIN'
|
271
|
+
INFO = 'INFO'
|
272
|
+
SERVLIST = 'SERVLIST'
|
273
|
+
SQUERY = 'SQUERY'
|
274
|
+
WHO = 'WHO'
|
275
|
+
WHOIS = 'WHOIS'
|
276
|
+
WHOWAS = 'WHOWAS'
|
277
|
+
KILL = 'KILL'
|
278
|
+
PING = 'PING'
|
279
|
+
PONG = 'PONG'
|
280
|
+
ERROR = 'ERROR'
|
281
|
+
AWAY = 'AWAY'
|
282
|
+
REHASH = 'REHASH'
|
283
|
+
DIE = 'DIE'
|
284
|
+
RESTART = 'RESTART'
|
285
|
+
SUMMON = 'SUMMON'
|
286
|
+
USERS = 'USERS'
|
287
|
+
WALLOPS = 'WALLOPS'
|
288
|
+
USERHOST = 'USERHOST'
|
289
|
+
ISON = 'ISON'
|
290
|
+
end
|
291
|
+
|
292
|
+
COMMANDS = Constants.constants.inject({}) {|r,i| # :nodoc:
|
293
|
+
r[Constants.const_get(i)] = i
|
294
|
+
r
|
295
|
+
}
|
296
|
+
|
297
|
+
class Prefix < String
|
298
|
+
def nick
|
299
|
+
extract[0]
|
300
|
+
end
|
301
|
+
|
302
|
+
def user
|
303
|
+
extract[1]
|
304
|
+
end
|
305
|
+
|
306
|
+
def host
|
307
|
+
extract[2]
|
308
|
+
end
|
309
|
+
|
310
|
+
# Extract prefix string to [nick, user, host] Array.
|
311
|
+
def extract
|
312
|
+
_, *ret = *self.match(/^([^\s!]+)!([^\s@]+)@(\S+)$/)
|
313
|
+
ret
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# Encoding to CTCP message. Prefix and postfix \x01.
|
318
|
+
def ctcp_encoding(str)
|
319
|
+
str = str.gsub(/\\/, "\\\\\\\\").gsub(/\x01/, '\a')
|
320
|
+
str = str.gsub(/\x10/, "\x10\x10").gsub(/\x00/, "\x10\x30").gsub(/\x0d/, "\x10r").gsub(/\x0a/, "\x10n")
|
321
|
+
"\x01#{str}\x01"
|
322
|
+
end
|
323
|
+
module_function :ctcp_encoding
|
324
|
+
|
325
|
+
# Decoding to CTCP message. Remove \x01.
|
326
|
+
def ctcp_decoding(str)
|
327
|
+
str = str.gsub(/\x01/, "")
|
328
|
+
str = str.gsub(/\x10n/, "\x0a").gsub(/\x10r/, "\x0d").gsub(/\x10\x30/, "\x00").gsub(/\x10\x10/, "\x10")
|
329
|
+
str = str.gsub(/\\a/, "\x01").gsub(/\\\\/, "\\")
|
330
|
+
str
|
331
|
+
end
|
332
|
+
module_function :ctcp_decoding
|
333
|
+
end
|
334
|
+
|
335
|
+
class Net::IRC::Message
|
336
|
+
include Net::IRC
|
337
|
+
|
338
|
+
class InvalidMessage < Net::IRC::IRCException; end
|
339
|
+
|
340
|
+
attr_reader :prefix, :command, :params
|
341
|
+
|
342
|
+
# Parse string and return new Message.
|
343
|
+
# If the string is invalid message, this method raises Net::IRC::Message::InvalidMessage.
|
344
|
+
def self.parse(str)
|
345
|
+
_, prefix, command, *rest = *PATTERN::MESSAGE_PATTERN.match(str)
|
346
|
+
raise InvalidMessage, "Invalid message: #{str.dump}" unless _
|
347
|
+
|
348
|
+
case
|
349
|
+
when rest[0] && !rest[0].empty?
|
350
|
+
middle, trailer, = *rest
|
351
|
+
when rest[2] && !rest[2].empty?
|
352
|
+
middle, trailer, = *rest[2, 2]
|
353
|
+
when rest[1]
|
354
|
+
params = []
|
355
|
+
trailer = rest[1]
|
356
|
+
when rest[3]
|
357
|
+
params = []
|
358
|
+
trailer = rest[3]
|
359
|
+
else
|
360
|
+
params = []
|
361
|
+
end
|
362
|
+
|
363
|
+
params ||= middle.split(/ /)[1..-1]
|
364
|
+
params << trailer if trailer
|
365
|
+
|
366
|
+
new(prefix, command, params)
|
367
|
+
end
|
368
|
+
|
369
|
+
def initialize(prefix, command, params)
|
370
|
+
@prefix = Prefix.new(prefix.to_s)
|
371
|
+
@command = command
|
372
|
+
@params = params
|
373
|
+
end
|
374
|
+
|
375
|
+
# Same as @params[n].
|
376
|
+
def [](n)
|
377
|
+
@params[n]
|
378
|
+
end
|
379
|
+
|
380
|
+
# Iterate params.
|
381
|
+
def each(&block)
|
382
|
+
@params.each(&block)
|
383
|
+
end
|
384
|
+
|
385
|
+
# Stringfy message to raw IRC message.
|
386
|
+
def to_s
|
387
|
+
str = ""
|
388
|
+
|
389
|
+
str << ":#{@prefix} " unless @prefix.empty?
|
390
|
+
str << @command
|
391
|
+
|
392
|
+
if @params
|
393
|
+
f = false
|
394
|
+
@params.each do |param|
|
395
|
+
str << " "
|
396
|
+
if !f && (param.size == 0 || / / =~ param || /^:/ =~ param)
|
397
|
+
str << ":#{param}"
|
398
|
+
f = true
|
399
|
+
else
|
400
|
+
str << param
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
str << "\x0D\x0A"
|
406
|
+
|
407
|
+
str
|
408
|
+
end
|
409
|
+
alias to_str to_s
|
410
|
+
|
411
|
+
# Same as params.
|
412
|
+
def to_a
|
413
|
+
@params
|
414
|
+
end
|
415
|
+
|
416
|
+
# If the message is CTCP, return true.
|
417
|
+
def ctcp?
|
418
|
+
message = @params[1]
|
419
|
+
message[0] == 1 && message[message.length-1] == 1
|
420
|
+
end
|
421
|
+
|
422
|
+
def inspect
|
423
|
+
'#<%s:0x%x prefix:%s command:%s params:%s>' % [
|
424
|
+
self.class,
|
425
|
+
self.object_id,
|
426
|
+
@prefix,
|
427
|
+
@command,
|
428
|
+
@params.inspect
|
429
|
+
]
|
430
|
+
end
|
431
|
+
|
432
|
+
end # Message
|
433
|
+
|
434
|
+
class Net::IRC::Client
|
435
|
+
include Net::IRC
|
436
|
+
include Constants
|
437
|
+
|
438
|
+
attr_reader :host, :port, :opts
|
439
|
+
attr_reader :prefix, :channels
|
440
|
+
|
441
|
+
def initialize(host, port, opts={})
|
442
|
+
@host = host
|
443
|
+
@port = port
|
444
|
+
@opts = OpenStruct.new(opts)
|
445
|
+
@log = @opts.logger || Logger.new($stdout)
|
446
|
+
@channels = {
|
447
|
+
# "#channel" => {
|
448
|
+
# :modes => [],
|
449
|
+
# :users => [],
|
450
|
+
# }
|
451
|
+
}
|
452
|
+
end
|
453
|
+
|
454
|
+
# Connect to server and start loop.
|
455
|
+
def start
|
456
|
+
@socket = TCPSocket.open(@host, @port)
|
457
|
+
on_connected
|
458
|
+
post PASS, @opts.pass if @opts.pass
|
459
|
+
post NICK, @opts.nick
|
460
|
+
post USER, @opts.user, "0", "*", @opts.real
|
461
|
+
while l = @socket.gets
|
462
|
+
begin
|
463
|
+
@log.debug "RECEIVE: #{l.chomp}"
|
464
|
+
m = Message.parse(l)
|
465
|
+
next if on_message(m) === true
|
466
|
+
name = "on_#{(COMMANDS[m.command.upcase] || m.command).downcase}"
|
467
|
+
send(name, m) if respond_to?(name)
|
468
|
+
rescue Exception => e
|
469
|
+
warn e
|
470
|
+
warn e.backtrace.join("\r\t")
|
471
|
+
raise
|
472
|
+
rescue Message::InvalidMessage
|
473
|
+
@log.error "MessageParse: " + l.inspect
|
474
|
+
end
|
475
|
+
end
|
476
|
+
rescue IOError
|
477
|
+
ensure
|
478
|
+
finish
|
479
|
+
end
|
480
|
+
|
481
|
+
# Close connection to server.
|
482
|
+
def finish
|
483
|
+
begin
|
484
|
+
@socket.close
|
485
|
+
rescue
|
486
|
+
end
|
487
|
+
on_disconnected
|
488
|
+
end
|
489
|
+
|
490
|
+
# Catch all messages.
|
491
|
+
# If this method return true, aother callback will not be called.
|
492
|
+
def on_message(m)
|
493
|
+
end
|
494
|
+
|
495
|
+
# Default RPL_WELCOME callback.
|
496
|
+
# This sets @prefix from the message.
|
497
|
+
def on_rpl_welcome(m)
|
498
|
+
@prefix = Prefix.new(m[1][/\S+!\S+@\S+/])
|
499
|
+
end
|
500
|
+
|
501
|
+
# Default PING callback. Response PONG.
|
502
|
+
def on_ping(m)
|
503
|
+
post PONG, @nick
|
504
|
+
end
|
505
|
+
|
506
|
+
# For managing channel
|
507
|
+
def on_rpl_namreply(m)
|
508
|
+
type = m[1]
|
509
|
+
channel = m[2]
|
510
|
+
init_channel(channel)
|
511
|
+
|
512
|
+
m[3].split(/\s+/).each do |u|
|
513
|
+
_, mode, nick = *u.match(/^([@+]?)(.+)/)
|
514
|
+
|
515
|
+
@channels[channel][:users] << nick
|
516
|
+
@channels[channel][:users].uniq!
|
517
|
+
|
518
|
+
case mode
|
519
|
+
when "@" # channel operator
|
520
|
+
@channels[channel][:modes] << ["o", nick]
|
521
|
+
when "+" # voiced (under moderating mode)
|
522
|
+
@channels[channel][:modes] << ["v", nick]
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
case type
|
527
|
+
when "@" # secret
|
528
|
+
@channels[channel][:modes] << ["s", nil]
|
529
|
+
when "*" # private
|
530
|
+
@channels[channel][:modes] << ["p", nil]
|
531
|
+
when "=" # public
|
532
|
+
end
|
533
|
+
|
534
|
+
@channels[channel][:modes].uniq!
|
535
|
+
end
|
536
|
+
|
537
|
+
# For managing channel
|
538
|
+
def on_part(m)
|
539
|
+
nick = m.prefix.nick
|
540
|
+
channel = m[0]
|
541
|
+
init_channel(channel)
|
542
|
+
|
543
|
+
info = @channels[channel]
|
544
|
+
if info
|
545
|
+
info[:users].delete(nick)
|
546
|
+
info[:modes].delete_if {|u|
|
547
|
+
u[1] == nick
|
548
|
+
}
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
# For managing channel
|
553
|
+
def on_quit(m)
|
554
|
+
nick = m.prefix.nick
|
555
|
+
|
556
|
+
@channels.each do |channel, info|
|
557
|
+
info[:users].delete(nick)
|
558
|
+
info[:modes].delete_if {|u|
|
559
|
+
u[1] == nick
|
560
|
+
}
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
564
|
+
# For managing channel
|
565
|
+
def on_kick(m)
|
566
|
+
users = m[1].split(/,/)
|
567
|
+
m[0].split(/,/).each do |chan|
|
568
|
+
init_channel(chan)
|
569
|
+
info = @channels[chan]
|
570
|
+
if info
|
571
|
+
users.each do |nick|
|
572
|
+
info[:users].delete(nick)
|
573
|
+
info[:modes].delete_if {|u|
|
574
|
+
u[1] == nick
|
575
|
+
}
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
# For managing channel
|
582
|
+
def on_join(m)
|
583
|
+
nick = m.prefix.nick
|
584
|
+
channel = m[0]
|
585
|
+
init_channel(channel)
|
586
|
+
|
587
|
+
@channels[channel][:users] << nick
|
588
|
+
@channels[channel][:users].uniq!
|
589
|
+
end
|
590
|
+
|
591
|
+
# For managing channel
|
592
|
+
def on_mode(m)
|
593
|
+
channel = m[0]
|
594
|
+
init_channel(channel)
|
595
|
+
|
596
|
+
positive_mode = []
|
597
|
+
negative_mode = []
|
598
|
+
|
599
|
+
mode = positive_mode
|
600
|
+
arg_pos = 0
|
601
|
+
m[1].each_byte do |c|
|
602
|
+
case c
|
603
|
+
when ?+
|
604
|
+
mode = positive_mode
|
605
|
+
when ?-
|
606
|
+
mode = negative_mode
|
607
|
+
when ?o, ?v, ?k, ?l, ?b, ?e, ?I
|
608
|
+
mode << [c.chr, m[arg_pos + 2]]
|
609
|
+
arg_pos += 1
|
610
|
+
else
|
611
|
+
mode << [c.chr, nil]
|
612
|
+
end
|
613
|
+
end
|
614
|
+
mode = nil
|
615
|
+
|
616
|
+
negative_mode.each do |m|
|
617
|
+
@channels[channel][:modes].delete(m)
|
618
|
+
end
|
619
|
+
|
620
|
+
positive_mode.each do |m|
|
621
|
+
@channels[channel][:modes] << m
|
622
|
+
end
|
623
|
+
|
624
|
+
@channels[channel][:modes].uniq!
|
625
|
+
[negative_mode, positive_mode]
|
626
|
+
end
|
627
|
+
|
628
|
+
# For managing channel
|
629
|
+
def init_channel(channel)
|
630
|
+
@channels[channel] ||= {
|
631
|
+
:modes => [],
|
632
|
+
:users => [],
|
633
|
+
}
|
634
|
+
end
|
635
|
+
|
636
|
+
# Do nothing.
|
637
|
+
# This is for avoiding error on calling super.
|
638
|
+
# So you can always call super at subclass.
|
639
|
+
def method_missing(name, *args)
|
640
|
+
end
|
641
|
+
|
642
|
+
# Call when socket connected.
|
643
|
+
def on_connected
|
644
|
+
end
|
645
|
+
|
646
|
+
# Call when socket closed.
|
647
|
+
def on_disconnected
|
648
|
+
end
|
649
|
+
|
650
|
+
private
|
651
|
+
|
652
|
+
# Post message to server.
|
653
|
+
#
|
654
|
+
# include Net::IRC::Constans
|
655
|
+
# post PRIVMSG, "#channel", "foobar"
|
656
|
+
def post(command, *params)
|
657
|
+
m = Message.new(nil, command, params.map {|s|
|
658
|
+
s.gsub(/[\r\n]/, " ")
|
659
|
+
})
|
660
|
+
@log.debug "SEND: #{m.to_s.chomp}"
|
661
|
+
@socket << m
|
662
|
+
end
|
663
|
+
end # Client
|
664
|
+
|
665
|
+
class Net::IRC::Server
|
666
|
+
def initialize(host, port, session_class, opts={})
|
667
|
+
@host = host
|
668
|
+
@port = port
|
669
|
+
@session_class = session_class
|
670
|
+
@opts = OpenStruct.new(opts)
|
671
|
+
@sessions = []
|
672
|
+
end
|
673
|
+
|
674
|
+
# Start server loop.
|
675
|
+
def start
|
676
|
+
@serv = TCPServer.new(@host, @port)
|
677
|
+
@log = @opts.logger || Logger.new($stdout)
|
678
|
+
@log.info "Host: #{@host} Port:#{@port}"
|
679
|
+
@accept = Thread.start do
|
680
|
+
loop do
|
681
|
+
Thread.start(@serv.accept) do |s|
|
682
|
+
begin
|
683
|
+
@log.info "Client connected, new session starting..."
|
684
|
+
s = @session_class.new(self, s, @log, @opts)
|
685
|
+
@sessions << s
|
686
|
+
s.start
|
687
|
+
rescue Exception => e
|
688
|
+
puts e
|
689
|
+
puts e.backtrace
|
690
|
+
ensure
|
691
|
+
@sessions.delete(s)
|
692
|
+
end
|
693
|
+
end
|
694
|
+
end
|
695
|
+
end
|
696
|
+
@accept.join
|
697
|
+
end
|
698
|
+
|
699
|
+
# Close all sessions.
|
700
|
+
def finish
|
701
|
+
Thread.exclusive do
|
702
|
+
@accept.kill
|
703
|
+
begin
|
704
|
+
@serv.close
|
705
|
+
rescue
|
706
|
+
end
|
707
|
+
@sessions.each do |s|
|
708
|
+
s.finish
|
709
|
+
end
|
710
|
+
end
|
711
|
+
end
|
712
|
+
|
713
|
+
|
714
|
+
class Session
|
715
|
+
include Net::IRC
|
716
|
+
include Constants
|
717
|
+
|
718
|
+
attr_reader :prefix, :nick, :real, :host
|
719
|
+
|
720
|
+
# Override subclass.
|
721
|
+
def server_name
|
722
|
+
"Net::IRC::Server::Session"
|
723
|
+
end
|
724
|
+
|
725
|
+
# Override subclass.
|
726
|
+
def server_version
|
727
|
+
"0.0.0"
|
728
|
+
end
|
729
|
+
|
730
|
+
# Override subclass.
|
731
|
+
def avaiable_user_modes
|
732
|
+
"eixwy"
|
733
|
+
end
|
734
|
+
|
735
|
+
# Override subclass.
|
736
|
+
def avaiable_channel_modes
|
737
|
+
"spknm"
|
738
|
+
end
|
739
|
+
|
740
|
+
def initialize(server, socket, logger, opts={})
|
741
|
+
@server, @socket, @log, @opts = server, socket, logger, opts
|
742
|
+
end
|
743
|
+
|
744
|
+
def self.start(*args)
|
745
|
+
new(*args).start
|
746
|
+
end
|
747
|
+
|
748
|
+
# Start session loop.
|
749
|
+
def start
|
750
|
+
on_connected
|
751
|
+
while l = @socket.gets
|
752
|
+
begin
|
753
|
+
@log.debug "RECEIVE: #{l.chomp}"
|
754
|
+
m = Message.parse(l)
|
755
|
+
next if on_message(m) === true
|
756
|
+
if m.command == QUIT
|
757
|
+
on_quit if respond_to?(:on_quit)
|
758
|
+
break
|
759
|
+
else
|
760
|
+
name = "on_#{(COMMANDS[m.command.upcase] || m.command).downcase}"
|
761
|
+
send(name, m) if respond_to?(name)
|
762
|
+
end
|
763
|
+
rescue Message::InvalidMessage
|
764
|
+
@log.error "MessageParse: " + l.inspect
|
765
|
+
end
|
766
|
+
end
|
767
|
+
rescue IOError
|
768
|
+
ensure
|
769
|
+
finish
|
770
|
+
end
|
771
|
+
|
772
|
+
# Close this session.
|
773
|
+
def finish
|
774
|
+
begin
|
775
|
+
@socket.close
|
776
|
+
rescue
|
777
|
+
end
|
778
|
+
on_disconnected
|
779
|
+
end
|
780
|
+
|
781
|
+
# Default PASS callback.
|
782
|
+
# Set @pass.
|
783
|
+
def on_pass(m)
|
784
|
+
@pass = m.params[0]
|
785
|
+
end
|
786
|
+
|
787
|
+
# Default NICK callback.
|
788
|
+
# Set @nick.
|
789
|
+
def on_nick(m)
|
790
|
+
@nick = m.params[0]
|
791
|
+
end
|
792
|
+
|
793
|
+
|
794
|
+
# Default USER callback.
|
795
|
+
# Set @user, @real, @host and call inital_message.
|
796
|
+
def on_user(m)
|
797
|
+
@user, @real = m.params[0], m.params[3]
|
798
|
+
@host = @socket.peeraddr[2]
|
799
|
+
@prefix = Prefix.new("#{@nick}!#{@user}@#{@host}")
|
800
|
+
inital_message
|
801
|
+
end
|
802
|
+
|
803
|
+
# Call when socket connected.
|
804
|
+
def on_connected
|
805
|
+
end
|
806
|
+
|
807
|
+
# Call when socket closed.
|
808
|
+
def on_disconnected
|
809
|
+
end
|
810
|
+
|
811
|
+
# Catch all messages.
|
812
|
+
# If this method return true, aother callback will not be called.
|
813
|
+
def on_message(m)
|
814
|
+
end
|
815
|
+
|
816
|
+
# Do nothing.
|
817
|
+
# This is for avoiding error on calling super.
|
818
|
+
# So you can always call super at subclass.
|
819
|
+
def method_missing(name, *args)
|
820
|
+
end
|
821
|
+
|
822
|
+
private
|
823
|
+
# Post message to server.
|
824
|
+
#
|
825
|
+
# include Net::IRC::Constans
|
826
|
+
# post prefix, PRIVMSG, "#channel", "foobar"
|
827
|
+
def post(prefix, command, *params)
|
828
|
+
m = Message.new(prefix, command, params.map {|s|
|
829
|
+
s.gsub(/[\r\n]/, " ")
|
830
|
+
})
|
831
|
+
@log.debug "SEND: #{m.to_s.chomp}"
|
832
|
+
@socket << m
|
833
|
+
end
|
834
|
+
|
835
|
+
# Call when client connected.
|
836
|
+
# Send RPL_WELCOME sequence. If you want to customize, override this method at subclass.
|
837
|
+
def inital_message
|
838
|
+
post nil, RPL_WELCOME, @nick, "Welcome to the Internet Relay Network #{@prefix}"
|
839
|
+
post nil, RPL_YOURHOST, @nick, "Your host is #{server_name}, running version #{server_version}"
|
840
|
+
post nil, RPL_CREATED, @nick, "This server was created #{Time.now}"
|
841
|
+
post nil, RPL_MYINFO, @nick, "#{server_name} #{server_version} #{avaiable_user_modes} #{avaiable_channel_modes}"
|
842
|
+
end
|
843
|
+
end
|
844
|
+
end # Server
|