1 """Command objects for the MUFF 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 # 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 def handle_user_input(user):
30 """The main handler, branches to a state-specific handler."""
32 # check to make sure the state is expected, then call that handler
33 if "handler_" + user.state in globals():
34 exec("handler_" + user.state + "(user)")
36 generic_menu_handler(user)
38 # since we got input, flag that the menu/prompt needs to be redisplayed
39 user.menu_seen = False
41 # if the user's client echo is off, send a blank line for aesthetics
42 if not user.echoing: user.send("", "")
44 def generic_menu_handler(user):
45 """A generic menu choice handler."""
47 # get a lower-case representation of the next line of input
49 choice = user.input_queue.pop(0)
50 if choice: choice = choice.lower()
53 # run any script related to this choice
54 exec(muffmenu.get_choice_action(user, choice))
56 # move on to the next state or return an error
57 new_state = muffmenu.get_choice_branch(user, choice)
58 if new_state: user.state = new_state
59 else: user.error = "default"
61 def handler_entering_account_name(user):
62 """Handle the login account name."""
64 # get the next waiting line of input
65 input_data = user.input_queue.pop(0)
67 # did the user enter anything?
70 # keep only the first word and convert to lower-case
71 user.proposed_name = string.split(input_data)[0].lower()
73 # if we have a password hash, time to request a password
74 if user.get_passhash():
75 user.state = "checking_password"
77 # otherwise, this could be a brand new user
79 user.name = user.proposed_name
80 user.proposed_name = None
82 muffmisc.log("New user: " + user.name)
83 user.state = "checking_new_account_name"
85 # if the user entered nothing for a name, then buhbye
87 user.state = "disconnecting"
89 def handler_checking_password(user):
90 """Handle the login account password."""
92 # get the next waiting line of input
93 input_data = user.input_queue.pop(0)
95 # does the hashed input equal the stored hash?
96 if md5.new(user.proposed_name + input_data).hexdigest() == user.passhash:
98 # if so, set the username and load from cold storage
99 user.name = user.proposed_name
100 del(user.proposed_name)
101 if not user.replace_old_connections():
104 user.state = "main_utility"
106 # if at first your hashes don't match, try, try again
107 elif user.password_tries < muffconf.getint("general", "password_tries"):
108 user.password_tries += 1
109 user.error = "incorrect"
111 # we've exceeded the maximum number of password failures, so disconnect
113 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
114 user.state = "disconnecting"
116 def handler_checking_new_account_name(user):
117 """Handle input for the new user menu."""
119 # get the next waiting line of input
120 input_data = user.input_queue.pop(0)
122 # if there's input, take the first character and lowercase it
124 choice = input_data.lower()[0]
126 # if there's no input, use the default
128 choice = muffmenu.get_default_menu_choice(user.state)
130 # user selected to disconnect
132 user.state = "disconnecting"
134 # go back to the login screen
136 user.state = "entering_account_name"
138 # new user, so ask for a password
140 user.state = "entering_new_password"
142 # user entered a non-existent option
144 user.error = "default"
146 def handler_entering_new_password(user):
147 """Handle a new password entry."""
149 # get the next waiting line of input
150 input_data = user.input_queue.pop(0)
152 # make sure the password is strong--at least one upper, one lower and
153 # one digit, seven or more characters in length
154 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)):
156 # hash and store it, then move on to verification
157 user.passhash = md5.new(user.name + input_data).hexdigest()
158 user.state = "verifying_new_password"
160 # the password was weak, try again if you haven't tried too many times
161 elif user.password_tries < muffconf.getint("general", "password_tries"):
162 user.password_tries += 1
165 # too many tries, so adios
167 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
168 user.state = "disconnecting"
170 def handler_verifying_new_password(user):
171 """Handle the re-entered new password for verification."""
173 # get the next waiting line of input
174 input_data = user.input_queue.pop(0)
176 # hash the input and match it to storage
177 if md5.new(user.name + input_data).hexdigest() == user.passhash:
181 # the hashes matched, so go active
182 if not user.replace_old_connections(): user.state = "main_utility"
184 # go back to entering the new password as long as you haven't tried
186 elif user.password_tries < muffconf.getint("general", "password_tries"):
187 user.password_tries += 1
188 user.error = "differs"
189 user.state = "entering_new_password"
191 # otherwise, sayonara
193 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
194 user.state = "disconnecting"
196 def handler_active(user):
197 """Handle input for active users."""
199 # get the next waiting line of input
200 input_data = user.input_queue.pop(0)
202 # split out the command (first word) and parameters (everything else)
204 inputlist = string.split(input_data, None, 1)
205 command = inputlist[0]
209 parameters = inputlist[1]
214 # lowercase the command
215 command = command.lower()
217 # the command matches a command word for which we have data
218 if command in muffuniv.universe.commands.keys():
219 exec(muffuniv.universe.commands[command].get("action"))
221 # no data matching the entered command word
222 elif command: command_error(user, command, parameters)
224 def command_halt(user, command="", parameters=""):
225 """Halt the world."""
227 # see if there's a message or use a generic one
228 if parameters: message = "Halting: " + parameters
229 else: message = "User " + user.name + " halted the world."
232 muffmisc.broadcast(message)
233 muffmisc.log(message)
235 # set a flag to terminate the world
236 muffvars.terminate_world = True
238 def command_reload(user, command="", parameters=""):
239 """Reload all code modules, configs and data."""
241 # let the user know and log
242 user.send("Reloading all code modules, configs and data.")
243 muffmisc.log("User " + user.name + " reloaded the world.")
245 # set a flag to reload
246 muffvars.reload_modules = True
248 def command_quit(user, command="", parameters=""):
249 """Quit the world."""
250 user.state = "disconnecting"
252 def command_time(user, command="", parameters=""):
253 """Show the current world time in elapsed increments."""
254 user.send(muffuniv.universe.internals["counters"].get("elapsed") + " increments elapsed since the world was created.")
256 def command_help(user, command="", parameters=""):
257 """List available commands and provide help for commands."""
259 # did the user ask for help on a specific command word?
262 # is the command word one for which we have data?
263 if parameters in muffuniv.universe.commands.keys():
265 # add a description if provided
266 description = muffuniv.universe.commands[parameters].get("description")
268 description = "(no short description provided)"
269 output = "$(grn)" + parameters + "$(nrm) - " + description + "$(eol)$(eol)"
271 # add the help text if provided
272 help_text = muffuniv.universe.commands[parameters].get("help")
274 help_text = "No help is provided for this command."
277 # no data for the requested command word
279 output = "That is not an available command."
281 # no specific command word was indicated
284 # give a sorted list of commands with descriptions if provided
285 output = "These are the commands available to you:$(eol)$(eol)"
286 sorted_commands = muffuniv.universe.commands.keys()
287 sorted_commands.sort()
288 for item in sorted_commands:
289 description = muffuniv.universe.commands[item].get("description")
291 description = "(no short description provided)"
292 output += " $(grn)" + item + "$(nrm) - " + description + "$(eol)"
293 output += "$(eol)Enter \"help COMMAND\" for help on a command named \"COMMAND\"."
295 # send the accumulated output to the user
298 def command_say(user, command="", parameters=""):
299 """Speak to others in the same room."""
301 # check for replacement macros
302 if muffmisc.replace_macros(user, parameters, True) != parameters:
303 user.send("You cannot speak $_(replacement macros).")
305 # the user entered a message
308 # get rid of quote marks on the ends of the message and
309 # capitalize the first letter
310 message = parameters.strip("\"'`").capitalize()
312 # a dictionary of punctuation:action pairs
314 for option in muffconf.config_data.options("language"):
315 if option.startswith("punctuation_"):
316 action = option.split("_")[1]
317 for mark in muffconf.config_data.get("language", option).split():
318 actions[mark] = action
320 # set the default action
321 action = actions[muffconf.config_data.get("language", "default_punctuation")]
323 # match the punctuation used, if any, to an action
324 default_punctuation = muffconf.config_data.get("language", "default_punctuation")
325 for mark in actions.keys():
326 if message.endswith(mark) and mark != default_punctuation:
327 action = actions[mark]
330 # if the action is default and there is no mark, add one
331 if action == actions[default_punctuation] and not message.endswith(default_punctuation):
332 message += default_punctuation
334 # capitalize a list of words within the message
335 capitalize = muffconf.get("language", "capitalize").split()
336 for word in capitalize:
337 message = message.replace(" " + word + " ", " " + word.capitalize() + " ")
340 # TODO: we won't be using broadcast once there are actual rooms
341 muffmisc.broadcast(user.name + " " + action + "s, \"" + message + "\"")
343 # there was no message
345 user.send("What do you want to say?")
347 def command_show(user, command="", parameters=""):
348 """Show program data."""
349 if parameters == "avatars":
350 message = "These are the avatars managed by your account:$(eol)"
351 avatars = user.list_avatar_names()
353 for avatar in avatars: message += "$(eol) $(grn)" + avatar + "$(nrm)"
354 elif parameters == "files":
355 message = "These are the current files containing the universe:$(eol)"
356 keys = muffuniv.universe.files.keys()
358 for key in keys: message += "$(eol) $(grn)" + key + "$(nrm)"
359 elif parameters == "universe":
360 message = "These are the current elements in the universe:$(eol)"
361 keys = muffuniv.universe.contents.keys()
363 for key in keys: message += "$(eol) $(grn)" + key + "$(nrm)"
364 elif parameters: message = "I don't know what \"" + parameters + "\" is."
365 else: message = "What do you want to show?"
368 def command_error(user, command="", parameters=""):
369 """Generic error for an unrecognized command word."""
371 # 90% of the time use a generic error
372 if random.randrange(10):
373 message = "I'm not sure what \"" + command
375 message += " " + parameters
376 message += "\" means..."
378 # 10% of the time use the classic diku error
380 message = "Arglebargle, glop-glyf!?!"
382 # send the error message