diff options
-rw-r--r-- | Rakefile | 2 | ||||
-rwxr-xr-x | bin/rbot | 2 | ||||
-rw-r--r-- | data/rbot/plugins/nickserv.rb | 4 | ||||
-rw-r--r-- | lib/rbot/channel.rb | 54 | ||||
-rw-r--r-- | lib/rbot/irc.rb | 463 | ||||
-rw-r--r-- | lib/rbot/ircbot.rb | 301 | ||||
-rw-r--r-- | lib/rbot/message.rb | 90 | ||||
-rw-r--r-- | lib/rbot/rfc2812.rb | 302 |
8 files changed, 757 insertions, 461 deletions
@@ -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 @@ -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 <code>Channel</code>s
#
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 <channel> [<key>] => join channel <channel> with secret key <key> if specified. #{@nick} also responds to invites if you have the required access level" + return "join <channel> [<key>] => join channel <channel> with secret key <key> if specified. #{myself} also responds to invites if you have the required access level" when "part" return "part <channel> => part channel <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 + # <nick>!<user>@<host>" + 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 <servername>, running version <ver>" - # TODO how standard is this "version <ver>? should i parse it? - data[:message] = argv[1] handle(:yourhost, data) when RPL_CREATED # "This server was created <date>" @@ -909,10 +941,21 @@ module Irc when RPL_MYINFO # "<servername> <version> <available user modes> # <available channel modes>" - 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 # "* <nick> :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 # "( "=" / "*" / "@" ) <channel> # :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> ) # - "@" 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 <integer> users and <integer> # services on <integer> 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 - # <nick>!<user>@<host>" - 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 # "<nick> :- <server> 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 ([+-]<modes> (<params>)*)* + # When a MODE message is received by a server, + # Type C will have parameters too, so we must + # be able to consume parameters for all + # but Type D modes + + data[:channel] = @server.user_or_channel(argv[0]) + data[:modestring] = argv[1..-1].join(" ") + case data[:channel] + when User + # TODO 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 |