mirror of
https://github.com/evennia/evennia.git
synced 2026-03-18 13:56:30 +01:00
587 lines
19 KiB
Python
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)
|