""" In-Game Reporting System This contrib provides an in-game reporting system, with player-facing commands and a staff management interface. # Installation To install, just add the provided cmdset to your default AccountCmdSet: # in commands/default_cmdset.py from evennia.contrib.base_systems.ingame_reports import ReportsCmdSet class AccountCmdSet(default_cmds.AccountCmdSet): # ... def at_cmdset_creation(self): # ... self.add(ReportsCmdSet) # Features The contrib provides three commands by default and their associated report types: `CmdBug`, `CmdIdea`, and `CmdReport` (which is for reporting other players). The `ReportCmdBase` class holds most of the functionality for creating new reports, providing a convenient parent class for adding your own categories of reports. The contrib can be further configured through two settings, `INGAME_REPORT_TYPES` and `INGAME_REPORT_STATUS_TAGS` """ from django.conf import settings from evennia import CmdSet from evennia.utils import create, evmenu, logger, search from evennia.utils.utils import class_from_module, datetime_format, is_iter, iter_to_str from evennia.commands.default.muxcommand import MuxCommand from evennia.comms.models import Msg from . import menu # TODO: use actual default command class _DEFAULT_COMMAND_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) # the default report types _REPORT_TYPES = ("bugs", "ideas", "players") if hasattr(settings, "INGAME_REPORT_TYPES"): if is_iter(settings.INGAME_REPORT_TYPES): _REPORT_TYPES = settings.INGAME_REPORT_TYPES else: logger.log_warn( "The 'INGAME_REPORT_TYPES' setting must be an iterable of strings; falling back to defaults." ) def _get_report_hub(report_type): """ A helper function to retrieve the global script which acts as the hub for a given report type. Args: report_type (str): The category of reports to retrieve the script for. Returns: Script or None: The global script, or None if it couldn't be retrieved or created Note: If no matching valid script exists, this function will attempt to create it. """ hub_key = f"{report_type}_reports" # NOTE: due to a regression in GLOBAL_SCRIPTS, we use search_script instead of the container if not (hub := search.search_script(hub_key)): hub = create.create_script(key=hub_key) return hub or None class CmdManageReports(_DEFAULT_COMMAND_CLASS): """ manage the various reports Usage: manage [report type] Available report types: bugs ideas players Initializes a menu for reviewing and changing the status of current reports. """ key = "manage reports" aliases = tuple(f"manage {report_type}" for report_type in _REPORT_TYPES) locks = "cmd:pperm(Admin)" def get_help(self): """Returns a help string containing the configured available report types""" report_types = iter_to_str("\n ".join(_REPORT_TYPES)) helptext = f"""\ manage the various reports Usage: manage [report type] Available report types: {report_types} Initializes a menu for reviewing and changing the status of current reports. """ return helptext def func(self): report_type = self.cmdstring.split()[-1] if report_type == "reports": report_type = "players" if report_type not in _REPORT_TYPES: self.msg(f"'{report_type}' is not a valid report category.") return # remove the trailing s, just so everything reads nicer report_type = report_type[:-1] hub = _get_report_hub(report_type) if not hub: self.msg("You cannot manage that.") evmenu.EvMenu( self.account, menu, startnode="menunode_list_reports", hub=hub, persistent=True ) class ReportCmdBase(_DEFAULT_COMMAND_CLASS): """ A parent class for creating report commands. This help text may be displayed if your command's help text is not properly configured. """ help_category = "reports" # defines what locks the reports generated by this command will have set report_locks = "read:pperm(Admin)" # determines if the report can be filed without a target require_target = False # the message sent to the reporter after the report has been created success_msg = "Your report has been filed." # the report type for this command, if different from the key report_type = None def at_pre_cmd(self): """validate that the needed hub script exists - if not, cancel the command""" hub = _get_report_hub(self.report_type or self.key) if not hub: # a return value of True from `at_pre_cmd` cancels the command return True self.hub = hub return super().at_pre_cmd() def func(self): hub = self.hub if not self.args: self.msg("You must provide a message.") return if self.rhs: message = self.rhs target_str = self.lhs else: message = self.lhs target_str = "" target = None if target_str: target = self.caller.search(target_str) if not target: return elif self.require_target: self.msg("You must include a target.") return receivers = [hub] if target: receivers.append(target) if create.create_message( self.account, message, receivers=receivers, locks=self.report_locks, tags=["report"] ): # the report Msg was successfully created self.msg(self.success_msg) else: # something went wrong self.msg( "Something went wrong creating your report. Please try again later or contact staff directly." ) # The commands below are the usable reporting commands class CmdBug(ReportCmdBase): """ file a bug Usage: bug [ =] Note: If a specific object, location or character is bugged, please target it for the report. Examples: bug hammer = This doesn't work as a crafting tool but it should bug every time I go through a door I get the message twice """ key = "bug" report_locks = "read:pperm(Developer)" class CmdReport(ReportCmdBase): """ report a player Usage: report = All player reports will be reviewed. """ key = "report" report_type = "player" require_target = True account_caller = True class CmdIdea(ReportCmdBase): """ submit a suggestion Usage: ideas idea Example: idea wouldn't it be cool if we had horses we could ride """ key = "idea" aliases = ("ideas",) report_locks = "read:pperm(Builder)" success_msg = "Thank you for your suggestion!" def func(self): # we add an extra feature to this command, allowing you to see all your submitted ideas if self.cmdstring == "ideas": # list your ideas if ( ideas := Msg.objects.search_message(sender=self.account, receiver=self.hub) .order_by("-db_date_created") .exclude(db_tags__db_key="closed") ): # todo: use a paginated menu self.msg( "Ideas you've submitted:\n " + "\n ".join( f"|w{item.message}|n (submitted {datetime_format(item.date_created)})" for item in ideas ) ) else: self.msg("You have no open suggestions.") return # proceed to do the normal report-command functionality super().func() class ReportsCmdSet(CmdSet): key = "Reports CmdSet" def at_cmdset_creation(self): super().at_cmdset_creation() if "bugs" in _REPORT_TYPES: self.add(CmdBug) if "ideas" in _REPORT_TYPES: self.add(CmdIdea) if "players" in _REPORT_TYPES: self.add(CmdReport) self.add(CmdManageReports)