diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/rbot/botuser.rb | 1848 | ||||
-rw-r--r-- | lib/rbot/core/auth.rb | 1936 | ||||
-rw-r--r-- | lib/rbot/core/basics.rb | 376 | ||||
-rw-r--r-- | lib/rbot/core/config.rb | 650 | ||||
-rw-r--r-- | lib/rbot/irc.rb | 3916 | ||||
-rw-r--r-- | lib/rbot/plugins/opmeh.rb | 38 |
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") |