summaryrefslogtreecommitdiff
path: root/lib/rbot
diff options
context:
space:
mode:
authorGiuseppe Bilotta <giuseppe.bilotta@gmail.com>2008-03-04 18:24:57 +0100
committerGiuseppe Bilotta <giuseppe.bilotta@gmail.com>2008-03-04 18:24:57 +0100
commit6f9bfa43ac907700fcba394e0f6b9d987b1192fb (patch)
tree90058ef94a1cdf86b51b70bfdafd17b8f26ea33a /lib/rbot
parent02e8b672cdf3bf8c67f78a2a7fc028c4c6304a30 (diff)
Unixify all line endings.
Some files had DOS-style line endings. Change all of them to Unix-style.
Diffstat (limited to 'lib/rbot')
-rw-r--r--lib/rbot/botuser.rb1848
-rw-r--r--lib/rbot/core/auth.rb1936
-rw-r--r--lib/rbot/core/basics.rb376
-rw-r--r--lib/rbot/core/config.rb650
-rw-r--r--lib/rbot/irc.rb3916
-rw-r--r--lib/rbot/plugins/opmeh.rb38
6 files changed, 4382 insertions, 4382 deletions
diff --git a/lib/rbot/botuser.rb b/lib/rbot/botuser.rb
index b388b7f4..c77db4a5 100644
--- a/lib/rbot/botuser.rb
+++ b/lib/rbot/botuser.rb
@@ -1,924 +1,924 @@
-#-- vim:sw=2:et
-#++
-# :title: User management
-#
-# rbot user management
-# Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)
-# Copyright:: Copyright (c) 2006 Giuseppe Bilotta
-# License:: GPLv2
-
-require 'singleton'
-require 'set'
-require 'rbot/maskdb'
-
-# This would be a good idea if it was failproof, but the truth
-# is that other methods can indirectly modify the hash. *sigh*
-#
-# class AuthNotifyingHash < Hash
-# %w(clear default= delete delete_if replace invert
-# merge! update rehash reject! replace shift []= store).each { |m|
-# class_eval {
-# define_method(m) { |*a|
-# r = super(*a)
-# Irc::Bot::Auth.manager.set_changed
-# r
-# }
-# }
-# }
-# end
-#
-
-module Irc
-class Bot
-
-
- # This module contains the actual Authentication stuff
- #
- module Auth
-
- Config.register Config::StringValue.new( 'auth.password',
- :default => 'rbotauth', :wizard => true,
- :on_change => Proc.new {|bot, v| bot.auth.botowner.password = v},
- :desc => _('Password for the bot owner'))
- Config.register Config::BooleanValue.new( 'auth.login_by_mask',
- :default => 'true',
- :desc => _('Set false to prevent new botusers from logging in without a password when the user netmask is known'))
- Config.register Config::BooleanValue.new( 'auth.autologin',
- :default => 'true',
- :desc => _('Set false to prevent new botusers from recognizing IRC users without a need to manually login'))
- Config.register Config::BooleanValue.new( 'auth.autouser',
- :default => 'false',
- :desc => _('Set true to allow new botusers to be created automatically'))
- # Config.register Config::IntegerValue.new( 'auth.default_level',
- # :default => 10, :wizard => true,
- # :desc => 'The default level for new/unknown users' )
-
- # Generate a random password of length _l_
- #
- def Auth.random_password(l=8)
- pwd = ""
- l.times do
- pwd << (rand(26) + (rand(2) == 0 ? 65 : 97) ).chr
- end
- return pwd
- end
-
-
- # An Irc::Bot::Auth::Command defines a command by its "path":
- #
- # base::command::subcommand::subsubcommand::subsubsubcommand
- #
- class Command
-
- attr_reader :command, :path
-
- # A method that checks if a given _cmd_ is in a form that can be
- # reduced into a canonical command path, and if so, returns it
- #
- def sanitize_command_path(cmd)
- pre = cmd.to_s.downcase.gsub(/^\*?(?:::)?/,"").gsub(/::$/,"")
- return pre if pre.empty?
- return pre if pre =~ /^\S+(::\S+)*$/
- raise TypeError, "#{cmd.inspect} is not a valid command"
- end
-
- # Creates a new Command from a given string; you can then access
- # the command as a symbol with the :command method and the whole
- # path as :path
- #
- # Command.new("core::auth::save").path => [:"*", :"core", :"core::auth", :"core::auth::save"]
- #
- # Command.new("core::auth::save").command => :"core::auth::save"
- #
- def initialize(cmd)
- cmdpath = sanitize_command_path(cmd).split('::')
- seq = cmdpath.inject(["*"]) { |list, cmd|
- list << (list.length > 1 ? list.last + "::" : "") + cmd
- }
- @path = seq.map { |k|
- k.to_sym
- }
- @command = path.last
- debug "Created command #{@command.inspect} with path #{@path.pretty_inspect}"
- end
-
- # Returs self
- def to_irc_auth_command
- self
- end
-
- end
-
- end
-
-end
-end
-
-
-class String
-
- # Returns an Irc::Bot::Auth::Comand from the receiver
- def to_irc_auth_command
- Irc::Bot::Auth::Command.new(self)
- end
-
-end
-
-
-class Symbol
-
- # Returns an Irc::Bot::Auth::Comand from the receiver
- def to_irc_auth_command
- Irc::Bot::Auth::Command.new(self)
- end
-
-end
-
-
-module Irc
-class Bot
-
-
- module Auth
-
-
- # This class describes a permission set
- class PermissionSet
-
- attr_reader :perm
- # Create a new (empty) PermissionSet
- #
- def initialize
- @perm = {}
- end
-
- # Inspection simply inspects the internal hash
- def inspect
- @perm.inspect
- end
-
- # Sets the permission for command _cmd_ to _val_,
- #
- def set_permission(str, val)
- cmd = str.to_irc_auth_command
- case val
- when true, false
- @perm[cmd.command] = val
- when nil
- @perm.delete(cmd.command)
- else
- raise TypeError, "#{val.inspect} must be true or false" unless [true,false].include?(val)
- end
- end
-
- # Resets the permission for command _cmd_
- #
- def reset_permission(cmd)
- set_permission(cmd, nil)
- end
-
- # Tells if command _cmd_ is permitted. We do this by returning
- # the value of the deepest Command#path that matches.
- #
- def permit?(str)
- cmd = str.to_irc_auth_command
- # TODO user-configurable list of always-allowed commands,
- # for admins that want to set permissions -* for everybody
- return true if cmd.command == :login
- allow = nil
- cmd.path.reverse.each { |k|
- if @perm.has_key?(k)
- allow = @perm[k]
- break
- end
- }
- return allow
- end
-
- end
-
-
- # This is the error that gets raised when an invalid password is met
- #
- class InvalidPassword < RuntimeError
- end
-
-
- # This is the basic class for bot users: they have a username, a
- # password, a list of netmasks to match against, and a list of
- # permissions. A BotUser can be marked as 'transient', usually meaning
- # it's not intended for permanent storage. Transient BotUsers have lower
- # priority than nontransient ones for autologin purposes.
- #
- # To initialize a BotUser, you pass a _username_ and an optional
- # hash of options. Currently, only two options are recognized:
- #
- # transient:: true or false, determines if the BotUser is transient or
- # permanent (default is false, permanent BotUser).
- #
- # Transient BotUsers are initialized by prepending an
- # asterisk (*) to the username, and appending a sanitized
- # version of the object_id. The username can be empty.
- # A random password is generated.
- #
- # Permanent Botusers need the username as is, and no
- # password is generated.
- #
- # masks:: an array of Netmasks to initialize the NetmaskList. This
- # list is used as-is for permanent BotUsers.
- #
- # Transient BotUsers will alter the list elements which are
- # Irc::User by globbing the nick and any initial nonletter
- # part of the ident.
- #
- # The masks option is optional for permanent BotUsers, but
- # obligatory (non-empty) for transients.
- #
- class BotUser
-
- attr_reader :username
- attr_reader :password
- attr_reader :netmasks
- attr_reader :perm
- attr_writer :login_by_mask
- attr_writer :transient
-
- def autologin=(vnew)
- vold = @autologin
- @autologin = vnew
- if vold && !vnew
- @netmasks.each { |n| Auth.manager.maskdb.remove(self, n) }
- elsif vnew && !vold
- @netmasks.each { |n| Auth.manager.maskdb.add(self, n) }
- end
- end
-
- # Checks if the BotUser is transient
- def transient?
- @transient
- end
-
- # Checks if the BotUser is permanent (not transient)
- def permanent?
- !@transient
- end
-
- # Sets if the BotUser is permanent or not
- def permanent=(bool)
- @transient=!bool
- end
-
- # Make the BotUser permanent
- def make_permanent(name)
- raise TypeError, "permanent already" if permanent?
- @username = BotUser.sanitize_username(name)
- @transient = false
- reset_autologin
- reset_password # or not?
- @netmasks.dup.each do |m|
- delete_netmask(m)
- add_netmask(m.generalize)
- end
- end
-
- # Create a new BotUser with given username
- def initialize(username, options={})
- opts = {:transient => false}.merge(options)
- @transient = opts[:transient]
-
- if @transient
- @username = "*"
- @username << BotUser.sanitize_username(username) if username and not username.to_s.empty?
- @username << BotUser.sanitize_username(object_id)
- reset_password
- @login_by_mask=true
- @autologin=true
- else
- @username = BotUser.sanitize_username(username)
- @password = nil
- reset_login_by_mask
- reset_autologin
- end
-
- @netmasks = NetmaskList.new
- if opts.key?(:masks) and opts[:masks]
- masks = opts[:masks]
- masks = [masks] unless masks.respond_to?(:each)
- masks.each { |m|
- mask = m.to_irc_netmask
- if @transient and User === m
- mask.nick = "*"
- mask.host = m.host.dup
- mask.user = "*" + m.user.sub(/^\w?[^\w]+/,'')
- end
- add_netmask(mask) unless mask.to_s == "*"
- }
- end
- raise "must provide a usable mask for transient BotUser #{@username}" if @transient and @netmasks.empty?
-
- @perm = {}
- end
-
- # Inspection
- def inspect
- str = self.__to_s__[0..-2]
- str << " (transient)" if @transient
- str << ":"
- str << " @username=#{@username.inspect}"
- str << " @netmasks=#{@netmasks.inspect}"
- str << " @perm=#{@perm.inspect}"
- str << " @login_by_mask=#{@login_by_mask}"
- str << " @autologin=#{@autologin}"
- str << ">"
- end
-
- # In strings
- def to_s
- @username
- end
-
- # Convert into a hash
- def to_hash
- {
- :username => @username,
- :password => @password,
- :netmasks => @netmasks,
- :perm => @perm,
- :login_by_mask => @login_by_mask,
- :autologin => @autologin,
- }
- end
-
- # Do we allow logging in without providing the password?
- #
- def login_by_mask?
- @login_by_mask
- end
-
- # Reset the login-by-mask option
- #
- def reset_login_by_mask
- @login_by_mask = Auth.manager.bot.config['auth.login_by_mask'] unless defined?(@login_by_mask)
- end
-
- # Reset the autologin option
- #
- def reset_autologin
- @autologin = Auth.manager.bot.config['auth.autologin'] unless defined?(@autologin)
- end
-
- # Do we allow automatic logging in?
- #
- def autologin?
- @autologin
- end
-
- # Restore from hash
- def from_hash(h)
- @username = h[:username] if h.has_key?(:username)
- @password = h[:password] if h.has_key?(:password)
- @login_by_mask = h[:login_by_mask] if h.has_key?(:login_by_mask)
- @autologin = h[:autologin] if h.has_key?(:autologin)
- if h.has_key?(:netmasks)
- @netmasks = h[:netmasks]
- debug @netmasks
- @netmasks.each { |n| Auth.manager.maskdb.add(self, n) } if @autologin
- debug @netmasks
- end
- @perm = h[:perm] if h.has_key?(:perm)
- end
-
- # This method sets the password if the proposed new password
- # is valid
- def password=(pwd=nil)
- pass = pwd.to_s
- if pass.empty?
- reset_password
- else
- begin
- raise InvalidPassword, "#{pass} contains invalid characters" if pass !~ /^[\x21-\x7e]+$/
- raise InvalidPassword, "#{pass} too short" if pass.length < 4
- @password = pass
- rescue InvalidPassword => e
- raise e
- rescue => e
- raise InvalidPassword, "Exception #{e.inspect} while checking #{pass.inspect} (#{pwd.inspect})"
- end
- end
- end
-
- # Resets the password by creating a new onw
- def reset_password
- @password = Auth.random_password
- end
-
- # Sets the permission for command _cmd_ to _val_ on channel _chan_
- #
- def set_permission(cmd, val, chan="*")
- k = chan.to_s.to_sym
- @perm[k] = PermissionSet.new unless @perm.has_key?(k)
- @perm[k].set_permission(cmd, val)
- end
-
- # Resets the permission for command _cmd_ on channel _chan_
- #
- def reset_permission(cmd, chan ="*")
- set_permission(cmd, nil, chan)
- end
-
- # Checks if BotUser is allowed to do something on channel _chan_,
- # or on all channels if _chan_ is nil
- #
- def permit?(cmd, chan=nil)
- if chan
- k = chan.to_s.to_sym
- else
- k = :*
- end
- allow = nil
- if @perm.has_key?(k)
- allow = @perm[k].permit?(cmd)
- end
- return allow
- end
-
- # Adds a Netmask
- #
- def add_netmask(mask)
- m = mask.to_irc_netmask
- @netmasks << m
- if self.autologin?
- Auth.manager.maskdb.add(self, m)
- Auth.manager.logout_transients(m) if self.permanent?
- end
- end
-
- # Removes a Netmask
- #
- def delete_netmask(mask)
- m = mask.to_irc_netmask
- @netmasks.delete(m)
- Auth.manager.maskdb.remove(self, m) if self.autologin?
- end
-
- # Reset Netmasks, clearing @netmasks
- #
- def reset_netmasks
- @netmasks.each { |m|
- Auth.manager.maskdb.remove(self, m) if self.autologin?
- }
- @netmasks.clear
- end
-
- # This method checks if BotUser has a Netmask that matches _user_
- #
- def knows?(usr)
- user = usr.to_irc_user
- !!@netmasks.find { |n| user.matches? n }
- end
-
- # This method gets called when User _user_ wants to log in.
- # It returns true or false depending on whether the password
- # is right. If it is, the Netmask of the user is added to the
- # list of acceptable Netmask unless it's already matched.
- def login(user, password=nil)
- if password == @password or (password.nil? and (@login_by_mask || @autologin) and knows?(user))
- add_netmask(user) unless knows?(user)
- debug "#{user} logged in as #{self.inspect}"
- return true
- else
- return false
- end
- end
-
- # # This method gets called when User _user_ has logged out as this BotUser
- # def logout(user)
- # delete_netmask(user) if knows?(user)
- # end
-
- # This method sanitizes a username by chomping, downcasing
- # and replacing any nonalphanumeric character with _
- #
- def BotUser.sanitize_username(name)
- candidate = name.to_s.chomp.downcase.gsub(/[^a-z0-9]/,"_")
- raise "sanitized botusername #{candidate} too short" if candidate.length < 3
- return candidate
- end
-
- end
-
- # This is the default BotUser: it's used for all users which haven't
- # identified with the bot
- #
- class DefaultBotUserClass < BotUser
-
- private :add_netmask, :delete_netmask
-
- include Singleton
-
- # The default BotUser is named 'everyone'
- #
- def initialize
- reset_login_by_mask
- reset_autologin
- super("everyone")
- @default_perm = PermissionSet.new
- end
-
- # This method returns without changing anything
- #
- def login_by_mask=(val)
- debug "Tried to change the login-by-mask for default bot user, ignoring"
- return @login_by_mask
- end
-
- # The default botuser allows logins by mask
- #
- def reset_login_by_mask
- @login_by_mask = true
- end
-
- # This method returns without changing anything
- #
- def autologin=(val)
- debug "Tried to change the autologin for default bot user, ignoring"
- return
- end
-
- # The default botuser doesn't allow autologin (meaningless)
- #
- def reset_autologin
- @autologin = false
- end
-
- # Sets the default permission for the default user (i.e. the ones
- # set by the BotModule writers) on all channels
- #
- def set_default_permission(cmd, val)
- @default_perm.set_permission(Command.new(cmd), val)
- debug "Default permissions now: #{@default_perm.pretty_inspect}"
- end
-
- # default knows everybody
- #
- def knows?(user)
- return true if user.to_irc_user
- end
-
- # We always allow logging in as the default user
- def login(user, password)
- return true
- end
-
- # DefaultBotUser will check the default_perm after checking
- # the global ones
- # or on all channels if _chan_ is nil
- #
- def permit?(cmd, chan=nil)
- allow = super(cmd, chan)
- if allow.nil? && chan.nil?
- allow = @default_perm.permit?(cmd)
- end
- return allow
- end
-
- end
-
- # Returns the only instance of DefaultBotUserClass
- #
- def Auth.defaultbotuser
- return DefaultBotUserClass.instance
- end
-
- # This is the BotOwner: he can do everything
- #
- class BotOwnerClass < BotUser
-
- include Singleton
-
- def initialize
- @login_by_mask = false
- @autologin = true
- super("owner")
- end
-
- def permit?(cmd, chan=nil)
- return true
- end
-
- end
-
- # Returns the only instance of BotOwnerClass
- #
- def Auth.botowner
- return BotOwnerClass.instance
- end
-
-
- class BotUser
- # Check if the current BotUser is the default one
- def default?
- return DefaultBotUserClass === self
- end
-
- # Check if the current BotUser is the owner
- def owner?
- return BotOwnerClass === self
- end
- end
-
-
- # This is the ManagerClass singleton, used to manage
- # Irc::User/Irc::Bot::Auth::BotUser connections and everything
- #
- class ManagerClass
-
- include Singleton
-
- attr_reader :maskdb
- attr_reader :everyone
- attr_reader :botowner
- attr_reader :bot
-
- # The instance manages two <code>Hash</code>es: one that maps
- # <code>Irc::User</code>s onto <code>BotUser</code>s, and the other that maps
- # usernames onto <code>BotUser</code>
- def initialize
- @everyone = Auth::defaultbotuser
- @botowner = Auth::botowner
- bot_associate(nil)
- end
-
- def bot_associate(bot)
- raise "Cannot associate with a new bot! Save first" if defined?(@has_changes) && @has_changes
-
- reset_hashes
-
- # Associated bot
- @bot = bot
-
- # This variable is set to true when there have been changes
- # to the botusers list, so that we know when to save
- @has_changes = false
- end
-
- def set_changed
- @has_changes = true
- end
-
- def reset_changed
- @has_changes = false
- end
-
- def changed?
- @has_changes
- end
-
- # resets the hashes
- def reset_hashes
- @botusers = Hash.new
- @maskdb = NetmaskDb.new
- @allbotusers = Hash.new
- [everyone, botowner].each do |x|
- @allbotusers[x.username.to_sym] = x
- end
- end
-
- def load_array(ary, forced)
- unless ary
- warning "Tried to load an empty array"
- return
- end
- raise "Won't load with unsaved changes" if @has_changes and not forced
- reset_hashes
- ary.each { |x|
- raise TypeError, "#{x} should be a Hash" unless x.kind_of?(Hash)
- u = x[:username]
- unless include?(u)
- create_botuser(u)
- end
- get_botuser(u).from_hash(x)
- get_botuser(u).transient = false
- }
- @has_changes=false
- end
-
- def save_array
- @allbotusers.values.map { |x|
- x.transient? ? nil : x.to_hash
- }.compact
- end
-
- # checks if we know about a certain BotUser username
- def include?(botusername)
- @allbotusers.has_key?(botusername.to_sym)
- end
-
- # Maps <code>Irc::User</code> to BotUser
- def irc_to_botuser(ircuser)
- logged = @botusers[ircuser.to_irc_user]
- return logged if logged
- return autologin(ircuser)
- end
-
- # creates a new BotUser
- def create_botuser(name, password=nil)
- n = BotUser.sanitize_username(name)
- k = n.to_sym
- raise "botuser #{n} exists" if include?(k)
- bu = BotUser.new(n)
- bu.password = password
- @allbotusers[k] = bu
- return bu
- end
-
- # returns the botuser with name _name_
- def get_botuser(name)
- @allbotusers.fetch(BotUser.sanitize_username(name).to_sym)
- end
-
- # Logs Irc::User _user_ in to BotUser _botusername_ with password _pwd_
- #
- # raises an error if _botusername_ is not a known BotUser username
- #
- # It is possible to autologin by Netmask, on request
- #
- def login(user, botusername, pwd=nil)
- ircuser = user.to_irc_user
- n = BotUser.sanitize_username(botusername)
- k = n.to_sym
- raise "No such BotUser #{n}" unless include?(k)
- if @botusers.has_key?(ircuser)
- return true if @botusers[ircuser].username == n
- # TODO
- # @botusers[ircuser].logout(ircuser)
- end
- bu = @allbotusers[k]
- if bu.login(ircuser, pwd)
- @botusers[ircuser] = bu
- return true
- end
- return false
- end
-
- # Tries to auto-login Irc::User _user_ by looking at the known botusers that allow autologin
- # and trying to login without a password
- #
- def autologin(user)
- ircuser = user.to_irc_user
- debug "Trying to autologin #{ircuser}"
- return @botusers[ircuser] if @botusers.has_key?(ircuser)
- bu = maskdb.find(ircuser)
- if bu
- debug "trying #{bu}"
- bu.login(ircuser) or raise '...what?!'
- @botusers[ircuser] = bu
- return bu
- end
- # Finally, create a transient if we're set to allow it
- if @bot.config['auth.autouser']
- bu = create_transient_botuser(ircuser)
- @botusers[ircuser] = bu
- return bu
- end
- return everyone
- end
-
- # Creates a new transient BotUser associated with Irc::User _user_,
- # automatically logging him in. Note that transient botuser creation can
- # fail, typically if we don't have the complete user netmask (e.g. for
- # messages coming in from a linkbot)
- #
- def create_transient_botuser(user)
- ircuser = user.to_irc_user
- bu = everyone
- begin
- bu = BotUser.new(ircuser, :transient => true, :masks => ircuser)
- bu.login(ircuser)
- rescue
- warning "failed to create transient for #{user}"
- error $!
- end
- return bu
- end
-
- # Logs out any Irc::User matching Irc::Netmask _m_ and logged in
- # to a transient BotUser
- #
- def logout_transients(m)
- debug "to check: #{@botusers.keys.join ' '}"
- @botusers.keys.each do |iu|
- debug "checking #{iu.fullform} against #{m.fullform}"
- bu = @botusers[iu]
- bu.transient? or next
- iu.matches?(m) or next
- @botusers.delete(iu).autologin = false
- end
- end
-
- # Makes transient BotUser _user_ into a permanent BotUser
- # named _name_; if _user_ is an Irc::User, act on the transient
- # BotUser (if any) it's logged in as
- #
- def make_permanent(user, name)
- buname = BotUser.sanitize_username(name)
- # TODO merge BotUser instead?
- raise "there's already a BotUser called #{name}" if include?(buname)
-
- tuser = nil
- case user
- when String, Irc::User
- tuser = irc_to_botuser(user)
- when BotUser
- tuser = user
- else
- raise TypeError, "sorry, don't know how to make #{user.class} into a permanent BotUser"
- end
- return nil unless tuser
- raise TypeError, "#{tuser} is not transient" unless tuser.transient?
-
- tuser.make_permanent(buname)
- @allbotusers[tuser.username.to_sym] = tuser
-
- return tuser
- end
-
- # Checks if User _user_ can do _cmd_ on _chan_.
- #
- # Permission are checked in this order, until a true or false
- # is returned:
- # * associated BotUser on _chan_
- # * associated BotUser on all channels
- # * everyone on _chan_
- # * everyone on all channels
- #
- def permit?(user, cmdtxt, channel=nil)
- if user.class <= BotUser
- botuser = user
- else
- botuser = irc_to_botuser(user)
- end
- cmd = cmdtxt.to_irc_auth_command
-
- chan = channel
- case chan
- when User
- chan = "?"
- when Channel
- chan = chan.name
- end
-
- allow = nil
-
- allow = botuser.permit?(cmd, chan) if chan
- return allow unless allow.nil?
- allow = botuser.permit?(cmd)
- return allow unless allow.nil?
-
- unless botuser == everyone
- allow = everyone.permit?(cmd, chan) if chan
- return allow unless allow.nil?
- allow = everyone.permit?(cmd)
- return allow unless allow.nil?
- end
-
- raise "Could not check permission for user #{user.inspect} to run #{cmdtxt.inspect} on #{chan.inspect}"
- end
-
- # Checks if command _cmd_ is allowed to User _user_ on _chan_, optionally
- # telling if the user is authorized
- #
- def allow?(cmdtxt, user, chan=nil)
- if permit?(user, cmdtxt, chan)
- return true
- else
- # cmds = cmdtxt.split('::')
- # @bot.say chan, "you don't have #{cmds.last} (#{cmds.first}) permissions here" if chan
- @bot.say chan, _("%{user}, you don't have '%{command}' permissions here") %
- {:user=>user, :command=>cmdtxt} if chan
- return false
- end
- end
-
- end
-
- # Returns the only instance of ManagerClass
- #
- def Auth.manager
- return ManagerClass.instance
- end
-
- end
-end
-
- class User
-
- # A convenience method to automatically found the botuser
- # associated with the receiver
- #
- def botuser
- Irc::Bot::Auth.manager.irc_to_botuser(self)
- end
- end
-
-end
+#-- vim:sw=2:et
+#++
+# :title: User management
+#
+# rbot user management
+# Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)
+# Copyright:: Copyright (c) 2006 Giuseppe Bilotta
+# License:: GPLv2
+
+require 'singleton'
+require 'set'
+require 'rbot/maskdb'
+
+# This would be a good idea if it was failproof, but the truth
+# is that other methods can indirectly modify the hash. *sigh*
+#
+# class AuthNotifyingHash < Hash
+# %w(clear default= delete delete_if replace invert
+# merge! update rehash reject! replace shift []= store).each { |m|
+# class_eval {
+# define_method(m) { |*a|
+# r = super(*a)
+# Irc::Bot::Auth.manager.set_changed
+# r
+# }
+# }
+# }
+# end
+#
+
+module Irc
+class Bot
+
+
+ # This module contains the actual Authentication stuff
+ #
+ module Auth
+
+ Config.register Config::StringValue.new( 'auth.password',
+ :default => 'rbotauth', :wizard => true,
+ :on_change => Proc.new {|bot, v| bot.auth.botowner.password = v},
+ :desc => _('Password for the bot owner'))
+ Config.register Config::BooleanValue.new( 'auth.login_by_mask',
+ :default => 'true',
+ :desc => _('Set false to prevent new botusers from logging in without a password when the user netmask is known'))
+ Config.register Config::BooleanValue.new( 'auth.autologin',
+ :default => 'true',
+ :desc => _('Set false to prevent new botusers from recognizing IRC users without a need to manually login'))
+ Config.register Config::BooleanValue.new( 'auth.autouser',
+ :default => 'false',
+ :desc => _('Set true to allow new botusers to be created automatically'))
+ # Config.register Config::IntegerValue.new( 'auth.default_level',
+ # :default => 10, :wizard => true,
+ # :desc => 'The default level for new/unknown users' )
+
+ # Generate a random password of length _l_
+ #
+ def Auth.random_password(l=8)
+ pwd = ""
+ l.times do
+ pwd << (rand(26) + (rand(2) == 0 ? 65 : 97) ).chr
+ end
+ return pwd
+ end
+
+
+ # An Irc::Bot::Auth::Command defines a command by its "path":
+ #
+ # base::command::subcommand::subsubcommand::subsubsubcommand
+ #
+ class Command
+
+ attr_reader :command, :path
+
+ # A method that checks if a given _cmd_ is in a form that can be
+ # reduced into a canonical command path, and if so, returns it
+ #
+ def sanitize_command_path(cmd)
+ pre = cmd.to_s.downcase.gsub(/^\*?(?:::)?/,"").gsub(/::$/,"")
+ return pre if pre.empty?
+ return pre if pre =~ /^\S+(::\S+)*$/
+ raise TypeError, "#{cmd.inspect} is not a valid command"
+ end
+
+ # Creates a new Command from a given string; you can then access
+ # the command as a symbol with the :command method and the whole
+ # path as :path
+ #
+ # Command.new("core::auth::save").path => [:"*", :"core", :"core::auth", :"core::auth::save"]
+ #
+ # Command.new("core::auth::save").command => :"core::auth::save"
+ #
+ def initialize(cmd)
+ cmdpath = sanitize_command_path(cmd).split('::')
+ seq = cmdpath.inject(["*"]) { |list, cmd|
+ list << (list.length > 1 ? list.last + "::" : "") + cmd
+ }
+ @path = seq.map { |k|
+ k.to_sym
+ }
+ @command = path.last
+ debug "Created command #{@command.inspect} with path #{@path.pretty_inspect}"
+ end
+
+ # Returs self
+ def to_irc_auth_command
+ self
+ end
+
+ end
+
+ end
+
+end
+end
+
+
+class String
+
+ # Returns an Irc::Bot::Auth::Comand from the receiver
+ def to_irc_auth_command
+ Irc::Bot::Auth::Command.new(self)
+ end
+
+end
+
+
+class Symbol
+
+ # Returns an Irc::Bot::Auth::Comand from the receiver
+ def to_irc_auth_command
+ Irc::Bot::Auth::Command.new(self)
+ end
+
+end
+
+
+module Irc
+class Bot
+
+
+ module Auth
+
+
+ # This class describes a permission set
+ class PermissionSet
+
+ attr_reader :perm
+ # Create a new (empty) PermissionSet
+ #
+ def initialize
+ @perm = {}
+ end
+
+ # Inspection simply inspects the internal hash
+ def inspect
+ @perm.inspect
+ end
+
+ # Sets the permission for command _cmd_ to _val_,
+ #
+ def set_permission(str, val)
+ cmd = str.to_irc_auth_command
+ case val
+ when true, false
+ @perm[cmd.command] = val
+ when nil
+ @perm.delete(cmd.command)
+ else
+ raise TypeError, "#{val.inspect} must be true or false" unless [true,false].include?(val)
+ end
+ end
+
+ # Resets the permission for command _cmd_
+ #
+ def reset_permission(cmd)
+ set_permission(cmd, nil)
+ end
+
+ # Tells if command _cmd_ is permitted. We do this by returning
+ # the value of the deepest Command#path that matches.
+ #
+ def permit?(str)
+ cmd = str.to_irc_auth_command
+ # TODO user-configurable list of always-allowed commands,
+ # for admins that want to set permissions -* for everybody
+ return true if cmd.command == :login
+ allow = nil
+ cmd.path.reverse.each { |k|
+ if @perm.has_key?(k)
+ allow = @perm[k]
+ break
+ end
+ }
+ return allow
+ end
+
+ end
+
+
+ # This is the error that gets raised when an invalid password is met
+ #
+ class InvalidPassword < RuntimeError
+ end
+
+
+ # This is the basic class for bot users: they have a username, a
+ # password, a list of netmasks to match against, and a list of
+ # permissions. A BotUser can be marked as 'transient', usually meaning
+ # it's not intended for permanent storage. Transient BotUsers have lower
+ # priority than nontransient ones for autologin purposes.
+ #
+ # To initialize a BotUser, you pass a _username_ and an optional
+ # hash of options. Currently, only two options are recognized:
+ #
+ # transient:: true or false, determines if the BotUser is transient or
+ # permanent (default is false, permanent BotUser).
+ #
+ # Transient BotUsers are initialized by prepending an
+ # asterisk (*) to the username, and appending a sanitized
+ # version of the object_id. The username can be empty.
+ # A random password is generated.
+ #
+ # Permanent Botusers need the username as is, and no
+ # password is generated.
+ #
+ # masks:: an array of Netmasks to initialize the NetmaskList. This
+ # list is used as-is for permanent BotUsers.
+ #
+ # Transient BotUsers will alter the list elements which are
+ # Irc::User by globbing the nick and any initial nonletter
+ # part of the ident.
+ #
+ # The masks option is optional for permanent BotUsers, but
+ # obligatory (non-empty) for transients.
+ #
+ class BotUser
+
+ attr_reader :username
+ attr_reader :password
+ attr_reader :netmasks
+ attr_reader :perm
+ attr_writer :login_by_mask
+ attr_writer :transient
+
+ def autologin=(vnew)
+ vold = @autologin
+ @autologin = vnew
+ if vold && !vnew
+ @netmasks.each { |n| Auth.manager.maskdb.remove(self, n) }
+ elsif vnew && !vold
+ @netmasks.each { |n| Auth.manager.maskdb.add(self, n) }
+ end
+ end
+
+ # Checks if the BotUser is transient
+ def transient?
+ @transient
+ end
+
+ # Checks if the BotUser is permanent (not transient)
+ def permanent?
+ !@transient
+ end
+
+ # Sets if the BotUser is permanent or not
+ def permanent=(bool)
+ @transient=!bool
+ end
+
+ # Make the BotUser permanent
+ def make_permanent(name)
+ raise TypeError, "permanent already" if permanent?
+ @username = BotUser.sanitize_username(name)
+ @transient = false
+ reset_autologin
+ reset_password # or not?
+ @netmasks.dup.each do |m|
+ delete_netmask(m)
+ add_netmask(m.generalize)
+ end
+ end
+
+ # Create a new BotUser with given username
+ def initialize(username, options={})
+ opts = {:transient => false}.merge(options)
+ @transient = opts[:transient]
+
+ if @transient
+ @username = "*"
+ @username << BotUser.sanitize_username(username) if username and not username.to_s.empty?
+ @username << BotUser.sanitize_username(object_id)
+ reset_password
+ @login_by_mask=true
+ @autologin=true
+ else
+ @username = BotUser.sanitize_username(username)
+ @password = nil
+ reset_login_by_mask
+ reset_autologin
+ end
+
+ @netmasks = NetmaskList.new
+ if opts.key?(:masks) and opts[:masks]
+ masks = opts[:masks]
+ masks = [masks] unless masks.respond_to?(:each)
+ masks.each { |m|
+ mask = m.to_irc_netmask
+ if @transient and User === m
+ mask.nick = "*"
+ mask.host = m.host.dup
+ mask.user = "*" + m.user.sub(/^\w?[^\w]+/,'')
+ end
+ add_netmask(mask) unless mask.to_s == "*"
+ }
+ end
+ raise "must provide a usable mask for transient BotUser #{@username}" if @transient and @netmasks.empty?
+
+ @perm = {}
+ end
+
+ # Inspection
+ def inspect
+ str = self.__to_s__[0..-2]
+ str << " (transient)" if @transient
+ str << ":"
+ str << " @username=#{@username.inspect}"
+ str << " @netmasks=#{@netmasks.inspect}"
+ str << " @perm=#{@perm.inspect}"
+ str << " @login_by_mask=#{@login_by_mask}"
+ str << " @autologin=#{@autologin}"
+ str << ">"
+ end
+
+ # In strings
+ def to_s
+ @username
+ end
+
+ # Convert into a hash
+ def to_hash
+ {
+ :username => @username,
+ :password => @password,
+ :netmasks => @netmasks,
+ :perm => @perm,
+ :login_by_mask => @login_by_mask,
+ :autologin => @autologin,
+ }
+ end
+
+ # Do we allow logging in without providing the password?
+ #
+ def login_by_mask?
+ @login_by_mask
+ end
+
+ # Reset the login-by-mask option
+ #
+ def reset_login_by_mask
+ @login_by_mask = Auth.manager.bot.config['auth.login_by_mask'] unless defined?(@login_by_mask)
+ end
+
+ # Reset the autologin option
+ #
+ def reset_autologin
+ @autologin = Auth.manager.bot.config['auth.autologin'] unless defined?(@autologin)
+ end
+
+ # Do we allow automatic logging in?
+ #
+ def autologin?
+ @autologin
+ end
+
+ # Restore from hash
+ def from_hash(h)
+ @username = h[:username] if h.has_key?(:username)
+ @password = h[:password] if h.has_key?(:password)
+ @login_by_mask = h[:login_by_mask] if h.has_key?(:login_by_mask)
+ @autologin = h[:autologin] if h.has_key?(:autologin)
+ if h.has_key?(:netmasks)
+ @netmasks = h[:netmasks]
+ debug @netmasks
+ @netmasks.each { |n| Auth.manager.maskdb.add(self, n) } if @autologin
+ debug @netmasks
+ end
+ @perm = h[:perm] if h.has_key?(:perm)
+ end
+
+ # This method sets the password if the proposed new password
+ # is valid
+ def password=(pwd=nil)
+ pass = pwd.to_s
+ if pass.empty?
+ reset_password
+ else
+ begin
+ raise InvalidPassword, "#{pass} contains invalid characters" if pass !~ /^[\x21-\x7e]+$/
+ raise InvalidPassword, "#{pass} too short" if pass.length < 4
+ @password = pass
+ rescue InvalidPassword => e
+ raise e
+ rescue => e
+ raise InvalidPassword, "Exception #{e.inspect} while checking #{pass.inspect} (#{pwd.inspect})"
+ end
+ end
+ end
+
+ # Resets the password by creating a new onw
+ def reset_password
+ @password = Auth.random_password
+ end
+
+ # Sets the permission for command _cmd_ to _val_ on channel _chan_
+ #
+ def set_permission(cmd, val, chan="*")
+ k = chan.to_s.to_sym
+ @perm[k] = PermissionSet.new unless @perm.has_key?(k)
+ @perm[k].set_permission(cmd, val)
+ end
+
+ # Resets the permission for command _cmd_ on channel _chan_
+ #
+ def reset_permission(cmd, chan ="*")
+ set_permission(cmd, nil, chan)
+ end
+
+ # Checks if BotUser is allowed to do something on channel _chan_,
+ # or on all channels if _chan_ is nil
+ #
+ def permit?(cmd, chan=nil)
+ if chan
+ k = chan.to_s.to_sym
+ else
+ k = :*
+ end
+ allow = nil
+ if @perm.has_key?(k)
+ allow = @perm[k].permit?(cmd)
+ end
+ return allow
+ end
+
+ # Adds a Netmask
+ #
+ def add_netmask(mask)
+ m = mask.to_irc_netmask
+ @netmasks << m
+ if self.autologin?
+ Auth.manager.maskdb.add(self, m)
+ Auth.manager.logout_transients(m) if self.permanent?
+ end
+ end
+
+ # Removes a Netmask
+ #
+ def delete_netmask(mask)
+ m = mask.to_irc_netmask
+ @netmasks.delete(m)
+ Auth.manager.maskdb.remove(self, m) if self.autologin?
+ end
+
+ # Reset Netmasks, clearing @netmasks
+ #
+ def reset_netmasks
+ @netmasks.each { |m|
+ Auth.manager.maskdb.remove(self, m) if self.autologin?
+ }
+ @netmasks.clear
+ end
+
+ # This method checks if BotUser has a Netmask that matches _user_
+ #
+ def knows?(usr)
+ user = usr.to_irc_user
+ !!@netmasks.find { |n| user.matches? n }
+ end
+
+ # This method gets called when User _user_ wants to log in.
+ # It returns true or false depending on whether the password
+ # is right. If it is, the Netmask of the user is added to the
+ # list of acceptable Netmask unless it's already matched.
+ def login(user, password=nil)
+ if password == @password or (password.nil? and (@login_by_mask || @autologin) and knows?(user))
+ add_netmask(user) unless knows?(user)
+ debug "#{user} logged in as #{self.inspect}"
+ return true
+ else
+ return false
+ end
+ end
+
+ # # This method gets called when User _user_ has logged out as this BotUser
+ # def logout(user)
+ # delete_netmask(user) if knows?(user)
+ # end
+
+ # This method sanitizes a username by chomping, downcasing
+ # and replacing any nonalphanumeric character with _
+ #
+ def BotUser.sanitize_username(name)
+ candidate = name.to_s.chomp.downcase.gsub(/[^a-z0-9]/,"_")
+ raise "sanitized botusername #{candidate} too short" if candidate.length < 3
+ return candidate
+ end
+
+ end
+
+ # This is the default BotUser: it's used for all users which haven't
+ # identified with the bot
+ #
+ class DefaultBotUserClass < BotUser
+
+ private :add_netmask, :delete_netmask
+
+ include Singleton
+
+ # The default BotUser is named 'everyone'
+ #
+ def initialize
+ reset_login_by_mask
+ reset_autologin
+ super("everyone")
+ @default_perm = PermissionSet.new
+ end
+
+ # This method returns without changing anything
+ #
+ def login_by_mask=(val)
+ debug "Tried to change the login-by-mask for default bot user, ignoring"
+ return @login_by_mask
+ end
+
+ # The default botuser allows logins by mask
+ #
+ def reset_login_by_mask
+ @login_by_mask = true
+ end
+
+ # This method returns without changing anything
+ #
+ def autologin=(val)
+ debug "Tried to change the autologin for default bot user, ignoring"
+ return
+ end
+
+ # The default botuser doesn't allow autologin (meaningless)
+ #
+ def reset_autologin
+ @autologin = false
+ end
+
+ # Sets the default permission for the default user (i.e. the ones
+ # set by the BotModule writers) on all channels
+ #
+ def set_default_permission(cmd, val)
+ @default_perm.set_permission(Command.new(cmd), val)
+ debug "Default permissions now: #{@default_perm.pretty_inspect}"
+ end
+
+ # default knows everybody
+ #
+ def knows?(user)
+ return true if user.to_irc_user
+ end
+
+ # We always allow logging in as the default user
+ def login(user, password)
+ return true
+ end
+
+ # DefaultBotUser will check the default_perm after checking
+ # the global ones
+ # or on all channels if _chan_ is nil
+ #
+ def permit?(cmd, chan=nil)
+ allow = super(cmd, chan)
+ if allow.nil? && chan.nil?
+ allow = @default_perm.permit?(cmd)
+ end
+ return allow
+ end
+
+ end
+
+ # Returns the only instance of DefaultBotUserClass
+ #
+ def Auth.defaultbotuser
+ return DefaultBotUserClass.instance
+ end
+
+ # This is the BotOwner: he can do everything
+ #
+ class BotOwnerClass < BotUser
+
+ include Singleton
+
+ def initialize
+ @login_by_mask = false
+ @autologin = true
+ super("owner")
+ end
+
+ def permit?(cmd, chan=nil)
+ return true
+ end
+
+ end
+
+ # Returns the only instance of BotOwnerClass
+ #
+ def Auth.botowner
+ return BotOwnerClass.instance
+ end
+
+
+ class BotUser
+ # Check if the current BotUser is the default one
+ def default?
+ return DefaultBotUserClass === self
+ end
+
+ # Check if the current BotUser is the owner
+ def owner?
+ return BotOwnerClass === self
+ end
+ end
+
+
+ # This is the ManagerClass singleton, used to manage
+ # Irc::User/Irc::Bot::Auth::BotUser connections and everything
+ #
+ class ManagerClass
+
+ include Singleton
+
+ attr_reader :maskdb
+ attr_reader :everyone
+ attr_reader :botowner
+ attr_reader :bot
+
+ # The instance manages two <code>Hash</code>es: one that maps
+ # <code>Irc::User</code>s onto <code>BotUser</code>s, and the other that maps
+ # usernames onto <code>BotUser</code>
+ def initialize
+ @everyone = Auth::defaultbotuser
+ @botowner = Auth::botowner
+ bot_associate(nil)
+ end
+
+ def bot_associate(bot)
+ raise "Cannot associate with a new bot! Save first" if defined?(@has_changes) && @has_changes
+
+ reset_hashes
+
+ # Associated bot
+ @bot = bot
+
+ # This variable is set to true when there have been changes
+ # to the botusers list, so that we know when to save
+ @has_changes = false
+ end
+
+ def set_changed
+ @has_changes = true
+ end
+
+ def reset_changed
+ @has_changes = false
+ end
+
+ def changed?
+ @has_changes
+ end
+
+ # resets the hashes
+ def reset_hashes
+ @botusers = Hash.new
+ @maskdb = NetmaskDb.new
+ @allbotusers = Hash.new
+ [everyone, botowner].each do |x|
+ @allbotusers[x.username.to_sym] = x
+ end
+ end
+
+ def load_array(ary, forced)
+ unless ary
+ warning "Tried to load an empty array"
+ return
+ end
+ raise "Won't load with unsaved changes" if @has_changes and not forced
+ reset_hashes
+ ary.each { |x|
+ raise TypeError, "#{x} should be a Hash" unless x.kind_of?(Hash)
+ u = x[:username]
+ unless include?(u)
+ create_botuser(u)
+ end
+ get_botuser(u).from_hash(x)
+ get_botuser(u).transient = false
+ }
+ @has_changes=false
+ end
+
+ def save_array
+ @allbotusers.values.map { |x|
+ x.transient? ? nil : x.to_hash
+ }.compact
+ end
+
+ # checks if we know about a certain BotUser username
+ def include?(botusername)
+ @allbotusers.has_key?(botusername.to_sym)
+ end
+
+ # Maps <code>Irc::User</code> to BotUser
+ def irc_to_botuser(ircuser)
+ logged = @botusers[ircuser.to_irc_user]
+ return logged if logged
+ return autologin(ircuser)
+ end
+
+ # creates a new BotUser
+ def create_botuser(name, password=nil)
+ n = BotUser.sanitize_username(name)
+ k = n.to_sym
+ raise "botuser #{n} exists" if include?(k)
+ bu = BotUser.new(n)
+ bu.password = password
+ @allbotusers[k] = bu
+ return bu
+ end
+
+ # returns the botuser with name _name_
+ def get_botuser(name)
+ @allbotusers.fetch(BotUser.sanitize_username(name).to_sym)
+ end
+
+ # Logs Irc::User _user_ in to BotUser _botusername_ with password _pwd_
+ #
+ # raises an error if _botusername_ is not a known BotUser username
+ #
+ # It is possible to autologin by Netmask, on request
+ #
+ def login(user, botusername, pwd=nil)
+ ircuser = user.to_irc_user
+ n = BotUser.sanitize_username(botusername)
+ k = n.to_sym
+ raise "No such BotUser #{n}" unless include?(k)
+ if @botusers.has_key?(ircuser)
+ return true if @botusers[ircuser].username == n
+ # TODO
+ # @botusers[ircuser].logout(ircuser)
+ end
+ bu = @allbotusers[k]
+ if bu.login(ircuser, pwd)
+ @botusers[ircuser] = bu
+ return true
+ end
+ return false
+ end
+
+ # Tries to auto-login Irc::User _user_ by looking at the known botusers that allow autologin
+ # and trying to login without a password
+ #
+ def autologin(user)
+ ircuser = user.to_irc_user
+ debug "Trying to autologin #{ircuser}"
+ return @botusers[ircuser] if @botusers.has_key?(ircuser)
+ bu = maskdb.find(ircuser)
+ if bu
+ debug "trying #{bu}"
+ bu.login(ircuser) or raise '...what?!'
+ @botusers[ircuser] = bu
+ return bu
+ end
+ # Finally, create a transient if we're set to allow it
+ if @bot.config['auth.autouser']
+ bu = create_transient_botuser(ircuser)
+ @botusers[ircuser] = bu
+ return bu
+ end
+ return everyone
+ end
+
+ # Creates a new transient BotUser associated with Irc::User _user_,
+ # automatically logging him in. Note that transient botuser creation can
+ # fail, typically if we don't have the complete user netmask (e.g. for
+ # messages coming in from a linkbot)
+ #
+ def create_transient_botuser(user)
+ ircuser = user.to_irc_user
+ bu = everyone
+ begin
+ bu = BotUser.new(ircuser, :transient => true, :masks => ircuser)
+ bu.login(ircuser)
+ rescue
+ warning "failed to create transient for #{user}"
+ error $!
+ end
+ return bu
+ end
+
+ # Logs out any Irc::User matching Irc::Netmask _m_ and logged in
+ # to a transient BotUser
+ #
+ def logout_transients(m)
+ debug "to check: #{@botusers.keys.join ' '}"
+ @botusers.keys.each do |iu|
+ debug "checking #{iu.fullform} against #{m.fullform}"
+ bu = @botusers[iu]
+ bu.transient? or next
+ iu.matches?(m) or next
+ @botusers.delete(iu).autologin = false
+ end
+ end
+
+ # Makes transient BotUser _user_ into a permanent BotUser
+ # named _name_; if _user_ is an Irc::User, act on the transient
+ # BotUser (if any) it's logged in as
+ #
+ def make_permanent(user, name)
+ buname = BotUser.sanitize_username(name)
+ # TODO merge BotUser instead?
+ raise "there's already a BotUser called #{name}" if include?(buname)
+
+ tuser = nil
+ case user
+ when String, Irc::User
+ tuser = irc_to_botuser(user)
+ when BotUser
+ tuser = user
+ else
+ raise TypeError, "sorry, don't know how to make #{user.class} into a permanent BotUser"
+ end
+ return nil unless tuser
+ raise TypeError, "#{tuser} is not transient" unless tuser.transient?
+
+ tuser.make_permanent(buname)
+ @allbotusers[tuser.username.to_sym] = tuser
+
+ return tuser
+ end
+
+ # Checks if User _user_ can do _cmd_ on _chan_.
+ #
+ # Permission are checked in this order, until a true or false
+ # is returned:
+ # * associated BotUser on _chan_
+ # * associated BotUser on all channels
+ # * everyone on _chan_
+ # * everyone on all channels
+ #
+ def permit?(user, cmdtxt, channel=nil)
+ if user.class <= BotUser
+ botuser = user
+ else
+ botuser = irc_to_botuser(user)
+ end
+ cmd = cmdtxt.to_irc_auth_command
+
+ chan = channel
+ case chan
+ when User
+ chan = "?"
+ when Channel
+ chan = chan.name
+ end
+
+ allow = nil
+
+ allow = botuser.permit?(cmd, chan) if chan
+ return allow unless allow.nil?
+ allow = botuser.permit?(cmd)
+ return allow unless allow.nil?
+
+ unless botuser == everyone
+ allow = everyone.permit?(cmd, chan) if chan
+ return allow unless allow.nil?
+ allow = everyone.permit?(cmd)
+ return allow unless allow.nil?
+ end
+
+ raise "Could not check permission for user #{user.inspect} to run #{cmdtxt.inspect} on #{chan.inspect}"
+ end
+
+ # Checks if command _cmd_ is allowed to User _user_ on _chan_, optionally
+ # telling if the user is authorized
+ #
+ def allow?(cmdtxt, user, chan=nil)
+ if permit?(user, cmdtxt, chan)
+ return true
+ else
+ # cmds = cmdtxt.split('::')
+ # @bot.say chan, "you don't have #{cmds.last} (#{cmds.first}) permissions here" if chan
+ @bot.say chan, _("%{user}, you don't have '%{command}' permissions here") %
+ {:user=>user, :command=>cmdtxt} if chan
+ return false
+ end
+ end
+
+ end
+
+ # Returns the only instance of ManagerClass
+ #
+ def Auth.manager
+ return ManagerClass.instance
+ end
+
+ end
+end
+
+ class User
+
+ # A convenience method to automatically found the botuser
+ # associated with the receiver
+ #
+ def botuser
+ Irc::Bot::Auth.manager.irc_to_botuser(self)
+ end
+ end
+
+end
diff --git a/lib/rbot/core/auth.rb b/lib/rbot/core/auth.rb
index b0aa12c1..d6167535 100644
--- a/lib/rbot/core/auth.rb
+++ b/lib/rbot/core/auth.rb
@@ -1,968 +1,968 @@
-#-- vim:sw=2:et
-#++
-#
-# :title: rbot auth management from IRC
-#
-# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
-# Copyright:: (C) 2006,2007 Giuseppe Bilotta
-# License:: GPL v2
-
-class AuthModule < CoreBotModule
-
- def initialize
- super
-
- # The namespace migration causes each Irc::Auth::PermissionSet to be
- # unrecoverable, and we have to rename their class name to
- # Irc::Bot::Auth::PermissionSet
- @registry.recovery = Proc.new { |val|
- patched = val.sub("o:\035Irc::Auth::PermissionSet", "o:\042Irc::Bot::Auth::PermissionSet")
- Marshal.restore(patched)
- }
-
- load_array(:default, true)
- debug "initialized auth. Botusers: #{@bot.auth.save_array.pretty_inspect}"
- end
-
- def save
- save_array
- end
-
- def save_array(key=:default)
- if @bot.auth.changed?
- @registry[key] = @bot.auth.save_array
- @bot.auth.reset_changed
- debug "saved botusers (#{key}): #{@registry[key].pretty_inspect}"
- end
- end
-
- def load_array(key=:default, forced=false)
- debug "loading botusers (#{key}): #{@registry[key].pretty_inspect}"
- @bot.auth.load_array(@registry[key], forced) if @registry.has_key?(key)
- end
-
- # The permission parameters accept arguments with the following syntax:
- # cmd_path... [on #chan .... | in here | in private]
- # This auxiliary method scans the array _ar_ to see if it matches
- # the given syntax: it expects + or - signs in front of _cmd_path_
- # elements when _setting_ = true
- #
- # It returns an array whose first element is the array of cmd_path,
- # the second element is an array of locations and third an array of
- # warnings occurred while parsing the strings
- #
- def parse_args(ar, setting)
- cmds = []
- locs = []
- warns = []
- doing_cmds = true
- next_must_be_chan = false
- want_more = false
- last_idx = 0
- ar.each_with_index { |x, i|
- if doing_cmds # parse cmd_path
- # check if the list is done
- if x == "on" or x == "in"
- doing_cmds = false
- next_must_be_chan = true if x == "on"
- next
- end
- if "+-".include?(x[0])
- warns << ArgumentError.new(_("please do not use + or - in front of command %{command} when resetting") % {:command => x}) unless setting
- else
- warns << ArgumentError.new(_("+ or - expected in front of %{string}") % {:string => x}) if setting
- end
- cmds << x
- else # parse locations
- if x[-1].chr == ','
- want_more = true
- else
- want_more = false
- end
- case next_must_be_chan
- when false
- locs << x.gsub(/^here$/,'_').gsub(/^private$/,'?')
- else
- warns << ArgumentError.new(_("'%{string}' doesn't look like a channel name") % {:string => x}) unless @bot.server.supports[:chantypes].include?(x[0])
- locs << x
- end
- unless want_more
- last_idx = i
- break
- end
- end
- }
- warns << _("trailing comma") if want_more
- warns << _("you probably forgot a comma") unless last_idx == ar.length - 1
- return cmds, locs, warns
- end
-
- def auth_edit_perm(m, params)
-
- setting = m.message.split[1] == "set"
- splits = params[:args]
-
- has_for = splits[-2] == "for"
- return usage(m) unless has_for
-
- begin
- user = @bot.auth.get_botuser(splits[-1].sub(/^all$/,"everyone"))
- rescue
- return m.reply(_("couldn't find botuser %{name}") % {:name => splits[-1]})
- end
- return m.reply(_("you can't change permissions for %{username}") % {:username => user.username}) if user.owner?
- splits.slice!(-2,2) if has_for
-
- cmds, locs, warns = parse_args(splits, setting)
- errs = warns.select { |w| w.kind_of?(Exception) }
-
- unless errs.empty?
- m.reply _("couldn't satisfy your request: %{errors}") % {:errors => errs.join(',')}
- return
- end
-
- if locs.empty?
- locs << "*"
- end
- begin
- locs.each { |loc|
- ch = loc
- if m.private?
- ch = "?" if loc == "_"
- else
- ch = m.target.to_s if loc == "_"
- end
- cmds.each { |setval|
- if setting
- val = setval[0].chr == '+'
- cmd = setval[1..-1]
- user.set_permission(cmd, val, ch)
- else
- cmd = setval
- user.reset_permission(cmd, ch)
- end
- }
- }
- rescue => e
- m.reply "something went wrong while trying to set the permissions"
- raise
- end
- @bot.auth.set_changed
- debug "user #{user} permissions changed"
- m.okay
- end
-
- def auth_view_perm(m, params)
- begin
- if params[:user].nil?
- user = get_botusername_for(m.source)
- return m.reply(_("you are owner, you can do anything")) if user.owner?
- else
- user = @bot.auth.get_botuser(params[:user].sub(/^all$/,"everyone"))
- return m.reply(_("owner can do anything")) if user.owner?
- end
- rescue
- return m.reply(_("couldn't find botuser %{name}") % {:name => params[:user]})
- end
- perm = user.perm
- str = []
- perm.each { |k, val|
- next if val.perm.empty?
- case k
- when :*
- str << _("on any channel: ")
- when :"?"
- str << _("in private: ")
- else
- str << _("on #{k}: ")
- end
- sub = []
- val.perm.each { |cmd, bool|
- sub << (bool ? "+" : "-")
- sub.last << cmd.to_s
- }
- str.last << sub.join(', ')
- }
- if str.empty?
- m.reply _("no permissions set for %{user}") % {:user => user.username}
- else
- m.reply _("permissions for %{user}:: %{permissions}") %
- { :user => user.username, :permissions => str.join('; ')}
- end
- end
-
- def auth_search_perm(m, p)
- pattern = Regexp.new(p[:pattern].to_s)
- results = @bot.plugins.maps.select { |k, v| k.match(pattern) }
- count = results.length
- max = @bot.config['send.max_lines']
- extra = (count > max ? _(". only %{max} will be shown") : "") % { :max => max }
- m.reply _("%{count} commands found matching %{pattern}%{extra}") % {
- :count => count, :pattern => pattern, :extra => extra
- }
- return if count == 0
- results[0,max].each { |cmd, hash|
- m.reply _("%{cmd}: %{perms}") % {
- :cmd => cmd,
- :perms => hash[:auth].join(", ")
- }
- }
- end
-
- def get_botuser_for(user)
- @bot.auth.irc_to_botuser(user)
- end
-
- def get_botusername_for(user)
- get_botuser_for(user).username
- end
-
- def welcome(user)
- _("welcome, %{user}") % {:user => get_botusername_for(user)}
- end
-
- def auth_auth(m, params)
- params[:botuser] = 'owner'
- auth_login(m,params)
- end
-
- def auth_login(m, params)
- begin
- case @bot.auth.login(m.source, params[:botuser], params[:password])
- when true
- m.reply welcome(m.source)
- @bot.auth.set_changed
- else
- m.reply _("sorry, can't do")
- end
- rescue => e
- m.reply _("couldn't login: %{exception}") % {:exception => e}
- raise
- end
- end
-
- def auth_autologin(m, params)
- u = do_autologin(m.source)
- if u.default?
- m.reply _("I couldn't find anything to let you login automatically")
- else
- m.reply welcome(m.source)
- end
- end
-
- def do_autologin(user)
- @bot.auth.autologin(user)
- end
-
- def auth_whoami(m, params)
- m.reply _("you are %{who}") % {
- :who => get_botusername_for(m.source).gsub(
- /^everyone$/, _("no one that I know")).gsub(
- /^owner$/, _("my boss"))
- }
- end
-
- def auth_whois(m, params)
- return auth_whoami(m, params) if !m.public?
- u = m.channel.users[params[:user]]
-
- return m.reply("I don't see anyone named '#{params[:user]}' here") unless u
-
- m.reply _("#{params[:user]} is %{who}") % {
- :who => get_botusername_for(u).gsub(
- /^everyone$/, _("no one that I know")).gsub(
- /^owner$/, _("my boss"))
- }
- end
-
- def help(cmd, topic="")
- case cmd
- when "login"
- return _("login [<botuser>] [<pass>]: logs in to the bot as botuser <botuser> with password <pass>. When using the full form, you must contact the bot in private. <pass> can be omitted if <botuser> allows login-by-mask and your netmask is among the known ones. if <botuser> is omitted too autologin will be attempted")
- when "whoami"
- return _("whoami: names the botuser you're linked to")
- when "who"
- return _("who is <user>: names the botuser <user> is linked to")
- when /^permission/
- case topic
- when "syntax"
- return _("a permission is specified as module::path::to::cmd; when you want to enable it, prefix it with +; when you want to disable it, prefix it with -; when using the +reset+ command, do not use any prefix")
- when "set", "reset", "[re]set", "(re)set"
- return _("permissions [re]set <permission> [in <channel>] for <user>: sets or resets the permissions for botuser <user> in channel <channel> (use ? to change the permissions for private addressing)")
- when "view"
- return _("permissions view [for <user>]: display the permissions for user <user>")
- when "searc"
- return _("permissions search <pattern>: display the permissions associated with the commands matching <pattern>")
- else
- return _("permission topics: syntax, (re)set, view, search")
- end
- when "user"
- case topic
- when "show"
- return _("user show <what> : shows info about the user; <what> can be any of autologin, login-by-mask, netmasks")
- when /^(en|dis)able/
- return _("user enable|disable <what> : turns on or off <what> (autologin, login-by-mask)")
- when "set"
- return _("user set password <blah> : sets the user password to <blah>; passwords can only contain upper and lowercase letters and numbers, and must be at least 4 characters long")
- when "add", "rm"
- return _("user add|rm netmask <mask> : adds/removes netmask <mask> from the list of netmasks known to the botuser you're linked to")
- when "reset"
- return _("user reset <what> : resets <what> to the default values. <what> can be +netmasks+ (the list will be emptied), +autologin+ or +login-by-mask+ (will be reset to the default value) or +password+ (a new one will be generated and you'll be told in private)")
- when "tell"
- return _("user tell <who> the password for <botuser> : contacts <who> in private to tell him/her the password for <botuser>")
- when "create"
- return _("user create <name> <password> : create botuser named <name> with password <password>. The password can be omitted, in which case a random one will be generated. The <name> should only contain alphanumeric characters and the underscore (_)")
- when "list"
- return _("user list : lists all the botusers")
- when "destroy"
- return _("user destroy <botuser> : destroys <botuser>. This function %{highlight}must%{highlight} be called in two steps. On the first call <botuser> is queued for destruction. On the second call, which must be in the form 'user confirm destroy <botuser>', the botuser will be destroyed. If you want to cancel the destruction, issue the command 'user cancel destroy <botuser>'") % {:highlight => Bold}
- else
- return _("user topics: show, enable|disable, add|rm netmask, set, reset, tell, create, list, destroy")
- end
- when "auth"
- return _("auth <masterpassword>: log in as the bot owner; other commands: login, whoami, permission syntax, permissions [re]set, permissions view, user, meet, hello")
- when "meet"
- return _("meet <nick> [as <user>]: creates a bot user for nick, calling it user (defaults to the nick itself)")
- when "hello"
- return _("hello: creates a bot user for the person issuing the command")
- else
- return _("auth commands: auth, login, whoami, who, permission[s], user, meet, hello")
- end
- end
-
- def need_args(cmd)
- _("sorry, I need more arguments to %{command}") % {:command => cmd}
- end
-
- def not_args(cmd, *stuff)
- _("I can only %{command} these: %{arguments}") %
- {:command => cmd, :arguments => stuff.join(', ')}
- end
-
- def set_prop(botuser, prop, val)
- k = prop.to_s.gsub("-","_")
- botuser.send( (k + "=").to_sym, val)
- if prop == :password and botuser == @bot.auth.botowner
- @bot.config.items[:'auth.password'].set_string(@bot.auth.botowner.password)
- end
- end
-
- def reset_prop(botuser, prop)
- k = prop.to_s.gsub("-","_")
- botuser.send( ("reset_"+k).to_sym)
- end
-
- def ask_bool_prop(botuser, prop)
- k = prop.to_s.gsub("-","_")
- botuser.send( (k + "?").to_sym)
- end
-
- def auth_manage_user(m, params)
- splits = params[:data]
-
- cmd = splits.first
- return auth_whoami(m, params) if cmd.nil?
-
- botuser = get_botuser_for(m.source)
- # By default, we do stuff on the botuser the irc user is bound to
- butarget = botuser
-
- has_for = splits[-2] == "for"
- if has_for
- butarget = @bot.auth.get_botuser(splits[-1]) rescue nil
- return m.reply(_("no such bot user %{user}") % {:user => splits[-1]}) unless butarget
- splits.slice!(-2,2)
- end
- return m.reply(_("you can't mess with %{user}") % {:user => butarget.username}) if butarget.owner? && botuser != butarget
-
- bools = [:autologin, :"login-by-mask"]
- can_set = [:password]
- can_addrm = [:netmasks]
- can_reset = bools + can_set + can_addrm
- can_show = can_reset + ["perms"]
-
- begin
- case cmd.to_sym
-
- when :show
- return m.reply(_("you can't see the properties of %{user}") %
- {:user => butarget.username}) if botuser != butarget &&
- !botuser.permit?("auth::show::other")
-
- case splits[1]
- when nil, "all"
- props = can_reset
- when "password"
- if botuser != butarget
- return m.reply(_("no way I'm telling you the master password!")) if butarget == @bot.auth.botowner
- return m.reply(_("you can't ask for someone else's password"))
- end
- return m.reply(_("c'mon, you can't be asking me seriously to tell you the password in public!")) if m.public?
- return m.reply(_("the password for %{user} is %{password}") %
- { :user => butarget.username, :password => butarget.password })
- else
- props = splits[1..-1]
- end
-
- str = []
-
- props.each { |arg|
- k = arg.to_sym
- next if k == :password
- case k
- when *bools
- if ask_bool_prop(butarget, k)
- str << _("can %{action}") % {:action => k}
- else
- str << _("can not %{action}") % {:action => k}
- end
- when :netmasks
- if butarget.netmasks.empty?
- str << _("knows no netmasks")
- else
- str << _("knows %{netmasks}") % {:netmasks => butarget.netmasks.join(", ")}
- end
- end
- }
- return m.reply("#{butarget.username} #{str.join('; ')}")
-
- when :enable, :disable
- return m.reply(_("you can't change the default user")) if butarget.default? && !botuser.permit?("auth::edit::other::default")
- return m.reply(_("you can't edit %{user}") % {:user => butarget.username}) if butarget != botuser && !botuser.permit?("auth::edit::other")
-
- return m.reply(need_args(cmd)) unless splits[1]
- things = []
- skipped = []
- splits[1..-1].each { |a|
- arg = a.to_sym
- if bools.include?(arg)
- set_prop(butarget, arg, cmd.to_sym == :enable)
- things << a
- else
- skipped << a
- end
- }
-
- m.reply(_("I ignored %{things} because %{reason}") % {
- :things => skipped.join(', '),
- :reason => not_args(cmd, *bools)}) unless skipped.empty?
- if things.empty?
- m.reply _("I haven't changed anything")
- else
- @bot.auth.set_changed
- return auth_manage_user(m, {:data => ["show"] + things + ["for", butarget.username] })
- end
-
- when :set
- return m.reply(_("you can't change the default user")) if
- butarget.default? && !botuser.permit?("auth::edit::default")
- return m.reply(_("you can't edit %{user}") % {:user=>butarget.username}) if
- butarget != botuser && !botuser.permit?("auth::edit::other")
-
- return m.reply(need_args(cmd)) unless splits[1]
- arg = splits[1].to_sym
- return m.reply(not_args(cmd, *can_set)) unless can_set.include?(arg)
- argarg = splits[2]
- return m.reply(need_args([cmd, splits[1]].join(" "))) unless argarg
- if arg == :password && m.public?
- return m.reply(_("is that a joke? setting the password in public?"))
- end
- set_prop(butarget, arg, argarg)
- @bot.auth.set_changed
- auth_manage_user(m, {:data => ["show", arg.to_s, "for", butarget.username] })
-
- when :reset
- return m.reply(_("you can't change the default user")) if
- butarget.default? && !botuser.permit?("auth::edit::default")
- return m.reply(_("you can't edit %{user}") % {:user=>butarget.username}) if
- butarget != botuser && !botuser.permit?("auth::edit::other")
-
- return m.reply(need_args(cmd)) unless splits[1]
- things = []
- skipped = []
- splits[1..-1].each { |a|
- arg = a.to_sym
- if can_reset.include?(arg)
- reset_prop(butarget, arg)
- things << a
- else
- skipped << a
- end
- }
-
- m.reply(_("I ignored %{things} because %{reason}") %
- { :things => skipped.join(', '),
- :reason => not_args(cmd, *can_reset)}) unless skipped.empty?
- if things.empty?
- m.reply _("I haven't changed anything")
- else
- @bot.auth.set_changed
- @bot.say(m.source, _("the password for %{user} is now %{password}") %
- {:user => butarget.username, :password => butarget.password}) if
- things.include?("password")
- return auth_manage_user(m, {:data => (["show"] + things - ["password"]) + ["for", butarget.username]})
- end
-
- when :add, :rm, :remove, :del, :delete
- return m.reply(_("you can't change the default user")) if
- butarget.default? && !botuser.permit?("auth::edit::default")
- return m.reply(_("you can't edit %{user}") % {:user => butarget.username}) if
- butarget != botuser && !botuser.permit?("auth::edit::other")
-
- arg = splits[1]
- if arg.nil? or arg !~ /netmasks?/ or splits[2].nil?
- return m.reply(_("I can only add/remove netmasks. See +help user add+ for more instructions"))
- end
-
- method = cmd.to_sym == :add ? :add_netmask : :delete_netmask
-
- failed = []
-
- splits[2..-1].each { |mask|
- begin
- butarget.send(method, mask.to_irc_netmask(:server => @bot.server))
- rescue => e
- debug "failed with #{e.message}"
- debug e.backtrace.join("\n")
- failed << mask
- end
- }
- m.reply "I failed to #{cmd} #{failed.join(', ')}" unless failed.empty?
- @bot.auth.set_changed
- return auth_manage_user(m, {:data => ["show", "netmasks", "for", butarget.username] })
-
- else
- m.reply _("sorry, I don't know how to %{request}") % {:request => m.message}
- end
- rescue => e
- m.reply _("couldn't %{cmd}: %{exception}") % {:cmd => cmd, :exception => e}
- end
- end
-
- def auth_meet(m, params)
- nick = params[:nick]
- if !nick
- # we are actually responding to a 'hello' command
- unless m.botuser.transient?
- m.reply @bot.lang.get('hello_X') % m.botuser
- return
- end
- nick = m.sourcenick
- irc_user = m.source
- else
- # m.channel is always an Irc::Channel because the command is either
- # public-only 'meet' or private/public 'hello' which was handled by
- # the !nick case, so this shouldn't fail
- irc_user = m.channel.users[nick]
- return m.reply("I don't see anyone named '#{nick}' here") unless irc_user
- end
- # BotUser name
- buname = params[:user] || nick
- begin
- call_event(:botuser,:pre_perm, {:irc_user => irc_user, :bot_user => buname})
- met = @bot.auth.make_permanent(irc_user, buname)
- @bot.auth.set_changed
- call_event(:botuser,:post_perm, {:irc_user => irc_user, :bot_user => buname})
- m.reply @bot.lang.get('hello_X') % met
- @bot.say nick, _("you are now registered as %{buname}. I created a random password for you : %{pass} and you can change it at any time by telling me 'user set password <password>' in private" % {
- :buname => buname,
- :pass => met.password
- })
- rescue RuntimeError
- # or can this happen for other cases too?
- # TODO autologin if forced
- m.reply _("but I already know %{buname}" % {:buname => buname})
- rescue => e
- m.reply _("I had problems meeting %{nick}: %{e}" % { :nick => nick, :e => e })
- end
- end
-
- def auth_tell_password(m, params)
- user = params[:user]
- begin
- botuser = @bot.auth.get_botuser(params[:botuser])
- rescue
- return m.reply(_("couldn't find botuser %{user}") % {:user => params[:botuser]})
- end
- m.reply(_("I'm not telling the master password to anyway, pal")) if botuser == @bot.auth.botowner
- msg = _("the password for botuser %{user} is %{password}") %
- {:user => botuser.username, :password => botuser.password}
- @bot.say user, msg
- @bot.say m.source, _("I told %{user} that %{message}") % {:user => user, :message => msg}
- end
-
- def auth_create_user(m, params)
- name = params[:name]
- password = params[:password]
- return m.reply(_("are you nuts, creating a botuser with a publicly known password?")) if m.public? and not password.nil?
- begin
- bu = @bot.auth.create_botuser(name, password)
- @bot.auth.set_changed
- rescue => e
- m.reply(_("failed to create %{user}: %{exception}") % {:user => name, :exception => e})
- debug e.inspect + "\n" + e.backtrace.join("\n")
- return
- end
- m.reply(_("created botuser %{user}") % {:user => bu.username})
- end
-
- def auth_list_users(m, params)
- # TODO name regexp to filter results
- list = @bot.auth.save_array.inject([]) { |list, x| ['everyone', 'owner'].include?(x[:username]) ? list : list << x[:username] }
- if defined?(@destroy_q)
- list.map! { |x|
- @destroy_q.include?(x) ? x + _(" (queued for destruction)") : x
- }
- end
- return m.reply(_("I have no botusers other than the default ones")) if list.empty?
- return m.reply(n_("botuser: %{list}", "botusers: %{list}", list.length) %
- {:list => list.join(', ')})
- end
-
- def auth_destroy_user(m, params)
- @destroy_q = [] unless defined?(@destroy_q)
- buname = params[:name]
- return m.reply(_("You can't destroy %{user}") % {:user => buname}) if
- ["everyone", "owner"].include?(buname)
- mod = params[:modifier].to_sym rescue nil
-
- buser_array = @bot.auth.save_array
- buser_hash = buser_array.inject({}) { |h, u|
- h[u[:username]] = u
- h
- }
-
- return m.reply(_("no such botuser %{user}") % {:user=>buname}) unless
- buser_hash.keys.include?(buname)
-
- case mod
- when :cancel
- if @destroy_q.include?(buname)
- @destroy_q.delete(buname)
- m.reply(_("%{user} removed from the destruction queue") % {:user=>buname})
- else
- m.reply(_("%{user} was not queued for destruction") % {:user=>buname})
- end
- return
- when nil
- if @destroy_q.include?(buname)
- return m.reply(_("%{user} already queued for destruction, use %{highlight}user confirm destroy %{user}%{highlight} to destroy it") % {:user=>buname, :highlight=>Bold})
- else
- @destroy_q << buname
- return m.reply(_("%{user} queued for destruction, use %{highlight}user confirm destroy %{user}%{highlight} to destroy it") % {:user=>buname, :highlight=>Bold})
- end
- when :confirm
- begin
- return m.reply(_("%{user} is not queued for destruction yet") %
- {:user=>buname}) unless @destroy_q.include?(buname)
- buser_array.delete_if { |u|
- u[:username] == buname
- }
- @destroy_q.delete(buname)
- @bot.auth.load_array(buser_array, true)
- @bot.auth.set_changed
- rescue => e
- return m.reply(_("failed: %{exception}") % {:exception => e})
- end
- return m.reply(_("botuser %{user} destroyed") % {:user => buname})
- end
- end
-
- def auth_copy_ren_user(m, params)
- source = Auth::BotUser.sanitize_username(params[:source])
- dest = Auth::BotUser.sanitize_username(params[:dest])
- return m.reply(_("please don't touch the default users")) unless
- (["everyone", "owner"] & [source, dest]).empty?
-
- buser_array = @bot.auth.save_array
- buser_hash = buser_array.inject({}) { |h, u|
- h[u[:username]] = u
- h
- }
-
- return m.reply(_("no such botuser %{source}") % {:source=>source}) unless
- buser_hash.keys.include?(source)
- return m.reply(_("botuser %{dest} exists already") % {:dest=>dest}) if
- buser_hash.keys.include?(dest)
-
- copying = m.message.split[1] == "copy"
- begin
- if copying
- h = {}
- buser_hash[source].each { |k, val|
- h[k] = val.dup
- }
- else
- h = buser_hash[source]
- end
- h[:username] = dest
- buser_array << h if copying
-
- @bot.auth.load_array(buser_array, true)
- @bot.auth.set_changed
- call_event(:botuser, copying ? :copy : :rename, :source => source, :dest => dest)
- rescue => e
- return m.reply(_("failed: %{exception}") % {:exception=>e})
- end
- if copying
- m.reply(_("botuser %{source} copied to %{dest}") %
- {:source=>source, :dest=>dest})
- else
- m.reply(_("botuser %{source} renamed to %{dest}") %
- {:source=>source, :dest=>dest})
- end
-
- end
-
- def auth_export(m, params)
-
- exportfile = "#{@bot.botclass}/new-auth.users"
-
- what = params[:things]
-
- has_to = what[-2] == "to"
- if has_to
- exportfile = "#{@bot.botclass}/#{what[-1]}"
- what.slice!(-2,2)
- end
-
- what.delete("all")
-
- m.reply _("selecting data to export ...")
-
- buser_array = @bot.auth.save_array
- buser_hash = buser_array.inject({}) { |h, u|
- h[u[:username]] = u
- h
- }
-
- if what.empty?
- we_want = buser_hash
- else
- we_want = buser_hash.delete_if { |key, val|
- not what.include?(key)
- }
- end
-
- m.reply _("preparing data for export ...")
- begin
- yaml_hash = {}
- we_want.each { |k, val|
- yaml_hash[k] = {}
- val.each { |kk, v|
- case kk
- when :username
- next
- when :netmasks
- yaml_hash[k][kk] = []
- v.each { |nm|
- yaml_hash[k][kk] << {
- :fullform => nm.fullform,
- :casemap => nm.casemap.to_s
- }
- }
- else
- yaml_hash[k][kk] = v
- end
- }
- }
- rescue => e
- m.reply _("failed to prepare data: %{exception}") % {:exception=>e}
- debug e.backtrace.dup.unshift(e.inspect).join("\n")
- return
- end
-
- m.reply _("exporting to %{file} ...") % {:file=>exportfile}
- begin
- # m.reply yaml_hash.inspect
- File.open(exportfile, "w") do |file|
- file.puts YAML::dump(yaml_hash)
- end
- rescue => e
- m.reply _("failed to export users: %{exception}") % {:exception=>e}
- debug e.backtrace.dup.unshift(e.inspect).join("\n")
- return
- end
- m.reply _("done")
- end
-
- def auth_import(m, params)
-
- importfile = "#{@bot.botclass}/new-auth.users"
-
- what = params[:things]
-
- has_from = what[-2] == "from"
- if has_from
- importfile = "#{@bot.botclass}/#{what[-1]}"
- what.slice!(-2,2)
- end
-
- what.delete("all")
-
- m.reply _("reading %{file} ...") % {:file=>importfile}
- begin
- yaml_hash = YAML::load_file(importfile)
- rescue => e
- m.reply _("failed to import from: %{exception}") % {:exception=>e}
- debug e.backtrace.dup.unshift(e.inspect).join("\n")
- return
- end
-
- # m.reply yaml_hash.inspect
-
- m.reply _("selecting data to import ...")
-
- if what.empty?
- we_want = yaml_hash
- else
- we_want = yaml_hash.delete_if { |key, val|
- not what.include?(key)
- }
- end
-
- m.reply _("parsing data from import ...")
-
- buser_hash = {}
-
- begin
- yaml_hash.each { |k, val|
- buser_hash[k] = { :username => k }
- val.each { |kk, v|
- case kk
- when :netmasks
- buser_hash[k][kk] = []
- v.each { |nm|
- buser_hash[k][kk] << nm[:fullform].to_irc_netmask(:casemap => nm[:casemap].to_irc_casemap).to_irc_netmask(:server => @bot.server)
- }
- else
- buser_hash[k][kk] = v
- end
- }
- }
- rescue => e
- m.reply _("failed to parse data: %{exception}") % {:exception=>e}
- debug e.backtrace.dup.unshift(e.inspect).join("\n")
- return
- end
-
- # m.reply buser_hash.inspect
-
- org_buser_array = @bot.auth.save_array
- org_buser_hash = org_buser_array.inject({}) { |h, u|
- h[u[:username]] = u
- h
- }
-
- # TODO we may want to do a(n optional) key-by-key merge
- #
- org_buser_hash.merge!(buser_hash)
- new_buser_array = org_buser_hash.values
- @bot.auth.load_array(new_buser_array, true)
- @bot.auth.set_changed
-
- m.reply _("done")
- end
-
-end
-
-auth = AuthModule.new
-
-auth.map "user export *things",
- :action => 'auth_export',
- :defaults => { :things => ['all'] },
- :auth_path => ':manage:fedex:'
-
-auth.map "user import *things",
- :action => 'auth_import',
- :auth_path => ':manage:fedex:'
-
-auth.map "user create :name :password",
- :action => 'auth_create_user',
- :defaults => {:password => nil},
- :auth_path => ':manage:'
-
-auth.map "user [:modifier] destroy :name",
- :action => 'auth_destroy_user',
- :requirements => { :modifier => /^(cancel|confirm)?$/ },
- :defaults => { :modifier => '' },
- :auth_path => ':manage::destroy!'
-
-auth.map "user copy :source [to] :dest",
- :action => 'auth_copy_ren_user',
- :auth_path => ':manage:'
-
-auth.map "user rename :source [to] :dest",
- :action => 'auth_copy_ren_user',
- :auth_path => ':manage:'
-
-auth.map "meet :nick [as :user]",
- :action => 'auth_meet',
- :auth_path => 'user::manage', :private => false
-
-auth.map "hello",
- :action => 'auth_meet',
- :auth_path => 'user::manage::meet'
-
-auth.default_auth("user::manage", false)
-auth.default_auth("user::manage::meet::hello", true)
-
-auth.map "user tell :user the password for :botuser",
- :action => 'auth_tell_password',
- :auth_path => ':manage:'
-
-auth.map "user list",
- :action => 'auth_list_users',
- :auth_path => '::'
-
-auth.map "user *data",
- :action => 'auth_manage_user'
-
-auth.default_auth("user", true)
-auth.default_auth("edit::other", false)
-
-auth.map "whoami",
- :action => 'auth_whoami',
- :auth_path => '!*!'
-
-auth.map "who is :user",
- :action => 'auth_whois',
- :auth_path => '!*!'
-
-auth.map "auth :password",
- :action => 'auth_auth',
- :public => false,
- :auth_path => '!login!'
-
-auth.map "login :botuser :password",
- :action => 'auth_login',
- :public => false,
- :defaults => { :password => nil },
- :auth_path => '!login!'
-
-auth.map "login :botuser",
- :action => 'auth_login',
- :auth_path => '!login!'
-
-auth.map "login",
- :action => 'auth_autologin',
- :auth_path => '!login!'
-
-auth.map "permissions set *args",
- :action => 'auth_edit_perm',
- :auth_path => ':edit::set:'
-
-auth.map "permissions reset *args",
- :action => 'auth_edit_perm',
- :auth_path => ':edit::set:'
-
-auth.map "permissions view [for :user]",
- :action => 'auth_view_perm',
- :auth_path => '::'
-
-auth.map "permissions search *pattern",
- :action => 'auth_search_perm',
- :auth_path => '::'
-
-auth.default_auth('*', false)
-
+#-- vim:sw=2:et
+#++
+#
+# :title: rbot auth management from IRC
+#
+# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
+# Copyright:: (C) 2006,2007 Giuseppe Bilotta
+# License:: GPL v2
+
+class AuthModule < CoreBotModule
+
+ def initialize
+ super
+
+ # The namespace migration causes each Irc::Auth::PermissionSet to be
+ # unrecoverable, and we have to rename their class name to
+ # Irc::Bot::Auth::PermissionSet
+ @registry.recovery = Proc.new { |val|
+ patched = val.sub("o:\035Irc::Auth::PermissionSet", "o:\042Irc::Bot::Auth::PermissionSet")
+ Marshal.restore(patched)
+ }
+
+ load_array(:default, true)
+ debug "initialized auth. Botusers: #{@bot.auth.save_array.pretty_inspect}"
+ end
+
+ def save
+ save_array
+ end
+
+ def save_array(key=:default)
+ if @bot.auth.changed?
+ @registry[key] = @bot.auth.save_array
+ @bot.auth.reset_changed
+ debug "saved botusers (#{key}): #{@registry[key].pretty_inspect}"
+ end
+ end
+
+ def load_array(key=:default, forced=false)
+ debug "loading botusers (#{key}): #{@registry[key].pretty_inspect}"
+ @bot.auth.load_array(@registry[key], forced) if @registry.has_key?(key)
+ end
+
+ # The permission parameters accept arguments with the following syntax:
+ # cmd_path... [on #chan .... | in here | in private]
+ # This auxiliary method scans the array _ar_ to see if it matches
+ # the given syntax: it expects + or - signs in front of _cmd_path_
+ # elements when _setting_ = true
+ #
+ # It returns an array whose first element is the array of cmd_path,
+ # the second element is an array of locations and third an array of
+ # warnings occurred while parsing the strings
+ #
+ def parse_args(ar, setting)
+ cmds = []
+ locs = []
+ warns = []
+ doing_cmds = true
+ next_must_be_chan = false
+ want_more = false
+ last_idx = 0
+ ar.each_with_index { |x, i|
+ if doing_cmds # parse cmd_path
+ # check if the list is done
+ if x == "on" or x == "in"
+ doing_cmds = false
+ next_must_be_chan = true if x == "on"
+ next
+ end
+ if "+-".include?(x[0])
+ warns << ArgumentError.new(_("please do not use + or - in front of command %{command} when resetting") % {:command => x}) unless setting
+ else
+ warns << ArgumentError.new(_("+ or - expected in front of %{string}") % {:string => x}) if setting
+ end
+ cmds << x
+ else # parse locations
+ if x[-1].chr == ','
+ want_more = true
+ else
+ want_more = false
+ end
+ case next_must_be_chan
+ when false
+ locs << x.gsub(/^here$/,'_').gsub(/^private$/,'?')
+ else
+ warns << ArgumentError.new(_("'%{string}' doesn't look like a channel name") % {:string => x}) unless @bot.server.supports[:chantypes].include?(x[0])
+ locs << x
+ end
+ unless want_more
+ last_idx = i
+ break
+ end
+ end
+ }
+ warns << _("trailing comma") if want_more
+ warns << _("you probably forgot a comma") unless last_idx == ar.length - 1
+ return cmds, locs, warns
+ end
+
+ def auth_edit_perm(m, params)
+
+ setting = m.message.split[1] == "set"
+ splits = params[:args]
+
+ has_for = splits[-2] == "for"
+ return usage(m) unless has_for
+
+ begin
+ user = @bot.auth.get_botuser(splits[-1].sub(/^all$/,"everyone"))
+ rescue
+ return m.reply(_("couldn't find botuser %{name}") % {:name => splits[-1]})
+ end
+ return m.reply(_("you can't change permissions for %{username}") % {:username => user.username}) if user.owner?
+ splits.slice!(-2,2) if has_for
+
+ cmds, locs, warns = parse_args(splits, setting)
+ errs = warns.select { |w| w.kind_of?(Exception) }
+
+ unless errs.empty?
+ m.reply _("couldn't satisfy your request: %{errors}") % {:errors => errs.join(',')}
+ return
+ end
+
+ if locs.empty?
+ locs << "*"
+ end
+ begin
+ locs.each { |loc|
+ ch = loc
+ if m.private?
+ ch = "?" if loc == "_"
+ else
+ ch = m.target.to_s if loc == "_"
+ end
+ cmds.each { |setval|
+ if setting
+ val = setval[0].chr == '+'
+ cmd = setval[1..-1]
+ user.set_permission(cmd, val, ch)
+ else
+ cmd = setval
+ user.reset_permission(cmd, ch)
+ end
+ }
+ }
+ rescue => e
+ m.reply "something went wrong while trying to set the permissions"
+ raise
+ end
+ @bot.auth.set_changed
+ debug "user #{user} permissions changed"
+ m.okay
+ end
+
+ def auth_view_perm(m, params)
+ begin
+ if params[:user].nil?
+ user = get_botusername_for(m.source)
+ return m.reply(_("you are owner, you can do anything")) if user.owner?
+ else
+ user = @bot.auth.get_botuser(params[:user].sub(/^all$/,"everyone"))
+ return m.reply(_("owner can do anything")) if user.owner?
+ end
+ rescue
+ return m.reply(_("couldn't find botuser %{name}") % {:name => params[:user]})
+ end
+ perm = user.perm
+ str = []
+ perm.each { |k, val|
+ next if val.perm.empty?
+ case k
+ when :*
+ str << _("on any channel: ")
+ when :"?"
+ str << _("in private: ")
+ else
+ str << _("on #{k}: ")
+ end
+ sub = []
+ val.perm.each { |cmd, bool|
+ sub << (bool ? "+" : "-")
+ sub.last << cmd.to_s
+ }
+ str.last << sub.join(', ')
+ }
+ if str.empty?
+ m.reply _("no permissions set for %{user}") % {:user => user.username}
+ else
+ m.reply _("permissions for %{user}:: %{permissions}") %
+ { :user => user.username, :permissions => str.join('; ')}
+ end
+ end
+
+ def auth_search_perm(m, p)
+ pattern = Regexp.new(p[:pattern].to_s)
+ results = @bot.plugins.maps.select { |k, v| k.match(pattern) }
+ count = results.length
+ max = @bot.config['send.max_lines']
+ extra = (count > max ? _(". only %{max} will be shown") : "") % { :max => max }
+ m.reply _("%{count} commands found matching %{pattern}%{extra}") % {
+ :count => count, :pattern => pattern, :extra => extra
+ }
+ return if count == 0
+ results[0,max].each { |cmd, hash|
+ m.reply _("%{cmd}: %{perms}") % {
+ :cmd => cmd,
+ :perms => hash[:auth].join(", ")
+ }
+ }
+ end
+
+ def get_botuser_for(user)
+ @bot.auth.irc_to_botuser(user)
+ end
+
+ def get_botusername_for(user)
+ get_botuser_for(user).username
+ end
+
+ def welcome(user)
+ _("welcome, %{user}") % {:user => get_botusername_for(user)}
+ end
+
+ def auth_auth(m, params)
+ params[:botuser] = 'owner'
+ auth_login(m,params)
+ end
+
+ def auth_login(m, params)
+ begin
+ case @bot.auth.login(m.source, params[:botuser], params[:password])
+ when true
+ m.reply welcome(m.source)
+ @bot.auth.set_changed
+ else
+ m.reply _("sorry, can't do")
+ end
+ rescue => e
+ m.reply _("couldn't login: %{exception}") % {:exception => e}
+ raise
+ end
+ end
+
+ def auth_autologin(m, params)
+ u = do_autologin(m.source)
+ if u.default?
+ m.reply _("I couldn't find anything to let you login automatically")
+ else
+ m.reply welcome(m.source)
+ end
+ end
+
+ def do_autologin(user)
+ @bot.auth.autologin(user)
+ end
+
+ def auth_whoami(m, params)
+ m.reply _("you are %{who}") % {
+ :who => get_botusername_for(m.source).gsub(
+ /^everyone$/, _("no one that I know")).gsub(
+ /^owner$/, _("my boss"))
+ }
+ end
+
+ def auth_whois(m, params)
+ return auth_whoami(m, params) if !m.public?
+ u = m.channel.users[params[:user]]
+
+ return m.reply("I don't see anyone named '#{params[:user]}' here") unless u
+
+ m.reply _("#{params[:user]} is %{who}") % {
+ :who => get_botusername_for(u).gsub(
+ /^everyone$/, _("no one that I know")).gsub(
+ /^owner$/, _("my boss"))
+ }
+ end
+
+ def help(cmd, topic="")
+ case cmd
+ when "login"
+ return _("login [<botuser>] [<pass>]: logs in to the bot as botuser <botuser> with password <pass>. When using the full form, you must contact the bot in private. <pass> can be omitted if <botuser> allows login-by-mask and your netmask is among the known ones. if <botuser> is omitted too autologin will be attempted")
+ when "whoami"
+ return _("whoami: names the botuser you're linked to")
+ when "who"
+ return _("who is <user>: names the botuser <user> is linked to")
+ when /^permission/
+ case topic
+ when "syntax"
+ return _("a permission is specified as module::path::to::cmd; when you want to enable it, prefix it with +; when you want to disable it, prefix it with -; when using the +reset+ command, do not use any prefix")
+ when "set", "reset", "[re]set", "(re)set"
+ return _("permissions [re]set <permission> [in <channel>] for <user>: sets or resets the permissions for botuser <user> in channel <channel> (use ? to change the permissions for private addressing)")
+ when "view"
+ return _("permissions view [for <user>]: display the permissions for user <user>")
+ when "searc"
+ return _("permissions search <pattern>: display the permissions associated with the commands matching <pattern>")
+ else
+ return _("permission topics: syntax, (re)set, view, search")
+ end
+ when "user"
+ case topic
+ when "show"
+ return _("user show <what> : shows info about the user; <what> can be any of autologin, login-by-mask, netmasks")
+ when /^(en|dis)able/
+ return _("user enable|disable <what> : turns on or off <what> (autologin, login-by-mask)")
+ when "set"
+ return _("user set password <blah> : sets the user password to <blah>; passwords can only contain upper and lowercase letters and numbers, and must be at least 4 characters long")
+ when "add", "rm"
+ return _("user add|rm netmask <mask> : adds/removes netmask <mask> from the list of netmasks known to the botuser you're linked to")
+ when "reset"
+ return _("user reset <what> : resets <what> to the default values. <what> can be +netmasks+ (the list will be emptied), +autologin+ or +login-by-mask+ (will be reset to the default value) or +password+ (a new one will be generated and you'll be told in private)")
+ when "tell"
+ return _("user tell <who> the password for <botuser> : contacts <who> in private to tell him/her the password for <botuser>")
+ when "create"
+ return _("user create <name> <password> : create botuser named <name> with password <password>. The password can be omitted, in which case a random one will be generated. The <name> should only contain alphanumeric characters and the underscore (_)")
+ when "list"
+ return _("user list : lists all the botusers")
+ when "destroy"
+ return _("user destroy <botuser> : destroys <botuser>. This function %{highlight}must%{highlight} be called in two steps. On the first call <botuser> is queued for destruction. On the second call, which must be in the form 'user confirm destroy <botuser>', the botuser will be destroyed. If you want to cancel the destruction, issue the command 'user cancel destroy <botuser>'") % {:highlight => Bold}
+ else
+ return _("user topics: show, enable|disable, add|rm netmask, set, reset, tell, create, list, destroy")
+ end
+ when "auth"
+ return _("auth <masterpassword>: log in as the bot owner; other commands: login, whoami, permission syntax, permissions [re]set, permissions view, user, meet, hello")
+ when "meet"
+ return _("meet <nick> [as <user>]: creates a bot user for nick, calling it user (defaults to the nick itself)")
+ when "hello"
+ return _("hello: creates a bot user for the person issuing the command")
+ else
+ return _("auth commands: auth, login, whoami, who, permission[s], user, meet, hello")
+ end
+ end
+
+ def need_args(cmd)
+ _("sorry, I need more arguments to %{command}") % {:command => cmd}
+ end
+
+ def not_args(cmd, *stuff)
+ _("I can only %{command} these: %{arguments}") %
+ {:command => cmd, :arguments => stuff.join(', ')}
+ end
+
+ def set_prop(botuser, prop, val)
+ k = prop.to_s.gsub("-","_")
+ botuser.send( (k + "=").to_sym, val)
+ if prop == :password and botuser == @bot.auth.botowner
+ @bot.config.items[:'auth.password'].set_string(@bot.auth.botowner.password)
+ end
+ end
+
+ def reset_prop(botuser, prop)
+ k = prop.to_s.gsub("-","_")
+ botuser.send( ("reset_"+k).to_sym)
+ end
+
+ def ask_bool_prop(botuser, prop)
+ k = prop.to_s.gsub("-","_")
+ botuser.send( (k + "?").to_sym)
+ end
+
+ def auth_manage_user(m, params)
+ splits = params[:data]
+
+ cmd = splits.first
+ return auth_whoami(m, params) if cmd.nil?
+
+ botuser = get_botuser_for(m.source)
+ # By default, we do stuff on the botuser the irc user is bound to
+ butarget = botuser
+
+ has_for = splits[-2] == "for"
+ if has_for
+ butarget = @bot.auth.get_botuser(splits[-1]) rescue nil
+ return m.reply(_("no such bot user %{user}") % {:user => splits[-1]}) unless butarget
+ splits.slice!(-2,2)
+ end
+ return m.reply(_("you can't mess with %{user}") % {:user => butarget.username}) if butarget.owner? && botuser != butarget
+
+ bools = [:autologin, :"login-by-mask"]
+ can_set = [:password]
+ can_addrm = [:netmasks]
+ can_reset = bools + can_set + can_addrm
+ can_show = can_reset + ["perms"]
+
+ begin
+ case cmd.to_sym
+
+ when :show
+ return m.reply(_("you can't see the properties of %{user}") %
+ {:user => butarget.username}) if botuser != butarget &&
+ !botuser.permit?("auth::show::other")
+
+ case splits[1]
+ when nil, "all"
+ props = can_reset
+ when "password"
+ if botuser != butarget
+ return m.reply(_("no way I'm telling you the master password!")) if butarget == @bot.auth.botowner
+ return m.reply(_("you can't ask for someone else's password"))
+ end
+ return m.reply(_("c'mon, you can't be asking me seriously to tell you the password in public!")) if m.public?
+ return m.reply(_("the password for %{user} is %{password}") %
+ { :user => butarget.username, :password => butarget.password })
+ else
+ props = splits[1..-1]
+ end
+
+ str = []
+
+ props.each { |arg|
+ k = arg.to_sym
+ next if k == :password
+ case k
+ when *bools
+ if ask_bool_prop(butarget, k)
+ str << _("can %{action}") % {:action => k}
+ else
+ str << _("can not %{action}") % {:action => k}
+ end
+ when :netmasks
+ if butarget.netmasks.empty?
+ str << _("knows no netmasks")
+ else
+ str << _("knows %{netmasks}") % {:netmasks => butarget.netmasks.join(", ")}
+ end
+ end
+ }
+ return m.reply("#{butarget.username} #{str.join('; ')}")
+
+ when :enable, :disable
+ return m.reply(_("you can't change the default user")) if butarget.default? && !botuser.permit?("auth::edit::other::default")
+ return m.reply(_("you can't edit %{user}") % {:user => butarget.username}) if butarget != botuser && !botuser.permit?("auth::edit::other")
+
+ return m.reply(need_args(cmd)) unless splits[1]
+ things = []
+ skipped = []
+ splits[1..-1].each { |a|
+ arg = a.to_sym
+ if bools.include?(arg)
+ set_prop(butarget, arg, cmd.to_sym == :enable)
+ things << a
+ else
+ skipped << a
+ end
+ }
+
+ m.reply(_("I ignored %{things} because %{reason}") % {
+ :things => skipped.join(', '),
+ :reason => not_args(cmd, *bools)}) unless skipped.empty?
+ if things.empty?
+ m.reply _("I haven't changed anything")
+ else
+ @bot.auth.set_changed
+ return auth_manage_user(m, {:data => ["show"] + things + ["for", butarget.username] })
+ end
+
+ when :set
+ return m.reply(_("you can't change the default user")) if
+ butarget.default? && !botuser.permit?("auth::edit::default")
+ return m.reply(_("you can't edit %{user}") % {:user=>butarget.username}) if
+ butarget != botuser && !botuser.permit?("auth::edit::other")
+
+ return m.reply(need_args(cmd)) unless splits[1]
+ arg = splits[1].to_sym
+ return m.reply(not_args(cmd, *can_set)) unless can_set.include?(arg)
+ argarg = splits[2]
+ return m.reply(need_args([cmd, splits[1]].join(" "))) unless argarg
+ if arg == :password && m.public?
+ return m.reply(_("is that a joke? setting the password in public?"))
+ end
+ set_prop(butarget, arg, argarg)
+ @bot.auth.set_changed
+ auth_manage_user(m, {:data => ["show", arg.to_s, "for", butarget.username] })
+
+ when :reset
+ return m.reply(_("you can't change the default user")) if
+ butarget.default? && !botuser.permit?("auth::edit::default")
+ return m.reply(_("you can't edit %{user}") % {:user=>butarget.username}) if
+ butarget != botuser && !botuser.permit?("auth::edit::other")
+
+ return m.reply(need_args(cmd)) unless splits[1]
+ things = []
+ skipped = []
+ splits[1..-1].each { |a|
+ arg = a.to_sym
+ if can_reset.include?(arg)
+ reset_prop(butarget, arg)
+ things << a
+ else
+ skipped << a
+ end
+ }
+
+ m.reply(_("I ignored %{things} because %{reason}") %
+ { :things => skipped.join(', '),
+ :reason => not_args(cmd, *can_reset)}) unless skipped.empty?
+ if things.empty?
+ m.reply _("I haven't changed anything")
+ else
+ @bot.auth.set_changed
+ @bot.say(m.source, _("the password for %{user} is now %{password}") %
+ {:user => butarget.username, :password => butarget.password}) if
+ things.include?("password")
+ return auth_manage_user(m, {:data => (["show"] + things - ["password"]) + ["for", butarget.username]})
+ end
+
+ when :add, :rm, :remove, :del, :delete
+ return m.reply(_("you can't change the default user")) if
+ butarget.default? && !botuser.permit?("auth::edit::default")
+ return m.reply(_("you can't edit %{user}") % {:user => butarget.username}) if
+ butarget != botuser && !botuser.permit?("auth::edit::other")
+
+ arg = splits[1]
+ if arg.nil? or arg !~ /netmasks?/ or splits[2].nil?
+ return m.reply(_("I can only add/remove netmasks. See +help user add+ for more instructions"))
+ end
+
+ method = cmd.to_sym == :add ? :add_netmask : :delete_netmask
+
+ failed = []
+
+ splits[2..-1].each { |mask|
+ begin
+ butarget.send(method, mask.to_irc_netmask(:server => @bot.server))
+ rescue => e
+ debug "failed with #{e.message}"
+ debug e.backtrace.join("\n")
+ failed << mask
+ end
+ }
+ m.reply "I failed to #{cmd} #{failed.join(', ')}" unless failed.empty?
+ @bot.auth.set_changed
+ return auth_manage_user(m, {:data => ["show", "netmasks", "for", butarget.username] })
+
+ else
+ m.reply _("sorry, I don't know how to %{request}") % {:request => m.message}
+ end
+ rescue => e
+ m.reply _("couldn't %{cmd}: %{exception}") % {:cmd => cmd, :exception => e}
+ end
+ end
+
+ def auth_meet(m, params)
+ nick = params[:nick]
+ if !nick
+ # we are actually responding to a 'hello' command
+ unless m.botuser.transient?
+ m.reply @bot.lang.get('hello_X') % m.botuser
+ return
+ end
+ nick = m.sourcenick
+ irc_user = m.source
+ else
+ # m.channel is always an Irc::Channel because the command is either
+ # public-only 'meet' or private/public 'hello' which was handled by
+ # the !nick case, so this shouldn't fail
+ irc_user = m.channel.users[nick]
+ return m.reply("I don't see anyone named '#{nick}' here") unless irc_user
+ end
+ # BotUser name
+ buname = params[:user] || nick
+ begin
+ call_event(:botuser,:pre_perm, {:irc_user => irc_user, :bot_user => buname})
+ met = @bot.auth.make_permanent(irc_user, buname)
+ @bot.auth.set_changed
+ call_event(:botuser,:post_perm, {:irc_user => irc_user, :bot_user => buname})
+ m.reply @bot.lang.get('hello_X') % met
+ @bot.say nick, _("you are now registered as %{buname}. I created a random password for you : %{pass} and you can change it at any time by telling me 'user set password <password>' in private" % {
+ :buname => buname,
+ :pass => met.password
+ })
+ rescue RuntimeError
+ # or can this happen for other cases too?
+ # TODO autologin if forced
+ m.reply _("but I already know %{buname}" % {:buname => buname})
+ rescue => e
+ m.reply _("I had problems meeting %{nick}: %{e}" % { :nick => nick, :e => e })
+ end
+ end
+
+ def auth_tell_password(m, params)
+ user = params[:user]
+ begin
+ botuser = @bot.auth.get_botuser(params[:botuser])
+ rescue
+ return m.reply(_("couldn't find botuser %{user}") % {:user => params[:botuser]})
+ end
+ m.reply(_("I'm not telling the master password to anyway, pal")) if botuser == @bot.auth.botowner
+ msg = _("the password for botuser %{user} is %{password}") %
+ {:user => botuser.username, :password => botuser.password}
+ @bot.say user, msg
+ @bot.say m.source, _("I told %{user} that %{message}") % {:user => user, :message => msg}
+ end
+
+ def auth_create_user(m, params)
+ name = params[:name]
+ password = params[:password]
+ return m.reply(_("are you nuts, creating a botuser with a publicly known password?")) if m.public? and not password.nil?
+ begin
+ bu = @bot.auth.create_botuser(name, password)
+ @bot.auth.set_changed
+ rescue => e
+ m.reply(_("failed to create %{user}: %{exception}") % {:user => name, :exception => e})
+ debug e.inspect + "\n" + e.backtrace.join("\n")
+ return
+ end
+ m.reply(_("created botuser %{user}") % {:user => bu.username})
+ end
+
+ def auth_list_users(m, params)
+ # TODO name regexp to filter results
+ list = @bot.auth.save_array.inject([]) { |list, x| ['everyone', 'owner'].include?(x[:username]) ? list : list << x[:username] }
+ if defined?(@destroy_q)
+ list.map! { |x|
+ @destroy_q.include?(x) ? x + _(" (queued for destruction)") : x
+ }
+ end
+ return m.reply(_("I have no botusers other than the default ones")) if list.empty?
+ return m.reply(n_("botuser: %{list}", "botusers: %{list}", list.length) %
+ {:list => list.join(', ')})
+ end
+
+ def auth_destroy_user(m, params)
+ @destroy_q = [] unless defined?(@destroy_q)
+ buname = params[:name]
+ return m.reply(_("You can't destroy %{user}") % {:user => buname}) if
+ ["everyone", "owner"].include?(buname)
+ mod = params[:modifier].to_sym rescue nil
+
+ buser_array = @bot.auth.save_array
+ buser_hash = buser_array.inject({}) { |h, u|
+ h[u[:username]] = u
+ h
+ }
+
+ return m.reply(_("no such botuser %{user}") % {:user=>buname}) unless
+ buser_hash.keys.include?(buname)
+
+ case mod
+ when :cancel
+ if @destroy_q.include?(buname)
+ @destroy_q.delete(buname)
+ m.reply(_("%{user} removed from the destruction queue") % {:user=>buname})
+ else
+ m.reply(_("%{user} was not queued for destruction") % {:user=>buname})
+ end
+ return
+ when nil
+ if @destroy_q.include?(buname)
+ return m.reply(_("%{user} already queued for destruction, use %{highlight}user confirm destroy %{user}%{highlight} to destroy it") % {:user=>buname, :highlight=>Bold})
+ else
+ @destroy_q << buname
+ return m.reply(_("%{user} queued for destruction, use %{highlight}user confirm destroy %{user}%{highlight} to destroy it") % {:user=>buname, :highlight=>Bold})
+ end
+ when :confirm
+ begin
+ return m.reply(_("%{user} is not queued for destruction yet") %
+ {:user=>buname}) unless @destroy_q.include?(buname)
+ buser_array.delete_if { |u|
+ u[:username] == buname
+ }
+ @destroy_q.delete(buname)
+ @bot.auth.load_array(buser_array, true)
+ @bot.auth.set_changed
+ rescue => e
+ return m.reply(_("failed: %{exception}") % {:exception => e})
+ end
+ return m.reply(_("botuser %{user} destroyed") % {:user => buname})
+ end
+ end
+
+ def auth_copy_ren_user(m, params)
+ source = Auth::BotUser.sanitize_username(params[:source])
+ dest = Auth::BotUser.sanitize_username(params[:dest])
+ return m.reply(_("please don't touch the default users")) unless
+ (["everyone", "owner"] & [source, dest]).empty?
+
+ buser_array = @bot.auth.save_array
+ buser_hash = buser_array.inject({}) { |h, u|
+ h[u[:username]] = u
+ h
+ }
+
+ return m.reply(_("no such botuser %{source}") % {:source=>source}) unless
+ buser_hash.keys.include?(source)
+ return m.reply(_("botuser %{dest} exists already") % {:dest=>dest}) if
+ buser_hash.keys.include?(dest)
+
+ copying = m.message.split[1] == "copy"
+ begin
+ if copying
+ h = {}
+ buser_hash[source].each { |k, val|
+ h[k] = val.dup
+ }
+ else
+ h = buser_hash[source]
+ end
+ h[:username] = dest
+ buser_array << h if copying
+
+ @bot.auth.load_array(buser_array, true)
+ @bot.auth.set_changed
+ call_event(:botuser, copying ? :copy : :rename, :source => source, :dest => dest)
+ rescue => e
+ return m.reply(_("failed: %{exception}") % {:exception=>e})
+ end
+ if copying
+ m.reply(_("botuser %{source} copied to %{dest}") %
+ {:source=>source, :dest=>dest})
+ else
+ m.reply(_("botuser %{source} renamed to %{dest}") %
+ {:source=>source, :dest=>dest})
+ end
+
+ end
+
+ def auth_export(m, params)
+
+ exportfile = "#{@bot.botclass}/new-auth.users"
+
+ what = params[:things]
+
+ has_to = what[-2] == "to"
+ if has_to
+ exportfile = "#{@bot.botclass}/#{what[-1]}"
+ what.slice!(-2,2)
+ end
+
+ what.delete("all")
+
+ m.reply _("selecting data to export ...")
+
+ buser_array = @bot.auth.save_array
+ buser_hash = buser_array.inject({}) { |h, u|
+ h[u[:username]] = u
+ h
+ }
+
+ if what.empty?
+ we_want = buser_hash
+ else
+ we_want = buser_hash.delete_if { |key, val|
+ not what.include?(key)
+ }
+ end
+
+ m.reply _("preparing data for export ...")
+ begin
+ yaml_hash = {}
+ we_want.each { |k, val|
+ yaml_hash[k] = {}
+ val.each { |kk, v|
+ case kk
+ when :username
+ next
+ when :netmasks
+ yaml_hash[k][kk] = []
+ v.each { |nm|
+ yaml_hash[k][kk] << {
+ :fullform => nm.fullform,
+ :casemap => nm.casemap.to_s
+ }
+ }
+ else
+ yaml_hash[k][kk] = v
+ end
+ }
+ }
+ rescue => e
+ m.reply _("failed to prepare data: %{exception}") % {:exception=>e}
+ debug e.backtrace.dup.unshift(e.inspect).join("\n")
+ return
+ end
+
+ m.reply _("exporting to %{file} ...") % {:file=>exportfile}
+ begin
+ # m.reply yaml_hash.inspect
+ File.open(exportfile, "w") do |file|
+ file.puts YAML::dump(yaml_hash)
+ end
+ rescue => e
+ m.reply _("failed to export users: %{exception}") % {:exception=>e}
+ debug e.backtrace.dup.unshift(e.inspect).join("\n")
+ return
+ end
+ m.reply _("done")
+ end
+
+ def auth_import(m, params)
+
+ importfile = "#{@bot.botclass}/new-auth.users"
+
+ what = params[:things]
+
+ has_from = what[-2] == "from"
+ if has_from
+ importfile = "#{@bot.botclass}/#{what[-1]}"
+ what.slice!(-2,2)
+ end
+
+ what.delete("all")
+
+ m.reply _("reading %{file} ...") % {:file=>importfile}
+ begin
+ yaml_hash = YAML::load_file(importfile)
+ rescue => e
+ m.reply _("failed to import from: %{exception}") % {:exception=>e}
+ debug e.backtrace.dup.unshift(e.inspect).join("\n")
+ return
+ end
+
+ # m.reply yaml_hash.inspect
+
+ m.reply _("selecting data to import ...")
+
+ if what.empty?
+ we_want = yaml_hash
+ else
+ we_want = yaml_hash.delete_if { |key, val|
+ not what.include?(key)
+ }
+ end
+
+ m.reply _("parsing data from import ...")
+
+ buser_hash = {}
+
+ begin
+ yaml_hash.each { |k, val|
+ buser_hash[k] = { :username => k }
+ val.each { |kk, v|
+ case kk
+ when :netmasks
+ buser_hash[k][kk] = []
+ v.each { |nm|
+ buser_hash[k][kk] << nm[:fullform].to_irc_netmask(:casemap => nm[:casemap].to_irc_casemap).to_irc_netmask(:server => @bot.server)
+ }
+ else
+ buser_hash[k][kk] = v
+ end
+ }
+ }
+ rescue => e
+ m.reply _("failed to parse data: %{exception}") % {:exception=>e}
+ debug e.backtrace.dup.unshift(e.inspect).join("\n")
+ return
+ end
+
+ # m.reply buser_hash.inspect
+
+ org_buser_array = @bot.auth.save_array
+ org_buser_hash = org_buser_array.inject({}) { |h, u|
+ h[u[:username]] = u
+ h
+ }
+
+ # TODO we may want to do a(n optional) key-by-key merge
+ #
+ org_buser_hash.merge!(buser_hash)
+ new_buser_array = org_buser_hash.values
+ @bot.auth.load_array(new_buser_array, true)
+ @bot.auth.set_changed
+
+ m.reply _("done")
+ end
+
+end
+
+auth = AuthModule.new
+
+auth.map "user export *things",
+ :action => 'auth_export',
+ :defaults => { :things => ['all'] },
+ :auth_path => ':manage:fedex:'
+
+auth.map "user import *things",
+ :action => 'auth_import',
+ :auth_path => ':manage:fedex:'
+
+auth.map "user create :name :password",
+ :action => 'auth_create_user',
+ :defaults => {:password => nil},
+ :auth_path => ':manage:'
+
+auth.map "user [:modifier] destroy :name",
+ :action => 'auth_destroy_user',
+ :requirements => { :modifier => /^(cancel|confirm)?$/ },
+ :defaults => { :modifier => '' },
+ :auth_path => ':manage::destroy!'
+
+auth.map "user copy :source [to] :dest",
+ :action => 'auth_copy_ren_user',
+ :auth_path => ':manage:'
+
+auth.map "user rename :source [to] :dest",
+ :action => 'auth_copy_ren_user',
+ :auth_path => ':manage:'
+
+auth.map "meet :nick [as :user]",
+ :action => 'auth_meet',
+ :auth_path => 'user::manage', :private => false
+
+auth.map "hello",
+ :action => 'auth_meet',
+ :auth_path => 'user::manage::meet'
+
+auth.default_auth("user::manage", false)
+auth.default_auth("user::manage::meet::hello", true)
+
+auth.map "user tell :user the password for :botuser",
+ :action => 'auth_tell_password',
+ :auth_path => ':manage:'
+
+auth.map "user list",
+ :action => 'auth_list_users',
+ :auth_path => '::'
+
+auth.map "user *data",
+ :action => 'auth_manage_user'
+
+auth.default_auth("user", true)
+auth.default_auth("edit::other", false)
+
+auth.map "whoami",
+ :action => 'auth_whoami',
+ :auth_path => '!*!'
+
+auth.map "who is :user",
+ :action => 'auth_whois',
+ :auth_path => '!*!'
+
+auth.map "auth :password",
+ :action => 'auth_auth',
+ :public => false,
+ :auth_path => '!login!'
+
+auth.map "login :botuser :password",
+ :action => 'auth_login',
+ :public => false,
+ :defaults => { :password => nil },
+ :auth_path => '!login!'
+
+auth.map "login :botuser",
+ :action => 'auth_login',
+ :auth_path => '!login!'
+
+auth.map "login",
+ :action => 'auth_autologin',
+ :auth_path => '!login!'
+
+auth.map "permissions set *args",
+ :action => 'auth_edit_perm',
+ :auth_path => ':edit::set:'
+
+auth.map "permissions reset *args",
+ :action => 'auth_edit_perm',
+ :auth_path => ':edit::set:'
+
+auth.map "permissions view [for :user]",
+ :action => 'auth_view_perm',
+ :auth_path => '::'
+
+auth.map "permissions search *pattern",
+ :action => 'auth_search_perm',
+ :auth_path => '::'
+
+auth.default_auth('*', false)
+
diff --git a/lib/rbot/core/basics.rb b/lib/rbot/core/basics.rb
index 4b5ab7d3..7a5d82c1 100644
--- a/lib/rbot/core/basics.rb
+++ b/lib/rbot/core/basics.rb
@@ -1,188 +1,188 @@
-#-- vim:sw=2:et
-#++
-#
-# :title: rbot basic management from IRC
-#
-# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
-# Copyright:: (C) 2006,2007 Giuseppe Bilotta
-# License:: GPL v2
-
-class BasicsModule < CoreBotModule
-
- def ctcp_listen(m)
- who = m.private? ? "me" : m.target
- case m.ctcp.intern
- when :PING
- m.ctcp_reply m.message
- @bot.irclog "@ #{m.source} pinged #{who}"
- when :TIME
- m.ctcp_reply Time.now.to_s
- @bot.irclog "@ #{m.source} asked #{who} what time it is"
- end
- end
-
- def bot_join(m, param)
- if param[:pass]
- @bot.join param[:chan], param[:pass]
- else
- @bot.join param[:chan]
- end
- end
-
- def bot_part(m, param)
- if param[:chan]
- @bot.part param[:chan]
- else
- @bot.part m.target if m.public?
- end
- end
-
- def bot_quit(m, param)
- @bot.quit param[:msg].to_s
- end
-
- def bot_restart(m, param)
- @bot.restart param[:msg].to_s
- end
-
- def bot_hide(m, param)
- @bot.join 0
- end
-
- def bot_say(m, param)
- @bot.say param[:where], param[:what].to_s
- end
-
- def bot_action(m, param)
- @bot.action param[:where], param[:what].to_s
- end
-
- def bot_mode(m, param)
- @bot.mode param[:where], param[:what], param[:who].join(" ")
- end
-
- def bot_ping(m, param)
- m.reply "pong"
- end
-
- def bot_quiet(m, param)
- if param.has_key?(:where)
- @bot.set_quiet param[:where].sub(/^here$/, m.target.downcase)
- else
- @bot.set_quiet
- end
- # Make sense when the commmand is given in private or in a non-quieted
- # channel
- m.okay
- end
-
- def bot_talk(m, param)
- if param.has_key?(:where)
- @bot.reset_quiet param[:where].sub(/^here$/, m.target.downcase)
- else
- @bot.reset_quiet
- end
- # Make sense when the commmand is given in private or in a non-quieted
- # channel
- m.okay
- end
-
- def bot_help(m, param)
- m.reply @bot.help(param[:topic].join(" "))
- end
-
- #TODO move these to a "chatback" plugin
- # when (/^(botsnack|ciggie)$/i)
- # @bot.say m.replyto, @lang.get("thanks_X") % m.sourcenick if(m.public?)
- # @bot.say m.replyto, @lang.get("thanks") if(m.private?)
- # when (/^#{Regexp.escape(@bot.nick)}!*$/)
- # @bot.say m.replyto, @lang.get("hello_X") % m.sourcenick
-
- # handle help requests for "core" topics
- def help(cmd, topic="")
- case cmd
- when "quit"
- _("quit [<message>] => quit IRC with message <message>")
- when "restart"
- _("restart => completely stop and restart the bot (including reconnect)")
- when "join"
- _("join <channel> [<key>] => join channel <channel> with secret key <key> if specified. #{@bot.myself} also responds to invites if you have the required access level")
- when "part"
- _("part <channel> => part channel <channel>")
- when "hide"
- _("hide => part all channels")
- when "nick"
- _("nick <nick> => attempt to change nick to <nick>")
- when "say"
- _("say <channel>|<nick> <message> => say <message> to <channel> or in private message to <nick>")
- when "action"
- _("action <channel>|<nick> <message> => does a /me <message> to <channel> or in private message to <nick>")
- when "quiet"
- _("quiet [in here|<channel>] => with no arguments, stop speaking in all channels, if \"in here\", stop speaking in this channel, or stop speaking in <channel>")
- when "talk"
- _("talk [in here|<channel>] => with no arguments, resume speaking in all channels, if \"in here\", resume speaking in this channel, or resume speaking in <channel>")
- when "ping"
- _("ping => replies with a pong")
- when "mode"
- _("mode <channel> <mode> <nicks> => set channel modes for <nicks> on <channel> to <mode>")
- # when "botsnack"
- # return "botsnack => reward #{@bot.myself} for being good"
- # when "hello"
- # return "hello|hi|hey|yo [#{@bot.myself}] => greet the bot"
- else
- _("%{name}: quit, restart, join, part, hide, save, nick, say, action, topic, quiet, talk, ping, mode") % {:name=>name}
- #, botsnack, hello
- end
- end
-end
-
-basics = BasicsModule.new
-
-basics.map "quit *msg",
- :action => 'bot_quit',
- :defaults => { :msg => nil },
- :auth_path => 'quit'
-basics.map "restart *msg",
- :action => 'bot_restart',
- :defaults => { :msg => nil },
- :auth_path => 'quit'
-
-basics.map "quiet [in] [:where]",
- :action => 'bot_quiet',
- :auth_path => 'talk::set'
-basics.map "talk [in] [:where]",
- :action => 'bot_talk',
- :auth_path => 'talk::set'
-
-basics.map "say :where *what",
- :action => 'bot_say',
- :auth_path => 'talk::do'
-basics.map "action :where *what",
- :action => 'bot_action',
- :auth_path => 'talk::do'
-basics.map "mode :where :what *who",
- :action => 'bot_mode',
- :auth_path => 'talk::do'
-
-basics.map "join :chan :pass",
- :action => 'bot_join',
- :defaults => {:pass => nil},
- :auth_path => 'move'
-basics.map "part :chan",
- :action => 'bot_part',
- :defaults => {:chan => nil},
- :auth_path => 'move'
-basics.map "hide",
- :action => 'bot_hide',
- :auth_path => 'move'
-
-basics.map "ping",
- :action => 'bot_ping',
- :auth_path => '!ping!'
-basics.map "help *topic",
- :action => 'bot_help',
- :defaults => { :topic => [""] },
- :auth_path => '!help!'
-
-basics.default_auth('*', false)
-
+#-- vim:sw=2:et
+#++
+#
+# :title: rbot basic management from IRC
+#
+# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
+# Copyright:: (C) 2006,2007 Giuseppe Bilotta
+# License:: GPL v2
+
+class BasicsModule < CoreBotModule
+
+ def ctcp_listen(m)
+ who = m.private? ? "me" : m.target
+ case m.ctcp.intern
+ when :PING
+ m.ctcp_reply m.message
+ @bot.irclog "@ #{m.source} pinged #{who}"
+ when :TIME
+ m.ctcp_reply Time.now.to_s
+ @bot.irclog "@ #{m.source} asked #{who} what time it is"
+ end
+ end
+
+ def bot_join(m, param)
+ if param[:pass]
+ @bot.join param[:chan], param[:pass]
+ else
+ @bot.join param[:chan]
+ end
+ end
+
+ def bot_part(m, param)
+ if param[:chan]
+ @bot.part param[:chan]
+ else
+ @bot.part m.target if m.public?
+ end
+ end
+
+ def bot_quit(m, param)
+ @bot.quit param[:msg].to_s
+ end
+
+ def bot_restart(m, param)
+ @bot.restart param[:msg].to_s
+ end
+
+ def bot_hide(m, param)
+ @bot.join 0
+ end
+
+ def bot_say(m, param)
+ @bot.say param[:where], param[:what].to_s
+ end
+
+ def bot_action(m, param)
+ @bot.action param[:where], param[:what].to_s
+ end
+
+ def bot_mode(m, param)
+ @bot.mode param[:where], param[:what], param[:who].join(" ")
+ end
+
+ def bot_ping(m, param)
+ m.reply "pong"
+ end
+
+ def bot_quiet(m, param)
+ if param.has_key?(:where)
+ @bot.set_quiet param[:where].sub(/^here$/, m.target.downcase)
+ else
+ @bot.set_quiet
+ end
+ # Make sense when the commmand is given in private or in a non-quieted
+ # channel
+ m.okay
+ end
+
+ def bot_talk(m, param)
+ if param.has_key?(:where)
+ @bot.reset_quiet param[:where].sub(/^here$/, m.target.downcase)
+ else
+ @bot.reset_quiet
+ end
+ # Make sense when the commmand is given in private or in a non-quieted
+ # channel
+ m.okay
+ end
+
+ def bot_help(m, param)
+ m.reply @bot.help(param[:topic].join(" "))
+ end
+
+ #TODO move these to a "chatback" plugin
+ # when (/^(botsnack|ciggie)$/i)
+ # @bot.say m.replyto, @lang.get("thanks_X") % m.sourcenick if(m.public?)
+ # @bot.say m.replyto, @lang.get("thanks") if(m.private?)
+ # when (/^#{Regexp.escape(@bot.nick)}!*$/)
+ # @bot.say m.replyto, @lang.get("hello_X") % m.sourcenick
+
+ # handle help requests for "core" topics
+ def help(cmd, topic="")
+ case cmd
+ when "quit"
+ _("quit [<message>] => quit IRC with message <message>")
+ when "restart"
+ _("restart => completely stop and restart the bot (including reconnect)")
+ when "join"
+ _("join <channel> [<key>] => join channel <channel> with secret key <key> if specified. #{@bot.myself} also responds to invites if you have the required access level")
+ when "part"
+ _("part <channel> => part channel <channel>")
+ when "hide"
+ _("hide => part all channels")
+ when "nick"
+ _("nick <nick> => attempt to change nick to <nick>")
+ when "say"
+ _("say <channel>|<nick> <message> => say <message> to <channel> or in private message to <nick>")
+ when "action"
+ _("action <channel>|<nick> <message> => does a /me <message> to <channel> or in private message to <nick>")
+ when "quiet"
+ _("quiet [in here|<channel>] => with no arguments, stop speaking in all channels, if \"in here\", stop speaking in this channel, or stop speaking in <channel>")
+ when "talk"
+ _("talk [in here|<channel>] => with no arguments, resume speaking in all channels, if \"in here\", resume speaking in this channel, or resume speaking in <channel>")
+ when "ping"
+ _("ping => replies with a pong")
+ when "mode"
+ _("mode <channel> <mode> <nicks> => set channel modes for <nicks> on <channel> to <mode>")
+ # when "botsnack"
+ # return "botsnack => reward #{@bot.myself} for being good"
+ # when "hello"
+ # return "hello|hi|hey|yo [#{@bot.myself}] => greet the bot"
+ else
+ _("%{name}: quit, restart, join, part, hide, save, nick, say, action, topic, quiet, talk, ping, mode") % {:name=>name}
+ #, botsnack, hello
+ end
+ end
+end
+
+basics = BasicsModule.new
+
+basics.map "quit *msg",
+ :action => 'bot_quit',
+ :defaults => { :msg => nil },
+ :auth_path => 'quit'
+basics.map "restart *msg",
+ :action => 'bot_restart',
+ :defaults => { :msg => nil },
+ :auth_path => 'quit'
+
+basics.map "quiet [in] [:where]",
+ :action => 'bot_quiet',
+ :auth_path => 'talk::set'
+basics.map "talk [in] [:where]",
+ :action => 'bot_talk',
+ :auth_path => 'talk::set'
+
+basics.map "say :where *what",
+ :action => 'bot_say',
+ :auth_path => 'talk::do'
+basics.map "action :where *what",
+ :action => 'bot_action',
+ :auth_path => 'talk::do'
+basics.map "mode :where :what *who",
+ :action => 'bot_mode',
+ :auth_path => 'talk::do'
+
+basics.map "join :chan :pass",
+ :action => 'bot_join',
+ :defaults => {:pass => nil},
+ :auth_path => 'move'
+basics.map "part :chan",
+ :action => 'bot_part',
+ :defaults => {:chan => nil},
+ :auth_path => 'move'
+basics.map "hide",
+ :action => 'bot_hide',
+ :auth_path => 'move'
+
+basics.map "ping",
+ :action => 'bot_ping',
+ :auth_path => '!ping!'
+basics.map "help *topic",
+ :action => 'bot_help',
+ :defaults => { :topic => [""] },
+ :auth_path => '!help!'
+
+basics.default_auth('*', false)
+
diff --git a/lib/rbot/core/config.rb b/lib/rbot/core/config.rb
index ad9b7c74..1b14ebd8 100644
--- a/lib/rbot/core/config.rb
+++ b/lib/rbot/core/config.rb
@@ -1,325 +1,325 @@
-#-- vim:sw=2:et
-#++
-#
-# :title: rbot config management from IRC
-#
-# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
-# Copyright:: (C) 2006,2007 Giuseppe Bilotta
-# License:: GPL v2
-
-class ConfigModule < CoreBotModule
-
- def version_string
- _("I'm a v. %{version} rubybot%{copyright}%{url}") % {
- :version => $version,
- :copyright => ", #{Irc::Bot::COPYRIGHT_NOTICE}",
- :url => " - #{Irc::Bot::SOURCE_URL}"
- }
- end
-
- def save
- @bot.config.save
- end
-
- def handle_list(m, params)
- modules = []
- if params[:module]
- @bot.config.items.each_key do |key|
- mod, name = key.to_s.split('.')
- next unless mod == params[:module]
- modules.push key unless modules.include?(name)
- end
- if modules.empty?
- m.reply _("no such module %{module}") % {:module => params[:module]}
- else
- m.reply modules.join(", ")
- end
- else
- @bot.config.items.each_key do |key|
- name = key.to_s.split('.').first
- modules.push name unless modules.include?(name)
- end
- m.reply "modules: " + modules.join(", ")
- end
- end
-
- def handle_get(m, params)
- key = params[:key].to_s.intern
- unless @bot.config.items.has_key?(key)
- m.reply _("no such config key %{key}") % {:key => key}
- return
- end
- return if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto)
- value = @bot.config.items[key].to_s
- m.reply "#{key}: #{value}"
- end
-
- def handle_desc(m, params)
- key = params[:key].to_s.intern
- unless @bot.config.items.has_key?(key)
- m.reply _("no such config key %{key}") % {:key => key}
- end
- m.reply "#{key}: #{@bot.config.items[key].desc}"
- end
-
- def handle_search(m, params)
- rx = Regexp.new(params[:rx].to_s, true)
- cfs = []
- @bot.config.items.each do |k, v|
- cfs << v if k.to_s.match(rx) or (v.desc.match(rx) rescue false)
- end
- if cfs.empty?
- m.reply _("no config key found matching %{r}") % { :r => params[:rx].to_s}
- else
- m.reply _("possible keys: %{kl}") % { :kl => cfs.map { |c| c.key}.join(', ') }
- m.reply cfs.map { |c| [c.key, c.desc].join(': ') }.join("\n")
- end
- end
-
- def handle_unset(m, params)
- key = params[:key].to_s.intern
- unless @bot.config.items.has_key?(key)
- m.reply _("no such config key %{key}") % {:key => key}
- end
- return if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto)
- @bot.config.items[key].unset
- handle_get(m, params)
- m.reply _("this config change will take effect on the next restart") if @bot.config.items[key].requires_restart
- m.reply _("this config change will take effect on the next rescan") if @bot.config.items[key].requires_rescan
- end
-
- def handle_set(m, params)
- key = params[:key].to_s.intern
- value = params[:value].join(" ")
- unless @bot.config.items.has_key?(key)
- m.reply _("no such config key %{key}") % {:key => key} unless params[:silent]
- return false
- end
- return false if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto)
- begin
- @bot.config.items[key].set_string(value)
- rescue ArgumentError => e
- m.reply _("failed to set %{key}: %{error}") % {:key => key, :error => e.message} unless params[:silent]
- return false
- end
- if @bot.config.items[key].requires_restart
- m.reply _("this config change will take effect on the next restart") unless params[:silent]
- return :restart
- elsif @bot.config.items[key].requires_rescan
- m.reply _("this config change will take effect on the next rescan") unless params[:silent]
- return :rescan
- else
- m.okay unless params[:silent]
- return true
- end
- end
-
- def handle_add(m, params)
- key = params[:key].to_s.intern
- value = params[:value]
- unless @bot.config.items.has_key?(key)
- m.reply _("no such config key %{key}") % {:key => key}
- return
- end
- unless @bot.config.items[key].kind_of?(Config::ArrayValue)
- m.reply _("config key %{key} is not an array") % {:key => key}
- return
- end
- return if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto)
- begin
- @bot.config.items[key].add(value)
- rescue ArgumentError => e
- m.reply _("failed to add %{value} to %{key}: %{error}") % {:value => value, :key => key, :error => e.message}
- return
- end
- handle_get(m,{:key => key})
- m.reply _("this config change will take effect on the next restart") if @bot.config.items[key].requires_restart
- m.reply _("this config change will take effect on the next rescan") if @bot.config.items[key].requires_rescan
- end
-
- def handle_rm(m, params)
- key = params[:key].to_s.intern
- value = params[:value]
- unless @bot.config.items.has_key?(key)
- m.reply _("no such config key %{key}") % {:key => key}
- return
- end
- unless @bot.config.items[key].kind_of?(Config::ArrayValue)
- m.reply _("config key %{key} is not an array") % {:key => key}
- return
- end
- return if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto)
- begin
- @bot.config.items[key].rm(value)
- rescue ArgumentError => e
- m.reply _("failed to remove %{value} from %{key}: %{error}") % {:value => value, :key => key, :error => e.message}
- return
- end
- handle_get(m,{:key => key})
- m.reply _("this config change will take effect on the next restart") if @bot.config.items[key].requires_restart
- m.reply _("this config change will take effect on the next rescan") if @bot.config.items[key].requires_rescan
- end
-
- def bot_save(m, param)
- @bot.save
- m.okay
- end
-
- def bot_rescan(m, param)
- m.reply _("saving ...")
- @bot.save
- m.reply _("rescanning ...")
- @bot.rescan
- m.reply _("done. %{plugin_status}") % {:plugin_status => @bot.plugins.status(true)}
- end
-
- def bot_nick(m, param)
- @bot.nickchg(param[:nick])
- end
-
- def bot_status(m, param)
- m.reply @bot.status
- end
-
- # TODO is this one of the methods that disappeared when the bot was moved
- # from the single-file to the multi-file registry?
- #
- # def bot_reg_stat(m, param)
- # m.reply @registry.stat.inspect
- # end
-
- def bot_version(m, param)
- m.reply version_string
- end
-
- def ctcp_listen(m)
- who = m.private? ? "me" : m.target
- case m.ctcp.intern
- when :VERSION
- m.ctcp_reply version_string
- @bot.irclog "@ #{m.source} asked #{who} about version info"
- when :SOURCE
- m.ctcp_reply Irc::Bot::SOURCE_URL
- @bot.irclog "@ #{m.source} asked #{who} about source info"
- end
- end
-
- def handle_help(m, params)
- m.reply help(params[:topic])
- end
-
- def help(plugin, topic="")
- case plugin
- when "config"
- case topic
- when "list"
- _("config list => list configuration modules, config list <module> => list configuration keys for module <module>")
- when "get"
- _("config get <key> => get configuration value for key <key>")
- when "unset"
- _("reset key <key> to the default")
- when "set"
- _("config set <key> <value> => set configuration value for key <key> to <value>")
- when "desc"
- _("config desc <key> => describe what key <key> configures")
- when "add"
- _("config add <value> to <key> => add value <value> to key <key> if <key> is an array")
- when "rm"
- _("config rm <value> from <key> => remove value <value> from key <key> if <key> is an array")
- else
- _("config module - bot configuration. usage: list, desc, get, set, unset, add, rm")
- # else
- # "no help for config #{topic}"
- end
- when "nick"
- _("nick <newnick> => change the bot nick to <newnick>, if possible")
- when "status"
- _("status => display some information on the bot's status")
- when "save"
- _("save => save current dynamic data and configuration")
- when "rescan"
- _("rescan => reload modules and static facts")
- when "version"
- _("version => describes software version")
- else
- _("config-related tasks: config, save, rescan, version, nick, status")
- end
- end
-
-end
-
-conf = ConfigModule.new
-
-conf.map 'config list :module',
- :action => 'handle_list',
- :defaults => {:module => false},
- :auth_path => 'show'
-# TODO this one is presently a security risk, since the bot
-# stores the master password in the config. Do we need auth levels
-# on the Bot::Config keys too?
-conf.map 'config get :key',
- :action => 'handle_get',
- :auth_path => 'show'
-conf.map 'config desc :key',
- :action => 'handle_desc',
- :auth_path => 'show'
-conf.map 'config describe :key',
- :action => 'handle_desc',
- :auth_path => 'show::desc!'
-conf.map 'config search *rx',
- :action => 'handle_search',
- :auth_path => 'show'
-
-conf.map "save",
- :action => 'bot_save'
-conf.map "rescan",
- :action => 'bot_rescan'
-conf.map "nick :nick",
- :action => 'bot_nick'
-conf.map "status",
- :action => 'bot_status',
- :auth_path => 'show::status'
-# TODO see above
-#
-# conf.map "registry stats",
-# :action => 'bot_reg_stat',
-# :auth_path => '!config::status'
-conf.map "version",
- :action => 'bot_version',
- :auth_path => 'show::status'
-
-conf.map 'config set :key *value',
- :action => 'handle_set',
- :auth_path => 'edit'
-conf.map 'config add :value to :key',
- :action => 'handle_add',
- :auth_path => 'edit'
-conf.map 'config rm :value from :key',
- :action => 'handle_rm',
- :auth_path => 'edit'
-conf.map 'config del :value from :key',
- :action => 'handle_rm',
- :auth_path => 'edit'
-conf.map 'config delete :value from :key',
- :action => 'handle_rm',
- :auth_path => 'edit'
-conf.map 'config unset :key',
- :action => 'handle_unset',
- :auth_path => 'edit'
-conf.map 'config reset :key',
- :action => 'handle_unset',
- :auth_path => 'edit'
-
-conf.map 'config help :topic',
- :action => 'handle_help',
- :defaults => {:topic => false},
- :auth_path => '!help!'
-
-conf.default_auth('*', false)
-conf.default_auth('show', true)
-conf.default_auth('show::get', false)
-# TODO these shouldn't be set here, we need a way to let the default
-# permission be specified together with the ConfigValue
-conf.default_auth('key', true)
-conf.default_auth('key::auth::password', false)
-
+#-- vim:sw=2:et
+#++
+#
+# :title: rbot config management from IRC
+#
+# Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
+# Copyright:: (C) 2006,2007 Giuseppe Bilotta
+# License:: GPL v2
+
+class ConfigModule < CoreBotModule
+
+ def version_string
+ _("I'm a v. %{version} rubybot%{copyright}%{url}") % {
+ :version => $version,
+ :copyright => ", #{Irc::Bot::COPYRIGHT_NOTICE}",
+ :url => " - #{Irc::Bot::SOURCE_URL}"
+ }
+ end
+
+ def save
+ @bot.config.save
+ end
+
+ def handle_list(m, params)
+ modules = []
+ if params[:module]
+ @bot.config.items.each_key do |key|
+ mod, name = key.to_s.split('.')
+ next unless mod == params[:module]
+ modules.push key unless modules.include?(name)
+ end
+ if modules.empty?
+ m.reply _("no such module %{module}") % {:module => params[:module]}
+ else
+ m.reply modules.join(", ")
+ end
+ else
+ @bot.config.items.each_key do |key|
+ name = key.to_s.split('.').first
+ modules.push name unless modules.include?(name)
+ end
+ m.reply "modules: " + modules.join(", ")
+ end
+ end
+
+ def handle_get(m, params)
+ key = params[:key].to_s.intern
+ unless @bot.config.items.has_key?(key)
+ m.reply _("no such config key %{key}") % {:key => key}
+ return
+ end
+ return if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto)
+ value = @bot.config.items[key].to_s
+ m.reply "#{key}: #{value}"
+ end
+
+ def handle_desc(m, params)
+ key = params[:key].to_s.intern
+ unless @bot.config.items.has_key?(key)
+ m.reply _("no such config key %{key}") % {:key => key}
+ end
+ m.reply "#{key}: #{@bot.config.items[key].desc}"
+ end
+
+ def handle_search(m, params)
+ rx = Regexp.new(params[:rx].to_s, true)
+ cfs = []
+ @bot.config.items.each do |k, v|
+ cfs << v if k.to_s.match(rx) or (v.desc.match(rx) rescue false)
+ end
+ if cfs.empty?
+ m.reply _("no config key found matching %{r}") % { :r => params[:rx].to_s}
+ else
+ m.reply _("possible keys: %{kl}") % { :kl => cfs.map { |c| c.key}.join(', ') }
+ m.reply cfs.map { |c| [c.key, c.desc].join(': ') }.join("\n")
+ end
+ end
+
+ def handle_unset(m, params)
+ key = params[:key].to_s.intern
+ unless @bot.config.items.has_key?(key)
+ m.reply _("no such config key %{key}") % {:key => key}
+ end
+ return if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto)
+ @bot.config.items[key].unset
+ handle_get(m, params)
+ m.reply _("this config change will take effect on the next restart") if @bot.config.items[key].requires_restart
+ m.reply _("this config change will take effect on the next rescan") if @bot.config.items[key].requires_rescan
+ end
+
+ def handle_set(m, params)
+ key = params[:key].to_s.intern
+ value = params[:value].join(" ")
+ unless @bot.config.items.has_key?(key)
+ m.reply _("no such config key %{key}") % {:key => key} unless params[:silent]
+ return false
+ end
+ return false if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto)
+ begin
+ @bot.config.items[key].set_string(value)
+ rescue ArgumentError => e
+ m.reply _("failed to set %{key}: %{error}") % {:key => key, :error => e.message} unless params[:silent]
+ return false
+ end
+ if @bot.config.items[key].requires_restart
+ m.reply _("this config change will take effect on the next restart") unless params[:silent]
+ return :restart
+ elsif @bot.config.items[key].requires_rescan
+ m.reply _("this config change will take effect on the next rescan") unless params[:silent]
+ return :rescan
+ else
+ m.okay unless params[:silent]
+ return true
+ end
+ end
+
+ def handle_add(m, params)
+ key = params[:key].to_s.intern
+ value = params[:value]
+ unless @bot.config.items.has_key?(key)
+ m.reply _("no such config key %{key}") % {:key => key}
+ return
+ end
+ unless @bot.config.items[key].kind_of?(Config::ArrayValue)
+ m.reply _("config key %{key} is not an array") % {:key => key}
+ return
+ end
+ return if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto)
+ begin
+ @bot.config.items[key].add(value)
+ rescue ArgumentError => e
+ m.reply _("failed to add %{value} to %{key}: %{error}") % {:value => value, :key => key, :error => e.message}
+ return
+ end
+ handle_get(m,{:key => key})
+ m.reply _("this config change will take effect on the next restart") if @bot.config.items[key].requires_restart
+ m.reply _("this config change will take effect on the next rescan") if @bot.config.items[key].requires_rescan
+ end
+
+ def handle_rm(m, params)
+ key = params[:key].to_s.intern
+ value = params[:value]
+ unless @bot.config.items.has_key?(key)
+ m.reply _("no such config key %{key}") % {:key => key}
+ return
+ end
+ unless @bot.config.items[key].kind_of?(Config::ArrayValue)
+ m.reply _("config key %{key} is not an array") % {:key => key}
+ return
+ end
+ return if !@bot.auth.allow?(@bot.config.items[key].auth_path, m.source, m.replyto)
+ begin
+ @bot.config.items[key].rm(value)
+ rescue ArgumentError => e
+ m.reply _("failed to remove %{value} from %{key}: %{error}") % {:value => value, :key => key, :error => e.message}
+ return
+ end
+ handle_get(m,{:key => key})
+ m.reply _("this config change will take effect on the next restart") if @bot.config.items[key].requires_restart
+ m.reply _("this config change will take effect on the next rescan") if @bot.config.items[key].requires_rescan
+ end
+
+ def bot_save(m, param)
+ @bot.save
+ m.okay
+ end
+
+ def bot_rescan(m, param)
+ m.reply _("saving ...")
+ @bot.save
+ m.reply _("rescanning ...")
+ @bot.rescan
+ m.reply _("done. %{plugin_status}") % {:plugin_status => @bot.plugins.status(true)}
+ end
+
+ def bot_nick(m, param)
+ @bot.nickchg(param[:nick])
+ end
+
+ def bot_status(m, param)
+ m.reply @bot.status
+ end
+
+ # TODO is this one of the methods that disappeared when the bot was moved
+ # from the single-file to the multi-file registry?
+ #
+ # def bot_reg_stat(m, param)
+ # m.reply @registry.stat.inspect
+ # end
+
+ def bot_version(m, param)
+ m.reply version_string
+ end
+
+ def ctcp_listen(m)
+ who = m.private? ? "me" : m.target
+ case m.ctcp.intern
+ when :VERSION
+ m.ctcp_reply version_string
+ @bot.irclog "@ #{m.source} asked #{who} about version info"
+ when :SOURCE
+ m.ctcp_reply Irc::Bot::SOURCE_URL
+ @bot.irclog "@ #{m.source} asked #{who} about source info"
+ end
+ end
+
+ def handle_help(m, params)
+ m.reply help(params[:topic])
+ end
+
+ def help(plugin, topic="")
+ case plugin
+ when "config"
+ case topic
+ when "list"
+ _("config list => list configuration modules, config list <module> => list configuration keys for module <module>")
+ when "get"
+ _("config get <key> => get configuration value for key <key>")
+ when "unset"
+ _("reset key <key> to the default")
+ when "set"
+ _("config set <key> <value> => set configuration value for key <key> to <value>")
+ when "desc"
+ _("config desc <key> => describe what key <key> configures")
+ when "add"
+ _("config add <value> to <key> => add value <value> to key <key> if <key> is an array")
+ when "rm"
+ _("config rm <value> from <key> => remove value <value> from key <key> if <key> is an array")
+ else
+ _("config module - bot configuration. usage: list, desc, get, set, unset, add, rm")
+ # else
+ # "no help for config #{topic}"
+ end
+ when "nick"
+ _("nick <newnick> => change the bot nick to <newnick>, if possible")
+ when "status"
+ _("status => display some information on the bot's status")
+ when "save"
+ _("save => save current dynamic data and configuration")
+ when "rescan"
+ _("rescan => reload modules and static facts")
+ when "version"
+ _("version => describes software version")
+ else
+ _("config-related tasks: config, save, rescan, version, nick, status")
+ end
+ end
+
+end
+
+conf = ConfigModule.new
+
+conf.map 'config list :module',
+ :action => 'handle_list',
+ :defaults => {:module => false},
+ :auth_path => 'show'
+# TODO this one is presently a security risk, since the bot
+# stores the master password in the config. Do we need auth levels
+# on the Bot::Config keys too?
+conf.map 'config get :key',
+ :action => 'handle_get',
+ :auth_path => 'show'
+conf.map 'config desc :key',
+ :action => 'handle_desc',
+ :auth_path => 'show'
+conf.map 'config describe :key',
+ :action => 'handle_desc',
+ :auth_path => 'show::desc!'
+conf.map 'config search *rx',
+ :action => 'handle_search',
+ :auth_path => 'show'
+
+conf.map "save",
+ :action => 'bot_save'
+conf.map "rescan",
+ :action => 'bot_rescan'
+conf.map "nick :nick",
+ :action => 'bot_nick'
+conf.map "status",
+ :action => 'bot_status',
+ :auth_path => 'show::status'
+# TODO see above
+#
+# conf.map "registry stats",
+# :action => 'bot_reg_stat',
+# :auth_path => '!config::status'
+conf.map "version",
+ :action => 'bot_version',
+ :auth_path => 'show::status'
+
+conf.map 'config set :key *value',
+ :action => 'handle_set',
+ :auth_path => 'edit'
+conf.map 'config add :value to :key',
+ :action => 'handle_add',
+ :auth_path => 'edit'
+conf.map 'config rm :value from :key',
+ :action => 'handle_rm',
+ :auth_path => 'edit'
+conf.map 'config del :value from :key',
+ :action => 'handle_rm',
+ :auth_path => 'edit'
+conf.map 'config delete :value from :key',
+ :action => 'handle_rm',
+ :auth_path => 'edit'
+conf.map 'config unset :key',
+ :action => 'handle_unset',
+ :auth_path => 'edit'
+conf.map 'config reset :key',
+ :action => 'handle_unset',
+ :auth_path => 'edit'
+
+conf.map 'config help :topic',
+ :action => 'handle_help',
+ :defaults => {:topic => false},
+ :auth_path => '!help!'
+
+conf.default_auth('*', false)
+conf.default_auth('show', true)
+conf.default_auth('show::get', false)
+# TODO these shouldn't be set here, we need a way to let the default
+# permission be specified together with the ConfigValue
+conf.default_auth('key', true)
+conf.default_auth('key::auth::password', false)
+
diff --git a/lib/rbot/irc.rb b/lib/rbot/irc.rb
index 8ef848f7..fe1aa9fa 100644
--- a/lib/rbot/irc.rb
+++ b/lib/rbot/irc.rb
@@ -1,1958 +1,1958 @@
-#-- vim:sw=2:et
-# General TODO list
-# * 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 we can remove him from the Server @users list
-# * Maybe ChannelList and UserList should be HashesOf instead of ArrayOf?
-# See items marked as TODO Ho.
-# The framework to do this is now in place, thanks to the new [] method
-# for NetmaskList, which allows retrieval by Netmask or String
-#++
-# :title: IRC module
-#
-# Basic IRC stuff
-#
-# This module defines the fundamental building blocks for IRC
-#
-# Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)
-# Copyright:: Copyright (c) 2006 Giuseppe Bilotta
-# License:: GPLv2
-
-require 'singleton'
-
-class Object
-
- # We extend the Object class with a method that
- # checks if the receiver is nil or empty
- def nil_or_empty?
- return true unless self
- return true if self.respond_to? :empty? and self.empty?
- return false
- end
-
- # We alias the to_s method to __to_s__ to make
- # it accessible in all classes
- alias :__to_s__ :to_s
-end
-
-# The Irc module is used to keep all IRC-related classes
-# in the same namespace
-#
-module Irc
-
-
- # Due to its Scandinavian origins, IRC has strange case mappings, which
- # consider the characters <tt>{}|^</tt> as the uppercase
- # equivalents of # <tt>[]\~</tt>.
- #
- # This is however not the same on all IRC servers: some use standard ASCII
- # casemapping, other do not consider <tt>^</tt> as the uppercase of
- # <tt>~</tt>
- #
- class Casemap
- @@casemaps = {}
-
- # Create a new casemap with name _name_, uppercase characters _upper_ and
- # lowercase characters _lower_
- #
- def initialize(name, upper, lower)
- @key = name.to_sym
- raise "Casemap #{name.inspect} already exists!" if @@casemaps.has_key?(@key)
- @@casemaps[@key] = {
- :upper => upper,
- :lower => lower,
- :casemap => self
- }
- end
-
- # Returns the Casemap with the given name
- #
- def Casemap.get(name)
- @@casemaps[name.to_sym][:casemap]
- end
-
- # Retrieve the 'uppercase characters' of this Casemap
- #
- def upper
- @@casemaps[@key][:upper]
- end
-
- # Retrieve the 'lowercase characters' of this Casemap
- #
- def lower
- @@casemaps[@key][:lower]
- end
-
- # Return a Casemap based on the receiver
- #
- def to_irc_casemap
- self
- end
-
- # A Casemap is represented by its lower/upper mappings
- #
- def inspect
- self.__to_s__[0..-2] + " #{upper.inspect} ~(#{self})~ #{lower.inspect}>"
- end
-
- # As a String we return our name
- #
- def to_s
- @key.to_s
- end
-
- # Two Casemaps are equal if they have the same upper and lower ranges
- #
- def ==(arg)
- other = arg.to_irc_casemap
- return self.upper == other.upper && self.lower == other.lower
- end
-
- # Give a warning if _arg_ and self are not the same Casemap
- #
- def must_be(arg)
- other = arg.to_irc_casemap
- if self == other
- return true
- else
- warn "Casemap mismatch (#{self.inspect} != #{other.inspect})"
- return false
- end
- end
-
- end
-
- # The rfc1459 casemap
- #
- class RfcCasemap < Casemap
- include Singleton
-
- def initialize
- super('rfc1459', "\x41-\x5e", "\x61-\x7e")
- end
-
- end
- RfcCasemap.instance
-
- # The strict-rfc1459 Casemap
- #
- class StrictRfcCasemap < Casemap
- include Singleton
-
- def initialize
- super('strict-rfc1459', "\x41-\x5d", "\x61-\x7d")
- end
-
- end
- StrictRfcCasemap.instance
-
- # The ascii Casemap
- #
- class AsciiCasemap < Casemap
- include Singleton
-
- def initialize
- super('ascii', "\x41-\x5a", "\x61-\x7a")
- end
-
- end
- AsciiCasemap.instance
-
-
- # This module is included by all classes that are either bound to a server
- # or should have a casemap.
- #
- module ServerOrCasemap
-
- attr_reader :server
-
- # This method initializes the instance variables @server and @casemap
- # according to the values of the hash keys :server and :casemap in _opts_
- #
- def init_server_or_casemap(opts={})
- @server = opts.fetch(:server, nil)
- raise TypeError, "#{@server} is not a valid Irc::Server" if @server and not @server.kind_of?(Server)
-
- @casemap = opts.fetch(:casemap, nil)
- if @server
- if @casemap
- @server.casemap.must_be(@casemap)
- @casemap = nil
- end
- else
- @casemap = (@casemap || 'rfc1459').to_irc_casemap
- end
- end
-
- # This is an auxiliary method: it returns true if the receiver fits the
- # server and casemap specified in _opts_, false otherwise.
- #
- def fits_with_server_and_casemap?(opts={})
- srv = opts.fetch(:server, nil)
- cmap = opts.fetch(:casemap, nil)
- cmap = cmap.to_irc_casemap unless cmap.nil?
-
- if srv.nil?
- return true if cmap.nil? or cmap == casemap
- else
- return true if srv == @server and (cmap.nil? or cmap == casemap)
- end
- return false
- end
-
- # Returns the casemap of the receiver, by looking at the bound
- # @server (if possible) or at the @casemap otherwise
- #
- def casemap
- return @server.casemap if defined?(@server) and @server
- return @casemap
- end
-
- # Returns a hash with the current @server and @casemap as values of
- # :server and :casemap
- #
- def server_and_casemap
- h = {}
- h[:server] = @server if defined?(@server) and @server
- h[:casemap] = @casemap if defined?(@casemap) and @casemap
- return h
- end
-
- # We allow up/downcasing with a different casemap
- #
- def irc_downcase(cmap=casemap)
- self.to_s.irc_downcase(cmap)
- end
-
- # Up/downcasing something that includes this module returns its
- # Up/downcased to_s form
- #
- def downcase
- self.irc_downcase
- end
-
- # We allow up/downcasing with a different casemap
- #
- def irc_upcase(cmap=casemap)
- self.to_s.irc_upcase(cmap)
- end
-
- # Up/downcasing something that includes this module returns its
- # Up/downcased to_s form
- #
- def upcase
- self.irc_upcase
- end
-
- end
-
-end
-
-
-# We start by extending the String class
-# with some IRC-specific methods
-#
-class String
-
- # This method returns the Irc::Casemap whose name is the receiver
- #
- def to_irc_casemap
- Irc::Casemap.get(self) rescue raise TypeError, "Unkown Irc::Casemap #{self.inspect}"
- end
-
- # This method returns a string which is the downcased version of the
- # receiver, according to the given _casemap_
- #
- #
- def irc_downcase(casemap='rfc1459')
- cmap = casemap.to_irc_casemap
- self.tr(cmap.upper, cmap.lower)
- end
-
- # This is the same as the above, except that the string is altered in place
- #
- # See also the discussion about irc_downcase
- #
- def irc_downcase!(casemap='rfc1459')
- cmap = casemap.to_irc_casemap
- self.tr!(cmap.upper, cmap.lower)
- end
-
- # Upcasing functions are provided too
- #
- # See also the discussion about irc_downcase
- #
- def irc_upcase(casemap='rfc1459')
- cmap = casemap.to_irc_casemap
- self.tr(cmap.lower, cmap.upper)
- end
-
- # In-place upcasing
- #
- # See also the discussion about irc_downcase
- #
- def irc_upcase!(casemap='rfc1459')
- cmap = casemap.to_irc_casemap
- self.tr!(cmap.lower, cmap.upper)
- end
-
- # This method checks if the receiver contains IRC glob characters
- #
- # IRC has a very primitive concept of globs: a <tt>*</tt> stands for "any
- # number of arbitrary characters", a <tt>?</tt> stands for "one and exactly
- # one arbitrary character". These characters can be escaped by prefixing them
- # with a slash (<tt>\\</tt>).
- #
- # A known limitation of this glob syntax is that there is no way to escape
- # the escape character itself, so it's not possible to build a glob pattern
- # where the escape character precedes a glob.
- #
- def has_irc_glob?
- self =~ /^[*?]|[^\\][*?]/
- end
-
- # This method is used to convert the receiver into a Regular Expression
- # that matches according to the IRC glob syntax
- #
- def to_irc_regexp
- regmask = Regexp.escape(self)
- regmask.gsub!(/(\\\\)?\\[*?]/) { |m|
- case m
- when /\\(\\[*?])/
- $1
- when /\\\*/
- '.*'
- when /\\\?/
- '.'
- else
- raise "Unexpected match #{m} when converting #{self}"
- end
- }
- Regexp.new("^#{regmask}$")
- end
-
-end
-
-
-# ArrayOf is a subclass of Array whose elements are supposed to be all
-# of the same class. This is not intended to be used directly, but rather
-# to be subclassed as needed (see for example Irc::UserList and Irc::NetmaskList)
-#
-# Presently, only very few selected methods from Array are overloaded to check
-# if the new elements are the correct class. An orthodox? method is provided
-# to check the entire ArrayOf against the appropriate class.
-#
-class ArrayOf < Array
-
- attr_reader :element_class
-
- # Create a new ArrayOf whose elements are supposed to be all of type _kl_,
- # optionally filling it with the elements from the Array argument.
- #
- def initialize(kl, ar=[])
- raise TypeError, "#{kl.inspect} must be a class name" unless kl.kind_of?(Class)
- super()
- @element_class = kl
- case ar
- when Array
- insert(0, *ar)
- else
- raise TypeError, "#{self.class} can only be initialized from an Array"
- end
- end
-
- def inspect
- self.__to_s__[0..-2].sub(/:[^:]+$/,"[#{@element_class}]\\0") + " #{super}>"
- end
-
- # Private method to check the validity of the elements passed to it
- # and optionally raise an error
- #
- # TODO should it accept nils as valid?
- #
- def internal_will_accept?(raising, *els)
- els.each { |el|
- unless el.kind_of?(@element_class)
- raise TypeError, "#{el.inspect} is not of class #{@element_class}" if raising
- return false
- end
- }
- return true
- end
- private :internal_will_accept?
-
- # This method checks if the passed arguments are acceptable for our ArrayOf
- #
- def will_accept?(*els)
- internal_will_accept?(false, *els)
- end
-
- # This method checks that all elements are of the appropriate class
- #
- def valid?
- will_accept?(*self)
- end
-
- # This method is similar to the above, except that it raises an exception
- # if the receiver is not valid
- #
- def validate
- raise TypeError unless valid?
- end
-
- # Overloaded from Array#<<, checks for appropriate class of argument
- #
- def <<(el)
- super(el) if internal_will_accept?(true, el)
- end
-
- # Overloaded from Array#&, checks for appropriate class of argument elements
- #
- def &(ar)
- r = super(ar)
- ArrayOf.new(@element_class, r) if internal_will_accept?(true, *r)
- end
-
- # Overloaded from Array#+, checks for appropriate class of argument elements
- #
- def +(ar)
- ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)
- end
-
- # Overloaded from Array#-, so that an ArrayOf is returned. There is no need
- # to check the validity of the elements in the argument
- #
- def -(ar)
- ArrayOf.new(@element_class, super(ar)) # if internal_will_accept?(true, *ar)
- end
-
- # Overloaded from Array#|, checks for appropriate class of argument elements
- #
- def |(ar)
- ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)
- end
-
- # Overloaded from Array#concat, checks for appropriate class of argument
- # elements
- #
- def concat(ar)
- super(ar) if internal_will_accept?(true, *ar)
- end
-
- # Overloaded from Array#insert, checks for appropriate class of argument
- # elements
- #
- def insert(idx, *ar)
- super(idx, *ar) if internal_will_accept?(true, *ar)
- end
-
- # Overloaded from Array#replace, checks for appropriate class of argument
- # elements
- #
- def replace(ar)
- super(ar) if (ar.kind_of?(ArrayOf) && ar.element_class <= @element_class) or internal_will_accept?(true, *ar)
- end
-
- # Overloaded from Array#push, checks for appropriate class of argument
- # elements
- #
- def push(*ar)
- super(*ar) if internal_will_accept?(true, *ar)
- end
-
- # Overloaded from Array#unshift, checks for appropriate class of argument(s)
- #
- def unshift(*els)
- els.each { |el|
- super(el) if internal_will_accept?(true, *els)
- }
- end
-
- # We introduce the 'downcase' method, which maps downcase() to all the Array
- # elements, properly failing when the elements don't have a downcase method
- #
- def downcase
- self.map { |el| el.downcase }
- end
-
- # Modifying methods which we don't handle yet are made private
- #
- private :[]=, :collect!, :map!, :fill, :flatten!
-
-end
-
-
-# We extend the Regexp class with an Irc module which will contain some
-# Irc-specific regexps
-#
-class Regexp
-
- # We start with some general-purpose ones which will be used in the
- # Irc module too, but are useful regardless
- DIGITS = /\d+/
- HEX_DIGIT = /[0-9A-Fa-f]/
- HEX_DIGITS = /#{HEX_DIGIT}+/
- HEX_OCTET = /#{HEX_DIGIT}#{HEX_DIGIT}?/
- DEC_OCTET = /[01]?\d?\d|2[0-4]\d|25[0-5]/
- DEC_IP_ADDR = /#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}/
- HEX_IP_ADDR = /#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}/
- IP_ADDR = /#{DEC_IP_ADDR}|#{HEX_IP_ADDR}/
-
- # IPv6, from Resolv::IPv6, without the \A..\z anchors
- HEX_16BIT = /#{HEX_DIGIT}{1,4}/
- IP6_8Hex = /(?:#{HEX_16BIT}:){7}#{HEX_16BIT}/
- IP6_CompressedHex = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)/
- IP6_6Hex4Dec = /((?:#{HEX_16BIT}:){6,6})#{DEC_IP_ADDR}/
- IP6_CompressedHex4Dec = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}:)*)#{DEC_IP_ADDR}/
- IP6_ADDR = /(?:#{IP6_8Hex})|(?:#{IP6_CompressedHex})|(?:#{IP6_6Hex4Dec})|(?:#{IP6_CompressedHex4Dec})/
-
- # We start with some IRC related regular expressions, used to match
- # Irc::User nicks and users and Irc::Channel names
- #
- # For each of them we define two versions of the regular expression:
- # * a generic one, which should match for any server but may turn out to
- # match more than a specific server would accept
- # * an RFC-compliant matcher
- #
- module Irc
-
- # Channel-name-matching regexps
- CHAN_FIRST = /[#&+]/
- CHAN_SAFE = /![A-Z0-9]{5}/
- CHAN_ANY = /[^\x00\x07\x0A\x0D ,:]/
- GEN_CHAN = /(?:#{CHAN_FIRST}|#{CHAN_SAFE})#{CHAN_ANY}+/
- RFC_CHAN = /#{CHAN_FIRST}#{CHAN_ANY}{1,49}|#{CHAN_SAFE}#{CHAN_ANY}{1,44}/
-
- # Nick-matching regexps
- SPECIAL_CHAR = /[\x5b-\x60\x7b-\x7d]/
- NICK_FIRST = /#{SPECIAL_CHAR}|[[:alpha:]]/
- NICK_ANY = /#{SPECIAL_CHAR}|[[:alnum:]]|-/
- GEN_NICK = /#{NICK_FIRST}#{NICK_ANY}+/
- RFC_NICK = /#{NICK_FIRST}#{NICK_ANY}{0,8}/
-
- USER_CHAR = /[^\x00\x0a\x0d @]/
- GEN_USER = /#{USER_CHAR}+/
-
- # Host-matching regexps
- HOSTNAME_COMPONENT = /[[:alnum:]](?:[[:alnum:]]|-)*[[:alnum:]]*/
- HOSTNAME = /#{HOSTNAME_COMPONENT}(?:\.#{HOSTNAME_COMPONENT})*/
- HOSTADDR = /#{IP_ADDR}|#{IP6_ADDR}/
-
- GEN_HOST = /#{HOSTNAME}|#{HOSTADDR}/
-
- # # FreeNode network replaces the host of affiliated users with
- # # 'virtual hosts'
- # # FIXME we need the true syntax to match it properly ...
- # PDPC_HOST_PART = /[0-9A-Za-z.-]+/
- # PDPC_HOST = /#{PDPC_HOST_PART}(?:\/#{PDPC_HOST_PART})+/
-
- # # NOTE: the final optional and non-greedy dot is needed because some
- # # servers (e.g. FreeNode) send the hostname of the services as "services."
- # # which is not RFC compliant, but sadly done.
- # GEN_HOST_EXT = /#{PDPC_HOST}|#{GEN_HOST}\.??/
-
- # Sadly, different networks have different, RFC-breaking ways of cloaking
- # the actualy host address: see above for an example to handle FreeNode.
- # Another example would be Azzurra, wich also inserts a "=" in the
- # cloacked host. So let's just not care about this and go with the simplest
- # thing:
- GEN_HOST_EXT = /\S+/
-
- # User-matching Regexp
- GEN_USER_ID = /(#{GEN_NICK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/
-
- # Things such has the BIP proxy send invalid nicks in a complete netmask,
- # so we want to match this, rather: this matches either a compliant nick
- # or a a string with a very generic nick, a very generic hostname after an
- # @ sign, and an optional user after a !
- BANG_AT = /#{GEN_NICK}|\S+?(?:!\S+?)?@\S+?/
-
- # # For Netmask, we want to allow wildcards * and ? in the nick
- # # (they are already allowed in the user and host part
- # GEN_NICK_MASK = /(?:#{NICK_FIRST}|[?*])?(?:#{NICK_ANY}|[?*])+/
-
- # # Netmask-matching Regexp
- # GEN_MASK = /(#{GEN_NICK_MASK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/
-
- end
-
-end
-
-
-module Irc
-
-
- # A Netmask identifies each user by collecting its nick, username and
- # hostname in the form <tt>nick!user@host</tt>
- #
- # Netmasks can also contain glob patterns in any of their components; in
- # this form they are used to refer to more than a user or to a user
- # appearing under different forms.
- #
- # Example:
- # * <tt>*!*@*</tt> refers to everybody
- # * <tt>*!someuser@somehost</tt> refers to user +someuser+ on host +somehost+
- # regardless of the nick used.
- #
- class Netmask
-
- # Netmasks have an associated casemap unless they are bound to a server
- #
- include ServerOrCasemap
-
- attr_reader :nick, :user, :host
- alias :ident :user
-
- # Create a new Netmask from string _str_, which must be in the form
- # _nick_!_user_@_host_
- #
- # It is possible to specify a server or a casemap in the optional Hash:
- # these are used to associate the Netmask with the given server and to set
- # its casemap: if a server is specified and a casemap is not, the server's
- # casemap is used. If both a server and a casemap are specified, the
- # casemap must match the server's casemap or an exception will be raised.
- #
- # Empty +nick+, +user+ or +host+ are converted to the generic glob pattern
- #
- def initialize(str="", opts={})
- # First of all, check for server/casemap option
- #
- init_server_or_casemap(opts)
-
- # Now we can see if the given string _str_ is an actual Netmask
- if str.respond_to?(:to_str)
- case str.to_str
- # We match a pretty generic string, to work around non-compliant
- # servers
- when /^(?:(\S+?)(?:(?:!(\S+?))?@(\S+))?)?$/
- # We do assignment using our internal methods
- self.nick = $1
- self.user = $2
- self.host = $3
- else
- raise ArgumentError, "#{str.to_str.inspect} does not represent a valid #{self.class}"
- end
- else
- raise TypeError, "#{str} cannot be converted to a #{self.class}"
- end
- end
-
- # A Netmask is easily converted to a String for the usual representation.
- # We skip the user or host parts if they are "*", unless we've been asked
- # for the full form
- #
- def to_s
- ret = nick.dup
- ret << "!" << user unless user == "*"
- ret << "@" << host unless host == "*"
- return ret
- end
-
- def fullform
- "#{nick}!#{user}@#{host}"
- end
-
- alias :to_str :fullform
-
- # This method downcases the fullform of the netmask. While this may not be
- # significantly different from the #downcase() method provided by the
- # ServerOrCasemap mixin, it's significantly different for Netmask
- # subclasses such as User whose simple downcasing uses the nick only.
- #
- def full_irc_downcase(cmap=casemap)
- self.fullform.irc_downcase(cmap)
- end
-
- # full_downcase() will return the fullform downcased according to the
- # User's own casemap
- #
- def full_downcase
- self.full_irc_downcase
- end
-
- # This method returns a new Netmask which is the fully downcased version
- # of the receiver
- def downcased
- return self.full_downcase.to_irc_netmask(server_and_casemap)
- end
-
- # Converts the receiver into a Netmask with the given (optional)
- # server/casemap association. We return self unless a conversion
- # is needed (different casemap/server)
- #
- # Subclasses of Netmask will return a new Netmask, using full_downcase
- #
- def to_irc_netmask(opts={})
- if self.class == Netmask
- return self if fits_with_server_and_casemap?(opts)
- end
- return self.full_downcase.to_irc_netmask(server_and_casemap.merge(opts))
- end
-
- # Converts the receiver into a User with the given (optional)
- # server/casemap association. We return self unless a conversion
- # is needed (different casemap/server)
- #
- def to_irc_user(opts={})
- self.fullform.to_irc_user(server_and_casemap.merge(opts))
- end
-
- # Inspection of a Netmask reveals the server it's bound to (if there is
- # one), its casemap and the nick, user and host part
- #
- def inspect
- str = self.__to_s__[0..-2]
- str << " @server=#{@server}" if defined?(@server) and @server
- str << " @nick=#{@nick.inspect} @user=#{@user.inspect}"
- str << " @host=#{@host.inspect} casemap=#{casemap.inspect}"
- str << ">"
- end
-
- # Equality: two Netmasks are equal if they downcase to the same thing
- #
- # TODO we may want it to try other.to_irc_netmask
- #
- def ==(other)
- return false unless other.kind_of?(self.class)
- self.downcase == other.downcase
- end
-
- # This method changes the nick of the Netmask, defaulting to the generic
- # glob pattern if the result is the null string.
- #
- def nick=(newnick)
- @nick = newnick.to_s
- @nick = "*" if @nick.empty?
- end
-
- # This method changes the user of the Netmask, defaulting to the generic
- # glob pattern if the result is the null string.
- #
- def user=(newuser)
- @user = newuser.to_s
- @user = "*" if @user.empty?
- end
- alias :ident= :user=
-
- # This method changes the hostname of the Netmask, defaulting to the generic
- # glob pattern if the result is the null string.
- #
- def host=(newhost)
- @host = newhost.to_s
- @host = "*" if @host.empty?
- end
-
- # We can replace everything at once with data from another Netmask
- #
- def replace(other)
- case other
- when Netmask
- nick = other.nick
- user = other.user
- host = other.host
- @server = other.server
- @casemap = other.casemap unless @server
- else
- replace(other.to_irc_netmask(server_and_casemap))
- end
- end
-
- # This method checks if a Netmask is definite or not, by seeing if
- # any of its components are defined by globs
- #
- def has_irc_glob?
- return @nick.has_irc_glob? || @user.has_irc_glob? || @host.has_irc_glob?
- end
-
- def generalize
- u = user.dup
- unless u.has_irc_glob?
- u.sub!(/^[in]=/, '=') or u.sub!(/^\W(\w+)/, '\1')
- u = '*' + u
- end
-
- h = host.dup
- unless h.has_irc_glob?
- if h.include? '/'
- h.sub!(/x-\w+$/, 'x-*')
- else
- h.match(/^[^\.]+\.[^\.]+$/) or
- h.sub!(/azzurra[=-][0-9a-f]+/i, '*') or # hello, azzurra, you suck!
- h.sub!(/^(\d+\.\d+\.\d+\.)\d+$/, '\1*') or
- h.sub!(/^[^\.]+\./, '*.')
- end
- end
- return Netmask.new("*!#{u}@#{h}", server_and_casemap)
- end
-
- # This method is used to match the current Netmask against another one
- #
- # The method returns true if each component of the receiver matches the
- # corresponding component of the argument. By _matching_ here we mean
- # that any netmask described by the receiver is also described by the
- # argument.
- #
- # In this sense, matching is rather simple to define in the case when the
- # receiver has no globs: it is just necessary to check if the argument
- # describes the receiver, which can be done by matching it against the
- # argument converted into an IRC Regexp (see String#to_irc_regexp).
- #
- # The situation is also easy when the receiver has globs and the argument
- # doesn't, since in this case the result is false.
- #
- # The more complex case in which both the receiver and the argument have
- # globs is not handled yet.
- #
- def matches?(arg)
- cmp = arg.to_irc_netmask(:casemap => casemap)
- debug "Matching #{self.fullform} against #{arg.inspect} (#{cmp.fullform})"
- [:nick, :user, :host].each { |component|
- us = self.send(component).irc_downcase(casemap)
- them = cmp.send(component).irc_downcase(casemap)
- if us.has_irc_glob? && them.has_irc_glob?
- next if us == them
- warn NotImplementedError
- return false
- end
- return false if us.has_irc_glob? && !them.has_irc_glob?
- return false unless us =~ them.to_irc_regexp
- }
- return true
- end
-
- # Case equality. Checks if arg matches self
- #
- def ===(arg)
- arg.to_irc_netmask(:casemap => casemap).matches?(self)
- end
-
- # Sorting is done via the fullform
- #
- def <=>(arg)
- case arg
- when Netmask
- self.fullform.irc_downcase(casemap) <=> arg.fullform.irc_downcase(casemap)
- else
- self.downcase <=> arg.downcase
- end
- end
-
- end
-
-
- # A NetmaskList is an ArrayOf <code>Netmask</code>s
- #
- class NetmaskList < ArrayOf
-
- # Create a new NetmaskList, optionally filling it with the elements from
- # the Array argument fed to it.
- #
- def initialize(ar=[])
- super(Netmask, ar)
- end
-
- # We enhance the [] method by allowing it to pick an element that matches
- # a given Netmask, a String or a Regexp
- # TODO take into consideration the opportunity to use select() instead of
- # find(), and/or a way to let the user choose which one to take (second
- # argument?)
- #
- def [](*args)
- if args.length == 1
- case args[0]
- when Netmask
- self.find { |mask|
- mask.matches?(args[0])
- }
- when String
- self.find { |mask|
- mask.matches?(args[0].to_irc_netmask(:casemap => mask.casemap))
- }
- when Regexp
- self.find { |mask|
- mask.fullform =~ args[0]
- }
- else
- super(*args)
- end
- else
- super(*args)
- end
- end
-
- end
-
-end
-
-
-class String
-
- # We keep extending String, this time adding a method that converts a
- # String into an Irc::Netmask object
- #
- def to_irc_netmask(opts={})
- Irc::Netmask.new(self, opts)
- end
-
-end
-
-
-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.
- #
- # Ideally, the user and host information of an IRC User should never
- # change, and it shouldn't contain glob patterns. However, IRC is somewhat
- # idiosincratic and it may be possible to know the nick of a User much before
- # its user and host are known. Moreover, some networks (namely Freenode) may
- # change the hostname of a User when (s)he identifies with Nickserv.
- #
- # As a consequence, we must allow changes to a User host and user attributes.
- # We impose a restriction, though: they may not contain glob patterns, except
- # for the special case of an unknown user/host which is represented by a *.
- #
- # It is possible to create a totally unknown User (e.g. for initializations)
- # by setting the nick to * too.
- #
- # TODO list:
- # * see if it's worth to add the other USER data
- # * see if it's worth to add NICKSERV status
- #
- class User < Netmask
- alias :to_s :nick
-
- attr_accessor :real_name
-
- # 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="", opts={})
- super
- 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
- @real_name = String.new
- end
-
- # The nick of a User may be changed freely, but it must not contain glob patterns.
- #
- def nick=(newnick)
- raise "Can't change the nick to #{newnick}" if defined?(@nick) and newnick.has_irc_glob?
- super
- end
-
- # We have to allow changing the user of an Irc User due to some networks
- # (e.g. Freenode) changing hostmasks on the fly. We still check if the new
- # user data has glob patterns though.
- #
- def user=(newuser)
- raise "Can't change the username to #{newuser}" if defined?(@user) and newuser.has_irc_glob?
- super
- end
-
- # We have to allow changing the host of an Irc User due to some networks
- # (e.g. Freenode) changing hostmasks on the fly. We still check if the new
- # host data has glob patterns though.
- #
- def host=(newhost)
- raise "Can't change the hostname to #{newhost}" if defined?(@host) and newhost.has_irc_glob?
- super
- end
-
- # Checks if a User is well-known or not by looking at the hostname and user
- #
- def known?
- return nick != "*" && 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
-
- # Since to_irc_user runs the same checks on server and channel as
- # to_irc_netmask, we just try that and return self if it works.
- #
- # Subclasses of User will return self if possible.
- #
- def to_irc_user(opts={})
- return self if fits_with_server_and_casemap?(opts)
- return self.full_downcase.to_irc_user(opts)
- end
-
- # We can replace everything at once with data from another User
- #
- def replace(other)
- case other
- when User
- self.nick = other.nick
- self.user = other.user
- self.host = other.host
- @server = other.server
- @casemap = other.casemap unless @server
- @away = other.away?
- else
- self.replace(other.to_irc_user(server_and_casemap))
- end
- end
-
- def modes_on(channel)
- case channel
- when Channel
- channel.modes_of(self)
- else
- return @server.channel(channel).modes_of(self) if @server
- raise "Can't resolve channel #{channel}"
- end
- end
-
- def is_op?(channel)
- case channel
- when Channel
- channel.has_op?(self)
- else
- return @server.channel(channel).has_op?(self) if @server
- raise "Can't resolve channel #{channel}"
- end
- end
-
- def is_voice?(channel)
- case channel
- when Channel
- channel.has_voice?(self)
- else
- return @server.channel(channel).has_voice?(self) if @server
- raise "Can't resolve channel #{channel}"
- end
- end
- end
-
-
- # A UserList is an ArrayOf <code>User</code>s
- # We derive it from NetmaskList, which allows us to inherit any special
- # NetmaskList method
- #
- class UserList < NetmaskList
-
- # Create a new UserList, optionally filling it with the elements from
- # the Array argument fed to it.
- #
- def initialize(ar=[])
- super(ar)
- @element_class = User
- end
-
- # Convenience method: convert the UserList to a list of nicks. The indices
- # are preserved
- #
- def nicks
- self.map { |user| user.nick }
- end
-
- end
-
-end
-
-class String
-
- # We keep extending String, this time adding a method that converts a
- # String into an Irc::User object
- #
- def to_irc_user(opts={})
- Irc::User.new(self, opts)
- end
-
-end
-
-module Irc
-
- # An IRC Channel is identified by its name, and it has a set of properties:
- # * a Channel::Topic
- # * a UserList
- # * a set of Channel::Modes
- #
- # The Channel::Topic and Channel::Mode classes are defined within the
- # Channel namespace because they only make sense there
- #
- class Channel
-
-
- # Mode on a Channel
- #
- class Mode
- attr_reader :channel
- def initialize(ch)
- @channel = ch
- end
-
- end
-
-
- # Channel modes of type A manipulate lists
- #
- # Example: b (banlist)
- #
- class ModeTypeA < Mode
- attr_reader :list
- def initialize(ch)
- super
- @list = NetmaskList.new
- end
-
- def set(val)
- nm = @channel.server.new_netmask(val)
- @list << nm unless @list.include?(nm)
- end
-
- def reset(val)
- nm = @channel.server.new_netmask(val)
- @list.delete(nm)
- end
-
- end
-
-
- # Channel modes of type B need an argument
- #
- # Example: k (key)
- #
- class ModeTypeB < Mode
- def initialize(ch)
- super
- @arg = nil
- end
-
- def status
- @arg
- end
- alias :value :status
-
- def set(val)
- @arg = val
- end
-
- 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 UserMode < ModeTypeB
- attr_reader :list
- alias :users :list
- def initialize(ch)
- super
- @list = UserList.new
- end
-
- def set(val)
- u = @channel.server.user(val)
- @list << u unless @list.include?(u)
- end
-
- def reset(val)
- u = @channel.server.user(val)
- @list.delete(u)
- end
-
- end
-
-
- # Channel modes of type C need an argument when set,
- # but not when they get reset
- #
- # Example: l (limit)
- #
- class ModeTypeC < Mode
- def initialize(ch)
- super
- @arg = nil
- end
-
- def status
- @arg
- end
- alias :value :status
-
- def set(val)
- @arg = val
- end
-
- def reset
- @arg = nil
- end
-
- end
-
-
- # Channel modes of type D are basically booleans
- #
- # Example: m (moderate)
- #
- class ModeTypeD < Mode
- def initialize(ch)
- super
- @set = false
- end
-
- def set?
- return @set
- end
-
- def set
- @set = true
- end
-
- def reset
- @set = false
- end
-
- end
-
-
- # A Topic represents the topic of a channel. It consists of
- # the topic itself, who set it and when
- #
- class Topic
- attr_accessor :text, :set_by, :set_on
- alias :to_s :text
-
- # Create a new Topic setting the text, the creator and
- # the creation time
- #
- def initialize(text="", set_by="", set_on=Time.new)
- @text = text
- @set_by = set_by.to_irc_netmask
- @set_on = set_on
- end
-
- # Replace a Topic with another one
- #
- def replace(topic)
- raise TypeError, "#{topic.inspect} is not of class #{self.class}" unless topic.kind_of?(self.class)
- @text = topic.text.dup
- @set_by = topic.set_by.dup
- @set_on = topic.set_on.dup
- end
-
- # Returns self
- #
- def to_irc_channel_topic
- self
- end
-
- end
-
- end
-
-end
-
-
-class String
-
- # Returns an Irc::Channel::Topic with self as text
- #
- def to_irc_channel_topic
- Irc::Channel::Topic.new(self)
- end
-
-end
-
-
-module Irc
-
-
- # Here we start with the actual Channel class
- #
- class Channel
-
- include ServerOrCasemap
- attr_reader :name, :topic, :mode, :users
- alias :to_s :name
-
- def inspect
- str = self.__to_s__[0..-2]
- str << " on server #{server}" if server
- str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"
- str << " @users=[#{user_nicks.sort.join(', ')}]"
- str << ">"
- end
-
- # Returns self
- #
- def to_irc_channel
- self
- end
-
- # TODO Ho
- def user_nicks
- @users.map { |u| u.downcase }
- end
-
- # Checks if the receiver already has a user with the given _nick_
- #
- def has_user?(nick)
- @users.index(nick.to_irc_user(server_and_casemap))
- end
-
- # Returns the user with nick _nick_, if available
- #
- def get_user(nick)
- idx = has_user?(nick)
- @users[idx] if idx
- end
-
- # Adds a user to the channel
- #
- def add_user(user, opts={})
- silent = opts.fetch(:silent, false)
- if has_user?(user)
- warn "Trying to add user #{user} to channel #{self} again" unless silent
- else
- @users << user.to_irc_user(server_and_casemap)
- end
- end
-
- # Creates a new channel with the given name, optionally setting the topic
- # and an initial users list.
- #
- # No additional info is created here, because the channel flags and userlists
- # allowed depend on the server.
- #
- def initialize(name, topic=nil, users=[], opts={})
- raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?
- warn "Unknown channel prefix #{name[0].chr}" if name !~ /^[&#+!]/
- raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/
-
- init_server_or_casemap(opts)
-
- @name = name
-
- @topic = topic ? topic.to_irc_channel_topic : Channel::Topic.new
-
- @users = UserList.new
-
- users.each { |u|
- add_user(u)
- }
-
- # Flags
- @mode = {}
- end
-
- # Removes a user from the channel
- #
- def delete_user(user)
- @mode.each { |sym, mode|
- mode.reset(user) if mode.kind_of?(UserMode)
- }
- @users.delete(user)
- end
-
- # The channel prefix
- #
- def prefix
- name[0].chr
- end
-
- # A channel is local to a server if it has the '&' prefix
- #
- def local?
- name[0] == 0x26
- end
-
- # A channel is modeless if it has the '+' prefix
- #
- def modeless?
- name[0] == 0x2b
- end
-
- # A channel is safe if it has the '!' prefix
- #
- def safe?
- name[0] == 0x21
- end
-
- # A channel is normal if it has the '#' prefix
- #
- def normal?
- name[0] == 0x23
- end
-
- # Create a new mode
- #
- def create_mode(sym, kl)
- @mode[sym.to_sym] = kl.new(self)
- end
-
- def modes_of(user)
- l = []
- @mode.map { |s, m|
- l << s if (m.class <= UserMode and m.list[user])
- }
- l
- end
-
- def has_op?(user)
- @mode.has_key?(:o) and @mode[:o].list[user]
- end
-
- def has_voice?(user)
- @mode.has_key?(:v) and @mode[:v].list[user]
- end
- end
-
-
- # A ChannelList is an ArrayOf <code>Channel</code>s
- #
- class ChannelList < ArrayOf
-
- # Create a new ChannelList, optionally filling it with the elements from
- # the Array argument fed to it.
- #
- def initialize(ar=[])
- super(Channel, ar)
- end
-
- # Convenience method: convert the ChannelList to a list of channel names.
- # The indices are preserved
- #
- def names
- self.map { |chan| chan.name }
- end
-
- end
-
-end
-
-
-class String
-
- # We keep extending String, this time adding a method that converts a
- # String into an Irc::Channel object
- #
- def to_irc_channel(opts={})
- Irc::Channel.new(self, opts)
- end
-
-end
-
-
-module Irc
-
-
- # An IRC Server represents the Server the client is connected to.
- #
- class Server
-
- attr_reader :hostname, :version, :usermodes, :chanmodes
- alias :to_s :hostname
- attr_reader :supports, :capabilities
-
- attr_reader :channels, :users
-
- # TODO Ho
- def channel_names
- @channels.map { |ch| ch.downcase }
- end
-
- # TODO Ho
- def user_nicks
- @users.map { |u| u.downcase }
- end
-
- def inspect
- chans, users = [@channels, @users].map {|d|
- d.sort { |a, b|
- a.downcase <=> b.downcase
- }.map { |x|
- x.inspect
- }
- }
-
- str = self.__to_s__[0..-2]
- str << " @hostname=#{hostname}"
- str << " @channels=#{chans}"
- str << " @users=#{users}"
- str << ">"
- end
-
- # Create a new Server, with all instance variables reset to nil (for
- # scalar variables), empty channel and user lists and @supports
- # initialized to the default values for all known supported features.
- #
- def initialize
- @hostname = @version = @usermodes = @chanmodes = nil
-
- @channels = ChannelList.new
-
- @users = UserList.new
-
- reset_capabilities
- end
-
- # Resets the server capabilities
- #
- def reset_capabilities
- @supports = {
- :casemapping => 'rfc1459'.to_irc_casemap,
- :chanlimit => {},
- :chanmodes => {
- :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 => 50,
- :chantypes => "#&!+",
- :excepts => nil,
- :idchan => {},
- :invex => nil,
- :kicklen => nil,
- :maxlist => {},
- :modes => 3,
- :network => nil,
- :nicklen => 9,
- :prefix => {
- :modes => [:o, :v],
- :prefixes => [:"@", :+]
- },
- :safelist => nil,
- :statusmsg => nil,
- :std => nil,
- :targmax => {},
- :topiclen => nil
- }
- @capabilities = {}
- end
-
- # Convert a mode (o, v, h, ...) to the corresponding
- # prefix (@, +, %, ...). See also mode_for_prefix
- def prefix_for_mode(mode)
- return @supports[:prefix][:prefixes][
- @supports[:prefix][:modes].index(mode.to_sym)
- ]
- end
-
- # Convert a prefix (@, +, %, ...) to the corresponding
- # mode (o, v, h, ...). See also prefix_for_mode
- def mode_for_prefix(pfx)
- return @supports[:prefix][:modes][
- @supports[:prefix][:prefixes].index(pfx.to_sym)
- ]
- end
-
- # Resets the Channel and User list
- #
- def reset_lists
- @users.reverse_each { |u|
- delete_user(u)
- }
- @channels.reverse_each { |u|
- delete_channel(u)
- }
- end
-
- # Clears the server
- #
- def clear
- reset_lists
- reset_capabilities
- @hostname = @version = @usermodes = @chanmodes = nil
- end
-
- # This method is used to parse a 004 RPL_MY_INFO line
- #
- def parse_my_info(line)
- ar = line.split(' ')
- @hostname = ar[0]
- @version = ar[1]
- @usermodes = ar[2]
- @chanmodes = ar[3]
- end
-
- def noval_warn(key, val, &block)
- if val
- yield if block_given?
- else
- warn "No #{key.to_s.upcase} value"
- end
- end
-
- def val_warn(key, val, &block)
- if val == true or val == false or val.nil?
- yield if block_given?
- else
- warn "No #{key.to_s.upcase} value must be specified, got #{val}"
- end
- end
- private :noval_warn, :val_warn
-
- # This method is used to parse a 005 RPL_ISUPPORT line
- #
- # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt]
- #
- def parse_isupport(line)
- debug "Parsing ISUPPORT #{line.inspect}"
- ar = line.split(' ')
- reparse = ""
- ar.each { |en|
- prekey, val = en.split('=', 2)
- if prekey =~ /^-(.*)/
- key = $1.downcase.to_sym
- val = false
- else
- key = prekey.downcase.to_sym
- end
- case key
- when :casemapping
- noval_warn(key, val) {
- @supports[key] = val.to_irc_casemap
- }
- when :chanlimit, :idchan, :maxlist, :targmax
- noval_warn(key, val) {
- groups = val.split(',')
- groups.each { |g|
- k, v = g.split(':')
- @supports[key][k] = v.to_i || 0
- if @supports[key][k] == 0
- warn "Deleting #{key} limit of 0 for #{k}"
- @supports[key].delete(k)
- end
- }
- }
- when :chanmodes
- noval_warn(key, val) {
- groups = val.split(',')
- @supports[key][:typea] = groups[0].scan(/./).map { |x| x.to_sym}
- @supports[key][:typeb] = groups[1].scan(/./).map { |x| x.to_sym}
- @supports[key][:typec] = groups[2].scan(/./).map { |x| x.to_sym}
- @supports[key][:typed] = groups[3].scan(/./).map { |x| x.to_sym}
- }
- when :channellen, :kicklen, :modes, :topiclen
- if val
- @supports[key] = val.to_i
- else
- @supports[key] = nil
- end
- when :chantypes
- @supports[key] = val # can also be nil
- when :excepts
- val ||= 'e'
- @supports[key] = val
- when :invex
- val ||= 'I'
- @supports[key] = val
- when :maxchannels
- noval_warn(key, val) {
- reparse += "CHANLIMIT=(chantypes):#{val} "
- }
- when :maxtargets
- noval_warn(key, val) {
- @supports[:targmax]['PRIVMSG'] = val.to_i
- @supports[:targmax]['NOTICE'] = val.to_i
- }
- when :network
- noval_warn(key, val) {
- @supports[key] = val
- }
- when :nicklen
- noval_warn(key, val) {
- @supports[key] = val.to_i
- }
- when :prefix
- if val
- val.scan(/\((.*)\)(.*)/) { |m, p|
- @supports[key][:modes] = m.scan(/./).map { |x| x.to_sym}
- @supports[key][:prefixes] = p.scan(/./).map { |x| x.to_sym}
- }
- else
- @supports[key][:modes] = nil
- @supports[key][:prefixes] = nil
- end
- when :safelist
- val_warn(key, val) {
- @supports[key] = val.nil? ? true : val
- }
- when :statusmsg
- noval_warn(key, val) {
- @supports[key] = val.scan(/./)
- }
- when :std
- noval_warn(key, val) {
- @supports[key] = val.split(',')
- }
- else
- @supports[key] = val.nil? ? true : val
- end
- }
- reparse.gsub!("(chantypes)",@supports[:chantypes])
- parse_isupport(reparse) unless reparse.empty?
- end
-
- # Returns the casemap of the server.
- #
- def casemap
- @supports[:casemapping]
- 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])
- 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])
- 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)
- return false if name.nil_or_empty?
- channel_names.index(name.irc_downcase(casemap))
- end
- alias :has_chan? :has_channel?
-
- # Returns the channel with name _name_, if available
- #
- def get_channel(name)
- return nil if name.nil_or_empty?
- idx = has_channel?(name)
- channels[idx] if idx
- end
- alias :get_chan :get_channel
-
- # Create a new Channel object bound to the receiver and add it to the
- # list of <code>Channel</code>s on the receiver, unless the channel was
- # present already. In this case, the default action is to raise an
- # exception, unless _fails_ is set to false. An exception can also be
- # raised if _str_ is nil or empty, again only if _fails_ is set to true;
- # otherwise, the method just returns nil
- #
- def new_channel(name, topic=nil, users=[], fails=true)
- if name.nil_or_empty?
- raise "Tried to look for empty or nil channel name #{name.inspect}" if fails
- return nil
- end
- ex = get_chan(name)
- if ex
- raise "Channel #{name} already exists on server #{self}" if fails
- return ex
- else
-
- prefix = name[0].chr
-
- # Give a warning if the new Channel goes over some server limits.
- #
- # FIXME might need to raise an exception
- #
- warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix)
- warn "#{self} doesn't support channel names this long (#{name.length} > #{@supports[: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.include?(prefix)
- count = 0
- channel_names.each { |n|
- count += 1 if k.include?(n[0])
- }
- # raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k]
- warn "Already joined #{count}/#{@supports[:chanlimit][k]} channels with prefix #{k}, we may be going over server limits" if count >= @supports[:chanlimit][k]
- }
-
- # So far, everything is fine. Now create the actual Channel
- #
- chan = Channel.new(name, topic, users, :server => self)
-
- # We wade through +prefix+ and +chanmodes+ to create appropriate
- # lists and flags for this channel
-
- @supports[:prefix][:modes].each { |mode|
- chan.create_mode(mode, Channel::UserMode)
- } if @supports[:prefix][:modes]
-
- @supports[:chanmodes].each { |k, val|
- if val
- case k
- when :typea
- val.each { |mode|
- chan.create_mode(mode, Channel::ModeTypeA)
- }
- when :typeb
- val.each { |mode|
- chan.create_mode(mode, Channel::ModeTypeB)
- }
- when :typec
- val.each { |mode|
- chan.create_mode(mode, Channel::ModeTypeC)
- }
- when :typed
- val.each { |mode|
- chan.create_mode(mode, Channel::ModeTypeD)
- }
- end
- end
- }
-
- @channels << chan
- # debug "Created channel #{chan.inspect}"
- return chan
- end
- 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)
- idx = has_channel?(name)
- raise "Tried to remove unmanaged channel #{name}" unless idx
- @channels.delete_at(idx)
- end
-
- # Checks if the receiver already has a user with the given _nick_
- #
- def has_user?(nick)
- return false if nick.nil_or_empty?
- user_nicks.index(nick.irc_downcase(casemap))
- end
-
- # Returns the user with nick _nick_, if available
- #
- def get_user(nick)
- idx = has_user?(nick)
- @users[idx] if idx
- end
-
- # Create a new User object bound to the receiver and add it to the list
- # of <code>User</code>s on the receiver, unless the User was present
- # already. In this case, the default action is to raise an exception,
- # unless _fails_ is set to false. An exception can also be raised
- # if _str_ is nil or empty, again only if _fails_ is set to true;
- # otherwise, the method just returns nil
- #
- def new_user(str, fails=true)
- if str.nil_or_empty?
- raise "Tried to look for empty or nil user name #{str.inspect}" if fails
- return nil
- end
- tmp = str.to_irc_user(:server => self)
- old = get_user(tmp.nick)
- # debug "Tmp: #{tmp.inspect}"
- # debug "Old: #{old.inspect}"
- if old
- # debug "User already existed as #{old.inspect}"
- if tmp.known?
- if old.known?
- # debug "Both were known"
- # Do not raise an error: things like Freenode change the hostname after identification
- warning "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp
- raise "User #{tmp} already exists on server #{self}" if fails
- end
- if old.fullform.downcase != tmp.fullform.downcase
- old.replace(tmp)
- # debug "Known user now #{old.inspect}"
- end
- end
- return old
- else
- warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen]
- @users << tmp
- return @users.last
- end
- end
-
- # Returns the User with the given Netmask on the server,
- # creating it if necessary. This is a short form for
- # new_user(_str_, +false+)
- #
- def user(str)
- new_user(str, false)
- end
-
- # Deletes User _user_ from Channel _channel_
- #
- def delete_user_from_channel(user, channel)
- channel.delete_user(user)
- end
-
- # Remove User _someuser_ from the list of <code>User</code>s.
- # _someuser_ must be specified with the full Netmask.
- #
- def delete_user(someuser)
- idx = has_user?(someuser)
- raise "Tried to remove unmanaged user #{user}" unless idx
- have = self.user(someuser)
- @channels.each { |ch|
- delete_user_from_channel(have, ch)
- }
- @users.delete_at(idx)
- end
-
- # Create a new Netmask object with the appropriate casemap
- #
- def new_netmask(str)
- str.to_irc_netmask(:server => self)
- end
-
- # Finds all <code>User</code>s on server whose Netmask matches _mask_
- #
- def find_users(mask)
- nm = new_netmask(mask)
- @users.inject(UserList.new) {
- |list, user|
- if user.user == "*" or user.host == "*"
- list << user if user.nick.irc_downcase(casemap) =~ nm.nick.irc_downcase(casemap).to_irc_regexp
- else
- list << user if user.matches?(nm)
- end
- list
- }
- end
-
- end
-
-end
-
+#-- vim:sw=2:et
+# General TODO list
+# * 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 we can remove him from the Server @users list
+# * Maybe ChannelList and UserList should be HashesOf instead of ArrayOf?
+# See items marked as TODO Ho.
+# The framework to do this is now in place, thanks to the new [] method
+# for NetmaskList, which allows retrieval by Netmask or String
+#++
+# :title: IRC module
+#
+# Basic IRC stuff
+#
+# This module defines the fundamental building blocks for IRC
+#
+# Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)
+# Copyright:: Copyright (c) 2006 Giuseppe Bilotta
+# License:: GPLv2
+
+require 'singleton'
+
+class Object
+
+ # We extend the Object class with a method that
+ # checks if the receiver is nil or empty
+ def nil_or_empty?
+ return true unless self
+ return true if self.respond_to? :empty? and self.empty?
+ return false
+ end
+
+ # We alias the to_s method to __to_s__ to make
+ # it accessible in all classes
+ alias :__to_s__ :to_s
+end
+
+# The Irc module is used to keep all IRC-related classes
+# in the same namespace
+#
+module Irc
+
+
+ # Due to its Scandinavian origins, IRC has strange case mappings, which
+ # consider the characters <tt>{}|^</tt> as the uppercase
+ # equivalents of # <tt>[]\~</tt>.
+ #
+ # This is however not the same on all IRC servers: some use standard ASCII
+ # casemapping, other do not consider <tt>^</tt> as the uppercase of
+ # <tt>~</tt>
+ #
+ class Casemap
+ @@casemaps = {}
+
+ # Create a new casemap with name _name_, uppercase characters _upper_ and
+ # lowercase characters _lower_
+ #
+ def initialize(name, upper, lower)
+ @key = name.to_sym
+ raise "Casemap #{name.inspect} already exists!" if @@casemaps.has_key?(@key)
+ @@casemaps[@key] = {
+ :upper => upper,
+ :lower => lower,
+ :casemap => self
+ }
+ end
+
+ # Returns the Casemap with the given name
+ #
+ def Casemap.get(name)
+ @@casemaps[name.to_sym][:casemap]
+ end
+
+ # Retrieve the 'uppercase characters' of this Casemap
+ #
+ def upper
+ @@casemaps[@key][:upper]
+ end
+
+ # Retrieve the 'lowercase characters' of this Casemap
+ #
+ def lower
+ @@casemaps[@key][:lower]
+ end
+
+ # Return a Casemap based on the receiver
+ #
+ def to_irc_casemap
+ self
+ end
+
+ # A Casemap is represented by its lower/upper mappings
+ #
+ def inspect
+ self.__to_s__[0..-2] + " #{upper.inspect} ~(#{self})~ #{lower.inspect}>"
+ end
+
+ # As a String we return our name
+ #
+ def to_s
+ @key.to_s
+ end
+
+ # Two Casemaps are equal if they have the same upper and lower ranges
+ #
+ def ==(arg)
+ other = arg.to_irc_casemap
+ return self.upper == other.upper && self.lower == other.lower
+ end
+
+ # Give a warning if _arg_ and self are not the same Casemap
+ #
+ def must_be(arg)
+ other = arg.to_irc_casemap
+ if self == other
+ return true
+ else
+ warn "Casemap mismatch (#{self.inspect} != #{other.inspect})"
+ return false
+ end
+ end
+
+ end
+
+ # The rfc1459 casemap
+ #
+ class RfcCasemap < Casemap
+ include Singleton
+
+ def initialize
+ super('rfc1459', "\x41-\x5e", "\x61-\x7e")
+ end
+
+ end
+ RfcCasemap.instance
+
+ # The strict-rfc1459 Casemap
+ #
+ class StrictRfcCasemap < Casemap
+ include Singleton
+
+ def initialize
+ super('strict-rfc1459', "\x41-\x5d", "\x61-\x7d")
+ end
+
+ end
+ StrictRfcCasemap.instance
+
+ # The ascii Casemap
+ #
+ class AsciiCasemap < Casemap
+ include Singleton
+
+ def initialize
+ super('ascii', "\x41-\x5a", "\x61-\x7a")
+ end
+
+ end
+ AsciiCasemap.instance
+
+
+ # This module is included by all classes that are either bound to a server
+ # or should have a casemap.
+ #
+ module ServerOrCasemap
+
+ attr_reader :server
+
+ # This method initializes the instance variables @server and @casemap
+ # according to the values of the hash keys :server and :casemap in _opts_
+ #
+ def init_server_or_casemap(opts={})
+ @server = opts.fetch(:server, nil)
+ raise TypeError, "#{@server} is not a valid Irc::Server" if @server and not @server.kind_of?(Server)
+
+ @casemap = opts.fetch(:casemap, nil)
+ if @server
+ if @casemap
+ @server.casemap.must_be(@casemap)
+ @casemap = nil
+ end
+ else
+ @casemap = (@casemap || 'rfc1459').to_irc_casemap
+ end
+ end
+
+ # This is an auxiliary method: it returns true if the receiver fits the
+ # server and casemap specified in _opts_, false otherwise.
+ #
+ def fits_with_server_and_casemap?(opts={})
+ srv = opts.fetch(:server, nil)
+ cmap = opts.fetch(:casemap, nil)
+ cmap = cmap.to_irc_casemap unless cmap.nil?
+
+ if srv.nil?
+ return true if cmap.nil? or cmap == casemap
+ else
+ return true if srv == @server and (cmap.nil? or cmap == casemap)
+ end
+ return false
+ end
+
+ # Returns the casemap of the receiver, by looking at the bound
+ # @server (if possible) or at the @casemap otherwise
+ #
+ def casemap
+ return @server.casemap if defined?(@server) and @server
+ return @casemap
+ end
+
+ # Returns a hash with the current @server and @casemap as values of
+ # :server and :casemap
+ #
+ def server_and_casemap
+ h = {}
+ h[:server] = @server if defined?(@server) and @server
+ h[:casemap] = @casemap if defined?(@casemap) and @casemap
+ return h
+ end
+
+ # We allow up/downcasing with a different casemap
+ #
+ def irc_downcase(cmap=casemap)
+ self.to_s.irc_downcase(cmap)
+ end
+
+ # Up/downcasing something that includes this module returns its
+ # Up/downcased to_s form
+ #
+ def downcase
+ self.irc_downcase
+ end
+
+ # We allow up/downcasing with a different casemap
+ #
+ def irc_upcase(cmap=casemap)
+ self.to_s.irc_upcase(cmap)
+ end
+
+ # Up/downcasing something that includes this module returns its
+ # Up/downcased to_s form
+ #
+ def upcase
+ self.irc_upcase
+ end
+
+ end
+
+end
+
+
+# We start by extending the String class
+# with some IRC-specific methods
+#
+class String
+
+ # This method returns the Irc::Casemap whose name is the receiver
+ #
+ def to_irc_casemap
+ Irc::Casemap.get(self) rescue raise TypeError, "Unkown Irc::Casemap #{self.inspect}"
+ end
+
+ # This method returns a string which is the downcased version of the
+ # receiver, according to the given _casemap_
+ #
+ #
+ def irc_downcase(casemap='rfc1459')
+ cmap = casemap.to_irc_casemap
+ self.tr(cmap.upper, cmap.lower)
+ end
+
+ # This is the same as the above, except that the string is altered in place
+ #
+ # See also the discussion about irc_downcase
+ #
+ def irc_downcase!(casemap='rfc1459')
+ cmap = casemap.to_irc_casemap
+ self.tr!(cmap.upper, cmap.lower)
+ end
+
+ # Upcasing functions are provided too
+ #
+ # See also the discussion about irc_downcase
+ #
+ def irc_upcase(casemap='rfc1459')
+ cmap = casemap.to_irc_casemap
+ self.tr(cmap.lower, cmap.upper)
+ end
+
+ # In-place upcasing
+ #
+ # See also the discussion about irc_downcase
+ #
+ def irc_upcase!(casemap='rfc1459')
+ cmap = casemap.to_irc_casemap
+ self.tr!(cmap.lower, cmap.upper)
+ end
+
+ # This method checks if the receiver contains IRC glob characters
+ #
+ # IRC has a very primitive concept of globs: a <tt>*</tt> stands for "any
+ # number of arbitrary characters", a <tt>?</tt> stands for "one and exactly
+ # one arbitrary character". These characters can be escaped by prefixing them
+ # with a slash (<tt>\\</tt>).
+ #
+ # A known limitation of this glob syntax is that there is no way to escape
+ # the escape character itself, so it's not possible to build a glob pattern
+ # where the escape character precedes a glob.
+ #
+ def has_irc_glob?
+ self =~ /^[*?]|[^\\][*?]/
+ end
+
+ # This method is used to convert the receiver into a Regular Expression
+ # that matches according to the IRC glob syntax
+ #
+ def to_irc_regexp
+ regmask = Regexp.escape(self)
+ regmask.gsub!(/(\\\\)?\\[*?]/) { |m|
+ case m
+ when /\\(\\[*?])/
+ $1
+ when /\\\*/
+ '.*'
+ when /\\\?/
+ '.'
+ else
+ raise "Unexpected match #{m} when converting #{self}"
+ end
+ }
+ Regexp.new("^#{regmask}$")
+ end
+
+end
+
+
+# ArrayOf is a subclass of Array whose elements are supposed to be all
+# of the same class. This is not intended to be used directly, but rather
+# to be subclassed as needed (see for example Irc::UserList and Irc::NetmaskList)
+#
+# Presently, only very few selected methods from Array are overloaded to check
+# if the new elements are the correct class. An orthodox? method is provided
+# to check the entire ArrayOf against the appropriate class.
+#
+class ArrayOf < Array
+
+ attr_reader :element_class
+
+ # Create a new ArrayOf whose elements are supposed to be all of type _kl_,
+ # optionally filling it with the elements from the Array argument.
+ #
+ def initialize(kl, ar=[])
+ raise TypeError, "#{kl.inspect} must be a class name" unless kl.kind_of?(Class)
+ super()
+ @element_class = kl
+ case ar
+ when Array
+ insert(0, *ar)
+ else
+ raise TypeError, "#{self.class} can only be initialized from an Array"
+ end
+ end
+
+ def inspect
+ self.__to_s__[0..-2].sub(/:[^:]+$/,"[#{@element_class}]\\0") + " #{super}>"
+ end
+
+ # Private method to check the validity of the elements passed to it
+ # and optionally raise an error
+ #
+ # TODO should it accept nils as valid?
+ #
+ def internal_will_accept?(raising, *els)
+ els.each { |el|
+ unless el.kind_of?(@element_class)
+ raise TypeError, "#{el.inspect} is not of class #{@element_class}" if raising
+ return false
+ end
+ }
+ return true
+ end
+ private :internal_will_accept?
+
+ # This method checks if the passed arguments are acceptable for our ArrayOf
+ #
+ def will_accept?(*els)
+ internal_will_accept?(false, *els)
+ end
+
+ # This method checks that all elements are of the appropriate class
+ #
+ def valid?
+ will_accept?(*self)
+ end
+
+ # This method is similar to the above, except that it raises an exception
+ # if the receiver is not valid
+ #
+ def validate
+ raise TypeError unless valid?
+ end
+
+ # Overloaded from Array#<<, checks for appropriate class of argument
+ #
+ def <<(el)
+ super(el) if internal_will_accept?(true, el)
+ end
+
+ # Overloaded from Array#&, checks for appropriate class of argument elements
+ #
+ def &(ar)
+ r = super(ar)
+ ArrayOf.new(@element_class, r) if internal_will_accept?(true, *r)
+ end
+
+ # Overloaded from Array#+, checks for appropriate class of argument elements
+ #
+ def +(ar)
+ ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)
+ end
+
+ # Overloaded from Array#-, so that an ArrayOf is returned. There is no need
+ # to check the validity of the elements in the argument
+ #
+ def -(ar)
+ ArrayOf.new(@element_class, super(ar)) # if internal_will_accept?(true, *ar)
+ end
+
+ # Overloaded from Array#|, checks for appropriate class of argument elements
+ #
+ def |(ar)
+ ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)
+ end
+
+ # Overloaded from Array#concat, checks for appropriate class of argument
+ # elements
+ #
+ def concat(ar)
+ super(ar) if internal_will_accept?(true, *ar)
+ end
+
+ # Overloaded from Array#insert, checks for appropriate class of argument
+ # elements
+ #
+ def insert(idx, *ar)
+ super(idx, *ar) if internal_will_accept?(true, *ar)
+ end
+
+ # Overloaded from Array#replace, checks for appropriate class of argument
+ # elements
+ #
+ def replace(ar)
+ super(ar) if (ar.kind_of?(ArrayOf) && ar.element_class <= @element_class) or internal_will_accept?(true, *ar)
+ end
+
+ # Overloaded from Array#push, checks for appropriate class of argument
+ # elements
+ #
+ def push(*ar)
+ super(*ar) if internal_will_accept?(true, *ar)
+ end
+
+ # Overloaded from Array#unshift, checks for appropriate class of argument(s)
+ #
+ def unshift(*els)
+ els.each { |el|
+ super(el) if internal_will_accept?(true, *els)
+ }
+ end
+
+ # We introduce the 'downcase' method, which maps downcase() to all the Array
+ # elements, properly failing when the elements don't have a downcase method
+ #
+ def downcase
+ self.map { |el| el.downcase }
+ end
+
+ # Modifying methods which we don't handle yet are made private
+ #
+ private :[]=, :collect!, :map!, :fill, :flatten!
+
+end
+
+
+# We extend the Regexp class with an Irc module which will contain some
+# Irc-specific regexps
+#
+class Regexp
+
+ # We start with some general-purpose ones which will be used in the
+ # Irc module too, but are useful regardless
+ DIGITS = /\d+/
+ HEX_DIGIT = /[0-9A-Fa-f]/
+ HEX_DIGITS = /#{HEX_DIGIT}+/
+ HEX_OCTET = /#{HEX_DIGIT}#{HEX_DIGIT}?/
+ DEC_OCTET = /[01]?\d?\d|2[0-4]\d|25[0-5]/
+ DEC_IP_ADDR = /#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}/
+ HEX_IP_ADDR = /#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}/
+ IP_ADDR = /#{DEC_IP_ADDR}|#{HEX_IP_ADDR}/
+
+ # IPv6, from Resolv::IPv6, without the \A..\z anchors
+ HEX_16BIT = /#{HEX_DIGIT}{1,4}/
+ IP6_8Hex = /(?:#{HEX_16BIT}:){7}#{HEX_16BIT}/
+ IP6_CompressedHex = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)/
+ IP6_6Hex4Dec = /((?:#{HEX_16BIT}:){6,6})#{DEC_IP_ADDR}/
+ IP6_CompressedHex4Dec = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}:)*)#{DEC_IP_ADDR}/
+ IP6_ADDR = /(?:#{IP6_8Hex})|(?:#{IP6_CompressedHex})|(?:#{IP6_6Hex4Dec})|(?:#{IP6_CompressedHex4Dec})/
+
+ # We start with some IRC related regular expressions, used to match
+ # Irc::User nicks and users and Irc::Channel names
+ #
+ # For each of them we define two versions of the regular expression:
+ # * a generic one, which should match for any server but may turn out to
+ # match more than a specific server would accept
+ # * an RFC-compliant matcher
+ #
+ module Irc
+
+ # Channel-name-matching regexps
+ CHAN_FIRST = /[#&+]/
+ CHAN_SAFE = /![A-Z0-9]{5}/
+ CHAN_ANY = /[^\x00\x07\x0A\x0D ,:]/
+ GEN_CHAN = /(?:#{CHAN_FIRST}|#{CHAN_SAFE})#{CHAN_ANY}+/
+ RFC_CHAN = /#{CHAN_FIRST}#{CHAN_ANY}{1,49}|#{CHAN_SAFE}#{CHAN_ANY}{1,44}/
+
+ # Nick-matching regexps
+ SPECIAL_CHAR = /[\x5b-\x60\x7b-\x7d]/
+ NICK_FIRST = /#{SPECIAL_CHAR}|[[:alpha:]]/
+ NICK_ANY = /#{SPECIAL_CHAR}|[[:alnum:]]|-/
+ GEN_NICK = /#{NICK_FIRST}#{NICK_ANY}+/
+ RFC_NICK = /#{NICK_FIRST}#{NICK_ANY}{0,8}/
+
+ USER_CHAR = /[^\x00\x0a\x0d @]/
+ GEN_USER = /#{USER_CHAR}+/
+
+ # Host-matching regexps
+ HOSTNAME_COMPONENT = /[[:alnum:]](?:[[:alnum:]]|-)*[[:alnum:]]*/
+ HOSTNAME = /#{HOSTNAME_COMPONENT}(?:\.#{HOSTNAME_COMPONENT})*/
+ HOSTADDR = /#{IP_ADDR}|#{IP6_ADDR}/
+
+ GEN_HOST = /#{HOSTNAME}|#{HOSTADDR}/
+
+ # # FreeNode network replaces the host of affiliated users with
+ # # 'virtual hosts'
+ # # FIXME we need the true syntax to match it properly ...
+ # PDPC_HOST_PART = /[0-9A-Za-z.-]+/
+ # PDPC_HOST = /#{PDPC_HOST_PART}(?:\/#{PDPC_HOST_PART})+/
+
+ # # NOTE: the final optional and non-greedy dot is needed because some
+ # # servers (e.g. FreeNode) send the hostname of the services as "services."
+ # # which is not RFC compliant, but sadly done.
+ # GEN_HOST_EXT = /#{PDPC_HOST}|#{GEN_HOST}\.??/
+
+ # Sadly, different networks have different, RFC-breaking ways of cloaking
+ # the actualy host address: see above for an example to handle FreeNode.
+ # Another example would be Azzurra, wich also inserts a "=" in the
+ # cloacked host. So let's just not care about this and go with the simplest
+ # thing:
+ GEN_HOST_EXT = /\S+/
+
+ # User-matching Regexp
+ GEN_USER_ID = /(#{GEN_NICK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/
+
+ # Things such has the BIP proxy send invalid nicks in a complete netmask,
+ # so we want to match this, rather: this matches either a compliant nick
+ # or a a string with a very generic nick, a very generic hostname after an
+ # @ sign, and an optional user after a !
+ BANG_AT = /#{GEN_NICK}|\S+?(?:!\S+?)?@\S+?/
+
+ # # For Netmask, we want to allow wildcards * and ? in the nick
+ # # (they are already allowed in the user and host part
+ # GEN_NICK_MASK = /(?:#{NICK_FIRST}|[?*])?(?:#{NICK_ANY}|[?*])+/
+
+ # # Netmask-matching Regexp
+ # GEN_MASK = /(#{GEN_NICK_MASK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/
+
+ end
+
+end
+
+
+module Irc
+
+
+ # A Netmask identifies each user by collecting its nick, username and
+ # hostname in the form <tt>nick!user@host</tt>
+ #
+ # Netmasks can also contain glob patterns in any of their components; in
+ # this form they are used to refer to more than a user or to a user
+ # appearing under different forms.
+ #
+ # Example:
+ # * <tt>*!*@*</tt> refers to everybody
+ # * <tt>*!someuser@somehost</tt> refers to user +someuser+ on host +somehost+
+ # regardless of the nick used.
+ #
+ class Netmask
+
+ # Netmasks have an associated casemap unless they are bound to a server
+ #
+ include ServerOrCasemap
+
+ attr_reader :nick, :user, :host
+ alias :ident :user
+
+ # Create a new Netmask from string _str_, which must be in the form
+ # _nick_!_user_@_host_
+ #
+ # It is possible to specify a server or a casemap in the optional Hash:
+ # these are used to associate the Netmask with the given server and to set
+ # its casemap: if a server is specified and a casemap is not, the server's
+ # casemap is used. If both a server and a casemap are specified, the
+ # casemap must match the server's casemap or an exception will be raised.
+ #
+ # Empty +nick+, +user+ or +host+ are converted to the generic glob pattern
+ #
+ def initialize(str="", opts={})
+ # First of all, check for server/casemap option
+ #
+ init_server_or_casemap(opts)
+
+ # Now we can see if the given string _str_ is an actual Netmask
+ if str.respond_to?(:to_str)
+ case str.to_str
+ # We match a pretty generic string, to work around non-compliant
+ # servers
+ when /^(?:(\S+?)(?:(?:!(\S+?))?@(\S+))?)?$/
+ # We do assignment using our internal methods
+ self.nick = $1
+ self.user = $2
+ self.host = $3
+ else
+ raise ArgumentError, "#{str.to_str.inspect} does not represent a valid #{self.class}"
+ end
+ else
+ raise TypeError, "#{str} cannot be converted to a #{self.class}"
+ end
+ end
+
+ # A Netmask is easily converted to a String for the usual representation.
+ # We skip the user or host parts if they are "*", unless we've been asked
+ # for the full form
+ #
+ def to_s
+ ret = nick.dup
+ ret << "!" << user unless user == "*"
+ ret << "@" << host unless host == "*"
+ return ret
+ end
+
+ def fullform
+ "#{nick}!#{user}@#{host}"
+ end
+
+ alias :to_str :fullform
+
+ # This method downcases the fullform of the netmask. While this may not be
+ # significantly different from the #downcase() method provided by the
+ # ServerOrCasemap mixin, it's significantly different for Netmask
+ # subclasses such as User whose simple downcasing uses the nick only.
+ #
+ def full_irc_downcase(cmap=casemap)
+ self.fullform.irc_downcase(cmap)
+ end
+
+ # full_downcase() will return the fullform downcased according to the
+ # User's own casemap
+ #
+ def full_downcase
+ self.full_irc_downcase
+ end
+
+ # This method returns a new Netmask which is the fully downcased version
+ # of the receiver
+ def downcased
+ return self.full_downcase.to_irc_netmask(server_and_casemap)
+ end
+
+ # Converts the receiver into a Netmask with the given (optional)
+ # server/casemap association. We return self unless a conversion
+ # is needed (different casemap/server)
+ #
+ # Subclasses of Netmask will return a new Netmask, using full_downcase
+ #
+ def to_irc_netmask(opts={})
+ if self.class == Netmask
+ return self if fits_with_server_and_casemap?(opts)
+ end
+ return self.full_downcase.to_irc_netmask(server_and_casemap.merge(opts))
+ end
+
+ # Converts the receiver into a User with the given (optional)
+ # server/casemap association. We return self unless a conversion
+ # is needed (different casemap/server)
+ #
+ def to_irc_user(opts={})
+ self.fullform.to_irc_user(server_and_casemap.merge(opts))
+ end
+
+ # Inspection of a Netmask reveals the server it's bound to (if there is
+ # one), its casemap and the nick, user and host part
+ #
+ def inspect
+ str = self.__to_s__[0..-2]
+ str << " @server=#{@server}" if defined?(@server) and @server
+ str << " @nick=#{@nick.inspect} @user=#{@user.inspect}"
+ str << " @host=#{@host.inspect} casemap=#{casemap.inspect}"
+ str << ">"
+ end
+
+ # Equality: two Netmasks are equal if they downcase to the same thing
+ #
+ # TODO we may want it to try other.to_irc_netmask
+ #
+ def ==(other)
+ return false unless other.kind_of?(self.class)
+ self.downcase == other.downcase
+ end
+
+ # This method changes the nick of the Netmask, defaulting to the generic
+ # glob pattern if the result is the null string.
+ #
+ def nick=(newnick)
+ @nick = newnick.to_s
+ @nick = "*" if @nick.empty?
+ end
+
+ # This method changes the user of the Netmask, defaulting to the generic
+ # glob pattern if the result is the null string.
+ #
+ def user=(newuser)
+ @user = newuser.to_s
+ @user = "*" if @user.empty?
+ end
+ alias :ident= :user=
+
+ # This method changes the hostname of the Netmask, defaulting to the generic
+ # glob pattern if the result is the null string.
+ #
+ def host=(newhost)
+ @host = newhost.to_s
+ @host = "*" if @host.empty?
+ end
+
+ # We can replace everything at once with data from another Netmask
+ #
+ def replace(other)
+ case other
+ when Netmask
+ nick = other.nick
+ user = other.user
+ host = other.host
+ @server = other.server
+ @casemap = other.casemap unless @server
+ else
+ replace(other.to_irc_netmask(server_and_casemap))
+ end
+ end
+
+ # This method checks if a Netmask is definite or not, by seeing if
+ # any of its components are defined by globs
+ #
+ def has_irc_glob?
+ return @nick.has_irc_glob? || @user.has_irc_glob? || @host.has_irc_glob?
+ end
+
+ def generalize
+ u = user.dup
+ unless u.has_irc_glob?
+ u.sub!(/^[in]=/, '=') or u.sub!(/^\W(\w+)/, '\1')
+ u = '*' + u
+ end
+
+ h = host.dup
+ unless h.has_irc_glob?
+ if h.include? '/'
+ h.sub!(/x-\w+$/, 'x-*')
+ else
+ h.match(/^[^\.]+\.[^\.]+$/) or
+ h.sub!(/azzurra[=-][0-9a-f]+/i, '*') or # hello, azzurra, you suck!
+ h.sub!(/^(\d+\.\d+\.\d+\.)\d+$/, '\1*') or
+ h.sub!(/^[^\.]+\./, '*.')
+ end
+ end
+ return Netmask.new("*!#{u}@#{h}", server_and_casemap)
+ end
+
+ # This method is used to match the current Netmask against another one
+ #
+ # The method returns true if each component of the receiver matches the
+ # corresponding component of the argument. By _matching_ here we mean
+ # that any netmask described by the receiver is also described by the
+ # argument.
+ #
+ # In this sense, matching is rather simple to define in the case when the
+ # receiver has no globs: it is just necessary to check if the argument
+ # describes the receiver, which can be done by matching it against the
+ # argument converted into an IRC Regexp (see String#to_irc_regexp).
+ #
+ # The situation is also easy when the receiver has globs and the argument
+ # doesn't, since in this case the result is false.
+ #
+ # The more complex case in which both the receiver and the argument have
+ # globs is not handled yet.
+ #
+ def matches?(arg)
+ cmp = arg.to_irc_netmask(:casemap => casemap)
+ debug "Matching #{self.fullform} against #{arg.inspect} (#{cmp.fullform})"
+ [:nick, :user, :host].each { |component|
+ us = self.send(component).irc_downcase(casemap)
+ them = cmp.send(component).irc_downcase(casemap)
+ if us.has_irc_glob? && them.has_irc_glob?
+ next if us == them
+ warn NotImplementedError
+ return false
+ end
+ return false if us.has_irc_glob? && !them.has_irc_glob?
+ return false unless us =~ them.to_irc_regexp
+ }
+ return true
+ end
+
+ # Case equality. Checks if arg matches self
+ #
+ def ===(arg)
+ arg.to_irc_netmask(:casemap => casemap).matches?(self)
+ end
+
+ # Sorting is done via the fullform
+ #
+ def <=>(arg)
+ case arg
+ when Netmask
+ self.fullform.irc_downcase(casemap) <=> arg.fullform.irc_downcase(casemap)
+ else
+ self.downcase <=> arg.downcase
+ end
+ end
+
+ end
+
+
+ # A NetmaskList is an ArrayOf <code>Netmask</code>s
+ #
+ class NetmaskList < ArrayOf
+
+ # Create a new NetmaskList, optionally filling it with the elements from
+ # the Array argument fed to it.
+ #
+ def initialize(ar=[])
+ super(Netmask, ar)
+ end
+
+ # We enhance the [] method by allowing it to pick an element that matches
+ # a given Netmask, a String or a Regexp
+ # TODO take into consideration the opportunity to use select() instead of
+ # find(), and/or a way to let the user choose which one to take (second
+ # argument?)
+ #
+ def [](*args)
+ if args.length == 1
+ case args[0]
+ when Netmask
+ self.find { |mask|
+ mask.matches?(args[0])
+ }
+ when String
+ self.find { |mask|
+ mask.matches?(args[0].to_irc_netmask(:casemap => mask.casemap))
+ }
+ when Regexp
+ self.find { |mask|
+ mask.fullform =~ args[0]
+ }
+ else
+ super(*args)
+ end
+ else
+ super(*args)
+ end
+ end
+
+ end
+
+end
+
+
+class String
+
+ # We keep extending String, this time adding a method that converts a
+ # String into an Irc::Netmask object
+ #
+ def to_irc_netmask(opts={})
+ Irc::Netmask.new(self, opts)
+ end
+
+end
+
+
+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.
+ #
+ # Ideally, the user and host information of an IRC User should never
+ # change, and it shouldn't contain glob patterns. However, IRC is somewhat
+ # idiosincratic and it may be possible to know the nick of a User much before
+ # its user and host are known. Moreover, some networks (namely Freenode) may
+ # change the hostname of a User when (s)he identifies with Nickserv.
+ #
+ # As a consequence, we must allow changes to a User host and user attributes.
+ # We impose a restriction, though: they may not contain glob patterns, except
+ # for the special case of an unknown user/host which is represented by a *.
+ #
+ # It is possible to create a totally unknown User (e.g. for initializations)
+ # by setting the nick to * too.
+ #
+ # TODO list:
+ # * see if it's worth to add the other USER data
+ # * see if it's worth to add NICKSERV status
+ #
+ class User < Netmask
+ alias :to_s :nick
+
+ attr_accessor :real_name
+
+ # 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="", opts={})
+ super
+ 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
+ @real_name = String.new
+ end
+
+ # The nick of a User may be changed freely, but it must not contain glob patterns.
+ #
+ def nick=(newnick)
+ raise "Can't change the nick to #{newnick}" if defined?(@nick) and newnick.has_irc_glob?
+ super
+ end
+
+ # We have to allow changing the user of an Irc User due to some networks
+ # (e.g. Freenode) changing hostmasks on the fly. We still check if the new
+ # user data has glob patterns though.
+ #
+ def user=(newuser)
+ raise "Can't change the username to #{newuser}" if defined?(@user) and newuser.has_irc_glob?
+ super
+ end
+
+ # We have to allow changing the host of an Irc User due to some networks
+ # (e.g. Freenode) changing hostmasks on the fly. We still check if the new
+ # host data has glob patterns though.
+ #
+ def host=(newhost)
+ raise "Can't change the hostname to #{newhost}" if defined?(@host) and newhost.has_irc_glob?
+ super
+ end
+
+ # Checks if a User is well-known or not by looking at the hostname and user
+ #
+ def known?
+ return nick != "*" && 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
+
+ # Since to_irc_user runs the same checks on server and channel as
+ # to_irc_netmask, we just try that and return self if it works.
+ #
+ # Subclasses of User will return self if possible.
+ #
+ def to_irc_user(opts={})
+ return self if fits_with_server_and_casemap?(opts)
+ return self.full_downcase.to_irc_user(opts)
+ end
+
+ # We can replace everything at once with data from another User
+ #
+ def replace(other)
+ case other
+ when User
+ self.nick = other.nick
+ self.user = other.user
+ self.host = other.host
+ @server = other.server
+ @casemap = other.casemap unless @server
+ @away = other.away?
+ else
+ self.replace(other.to_irc_user(server_and_casemap))
+ end
+ end
+
+ def modes_on(channel)
+ case channel
+ when Channel
+ channel.modes_of(self)
+ else
+ return @server.channel(channel).modes_of(self) if @server
+ raise "Can't resolve channel #{channel}"
+ end
+ end
+
+ def is_op?(channel)
+ case channel
+ when Channel
+ channel.has_op?(self)
+ else
+ return @server.channel(channel).has_op?(self) if @server
+ raise "Can't resolve channel #{channel}"
+ end
+ end
+
+ def is_voice?(channel)
+ case channel
+ when Channel
+ channel.has_voice?(self)
+ else
+ return @server.channel(channel).has_voice?(self) if @server
+ raise "Can't resolve channel #{channel}"
+ end
+ end
+ end
+
+
+ # A UserList is an ArrayOf <code>User</code>s
+ # We derive it from NetmaskList, which allows us to inherit any special
+ # NetmaskList method
+ #
+ class UserList < NetmaskList
+
+ # Create a new UserList, optionally filling it with the elements from
+ # the Array argument fed to it.
+ #
+ def initialize(ar=[])
+ super(ar)
+ @element_class = User
+ end
+
+ # Convenience method: convert the UserList to a list of nicks. The indices
+ # are preserved
+ #
+ def nicks
+ self.map { |user| user.nick }
+ end
+
+ end
+
+end
+
+class String
+
+ # We keep extending String, this time adding a method that converts a
+ # String into an Irc::User object
+ #
+ def to_irc_user(opts={})
+ Irc::User.new(self, opts)
+ end
+
+end
+
+module Irc
+
+ # An IRC Channel is identified by its name, and it has a set of properties:
+ # * a Channel::Topic
+ # * a UserList
+ # * a set of Channel::Modes
+ #
+ # The Channel::Topic and Channel::Mode classes are defined within the
+ # Channel namespace because they only make sense there
+ #
+ class Channel
+
+
+ # Mode on a Channel
+ #
+ class Mode
+ attr_reader :channel
+ def initialize(ch)
+ @channel = ch
+ end
+
+ end
+
+
+ # Channel modes of type A manipulate lists
+ #
+ # Example: b (banlist)
+ #
+ class ModeTypeA < Mode
+ attr_reader :list
+ def initialize(ch)
+ super
+ @list = NetmaskList.new
+ end
+
+ def set(val)
+ nm = @channel.server.new_netmask(val)
+ @list << nm unless @list.include?(nm)
+ end
+
+ def reset(val)
+ nm = @channel.server.new_netmask(val)
+ @list.delete(nm)
+ end
+
+ end
+
+
+ # Channel modes of type B need an argument
+ #
+ # Example: k (key)
+ #
+ class ModeTypeB < Mode
+ def initialize(ch)
+ super
+ @arg = nil
+ end
+
+ def status
+ @arg
+ end
+ alias :value :status
+
+ def set(val)
+ @arg = val
+ end
+
+ 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 UserMode < ModeTypeB
+ attr_reader :list
+ alias :users :list
+ def initialize(ch)
+ super
+ @list = UserList.new
+ end
+
+ def set(val)
+ u = @channel.server.user(val)
+ @list << u unless @list.include?(u)
+ end
+
+ def reset(val)
+ u = @channel.server.user(val)
+ @list.delete(u)
+ end
+
+ end
+
+
+ # Channel modes of type C need an argument when set,
+ # but not when they get reset
+ #
+ # Example: l (limit)
+ #
+ class ModeTypeC < Mode
+ def initialize(ch)
+ super
+ @arg = nil
+ end
+
+ def status
+ @arg
+ end
+ alias :value :status
+
+ def set(val)
+ @arg = val
+ end
+
+ def reset
+ @arg = nil
+ end
+
+ end
+
+
+ # Channel modes of type D are basically booleans
+ #
+ # Example: m (moderate)
+ #
+ class ModeTypeD < Mode
+ def initialize(ch)
+ super
+ @set = false
+ end
+
+ def set?
+ return @set
+ end
+
+ def set
+ @set = true
+ end
+
+ def reset
+ @set = false
+ end
+
+ end
+
+
+ # A Topic represents the topic of a channel. It consists of
+ # the topic itself, who set it and when
+ #
+ class Topic
+ attr_accessor :text, :set_by, :set_on
+ alias :to_s :text
+
+ # Create a new Topic setting the text, the creator and
+ # the creation time
+ #
+ def initialize(text="", set_by="", set_on=Time.new)
+ @text = text
+ @set_by = set_by.to_irc_netmask
+ @set_on = set_on
+ end
+
+ # Replace a Topic with another one
+ #
+ def replace(topic)
+ raise TypeError, "#{topic.inspect} is not of class #{self.class}" unless topic.kind_of?(self.class)
+ @text = topic.text.dup
+ @set_by = topic.set_by.dup
+ @set_on = topic.set_on.dup
+ end
+
+ # Returns self
+ #
+ def to_irc_channel_topic
+ self
+ end
+
+ end
+
+ end
+
+end
+
+
+class String
+
+ # Returns an Irc::Channel::Topic with self as text
+ #
+ def to_irc_channel_topic
+ Irc::Channel::Topic.new(self)
+ end
+
+end
+
+
+module Irc
+
+
+ # Here we start with the actual Channel class
+ #
+ class Channel
+
+ include ServerOrCasemap
+ attr_reader :name, :topic, :mode, :users
+ alias :to_s :name
+
+ def inspect
+ str = self.__to_s__[0..-2]
+ str << " on server #{server}" if server
+ str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"
+ str << " @users=[#{user_nicks.sort.join(', ')}]"
+ str << ">"
+ end
+
+ # Returns self
+ #
+ def to_irc_channel
+ self
+ end
+
+ # TODO Ho
+ def user_nicks
+ @users.map { |u| u.downcase }
+ end
+
+ # Checks if the receiver already has a user with the given _nick_
+ #
+ def has_user?(nick)
+ @users.index(nick.to_irc_user(server_and_casemap))
+ end
+
+ # Returns the user with nick _nick_, if available
+ #
+ def get_user(nick)
+ idx = has_user?(nick)
+ @users[idx] if idx
+ end
+
+ # Adds a user to the channel
+ #
+ def add_user(user, opts={})
+ silent = opts.fetch(:silent, false)
+ if has_user?(user)
+ warn "Trying to add user #{user} to channel #{self} again" unless silent
+ else
+ @users << user.to_irc_user(server_and_casemap)
+ end
+ end
+
+ # Creates a new channel with the given name, optionally setting the topic
+ # and an initial users list.
+ #
+ # No additional info is created here, because the channel flags and userlists
+ # allowed depend on the server.
+ #
+ def initialize(name, topic=nil, users=[], opts={})
+ raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?
+ warn "Unknown channel prefix #{name[0].chr}" if name !~ /^[&#+!]/
+ raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/
+
+ init_server_or_casemap(opts)
+
+ @name = name
+
+ @topic = topic ? topic.to_irc_channel_topic : Channel::Topic.new
+
+ @users = UserList.new
+
+ users.each { |u|
+ add_user(u)
+ }
+
+ # Flags
+ @mode = {}
+ end
+
+ # Removes a user from the channel
+ #
+ def delete_user(user)
+ @mode.each { |sym, mode|
+ mode.reset(user) if mode.kind_of?(UserMode)
+ }
+ @users.delete(user)
+ end
+
+ # The channel prefix
+ #
+ def prefix
+ name[0].chr
+ end
+
+ # A channel is local to a server if it has the '&' prefix
+ #
+ def local?
+ name[0] == 0x26
+ end
+
+ # A channel is modeless if it has the '+' prefix
+ #
+ def modeless?
+ name[0] == 0x2b
+ end
+
+ # A channel is safe if it has the '!' prefix
+ #
+ def safe?
+ name[0] == 0x21
+ end
+
+ # A channel is normal if it has the '#' prefix
+ #
+ def normal?
+ name[0] == 0x23
+ end
+
+ # Create a new mode
+ #
+ def create_mode(sym, kl)
+ @mode[sym.to_sym] = kl.new(self)
+ end
+
+ def modes_of(user)
+ l = []
+ @mode.map { |s, m|
+ l << s if (m.class <= UserMode and m.list[user])
+ }
+ l
+ end
+
+ def has_op?(user)
+ @mode.has_key?(:o) and @mode[:o].list[user]
+ end
+
+ def has_voice?(user)
+ @mode.has_key?(:v) and @mode[:v].list[user]
+ end
+ end
+
+
+ # A ChannelList is an ArrayOf <code>Channel</code>s
+ #
+ class ChannelList < ArrayOf
+
+ # Create a new ChannelList, optionally filling it with the elements from
+ # the Array argument fed to it.
+ #
+ def initialize(ar=[])
+ super(Channel, ar)
+ end
+
+ # Convenience method: convert the ChannelList to a list of channel names.
+ # The indices are preserved
+ #
+ def names
+ self.map { |chan| chan.name }
+ end
+
+ end
+
+end
+
+
+class String
+
+ # We keep extending String, this time adding a method that converts a
+ # String into an Irc::Channel object
+ #
+ def to_irc_channel(opts={})
+ Irc::Channel.new(self, opts)
+ end
+
+end
+
+
+module Irc
+
+
+ # An IRC Server represents the Server the client is connected to.
+ #
+ class Server
+
+ attr_reader :hostname, :version, :usermodes, :chanmodes
+ alias :to_s :hostname
+ attr_reader :supports, :capabilities
+
+ attr_reader :channels, :users
+
+ # TODO Ho
+ def channel_names
+ @channels.map { |ch| ch.downcase }
+ end
+
+ # TODO Ho
+ def user_nicks
+ @users.map { |u| u.downcase }
+ end
+
+ def inspect
+ chans, users = [@channels, @users].map {|d|
+ d.sort { |a, b|
+ a.downcase <=> b.downcase
+ }.map { |x|
+ x.inspect
+ }
+ }
+
+ str = self.__to_s__[0..-2]
+ str << " @hostname=#{hostname}"
+ str << " @channels=#{chans}"
+ str << " @users=#{users}"
+ str << ">"
+ end
+
+ # Create a new Server, with all instance variables reset to nil (for
+ # scalar variables), empty channel and user lists and @supports
+ # initialized to the default values for all known supported features.
+ #
+ def initialize
+ @hostname = @version = @usermodes = @chanmodes = nil
+
+ @channels = ChannelList.new
+
+ @users = UserList.new
+
+ reset_capabilities
+ end
+
+ # Resets the server capabilities
+ #
+ def reset_capabilities
+ @supports = {
+ :casemapping => 'rfc1459'.to_irc_casemap,
+ :chanlimit => {},
+ :chanmodes => {
+ :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 => 50,
+ :chantypes => "#&!+",
+ :excepts => nil,
+ :idchan => {},
+ :invex => nil,
+ :kicklen => nil,
+ :maxlist => {},
+ :modes => 3,
+ :network => nil,
+ :nicklen => 9,
+ :prefix => {
+ :modes => [:o, :v],
+ :prefixes => [:"@", :+]
+ },
+ :safelist => nil,
+ :statusmsg => nil,
+ :std => nil,
+ :targmax => {},
+ :topiclen => nil
+ }
+ @capabilities = {}
+ end
+
+ # Convert a mode (o, v, h, ...) to the corresponding
+ # prefix (@, +, %, ...). See also mode_for_prefix
+ def prefix_for_mode(mode)
+ return @supports[:prefix][:prefixes][
+ @supports[:prefix][:modes].index(mode.to_sym)
+ ]
+ end
+
+ # Convert a prefix (@, +, %, ...) to the corresponding
+ # mode (o, v, h, ...). See also prefix_for_mode
+ def mode_for_prefix(pfx)
+ return @supports[:prefix][:modes][
+ @supports[:prefix][:prefixes].index(pfx.to_sym)
+ ]
+ end
+
+ # Resets the Channel and User list
+ #
+ def reset_lists
+ @users.reverse_each { |u|
+ delete_user(u)
+ }
+ @channels.reverse_each { |u|
+ delete_channel(u)
+ }
+ end
+
+ # Clears the server
+ #
+ def clear
+ reset_lists
+ reset_capabilities
+ @hostname = @version = @usermodes = @chanmodes = nil
+ end
+
+ # This method is used to parse a 004 RPL_MY_INFO line
+ #
+ def parse_my_info(line)
+ ar = line.split(' ')
+ @hostname = ar[0]
+ @version = ar[1]
+ @usermodes = ar[2]
+ @chanmodes = ar[3]
+ end
+
+ def noval_warn(key, val, &block)
+ if val
+ yield if block_given?
+ else
+ warn "No #{key.to_s.upcase} value"
+ end
+ end
+
+ def val_warn(key, val, &block)
+ if val == true or val == false or val.nil?
+ yield if block_given?
+ else
+ warn "No #{key.to_s.upcase} value must be specified, got #{val}"
+ end
+ end
+ private :noval_warn, :val_warn
+
+ # This method is used to parse a 005 RPL_ISUPPORT line
+ #
+ # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt]
+ #
+ def parse_isupport(line)
+ debug "Parsing ISUPPORT #{line.inspect}"
+ ar = line.split(' ')
+ reparse = ""
+ ar.each { |en|
+ prekey, val = en.split('=', 2)
+ if prekey =~ /^-(.*)/
+ key = $1.downcase.to_sym
+ val = false
+ else
+ key = prekey.downcase.to_sym
+ end
+ case key
+ when :casemapping
+ noval_warn(key, val) {
+ @supports[key] = val.to_irc_casemap
+ }
+ when :chanlimit, :idchan, :maxlist, :targmax
+ noval_warn(key, val) {
+ groups = val.split(',')
+ groups.each { |g|
+ k, v = g.split(':')
+ @supports[key][k] = v.to_i || 0
+ if @supports[key][k] == 0
+ warn "Deleting #{key} limit of 0 for #{k}"
+ @supports[key].delete(k)
+ end
+ }
+ }
+ when :chanmodes
+ noval_warn(key, val) {
+ groups = val.split(',')
+ @supports[key][:typea] = groups[0].scan(/./).map { |x| x.to_sym}
+ @supports[key][:typeb] = groups[1].scan(/./).map { |x| x.to_sym}
+ @supports[key][:typec] = groups[2].scan(/./).map { |x| x.to_sym}
+ @supports[key][:typed] = groups[3].scan(/./).map { |x| x.to_sym}
+ }
+ when :channellen, :kicklen, :modes, :topiclen
+ if val
+ @supports[key] = val.to_i
+ else
+ @supports[key] = nil
+ end
+ when :chantypes
+ @supports[key] = val # can also be nil
+ when :excepts
+ val ||= 'e'
+ @supports[key] = val
+ when :invex
+ val ||= 'I'
+ @supports[key] = val
+ when :maxchannels
+ noval_warn(key, val) {
+ reparse += "CHANLIMIT=(chantypes):#{val} "
+ }
+ when :maxtargets
+ noval_warn(key, val) {
+ @supports[:targmax]['PRIVMSG'] = val.to_i
+ @supports[:targmax]['NOTICE'] = val.to_i
+ }
+ when :network
+ noval_warn(key, val) {
+ @supports[key] = val
+ }
+ when :nicklen
+ noval_warn(key, val) {
+ @supports[key] = val.to_i
+ }
+ when :prefix
+ if val
+ val.scan(/\((.*)\)(.*)/) { |m, p|
+ @supports[key][:modes] = m.scan(/./).map { |x| x.to_sym}
+ @supports[key][:prefixes] = p.scan(/./).map { |x| x.to_sym}
+ }
+ else
+ @supports[key][:modes] = nil
+ @supports[key][:prefixes] = nil
+ end
+ when :safelist
+ val_warn(key, val) {
+ @supports[key] = val.nil? ? true : val
+ }
+ when :statusmsg
+ noval_warn(key, val) {
+ @supports[key] = val.scan(/./)
+ }
+ when :std
+ noval_warn(key, val) {
+ @supports[key] = val.split(',')
+ }
+ else
+ @supports[key] = val.nil? ? true : val
+ end
+ }
+ reparse.gsub!("(chantypes)",@supports[:chantypes])
+ parse_isupport(reparse) unless reparse.empty?
+ end
+
+ # Returns the casemap of the server.
+ #
+ def casemap
+ @supports[:casemapping]
+ 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])
+ 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])
+ 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)
+ return false if name.nil_or_empty?
+ channel_names.index(name.irc_downcase(casemap))
+ end
+ alias :has_chan? :has_channel?
+
+ # Returns the channel with name _name_, if available
+ #
+ def get_channel(name)
+ return nil if name.nil_or_empty?
+ idx = has_channel?(name)
+ channels[idx] if idx
+ end
+ alias :get_chan :get_channel
+
+ # Create a new Channel object bound to the receiver and add it to the
+ # list of <code>Channel</code>s on the receiver, unless the channel was
+ # present already. In this case, the default action is to raise an
+ # exception, unless _fails_ is set to false. An exception can also be
+ # raised if _str_ is nil or empty, again only if _fails_ is set to true;
+ # otherwise, the method just returns nil
+ #
+ def new_channel(name, topic=nil, users=[], fails=true)
+ if name.nil_or_empty?
+ raise "Tried to look for empty or nil channel name #{name.inspect}" if fails
+ return nil
+ end
+ ex = get_chan(name)
+ if ex
+ raise "Channel #{name} already exists on server #{self}" if fails
+ return ex
+ else
+
+ prefix = name[0].chr
+
+ # Give a warning if the new Channel goes over some server limits.
+ #
+ # FIXME might need to raise an exception
+ #
+ warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix)
+ warn "#{self} doesn't support channel names this long (#{name.length} > #{@supports[: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.include?(prefix)
+ count = 0
+ channel_names.each { |n|
+ count += 1 if k.include?(n[0])
+ }
+ # raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k]
+ warn "Already joined #{count}/#{@supports[:chanlimit][k]} channels with prefix #{k}, we may be going over server limits" if count >= @supports[:chanlimit][k]
+ }
+
+ # So far, everything is fine. Now create the actual Channel
+ #
+ chan = Channel.new(name, topic, users, :server => self)
+
+ # We wade through +prefix+ and +chanmodes+ to create appropriate
+ # lists and flags for this channel
+
+ @supports[:prefix][:modes].each { |mode|
+ chan.create_mode(mode, Channel::UserMode)
+ } if @supports[:prefix][:modes]
+
+ @supports[:chanmodes].each { |k, val|
+ if val
+ case k
+ when :typea
+ val.each { |mode|
+ chan.create_mode(mode, Channel::ModeTypeA)
+ }
+ when :typeb
+ val.each { |mode|
+ chan.create_mode(mode, Channel::ModeTypeB)
+ }
+ when :typec
+ val.each { |mode|
+ chan.create_mode(mode, Channel::ModeTypeC)
+ }
+ when :typed
+ val.each { |mode|
+ chan.create_mode(mode, Channel::ModeTypeD)
+ }
+ end
+ end
+ }
+
+ @channels << chan
+ # debug "Created channel #{chan.inspect}"
+ return chan
+ end
+ 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)
+ idx = has_channel?(name)
+ raise "Tried to remove unmanaged channel #{name}" unless idx
+ @channels.delete_at(idx)
+ end
+
+ # Checks if the receiver already has a user with the given _nick_
+ #
+ def has_user?(nick)
+ return false if nick.nil_or_empty?
+ user_nicks.index(nick.irc_downcase(casemap))
+ end
+
+ # Returns the user with nick _nick_, if available
+ #
+ def get_user(nick)
+ idx = has_user?(nick)
+ @users[idx] if idx
+ end
+
+ # Create a new User object bound to the receiver and add it to the list
+ # of <code>User</code>s on the receiver, unless the User was present
+ # already. In this case, the default action is to raise an exception,
+ # unless _fails_ is set to false. An exception can also be raised
+ # if _str_ is nil or empty, again only if _fails_ is set to true;
+ # otherwise, the method just returns nil
+ #
+ def new_user(str, fails=true)
+ if str.nil_or_empty?
+ raise "Tried to look for empty or nil user name #{str.inspect}" if fails
+ return nil
+ end
+ tmp = str.to_irc_user(:server => self)
+ old = get_user(tmp.nick)
+ # debug "Tmp: #{tmp.inspect}"
+ # debug "Old: #{old.inspect}"
+ if old
+ # debug "User already existed as #{old.inspect}"
+ if tmp.known?
+ if old.known?
+ # debug "Both were known"
+ # Do not raise an error: things like Freenode change the hostname after identification
+ warning "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp
+ raise "User #{tmp} already exists on server #{self}" if fails
+ end
+ if old.fullform.downcase != tmp.fullform.downcase
+ old.replace(tmp)
+ # debug "Known user now #{old.inspect}"
+ end
+ end
+ return old
+ else
+ warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen]
+ @users << tmp
+ return @users.last
+ end
+ end
+
+ # Returns the User with the given Netmask on the server,
+ # creating it if necessary. This is a short form for
+ # new_user(_str_, +false+)
+ #
+ def user(str)
+ new_user(str, false)
+ end
+
+ # Deletes User _user_ from Channel _channel_
+ #
+ def delete_user_from_channel(user, channel)
+ channel.delete_user(user)
+ end
+
+ # Remove User _someuser_ from the list of <code>User</code>s.
+ # _someuser_ must be specified with the full Netmask.
+ #
+ def delete_user(someuser)
+ idx = has_user?(someuser)
+ raise "Tried to remove unmanaged user #{user}" unless idx
+ have = self.user(someuser)
+ @channels.each { |ch|
+ delete_user_from_channel(have, ch)
+ }
+ @users.delete_at(idx)
+ end
+
+ # Create a new Netmask object with the appropriate casemap
+ #
+ def new_netmask(str)
+ str.to_irc_netmask(:server => self)
+ end
+
+ # Finds all <code>User</code>s on server whose Netmask matches _mask_
+ #
+ def find_users(mask)
+ nm = new_netmask(mask)
+ @users.inject(UserList.new) {
+ |list, user|
+ if user.user == "*" or user.host == "*"
+ list << user if user.nick.irc_downcase(casemap) =~ nm.nick.irc_downcase(casemap).to_irc_regexp
+ else
+ list << user if user.matches?(nm)
+ end
+ list
+ }
+ end
+
+ end
+
+end
+
diff --git a/lib/rbot/plugins/opmeh.rb b/lib/rbot/plugins/opmeh.rb
index 2776de60..0702c906 100644
--- a/lib/rbot/plugins/opmeh.rb
+++ b/lib/rbot/plugins/opmeh.rb
@@ -1,19 +1,19 @@
-class OpMehPlugin < Plugin
-
- def help(plugin, topic="")
- return "opmeh <channel> => grant user ops in <channel>"
- end
-
- def privmsg(m)
- if(m.params)
- channel = m.params
- else
- channel = m.channel
- end
- target = m.sourcenick
- @bot.sendq("MODE #{channel} +o #{target}")
- m.okay
- end
-end
-plugin = OpMehPlugin.new
-plugin.register("opmeh")
+class OpMehPlugin < Plugin
+
+ def help(plugin, topic="")
+ return "opmeh <channel> => grant user ops in <channel>"
+ end
+
+ def privmsg(m)
+ if(m.params)
+ channel = m.params
+ else
+ channel = m.channel
+ end
+ target = m.sourcenick
+ @bot.sendq("MODE #{channel} +o #{target}")
+ m.okay
+ end
+end
+plugin = OpMehPlugin.new
+plugin.register("opmeh")