RPL_DATASTR=290
# implements RFC 2812 and prior IRC RFCs.
- # clients register handler proc{}s for different server events and IrcClient
+ # clients register handler proc{}s for different server events and Client
# handles dispatch
- class IrcClient
- # create a new IrcClient instance
+ class Client
+
+ attr_reader :server, :user
+
+ # create a new Client instance
def initialize
+ @server = Server.new # The Server
+ @user = @server.user("") # The User representing the client on this Server
+
@handlers = Hash.new
- @users = Array.new
+
+ # This is used by some messages to build lists of users that
+ # will be delegated when the ENDOF... message is received
+ @tmpusers = []
+ end
+
+ # clear the server and reset the User
+ def reset
+ @server.clear
+ @user = @server.user("")
end
# key:: server event to handle
#
# ==server events currently supported:
#
- # created:: when the server was started
+ # TODO handle errors ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL
+ # TODO handle errors ERR_CHANOPRIVSNEEDED, ERR_CANNOTSENDTOCHAN
+ #
+ # welcome:: server welcome message on connect
# yourhost:: your host details (on connection)
+ # created:: when the server was started
+ # isupport:: information about what this server supports
# ping:: server pings you (default handler returns a pong)
# nicktaken:: you tried to change nick to one that's in use
# badnick:: you tried to change nick to one that's invalid
# topicinfo:: on joining a channel or asking for the topic, tells you
# who set it and when
# names:: server sends list of channel members when you join
- # welcome:: server welcome message on connect
# motd:: server message of the day
# privmsg:: privmsg, the core of IRC, a message to you from someone
# public:: optionally instead of getting privmsg you can hook to only
data = Hash.new
data[:serverstring] = serverstring
- unless serverstring =~ /^(:(\S+)\s)?(\S+)(\s(.*))?/
- raise "Unparseable Server Message!!!: #{serverstring}"
+ unless serverstring.chomp =~ /^(:(\S+)\s)?(\S+)(\s(.*))?$/
+ raise "Unparseable Server Message!!!: #{serverstring.inspect}"
end
prefix, command, params = $2, $3, $5
if prefix != nil
- data[:source] = prefix
- if prefix =~ /^(\S+)!(\S+)$/
- data[:sourcenick] = $1
- data[:sourceaddress] = $2
+ # Most servers will send a full nick!user@host prefix for
+ # messages from users. Therefore, when the prefix doesn't match this
+ # syntax it's usually the server hostname.
+ #
+ # This is not always true, though, since some servers do not send a
+ # full hostmask for user messages.
+ #
+ if prefix =~ /^#{Regexp::Irc::BANG_AT}$/
+ data[:source] = @server.user(prefix)
+ else
+ if @server.hostname
+ if @server.hostname != prefix
+ # TODO do we want to be able to differentiate messages that are passed on to us from /other/ servers?
+ debug "Origin #{prefix} for message\n\t#{serverstring.inspect}\nis neither a user hostmask nor the server hostname\nI'll pretend that it's from the server anyway"
+ data[:source] = @server
+ else
+ data[:source] = @server
+ end
+ else
+ @server.instance_variable_set(:@hostname, prefix)
+ data[:source] = @server
+ end
end
end
argv = []
params.scan(/(?!:)(\S+)|:(.*)/) { argv << ($1 || $2) } if params
- case command
- when 'PING'
- data[:pingid] = argv[0]
- handle(:ping, data)
- when 'PONG'
- data[:pingid] = argv[0]
- handle(:pong, data)
- when /^(\d+)$/ # numeric server message
+ if command =~ /^(\d+)$/ # Numeric replies
+ data[:target] = argv[0]
+ # A numeric reply /should/ be directed at the client, except when we're connecting with a used nick, in which case
+ # it's directed at '*'
+ not_us = !([@user.nick, '*'].include?(data[:target]))
+ if not_us
+ warning "Server reply #{serverstring.inspect} directed at #{data[:target]} instead of client (#{@user.nick})"
+ end
+
num=command.to_i
case num
+ when RPL_WELCOME
+ # "Welcome to the Internet Relay Network
+ # <nick>!<user>@<host>"
+ if not_us
+ warning "Server thinks client (#{@user.inspect}) has a different nick"
+ @user.nick = data[:target]
+ end
+ if argv[1] =~ /([^@!\s]+)(?:!([^@!\s]+?))?@(\S+)/
+ nick = $1
+ user = $2
+ host = $3
+ warning "Welcome message nick mismatch (#{nick} vs #{data[:target]})" if nick != data[:target]
+ @user.user = user if user
+ @user.host = host if host
+ end
+ handle(:welcome, data)
when RPL_YOURHOST
# "Your host is <servername>, running version <ver>"
- # TODO how standard is this "version <ver>? should i parse it?
data[:message] = argv[1]
handle(:yourhost, data)
when RPL_CREATED
when RPL_MYINFO
# "<servername> <version> <available user modes>
# <available channel modes>"
- data[:servername] = argv[1]
- data[:version] = argv[2]
- data[:usermodes] = argv[3]
- data[:chanmodes] = argv[4]
+ @server.parse_my_info(params.split(' ', 2).last)
+ data[:servername] = @server.hostname
+ data[:version] = @server.version
+ data[:usermodes] = @server.usermodes
+ data[:chanmodes] = @server.chanmodes
+ handle(:myinfo, data)
+ when RPL_ISUPPORT
+ # "PREFIX=(ov)@+ CHANTYPES=#& :are supported by this server"
+ # "MODES=4 CHANLIMIT=#:20 NICKLEN=16 USERLEN=10 HOSTLEN=63
+ # TOPICLEN=450 KICKLEN=450 CHANNELLEN=30 KEYLEN=23 CHANTYPES=#
+ # PREFIX=(ov)@+ CASEMAPPING=ascii CAPAB IRCD=dancer :are available
+ # on this server"
+ #
+ @server.parse_isupport(argv[1..-2].join(' '))
+ handle(:isupport, data)
when ERR_NICKNAMEINUSE
# "* <nick> :Nickname is already in use"
data[:nick] = argv[1]
data[:message] = argv[2]
handle(:badnick, data)
when RPL_TOPIC
- data[:channel] = argv[1]
+ data[:channel] = @server.get_channel(argv[1])
data[:topic] = argv[2]
+
+ if data[:channel]
+ data[:channel].topic.text = data[:topic]
+ else
+ warning "Received topic #{data[:topic].inspect} for channel #{data[:channel].inspect} I was not on"
+ end
+
handle(:topic, data)
when RPL_TOPIC_INFO
- data[:nick] = argv[0]
- data[:channel] = argv[1]
- data[:source] = argv[2]
- data[:unixtime] = argv[3]
+ data[:nick] = @server.user(argv[0])
+ data[:channel] = @server.get_channel(argv[1])
+
+ # This must not be an IRC::User because it might not be an actual User,
+ # and we risk overwriting valid User data
+ data[:source] = argv[2].to_irc_netmask(:server => @server)
+
+ data[:time] = Time.at(argv[3].to_i)
+
+ if data[:channel]
+ data[:channel].topic.set_by = data[:source]
+ data[:channel].topic.set_on = data[:time]
+ else
+ warning "Received topic #{data[:topic].inspect} for channel #{data[:channel].inspect} I was not on"
+ end
+
handle(:topicinfo, data)
when RPL_NAMREPLY
# "( "=" / "*" / "@" ) <channel>
# :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> )
# - "@" is used for secret channels, "*" for private
# channels, and "=" for others (public channels).
+ data[:channeltype] = argv[1]
+ data[:channel] = argv[2]
+
+ chan = @server.get_channel(data[:channel])
+ unless chan
+ warning "Received names #{data[:topic].inspect} for channel #{data[:channel].inspect} I was not on"
+ return
+ end
+
+ users = []
argv[3].scan(/\S+/).each { |u|
- if(u =~ /^([@+])?(.*)$/)
- umode = $1 || ""
+ # FIXME beware of servers that allow multiple prefixes
+ if(u =~ /^([#{@server.supports[:prefix][:prefixes].join}])?(.*)$/)
+ umode = $1
user = $2
- @users << [user, umode]
+ users << [user, umode]
end
}
+
+ users.each { |ar|
+ u = @server.user(ar[0])
+ chan.add_user(u, :silent => true)
+ debug "Adding user #{u}"
+ if ar[1]
+ m = @server.supports[:prefix][:prefixes].index(ar[1].to_sym)
+ ms = @server.supports[:prefix][:modes][m]
+ debug "\twith mode #{ar[1]} (#{ms})"
+ chan.mode[ms].set(u)
+ end
+ }
+ @tmpusers += users
when RPL_ENDOFNAMES
data[:channel] = argv[1]
- data[:users] = @users
+ data[:users] = @tmpusers
handle(:names, data)
- @users = Array.new
- when RPL_ISUPPORT
- # "PREFIX=(ov)@+ CHANTYPES=#& :are supported by this server"
- # "MODES=4 CHANLIMIT=#:20 NICKLEN=16 USERLEN=10 HOSTLEN=63
- # TOPICLEN=450 KICKLEN=450 CHANNELLEN=30 KEYLEN=23 CHANTYPES=#
- # PREFIX=(ov)@+ CASEMAPPING=ascii CAPAB IRCD=dancer :are available
- # on this server"
- #
- argv[0,argv.length-1].each {|a|
- if a =~ /^(.*)=(.*)$/
- data[$1.downcase.to_sym] = $2
- debug "server's #{$1.downcase.to_sym} is #{$2}"
- else
- data[a.downcase.to_sym] = true
- debug "server supports #{a.downcase.to_sym}"
- end
- }
- handle(:isupport, data)
+ @tmpusers = Array.new
when RPL_LUSERCLIENT
# ":There are <integer> users and <integer>
# services on <integer> servers"
# (re)started)"
data[:message] = argv[1]
handle(:statsconn, data)
- when RPL_WELCOME
- # "Welcome to the Internet Relay Network
- # <nick>!<user>@<host>"
- case argv[1]
- when /((\S+)!(\S+))/
- data[:netmask] = $1
- data[:nick] = $2
- data[:address] = $3
- when /Welcome to the Internet Relay Network\s(\S+)/
- data[:nick] = $1
- when /Welcome.*\s+(\S+)$/
- data[:nick] = $1
- when /^(\S+)$/
- data[:nick] = $1
- end
- handle(:welcome, data)
when RPL_MOTDSTART
# "<nick> :- <server> Message of the Day -"
if argv[1] =~ /^-\s+(\S+)\s/
else
handle(:unknown, data)
end
- # end of numeric replies
- when 'PRIVMSG'
+ return # We've processed the numeric reply
+ end
+
+ # Otherwise, the command should be a single word
+ case command.to_sym
+ when :PING
+ data[:pingid] = argv[0]
+ handle(:ping, data)
+ when :PONG
+ data[:pingid] = argv[0]
+ handle(:pong, data)
+ when :PRIVMSG
# you can either bind to 'PRIVMSG', to get every one and
# parse it yourself, or you can bind to 'MSG', 'PUBLIC',
# etc and get it all nicely split up for you.
- data[:target] = argv[0]
+
+ begin
+ data[:target] = @server.user_or_channel(argv[0])
+ rescue
+ # The previous may fail e.g. when the target is a server or something
+ # like that (e.g. $<mask>). In any of these cases, we just use the
+ # String as a target
+ # FIXME we probably want to explicitly check for the #<mask> $<mask>
+ data[:target] = argv[0]
+ end
data[:message] = argv[1]
handle(:privmsg, data)
# Now we split it
- if(data[:target] =~ /^[#&!+].*/)
+ if data[:target].kind_of?(Channel)
handle(:public, data)
else
handle(:msg, data)
end
- when 'KICK'
- data[:channel] = argv[0]
- data[:target] = argv[1]
+ when :NOTICE
+ begin
+ data[:target] = @server.user_or_channel(argv[0])
+ rescue
+ # The previous may fail e.g. when the target is a server or something
+ # like that (e.g. $<mask>). In any of these cases, we just use the
+ # String as a target
+ # FIXME we probably want to explicitly check for the #<mask> $<mask>
+ data[:target] = argv[0]
+ end
+ data[:message] = argv[1]
+ case data[:source]
+ when User
+ handle(:notice, data)
+ else
+ # "server notice" (not from user, noone to reply to)
+ handle(:snotice, data)
+ end
+ when :KICK
+ data[:channel] = @server.channel(argv[0])
+ data[:target] = @server.user(argv[1])
data[:message] = argv[2]
+
+ @server.delete_user_from_channel(data[:target], data[:channel])
+ if data[:target] == @user
+ @server.delete_channel(data[:channel])
+ end
+
handle(:kick, data)
- when 'PART'
- data[:channel] = argv[0]
+ when :PART
+ data[:channel] = @server.channel(argv[0])
data[:message] = argv[1]
+
+ @server.delete_user_from_channel(data[:source], data[:channel])
+ if data[:source] == @user
+ @server.delete_channel(data[:channel])
+ end
+
handle(:part, data)
- when 'QUIT'
+ when :QUIT
data[:message] = argv[0]
+ data[:was_on] = @server.channels.inject(ChannelList.new) { |list, ch|
+ list << ch if ch.has_user?(data[:source])
+ list
+ }
+
+ @server.delete_user(data[:source])
+
handle(:quit, data)
- when 'JOIN'
- data[:channel] = argv[0]
+ when :JOIN
+ data[:channel] = @server.channel(argv[0])
+ data[:channel].add_user(data[:source])
+
handle(:join, data)
- when 'TOPIC'
- data[:channel] = argv[0]
- data[:topic] = argv[1]
+ when :TOPIC
+ data[:channel] = @server.channel(argv[0])
+ data[:topic] = Channel::Topic.new(argv[1], data[:source], Time.new)
+ data[:channel].topic.replace(data[:topic])
+
handle(:changetopic, data)
- when 'INVITE'
- data[:target] = argv[0]
- data[:channel] = argv[1]
+ when :INVITE
+ data[:target] = @server.user(argv[0])
+ data[:channel] = @server.channel(argv[1])
+
handle(:invite, data)
- when 'NICK'
- data[:nick] = argv[0]
+ when :NICK
+ data[:is_on] = @server.channels.inject(ChannelList.new) { |list, ch|
+ list << ch if ch.has_user?(data[:source])
+ list
+ }
+
+ data[:newnick] = argv[0]
+ data[:oldnick] = data[:source].nick.dup
+ data[:source].nick = data[:newnick]
+
+ debug "#{data[:oldnick]} (now #{data[:newnick]}) was on #{data[:is_on].join(', ')}"
+
handle(:nick, data)
- when 'MODE'
- data[:channel] = argv[0]
- data[:modestring] = argv[1]
- data[:targets] = argv[2]
- handle(:mode, data)
- when 'NOTICE'
- data[:target] = argv[0]
- data[:message] = argv[1]
- if data[:sourcenick]
- handle(:notice, data)
+ when :MODE
+ # MODE ([+-]<modes> (<params>)*)*
+ # When a MODE message is received by a server,
+ # Type C will have parameters too, so we must
+ # be able to consume parameters for all
+ # but Type D modes
+
+ data[:channel] = @server.user_or_channel(argv[0])
+ data[:modestring] = argv[1..-1].join(" ")
+ case data[:channel]
+ when User
+ # TODO
+ warning "Unhandled user mode message '#{serverstring}'"
else
- # "server notice" (not from user, noone to reply to
- handle(:snotice, data)
+ # data[:modes] is an array where each element
+ # is either a flag which doesn't need parameters
+ # or an array with a flag which needs parameters
+ # and the corresponding parameter
+ data[:modes] = []
+ # array of indices in data[:modes] where parameters
+ # are needed
+ who_wants_params = []
+
+ argv[1..-1].each { |arg|
+ setting = arg[0].chr
+ if "+-".include?(setting)
+ arg[1..-1].each_byte { |b|
+ m = b.chr
+ case m.to_sym
+ when *@server.supports[:chanmodes][:typea]
+ data[:modes] << [setting + m]
+ who_wants_params << data[:modes].length - 1
+ when *@server.supports[:chanmodes][:typeb]
+ data[:modes] << [setting + m]
+ who_wants_params << data[:modes].length - 1
+ when *@server.supports[:chanmodes][:typec]
+ if setting == "+"
+ data[:modes] << [setting + m]
+ who_wants_params << data[:modes].length - 1
+ else
+ data[:modes] << setting + m
+ end
+ when *@server.supports[:chanmodes][:typed]
+ data[:modes] << setting + m
+ when *@server.supports[:prefix][:modes]
+ data[:modes] << [setting + m]
+ who_wants_params << data[:modes].length - 1
+ else
+ warning "Unknown mode #{m} in #{serverstring.inspect}"
+ end
+ }
+ else
+ idx = who_wants_params.shift
+ if idx.nil?
+ warning "Oops, problems parsing #{serverstring.inspect}"
+ break
+ end
+ data[:modes][idx] << arg
+ end
+ }
end
+
+ data[:modes].each { |mode|
+ case mode
+ when Array
+ set = mode[0][0].chr == "+" ? :set : :reset
+ key = mode[0][1].chr.to_sym
+ val = mode[1]
+ data[:channel].mode[key].send(set, val)
+ else
+ set = mode[0].chr == "+" ? :set : :reset
+ key = mode[1].chr.to_sym
+ data[:channel].mode[key].send(set)
+ end
+ } if data[:modes]
+
+ handle(:mode, data)
else
+ warning "Unknown message #{serverstring.inspect}"
handle(:unknown, data)
end
end