X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=lib%2Frbot%2Frfc2812.rb;h=0839d1d52593db514d4458a92c19b57c4c6c7b1a;hb=8218e3f05e8ccd95497dd3c7aa115cfde8b01a40;hp=d4f1e46661f7104b51f7630cd470eb78a38ef374;hpb=4d193a6c8719351147faad15e9a91d391df94952;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/lib/rbot/rfc2812.rb b/lib/rbot/rfc2812.rb index d4f1e466..0839d1d5 100644 --- a/lib/rbot/rfc2812.rb +++ b/lib/rbot/rfc2812.rb @@ -4,7 +4,10 @@ # :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) + +class ServerMessageParseError < ServerError +end module Irc # - The server sends Replies 001 to 004 to a user upon @@ -144,6 +147,12 @@ module Irc # " " RPL_CHANNELMODEIS=324 + # " " + RPL_CREATIONTIME=329 + + # " " + RPL_CHANNEL_URL=328 + # " :No topic is set" RPL_NOTOPIC=331 @@ -940,6 +949,9 @@ module Irc ERR_NOSERVICEHOST=492 RPL_DATASTR=290 + # A structure to hold LIST data, in the Irc namespace + ListData = Struct.new :channel, :users, :topic + # Implements RFC 2812 and prior IRC RFCs. # # Clients should register Proc{}s to handle the various server events, and @@ -954,19 +966,22 @@ module Irc # 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 @@ -975,7 +990,6 @@ module Irc # # ==server events currently supported: # - # TODO handle errors ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL # TODO handle errors ERR_CHANOPRIVSNEEDED, ERR_CANNOTSENDTOCHAN # # welcome:: server welcome message on connect @@ -1022,7 +1036,7 @@ module Irc data[:serverstring] = serverstring unless serverstring.chomp =~ /^(:(\S+)\s)?(\S+)(\s(.*))?$/ - raise "Unparseable Server Message!!!: #{serverstring.inspect}" + raise ServerMessageParseError, (serverstring.chomp rescue serverstring) end prefix, command, params = $2, $3, $5 @@ -1069,13 +1083,14 @@ module Irc num=command.to_i case num when RPL_WELCOME + data[:message] = argv[1] # "Welcome to the Internet Relay Network # !@" 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 @@ -1121,19 +1136,14 @@ module Irc 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 @@ -1141,12 +1151,8 @@ module Irc 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 @@ -1155,13 +1161,7 @@ module Irc # - "@" 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| @@ -1185,10 +1185,21 @@ module Irc } @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 users and # services on servers" @@ -1246,12 +1257,17 @@ module Irc 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 @@ -1264,8 +1280,6 @@ module Irc 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 @@ -1274,8 +1288,6 @@ module Irc # 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) @@ -1284,7 +1296,101 @@ module Irc 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_LISTSTART + # ignore + when RPL_LIST + @list ||= Hash.new + chan = argv[1] + users = argv[2] + topic = argv[3] + @list[chan] = ListData.new(chan, users, topic) + when RPL_LISTEND + @list ||= Hash.new + data[:list] = @list + handle(:list, data) + 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 @@ -1400,96 +1506,136 @@ module Irc handle(:nick, data) when :MODE - # MODE ([+-] ()*)* - # 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 ([+-] ()*)* + # 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