# 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
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
# 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:
"""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()
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."""
"""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 = muffvars.variable_data.getint("counters", "next_actor")
+ except:
+ muffmisc.log("get next_actor failed")
+ counter = 1
+ while muffuniv.element_exists("actor:" + repr(counter)): counter += 1
+ muffvars.variable_data.set("counters", "next_actor", counter + 1)
+ self.avatar = muffuniv.Element("actor:" + repr(counter))
+ 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