033c74c52cb8512292de668e8ad30d88516a5130
[mudpy.git] / lib / muff / muffcmds.py
1 """Command objects for the MUFF Engine"""
2
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.
5
6 # command data like descriptions, help text, limits, et cetera, are stored in
7 # ini-style configuration files supported by the ConfigParser module
8 import ConfigParser
9
10 # md5 hashing is used for verification of account passwords
11 import md5
12
13 # the os module is used to we can get a directory listing and build lists of
14 # multiple config files in one directory
15 import os
16
17 # the random module is useful for creating random conditional output
18 import random
19
20 # string.split is used extensively to tokenize user input (break up command
21 # names and parameters)
22 import string
23
24 # bit of a hack to load all modules in the muff package
25 import muff
26 for module in muff.__all__:
27         exec("import " + module)
28
29 # does the files:commands setting exist yet?
30 try:
31         if muffconf.config_data.get("files", "commands"): pass
32
33 # if not, reload the muffconf module
34 except AttributeError:
35         reload(muffconf)
36
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")
39 command_files = []
40 for each_file in os.listdir(command_path):
41         command_files.append(command_path + "/" + each_file)
42
43 # read the command data files
44 command_data = ConfigParser.SafeConfigParser()
45 command_data.read(command_files)
46
47 # this creates a list of commands mentioned in the data files
48 command_list = command_data.sections()
49
50 def handle_user_input(user, input_data):
51         """The main handler, branches to a state-specific handler."""
52
53         # the pairings of user state and command to run
54         handler_dictionary = {
55                 "active": handler_active,
56                 "entering account name": handler_entering_account_name,
57                 "checking password": handler_checking_password,
58                 "checking new account name": handler_checking_new_account_name,
59                 "entering new password": handler_entering_new_password,
60                 "verifying new password": handler_verifying_new_password
61                 }
62         # check to make sure the state is expected, then call that handler
63         if user.state in handler_dictionary.keys():
64                 handler_dictionary[user.state](user, input_data)
65
66         # if there's input with an unknown user state, something is wrong
67         else: handler_fallthrough(user, input_data)
68
69         # since we got input, flag that the menu/prompt needs to be redisplayed
70         user.menu_seen = False
71
72 def handler_entering_account_name(user, input_data):
73         """Handle the login account name."""
74
75         # did the user enter anything?
76         if input_data:
77                 
78                 # keep only the first word and convert to lower-case
79                 user.proposed_name = string.split(input_data)[0].lower()
80
81                 # if we have a password hash, time to request a password
82                 if user.get_passhash():
83                         user.state = "checking password"
84
85                 # otherwise, this could be a brand new user
86                 else:
87                         user.name = user.proposed_name
88                         user.proposed_name = None
89                         user.load()
90                         user.state = "checking new account name"
91
92         # if the user entered nothing for a name, then buhbye
93         else:
94                 user.state = "disconnecting"
95
96 def handler_checking_password(user, input_data):
97         """Handle the login account password."""
98
99         # does the hashed input equal the stored hash?
100         if md5.new(user.proposed_name + input_data).hexdigest() == user.passhash:
101
102                 # if so, set the username and load from cold storage
103                 user.name = user.proposed_name
104                 del(user.proposed_name)
105                 user.load()
106
107                 # now go active
108                 # TODO: branch to character creation and selection menus
109                 user.state = "active"
110
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"
115
116         # we've exceeded the maximum number of password failures, so disconnect
117         else:
118                 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
119                 user.state = "disconnecting"
120
121 def handler_checking_new_account_name(user, input_data):
122         """Handle input for the new user menu."""
123
124         # if there's input, take the first character and lowercase it
125         if input_data:
126                 choice = input_data.lower()[0]
127
128         # if there's no input, use the default
129         else:
130                 choice = muffmenu.get_default(user)
131
132         # user selected to disconnect
133         if choice == "d":
134                 user.state == "disconnecting"
135
136         # go back to the login screen
137         elif choice == "g":
138                 user.state = "entering account name"
139
140         # new user, so ask for a password
141         elif choice == "n":
142                 user.state = "entering new password"
143
144         # user entered a non-existent option
145         else:
146                 user.error = "default"
147
148 def handler_entering_new_password(user, input_data):
149         """Handle a new password entry."""
150
151         # make sure the password is strong--at least one upper, one lower and
152         # one digit, seven or more characters in length
153         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)):
154
155                 # hash and store it, then move on to verification
156                 user.passhash = md5.new(user.name + input_data).hexdigest()
157                 user.state = "verifying new password"
158
159         # the password was weak, try again if you haven't tried too many times
160         elif user.password_tries < muffconf.config_data.getint("general", "password_tries"):
161                 user.password_tries += 1
162                 user.error = "weak"
163
164         # too many tries, so adios
165         else:
166                 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
167                 user.state = "disconnecting"
168
169 def handler_verifying_new_password(user, input_data):
170         """Handle the re-entered new password for verification."""
171
172         # hash the input and match it to storage
173         if md5.new(user.name + input_data).hexdigest() == user.passhash:
174
175                 # the hashes matched, so go active
176                 # TODO: branch to character creation and selection menus
177                 user.state = "active"
178
179         # go back to entering the new password as long as you haven't tried
180         # too many times
181         elif user.password_tries < muffconf.config_data.getint("general", "password_tries"):
182                 user.password_tries += 1
183                 user.error = "differs"
184                 user.state = "entering new password"
185
186         # otherwise, sayonara
187         else:
188                 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
189                 user.state = "disconnecting"
190
191 def handler_active(user, input_data):
192         """Handle input for active users."""
193
194         # users reaching this stage should be considered authenticated
195         # TODO: this should actually happen before or in load() instead
196         if not user.authenticated: user.authenticated = True
197
198         # split out the command (first word) and parameters (everything else)
199         try:
200                 inputlist = string.split(input_data, None, 1)
201                 command = inputlist[0]
202         except:
203                 command = input_data
204         try:
205                 parameters = inputlist[1]
206         except:
207                 parameters = ""
208         del(inputlist)
209
210         # lowercase the command
211         command = command.lower()
212
213         # the command matches a command word for which we have data
214         if command in command_list: exec("command_" + command + "(user, command, parameters)")
215
216         # no data matching the entered command word
217         elif command: command_error(user, command, parameters)
218
219 def handler_fallthrough(user, input_data):
220         """Input received in an unknown user state."""
221         if input_data:
222                 muffmisc.log("User \"" + user + "\" entered \"" + input_data + "\" while in unknown state \"" + user.state + "\".")
223
224 def command_halt(user, command="", parameters=""):
225         """Halt the world."""
226
227         # let everyone know
228         # TODO: optionally take input for the message
229         muffmisc.broadcast(user.name + " halts the world.")
230
231         # save everyone
232         # TODO: probably want a misc function for this
233         for each_user in muffvars.userlist:
234                 each_user.save()
235
236         # set a flag to terminate the world
237         muffvars.terminate_world = True
238
239 def command_reload(user, command="", parameters=""):
240         """Reload all code modules, configs and data."""
241
242         # let the user know
243         user.send("Reloading all code modules, configs and data.")
244
245         # set a flag to reload
246         muffvars.reload_modules = True
247
248 def command_quit(user, command="", parameters=""):
249         """Quit the world."""
250         user.state = "disconnecting"
251
252 def command_help(user, command="", parameters=""):
253         """List available commands and provide help for commands."""
254
255         # did the user ask for help on a specific command word?
256         if parameters:
257
258                 # is the command word one for which we have data?
259                 if parameters in command_list:
260
261                         # add a description if provided
262                         try:
263                                 description = command_data.get(parameters, "description")
264                         except:
265                                 description = "(no short description provided)"
266                         output = "$(grn)" + parameters + "$(nrm) - " + command_data.get(parameters, "description") + "$(eol)$(eol)"
267
268                         # add the help text if provided
269                         try:
270                                 help_text = command_data.get(parameters, "help")
271                         except:
272                                 help_text = "No help is provided for this command."
273                         output += help_text
274
275                 # no data for the requested command word
276                 else:
277                         output = "That is not an available command."
278
279         # no specific command word was indicated
280         else:
281
282                 # give a sorted list of commands with descriptions if provided
283                 output = "These are the commands available to you:$(eol)$(eol)"
284                 sorted_commands = command_list
285                 sorted_commands.sort()
286                 for item in sorted_commands:
287                         try:
288                                 description = command_data.get(item, "description")
289                         except:
290                                 description = "(no short description provided)"
291                         output += "   $(grn)" + item + "$(nrm) - " + command_data.get(item, "description") + "$(eol)"
292                 output += "$(eol)Enter \"help COMMAND\" for help on a command named \"COMMAND\"."
293
294         # send the accumulated output to the user
295         user.send(output)
296
297 def command_say(user, command="", parameters=""):
298         """Speak to others in the same room."""
299
300         # the user entered a message
301         if parameters:
302
303                 # get rid of quote marks on the ends of the message and
304                 # capitalize the first letter
305                 message = parameters.strip("\"'`").capitalize()
306
307                 # exclaim because the message ended in an exclamation mark
308                 # TODO: use the ends() function instead of an index throughout
309                 if message[-1] == "!":
310                         action = "exclaim"
311
312                 # begin because the message ended in miscellaneous punctuation
313                 elif message[-1] in [ ",", "-", ":", ";" ]:
314                         action = "begin"
315
316                 # muse because the message ended in an ellipsis
317                 elif message[-3:] == "...":
318                         action = "muse"
319
320                 # ask because the message ended in a question mark
321                 elif message[-1] == "?":
322                         action = "ask"
323
324                 # say because the message ended in a singular period
325                 else:
326                         action = "say"
327                         if message.endswith("."):
328                                 message += "."
329
330                 # capitalize a list of words within the message
331                 # TODO: move this list to the config
332                 capitalization = [ "i", "i'd", "i'll" ]
333                 for word in capitalization:
334                         message = message.replace(" " + word + " ", " " + word.capitalize() + " ")
335
336                 # tell the room
337                 # TODO: we won't be using broadcast once there are actual rooms
338                 muffmisc.broadcast(user.name + " " + action + "s, \"" + message + "\"")
339
340         # there was no message
341         else:
342                 user.send("What do you want to say?")
343
344 def command_error(user, command="", parameters=""):
345         """Generic error for an unrecognized command word."""
346
347         # 90% of the time use a generic error
348         if random.randrange(10):
349                 message = "I'm not sure what \"" + command
350                 if parameters:
351                         message += " " + parameters
352                 message += "\" means..."
353
354         # 10% of the time use the classic diku error
355         else:
356                 message = "Arglebargle, glop-glyf!?!"
357
358         # send the error message
359         user.send(message)
360