Log User object ID even when account is an admin
[mudpy.git] / mudpy / misc.py
index 76f4099..eea2f00 100644 (file)
@@ -5,6 +5,7 @@
 # provided in the LICENSE file distributed with this software.
 
 import codecs
+import datetime
 import os
 import random
 import re
@@ -456,10 +457,8 @@ class Universe:
         self.listening_socket.listen(1)
 
         # note that we're now ready for user connections
-        log(
-            "Listening for Telnet connections on: " +
-            host + ":" + str(port)
-        )
+        log("Listening for Telnet connections on %s port %s" % (
+                host, str(port)))
 
     def get_time(self):
         """Convenience method to get the elapsed time counter."""
@@ -638,14 +637,13 @@ class User:
         """Flag the user as authenticated and disconnect duplicates."""
         if self.state != "authenticated":
             self.authenticated = True
+            log("User %s authenticated for account %s." % (
+                    self, self.account.subkey), 2)
             if ("mudpy.limit" in universe.contents and self.account.subkey in
                     universe.contents["mudpy.limit"].get("admins")):
                 self.account.set("administrator", True)
-                log("Administrator %s authenticated." %
-                    self.account.get("name"), 2)
-            else:
-                log("User %s authenticated for account %s." % (
-                        self, self.account.subkey), 2)
+                log("Account %s is an administrator." % (
+                        self.account.subkey), 2)
 
     def show_menu(self):
         """Send the user their current menu."""
@@ -660,6 +658,30 @@ class User:
             self.error = False
             self.adjust_echoing()
 
+    def prompt(self):
+        """"Generate and return an input prompt."""
+
+        # Start with the user's preference, if one was provided
+        prompt = self.account.get("prompt")
+
+        # If the user has not set a prompt, then immediately return the default
+        # provided for the current state
+        if not prompt:
+            return get_menu_prompt(self.state)
+
+        # Allow including the World clock state
+        if "$_(time)" in prompt:
+            prompt = prompt.replace(
+                "$_(time)",
+                str(universe.groups["internal"]["counters"].get("elapsed")))
+
+        # Append a single space for clear separation from user input
+        if prompt[-1] != " ":
+            prompt = "%s " % prompt
+
+        # Return the cooked prompt
+        return prompt
+
     def adjust_echoing(self):
         """Adjust echoing to match state menu requirements."""
         if mudpy.telnet.is_enabled(self, mudpy.telnet.TELOPT_ECHO,
@@ -722,7 +744,7 @@ class User:
                 if not just_prompt:
                     output += "$(eol)"
                 if add_prompt:
-                    output += self.account.get("prompt", ">") + " "
+                    output += self.prompt()
                     mode = self.avatar.get("mode")
                     if mode:
                         output += "(" + mode + ") "
@@ -986,7 +1008,7 @@ def log(message, level=0):
         file_name = ""
         max_log_lines = 0
         syslog_name = ""
-    timestamp = time.asctime()[4:19]
+    timestamp = datetime.datetime.now().isoformat(' ')
 
     # turn the message into a list of nonempty lines
     lines = [x for x in [(x.rstrip()) for x in message.split("\n")] if x != ""]
@@ -1449,6 +1471,27 @@ def check_for_connection(listening_socket):
     return user
 
 
+def find_command(command_name):
+    """Try to find a command by name or abbreviation."""
+
+    # lowercase the command
+    command_name = command_name.lower()
+
+    command = None
+    if command_name in universe.groups["command"]:
+        # the command matches a command word for which we have data
+        command = universe.groups["command"][command_name]
+    else:
+        for candidate in sorted(universe.groups["command"]):
+            if candidate.startswith(command_name) and not universe.groups[
+                    "command"][candidate].get("administrative"):
+                # the command matches the start of a command word and is not
+                # restricted to administrators
+                command = universe.groups["command"][candidate]
+                break
+    return command
+
+
 def get_menu(state, error=None, choices=None):
     """Show the correct menu text to a user."""
 
@@ -1839,21 +1882,36 @@ def handler_active(user):
         else:
             command_name, parameters = first_word(input_data)
 
-        # lowercase the command
-        command_name = command_name.lower()
-
-        # the command matches a command word for which we have data
-        if command_name in universe.groups["command"]:
-            command = universe.groups["command"][command_name]
-        else:
-            command = None
+        # expand to an actual command
+        command = find_command(command_name)
 
         # if it's allowed, do it
+        ran = False
         if actor.can_run(command):
-            exec(command.get("action"))
-
-        # otherwise, give an error
-        elif command_name:
+            # dereference the relative object path for the requested function
+            action = mudpy
+            action_fname = command.get("action", command.key)
+            for component in action_fname.split("."):
+                try:
+                    action = getattr(action, component)
+                    ran = True
+                except AttributeError:
+                    log('Could not find action function "%s" for command "%s"'
+                        % (action_fname, command_name))
+                    action = None
+                    break
+            if action:
+                try:
+                    action(actor, parameters)
+                except Exception:
+                    log('Command string "%s" from user %s raised an '
+                        'exception...\n%s' % (
+                            input_data, actor.owner.account.get("name"),
+                            traceback.format_exc()))
+                    mudpy.command.error(actor, input_data)
+
+        # if the command was not run, give an error
+        if not ran:
             mudpy.command.error(actor, input_data)
 
     # if no input, just idle back with a prompt
@@ -2019,8 +2077,9 @@ def setup():
     log("Import path: %s" % ", ".join(sys.path), 1)
     log("Installed dependencies: %s" % universe.versions.dependencies_text, 1)
     log("Other python packages: %s" % universe.versions.environment_text, 1)
-    log("Started %s with command line: %s" % (
-        universe.versions.version, " ".join(sys.argv)), 1)
+    log("Running version: %s" % universe.versions.version, 1)
+    log("Initial directory: %s" % universe.startdir, 1)
+    log("Command line: %s" % " ".join(sys.argv), 1)
 
     # pass the initialized universe back
     return universe