net-irc 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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