Drop deprecation filters for pip and yamllint
[mudpy.git] / mudpy / command.py
1 """User command functions for the mudpy engine."""
2
3 # Copyright (c) 2004-2022 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 c_get(actor, parameters):
166     """Move a prop into inventory."""
167     if not parameters:
168         message = "What do you wish to get?"
169     else:
170         message = ('Not yet implemented.')
171     actor.send(message)
172     return True
173
174
175 def drop(actor, parameters):
176     """Move a prop out of inventory."""
177     if not parameters:
178         message = "What do you wish to drop?"
179     else:
180         message = ('Not yet implemented.')
181     actor.send(message)
182     return True
183
184
185 def halt(actor, parameters):
186     """Halt the world."""
187     if actor.owner:
188
189         # see if there's a message or use a generic one
190         if parameters:
191             message = "Halting: " + parameters
192         else:
193             message = "User " + actor.owner.account.get(
194                 "name"
195             ) + " halted the world."
196
197         # let everyone know
198         mudpy.misc.broadcast(message, add_prompt=False)
199         mudpy.misc.log(message, 8)
200
201         # set a flag to terminate the world
202         actor.universe.terminate_flag = True
203     return True
204
205
206 def help(actor, parameters):
207     """List available commands and provide help for commands."""
208
209     # did the user ask for help on a specific command word?
210     if parameters and actor.owner:
211
212         # is the command word one for which we have data?
213         command = mudpy.misc.find_command(parameters)
214
215         # only for allowed commands
216         if actor.can_run(command):
217
218             # add a description if provided
219             description = command.get("description")
220             if not description:
221                 description = "(no short description provided)"
222             if command.is_restricted():
223                 output = "$(red)"
224             else:
225                 output = "$(grn)"
226             output = "%s%s$(nrm) - %s$(eol)$(eol)" % (
227                 output, command.subkey, description)
228
229             # add the help text if provided
230             help_text = command.get("help")
231             if not help_text:
232                 help_text = "No help is provided for this command."
233             output += help_text
234
235             # list related commands
236             see_also = command.get("see_also")
237             if see_also:
238                 really_see_also = ""
239                 for item in see_also:
240                     if item in actor.universe.groups["command"]:
241                         command = actor.universe.groups["command"][item]
242                         if actor.can_run(command):
243                             if really_see_also:
244                                 really_see_also += ", "
245                             if command.is_restricted():
246                                 really_see_also += "$(red)"
247                             else:
248                                 really_see_also += "$(grn)"
249                             really_see_also += item + "$(nrm)"
250                 if really_see_also:
251                     output += "$(eol)$(eol)See also: " + really_see_also
252
253         # no data for the requested command word
254         else:
255             output = "That is not an available command."
256
257     # no specific command word was indicated
258     else:
259
260         # preamble text
261         output = ("These are the commands available to you [brackets indicate "
262                   "optional portion]:$(eol)$(eol)")
263
264         # list command names in alphabetical order
265         for command_name, command in sorted(
266                 actor.universe.groups["command"].items()):
267
268             # skip over disallowed commands
269             if actor.can_run(command):
270
271                 # start incrementing substrings
272                 for position in range(1, len(command_name) + 1):
273
274                     # we've found our shortest possible abbreviation
275                     candidate = mudpy.misc.find_command(
276                             command_name[:position])
277                     try:
278                         if candidate.subkey == command_name:
279                             break
280                     except AttributeError:
281                         pass
282
283                 # use square brackets to indicate optional part of command name
284                 if position < len(command_name):
285                     abbrev = "%s[%s]" % (
286                         command_name[:position], command_name[position:])
287                 else:
288                     abbrev = command_name
289
290                 # supply a useful default if the short description is missing
291                 description = command.get(
292                     "description", "(no short description provided)")
293
294                 # administrative command names are in red, others in green
295                 if command.is_restricted():
296                     color = "red"
297                 else:
298                     color = "grn"
299
300                 # format the entry for this command
301                 output = "%s   $(%s)%s$(nrm) - %s$(eol)" % (
302                     output, color, abbrev, description)
303
304         # add a footer with instructions on getting additional information
305         output = ('%s $(eol)Enter "help COMMAND" for help on a command named '
306                   '"COMMAND".' % output)
307
308     # send the accumulated output to the user
309     actor.send(output)
310     return True
311
312
313 def inventory(actor, parameters):
314     """List the inventory."""
315     message = ('Not yet implemented.')
316     actor.send(message)
317     return True
318
319
320 def look(actor, parameters):
321     """Look around."""
322     if parameters:
323         actor.send("You can't look at or in anything yet.")
324     else:
325         actor.look_at(actor.get("location"))
326     return True
327
328
329 def move(actor, parameters):
330     """Move the avatar in a given direction."""
331     for portal in sorted(
332             actor.universe.contents[actor.get("location")].portals()):
333         if portal.startswith(parameters):
334             actor.move_direction(portal)
335             return portal
336     actor.send("You cannot go that way.")
337     return True
338
339
340 def preferences(actor, parameters):
341     """List, view and change actor preferences."""
342
343     # Escape replacement macros in preferences
344     parameters = mudpy.misc.escape_macros(parameters)
345
346     message = ""
347     arguments = parameters.split()
348     allowed_prefs = set()
349     base_prefs = []
350     user_config = actor.universe.contents.get("mudpy.user")
351     if user_config:
352         base_prefs = user_config.get("pref_allow", [])
353         allowed_prefs.update(base_prefs)
354         if actor.owner.account.get("administrator"):
355             allowed_prefs.update(user_config.get("pref_admin", []))
356     if not arguments:
357         message += "These are your current preferences:"
358
359         # color-code base and admin prefs
360         for pref in sorted(allowed_prefs):
361             if pref in base_prefs:
362                 color = "grn"
363             else:
364                 color = "red"
365             message += ("$(eol)   $(%s)%s$(nrm) - %s" % (
366                 color, pref, actor.owner.account.get(pref, "<not set>")))
367
368     elif arguments[0] not in allowed_prefs:
369         message += (
370             'Preference "%s" does not exist. Try the `preferences` command by '
371             "itself for a list of valid preferences." % arguments[0])
372     elif len(arguments) == 1:
373         message += "%s" % actor.owner.account.get(arguments[0], "<not set>")
374     else:
375         pref = arguments[0]
376         value = " ".join(arguments[1:])
377         try:
378             actor.owner.account.set(pref, value)
379             message += 'Preference "%s" set to "%s".' % (pref, value)
380         except ValueError:
381             message = (
382                 'Preference "%s" cannot be set to type "%s".' % (
383                     pref, type(value)))
384     actor.send(message)
385     return True
386
387
388 def quit(actor, parameters):
389     """Leave the world and go back to the main menu."""
390     if actor.owner:
391         actor.owner.state = "main_utility"
392         actor.owner.deactivate_avatar()
393     return True
394
395
396 def reload(actor, parameters):
397     """Reload all code modules, configs and data."""
398     if actor.owner:
399
400         # let the user know and log
401         actor.send("Reloading all code modules, configs and data.")
402         mudpy.misc.log(
403             "User " +
404             actor.owner.account.get("name") + " reloaded the world.",
405             6
406         )
407
408         # set a flag to reload
409         actor.universe.reload_flag = True
410     return True
411
412
413 def say(actor, parameters):
414     """Speak to others in the same area."""
415
416     # check for replacement macros and escape them
417     parameters = mudpy.misc.escape_macros(parameters)
418
419     # if the message is wrapped in quotes, remove them and leave contents
420     # intact
421     if parameters.startswith('"') and parameters.endswith('"'):
422         message = parameters[1:-1]
423         literal = True
424
425     # otherwise, get rid of stray quote marks on the ends of the message
426     else:
427         message = parameters.strip('''"'`''')
428         literal = False
429
430     # the user entered a message
431     if message:
432
433         # match the punctuation used, if any, to an action
434         if "mudpy.linguistic" in actor.universe.contents:
435             actions = actor.universe.contents[
436                 "mudpy.linguistic"].get("actions", {})
437             default_punctuation = (actor.universe.contents[
438                 "mudpy.linguistic"].get("default_punctuation", "."))
439         else:
440             actions = {}
441             default_punctuation = "."
442         action = ""
443
444         # reverse sort punctuation options so the longest match wins
445         for mark in sorted(actions.keys(), reverse=True):
446             if not literal and message.endswith(mark):
447                 action = actions[mark]
448                 break
449
450         # add punctuation if needed
451         if not action:
452             action = actions[default_punctuation]
453             if message and not (
454                literal or unicodedata.category(message[-1]) == "Po"
455                ):
456                 message += default_punctuation
457
458         # failsafe checks to avoid unwanted reformatting and null strings
459         if message and not literal:
460
461             # decapitalize the first letter to improve matching
462             message = message[0].lower() + message[1:]
463
464             # iterate over all words in message, replacing typos
465             if "mudpy.linguistic" in actor.universe.contents:
466                 typos = actor.universe.contents[
467                     "mudpy.linguistic"].get("typos", {})
468             else:
469                 typos = {}
470             words = message.split()
471             for index in range(len(words)):
472                 word = words[index]
473                 while unicodedata.category(word[0]) == "Po":
474                     word = word[1:]
475                 while unicodedata.category(word[-1]) == "Po":
476                     word = word[:-1]
477                 if word in typos.keys():
478                     words[index] = words[index].replace(word, typos[word])
479             message = " ".join(words)
480
481             # capitalize the first letter
482             message = message[0].upper() + message[1:]
483
484     # tell the area
485     if message:
486         actor.echo_to_location(
487             actor.get("name") + " " + action + 's, "' + message + '"'
488         )
489         actor.send("You " + action + ', "' + message + '"')
490
491     # there was no message
492     else:
493         actor.send("What do you want to say?")
494     return True
495
496
497 def c_set(actor, parameters):
498     """Set a facet of an element."""
499     if not parameters:
500         message = "You must specify an element, a facet and a value."
501     else:
502         arguments = parameters.split(" ", 2)
503         if len(arguments) == 1:
504             message = ('What facet of element "' + arguments[0]
505                        + '" would you like to set?')
506         elif len(arguments) == 2:
507             message = ('What value would you like to set for the "' +
508                        arguments[1] + '" facet of the "' + arguments[0]
509                        + '" element?')
510         else:
511             element, facet, value = arguments
512             if element not in actor.universe.contents:
513                 message = 'The "' + element + '" element does not exist.'
514             else:
515                 try:
516                     actor.universe.contents[element].set(facet, value)
517                 except PermissionError:
518                     message = ('The "%s" element is kept in read-only file '
519                                '"%s" and cannot be altered.' %
520                                (element, actor.universe.contents[
521                                         element].origin.source))
522                 except ValueError:
523                     message = ('Value "%s" of type "%s" cannot be coerced '
524                                'to the correct datatype for facet "%s".' %
525                                (value, type(value), facet))
526                 else:
527                     message = ('You have successfully (re)set the "' + facet
528                                + '" facet of element "' + element
529                                + '". Try "show element ' +
530                                element + '" for verification.')
531     actor.send(message)
532     return True
533
534
535 def show(actor, parameters):
536     """Show program data."""
537     message = ""
538     arguments = parameters.split()
539     if not parameters:
540         message = "What do you want to show?"
541     elif arguments[0] == "version":
542         message = repr(actor.universe.versions)
543     elif arguments[0] == "time":
544         message = "%s increments elapsed since the world was created." % (
545             str(actor.universe.groups["internal"]["counters"].get("elapsed")))
546     elif arguments[0] == "groups":
547         message = "These are the element groups:$(eol)"
548         groups = list(actor.universe.groups.keys())
549         groups.sort()
550         for group in groups:
551             message += "$(eol)   $(grn)" + group + "$(nrm)"
552     elif arguments[0] == "files":
553         message = "These are the current files containing the universe:$(eol)"
554         filenames = sorted(actor.universe.files)
555         for filename in filenames:
556             if actor.universe.files[filename].is_writeable():
557                 status = "rw"
558             else:
559                 status = "ro"
560             message += ("$(eol)   $(red)(%s) $(grn)%s$(nrm)" %
561                         (status, filename))
562             if actor.universe.files[filename].flags:
563                 message += (" $(yel)[%s]$(nrm)" %
564                             ",".join(actor.universe.files[filename].flags))
565     elif arguments[0] == "group":
566         if len(arguments) != 2:
567             message = "You must specify one group."
568         elif arguments[1] in actor.universe.groups:
569             message = ('These are the elements in the "' + arguments[1]
570                        + '" group:$(eol)')
571             elements = [
572                 (
573                     actor.universe.groups[arguments[1]][x].key
574                 ) for x in actor.universe.groups[arguments[1]].keys()
575             ]
576             elements.sort()
577             for element in elements:
578                 message += "$(eol)   $(grn)" + element + "$(nrm)"
579         else:
580             message = 'Group "' + arguments[1] + '" does not exist.'
581     elif arguments[0] == "file":
582         if len(arguments) != 2:
583             message = "You must specify one file."
584         elif arguments[1] in actor.universe.files:
585             message = ('These are the nodes in the "' + arguments[1]
586                        + '" file:$(eol)')
587             elements = sorted(actor.universe.files[arguments[1]].data)
588             for element in elements:
589                 message += "$(eol)   $(grn)" + element + "$(nrm)"
590         else:
591             message = 'File "%s" does not exist.' % arguments[1]
592     elif arguments[0] == "element":
593         if len(arguments) != 2:
594             message = "You must specify one element."
595         elif arguments[1].strip(".") in actor.universe.contents:
596             element = actor.universe.contents[arguments[1].strip(".")]
597             message = ('These are the properties of the "' + arguments[1]
598                        + '" element (in "' + element.origin.source
599                        + '"):$(eol)')
600             facets = element.facets()
601             for facet in sorted(facets):
602                 message += ("$(eol)   $(grn)%s: $(red)%s$(nrm)" %
603                             (facet, str(facets[facet])))
604         else:
605             message = 'Element "' + arguments[1] + '" does not exist.'
606     elif arguments[0] == "log":
607         if len(arguments) == 4:
608             if re.match(r"^\d+$", arguments[3]) and int(arguments[3]) >= 0:
609                 stop = int(arguments[3])
610             else:
611                 stop = -1
612         else:
613             stop = 0
614         if len(arguments) >= 3:
615             if re.match(r"^\d+$", arguments[2]) and int(arguments[2]) > 0:
616                 start = int(arguments[2])
617             else:
618                 start = -1
619         else:
620             start = 10
621         if len(arguments) >= 2:
622             if (re.match(r"^\d+$", arguments[1])
623                     and 0 <= int(arguments[1]) <= 9):
624                 level = int(arguments[1])
625             else:
626                 level = -1
627         elif 0 <= actor.owner.account.get("loglevel", 0) <= 9:
628             level = actor.owner.account.get("loglevel", 0)
629         else:
630             level = 1
631         if level > -1 and start > -1 and stop > -1:
632             message = mudpy.misc.get_loglines(level, start, stop)
633         else:
634             message = ("When specified, level must be 0-9 (default 1), "
635                        "start and stop must be >=1 (default 10 and 1).")
636     else:
637         message = '''I don't know what "''' + parameters + '" is.'
638     actor.send(message)
639     return True