Fix reload to use a copy of datafile keys
[mudpy.git] / lib / mudpy / misc.py
index 73f0253..d78eac6 100644 (file)
@@ -1,10 +1,26 @@
 # -*- coding: utf-8 -*-
 """Miscellaneous functions for the mudpy engine."""
 
-# Copyright (c) 2004-2011 Jeremy Stanley <fungi@yuggoth.org>. Permission
+# Copyright (c) 2004-2014 Jeremy Stanley <fungi@yuggoth.org>. Permission
 # to use, copy, modify, and distribute this software is granted under
 # terms provided in the LICENSE file distributed with this software.
 
+import codecs
+import ctypes
+import ctypes.util
+import os
+import random
+import re
+import signal
+import socket
+import sys
+import syslog
+import time
+import traceback
+import unicodedata
+
+import mudpy
+
 
 class Element:
 
@@ -12,8 +28,6 @@ class Element:
 
     def __init__(self, key, universe, filename=None):
         """Set up a new element."""
-        import data
-        import os.path
 
         # keep track of our key name
         self.key = key
@@ -56,7 +70,7 @@ class Element:
 
             # add the file if it doesn't exist yet
             if filename not in self.universe.files:
-                data.DataFile(filename, self.universe)
+                mudpy.data.DataFile(filename, self.universe)
 
         # record or reset a pointer to the origin file
         self.origin = self.universe.files[filename]
@@ -71,7 +85,7 @@ class Element:
 
     def reload(self):
         """Create a new element and replace this one."""
-        new_element = Element(self.key, self.universe, self.origin.filename)
+        Element(self.key, self.universe, self.origin.filename)
         del(self)
 
     def destroy(self):
@@ -142,7 +156,7 @@ class Element:
             return default
 
     def getint(self, facet, default=None):
-        """Return values as int/long type."""
+        """Return values as int type."""
         if default is None:
             default = 0
         if self.origin.data.has_option(self.key, facet):
@@ -169,56 +183,40 @@ class Element:
 
     def getlist(self, facet, default=None):
         """Return values as list type."""
-        import data
         if default is None:
             default = []
         value = self.get(facet)
         if value:
-            return data.makelist(value)
+            return mudpy.data.makelist(value)
         else:
             return default
 
     def getdict(self, facet, default=None):
         """Return values as dict type."""
-        import data
         if default is None:
             default = {}
         value = self.get(facet)
         if value:
-            return data.makedict(value)
+            return mudpy.data.makedict(value)
         else:
             return default
 
     def set(self, facet, value):
         """Set values."""
         if not self.has_facet(facet) or not self.get(facet) == value:
-            if type(value) is long or repr(type(value)) == "<type 'unicode'>":
-                value = str(value)
-            elif not type(value) is str:
+            if not type(value) is str:
                 value = repr(value)
             self.origin.data.set(self.key, facet, value)
             self.origin.modified = True
 
     def append(self, facet, value):
-        """Append value tp a list."""
-        if type(value) is long:
-            value = str(value)
-        elif not type(value) is str:
+        """Append value to a list."""
+        if not type(value) is str:
             value = repr(value)
         newlist = self.getlist(facet)
         newlist.append(value)
         self.set(facet, newlist)
 
-    def new_event(self, action, when=None):
-        """Create, attach and enqueue an event element."""
-
-        # if when isn't specified, that means now
-        if not when:
-            when = self.universe.get_time()
-
-        # events are elements themselves
-        event = Element("event:" + self.key + ":" + counter)
-
     def send(
         self,
         message,
@@ -350,7 +348,7 @@ class Element:
             description = element.get("description")
             if description:
                 message += description + "$(eol)"
-            portal_list = element.portals().keys()
+            portal_list = list(element.portals().keys())
             if portal_list:
                 portal_list.sort()
                 message += "$(cyn)[ Exits: " + ", ".join(
@@ -371,7 +369,6 @@ class Element:
 
     def portals(self):
         """Map the portal directions for a room to neighbors."""
-        import re
         portals = {}
         if re.match("""^location:-?\d+,-?\d+,-?\d+$""", self.key):
             coordinates = [(int(x))
@@ -421,14 +418,10 @@ class Universe:
 
     def __init__(self, filename="", load=False):
         """Initialize the universe."""
-        import os
-        import os.path
         self.categories = {}
         self.contents = {}
         self.default_origins = {}
         self.loglines = []
-        self.pending_events_long = {}
-        self.pending_events_short = {}
         self.private_files = []
         self.reload_flag = False
         self.startdir = os.getcwd()
@@ -457,7 +450,6 @@ class Universe:
 
     def load(self):
         """Load universe data from persistent storage."""
-        import data
 
         # the files dict must exist and filename needs to be read-only
         if not hasattr(
@@ -470,12 +462,12 @@ class Universe:
 
             # clear out all read-only files
             if hasattr(self, "files"):
-                for data_filename in self.files.keys():
+                for data_filename in list(self.files.keys()):
                     if not self.files[data_filename].is_writeable():
                         del self.files[data_filename]
 
             # start loading from the initial file
-            data.DataFile(self.filename, self)
+            mudpy.data.DataFile(self.filename, self)
 
         # make a list of inactive avatars
         inactive_avatars = []
@@ -519,7 +511,6 @@ class Universe:
 
     def initialize_server_socket(self):
         """Create and open the listening socket."""
-        import socket
 
         # need to know the local address and port number for the listener
         host = self.categories["internal"]["network"].get("host")
@@ -575,7 +566,6 @@ class User:
 
     def __init__(self):
         """Default values for the in-memory user variables."""
-        import telnet
         self.account = None
         self.address = ""
         self.authenticated = False
@@ -590,7 +580,7 @@ class User:
         self.menu_seen = False
         self.negotiation_pause = 0
         self.output_queue = []
-        self.partial_input = ""
+        self.partial_input = b""
         self.password_tries = 0
         self.state = "initial"
         self.telopts = {}
@@ -755,12 +745,14 @@ class User:
 
     def adjust_echoing(self):
         """Adjust echoing to match state menu requirements."""
-        import telnet
-        if telnet.is_enabled(self, telnet.TELOPT_ECHO, telnet.US):
+        if mudpy.telnet.is_enabled(self, mudpy.telnet.TELOPT_ECHO,
+                                   mudpy.telnet.US):
             if menu_echo_on(self.state):
-                telnet.disable(self, telnet.TELOPT_ECHO, telnet.US)
+                mudpy.telnet.disable(self, mudpy.telnet.TELOPT_ECHO,
+                                     mudpy.telnet.US)
         elif not menu_echo_on(self.state):
-            telnet.enable(self, telnet.TELOPT_ECHO, telnet.US)
+            mudpy.telnet.enable(self, mudpy.telnet.TELOPT_ECHO,
+                                mudpy.telnet.US)
 
     def remove(self):
         """Remove a user from the list of connected users."""
@@ -778,7 +770,6 @@ class User:
         prepend_padding=True
     ):
         """Send arbitrary text to a connected user."""
-        import telnet
 
         # unless raw mode is on, clean it up all nice and pretty
         if not raw:
@@ -798,15 +789,15 @@ class User:
             # and the ansi escape to return to normal text
             if not just_prompt and prepend_padding:
                 if not self.output_queue \
-                   or not self.output_queue[-1].endswith("\r\n"):
+                   or not self.output_queue[-1].endswith(b"\r\n"):
                     output = "$(eol)" + output
                 elif not self.output_queue[-1].endswith(
-                    "\r\n\x1b[0m\r\n"
+                    b"\r\n\x1b[0m\r\n"
                 ) and not self.output_queue[-1].endswith(
-                    "\r\n\r\n"
+                    b"\r\n\r\n"
                 ):
                     output = "$(eol)" + output
-            output += eol + unichr(27) + "[0m"
+            output += eol + chr(27) + "[0m"
 
             # tack on a prompt if active
             if self.state == "active":
@@ -830,7 +821,8 @@ class User:
                 output = wrap_ansi_text(output, wrap)
 
             # if supported by the client, encode it utf-8
-            if telnet.is_enabled(self, telnet.TELOPT_BINARY, telnet.US):
+            if mudpy.telnet.is_enabled(self, mudpy.telnet.TELOPT_BINARY,
+                                       mudpy.telnet.US):
                 encoded_output = output.encode("utf-8")
 
             # otherwise just send ascii
@@ -839,12 +831,14 @@ class User:
 
             # end with a terminator if requested
             if add_prompt or add_terminator:
-                if telnet.is_enabled(self, telnet.TELOPT_EOR, telnet.US):
-                    encoded_output += telnet.telnet_proto(telnet.IAC,
-                                                          telnet.EOR)
-                elif not telnet.is_enabled(self, telnet.TELOPT_SGA, telnet.US):
-                    encoded_output += telnet.telnet_proto(telnet.IAC,
-                                                          telnet.GA)
+                if mudpy.telnet.is_enabled(
+                        self, mudpy.telnet.TELOPT_EOR, mudpy.telnet.US):
+                    encoded_output += mudpy.telnet.telnet_proto(
+                        mudpy.telnet.IAC, mudpy.telnet.EOR)
+                elif not mudpy.telnet.is_enabled(
+                        self, mudpy.telnet.TELOPT_SGA, mudpy.telnet.US):
+                    encoded_output += mudpy.telnet.telnet_proto(
+                        mudpy.telnet.IAC, mudpy.telnet.GA)
 
             # and tack it onto the queue
             self.output_queue.append(encoded_output)
@@ -912,14 +906,12 @@ class User:
 
     def enqueue_input(self):
         """Process and enqueue any new input."""
-        import telnet
-        import unicodedata
 
         # check for some input
         try:
             raw_input = self.connection.recv(1024)
         except:
-            raw_input = ""
+            raw_input = b""
 
         # we got something
         if raw_input:
@@ -928,21 +920,21 @@ class User:
             self.partial_input += raw_input
 
             # reply to and remove any IAC negotiation codes
-            telnet.negotiate_telnet_options(self)
+            mudpy.telnet.negotiate_telnet_options(self)
 
             # separate multiple input lines
-            new_input_lines = self.partial_input.split("\n")
+            new_input_lines = self.partial_input.split(b"\n")
 
             # if input doesn't end in a newline, replace the
             # held partial input with the last line of it
-            if not self.partial_input.endswith("\n"):
+            if not self.partial_input.endswith(b"\n"):
                 self.partial_input = new_input_lines.pop()
 
             # otherwise, chop off the extra null input and reset
             # the held partial input
             else:
                 new_input_lines.pop()
-                self.partial_input = ""
+                self.partial_input = b""
 
             # iterate over the remaining lines
             for line in new_input_lines:
@@ -951,8 +943,10 @@ class User:
                 line = line.strip()
 
                 # log non-printable characters remaining
-                if telnet.is_enabled(self, telnet.TELOPT_BINARY, telnet.HIM):
-                    asciiline = filter(lambda x: " " <= x <= "~", line)
+                if mudpy.telnet.is_enabled(self, mudpy.telnet.TELOPT_BINARY,
+                                           mudpy.telnet.HIM):
+                    asciiline = b"".join(
+                        filter(lambda x: b" " <= x <= b"~", line))
                     if line != asciiline:
                         logline = "Non-ASCII characters from "
                         if self.account and self.account.get("name"):
@@ -963,10 +957,22 @@ class User:
                         log(logline, 4)
                         line = asciiline
 
+                try:
+                    line = line.decode("utf-8")
+                except UnicodeDecodeError:
+                    logline = "Non-UTF-8 characters from "
+                    if self.account and self.account.get("name"):
+                        logline += self.account.get("name") + ": "
+                    else:
+                        logline += "unknown user: "
+                    logline += repr(line)
+                    log(logline, 4)
+                    return
+
+                line = unicodedata.normalize("NFKC", line)
+
                 # put on the end of the queue
-                self.input_queue.append(
-                    unicodedata.normalize("NFKC", line.decode("utf-8"))
-                )
+                self.input_queue.append(line)
 
     def new_avatar(self):
         """Instantiate a new, unconfigured avatar for this user."""
@@ -1040,10 +1046,6 @@ def broadcast(message, add_prompt=True):
 
 def log(message, level=0):
     """Log a message."""
-    import codecs
-    import os.path
-    import syslog
-    import time
 
     # a couple references we need
     file_name = universe.categories["internal"]["logging"].get("file")
@@ -1157,7 +1159,6 @@ def get_loglines(level, start, stop):
 
 def glyph_columns(character):
     """Convenience function to return the column width of a glyph."""
-    import unicodedata
     if unicodedata.east_asian_width(character) in "FW":
         return 2
     else:
@@ -1166,7 +1167,6 @@ def glyph_columns(character):
 
 def wrap_ansi_text(text, width):
     """Wrap text with arbitrary width while ignoring ANSI colors."""
-    import unicodedata
 
     # the current position in the entire text string, including all
     # characters, printable or otherwise
@@ -1253,7 +1253,6 @@ def wrap_ansi_text(text, width):
 
 def weighted_choice(data):
     """Takes a dict weighted by value and returns a random key."""
-    import random
 
     # this will hold our expanded list of keys from the data
     expanded = []
@@ -1269,7 +1268,6 @@ def weighted_choice(data):
 
 def random_name():
     """Returns a random character name."""
-    import random
 
     # the vowels and consonants needed to create romaji syllables
     vowels = [
@@ -1321,9 +1319,6 @@ def random_name():
 
 def replace_macros(user, text, is_input=False):
     """Replaces macros in text output."""
-    import codecs
-    import data
-    import os.path
 
     # third person pronouns
     pronouns = {
@@ -1335,15 +1330,15 @@ def replace_macros(user, text, is_input=False):
     # a dict of replacement macros
     macros = {
         "eol": "\r\n",
-        "bld": unichr(27) + "[1m",
-        "nrm": unichr(27) + "[0m",
-        "blk": unichr(27) + "[30m",
-        "blu": unichr(27) + "[34m",
-        "cyn": unichr(27) + "[36m",
-        "grn": unichr(27) + "[32m",
-        "mgt": unichr(27) + "[35m",
-        "red": unichr(27) + "[31m",
-        "yel": unichr(27) + "[33m",
+        "bld": chr(27) + "[1m",
+        "nrm": chr(27) + "[0m",
+        "blk": chr(27) + "[30m",
+        "blu": chr(27) + "[34m",
+        "cyn": chr(27) + "[36m",
+        "grn": chr(27) + "[32m",
+        "mgt": chr(27) + "[35m",
+        "red": chr(27) + "[31m",
+        "yel": chr(27) + "[33m",
     }
 
     # add dynamic macros where possible
@@ -1372,7 +1367,7 @@ def replace_macros(user, text, is_input=False):
 
         # this is how we handle local file inclusion (dangerous!)
         elif macro.startswith("inc:"):
-            incfile = data.find_file(macro[4:], universe=universe)
+            incfile = mudpy.data.find_file(macro[4:], universe=universe)
             if os.path.exists(incfile):
                 incfd = codecs.open(incfile, "r", "utf-8")
                 replacement = ""
@@ -1420,7 +1415,6 @@ def first_word(text, separator=" "):
 
 def on_pulse():
     """The things which should happen on each pulse, aside from reloads."""
-    import time
 
     # open the listening socket if it hasn't been already
     if not hasattr(universe, "listening_socket"):
@@ -1495,7 +1489,6 @@ def reload_data():
 
 def check_for_connection(listening_socket):
     """Check for a waiting connection and return a new user object."""
-    import telnet
 
     # try to accept a new connection
     try:
@@ -1519,7 +1512,7 @@ def check_for_connection(listening_socket):
     user.address = address[0]
 
     # let the client know we WILL EOR (RFC 885)
-    telnet.enable(user, telnet.TELOPT_EOR, telnet.US)
+    mudpy.telnet.enable(user, mudpy.telnet.TELOPT_EOR, mudpy.telnet.US)
     user.negotiation_pause = 2
 
     # return the new user object
@@ -1645,7 +1638,7 @@ def get_menu_choices(user):
 def get_formatted_menu_choices(state, choices):
     """Returns a formatted string of menu choices."""
     choice_output = ""
-    choice_keys = choices.keys()
+    choice_keys = list(choices.keys())
     choice_keys.sort()
     for choice in choice_keys:
         choice_output += "   [$(red)" + choice + "$(nrm)]  " + choices[
@@ -1712,10 +1705,10 @@ def get_choice_action(user, choice):
 
 def handle_user_input(user):
     """The main handler, branches to a state-specific handler."""
-    import telnet
 
     # if the user's client echo is off, send a blank line for aesthetics
-    if telnet.is_enabled(user, telnet.TELOPT_ECHO, telnet.US):
+    if mudpy.telnet.is_enabled(user, mudpy.telnet.TELOPT_ECHO,
+                               mudpy.telnet.US):
         user.send("", add_prompt=False, prepend_padding=False)
 
     # check to make sure the state is expected, then call that handler
@@ -1765,9 +1758,9 @@ def handler_entering_account_name(user):
         name = input_data.lower()
 
         # fail if there are non-alphanumeric characters
-        if name != filter(
-           lambda x: x >= "0" and x <= "9" or x >= "a" and x <= "z", name
-           ):
+        if name != "".join(filter(
+                lambda x: x >= "0" and x <= "9" or x >= "a" and x <= "z",
+                name)):
             user.error = "bad_name"
 
         # if that account exists, time to request a password
@@ -1789,13 +1782,12 @@ def handler_entering_account_name(user):
 
 def handler_checking_password(user):
     """Handle the login account password."""
-    import password
 
     # get the next waiting line of input
     input_data = user.input_queue.pop(0)
 
     # does the hashed input equal the stored hash?
-    if password.verify(input_data, user.account.get("passhash")):
+    if mudpy.password.verify(input_data, user.account.get("passhash")):
 
         # if so, set the username and load from cold storage
         if not user.replace_old_connections():
@@ -1823,7 +1815,6 @@ def handler_checking_password(user):
 
 def handler_entering_new_password(user):
     """Handle a new password entry."""
-    import password
 
     # get the next waiting line of input
     input_data = user.input_queue.pop(0)
@@ -1831,15 +1822,15 @@ def handler_entering_new_password(user):
     # 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(
-       filter(lambda x: x >= "0" and x <= "9", input_data)
+       list(filter(lambda x: x >= "0" and x <= "9", input_data))
        ) and len(
-        filter(lambda x: x >= "A" and x <= "Z", input_data)
+        list(filter(lambda x: x >= "A" and x <= "Z", input_data))
     ) and len(
-        filter(lambda x: x >= "a" and x <= "z", input_data)
+        list(filter(lambda x: x >= "a" and x <= "z", input_data))
     ):
 
         # hash and store it, then move on to verification
-        user.account.set("passhash", password.create(input_data))
+        user.account.set("passhash", mudpy.password.create(input_data))
         user.state = "verifying_new_password"
 
     # the password was weak, try again if you haven't tried too many times
@@ -1864,13 +1855,12 @@ def handler_entering_new_password(user):
 
 def handler_verifying_new_password(user):
     """Handle the re-entered new password for verification."""
-    import password
 
     # get the next waiting line of input
     input_data = user.input_queue.pop(0)
 
     # hash the input and match it to storage
-    if password.verify(input_data, user.account.get("passhash")):
+    if mudpy.password.verify(input_data, user.account.get("passhash")):
         user.authenticate()
 
         # the hashes matched, so go active
@@ -2042,7 +2032,7 @@ def command_help(actor, parameters):
 
         # give a sorted list of commands with descriptions if provided
         output = "These are the commands available to you:$(eol)$(eol)"
-        sorted_commands = universe.categories["command"].keys()
+        sorted_commands = list(universe.categories["command"].keys())
         sorted_commands.sort()
         for item in sorted_commands:
             command = universe.categories["command"][item]
@@ -2080,7 +2070,6 @@ def command_look(actor, parameters):
 
 def command_say(actor, parameters):
     """Speak to others in the same room."""
-    import unicodedata
 
     # check for replacement macros and escape them
     parameters = escape_macros(parameters)
@@ -2171,7 +2160,6 @@ def command_chat(actor):
 
 def command_show(actor, parameters):
     """Show program data."""
-    import re
     message = ""
     arguments = parameters.split()
     if not parameters:
@@ -2182,13 +2170,13 @@ def command_show(actor, parameters):
         ) + " increments elapsed since the world was created."
     elif arguments[0] == "categories":
         message = "These are the element categories:$(eol)"
-        categories = universe.categories.keys()
+        categories = list(universe.categories.keys())
         categories.sort()
         for category in categories:
             message += "$(eol)   $(grn)" + category + "$(nrm)"
     elif arguments[0] == "files":
         message = "These are the current files containing the universe:$(eol)"
-        filenames = universe.files.keys()
+        filenames = list(universe.files.keys())
         filenames.sort()
         for filename in filenames:
             if universe.files[filename].is_writeable():
@@ -2395,7 +2383,6 @@ def command_delete(actor, parameters):
 
 def command_error(actor, input_data):
     """Generic error for an unrecognized command word."""
-    import random
 
     # 90% of the time use a generic error
     if random.randrange(10):
@@ -2411,20 +2398,13 @@ def command_error(actor, input_data):
 
 def daemonize(universe):
     """Fork and disassociate from everything."""
-    import codecs
-    import ctypes
-    import ctypes.util
-    import os
-    import os.path
-    import sys
 
     # only if this is what we're configured to do
     if universe.contents["internal:process"].getboolean("daemon"):
 
         # if possible, we want to rename the process to the same as the script
-        # (these will need to be byte type during 2to3 migration)
-        new_argv = "\0".join(sys.argv) + "\0"
-        new_short_argv0 = os.path.basename(sys.argv[0]) + "\0"
+        new_argv = b"\x00".join(x.encode("utf-8") for x in sys.argv) + b"\x00"
+        short_argv0 = os.path.basename(sys.argv[0]).encode("utf-8") + b"\x00"
 
         # attempt the linux way first
         try:
@@ -2443,7 +2423,7 @@ def daemonize(universe):
             ctypes.memmove(argc.contents, new_argv, len(new_argv))
             ctypes.CDLL(ctypes.util.find_library("c")).prctl(
                 15,
-                new_short_argv0,
+                short_argv0,
                 0,
                 0,
                 0
@@ -2497,9 +2477,6 @@ def daemonize(universe):
 
 def create_pidfile(universe):
     """Write a file containing the current process ID."""
-    import codecs
-    import os
-    import os.path
     pid = str(os.getpid())
     log("Process ID: " + pid)
     file_name = universe.contents["internal:process"].get("pidfile")
@@ -2514,8 +2491,6 @@ def create_pidfile(universe):
 
 def remove_pidfile(universe):
     """Remove the file containing the current process ID."""
-    import os
-    import os.path
     file_name = universe.contents["internal:process"].get("pidfile")
     if file_name:
         if not os.path.isabs(file_name):
@@ -2526,7 +2501,6 @@ def remove_pidfile(universe):
 
 def excepthook(excepttype, value, tracebackdata):
     """Handle uncaught exceptions."""
-    import traceback
 
     # assemble the list of errors into a single string
     message = "".join(
@@ -2548,7 +2522,6 @@ def excepthook(excepttype, value, tracebackdata):
 
 def sighook(what, where):
     """Handle external signals."""
-    import signal
 
     # a generic message
     message = "Caught signal: "
@@ -2573,20 +2546,17 @@ def sighook(what, where):
 
 def override_excepthook():
     """Redefine sys.excepthook with our own."""
-    import sys
     sys.excepthook = excepthook
 
 
 def assign_sighook():
     """Assign a customized handler for some signals."""
-    import signal
     signal.signal(signal.SIGHUP, sighook)
     signal.signal(signal.SIGTERM, sighook)
 
 
 def setup():
     """This contains functions to be performed when starting the engine."""
-    import sys
 
     # see if a configuration file was specified
     if len(sys.argv) > 1: