Imported from archive.
authorJeremy Stanley <fungi@yuggoth.org>
Wed, 31 Aug 2005 03:28:44 +0000 (03:28 +0000)
committerJeremy Stanley <fungi@yuggoth.org>
Wed, 31 Aug 2005 03:28:44 +0000 (03:28 +0000)
* command (control), menu (control): Locked these files to be
read-only, for added security.

* mudpy.conf (categories, control, include): Moved categories and
include elements to be control meta-element list facets called
default_files and include_files respectively, allowing them to be
utilized in multiple files within the data tree.
(control): Added a list facet called private_files which works like
default_files but creates them mode 0600, and moved the accounts
file to this list to prevent leaking MD5 password hashes for
accounts.
(internal:general): Renamed to internal:limits and added a
max_avatars int facet providing the means to cap the number an
account is allowed to have.

* mudpy.py (Element.get, Element.getdict, Element.getfloat)
(Element.getint, Element.getlist): Reworked to provide a means to
override the default values through a function parameter, similar to
getboolean.
(Element.set): Strip trailing L off long ints.
(handler_checking_new_account_name): Abstracted this entire function
out into the checking_new_account_name menu configuration.

command
menu
mudpy.conf
mudpy.py
testdata

diff --git a/command b/command
index c1447bd..a9ec05e 100644 (file)
--- a/command
+++ b/command
@@ -1,3 +1,6 @@
+[control]
+read_only = yes
+
 [command:quit]
 action = command_quit(user, command, parameters)
 description = Leave Example.
diff --git a/menu b/menu
index 3c67466..c3388b3 100644 (file)
--- a/menu
+++ b/menu
@@ -1,35 +1,40 @@
-[menu:choose_gender]
-choice_f = female
-prompt = Pick a gender for your new avatar:
-description = First, your new avatar needs a gender. In the world of Example, all avatars are either male or female.
-choice_m = male
-branch = choose_name
-action = user.avatar.set("gender", user.menu_choices[choice])
-
-[menu:entering_new_password]
-error_differs = The two passwords did not match. Try again...
-error_weak = That is a weak password... Try something at least 7 characters long with a combination of mixed-case letters, numbers and punctuation/spaces.
-prompt = Enter a new password for "$(account)":
-echo = off
+[control]
+read_only = yes
 
-[menu:verifying_new_password]
-prompt = Enter the same new password again:
-echo = off
+[menu:active]
+prompt = >
 
 [menu:checking_new_account_name]
+action_d = user.account.delete()
+action_g = user.account.delete()
+branch_d = disconnecting
+branch_g = entering_account_name
+branch_n = entering_new_password
 choice_d = disconnect now
 choice_g = go back
-prompt = Enter your choice:
-description = There is no existing account for "$(account)" (note that an account name is not the same as a character name). Would you like to create a new account by this name, go back and enter a different name or disconnect now?
-default = d
 choice_n = new account
+default = d
+description = There is no existing account for "$(account)" (note that an account name is not the same as a character name). Would you like to create a new account by this name, go back and enter a different name or disconnect now?
+prompt = Enter your choice:
 
-[menu:disconnecting_duplicates]
-prompt = $(red)Closing your previous connection...$(nrm)$(eol)
+[menu:checking_password]
+echo = off
+error_incorrect = Incorrect password, please try again...
+prompt = Password:
+
+[menu:choose_gender]
+action = user.avatar.set("gender", user.menu_choices[choice])
+branch = choose_name
+choice_f = female
+choice_m = male
+description = First, your new avatar needs a gender. In the world of Example, all avatars are either male or female.
+prompt = Pick a gender for your new avatar:
 
 [menu:choose_name]
-prompt = Choose a name for $(tpop):
-description = Your new avatar needs a name. This will be the name with which $(tpsp) grew up, and will initially be the name by which $(tpsp) is known in the world of Example. There are ways for your new avatar to make a name for $(tpop)self over time, so $(tpsp) won't be stuck going by such an unremarkable name forever.
+action = user.avatar.set("name", user.menu_choices[choice])
+branch = main_utility
+branch_m = choose_name
+choice_m = generate more names
 create_1 = random_name()
 create_3 = random_name()
 create_2 = random_name()
@@ -37,38 +42,46 @@ create_5 = random_name()
 create_4 = random_name()
 create_7 = random_name()
 create_6 = random_name()
-branch = active
-action = user.avatar.set("name", user.menu_choices[choice])
+description = Your new avatar needs a name. This will be the name with which $(tpsp) grew up, and will initially be the name by which $(tpsp) is known in the world of Example. There are ways for your new avatar to make a name for $(tpop)self over time, so $(tpsp) won't be stuck going by such an unremarkable name forever.
+prompt = Choose a name for $(tpop):
+
+[menu:disconnecting]
+description = $(red)Disconnecting...$(nrm)
+
+[menu:disconnecting_duplicates]
+prompt = $(red)Closing your previous connection...$(nrm)$(eol)
 
 [menu:entering_account_name]
-prompt = Identify yourself:
 description = Welcome to the mudpy example...
 error_bad_name = Your account name needs to contain only digits (0-9) and letters (a-z).
+prompt = Identify yourself:
 
-[menu:disconnecting]
-description = $(red)Disconnecting...$(nrm)
-
-[menu:active]
-prompt = >
+[menu:entering_new_password]
+echo = off
+error_weak = That is a weak password... Try something at least 7 characters long with a combination of mixed-case letters, numbers and punctuation/spaces.
+prompt = Enter a new password for "$(account)":
+error_differs = The two passwords did not match. Try again...
 
 [menu:main_utility]
-choice_d = delete an unwanted avatar
-branch_d = delete_avatar
 action_c = user.new_avatar()
-description = From here you can activate, create and delete avatars. An avatar is your persona in the world of Example.
+branch_a = active
 branch_c = choose_gender
-choice_c = create a new avatar
-choice_l = leave example for now
+branch_d = delete_avatar
 branch_l = disconnecting
-prompt = What would you like to do?
-error_no_avatars = You don't have any avatars yet. An avatar is your persona in the world of Example. It is recommended that you create one now.
-choice_p = permanently remove your account
 branch_p = delete_account
-branch_a = active
 choice_a = activate an existing avatar
+choice_c = create a new avatar
+choice_d = delete an unwanted avatar
+choice_l = leave example for now
+choice_p = permanently remove your account
+demand_a = user.account.get("avatars")
+demand_c = len(user.account.getlist("avatars")) < universe.categories["internal"]["limits"].getint("max_avatars")
+demand_d = user.account.get("avatars")
+description = From here you can activate, create and delete avatars. An avatar is your persona in the world of Example.
+error_no_avatars = You don't have any avatars yet. An avatar is your persona in the world of Example. It is recommended that you create one now.
+prompt = What would you like to do?
 
-[menu:checking_password]
-prompt = Password:
-error_incorrect = Incorrect password, please try again...
+[menu:verifying_new_password]
 echo = off
+prompt = Enter the same new password again:
 
index 3ccdcee..6ad101e 100644 (file)
@@ -1,30 +1,22 @@
 [control]
+default_files = {"account": "account", "actor": "actor", "command": "command", "internal": "internal", "location": "location", "menu": "menu", "other": "other" }
+include_files = testdata
+private_files = account
 read_only = yes
 
-[categories]
-account = account
-actor = actor
-command = command
-internal = internal
-location = location
-menu = menu
-other = other
-
-[include]
-testdata = testdata
-
-[internal:general]
-password_tries = 3
-
 [internal:language]
-capitalize = i i'd i'll i'm
+capitalize_words = [ "i", "i'd", "i'll", "i'm" ]
 default_punctuation = .
 punctuation_ask = ?
-punctuation_begin = , - : ;
+punctuation_begin = [ ",", "-", ":", ";" ]
 punctuation_exclaim = !
 punctuation_muse = ...
 punctuation_say = .
 
+[internal:limits]
+max_avatars = 7
+password_tries = 3
+
 [internal:network]
 host = 
 port = 6669
index 91ef16a..b149fc4 100644 (file)
--- a/mudpy.py
+++ b/mudpy.py
@@ -4,23 +4,15 @@
 # Licensed per terms in the LICENSE file distributed with this software.
 
 # import some things we need
-from ConfigParser import SafeConfigParser
+from ConfigParser import RawConfigParser
 from md5 import new as new_md5
-from os import F_OK, R_OK, access, getcwd, makedirs, sep
+from os import R_OK, access, chmod, makedirs, stat
+from os.path import abspath, dirname, exists, isabs, join as path_join
 from random import choice, randrange
 from socket import AF_INET, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET, socket
+from stat import S_IMODE, ST_MODE
 from time import asctime, sleep
 
-# a dict of replacement macros
-macros = {
-       "$(eol)": "\r\n",
-       "$(bld)": chr(27) + "[1m",
-       "$(nrm)": chr(27) + "[0m",
-       "$(blk)": chr(27) + "[30m",
-       "$(grn)": chr(27) + "[32m",
-       "$(red)": chr(27) + "[31m"
-       }
-
 class Element:
        """An element of the universe."""
        def __init__(self, key, universe, origin=""):
@@ -35,8 +27,8 @@ class Element:
                universe.categories[self.category][self.subkey] = self
                self.origin = origin
                if not self.origin: self.origin = universe.default_origins[self.category]
-               if not self.origin.startswith(sep):
-                       self.origin = getcwd() + sep + self.origin
+               if not isabs(self.origin):
+                       self.origin = abspath(self.origin)
                universe.contents[self.key] = self
                if not self.origin in universe.files:
                        DataFile(self.origin, universe)
@@ -51,62 +43,79 @@ class Element:
        def facets(self):
                """Return a list of facets for this element."""
                return universe.files[self.origin].data.options(self.key)
-       def get(self, facet):
+       def get(self, facet, default=""):
                """Retrieve values."""
                if universe.files[self.origin].data.has_option(self.key, facet):
                        return universe.files[self.origin].data.get(self.key, facet)
-               else:
-                       return ""
+               else: return default
        def getboolean(self, facet, default=False):
                """Retrieve values as boolean type."""
                if universe.files[self.origin].data.has_option(self.key, facet):
                        return universe.files[self.origin].data.getboolean(self.key, facet)
-               else:
-                       return default
-       def getint(self, facet):
-               """Convenience method to coerce return values as type int."""
+               else: return default
+       def getint(self, facet, default=0):
+               """Return values as int/long type."""
+               if universe.files[self.origin].data.has_option(self.key, facet):
+                       return universe.files[self.origin].data.getint(self.key, facet)
+               else: return default
+       def getfloat(self, facet, default=0.0):
+               """Return values as float type."""
+               if universe.files[self.origin].data.has_option(self.key, facet):
+                       return universe.files[self.origin].data.getfloat(self.key, facet)
+               else: return default
+       def getlist(self, facet, default=[]):
+               """Return values as list type."""
                value = self.get(facet)
-               if not value: value = 0
-               elif type(value) is str: value = value.rstrip("L")
-               return int(value)
-       def getfloat(self, facet):
-               """Convenience method to coerce return values as type float."""
+               if not value: return default
+               else: return makelist(value)
+       def getdict(self, facet, default={}):
+               """Return values as dict type."""
                value = self.get(facet)
-               if not value: value = 0
-               elif type(value) is str: value = value.rstrip("L")
-               return float(value)
+               if not value: return default
+               else: return makedict(value)
        def set(self, facet, value):
                """Set values."""
-               if not type(value) is str: value = repr(value)
+               if type(value) is long: value = repr(value).rstrip("L")
+               elif not type(value) is str: value = repr(value)
                universe.files[self.origin].data.set(self.key, facet, value)
 
 class DataFile:
        """A file containing universe elements."""
        def __init__(self, filename, universe):
-               filedir = sep.join(filename.split(sep)[:-1])
-               self.data = SafeConfigParser()
+               self.data = RawConfigParser()
                if access(filename, R_OK): self.data.read(filename)
                self.filename = filename
                universe.files[filename] = self
-               if "categories" in self.data.sections():
-                       for option in self.data.options("categories"):
-                               universe.default_origins[option] = self.data.get("categories", option)
-                               if not option in universe.categories:
-                                       universe.categories[option] = {}
+               if self.data.has_option("control", "include_files"):
+                       includes = makelist(self.data.get("control", "include_files"))
+               else: includes = []
+               if self.data.has_option("control", "default_files"):
+                       origins = makedict(self.data.get("control", "default_files"))
+                       for key in origins.keys():
+                               if not key in includes: includes.append(key)
+                               universe.default_origins[key] = origins[key]
+                               if not key in universe.categories:
+                                       universe.categories[key] = {}
+               if self.data.has_option("control", "private_files"):
+                       for item in makelist(self.data.get("control", "private_files")):
+                               if not item in includes: includes.append(item)
+                               if not item in universe.private_files:
+                                       if not isabs(item):
+                                               item = path_join(dirname(filename), item)
+                                       universe.private_files.append(item)
                for section in self.data.sections():
-                       if section == "categories" or section == "include":
-                               for option in self.data.options(section):
-                                       includefile = self.data.get(section, option)
-                                       if not includefile.startswith(sep):
-                                               includefile = filedir + sep + includefile
-                                       DataFile(includefile, universe)
-                       elif section != "control":
+                       if section != "control":
                                Element(section, universe, filename)
+               for include_file in includes:
+                       if not isabs(include_file):
+                               include_file = path_join(dirname(filename), include_file)
+                       DataFile(include_file, universe)
        def save(self):
-               if self.data.sections() and not ( "control" in self.data.sections() and self.data.getboolean("control", "read_only") ):
-                       basedir = sep.join(self.filename.split(sep)[:-1])
-                       if not access(basedir, F_OK): makedirs(basedir)
+               if self.data.sections() and ( not self.data.has_option("control", "read_only") or not self.data.getboolean("control", "read_only") ):
+                       if not exists(dirname(self.filename)): makedirs(dirname)
                        file_descriptor = file(self.filename, "w")
+                       if self.filename in universe.private_files and oct(S_IMODE(stat(self.filename)[ST_MODE])) != 0600:
+                               chmod(self.filename, 0600)
                        self.data.write(file_descriptor)
                        file_descriptor.flush()
                        file_descriptor.close()
@@ -119,6 +128,7 @@ class Universe:
                self.contents = {}
                self.default_origins = {}
                self.files = {}
+               self.private_files = []
                self.userlist = []
                self.terminate_world = False
                self.reload_modules = False
@@ -136,8 +146,8 @@ class Universe:
                                ]
                        for filename in possible_filenames:
                                if access(filename, R_OK): break
-               if not filename.startswith(sep):
-                       filename = getcwd() + sep + filename
+               if not isabs(filename):
+                       filename = abspath(filename)
                DataFile(filename, self)
        def save(self):
                """Save the universe to persistent storage."""
@@ -176,7 +186,7 @@ class User:
                self.last_address = ""
                self.connection = None
                self.authenticated = False
-               self.password_tries = 1
+               self.password_tries = 0
                self.state = "entering_account_name"
                self.menu_seen = False
                self.error = ""
@@ -189,7 +199,8 @@ class User:
 
        def quit(self):
                """Log, close the connection and remove."""
-               name = self.account.get("name")
+               if self.account: name = self.account.get("name")
+               else: name = ""
                if name: message = "User " + name
                else: message = "An unnamed user"
                message += " logged out."
@@ -393,21 +404,29 @@ class User:
                while "avatar:" + repr(counter + 1) in universe.categories["actor"].keys(): counter += 1
                universe.categories["internal"]["counters"].set("next_avatar", counter + 1)
                self.avatar = Element("actor:avatar:" + repr(counter), universe)
-               avatars = self.account.get("avatars").split()
+               avatars = self.account.getlist("avatars")
                avatars.append(self.avatar.key)
-               self.account.set("avatars", " ".join(avatars))
+               self.account.set("avatars", avatars)
 
        def list_avatar_names(self):
                """A test function to list names of assigned avatars."""
-               try:
-                       avatars = self.account.get("avatars").split()
-               except:
-                       avatars = []
+               avatars = self.account.getlist("avatars")
                avatar_names = []
                for avatar in avatars:
                        avatar_names.append(universe.contents[avatar].get("name"))
                return avatar_names
 
+def makelist(value):
+       """Turn string into list type."""
+       if value[0] + value[-1] == "[]": return eval(value)
+       else: return [ value ]
+
+def makedict(value):
+       """Turn string into dict type."""
+       if value[0] + value[-1] == "{}": return eval(value)
+       elif value.find(":") > 0: return eval("{" + value + "}")
+       else: return { value: None }
+
 def broadcast(message):
        """Send a message to all connected users."""
        for each_user in universe.userlist: each_user.send("$(eol)" + message)
@@ -531,7 +550,40 @@ def random_name():
 
 def replace_macros(user, text, is_input=False):
        """Replaces macros in text output."""
+
+       # loop until broken
        while True:
+
+               # third person pronouns
+               pronouns = {
+                       "female": { "obj": "her", "pos": "hers", "sub": "she" },
+                       "male": { "obj": "him", "pos": "his", "sub": "he" },
+                       "neuter": { "obj": "it", "pos": "its", "sub": "it" }
+                       }
+
+               # a dict of replacement macros
+               macros = {
+                       "$(eol)": "\r\n",
+                       "$(bld)": chr(27) + "[1m",
+                       "$(nrm)": chr(27) + "[0m",
+                       "$(blk)": chr(27) + "[30m",
+                       "$(grn)": chr(27) + "[32m",
+                       "$(red)": chr(27) + "[31m",
+                       }
+
+               # add dynamic macros where possible
+               if user.account:
+                       account_name = user.account.get("name")
+                       if account_name:
+                               macros["$(account)"] = account_name
+               if user.avatar:
+                       avatar_gender = user.avatar.get("gender")
+                       if avatar_gender:
+                               macros["$(tpop)"] = pronouns[avatar_gender]["obj"]
+                               macros["$(tppp)"] = pronouns[avatar_gender]["pos"]
+                               macros["$(tpsp)"] = pronouns[avatar_gender]["sub"]
+
+               # find and replace per the macros dict
                macro_start = text.find("$(")
                if macro_start == -1: break
                macro_end = text.find(")", macro_start) + 1
@@ -539,37 +591,6 @@ def replace_macros(user, text, is_input=False):
                if macro in macros.keys():
                        text = text.replace(macro, macros[macro])
 
-               # the user's account name
-               elif macro == "$(account)":
-                       text = text.replace(macro, user.account.get("name"))
-
-               # third person subjective pronoun
-               elif macro == "$(tpsp)":
-                       if user.avatar.get("gender") == "male":
-                               text = text.replace(macro, "he")
-                       elif user.avatar.get("gender") == "female":
-                               text = text.replace(macro, "she")
-                       else:
-                               text = text.replace(macro, "it")
-
-               # third person objective pronoun
-               elif macro == "$(tpop)":
-                       if user.avatar.get("gender") == "male":
-                               text = text.replace(macro, "him")
-                       elif user.avatar.get("gender") == "female":
-                               text = text.replace(macro, "her")
-                       else:
-                               text = text.replace(macro, "it")
-
-               # third person possessive pronoun
-               elif macro == "$(tppp)":
-                       if user.avatar.get("gender") == "male":
-                               text = text.replace(macro, "his")
-                       elif user.avatar.get("gender") == "female":
-                               text = text.replace(macro, "hers")
-                       else:
-                               text = text.replace(macro, "its")
-
                # if we get here, log and replace it with null
                else:
                        text = text.replace(macro, "")
@@ -736,11 +757,22 @@ def get_menu_prompt(state):
 def get_menu_choices(user):
        """Return a dict of choice:meaning."""
        choices = {}
+       ignores = []
+       options = {}
+       creates = {}
        for facet in universe.categories["menu"][user.state].facets():
-               if facet.startswith("choice_"):
-                       choices[facet.split("_", 2)[1]] = universe.categories["menu"][user.state].get(facet)
+               if facet.startswith("demand_") and not eval(universe.categories["menu"][user.state].get(facet)):
+                       ignores.append(facet.split("_", 2)[1])
+               elif facet.startswith("choice_"):
+                       options[facet] = facet.split("_", 2)[1]
                elif facet.startswith("create_"):
-                       choices[facet.split("_", 2)[1]] = eval(universe.categories["menu"][user.state].get(facet))
+                       creates[facet] = facet.split("_", 2)[1]
+       for facet in options.keys():
+               if not options[facet] in ignores:
+                       choices[options[facet]] = universe.categories["menu"][user.state].get(facet)
+       for facet in creates.keys():
+               if not creates[facet] in ignores:
+                       choices[creates[facet]] = eval(universe.categories["menu"][user.state].get(facet))
        return choices
 
 def get_formatted_menu_choices(state, choices):
@@ -817,12 +849,10 @@ def generic_menu_handler(user):
                if choice: choice = choice.lower()
        else: choice = ""
 
-       # run any script related to this choice
-       exec(get_choice_action(user, choice))
-
-       # move on to the next state or return an error
-       new_state = get_choice_branch(user, choice)
-       if new_state: user.state = new_state
+       if choice in user.menu_choices:
+               exec(get_choice_action(user, choice))
+               new_state = get_choice_branch(user, choice)
+               if new_state: user.state = new_state
        else: user.error = "default"
 
 def handler_entering_account_name(user):
@@ -872,7 +902,7 @@ def handler_checking_password(user):
                        user.state = "main_utility"
 
        # if at first your hashes don't match, try, try again
-       elif user.password_tries < universe.categories["internal"]["general"].getint("password_tries"):
+       elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
                user.password_tries += 1
                user.error = "incorrect"
 
@@ -881,38 +911,6 @@ def handler_checking_password(user):
                user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
                user.state = "disconnecting"
 
-def handler_checking_new_account_name(user):
-       """Handle input for the new user menu."""
-
-       # get the next waiting line of input
-       input_data = user.input_queue.pop(0)
-
-       # if there's input, take the first character and lowercase it
-       if input_data:
-               choice = input_data.lower()[0]
-
-       # if there's no input, use the default
-       else:
-               choice = get_default_menu_choice(user.state)
-
-       # user selected to disconnect
-       if choice == "d":
-               user.account.delete()
-               user.state = "disconnecting"
-
-       # go back to the login screen
-       elif choice == "g":
-               user.account.delete()
-               user.state = "entering_account_name"
-
-       # new user, so ask for a password
-       elif choice == "n":
-               user.state = "entering_new_password"
-
-       # user entered a non-existent option
-       else:
-               user.error = "default"
-
 def handler_entering_new_password(user):
        """Handle a new password entry."""
 
@@ -928,7 +926,7 @@ def handler_entering_new_password(user):
                user.state = "verifying_new_password"
 
        # the password was weak, try again if you haven't tried too many times
-       elif user.password_tries < universe.categories["internal"]["general"].getint("password_tries"):
+       elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
                user.password_tries += 1
                user.error = "weak"
 
@@ -953,7 +951,7 @@ def handler_verifying_new_password(user):
 
        # go back to entering the new password as long as you haven't tried
        # too many times
-       elif user.password_tries < universe.categories["internal"]["general"].getint("password_tries"):
+       elif user.password_tries < universe.categories["internal"]["limits"].getint("password_tries") - 1:
                user.password_tries += 1
                user.error = "differs"
                user.state = "entering_new_password"
@@ -1076,7 +1074,7 @@ def command_say(user, command="", parameters=""):
                for facet in universe.categories["internal"]["language"].facets():
                        if facet.startswith("punctuation_"):
                                action = facet.split("_")[1]
-                               for mark in universe.categories["internal"]["language"].get(facet).split():
+                               for mark in universe.categories["internal"]["language"].getlist(facet):
                                                actions[mark] = action
 
                # match the punctuation used, if any, to an action
@@ -1092,8 +1090,8 @@ def command_say(user, command="", parameters=""):
                        message += default_punctuation
 
                # capitalize a list of words within the message
-               capitalize = universe.categories["internal"]["language"].get("capitalize").split()
-               for word in capitalize:
+               capitalize_words = universe.categories["internal"]["language"].getlist("capitalize_words")
+               for word in capitalize_words:
                        message = message.replace(" " + word + " ", " " + word.capitalize() + " ")
 
                # tell the room
index 0b0d4ca..8b84c39 100644 (file)
--- a/testdata
+++ b/testdata
@@ -1,9 +1,9 @@
 [prop:example_prop]
 name = The Example Prop
 
-[actor:example_actor]
-name = The Example Actor
-
 [location:0:0:0:0]
 name = The Origin of the Universe
 
+[actor:example_actor]
+name = The Example Actor
+