""" Menu-driven login system Contribution - Griatch 2011 This is an alternative login system for Evennia, using the contrib.menusystem module. As opposed to the default system it doesn't use emails for authentication and also don't auto-creates a Character with the same name as the Player (instead assuming some sort of character-creation to come next). Install is simple: To your settings file, add/edit the line: CMDSET_UNLOGGEDIN = "contrib.menu_login.UnloggedInCmdSet" That's it. The cmdset in this module will now be used instead of the default one. The initial login "graphic" is taken from strings in the module given by settings.CONNECTION_SCREEN_MODULE. You will want to edit the string in that module (at least comment out the default string that mentions commands that are not available) and add something more suitable for the initial splash screen. """ import re import traceback from django.conf import settings from ev import db_players, db_serverconfs, db_channels from ev import utils, logger, create_player from ev import Command, CmdSet from ev import syscmdkeys from contrib.menusystem import MenuNode, MenuTree CMD_LOGINSTART = syscmdkeys.CMD_LOGINSTART CMD_NOINPUT = syscmdkeys.CMD_NOINPUT CMD_NOMATCH = syscmdkeys.CMD_NOMATCH CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE # Commands run on the unloggedin screen. Note that this is not using settings.UNLOGGEDIN_CMDSET but # the menu system, which is why some are named for the numbers in the menu. # # Also note that the menu system will automatically assign all # commands used in its structure a property "menutree" holding a reference # back to the menutree. This allows the commands to do direct manipulation # for example by triggering a conditional jump to another node. # # Menu entry 1a - Entering a Username class CmdBackToStart(Command): """ Step back to node0 """ key = CMD_NOINPUT locks = "cmd:all()" def func(self): "Execute the command" self.menutree.goto("START") class CmdUsernameSelect(Command): """ Handles the entering of a username and checks if it exists. """ key = CMD_NOMATCH locks = "cmd:all()" def func(self): "Execute the command" player = db_players.get_player_from_name(self.args) if not player: self.caller.msg("{rThis account name couldn't be found. Did you create it? If you did, make sure you spelled it right (case doesn't matter).{n") self.menutree.goto("node1a") else: self.menutree.player = player # store the player so next step can find it self.menutree.goto("node1b") # Menu entry 1b - Entering a Password class CmdPasswordSelectBack(Command): """ Steps back from the Password selection """ key = CMD_NOINPUT locks = "cmd:all()" def func(self): "Execute the command" self.menutree.goto("node1a") class CmdPasswordSelect(Command): """ Handles the entering of a password and logs into the game. """ key = CMD_NOMATCH locks = "cmd:all()" def func(self): "Execute the command" if not hasattr(self.menutree, "player"): self.caller.msg("{rSomething went wrong! The player was not remembered from last step!{n") self.menutree.goto("node1a") return player = self.menutree.player if not player.user.check_password(self.args): self.caller.msg("{rIncorrect password.{n") self.menutree.goto("node1b") return # before going on, check eventual bans bans = db_serverconfs.conf("server_bans") if bans and (any(tup[0]==player.name for tup in bans) or any(tup[2].match(player.sessions[0].address[0]) for tup in bans if tup[2])): # this is a banned IP or name! string = "{rYou have been banned and cannot continue from here." string += "\nIf you feel this ban is in error, please email an admin.{x" self.caller.msg(string) self.caller.session_disconnect() return # we are ok, log us in. self.caller.msg("{gWelcome %s! Logging in ...{n" % player.key) self.caller.session_login(player) # abort menu, do cleanup. self.menutree.goto("END") # we are logged in. Look around. character = player.character if character: character.execute_cmd("look") else: # we have no character yet; use player's look, if it exists player.execute_cmd("look") # Menu entry 2a - Creating a Username class CmdUsernameCreate(Command): """ Handle the creation of a valid username """ key = CMD_NOMATCH locks = "cmd:all()" def func(self): "Execute the command" playername = self.args # sanity check on the name if not re.findall('^[\w. @+-]+$', playername) or not (3 <= len(playername) <= 30): self.caller.msg("\n\r {rAccount name should be between 3 and 30 characters. Letters, spaces, dig\ its and @/./+/-/_ only.{n") # this echoes the restrictions made by django's auth module. self.menutree.goto("node2a") return if db_players.get_player_from_name(playername): self.caller.msg("\n\r {rAccount name %s already exists.{n" % playername) self.menutree.goto("node2a") return # store the name for the next step self.menutree.playername = playername self.menutree.goto("node2b") # Menu entry 2b - Creating a Password class CmdPasswordCreateBack(Command): "Step back from the password creation" key = CMD_NOINPUT locks = "cmd:all()" def func(self): "Execute the command" self.menutree.goto("node2a") class CmdPasswordCreate(Command): "Handle the creation of a password. This also creates the actual Player/User object." key = CMD_NOMATCH locks = "cmd:all()" def func(self): "Execute the command" password = self.args if not hasattr(self.menutree, 'playername'): self.caller.msg("{rSomething went wrong! Playername not remembered from previous step!{n") self.menutree.goto("node2a") return playername = self.menutree.playername if len(password) < 3: # too short password string = "{rYour password must be at least 3 characters or longer." string += "\n\rFor best security, make it at least 8 characters long, " string += "avoid making it a real word and mix numbers into it.{n" self.caller.msg(string) self.menutree.goto("node2b") return # everything's ok. Create the new player account. Don't create a Character here. try: permissions = settings.PERMISSION_PLAYER_DEFAULT typeclass = settings.BASE_PLAYER_TYPECLASS new_player = create_player(playername, None, password, typeclass=typeclass, permissions=permissions, create_character=False) if not new_player: self.msg("There was an error creating the Player. This error was logged. Contact an admin.") self.menutree.goto("START") return utils.init_new_player(new_player) # join the new player to the public channel pchanneldef = settings.CHANNEL_PUBLIC if pchanneldef: pchannel = db_channels.get_channel(pchanneldef[0]) if not pchannel.connect_to(new_player): string = "New player '%s' could not connect to public channel!" % new_player.key logger.log_errmsg(string) # tell the caller everything went well. string = "{gA new account '%s' was created. Now go log in from the menu!{n" self.caller.msg(string % (playername)) self.menutree.goto("START") 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." self.caller.msg(string % (traceback.format_exc())) logger.log_errmsg(traceback.format_exc()) # Menu entry 3 - help screen LOGIN_SCREEN_HELP = \ """ Welcome to %s! To login you need to first create an account. This is easy and free to do: Choose option {w(1){n in the menu and enter an account name and password when prompted. Obs- the account name is {wnot{n the name of the Character you will play in the game! It's always a good idea (not only here, but everywhere on the net) to not use a regular word for your password. Make it longer than 3 characters (ideally 6 or more) and mix numbers and capitalization into it. The password also handles whitespace, so why not make it a small sentence - easy to remember, hard for a computer to crack. Once you have an account, use option {w(2){n to log in using the account name and password you specified. Use the {whelp{n command once you're logged in to get more aid. Hope you enjoy your stay! (return to go back)""" % settings.SERVERNAME # Menu entry 4 class CmdUnloggedinQuit(Command): """ We maintain a different version of the quit command here for unconnected players for the sake of simplicity. The logged in version is a bit more complicated. """ key = "4" aliases = ["quit", "qu", "q"] locks = "cmd:all()" def func(self): "Simply close the connection." self.menutree.goto("END") self.caller.msg("Good bye! Disconnecting ...") self.caller.session_disconnect() # The login menu tree, using the commands above START = MenuNode("START", text=utils.string_from_module(CONNECTION_SCREEN_MODULE), links=["node1a", "node2a", "node3", "END"], linktexts=["Log in with an existing account", "Create a new account", "Help", "Quit",], selectcmds=[None, None, None, CmdUnloggedinQuit]) node1a = MenuNode("node1a", text="Please enter your account name (empty to abort).", links=["START", "node1b"], helptext=["Enter the account name you previously registered with."], keywords=[CMD_NOINPUT, CMD_NOMATCH], selectcmds=[CmdBackToStart, CmdUsernameSelect], nodefaultcmds=True) # if we don't, default help/look will be triggered by names starting with l/h ... node1b = MenuNode("node1b", text="Please enter your password (empty to go back).", links=["node1a", "END"], keywords=[CMD_NOINPUT, CMD_NOMATCH], selectcmds=[CmdPasswordSelectBack, CmdPasswordSelect], nodefaultcmds=True) node2a = MenuNode("node2a", text="Please enter your desired account name (empty to abort).", links=["START", "node2b"], helptext="Account name can max be 30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only.", keywords=[CMD_NOINPUT, CMD_NOMATCH], selectcmds=[CmdBackToStart, CmdUsernameCreate], nodefaultcmds=True) node2b = MenuNode("node2b", text="Please enter your password (empty to go back).", links=["node2a", "START"], helptext="Your password cannot contain any characters.", keywords=[CMD_NOINPUT, CMD_NOMATCH], selectcmds=[CmdPasswordCreateBack, CmdPasswordCreate], nodefaultcmds=True) node3 = MenuNode("node3", text=LOGIN_SCREEN_HELP, links=["START"], helptext="", keywords=[CMD_NOINPUT], selectcmds=[CmdBackToStart]) # access commands class UnloggedInCmdSet(CmdSet): "Cmdset for the unloggedin state" key = "UnloggedinState" priority = 0 def at_cmdset_creation(self): self.add(CmdUnloggedinLook()) class CmdUnloggedinLook(Command): """ An unloggedin version of the look command. This is called by the server when the player first connects. It sets up the menu before handing off to the menu's own look command.. """ key = CMD_LOGINSTART aliases = ["look", "l"] locks = "cmd:all()" def func(self): "Execute the menu" menu = MenuTree(self.caller, nodes=(START, node1a, node1b, node2a, node2b, node3), exec_end=None) menu.start()