X-Git-Url: https://mudpy.org/gitweb?p=mudpy.git;a=blobdiff_plain;f=mudpy%2Fmisc.py;h=32c0d688c20b94376e3e1cfe91c3b145febfdc12;hp=6c720d53f7af78a2d0235f20358046d0c04f762d;hb=faf8c4653aa2b1e184545aef15b5ba192dd78d03;hpb=91f6b37e5fc36953cc1f3150695382d0822b0672 diff --git a/mudpy/misc.py b/mudpy/misc.py index 6c720d5..32c0d68 100644 --- a/mudpy/misc.py +++ b/mudpy/misc.py @@ -1,6 +1,6 @@ """Miscellaneous functions for the mudpy engine.""" -# Copyright (c) 2004-2016 Jeremy Stanley . Permission +# Copyright (c) 2004-2017 Jeremy Stanley . Permission # to use, copy, modify, and distribute this software is granted under # terms provided in the LICENSE file distributed with this software. @@ -23,7 +23,7 @@ class Element: """An element of the universe.""" - def __init__(self, key, universe, filename=None, old_style=False): + def __init__(self, key, universe, origin=None, old_style=False): """Set up a new element.""" # TODO(fungi): This can be removed after the transition is complete @@ -62,21 +62,16 @@ class Element: self.category = "other" self.subkey = self.key if self.category not in self.universe.categories: - self.category = "other" - self.subkey = self.key + self.universe.categories[self.category] = {} - # get an appropriate filename for the origin - if not filename: - filename = self.universe.default_origins[self.category] - if not os.path.isabs(filename): - filename = os.path.abspath(filename) - - # add the file if it doesn't exist yet - if filename not in self.universe.files: - mudpy.data.DataFile(filename, self.universe) + # get an appropriate origin + if not origin: + self.universe.add_category(self.category) + origin = self.universe.files[ + self.universe.origins[self.category]["fallback"]] # record or reset a pointer to the origin file - self.origin = self.universe.files[filename] + self.origin = self.universe.files[origin.source] # add a data section to the origin if necessary if self.key not in self.origin.data: @@ -88,8 +83,7 @@ class Element: def reload(self): """Create a new element and replace this one.""" - Element(self.key, self.universe, self.origin.filename, - old_style=self.old_style) + Element(self.key, self.universe, self.origin, old_style=self.old_style) del(self) def destroy(self): @@ -154,6 +148,14 @@ class Element: def set(self, facet, value): """Set values.""" + if not self.origin.is_writeable() and not self.universe.loading: + # break if there is an attempt to update an element from a + # read-only file, unless the universe is in the midst of loading + # updated data from files + raise PermissionError("Altering elements in read-only files is " + "disallowed") + if facet in ["loglevel"]: + value = int(value) if not self.has_facet(facet) or not self.get(facet) == value: if self.old_style: if self.key not in self.origin.data: @@ -162,7 +164,7 @@ class Element: else: node = ".".join((self.key, facet)) self.origin.data[node] = value - self.facethash[node] = self.origin.data[node] + self.facethash[facet] = self.origin.data[node] self.origin.modified = True def append(self, facet, value): @@ -297,12 +299,13 @@ class Element: def portals(self): """Map the portal directions for an area to neighbors.""" portals = {} - if re.match("""^area:-?\d+,-?\d+,-?\d+$""", self.key): + if re.match(r"""^area:-?\d+,-?\d+,-?\d+$""", self.key): coordinates = [(int(x)) for x in self.key.split(":")[1].split(",")] - offsets = dict((x, - self.universe.contents["mudpy.movement.%s" % x].get("vector") - ) for x in self.universe.directions) + offsets = dict( + (x, + self.universe.contents["mudpy.movement.%s" % x].get("vector") + ) for x in self.universe.directions) for portal in self.get("gridlinks"): adjacent = map(lambda c, o: c + o, coordinates, offsets[portal]) @@ -342,10 +345,10 @@ class Universe: """Initialize the universe.""" self.categories = {} self.contents = {} - self.default_origins = {} self.directions = set() + self.loading = False self.loglines = [] - self.private_files = [] + self.origins = {} self.reload_flag = False self.setup_loglines = [] self.startdir = os.getcwd() @@ -372,6 +375,9 @@ class Universe: def load(self): """Load universe data from persistent storage.""" + # while loading, it's safe to update elements from read-only files + self.loading = True + # it's possible for this to enter before logging configuration is read pending_loglines = [] @@ -391,17 +397,26 @@ class Universe: del self.files[data_filename] # start loading from the initial file - mudpy.data.DataFile(self.filename, self) + mudpy.data.Data(self.filename, self) + + # load default storage locations for categories + if hasattr(self, "contents") and "mudpy.filing" in self.contents: + self.origins.update(self.contents["mudpy.filing"].get( + "categories", {})) + + # add some builtin categories we know we'll need + for category in ("account", "actor", "internal"): + self.add_category(category) # make a list of inactive avatars inactive_avatars = [] - for account in self.categories["account"].values(): + for account in self.categories.get("account", {}).values(): for avatar in account.get("avatars"): try: inactive_avatars.append(self.contents[avatar]) except KeyError: pending_loglines.append(( - "Missing avatar \"%s\", possible data corruption" % + 'Missing avatar "%s", possible data corruption' % avatar, 6)) for user in self.userlist: if user.avatar in inactive_avatars: @@ -422,6 +437,10 @@ class Universe: for element in self.contents.values(): element.update_location() element.clean_contents() + + # done loading, so disallow updating elements from read-only files + self.loading = False + return pending_loglines def new(self): @@ -488,6 +507,19 @@ class Universe: """Convenience method to get the elapsed time counter.""" return self.categories["internal"]["counters"].get("elapsed") + def add_category(self, category, fallback=None): + """Set up category tracking/metadata.""" + if category not in self.origins: + self.origins[category] = {} + if not fallback: + fallback = mudpy.data.find_file( + ".".join((category, "yaml")), universe=self) + if "fallback" not in self.origins[category]: + self.origins[category]["fallback"] = fallback + flags = self.origins[category].get("flags", None) + if fallback not in self.files: + mudpy.data.Data(fallback, self, flags=flags) + class User: @@ -533,9 +565,8 @@ class User: def check_idle(self): """Warn or disconnect idle users as appropriate.""" idletime = universe.get_time() - self.last_input - linkdead_dict = universe.categories["internal"]["time"].get( - "linkdead" - ) + linkdead_dict = universe.contents[ + "mudpy.timing.idle.disconnect"].facets() if self.state in linkdead_dict: linkdead_state = self.state else: @@ -557,7 +588,7 @@ class User: log(logline, 2) self.state = "disconnecting" self.menu_seen = False - idle_dict = universe.categories["internal"]["time"].get("idle") + idle_dict = universe.contents["mudpy.timing.idle.warn"].facets() if self.state in idle_dict: idle_state = self.state else: @@ -650,8 +681,8 @@ class User: if self.state is not "authenticated": log("User " + self.account.get("name") + " logged in.", 2) self.authenticated = True - if self.account.subkey in universe.contents["mudpy.limit"].get( - "admins"): + if ("mudpy.limit" in universe.contents and self.account.subkey in + universe.contents["mudpy.limit"].get("admins")): self.account.set("administrator", "True") def show_menu(self): @@ -901,7 +932,7 @@ class User: counter = 0 while "avatar:" + self.account.get("name") + ":" + str( counter - ) in universe.categories["actor"].keys(): + ) in universe.categories.get("actor", {}).keys(): counter += 1 self.avatar = Element( "actor:avatar:" + self.account.get("name") + ":" + str( @@ -958,7 +989,7 @@ class User: try: avatars.append(universe.contents[avatar].get("name")) except KeyError: - log("Missing avatar \"%s\", possible data corruption." % + log('Missing avatar "%s", possible data corruption.' % avatar, 6) return avatars @@ -973,9 +1004,14 @@ def log(message, level=0): """Log a message.""" # a couple references we need - file_name = universe.contents["mudpy.log"].get("file") - max_log_lines = universe.contents["mudpy.log"].get("lines") - syslog_name = universe.contents["mudpy.log"].get("syslog") + if "mudpy.log" in universe.contents: + file_name = universe.contents["mudpy.log"].get("file", "") + max_log_lines = universe.contents["mudpy.log"].get("lines", 0) + syslog_name = universe.contents["mudpy.log"].get("syslog", "") + else: + file_name = "" + max_log_lines = 0 + syslog_name = "" timestamp = time.asctime()[4:19] # turn the message into a list of nonempty lines @@ -992,7 +1028,8 @@ def log(message, level=0): file_descriptor.close() # send the timestamp and line to standard output - if universe.contents["mudpy.log"].get("stdout"): + if ("mudpy.log" in universe.contents and + universe.contents["mudpy.log"].get("stdout")): for line in lines: print(timestamp + " " + line) @@ -1299,7 +1336,7 @@ def replace_macros(user, text, is_input=False): replacement = replacement[:-2] else: replacement = "" - log("Couldn't read included " + incfile + " file.", 6) + log("Couldn't read included " + incfile + " file.", 7) # if we get here, log and replace it with null else: @@ -1353,18 +1390,14 @@ def on_pulse(): user.pulse() # add an element for counters if it doesn't exist - if "counters" not in universe.categories["internal"]: - universe.categories["internal"]["counters"] = Element( - "internal:counters", universe, old_style=True - ) + if "counters" not in universe.categories.get("internal", {}): + Element("internal:counters", universe, old_style=True) # update the log every now and then if not universe.categories["internal"]["counters"].get("mark"): log(str(len(universe.userlist)) + " connection(s)") universe.categories["internal"]["counters"].set( - "mark", universe.categories["internal"]["time"].get( - "frequency_log" - ) + "mark", universe.contents["mudpy.timing"].get("status") ) else: universe.categories["internal"]["counters"].set( @@ -1377,9 +1410,7 @@ def on_pulse(): if not universe.categories["internal"]["counters"].get("save"): universe.save() universe.categories["internal"]["counters"].set( - "save", universe.categories["internal"]["time"].get( - "frequency_save" - ) + "save", universe.contents["mudpy.timing"].get("save") ) else: universe.categories["internal"]["counters"].set( @@ -1389,8 +1420,7 @@ def on_pulse(): ) # pause for a configurable amount of time (decimal seconds) - time.sleep(universe.categories["internal"] - ["time"].get("increment")) + time.sleep(universe.contents["mudpy.timing"].get("increment")) # increase the elapsed increment counter universe.categories["internal"]["counters"].set( @@ -1687,7 +1717,7 @@ def handler_entering_account_name(user): user.error = "bad_name" # if that account exists, time to request a password - elif name in universe.categories["account"]: + elif name in universe.categories.get("account", {}): user.account = universe.categories["account"][name] user.state = "checking_password" @@ -1709,6 +1739,12 @@ def handler_checking_password(user): # get the next waiting line of input input_data = user.input_queue.pop(0) + if "mudpy.limit" in universe.contents: + max_password_tries = universe.contents["mudpy.limit"].get( + "password_tries", 3) + else: + max_password_tries = 3 + # does the hashed input equal the stored hash? if mudpy.password.verify(input_data, user.account.get("passhash")): @@ -1718,8 +1754,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.contents["mudpy.limit"].get( - "password_tries") - 1: + elif user.password_tries < max_password_tries - 1: user.password_tries += 1 user.error = "incorrect" @@ -1737,6 +1772,12 @@ def handler_entering_new_password(user): # get the next waiting line of input input_data = user.input_queue.pop(0) + if "mudpy.limit" in universe.contents: + max_password_tries = universe.contents["mudpy.limit"].get( + "password_tries", 3) + else: + max_password_tries = 3 + # make sure the password is strong--at least one upper, one lower and # one digit, seven or more characters in length if len(input_data) > 6 and len( @@ -1752,8 +1793,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.contents["mudpy.limit"].get( - "password_tries") - 1: + elif user.password_tries < max_password_tries - 1: user.password_tries += 1 user.error = "weak" @@ -1772,6 +1812,12 @@ def handler_verifying_new_password(user): # get the next waiting line of input input_data = user.input_queue.pop(0) + if "mudpy.limit" in universe.contents: + max_password_tries = universe.contents["mudpy.limit"].get( + "password_tries", 3) + else: + max_password_tries = 3 + # hash the input and match it to storage if mudpy.password.verify(input_data, user.account.get("passhash")): user.authenticate() @@ -1782,8 +1828,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.contents["mudpy.limit"].get( - "password_tries") - 1: + elif user.password_tries < max_password_tries - 1: user.password_tries += 1 user.error = "differs" user.state = "entering_new_password" @@ -1868,7 +1913,7 @@ def command_reload(actor): log( "User " + actor.owner.account.get("name") + " reloaded the world.", - 8 + 6 ) # set a flag to reload @@ -1953,8 +1998,8 @@ def command_help(actor, parameters): else: output += " $(grn)" output += item + "$(nrm) - " + description + "$(eol)" - output += ("$(eol)Enter \"help COMMAND\" for help on a command " - "named \"COMMAND\".") + output += ('$(eol)Enter "help COMMAND" for help on a command ' + 'named "COMMAND".') # send the accumulated output to the user actor.send(output) @@ -1984,25 +2029,26 @@ def command_say(actor, parameters): # if the message is wrapped in quotes, remove them and leave contents # intact - if parameters.startswith("\"") and parameters.endswith("\""): + if parameters.startswith('"') and parameters.endswith('"'): message = parameters[1:-1] literal = True # otherwise, get rid of stray quote marks on the ends of the message else: - message = parameters.strip("\"'`") + message = parameters.strip('''"'`''') literal = False # the user entered a message if message: # match the punctuation used, if any, to an action - actions = universe.contents["mudpy.linguistic"].get( - "actions" - ) - default_punctuation = ( - universe.contents["mudpy.linguistic"].get( - "default_punctuation")) + if "mudpy.linguistic" in universe.contents: + actions = universe.contents["mudpy.linguistic"].get("actions", {}) + default_punctuation = (universe.contents["mudpy.linguistic"].get( + "default_punctuation", ".")) + else: + actions = {} + default_punctuation = "." action = "" # reverse sort punctuation options so the longest match wins @@ -2026,9 +2072,10 @@ def command_say(actor, parameters): message = message[0].lower() + message[1:] # iterate over all words in message, replacing typos - typos = universe.contents["mudpy.linguistic"].get( - "typos" - ) + if "mudpy.linguistic" in universe.contents: + typos = universe.contents["mudpy.linguistic"].get("typos", {}) + else: + typos = {} words = message.split() for index in range(len(words)): word = words[index] @@ -2046,9 +2093,9 @@ def command_say(actor, parameters): # tell the area if message: actor.echo_to_location( - actor.get("name") + " " + action + "s, \"" + message + "\"" + actor.get("name") + " " + action + 's, "' + message + '"' ) - actor.send("You " + action + ", \"" + message + "\"") + actor.send("You " + action + ', "' + message + '"') # there was no message else: @@ -2086,21 +2133,23 @@ def command_show(actor, parameters): message += "$(eol) $(grn)" + category + "$(nrm)" elif arguments[0] == "files": message = "These are the current files containing the universe:$(eol)" - filenames = list(universe.files.keys()) - filenames.sort() + filenames = sorted(universe.files) for filename in filenames: if universe.files[filename].is_writeable(): status = "rw" else: status = "ro" - message += ("$(eol) $(red)(" + status + ") $(grn)" + filename - + "$(nrm)") + message += ("$(eol) $(red)(%s) $(grn)%s$(nrm)" % + (status, filename)) + if universe.files[filename].flags: + message += (" $(yel)[%s]$(nrm)" % + ",".join(universe.files[filename].flags)) elif arguments[0] == "category": if len(arguments) != 2: message = "You must specify one category." elif arguments[1] in universe.categories: - message = ("These are the elements in the \"" + arguments[1] - + "\" category:$(eol)") + message = ('These are the elements in the "' + arguments[1] + + '" category:$(eol)') elements = [ ( universe.categories[arguments[1]][x].key @@ -2110,27 +2159,27 @@ def command_show(actor, parameters): for element in elements: message += "$(eol) $(grn)" + element + "$(nrm)" else: - message = "Category \"" + arguments[1] + "\" does not exist." + message = 'Category "' + arguments[1] + '" does not exist.' elif arguments[0] == "file": if len(arguments) != 2: message = "You must specify one file." elif arguments[1] in universe.files: - message = ("These are the elements in the \"" + arguments[1] - + "\" file:$(eol)") + message = ('These are the elements in the "' + arguments[1] + + '" file:$(eol)') elements = universe.files[arguments[1]].data.keys() elements.sort() for element in elements: message += "$(eol) $(grn)" + element + "$(nrm)" else: - message = "Category \"" + arguments[1] + "\" does not exist." + message = 'Category "' + arguments[1] + '" does not exist.' elif arguments[0] == "element": if len(arguments) != 2: message = "You must specify one element." elif arguments[1].strip(".") in universe.contents: element = universe.contents[arguments[1].strip(".")] - message = ("These are the properties of the \"" + arguments[1] - + "\" element (in \"" + element.origin.filename - + "\"):$(eol)") + message = ('These are the properties of the "' + arguments[1] + + '" element (in "' + element.origin.source + + '"):$(eol)') facets = element.facets() for facet in sorted(facets): if element.old_style: @@ -2140,7 +2189,7 @@ def command_show(actor, parameters): message += ("$(eol) $(grn)%s: $(red)%s$(nrm)" % (facet, str(facets[facet]))) else: - message = "Element \"" + arguments[1] + "\" does not exist." + message = 'Element "' + arguments[1] + '" does not exist.' elif arguments[0] == "result": if len(arguments) < 2: message = "You need to specify an expression." @@ -2152,21 +2201,21 @@ def command_show(actor, parameters): "$(eol)$(bld)%s$(nrm)" % e) elif arguments[0] == "log": if len(arguments) == 4: - if re.match("^\d+$", arguments[3]) and int(arguments[3]) >= 0: + if re.match(r"^\d+$", arguments[3]) and int(arguments[3]) >= 0: stop = int(arguments[3]) else: stop = -1 else: stop = 0 if len(arguments) >= 3: - if re.match("^\d+$", arguments[2]) and int(arguments[2]) > 0: + if re.match(r"^\d+$", arguments[2]) and int(arguments[2]) > 0: start = int(arguments[2]) else: start = -1 else: start = 10 if len(arguments) >= 2: - if (re.match("^\d+$", arguments[1]) + if (re.match(r"^\d+$", arguments[1]) and 0 <= int(arguments[1]) <= 9): level = int(arguments[1]) else: @@ -2181,7 +2230,7 @@ def command_show(actor, parameters): message = ("When specified, level must be 0-9 (default 1), " "start and stop must be >=1 (default 10 and 1).") else: - message = "I don't know what \"" + parameters + "\" is." + message = '''I don't know what "''' + parameters + '" is.' actor.send(message) @@ -2198,10 +2247,10 @@ def command_create(actor, parameters): if len(arguments) == 2: element, filename = arguments if element in universe.contents: - message = "The \"" + element + "\" element already exists." + message = 'The "' + element + '" element already exists.' else: - message = ("You create \"" + - element + "\" within the universe.") + message = ('You create "' + + element + '" within the universe.') logline = actor.owner.account.get( "name" ) + " created an element: " + element @@ -2209,7 +2258,7 @@ def command_create(actor, parameters): logline += " in file " + filename if filename not in universe.files: message += ( - " Warning: \"" + filename + "\" is not yet " + ' Warning: "' + filename + '" is not yet ' "included in any other file and will not be read " "on startup unless this is remedied.") Element(element, universe, filename, old_style=True) @@ -2226,11 +2275,11 @@ def command_destroy(actor, parameters): message = "You must specify an element to destroy." else: if parameters not in universe.contents: - message = "The \"" + parameters + "\" element does not exist." + message = 'The "' + parameters + '" element does not exist.' else: universe.contents[parameters].destroy() - message = ("You destroy \"" + parameters - + "\" within the universe.") + message = ('You destroy "' + parameters + + '" within the universe.') log( actor.owner.account.get( "name" @@ -2247,22 +2296,33 @@ def command_set(actor, parameters): else: arguments = parameters.split(" ", 2) if len(arguments) == 1: - message = ("What facet of element \"" + arguments[0] - + "\" would you like to set?") + message = ('What facet of element "' + arguments[0] + + '" would you like to set?') elif len(arguments) == 2: - message = ("What value would you like to set for the \"" + - arguments[1] + "\" facet of the \"" + arguments[0] - + "\" element?") + message = ('What value would you like to set for the "' + + arguments[1] + '" facet of the "' + arguments[0] + + '" element?') else: element, facet, value = arguments if element not in universe.contents: - message = "The \"" + element + "\" element does not exist." + message = 'The "' + element + '" element does not exist.' else: - universe.contents[element].set(facet, value) - message = ("You have successfully (re)set the \"" + facet - + "\" facet of element \"" + element - + "\". Try \"show element " + - element + "\" for verification.") + try: + universe.contents[element].set(facet, value) + except PermissionError: + message = ('The "%s" element is kept in read-only file ' + '"%s" and cannot be altered.' % + (element, universe.contents[ + element].origin.source)) + except ValueError: + message = ('Value "%s" of type "%s" cannot be coerced ' + 'to the correct datatype for facet "%s".' % + (value, type(value), facet)) + else: + message = ('You have successfully (re)set the "' + facet + + '" facet of element "' + element + + '". Try "show element ' + + element + '" for verification.') actor.send(message) @@ -2273,23 +2333,23 @@ def command_delete(actor, parameters): else: arguments = parameters.split(" ") if len(arguments) == 1: - message = ("What facet of element \"" + arguments[0] - + "\" would you like to delete?") + message = ('What facet of element "' + arguments[0] + + '" would you like to delete?') elif len(arguments) != 2: message = "You may only specify an element and a facet." else: element, facet = arguments if element not in universe.contents: - message = "The \"" + element + "\" element does not exist." + message = 'The "' + element + '" element does not exist.' elif facet not in universe.contents[element].facets(): - message = ("The \"" + element + "\" element has no \"" + facet - + "\" facet.") + message = ('The "' + element + '" element has no "' + facet + + '" facet.') else: universe.contents[element].remove_facet(facet) - message = ("You have successfully deleted the \"" + facet - + "\" facet of element \"" + element - + "\". Try \"show element " + - element + "\" for verification.") + message = ('You have successfully deleted the "' + facet + + '" facet of element "' + element + + '". Try "show element ' + + element + '" for verification.') actor.send(message) @@ -2298,7 +2358,7 @@ def command_error(actor, input_data): # 90% of the time use a generic error if random.randrange(10): - message = "I'm not sure what \"" + input_data + "\" means..." + message = '''I'm not sure what "''' + input_data + '''" means...''' # 10% of the time use the classic diku error else: @@ -2312,7 +2372,8 @@ def daemonize(universe): """Fork and disassociate from everything.""" # only if this is what we're configured to do - if universe.contents["mudpy.process"].get("daemon"): + if "mudpy.process" in universe.contents and universe.contents[ + "mudpy.process"].get("daemon"): # log before we start forking around, so the terminal gets the message log("Disassociating from the controlling terminal.") @@ -2349,7 +2410,10 @@ def create_pidfile(universe): """Write a file containing the current process ID.""" pid = str(os.getpid()) log("Process ID: " + pid) - file_name = universe.contents["mudpy.process"].get("pidfile") + if "mudpy.process" in universe.contents: + file_name = universe.contents["mudpy.process"].get("pidfile", "") + else: + file_name = "" if file_name: if not os.path.isabs(file_name): file_name = os.path.join(universe.startdir, file_name) @@ -2361,7 +2425,10 @@ def create_pidfile(universe): def remove_pidfile(universe): """Remove the file containing the current process ID.""" - file_name = universe.contents["mudpy.process"].get("pidfile") + if "mudpy.process" in universe.contents: + file_name = universe.contents["mudpy.process"].get("pidfile", "") + else: + file_name = "" if file_name: if not os.path.isabs(file_name): file_name = os.path.join(universe.startdir, file_name)