Imported from archive.
authorJeremy Stanley <fungi@yuggoth.org>
Fri, 5 Aug 2005 15:54:48 +0000 (15:54 +0000)
committerJeremy Stanley <fungi@yuggoth.org>
Fri, 5 Aug 2005 15:54:48 +0000 (15:54 +0000)
* lib/menus/login (disconnecting duplicates), lib/menus/miscellaneous
(disconnecting), lib/muff/muffcmds.py (handler_checking_password):
Messages are now colored in ANSI red.

* lib/muff/muffcmds.py (command_halt): Added a reason message
parameter which gets logged and broadcast to all connected users.
(command_reload): Now gets logged when run.
(command_say): Disallowed replacement macro sequences and refactored
the correction routines, abstracting the punctuation and typo maps
out into configuration.
(command_time): New admin command to report current world time in
elapsed increments.
(handle_user_input): Switched to a generic caller based dynamically
based on the user state names.
(handler_disconnecting, handler_disconnecting_duplicates): Added
code to handle disconnecting an existing socket for a user who
successfully authenticates on a new socket.
(handler_entering_account_name): Now adds a log entry when a user
logs into an account.
(handler_fallthrough): Removed.

* lib/muff/muffconf.py: Refactored the initial config file finding
routines to be more robust.
(get, getfloat, getint, set): New convenience
functions for calling the corresponding Data class methods.

* lib/muff/muffmain.py (main): Ammended the loop to continue, giving
the next pulse an opportunity to more courteously disconnect active
user sockets.

* lib/muff/muffmain.py (main), lib/muff/muffmisc.py (on_pulse):
Relocated more loop content.

* lib/muff/muffmisc.py (broadcast): For aesthetics, an additional
blank line is now prepended to output.
(getlong, repr_long, setlong): Additional functions implemented to
deal with Python's long integer representation format.
(reload_data): New function allowing the admin reload command to
refreshe already instantiated objects by migrating their contents to
new ones and deleting the originals.

* lib/muff/muffuser.py (User.save): The account file is now
unconditionally set to 0600 permissions for additional protection of
the MD5 password hashes.

14 files changed:
lib/commands/active
lib/commands/index [new file with mode: 0644]
lib/menus/index [new file with mode: 0644]
lib/menus/login
lib/menus/miscellaneous
lib/muff/muffcmds.py
lib/muff/muffconf.py
lib/muff/muffmain.py
lib/muff/muffmenu.py
lib/muff/muffmisc.py
lib/muff/muffsock.py
lib/muff/muffuser.py
lib/muff/muffvars.py
muff.conf

index 885520e..c977538 100644 (file)
@@ -18,3 +18,7 @@ help = This will reload all python code modules, reload configuration files and
 description = State something out loud.
 help = This allows you to speak to other characters within the same room. If you end your sentence with specific punctuation, the aparent speech action (ask, exclaim, et cetera) will be adapted accordingly. It will also add punctuation and capitalize your message where needed.
 
+[time]
+description = Show the current world time in elapsed increments.
+help = This will show the current world time in elapsed increments.
+
diff --git a/lib/commands/index b/lib/commands/index
new file mode 100644 (file)
index 0000000..6ab31ad
--- /dev/null
@@ -0,0 +1,3 @@
+[index]
+files = active
+
diff --git a/lib/menus/index b/lib/menus/index
new file mode 100644 (file)
index 0000000..9b5802e
--- /dev/null
@@ -0,0 +1,3 @@
+[index]
+files = account_creation active login miscellaneous
+
index 1acf366..cf2659c 100644 (file)
@@ -7,3 +7,6 @@ prompt = Password:
 echo = off
 error_incorrect = Incorrect password, please try again...
 
+[disconnecting duplicates]
+prompt = $(red)Closing your previous connection...$(nrm)$(eol)
+
index 6c3375b..f93a6c3 100644 (file)
@@ -1,3 +1,3 @@
 [disconnecting]
-description = Disconnecting...
+description = $(red)Disconnecting...$(nrm)
 
index 033c74c..6ade3b1 100644 (file)
@@ -28,16 +28,18 @@ for module in muff.__all__:
 
 # does the files:commands setting exist yet?
 try:
-       if muffconf.config_data.get("files", "commands"): pass
+       if muffconf.get("files", "commands"): pass
 
 # if not, reload the muffconf module
 except AttributeError:
        reload(muffconf)
 
 # now we can safely nab the command path setting and build a list of data files
-command_path = muffconf.config_data.get("files", "commands")
+command_path = muffconf.get("files", "commands")
+command_files_index = ConfigParser.SafeConfigParser()
+command_files_index.read(command_path + "/index")
 command_files = []
-for each_file in os.listdir(command_path):
+for each_file in command_files_index.get("index", "files").split():
        command_files.append(command_path + "/" + each_file)
 
 # read the command data files
@@ -47,31 +49,21 @@ command_data.read(command_files)
 # this creates a list of commands mentioned in the data files
 command_list = command_data.sections()
 
-def handle_user_input(user, input_data):
+def handle_user_input(user):
        """The main handler, branches to a state-specific handler."""
 
-       # the pairings of user state and command to run
-       handler_dictionary = {
-               "active": handler_active,
-               "entering account name": handler_entering_account_name,
-               "checking password": handler_checking_password,
-               "checking new account name": handler_checking_new_account_name,
-               "entering new password": handler_entering_new_password,
-               "verifying new password": handler_verifying_new_password
-               }
        # check to make sure the state is expected, then call that handler
-       if user.state in handler_dictionary.keys():
-               handler_dictionary[user.state](user, input_data)
-
-       # if there's input with an unknown user state, something is wrong
-       else: handler_fallthrough(user, input_data)
+       exec("handler_" + user.state.replace(" ", "_") + "(user)")
 
        # since we got input, flag that the menu/prompt needs to be redisplayed
        user.menu_seen = False
 
-def handler_entering_account_name(user, input_data):
+def handler_entering_account_name(user):
        """Handle the login account name."""
 
+       # get the next waiting line of input
+       input_data = user.input_queue.pop(0)
+
        # did the user enter anything?
        if input_data:
                
@@ -87,29 +79,33 @@ def handler_entering_account_name(user, input_data):
                        user.name = user.proposed_name
                        user.proposed_name = None
                        user.load()
+                       muffmisc.log("New user: " + user.name)
                        user.state = "checking new account name"
 
        # if the user entered nothing for a name, then buhbye
        else:
                user.state = "disconnecting"
 
-def handler_checking_password(user, input_data):
+def handler_checking_password(user):
        """Handle the login account password."""
 
+       # get the next waiting line of input
+       input_data = user.input_queue.pop(0)
+
        # does the hashed input equal the stored hash?
        if md5.new(user.proposed_name + input_data).hexdigest() == user.passhash:
 
                # if so, set the username and load from cold storage
+               # TODO: branch to character creation and selection menus
                user.name = user.proposed_name
                del(user.proposed_name)
-               user.load()
-
-               # now go active
-               # TODO: branch to character creation and selection menus
-               user.state = "active"
+               if not user.replace_old_connections():
+                       user.load()
+                       user.authenticate()
+                       user.state = "active"
 
        # if at first your hashes don't match, try, try again
-       elif user.password_tries < muffconf.config_data.getint("general", "password_tries"):
+       elif user.password_tries < muffconf.getint("general", "password_tries"):
                user.password_tries += 1
                user.error = "incorrect"
 
@@ -118,9 +114,20 @@ def handler_checking_password(user, input_data):
                user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
                user.state = "disconnecting"
 
-def handler_checking_new_account_name(user, input_data):
+def handler_disconnecting(user):
+       """Waiting for the user's connection to close."""
+       pass
+
+def handler_disconnecting_duplicates(user):
+       """Waiting for duplicate connections to close."""
+       pass
+
+def handler_checking_new_account_name(user):
        """Handle input for the new user menu."""
 
+       # get the next waiting line of input
+       input_data = user.input_queue.pop(0)
+
        # if there's input, take the first character and lowercase it
        if input_data:
                choice = input_data.lower()[0]
@@ -131,7 +138,7 @@ def handler_checking_new_account_name(user, input_data):
 
        # user selected to disconnect
        if choice == "d":
-               user.state == "disconnecting"
+               user.state = "disconnecting"
 
        # go back to the login screen
        elif choice == "g":
@@ -145,9 +152,12 @@ def handler_checking_new_account_name(user, input_data):
        else:
                user.error = "default"
 
-def handler_entering_new_password(user, input_data):
+def handler_entering_new_password(user):
        """Handle a new password entry."""
 
+       # get the next waiting line of input
+       input_data = user.input_queue.pop(0)
+
        # make sure the password is strong--at least one upper, one lower and
        # one digit, seven or more characters in length
        if len(input_data) > 6 and len(filter(lambda x: x>="0" and x<="9", input_data)) and len(filter(lambda x: x>="A" and x<="Z", input_data)) and len(filter(lambda x: x>="a" and x<="z", input_data)):
@@ -157,7 +167,7 @@ def handler_entering_new_password(user, input_data):
                user.state = "verifying new password"
 
        # the password was weak, try again if you haven't tried too many times
-       elif user.password_tries < muffconf.config_data.getint("general", "password_tries"):
+       elif user.password_tries < muffconf.getint("general", "password_tries"):
                user.password_tries += 1
                user.error = "weak"
 
@@ -166,19 +176,24 @@ def handler_entering_new_password(user, input_data):
                user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
                user.state = "disconnecting"
 
-def handler_verifying_new_password(user, input_data):
+def handler_verifying_new_password(user):
        """Handle the re-entered new password for verification."""
 
+       # get the next waiting line of input
+       input_data = user.input_queue.pop(0)
+
        # hash the input and match it to storage
        if md5.new(user.name + input_data).hexdigest() == user.passhash:
+               user.authenticate()
+               user.save()
 
                # the hashes matched, so go active
                # TODO: branch to character creation and selection menus
-               user.state = "active"
+               if not user.replace_old_connections(): user.state = "active"
 
        # go back to entering the new password as long as you haven't tried
        # too many times
-       elif user.password_tries < muffconf.config_data.getint("general", "password_tries"):
+       elif user.password_tries < muffconf.getint("general", "password_tries"):
                user.password_tries += 1
                user.error = "differs"
                user.state = "entering new password"
@@ -188,12 +203,11 @@ def handler_verifying_new_password(user, input_data):
                user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
                user.state = "disconnecting"
 
-def handler_active(user, input_data):
+def handler_active(user):
        """Handle input for active users."""
 
-       # users reaching this stage should be considered authenticated
-       # TODO: this should actually happen before or in load() instead
-       if not user.authenticated: user.authenticated = True
+       # get the next waiting line of input
+       input_data = user.input_queue.pop(0)
 
        # split out the command (first word) and parameters (everything else)
        try:
@@ -216,22 +230,16 @@ def handler_active(user, input_data):
        # no data matching the entered command word
        elif command: command_error(user, command, parameters)
 
-def handler_fallthrough(user, input_data):
-       """Input received in an unknown user state."""
-       if input_data:
-               muffmisc.log("User \"" + user + "\" entered \"" + input_data + "\" while in unknown state \"" + user.state + "\".")
-
 def command_halt(user, command="", parameters=""):
        """Halt the world."""
 
-       # let everyone know
-       # TODO: optionally take input for the message
-       muffmisc.broadcast(user.name + " halts the world.")
+       # see if there's a message or use a generic one
+       if parameters: message = "Halting: " + parameters
+       else: message = "User " + user.name + " halted the world."
 
-       # save everyone
-       # TODO: probably want a misc function for this
-       for each_user in muffvars.userlist:
-               each_user.save()
+       # let everyone know
+       muffmisc.broadcast(message)
+       muffmisc.log(message)
 
        # set a flag to terminate the world
        muffvars.terminate_world = True
@@ -239,8 +247,9 @@ def command_halt(user, command="", parameters=""):
 def command_reload(user, command="", parameters=""):
        """Reload all code modules, configs and data."""
 
-       # let the user know
+       # let the user know and log
        user.send("Reloading all code modules, configs and data.")
+       muffmisc.log("User " + user.name + " reloaded the world.")
 
        # set a flag to reload
        muffvars.reload_modules = True
@@ -249,6 +258,11 @@ def command_quit(user, command="", parameters=""):
        """Quit the world."""
        user.state = "disconnecting"
 
+def command_time(user, command="", parameters=""):
+       """Show the current world time in elapsed increments."""
+       user.send(muffmisc.repr_long(muffmisc.getlong(muffvars.variable_data,
+               "time", "elapsed")) + " increments elapsed since the world was created.")
+
 def command_help(user, command="", parameters=""):
        """List available commands and provide help for commands."""
 
@@ -297,40 +311,42 @@ def command_help(user, command="", parameters=""):
 def command_say(user, command="", parameters=""):
        """Speak to others in the same room."""
 
+       # check for replacement macros
+       if muffmisc.replace_macros(user, parameters, True) != parameters:
+               user.send("You cannot speak $_(replacement macros).")
+
        # the user entered a message
-       if parameters:
+       elif parameters:
 
                # get rid of quote marks on the ends of the message and
                # capitalize the first letter
                message = parameters.strip("\"'`").capitalize()
 
-               # exclaim because the message ended in an exclamation mark
-               # TODO: use the ends() function instead of an index throughout
-               if message[-1] == "!":
-                       action = "exclaim"
+               # a dictionary of punctuation:action pairs
+               actions = {}
+               for option in muffconf.config_data.options("language"):
+                       if option.startswith("punctuation_"):
+                               action = option.split("_")[1]
+                               for mark in muffconf.config_data.get("language", option).split():
+                                               actions[mark] = action
 
-               # begin because the message ended in miscellaneous punctuation
-               elif message[-1] in [ ",", "-", ":", ";" ]:
-                       action = "begin"
+               # set the default action
+               action = actions[muffconf.config_data.get("language", "default_punctuation")]
 
-               # muse because the message ended in an ellipsis
-               elif message[-3:] == "...":
-                       action = "muse"
+               # match the punctuation used, if any, to an action
+               default_punctuation = muffconf.config_data.get("language", "default_punctuation")
+               for mark in actions.keys():
+                       if message.endswith(mark) and mark != default_punctuation:
+                               action = actions[mark]
+                               break
 
-               # ask because the message ended in a question mark
-               elif message[-1] == "?":
-                       action = "ask"
-
-               # say because the message ended in a singular period
-               else:
-                       action = "say"
-                       if message.endswith("."):
-                               message += "."
+               # if the action is default and there is no mark, add one
+               if action == actions[default_punctuation] and not message.endswith(default_punctuation):
+                       message += default_punctuation
 
                # capitalize a list of words within the message
-               # TODO: move this list to the config
-               capitalization = [ "i", "i'd", "i'll" ]
-               for word in capitalization:
+               capitalize = muffconf.get("language", "capitalize").split()
+               for word in capitalize:
                        message = message.replace(" " + word + " ", " " + word.capitalize() + " ")
 
                # tell the room
index 67bdf1b..7992f12 100644 (file)
@@ -6,6 +6,9 @@
 # muff configuration files use the ini format supported by ConfigParser
 import ConfigParser
 
+# need os for testing whether the config file exists and is readable
+import os
+
 # hack to load all modules in teh muff package
 import muff
 for module in muff.__all__:
@@ -17,12 +20,28 @@ config_dirs = [".", "./etc", "/usr/local/muff", "/usr/local/muff/etc", "/etc/muf
 # name of the config file
 config_name = "muff.conf"
 
-# build a list of possible config files
-config_files = []
+# find the config file
 for each_dir in config_dirs:
-       config_files.append(each_dir + "/" + config_name)
+       config_file = each_dir + "/" + config_name
+       if os.access(config_file, os.R_OK): break
 
 # read the config
 config_data = ConfigParser.SafeConfigParser()
-config_data.read(config_files)
+config_data.read(config_file)
+
+def get(section, option):
+       """Convenience function to get configuration data."""
+       return config_data.get(section, option)
+
+def getfloat(section, option):
+       "Convenience function to get floating-point configuration data."""
+       return config_data.getfloat(section, option)
+
+def getint(section, option):
+       """Convenience function to get integer configuration data."""
+       return config_data.getint(section, option)
+
+def set(section, option, value):
+       """Convenienve function to set miscellaneous configuration data."""
+       return muffmisc.setlong(config_data, section, option, value)
 
index 56a9e57..89725bc 100644 (file)
@@ -17,14 +17,9 @@ for module in muff.__all__:
 def main():
        """The main loop."""
 
-       # loop indefinitely while the world is not flagged for termination
-       while not muffvars.terminate_world:
-
-               # open the listening socket if it hasn't been already
-               if not muffvars.newsocket: muffsock.initialize_server_socket()
-
-               # pause for a configurable amount of time (decimal seconds)
-               time.sleep(muffconf.config_data.getfloat("general", "increment"))
+       # loop indefinitely while the world is not flagged for termination or
+       # there are connected users
+       while not muffvars.terminate_world or muffvars.userlist:
 
                # the world was flagged for a reload of all code/data
                if muffvars.reload_modules:
@@ -36,78 +31,17 @@ def main():
                        for module in muff.__all__:
                                exec("reload(muff." + module + ")")
 
+                       # move data into new persistent objects
+                       muffmisc.reload_data()
+
                        # reset the reload flag
                        muffvars.reload_modules = False
 
-               # assign a user if a new connection is waiting
-               user = muffsock.check_for_connection(muffvars.newsocket)
-
-               # there was a new connection
-               if user:
-
-                       # welcome to the user list
-                       muffvars.userlist.append(user)
-
-                       # make a note of it
-                       muffmisc.log(str(len(muffvars.userlist)) + " connection(s)")
-
-               # iterate over the connected users
-               for each_user in muffvars.userlist:
-
-                       # show the user a menu as needed
-                       each_user.show_menu()
-
-                       # disconnect users with the appropriate state
-                       if each_user.state == "disconnecting":
-
-                               # save to cold storage
-                               each_user.save()
-
-                               # close the connection
-                               each_user.connection.close()
-
-                               # remove from the list
-                               each_user.remove()
-
-                       else:
-
-                               # check for some input
-                               # TODO: make a separate function for this
-                               try:
-                                       input_data = each_user.connection.recv(1024)
-                               except:
-                                       input_data = ""
-                               # we got something
-                               if input_data:
-
-                                       # tack this on to any previous partial
-                                       each_user.partial_input += input_data
-
-                                       # the held input ends in a newline
-                                       if each_user.partial_input[-1] == "\n":
-
-                                               # filter out non-printables
-                                               each_user.partial_input = filter(lambda x: x>=' ' and x<='~', each_user.partial_input)
-
-                                               # strip off extra whitespace
-                                               each_user.partial_input = string.strip(each_user.partial_input)
-
-                                               # put on the end of the queue
-                                               each_user.input_queue.append(each_user.partial_input)
-
-                                               # reset the held partial input
-                                               each_user.partial_input = ""
-
-                                               # pass first item in the input
-                                               # queue to the main handler
-                                               muffcmds.handle_user_input(each_user, each_user.input_queue[0])
-
-                                               # then remove it from the queue
-                                               each_user.input_queue.remove(each_user.input_queue[0])
+               # do what needs to be done on each pulse
+               muffmisc.on_pulse()
 
-       # the loop has terminated, so tear down all sockets
-       # TODO: move the save from command_halt() to here
-       muffsock.destroy_all_sockets()
+       # the loop has terminated, so save persistent variables
+       muffvars.save()
 
        # log a final message
        muffmisc.log("Shutting down now.")
index b84f783..cdf0d5c 100644 (file)
@@ -19,16 +19,18 @@ for module in muff.__all__:
 
 # see if the menupath can be retrieved from muffconf
 try:
-       if muffconf.config_data.get("files", "menus"): pass
+       if muffconf.get("files", "menus"): pass
 
 # otherwise, reload muffconf
 except AttributeError:
        reload(muffconf)
 
-# build a list of files in the menus directory
+# now we can safely nab the menu path setting and build a list of data files
+menu_path = muffconf.get("files", "menus")
+menu_files_index = ConfigParser.SafeConfigParser()
+menu_files_index.read(menu_path + "/index")
 menu_files = []
-menu_path = muffconf.config_data.get("files", "menus")
-for each_file in os.listdir(menu_path):
+for each_file in menu_files_index.get("index", "files").split():
        menu_files.append(menu_path + "/" + each_file)
 
 # read the menu files
index d0765ef..d2848b2 100644 (file)
@@ -6,6 +6,9 @@
 # used by several functions for random calls
 import random
 
+# used to match the 'L' at the end of a long int in repr_long
+import re
+
 # random_name uses string.strip
 import string
 
@@ -19,7 +22,7 @@ for module in muff.__all__:
 
 def broadcast(message):
        """Send a message to all connected users."""
-       for each_user in muffvars.userlist: each_user.send(message)
+       for each_user in muffvars.userlist: each_user.send("$(eol)" + message)
 
 def log(message):
        """Log a message."""
@@ -138,3 +141,82 @@ def random_name():
        # strip any leading quotemark, capitalize and return the name
        return string.strip(name, "'").capitalize()
 
+def repr_long(value):
+       string_value = repr(value)
+       if re.match('\d*L$', string_value): return string_value.strip("L")
+       else: return string_value
+
+def getlong(config, section, option):
+       return int(config.get(section, option).strip("L"))
+
+def setlong(config, section, option, value):
+       return config.set(section, option, repr_long(value))
+
+def replace_macros(user, text, is_input=False):
+       while True:
+               macro_start = string.find(text, "$(")
+               if macro_start == -1: break
+               macro_end = string.find(text, ")", macro_start) + 1
+               macro = text[macro_start:macro_end]
+               if macro in muffvars.macros.keys():
+                       text = string.replace(text, macro, muffvars.macros[macro])
+
+               # the user's account name
+               elif macro == "$(account)":
+                       text = string.replace(text, macro, user.name)
+
+               # if we get here, log and replace it with null
+               else:
+                       text = string.replace(text, macro, "")
+                       if not is_input:
+                               log("Unexpected replacement macro " + macro + " encountered.")
+
+       # replace the look-like-a-macro sequence
+       text = string.replace(text, "$_(", "$(")
+
+       return text
+
+def check_time(frequency):
+       """Check for a factor of the current increment count."""
+       if type(frequency) is str:
+               frequency = muffconf.getint("time", frequency)
+       return not getlong(muffvars.variable_data, "time", "elapsed") % frequency
+
+def on_pulse():
+       """The things which should happen on each pulse, aside from reloads."""
+
+       # open the listening socket if it hasn't been already
+       if not muffvars.newsocket: muffsock.initialize_server_socket()
+
+       # assign a user if a new connection is waiting
+       user = muffsock.check_for_connection(muffvars.newsocket)
+       if user: muffvars.userlist.append(user)
+
+       # iterate over the connected users
+       for user in muffvars.userlist: user.pulse()
+
+       # update the log every now and then
+       if check_time("frequency_log"):
+               log(repr(len(muffvars.userlist)) + " connection(s)")
+
+       # periodically save everything
+       if check_time("frequency_save"):
+               muffvars.save()
+               for user in muffvars.userlist: user.save()
+
+       # pause for a configurable amount of time (decimal seconds)
+       time.sleep(muffconf.getfloat("time", "increment"))
+
+       # increment the elapsed increment counter
+       setlong(muffvars.variable_data, "time", "elapsed",
+               getlong(muffvars.variable_data, "time", "elapsed") + 1)
+
+def reload_data():
+       """Reload data into new persistent objects."""
+
+       # reload the users
+       temporary_userlist = []
+       for user in muffvars.userlist: temporary_userlist.append(user)
+       for user in temporary_userlist: user.reload()
+       del(temporary_userlist)
+
index 40fffa5..fe47061 100644 (file)
@@ -27,7 +27,7 @@ def check_for_connection(newsocket):
        connection.setblocking(0)
 
        # create a new user object
-       user = muffuser.User();
+       user = muffuser.User()
 
        # associate this connection with it
        user.connection = connection
@@ -50,7 +50,7 @@ def initialize_server_socket():
        newsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
        # bind the socket to to our desired server ipa and port
-       newsocket.bind((muffconf.config_data.get("network", "host"), muffconf.config_data.getint("network", "port")))
+       newsocket.bind((muffconf.get("network", "host"), muffconf.getint("network", "port")))
 
        # disable blocking so we can proceed whether or not we can send/receive
        newsocket.setblocking(0)
@@ -64,13 +64,3 @@ def initialize_server_socket():
        # store this in a globally-accessible place
        muffvars.newsocket = newsocket
 
-def destroy_all_sockets():
-       """Go through all connected users and close their sockets."""
-
-       # note that we're closing all sockets
-       muffmisc.log("Closing remaining connections...")
-
-       # iterate over each connected user and close their associated sockets
-       for user in muffvars.userlist:
-               user.connection.close()
-
index ebe0c32..9a5169c 100644 (file)
@@ -68,11 +68,106 @@ class User:
                # 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 +183,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()
@@ -120,27 +215,29 @@ 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
                        try:
                                if os.listdir(account_path): pass
                        except:
-                               os.mkdir(account_path)
+                               os.mkdir(account_path)
 
                        # open the user account file for writing
-                       # TODO: create filename with 0600 perms
                        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))
@@ -157,21 +254,11 @@ class User:
 
                        # start with a newline, append the message, then end
                        # with the optional eol string passed to this function
-                       output = "$(eol)" + output + eol
-
-                       # replace eol markers with a crlf
-                       # TODO: search for markers and replace from a dict
-                       output = string.replace(output, "$(eol)", "\r\n")
+                       # and the ansi escape to return to normal text
+                       output = "\r\n" + output + eol + chr(27) + "[0m"
 
-                       # 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")
-
-                       # the user's account name
-                       output = string.replace(output, "$(account)", self.name)
+                       # 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
@@ -191,3 +278,68 @@ class User:
                        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)
+
index cededce..9e6f9f1 100644 (file)
@@ -3,11 +3,27 @@
 # Copyright (c) 2005 mudpy, Jeremy Stanley <fungi@yuggoth.org>, all rights reserved.
 # Licensed per terms in the LICENSE file distributed with this software.
 
+# persistent variables are stored in ini-style files supported by ConfigParser
+import ConfigParser
+
 # hack to load all modules in the muff package
 import muff
 for module in muff.__all__:
        exec("import " + module)
 
+# does the files:variable setting exist yet?
+try:
+       if muffconf.get("files", "variable"): pass
+
+# if not, reload the muffconf module
+except AttributeError:
+       reload(muffconf)
+
+# now we can safely load persistent variables from file
+variable_file = muffconf.get("files", "variable")
+variable_data = ConfigParser.SafeConfigParser()
+variable_data.read(variable_file)
+
 # if there is no userlist, create an empty one
 try:
        if userlist: pass
@@ -26,3 +42,20 @@ terminate_world = False
 # flag to raise when all code modules, config and data should be reloaded
 reload_modules = False
 
+# a dict of replacement macros
+macros = {
+       "$(eol)": "\r\n",
+       "$(bld)": chr(27) + "[1m",
+       "$(nrm)": chr(27) + "[0m",
+       "$(blk)": chr(27) + "[30m",
+       "$(grn)": chr(27) + "[32m",
+       "$(red)": chr(27) + "[31m"
+       }
+
+# function to save persistent variables to file
+def save():
+       file_descriptor = open(variable_file, "w")
+       variable_data.write(file_descriptor)
+       file_descriptor.flush()
+       file_descriptor.close()
+
index da57ea8..9e9331b 100644 (file)
--- a/muff.conf
+++ b/muff.conf
@@ -3,12 +3,33 @@ accounts = ./lib/accounts
 commands = ./lib/commands
 menus = ./lib/menus
 modules = ./lib
+variable = ./lib/save/variable
 
 [general]
-increment = 0.1
 password_tries = 3
 
+[language]
+capitalize = i i'd i'll i'm
+default_punctuation = .
+punctuation_ask = ?
+punctuation_begin = , - : ;
+punctuation_exclaim = !
+punctuation_muse = ...
+punctuation_say = .
+
 [network]
-host =
+host = 
 port = 6669
 
+[time]
+definition_d = 24h
+definition_h = 60mi
+definition_mi = 10r
+definition_mo = 28d
+definition_r = 6
+definition_w = 7d
+definition_y = 12mo
+frequency_log = 6000
+frequency_save = 600
+increment = 0.1
+