diff --git a/docs/cog_guides/core.rst b/docs/cog_guides/core.rst index 69e1895494f..71bc5113dc8 100644 --- a/docs/cog_guides/core.rst +++ b/docs/cog_guides/core.rst @@ -1475,9 +1475,6 @@ helpset maxpages Set the maximum number of help pages sent in a server channel. -.. Note:: This setting does not apply to menu help. - - If a help message contains more pages than this value, the help message will be sent to the command author via DM. This is to help reduce spam in server text channels. @@ -4378,4 +4375,4 @@ uptime **Description** -Shows Red's uptime. \ No newline at end of file +Shows Red's uptime. diff --git a/redbot/cogs/alias/alias.py b/redbot/cogs/alias/alias.py index b47d39ff978..cdc38066842 100644 --- a/redbot/cogs/alias/alias.py +++ b/redbot/cogs/alias/alias.py @@ -150,6 +150,15 @@ async def get_prefix(self, message: discord.Message) -> str: raise ValueError("No prefix found.") async def call_alias(self, message: discord.Message, prefix: str, alias: AliasEntry): + new_message = self.translate_alias_message(message, prefix, alias) + await self.bot.process_commands(new_message) + + def translate_alias_message(self, message: discord.Message, prefix: str, alias: AliasEntry): + """ + Translates a discord message using an alias + for a command to a discord message using the + alias' base command. + """ new_message = copy(message) try: args = alias.get_extra_args_from_alias(message, prefix) @@ -163,7 +172,8 @@ async def call_alias(self, message: discord.Message, prefix: str, alias: AliasEn new_message.content = "{}{} {}".format( prefix, command, " ".join(args[trackform.max + 1 :]) ).strip() - await self.bot.process_commands(new_message) + + return new_message async def paginate_alias_list( self, ctx: commands.Context, alias_list: List[AliasEntry] diff --git a/redbot/cogs/alias/alias_entry.py b/redbot/cogs/alias/alias_entry.py index 49cf224ab6b..159e68fa4ab 100644 --- a/redbot/cogs/alias/alias_entry.py +++ b/redbot/cogs/alias/alias_entry.py @@ -62,8 +62,7 @@ def get_extra_args_from_alias(self, message: discord.Message, prefix: str) -> st word = view.get_quoted_word() if len(word) < view.index - prev: word = "".join((view.buffer[prev], word, view.buffer[view.index - 1])) - extra.append(word) - view.skip_ws() + extra.append(word.strip(" ")) return extra def to_json(self) -> dict: diff --git a/redbot/cogs/streams/streams.py b/redbot/cogs/streams/streams.py index 60ceb619d41..54e32f6ec11 100644 --- a/redbot/cogs/streams/streams.py +++ b/redbot/cogs/streams/streams.py @@ -418,10 +418,10 @@ async def streamalert_list(self, ctx: commands.Context): return for channel_id, stream_platform in streams_list.items(): - msg += f"** - #{ctx.guild.get_channel(channel_id)}**\n" + msg += f"- {ctx.guild.get_channel(channel_id).mention}\n" for platform, streams in stream_platform.items(): - msg += f"\t** - {platform}**\n" - msg += f"\t\t{humanize_list(streams)}\n" + msg += f" - **{platform}**\n" + msg += f" {humanize_list(streams)}\n" for page in pagify(msg): await ctx.send(page) diff --git a/redbot/core/_cli.py b/redbot/core/_cli.py index bc263713f73..b8dbc65e492 100644 --- a/redbot/core/_cli.py +++ b/redbot/core/_cli.py @@ -80,7 +80,7 @@ async def interactive_config(red, token_set, prefix_set, *, print_header=True): print( "\nPick a prefix. A prefix is what you type before a " "command. Example:\n" - "!help\n^ The exclamation mark is the prefix in this case.\n" + "!help\n^ The exclamation mark (!) is the prefix in this case.\n" "The prefix can be multiple characters. You will be able to change it " "later and add more of them.\nChoose your prefix:\n" ) @@ -94,6 +94,12 @@ async def interactive_config(red, token_set, prefix_set, *, print_header=True): "Prefixes cannot start with '/', as it conflicts with Discord's slash commands." ) prefix = "" + if prefix and not confirm( + f'You chose "{prefix}" as your prefix. To run the help command,' + f" you will have to send:\n{prefix}help\n\n" + "Do you want to continue with this prefix?" + ): + prefix = "" if prefix: await red._config.prefix.set([prefix]) diff --git a/redbot/core/commands/help.py b/redbot/core/commands/help.py index dcff3f0410e..66920780d59 100644 --- a/redbot/core/commands/help.py +++ b/redbot/core/commands/help.py @@ -856,23 +856,36 @@ async def send_pages( if help_settings.use_menus.value >= HelpMenuSetting.buttons.value: use_select = help_settings.use_menus.value == 3 select_only = help_settings.use_menus.value == 4 - await SimpleMenu( + menu = SimpleMenu( pages, timeout=help_settings.react_timeout, use_select_menu=use_select, use_select_only=select_only, - ).start(ctx) + ) + # Send menu to DMs if max pages is 0 + if help_settings.max_pages_in_guild == 0: + await menu.start_dm(ctx.author) + else: + await menu.start(ctx) elif ( can_user_react_in(ctx.me, ctx.channel) and help_settings.use_menus is HelpMenuSetting.reactions ): + use_DMs = help_settings.max_pages_in_guild == 0 + destination = ctx.author if use_DMs else ctx.channel # Specifically ensuring the menu's message is sent prior to returning - m = await (ctx.send(embed=pages[0]) if embed else ctx.send(pages[0])) + m = await (destination.send(embed=pages[0]) if embed else destination.send(pages[0])) c = menus.DEFAULT_CONTROLS if len(pages) > 1 else {"\N{CROSS MARK}": menus.close_menu} # Allow other things to happen during menu timeout/interaction. + if use_DMs: + menu_ctx = await ctx.bot.get_context(m) + # Monkeypatch so help listens for reactions from the original author, not the bot + menu_ctx.author = ctx.author + else: + menu_ctx = ctx asyncio.create_task( - menus.menu(ctx, pages, c, message=m, timeout=help_settings.react_timeout) + menus.menu(menu_ctx, pages, c, message=m, timeout=help_settings.react_timeout) ) # menu needs reactions added manually since we fed it a message menus.start_adding_reactions(m, c.keys()) diff --git a/redbot/core/commands/requires.py b/redbot/core/commands/requires.py index ca455121c59..b5526a6712c 100644 --- a/redbot/core/commands/requires.py +++ b/redbot/core/commands/requires.py @@ -357,6 +357,7 @@ def decorator(func: "_CommandOrCoro") -> "_CommandOrCoro": if user_perms is None: func.__requires_user_perms__ = None else: + _validate_perms_dict(user_perms) if getattr(func, "__requires_user_perms__", None) is None: func.__requires_user_perms__ = discord.Permissions.none() func.__requires_user_perms__.update(**user_perms) diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py index 3930ac79408..13c3d4c86b7 100644 --- a/redbot/core/core_commands.py +++ b/redbot/core/core_commands.py @@ -117,6 +117,7 @@ def entity_transformer(statement: str) -> str: TokenConverter = commands.get_dict_converter(delims=[" ", ",", ";"]) MAX_PREFIX_LENGTH = 25 +MINIMUM_PREFIX_LENGTH = 1 class CoreLogic: @@ -2377,7 +2378,7 @@ async def slash_sync_error(self, ctx: commands.Context, error: commands.CommandE await ctx.send( _( "You seem to be attempting to sync after recently syncing. Discord does not like it " - "when bots sync more often than neccecary, so this command has a cooldown. You " + "when bots sync more often than necessary, so this command has a cooldown. You " "should enable/disable all commands you want to change first, and run this command " "one time only after all changes have been made. " ) @@ -4052,6 +4053,24 @@ async def _set_prefix(self, ctx: commands.Context, *prefixes: str): _("Prefixes cannot start with '/', as it conflicts with Discord's slash commands.") ) return + if any(len(x) < MINIMUM_PREFIX_LENGTH for x in prefixes): + await ctx.send( + _( + "Warning: A prefix is below the recommended length (1 character).\n" + "Do you want to continue?" + ) + + " (yes/no)" + ) + pred = MessagePredicate.yes_or_no(ctx) + try: + await self.bot.wait_for("message", check=pred, timeout=30) + except asyncio.TimeoutError: + await ctx.send(_("Response timed out.")) + return + else: + if pred.result is False: + await ctx.send(_("Cancelled.")) + return if any(len(x) > MAX_PREFIX_LENGTH for x in prefixes): await ctx.send( _( @@ -4111,6 +4130,9 @@ async def _set_serverprefix( _("Prefixes cannot start with '/', as it conflicts with Discord's slash commands.") ) return + if any(len(x) < MINIMUM_PREFIX_LENGTH for x in prefixes): + await ctx.send(_("You cannot have a prefix shorter than 1 character.")) + return if any(len(x) > MAX_PREFIX_LENGTH for x in prefixes): await ctx.send(_("You cannot have a prefix longer than 25 characters.")) return @@ -4446,8 +4468,6 @@ async def helpset_pagecharlimt(self, ctx: commands.Context, limit: int): async def helpset_maxpages(self, ctx: commands.Context, pages: int): """Set the maximum number of help pages sent in a server channel. - Note: This setting does not apply to menu help. - If a help message contains more pages than this value, the help message will be sent to the command author via DM. This is to help reduce spam in server text channels. diff --git a/redbot/core/utils/views.py b/redbot/core/utils/views.py index 10c1f23ecab..f6d2fea48a1 100644 --- a/redbot/core/utils/views.py +++ b/redbot/core/utils/views.py @@ -242,6 +242,19 @@ async def start(self, ctx: Context, *, ephemeral: bool = False): kwargs = await self.get_page(self.current_page) self.message = await ctx.send(**kwargs, ephemeral=ephemeral) + async def start_dm(self, user: discord.User): + """ + Used to start displaying the menu in a direct message. + + Parameters + ---------- + user: `discord.User` + The user that will be direct messaged by the bot. + """ + self.author = user + kwargs = await self.get_page(self.current_page) + self.message = await user.send(**kwargs) + async def get_page(self, page_num: int) -> Dict[str, Optional[Any]]: try: page = await self.source.get_page(page_num) diff --git a/redbot/pytest/core.py b/redbot/pytest/core.py index 4eb60b3cf05..57450f8e41c 100644 --- a/redbot/pytest/core.py +++ b/redbot/pytest/core.py @@ -23,7 +23,9 @@ "empty_role", "empty_user", "member_factory", + "newline_message", "user_factory", + "prefix", "ctx", ] @@ -142,6 +144,18 @@ def empty_message(): return mock_msg("No content.") +@pytest.fixture(scope="module") +def newline_message(): + mock_msg = type("", (), {})() + mock_msg.content = "!test a\nb\nc" + return mock_msg + + +@pytest.fixture(scope="module") +def prefix(): + return "!" + + @pytest.fixture() def ctx(empty_member, empty_channel, red): mock_ctx = namedtuple("Context", "author guild channel message bot") diff --git a/tests/cogs/test_alias.py b/tests/cogs/test_alias.py index 261c85dcb38..97b20058240 100644 --- a/tests/cogs/test_alias.py +++ b/tests/cogs/test_alias.py @@ -30,6 +30,20 @@ async def test_add_guild_alias(alias, ctx): assert alias_obj.name == "test" +async def test_translate_alias_message(alias, ctx, newline_message, prefix): + await create_test_guild_alias(alias, ctx) + alias_obj = await alias._aliases.get_alias(ctx.guild, "test") + + translated_message = alias.translate_alias_message(newline_message, prefix, alias_obj) + + original_content = newline_message.content.split(" ", 1)[1] + original_content = original_content.replace(" ", "") + new_content = translated_message.content.split(" ", 1)[1] + new_content = new_content.replace(" ", "") + + assert new_content == original_content + + async def test_delete_guild_alias(alias, ctx): await create_test_guild_alias(alias, ctx) alias_obj = await alias._aliases.get_alias(ctx.guild, "test")