diff --git a/src/commands/default/comms.py b/src/commands/default/comms.py index f77b7ad272..b69d898f6e 100644 --- a/src/commands/default/comms.py +++ b/src/commands/default/comms.py @@ -422,7 +422,7 @@ class CmdCemit(MuxPlayerCommand): key = "@cemit" aliases = ["@cmsg"] - locks = "cmd: not pperm(channel_banned)" + locks = "cmd: not pperm(channel_banned) and pperm(Players)" help_category = "Comms" def func(self): @@ -498,7 +498,7 @@ class CmdChannelCreate(MuxPlayerCommand): key = "@ccreate" aliases = "channelcreate" - locks = "cmd:not pperm(channel_banned)" + locks = "cmd:not pperm(channel_banned) and pperm(Players)" help_category = "Comms" def func(self): diff --git a/src/commands/default/player.py b/src/commands/default/player.py index c0c0980384..b50c3639e7 100644 --- a/src/commands/default/player.py +++ b/src/commands/default/player.py @@ -162,7 +162,7 @@ class CmdCharCreate(MuxPlayerCommand): if you want. """ key = "@charcreate" - locks = "cmd:all()" + locks = "cmd:pperm(Players)" help_category = "General" def func(self): @@ -285,7 +285,7 @@ class CmdOOC(MuxPlayerCommand): key = "@ooc" # lock must be all(), for different puppeted objects to access it. - locks = "cmd:all()" + locks = "cmd:pperm(Players)" aliases = "@unpuppet" help_category = "General" @@ -491,7 +491,7 @@ class CmdPassword(MuxPlayerCommand): Changes your password. Make sure to pick a safe one. """ key = "@password" - locks = "cmd:all()" + locks = "cmd:pperm(Players)" def func(self): "hook function." @@ -650,7 +650,7 @@ class CmdQuell(MuxPlayerCommand): key = "@quell" aliases = ["@unquell"] - locks = "cmd:all()" + locks = "cmd:pperm(Players)" help_category = "General" def _recache_locks(self, player): diff --git a/src/commands/default/unloggedin.py b/src/commands/default/unloggedin.py index 04711d6975..a516ae22a7 100644 --- a/src/commands/default/unloggedin.py +++ b/src/commands/default/unloggedin.py @@ -2,6 +2,7 @@ Commands that are available from the connect screen. """ import re +from random import getrandbits import traceback from django.conf import settings from src.players.models import PlayerDB @@ -59,8 +60,44 @@ class CmdUnconnectedConnect(MuxCommand): # extract quoted parts parts = [part.strip() for part in re.split(r"\"|\'", args) if part.strip()] if len(parts) == 1: - # this was (hopefully) due to no quotes being found + # this was (hopefully) due to no quotes being found, or a guest login parts = parts[0].split(None, 1) + # Guest login + if len(parts) == 1 and parts[0].lower() == "guest" and settings.GUEST_ENABLED: + try: + # Find an available guest name. + for playername in settings.GUEST_LIST: + if not PlayerDB.objects.filter(username__iexact=playername): + break + playername = None + if playername == None: + session.msg("All guest accounts are in use. Please try again later.") + return + + password = "%016x" % getrandbits(64) + home = ObjectDB.objects.get_id(settings.GUEST_HOME) + permissions = settings.PERMISSION_GUEST_DEFAULT + typeclass = settings.BASE_CHARACTER_TYPECLASS + ptypeclass = settings.BASE_GUEST_TYPECLASS + start_location = ObjectDB.objects.get_id(settings.GUEST_START_LOCATION) + + new_player = CreatePlayer(session, playername, password, + home, permissions, ptypeclass) + if new_player: + CreateCharacter(session, new_player, typeclass, start_location, + home, permissions) + session.sessionhandler.login(session, new_player) + + except Exception: + # We are in the middle between logged in and -not, so we have + # to handle tracebacks ourselves at this point. If we don't, + # we won't see any errors at all. + string = "%s\nThis is a bug. Please e-mail an admin if the problem persists." + session.msg(string % (traceback.format_exc())) + logger.log_errmsg(traceback.format_exc()) + finally: + return + if len(parts) != 2: session.msg("\n\r Usage (without <>): connect ") return @@ -151,6 +188,11 @@ class CmdUnconnectedCreate(MuxCommand): # player already exists (we also ignore capitalization here) session.msg("Sorry, there is already a player with the name '%s'." % playername) return + # Reserve playernames found in GUEST_LIST + if settings.GUEST_LIST and playername.lower() in map(str.lower, settings.GUEST_LIST): + string = "\n\r That name is reserved. Please choose another Playername." + session.msg(string) + return if not re.findall('^[\w. @+-]+$', password) or not (3 < len(password)): string = "\n\r Password should be longer than 3 characers. Letters, spaces, digits and @\.\+\-\_ only." string += "\nFor best security, make it longer than 8 characters. You can also use a phrase of" @@ -173,63 +215,22 @@ class CmdUnconnectedCreate(MuxCommand): # everything's ok. Create the new player account. try: default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) - - typeclass = settings.BASE_CHARACTER_TYPECLASS permissions = settings.PERMISSION_PLAYER_DEFAULT - - try: - new_player = create.create_player(playername, None, password, - permissions=permissions) - - except Exception, e: - session.msg("There was an error creating the default Player/Character:\n%s\n If this problem persists, contact an admin." % e) - logger.log_trace() - return - - # This needs to be called so the engine knows this player is - # logging in for the first time. (so it knows to call the right - # hooks during login later) - utils.init_new_player(new_player) - - # join the new player to the public channel - pchanneldef = settings.CHANNEL_PUBLIC - if pchanneldef: - pchannel = ChannelDB.objects.get_channel(pchanneldef[0]) - if not pchannel.connect(new_player): - string = "New player '%s' could not connect to public channel!" % new_player.key - logger.log_errmsg(string) - - if MULTISESSION_MODE < 2: - # if we only allow one character, create one with the same name as Player - # (in mode 2, the character must be created manually once logging in) - start_location = ObjectDB.objects.get_id(settings.START_LOCATION) - if not start_location: - start_location = default_home # fallback - - new_character = create.create_object(typeclass, key=playername, - location=start_location, home=default_home, - permissions=permissions) - # set playable character list - new_player.db._playable_characters.append(new_character) - - # allow only the character itself and the player to puppet this character (and Immortals). - new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" % - (new_character.id, new_player.id)) - - # If no description is set, set a default description - if not new_character.db.desc: - new_character.db.desc = "This is a Player." - # We need to set this to have @ic auto-connect to this character - new_player.db._last_puppet = new_character - - # tell the caller everything went well. - string = "A new account '%s' was created. Welcome!" - if " " in playername: - string += "\n\nYou can now log in with the command 'connect \"%s\" '." - else: - string += "\n\nYou can now log with the command 'connect %s '." - session.msg(string % (playername, playername)) - + typeclass = settings.BASE_CHARACTER_TYPECLASS + new_player = CreatePlayer(session, playername, password, default_home, permissions) + start_location = ObjectDB.objects.get_id(settings.START_LOCATION) + if new_player: + if MULTISESSION_MODE < 2: + CreateCharacter(session, new_player, typeclass, start_location, + default_home, permissions) + # tell the caller everything went well. + string = "A new account '%s' was created. Welcome!" + if " " in playername: + string += "\n\nYou can now log in with the command 'connect \"%s\" '." + else: + string += "\n\nYou can now log with the command 'connect %s '." + session.msg(string % (playername, playername)) + except Exception: # We are in the middle between logged in and -not, so we have # to handle tracebacks ourselves at this point. If we don't, @@ -331,3 +332,62 @@ To login to the system, you need to do one of the following: You can use the {wlook{n command if you want to see the connect screen again. """ self.caller.msg(string) + + +def CreatePlayer(session, playername, password, + default_home, permissions, typeclass=None): + """ + Creates a player of the specified typeclass. + """ + try: + new_player = create.create_player(playername, None, password, + permissions=permissions, typeclass=typeclass) + + except Exception, e: + session.msg("There was an error creating the Player:\n%s\n If this problem persists, contact an admin." % e) + logger.log_trace() + return False + + # This needs to be called so the engine knows this player is + # logging in for the first time. (so it knows to call the right + # hooks during login later) + utils.init_new_player(new_player) + + # join the new player to the public channel + pchanneldef = settings.CHANNEL_PUBLIC + if pchanneldef: + pchannel = ChannelDB.objects.get_channel(pchanneldef[0]) + if not pchannel.connect(new_player): + string = "New player '%s' could not connect to public channel!" % new_player.key + logger.log_errmsg(string) + return new_player + + +def CreateCharacter(session, new_player, typeclass, start_location, home, permissions): + """ + Creates a character based on a player's name. This is meant for Guest and + MULTISESSION_MODE <2 situations. + """ + try: + if not start_location: + start_location = home # fallback + new_character = create.create_object(typeclass, key=new_player.key, + location=start_location, home=home, + permissions=permissions) + # set playable character list + new_player.db._playable_characters.append(new_character) + + # allow only the character itself and the player to puppet this character (and Immortals). + new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" % + (new_character.id, new_player.id)) + + # If no description is set, set a default description + if not new_character.db.desc: + new_character.db.desc = "This is a Player." + # We need to set this to have @ic auto-connect to this character + new_player.db._last_puppet = new_character + except Exception, e: + session.msg("There was an error creating the Character:\n%s\n If this problem persists, contact an admin." % e) + logger.log_trace() + return False + diff --git a/src/players/player.py b/src/players/player.py index 9935a6c6fe..f303804c18 100644 --- a/src/players/player.py +++ b/src/players/player.py @@ -423,3 +423,43 @@ class Player(TypeClass): (i.e. not for a restart). """ pass + +class Guest(Player): + """ + This class is used for guest logins. Unlike Players, Guests and their + characters are deleted after disconnection. + """ + def at_post_login(self, sessid=None): + """ + In theory, guests only have one character regardless of which + MULTISESSION_MODE we're in. They don't get a choice. + """ + self._send_to_connect_channel("{G%s connected{n" % self.key) + self.execute_cmd("@ic", sessid=sessid) + + def at_disconnect(self): + """ + A Guest's characters aren't meant to linger on the server. When a + Guest disconnects, we remove its character. + """ + super(Guest, self).at_disconnect() + characters = self.db._playable_characters + for character in filter(None, characters): + character.delete() + + def at_server_shutdown(self): + """ + We repeat at_disconnect() here just to be on the safe side. + """ + super(Guest, self).at_server_shutdown() + characters = self.db._playable_characters + for character in filter(None, characters): + character.delete() + + def at_post_disconnect(self): + """ + Guests aren't meant to linger on the server, either. We need to wait + until after the Guest disconnects to delete it, though. + """ + super(Guest, self).at_post_disconnect() + self.delete() diff --git a/src/server/server.py b/src/server/server.py index 5e7166ce1e..42c2bafa0b 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -73,6 +73,8 @@ AMP_INTERFACE = settings.AMP_INTERFACE WEBSERVER_PORTS = settings.WEBSERVER_PORTS WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES +GUEST_ENABLED = settings.GUEST_ENABLED + # server-channel mappings WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES IMC2_ENABLED = settings.IMC2_ENABLED @@ -240,17 +242,16 @@ class Evennia(object): from src.scripts.tickerhandler import TICKER_HANDLER TICKER_HANDLER.restore() - if SERVER_STARTSTOP_MODULE: - # call correct server hook based on start file value - if mode in ('True', 'reload'): - # True was the old reload flag, kept for compatibilty - SERVER_STARTSTOP_MODULE.at_server_reload_start() - elif mode in ('reset', 'shutdown'): - SERVER_STARTSTOP_MODULE.at_server_cold_start() - # clear eventual lingering session storages - ObjectDB.objects.clear_all_sessids() - # always call this regardless of start type - SERVER_STARTSTOP_MODULE.at_server_start() + # call correct server hook based on start file value + if mode in ('True', 'reload'): + # True was the old reload flag, kept for compatibilty + self.at_server_reload_start() + elif mode in ('reset', 'shutdown'): + self.at_server_cold_start() + # clear eventual lingering session storages + ObjectDB.objects.clear_all_sessids() + # always call this regardless of start type + self.at_server_start() def set_restart_mode(self, mode=None): """ @@ -316,8 +317,7 @@ class Evennia(object): from src.scripts.tickerhandler import TICKER_HANDLER TICKER_HANDLER.save() - if SERVER_STARTSTOP_MODULE: - SERVER_STARTSTOP_MODULE.at_server_reload_stop() + self.at_server_reload_stop() else: if mode == 'reset': @@ -339,15 +339,13 @@ class Evennia(object): yield ObjectDB.objects.clear_all_sessids() ServerConfig.objects.conf("server_restart_mode", "reset") - if SERVER_STARTSTOP_MODULE: - SERVER_STARTSTOP_MODULE.at_server_cold_stop() + self.at_server_cold_stop() # stopping time from src.utils import gametime gametime.save() - if SERVER_STARTSTOP_MODULE: - SERVER_STARTSTOP_MODULE.at_server_stop() + self.at_server_stop() # if _reactor_stopping is true, reactor does not need to # be stopped again. if os.name == 'nt' and os.path.exists(SERVER_PIDFILE): @@ -358,6 +356,62 @@ class Evennia(object): # flag to avoid loops. self.shutdown_complete = True reactor.callLater(0, reactor.stop) + + # server start/stop hooks + + def at_server_start(self): + """ + This is called every time the server starts up, regardless of + how it was shut down. + """ + if SERVER_STARTSTOP_MODULE: + SERVER_STARTSTOP_MODULE.at_server_start() + + + def at_server_stop(self): + """ + This is called just before a server is shut down, regardless + of it is fore a reload, reset or shutdown. + """ + if SERVER_STARTSTOP_MODULE: + SERVER_STARTSTOP_MODULE.at_server_stop() + + + def at_server_reload_start(self): + """ + This is called only when server starts back up after a reload. + """ + if SERVER_STARTSTOP_MODULE: + SERVER_STARTSTOP_MODULE.at_server_reload_start() + + + def at_server_reload_stop(self): + """ + This is called only time the server stops before a reload. + """ + if SERVER_STARTSTOP_MODULE: + SERVER_STARTSTOP_MODULE.at_server_reload_stop() + + + def at_server_cold_start(self): + """ + This is called only when the server starts "cold", i.e. after a + shutdown or a reset. + """ + if GUEST_ENABLED: + for guest in PlayerDB.objects.all().filter(db_typeclass_path=settings.BASE_GUEST_TYPECLASS): + for character in filter(None, guest.db._playable_characters): + character.delete() + guest.delete() + if SERVER_STARTSTOP_MODULE: + SERVER_STARTSTOP_MODULE.at_server_cold_start() + + def at_server_cold_stop(self): + """ + This is called only when the server goes down due to a shutdown or reset. + """ + if SERVER_STARTSTOP_MODULE: + SERVER_STARTSTOP_MODULE.at_server_cold_stop() #------------------------------------------------------------ # diff --git a/src/settings_default.py b/src/settings_default.py index 10dc7cde28..7379a34ac8 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -287,6 +287,8 @@ CHANNEL_TYPECLASS_PATHS = ["game.gamesrc.conf", "contrib"] # Typeclass for player objects (linked to a character) (fallback) BASE_PLAYER_TYPECLASS = "src.players.player.Player" +# Typeclass for guest player objects (linked to a character) +BASE_GUEST_TYPECLASS = "src.players.player.Guest" # Typeclass and base for all objects (fallback) BASE_OBJECT_TYPECLASS = "src.objects.objects.Object" # Typeclass for character objects linked to a player (fallback) @@ -304,10 +306,20 @@ BASE_SCRIPT_TYPECLASS = "src.scripts.scripts.DoNothing" # fallback if an object's normal home location is deleted. Default # is Limbo (#2). DEFAULT_HOME = "#2" +# This enables guest logins. +GUEST_ENABLED = True +# The default home location used for guests. +GUEST_HOME = "#2" # The start position for new characters. Default is Limbo (#2). # MULTISESSION_MODE = 0, 1 - used by default unloggedin create command # MULTISESSION_MODE = 2 - used by default character_create command START_LOCATION = "#2" +# The start position used for guest characters. +GUEST_START_LOCATION = "#2" +# The naming convention for guest players/characters. The size of this list +# also detemines how many guests may be on the game at once. The default is +# a maximum of nine guests, named Guest1 through Guest9. +GUEST_LIST = ["Guest" + str(s+1) for s in range(9)] # Lookups of Attributes, Tags, Nicks, Aliases can be aggressively # cached to avoid repeated database hits. This often gives noticeable # performance gains since they are called so often. Drawback is that @@ -369,13 +381,16 @@ MAX_NR_CHARACTERS = 1 # The access hiearchy, in climbing order. A higher permission in the # hierarchy includes access of all levels below it. Used by the perm()/pperm() # lock functions. -PERMISSION_HIERARCHY = ("Players", +PERMISSION_HIERARCHY = ("Guests", + "Players", "PlayerHelpers", "Builders", "Wizards", "Immortals") # The default permission given to all new players PERMISSION_PLAYER_DEFAULT = "Players" +# The permission given to guests +PERMISSION_GUEST_DEFAULT = "Guests" ###################################################################### # In-game Channels created from server start