Repository: Cog-Creators/Red-DiscordBot

Issue:
[Cleanup] cleanup with 0 acts as "all"
# Command bugs

#### Command name

`[p]cleanup <type>`

#### What cog is this command from?

`Cleanup`

#### What were you expecting to happen?

Get error or do nothing.

#### What actually happened?

Bot cleaned up two weeks ~~(presumably)~~ of messages

#### How can we reproduce this issue?

1. Load cleanup cog
2. Use any cleanup with 0 as number (e.g. `[p]cleanup self 0`)
3. All of messages will be deleted, as it used `None`



Discussion:

Resolving patch:
diff --git a/redbot/cogs/cleanup/cleanup.py b/redbot/cogs/cleanup/cleanup.py
--- a/redbot/cogs/cleanup/cleanup.py
+++ b/redbot/cogs/cleanup/cleanup.py
@@ -1,6 +1,6 @@
 import logging
 from datetime import datetime, timedelta
-from typing import Union, List, Callable, Set
+from typing import Callable, List, Optional, Set, Union
 
 import discord
 
@@ -10,7 +10,7 @@
 from redbot.core.utils.chat_formatting import humanize_number
 from redbot.core.utils.mod import slow_deletion, mass_purge
 from redbot.core.utils.predicates import MessagePredicate
-from .converters import RawMessageIds
+from .converters import PositiveInt, RawMessageIds, positive_int
 
 _ = Translator("Cleanup", __file__)
 
@@ -60,9 +60,9 @@ async def check_100_plus(ctx: commands.Context, number: int) -> bool:
     async def get_messages_for_deletion(
         *,
         channel: discord.TextChannel,
-        number: int = None,
+        number: Optional[PositiveInt] = None,
         check: Callable[[discord.Message], bool] = lambda x: True,
-        limit: int = None,
+        limit: Optional[PositiveInt] = None,
         before: Union[discord.Message, datetime] = None,
         after: Union[discord.Message, datetime] = None,
         delete_pinned: bool = False,
@@ -105,7 +105,7 @@ def message_filter(message):
                 break
             if message_filter(message):
                 collected.append(message)
-                if number and number <= len(collected):
+                if number is not None and number <= len(collected):
                     break
 
         return collected
@@ -120,7 +120,7 @@ async def cleanup(self, ctx: commands.Context):
     @commands.guild_only()
     @commands.bot_has_permissions(manage_messages=True)
     async def text(
-        self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
+        self, ctx: commands.Context, text: str, number: positive_int, delete_pinned: bool = False
     ):
         """Delete the last X messages matching the specified text.
 
@@ -169,7 +169,7 @@ def check(m):
     @commands.guild_only()
     @commands.bot_has_permissions(manage_messages=True)
     async def user(
-        self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
+        self, ctx: commands.Context, user: str, number: positive_int, delete_pinned: bool = False
     ):
         """Delete the last X messages from a specified user.
 
@@ -270,7 +270,7 @@ async def before(
         self,
         ctx: commands.Context,
         message_id: RawMessageIds,
-        number: int,
+        number: positive_int,
         delete_pinned: bool = False,
     ):
         """Deletes X messages before specified message.
@@ -351,7 +351,9 @@ async def between(
     @cleanup.command()
     @commands.guild_only()
     @commands.bot_has_permissions(manage_messages=True)
-    async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
+    async def messages(
+        self, ctx: commands.Context, number: positive_int, delete_pinned: bool = False
+    ):
         """Delete the last X messages.
 
         Example:
@@ -381,7 +383,9 @@ async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool
     @cleanup.command(name="bot")
     @commands.guild_only()
     @commands.bot_has_permissions(manage_messages=True)
-    async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
+    async def cleanup_bot(
+        self, ctx: commands.Context, number: positive_int, delete_pinned: bool = False
+    ):
         """Clean up command messages and messages from the bot."""
 
         channel = ctx.channel
@@ -458,7 +462,7 @@ def check(m):
     async def cleanup_self(
         self,
         ctx: commands.Context,
-        number: int,
+        number: positive_int,
         match_pattern: str = None,
         delete_pinned: bool = False,
     ):
@@ -531,7 +535,7 @@ def check(m):
     @cleanup.command(name="spam")
     @commands.guild_only()
     @commands.bot_has_permissions(manage_messages=True)
-    async def cleanup_spam(self, ctx: commands.Context, number: int = 50):
+    async def cleanup_spam(self, ctx: commands.Context, number: positive_int = PositiveInt(50)):
         """Deletes duplicate messages in the channel from the last X messages and keeps only one copy.
 
         Defaults to 50.
diff --git a/redbot/cogs/cleanup/converters.py b/redbot/cogs/cleanup/converters.py
--- a/redbot/cogs/cleanup/converters.py
+++ b/redbot/cogs/cleanup/converters.py
@@ -1,12 +1,30 @@
-from redbot.core.commands import Converter, BadArgument
+from typing import NewType, TYPE_CHECKING
+
+from redbot.core.commands import BadArgument, Context, Converter
 from redbot.core.i18n import Translator
+from redbot.core.utils.chat_formatting import inline
 
 _ = Translator("Cleanup", __file__)
 
 
 class RawMessageIds(Converter):
-    async def convert(self, ctx, argument):
+    async def convert(self, ctx: Context, argument: str) -> int:
         if argument.isnumeric() and len(argument) >= 17:
             return int(argument)
 
         raise BadArgument(_("{} doesn't look like a valid message ID.").format(argument))
+
+
+PositiveInt = NewType("PositiveInt", int)
+if TYPE_CHECKING:
+    positive_int = PositiveInt
+else:
+
+    def positive_int(arg: str) -> int:
+        try:
+            ret = int(arg)
+        except ValueError:
+            raise BadArgument(_("{arg} is not an integer.").format(arg=inline(arg)))
+        if ret <= 0:
+            raise BadArgument(_("{arg} is not a positive integer.").format(arg=inline(arg)))
+        return ret


Test patch: