summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGiuseppe Bilotta <giuseppe.bilotta@gmail.com>2006-07-31 15:33:15 +0000
committerGiuseppe Bilotta <giuseppe.bilotta@gmail.com>2006-07-31 15:33:15 +0000
commit2a27c12fffa359898c5601a211fe19425da82fa6 (patch)
tree68d08b03f5552c1c6da172b7359a2df005984832
parent808c62190d6ed88f29df92871f810177b540c8a1 (diff)
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
-rw-r--r--Rakefile2
-rwxr-xr-xbin/rbot2
-rw-r--r--data/rbot/plugins/nickserv.rb4
-rw-r--r--lib/rbot/channel.rb54
-rw-r--r--lib/rbot/irc.rb463
-rw-r--r--lib/rbot/ircbot.rb301
-rw-r--r--lib/rbot/message.rb90
-rw-r--r--lib/rbot/rfc2812.rb302
8 files changed, 757 insertions, 461 deletions
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 <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