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