2 # CyBot - User management.
12 def include_mask?(str)
15 re = Regexp.new(e.gsub('.', '\.').gsub('*', '.*'))
19 return true if e == str
29 class User < PluginBase
31 # Settings that are considered capabilities.
32 CapsList = ['owner', 'admin', 'op', 'voice', 'greet']
34 # Called from other plugins.
41 class SecurityError < Exception
46 @brief_help = 'Manages registrated bot users.'
49 # TODO/FIXME: This should be updated by notifications.
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]
60 # Set global variable.
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 = []
72 :help => 'User may become channel operator.'
76 :help => 'User may gain voice.'
80 :help => 'User is bot owner.'
84 :help => 'User is bot administrator.'
88 :help => 'User may set a greeting.'
92 :help => 'Command filters.'
96 :help => 'Plugin filters.'
99 # Config space addition.
104 :help => 'Track nick name changes when identified.',
109 :help => 'Default user capabilities.',
111 'voice' => voice_com,
112 'owner' => owner_com,
113 'admin' => admin_com,
114 'greet' => greet_com,
115 'commands' => cmds_com,
116 'plugins' => plug_com
125 :help => 'Default capabilities for all users on this server.',
127 'voice' => voice_com,
128 'owner' => owner_com,
129 'admin' => admin_com,
130 'greet' => greet_com,
131 'commands' => cmds_com,
132 'plugins' => plug_com
135 :help => 'User list for this server.',
138 :help => 'User settings.',
141 'voice' => voice_com,
142 'owner' => owner_com,
143 'admin' => admin_com,
144 'greet' => greet_com,
145 'commands' => cmds_com,
146 'plugins' => plug_com,
149 :help => 'If set, time durations will be given in a more fuzzy format.'
152 "The security level used when authenticating the user. One of 'hostmask', 'trusted' and 'manual'.",
155 :help => 'User capabilities for various channels.',
159 'voice' => voice_com,
160 'greet' => greet_com,
161 'greeting' => "Greeting text for this channel. Ignored unless user has the 'greet' capability."
165 :help => "Host masks for this user.",
168 'password' => 'Password for identification.',
170 :help => 'User flags.',
172 :on_change => ConfigSpace.set_semantics(['secure', 'foo', 'bar'])
182 :help => 'Default user capabilities for this channel.',
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)
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
209 else (u == true) ? nn : u 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)
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)}"]]
222 user.reply "Do I know you? At least not right now."
226 sn = sn.server.name if sn.kind_of?(IrcWrapper)
227 $config["servers/#{sn}/users/#{IRC::Address.normalize(user)}"]
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)
242 if !(s = @serv[sn]) or !(u = s[IRC::Address.normalize(nn)])
243 irc.reply "You're not identified." if irc
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)
259 if !(s = @serv[sn]) or !(u = s[IRC::Address.normalize(nn)])
260 irc.reply "You're not identified." if irc
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
272 # Register a new user.
273 def cmd_register(irc, password)
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>.'
280 map = $config.ensure("servers/#{irc.server.name}/users/#{from.nnick}")
282 map['password'] = Digest::SHA1.hexdigest(password)
283 irc.reply "Ok, you've been registrated with password '#{password}'. Default authentication mode is 'trusted'."
285 irc.reply "You're already registrated, or someone else is with YOUR nick! *gasp*"
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
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."
307 else silent = false end
309 # Auto detect hashed passwords.
311 if (p = u['password'])
312 if p.length == 40 and p =~ /^[0-9a-f]{40}$/
313 pass = Digest::SHA1.hexdigest pass
318 # Check if the given password is correct.
320 irc.reply 'Incorrect password given.'
324 # Auto hash password, if needed.
326 u['password'] = Digest::SHA1.hexdigest pass
327 irc.reply "Your password was rehashed." unless silent
330 if (s = @serv[sn]) and s[nnn]
331 irc.reply "You're already identified, #{nn} :-)." unless silent
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)
339 usr_str = (real_nn == nnn) ? '' : "real nick: #{nn}, "
340 irc.reply "Alright, you're now identified (#{usr_str}using mask: #{irc.from.mask})!"
342 global_actions(irc, nnn)
346 irc.reply 'You need to join at least one channel I am on, before you identify.'
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."
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 :-)."
358 irc.reply "Weeell.. looks like you're \x02#{rn}\x0f disguised as \x02#{irc.from.nick}\x0f, you sneaky thing you!"
362 help :whoami, 'Displays whether or not I know you, including if I am tracking you after a nick change.'
365 def chan_whoarewe(irc, chan, line)
366 if (s = @serv[irc.server.name])
369 nicks << ((v == true) ? k : "#{k} (#{v})") if chan.users.has_key? k
372 irc.reply "I know these people: #{nicks.join(', ')}."
376 irc.reply "I don't know anyone here :-("
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}"
384 help :hostmask, 'Displays your current hostmask.'
386 def cmd_hostmasks(irc, line)
387 rn, ud = get_data(irc)
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}"
395 irc.reply "You have no hostmasks assigned to you."
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)
404 m = ud['masks'] || (ud['masks'] = [])
405 mask = (line && !line.empty?) ? line : irc.from.mask
407 irc.reply "The hostmask #{mask} is already in your list."
410 irc.reply "Hostmask #{mask} added to your list."
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>"
419 rn, ud = get_data(irc)
424 if i >= 1 and i <= m.length
425 mask = m.delete_at(i - 1)
426 irc.reply "Deleted hostmask #{i}: #{mask}"
428 irc.reply "Hostmask number is out of range! You have #{m.length} hostmasks."
431 if (mask = m.delete(line))
432 irc.reply "Deleted hostmask: #{mask}"
434 irc.reply "No such mask in your list."
438 irc.reply "You have no hostmasks!"
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."
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 "Ehm, that person is already operator, dude."
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 }
458 elsif (u = chan.users[irc.from.nnick])
459 if u.op?: irc.reply "Already there, bro."
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."
470 irc.reply "I don't appear to be operator on this channel, sorry."
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."
476 else irc.reply "No such nick name." end
478 nicks.each { |n| u = chan.users[IRC::Address.normalize(n)] and u.op? and u.op(false) }
480 elsif (u = chan.users[irc.from.nnick])
481 if !u.op?: irc.reply "Ok... done. See? No difference."
485 help :deop, 'Removes operator status from you or the given nick.'
487 # Display your channel caps.
488 def chan_caps(irc, chan, line)
494 cap = dir.reject { |k,v| !CapsList.include?(k) or !v }
497 irc.reply "Channel capabilities: #{cap.keys.join(', ')}."
499 help :caps, 'Displays your current capabilities for this channel.'
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."
507 irc.reply "I'm not operator. Can't do this."
509 irc.reply "USAGE: kick <nick> [reason]"
510 elsif !chan.users[IRC::Address.normalize(n)]
511 irc.reply "No such nick, #{n}."
513 irc.server.cmd('KICK', chan.name, n, r || 'Chill')
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."
526 irc.reply "I'm not operator. Can't do this."
529 elsif !(u = chan.users[IRC::Address.normalize(n)])
530 irc.reply "No such nick, #{n}."
533 # Figure out arguments :-p.
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
556 irc.server.cmd('MODE', chan.name, '-b', ban_mask)
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."
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>'
573 # Figure out type of mask.
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."
584 irc.server.cmd('MODE', chan.name, '-b', mask)
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])
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
603 $log.puts "User #{on} changed nick to #{args[0]} (tracking)."
606 $log.puts "User #{on} has left (nick name change)."
607 irc.from.nick = new_nn
608 lost_user(irc, s, nn, u)
613 return unless (u = s[nn = irc.from.nnick])
614 $log.puts "User #{nn} has left (quit)."
615 lost_user(irc, s, nn, u)
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)
625 # Do stuff when we recognize a new user through JOIN or identify.
626 def join_actions(irc, real_nick, bot_join = false)
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)
636 # Called to perform channel actions. Maps may be nil.
637 def channel_actions(irc, user_map, chan_map, bot_join = false)
640 cap = caps(irc, 'op', 'voice', 'greet')
644 if chan_map['enforce'] or chan_map['promote']
645 if cap[0] and chan.me.op?
648 if cap[1] and chan.me.op?
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))
661 # Perform actions on 'global' identification. Nick names must be normalized.
662 def global_actions(irc, real_nick = nil, bot_join = false)
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|
677 channel_actions(irc, user_map, chan_map ? chan_map[chan_name] : nil, bot_join)
680 irc.channel = old_chan
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.
690 nick_name = user.nick
691 nnick = IRC::Address.normalize(nick_name)
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']
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)
715 if auth >= 2 and (s = @serv_nswi[server_name])
717 server.cmd('WHOIS', nick_name)
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
730 server_map[nick] = real_nick
731 server_map[real_nick] = true
733 @user_watch_hooks.each { |m| m.call(true, irc, real_nick) }
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)
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|
750 auto_ident(irc, true)
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)
760 lost_user(irc, s, nick, u)
761 $log.puts "User #{nick} has left."
764 # Channel command hook so we can watch JOIN, PART, KICK and QUITs.
765 def hook_command_chan(irc, handled, cmd, *args)
771 part_or_kick(irc, irc.from.nnick)
773 part_or_kick(irc, IRC::Address.normalize(args[0]))
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]
789 s[1] = "#{data[0]}!#{data[1]}@#{data[2]}"
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...
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])
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}"
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}'.")
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}'.")
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>
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.
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.
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}."
877 # So… if the above checked out…
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])
888 cap.each_with_index { |e,i| res[i] = chan[e] if res[i].nil? }
892 # Look for server settings for this user.
896 cap.each_with_index { |e,i| res[i] = user[e] if res[i].nil? }
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'])
906 cap.each_with_index { |e,i| res[i] = chan[e] if res[i].nil? }
910 # And server settings.
911 if (c = serv['defaults'])
915 cap.each_with_index { |e,i| res[i] = c[e] if res[i].nil? }
920 # Finally, global settings.
921 unless (c = $config["irc/defaults"]).nil?
925 cap.each_with_index { |e,i| res[i] = c[e] if res[i].nil? }
928 (res.nil? or res.length > 1) ? res : res[0]