+++ /dev/null
-"""User objects for the MUFF Engine"""
-
-# Copyright (c) 2005 mudpy, Jeremy Stanley <fungi@yuggoth.org>, all rights reserved.
-# Licensed per terms in the LICENSE file distributed with this software.
-
-# user accounts are stored in ini-style files supported by ConfigParser
-import ConfigParser
-
-# 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
-import string
-
-# hack to load all modules in the muff package
-import muff
-for module in muff.__all__:
- exec("import " + module)
-
-class User:
- """This is a connected user."""
-
- def __init__(self):
- """Default values for the in-memory user variables."""
-
- # the account name
- self.name = ""
-
- # the password hash
- self.passhash = ""
-
- # the current client ip address
- self.address = ""
-
- # the previous client ip address
- self.last_address = ""
-
- # the current socket connection object
- self.connection = None
-
- # a flag to denote whether the user is authenticated
- self.authenticated = False
-
- # number of times password entry has failed during this session
- self.password_tries = 1
-
- # the current state of the user
- self.state = "entering_account_name"
-
- # flag to indicate whether a menu has been displayed
- self.menu_seen = False
-
- # current error condition, if any
- self.error = ""
-
- # fifo-style queue for lines of user input
- self.input_queue = []
-
- # fifo-style queue for blocks of user output
- self.output_queue = []
-
- # holding pen for unterminated user input
- self.partial_input = ""
-
- # 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.get("files", "accounts") + "/" + self.name
-
- # try to load the password hash and last connection ipa
- try:
- self.record.read(filename)
- self.passhash = self.record.get("account", "passhash")
- self.last_address = self.record.get("account", "last_address", self.address)
-
- # if we can't, that's okay too
- except:
- pass
-
- def get_passhash(self):
- """Retrieve the user's account password hash from storage."""
-
- # what the filename for the user account could be
- filename = muffconf.get("files", "accounts") + "/" + self.proposed_name
-
- # create a temporary account record object
- temporary_record = ConfigParser.SafeConfigParser()
-
- # try to load the indicated account and get a password hash
- 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."""
-
- # the user account must be authenticated to save
- if self.authenticated:
-
- # create an account section if it doesn't exist
- if not self.record.has_section("account"):
- self.record.add_section("account")
-
- # write some in-memory data to the record
- self.record.set("account", "name", self.name)
- self.record.set("account", "passhash", self.passhash)
- self.record.set("account", "last_address", self.address)
-
- # the account files live here
- account_path = muffconf.get("files", "accounts")
- # the filename to which we'll write
- filename = account_path + "/" + self.name.lower()
-
- # open the user account file for writing
- try:
- 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."""
- 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."""
- muffvars.userlist.remove(self)
-
- def send(self, output, eol="$(eol)"):
- """Send arbitrary text to a connected user."""
-
- # only when there is actual output
- #if output:
-
- # 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"
-
- # find and replace macros in the output
- output = muffmisc.replace_macros(self, output)
-
- # wrap the text at 80 characters
- # TODO: prompt user for preferred wrap width
- output = muffmisc.wrap_ansi_text(output, 80)
-
- # drop the formatted output into the output queue
- self.output_queue.append(output)
-
- # 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
-
- # but if we can't, that's okay too
- except:
- pass
-
- 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()
-
- # 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
-