]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/botuser.rb
refactor: remove global bot instance, closes #42
[user/henk/code/ruby/rbot.git] / lib / rbot / botuser.rb
1 #-- vim:sw=2:et
2 #++
3 # :title: User management
4 #
5 # rbot user management
6 # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)
7
8 require 'singleton'
9 require 'set'
10 require 'rbot/maskdb'
11
12 # This would be a good idea if it was failproof, but the truth
13 # is that other methods can indirectly modify the hash. *sigh*
14 #
15 # class AuthNotifyingHash < Hash
16 #   %w(clear default= delete delete_if replace invert
17 #      merge! update rehash reject! replace shift []= store).each { |m|
18 #     class_eval {
19 #       define_method(m) { |*a|
20 #         r = super(*a)
21 #         Irc::Bot::Auth.manager.set_changed
22 #         r
23 #       }
24 #     }
25 #   }
26 # end
27 #
28
29 module Irc
30 class Bot
31
32
33   # This module contains the actual Authentication stuff
34   #
35   module Auth
36
37     Config.register Config::StringValue.new( 'auth.password',
38       :default => [*?a..?z,*?A..?Z,*?0..?9].sample(8).join, :store_default => true,
39       :wizard => true,
40       :on_change => Proc.new {|bot, v| bot.auth.botowner.password = v},
41       :desc => _('Password for the bot owner'))
42     Config.register Config::BooleanValue.new( 'auth.login_by_mask',
43       :default => 'true',
44       :desc => _('Set false to prevent new botusers from logging in without a password when the user netmask is known'))
45     Config.register Config::BooleanValue.new( 'auth.autologin',
46       :default => 'true',
47       :desc => _('Set false to prevent new botusers from recognizing IRC users without a need to manually login'))
48     Config.register Config::BooleanValue.new( 'auth.autouser',
49       :default => 'false',
50       :desc => _('Set true to allow new botusers to be created automatically'))
51     # Config.register Config::IntegerValue.new( 'auth.default_level',
52     #   :default => 10, :wizard => true,
53     #   :desc => 'The default level for new/unknown users' )
54
55     # Generate a random password of length _l_
56     #
57     def Auth.random_password(l=8)
58       pwd = ""
59       l.times do
60         pwd << (rand(26) + (rand(2) == 0 ? 65 : 97) ).chr
61       end
62       return pwd
63     end
64
65
66     # An Irc::Bot::Auth::Command defines a command by its "path":
67     #
68     #   base::command::subcommand::subsubcommand::subsubsubcommand
69     #
70     class Command
71
72       attr_reader :command, :path
73
74       # A method that checks if a given _cmd_ is in a form that can be
75       # reduced into a canonical command path, and if so, returns it
76       #
77       def sanitize_command_path(cmd)
78         pre = cmd.to_s.downcase.gsub(/^\*?(?:::)?/,"").gsub(/::$/,"")
79         return pre if pre.empty?
80         return pre if pre =~ /^\S+(::\S+)*$/
81         raise TypeError, "#{cmd.inspect} is not a valid command"
82       end
83
84       # Creates a new Command from a given string; you can then access
85       # the command as a symbol with the :command method and the whole
86       # path as :path
87       #
88       #   Command.new("core::auth::save").path => [:"*", :"core", :"core::auth", :"core::auth::save"]
89       #
90       #   Command.new("core::auth::save").command => :"core::auth::save"
91       #
92       def initialize(cmd)
93         cmdpath = sanitize_command_path(cmd).split('::')
94         seq = cmdpath.inject(["*"]) { |list, cc|
95           list << (list.length > 1 ? list.last + "::" : "") + cc
96         }
97         @path = seq.map { |k|
98           k.to_sym
99         }
100         @command = path.last
101         debug "Created command #{@command.inspect} with path #{@path.pretty_inspect}"
102       end
103
104       # Returs self
105       def to_irc_auth_command
106         self
107       end
108
109     end
110
111   end
112
113 end
114 end
115
116
117 class String
118
119   # Returns an Irc::Bot::Auth::Comand from the receiver
120   def to_irc_auth_command
121     Irc::Bot::Auth::Command.new(self)
122   end
123
124 end
125
126
127 class Symbol
128
129   # Returns an Irc::Bot::Auth::Comand from the receiver
130   def to_irc_auth_command
131     Irc::Bot::Auth::Command.new(self)
132   end
133
134 end
135
136
137 module Irc
138 class Bot
139
140
141   module Auth
142
143
144     # This class describes a permission set
145     class PermissionSet
146
147       attr_reader :perm
148       # Create a new (empty) PermissionSet
149       #
150       def initialize
151         @perm = {}
152       end
153
154       # Inspection simply inspects the internal hash
155       def inspect
156         @perm.inspect
157       end
158
159       # Sets the permission for command _cmd_ to _val_,
160       #
161       def set_permission(str, val)
162         cmd = str.to_irc_auth_command
163         case val
164         when true, false
165           @perm[cmd.command] = val
166         when nil
167           @perm.delete(cmd.command)
168         else
169           raise TypeError, "#{val.inspect} must be true or false" unless [true,false].include?(val)
170         end
171       end
172
173       # Resets the permission for command _cmd_
174       #
175       def reset_permission(cmd)
176         set_permission(cmd, nil)
177       end
178
179       # Tells if command _cmd_ is permitted. We do this by returning
180       # the value of the deepest Command#path that matches.
181       #
182       def permit?(str)
183         cmd = str.to_irc_auth_command
184         # TODO user-configurable list of always-allowed commands,
185         # for admins that want to set permissions -* for everybody
186         return true if cmd.command == :login
187         allow = nil
188         cmd.path.reverse.each { |k|
189           if @perm.has_key?(k)
190             allow = @perm[k]
191             break
192           end
193         }
194         return allow
195       end
196
197     end
198
199
200     # This is the error that gets raised when an invalid password is met
201     #
202     class InvalidPassword < RuntimeError
203     end
204
205
206     # This is the basic class for bot users: they have a username, a
207     # password, a list of netmasks to match against, and a list of
208     # permissions. A BotUser can be marked as 'transient', usually meaning
209     # it's not intended for permanent storage. Transient BotUsers have lower
210     # priority than nontransient ones for autologin purposes.
211     #
212     # To initialize a BotUser, you pass a _username_ and an optional
213     # hash of options. Currently, only two options are recognized:
214     #
215     # transient:: true or false, determines if the BotUser is transient or
216     #             permanent (default is false, permanent BotUser).
217     #
218     #             Transient BotUsers are initialized by prepending an
219     #             asterisk (*) to the username, and appending a sanitized
220     #             version of the object_id. The username can be empty.
221     #             A random password is generated.
222     #
223     #             Permanent Botusers need the username as is, and no
224     #             password is generated.
225     #
226     # masks::     an array of Netmasks to initialize the NetmaskList. This
227     #             list is used as-is for permanent BotUsers.
228     #
229     #             Transient BotUsers will alter the list elements which are
230     #             Irc::User by globbing the nick and any initial nonletter
231     #             part of the ident.
232     #
233     #             The masks option is optional for permanent BotUsers, but
234     #             obligatory (non-empty) for transients.
235     #
236     class BotUser
237
238       attr_reader :username
239       attr_reader :password
240       attr_reader :netmasks
241       attr_reader :perm
242       attr_reader :perm_temp
243       attr_writer :login_by_mask
244       attr_writer :transient
245
246       def autologin=(vnew)
247         vold = @autologin
248         @autologin = vnew
249         if vold && !vnew
250           @netmasks.each { |n| Auth.manager.maskdb.remove(self, n) }
251         elsif vnew && !vold
252           @netmasks.each { |n| Auth.manager.maskdb.add(self, n) }
253         end
254       end
255
256       # Checks if the BotUser is transient
257       def transient?
258         @transient
259       end
260
261       # Checks if the BotUser is permanent (not transient)
262       def permanent?
263         !@transient
264       end
265
266       # Sets if the BotUser is permanent or not
267       def permanent=(bool)
268         @transient=!bool
269       end
270
271       # Make the BotUser permanent
272       def make_permanent(name)
273         raise TypeError, "permanent already" if permanent?
274         @username = BotUser.sanitize_username(name)
275         @transient = false
276         reset_autologin
277         reset_password # or not?
278         @netmasks.dup.each do |m|
279           delete_netmask(m)
280           add_netmask(m.generalize)
281         end
282       end
283
284       # Create a new BotUser with given username
285       def initialize(username, options={})
286         opts = {:transient => false}.merge(options)
287         @transient = opts[:transient]
288
289         if @transient
290           @username = "*"
291           @username << BotUser.sanitize_username(username) if username and not username.to_s.empty?
292           @username << BotUser.sanitize_username(object_id)
293           reset_password
294           @login_by_mask=true
295           @autologin=true
296         else
297           @username = BotUser.sanitize_username(username)
298           @password = nil
299           reset_login_by_mask
300           reset_autologin
301         end
302
303         @netmasks = NetmaskList.new
304         if opts.key?(:masks) and opts[:masks]
305           masks = opts[:masks]
306           masks = [masks] unless masks.respond_to?(:each)
307           masks.each { |m|
308             mask = m.to_irc_netmask
309             if @transient and User === m
310               mask.nick = "*"
311               mask.host = m.host.dup
312               mask.user = "*" + m.user.sub(/^\w?[^\w]+/,'')
313             end
314             add_netmask(mask) unless mask.to_s == "*"
315           }
316         end
317         raise "must provide a usable mask for transient BotUser #{@username}" if @transient and @netmasks.empty?
318
319         @perm = {}
320         @perm_temp = {}
321       end
322
323       # Inspection
324       def inspect
325         str = self.__to_s__[0..-2]
326         str << " (transient)" if @transient
327         str << ":"
328         str << " @username=#{@username.inspect}"
329         str << " @netmasks=#{@netmasks.inspect}"
330         str << " @perm=#{@perm.inspect}"
331         str << " @perm_temp=#{@perm_temp.inspect}" unless @perm_temp.empty?
332         str << " @login_by_mask=#{@login_by_mask}"
333         str << " @autologin=#{@autologin}"
334         str << ">"
335       end
336
337       # In strings
338       def to_s
339         @username
340       end
341
342       # Convert into a hash
343       def to_hash
344         {
345           :username => @username,
346           :password => @password,
347           :netmasks => @netmasks,
348           :perm => @perm,
349           :login_by_mask => @login_by_mask,
350           :autologin => @autologin,
351         }
352       end
353
354       # Do we allow logging in without providing the password?
355       #
356       def login_by_mask?
357         @login_by_mask
358       end
359
360       # Reset the login-by-mask option
361       #
362       def reset_login_by_mask
363         @login_by_mask = Auth.manager.bot.config['auth.login_by_mask'] unless defined?(@login_by_mask)
364       end
365
366       # Reset the autologin option
367       #
368       def reset_autologin
369         @autologin = Auth.manager.bot.config['auth.autologin'] unless defined?(@autologin)
370       end
371
372       # Do we allow automatic logging in?
373       #
374       def autologin?
375         @autologin
376       end
377
378       # Restore from hash
379       def from_hash(h)
380         @username = h[:username] if h.has_key?(:username)
381         @password = h[:password] if h.has_key?(:password)
382         @login_by_mask = h[:login_by_mask] if h.has_key?(:login_by_mask)
383         @autologin = h[:autologin] if h.has_key?(:autologin)
384         if h.has_key?(:netmasks)
385           @netmasks = h[:netmasks]
386           debug @netmasks
387           @netmasks.each { |n| Auth.manager.maskdb.add(self, n) } if @autologin
388           debug @netmasks
389         end
390         @perm = h[:perm] if h.has_key?(:perm)
391       end
392
393       # This method sets the password if the proposed new password
394       # is valid
395       def password=(pwd=nil)
396         pass = pwd.to_s
397         if pass.empty?
398           reset_password
399         else
400           begin
401             raise InvalidPassword, "#{pass} contains invalid characters" if pass !~ /^[\x21-\x7e]+$/
402             raise InvalidPassword, "#{pass} too short" if pass.length < 4
403             @password = pass
404           rescue InvalidPassword => e
405             raise e
406           rescue => e
407             raise InvalidPassword, "Exception #{e.inspect} while checking #{pass.inspect} (#{pwd.inspect})"
408           end
409         end
410       end
411
412       # Resets the password by creating a new onw
413       def reset_password
414         @password = Auth.random_password
415       end
416
417       # Sets the permission for command _cmd_ to _val_ on channel _chan_
418       #
419       def set_permission(cmd, val, chan="*")
420         k = chan.to_s.to_sym
421         @perm[k] = PermissionSet.new unless @perm.has_key?(k)
422         @perm[k].set_permission(cmd, val)
423       end
424
425       # Resets the permission for command _cmd_ on channel _chan_
426       #
427       def reset_permission(cmd, chan ="*")
428         set_permission(cmd, nil, chan)
429       end
430
431       # Sets the temporary permission for command _cmd_ to _val_ on channel _chan_
432       #
433       def set_temp_permission(cmd, val, chan="*")
434         k = chan.to_s.to_sym
435         @perm_temp[k] = PermissionSet.new unless @perm_temp.has_key?(k)
436         @perm_temp[k].set_permission(cmd, val)
437       end
438
439       # Resets the temporary permission for command _cmd_ on channel _chan_
440       #
441       def reset_temp_permission(cmd, chan ="*")
442         set_temp_permission(cmd, nil, chan)
443       end
444
445       # Checks if BotUser is allowed to do something on channel _chan_,
446       # or on all channels if _chan_ is nil
447       #
448       def permit?(cmd, chan=nil)
449         if chan
450           k = chan.to_s.to_sym
451         else
452           k = :*
453         end
454         allow = nil
455         pt = @perm.merge @perm_temp
456         if pt.has_key?(k)
457           allow = pt[k].permit?(cmd)
458         end
459         return allow
460       end
461
462       # Adds a Netmask
463       #
464       def add_netmask(mask)
465         m = mask.to_irc_netmask
466         @netmasks << m
467         if self.autologin?
468           Auth.manager.maskdb.add(self, m)
469           Auth.manager.logout_transients(m) if self.permanent?
470         end
471       end
472
473       # Removes a Netmask
474       #
475       def delete_netmask(mask)
476         m = mask.to_irc_netmask
477         @netmasks.delete(m)
478         Auth.manager.maskdb.remove(self, m) if self.autologin?
479       end
480
481       # Reset Netmasks, clearing @netmasks
482       #
483       def reset_netmasks
484         @netmasks.each { |m|
485           Auth.manager.maskdb.remove(self, m) if self.autologin?
486         }
487         @netmasks.clear
488       end
489
490       # This method checks if BotUser has a Netmask that matches _user_
491       #
492       def knows?(usr)
493         user = usr.to_irc_user
494         !!@netmasks.find { |n| user.matches? n }
495       end
496
497       # This method gets called when User _user_ wants to log in.
498       # It returns true or false depending on whether the password
499       # is right. If it is, the Netmask of the user is added to the
500       # list of acceptable Netmask unless it's already matched.
501       def login(user, password=nil)
502         if password == @password or (password.nil? and (@login_by_mask || @autologin) and knows?(user))
503           add_netmask(user) unless knows?(user)
504           debug "#{user} logged in as #{self.inspect}"
505           return true
506         else
507           return false
508         end
509       end
510
511       # # This method gets called when User _user_ has logged out as this BotUser
512       # def logout(user)
513       #   delete_netmask(user) if knows?(user)
514       # end
515
516       # This method sanitizes a username by chomping, downcasing
517       # and replacing any nonalphanumeric character with _
518       #
519       def BotUser.sanitize_username(name)
520         candidate = name.to_s.chomp.downcase.gsub(/[^a-z0-9]/,"_")
521         raise "sanitized botusername #{candidate} too short" if candidate.length < 3
522         return candidate
523       end
524
525     end
526
527     # This is the default BotUser: it's used for all users which haven't
528     # identified with the bot
529     #
530     class DefaultBotUserClass < BotUser
531
532       private :add_netmask, :delete_netmask
533
534       include Singleton
535
536       # The default BotUser is named 'everyone'
537       #
538       def initialize
539         reset_login_by_mask
540         reset_autologin
541         super("everyone")
542         @default_perm = PermissionSet.new
543       end
544
545       # This method returns without changing anything
546       #
547       def login_by_mask=(val)
548         debug "Tried to change the login-by-mask for default bot user, ignoring"
549         return @login_by_mask
550       end
551
552       # The default botuser allows logins by mask
553       #
554       def reset_login_by_mask
555         @login_by_mask = true
556       end
557
558       # This method returns without changing anything
559       #
560       def autologin=(val)
561         debug "Tried to change the autologin for default bot user, ignoring"
562         return
563       end
564
565       # The default botuser doesn't allow autologin (meaningless)
566       #
567       def reset_autologin
568         @autologin = false
569       end
570
571       # Sets the default permission for the default user (i.e. the ones
572       # set by the BotModule writers) on all channels
573       #
574       def set_default_permission(cmd, val)
575         @default_perm.set_permission(Command.new(cmd), val)
576         debug "Default permissions now: #{@default_perm.pretty_inspect}"
577       end
578
579       # default knows everybody
580       #
581       def knows?(user)
582         return true if user.to_irc_user
583       end
584
585       # We always allow logging in as the default user
586       def login(user, password)
587         return true
588       end
589
590       # DefaultBotUser will check the default_perm after checking
591       # the global ones
592       # or on all channels if _chan_ is nil
593       #
594       def permit?(cmd, chan=nil)
595         allow = super(cmd, chan)
596         if allow.nil? && chan.nil?
597           allow = @default_perm.permit?(cmd)
598         end
599         return allow
600       end
601
602     end
603
604     # Returns the only instance of DefaultBotUserClass
605     #
606     def Auth.defaultbotuser
607       return DefaultBotUserClass.instance
608     end
609
610     # This is the BotOwner: he can do everything
611     #
612     class BotOwnerClass < BotUser
613
614       include Singleton
615
616       def initialize
617         @login_by_mask = false
618         @autologin = true
619         super("owner")
620       end
621
622       def permit?(cmd, chan=nil)
623         return true
624       end
625
626     end
627
628     # Returns the only instance of BotOwnerClass
629     #
630     def Auth.botowner
631       return BotOwnerClass.instance
632     end
633
634
635     class BotUser
636       # Check if the current BotUser is the default one
637       def default?
638         return DefaultBotUserClass === self
639       end
640
641       # Check if the current BotUser is the owner
642       def owner?
643         return BotOwnerClass === self
644       end
645     end
646
647
648     # This is the ManagerClass singleton, used to manage
649     # Irc::User/Irc::Bot::Auth::BotUser connections and everything
650     #
651     class ManagerClass
652
653       include Singleton
654
655       attr_reader :maskdb
656       attr_reader :everyone
657       attr_reader :botowner
658       attr_reader :bot
659
660       # The instance manages two <code>Hash</code>es: one that maps
661       # <code>Irc::User</code>s onto <code>BotUser</code>s, and the other that maps
662       # usernames onto <code>BotUser</code>
663       def initialize
664         @everyone = Auth::defaultbotuser
665         @botowner = Auth::botowner
666         bot_associate(nil)
667       end
668
669       def bot_associate(bot)
670         raise "Cannot associate with a new bot! Save first" if defined?(@has_changes) && @has_changes
671
672         reset_hashes
673
674         # Associated bot
675         @bot = bot
676
677         # This variable is set to true when there have been changes
678         # to the botusers list, so that we know when to save
679         @has_changes = false
680       end
681
682       def set_changed
683         @has_changes = true
684       end
685
686       def reset_changed
687         @has_changes = false
688       end
689
690       def changed?
691         @has_changes
692       end
693
694       # resets the hashes
695       def reset_hashes
696         @botusers = Hash.new
697         @maskdb = NetmaskDb.new
698         @allbotusers = Hash.new
699         [everyone, botowner].each do |x|
700           @allbotusers[x.username.to_sym] = x
701         end
702       end
703
704       def load_array(ary, forced)
705         unless ary
706           warning "Tried to load an empty array"
707           return
708         end
709         raise "Won't load with unsaved changes" if @has_changes and not forced
710         reset_hashes
711         ary.each { |x|
712           raise TypeError, "#{x} should be a Hash" unless x.kind_of?(Hash)
713           u = x[:username]
714           unless include?(u)
715             create_botuser(u)
716           end
717           get_botuser(u).from_hash(x)
718           get_botuser(u).transient = false
719         }
720         @has_changes=false
721       end
722
723       def save_array
724         @allbotusers.values.map { |x|
725           x.transient? ? nil : x.to_hash
726         }.compact
727       end
728
729       # checks if we know about a certain BotUser username
730       def include?(botusername)
731         @allbotusers.has_key?(botusername.to_sym)
732       end
733
734       # Maps <code>Irc::User</code> to BotUser
735       def irc_to_botuser(ircuser)
736         logged = @botusers[ircuser.to_irc_user]
737         return logged if logged
738         return autologin(ircuser)
739       end
740
741       # creates a new BotUser
742       def create_botuser(name, password=nil)
743         n = BotUser.sanitize_username(name)
744         k = n.to_sym
745         raise "botuser #{n} exists" if include?(k)
746         bu = BotUser.new(n)
747         bu.password = password
748         @allbotusers[k] = bu
749         return bu
750       end
751
752       # returns the botuser with name _name_
753       def get_botuser(name)
754         @allbotusers.fetch(BotUser.sanitize_username(name).to_sym)
755       end
756
757       # Logs Irc::User _user_ in to BotUser _botusername_ with password _pwd_
758       #
759       # raises an error if _botusername_ is not a known BotUser username
760       #
761       # It is possible to autologin by Netmask, on request
762       #
763       def login(user, botusername, pwd=nil)
764         ircuser = user.to_irc_user
765         n = BotUser.sanitize_username(botusername)
766         k = n.to_sym
767         raise "No such BotUser #{n}" unless include?(k)
768         if @botusers.has_key?(ircuser)
769           return true if @botusers[ircuser].username == n
770           # TODO
771           # @botusers[ircuser].logout(ircuser)
772         end
773         bu = @allbotusers[k]
774         if bu.login(ircuser, pwd)
775           @botusers[ircuser] = bu
776           return true
777         end
778         return false
779       end
780
781       # Tries to auto-login Irc::User _user_ by looking at the known botusers that allow autologin
782       # and trying to login without a password
783       #
784       def autologin(user)
785         ircuser = user.to_irc_user
786         debug "Trying to autologin #{ircuser}"
787         return @botusers[ircuser] if @botusers.has_key?(ircuser)
788         bu = maskdb.find(ircuser)
789         if bu
790           debug "trying #{bu}"
791           bu.login(ircuser) or raise '...what?!'
792           @botusers[ircuser] = bu
793           return bu
794         end
795         # Finally, create a transient if we're set to allow it
796         if @bot.config['auth.autouser']
797           bu = create_transient_botuser(ircuser)
798           @botusers[ircuser] = bu
799           return bu
800         end
801         return everyone
802       end
803
804       # Creates a new transient BotUser associated with Irc::User _user_,
805       # automatically logging him in. Note that transient botuser creation can
806       # fail, typically if we don't have the complete user netmask (e.g. for
807       # messages coming in from a linkbot)
808       #
809       def create_transient_botuser(user)
810         ircuser = user.to_irc_user
811         bu = everyone
812         begin
813           bu = BotUser.new(ircuser, :transient => true, :masks => ircuser)
814           bu.login(ircuser)
815         rescue
816           warning "failed to create transient for #{user}"
817           error $!
818         end
819         return bu
820       end
821
822       # Logs out any Irc::User matching Irc::Netmask _m_ and logged in
823       # to a transient BotUser
824       #
825       def logout_transients(m)
826         debug "to check: #{@botusers.keys.join ' '}"
827         @botusers.keys.each do |iu|
828           debug "checking #{iu.fullform} against #{m.fullform}"
829           bu = @botusers[iu]
830           bu.transient? or next
831           iu.matches?(m) or next
832           @botusers.delete(iu).autologin = false
833         end
834       end
835
836       # Makes transient BotUser _user_ into a permanent BotUser
837       # named _name_; if _user_ is an Irc::User, act on the transient
838       # BotUser (if any) it's logged in as
839       #
840       def make_permanent(user, name)
841         buname = BotUser.sanitize_username(name)
842         # TODO merge BotUser instead?
843         raise "there's already a BotUser called #{name}" if include?(buname)
844
845         tuser = nil
846         case user
847         when String, Irc::User
848           tuser = irc_to_botuser(user)
849         when BotUser
850           tuser = user
851         else
852           raise TypeError, "sorry, don't know how to make #{user.class} into a permanent BotUser"
853         end
854         return nil unless tuser
855         raise TypeError, "#{tuser} is not transient" unless tuser.transient?
856
857         tuser.make_permanent(buname)
858         @allbotusers[tuser.username.to_sym] = tuser
859
860         return tuser
861       end
862
863       # Checks if User _user_ can do _cmd_ on _chan_.
864       #
865       # Permission are checked in this order, until a true or false
866       # is returned:
867       # * associated BotUser on _chan_
868       # * associated BotUser on all channels
869       # * everyone on _chan_
870       # * everyone on all channels
871       #
872       def permit?(user, cmdtxt, channel=nil)
873         if user.class <= BotUser
874           botuser = user
875         else
876           botuser = user.botuser
877         end
878         cmd = cmdtxt.to_irc_auth_command
879
880         chan = channel
881         case chan
882         when User
883           chan = "?"
884         when Channel
885           chan = chan.name
886         end
887
888         allow = nil
889
890         allow = botuser.permit?(cmd, chan) if chan
891         return allow unless allow.nil?
892         allow = botuser.permit?(cmd)
893         return allow unless allow.nil?
894
895         unless botuser == everyone
896           allow = everyone.permit?(cmd, chan) if chan
897           return allow unless allow.nil?
898           allow = everyone.permit?(cmd)
899           return allow unless allow.nil?
900         end
901
902         raise "Could not check permission for user #{user.inspect} to run #{cmdtxt.inspect} on #{chan.inspect}"
903       end
904
905       # Checks if command _cmd_ is allowed to User _user_ on _chan_, optionally
906       # telling if the user is authorized
907       #
908       def allow?(cmdtxt, user, chan=nil)
909         if permit?(user, cmdtxt, chan)
910           return true
911         else
912           # cmds = cmdtxt.split('::')
913           # @bot.say chan, "you don't have #{cmds.last} (#{cmds.first}) permissions here" if chan
914           @bot.say chan, _("%{user}, you don't have '%{command}' permissions here") %
915                         {:user=>user, :command=>cmdtxt} if chan
916           return false
917         end
918       end
919
920     end
921
922     # Returns the only instance of ManagerClass
923     #
924     def Auth.manager
925       return ManagerClass.instance
926     end
927
928   end
929 end
930
931   class User
932
933     # A convenience method to automatically found the botuser
934     # associated with the receiver
935     #
936     def botuser
937       Irc::Bot::Auth.manager.irc_to_botuser(self)
938     end
939   end
940
941 end