Imported from archive.
[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):
51         """The main handler, branches to a state-specific handler."""
52
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)
60
61         # if there's input with an unknown user state, something is wrong
62         else: handler_fallthrough(user, input)
63
64         # since we got input, flag that the menu/prompt needs to be redisplayed
65         user.menu_seen = False
66
67 def handler_entering_account_name(user, input):
68         """Handle the login account name."""
69
70         # did the user enter anything?
71         if input:
72                 
73                 # keep only the first word and convert to lower-case
74                 user.proposed_name = string.split(input)[0].lower()
75
76                 # try to get a password hash for the proposed name
77                 user.get_passhash()
78
79                 # if we have a password hash, time to request a password
80                 # TODO: make get_passhash() return pass/fail and test that
81                 if user.passhash:
82                         user.state = "checking password"
83
84                 # otherwise, this could be a brand new user
85                 else:
86                         user.name = user.proposed_name
87                         user.proposed_name = None
88                         user.load()
89                         user.state = "checking new account name"
90
91         # if the user entered nothing for a name, then buhbye
92         # TODO: make a disconnect state instead of calling command_quit()
93         else:
94                 command_quit(user)
95
96 def handler_checking_password(user, input):
97         """Handle the login account password."""
98
99         # does the hashed input equal the stored hash?
100         if md5.new(user.proposed_name + input).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         # TODO: make a disconnect state instead of calling command_quit()
118         else:
119                 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
120                 command_quit(user)
121
122 def handler_checking_new_account_name(user, input):
123         """Handle input for the new user menu."""
124
125         # if there's input, take the first character and lowercase it
126         if input:
127                 choice = input.lower()[0]
128
129         # if there's no input, use the default
130         else:
131                 choice = muffmenu.get_default(user)
132
133         # user selected to disconnect
134         # TODO: make a disconnect state instead of calling command_quit()
135         if choice == "d":
136                 command_quit(user)
137
138         # go back to the login screen
139         elif choice == "g":
140                 user.state = "entering account name"
141
142         # new user, so ask for a password
143         elif choice == "n":
144                 user.state = "entering new password"
145
146         # user entered a non-existent option
147         else:
148                 user.error = "default"
149
150 def handler_entering_new_password(user, input):
151         """Handle a new password entry."""
152
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)):
156
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"
160
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
164                 user.error = "weak"
165
166         # too many tries, so adios
167         # TODO: make a disconnect state instead of calling command_quit()
168         else:
169                 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
170                 command_quit(user)
171
172 def handler_verifying_new_password(user, input):
173         """Handle the re-entered new password for verification."""
174
175         # hash the input and match it to storage
176         if md5.new(user.name + input).hexdigest() == user.passhash:
177
178                 # the hashes matched, so go active
179                 # TODO: branch to character creation and selection menus
180                 user.state = "active"
181
182         # go back to entering the new password as long as you haven't tried
183         # too many times
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"
188
189         # otherwise, sayonara
190         # TODO: make a disconnect state instead of calling command_quit()
191         else:
192                 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
193                 command_quit(user)
194
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."""
199
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
203
204         # split out the command (first word) and parameters (everything else)
205         try:
206                 inputlist = string.split(input, None, 1)
207                 command = inputlist[0]
208         except:
209                 command = input
210         try:
211                 parameters = inputlist[1]
212         except:
213                 parameters = ""
214         del(inputlist)
215
216         # lowercase the command
217         command = command.lower()
218
219         # the command matches a command word for which we have data
220         if command in command_list: exec("command_" + command + "(user, command, parameters)")
221
222         # no data matching the entered command word
223         elif command: command_error(user, command, parameters)
224
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."""
228         if input:
229                 print("User \"" + user + "\" entered \"" + input + "\" while in unknown state \"" + user.state + "\".")
230
231 def command_halt(user, command="", parameters=""):
232         """Halt the world."""
233
234         # let everyone know
235         # TODO: optionally take input for the message
236         muffmisc.broadcast(user.name + " halts the world.")
237
238         # save everyone
239         # TODO: probably want a misc function for this
240         for each_user in muffvars.userlist:
241                 each_user.save()
242
243         # set a flag to terminate the world
244         muffvars.terminate_world = True
245
246 def command_reload(user, command="", parameters=""):
247         """Reload all code modules, configs and data."""
248
249         # let the user know
250         user.send("Reloading all code modules, configs and data.")
251
252         # set a flag to reload
253         muffvars.reload_modules = True
254
255 def command_quit(user, command="", parameters=""):
256         """Quit the world."""
257
258         # save to cold storage
259         user.save()
260
261         # close the connection
262         user.connection.close()
263
264         # remove from the list
265         user.remove()
266
267 def command_help(user, command="", parameters=""):
268         """List available commands and provide help for commands."""
269
270         # did the user ask for help on a specific command word?
271         if parameters:
272
273                 # is the command word one for which we have data?
274                 if parameters in command_list:
275
276                         # add a description if provided
277                         try:
278                                 description = command_data.get(parameters, "description")
279                         except:
280                                 description = "(no short description provided)"
281                         output = "$(grn)" + parameters + "$(nrm) - " + command_data.get(parameters, "description") + "$(eol)$(eol)"
282
283                         # add the help text if provided
284                         try:
285                                 help_text = command_data.get(parameters, "help")
286                         except:
287                                 help_text = "No help is provided for this command."
288                         output += help_text
289
290                 # no data for the requested command word
291                 else:
292                         output = "That is not an available command."
293
294         # no specific command word was indicated
295         else:
296
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:
302                         try:
303                                 description = command_data.get(item, "description")
304                         except:
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\"."
308
309         # send the accumulated output to the user
310         user.send(output)
311
312 def command_say(user, command="", parameters=""):
313         """Speak to others in the same room."""
314
315         # the user entered a message
316         if parameters:
317
318                 # get rid of quote marks on the ends of the message and
319                 # capitalize the first letter
320                 message = parameters.strip("\"'`").capitalize()
321
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] == "!":
325                         action = "exclaim"
326
327                 # begin because the message ended in miscellaneous punctuation
328                 elif message[-1] in [ ",", "-", ":", ";" ]:
329                         action = "begin"
330
331                 # muse because the message ended in an ellipsis
332                 elif message[-3:] == "...":
333                         action = "muse"
334
335                 # ask because the message ended in a question mark
336                 elif message[-1] == "?":
337                         action = "ask"
338
339                 # say because the message ended in a singular period
340                 # TODO: entering one period results in a double-period--oops!
341                 else:
342                         action = "say"
343                         message += "."
344
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() + " ")
350
351                 # tell the room
352                 # TODO: we won't be using broadcast once there are actual rooms
353                 muffmisc.broadcast(user.name + " " + action + "s, \"" + message + "\"")
354
355         # there was no message
356         else:
357                 user.send("What do you want to say?")
358
359 def command_error(user, command="", parameters=""):
360         """Generic error for an unrecognized command word."""
361
362         # 90% of the time use a generic error
363         if random.random() > 0.1:
364                 message = "I'm not sure what \"" + command
365                 if parameters:
366                         message += " " + parameters
367                 message += "\" means..."
368
369         # 10% of the time use the classic diku error
370         else:
371                 message = "Arglebargle, glop-glyf!?!"
372
373         # send the error message
374         user.send(message)
375