1 """Command objects for the MUFF Engine"""
3 # Copyright (c) 2005 MUDpy, The Fungi <fungi@yuggoth.org>, all rights reserved.
4 # Licensed per terms in the LICENSE file distributed with this software.
6 # command data like descriptions, help text, limits, et cetera, are stored in
7 # ini-style configuration files supported by the ConfigParser module
10 # md5 hashing is used for verification of account passwords
13 # the os module is used to we can get a directory listing and build lists of
14 # multiple config files in one directory
17 # the random module is useful for creating random conditional output
20 # string.split is used extensively to tokenize user input (break up command
21 # names and parameters)
24 # bit of a hack to load all modules in the muff package
26 for module in muff.__all__:
27 exec("import " + module)
29 # does the files:commands setting exist yet?
31 if muffconf.config_data.get("files", "commands"): pass
33 # if not, reload the muffconf module
34 except AttributeError:
37 # now we can safely nab the command path setting and build a list of data files
38 command_path = muffconf.config_data.get("files", "commands")
40 for each_file in os.listdir(command_path):
41 command_files.append(command_path + "/" + each_file)
43 # read the command data files
44 command_data = ConfigParser.SafeConfigParser()
45 command_data.read(command_files)
47 # this creates a list of commands mentioned in the data files
48 command_list = command_data.sections()
50 def handle_user_input(user, input):
51 """The main handler, branches to a state-specific handler."""
53 # TODO: change this to use a dict
54 if user.state == "active": handler_active(user, input)
55 elif user.state == "entering account name": handler_entering_account_name(user, input)
56 elif user.state == "checking password": handler_checking_password(user, input)
57 elif user.state == "checking new account name": handler_checking_new_account_name(user, input)
58 elif user.state == "entering new password": handler_entering_new_password(user, input)
59 elif user.state == "verifying new password": handler_verifying_new_password(user, input)
61 # if there's input with an unknown user state, something is wrong
62 else: handler_fallthrough(user, input)
64 # since we got input, flag that the menu/prompt needs to be redisplayed
65 user.menu_seen = False
67 def handler_entering_account_name(user, input):
68 """Handle the login account name."""
70 # did the user enter anything?
73 # keep only the first word and convert to lower-case
74 user.proposed_name = string.split(input)[0].lower()
76 # try to get a password hash for the proposed name
79 # if we have a password hash, time to request a password
80 # TODO: make get_passhash() return pass/fail and test that
82 user.state = "checking password"
84 # otherwise, this could be a brand new user
86 user.name = user.proposed_name
87 user.proposed_name = None
89 user.state = "checking new account name"
91 # if the user entered nothing for a name, then buhbye
92 # TODO: make a disconnect state instead of calling command_quit()
96 def handler_checking_password(user, input):
97 """Handle the login account password."""
99 # does the hashed input equal the stored hash?
100 if md5.new(user.proposed_name + input).hexdigest() == user.passhash:
102 # if so, set the username and load from cold storage
103 user.name = user.proposed_name
104 del(user.proposed_name)
108 # TODO: branch to character creation and selection menus
109 user.state = "active"
111 # if at first your hashes don't match, try, try again
112 elif user.password_tries < muffconf.config_data.getint("general", "password_tries"):
113 user.password_tries += 1
114 user.error = "incorrect"
116 # we've exceeded the maximum number of password failures, so disconnect
117 # TODO: make a disconnect state instead of calling command_quit()
119 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
122 def handler_checking_new_account_name(user, input):
123 """Handle input for the new user menu."""
125 # if there's input, take the first character and lowercase it
127 choice = input.lower()[0]
129 # if there's no input, use the default
131 choice = muffmenu.get_default(user)
133 # user selected to disconnect
134 # TODO: make a disconnect state instead of calling command_quit()
138 # go back to the login screen
140 user.state = "entering account name"
142 # new user, so ask for a password
144 user.state = "entering new password"
146 # user entered a non-existent option
148 user.error = "default"
150 def handler_entering_new_password(user, input):
151 """Handle a new password entry."""
153 # make sure the password is strong--at least one upper, one lower and
154 # one digit, seven or more characters in length
155 if len(input) > 6 and len(filter(lambda x: x>="0" and x<="9", input)) and len(filter(lambda x: x>="A" and x<="Z", input)) and len(filter(lambda x: x>="a" and x<="z", input)):
157 # hash and store it, then move on to verification
158 user.passhash = md5.new(user.name + input).hexdigest()
159 user.state = "verifying new password"
161 # the password was weak, try again if you haven't tried too many times
162 elif user.password_tries < muffconf.config_data.getint("general", "password_tries"):
163 user.password_tries += 1
166 # too many tries, so adios
167 # TODO: make a disconnect state instead of calling command_quit()
169 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
172 def handler_verifying_new_password(user, input):
173 """Handle the re-entered new password for verification."""
175 # hash the input and match it to storage
176 if md5.new(user.name + input).hexdigest() == user.passhash:
178 # the hashes matched, so go active
179 # TODO: branch to character creation and selection menus
180 user.state = "active"
182 # go back to entering the new password as long as you haven't tried
184 elif user.password_tries < muffconf.config_data.getint("general", "password_tries"):
185 user.password_tries += 1
186 user.error = "differs"
187 user.state = "entering new password"
189 # otherwise, sayonara
190 # TODO: make a disconnect state instead of calling command_quit()
192 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
195 # TODO: um, input is a reserved word. better replace it in all sources with
196 # something else, like input_data
197 def handler_active(user, input):
198 """Handle input for active users."""
200 # users reaching this stage should be considered authenticated
201 # TODO: this should actually happen before or in load() instead
202 if not user.authenticated: user.authenticated = True
204 # split out the command (first word) and parameters (everything else)
206 inputlist = string.split(input, None, 1)
207 command = inputlist[0]
211 parameters = inputlist[1]
216 # lowercase the command
217 command = command.lower()
219 # the command matches a command word for which we have data
220 if command in command_list: exec("command_" + command + "(user, command, parameters)")
222 # no data matching the entered command word
223 elif command: command_error(user, command, parameters)
225 # TODO: need a log function to handle conditions like this instead of print()
226 def handler_fallthrough(user, input):
227 """Input received in an unknown user state."""
229 print("User \"" + user + "\" entered \"" + input + "\" while in unknown state \"" + user.state + "\".")
231 def command_halt(user, command="", parameters=""):
232 """Halt the world."""
235 # TODO: optionally take input for the message
236 muffmisc.broadcast(user.name + " halts the world.")
239 # TODO: probably want a misc function for this
240 for each_user in muffvars.userlist:
243 # set a flag to terminate the world
244 muffvars.terminate_world = True
246 def command_reload(user, command="", parameters=""):
247 """Reload all code modules, configs and data."""
250 user.send("Reloading all code modules, configs and data.")
252 # set a flag to reload
253 muffvars.reload_modules = True
255 def command_quit(user, command="", parameters=""):
256 """Quit the world."""
258 # save to cold storage
261 # close the connection
262 user.connection.close()
264 # remove from the list
267 def command_help(user, command="", parameters=""):
268 """List available commands and provide help for commands."""
270 # did the user ask for help on a specific command word?
273 # is the command word one for which we have data?
274 if parameters in command_list:
276 # add a description if provided
278 description = command_data.get(parameters, "description")
280 description = "(no short description provided)"
281 output = "$(grn)" + parameters + "$(nrm) - " + command_data.get(parameters, "description") + "$(eol)$(eol)"
283 # add the help text if provided
285 help_text = command_data.get(parameters, "help")
287 help_text = "No help is provided for this command."
290 # no data for the requested command word
292 output = "That is not an available command."
294 # no specific command word was indicated
297 # give a sorted list of commands with descriptions if provided
298 output = "These are the commands available to you:$(eol)$(eol)"
299 sorted_commands = command_list
300 sorted_commands.sort()
301 for item in sorted_commands:
303 description = command_data.get(item, "description")
305 description = "(no short description provided)"
306 output += " $(grn)" + item + "$(nrm) - " + command_data.get(item, "description") + "$(eol)"
307 output += "$(eol)Enter \"help COMMAND\" for help on a command named \"COMMAND\"."
309 # send the accumulated output to the user
312 def command_say(user, command="", parameters=""):
313 """Speak to others in the same room."""
315 # the user entered a message
318 # get rid of quote marks on the ends of the message and
319 # capitalize the first letter
320 message = parameters.strip("\"'`").capitalize()
322 # exclaim because the message ended in an exclamation mark
323 # TODO: use the ends() function instead of an index throughout
324 if message[-1] == "!":
327 # begin because the message ended in miscellaneous punctuation
328 elif message[-1] in [ ",", "-", ":", ";" ]:
331 # muse because the message ended in an ellipsis
332 elif message[-3:] == "...":
335 # ask because the message ended in a question mark
336 elif message[-1] == "?":
339 # say because the message ended in a singular period
340 # TODO: entering one period results in a double-period--oops!
345 # capitalize a list of words within the message
346 # TODO: move this list to the config
347 capitalization = [ "i", "i'd", "i'll" ]
348 for word in capitalization:
349 message = message.replace(" " + word + " ", " " + word.capitalize() + " ")
352 # TODO: we won't be using broadcast once there are actual rooms
353 muffmisc.broadcast(user.name + " " + action + "s, \"" + message + "\"")
355 # there was no message
357 user.send("What do you want to say?")
359 def command_error(user, command="", parameters=""):
360 """Generic error for an unrecognized command word."""
362 # 90% of the time use a generic error
363 if random.random() > 0.1:
364 message = "I'm not sure what \"" + command
366 message += " " + parameters
367 message += "\" means..."
369 # 10% of the time use the classic diku error
371 message = "Arglebargle, glop-glyf!?!"
373 # send the error message