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.
Files changed (8) hide show
  1. data/ChangeLog +0 -0
  2. data/README +92 -0
  3. data/Rakefile +131 -0
  4. data/examples/lig.rb +276 -0
  5. data/examples/tig.rb +446 -0
  6. data/examples/wig.rb +132 -0
  7. data/lib/net/irc.rb +844 -0
  8. 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