Imported from archive.
[mudpy.git] / lib / muff / muffuser.py
index 6881869..75e6459 100644 (file)
@@ -6,7 +6,7 @@
 # user accounts are stored in ini-style files supported by ConfigParser
 import ConfigParser
 
-# test for existence of the account dir with os.listdir and os.mkdir to make it
+# os is used to test for existence of the account dir and, if necessary, make it
 import os
 
 # string.replace is used to perform substitutions for color codes and the like
@@ -45,7 +45,7 @@ class User:
                self.password_tries = 1
 
                # the current state of the user
-               self.state = "entering account name"
+               self.state = "entering_account_name"
 
                # flag to indicate whether a menu has been displayed
                self.menu_seen = False
@@ -65,14 +65,112 @@ class User:
                # flag to indicate the current echo status of the client
                self.echoing = True
 
+               # the active avatar
+               self.avatar = None
+
                # an object containing persistent account data
                self.record = ConfigParser.SafeConfigParser()
 
+       def quit(self):
+               """Log, save, close the connection and remove."""
+               if self.name: message = "User " + self.name
+               else: message = "An unnamed user"
+               message += " logged out."
+               muffmisc.log(message)
+               self.save()
+               self.connection.close()
+               self.remove()
+
+       def reload(self):
+               """Save, load a new user and relocate the connection."""
+
+               # unauthenticated connections get the boot
+               if not self.authenticated:
+                       muffmisc.log("An unauthenticated user was disconnected during reload.")
+                       self.state = "disconnecting"
+
+               # authenticated users
+               else:
+
+                       # save and get out of the list
+                       self.save()
+                       self.remove()
+
+                       # create a new user object
+                       new_user = muffuser.User()
+
+                       # give it the same name
+                       new_user.name = self.name
+
+                       # load from file
+                       new_user.load()
+
+                       # set everything else equivalent
+                       new_user.address = self.address
+                       new_user.last_address = self.last_address
+                       new_user.connection = self.connection
+                       new_user.authenticated = self.authenticated
+                       new_user.password_tries = self.password_tries
+                       new_user.state = self.state
+                       new_user.menu_seen = self.menu_seen
+                       new_user.error = self.error
+                       new_user.input_queue = self.input_queue
+                       new_user.output_queue = self.output_queue
+                       new_user.partial_input = self.partial_input
+                       new_user.echoing = self.echoing
+
+                       # add it to the list
+                       muffvars.userlist.append(new_user)
+
+                       # get rid of the old user object
+                       del(self)
+
+       def replace_old_connections(self):
+               """Disconnect active users with the same name."""
+
+               # the default return value
+               return_value = False
+
+               # iterate over each user in the list
+               for old_user in muffvars.userlist:
+
+                       # the name is the same but it's not us
+                       if old_user.name == self.name and old_user is not self:
+
+                               # make a note of it
+                               muffmisc.log("User " + self.name + " reconnected--closing old connection to " + old_user.address + ".")
+                               old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)")
+                               self.send("$(eol)$(red)Taking over old connection from " + old_user.address + ".$(nrm)")
+
+                               # close the old connection
+                               old_user.connection.close()
+
+                               # replace the old connection with this one
+                               old_user.connection = self.connection
+                               old_user.last_address = old_user.address
+                               old_user.address = self.address
+                               old_user.echoing = self.echoing
+
+                               # take this one out of the list and delete
+                               self.remove()
+                               del(self)
+                               return_value = True
+                               break
+
+               # true if an old connection was replaced, false if not
+               return return_value
+
+       def authenticate(self):
+               """Flag the user as authenticated and disconnect duplicates."""
+               if not self.state is "authenticated":
+                       muffmisc.log("User " + self.name + " logged in.")
+                       self.authenticated = True
+
        def load(self):
                """Retrieve account data from cold storage."""
 
                # what the filename for the user account should be
-               filename = muffconf.config_data.get("files", "accounts") + "/" + self.name
+               filename = muffconf.get("files", "accounts") + "/" + self.name
 
                # try to load the password hash and last connection ipa
                try:
@@ -88,7 +186,7 @@ class User:
                """Retrieve the user's account password hash from storage."""
 
                # what the filename for the user account could be
-               filename = muffconf.config_data.get("files", "accounts") + "/" + self.proposed_name
+               filename = muffconf.get("files", "accounts") + "/" + self.proposed_name
 
                # create a temporary account record object
                temporary_record = ConfigParser.SafeConfigParser()
@@ -97,10 +195,12 @@ class User:
                try:
                        temporary_record.read(filename)
                        self.passhash = temporary_record.get("account", "passhash")
+                       return True
 
                # otherwise, the password hash is empty
                except:
                        self.passhash = ""
+                       return False
 
        def save(self):
                """Record account data to cold storage."""
@@ -118,30 +218,42 @@ class User:
                        self.record.set("account", "last_address", self.address)
 
                        # the account files live here
-                       account_path = muffconf.config_data.get("files", "accounts")
+                       account_path = muffconf.get("files", "accounts")
                        # the filename to which we'll write
                        filename = account_path + "/" + self.name.lower()
 
-                       # if the directory doesn't exist, create it
-                       # TODO: create account_path with 0700 perms
+                       # open the user account file for writing
                        try:
-                               if os.listdir(account_path): pass
-                       except:
-                               os.mkdir(account_path, )
+                               record_file = file(filename, "w")
 
-                       # open the user account file for writing
-                       # TODO: create filename with 0600 perms
-                       record_file = file(filename, "w")
+                       # if the directory doesn't exist, create it first
+                       except IOError:
+                               os.makedirs(account_path)
+                               record_file = file(filename, "w")
 
                        # dump the account data to it
                        self.record.write(record_file)
 
                        # close the user account file
+                       record_file.flush()
                        record_file.close()
 
+                       # set the permissions to 0600
+                       os.chmod(filename, 0600)
+
        def show_menu(self):
                """Send the user their current menu."""
-               self.send(muffmenu.get_menu(self))
+               if not self.menu_seen:
+                       self.menu_choices = muffmenu.get_menu_choices(self)
+                       self.send(muffmenu.get_menu(self.state, self.error, self.echoing, self.menu_choices), "")
+                       self.menu_seen = True
+                       self.error = False
+                       self.adjust_echoing()
+
+       def adjust_echoing(self):
+               """Adjust echoing to match state menu requirements."""
+               if self.echoing and not muffmenu.menu_echo_on(self.state): self.echoing = False
+               elif not self.echoing and muffmenu.menu_echo_on(self.state): self.echoing = True
 
        def remove(self):
                """Remove a user from the list of connected users."""
@@ -151,41 +263,124 @@ class User:
                """Send arbitrary text to a connected user."""
 
                # only when there is actual output
-               if output:
+               #if output:
 
-                       # start with a newline, append the message, then end
-                       # with the optional eol string passed to this function
-                       output = "$(eol)" + output + eol
+               # start with a newline, append the message, then end
+               # with the optional eol string passed to this function
+               # and the ansi escape to return to normal text
+               output = "\r\n" + output + eol + chr(27) + "[0m"
 
-                       # replace eol markers with a crlf
-                       # TODO: search for markers and replace from a dict
-                       output = string.replace(output, "$(eol)", "\r\n")
+               # find and replace macros in the output
+               output = muffmisc.replace_macros(self, output)
 
-                       # replace display markers with ansi escapse sequences
-                       output = string.replace(output, "$(bld)", chr(27)+"[1m")
-                       output = string.replace(output, "$(nrm)", chr(27)+"[0m")
-                       output = string.replace(output, "$(blk)", chr(27)+"[30m")
-                       output = string.replace(output, "$(grn)", chr(27)+"[32m")
-                       output = string.replace(output, "$(red)", chr(27)+"[31m")
+               # wrap the text at 80 characters
+               # TODO: prompt user for preferred wrap width
+               output = muffmisc.wrap_ansi_text(output, 80)
 
-                       # the user's account name
-                       output = string.replace(output, "$(account)", self.name)
+               # drop the formatted output into the output queue
+               self.output_queue.append(output)
 
-                       # wrap the text at 80 characters
-                       # TODO: prompt user for preferred wrap width
-                       output = muffmisc.wrap_ansi_text(output, 80)
+               # try to send the last item in the queue, remove it and
+               # flag that menu display is not needed
+               try:
+                       self.connection.send(self.output_queue[0])
+                       self.output_queue.remove(self.output_queue[0])
+                       self.menu_seen = False
 
-                       # drop the formatted output into the output queue
-                       self.output_queue.append(output)
+               # but if we can't, that's okay too
+               except:
+                       pass
 
-                       # try to send the last item in the queue, remove it and
-                       # flag that menu display is not needed
-                       try:
-                               self.connection.send(self.output_queue[0])
-                               self.output_queue.remove(self.output_queue[0])
-                               self.menu_seen = False
+       def pulse(self):
+               """All the things to do to the user per increment."""
+
+               # if the world is terminating, disconnect
+               if muffvars.terminate_world:
+                       self.state = "disconnecting"
+                       self.menu_seen = False
+
+               # show the user a menu as needed
+               self.show_menu()
+
+               # disconnect users with the appropriate state
+               if self.state == "disconnecting":
+                       self.quit()
+
+               # the user is unique and not flagged to disconnect
+               else:
+               
+                       # check for input and add it to the queue
+                       self.enqueue_input()
+
+                       # there is input waiting in the queue
+                       if self.input_queue: muffcmds.handle_user_input(self)
+
+       def enqueue_input(self):
+               """Process and enqueue any new input."""
+
+               # check for some input
+               try:
+                       input_data = self.connection.recv(1024)
+               except:
+                       input_data = ""
+
+               # we got something
+               if input_data:
+
+                       # tack this on to any previous partial
+                       self.partial_input += input_data
+
+                       # separate multiple input lines
+                       new_input_lines = self.partial_input.split("\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"):
+                               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 = ""
+
+                       # iterate over the remaining lines
+                       for line in new_input_lines:
+
+                               # filter out non-printables
+                               line = filter(lambda x: x>=' ' and x<='~', line)
+
+                               # strip off extra whitespace
+                               line = line.strip()
 
-                       # but if we can't, that's okay too
-                       except:
-                               pass
+                               # put on the end of the queue
+                               self.input_queue.append(line)
+
+       def new_avatar(self):
+               """Instantiate a new, unconfigured avatar for this user."""
+               try:
+                       counter = muffuniv.universe.internals["counters"].getint("next_actor")
+               except:
+                       muffmisc.log("get next_actor failed")
+                       counter = 1
+               while muffuniv.element_exists("actor:" + repr(counter)): counter += 1
+               muffuniv.universe.internals["counters"].set("next_actor", counter + 1)
+               self.avatar = muffuniv.Element("actor:" + repr(counter), muffconf.get("files", "avatars"), muffuniv.universe)
+               try:
+                       avatars = self.record.get("account", "avatars").split()
+               except:
+                       avatars = []
+               avatars.append(self.avatar.key)
+               self.record.set("account", "avatars", " ".join(avatars))
+
+       def list_avatar_names(self):
+               """A test function to list names of assigned avatars."""
+               try:
+                       avatars = self.record.get("account", "avatars").split()
+               except:
+                       avatars = []
+               avatar_names = []
+               for avatar in avatars:
+                       avatar_names.append(muffuniv.universe.contents[avatar].get("name"))
+               return avatar_names