""" 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] [: 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] [: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 [ [: 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 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] [: 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] [: 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] [, , ... =] @remit [, , ... =] @pemit [, , ... =] 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] [, , ... =] " string += "\n@remit [, , ... =] " string += "\n@pemit [, , ... =] " 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 = 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 = ") 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] [= [,,...]] @perm[/switch] * [= [,,...]] Switches: del : delete the given permission from or . 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 . """ 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 += "" 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 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 ") return message = "%s shouts \"%s\"" % (self.caller.name, self.args) self.msg("Announcing to all connected players ...") SESSIONS.announce_all(message)