Use consistent spacing in tox variables and lists
[mudpy.git] / mudpy / command.py
1 """User command functions for the mudpy engine."""
2
3 # Copyright (c) 2004-2020 mudpy authors. Permission to use, copy,
4 # modify, and distribute this software is granted under terms
5 # provided in the LICENSE file distributed with this software.
6
7 import random
8 import re
9 import traceback
10 import unicodedata
11
12 import mudpy
13
14
15 def chat(actor, parameters):
16     """Toggle chat mode."""
17     mode = actor.get("mode")
18     if not mode:
19         actor.set("mode", "chat")
20         actor.send("Entering chat mode (use $(grn)!chat$(nrm) to exit).")
21     elif mode == "chat":
22         actor.remove_facet("mode")
23         actor.send("Exiting chat mode.")
24     else:
25         actor.send("Sorry, but you're already busy with something else!")
26     return True
27
28
29 def create(actor, parameters):
30     """Create an element if it does not exist."""
31     if not parameters:
32         message = "You must at least specify an element to create."
33     elif not actor.owner:
34         message = ""
35     else:
36         arguments = parameters.split()
37         if len(arguments) == 1:
38             arguments.append("")
39         if len(arguments) == 2:
40             element, filename = arguments
41             if element in actor.universe.contents:
42                 message = 'The "' + element + '" element already exists.'
43             else:
44                 message = ('You create "' +
45                            element + '" within the universe.')
46                 logline = actor.owner.account.get(
47                     "name"
48                 ) + " created an element: " + element
49                 if filename:
50                     logline += " in file " + filename
51                     if filename not in actor.universe.files:
52                         message += (
53                             ' Warning: "' + filename + '" is not yet '
54                             "included in any other file and will not be read "
55                             "on startup unless this is remedied.")
56                 mudpy.misc.Element(element, actor.universe, filename)
57                 mudpy.misc.log(logline, 6)
58         elif len(arguments) > 2:
59             message = "You can only specify an element and a filename."
60     actor.send(message)
61     return True
62
63
64 def delete(actor, parameters):
65     """Delete a facet from an element."""
66     if not parameters:
67         message = "You must specify an element and a facet."
68     else:
69         arguments = parameters.split(" ")
70         if len(arguments) == 1:
71             message = ('What facet of element "' + arguments[0]
72                        + '" would you like to delete?')
73         elif len(arguments) != 2:
74             message = "You may only specify an element and a facet."
75         else:
76             element, facet = arguments
77             if element not in actor.universe.contents:
78                 message = 'The "' + element + '" element does not exist.'
79             elif facet not in actor.universe.contents[element].facets():
80                 message = ('The "' + element + '" element has no "' + facet
81                            + '" facet.')
82             else:
83                 actor.universe.contents[element].remove_facet(facet)
84                 message = ('You have successfully deleted the "' + facet
85                            + '" facet of element "' + element
86                            + '". Try "show element ' +
87                            element + '" for verification.')
88     actor.send(message)
89     return True
90
91
92 def destroy(actor, parameters):
93     """Destroy an element if it exists."""
94     if actor.owner:
95         if not parameters:
96             message = "You must specify an element to destroy."
97         else:
98             if parameters not in actor.universe.contents:
99                 message = 'The "' + parameters + '" element does not exist.'
100             else:
101                 actor.universe.contents[parameters].destroy()
102                 message = ('You destroy "' + parameters
103                            + '" within the universe.')
104                 mudpy.misc.log(
105                     actor.owner.account.get(
106                         "name"
107                     ) + " destroyed an element: " + parameters,
108                     6
109                 )
110         actor.send(message)
111     return True
112
113
114 def error(actor, input_data):
115     """Generic error for an unrecognized command word."""
116
117     # 90% of the time use a generic error
118     # Allow the random.randrange() call in bandit since it's not used for
119     # security/cryptographic purposes
120     if random.randrange(10):  # nosec
121         message = '''I'm not sure what "''' + input_data + '''" means...'''
122
123     # 10% of the time use the classic diku error
124     else:
125         message = "Arglebargle, glop-glyf!?!"
126
127     # try to send the error message, and log if we can't
128     try:
129         actor.send(message)
130     except Exception:
131         mudpy.misc.log(
132             'Sending a command error to user %s raised exception...\n%s' % (
133                 actor.owner.account.get("name"), traceback.format_exc()))
134     return True
135
136
137 def evaluate(actor, parameters):
138     """Evaluate a Python expression."""
139
140     if not parameters:
141         message = "You need to supply a Python expression."
142     elif "__" in parameters:
143         message = "Double-underscores (__) are not allowed in expressions."
144     elif "lambda" in parameters:
145         message = "Lambda functions are not allowed in expressions."
146     else:
147         # Strictly limit the allowed builtins and modules
148         eval_globals = {"__builtins__": dict()}
149         for allowed in ("dir", "globals", "len", "locals"):
150             eval_globals["__builtins__"][allowed] = __builtins__[allowed]
151         eval_globals["mudpy"] = mudpy
152         eval_globals["universe"] = actor.universe
153         try:
154             # there is no other option than to use eval() for this, since
155             # its purpose is to evaluate arbitrary expressions, so do what
156             # we can to secure it and allow it for bandit analysis
157             message = repr(eval(parameters, eval_globals))  # nosec
158         except Exception as e:
159             message = ("$(red)Your expression raised an exception...$(eol)"
160                        "$(eol)$(bld)%s$(nrm)" % e)
161     actor.send(message)
162     return True
163
164
165 def halt(actor, parameters):
166     """Halt the world."""
167     if actor.owner:
168
169         # see if there's a message or use a generic one
170         if parameters:
171             message = "Halting: " + parameters
172         else:
173             message = "User " + actor.owner.account.get(
174                 "name"
175             ) + " halted the world."
176
177         # let everyone know
178         mudpy.misc.broadcast(message, add_prompt=False)
179         mudpy.misc.log(message, 8)
180
181         # set a flag to terminate the world
182         actor.universe.terminate_flag = True
183     return True
184
185
186 def help(actor, parameters):
187     """List available commands and provide help for commands."""
188
189     # did the user ask for help on a specific command word?
190     if parameters and actor.owner:
191
192         # is the command word one for which we have data?
193         command = mudpy.misc.find_command(parameters)
194
195         # only for allowed commands
196         if actor.can_run(command):
197
198             # add a description if provided
199             description = command.get("description")
200             if not description:
201                 description = "(no short description provided)"
202             if command.is_restricted():
203                 output = "$(red)"
204             else:
205                 output = "$(grn)"
206             output = "%s%s$(nrm) - %s$(eol)$(eol)" % (
207                 output, command.subkey, description)
208
209             # add the help text if provided
210             help_text = command.get("help")
211             if not help_text:
212                 help_text = "No help is provided for this command."
213             output += help_text
214
215             # list related commands
216             see_also = command.get("see_also")
217             if see_also:
218                 really_see_also = ""
219                 for item in see_also:
220                     if item in actor.universe.groups["command"]:
221                         command = actor.universe.groups["command"][item]
222                         if actor.can_run(command):
223                             if really_see_also:
224                                 really_see_also += ", "
225                             if command.is_restricted():
226                                 really_see_also += "$(red)"
227                             else:
228                                 really_see_also += "$(grn)"
229                             really_see_also += item + "$(nrm)"
230                 if really_see_also:
231                     output += "$(eol)$(eol)See also: " + really_see_also
232
233         # no data for the requested command word
234         else:
235             output = "That is not an available command."
236
237     # no specific command word was indicated
238     else:
239
240         # preamble text
241         output = ("These are the commands available to you [brackets indicate "
242                   "optional portion]:$(eol)$(eol)")
243
244         # list command names in alphabetical order
245         for command_name, command in sorted(
246                 actor.universe.groups["command"].items()):
247
248             # skip over disallowed commands
249             if actor.can_run(command):
250
251                 # start incrementing substrings
252                 for position in range(1, len(command_name) + 1):
253
254                     # we've found our shortest possible abbreviation
255                     candidate = mudpy.misc.find_command(
256                             command_name[:position])
257                     try:
258                         if candidate.subkey == command_name:
259                             break
260                     except AttributeError:
261                         pass
262
263                 # use square brackets to indicate optional part of command name
264                 if position < len(command_name):
265                     abbrev = "%s[%s]" % (
266                         command_name[:position], command_name[position:])
267                 else:
268                     abbrev = command_name
269
270                 # supply a useful default if the short description is missing
271                 description = command.get(
272                     "description", "(no short description provided)")
273
274                 # administrative command names are in red, others in green
275                 if command.is_restricted():
276                     color = "red"
277                 else:
278                     color = "grn"
279
280                 # format the entry for this command
281                 output = "%s   $(%s)%s$(nrm) - %s$(eol)" % (
282                     output, color, abbrev, description)
283
284         # add a footer with instructions on getting additional information
285         output = ('%s $(eol)Enter "help COMMAND" for help on a command named '
286                   '"COMMAND".' % output)
287
288     # send the accumulated output to the user
289     actor.send(output)
290     return True
291
292
293 def look(actor, parameters):
294     """Look around."""
295     if parameters:
296         actor.send("You can't look at or in anything yet.")
297     else:
298         actor.look_at(actor.get("location"))
299     return True
300
301
302 def move(actor, parameters):
303     """Move the avatar in a given direction."""
304     for portal in sorted(
305             actor.universe.contents[actor.get("location")].portals()):
306         if portal.startswith(parameters):
307             actor.move_direction(portal)
308             return portal
309     actor.send("You cannot go that way.")
310     return True
311
312
313 def preferences(actor, parameters):
314     """List, view and change actor preferences."""
315
316     # Escape replacement macros in preferences
317     parameters = mudpy.misc.escape_macros(parameters)
318
319     message = ""
320     arguments = parameters.split()
321     allowed_prefs = set()
322     base_prefs = []
323     user_config = actor.universe.contents.get("mudpy.user")
324     if user_config:
325         base_prefs = user_config.get("pref_allow", [])
326         allowed_prefs.update(base_prefs)
327         if actor.owner.account.get("administrator"):
328             allowed_prefs.update(user_config.get("pref_admin", []))
329     if not arguments:
330         message += "These are your current preferences:"
331
332         # color-code base and admin prefs
333         for pref in sorted(allowed_prefs):
334             if pref in base_prefs:
335                 color = "grn"
336             else:
337                 color = "red"
338             message += ("$(eol)   $(%s)%s$(nrm) - %s" % (
339                 color, pref, actor.owner.account.get(pref, "<not set>")))
340
341     elif arguments[0] not in allowed_prefs:
342         message += (
343             'Preference "%s" does not exist. Try the `preferences` command by '
344             "itself for a list of valid preferences." % arguments[0])
345     elif len(arguments) == 1:
346         message += "%s" % actor.owner.account.get(arguments[0], "<not set>")
347     else:
348         pref = arguments[0]
349         value = " ".join(arguments[1:])
350         try:
351             actor.owner.account.set(pref, value)
352             message += 'Preference "%s" set to "%s".' % (pref, value)
353         except ValueError:
354             message = (
355                 'Preference "%s" cannot be set to type "%s".' % (
356                     pref, type(value)))
357     actor.send(message)
358     return True
359
360
361 def quit(actor, parameters):
362     """Leave the world and go back to the main menu."""
363     if actor.owner:
364         actor.owner.state = "main_utility"
365         actor.owner.deactivate_avatar()
366     return True
367
368
369 def reload(actor, parameters):
370     """Reload all code modules, configs and data."""
371     if actor.owner:
372
373         # let the user know and log
374         actor.send("Reloading all code modules, configs and data.")
375         mudpy.misc.log(
376             "User " +
377             actor.owner.account.get("name") + " reloaded the world.",
378             6
379         )
380
381         # set a flag to reload
382         actor.universe.reload_flag = True
383     return True
384
385
386 def say(actor, parameters):
387     """Speak to others in the same area."""
388
389     # check for replacement macros and escape them
390     parameters = mudpy.misc.escape_macros(parameters)
391
392     # if the message is wrapped in quotes, remove them and leave contents
393     # intact
394     if parameters.startswith('"') and parameters.endswith('"'):
395         message = parameters[1:-1]
396         literal = True
397
398     # otherwise, get rid of stray quote marks on the ends of the message
399     else:
400         message = parameters.strip('''"'`''')
401         literal = False
402
403     # the user entered a message
404     if message:
405
406         # match the punctuation used, if any, to an action
407         if "mudpy.linguistic" in actor.universe.contents:
408             actions = actor.universe.contents[
409                 "mudpy.linguistic"].get("actions", {})
410             default_punctuation = (actor.universe.contents[
411                 "mudpy.linguistic"].get("default_punctuation", "."))
412         else:
413             actions = {}
414             default_punctuation = "."
415         action = ""
416
417         # reverse sort punctuation options so the longest match wins
418         for mark in sorted(actions.keys(), reverse=True):
419             if not literal and message.endswith(mark):
420                 action = actions[mark]
421                 break
422
423         # add punctuation if needed
424         if not action:
425             action = actions[default_punctuation]
426             if message and not (
427                literal or unicodedata.category(message[-1]) == "Po"
428                ):
429                 message += default_punctuation
430
431         # failsafe checks to avoid unwanted reformatting and null strings
432         if message and not literal:
433
434             # decapitalize the first letter to improve matching
435             message = message[0].lower() + message[1:]
436
437             # iterate over all words in message, replacing typos
438             if "mudpy.linguistic" in actor.universe.contents:
439                 typos = actor.universe.contents[
440                     "mudpy.linguistic"].get("typos", {})
441             else:
442                 typos = {}
443             words = message.split()
444             for index in range(len(words)):
445                 word = words[index]
446                 while unicodedata.category(word[0]) == "Po":
447                     word = word[1:]
448                 while unicodedata.category(word[-1]) == "Po":
449                     word = word[:-1]
450                 if word in typos.keys():
451                     words[index] = words[index].replace(word, typos[word])
452             message = " ".join(words)
453
454             # capitalize the first letter
455             message = message[0].upper() + message[1:]
456
457     # tell the area
458     if message:
459         actor.echo_to_location(
460             actor.get("name") + " " + action + 's, "' + message + '"'
461         )
462         actor.send("You " + action + ', "' + message + '"')
463
464     # there was no message
465     else:
466         actor.send("What do you want to say?")
467     return True
468
469
470 def c_set(actor, parameters):
471     """Set a facet of an element."""
472     if not parameters:
473         message = "You must specify an element, a facet and a value."
474     else:
475         arguments = parameters.split(" ", 2)
476         if len(arguments) == 1:
477             message = ('What facet of element "' + arguments[0]
478                        + '" would you like to set?')
479         elif len(arguments) == 2:
480             message = ('What value would you like to set for the "' +
481                        arguments[1] + '" facet of the "' + arguments[0]
482                        + '" element?')
483         else:
484             element, facet, value = arguments
485             if element not in actor.universe.contents:
486                 message = 'The "' + element + '" element does not exist.'
487             else:
488                 try:
489                     actor.universe.contents[element].set(facet, value)
490                 except PermissionError:
491                     message = ('The "%s" element is kept in read-only file '
492                                '"%s" and cannot be altered.' %
493                                (element, actor.universe.contents[
494                                         element].origin.source))
495                 except ValueError:
496                     message = ('Value "%s" of type "%s" cannot be coerced '
497                                'to the correct datatype for facet "%s".' %
498                                (value, type(value), facet))
499                 else:
500                     message = ('You have successfully (re)set the "' + facet
501                                + '" facet of element "' + element
502                                + '". Try "show element ' +
503                                element + '" for verification.')
504     actor.send(message)
505     return True
506
507
508 def show(actor, parameters):
509     """Show program data."""
510     message = ""
511     arguments = parameters.split()
512     if not parameters:
513         message = "What do you want to show?"
514     elif arguments[0] == "version":
515         message = repr(actor.universe.versions)
516     elif arguments[0] == "time":
517         message = "%s increments elapsed since the world was created." % (
518             str(actor.universe.groups["internal"]["counters"].get("elapsed")))
519     elif arguments[0] == "groups":
520         message = "These are the element groups:$(eol)"
521         groups = list(actor.universe.groups.keys())
522         groups.sort()
523         for group in groups:
524             message += "$(eol)   $(grn)" + group + "$(nrm)"
525     elif arguments[0] == "files":
526         message = "These are the current files containing the universe:$(eol)"
527         filenames = sorted(actor.universe.files)
528         for filename in filenames:
529             if actor.universe.files[filename].is_writeable():
530                 status = "rw"
531             else:
532                 status = "ro"
533             message += ("$(eol)   $(red)(%s) $(grn)%s$(nrm)" %
534                         (status, filename))
535             if actor.universe.files[filename].flags:
536                 message += (" $(yel)[%s]$(nrm)" %
537                             ",".join(actor.universe.files[filename].flags))
538     elif arguments[0] == "group":
539         if len(arguments) != 2:
540             message = "You must specify one group."
541         elif arguments[1] in actor.universe.groups:
542             message = ('These are the elements in the "' + arguments[1]
543                        + '" group:$(eol)')
544             elements = [
545                 (
546                     actor.universe.groups[arguments[1]][x].key
547                 ) for x in actor.universe.groups[arguments[1]].keys()
548             ]
549             elements.sort()
550             for element in elements:
551                 message += "$(eol)   $(grn)" + element + "$(nrm)"
552         else:
553             message = 'Group "' + arguments[1] + '" does not exist.'
554     elif arguments[0] == "file":
555         if len(arguments) != 2:
556             message = "You must specify one file."
557         elif arguments[1] in actor.universe.files:
558             message = ('These are the nodes in the "' + arguments[1]
559                        + '" file:$(eol)')
560             elements = sorted(actor.universe.files[arguments[1]].data)
561             for element in elements:
562                 message += "$(eol)   $(grn)" + element + "$(nrm)"
563         else:
564             message = 'File "%s" does not exist.' % arguments[1]
565     elif arguments[0] == "element":
566         if len(arguments) != 2:
567             message = "You must specify one element."
568         elif arguments[1].strip(".") in actor.universe.contents:
569             element = actor.universe.contents[arguments[1].strip(".")]
570             message = ('These are the properties of the "' + arguments[1]
571                        + '" element (in "' + element.origin.source
572                        + '"):$(eol)')
573             facets = element.facets()
574             for facet in sorted(facets):
575                 message += ("$(eol)   $(grn)%s: $(red)%s$(nrm)" %
576                             (facet, str(facets[facet])))
577         else:
578             message = 'Element "' + arguments[1] + '" does not exist.'
579     elif arguments[0] == "log":
580         if len(arguments) == 4:
581             if re.match(r"^\d+$", arguments[3]) and int(arguments[3]) >= 0:
582                 stop = int(arguments[3])
583             else:
584                 stop = -1
585         else:
586             stop = 0
587         if len(arguments) >= 3:
588             if re.match(r"^\d+$", arguments[2]) and int(arguments[2]) > 0:
589                 start = int(arguments[2])
590             else:
591                 start = -1
592         else:
593             start = 10
594         if len(arguments) >= 2:
595             if (re.match(r"^\d+$", arguments[1])
596                     and 0 <= int(arguments[1]) <= 9):
597                 level = int(arguments[1])
598             else:
599                 level = -1
600         elif 0 <= actor.owner.account.get("loglevel", 0) <= 9:
601             level = actor.owner.account.get("loglevel", 0)
602         else:
603             level = 1
604         if level > -1 and start > -1 and stop > -1:
605             message = mudpy.misc.get_loglines(level, start, stop)
606         else:
607             message = ("When specified, level must be 0-9 (default 1), "
608                        "start and stop must be >=1 (default 10 and 1).")
609     else:
610         message = '''I don't know what "''' + parameters + '" is.'
611     actor.send(message)
612     return True