summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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