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 def handle_user_input(user):
30         """The main handler, branches to a state-specific handler."""
31
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)")
35         else:
36                 generic_menu_handler(user)
37
38         # since we got input, flag that the menu/prompt needs to be redisplayed
39         user.menu_seen = False
40
41         # if the user's client echo is off, send a blank line for aesthetics
42         if not user.echoing: user.send("", "")
43
44 def generic_menu_handler(user):
45         """A generic menu choice handler."""
46
47         # get a lower-case representation of the next line of input
48         if user.input_queue:
49                 choice = user.input_queue.pop(0)
50                 if choice: choice = choice.lower()
51         else: choice = ""
52
53         # run any script related to this choice
54         exec(muffmenu.get_choice_action(user, choice))
55
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"
60
61 def handler_entering_account_name(user):
62         """Handle the login account name."""
63
64         # get the next waiting line of input
65         input_data = user.input_queue.pop(0)
66
67         # did the user enter anything?
68         if input_data:
69                 
70                 # keep only the first word and convert to lower-case
71                 user.proposed_name = string.split(input_data)[0].lower()
72
73                 # if we have a password hash, time to request a password
74                 if user.get_passhash():
75                         user.state = "checking_password"
76
77                 # otherwise, this could be a brand new user
78                 else:
79                         user.name = user.proposed_name
80                         user.proposed_name = None
81                         user.load()
82                         muffmisc.log("New user: " + user.name)
83                         user.state = "checking_new_account_name"
84
85         # if the user entered nothing for a name, then buhbye
86         else:
87                 user.state = "disconnecting"
88
89 def handler_checking_password(user):
90         """Handle the login account password."""
91
92         # get the next waiting line of input
93         input_data = user.input_queue.pop(0)
94
95         # does the hashed input equal the stored hash?
96         if md5.new(user.proposed_name + input_data).hexdigest() == user.passhash:
97
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():
102                         user.load()
103                         user.authenticate()
104                         user.state = "main_utility"
105
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"
110
111         # we've exceeded the maximum number of password failures, so disconnect
112         else:
113                 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
114                 user.state = "disconnecting"
115
116 def handler_checking_new_account_name(user):
117         """Handle input for the new user menu."""
118
119         # get the next waiting line of input
120         input_data = user.input_queue.pop(0)
121
122         # if there's input, take the first character and lowercase it
123         if input_data:
124                 choice = input_data.lower()[0]
125
126         # if there's no input, use the default
127         else:
128                 choice = muffmenu.get_default_menu_choice(user.state)
129
130         # user selected to disconnect
131         if choice == "d":
132                 user.state = "disconnecting"
133
134         # go back to the login screen
135         elif choice == "g":
136                 user.state = "entering_account_name"
137
138         # new user, so ask for a password
139         elif choice == "n":
140                 user.state = "entering_new_password"
141
142         # user entered a non-existent option
143         else:
144                 user.error = "default"
145
146 def handler_entering_new_password(user):
147         """Handle a new password entry."""
148
149         # get the next waiting line of input
150         input_data = user.input_queue.pop(0)
151
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)):
155
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"
159
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
163                 user.error = "weak"
164
165         # too many tries, so adios
166         else:
167                 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
168                 user.state = "disconnecting"
169
170 def handler_verifying_new_password(user):
171         """Handle the re-entered new password for verification."""
172
173         # get the next waiting line of input
174         input_data = user.input_queue.pop(0)
175
176         # hash the input and match it to storage
177         if md5.new(user.name + input_data).hexdigest() == user.passhash:
178                 user.authenticate()
179                 user.save()
180
181                 # the hashes matched, so go active
182                 if not user.replace_old_connections(): user.state = "main_utility"
183
184         # go back to entering the new password as long as you haven't tried
185         # too many times
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"
190
191         # otherwise, sayonara
192         else:
193                 user.send("$(eol)$(red)Too many failed password attempts...$(nrm)$(eol)")
194                 user.state = "disconnecting"
195
196 def handler_active(user):
197         """Handle input for active users."""
198
199         # get the next waiting line of input
200         input_data = user.input_queue.pop(0)
201
202         # split out the command (first word) and parameters (everything else)
203         try:
204                 inputlist = string.split(input_data, None, 1)
205                 command = inputlist[0]
206         except:
207                 command = input_data
208         try:
209                 parameters = inputlist[1]
210         except:
211                 parameters = ""
212         del(inputlist)
213
214         # lowercase the command
215         command = command.lower()
216
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"))
220
221         # no data matching the entered command word
222         elif command: command_error(user, command, parameters)
223
224 def command_halt(user, command="", parameters=""):
225         """Halt the world."""
226
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."
230
231         # let everyone know
232         muffmisc.broadcast(message)
233         muffmisc.log(message)
234
235         # set a flag to terminate the world
236         muffvars.terminate_world = True
237
238 def command_reload(user, command="", parameters=""):
239         """Reload all code modules, configs and data."""
240
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.")
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_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.")
255
256 def command_help(user, command="", parameters=""):
257         """List available commands and provide help for commands."""
258
259         # did the user ask for help on a specific command word?
260         if parameters:
261
262                 # is the command word one for which we have data?
263                 if parameters in muffuniv.universe.commands.keys():
264
265                         # add a description if provided
266                         description = muffuniv.universe.commands[parameters].get("description")
267                         if not description:
268                                 description = "(no short description provided)"
269                         output = "$(grn)" + parameters + "$(nrm) - " + description + "$(eol)$(eol)"
270
271                         # add the help text if provided
272                         help_text = muffuniv.universe.commands[parameters].get("help")
273                         if not help_text:
274                                 help_text = "No help is provided for this command."
275                         output += help_text
276
277                 # no data for the requested command word
278                 else:
279                         output = "That is not an available command."
280
281         # no specific command word was indicated
282         else:
283
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")
290                         if not 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\"."
294
295         # send the accumulated output to the user
296         user.send(output)
297
298 def command_say(user, command="", parameters=""):
299         """Speak to others in the same room."""
300
301         # check for replacement macros
302         if muffmisc.replace_macros(user, parameters, True) != parameters:
303                 user.send("You cannot speak $_(replacement macros).")
304
305         # the user entered a message
306         elif parameters:
307
308                 # get rid of quote marks on the ends of the message and
309                 # capitalize the first letter
310                 message = parameters.strip("\"'`").capitalize()
311
312                 # a dictionary of punctuation:action pairs
313                 actions = {}
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
319
320                 # set the default action
321                 action = actions[muffconf.config_data.get("language", "default_punctuation")]
322
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]
328                                 break
329
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
333
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() + " ")
338
339                 # tell the room
340                 # TODO: we won't be using broadcast once there are actual rooms
341                 muffmisc.broadcast(user.name + " " + action + "s, \"" + message + "\"")
342
343         # there was no message
344         else:
345                 user.send("What do you want to say?")
346
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()
352                 avatars.sort()
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()
357                 keys.sort()
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()
362                 keys.sort()
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?"
366         user.send(message)
367
368 def command_error(user, command="", parameters=""):
369         """Generic error for an unrecognized command word."""
370
371         # 90% of the time use a generic error
372         if random.randrange(10):
373                 message = "I'm not sure what \"" + command
374                 if parameters:
375                         message += " " + parameters
376                 message += "\" means..."
377
378         # 10% of the time use the classic diku error
379         else:
380                 message = "Arglebargle, glop-glyf!?!"
381
382         # send the error message
383         user.send(message)
384