X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=lib%2Frbot%2Frfc2812.rb;h=9ca6571a85b374e1f9723b0f3cd590344ab7ff71;hb=9c50738a84bec26402902513a4cd21b54dcc0a80;hp=965da0a1f884fcb7b362c1540fe6c7ab77756166;hpb=519d8144025dc734c11aac05a8b5e61c488dfb8a;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/lib/rbot/rfc2812.rb b/lib/rbot/rfc2812.rb index 965da0a1..9ca6571a 100644 --- a/lib/rbot/rfc2812.rb +++ b/lib/rbot/rfc2812.rb @@ -815,10 +815,19 @@ module Irc # clients register handler proc{}s for different server events and IrcClient # handles dispatch class IrcClient + + attr_reader :server, :client + # create a new IrcClient instance def initialize + @server = Server.new # The Server + @client = @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 # key:: server event to handle @@ -827,8 +836,10 @@ module Irc # # ==server events currently supported: # - # created:: when the server was started + # 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 @@ -836,7 +847,6 @@ module Irc # 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 @@ -876,10 +886,27 @@ module Irc 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 =~ /^(?:\S+)(?:!\S+)?@(?:\S+)$/ + data[:source] = @server.user(prefix) + else + if @server.hostname + if @server.hostname != prefix + debug "Origin #{prefix} for message\n\t#{serverstring.inspect}\nis neither a user hostmask nor the server hostname, assuming it's a nick" + data[:source] = @server.user(prefix) + else + data[:source] = @server + end + else + @server.instance_variable_set(:@hostname, data[:source]) + data[:source] = @server + end end end @@ -894,13 +921,30 @@ module Irc when 'PONG' data[:pingid] = argv[0] handle(:pong, data) - when /^(\d+)$/ # numeric server message + when /^(\d+)$/ # numerical server message num=command.to_i case num + when RPL_WELCOME + # "Welcome to the Internet Relay Network + # !@" + case argv[1] + when /((\S+)!(\S+))/ + data[:netmask] = $1 + data[:nick] = $2 + data[:address] = $3 + @client = @server.user(data[:netmask]) + set = true + 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 + @client = @server.user(data[:nick]) unless set + handle(:welcome, data) when RPL_YOURHOST # "Your host is , running version " - # TODO how standard is this "version ? should i parse it? - data[:message] = argv[1] handle(:yourhost, data) when RPL_CREATED # "This server was created " @@ -909,10 +953,21 @@ module Irc when RPL_MYINFO # " # " - 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 # "* :Nickname is already in use" data[:nick] = argv[1] @@ -924,49 +979,71 @@ module Irc 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]) + data[:source] = @server.user(argv[2]) + data[:time] = Time.at(argv[3].to_i) + + if data[:channel] + data[:channel].topic.set_by = data[:nick] + 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 # "( "=" / "*" / "@" ) # :[ "@" / "+" ] *( " " [ "@" / "+" ] ) # - "@" 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 users and # services on servers" @@ -1005,22 +1082,6 @@ module Irc # (re)started)" data[:message] = argv[1] handle(:statsconn, data) - when RPL_WELCOME - # "Welcome to the Internet Relay Network - # !@" - 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 # " :- Message of the Day -" if argv[1] =~ /^-\s+(\S+)\s/ @@ -1046,57 +1107,182 @@ module Irc # 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. $). In any of these cases, we just use the + # String as a target + # FIXME we probably want to explicitly check for the # $ + 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 '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. $). In any of these cases, we just use the + # String as a target + # FIXME we probably want to explicitly check for the # $ + 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] = argv[0] - data[:target] = argv[1] + 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] == @client + @server.delete_channel(data[:channel]) + end + handle(:kick, data) when 'PART' - data[:channel] = argv[0] + data[:channel] = @server.channel(argv[0]) data[:message] = argv[1] + + @server.delete_user_from_channel(data[:source], data[:channel]) + if data[:source] == @client + @server.delete_channel(data[:channel]) + end + handle(:part, data) 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] + 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] + 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] + data[:target] = @server.user(argv[0]) + data[:channel] = @server.channel(argv[1]) + handle(:invite, data) when 'NICK' - data[:nick] = argv[0] + 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) + # 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 + warn "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 + warn "Unknown mode #{m} in #{serverstring.inspect}" + end + } + else + idx = who_wants_params.shift + if idx.nil? + warn "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 + warn "Unknown message #{serverstring.inspect}" handle(:unknown, data) end end