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