X-Git-Url: https://mudpy.org/gitweb?a=blobdiff_plain;f=lib%2Fmuff%2Fmuffuser.py;h=7a5b898857c4d6d6c5c5fc208b7d75a96fbfde92;hb=0c835a0a43cc3c7782f46857d72e518539725eca;hp=98e10a6cf25d74f0c502a88db5b3b9aa8737c477;hpb=76660ba460dec6c0c70dcad634afe79c09fa3a27;p=mudpy.git diff --git a/lib/muff/muffuser.py b/lib/muff/muffuser.py index 98e10a6..7a5b898 100644 --- a/lib/muff/muffuser.py +++ b/lib/muff/muffuser.py @@ -1,103 +1,386 @@ """User objects for the MUFF Engine""" -# Copyright (c) 2005 MUDpy, The Fungi -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# - Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# - Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# Copyright (c) 2005 MUDpy, The Fungi , 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 - self.state = "entering account name" + + # 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): - filename = muffconf.config_data.get("general", "account_path") + "/" + self.name + """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): - filename = muffconf.config_data.get("general", "account_path") + "/" + self.proposed_name + """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: - filename = muffconf.config_data.get("general", "account_path") + "/" + self.name.lower() + + # 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) - record_file = file(filename, "w") + + # 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): - self.send(muffmenu.get_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)"): - if output: - output = "$(eol)" + output + eol - output = string.replace(output, "$(eol)", "\r\n") - output = string.replace(output, "$(div)", "\r\n\r\n") - 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") - output = string.replace(output, "$(account)", self.name) - output = muffmisc.wrap_ansi_text(output, 80) - self.output_queue.append(output) - try: - self.connection.send(self.output_queue[0]) - self.output_queue.remove(self.output_queue[0]) - self.menu_seen = False - except: - pass + """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