From aa5a07f6d02abd0de17fc1b3d44313425e84e7e1 Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 19 Apr 2021 09:19:15 +0200 Subject: [PATCH] Continuing unittest work --- evennia/commands/default/cmdset_account.py | 1 + evennia/commands/default/comms.py | 266 ++++++++++++++++++--- evennia/commands/default/tests.py | 166 ++++++++++++- evennia/utils/evmenu.py | 10 +- 4 files changed, 401 insertions(+), 42 deletions(-) diff --git a/evennia/commands/default/cmdset_account.py b/evennia/commands/default/cmdset_account.py index 553c7fdf7d..606af42df2 100644 --- a/evennia/commands/default/cmdset_account.py +++ b/evennia/commands/default/cmdset_account.py @@ -59,6 +59,7 @@ class AccountCmdSet(CmdSet): self.add(admin.CmdNewPassword()) # Comm commands + self.add(comms.CmdChannel()) self.add(comms.CmdAddCom()) self.add(comms.CmdDelCom()) self.add(comms.CmdAllCom()) diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index 1846915ddc..ce4aaf80e3 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -30,7 +30,7 @@ __all__ = ( "CmdAddCom", "CmdDelCom", "CmdAllCom", - "CmdChannels", + #"CmdChannels", "CmdCdestroy", "CmdCBoot", "CmdCemit", @@ -55,25 +55,27 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): Talk on and manage in-game channels. Usage: - channel channelname [= ] - channel - channel/list - channel/all - channel/history channelname [= index] - channel/sub channelname [= alias] - channel/unsub channelname[,channelname, ...] - channel/alias channelname = alias - channel/unalias channelname = alias - channel/mute channelname[,channelname,...] - channel/unmute channelname[,channelname,...] - channel/create channelname;alias;alias:typeclass [= description] - channel/destroy channelname [= reason] - channel/lock channelname = lockstring - channel/desc channelname = description - channel/boot[/quiet] channelname = subscribername [: reason] - channel/ban channelname - channel/ban[/quiet] channelname = subscribername [: reason] - channel/who channelname + channel channelname [= ] + channel + channel/list + channel/all + channel/history channelname [= index] + channel/sub channelname [= alias] + channel/unsub channelname[,channelname, ...] + channel/alias channelname = alias + channel/unalias channelname = alias + channel/mute channelname[,channelname,...] + channel/unmute channelname[,channelname,...] + channel/create channelname;alias;alias:typeclass [= description] + channel/destroy channelname [= reason] + channel/desc channelname = description + channel/lock channelname = lockstring + channel/unlock channelname = lockstring + channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason] + channel/ban channelname (list bans) + channel/ban[/quiet] channelname[, channelname, ...] = subscribername [: reason] + channel/unban[/quiet] channelname[, channelname, ...] = subscribername + channel/who channelname This handles all operations on channels. @@ -82,8 +84,8 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): aliases = ["chan", "channels"] locks = "cmd: not pperm(channel_banned)" switch_options = ( - "history", "sub", "unsub", "mute", "alias", "unalias", "create", - "destroy", "desc", "boot", "who") + "list", "all", "history", "sub", "unsub", "mute", "unmute", "alias", "unalias", + "create", "destroy", "desc", "lock", "unlock", "boot", "ban", "unban", "who",) def search_channel(self, channelname, exact=False): """ @@ -152,7 +154,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): caller = self.caller log_file = channel.attributes.get( - "log_file", default=channel.log_file.format(channelkey=channel.key)) + "log_file", default=channel.log_to_file.format(channel_key=channel.key)) def send_msg(lines): return caller.msg( @@ -336,7 +338,8 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): def set_lock(self, channel, lockstring): """ - Set a lockstring on a channel. + Set a lockstring on a channel. Permissions must have been + checked before this call. Args: channel (Channel): The channel to operate on. @@ -353,6 +356,26 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): return False, err return True, "" + def unset_lock(self, channel, lockstring): + """ + Remove locks in a lockstring on a channel. Permissions must have been + checked before this call. + + Args: + channel (Channel): The channel to operate on. + lockstring (str): A lockstring on the form 'type:lockfunc();...' + + Returns: + bool, str: True, None if setting lock was successful. If False, + the second part is an error string. + + """ + try: + channel.locks.remove(lockstring) + except LockException as err: + return False, err + return True, "" + def set_desc(self, channel, description): """ Set a channel description. This is shown in listings etc. @@ -443,6 +466,19 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): return True, "" return False, f"{target} was not previously banned from this channel." + def channel_list_bans(self, channel): + """ + Show a channel's bans. + + Args: + channel (Channel): The channel to operate on. + + Returns: + list: A list of strings, each the name of a banned user. + + """ + return [banned.key for banned in channel.banlist] + def channel_list_who(self, channel): """ Show a list of online people is subscribing to a channel. This will check @@ -492,7 +528,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): """ caller = self.caller - subscribed_channels = channelcls.objects.get_subscriptions(caller) + subscribed_channels = list(channelcls.objects.get_subscriptions(caller)) unsubscribed_available_channels = [ chan for chan in channelcls.objects.get_all_channels() @@ -531,6 +567,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): ",".join(nick.db_key for nick in make_iter(nicks) if nick and nick.value[3].lower() == clower), chan.db.desc)) + return comtable def display_all_channels(self, subscribed, available): """ @@ -583,10 +620,11 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): """ Main functionality of command. """ + # from evennia import set_trace;set_trace() caller = self.caller switches = self.switches - channel_names = self.lhslist + channel_names = [name for name in self.lhslist if name] if not channel_names: if 'all' in switches: @@ -604,9 +642,13 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): table = self.display_subbed_channels(subscribed) self.msg("\n|wChannel subscriptions|n " - f"(use |w/all|n to see all available)\n{table}") + f"(use |w/all|n to see all available):\n{table}") return + if not self.switches and not self.args: + caller.msg("Usage[/switches]: channel [= message]") + return + if 'create' in switches: # create a new channel config = self.lhs @@ -636,19 +678,28 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): self.msg("Multiple possible channel matches/alias for " "'{channel_name}':\n" + ", ".join(chan.key for chan in channel)) return - channels.append(channel) + channels.extend(channel) # we have at least one channel at this point channel = channels[0] if not switches: - # send a message to channel(s) - message = self.rhs - if not message: - self.msg("To send: channel = ") - return - for chan in channels: - self.msg_channel(chan, message) + if self.rhs: + # send message to channel + self.msg_channel(channel, self.rhs.strip()) + else: + # inspect a given channel + subscribed, available = self.list_channels() + if channel in subscribed: + table = self.display_subbed_channels[channel] + self.msg( + "\n|wSubscribed to Channel|n " + f"(use |w/all|n to see all available)\n{table}") + elif channel in available: + table = self.display_all_channels([], [channel]) + self.msg( + "\n|wNot subscribed Channel|n (use /list to " + f"show all subscriptions)\n{table}") return if 'history' in switches or 'hist' in switches: @@ -734,8 +785,151 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): self.msg("You can only delete channels you control.") return - def _perform_delete(caller, prompt, result): + def _perform_delete(caller, *args, **kwargs): self.destroy_channel(channel, message=reason) + caller.msg(f"Channel {channel.key} was successfully deleted.") + + ask_yes_no( + caller, + f"Are you sure you want to delete channel '{channel.key}'" + "(make sure name is correct!)? This will disconnect and " + "remove all users' aliases. {yesno}?", + _perform_delete, + "Aborted." + ) + + if 'lock' in switches: + # add a lockstring to channel + lockstring = self.rhs.strip() + + if not lockstring: + self.msg("Usage: channel/lock channelname = lockstring") + return + + if not channel.access(caller, "control"): + self.msg("You need 'control'-access to change locks on this channel.") + return + + success, err = self.set_lock(channel, self.rhs) + if success: + caller.msg("Added/updated lock on channel.") + else: + caller.msg(f"Could not add/update lock: {err}") + return + + if 'unlock' in switches: + # remove/update lockstring from channel + lockstring = self.rhs.strip() + + if not lockstring: + self.msg("Usage: channel/unlock channelname = lockstring") + return + + if not channel.access(caller, "control"): + self.msg("You need 'control'-access to change locks on this channel.") + return + + success, err = self.set_lock(channel, self.rhs) + if success: + caller.msg("Removed lock on channel.") + else: + caller.msg(f"Could not remove lock: {err}") + return + + if 'boot' in switches: + # boot a user from channel(s) + + if not self.rhs: + caller.msg("Usage: channel/boot channel[,channel,...] = username [:reason]") + return + + target_str, *reason = self.rhs.rsplit(":", 1) + reason = reason[0].strip() if reason else "" + + for chan in channels: + + if not chan.access(caller, "admin"): + self.msg("You need 'control'-access to boot a user from {chan.key}.") + return + + # the target must be a member of all given channels + target = self.search(target_str, candidates=chan.subscriptions.all()) + if not target: + caller.msg(f"Cannot boot '{target_str}' - not in channel {chan.key}.") + return + + def _boot_user(caller, *args, **kwargs): + for chan in channels: + success, err = self.boot_user(chan, target, quiet=False, reason=reason) + if success: + caller.msg(f"Booted {target.key} from channel {chan.key}.") + else: + caller.msg(f"Cannot boot {target.key} from channel {chan.key}: {err}") + + channames = ", ".join(chan.key for chan in channels) + ask_yes_no( + caller, + f"Are you sure you want to boot user {target.key} from " + f"channel(s) {channames} (make sure name/channels are correct) " + "{yesno}?", + _boot_user, + "Aborted.", + default="Y" + ) + return + + if 'ban' in switches: + # ban a user from channel(s) + + if not self.rhs: + # view bans for channels + + if not channel.access(caller, "control"): + self.msg("You need 'control'-access to view bans on channel {channel.key}") + return + + bans = ["Channel bans " + "(to ban, use channel/ban channel[,channel,...] = username [:reason]"] + bans.expand(self.channel_list_bans(channel)) + self.msg("\n".join(bans)) + return + + target_str, *reason = self.rhs.rsplit(":", 1) + reason = reason[0].strip() if reason else "" + + for chan in channels: + # the target must be a member of all given channels + target = self.search(target_str, candidates=chan.subscriptions.all()) + if not target: + caller.msg(f"Cannot ban '{target_str}' - not in channel {chan.key}.") + return + + def _ban_user(caller, *args, **kwargs): + for chan in channels: + success, err = self.ban_user(chan, target, quiet=False, reason=reason) + if success: + caller.msg(f"Banned {target.key} from channel {chan.key}.") + else: + caller.msg(f"Cannot boot {target.key} from channel {chan.key}: {err}") + + channames = ", ".join(chan.key for chan in channels) + ask_yes_no( + caller, + f"Are you sure you want to ban user {target.key} from " + f"channel(s) {channames} (make sure name/channels are correct) " + "{yesno}?", + _ban_user, + "Aborted.", + ) + return + + if "who" in switches: + # view who's a member of a channel + + who_list = [f"Subscribed to {channel.key}:"] + who_list.expand(self.channel_list_who(channel)) + caller.msg("\n".join(who_list)) + return class CmdAddCom(COMMAND_DEFAULT_CLASS): diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 62135ef920..28048d7aa5 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -202,7 +202,7 @@ class CommandTest(EvenniaTest): else: # a single expected string and thus a single receiver (defaults to caller) receiver = receiver if receiver else caller - receiver_mapping[receiver] = str(msg).strip() if msg else None + receiver_mapping[receiver] = str(msg).strip() if msg is not None else None unmocked_msg_methods = {} for receiver in receiver_mapping: @@ -1647,6 +1647,170 @@ class TestComms(CommandTest): ) +from evennia.utils.create import create_channel # noqa + +class TestCommsChannel(CommandTest): + """ + Test the central `channel` command. + + """ + def setUp(self): + super(CommandTest, self).setUp() + self.channel = create_channel( + key="testchannel", + desc="A test channel") + self.channel.connect(self.char1) + + def tearDown(self): + self.channel.delete() + + # test channel command + def test_channel__noarg(self): + self.call( + comms.CmdChannel(), + "", + "Usage" + ) + + def test_channel__msg(self): + self.channel.msg = Mock() + self.call( + comms.CmdChannel(), + "testchannel = Test message", + "" + ) + self.channel.msg.assert_called_with("Test message", senders=self.char1) + + def test_channel__list(self): + self.call( + comms.CmdChannel(), + "/list", + "Channel subscriptions" + ) + + def test_channel__all(self): + self.call( + comms.CmdChannel(), + "/all", + "Available channels" + ) + + def test_channel__history(self): + with patch("evennia.commands.default.comms.tail_log_file") as mock_tail: + self.call( + comms.CmdChannel(), + "/history testchannel", + "" + ) + mock_tail.assert_called() + + def test_channel__sub(self): + self.channel.disconnect(self.char1) + + self.call( + comms.CmdChannel(), + "/sub testchannel", + "You are now subscribed" + ) + self.assertTrue(self.char1 in self.channel.subscriptions) + + def test_channel__unsub(self): + self.call( + comms.CmdChannel(), + "/unsub testchannel", + "You un-subscribed" + ) + self.assertFalse(self.char1 in self.channel.subscriptions) + + def test_channel__alias(self): + self.call( + comms.CmdChannel(), + "/alias testchannel = foo", + "" + ) + self.assertEqual(self.chan1.aliases.get('foo'), "") + + def test_channel__unalias(self): + + self.chan1.aliases.add("foo", "", "channel") + + self.call( + comms.CmdChannel(), + "/unalias testchannel = foo", + "" + ) + self.assertEqual(self.chan1.aliases.get('foo'), None) + + def test_channel__mute(self): + self.call( + comms.CmdChannel(), + "/mute testchannel", + "" + ) + self.assertTrue(self.char1 in self.channel.mutelist) + + def test_channel__unmute(self): + self.channel.mute(self.char1) + + self.call( + comms.CmdChannel(), + "/unmute testchannel = Char1", + "" + ) + self.assertFalse(self.char1 in self.channel.mutelist) + + def test_channel__create(self): + self.call( + comms.CmdChannel(), + "/create testchannel2", + "" + ) + + def test_channel__destroy(self): + self.call( + comms.CmdChannel(), + "/create testchannel2", + "" + ) + + def test_channel__desc(self): + self.call( + comms.CmdChannel(), + "/desc testchannel = Another description", + "" + ) + + def test_channel__lock(self): + self.call( + comms.CmdChannel(), + "/lock testchannel = foo:bar()", + "" + ) + self.assertEqual(self.channel.locks.all(), []) + + def test_channel__unlock(self): + self.channel.locks.add("foo:bar()") + self.call( + comms.CmdChannel(), + "/unlock testchannel = foo:bar()", + "" + ) + self.assertEqual(self.channel.locks.all(), []) + + def test_channel__boot(self): + pass + + def test_channel__ban(self): + pass + + def test_channel__who(self): + self.call( + comms.CmdChannel(), + "/who testchannel", + "" + ) + + class TestBatchProcess(CommandTest): @patch("evennia.contrib.tutorial_examples.red_button.repeat") diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 8d7e25dc58..5a27a4e65f 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1710,7 +1710,7 @@ def ask_yes_no(caller, prompt, yes_action, no_action, default=None, Args: prompt (str): The yes/no question to ask. This takes an optional formatting - marker `{suffix}` which will be filled with 'Y/N', [Y]/N or Y/[N] + marker `{yesno}` which will be filled with 'Y/N', [Y]/N or Y/[N] depending on the setting of `default`. If `allow_abort`, then the `A(bort)` will also be available. yes_action (callable or str): If a callable, this will be called @@ -1736,9 +1736,9 @@ def ask_yes_no(caller, prompt, yes_action, no_action, default=None, Example: - ask_yes_no(caller, "Are you happy {suffix}?", + ask_yes_no(caller, "Are you happy {yesno}?", "you answered yes", "you answered no") - ask_yes_no(caller, "Are you sad {suffix}?", + ask_yes_no(caller, "Are you sad {yesno}?", _callable_yes, _callable_no, allow_abort=True) """ @@ -1760,7 +1760,7 @@ def ask_yes_no(caller, prompt, yes_action, no_action, default=None, kwargs['no_txt'] = str(no_action) no_action = _callable_no_txt - # prepare the prompt with suffix + # prepare the prompt with yesno suffix suffix = "Y/N" abort_txt = "/Abort" if allow_abort else "" if default: @@ -1773,7 +1773,7 @@ def ask_yes_no(caller, prompt, yes_action, no_action, default=None, allow_abort = True abort_txt = "/[A]bort" suffix += abort_txt - prompt = prompt.format(suffix=suffix) + prompt = prompt.format(yesno=suffix) caller.ndb._yes_no_question = _Prompt() caller.ndb._yes_no_question.session = session