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