Use singular form of notes when there is only one (new) note
[six.git] / lib / user.rb
blobe285bb62999f9c244aa45bc173d461cf57579ea3
2 # CyBot - User management.
5 require 'digest/sha1'
8 # Neat array stuff.
9 class Array
11   # Matching hostmasks.
12   def include_mask?(str)
13     each do |e|
14       if e.include? ?*
15         re = Regexp.new(e.gsub('.', '\.').gsub('*', '.*'))
16         rc = re =~ str
17         return rc if rc
18       else
19         return true if e == str
20       end
21     end
22     false
23   end
25 end
29 class User < PluginBase
31   # Settings that are considered capabilities.
32   CapsList = ['owner', 'admin', 'op', 'voice', 'greet']
34   # Called from other plugins.
35   def add_caps(*caps)
36     CapsList.concat caps
37   end
40   # Security error.
41   class SecurityError < Exception
42   end
44   def initialize
46     @brief_help = 'Manages registrated bot users.'
47     super
49     # TODO/FIXME: This should be updated by notifications.
50     # For NickServ WHOIS.
51     @serv_nswi = {}
52     if (cfg = $config['servers'])
53       cfg.each do |name, serv|
54         if (c = serv['services']) and (c = c['nickserv']) and (c = c['whois-code'])
55           @serv_nswi[name] = [c, nil, false, false]
56         end
57       end
58     end
60     # Set global variable.
61     @serv = {}
62     $user = self
64     # Other plugins can add methods to this. It's important to remember to
65     # remove them again, before unload/reload. Method will be called like this:
66     # hook(seen_or_lost, irc_wrapper, user_nick)
67     @user_watch_hooks = []
69     # Commons.
70     op_com = {
71       :type => Boolean,
72       :help => 'User may become channel operator.'
73     }
74     voice_com = {
75       :type => Boolean,
76       :help => 'User may gain voice.'
77     }
78     owner_com = {
79       :type => Boolean,
80       :help => 'User is bot owner.'
81     }
82     admin_com = {
83       :type => Boolean,
84       :help => 'User is bot administrator.'
85     }
86     greet_com = {
87       :type => Boolean,
88       :help => 'User may set a greeting.'
89     }
90     cmds_com = {
91       :type => Array,
92       :help => 'Command filters.'
93     }
94     plug_com = {
95       :type => Array,
96       :help => 'Plugin filters.'
97     }
99     # Config space addition.
100     $config.merge(
101       'irc' => {
102         :dir => true,
103         'track-nicks' => {
104           :help => 'Track nick name changes when identified.',
105           :type => Boolean
106         },
107         'defaults' => {
108           :dir => true,
109           :help => 'Default user capabilities.',
110           'op' => op_com,
111           'voice' => voice_com,
112           'owner' => owner_com,
113           'admin' => admin_com,
114           'greet' => greet_com,
115           'commands' => cmds_com,
116           'plugins' => plug_com
117         }
118       },
119       'servers' => {
120         :dir => true,
121         :skel => {
122           :dir => true,
123           'defaults' => {
124             :dir => true,
125             :help => 'Default capabilities for all users on this server.',
126             'op' => op_com,
127             'voice' => voice_com,
128             'owner' => owner_com,
129             'admin' => admin_com,
130             'greet' => greet_com,
131             'commands' => cmds_com,
132             'plugins' => plug_com
133           },
134           'users' => {
135             :help => 'User list for this server.',
136             :dir => true,
137             :skel => {
138               :help => 'User settings.',
139               :dir => true,
140               'op' => op_com,
141               'voice' => voice_com,
142               'owner' => owner_com,
143               'admin' => admin_com,
144               'greet' => greet_com,
145               'commands' => cmds_com,
146               'plugins' => plug_com,
147               'fuzzy-time' => {
148                 :type => Boolean,
149                 :help => 'If set, time durations will be given in a more fuzzy format.'
150               },
151               'auth' =>
152                 "The security level used when authenticating the user. One of 'hostmask', 'trusted' and 'manual'.",
153               'channels' => {
154                 :dir => true,
155                 :help => 'User capabilities for various channels.',
156                 :skel => {
157                   :dir => true,
158                   'op' => op_com,
159                   'voice' => voice_com,
160                   'greet' => greet_com,
161                   'greeting' => "Greeting text for this channel. Ignored unless user has the 'greet' capability."
162                 }
163               },
164               'masks' => {
165                 :help => "Host masks for this user.",
166                 :type => Array
167               },
168               'password' => 'Password for identification.',
169               'flags' => {
170                 :help => 'User flags.',
171                 :type => Array,
172                 :on_change => ConfigSpace.set_semantics(['secure', 'foo', 'bar'])
173               },
174             }
175           },
176           'channels' => {
177             :dir => true,
178             :skel => {
179               :dir => true,
180               'defaults' => {
181                 :dir => true,
182                 :help => 'Default user capabilities for this channel.',
183                 'op' => op_com,
184                 'voice' => voice_com
185               }
186             }
187           }
188         }
189       })
191   end
193   attr_reader :user_watch_hooks
195   # Retrieve the nick name of the given recognized user, which could be different
196   # from the given nick if nick tracking is enabled. Returns nil if user is not
197   # known. If passed an IrcWrapper, a server name need not be given. Server name
198   # can itself be an IrcWrapper.
199   def get_nick(irc, sn = nil)
200     if sn: sn = sn.server.name if sn.kind_of?(IrcWrapper)
201     else sn = irc.server.name end
202     if irc.kind_of?(String)
203       nn = irc
204       irc = nil
205     else nn = irc.from.nick end
206     if !(s = @serv[sn]) or !(u = s[IRC::Address.normalize(nn)])
207       irc.reply "Do I know you?  At least not right now." if irc
208       nil
209     else (u == true) ? nn : u end
210   end
212   # Retrieve the user data for the given user. Real user names must be used, which
213   # is not necessarily the current nick of the user. Use get_nick above to resolve
214   # that, if needed. User can be given as an IrcWrapper.
215   def get_data(user, sn = nil)
216     if user.kind_of?(IrcWrapper)
217       nn = user.from.nick
218       if (s = @serv[sn = user.server.name]) and (s = s[IRC::Address.normalize(nn)])
219         rn = (s == true) ? nn : s
220         [rn, $config["servers/#{sn}/users/#{IRC::Address.normalize(rn)}"]]
221       else
222         user.reply "Do I know you?  At least not right now."
223         nil
224       end
225     else
226       sn = sn.server.name if sn.kind_of?(IrcWrapper)
227       $config["servers/#{sn}/users/#{IRC::Address.normalize(user)}"]
228     end
229   end
231   # Get user if he's known (returns the real nickname).
232   # Argument can be an IrcWrapper or a name. The latter form will
233   # fail silently, and needs a server name passed in.
234   def get_user(irc, sn = nil)
235     sn = irc.server.name unless sn
236     if irc.kind_of?(String)
237       nn = irc
238       irc = nil
239     else
240       nn = irc.from.nick
241     end
242     if !(s = @serv[sn]) or !(u = s[IRC::Address.normalize(nn)])
243       irc.reply "You're not identified." if irc
244       nil
245     else
246       (u == true) ? nn : u
247     end
248   end
250   # Get user data or report the error and fail. Same usage as above.
251   def get_user_data(irc, sn = nil)
252     sn = irc.server.name unless sn
253     if irc.kind_of?(String)
254       nn = irc
255       irc = nil
256     else
257       nn = irc.from.nick
258     end
259     if !(s = @serv[sn]) or !(u = s[IRC::Address.normalize(nn)])
260       irc.reply "You're not identified." if irc
261     else
262       rn = (u == true) ? nn : u
263       if !(ud = $config["servers/#{sn}/users/#{IRC::Address.normalize(rn)}"])
264         irc.reply "Error accessing user data. Get a hold of the bot owner." if irc
265       else
266         return ud, rn
267       end
268     end
269     nil
270   end
272   # Register a new user.
273   def cmd_register(irc, password)
274     if irc.channel
275       irc.reply 'This command can only be used in private.'
276     elsif !password or password.length == 0
277       irc.reply 'Empty password given. USAGE: register <password>.'
278     else
279       from = irc.from
280       map = $config.ensure("servers/#{irc.server.name}/users/#{from.nnick}")
281       if map.empty?
282         map['password'] = Digest::SHA1.hexdigest(password)
283         irc.reply "Ok, you've been registrated with password '#{password}'. Default authentication mode is 'trusted'."
284       else
285         irc.reply "You're already registrated, or someone else is with YOUR nick! *gasp*"
286       end
287     end
288   end
289   help :register, "Use 'register <password>' in a private message, to register yourself as a user with your current nick and hostmask."
291   # Identify a 'secure mode' user.
292   def cmd_identify(irc, line)
293     pass, nick = line ? line.split(' ', 2) : nil
294     nick = nil if nick and nick.length == 0
295     if irc.channel
296       irc.reply 'This command can only be used in private.'
297     elsif !line or line.length == 0
298       irc.reply 'Empty password given. USAGE: identify <password> [nick-name].'
299     elsif nick and !$config['irc/track-nicks']
300       irc.reply "The nick-name argument is only supported when nick-name tracking is enabled, which it isn't."
301     elsif !(u = $config["servers/#{sn = irc.server.name}/users/#{nnn = IRC::Address.normalize(nn = nick ? nick : irc.from.nick)}"])
302       irc.reply "Do I know you? You'll want to 'register' instead, pal."
303     else
304       if pass[0] == ?-
305         silent = true
306         pass = pass[1..-1]
307       else silent = false end
309       # Auto detect hashed passwords.
310       hashed_pw = false
311       if (p = u['password'])
312         if p.length == 40 and p =~ /^[0-9a-f]{40}$/
313           pass = Digest::SHA1.hexdigest pass
314           hashed_pw = true
315         end
316       end
318       # Check if the given password is correct.
319       if !p or p != pass
320         irc.reply 'Incorrect password given.'
321         return
322       end
324       # Auto hash password, if needed.
325       unless hashed_pw
326         u['password'] = Digest::SHA1.hexdigest pass
327         irc.reply "Your password was rehashed." unless silent
328       end
330       if (s = @serv[sn]) and s[nnn]
331         irc.reply "You're already identified, #{nn} :-)." unless silent
332       else
333         real_nn = irc.from.nnick
334         irc.server.channels.each_value do |chan|
335           if chan.users.include?(real_nn)
336             @serv[sn] = (s = {}) unless s
337             seen_user(irc, s, real_nn, nnn)
338             unless silent
339               usr_str = (real_nn == nnn) ? '' : "real nick: #{nn}, "
340               irc.reply "Alright, you're now identified (#{usr_str}using mask: #{irc.from.mask})!"
341             end
342             global_actions(irc, nnn)
343             return
344           end
345         end
346         irc.reply 'You need to join at least one channel I am on, before you identify.'
347       end
348     end
349   end
350   help :identify, "Identify yourself. This is needed if you have authentication mode set to 'manual', or no trusted authentication source is available. Also needed if your current hostmask doesn't match the ones in your list."
352   # Who am I?
353   def cmd_whoami(irc, line)
354     if (rn = get_nick(irc))
355       if IRC::Address.normalize(rn) == irc.from.nnick
356         irc.reply "Why you're \x02#{rn}\x0f of course, silly :-)."
357       else
358         irc.reply "Weeell.. looks like you're \x02#{rn}\x0f disguised as \x02#{irc.from.nick}\x0f, you sneaky thing you!"
359       end
360     end
361   end
362   help :whoami, 'Displays whether or not I know you, including if I am tracking you after a nick change.'
364   # Who are we all?
365   def chan_whoarewe(irc, chan, line)
366     if (s = @serv[irc.server.name])
367       nicks = []
368       s.each do |k,v|
369         nicks << ((v == true) ? k : "#{k} (#{v})") if chan.users.has_key? k
370       end
371       unless nicks.empty?
372         irc.reply "I know these people: #{nicks.join(', ')}."
373         return
374       end
375     end
376     irc.reply "I don't know anyone here :-("
377   end
378   help :whoarewe, 'Displays a list of all currently identified users in the channel.'
380   # Hostmasks for this user.
381   def cmd_hostmask(irc, line)
382     irc.reply "Your current hostmask is #{irc.from.mask}"
383   end
384   help :hostmask, 'Displays your current hostmask.'
386   def cmd_hostmasks(irc, line)
387     rn, ud = get_data(irc)
388     return unless ud
389     if (m = ud['masks'])
390       i = 0;
391       masks = m.map { |e| i += 1; "[#{i}] #{e}" }.join(', ')
392       nn_str = " #{rn}" if IRC::Address.normalize(rn) != irc.from.nnick
393       irc.reply "The following hostmasks are assigned to you#{nn_str}: #{masks}"
394     else
395       irc.reply "You have no hostmasks assigned to you."
396     end
397   end
398   help :hostmasks, 'For identified users, displays your list of allowed hostmasks.'
400   # Adding and removing ditto.
401   def cmd_addmask(irc, line)
402     rn, ud = get_data(irc)
403     return unless ud
404     m = ud['masks'] || (ud['masks'] = [])
405     mask = (line && !line.empty?) ? line : irc.from.mask
406     if m.include?(mask)
407       irc.reply "The hostmask #{mask} is already in your list."
408     else
409       m << mask
410       irc.reply "Hostmask #{mask} added to your list."
411     end
412   end
413   help :addmask, "Adds your current hostmask (use 'hostmask' to see it) to your current list of known hostmasks. Please be careful, as anyone matching your current hostmask will be allowed to identify as you after this (unless you have secure mode on)."
415   def cmd_delmask(irc, line)
416     unless line and !line.empty?
417       irc.reply "USAGE: delmask <number or mask>"
418     end
419     rn, ud = get_data(irc)
420     return unless ud
421     if (m = ud['masks'])
422       begin
423         i = Integer(line)
424         if i >= 1 and i <= m.length
425           mask = m.delete_at(i - 1)
426           irc.reply "Deleted hostmask #{i}: #{mask}"
427         else
428           irc.reply "Hostmask number is out of range! You have #{m.length} hostmasks."
429         end
430       rescue ArgumentError
431         if (mask = m.delete(line))
432           irc.reply "Deleted hostmask: #{mask}"
433         else
434           irc.reply "No such mask in your list."
435         end
436       end
437     else
438       irc.reply "You have no hostmasks!"
439     end
440   end
441   help :delmask, 'Deletes the given hostmask from your list of hostmasks.'
443   # Make me or someone else operator.
444   def chan_op(irc, chan, nick)
445     if !caps(irc, 'op', 'owner').any?
446       irc.reply "You don't have the 'op' capability for this channel; go away."
447     elsif !chan.me.op?
448       irc.reply "I don't appear to be operator on this channel, sorry."
449     elsif nick
450       if (nicks = nick.split).length == 1
451         if (u = chan.users[IRC::Address.normalize(nicks[0])])
452           if u.op?: irc.reply "Ehm, that person is already operator, dude."
453           else u.op end
454         else irc.reply "No such nick name." end
455       else
456         nicks.each { |n| u = chan.users[IRC::Address.normalize(n)] and !u.op? and u.op }
457       end
458     elsif (u = chan.users[irc.from.nnick])
459       if u.op?: irc.reply "Already there, bro."
460       else u.op end
461     end
462   end
463   help :op, 'Gives you, or a given nick, operator status on the channel.'
465   # Deop me or someone else.
466   def chan_deop(irc, chan, nick)
467     if !caps(irc, 'op', 'owner').any?
468       irc.reply "You don't have the 'op' capability for this channel; go away."
469     elsif !chan.me.op?
470       irc.reply "I don't appear to be operator on this channel, sorry."
471     elsif nick
472       if (nicks = nick.split).length == 1
473         if (u = chan.users[IRC::Address.normalize(nicks[0])])
474           if !u.op?: irc.reply "That person is already a commoner, don't waste my time."
475           else u.op(false) end
476         else irc.reply "No such nick name." end
477       else
478         nicks.each { |n| u = chan.users[IRC::Address.normalize(n)] and u.op? and u.op(false) }
479       end
480     elsif (u = chan.users[irc.from.nnick])
481       if !u.op?: irc.reply "Ok... done.  See?  No difference."
482       else u.op(false) end
483     end
484   end
485   help :deop, 'Removes operator status from you or the given nick.'
487   # Display your channel caps.
488   def chan_caps(irc, chan, line)
489     cap = {}
490     plugs = []
491     cmds = []
492     caps(irc) do |dir|
493       old = cap
494       cap = dir.reject { |k,v| !CapsList.include?(k) or !v }
495       cap.merge! old
496     end
497     irc.reply "Channel capabilities: #{cap.keys.join(', ')}."
498   end
499   help :caps, 'Displays your current capabilities for this channel.'
501   # Kicks a user.
502   def chan_kick(irc, chan, line)
503     n, r = line.split(' ', 2) if line
504     if !caps(irc, 'op', 'owner').any?
505       irc.reply "You're not allowed to do that."
506     elsif !chan.me.op?
507       irc.reply "I'm not operator. Can't do this."
508     elsif !line or !n
509       irc.reply "USAGE: kick <nick> [reason]"
510     elsif !chan.users[IRC::Address.normalize(n)]
511       irc.reply "No such nick, #{n}."
512     else
513       irc.server.cmd('KICK', chan.name, n, r || 'Chill')
514     end
515   end
516   help :kick, 'Kicks a user from the channel.'
518   # Kicks and bans a user.
519   # TODO: Allow for ban-mask control.
520   def chan_kban(irc, chan, line)
521     usage = "USAGE: kban <nick> [timeout] [reason]"
522     n, a1, a2 = line.split(' ', 3) if line
523     if !caps(irc, 'op', 'owner').any?
524       irc.reply "You're not allowed to do that."
525     elsif !chan.me.op?
526       irc.reply "I'm not operator. Can't do this."
527     elsif !line or !n
528       irc.reply usage
529     elsif !(u = chan.users[IRC::Address.normalize(n)])
530       irc.reply "No such nick, #{n}."
531     else
533       # Figure out arguments :-p.
534       time = 0
535       reason = 'Chill'
536       if a1: begin
537         time = Integer(a1)
538         reason = a2
539       rescue ArgumentError
540         reason = a1
541         if a2: begin
542           time = Integer(a2)
543         rescue ArgumentError
544           irc.reply usage
545         end end
546       end end
548       # Ban, then kick.
549       ban_mask = "*!*@#{u.host}"
550       irc.server.cmd('MODE', chan.name, '+b', ban_mask)
551       irc.server.cmd('KICK', chan.name, n, reason)
553       # Timeout to remove the ban, if needed.
554       if time > 0: Thread.new do
555         sleep(time)
556         irc.server.cmd('MODE', chan.name, '-b', ban_mask)
557       end end
559     end
560   end
561   help :kban, "Kicks and bans a user from the channel. Type 'kban <nick> [time] [reason]' to ban the person for [time] seconds, with an optional reason. If [time] is not given, or given as 0, the ban is permanent."
563   # Unbans a nick or mask.
564   def chan_unban(irc, chan, mask)
565     if !caps(irc, 'op', 'owner').any?
566       irc.reply "You're not allowed to do that."
567     elsif !chan.me.op?
568       irc.reply "I'm not operator. Can't do it."
569     elsif !mask or mask.empty?
570       irc.reply 'USAGE: unban <mask or nick>'
571     else
573       # Figure out type of mask.
574       if mask[0] == ?!
575         mask = "*#{mask}"
576       elsif mask[0] == ?@
577         mask = "*!*#{mask}"
578       end
580       # See if we have, and unban.
581       if !chan.bans.include? mask
582         irc.reply "I don't see that mask in the channel ban list, but I'll try anyway, just for you."
583       end
584       irc.server.cmd('MODE', chan.name, '-b', mask)
586     end
587   end
588   help :unban, 'Removes the given hostmask from the channel ban list.'
590   # Server command hook so we can watch NICKs.
591   def hook_command_serv(irc, handled, cmd, *args)
592     return unless (s = @serv[sn = irc.server.name])
593     case cmd
595     when 'NICK'
596       return unless (u = s[nn = IRC::Address.normalize(on = irc.from.nick)])
597       if $config['irc/track-nicks']
598         new_nn = IRC::Address.normalize(args[0])
599         return if new_nn == nn
600         if u == true: s[new_nn] = nn
601         else          s[new_nn] = (new_nn == u) ? true : u
602         end
603         $log.puts "User #{on} changed nick to #{args[0]} (tracking)."
604         s.delete(nn)
605       else
606         $log.puts "User #{on} has left (nick name change)."
607         irc.from.nick = new_nn
608         lost_user(irc, s, nn, u)
609         irc.from.nick = on
610       end
612     when 'QUIT'
613       return unless (u = s[nn = irc.from.nnick])
614       $log.puts "User #{nn} has left (quit)."
615       lost_user(irc, s, nn, u)
617     when 'KILL'
618       return unless (u = s[nn = IRC::Address.normalize(args[0])])
619       $log.puts "User #{nn} has left (kill)."
620       lost_user(irc, s, nn, u)
622     end
623   end
625   # Do stuff when we recognize a new user through JOIN or identify.
626   def join_actions(irc, real_nick, bot_join = false)
627     chan = irc.channel
628     nick = irc.from.nick
629     real_nick = nick unless real_nick
630     serv_name = irc.server.name
631     user_map = $config["servers/#{serv_name}/users/#{IRC::Address.normalize(real_nick)}"]
632     chan_map = $config["servers/#{serv_name}/channels/#{chan.name}"]
633     channel_actions(irc, user_map, chan_map, bot_join)
634   end
636   # Called to perform channel actions. Maps may be nil.
637   def channel_actions(irc, user_map, chan_map, bot_join = false)
639     # Operator or voice?
640     cap = caps(irc, 'op', 'voice', 'greet')
641     chan = irc.channel
642     nick = irc.from.nick
643     if chan_map
644       if chan_map['enforce'] or chan_map['promote']
645         if cap[0] and chan.me.op?
646           chan.op(nick)
647         end
648         if cap[1] and chan.me.op?
649           chan.voice(nick)
650         end
651       end
652     end
654     # Greeting?
655     if !bot_join and cap[2] and user_map and (g = user_map['channels']) and (g = g[chan.name]) and (g = g['greeting'])
656       chan.privmsg(g.gsub('%n', nick))
657     end
659   end
661   # Perform actions on 'global' identification. Nick names must be normalized.
662   def global_actions(irc, real_nick = nil, bot_join = false)
664     # Grab some maps.
665     server = irc.server
666     server_name = server.name
667     nick = irc.from.nnick
668     real_nick = nick unless real_nick
669     user_map = $config["servers/#{server_name}/users/#{real_nick}"]
670     chan_map = $config["servers/#{server_name}/channels"]
672     # Loop through channels, if applicable.
673     old_chan = irc.channel
674     server.channels.each do |chan_name, chan|
675       if chan.users[nick]
676         irc.channel = chan
677         channel_actions(irc, user_map, chan_map ? chan_map[chan_name] : nil, bot_join)
678       end
679     end
680     irc.channel = old_chan
682   end
684   # Auto-identify a joined user.
685   # FIXME: Make an internal one for speed-up of bot-join.
686   def auto_ident(irc, bot_join = false)
688     # Check if we know the user.
689     user = irc.from
690     nick_name = user.nick
691     nnick = IRC::Address.normalize(nick_name)
692     server = irc.server
693     server_name = server.name
694     if (s = @serv[server_name]) and (rn = s[nnick])
695       join_actions(irc, rn == true ? nnick : rn, bot_join)
697     elsif (users = $config["servers/#{server_name}/users/#{nnick}"])
699       auth = case users['auth']
700         when 'hostmask': 3
701         when 'manual': 1
702         else 2
703       end
705       # Hostmask ident?
706       if auth >= 3 and users['masks'].include_mask?(user.mask)
707         (@serv[server_name] = (s = {})) unless s
708         seen_user(irc, s, nnick)
709         $log.puts "User #{nick_name} has been recognized!"
710         join_actions(irc, nnick, bot_join)
711         return
712       end
714       # NickServ ident?
715       if auth >= 2 and (s = @serv_nswi[server_name])
716         s[3] = bot_join
717         server.cmd('WHOIS', nick_name)
718       end
720     end
721   end
723   # Called to add a user to the list of currently known users. Normalized nick names must be used.
724   def seen_user(irc, server_map, nick = nil, real_nick = nil)
725     nick = irc.from.nnick unless nick
726     if !real_nick or nick == real_nick
727       server_map[nick] = true
728       real_nick ||= nick
729     else
730       server_map[nick] = real_nick
731       server_map[real_nick] = true
732     end
733     @user_watch_hooks.each { |m| m.call(true, irc, real_nick) }
734   end
736   # Called to erase a user from the list of currently known users.
737   def lost_user(irc, server_map, nick = nil, user = nil)
738     nick = irc.from.nnick unless nick
739     user = server_map[nick] unless user
740     real_nick = user == true ? nick : user
741     @user_watch_hooks.each { |m| m.call(false, irc, real_nick) }
742     server_map.delete(nick)
743   end
745   # Called to initialize a new channel, after the user list and modes are fetched.
746   # FIXME: Wrapper? hmm.
747   def hook_init_chan(irc)
748     irc.channel.users.each do |nn, u|
749       irc.from = u
750       auto_ident(irc, true)
751     end
752   end
754   # Internal helper.
755   def part_or_kick(irc, nick)
756     return unless (s = @serv[(serv = irc.server).name]) and (u = s[nick])
757     serv.channels.each_value do |chan|
758       return if chan.users.keys.include?(nick)
759     end
760     lost_user(irc, s, nick, u)
761     $log.puts "User #{nick} has left."
762   end
764   # Channel command hook so we can watch JOIN, PART, KICK and QUITs.
765   def hook_command_chan(irc, handled, cmd, *args)
766     case cmd
768     when 'JOIN'
769       auto_ident(irc)
770     when 'PART'
771       part_or_kick(irc, irc.from.nnick)
772     when 'KICK'
773       part_or_kick(irc, IRC::Address.normalize(args[0]))
775     end
776   end
778   # To capture WHOIS output (for NickServ auto-ident).
779   # TODO/FIXME: Store in a map if the WHOIS was triggered by bot-join?
780   def hook_reply_serv(irc, code, *data)
781     if (s = @serv_nswi[sn = (server = irc.server).name])
783       # Only for un-indentified people.
784       nick_name = IRC::Address.normalize(data[0])
785       return if (serv = @serv[sn]) and serv[nick_name]
787       # Mask info.
788       if code == 311
789         s[1] = "#{data[0]}!#{data[1]}@#{data[2]}"
791       # Ident info.
792       elsif code == s[0]
793         s[2] = true
795       # End of who-is.
796       elsif code == 318
797         if s[2] and (mask = s[1])
798           if (users = $config["servers/#{sn}/users/#{nick_name}"])
799             (@serv[sn] = (serv = {})) unless serv
801             # We need to fake-up an Address...
802             old_from = irc.from
803             irc.from = IRC::Address.new(mask, server)
804             seen_user(irc, serv, nick_name)
805             $log.puts "User #{nick_name} has been recognized via NickServ WHOIS (server: #{sn})!"
806             global_actions(irc, nil, s[3])
807             irc.from = old_from
809           end
810         end
811         s[1] = nil
812         s[2] = false
813       end
815     end
816   end
818   # Check if a command can be executed.
819   def command_check(irc, plugin_name, command_name)
820     nplugin_name = "-#{plugin_name}"
821     ncommand_name = "-#{command_name}"
822     caps(irc) do |map|
823       if (m = map['commands'])
824         return if m.include? command_name
825         if m.include? ncommand_name
826           raise SecurityError.new("Error: You're not allowed to use the command '#{command_name}'.")
827         end
828       end
829       if (m = map['plugins'])
830         return if m.include? plugin_name
831         if m.include? nplugin_name
832           raise SecurityError.new("Error: You're not allowed to use commands from the plugin '#{plugin_name}'.")
833         end
834       end
835     end
836   end
838   # Retrieve the given capability flag for the user.
839   # We look in up to 5 places for the most local version:
840   #  1. servers/<name>/users/<name>/channels/<name>/<cap>
841   #  2. servers/<name>/users/<name>/<cap>
842   #  3. servers/<name>/channels/<name>/defaults/<cap>
843   #  4. servers/<name>/defaults/<cap>
844   #  5. irc/defaults/<cap>
845   #
846   # For non-channel caps, only steps 2, 4 and 5 are performed. Pass 'false' as
847   # the first argument after irc, to trigger this. Otherwise, if a block is given
848   # arguments are disregarded and each level map is yielded to the block.
849   #
850   def caps(irc, *cap)
852     # Prepare.
853     if cap[0] == false
854       skip_chan = true
855       cap.shift
856     else
857       ship_chan = false
858     end
859     res = block_given? ? nil : Array.new(cap.length)
861     # Walk to current server. This shouldn't fail.
862     cn = irc.channel and cn = cn.name
863     if (serv = $config["servers/#{sn = irc.server.name}"])
865       # Look for user settings if we're identified.
866       irc_user = irc.from
867       nick = irc_user.nnick
868       users = serv['users']
869       if (s = @serv[sn]) and (u = s[nick])
870         nick = u unless u == true
871       elsif users and (user = users[nick]) and
872         user['auth'] == 'hostmask' and user['masks'].include_mask?(irc_user.mask)
873         $log.puts "On-the-fly auth for user #{nick}."
874         u = true
875       end
877       # So… if the above checked out…
878       if u
880         # This shouldn't fail, since we're known.
881         if users and (user = users[nick])
883           # Look for channel settings for this user, if we need them at all.
884           if !skip_chan and cn and (chan = user['channels']) and (chan = chan[cn])
885             if block_given?
886               yield chan
887             else
888               cap.each_with_index { |e,i| res[i] = chan[e] if res[i].nil? }
889             end
890           end
892           # Look for server settings for this user.
893           if block_given?
894             yield user
895           else
896             cap.each_with_index { |e,i| res[i] = user[e] if res[i].nil? }
897           end
898         end
899       end
901       # Now look for channel settings, if needed.
902       if !skip_chan and cn and (chan = serv['channels']) and (chan = chan[cn]) and (chan = chan['defaults'])
903         if block_given?
904           yield chan
905         else
906           cap.each_with_index { |e,i| res[i] = chan[e] if res[i].nil? }
907         end
908       end
910       # And server settings.
911       if (c = serv['defaults'])
912         if block_given?
913           yield c
914         else
915           cap.each_with_index { |e,i| res[i] = c[e] if res[i].nil? }
916         end
917       end
918     end
920     # Finally, global settings.
921     unless (c = $config["irc/defaults"]).nil?
922       if block_given?
923         yield c
924       else
925         cap.each_with_index { |e,i| res[i] = c[e] if res[i].nil? }
926       end
927     end
928     (res.nil? or res.length > 1) ? res : res[0]
930   end