# :title: RFC 2821 Client Protocol module
#
# This module defines the Irc::Client class, a class that can handle and
-# dispatch messages based on RFC 2821i (Internet Relay Chat: Client Protocol)
+# dispatch messages based on RFC 2821 (Internet Relay Chat: Client Protocol)
module Irc
# - The server sends Replies 001 to 004 to a user upon
# "<channel> <mode> <mode params>"
RPL_CHANNELMODEIS=324
+ # "<channel> <unixtime>"
+ RPL_CREATIONTIME=329
+
+ # "<channel> <url>"
+ RPL_CHANNEL_URL=328
+
# "<channel> :No topic is set"
RPL_NOTOPIC=331
# Create a new Client instance
def initialize
@server = Server.new # The Server
- @user = @server.user("") # The User representing the client on this Server
+ @user = @server.user("*!*@*") # The User representing the client on this Server
@handlers = Hash.new
# This is used by some messages to build lists of users that
# will be delegated when the ENDOF... message is received
@tmpusers = []
+
+ # Same as above, just for bans
+ @tmpbans = []
end
# Clear the server and reset the user
def reset
@server.clear
- @user = @server.user("")
+ @user = @server.user("*!*@*")
end
# key:: server event to handle
#
# ==server events currently supported:
#
- # TODO handle errors ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL
# TODO handle errors ERR_CHANOPRIVSNEEDED, ERR_CANNOTSENDTOCHAN
#
# welcome:: server welcome message on connect
num=command.to_i
case num
when RPL_WELCOME
+ data[:message] = argv[1]
# "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+)/
+ if data[:message] =~ /([^@!\s]+)(?:!([^@!\s]+?))?@(\S+)/
nick = $1
user = $2
host = $3
data[:message] = argv[2]
handle(:badnick, data)
when RPL_TOPIC
- data[:channel] = @server.get_channel(argv[1])
+ data[:channel] = @server.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
+ data[:channel].topic.text = data[:topic]
handle(:topic, data)
when RPL_TOPIC_INFO
data[:nick] = @server.user(argv[0])
- data[:channel] = @server.get_channel(argv[1])
+ data[:channel] = @server.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[: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
+ data[:channel].topic.set_by = data[:source]
+ data[:channel].topic.set_on = data[:time]
handle(:topicinfo, data)
when RPL_NAMREPLY
# - "@" 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
+ data[:channel] = chan = @server.channel(argv[2])
users = []
argv[3].scan(/\S+/).each { |u|
}
@tmpusers += users
when RPL_ENDOFNAMES
- data[:channel] = argv[1]
+ data[:channel] = @server.channel(argv[1])
data[:users] = @tmpusers
handle(:names, data)
@tmpusers = Array.new
+ when RPL_BANLIST
+ data[:channel] = @server.channel(argv[1])
+ data[:mask] = argv[2]
+ data[:by] = argv[3]
+ data[:at] = argv[4]
+ @tmpbans << data
+ when RPL_ENDOFBANLIST
+ data[:channel] = @server.channel(argv[1])
+ data[:bans] = @tmpbans
+ handle(:banlist, data)
+ @tmpbans = Array.new
when RPL_LUSERCLIENT
# ":There are <integer> users and <integer>
# services on <integer> servers"
when RPL_DATASTR
data[:text] = argv[1]
handle(:datastr, data)
+ when RPL_AWAY
+ data[:nick] = user = @server.user(argv[1])
+ data[:message] = argv[-1]
+ user.away = data[:message]
+ handle(:away, data)
when RPL_WHOREPLY
- data[:channel] = argv[1]
+ data[:channel] = channel = @server.channel(argv[1])
data[:user] = argv[2]
data[:host] = argv[3]
data[:userserver] = argv[4]
- data[:nick] = argv[5]
+ data[:nick] = user = @server.user(argv[5])
if argv[6] =~ /^(H|G)(\*)?(.*)?$/
data[:away] = ($1 == 'G')
data[:ircop] = $2
end
data[:hopcount], data[:real_name] = argv[7].split(" ", 2)
- user = @server.get_user(data[:nick])
-
user.user = data[:user]
user.host = data[:host]
user.away = data[:away] # FIXME doesn't provide the actual message
# TODO hopcount
user.real_name = data[:real_name]
- channel = @server.get_channel(data[:channel])
-
channel.add_user(user, :silent=>true)
data[:modes].map { |mode|
channel.mode[mode].set(user)
handle(:who, data)
when RPL_ENDOFWHO
handle(:eowho, data)
+ when RPL_WHOISUSER
+ @whois ||= Hash.new
+ @whois[:nick] = argv[1]
+ @whois[:user] = argv[2]
+ @whois[:host] = argv[3]
+ @whois[:real_name] = argv[-1]
+
+ user = @server.user(@whois[:nick])
+ user.user = @whois[:user]
+ user.host = @whois[:host]
+ user.real_name = @whois[:real_name]
+ when RPL_WHOISSERVER
+ @whois ||= Hash.new
+ @whois[:nick] = argv[1]
+ @whois[:server] = argv[2]
+ @whois[:server_info] = argv[-1]
+ # TODO update user info
+ when RPL_WHOISOPERATOR
+ @whois ||= Hash.new
+ @whois[:nick] = argv[1]
+ @whois[:operator] = argv[-1]
+ # TODO update user info
+ when RPL_WHOISIDLE
+ @whois ||= Hash.new
+ @whois[:nick] = argv[1]
+ user = @server.user(@whois[:nick])
+ @whois[:idle] = argv[2].to_i
+ user.idle_since = Time.now - @whois[:idle]
+ if argv[-1] == 'seconds idle, signon time'
+ @whois[:signon] = Time.at(argv[3].to_i)
+ user.signon = @whois[:signon]
+ end
+ when RPL_ENDOFWHOIS
+ @whois ||= Hash.new
+ @whois[:nick] = argv[1]
+ data[:whois] = @whois.dup
+ @whois.clear
+ handle(:whois, data)
+ when RPL_WHOISCHANNELS
+ @whois ||= Hash.new
+ @whois[:nick] = argv[1]
+ @whois[:channels] ||= []
+ user = @server.user(@whois[:nick])
+ argv[-1].split.each do |prechan|
+ pfx = prechan.scan(/[#{@server.supports[:prefix][:prefixes].join}]/)
+ modes = pfx.map { |p| @server.mode_for_prefix p }
+ chan = prechan[pfx.length..prechan.length]
+
+ channel = @server.channel(chan)
+ channel.add_user(user, :silent => true)
+ modes.map { |mode| channel.mode[mode].set(user) }
+
+ @whois[:channels] << [chan, modes]
+ end
+ when RPL_CHANNELMODEIS
+ parse_mode(serverstring, argv[1..-1], data)
+ handle(:mode, data)
+ when RPL_CREATIONTIME
+ data[:channel] = @server.channel(argv[1])
+ data[:time] = Time.at(argv[2].to_i)
+ data[:channel].creation_time=data[:time]
+ handle(:creationtime, data)
+ when RPL_CHANNEL_URL
+ data[:channel] = @server.channel(argv[1])
+ data[:url] = argv[2]
+ data[:channel].url=data[:url].dup
+ handle(:channel_url, data)
+ when ERR_NOSUCHNICK
+ data[:target] = argv[1]
+ data[:message] = argv[2]
+ handle(:nosuchtarget, data)
+ if user = @server.get_user(data[:target])
+ @server.delete_user(user)
+ end
+ when ERR_NOSUCHCHANNEL
+ data[:target] = argv[1]
+ data[:message] = argv[2]
+ handle(:nosuchtarget, data)
+ if channel = @server.get_channel(data[:target])
+ @server.delete_channel(channel)
+ end
else
+ warning "Unknown message #{serverstring.inspect}"
handle(:unknown, data)
end
return # We've processed the numeric reply
handle(:nick, 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
- # 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
+ parse_mode(serverstring, argv, data)
+ handle(:mode, data)
+ when :ERROR
+ data[:message] = argv[1]
+ handle(:error, data)
+ else
+ warning "Unknown message #{serverstring.inspect}"
+ handle(:unknown, data)
+ end
+ end
+
+ private
+
+ # key:: server event name
+ # data:: hash containing data about the event, passed to the proc
+ # call client's proc for an event, if they set one as a handler
+ def handle(key, data)
+ if(@handlers.has_key?(key))
+ @handlers[key].call(data)
+ end
+ end
+
+ # RPL_CHANNELMODEIS
+ # 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
+ def parse_mode(serverstring, argv, data)
+ data[:target] = @server.user_or_channel(argv[0])
+ data[:modestring] = argv[1..-1].join(" ")
+ # data[:modes] is an array where each element
+ # is an array with two elements, the first of which
+ # is either :set or :reset, and the second symbol
+ # is the mode letter. An optional third element
+ # is present e.g. for channel modes that need
+ # a parameter
+ data[:modes] = []
+ case data[:target]
+ when User
+ # User modes aren't currently handled internally,
+ # but we still parse them and delegate to the client
+ warning "Unhandled user mode message '#{serverstring}'"
+ argv[1..-1].each { |arg|
+ setting = arg[0].chr
+ if "+-".include?(setting)
+ setting = setting == "+" ? :set : :reset
+ arg[1..-1].each_byte { |b|
+ m = b.chr.intern
+ data[:modes] << [setting, m]
+ }
+ else
+ # Although typically User modes don't take an argument,
+ # this is not true for all modes on all servers. Since
+ # we have no knowledge of which modes take parameters
+ # and which don't we just assign it to the last
+ # mode. This is not going to do strange things often,
+ # as usually User modes are only set one at a time
+ warning "Unhandled user mode parameter #{arg} found"
+ data[:modes].last << arg
+ end
+ }
+ when Channel
+ # array of indices in data[:modes] where parameters
+ # are needed
+ who_wants_params = []
+
+ modes = argv[1..-1].dup
+ debug modes
+ getting_args = false
+ while arg = modes.shift
+ debug arg
+ if getting_args
+ # getting args for previously set modes
+ idx = who_wants_params.shift
+ if idx.nil?
+ warning "Oops, problems parsing #{serverstring.inspect}"
+ break
+ end
+ data[:modes][idx] << arg
+ getting_args = false if who_wants_params.empty?
+ else
+ debug @server.supports[:chanmodes]
+ setting = :set
+ arg.each_byte do |c|
+ m = c.chr.intern
+ case m
+ when :+
+ setting = :set
+ when :-
+ setting = :reset
+ else
+ data[:modes] << [setting, m]
+ case m
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]
+ if setting == :set
who_wants_params << data[:modes].length - 1
- else
- data[:modes] << setting + m
end
when *@server.supports[:chanmodes][:typed]
- data[:modes] << setting + m
+ # Nothing to do
when *@server.supports[:prefix][:modes]
- data[:modes] << [setting + m]
who_wants_params << data[:modes].length - 1
else
- warning "Unknown mode #{m} in #{serverstring.inspect}"
+ warning "Ignoring unknown mode #{m} in #{serverstring.inspect}"
+ data[:modes].pop
end
- }
- else
- idx = who_wants_params.shift
- if idx.nil?
- warning "Oops, problems parsing #{serverstring.inspect}"
- break
end
- data[:modes][idx] << arg
end
- }
+ getting_args = true unless who_wants_params.empty?
+ end
+ end
+ unless who_wants_params.empty?
+ warning "Unhandled malformed modeline #{data[:modestring]} (unexpected empty arguments)"
+ return
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)
+ set, key, val = mode
+ if val
+ data[:target].mode[key].send(set, val)
else
- set = mode[0].chr == "+" ? :set : :reset
- key = mode[1].chr.to_sym
- data[:channel].mode[key].send(set)
+ data[:target].mode[key].send(set)
end
- } if data[:modes]
-
- handle(:mode, data)
+ }
else
- warning "Unknown message #{serverstring.inspect}"
- handle(:unknown, data)
- end
- end
-
- private
-
- # key:: server event name
- # data:: hash containing data about the event, passed to the proc
- # call client's proc for an event, if they set one as a handler
- def handle(key, data)
- if(@handlers.has_key?(key))
- @handlers[key].call(data)
+ warning "Ignoring #{data[:modestring]} for unrecognized target #{argv[0]} (#{data[:target].inspect})"
end
end
end