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