X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=lib%2Frbot%2Frfc2812.rb;h=0839d1d52593db514d4458a92c19b57c4c6c7b1a;hb=6cf365c49ce5fbe24c0a4ff0663550390b501fea;hp=819ea29152f0790329939e41c8490cf206dd20c6;hpb=257acdbfb3a4b0de7e3280e9f04d173e3016efbe;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/lib/rbot/rfc2812.rb b/lib/rbot/rfc2812.rb index 819ea291..0839d1d5 100644 --- a/lib/rbot/rfc2812.rb +++ b/lib/rbot/rfc2812.rb @@ -6,6 +6,9 @@ # This module defines the Irc::Client class, a class that can handle and # 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 # successful registration. @@ -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 @@ -961,6 +973,9 @@ module Irc # 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 @@ -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 @@ -1122,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 @@ -1142,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 @@ -1156,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| @@ -1186,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" @@ -1247,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 @@ -1265,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 @@ -1275,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) @@ -1285,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 @@ -1401,109 +1506,11 @@ 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[: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 - } - else - # 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_char do |c| - m = c.intern - case m - when :+ - setting = :set - when :- - setting = :reset - else - data[:modes] << [setting, m] - case m - when *@server.supports[:chanmodes][:typea] - who_wants_params << data[:modes].length - 1 - when *@server.supports[:chanmodes][:typeb] - who_wants_params << data[:modes].length - 1 - when *@server.supports[:chanmodes][:typec] - if setting == :set - who_wants_params << data[:modes].length - 1 - end - when *@server.supports[:chanmodes][:typed] - # Nothing to do - when *@server.supports[:prefix][:modes] - who_wants_params << data[:modes].length - 1 - else - warning "Unknown mode #{m} in #{serverstring.inspect}" - end - end - end - getting_args = true unless who_wants_params.empty? - end - end - - data[:modes].each { |mode| - set, key, val = mode - if val - data[:target].mode[key].send(set, val) - else - data[:target].mode[key].send(set) - end - } - end - + 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) @@ -1520,5 +1527,116 @@ module Irc @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] + who_wants_params << data[:modes].length - 1 + when *@server.supports[:chanmodes][:typeb] + who_wants_params << data[:modes].length - 1 + when *@server.supports[:chanmodes][:typec] + if setting == :set + who_wants_params << data[:modes].length - 1 + end + when *@server.supports[:chanmodes][:typed] + # Nothing to do + when *@server.supports[:prefix][:modes] + who_wants_params << data[:modes].length - 1 + else + warning "Ignoring unknown mode #{m} in #{serverstring.inspect}" + data[:modes].pop + end + end + 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| + set, key, val = mode + if val + data[:target].mode[key].send(set, val) + else + data[:target].mode[key].send(set) + end + } + else + warning "Ignoring #{data[:modestring]} for unrecognized target #{argv[0]} (#{data[:target].inspect})" + end + end end end