lib/net/irc.rb in net-irc-0.0.3 vs lib/net/irc.rb in net-irc-0.0.4

- old
+ new

@@ -7,295 +7,20 @@ require "monitor" module Net; end module Net::IRC - VERSION = "0.0.3" + VERSION = "0.0.4" class IRCException < StandardError; end - module PATTERN # :nodoc: - # letter = %x41-5A / %x61-7A ; A-Z / a-z - # digit = %x30-39 ; 0-9 - # hexdigit = digit / "A" / "B" / "C" / "D" / "E" / "F" - # special = %x5B-60 / %x7B-7D - # ; "[", "]", "\", "`", "_", "^", "{", "|", "}" - LETTER = 'A-Za-z' - DIGIT = '\d' - HEXDIGIT = "#{DIGIT}A-Fa-f" - SPECIAL = '\x5B-\x60\x7B-\x7D' + require "net/irc/constants" + require "net/irc/pattern" - # shortname = ( letter / digit ) *( letter / digit / "-" ) - # *( letter / digit ) - # ; as specified in RFC 1123 [HNAME] - # hostname = shortname *( "." shortname ) - SHORTNAME = "[#{LETTER}#{DIGIT}](?:[-#{LETTER}#{DIGIT}]*[#{LETTER}#{DIGIT}])?" - HOSTNAME = "#{SHORTNAME}(?:\\.#{SHORTNAME})*" + autoload :Message, "net/irc/message" + autoload :Client, "net/irc/client" + autoload :Server, "net/irc/server" - # servername = hostname - SERVERNAME = HOSTNAME - - # nickname = ( letter / special ) *8( letter / digit / special / "-" ) - #NICKNAME = "[#{LETTER}#{SPECIAL}\\w][-#{LETTER}#{DIGIT}#{SPECIAL}]*" - NICKNAME = "\\S+" # for multibytes - - # user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF ) - # ; any octet except NUL, CR, LF, " " and "@" - USER = '[\x01-\x09\x0B-\x0C\x0E-\x1F\x21-\x3F\x41-\xFF]+' - - # ip4addr = 1*3digit "." 1*3digit "." 1*3digit "." 1*3digit - IP4ADDR = "[#{DIGIT}]{1,3}(?:\\.[#{DIGIT}]{1,3}){3}" - # ip6addr = 1*hexdigit 7( ":" 1*hexdigit ) - # ip6addr =/ "0:0:0:0:0:" ( "0" / "FFFF" ) ":" ip4addr - IP6ADDR = "(?:[#{HEXDIGIT}]+(?::[#{HEXDIGIT}]+){7}|0:0:0:0:0:(?:0|FFFF):#{IP4ADDR})" - # hostaddr = ip4addr / ip6addr - HOSTADDR = "(?:#{IP4ADDR}|#{IP6ADDR})" - - # host = hostname / hostaddr - HOST = "(?:#{HOSTNAME}|#{HOSTADDR})" - - # prefix = servername / ( nickname [ [ "!" user ] "@" host ] ) - PREFIX = "(?:#{NICKNAME}(?:(?:!#{USER})?@#{HOST})?|#{SERVERNAME})" - - # nospcrlfcl = %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF - # ; any octet except NUL, CR, LF, " " and ":" - NOSPCRLFCL = '\x01-\x09\x0B-\x0C\x0E-\x1F\x21-\x39\x3B-\xFF' - - # command = 1*letter / 3digit - COMMAND = "(?:[#{LETTER}]+|[#{DIGIT}]{3})" - - # SPACE = %x20 ; space character - # middle = nospcrlfcl *( ":" / nospcrlfcl ) - # trailing = *( ":" / " " / nospcrlfcl ) - # params = *14( SPACE middle ) [ SPACE ":" trailing ] - # =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ] - MIDDLE = "[#{NOSPCRLFCL}][:#{NOSPCRLFCL}]*" - TRAILING = "[: #{NOSPCRLFCL}]*" - PARAMS = "(?:((?: #{MIDDLE}){0,14})(?: :(#{TRAILING}))?|((?: #{MIDDLE}){14})(?::?)?(#{TRAILING}))" - - # crlf = %x0D %x0A ; "carriage return" "linefeed" - # message = [ ":" prefix SPACE ] command [ params ] crlf - CRLF = '\x0D\x0A' - MESSAGE = "(?::(#{PREFIX}) )?(#{COMMAND})#{PARAMS}\s*#{CRLF}" - - CLIENT_PATTERN = /\A#{NICKNAME}(?:(?:!#{USER})?@#{HOST})\z/on - MESSAGE_PATTERN = /\A#{MESSAGE}\z/on - end # PATTERN - - module Constants # :nodoc: - RPL_WELCOME = '001' - RPL_YOURHOST = '002' - RPL_CREATED = '003' - RPL_MYINFO = '004' - RPL_BOUNCE = '005' - RPL_USERHOST = '302' - RPL_ISON = '303' - RPL_AWAY = '301' - RPL_UNAWAY = '305' - RPL_NOWAWAY = '306' - RPL_WHOISUSER = '311' - RPL_WHOISSERVER = '312' - RPL_WHOISOPERATOR = '313' - RPL_WHOISIDLE = '317' - RPL_ENDOFWHOIS = '318' - RPL_WHOISCHANNELS = '319' - RPL_WHOWASUSER = '314' - RPL_ENDOFWHOWAS = '369' - RPL_LISTSTART = '321' - RPL_LIST = '322' - RPL_LISTEND = '323' - RPL_UNIQOPIS = '325' - RPL_CHANNELMODEIS = '324' - RPL_NOTOPIC = '331' - RPL_TOPIC = '332' - RPL_INVITING = '341' - RPL_SUMMONING = '342' - RPL_INVITELIST = '346' - RPL_ENDOFINVITELIST = '347' - RPL_EXCEPTLIST = '348' - RPL_ENDOFEXCEPTLIST = '349' - RPL_VERSION = '351' - RPL_WHOREPLY = '352' - RPL_ENDOFWHO = '315' - RPL_NAMREPLY = '353' - RPL_ENDOFNAMES = '366' - RPL_LINKS = '364' - RPL_ENDOFLINKS = '365' - RPL_BANLIST = '367' - RPL_ENDOFBANLIST = '368' - RPL_INFO = '371' - RPL_ENDOFINFO = '374' - RPL_MOTDSTART = '375' - RPL_MOTD = '372' - RPL_ENDOFMOTD = '376' - RPL_YOUREOPER = '381' - RPL_REHASHING = '382' - RPL_YOURESERVICE = '383' - RPL_TIME = '391' - RPL_USERSSTART = '392' - RPL_USERS = '393' - RPL_ENDOFUSERS = '394' - RPL_NOUSERS = '395' - RPL_TRACELINK = '200' - RPL_TRACECONNECTING = '201' - RPL_TRACEHANDSHAKE = '202' - RPL_TRACEUNKNOWN = '203' - RPL_TRACEOPERATOR = '204' - RPL_TRACEUSER = '205' - RPL_TRACESERVER = '206' - RPL_TRACESERVICE = '207' - RPL_TRACENEWTYPE = '208' - RPL_TRACECLASS = '209' - RPL_TRACERECONNECT = '210' - RPL_TRACELOG = '261' - RPL_TRACEEND = '262' - RPL_STATSLINKINFO = '211' - RPL_STATSCOMMANDS = '212' - RPL_ENDOFSTATS = '219' - RPL_STATSUPTIME = '242' - RPL_STATSOLINE = '243' - RPL_UMODEIS = '221' - RPL_SERVLIST = '234' - RPL_SERVLISTEND = '235' - RPL_LUSERCLIENT = '251' - RPL_LUSEROP = '252' - RPL_LUSERUNKNOWN = '253' - RPL_LUSERCHANNELS = '254' - RPL_LUSERME = '255' - RPL_ADMINME = '256' - RPL_ADMINLOC1 = '257' - RPL_ADMINLOC2 = '258' - RPL_ADMINEMAIL = '259' - RPL_TRYAGAIN = '263' - ERR_NOSUCHNICK = '401' - ERR_NOSUCHSERVER = '402' - ERR_NOSUCHCHANNEL = '403' - ERR_CANNOTSENDTOCHAN = '404' - ERR_TOOMANYCHANNELS = '405' - ERR_WASNOSUCHNICK = '406' - ERR_TOOMANYTARGETS = '407' - ERR_NOSUCHSERVICE = '408' - ERR_NOORIGIN = '409' - ERR_NORECIPIENT = '411' - ERR_NOTEXTTOSEND = '412' - ERR_NOTOPLEVEL = '413' - ERR_WILDTOPLEVEL = '414' - ERR_BADMASK = '415' - ERR_UNKNOWNCOMMAND = '421' - ERR_NOMOTD = '422' - ERR_NOADMININFO = '423' - ERR_FILEERROR = '424' - ERR_NONICKNAMEGIVEN = '431' - ERR_ERRONEUSNICKNAME = '432' - ERR_NICKNAMEINUSE = '433' - ERR_NICKCOLLISION = '436' - ERR_UNAVAILRESOURCE = '437' - ERR_USERNOTINCHANNEL = '441' - ERR_NOTONCHANNEL = '442' - ERR_USERONCHANNEL = '443' - ERR_NOLOGIN = '444' - ERR_SUMMONDISABLED = '445' - ERR_USERSDISABLED = '446' - ERR_NOTREGISTERED = '451' - ERR_NEEDMOREPARAMS = '461' - ERR_ALREADYREGISTRED = '462' - ERR_NOPERMFORHOST = '463' - ERR_PASSWDMISMATCH = '464' - ERR_YOUREBANNEDCREEP = '465' - ERR_YOUWILLBEBANNED = '466' - ERR_KEYSET = '467' - ERR_CHANNELISFULL = '471' - ERR_UNKNOWNMODE = '472' - ERR_INVITEONLYCHAN = '473' - ERR_BANNEDFROMCHAN = '474' - ERR_BADCHANNELKEY = '475' - ERR_BADCHANMASK = '476' - ERR_NOCHANMODES = '477' - ERR_BANLISTFULL = '478' - ERR_NOPRIVILEGES = '481' - ERR_CHANOPRIVSNEEDED = '482' - ERR_CANTKILLSERVER = '483' - ERR_RESTRICTED = '484' - ERR_UNIQOPPRIVSNEEDED = '485' - ERR_NOOPERHOST = '491' - ERR_UMODEUNKNOWNFLAG = '501' - ERR_USERSDONTMATCH = '502' - RPL_SERVICEINFO = '231' - RPL_ENDOFSERVICES = '232' - RPL_SERVICE = '233' - RPL_NONE = '300' - RPL_WHOISCHANOP = '316' - RPL_KILLDONE = '361' - RPL_CLOSING = '362' - RPL_CLOSEEND = '363' - RPL_INFOSTART = '373' - RPL_MYPORTIS = '384' - RPL_STATSCLINE = '213' - RPL_STATSNLINE = '214' - RPL_STATSILINE = '215' - RPL_STATSKLINE = '216' - RPL_STATSQLINE = '217' - RPL_STATSYLINE = '218' - RPL_STATSVLINE = '240' - RPL_STATSLLINE = '241' - RPL_STATSHLINE = '244' - RPL_STATSSLINE = '244' - RPL_STATSPING = '246' - RPL_STATSBLINE = '247' - RPL_STATSDLINE = '250' - ERR_NOSERVICEHOST = '492' - - PASS = 'PASS' - NICK = 'NICK' - USER = 'USER' - OPER = 'OPER' - MODE = 'MODE' - SERVICE = 'SERVICE' - QUIT = 'QUIT' - SQUIT = 'SQUIT' - JOIN = 'JOIN' - PART = 'PART' - TOPIC = 'TOPIC' - NAMES = 'NAMES' - LIST = 'LIST' - INVITE = 'INVITE' - KICK = 'KICK' - PRIVMSG = 'PRIVMSG' - NOTICE = 'NOTICE' - MOTD = 'MOTD' - LUSERS = 'LUSERS' - VERSION = 'VERSION' - STATS = 'STATS' - LINKS = 'LINKS' - TIME = 'TIME' - CONNECT = 'CONNECT' - TRACE = 'TRACE' - ADMIN = 'ADMIN' - INFO = 'INFO' - SERVLIST = 'SERVLIST' - SQUERY = 'SQUERY' - WHO = 'WHO' - WHOIS = 'WHOIS' - WHOWAS = 'WHOWAS' - KILL = 'KILL' - PING = 'PING' - PONG = 'PONG' - ERROR = 'ERROR' - AWAY = 'AWAY' - REHASH = 'REHASH' - DIE = 'DIE' - RESTART = 'RESTART' - SUMMON = 'SUMMON' - USERS = 'USERS' - WALLOPS = 'WALLOPS' - USERHOST = 'USERHOST' - ISON = 'ISON' - end - - COMMANDS = Constants.constants.inject({}) {|r,i| # :nodoc: - r.update(Constants.const_get(i) => i) - } - class Prefix < String def nick extract[0] end @@ -330,535 +55,5 @@ str end module_function :ctcp_decoding end -class Net::IRC::Message - include Net::IRC - - class InvalidMessage < Net::IRC::IRCException; end - - attr_reader :prefix, :command, :params - - # Parse string and return new Message. - # If the string is invalid message, this method raises Net::IRC::Message::InvalidMessage. - def self.parse(str) - _, prefix, command, *rest = *PATTERN::MESSAGE_PATTERN.match(str) - raise InvalidMessage, "Invalid message: #{str.dump}" unless _ - - case - when rest[0] && !rest[0].empty? - middle, trailer, = *rest - when rest[2] && !rest[2].empty? - middle, trailer, = *rest[2, 2] - when rest[1] - params = [] - trailer = rest[1] - when rest[3] - params = [] - trailer = rest[3] - else - params = [] - end - - params ||= middle.split(/ /)[1..-1] - params << trailer if trailer - - new(prefix, command, params) - end - - def initialize(prefix, command, params) - @prefix = Prefix.new(prefix.to_s) - @command = command - @params = params - end - - # Same as @params[n]. - def [](n) - @params[n] - end - - # Iterate params. - def each(&block) - @params.each(&block) - end - - # Stringfy message to raw IRC message. - def to_s - str = "" - - str << ":#{@prefix} " unless @prefix.empty? - str << @command - - if @params - f = false - @params.each do |param| - str << " " - if !f && (param.size == 0 || / / =~ param || /^:/ =~ param) - str << ":#{param}" - f = true - else - str << param - end - end - end - - str << "\x0D\x0A" - - str - end - alias to_str to_s - - # Same as params. - def to_a - @params - end - - # If the message is CTCP, return true. - def ctcp? - message = @params[1] - message[0] == 1 && message[message.length-1] == 1 - end - - def inspect - '#<%s:0x%x prefix:%s command:%s params:%s>' % [ - self.class, - self.object_id, - @prefix, - @command, - @params.inspect - ] - end - -end # Message - -class Net::IRC::Client - include Net::IRC - include Constants - - attr_reader :host, :port, :opts - attr_reader :prefix, :channels - - def initialize(host, port, opts={}) - @host = host - @port = port - @opts = OpenStruct.new(opts) - @log = @opts.logger || Logger.new($stdout) - @channels = { -# "#channel" => { -# :modes => [], -# :users => [], -# } - } - @channels.extend(MonitorMixin) - end - - # Connect to server and start loop. - def start - @socket = TCPSocket.open(@host, @port) - on_connected - post PASS, @opts.pass if @opts.pass - post NICK, @opts.nick - post USER, @opts.user, "0", "*", @opts.real - while l = @socket.gets - begin - @log.debug "RECEIVE: #{l.chomp}" - m = Message.parse(l) - next if on_message(m) === true - name = "on_#{(COMMANDS[m.command.upcase] || m.command).downcase}" - send(name, m) if respond_to?(name) - rescue Exception => e - warn e - warn e.backtrace.join("\r\t") - raise - rescue Message::InvalidMessage - @log.error "MessageParse: " + l.inspect - end - end - rescue IOError - ensure - finish - end - - # Close connection to server. - def finish - begin - @socket.close - rescue - end - on_disconnected - end - - # Catch all messages. - # If this method return true, aother callback will not be called. - def on_message(m) - end - - # Default RPL_WELCOME callback. - # This sets @prefix from the message. - def on_rpl_welcome(m) - @prefix = Prefix.new(m[1][/\S+$/]) - end - - # Default PING callback. Response PONG. - def on_ping(m) - post PONG, @prefix ? @prefix.nick : "" - end - - # For managing channel - def on_rpl_namreply(m) - type = m[1] - channel = m[2] - init_channel(channel) - - @channels.synchronize do - m[3].split(/\s+/).each do |u| - _, mode, nick = *u.match(/^([@+]?)(.+)/) - - @channels[channel][:users] << nick - @channels[channel][:users].uniq! - - case mode - when "@" # channel operator - @channels[channel][:modes] << ["o", nick] - when "+" # voiced (under moderating mode) - @channels[channel][:modes] << ["v", nick] - end - end - - case type - when "@" # secret - @channels[channel][:modes] << ["s", nil] - when "*" # private - @channels[channel][:modes] << ["p", nil] - when "=" # public - end - - @channels[channel][:modes].uniq! - end - end - - # For managing channel - def on_part(m) - nick = m.prefix.nick - channel = m[0] - init_channel(channel) - - @channels.synchronize do - info = @channels[channel] - if info - info[:users].delete(nick) - info[:modes].delete_if {|u| - u[1] == nick - } - end - end - end - - # For managing channel - def on_quit(m) - nick = m.prefix.nick - - @channels.synchronize do - @channels.each do |channel, info| - info[:users].delete(nick) - info[:modes].delete_if {|u| - u[1] == nick - } - end - end - end - - # For managing channel - def on_kick(m) - users = m[1].split(/,/) - - @channels.synchronize do - m[0].split(/,/).each do |chan| - init_channel(chan) - info = @channels[chan] - if info - users.each do |nick| - info[:users].delete(nick) - info[:modes].delete_if {|u| - u[1] == nick - } - end - end - end - end - end - - # For managing channel - def on_join(m) - nick = m.prefix.nick - channel = m[0] - - @channels.synchronize do - init_channel(channel) - - @channels[channel][:users] << nick - @channels[channel][:users].uniq! - end - end - - # For managing channel - def on_mode(m) - channel = m[0] - @channels.synchronize do - init_channel(channel) - - positive_mode = [] - negative_mode = [] - - mode = positive_mode - arg_pos = 0 - m[1].each_byte do |c| - case c - when ?+ - mode = positive_mode - when ?- - mode = negative_mode - when ?o, ?v, ?k, ?l, ?b, ?e, ?I - mode << [c.chr, m[arg_pos + 2]] - arg_pos += 1 - else - mode << [c.chr, nil] - end - end - mode = nil - - negative_mode.each do |m| - @channels[channel][:modes].delete(m) - end - - positive_mode.each do |m| - @channels[channel][:modes] << m - end - - @channels[channel][:modes].uniq! - [negative_mode, positive_mode] - end - end - - # For managing channel - def init_channel(channel) - @channels[channel] ||= { - :modes => [], - :users => [], - } - end - - # Do nothing. - # This is for avoiding error on calling super. - # So you can always call super at subclass. - def method_missing(name, *args) - end - - # Call when socket connected. - def on_connected - end - - # Call when socket closed. - def on_disconnected - end - - private - - # Post message to server. - # - # include Net::IRC::Constans - # post PRIVMSG, "#channel", "foobar" - def post(command, *params) - m = Message.new(nil, command, params.map {|s| - s ? s.gsub(/[\r\n]/, " ") : "" - }) - - @log.debug "SEND: #{m.to_s.chomp}" - @socket << m - end -end # Client - -class Net::IRC::Server - def initialize(host, port, session_class, opts={}) - @host = host - @port = port - @session_class = session_class - @opts = OpenStruct.new(opts) - @sessions = [] - end - - # Start server loop. - def start - @serv = TCPServer.new(@host, @port) - @log = @opts.logger || Logger.new($stdout) - @log.info "Host: #{@host} Port:#{@port}" - @accept = Thread.start do - loop do - Thread.start(@serv.accept) do |s| - begin - @log.info "Client connected, new session starting..." - s = @session_class.new(self, s, @log, @opts) - @sessions << s - s.start - rescue Exception => e - puts e - puts e.backtrace - ensure - @sessions.delete(s) - end - end - end - end - @accept.join - end - - # Close all sessions. - def finish - Thread.exclusive do - @accept.kill - begin - @serv.close - rescue - end - @sessions.each do |s| - s.finish - end - end - end - - - class Session - include Net::IRC - include Constants - - attr_reader :prefix, :nick, :real, :host - - # Override subclass. - def server_name - "net-irc" - end - - # Override subclass. - def server_version - "0.0.0" - end - - # Override subclass. - def avaiable_user_modes - "eixwy" - end - - # Override subclass. - def avaiable_channel_modes - "spknm" - end - - def initialize(server, socket, logger, opts={}) - @server, @socket, @log, @opts = server, socket, logger, opts - end - - def self.start(*args) - new(*args).start - end - - # Start session loop. - def start - on_connected - while l = @socket.gets - begin - @log.debug "RECEIVE: #{l.chomp}" - m = Message.parse(l) - next if on_message(m) === true - - name = "on_#{(COMMANDS[m.command.upcase] || m.command).downcase}" - send(name, m) if respond_to?(name) - - break if m.command == QUIT - rescue Message::InvalidMessage - @log.error "MessageParse: " + l.inspect - end - end - rescue IOError - ensure - finish - end - - # Close this session. - def finish - begin - @socket.close - rescue - end - on_disconnected - end - - # Default PASS callback. - # Set @pass. - def on_pass(m) - @pass = m.params[0] - end - - # Default NICK callback. - # Set @nick. - def on_nick(m) - @nick = m.params[0] - end - - # Default USER callback. - # Set @user, @real, @host and call initial_message. - def on_user(m) - @user, @real = m.params[0], m.params[3] - @host = @socket.peeraddr[2] - @prefix = Prefix.new("#{@nick}!#{@user}@#{@host}") - initial_message - end - - # Call when socket connected. - def on_connected - end - - # Call when socket closed. - def on_disconnected - end - - # Catch all messages. - # If this method return true, aother callback will not be called. - def on_message(m) - end - - # Default PING callback. Response PONG. - def on_ping(m) - post server_name, PONG, m.params[0] - end - - # Do nothing. - # This is for avoiding error on calling super. - # So you can always call super at subclass. - def method_missing(name, *args) - end - - private - # Post message to server. - # - # include Net::IRC::Constans - # post prefix, PRIVMSG, "#channel", "foobar" - def post(prefix, command, *params) - m = Message.new(prefix, command, params.map {|s| - s.gsub(/[\r\n]/, " ") - }) - @log.debug "SEND: #{m.to_s.chomp}" - @socket << m - rescue IOError - finish - end - - # Call when client connected. - # Send RPL_WELCOME sequence. If you want to customize, override this method at subclass. - def initial_message - post server_name, RPL_WELCOME, @nick, "Welcome to the Internet Relay Network #{@prefix}" - post server_name, RPL_YOURHOST, @nick, "Your host is #{server_name}, running version #{server_version}" - post server_name, RPL_CREATED, @nick, "This server was created #{Time.now}" - post server_name, RPL_MYINFO, @nick, "#{server_name} #{server_version} #{avaiable_user_modes} #{avaiable_channel_modes}" - end - end -end # Server