1 """Core objects for the mudpy engine."""
3 # Copyright (c) 2005 mudpy, Jeremy Stanley <fungi@yuggoth.org>, all rights reserved.
4 # Licensed per terms in the LICENSE file distributed with this software.
6 # import some things we need
7 from ConfigParser import SafeConfigParser
8 from md5 import new as new_md5
9 from os import F_OK, R_OK, access, getcwd, makedirs, sep
10 from random import choice, randrange
11 from socket import AF_INET, SO_REUSEADDR, SOCK_STREAM, SOL_SOCKET, socket
12 from time import asctime, sleep
14 # a dict of replacement macros
17 "$(bld)": chr(27) + "[1m",
18 "$(nrm)": chr(27) + "[0m",
19 "$(blk)": chr(27) + "[30m",
20 "$(grn)": chr(27) + "[32m",
21 "$(red)": chr(27) + "[31m"
25 """An element of the universe."""
26 def __init__(self, key, universe, origin=""):
27 """Default values for the in-memory element variables."""
29 if self.key.find(":") > 0:
30 self.category, self.subkey = self.key.split(":", 1)
32 self.category = "other"
33 self.subkey = self.key
34 if not self.category in universe.categories: self.category = "other"
35 universe.categories[self.category][self.subkey] = self
37 if not self.origin: self.origin = universe.default_origins[self.category]
38 if not self.origin.startswith(sep):
39 self.origin = getcwd() + sep + self.origin
40 universe.contents[self.key] = self
41 if not self.origin in universe.files:
42 DataFile(self.origin, universe)
43 if not universe.files[self.origin].data.has_section(self.key):
44 universe.files[self.origin].data.add_section(self.key)
46 log("Deleting: " + self.key + ".")
47 universe.files[self.origin].data.remove_section(self.key)
48 del universe.categories[self.category][self.subkey]
49 del universe.contents[self.key]
52 """Return a list of facets for this element."""
53 return universe.files[self.origin].data.options(self.key)
55 """Retrieve values."""
56 if universe.files[self.origin].data.has_option(self.key, facet):
57 return universe.files[self.origin].data.get(self.key, facet)
60 def getboolean(self, facet, default=False):
61 """Retrieve values as boolean type."""
62 if universe.files[self.origin].data.has_option(self.key, facet):
63 return universe.files[self.origin].data.getboolean(self.key, facet)
66 def getint(self, facet):
67 """Convenience method to coerce return values as type int."""
68 value = self.get(facet)
69 if not value: value = 0
70 elif type(value) is str: value = value.rstrip("L")
72 def getfloat(self, facet):
73 """Convenience method to coerce return values as type float."""
74 value = self.get(facet)
75 if not value: value = 0
76 elif type(value) is str: value = value.rstrip("L")
78 def set(self, facet, value):
80 if not type(value) is str: value = repr(value)
81 universe.files[self.origin].data.set(self.key, facet, value)
84 """A file containing universe elements."""
85 def __init__(self, filename, universe):
86 filedir = sep.join(filename.split(sep)[:-1])
87 self.data = SafeConfigParser()
88 if access(filename, R_OK): self.data.read(filename)
89 self.filename = filename
90 universe.files[filename] = self
91 if "categories" in self.data.sections():
92 for option in self.data.options("categories"):
93 universe.default_origins[option] = self.data.get("categories", option)
94 if not option in universe.categories:
95 universe.categories[option] = {}
96 for section in self.data.sections():
97 if section == "categories" or section == "include":
98 for option in self.data.options(section):
99 includefile = self.data.get(section, option)
100 if not includefile.startswith(sep):
101 includefile = filedir + sep + includefile
102 DataFile(includefile, universe)
103 elif section != "control":
104 Element(section, universe, filename)
106 if self.data.sections() and not ( "control" in self.data.sections() and self.data.getboolean("control", "read_only") ):
107 basedir = sep.join(self.filename.split(sep)[:-1])
108 if not access(basedir, F_OK): makedirs(basedir)
109 file_descriptor = file(self.filename, "w")
110 self.data.write(file_descriptor)
111 file_descriptor.flush()
112 file_descriptor.close()
116 def __init__(self, filename=""):
117 """Initialize the universe."""
120 self.default_origins = {}
123 self.terminate_world = False
124 self.reload_modules = False
126 possible_filenames = [
132 "/usr/local/mudpy/mudpy.conf",
133 "/usr/local/mudpy/etc/mudpy.conf",
134 "/etc/mudpy/mudpy.conf",
137 for filename in possible_filenames:
138 if access(filename, R_OK): break
139 if not filename.startswith(sep):
140 filename = getcwd() + sep + filename
141 DataFile(filename, self)
143 """Save the universe to persistent storage."""
144 for key in self.files: self.files[key].save()
146 def initialize_server_socket(self):
147 """Create and open the listening socket."""
149 # create a new ipv4 stream-type socket object
150 self.listening_socket = socket(AF_INET, SOCK_STREAM)
152 # set the socket options to allow existing open ones to be
153 # reused (fixes a bug where the server can't bind for a minute
154 # when restarting on linux systems)
155 self.listening_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
157 # bind the socket to to our desired server ipa and port
158 self.listening_socket.bind((self.categories["internal"]["network"].get("host"), self.categories["internal"]["network"].getint("port")))
160 # disable blocking so we can proceed whether or not we can
162 self.listening_socket.setblocking(0)
164 # start listening on the socket
165 self.listening_socket.listen(1)
167 # note that we're now ready for user connections
168 log("Waiting for connection(s)...")
171 """This is a connected user."""
174 """Default values for the in-memory user variables."""
176 self.last_address = ""
177 self.connection = None
178 self.authenticated = False
179 self.password_tries = 1
180 self.state = "entering_account_name"
181 self.menu_seen = False
183 self.input_queue = []
184 self.output_queue = []
185 self.partial_input = ""
191 """Log, close the connection and remove."""
192 name = self.account.get("name")
193 if name: message = "User " + name
194 else: message = "An unnamed user"
195 message += " logged out."
197 self.connection.close()
201 """Save, load a new user and relocate the connection."""
203 # get out of the list
206 # create a new user object
209 # set everything else equivalent
226 exec("new_user." + attribute + " = self." + attribute)
229 universe.userlist.append(new_user)
231 # get rid of the old user object
234 def replace_old_connections(self):
235 """Disconnect active users with the same name."""
237 # the default return value
240 # iterate over each user in the list
241 for old_user in universe.userlist:
243 # the name is the same but it's not us
244 if old_user.account.get("name") == self.account.get("name") and old_user is not self:
247 log("User " + self.account.get("name") + " reconnected--closing old connection to " + old_user.address + ".")
248 old_user.send("$(eol)$(red)New connection from " + self.address + ". Terminating old connection...$(nrm)$(eol)")
249 self.send("$(eol)$(red)Taking over old connection from " + old_user.address + ".$(nrm)")
251 # close the old connection
252 old_user.connection.close()
254 # replace the old connection with this one
255 old_user.connection = self.connection
256 old_user.last_address = old_user.address
257 old_user.address = self.address
258 old_user.echoing = self.echoing
260 # take this one out of the list and delete
266 # true if an old connection was replaced, false if not
269 def authenticate(self):
270 """Flag the user as authenticated and disconnect duplicates."""
271 if not self.state is "authenticated":
272 log("User " + self.account.get("name") + " logged in.")
273 self.authenticated = True
276 """Send the user their current menu."""
277 if not self.menu_seen:
278 self.menu_choices = get_menu_choices(self)
279 self.send(get_menu(self.state, self.error, self.echoing, self.menu_choices), "")
280 self.menu_seen = True
282 self.adjust_echoing()
284 def adjust_echoing(self):
285 """Adjust echoing to match state menu requirements."""
286 if self.echoing and not menu_echo_on(self.state): self.echoing = False
287 elif not self.echoing and menu_echo_on(self.state): self.echoing = True
290 """Remove a user from the list of connected users."""
291 universe.userlist.remove(self)
293 def send(self, output, eol="$(eol)"):
294 """Send arbitrary text to a connected user."""
296 # only when there is actual output
299 # start with a newline, append the message, then end
300 # with the optional eol string passed to this function
301 # and the ansi escape to return to normal text
302 output = "\r\n" + output + eol + chr(27) + "[0m"
304 # find and replace macros in the output
305 output = replace_macros(self, output)
307 # wrap the text at 80 characters
308 # TODO: prompt user for preferred wrap width
309 output = wrap_ansi_text(output, 80)
311 # drop the formatted output into the output queue
312 self.output_queue.append(output)
314 # try to send the last item in the queue, remove it and
315 # flag that menu display is not needed
317 self.connection.send(self.output_queue[0])
318 self.output_queue.remove(self.output_queue[0])
319 self.menu_seen = False
321 # but if we can't, that's okay too
326 """All the things to do to the user per increment."""
328 # if the world is terminating, disconnect
329 if universe.terminate_world:
330 self.state = "disconnecting"
331 self.menu_seen = False
333 # show the user a menu as needed
336 # disconnect users with the appropriate state
337 if self.state == "disconnecting":
340 # the user is unique and not flagged to disconnect
343 # check for input and add it to the queue
346 # there is input waiting in the queue
347 if self.input_queue: handle_user_input(self)
349 def enqueue_input(self):
350 """Process and enqueue any new input."""
352 # check for some input
354 input_data = self.connection.recv(1024)
361 # tack this on to any previous partial
362 self.partial_input += input_data
364 # separate multiple input lines
365 new_input_lines = self.partial_input.split("\n")
367 # if input doesn't end in a newline, replace the
368 # held partial input with the last line of it
369 if not self.partial_input.endswith("\n"):
370 self.partial_input = new_input_lines.pop()
372 # otherwise, chop off the extra null input and reset
373 # the held partial input
375 new_input_lines.pop()
376 self.partial_input = ""
378 # iterate over the remaining lines
379 for line in new_input_lines:
381 # filter out non-printables
382 line = filter(lambda x: x>=' ' and x<='~', line)
384 # strip off extra whitespace
387 # put on the end of the queue
388 self.input_queue.append(line)
390 def new_avatar(self):
391 """Instantiate a new, unconfigured avatar for this user."""
392 counter = universe.categories["internal"]["counters"].getint("next_avatar")
393 while "avatar:" + repr(counter + 1) in universe.categories["actor"].keys(): counter += 1
394 universe.categories["internal"]["counters"].set("next_avatar", counter + 1)
395 self.avatar = Element("actor:avatar:" + repr(counter), universe)
396 avatars = self.account.get("avatars").split()
397 avatars.append(self.avatar.key)
398 self.account.set("avatars", " ".join(avatars))
400 def list_avatar_names(self):
401 """A test function to list names of assigned avatars."""
403 avatars = self.account.get("avatars").split()
407 for avatar in avatars:
408 avatar_names.append(universe.contents[avatar].get("name"))
411 def broadcast(message):
412 """Send a message to all connected users."""
413 for each_user in universe.userlist: each_user.send("$(eol)" + message)
418 # the time in posix log timestamp format
419 timestamp = asctime()[4:19]
421 # send the timestamp and message to standard output
422 print(timestamp + " " + message)
424 def wrap_ansi_text(text, width):
425 """Wrap text with arbitrary width while ignoring ANSI colors."""
427 # the current position in the entire text string, including all
428 # characters, printable or otherwise
429 absolute_position = 0
431 # the current text position relative to the begining of the line,
432 # ignoring color escape sequences
433 relative_position = 0
435 # whether the current character is part of a color escape sequence
438 # iterate over each character from the begining of the text
439 for each_character in text:
441 # the current character is the escape character
442 if each_character == chr(27):
445 # the current character is within an escape sequence
448 # the current character is m, which terminates the
449 # current escape sequence
450 if each_character == "m":
453 # the current character is a newline, so reset the relative
454 # position (start a new line)
455 elif each_character == "\n":
456 relative_position = 0
458 # the current character meets the requested maximum line width,
459 # so we need to backtrack and find a space at which to wrap
460 elif relative_position == width:
462 # distance of the current character examined from the
466 # count backwards until we find a space
467 while text[absolute_position - wrap_offset] != " ":
470 # insert an eol in place of the space
471 text = text[:absolute_position - wrap_offset] + "\r\n" + text[absolute_position - wrap_offset + 1:]
473 # increase the absolute position because an eol is two
474 # characters but the space it replaced was only one
475 absolute_position += 1
477 # now we're at the begining of a new line, plus the
478 # number of characters wrapped from the previous line
479 relative_position = wrap_offset
481 # as long as the character is not a carriage return and the
482 # other above conditions haven't been met, count it as a
483 # printable character
484 elif each_character != "\r":
485 relative_position += 1
487 # increase the absolute position for every character
488 absolute_position += 1
490 # return the newly-wrapped text
493 def weighted_choice(data):
494 """Takes a dict weighted by value and returns a random key."""
496 # this will hold our expanded list of keys from the data
499 # create thee expanded list of keys
500 for key in data.keys():
501 for count in range(data[key]):
504 # return one at random
505 return choice(expanded)
508 """Returns a random character name."""
510 # the vowels and consonants needed to create romaji syllables
511 vowels = [ "a", "i", "u", "e", "o" ]
512 consonants = ["'", "k", "z", "s", "sh", "z", "j", "t", "ch", "ts", "d", "n", "h", "f", "m", "y", "r", "w" ]
514 # this dict will hold our weighted list of syllables
517 # generate the list with an even weighting
518 for consonant in consonants:
520 syllables[consonant + vowel] = 1
522 # we'll build the name into this string
525 # create a name of random length from the syllables
526 for syllable in range(randrange(2, 6)):
527 name += weighted_choice(syllables)
529 # strip any leading quotemark, capitalize and return the name
530 return name.strip("'").capitalize()
532 def replace_macros(user, text, is_input=False):
533 """Replaces macros in text output."""
535 macro_start = text.find("$(")
536 if macro_start == -1: break
537 macro_end = text.find(")", macro_start) + 1
538 macro = text[macro_start:macro_end]
539 if macro in macros.keys():
540 text = text.replace(macro, macros[macro])
542 # the user's account name
543 elif macro == "$(account)":
544 text = text.replace(macro, user.account.get("name"))
546 # third person subjective pronoun
547 elif macro == "$(tpsp)":
548 if user.avatar.get("gender") == "male":
549 text = text.replace(macro, "he")
550 elif user.avatar.get("gender") == "female":
551 text = text.replace(macro, "she")
553 text = text.replace(macro, "it")
555 # third person objective pronoun
556 elif macro == "$(tpop)":
557 if user.avatar.get("gender") == "male":
558 text = text.replace(macro, "him")
559 elif user.avatar.get("gender") == "female":
560 text = text.replace(macro, "her")
562 text = text.replace(macro, "it")
564 # third person possessive pronoun
565 elif macro == "$(tppp)":
566 if user.avatar.get("gender") == "male":
567 text = text.replace(macro, "his")
568 elif user.avatar.get("gender") == "female":
569 text = text.replace(macro, "hers")
571 text = text.replace(macro, "its")
573 # if we get here, log and replace it with null
575 text = text.replace(macro, "")
577 log("Unexpected replacement macro " + macro + " encountered.")
579 # replace the look-like-a-macro sequence
580 text = text.replace("$_(", "$(")
584 def check_time(frequency):
585 """Check for a factor of the current increment count."""
586 if type(frequency) is str:
587 frequency = universe.categories["internal"]["time"].getint(frequency)
588 if not "counters" in universe.categories["internal"]:
589 Element("internal:counters", universe)
590 return not universe.categories["internal"]["counters"].getint("elapsed") % frequency
593 """The things which should happen on each pulse, aside from reloads."""
595 # open the listening socket if it hasn't been already
596 if not hasattr(universe, "listening_socket"):
597 universe.initialize_server_socket()
599 # assign a user if a new connection is waiting
600 user = check_for_connection(universe.listening_socket)
601 if user: universe.userlist.append(user)
603 # iterate over the connected users
604 for user in universe.userlist: user.pulse()
606 # update the log every now and then
607 if check_time("frequency_log"):
608 log(repr(len(universe.userlist)) + " connection(s)")
610 # periodically save everything
611 if check_time("frequency_save"):
614 # pause for a configurable amount of time (decimal seconds)
615 sleep(universe.categories["internal"]["time"].getfloat("increment"))
617 # increment the elapsed increment counter
618 universe.categories["internal"]["counters"].set("elapsed", universe.categories["internal"]["counters"].getint("elapsed") + 1)
621 """Reload data into new persistent objects."""
622 for user in universe.userlist[:]: user.reload()
624 def check_for_connection(listening_socket):
625 """Check for a waiting connection and return a new user object."""
627 # try to accept a new connection
629 connection, address = listening_socket.accept()
633 # note that we got one
634 log("Connection from " + address[0])
636 # disable blocking so we can proceed whether or not we can send/receive
637 connection.setblocking(0)
639 # create a new user object
642 # associate this connection with it
643 user.connection = connection
645 # set the user's ipa from the connection's ipa
646 user.address = address[0]
648 # return the new user object
651 def get_menu(state, error=None, echoing=True, choices={}):
652 """Show the correct menu text to a user."""
654 # begin with a telnet echo command sequence if needed
655 message = get_echo_sequence(state, echoing)
657 # get the description or error text
658 message += get_menu_description(state, error)
660 # get menu choices for the current state
661 message += get_formatted_menu_choices(state, choices)
663 # try to get a prompt, if it was defined
664 message += get_menu_prompt(state)
666 # throw in the default choice, if it exists
667 message += get_formatted_default_menu_choice(state)
669 # display a message indicating if echo is off
670 message += get_echo_message(state)
672 # return the assembly of various strings defined above
675 def menu_echo_on(state):
676 """True if echo is on, false if it is off."""
677 return universe.categories["menu"][state].getboolean("echo", True)
679 def get_echo_sequence(state, echoing):
680 """Build the appropriate IAC ECHO sequence as needed."""
682 # if the user has echo on and the menu specifies it should be turned
683 # off, send: iac + will + echo + null
684 if echoing and not menu_echo_on(state): return chr(255) + chr(251) + chr(1) + chr(0)
686 # if echo is not set to off in the menu and the user curently has echo
687 # off, send: iac + wont + echo + null
688 elif not echoing and menu_echo_on(state): return chr(255) + chr(252) + chr(1) + chr(0)
690 # default is not to send an echo control sequence at all
693 def get_echo_message(state):
694 """Return a message indicating that echo is off."""
695 if menu_echo_on(state): return ""
696 else: return "(won't echo) "
698 def get_default_menu_choice(state):
699 """Return the default choice for a menu."""
700 return universe.categories["menu"][state].get("default")
702 def get_formatted_default_menu_choice(state):
703 """Default menu choice foratted for inclusion in a prompt string."""
704 default = get_default_menu_choice(state)
705 if default: return "[$(red)" + default + "$(nrm)] "
708 def get_menu_description(state, error):
709 """Get the description or error text."""
711 # an error condition was raised by the handler
714 # try to get an error message matching the condition
716 description = universe.categories["menu"][state].get("error_" + error)
717 if not description: description = "That is not a valid choice..."
718 description = "$(red)" + description + "$(nrm)"
720 # there was no error condition
723 # try to get a menu description for the current state
724 description = universe.categories["menu"][state].get("description")
726 # return the description or error message
727 if description: description += "$(eol)$(eol)"
730 def get_menu_prompt(state):
731 """Try to get a prompt, if it was defined."""
732 prompt = universe.categories["menu"][state].get("prompt")
733 if prompt: prompt += " "
736 def get_menu_choices(user):
737 """Return a dict of choice:meaning."""
739 for facet in universe.categories["menu"][user.state].facets():
740 if facet.startswith("choice_"):
741 choices[facet.split("_", 2)[1]] = universe.categories["menu"][user.state].get(facet)
742 elif facet.startswith("create_"):
743 choices[facet.split("_", 2)[1]] = eval(universe.categories["menu"][user.state].get(facet))
746 def get_formatted_menu_choices(state, choices):
747 """Returns a formatted string of menu choices."""
749 choice_keys = choices.keys()
751 for choice in choice_keys:
752 choice_output += " [$(red)" + choice + "$(nrm)] " + choices[choice] + "$(eol)"
753 if choice_output: choice_output += "$(eol)"
756 def get_menu_branches(state):
757 """Return a dict of choice:branch."""
759 for facet in universe.categories["menu"][state].facets():
760 if facet.startswith("branch_"):
761 branches[facet.split("_", 2)[1]] = universe.categories["menu"][state].get(facet)
764 def get_default_branch(state):
765 """Return the default branch."""
766 return universe.categories["menu"][state].get("branch")
768 def get_choice_branch(user, choice):
769 """Returns the new state matching the given choice."""
770 branches = get_menu_branches(user.state)
771 if not choice: choice = get_default_menu_choice(user.state)
772 if choice in branches.keys(): return branches[choice]
773 elif choice in user.menu_choices.keys(): return get_default_branch(user.state)
776 def get_menu_actions(state):
777 """Return a dict of choice:branch."""
779 for facet in universe.categories["menu"][state].facets():
780 if facet.startswith("action_"):
781 actions[facet.split("_", 2)[1]] = universe.categories["menu"][state].get(facet)
784 def get_default_action(state):
785 """Return the default action."""
786 return universe.categories["menu"][state].get("action")
788 def get_choice_action(user, choice):
789 """Run any indicated script for the given choice."""
790 actions = get_menu_actions(user.state)
791 if not choice: choice = get_default_menu_choice(user.state)
792 if choice in actions.keys(): return actions[choice]
793 elif choice in user.menu_choices.keys(): return get_default_action(user.state)
796 def handle_user_input(user):
797 """The main handler, branches to a state-specific handler."""
799 # check to make sure the state is expected, then call that handler
800 if "handler_" + user.state in globals():
801 exec("handler_" + user.state + "(user)")
803 generic_menu_handler(user)
805 # since we got input, flag that the menu/prompt needs to be redisplayed
806 user.menu_seen = False
808 # if the user's client echo is off, send a blank line for aesthetics
809 if not user.echoing: user.send("", "")
811 def generic_menu_handler(user):
812 """A generic menu choice handler."""
814 # get a lower-case representation of the next line of input
816 choice = user.input_queue.pop(0)
817 if choice: choice = choice.lower()
820 # run any script related to this choice
821 exec(get_choice_action(user, choice))
823 # move on to the next state or return an error
824 new_state = get_choice_branch(user, choice)
825 if new_state: user.state = new_state
826 else: user.error = "default"
828 def handler_entering_account_name(user):
829 """Handle the login account name."""
831 # get the next waiting line of input
832 input_data = user.input_queue.pop(0)
834 # did the user enter anything?
837 # keep only the first word and convert to lower-case
838 name = input_data.lower()
840 # fail if there are non-alphanumeric characters
841 if name != filter(lambda x: x>="0" and x<="9" or x>="a" and x<="z", name):
842 user.error = "bad_name"
844 # if that account exists, time to request a password
845 elif name in universe.categories["account"]:
846 user.account = universe.categories["account"][name]
847 user.state = "checking_password"
849 # otherwise, this could be a brand new user
851 user.account = Element("account:" + name, universe)
852 user.account.set("name", name)
853 log("New user: " + name)
854 user.state = "checking_new_account_name"
856 # if the user entered nothing for a name, then buhbye
858 user.state = "disconnecting"
860 def handler_checking_password(user):
861 """Handle the login account password."""
863 # get the next waiting line of input
864 input_data = user.input_queue.pop(0)
866 # does the hashed input equal the stored hash?
867 if new_md5(user.account.get("name") + input_data).hexdigest() == user.account.get("passhash"):
869 # if so, set the username and load from cold storage
870 if not user.replace_old_connections():
872 user.state = "main_utility"
874 # if at first your hashes don't match, try, try again
875 elif user.password_tries < universe.categories["internal"]["general"].getint("password_tries"):
876 user.password_tries += 1
877 user.error = "incorrect"
879 # we've exceeded the maximum number of password failures, so disconnect
881 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
882 user.state = "disconnecting"
884 def handler_checking_new_account_name(user):
885 """Handle input for the new user menu."""
887 # get the next waiting line of input
888 input_data = user.input_queue.pop(0)
890 # if there's input, take the first character and lowercase it
892 choice = input_data.lower()[0]
894 # if there's no input, use the default
896 choice = get_default_menu_choice(user.state)
898 # user selected to disconnect
900 user.account.delete()
901 user.state = "disconnecting"
903 # go back to the login screen
905 user.account.delete()
906 user.state = "entering_account_name"
908 # new user, so ask for a password
910 user.state = "entering_new_password"
912 # user entered a non-existent option
914 user.error = "default"
916 def handler_entering_new_password(user):
917 """Handle a new password entry."""
919 # get the next waiting line of input
920 input_data = user.input_queue.pop(0)
922 # make sure the password is strong--at least one upper, one lower and
923 # one digit, seven or more characters in length
924 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)):
926 # hash and store it, then move on to verification
927 user.account.set("passhash", new_md5(user.account.get("name") + input_data).hexdigest())
928 user.state = "verifying_new_password"
930 # the password was weak, try again if you haven't tried too many times
931 elif user.password_tries < universe.categories["internal"]["general"].getint("password_tries"):
932 user.password_tries += 1
935 # too many tries, so adios
937 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
938 user.account.delete()
939 user.state = "disconnecting"
941 def handler_verifying_new_password(user):
942 """Handle the re-entered new password for verification."""
944 # get the next waiting line of input
945 input_data = user.input_queue.pop(0)
947 # hash the input and match it to storage
948 if new_md5(user.account.get("name") + input_data).hexdigest() == user.account.get("passhash"):
951 # the hashes matched, so go active
952 if not user.replace_old_connections(): user.state = "main_utility"
954 # go back to entering the new password as long as you haven't tried
956 elif user.password_tries < universe.categories["internal"]["general"].getint("password_tries"):
957 user.password_tries += 1
958 user.error = "differs"
959 user.state = "entering_new_password"
961 # otherwise, sayonara
963 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
964 user.account.delete()
965 user.state = "disconnecting"
967 def handler_active(user):
968 """Handle input for active users."""
970 # get the next waiting line of input
971 input_data = user.input_queue.pop(0)
973 # split out the command (first word) and parameters (everything else)
974 if input_data.find(" ") > 0:
975 command, parameters = input_data.split(" ", 1)
980 # lowercase the command
981 command = command.lower()
983 # the command matches a command word for which we have data
984 if command in universe.categories["command"]:
985 exec(universe.categories["command"][command].get("action"))
987 # no data matching the entered command word
988 elif command: command_error(user, command, parameters)
990 def command_halt(user, command="", parameters=""):
991 """Halt the world."""
993 # see if there's a message or use a generic one
994 if parameters: message = "Halting: " + parameters
995 else: message = "User " + user.account.get("name") + " halted the world."
1001 # set a flag to terminate the world
1002 universe.terminate_world = True
1004 def command_reload(user, command="", parameters=""):
1005 """Reload all code modules, configs and data."""
1007 # let the user know and log
1008 user.send("Reloading all code modules, configs and data.")
1009 log("User " + user.account.get("name") + " reloaded the world.")
1011 # set a flag to reload
1012 universe.reload_modules = True
1014 def command_quit(user, command="", parameters=""):
1015 """Quit the world."""
1016 user.state = "disconnecting"
1018 def command_help(user, command="", parameters=""):
1019 """List available commands and provide help for commands."""
1021 # did the user ask for help on a specific command word?
1024 # is the command word one for which we have data?
1025 if parameters in universe.categories["command"]:
1027 # add a description if provided
1028 description = universe.categories["command"][parameters].get("description")
1030 description = "(no short description provided)"
1031 output = "$(grn)" + parameters + "$(nrm) - " + description + "$(eol)$(eol)"
1033 # add the help text if provided
1034 help_text = universe.categories["command"][parameters].get("help")
1036 help_text = "No help is provided for this command."
1039 # no data for the requested command word
1041 output = "That is not an available command."
1043 # no specific command word was indicated
1046 # give a sorted list of commands with descriptions if provided
1047 output = "These are the commands available to you:$(eol)$(eol)"
1048 sorted_commands = universe.categories["command"].keys()
1049 sorted_commands.sort()
1050 for item in sorted_commands:
1051 description = universe.categories["command"][item].get("description")
1053 description = "(no short description provided)"
1054 output += " $(grn)" + item + "$(nrm) - " + description + "$(eol)"
1055 output += "$(eol)Enter \"help COMMAND\" for help on a command named \"COMMAND\"."
1057 # send the accumulated output to the user
1060 def command_say(user, command="", parameters=""):
1061 """Speak to others in the same room."""
1063 # check for replacement macros
1064 if replace_macros(user, parameters, True) != parameters:
1065 user.send("You cannot speak $_(replacement macros).")
1067 # the user entered a message
1070 # get rid of quote marks on the ends of the message and
1071 # capitalize the first letter
1072 message = parameters.strip("\"'`").capitalize()
1074 # a dictionary of punctuation:action pairs
1076 for facet in universe.categories["internal"]["language"].facets():
1077 if facet.startswith("punctuation_"):
1078 action = facet.split("_")[1]
1079 for mark in universe.categories["internal"]["language"].get(facet).split():
1080 actions[mark] = action
1082 # match the punctuation used, if any, to an action
1083 default_punctuation = universe.categories["internal"]["language"].get("default_punctuation")
1084 action = actions[default_punctuation]
1085 for mark in actions.keys():
1086 if message.endswith(mark) and mark != default_punctuation:
1087 action = actions[mark]
1090 # if the action is default and there is no mark, add one
1091 if action == actions[default_punctuation] and not message.endswith(default_punctuation):
1092 message += default_punctuation
1094 # capitalize a list of words within the message
1095 capitalize = universe.categories["internal"]["language"].get("capitalize").split()
1096 for word in capitalize:
1097 message = message.replace(" " + word + " ", " " + word.capitalize() + " ")
1100 # TODO: we won't be using broadcast once there are actual rooms
1101 broadcast(user.account.get("name") + " " + action + "s, \"" + message + "\"")
1103 # there was no message
1105 user.send("What do you want to say?")
1107 def command_show(user, command="", parameters=""):
1108 """Show program data."""
1109 if parameters == "avatars":
1110 message = "These are the avatars managed by your account:$(eol)"
1111 avatars = user.list_avatar_names()
1113 for avatar in avatars: message += "$(eol) $(grn)" + avatar + "$(nrm)"
1114 elif parameters == "files":
1115 message = "These are the current files containing the universe:$(eol)"
1116 keys = universe.files.keys()
1118 for key in keys: message += "$(eol) $(grn)" + key + "$(nrm)"
1119 elif parameters == "universe":
1120 message = "These are the current elements in the universe:$(eol)"
1121 keys = universe.contents.keys()
1123 for key in keys: message += "$(eol) $(grn)" + key + "$(nrm)"
1124 elif parameters == "time":
1125 message = universe.categories["internal"]["counters"].get("elapsed") + " increments elapsed since the world was created."
1126 elif parameters: message = "I don't know what \"" + parameters + "\" is."
1127 else: message = "What do you want to show?"
1130 def command_error(user, command="", parameters=""):
1131 """Generic error for an unrecognized command word."""
1133 # 90% of the time use a generic error
1135 message = "I'm not sure what \"" + command
1137 message += " " + parameters
1138 message += "\" means..."
1140 # 10% of the time use the classic diku error
1142 message = "Arglebargle, glop-glyf!?!"
1144 # send the error message
1147 # if there is no universe, create an empty one
1148 if not "universe" in locals(): universe = Universe()