2 # CyBot - User management.
10 def include_mask?(str)
13 re = Regexp.new(e.gsub('.', '\.').gsub('*', '.*'))
17 return true if e == str
27 class User < PluginBase
29 # Settings that are considered capabilities.
30 CapsList = ['owner', 'admin', 'op', 'voice', 'greet']
32 # Called from other plugins.
39 class SecurityError < Exception
44 @brief_help = 'Manages registrated bot users.'
47 # TODO/FIXME: This should be updated by notifications.
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]
58 # Set global variable.
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 = []
70 :help => 'User may become channel operator.'
74 :help => 'User may gain voice.'
78 :help => 'User is bot owner.'
82 :help => 'User is bot administrator.'
86 :help => 'User may set a greeting.'
90 :help => 'Command filters.'
94 :help => 'Plugin filters.'
97 # Config space addition.
102 :help => 'Track nick name changes when identified.',
107 :help => 'Default user capabilities.',
109 'voice' => voice_com,
110 'owner' => owner_com,
111 'admin' => admin_com,
112 'greet' => greet_com,
113 'commands' => cmds_com,
114 'plugins' => plug_com
123 :help => 'Default capabilities for all users on this server.',
125 'voice' => voice_com,
126 'owner' => owner_com,
127 'admin' => admin_com,
128 'greet' => greet_com,
129 'commands' => cmds_com,
130 'plugins' => plug_com
133 :help => 'User list for this server.',
136 :help => 'User settings.',
139 'voice' => voice_com,
140 'owner' => owner_com,
141 'admin' => admin_com,
142 'greet' => greet_com,
143 'commands' => cmds_com,
144 'plugins' => plug_com,
147 :help => 'If set, time durations will be given in a more fuzzy format.'
150 "The security level used when authenticating the user. One of 'hostmask', 'trusted' and 'manual'.",
153 :help => 'User capabilities for various channels.',
157 'voice' => voice_com,
158 'greet' => greet_com,
159 'greeting' => "Greeting text for this channel. Ignored unless user has the 'greet' capability."
163 :help => "Host masks for this user.",
166 'password' => 'Password for identification.',
168 :help => 'User flags.',
170 :on_change => ConfigSpace.set_semantics(['secure', 'foo', 'bar'])
180 :help => 'Default user capabilities for this channel.',
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)
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
207 else (u == true) ? nn : u 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)
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)}"]]
220 user.reply "Do I know you? At least not right now."
224 sn = sn.server.name if sn.kind_of?(IrcWrapper)
225 $config["servers/#{sn}/users/#{IRC::Address.normalize(user)}"]
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)
240 if !(s = @serv[sn]) or !(u = s[IRC::Address.normalize(nn)])
241 irc.reply "You're not identified." if irc
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)
257 if !(s = @serv[sn]) or !(u = s[IRC::Address.normalize(nn)])
258 irc.reply "You're not identified." if irc
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
270 # Register a new user.
271 def cmd_register(irc, password)
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>.'
278 map = $config.ensure("servers/#{irc.server.name}/users/#{from.nnick}")
280 map['password'] = password
281 irc.reply "Ok, you've been registrated with password '#{password}'. Default authentication mode is 'trusted'."
283 irc.reply "You're already registrated, or someone else is with YOUR nick! *gasp*"
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
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."
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
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)
317 usr_str = (real_nn == nnn) ? '' : "real nick: #{nn}, "
318 irc.reply "Alright, you're now identified (#{usr_str}using mask: #{irc.from.mask})!"
320 global_actions(irc, nnn)
324 irc.reply 'You need to join at least one channel I am on, before you identify.'
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."
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 :-)."
336 irc.reply "Weeell.. looks like you're \x02#{rn}\x0f disguised as \x02#{irc.from.nick}\x0f, you sneaky thing you!"
340 help :whoami, 'Displays whether or not I know you, including if I am tracking you after a nick change.'
343 def chan_whoarewe(irc, chan, line)
344 if (s = @serv[irc.server.name])
347 nicks << ((v == true) ? k : "#{k} (#{v})") if chan.users.has_key? k
350 irc.reply "I know these people: #{nicks.join(', ')}."
354 irc.reply "I don't know anyone here :-("
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}"
362 help :hostmask, 'Displays your current hostmask.'
364 def cmd_hostmasks(irc, line)
365 rn, ud = get_data(irc)
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}"
373 irc.reply "You have no hostmasks assigned to you."
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)
382 m = ud['masks'] || (ud['masks'] = [])
383 mask = (line && !line.empty?) ? line : irc.from.mask
385 irc.reply "The hostmask #{mask} is already in your list."
388 irc.reply "Hostmask #{mask} added to your list."
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>"
397 rn, ud = get_data(irc)
402 if i >= 1 and i <= m.length
403 mask = m.delete_at(i - 1)
404 irc.reply "Deleted hostmask #{i}: #{mask}"
406 irc.reply "Hostmask number is out of range! You have #{m.length} hostmasks."
409 if (mask = m.delete(line))
410 irc.reply "Deleted hostmask: #{mask}"
412 irc.reply "No such mask in your list."
416 irc.reply "You have no hostmasks!"
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."
426 irc.reply "I don't appear to be operator on this channel, sorry."
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."
432 else irc.reply "No such nick name." end
434 nicks.each { |n| u = chan.users[IRC::Address.normalize(n)] and !u.op? and u.op }
436 elsif (u = chan.users[irc.from.nnick])
437 if u.op?: irc.reply "Already there, bro."
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."
448 irc.reply "I don't appear to be operator on this channel, sorry."
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."
454 else irc.reply "No such nick name." end
456 nicks.each { |n| u = chan.users[IRC::Address.normalize(n)] and u.op? and u.op(false) }
458 elsif (u = chan.users[irc.from.nnick])
459 if !u.op?: irc.reply "Ok... done. See? No difference."
463 help :deop, 'Removes operator status from you or the given nick.'
465 # Display your channel caps.
466 def chan_caps(irc, chan, line)
472 cap = dir.reject { |k,v| !CapsList.include?(k) or !v }
475 irc.reply "Channel capabilities: #{cap.keys.join(', ')}."
477 help :caps, 'Displays your current capabilities for this channel.'
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."
485 irc.reply "I'm not operator. Can't do this."
487 irc.reply "USAGE: kick <nick> [reason]"
488 elsif !chan.users[IRC::Address.normalize(n)]
489 irc.reply "No such nick, #{n}."
491 irc.server.cmd('KICK', chan.name, n, r || 'Chill')
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."
504 irc.reply "I'm not operator. Can't do this."
507 elsif !(u = chan.users[IRC::Address.normalize(n)])
508 irc.reply "No such nick, #{n}."
511 # Figure out arguments :-p.
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
534 irc.server.cmd('MODE', chan.name, '-b', ban_mask)
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."
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>'
551 # Figure out type of mask.
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."
562 irc.server.cmd('MODE', chan.name, '-b', mask)
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])
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
581 Kernel.puts "User #{on} changed nick to #{args[0]} (tracking)."
584 Kernel.puts "User #{on} has left (nick name change)."
585 irc.from.nick = new_nn
586 lost_user(irc, s, nn, u)
591 return unless (u = s[nn = irc.from.nnick])
592 Kernel.puts "User #{nn} has left (quit)."
593 lost_user(irc, s, nn, u)
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)
603 # Do stuff when we recognize a new user through JOIN or identify.
604 def join_actions(irc, real_nick, bot_join = false)
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)
614 # Called to perform channel actions. Maps may be nil.
615 def channel_actions(irc, user_map, chan_map, bot_join = false)
618 cap = caps(irc, 'op', 'voice', 'greet')
622 if chan_map['enforce'] or chan_map['promote']
623 if cap[0] and chan.me.op?
626 if cap[1] and chan.me.op?
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))
639 # Perform actions on 'global' identification. Nick names must be normalized.
640 def global_actions(irc, real_nick = nil, bot_join = false)
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|
655 channel_actions(irc, user_map, chan_map ? chan_map[chan_name] : nil, bot_join)
658 irc.channel = old_chan
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.
668 nick_name = user.nick
669 nnick = IRC::Address.normalize(nick_name)
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']
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)
693 if auth >= 2 and (s = @serv_nswi[server_name])
695 server.cmd('WHOIS', nick_name)
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
708 server_map[nick] = real_nick
709 server_map[real_nick] = true
711 @user_watch_hooks.each { |m| m.call(true, irc, real_nick) }
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)
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|
728 auto_ident(irc, true)
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)
738 lost_user(irc, s, nick, u)
739 Kernel.puts "User #{nick} has left."
742 # Channel command hook so we can watch JOIN, PART, KICK and QUITs.
743 def hook_command_chan(irc, handled, cmd, *args)
749 part_or_kick(irc, irc.from.nnick)
751 part_or_kick(irc, IRC::Address.normalize(args[0]))
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]
767 s[1] = "#{data[0]}!#{data[1]}@#{data[2]}"
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...
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])
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}"
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}'.")
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}'.")
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>
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.
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.
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}."
855 # So… if the above checked out…
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])
866 cap.each_with_index { |e,i| res[i] = chan[e] if res[i].nil? }
870 # Look for server settings for this user.
874 cap.each_with_index { |e,i| res[i] = user[e] if res[i].nil? }
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'])
884 cap.each_with_index { |e,i| res[i] = chan[e] if res[i].nil? }
888 # And server settings.
889 if (c = serv['defaults'])
893 cap.each_with_index { |e,i| res[i] = c[e] if res[i].nil? }
898 # Finally, global settings.
899 unless (c = $config["irc/defaults"]).nil?
903 cap.each_with_index { |e,i| res[i] = c[e] if res[i].nil? }
906 (res.nil? or res.length > 1) ? res : res[0]