evennia/src/commands/default/admin.py

587 lines
19 KiB
Python

"""
Admin commands
"""
import time
import re
from django.conf import settings
from django.contrib.auth.models import User
from src.server.sessionhandler import SESSIONS
from src.server.models import ServerConfig
from src.utils import utils, prettytable, search
from src.commands.default.muxcommand import MuxCommand
PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
# limit members for API inclusion
__all__ = ("CmdBoot", "CmdBan", "CmdUnban", "CmdDelPlayer",
"CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall")
class CmdBoot(MuxCommand):
"""
@boot
Usage
@boot[/switches] <player obj> [: reason]
Switches:
quiet - Silently boot without informing player
port - boot by port number instead of name or dbref
Boot a player object from the server. If a reason is
supplied it will be echoed to the user unless /quiet is set.
"""
key = "@boot"
locks = "cmd:perm(boot) or perm(Wizards)"
help_category = "Admin"
def func(self):
"Implementing the function"
caller = self.caller
args = self.args
if not args:
caller.msg("Usage: @boot[/switches] <player> [:reason]")
return
if ':' in args:
args, reason = [a.strip() for a in args.split(':', 1)]
else:
args, reason = args, ""
boot_list = []
if 'port' in self.switches:
# Boot a particular port.
sessions = SESSIONS.get_session_list(True)
for sess in sessions:
# Find the session with the matching port number.
if sess.getClientAddress()[1] == int(args):
boot_list.append(sess)
break
else:
# Boot by player object
pobj = search.player_search(args)
if not pobj:
self.caller("Player %s was not found." % pobj.key)
return
pobj = pobj[0]
if not pobj.access(caller, 'boot'):
string = "You don't have the permission to boot %s."
pobj.msg(string)
return
# we have a bootable object with a connected user
matches = SESSIONS.sessions_from_player(pobj)
for match in matches:
boot_list.append(match)
if not boot_list:
caller.msg("No matching sessions found. The Player does not seem to be online.")
return
# Carry out the booting of the sessions in the boot list.
feedback = None
if not 'quiet' in self.switches:
feedback = "You have been disconnected by %s.\n" % caller.name
if reason:
feedback += "\nReason given: %s" % reason
for session in boot_list:
session.msg(feedback)
pobj.disconnect_session_from_player(session.sessid)
# regex matching IP addresses with wildcards, eg. 233.122.4.*
IPREGEX = re.compile(r"[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}")
def list_bans(banlist):
"""
Helper function to display a list of active bans. Input argument
is the banlist read into the two commands @ban and @unban below.
"""
if not banlist:
return "No active bans were found."
table = prettytable.PrettyTable(["{wid", "{wname/ip", "{wdate", "{wreason"])
for inum, ban in enumerate(banlist):
table.add_row([str(inum + 1),
ban[0] and ban[0] or ban[1],
ban[3], ban[4]])
string = "{wActive bans:{n\n%s" % table
return string
class CmdBan(MuxCommand):
"""
ban a player from the server
Usage:
@ban [<name or ip> [: reason]]
Without any arguments, shows numbered list of active bans.
This command bans a user from accessing the game. Supply an
optional reason to be able to later remember why the ban was put in
place
It is often to
prefer over deleting a player with @delplayer. If banned by name,
that player account can no longer be logged into.
IP (Internet Protocol) address banning allows to block all access
from a specific address or subnet. Use the asterisk (*) as a
wildcard.
Examples:
@ban thomas - ban account 'thomas'
@ban/ip 134.233.2.111 - ban specific ip address
@ban/ip 134.233.2.* - ban all in a subnet
@ban/ip 134.233.*.* - even wider ban
A single IP filter is easy to circumvent by changing the computer
(also, some ISPs assign only temporary IPs to their users in the
first placer. Widening the IP block filter with wildcards might be
tempting, but remember that blocking too much may accidentally
also block innocent users connecting from the same country and
region.
"""
key = "@ban"
aliases = ["@bans"]
locks = "cmd:perm(ban) or perm(Immortals)"
help_category = "Admin"
def func(self):
"""
Bans are stored in a serverconf db object as a list of
dictionaries:
[ (name, ip, ipregex, date, reason),
(name, ip, ipregex, date, reason),... ]
where name and ip are set by the user and are shown in
lists. ipregex is a converted form of ip where the * is
replaced by an appropriate regex pattern for fast
matching. date is the time stamp the ban was instigated and
'reason' is any optional info given to the command. Unset
values in each tuple is set to the empty string.
"""
banlist = ServerConfig.objects.conf('server_bans')
if not banlist:
banlist = []
if not self.args or (self.switches
and not any(switch in ('ip', 'name')
for switch in self.switches)):
self.caller.msg(list_bans(banlist))
return
now = time.ctime()
reason = ""
if ':' in self.args:
ban, reason = self.args.rsplit(':', 1)
else:
ban = self.args
ban = ban.lower()
ipban = IPREGEX.findall(ban)
if not ipban:
# store as name
typ = "Name"
bantup = (ban, "", "", now, reason)
else:
# an ip address.
typ = "IP"
ban = ipban[0]
# replace * with regex form and compile it
ipregex = ban.replace('.', '\.')
ipregex = ipregex.replace('*', '[0-9]{1,3}')
#print "regex:",ipregex
ipregex = re.compile(r"%s" % ipregex)
bantup = ("", ban, ipregex, now, reason)
# save updated banlist
banlist.append(bantup)
ServerConfig.objects.conf('server_bans', banlist)
self.caller.msg("%s-Ban {w%s{x was added." % (typ, ban))
class CmdUnban(MuxCommand):
"""
remove a ban
Usage:
@unban <banid>
This will clear a player name/ip ban previously set with the @ban
command. Use this command without an argument to view a numbered
list of bans. Use the numbers in this list to select which one to
unban.
"""
key = "@unban"
locks = "cmd:perm(unban) or perm(Immortals)"
help_category = "Admin"
def func(self):
"Implement unbanning"
banlist = ServerConfig.objects.conf('server_bans')
if not self.args:
self.caller.msg(list_bans(banlist))
return
try:
num = int(self.args)
except Exception:
self.caller.msg("You must supply a valid ban id to clear.")
return
if not banlist:
self.caller.msg("There are no bans to clear.")
elif not (0 < num < len(banlist) + 1):
self.caller.msg("Ban id {w%s{x was not found." % self.args)
else:
# all is ok, clear ban
ban = banlist[num - 1]
del banlist[num - 1]
ServerConfig.objects.conf('server_bans', banlist)
self.caller.msg("Cleared ban %s: %s" %
(num, " ".join([s for s in ban[:2]])))
class CmdDelPlayer(MuxCommand):
"""
delplayer - delete player from server
Usage:
@delplayer[/switch] <name> [: reason]
Switch:
delobj - also delete the player's currently
assigned in-game object.
Completely deletes a user from the server database,
making their nick and e-mail again available.
"""
key = "@delplayer"
locks = "cmd:perm(delplayer) or perm(Immortals)"
help_category = "Admin"
def func(self):
"Implements the command."
caller = self.caller
args = self.args
if hasattr(caller, 'player'):
caller = caller.player
if not args:
self.msg("Usage: @delplayer[/delobj] <player/user name or #id> [: reason]")
return
reason = ""
if ':' in args:
args, reason = [arg.strip() for arg in args.split(':', 1)]
# We use player_search since we want to be sure to find also players
# that lack characters.
players = caller.search_player(args, quiet=True)
if not players:
# try to find a user instead of a Player
try:
user = User.objects.get(id=args)
except Exception:
try:
user = User.objects.get(username__iexact=args)
except Exception:
string = "No Player nor User found matching '%s'." % args
self.msg(string)
return
if user and not user.access(caller, 'delete'):
string = "You don't have the permissions to delete this player."
self.msg(string)
return
string = ""
name = user.username
user.delete()
if user:
name = user.name
user.delete()
string = "Player %s was deleted." % name
else:
string += "The User %s was deleted. It had no Player associated with it." % name
self.msg(string)
return
elif utils.is_iter(players):
string = "There were multiple matches:"
for user in players:
string += "\n %s %s" % (user.id, user.key)
return
else:
# one single match
user = players
user = user.user
if not user.access(caller, 'delete'):
string = "You don't have the permissions to delete that player."
self.msg(string)
return
uname = user.username
# boot the player then delete
self.msg("Informing and disconnecting player ...")
string = "\nYour account '%s' is being *permanently* deleted.\n" % uname
if reason:
string += " Reason given:\n '%s'" % reason
user.unpuppet_all()
for session in SESSIONS.sessions_from_player(user):
user.msg(string, sessid=session.sessid)
user.disconnect_session_from_player(session.sessid)
user.delete()
user.delete()
self.msg("Player %s was successfully deleted." % uname)
class CmdEmit(MuxCommand):
"""
@emit
Usage:
@emit[/switches] [<obj>, <obj>, ... =] <message>
@remit [<obj>, <obj>, ... =] <message>
@pemit [<obj>, <obj>, ... =] <message>
Switches:
room : limit emits to rooms only (default)
players : limit emits to players only
contents : send to the contents of matched objects too
Emits a message to the selected objects or to
your immediate surroundings. If the object is a room,
send to its contents. @remit and @pemit are just
limited forms of @emit, for sending to rooms and
to players respectively.
"""
key = "@emit"
aliases = ["@pemit", "@remit"]
locks = "cmd:perm(emit) or perm(Builders)"
help_category = "Admin"
def func(self):
"Implement the command"
caller = self.caller
args = self.args
if not args:
string = "Usage: "
string += "\n@emit[/switches] [<obj>, <obj>, ... =] <message>"
string += "\n@remit [<obj>, <obj>, ... =] <message>"
string += "\n@pemit [<obj>, <obj>, ... =] <message>"
caller.msg(string)
return
rooms_only = 'rooms' in self.switches
players_only = 'players' in self.switches
send_to_contents = 'contents' in self.switches
# we check which command was used to force the switches
if self.cmdstring == '@remit':
rooms_only = True
elif self.cmdstring == '@pemit':
players_only = True
if not self.rhs:
message = self.args
objnames = [caller.location.key]
else:
message = self.rhs
objnames = self.lhslist
# send to all objects
for objname in objnames:
obj = caller.search(objname, global_search=True)
if not obj:
return
if rooms_only and not obj.location is None:
caller.msg("%s is not a room. Ignored." % objname)
continue
if players_only and not obj.has_player:
caller.msg("%s has no active player. Ignored." % objname)
continue
if obj.access(caller, 'tell'):
obj.msg(message)
if send_to_contents and hasattr(obj, "msg_contents"):
obj.msg_contents(message)
caller.msg("Emitted to %s and its contents." % objname)
else:
caller.msg("Emitted to %s." % objname)
else:
caller.msg("You are not allowed to emit to %s." % objname)
class CmdNewPassword(MuxCommand):
"""
@userpassword
Usage:
@userpassword <user obj> = <new password>
Set a player's password.
"""
key = "@userpassword"
locks = "cmd:perm(newpassword) or perm(Wizards)"
help_category = "Admin"
def func(self):
"Implement the function."
caller = self.caller
if not self.rhs:
self.msg("Usage: @userpassword <user obj> = <new password>")
return
# the player search also matches 'me' etc.
player = caller.search_player(self.lhs)
if not player:
return
player.user.set_password(self.rhs)
player.user.save()
self.msg("%s - new password set to '%s'." % (player.name, self.rhs))
if player.character != caller:
player.msg("%s has changed your password to '%s'." % (caller.name,
self.rhs))
class CmdPerm(MuxCommand):
"""
@perm - set permissions
Usage:
@perm[/switch] <object> [= <permission>[,<permission>,...]]
@perm[/switch] *<player> [= <permission>[,<permission>,...]]
Switches:
del : delete the given permission from <object> or <player>.
player : set permission on a player (same as adding * to name)
This command sets/clears individual permission strings on an object
or player. If no permission is given, list all permissions on <object>.
"""
key = "@perm"
aliases = "@setperm"
locks = "cmd:perm(perm) or perm(Immortals)"
help_category = "Admin"
def func(self):
"Implement function"
caller = self.caller
switches = self.switches
lhs, rhs = self.lhs, self.rhs
if not self.args:
string = "Usage: @perm[/switch] object [ = permission, permission, ...]"
caller.msg(string)
return
playermode = 'player' in self.switches or lhs.startswith('*')
if playermode:
obj = caller.search_player(lhs)
else:
obj = caller.search(lhs, global_search=True)
if not obj:
return
if not rhs:
if not obj.access(caller, 'examine'):
caller.msg("You are not allowed to examine this object.")
return
string = "Permissions on {w%s{n: " % obj.key
if not obj.permissions.all():
string += "<None>"
else:
string += ", ".join(obj.permissions.all())
if (hasattr(obj, 'player') and
hasattr(obj.player, 'is_superuser') and
obj.player.is_superuser):
string += "\n(... but this object is currently controlled by a SUPERUSER! "
string += "All access checks are passed automatically.)"
caller.msg(string)
return
# we supplied an argument on the form obj = perm
if not obj.access(caller, 'control'):
caller.msg("You are not allowed to edit this object's permissions.")
return
cstring = ""
tstring = ""
if 'del' in switches:
# delete the given permission(s) from object.
obj.permissions.remove(self.rhslist)
if obj.permissions.get(self.rhslist):
cstring += "\nPermissions(s) %s could not be removed from %s." % (", ".join(self.rhslist), obj.name)
else:
cstring += "\nPermission(s) %s removed from %s (if they existed)." % (", ".join(self.rhslist), obj.name)
tstring += "\n%s revokes the permission(s) %s from you." % (caller.name, ", ".join(self.rhslist))
else:
# add a new permission
permissions = obj.permissions.all()
for perm in self.rhslist:
# don't allow to set a permission higher in the hierarchy than
# the one the caller has (to prevent self-escalation)
if (perm.lower() in PERMISSION_HIERARCHY and not
obj.locks.check_lockstring(caller, "dummy:perm(%s)" % perm)):
caller.msg("You cannot assign a permission higher than the one you have yourself.")
return
if perm in permissions:
cstring += "\nPermission '%s' is already defined on %s." % (rhs, obj.name)
else:
obj.permissions.add(perm)
cstring += "\nPermission '%s' given to %s." % (rhs, obj.name)
tstring += "\n%s gives you the permission '%s'." % (caller.name, rhs)
caller.msg(cstring.strip())
if tstring:
obj.msg(tstring.strip())
class CmdWall(MuxCommand):
"""
@wall
Usage:
@wall <message>
Announces a message to all connected players.
"""
key = "@wall"
locks = "cmd:perm(wall) or perm(Wizards)"
help_category = "Admin"
def func(self):
"Implements command"
if not self.args:
self.caller.msg("Usage: @wall <message>")
return
message = "%s shouts \"%s\"" % (self.caller.name, self.args)
self.msg("Announcing to all connected players ...")
SESSIONS.announce_all(message)