From 6f9bfa43ac907700fcba394e0f6b9d987b1192fb Mon Sep 17 00:00:00 2001 From: Giuseppe Bilotta Date: Tue, 4 Mar 2008 18:24:57 +0100 Subject: Unixify all line endings. Some files had DOS-style line endings. Change all of them to Unix-style. --- lib/rbot/botuser.rb | 1848 ++++++++++----------- lib/rbot/core/auth.rb | 1936 +++++++++++----------- lib/rbot/core/basics.rb | 376 ++--- lib/rbot/core/config.rb | 650 ++++---- lib/rbot/irc.rb | 3916 ++++++++++++++++++++++----------------------- lib/rbot/plugins/opmeh.rb | 38 +- 6 files changed, 4382 insertions(+), 4382 deletions(-) (limited to 'lib/rbot') 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 Hashes: one that maps - # Irc::Users onto BotUsers, and the other that maps - # usernames onto BotUser - 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 Irc::User 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 Hashes: one that maps + # Irc::Users onto BotUsers, and the other that maps + # usernames onto BotUser + 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 Irc::User 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 -# 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 [] []: logs in to the bot as botuser with password . When using the full form, you must contact the bot in private. can be omitted if allows login-by-mask and your netmask is among the known ones. if is omitted too autologin will be attempted") - when "whoami" - return _("whoami: names the botuser you're linked to") - when "who" - return _("who is : names the botuser 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 [in ] for : sets or resets the permissions for botuser in channel (use ? to change the permissions for private addressing)") - when "view" - return _("permissions view [for ]: display the permissions for user ") - when "searc" - return _("permissions search : display the permissions associated with the commands matching ") - else - return _("permission topics: syntax, (re)set, view, search") - end - when "user" - case topic - when "show" - return _("user show : shows info about the user; can be any of autologin, login-by-mask, netmasks") - when /^(en|dis)able/ - return _("user enable|disable : turns on or off (autologin, login-by-mask)") - when "set" - return _("user set password : sets the user password to ; 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 : adds/removes netmask from the list of netmasks known to the botuser you're linked to") - when "reset" - return _("user reset : resets to the default values. 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 the password for : contacts in private to tell him/her the password for ") - when "create" - return _("user create : create botuser named with password . The password can be omitted, in which case a random one will be generated. The should only contain alphanumeric characters and the underscore (_)") - when "list" - return _("user list : lists all the botusers") - when "destroy" - return _("user destroy : destroys . This function %{highlight}must%{highlight} be called in two steps. On the first call is queued for destruction. On the second call, which must be in the form 'user confirm destroy ', the botuser will be destroyed. If you want to cancel the destruction, issue the command 'user cancel destroy '") % {:highlight => Bold} - else - return _("user topics: show, enable|disable, add|rm netmask, set, reset, tell, create, list, destroy") - end - when "auth" - return _("auth : log in as the bot owner; other commands: login, whoami, permission syntax, permissions [re]set, permissions view, user, meet, hello") - when "meet" - return _("meet [as ]: 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 ' 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 +# 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 [] []: logs in to the bot as botuser with password . When using the full form, you must contact the bot in private. can be omitted if allows login-by-mask and your netmask is among the known ones. if is omitted too autologin will be attempted") + when "whoami" + return _("whoami: names the botuser you're linked to") + when "who" + return _("who is : names the botuser 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 [in ] for : sets or resets the permissions for botuser in channel (use ? to change the permissions for private addressing)") + when "view" + return _("permissions view [for ]: display the permissions for user ") + when "searc" + return _("permissions search : display the permissions associated with the commands matching ") + else + return _("permission topics: syntax, (re)set, view, search") + end + when "user" + case topic + when "show" + return _("user show : shows info about the user; can be any of autologin, login-by-mask, netmasks") + when /^(en|dis)able/ + return _("user enable|disable : turns on or off (autologin, login-by-mask)") + when "set" + return _("user set password : sets the user password to ; 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 : adds/removes netmask from the list of netmasks known to the botuser you're linked to") + when "reset" + return _("user reset : resets to the default values. 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 the password for : contacts in private to tell him/her the password for ") + when "create" + return _("user create : create botuser named with password . The password can be omitted, in which case a random one will be generated. The should only contain alphanumeric characters and the underscore (_)") + when "list" + return _("user list : lists all the botusers") + when "destroy" + return _("user destroy : destroys . This function %{highlight}must%{highlight} be called in two steps. On the first call is queued for destruction. On the second call, which must be in the form 'user confirm destroy ', the botuser will be destroyed. If you want to cancel the destruction, issue the command 'user cancel destroy '") % {:highlight => Bold} + else + return _("user topics: show, enable|disable, add|rm netmask, set, reset, tell, create, list, destroy") + end + when "auth" + return _("auth : log in as the bot owner; other commands: login, whoami, permission syntax, permissions [re]set, permissions view, user, meet, hello") + when "meet" + return _("meet [as ]: 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 ' 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 -# 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 [] => quit IRC with message ") - when "restart" - _("restart => completely stop and restart the bot (including reconnect)") - when "join" - _("join [] => join channel with secret key if specified. #{@bot.myself} also responds to invites if you have the required access level") - when "part" - _("part => part channel ") - when "hide" - _("hide => part all channels") - when "nick" - _("nick => attempt to change nick to ") - when "say" - _("say | => say to or in private message to ") - when "action" - _("action | => does a /me to or in private message to ") - when "quiet" - _("quiet [in here|] => with no arguments, stop speaking in all channels, if \"in here\", stop speaking in this channel, or stop speaking in ") - when "talk" - _("talk [in here|] => with no arguments, resume speaking in all channels, if \"in here\", resume speaking in this channel, or resume speaking in ") - when "ping" - _("ping => replies with a pong") - when "mode" - _("mode => set channel modes for on to ") - # 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 +# 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 [] => quit IRC with message ") + when "restart" + _("restart => completely stop and restart the bot (including reconnect)") + when "join" + _("join [] => join channel with secret key if specified. #{@bot.myself} also responds to invites if you have the required access level") + when "part" + _("part => part channel ") + when "hide" + _("hide => part all channels") + when "nick" + _("nick => attempt to change nick to ") + when "say" + _("say | => say to or in private message to ") + when "action" + _("action | => does a /me to or in private message to ") + when "quiet" + _("quiet [in here|] => with no arguments, stop speaking in all channels, if \"in here\", stop speaking in this channel, or stop speaking in ") + when "talk" + _("talk [in here|] => with no arguments, resume speaking in all channels, if \"in here\", resume speaking in this channel, or resume speaking in ") + when "ping" + _("ping => replies with a pong") + when "mode" + _("mode => set channel modes for on to ") + # 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 -# 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 => list configuration keys for module ") - when "get" - _("config get => get configuration value for key ") - when "unset" - _("reset key to the default") - when "set" - _("config set => set configuration value for key to ") - when "desc" - _("config desc => describe what key configures") - when "add" - _("config add to => add value to key if is an array") - when "rm" - _("config rm from => remove value from key if 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 => change the bot nick to , 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 +# 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 => list configuration keys for module ") + when "get" + _("config get => get configuration value for key ") + when "unset" + _("reset key to the default") + when "set" + _("config set => set configuration value for key to ") + when "desc" + _("config desc => describe what key configures") + when "add" + _("config add to => add value to key if is an array") + when "rm" + _("config rm from => remove value from key if 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 => change the bot nick to , 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 {}|^ as the uppercase - # equivalents of # []\~. - # - # This is however not the same on all IRC servers: some use standard ASCII - # casemapping, other do not consider ^ as the uppercase of - # ~ - # - 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 * stands for "any - # number of arbitrary characters", a ? stands for "one and exactly - # one arbitrary character". These characters can be escaped by prefixing them - # with a slash (\\). - # - # 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 nick!user@host - # - # 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: - # * *!*@* refers to everybody - # * *!someuser@somehost 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 Netmasks - # - 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 Users - # 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 Channels - # - 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 Channels 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 Channels - # - 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 Users 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 Users. - # _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 Users 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 {}|^ as the uppercase + # equivalents of # []\~. + # + # This is however not the same on all IRC servers: some use standard ASCII + # casemapping, other do not consider ^ as the uppercase of + # ~ + # + 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 * stands for "any + # number of arbitrary characters", a ? stands for "one and exactly + # one arbitrary character". These characters can be escaped by prefixing them + # with a slash (\\). + # + # 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 nick!user@host + # + # 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: + # * *!*@* refers to everybody + # * *!someuser@somehost 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 Netmasks + # + 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 Users + # 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 Channels + # + 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 Channels 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 Channels + # + 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 Users 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 Users. + # _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 Users 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 => grant user ops in " - 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 => grant user ops in " + 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") -- cgit v1.2.3