From 2a27c12fffa359898c5601a211fe19425da82fa6 Mon Sep 17 00:00:00 2001 From: Giuseppe Bilotta Date: Mon, 31 Jul 2006 15:33:15 +0000 Subject: [PATCH] First shot at the new Irc framework. Bot is usable (sort of), but not all functionality may work as expected (or at all). If you are testing it, please report. Auth is known to be nonfunctional --- Rakefile | 2 +- bin/rbot | 2 +- data/rbot/plugins/nickserv.rb | 4 +- lib/rbot/channel.rb | 54 ---- lib/rbot/irc.rb | 463 ++++++++++++++++++++++++---------- lib/rbot/ircbot.rb | 301 +++++++++++----------- lib/rbot/message.rb | 90 ++++--- lib/rbot/rfc2812.rb | 302 ++++++++++++++++------ 8 files changed, 757 insertions(+), 461 deletions(-) delete mode 100644 lib/rbot/channel.rb diff --git a/Rakefile b/Rakefile index 00355b06..6ad7f1bf 100644 --- a/Rakefile +++ b/Rakefile @@ -6,7 +6,7 @@ task :default => [:repackage] spec = Gem::Specification.new do |s| s.name = 'rbot' - s.version = '0.9.10' + s.version = '0.9.11' s.summary = <<-EOF A modular ruby IRC bot. EOF diff --git a/bin/rbot b/bin/rbot index 45dba848..8921eeb8 100755 --- a/bin/rbot +++ b/bin/rbot @@ -29,7 +29,7 @@ require 'etc' require 'getoptlong' require 'fileutils' -$version="0.9.10-svn" +$version="0.9.11-svn" $opts = Hash.new orig_opts = ARGV.dup diff --git a/data/rbot/plugins/nickserv.rb b/data/rbot/plugins/nickserv.rb index 9ff79f08..a5280b1f 100644 --- a/data/rbot/plugins/nickserv.rb +++ b/data/rbot/plugins/nickserv.rb @@ -7,8 +7,8 @@ class NickServPlugin < Plugin BotConfig.register BotConfigStringValue.new('nickserv.name', - :default => "NickServ", :requires_restart => false, - :desc => "Name of the nick server") + :default => "nickserv", :requires_restart => false, + :desc => "Name of the nick server (all lowercase)") BotConfig.register BotConfigStringValue.new('nickserv.ident_request', :default => "IDENTIFY", :requires_restart => false, :on_change => Proc.new { |bot, v| bot.plugins.delegate "set_ident_request", v }, diff --git a/lib/rbot/channel.rb b/lib/rbot/channel.rb deleted file mode 100644 index 34804c19..00000000 --- a/lib/rbot/channel.rb +++ /dev/null @@ -1,54 +0,0 @@ -module Irc - - # class to store IRC channel data (users, topic, per-channel configurations) - class IRCChannel - # name of channel - attr_reader :name - - # current channel topic - attr_reader :topic - - # hash containing users currently in the channel - attr_accessor :users - - # if true, bot won't talk in this channel - attr_accessor :quiet - - # name:: channel name - # create a new IRCChannel - def initialize(name) - @name = name - @users = Hash.new - @quiet = false - @topic = Topic.new - end - - # eg @bot.channels[chan].topic = topic - def topic=(name) - @topic.name = name - end - - # class to store IRC channel topic information - class Topic - # topic name - attr_accessor :name - - # timestamp - attr_accessor :timestamp - - # topic set by - attr_accessor :by - - def initialize - @name = "" - end - - # when called like "puts @bots.channels[chan].topic" - def to_s - @name - end - end - - end - -end diff --git a/lib/rbot/irc.rb b/lib/rbot/irc.rb index 31c4953e..d5621b0f 100644 --- a/lib/rbot/irc.rb +++ b/lib/rbot/irc.rb @@ -1,9 +1,9 @@ #-- vim:sw=2:et # General TODO list -# * when Users are deleted, we have to delete them from the appropriate -# channel lists too # * do we want to handle a Channel list for each User telling which # Channels is the User on (of those the client is on too)? +# We may want this so that when a User leaves all Channels and he hasn't +# sent us privmsgs, we know remove him from the Server @users list #++ # :title: IRC module # @@ -274,7 +274,13 @@ module Irc @user = str[:user].to_s @host = str[:host].to_s when String - if str.match(/(\S+)(?:!(\S+)@(?:(\S+))?)?/) + case str + when "" + @casemap = casemap || 'rfc1459' + @nick = nil + @user = nil + @host = nil + when /(\S+)(?:!(\S+)@(?:(\S+))?)?/ @casemap = casemap || 'rfc1459' @nick = $1.irc_downcase(@casemap) @user = $2 @@ -325,9 +331,10 @@ module Irc # A Netmask is easily converted to a String for the usual representation # - def to_s + def fullform return "#{nick}@#{user}!#{host}" end + alias :to_s :fullform # This method is used to match the current Netmask against another one # @@ -382,23 +389,73 @@ module Irc # An IRC User is identified by his/her Netmask (which must not have # globs). In fact, User is just a subclass of Netmask. However, - # a User will not allow one's host or user data to be changed: only the - # nick can be dynamic + # a User will not allow one's host or user data to be changed. + # + # Due to the idiosincrasies of the IRC protocol, we allow + # the creation of a user with an unknown mask represented by the + # glob pattern *@*. Only in this case they may be set. # # TODO list: # * see if it's worth to add the other USER data - # * see if it's worth to add AWAY status # * see if it's worth to add NICKSERV status # class User < Netmask - private :host=, :user= + alias :to_s :nick # Create a new IRC User from a given Netmask (or anything that can be converted # into a Netmask) provided that the given Netmask does not have globs. # - def initialize(str, casemap=nil) + def initialize(str="", casemap=nil) super - raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if has_irc_glob? + raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if nick.has_irc_glob? && nick != "*" + raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if user.has_irc_glob? && user != "*" + raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if host.has_irc_glob? && host != "*" + @away = false + end + + # We only allow the user to be changed if it was "*". Otherwise, + # we raise an exception if the new host is different from the old one + # + def user=(newuser) + if user == "*" + super + else + raise "Can't change the username of user #{self}" if user != newuser + end + end + + # We only allow the host to be changed if it was "*". Otherwise, + # we raise an exception if the new host is different from the old one + # + def host=(newhost) + if host == "*" + super + else + raise "Can't change the hostname of user #{self}" if host != newhost + end + end + + # Checks if a User is well-known or not by looking at the hostname and user + # + def known? + return user!="*" && host!="*" + end + + # Is the user away? + # + def away? + return @away + end + + # Set the away status of the user. Use away=(nil) or away=(false) + # to unset away + # + def away=(msg="") + if msg + @away = msg + else + @away = false + end end end @@ -415,67 +472,131 @@ module Irc end - # An IRC Channel is identified by its name, and it has a set of properties: - # * a topic - # * a UserList - # * a set of modes + # A ChannelTopic represents the topic of a channel. It consists of + # the topic itself, who set it and when + class ChannelTopic + attr_accessor :text, :set_by, :set_on + alias :to_s :text + + # Create a new ChannelTopic setting the text, the creator and + # the creation time + def initialize(text="", set_by="", set_on=Time.new) + @text = text + @set_by = set_by + @set_on = Time.new + end + end + + + # Mode on a channel + class ChannelMode + end + + + # Channel modes of type A manipulate lists # - class Channel - attr_reader :name, :type, :casemap + class ChannelModeTypeA < ChannelMode + def initialize + @list = NetmaskList.new + end - # Create a new method. Auxiliary function for the following - # auxiliary functions ... - # - def create_method(name, &block) - self.class.send(:define_method, name, &block) + def set(val) + @list << val unless @list.include?(val) end - private :create_method - # Create a new channel boolean flag - # - def new_bool_flag(sym, acc=nil, default=false) - @flags[sym.to_sym] = default - racc = (acc||sym).to_s << "?" - wacc = (acc||sym).to_s << "=" - create_method(racc.to_sym) { @flags[sym.to_sym] } - create_method(wacc.to_sym) { |val| - @flags[sym.to_sym] = val - } + def reset(val) + @list.delete_if(val) if @list.include?(val) end + end - # Create a new channel flag with data - # - def new_data_flag(sym, acc=nil, default=false) - @flags[sym.to_sym] = default - racc = (acc||sym).to_s - wacc = (acc||sym).to_s << "=" - create_method(racc.to_sym) { @flags[sym.to_sym] } - create_method(wacc.to_sym) { |val| - @flags[sym.to_sym] = val - } + # Channel modes of type B need an argument + # + class ChannelModeTypeB < ChannelMode + def initialize + @arg = nil end - # Create a new variable with accessors - # - def new_variable(name, default=nil) - v = "@#{name}".to_sym - instance_variable_set(v, default) - create_method(name.to_sym) { instance_variable_get(v) } - create_method("#{name}=".to_sym) { |val| - instance_variable_set(v, val) - } + def set(val) + @arg = val end - # Create a new UserList - # - def new_userlist(name, default=UserList.new) - new_variable(name, default) + def reset(val) + @arg = nil if @arg == val + end + end + + # Channel modes that change the User prefixes are like + # Channel modes of type B, except that they manipulate + # lists of Users, so they are somewhat similar to channel + # modes of type A + # + class ChannelUserMode < ChannelModeTypeB + def initialize + @list = UserList.new + end + + def set(val) + @list << val unless @list.include?(val) + end + + def reset(val) + @list.delete_if { |x| x == val } + end + end + + # Channel modes of type C need an argument when set, + # but not when they get reset + # + class ChannelModeTypeC < ChannelMode + def initialize + @arg = false + end + + def set(val) + @arg = val + end + + def reset + @arg = false + end + end + + # Channel modes of type D are basically booleans + class ChannelModeTypeD + def initialize + @set = false + end + + def set? + return @set + end + + def set + @set = true + end + + def reset + @set = false end + end + - # Create a new NetmaskList + # An IRC Channel is identified by its name, and it has a set of properties: + # * a topic + # * a UserList + # * a set of modes + # + class Channel + attr_reader :name, :topic, :casemap, :mode, :users + alias :to_s :name + + # A String describing the Channel and (some of its) internals # - def new_netmasklist(name, default=NetmaskList.new) - new_variable(name, default) + def inspect + str = "<#{self.class}:#{'0x%08x' % self.object_id}:" + str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}" + str << " @users=<#{@users.join(', ')}>" + str end # Creates a new channel with the given name, optionally setting the topic @@ -486,7 +607,7 @@ module Irc # # FIXME doesn't check if users have the same casemap as the channel yet # - def initialize(name, topic="", users=[], casemap=nil) + def initialize(name, topic=nil, users=[], casemap=nil) @casemap = casemap || 'rfc1459' raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty? @@ -495,45 +616,34 @@ module Irc @name = name.irc_downcase(@casemap) - new_variable(:topic, topic) + @topic = topic || ChannelTopic.new - new_userlist(:users) case users when UserList - @users = users.dup + @users = users when Array @users = UserList.new(users) else raise ArgumentError, "Invalid user list #{users.inspect}" end - # new_variable(:creator) - - # # Special users - # new_userlist(:super_ops) - # new_userlist(:ops) - # new_userlist(:half_ops) - # new_userlist(:voices) - - # # Ban and invite lists - # new_netmasklist(:banlist) - # new_netmasklist(:exceptlist) - # new_netmasklist(:invitelist) + # Flags + @mode = {} + end - # # Flags - @flags = {} - # new_bool_flag(:a, :anonymous) - # new_bool_flag(:i, :invite_only) - # new_bool_flag(:m, :moderated) - # new_bool_flag(:n, :no_externals) - # new_bool_flag(:q, :quiet) - # new_bool_flag(:p, :private) - # new_bool_flag(:s, :secret) - # new_bool_flag(:r, :will_reop) - # new_bool_flag(:t, :free_topic) + # Removes a user from the channel + # + def delete_user(user) + @users.delete_if { |x| x == user } + @mode.each { |sym, mode| + mode.reset(user) if mode.class <= ChannelUserMode + } + end - # new_data_flag(:k, :key) - # new_data_flag(:l, :limit) + # The channel prefix + # + def prefix + name[0].chr end # A channel is local to a server if it has the '&' prefix @@ -559,6 +669,12 @@ module Irc def normal? name[0] = 0x23 end + + # Create a new mode + # + def create_mode(sym, kl) + @mode[sym.to_sym] = kl.new + end end @@ -579,7 +695,8 @@ module Irc class Server attr_reader :hostname, :version, :usermodes, :chanmodes - attr_reader :supports, :capab + alias :to_s :hostname + attr_reader :supports, :capabilities attr_reader :channels, :users @@ -590,14 +707,27 @@ module Irc # def initialize @hostname = @version = @usermodes = @chanmodes = nil + + @channels = ChannelList.new + @channel_names = Array.new + + @users = UserList.new + @user_nicks = Array.new + + reset_capabilities + end + + # Resets the server capabilities + # + def reset_capabilities @supports = { :casemapping => 'rfc1459', :chanlimit => {}, :chanmodes => { - :addr_list => nil, # Type A - :has_param => nil, # Type B - :set_param => nil, # Type C - :no_params => nil # Type D + :typea => nil, # Type A: address lists + :typeb => nil, # Type B: needs a parameter + :typec => nil, # Type C: needs a parameter when set + :typed => nil # Type D: must not have a parameter }, :channellen => 200, :chantypes => "#&", @@ -619,13 +749,25 @@ module Irc :targmax => {}, :topiclen => nil } - @capab = {} + @capabilities = {} + end - @channels = ChannelList.new - @channel_names = Array.new + # Resets the Channel and User list + # + def reset_lists + @users.each { |u| + delete_user(u) + } + @channels.each { |u| + delete_channel(u) + } + end - @users = UserList.new - @user_nicks = Array.new + # Clears the server + # + def clear + reset_lists + reset_capabilities end # This method is used to parse a 004 RPL_MY_INFO line @@ -659,10 +801,6 @@ module Irc # # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt] # - # TODO this is just an initial draft that does nothing special. - # We want to properly parse most of the supported capabilities - # for later reuse. - # def parse_isupport(line) ar = line.split(' ') reparse = "" @@ -699,10 +837,10 @@ module Irc when :chanmodes noval_warn(key, val) { groups = val.split(',') - @supports[key][:addr_list] = groups[0].scan(/./) - @supports[key][:has_param] = groups[1].scan(/./) - @supports[key][:set_param] = groups[2].scan(/./) - @supports[key][:no_params] = groups[3].scan(/./) + @supports[key][:typea] = groups[0].scan(/./) + @supports[key][:typeb] = groups[1].scan(/./) + @supports[key][:typec] = groups[2].scan(/./) + @supports[key][:typed] = groups[3].scan(/./) } when :channellen, :kicklen, :modes, :topiclen if val @@ -758,17 +896,38 @@ module Irc @supports[:casemapping] || 'rfc1459' end + # Returns User or Channel depending on what _name_ can be + # a name of + # + def user_or_channel?(name) + if supports[:chantypes].include?(name[0].chr) + return Channel + else + return User + end + end + + # Returns the actual User or Channel object matching _name_ + # + def user_or_channel(name) + if supports[:chantypes].include?(name[0].chr) + return channel(name) + else + return user(name) + end + end + # Checks if the receiver already has a channel with the given _name_ # def has_channel?(name) - @channel_names.index(name) + @channel_names.index(name.to_s) end alias :has_chan? :has_channel? # Returns the channel with name _name_, if available # def get_channel(name) - idx = @channel_names.index(name) + idx = @channel_names.index(name.to_s) @channels[idx] if idx end alias :get_chan :get_channel @@ -780,7 +939,7 @@ module Irc # # The Channel is automatically created with the appropriate casemap # - def new_channel(name, topic="", users=[], fails=true) + def new_channel(name, topic=nil, users=[], fails=true) if !has_chan?(name) prefix = name[0].chr @@ -789,19 +948,19 @@ module Irc # # FIXME might need to raise an exception # - warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].includes?(prefix) + warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix) warn "#{self} doesn't support channel names this long (#{name.length} > #{@support[:channellen]}" unless name.length <= @supports[:channellen] # Next, we check if we hit the limit for channels of type +prefix+ # if the server supports +chanlimit+ # @supports[:chanlimit].keys.each { |k| - next unless k.includes?(prefix) + next unless k.include?(prefix) count = 0 @channel_names.each { |n| - count += 1 if k.includes?(n[0].chr) + count += 1 if k.include?(n[0].chr) } - raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimits][k] + raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k] } # So far, everything is fine. Now create the actual Channel @@ -812,41 +971,51 @@ module Irc # lists and flags for this channel @supports[:prefix][:modes].each { |mode| - chan.new_userlist(mode) + chan.create_mode(mode, ChannelUserMode) } if @supports[:prefix][:modes] @supports[:chanmodes].each { |k, val| if val case k - when :addr_list + when :typea val.each { |mode| - chan.new_netmasklist(mode) + chan.create_mode(mode, ChannelModeTypeA) } - when :has_param, :set_param + when :typeb val.each { |mode| - chan.new_data_flag(mode) + chan.create_mode(mode, ChannelModeTypeB) } - when :no_params + when :typec val.each { |mode| - chan.new_bool_flag(mode) + chan.create_mode(mode, ChannelModeTypeC) + } + when :typed + val.each { |mode| + chan.create_mode(mode, ChannelModeTypeD) } end end } - # * appropriate @flags - # * a UserList for each @supports[:prefix] - # * a NetmaskList for each @supports[:chanmodes] of type A - - @channels << newchan + @channels << chan @channel_names << name - return newchan + debug "Created channel #{chan.inspect}" + debug "Managing channels #{@channel_names.join(', ')}" + return chan end raise "Channel #{name} already exists on server #{self}" if fails return get_channel(name) end + # Returns the Channel with the given _name_ on the server, + # creating it if necessary. This is a short form for + # new_channel(_str_, nil, [], +false+) + # + def channel(str) + new_channel(str,nil,[],false) + end + # Remove Channel _name_ from the list of Channels # def delete_channel(name) @@ -859,13 +1028,13 @@ module Irc # Checks if the receiver already has a user with the given _nick_ # def has_user?(nick) - @user_nicks.index(nick) + @user_nicks.index(nick.to_s) end # Returns the user with nick _nick_, if available # def get_user(nick) - idx = @user_nicks.index(name) + idx = @user_nicks.index(nick.to_s) @users[idx] if idx end @@ -877,7 +1046,12 @@ module Irc # The User is automatically created with the appropriate casemap # def new_user(str, fails=true) - tmp = User.new(str, self.casemap) + case str + when User + tmp = str + else + tmp = User.new(str, self.casemap) + end if !has_user?(tmp.nick) warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@support[:nicklen]}" unless tmp.nick.length <= @supports[:nicklen] @users << tmp @@ -885,9 +1059,14 @@ module Irc return @users.last end old = get_user(tmp.nick) - raise "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old} but access was tried with #{tmp}" if old != tmp - raise "User #{tmp} already exists on server #{self}" if fails - return get_user(tmp) + if old.known? + raise "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old} but access was tried with #{tmp}" if old != tmp + raise "User #{tmp} already exists on server #{self}" if fails + else + old.user = tmp.user + old.host = tmp.host + end + return old end # Returns the User with the given Netmask on the server, @@ -902,10 +1081,13 @@ module Irc # _someuser_ must be specified with the full Netmask. # def delete_user(someuser) - idx = has_user?(user.nick) + idx = has_user?(someuser.nick) raise "Tried to remove unmanaged user #{user}" unless idx - have = self.user(user) - raise "User #{someuser.nick} has inconsistent Netmasks! #{self} knows #{have} but access was tried with #{someuser}" if have != someuser + have = self.user(someuser) + raise "User #{someuser.nick} has inconsistent Netmasks! #{self} knows #{have} but access was tried with #{someuser}" if have != someuser && have.user != "*" && have.host != "*" + @channels.each { |ch| + delete_user_from_channel(have, ch) + } @user_nicks.delete_at(idx) @users.delete_at(idx) end @@ -926,10 +1108,21 @@ module Irc nm = new_netmask(mask) @users.inject(UserList.new) { |list, user| - list << user if user.matches?(nm) + if user.user == "*" or user.host == "*" + list << user if user.nick =~ nm.nick.to_irc_regexp + else + list << user if user.matches?(nm) + end list } end + + # Deletes User from Channel + # + def delete_user_from_channel(user, channel) + channel.delete_user(user) + end + end end diff --git a/lib/rbot/ircbot.rb b/lib/rbot/ircbot.rb index 6226e55e..65b94172 100644 --- a/lib/rbot/ircbot.rb +++ b/lib/rbot/ircbot.rb @@ -71,13 +71,14 @@ require 'rbot/rbotconfig' require 'rbot/config' require 'rbot/utils' +require 'rbot/irc' require 'rbot/rfc2812' require 'rbot/keywords' require 'rbot/ircsocket' require 'rbot/auth' require 'rbot/timer' require 'rbot/plugins' -require 'rbot/channel' +# require 'rbot/channel' require 'rbot/message' require 'rbot/language' require 'rbot/dbhash' @@ -89,9 +90,6 @@ module Irc # Main bot class, which manages the various components, receives messages, # handles them or passes them to plugins, and contains core functionality. class IrcBot - # the bot's current nickname - attr_reader :nick - # the bot's IrcAuth data attr_reader :auth @@ -108,13 +106,12 @@ class IrcBot # bot's Language data attr_reader :lang - # capabilities info for the server - attr_reader :capabilities - - # channel info for channels the bot is in - attr_reader :channels + # server the bot is connected to + # TODO multiserver + attr_reader :server # bot's irc socket + # TODO multiserver attr_reader :socket # bot's object registry, plugins get an interface to this for persistant @@ -129,6 +126,14 @@ class IrcBot # proxies etc as defined by the bot configuration/environment attr_reader :httputil + # bot User in the client/server connection + attr_reader :myself + + # bot User in the client/server connection + def nick + myself.nick + end + # create a new IrcBot with botclass +botclass+ def initialize(botclass, params = {}) # BotConfig for the core bot @@ -308,14 +313,19 @@ class IrcBot log_session_start - @timer = Timer::Timer.new(1.0) # only need per-second granularity @registry = BotRegistry.new self + + @timer = Timer::Timer.new(1.0) # only need per-second granularity @timer.add(@config['core.save_every']) { save } if @config['core.save_every'] - @channels = Hash.new + @logs = Hash.new + @httputil = Utils::HttpUtil.new(self) + @lang = Language::Language.new(@config['core.language']) + @keywords = Keywords.new(self) + begin @auth = IrcAuth.new(self) rescue => e @@ -329,28 +339,51 @@ class IrcBot @plugins = Plugins::Plugins.new(self, ["#{botclass}/plugins"]) @socket = IrcSocket.new(@config['server.name'], @config['server.port'], @config['server.bindhost'], @config['server.sendq_delay'], @config['server.sendq_burst']) - @nick = @config['irc.nick'] - @client = IrcClient.new + @server = @client.server + @myself = @client.client + @myself.nick = @config['irc.nick'] + + # Channels where we are quiet + # It's nil when we are not quiet, an empty list when we are quiet + # in all channels, a list of channels otherwise + @quiet = nil + + + @client[:welcome] = proc {|data| + irclog "joined server #{@client.server} as #{myself}", "server" + + @plugins.delegate("connect") + + @config['irc.join_channels'].each { |c| + debug "autojoining channel #{c}" + if(c =~ /^(\S+)\s+(\S+)$/i) + join $1, $2 + else + join c if(c) + end + } + } @client[:isupport] = proc { |data| - if data[:capab] - sendq "CAPAB IDENTIFY-MSG" - end + # TODO this needs to go into rfc2812.rb + # Since capabs are two-steps processes, server.supports[:capab] + # should be a three-state: nil, [], [....] + sendq "CAPAB IDENTIFY-MSG" if @server.supports[:capab] } @client[:datastr] = proc { |data| - debug data.inspect + # TODO this needs to go into rfc2812.rb if data[:text] == "IDENTIFY-MSG" - @capabilities["identify-msg".to_sym] = true + @server.capabilities["identify-msg".to_sym] = true else debug "Not handling RPL_DATASTR #{data[:servermessage]}" end } @client[:privmsg] = proc { |data| - message = PrivMessage.new(self, data[:source], data[:target], data[:message]) + message = PrivMessage.new(self, @server, data[:source], data[:target], data[:message]) onprivmsg(message) } @client[:notice] = proc { |data| - message = NoticeMessage.new(self, data[:source], data[:target], data[:message]) + message = NoticeMessage.new(self, @server, data[:source], data[:target], data[:message]) # pass it off to plugins that want to hear everything @plugins.delegate "listen", message } @@ -373,125 +406,99 @@ class IrcBot @last_ping = nil } @client[:nick] = proc {|data| - sourcenick = data[:sourcenick] - nick = data[:nick] - m = NickMessage.new(self, data[:source], data[:sourcenick], data[:nick]) - if(sourcenick == @nick) - debug "my nick is now #{nick}" - @nick = nick + source = data[:source] + old = data[:oldnick] + new = data[:newnick] + m = NickMessage.new(self, @server, source, old, new) + if source == myself + debug "my nick is now #{new}" end - @channels.each {|k,v| - if(v.users.has_key?(sourcenick)) - irclog "@ #{sourcenick} is now known as #{nick}", k - v.users[nick] = v.users[sourcenick] - v.users.delete(sourcenick) - end + data[:is_on].each { |ch| + irclog "@ #{data[:old]} is now known as #{data[:new]}", ch } @plugins.delegate("listen", m) @plugins.delegate("nick", m) } @client[:quit] = proc {|data| - source = data[:source] - sourcenick = data[:sourcenick] - sourceurl = data[:sourceaddress] - message = data[:message] - m = QuitMessage.new(self, data[:source], data[:sourcenick], data[:message]) - if(data[:sourcenick] =~ /#{Regexp.escape(@nick)}/i) - else - @channels.each {|k,v| - if(v.users.has_key?(sourcenick)) - irclog "@ Quit: #{sourcenick}: #{message}", k - v.users.delete(sourcenick) - end - } - end + m = QuitMessage.new(self, @server, data[:source], data[:source], data[:message]) + data[:was_on].each { |ch| + irclog "@ Quit: #{sourcenick}: #{message}", ch + } @plugins.delegate("listen", m) @plugins.delegate("quit", m) } @client[:mode] = proc {|data| - source = data[:source] - sourcenick = data[:sourcenick] - sourceurl = data[:sourceaddress] - channel = data[:channel] - targets = data[:targets] - modestring = data[:modestring] - irclog "@ Mode #{modestring} #{targets} by #{sourcenick}", channel - } - @client[:welcome] = proc {|data| - irclog "joined server #{data[:source]} as #{data[:nick]}", "server" - debug "I think my nick is #{@nick}, server thinks #{data[:nick]}" - if data[:nick] && data[:nick].length > 0 - @nick = data[:nick] - end - - @plugins.delegate("connect") - - @config['irc.join_channels'].each {|c| - debug "autojoining channel #{c}" - if(c =~ /^(\S+)\s+(\S+)$/i) - join $1, $2 - else - join c if(c) - end - } + irclog "@ Mode #{data[:modestring]} by #{data[:sourcenick]}", data[:channel] } @client[:join] = proc {|data| - m = JoinMessage.new(self, data[:source], data[:channel], data[:message]) + m = JoinMessage.new(self, @server, data[:source], data[:channel], data[:message]) onjoin(m) } @client[:part] = proc {|data| - m = PartMessage.new(self, data[:source], data[:channel], data[:message]) + m = PartMessage.new(self, @server, data[:source], data[:channel], data[:message]) onpart(m) } @client[:kick] = proc {|data| - m = KickMessage.new(self, data[:source], data[:target],data[:channel],data[:message]) + m = KickMessage.new(self, @server, data[:source], data[:target], data[:channel],data[:message]) onkick(m) } @client[:invite] = proc {|data| - if(data[:target] =~ /^#{Regexp.escape(@nick)}$/i) - join data[:channel] if (@auth.allow?("join", data[:source], data[:sourcenick])) + if data[:target] == myself + join data[:channel] if @auth.allow?("join", data[:source], data[:source].nick) end } @client[:changetopic] = proc {|data| channel = data[:channel] - sourcenick = data[:sourcenick] + source = data[:source] topic = data[:topic] - timestamp = data[:unixtime] || Time.now.to_i - if(sourcenick == @nick) + if source == myself irclog "@ I set topic \"#{topic}\"", channel else - irclog "@ #{sourcenick} set topic \"#{topic}\"", channel + irclog "@ #{source} set topic \"#{topic}\"", channel end - m = TopicMessage.new(self, data[:source], data[:channel], timestamp, data[:topic]) + m = TopicMessage.new(self, @server, data[:source], data[:channel], data[:topic]) ontopic(m) @plugins.delegate("listen", m) @plugins.delegate("topic", m) } - @client[:topic] = @client[:topicinfo] = proc {|data| - channel = data[:channel] - m = TopicMessage.new(self, data[:source], data[:channel], data[:unixtime], data[:topic]) - ontopic(m) + @client[:topic] = @client[:topicinfo] = proc { |data| + m = TopicMessage.new(self, @server, data[:source], data[:channel], data[:channel].topic) + ontopic(m) } - @client[:names] = proc {|data| - channel = data[:channel] - users = data[:users] - unless(@channels[channel]) - warning "got names for channel '#{channel}' I didn't think I was in\n" - # exit 2 - end - @channels[channel].users.clear - users.each {|u| - @channels[channel].users[u[0].sub(/^[@&~+]/, '')] = ["mode", u[1]] - } + @client[:names] = proc { |data| @plugins.delegate "names", data[:channel], data[:users] } - @client[:unknown] = proc {|data| + @client[:unknown] = proc { |data| #debug "UNKNOWN: #{data[:serverstring]}" irclog data[:serverstring], ".unknown" } end + # checks if we should be quiet on a channel + def quiet_on?(channel) + return false unless @quiet + return true if @quiet.empty? + return @quiet.include?(channel.to_s) + end + + def set_quiet(channel=nil) + if channel + @quiet << channel.to_s unless @quiet.include?(channel.to_s) + else + @quiet = [] + end + end + + def reset_quiet(channel=nil) + if channel + @quiet.delete_if { |x| x == channel.to_s } + else + @quiet = nil + end + end + + # things to do when we receive a signal def got_sig(sig) debug "received #{sig}, queueing quit" $interrupted += 1 @@ -524,8 +531,7 @@ class IrcBot raise e.class, "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e end @socket.emergency_puts "PASS " + @config['server.password'] if @config['server.password'] - @socket.emergency_puts "NICK #{@nick}\nUSER #{@config['irc.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert" - @capabilities = Hash.new + @socket.emergency_puts "NICK #{@config['irc.nick']}\nUSER #{@config['irc.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert" start_server_pings end @@ -573,7 +579,7 @@ class IrcBot end stop_server_pings - @channels.clear + @server.clear if @socket.connected? @socket.clearq @socket.shutdown @@ -601,7 +607,7 @@ class IrcBot # and all the extra stuff # TODO allow something to do for commands that produce too many messages # TODO example: math 10**10000 - left = @socket.bytes_per - type.length - where.length - 4 + left = @socket.bytes_per - type.length - where.to_s.length - 4 begin if(left >= message.length) sendq "#{type} #{where} :#{message}", chan, ring @@ -626,17 +632,18 @@ class IrcBot end # send a notice message to channel/nick +where+ - def notice(where, message, mchan=nil, mring=-1) + def notice(where, message, mchan="", mring=-1) if mchan == "" chan = where else chan = mchan end if mring < 0 - if where =~ /^#/ - ring = 2 - else + case where + when User ring = 1 + else + ring = 2 end else ring = mring @@ -656,10 +663,11 @@ class IrcBot chan = mchan end if mring < 0 - if where =~ /^#/ - ring = 2 - else + case where + when User ring = 1 + else + ring = 2 end else ring = mring @@ -667,7 +675,7 @@ class IrcBot message.to_s.gsub(/[\r\n]+/, "\n").each_line { |line| line.chomp! next unless(line.length > 0) - unless((where =~ /^#/) && (@channels.has_key?(where) && @channels[where].quiet)) + unless quiet_on?(where) sendmsg "PRIVMSG", where, line, chan, ring end } @@ -681,7 +689,8 @@ class IrcBot chan = mchan end if mring < 0 - if where =~ /^#/ + case where + when Channel ring = 2 else ring = 1 @@ -690,12 +699,13 @@ class IrcBot ring = mring end sendq "PRIVMSG #{where} :\001ACTION #{message}\001", chan, ring - if(where =~ /^#/) - irclog "* #{@nick} #{message}", where - elsif (where =~ /^(\S*)!.*$/) - irclog "* #{@nick}[#{where}] #{message}", $1 + case where + when Channel + irclog "* #{myself} #{message}", where + when User + irclog "* #{myself}[#{where}] #{message}", $1 else - irclog "* #{@nick}[#{where}] #{message}", where + irclog "* #{myself}[#{where}] #{message}", where end end @@ -709,7 +719,7 @@ class IrcBot def irclog(message, where="server") message = message.chomp stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S") - where = where.gsub(/[:!?$*()\/\\<>|"']/, "_") + where = where.to_s.gsub(/[:!?$*()\/\\<>|"']/, "_") unless(@logs.has_key?(where)) @logs[where] = File.new("#{@botclass}/logs/#{where}", "a") @logs[where].sync = true @@ -746,8 +756,8 @@ class IrcBot @socket.shutdown end debug "Logging quits" - @channels.each_value {|v| - irclog "@ quit (#{message})", v.name + @server.channels.each { |ch| + irclog "@ quit (#{message})", ch } debug "Saving" save @@ -910,7 +920,7 @@ class IrcBot when "restart" return "restart => completely stop and restart the bot (including reconnect)" when "join" - return "join [] => join channel with secret key if specified. #{@nick} also responds to invites if you have the required access level" + return "join [] => join channel with secret key if specified. #{myself} also responds to invites if you have the required access level" when "part" return "part => part channel " when "hide" @@ -934,9 +944,9 @@ class IrcBot when "version" return "version => describes software version" when "botsnack" - return "botsnack => reward #{@nick} for being good" + return "botsnack => reward #{myself} for being good" when "hello" - return "hello|hi|hey|yo [#{@nick}] => greet the bot" + return "hello|hi|hey|yo [#{myself}] => greet the bot" else return "Core help topics: quit, restart, config, join, part, hide, save, rescan, nick, say, action, topic, quiet, talk, version, botsnack, hello" end @@ -1015,25 +1025,25 @@ class IrcBot when (/^quiet$/i) if(auth.allow?("talk", m.source, m.replyto)) m.okay - @channels.each_value {|c| c.quiet = true } + set_quiet end when (/^quiet in (\S+)$/i) where = $1 if(auth.allow?("talk", m.source, m.replyto)) m.okay where.gsub!(/^here$/, m.target) if m.public? - @channels[where].quiet = true if(@channels.has_key?(where)) + set_quiet(where) end when (/^talk$/i) if(auth.allow?("talk", m.source, m.replyto)) - @channels.each_value {|c| c.quiet = false } + reset_quiet m.okay end when (/^talk in (\S+)$/i) where = $1 if(auth.allow?("talk", m.source, m.replyto)) where.gsub!(/^here$/, m.target) if m.public? - @channels[where].quiet = false if(@channels.has_key?(where)) + reset_quiet(where) m.okay end when (/^status\??$/i) @@ -1059,9 +1069,9 @@ class IrcBot else # stuff to handle when not addressed case m.message - when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi|yo(\W|$))[\s,-.]+#{Regexp.escape(@nick)}$/i) + when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi|yo(\W|$))[\s,-.]+#{Regexp.escape(self.nick)}$/i) say m.replyto, @lang.get("hello_X") % m.sourcenick - when (/^#{Regexp.escape(@nick)}!*$/) + when (/^#{Regexp.escape(self.nick)}!*$/) say m.replyto, @lang.get("hello_X") % m.sourcenick else @keywords.privmsg(m) @@ -1073,17 +1083,19 @@ class IrcBot def log_sent(type, where, message) case type when "NOTICE" - if(where =~ /^#/) - irclog "-=#{@nick}=- #{message}", where - elsif (where =~ /(\S*)!.*/) + case where + when Channel + irclog "-=#{myself}=- #{message}", where + when User irclog "[-=#{where}=-] #{message}", $1 else - irclog "[-=#{where}=-] #{message}" + irclog "[-=#{where}=-] #{message}", where end when "PRIVMSG" - if(where =~ /^#/) - irclog "<#{@nick}> #{message}", where - elsif (where =~ /^(\S*)!.*$/) + case where + when Channel + irclog "<#{myself}> #{message}", where + when User irclog "[msg(#{where})] #{message}", $1 else irclog "[msg(#{where})] #{message}", where @@ -1092,14 +1104,11 @@ class IrcBot end def onjoin(m) - @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel)) - if(m.address?) + if m.address? debug "joined channel #{m.channel}" irclog "@ Joined channel #{m.channel}", m.channel else irclog "@ #{m.sourcenick} joined channel #{m.channel}", m.channel - @channels[m.channel].users[m.sourcenick] = Hash.new - @channels[m.channel].users[m.sourcenick]["mode"] = "" end @plugins.delegate("listen", m) @@ -1110,15 +1119,8 @@ class IrcBot if(m.address?) debug "left channel #{m.channel}" irclog "@ Left channel #{m.channel} (#{m.message})", m.channel - @channels.delete(m.channel) else irclog "@ #{m.sourcenick} left channel #{m.channel} (#{m.message})", m.channel - if @channels.has_key?(m.channel) - @channels[m.channel].users.delete(m.sourcenick) - else - warning "got part for channel '#{channel}' I didn't think I was in\n" - # exit 2 - end end # delegate to plugins @@ -1130,10 +1132,8 @@ class IrcBot def onkick(m) if(m.address?) debug "kicked from channel #{m.channel}" - @channels.delete(m.channel) irclog "@ You have been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel else - @channels[m.channel].users.delete(m.sourcenick) irclog "@ #{m.target} has been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel end @@ -1142,12 +1142,7 @@ class IrcBot end def ontopic(m) - @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel)) - @channels[m.channel].topic = m.topic if !m.topic.nil? - @channels[m.channel].topic.timestamp = m.timestamp if !m.timestamp.nil? - @channels[m.channel].topic.by = m.source if !m.source.nil? - - debug "topic of channel #{m.channel} is now #{@channels[m.channel].topic}" + debug "topic of channel #{m.channel} is now #{m.topic}" end # delegate a privmsg to auth, keyword or plugin handlers diff --git a/lib/rbot/message.rb b/lib/rbot/message.rb index 66b6175c..fff12194 100644 --- a/lib/rbot/message.rb +++ b/lib/rbot/message.rb @@ -17,19 +17,16 @@ module Irc # associated bot attr_reader :bot + # associated server + attr_reader :server + # when the message was received attr_reader :time - # hostmask of message source + # User that originated the message attr_reader :source - # nick of message source - attr_reader :sourcenick - - # url part of message source - attr_reader :sourceaddress - - # nick/channel message was sent to + # User/Channel message was sent to attr_reader :target # contents of the message @@ -40,10 +37,11 @@ module Irc # instantiate a new Message # bot:: associated bot class - # source:: hostmask of the message source - # target:: nick/channel message is destined for - # message:: message part - def initialize(bot, source, target, message) + # server:: Server where the message took place + # source:: User that sent the message + # target:: User/Channel is destined for + # message:: actual message + def initialize(bot, server, source, target, message) @msg_wants_id = false unless defined? @msg_wants_id @time = Time.now @@ -53,9 +51,10 @@ module Irc @target = target @message = BasicUserMessage.stripcolour message @replied = false + @server = server @identified = false - if @msg_wants_id && @bot.capabilities["identify-msg".to_sym] + if @msg_wants_id && @server.capabilities["identify-msg".to_sym] if @message =~ /([-+])(.*)/ @identified = ($1=="+") @message = $2 @@ -64,18 +63,25 @@ module Irc end end - # split source into consituent parts - if source =~ /^((\S+)!(\S+))$/ - @sourcenick = $2 - @sourceaddress = $3 - end - - if target && target.downcase == @bot.nick.downcase + if target && target == @bot.myself @address = true end end + # Access the nick of the source + # + def sourcenick + @source.nick + end + + # Access the user@host of the source + # + def sourceaddress + "#{@source.user}@#{@source.host}" + end + + # Was the message from an identified user? def identified? return @identified end @@ -133,18 +139,18 @@ module Irc # source:: hostmask of the message source # target:: nick/channel message is destined for # message:: message part - def initialize(bot, source, target, message) - super(bot, source, target, message) + def initialize(bot, server, source, target, message) + super(bot, server, source, target, message) @target = target @private = false @plugin = nil @action = false - if target.downcase == @bot.nick.downcase + if target == @bot.myself @private = true @address = true @channel = nil - @replyto = @sourcenick + @replyto = source else @replyto = @target @channel = @target @@ -223,7 +229,7 @@ module Irc # class to manage IRC PRIVMSGs class PrivMessage < UserMessage - def initialize(bot, source, target, message) + def initialize(bot, server, source, target, message) @msg_wants_id = true super end @@ -231,7 +237,7 @@ module Irc # class to manage IRC NOTICEs class NoticeMessage < UserMessage - def initialize(bot, source, target, message) + def initialize(bot, server, source, target, message) @msg_wants_id = true super end @@ -244,8 +250,8 @@ module Irc # channel user was kicked from attr_reader :channel - def initialize(bot, source, target, channel, message="") - super(bot, source, target, message) + def initialize(bot, server, source, target, channel, message="") + super(bot, server, source, target, message) @channel = channel end end @@ -253,14 +259,22 @@ module Irc # class to pass IRC Nick changes in. @message contains the old nickame, # @sourcenick contains the new one. class NickMessage < BasicUserMessage - def initialize(bot, source, oldnick, newnick) - super(bot, source, oldnick, newnick) + def initialize(bot, server, source, oldnick, newnick) + super(bot, server, source, oldnick, newnick) + end + + def oldnick + return @target + end + + def newnick + return @message end end class QuitMessage < BasicUserMessage - def initialize(bot, source, target, message="") - super(bot, source, target, message) + def initialize(bot, server, source, target, message="") + super(bot, server, source, target, message) end end @@ -272,10 +286,10 @@ module Irc # topic set on channel attr_reader :channel - def initialize(bot, source, channel, timestamp, topic="") - super(bot, source, channel, topic) + def initialize(bot, server, source, channel, topic=ChannelTopic.new) + super(bot, server, source, channel, topic.text) @topic = topic - @timestamp = timestamp + @timestamp = topic.set_on @channel = channel end end @@ -284,11 +298,11 @@ module Irc class JoinMessage < BasicUserMessage # channel joined attr_reader :channel - def initialize(bot, source, channel, message="") - super(bot, source, channel, message) + def initialize(bot, server, source, channel, message="") + super(bot, server, source, channel, message) @channel = channel # in this case sourcenick is the nick that could be the bot - @address = (sourcenick.downcase == @bot.nick.downcase) + @address = (source == @bot.myself) end end diff --git a/lib/rbot/rfc2812.rb b/lib/rbot/rfc2812.rb index 965da0a1..dee2920f 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 = User.new # 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 @@ -878,8 +888,14 @@ module Irc if prefix != nil data[:source] = prefix if prefix =~ /^(\S+)!(\S+)$/ - data[:sourcenick] = $1 - data[:sourceaddress] = $2 + data[:source] = @server.user($1) + else + if @server.hostname && @server.hostname != data[:source] + warning "Unknown origin #{data[:source]} for message\n#{serverstring.inspect}" + else + @server.instance_variable_set(:@hostname, data[:source]) + end + data[:source] = @server end end @@ -894,13 +910,29 @@ 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]) + 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 + @user ||= @server.user(data[:nick]) + 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 +941,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(params.split(' ', 2).last) + handle(:isupport, data) when ERR_NICKNAMEINUSE # "* :Nickname is already in use" data[:nick] = argv[1] @@ -924,49 +967,68 @@ 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 topic #{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 || "" + 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.users << u + if ar[1] + m = @server.supports[:prefix][:prefixes].index(ar[1]) + m = @server.supports[:prefix][:modes][m] + chan.mode[m.to_sym].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 +1067,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,56 +1092,158 @@ 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] + + data[:target] = @server.user_or_channel(argv[0]) data[:message] = argv[1] handle(:privmsg, data) # Now we split it - if(data[:target] =~ /^[#&!+].*/) + if(data[:target].class <= Channel) handle(:public, data) else handle(:msg, data) end + when 'NOTICE' + data[:target] = @server.user_or_channel(argv[0]) + 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.users.include?(data[:source]) + } + + @server.delete_user(data[:source]) + handle(:quit, data) when 'JOIN' - data[:channel] = argv[0] + data[:channel] = @server.channel(argv[0]) + data[:channel].users << data[:source] + handle(:join, data) when 'TOPIC' - data[:channel] = argv[0] - data[:topic] = argv[1] + data[:channel] = @server.channel(argv[0]) + data[:topic] = ChannelTopic.new(argv[1], data[:source], Time.new) + data[:channel].topic = 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.users.include?(data[:source]) + } + + data[:newnick] = argv[0] + data[:oldnick] = data[:source].nick.dup + data[:source].nick = data[:nick] + 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 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_want_params = [] + + argv[1..-1].each { |arg| + setting = arg[0].chr + if "+-".include?(setting) + arg[1..-1].each_byte { |m| + 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}" + end + } + else + idx = who_wants_params.shift + if idx.nil? + warn "Oops, problems parsing #{serverstring}" + 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 handle(:unknown, data) end -- 2.39.2