=begin = rice - Ruby Irc interfaCE $Id: irc.rb,v 1.9 2001/06/13 10:22:24 akira Exp $ Copyright (c) 2001 akira yamada You can redistribute it and/or modify it under the same term as Ruby. =end require 'socket' require 'thread' require 'monitor' module RICE class Error < StandardError; end class InvalidMessage < Error; end class UnknownCommand < Error; end =begin == RICE::Connection =end class Connection class Error < StandardError; end class Closed < Error; end =begin --- RICE::Connection::new =end def initialize(server, port, eol = "\r\n") @conn = [] @conn.extend(MonitorMixin) self.server = server self.port = port self.eol = eol @read_q = Queue.new @read_th = Thread.new(@read_q, @eol) do |read_q, eol| read_thread(read_q, eol) end @threads = {} @threads.extend(MonitorMixin) @dispatcher = Thread.new(@read_q) do |read_q| loop do x = read_q.pop ths = @threads.synchronize do @threads.keys end ths.each do |th| if th.status @threads[th].q.push(x) else @threads.delete(th) end end end # loop end @delay = 0.3 end attr :delay, true =begin --- RICE::Connection#server=(server) =end def server=(server) raise RuntimeError, "Already connected to #{@server}:#{@port}" unless @conn.empty? @server = server end =begin --- RICE::Connection#port=(port) =end def port=(port) raise RuntimeError, "Already connected to #{@server}:#{@port}" unless @conn.empty? @port = port end =begin --- RICE::Connection#eol=(eol) =end def eol=(eol) raise RuntimeError, "Already connected to #{@server}:#{@port}" unless @conn.empty? @eol = eol end =begin --- RICE::Connection#start(max_retry = 3, retry_wait = 30) =end def start(max_retry = 3, retry_wait = 30) @client_th = Thread.current # caller thread if alive? #sleep retry_wait return nil end if block_given? @main_th = Thread.new do yield end else @main_th = Thread.new do begin Thread.stop ensure @read_th.raise(Closed) if @read_th.status close(true) @client_th.raise(Closed) end end end begin open_conn rescue SystemCallError max_retry -= 1 if max_retry == 0 raise end sleep retry_wait retry end @main_th.join nil end def open_conn @conn.synchronize do @conn[0] = TCPSocket.new(@server, @port) end @conn[0].extend(MonitorMixin) @read_th.run ths = @threads.synchronize do @threads.keys end ths.each do |th| th.run if th.status && th.stop? end end private :open_conn =begin --- RICE::Connection#regist(raise_on_close, *args) {...} =end USER_THREAD = Struct.new('User_Thread', :q, :raise_on_close) def regist(raise_on_close = false, *args) read_q = Queue.new th = Thread.new(read_q, self, *args) do |read_q, conn, *args| yield(read_q, conn, *args) end @threads.synchronize do @threads[th] = USER_THREAD.new(read_q, raise_on_close) end th end =begin --- RICE::Connection#unregist(thread) =end def unregist(thread) th = nil @threads.synchronize do th = @threads.delete(th) end th.exit th end def read_thread(read_q, eol) begin read_q.clear Thread.stop begin conn = @conn[0] while l = conn.gets(eol) begin read_q.push(Message.parse(l)) rescue UnknownCommand $stderr.print l.inspect if $DEBUG rescue InvalidMessage begin read_q.push(Message.parse(l.sub(/\s*#{eol}\z/o, eol))) rescue $stderr.print l.inspect if $DEBUG end end end rescue IOError#, SystemCallError $stderr.print "#{self.inspect}: read_th get error #{$!}" if $DEBUG ensure raise Closed end rescue Closed begin @main_th.run rescue Closed end retry end end private :read_thread =begin --- RICE::Connection#close(restart = false) =end def close(restart = false) begin unless restart @main_th.exit if @main_th.alive? @read_th.exit if @read_th.alive? end conn = nil @conn.synchronize do conn = @conn.shift end conn.close if conn @threads.synchronize do @threads.each_key do |th| if restart if @threads[th].raise_on_close if @threads[th].raise_on_close.kind_of?(Exception) th.raise(@threads[th].raise_on_close) else th.raise(Closed) end end else th.exit end end end end end =begin --- RICE::Connection#alive? =end def alive? @main_th && @main_th.alive? end =begin --- RICE::Connection#push(message) =end def push(message) if @conn[0] @conn[0].synchronize do sleep(@delay) if @delay @conn[0].print message.to_s end else nil end end alias << push end # Connection =begin == RICE::Message =end class Message module PATTERN # 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' # 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})*" # servername = hostname SERVERNAME = HOSTNAME # nickname = ( letter / special ) *8( letter / digit / special / "-" ) NICKNAME = "[#{LETTER}#{SPECIAL}][-#{LETTER}#{DIGIT}#{SPECIAL}]{0,8}" # 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}#{CRLF}" CLIENT_PATTERN = /\A#{NICKNAME}(?:(?:!#{USER})?@#{HOST})\z/on MESSAGE_PATTERN = /\A#{MESSAGE}\z/on end # PATTERN =begin --- RICE::Message::parse(str) =end def self.parse(str) unless PATTERN::MESSAGE_PATTERN =~ str raise InvalidMessage, "Invalid message" else prefix = $1 command = $2 if $3 && $3.size > 0 middle = $3 trailer = $4 elsif $5 && $5.size > 0 middle = $5 trailer = $6 elsif $4 params = [] trailer = $4 elsif $6 params = [] trailer = $6 else params = [] end end params ||= middle.split(/ /)[1..-1] params << trailer if trailer self.build(prefix, command, params) end =begin --- RICE::Message::build(prefix, command, params) =end def self.build(prefix, command, params) if Command::Commands.include?(command) Command::Commands[command].new(prefix, command, params) elsif Reply::Replies.include?(command) Reply::Replies[command].new(prefix, command, params) else raise UnknownCommand, "unknown command: #{command}" end end =begin --- RICE::Message#prefix --- RICE::Message#command --- RICE::Message#params =end def initialize(prefix, command, params) @prefix = prefix @command = command @params = params end attr_reader :prefix, :command, :params =begin --- RICE::Message::#to_s =end def to_s str = '' if @prefix str << ':' str << @prefix str << ' ' end str << @command if @params f = false @params.each do |param| str << ' ' if !f && (param.size == 0 || /[: ]/ =~ param) str << ':' str << param f = true else str << param end end end str << "\x0D\x0A" str end =begin --- RICE::Message::#to_a =end def to_a [@prefix, @command, @params] end def inspect sprintf('#<%s:0x%x prefix:%s command:%s params:%s>', self.type, self.id, @prefix, @command, @params.inspect) end end # Message =begin == RICE::Command =end module Command class Command < Message end # Command Commands = {} %w(PASS NICK USER OPER MODE SERVICE QUIT SQUIT JOIN PART TOPIC NAMES LIST INVITE KICK PRIVMSG NOTICE MOTD LUSERS VERSION STATS LINKS TIME CONNECT TRACE ADMIN INFO SERVLIST SQUERY WHO WHOIS WHOWAS KILL PING PONG ERROR AWAY REHASH DIE RESTART SUMMON USERS WALLOPS USERHOST ISON ).each do |cmd| eval <