From aa8c9c350ee8b6cdc8032c82fee17d5eaaff5685 Mon Sep 17 00:00:00 2001 From: Michael H Date: Thu, 16 Aug 2018 10:00:57 +1000 Subject: [PATCH 1/5] [i18n] Start work on named format arguments (#1795) --- redbot/cogs/admin/admin.py | 59 ++++++++------ redbot/cogs/alias/alias.py | 52 +++++++------ redbot/cogs/bank/bank.py | 19 +++-- redbot/cogs/cleanup/cleanup.py | 2 +- redbot/cogs/customcom/customcom.py | 22 +++--- redbot/cogs/downloader/downloader.py | 46 ++++++----- redbot/cogs/economy/economy.py | 73 ++++++++++-------- redbot/cogs/general/general.py | 63 ++++++++------- redbot/cogs/mod/mod.py | 110 +++++++++++++++++---------- 9 files changed, 263 insertions(+), 183 deletions(-) diff --git a/redbot/cogs/admin/admin.py b/redbot/cogs/admin/admin.py index 18dae1029..30136b980 100644 --- a/redbot/cogs/admin/admin.py +++ b/redbot/cogs/admin/admin.py @@ -12,32 +12,35 @@ from .converters import MemberDefaultAuthor, SelfRole log = logging.getLogger("red.admin") -GENERIC_FORBIDDEN = ( +_ = Translator("Admin", __file__) + +GENERIC_FORBIDDEN = _( "I attempted to do something that Discord denied me permissions for." " Your command failed to successfully complete." ) -HIERARCHY_ISSUE = ( +HIERARCHY_ISSUE = _( "I tried to add {role.name} to {member.display_name} but that role" " is higher than my highest role in the Discord hierarchy so I was" " unable to successfully add it. Please give me a higher role and " "try again." ) -USER_HIERARCHY_ISSUE = ( +USER_HIERARCHY_ISSUE = _( "I tried to add {role.name} to {member.display_name} but that role" " is higher than your highest role in the Discord hierarchy so I was" " unable to successfully add it. Please get a higher role and " "try again." ) -RUNNING_ANNOUNCEMENT = ( +RUNNING_ANNOUNCEMENT = _( "I am already announcing something. If you would like to make a" " different announcement please use `{prefix}announce cancel`" " first." ) +@cog_i18n(_) class Admin(commands.Cog): def __init__(self, config=Config): super().__init__() @@ -103,8 +106,9 @@ class Admin(commands.Cog): await self.complain(ctx, GENERIC_FORBIDDEN) else: await ctx.send( - "I successfully added {role.name} to" - " {member.display_name}".format(role=role, member=member) + _("I successfully added {role.name} to {member.display_name}").format( + role=role, member=member + ) ) async def _removerole(self, ctx: commands.Context, member: discord.Member, role: discord.Role): @@ -117,8 +121,9 @@ class Admin(commands.Cog): await self.complain(ctx, GENERIC_FORBIDDEN) else: await ctx.send( - "I successfully removed {role.name} from" - " {member.display_name}".format(role=role, member=member) + _("I successfully removed {role.name} from {member.display_name}").format( + role=role, member=member + ) ) @commands.command() @@ -189,7 +194,7 @@ class Admin(commands.Cog): await self.complain(ctx, GENERIC_FORBIDDEN) else: log.info(reason) - await ctx.send("Done.") + await ctx.send(_("Done.")) @editrole.command(name="name") @checks.admin_or_permissions(administrator=True) @@ -215,7 +220,7 @@ class Admin(commands.Cog): await self.complain(ctx, GENERIC_FORBIDDEN) else: log.info(reason) - await ctx.send("Done.") + await ctx.send(_("Done.")) @commands.group(invoke_without_command=True) @checks.is_owner() @@ -229,7 +234,7 @@ class Admin(commands.Cog): self.__current_announcer = announcer - await ctx.send("The announcement has begun.") + await ctx.send(_("The announcement has begun.")) else: prefix = ctx.prefix await self.complain(ctx, RUNNING_ANNOUNCEMENT, prefix=prefix) @@ -245,7 +250,7 @@ class Admin(commands.Cog): except AttributeError: pass - await ctx.send("The current announcement has been cancelled.") + await ctx.send(_("The current announcement has been cancelled.")) @announce.command(name="channel") @commands.guild_only() @@ -258,7 +263,9 @@ class Admin(commands.Cog): channel = ctx.channel await self.conf.guild(ctx.guild).announce_channel.set(channel.id) - await ctx.send("The announcement channel has been set to {}".format(channel.mention)) + await ctx.send( + _("The announcement channel has been set to {channel.mention}").format(channel=channel) + ) @announce.command(name="ignore") @commands.guild_only() @@ -270,9 +277,16 @@ class Admin(commands.Cog): ignored = await self.conf.guild(ctx.guild).announce_ignore() await self.conf.guild(ctx.guild).announce_ignore.set(not ignored) - verb = "will" if ignored else "will not" - - await ctx.send(f"The server {ctx.guild.name} {verb} receive announcements.") + if ignored: # Keeping original logic.... + await ctx.send( + _("The server {guild.name} will receive announcements.").format(guild=ctx.guild) + ) + else: + await ctx.send( + _("The server {guild.name} will not receive announcements.").format( + guild=ctx.guild + ) + ) async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]: """ @@ -325,7 +339,7 @@ class Admin(commands.Cog): if role.id not in curr_selfroles: curr_selfroles.append(role.id) - await ctx.send("The selfroles list has been successfully modified.") + await ctx.send(_("The selfroles list has been successfully modified.")) @selfrole.command(name="delete") @checks.admin_or_permissions(manage_roles=True) @@ -338,7 +352,7 @@ class Admin(commands.Cog): async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles: curr_selfroles.remove(role.id) - await ctx.send("The selfroles list has been successfully modified.") + await ctx.send(_("The selfroles list has been successfully modified.")) @selfrole.command(name="list") async def selfrole_list(self, ctx: commands.Context): @@ -348,7 +362,7 @@ class Admin(commands.Cog): selfroles = await self._valid_selfroles(ctx.guild) fmt_selfroles = "\n".join(["+ " + r.name for r in selfroles]) - msg = "Available Selfroles:\n{}".format(fmt_selfroles) + msg = _("Available Selfroles:\n{selfroles}").format(selfroles=fmt_selfroles) await ctx.send(box(msg, "diff")) async def _serverlock_check(self, guild: discord.Guild) -> bool: @@ -371,9 +385,10 @@ class Admin(commands.Cog): serverlocked = await self.conf.serverlocked() await self.conf.serverlocked.set(not serverlocked) - verb = "is now" if not serverlocked else "is no longer" - - await ctx.send("The bot {} serverlocked.".format(verb)) + if serverlocked: # again with original logic I'm not sure of + await ctx.send(_("The bot is no longer serverlocked.")) + else: + await ctx.send(_("The bot is now serverlocked.")) # region Event Handlers async def on_guild_join(self, guild: discord.Guild): diff --git a/redbot/cogs/alias/alias.py b/redbot/cogs/alias/alias.py index 514b6fde6..6b3596e72 100644 --- a/redbot/cogs/alias/alias.py +++ b/redbot/cogs/alias/alias.py @@ -200,9 +200,9 @@ class Alias(commands.Cog): await ctx.send( _( "You attempted to create a new alias" - " with the name {} but that" + " with the name {name} but that" " name is already a command on this bot." - ).format(alias_name) + ).format(name=alias_name) ) return @@ -211,9 +211,9 @@ class Alias(commands.Cog): await ctx.send( _( "You attempted to create a new alias" - " with the name {} but that" + " with the name {name} but that" " alias already exists on this server." - ).format(alias_name) + ).format(name=alias_name) ) return @@ -222,10 +222,10 @@ class Alias(commands.Cog): await ctx.send( _( "You attempted to create a new alias" - " with the name {} but that" + " with the name {name} but that" " name is an invalid alias name. Alias" " names may not contain spaces." - ).format(alias_name) + ).format(name=alias_name) ) return # endregion @@ -235,7 +235,9 @@ class Alias(commands.Cog): await self.add_alias(ctx, alias_name, command) - await ctx.send(_("A new alias with the trigger `{}` has been created.").format(alias_name)) + await ctx.send( + _("A new alias with the trigger `{name}` has been created.").format(name=alias_name) + ) @checks.is_owner() @global_.command(name="add") @@ -249,9 +251,9 @@ class Alias(commands.Cog): await ctx.send( _( "You attempted to create a new global alias" - " with the name {} but that" + " with the name {name} but that" " name is already a command on this bot." - ).format(alias_name) + ).format(name=alias_name) ) return @@ -260,9 +262,9 @@ class Alias(commands.Cog): await ctx.send( _( "You attempted to create a new global alias" - " with the name {} but that" + " with the name {name} but that" " alias already exists on this server." - ).format(alias_name) + ).format(name=alias_name) ) return @@ -271,10 +273,10 @@ class Alias(commands.Cog): await ctx.send( _( "You attempted to create a new global alias" - " with the name {} but that" + " with the name {name} but that" " name is an invalid alias name. Alias" " names may not contain spaces." - ).format(alias_name) + ).format(name=alias_name) ) return # endregion @@ -282,7 +284,9 @@ class Alias(commands.Cog): await self.add_alias(ctx, alias_name, command, global_=True) await ctx.send( - _("A new global alias with the trigger `{}` has been created.").format(alias_name) + _("A new global alias with the trigger `{name}` has been created.").format( + name=alias_name + ) ) @alias.command(name="help") @@ -294,10 +298,12 @@ class Alias(commands.Cog): base_cmd = alias.command[0] new_msg = copy(ctx.message) - new_msg.content = "{}help {}".format(ctx.prefix, base_cmd) + new_msg.content = _("{prefix}help {command}").format( + prefix=ctx.prefix, command=base_cmd + ) await self.bot.process_commands(new_msg) else: - ctx.send(_("No such alias exists.")) + await ctx.send(_("No such alias exists.")) @alias.command(name="show") @commands.guild_only() @@ -307,10 +313,12 @@ class Alias(commands.Cog): if is_alias: await ctx.send( - _("The `{}` alias will execute the command `{}`").format(alias_name, alias.command) + _("The `{alias_name}` alias will execute the command `{command}`").format( + alias_name=alias_name, command=alias.command + ) ) else: - await ctx.send(_("There is no alias with the name `{}`").format(alias_name)) + await ctx.send(_("There is no alias with the name `{name}`").format(name=alias_name)) @checks.mod_or_permissions(manage_guild=True) @alias.command(name="del") @@ -328,10 +336,10 @@ class Alias(commands.Cog): if await self.delete_alias(ctx, alias_name): await ctx.send( - _("Alias with the name `{}` was successfully deleted.").format(alias_name) + _("Alias with the name `{name}` was successfully deleted.").format(name=alias_name) ) else: - await ctx.send(_("Alias with name `{}` was not found.").format(alias_name)) + await ctx.send(_("Alias with name `{name}` was not found.").format(name=alias_name)) @checks.is_owner() @global_.command(name="del") @@ -348,10 +356,10 @@ class Alias(commands.Cog): if await self.delete_alias(ctx, alias_name, global_=True): await ctx.send( - _("Alias with the name `{}` was successfully deleted.").format(alias_name) + _("Alias with the name `{name}` was successfully deleted.").format(name=alias_name) ) else: - await ctx.send(_("Alias with name `{}` was not found.").format(alias_name)) + await ctx.send(_("Alias with name `{name}` was not found.").format(name=alias_name)) @alias.command(name="list") @commands.guild_only() diff --git a/redbot/cogs/bank/bank.py b/redbot/cogs/bank/bank.py index bdb422cc7..6969fe30d 100644 --- a/redbot/cogs/bank/bank.py +++ b/redbot/cogs/bank/bank.py @@ -81,8 +81,11 @@ class Bank(commands.Cog): default_balance = await bank._conf.guild(ctx.guild).default_balance() settings = _( - "Bank settings:\n\nBank name: {}\nCurrency: {}\nDefault balance: {}" - ).format(bank_name, currency_name, default_balance) + "Bank settings:\n\nBank name: {bank_name}\nCurrency: {currency_name}\n" + "Default balance: {default_balance}" + ).format( + bank_name=bank_name, currency_name=currency_name, default_balance=default_balance + ) await ctx.send(box(settings)) @bankset.command(name="toggleglobal") @@ -97,26 +100,26 @@ class Bank(commands.Cog): if confirm is False: await ctx.send( _( - "This will toggle the bank to be {}, deleting all accounts " - "in the process! If you're sure, type `{}`" - ).format(word, "{}bankset toggleglobal yes".format(ctx.prefix)) + "This will toggle the bank to be {banktype}, deleting all accounts " + "in the process! If you're sure, type `{command}`" + ).format(banktype=word, command="{}bankset toggleglobal yes".format(ctx.prefix)) ) else: await bank.set_global(not cur_setting) - await ctx.send(_("The bank is now {}.").format(word)) + await ctx.send(_("The bank is now {banktype}.").format(banktype=word)) @bankset.command(name="bankname") @check_global_setting_guildowner() async def bankset_bankname(self, ctx: commands.Context, *, name: str): """Set the bank's name""" await bank.set_bank_name(name, ctx.guild) - await ctx.send(_("Bank's name has been set to {}").format(name)) + await ctx.send(_("Bank name has been set to: {name}").format(name=name)) @bankset.command(name="creditsname") @check_global_setting_guildowner() async def bankset_creditsname(self, ctx: commands.Context, *, name: str): """Set the name for the bank's currency""" await bank.set_currency_name(name, ctx.guild) - await ctx.send(_("Currency name has been set to {}").format(name)) + await ctx.send(_("Currency name has been set to: {name}").format(name=name)) # ENDSECTION diff --git a/redbot/cogs/cleanup/cleanup.py b/redbot/cogs/cleanup/cleanup.py index 85bd5fe89..94eab2364 100644 --- a/redbot/cogs/cleanup/cleanup.py +++ b/redbot/cogs/cleanup/cleanup.py @@ -33,7 +33,7 @@ class Cleanup(commands.Cog): """ prompt = await ctx.send( - _("Are you sure you want to delete {} messages? (y/n)").format(number) + _("Are you sure you want to delete {number} messages? (y/n)").format(number=number) ) response = await ctx.bot.wait_for("message", check=MessagePredicate.same_context(ctx)) diff --git a/redbot/cogs/customcom/customcom.py b/redbot/cogs/customcom/customcom.py index ab76794ec..870cf8923 100644 --- a/redbot/cogs/customcom/customcom.py +++ b/redbot/cogs/customcom/customcom.py @@ -52,11 +52,11 @@ class CommandObj: async def get_responses(self, ctx): intro = _( - "Welcome to the interactive random {} maker!\n" + "Welcome to the interactive random {cc} maker!\n" "Every message you send will be added as one of the random " "responses to choose from once this {} is " - "triggered. To exit this interactive menu, type `{}`" - ).format("customcommand", "customcommand", "exit()") + "triggered. To exit this interactive menu, type `{quit}`" + ).format(cc="customcommand", quit="exit()") await ctx.send(intro) responses = [] @@ -226,8 +226,8 @@ class CustomCommands(commands.Cog): await ctx.send(_("Custom command successfully added.")) except AlreadyExists: await ctx.send( - _("This command already exists. Use `{}` to edit it.").format( - "{}customcom edit".format(ctx.prefix) + _("This command already exists. Use `{command}` to edit it.").format( + command="{}customcom edit".format(ctx.prefix) ) ) @@ -249,8 +249,8 @@ class CustomCommands(commands.Cog): await ctx.send(_("Custom command successfully added.")) except AlreadyExists: await ctx.send( - _("This command already exists. Use `{}` to edit it.").format( - "{}customcom edit".format(ctx.prefix) + _("This command already exists. Use `{command}` to edit it.").format( + command="{}customcom edit".format(ctx.prefix) ) ) except ArgParseError as e: @@ -293,8 +293,8 @@ class CustomCommands(commands.Cog): await ctx.send(_("Custom command cooldown successfully edited.")) except NotFound: await ctx.send( - _("That command doesn't exist. Use `{}` to add it.").format( - "{}customcom add".format(ctx.prefix) + _("That command doesn't exist. Use `{command}` to add it.").format( + command="{}customcom add".format(ctx.prefix) ) ) @@ -341,8 +341,8 @@ class CustomCommands(commands.Cog): await ctx.send( _( "There are no custom commands in this server." - " Use `{}` to start adding some." - ).format("{}customcom add".format(ctx.prefix)) + " Use `{command}` to start adding some." + ).format(command="{}customcom add".format(ctx.prefix)) ) return diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py index cea8363e7..ec664a377 100644 --- a/redbot/cogs/downloader/downloader.py +++ b/redbot/cogs/downloader/downloader.py @@ -242,7 +242,7 @@ class Downloader(commands.Cog): exc_info=err, ) else: - await ctx.send(_("Repo `{}` successfully added.").format(name)) + await ctx.send(_("Repo `{name}` successfully added.").format(name=name)) if repo.install_msg is not None: await ctx.send(repo.install_msg.replace("[p]", ctx.prefix)) @@ -253,7 +253,9 @@ class Downloader(commands.Cog): """ await self._repo_manager.delete_repo(repo_name.name) - await ctx.send(_("The repo `{}` has been deleted successfully.").format(repo_name.name)) + await ctx.send( + _("The repo `{name}` has been deleted successfully.").format(name=repo_name.name) + ) @repo.command(name="list") async def _repo_list(self, ctx): @@ -276,10 +278,12 @@ class Downloader(commands.Cog): Lists information about a single repo """ if repo_name is None: - await ctx.send(_("There is no repo `{}`").format(repo_name.name)) + await ctx.send(_("There is no repo `{repo_name}`").format(repo_name=repo_name.name)) return - msg = _("Information on {}:\n{}").format(repo_name.name, repo_name.description or "") + msg = _("Information on {repo_name}:\n{description}").format( + repo_name=repo_name.name, description=repo_name.description or "" + ) await ctx.send(box(msg)) @commands.group() @@ -298,9 +302,9 @@ class Downloader(commands.Cog): cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable if cog is None: await ctx.send( - _("Error, there is no cog by the name of `{}` in the `{}` repo.").format( - cog_name, repo_name.name - ) + _( + "Error, there is no cog by the name of `{cog_name}` in the `{repo_name}` repo." + ).format(cog_name=cog_name, repo_name=repo_name.name) ) return elif cog.min_python_version > sys.version_info: @@ -313,9 +317,9 @@ class Downloader(commands.Cog): if not await repo_name.install_requirements(cog, self.LIB_PATH): await ctx.send( - _("Failed to install the required libraries for `{}`: `{}`").format( - cog.name, cog.requirements - ) + _( + "Failed to install the required libraries for `{cog_name}`: `{libraries}`" + ).format(cog_name=cog.name, libraries=cog.requirements) ) return @@ -325,7 +329,7 @@ class Downloader(commands.Cog): await repo_name.install_libraries(self.SHAREDLIB_PATH) - await ctx.send(_("`{}` cog successfully installed.").format(cog_name)) + await ctx.send(_("`{cog_name}` cog successfully installed.").format(cog_name=cog_name)) if cog.install_msg is not None: await ctx.send(cog.install_msg.replace("[p]", ctx.prefix)) @@ -343,7 +347,9 @@ class Downloader(commands.Cog): await self._delete_cog(poss_installed_path) # noinspection PyTypeChecker await self._remove_from_installed(cog_name) - await ctx.send(_("`{}` was successfully removed.").format(real_name)) + await ctx.send( + _("`{real_name}` was successfully removed.").format(real_name=real_name) + ) else: await ctx.send( _( @@ -453,12 +459,18 @@ class Downloader(commands.Cog): cog = discord.utils.get(repo_name.available_cogs, name=cog_name) if cog is None: await ctx.send( - _("There is no cog `{}` in the repo `{}`").format(cog_name, repo_name.name) + _("There is no cog `{cog_name}` in the repo `{repo_name}`").format( + cog_name=cog_name, repo_name=repo_name.name + ) ) return - msg = _("Information on {}:\n{}\n\nRequirements: {}").format( - cog.name, cog.description or "", ", ".join(cog.requirements) or "None" + msg = _( + "Information on {cog_name}:\n{description}\n\nRequirements: {requirements}" + ).format( + cog_name=cog.name, + description=cog.description or "", + requirements=", ".join(cog.requirements) or "None", ) await ctx.send(box(msg)) @@ -512,9 +524,9 @@ class Downloader(commands.Cog): repo_url = "https://github.com/Cog-Creators/Red-DiscordBot" cog_name = cog_installable.__class__.__name__ - msg = _("Command: {}\nMade by: {}\nRepo: {}\nCog name: {}") + msg = _("Command: {command}\nMade by: {author}\nRepo: {repo}\nCog name: {cog}") - return msg.format(command_name, made_by, repo_url, cog_name) + return msg.format(command=command_name, author=made_by, repo=repo_url, cog=cog_name) def cog_name_from_instance(self, instance: object) -> str: """Determines the cog name that Downloader knows from the cog instance. diff --git a/redbot/cogs/economy/economy.py b/redbot/cogs/economy/economy.py index 0059a9bc0..a6e8c2288 100644 --- a/redbot/cogs/economy/economy.py +++ b/redbot/cogs/economy/economy.py @@ -447,32 +447,30 @@ class Economy(commands.Cog): """Changes economy module settings""" guild = ctx.guild if ctx.invoked_subcommand is None: + fmt = {} if await bank.is_global(): - slot_min = await self.config.SLOT_MIN() - slot_max = await self.config.SLOT_MAX() - slot_time = await self.config.SLOT_TIME() - payday_time = await self.config.PAYDAY_TIME() - payday_amount = await self.config.PAYDAY_CREDITS() + fmt["slot_min"] = await self.config.SLOT_MIN() + fmt["slot_max"] = await self.config.SLOT_MAX() + fmt["slot_time"] = await self.config.SLOT_TIME() + fmt["payday_time"] = await self.config.PAYDAY_TIME() + fmt["payday_amount"] = await self.config.PAYDAY_CREDITS() else: - slot_min = await self.config.guild(guild).SLOT_MIN() - slot_max = await self.config.guild(guild).SLOT_MAX() - slot_time = await self.config.guild(guild).SLOT_TIME() - payday_time = await self.config.guild(guild).PAYDAY_TIME() - payday_amount = await self.config.guild(guild).PAYDAY_CREDITS() - register_amount = await bank.get_default_balance(guild) + fmt["slot_min"] = await self.config.guild(guild).SLOT_MIN() + fmt["slot_max"] = await self.config.guild(guild).SLOT_MAX() + fmt["slot_time"] = await self.config.guild(guild).SLOT_TIME() + fmt["payday_time"] = await self.config.guild(guild).PAYDAY_TIME() + fmt["payday_amount"] = await self.config.guild(guild).PAYDAY_CREDITS() + fmt["register_amount"] = await bank.get_default_balance(guild) msg = box( _( - "Minimum slot bid: {}\n" - "Maximum slot bid: {}\n" - "Slot cooldown: {}\n" - "Payday amount: {}\n" - "Payday cooldown: {}\n" - "Amount given at account registration: {}" - "" - ).format( - slot_min, slot_max, slot_time, payday_amount, payday_time, register_amount - ), - _("Current Economy settings:"), + "Current Economy settings:" + "Minimum slot bid: {slot_min}\n" + "Maximum slot bid: {slot_max}\n" + "Slot cooldown: {slot_time}\n" + "Payday amount: {payday_amount}\n" + "Payday cooldown: {payday_time}\n" + "Amount given at account registration: {register_amount}" + ).format(**fmt) ) await ctx.send(msg) @@ -488,7 +486,9 @@ class Economy(commands.Cog): else: await self.config.guild(guild).SLOT_MIN.set(bid) credits_name = await bank.get_currency_name(guild) - await ctx.send(_("Minimum bid is now {} {}.").format(bid, credits_name)) + await ctx.send( + _("Minimum bid is now {bid} {currency}.").format(bid=bid, currency=credits_name) + ) @economyset.command() async def slotmax(self, ctx: commands.Context, bid: int): @@ -503,7 +503,9 @@ class Economy(commands.Cog): await self.config.SLOT_MAX.set(bid) else: await self.config.guild(guild).SLOT_MAX.set(bid) - await ctx.send(_("Maximum bid is now {} {}.").format(bid, credits_name)) + await ctx.send( + _("Maximum bid is now {bid} {currency}.").format(bid=bid, currency=credits_name) + ) @economyset.command() async def slottime(self, ctx: commands.Context, seconds: int): @@ -513,7 +515,7 @@ class Economy(commands.Cog): await self.config.SLOT_TIME.set(seconds) else: await self.config.guild(guild).SLOT_TIME.set(seconds) - await ctx.send(_("Cooldown is now {} seconds.").format(seconds)) + await ctx.send(_("Cooldown is now {num} seconds.").format(num=seconds)) @economyset.command() async def paydaytime(self, ctx: commands.Context, seconds: int): @@ -524,7 +526,9 @@ class Economy(commands.Cog): else: await self.config.guild(guild).PAYDAY_TIME.set(seconds) await ctx.send( - _("Value modified. At least {} seconds must pass between each payday.").format(seconds) + _("Value modified. At least {num} seconds must pass between each payday.").format( + num=seconds + ) ) @economyset.command() @@ -539,7 +543,11 @@ class Economy(commands.Cog): await self.config.PAYDAY_CREDITS.set(creds) else: await self.config.guild(guild).PAYDAY_CREDITS.set(creds) - await ctx.send(_("Every payday will now give {} {}.").format(creds, credits_name)) + await ctx.send( + _("Every payday will now give {num} {currency}.").format( + num=creds, currency=credits_name + ) + ) @economyset.command() async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int): @@ -551,9 +559,10 @@ class Economy(commands.Cog): else: await self.config.role(role).PAYDAY_CREDITS.set(creds) await ctx.send( - _("Every payday will now give {} {} to people with the role {}.").format( - creds, credits_name, role.name - ) + _( + "Every payday will now give {num} {currency} " + "to people with the role {role_name}." + ).format(num=creds, currency=credits_name, role_name=role.name) ) @economyset.command() @@ -565,7 +574,9 @@ class Economy(commands.Cog): credits_name = await bank.get_currency_name(guild) await bank.set_default_balance(creds, guild) await ctx.send( - _("Registering an account will now give {} {}.").format(creds, credits_name) + _("Registering an account will now give {num} {currency}.").format( + num=creds, currency=credits_name + ) ) # What would I ever do without stackoverflow? diff --git a/redbot/cogs/general/general.py b/redbot/cogs/general/general.py index e2708d399..1c23e2069 100644 --- a/redbot/cogs/general/general.py +++ b/redbot/cogs/general/general.py @@ -131,11 +131,23 @@ class General(commands.Cog): outcome = cond[(player_choice, red_choice)] if outcome is True: - await ctx.send(_("{} You win {}!").format(red_choice.value, author.mention)) + await ctx.send( + _("{choice} You win {author.mention}!").format( + choice=red_choice.value, author=author + ) + ) elif outcome is False: - await ctx.send(_("{} You lose {}!").format(red_choice.value, author.mention)) + await ctx.send( + _("{choice} You lose {author.mention}!").format( + choice=red_choice.value, author=author + ) + ) else: - await ctx.send(_("{} We're square {}!").format(red_choice.value, author.mention)) + await ctx.send( + _("{choice} We're square {author.mention}!").format( + choice=red_choice.value, author=author + ) + ) @commands.command(name="8", aliases=["8ball"]) async def _8ball(self, ctx, *, question: str): @@ -198,12 +210,12 @@ class General(commands.Cog): text_channels = len(guild.text_channels) voice_channels = len(guild.voice_channels) passed = (ctx.message.created_at - guild.created_at).days - created_at = _("Since {}. That's over {} days ago!").format( - guild.created_at.strftime("%d %b %Y %H:%M"), passed + created_at = _("Since {date}. That's over {num} days ago!").format( + date=guild.created_at.strftime("%d %b %Y %H:%M"), num=passed ) data = discord.Embed(description=created_at, colour=(await ctx.embed_colour())) data.add_field(name=_("Region"), value=str(guild.region)) - data.add_field(name=_("Users"), value="{}/{}".format(online, total_users)) + data.add_field(name=_("Users"), value=f"{online}/{total_users}") data.add_field(name=_("Text Channels"), value=str(text_channels)) data.add_field(name=_("Voice Channels"), value=str(voice_channels)) data.add_field(name=_("Roles"), value=str(len(guild.roles))) @@ -223,7 +235,7 @@ class General(commands.Cog): @commands.command() async def urban(self, ctx, *, word): - """Searches urban dictionary entries using the unofficial api""" + """Searches urban dictionary entries using the unofficial API.""" try: url = "https://api.urbandictionary.com/v0/define?term=" + str(word).lower() @@ -236,8 +248,9 @@ class General(commands.Cog): except: await ctx.send( - _("No Urban dictionary entries were found or there was an error in the process") + _("No Urban dictionary entries were found, or there was an error in the process") ) + return if data.get("error") != 404: @@ -246,20 +259,20 @@ class General(commands.Cog): embeds = [] for ud in data["list"]: embed = discord.Embed() - embed.title = _("{} by {}").format(ud["word"].capitalize(), ud["author"]) + embed.title = _("{word} by {author}").format( + word=ud["word"].capitalize(), author=ud["author"] + ) embed.url = ud["permalink"] - description = "{} \n \n **Example : ** {}".format( - ud["definition"], ud.get("example", "N/A") - ) + description = _("{definition}\n\n**Example:** {example}").format(**ud) if len(description) > 2048: description = "{}...".format(description[:2045]) embed.description = description embed.set_footer( - text=_("{} Down / {} Up , Powered by urban dictionary").format( - ud["thumbs_down"], ud["thumbs_up"] - ) + text=_( + "{thumbs_down} Down / {thumbs_up} Up, Powered by Urban Dictionary." + ).format(**ud) ) embeds.append(embed) @@ -274,25 +287,17 @@ class General(commands.Cog): ) else: messages = [] + ud.set_default("example", "N/A") for ud in data["list"]: - description = _("{} \n \n **Example : ** {}").format( - ud["definition"], ud.get("example", "N/A") - ) + description = _("{definition}\n\n**Example:** {example}").format(**ud) if len(description) > 2048: description = "{}...".format(description[:2045]) description = description message = _( - "<{}> \n {} by {} \n \n {} \n \n {} Down / {} Up, Powered by urban " - "dictionary" - ).format( - ud["permalink"], - ud["word"].capitalize(), - ud["author"], - description, - ud["thumbs_down"], - ud["thumbs_up"], - ) + "<{permalink}>\n {word} by {author}\n\n{description}\n\n" + "{thumbs_down} Down / {thumbs_up} Up, Powered by urban dictionary" + ).format(word=ud.pop("word").capitalize(), **ud) messages.append(message) if messages is not None and len(messages) > 0: @@ -306,6 +311,6 @@ class General(commands.Cog): ) else: await ctx.send( - _("No Urban dictionary entries were found or there was an error in the process") + _("No Urban dictionary entries were found, or there was an error in the process.") ) return diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py index 6b4a192d7..ae214388a 100644 --- a/redbot/cogs/mod/mod.py +++ b/redbot/cogs/mod/mod.py @@ -178,17 +178,25 @@ class Mod(commands.Cog): delete_delay = await self.settings.guild(guild).delete_delay() reinvite_on_unban = await self.settings.guild(guild).reinvite_on_unban() msg = "" - msg += "Delete repeats: {}\n".format("Yes" if delete_repeats else "No") - msg += "Ban mention spam: {}\n".format( - "{} mentions".format(ban_mention_spam) + msg += _("Delete repeats: {yes_or_no}\n").format( + yes_or_no=_("Yes") if delete_repeats else _("No") + ) + msg += _("Ban mention spam: {num_mentions}\n").format( + num_mentions=_("{num} mentions").format(num=ban_mention_spam) if isinstance(ban_mention_spam, int) - else "No" + else _("No") ) - msg += "Respects hierarchy: {}\n".format("Yes" if respect_hierarchy else "No") - msg += "Delete delay: {}\n".format( - "{} seconds".format(delete_delay) if delete_delay != -1 else "None" + msg += _("Respects hierarchy: {yes_or_no}\n").format( + yes_or_no=_("Yes") if respect_hierarchy else _("No") + ) + msg += _("Delete delay: {num_seconds}\n").format( + num_seconds=_("{num} seconds").format(delete_delay) + if delete_delay != -1 + else _("None") + ) + msg += _("Reinvite on unban: {yes_or_no}\n").format( + yes_or_no=_("Yes") if respect_hierarchy else _("No") ) - msg += "Reinvite on unban: {}".format("Yes" if reinvite_on_unban else "No") await ctx.send(box(msg)) @modset.command() @@ -222,9 +230,9 @@ class Mod(commands.Cog): await ctx.send( _( "Autoban for mention spam enabled. " - "Anyone mentioning {} or more different people " + "Anyone mentioning {max_mentions} or more different people " "in a single message will be autobanned." - ).format(max_mentions) + ).format(max_mentions=max_mentions) ) else: cur_setting = await self.settings.guild(guild).ban_mention_spam() @@ -262,16 +270,16 @@ class Mod(commands.Cog): if time == -1: await ctx.send(_("Command deleting disabled.")) else: - await ctx.send(_("Delete delay set to {} seconds.").format(time)) + await ctx.send(_("Delete delay set to {num} seconds.").format(num=time)) else: delay = await self.settings.guild(guild).delete_delay() if delay != -1: await ctx.send( _( "Bot will delete command messages after" - " {} seconds. Set this value to -1 to" + " {num} seconds. Set this value to -1 to" " stop deleting messages" - ).format(delay) + ).format(num=delay) ) else: await ctx.send(_("I will not delete command messages.")) @@ -287,10 +295,16 @@ class Mod(commands.Cog): cur_setting = await self.settings.guild(guild).reinvite_on_unban() if not cur_setting: await self.settings.guild(guild).reinvite_on_unban.set(True) - await ctx.send(_("Users unbanned with {} will be reinvited.").format("[p]unban")) + await ctx.send( + _("Users unbanned with {command} will be reinvited.").format(f"{ctx.prefix}unban") + ) else: await self.settings.guild(guild).reinvite_on_unban.set(False) - await ctx.send(_("Users unbanned with {} will not be reinvited.").format("[p]unban")) + await ctx.send( + _("Users unbanned with {command} will not be reinvited.").format( + f"{ctx.prefix}unban" + ) + ) @commands.command() @commands.guild_only() @@ -305,7 +319,9 @@ class Mod(commands.Cog): if author == user: await ctx.send( - _("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}") + _("I cannot let you do that. Self-harm is bad {emoji}").format( + emoji="\N{PENSIVE FACE}" + ) ) return elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): @@ -515,9 +531,13 @@ class Mod(commands.Cog): try: # We don't want blocked DMs preventing us from banning msg = await user.send( _( - "You have been temporarily banned from {} until {}. " - "Here is an invite for when your ban expires: {}" - ).format(guild.name, unban_time.strftime("%m-%d-%Y %H:%M:%S"), invite) + "You have been temporarily banned from {server_name} until {date}. " + "Here is an invite for when your ban expires: {invite_link}" + ).format( + server_name=guild.name, + date=unban_time.strftime("%m-%d-%Y %H:%M:%S"), + invite_link=invite, + ) ) except discord.HTTPException: msg = None @@ -557,7 +577,9 @@ class Mod(commands.Cog): if author == user: await ctx.send( - _("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}") + _("I cannot let you do that. Self-harm is bad {emoji}").format( + emoji="\N{PENSIVE FACE}" + ) ) return elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): @@ -583,8 +605,8 @@ class Mod(commands.Cog): _( "You have been banned and " "then unbanned as a quick way to delete your messages.\n" - "You can now join the server again. {}" - ).format(invite) + "You can now join the server again. {invite_link}" + ).format(invite_link=invite) ) except discord.HTTPException: msg = None @@ -687,26 +709,26 @@ class Mod(commands.Cog): invite = await self.get_invite_for_reinvite(ctx) if invite: try: - user.send( + await user.send( _( - "You've been unbanned from {}.\n" - "Here is an invite for that server: {}" - ).format(guild.name, invite.url) + "You've been unbanned from {server}.\n" + "Here is an invite for that server: {invite_link}" + ).format(server=guild.name, invite_link=invite.url) ) except discord.Forbidden: await ctx.send( _( "I failed to send an invite to that user. " "Perhaps you may be able to send it for me?\n" - "Here's the invite link: {}" - ).format(invite.url) + "Here's the invite link: {invite_link}" + ).format(invite_link=invite.url) ) except discord.HTTPException: await ctx.send( _( "Something went wrong when attempting to send that user" - "an invite. Here's the link so you can try: {}" - ).format(invite.url) + "an invite. Here's the link so you can try: {invite_link}" + ).format(invite_link=invite.url) ) @staticmethod @@ -841,7 +863,9 @@ class Mod(commands.Cog): await ctx.send("Done.") except discord.Forbidden: await ctx.send( - _("I cannot do that, I lack the '{}' permission.").format("Manage Nicknames") + _("I cannot do that, I lack the '{perm}' permission.").format( + perm="Manage Nicknames" + ) ) @commands.group() @@ -868,9 +892,7 @@ class Mod(commands.Cog): audit_reason = get_audit_reason(ctx.author, reason) await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason) await ctx.send( - _("Muted {}#{} in channel {}").format( - user.name, user.discriminator, channel.name - ) + _("Muted {user} in channel {channel.name}").format(user, channel=channel) ) try: await modlog.create_case( @@ -888,7 +910,9 @@ class Mod(commands.Cog): await ctx.send(e) return elif channel.permissions_for(user).speak is False: - await ctx.send(_("That user is already muted in {}!").format(channel.name)) + await ctx.send( + _("That user is already muted in {channel}!").format(channel=channel.name) + ) return else: await ctx.send(_("That user is not in a voice channel right now!")) @@ -908,10 +932,10 @@ class Mod(commands.Cog): guild = ctx.guild if reason is None: - audit_reason = "Channel mute requested by {} (ID {})".format(author, author.id) + audit_reason = "Channel mute requested by {a} (ID {a.id})".format(a=author) else: - audit_reason = "Channel mute requested by {} (ID {}). Reason: {}".format( - author, author.id, reason + audit_reason = "Channel mute requested by {a} (ID {a.id}). Reason: {r}".format( + a=author, r=reason ) success, issue = await self.mute_user(guild, channel, author, user, audit_reason) @@ -944,11 +968,13 @@ class Mod(commands.Cog): guild = ctx.guild user_voice_state = user.voice if reason is None: - audit_reason = "server mute requested by {} (ID {})".format(author, author.id) - else: - audit_reason = "server mute requested by {} (ID {}). Reason: {}".format( - author, author.id, reason + audit_reason = "server mute requested by {author} (ID {author.id})".format( + author=author ) + else: + audit_reason = ( + "server mute requested by {author} (ID {author.id}). Reason: {reason}" + ).format(author=author, reason=reason) mute_success = [] for channel in guild.channels: From 3a20c113315fbae5dcc5aa43a5d498625c94777c Mon Sep 17 00:00:00 2001 From: Toby Harradine Date: Thu, 16 Aug 2018 10:10:03 +1000 Subject: [PATCH 2/5] [i18n] User-facing string pass over admin, alias and audio --- redbot/cogs/admin/admin.py | 97 +++-- redbot/cogs/admin/announcer.py | 7 +- redbot/cogs/admin/converters.py | 7 +- redbot/cogs/alias/alias.py | 56 +-- redbot/cogs/audio/audio.py | 638 ++++++++++++++++++-------------- 5 files changed, 439 insertions(+), 366 deletions(-) diff --git a/redbot/cogs/admin/admin.py b/redbot/cogs/admin/admin.py index 30136b980..35eb7a964 100644 --- a/redbot/cogs/admin/admin.py +++ b/redbot/cogs/admin/admin.py @@ -14,26 +14,30 @@ log = logging.getLogger("red.admin") _ = Translator("Admin", __file__) -GENERIC_FORBIDDEN = _( +# The following are all lambdas to allow us to fetch the translation +# during runtime, without having to copy the large strings everywhere +# in the code. + +generic_forbidden = lambda: _( "I attempted to do something that Discord denied me permissions for." " Your command failed to successfully complete." ) -HIERARCHY_ISSUE = _( +hierarchy_issue = lambda: _( "I tried to add {role.name} to {member.display_name} but that role" " is higher than my highest role in the Discord hierarchy so I was" " unable to successfully add it. Please give me a higher role and " "try again." ) -USER_HIERARCHY_ISSUE = _( +user_hierarchy_issue = lambda: _( "I tried to add {role.name} to {member.display_name} but that role" " is higher than your highest role in the Discord hierarchy so I was" " unable to successfully add it. Please get a higher role and " "try again." ) -RUNNING_ANNOUNCEMENT = _( +running_announcement = lambda: _( "I am already announcing something. If you would like to make a" " different announcement please use `{prefix}announce cancel`" " first." @@ -42,6 +46,7 @@ RUNNING_ANNOUNCEMENT = _( @cog_i18n(_) class Admin(commands.Cog): + """A collection of server administration utilities.""" def __init__(self, config=Config): super().__init__() self.conf = config.get_conf(self, 8237492837454039, force_registration=True) @@ -101,9 +106,9 @@ class Admin(commands.Cog): await member.add_roles(role) except discord.Forbidden: if not self.pass_hierarchy_check(ctx, role): - await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member) + await self.complain(ctx, hierarchy_issue(), role=role, member=member) else: - await self.complain(ctx, GENERIC_FORBIDDEN) + await self.complain(ctx, generic_forbidden()) else: await ctx.send( _("I successfully added {role.name} to {member.display_name}").format( @@ -116,9 +121,9 @@ class Admin(commands.Cog): await member.remove_roles(role) except discord.Forbidden: if not self.pass_hierarchy_check(ctx, role): - await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member) + await self.complain(ctx, hierarchy_issue(), role=role, member=member) else: - await self.complain(ctx, GENERIC_FORBIDDEN) + await self.complain(ctx, generic_forbidden()) else: await ctx.send( _("I successfully removed {role.name} from {member.display_name}").format( @@ -132,8 +137,8 @@ class Admin(commands.Cog): async def addrole( self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None ): - """ - Adds a role to a user. + """Add a role to a user. + If user is left blank it defaults to the author of the command. """ if user is None: @@ -142,7 +147,7 @@ class Admin(commands.Cog): # noinspection PyTypeChecker await self._addrole(ctx, user, rolename) else: - await self.complain(ctx, USER_HIERARCHY_ISSUE, member=ctx.author, role=rolename) + await self.complain(ctx, user_hierarchy_issue(), member=ctx.author, role=rolename) @commands.command() @commands.guild_only() @@ -150,8 +155,8 @@ class Admin(commands.Cog): async def removerole( self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None ): - """ - Removes a role from a user. + """Remove a role from a user. + If user is left blank it defaults to the author of the command. """ if user is None: @@ -160,38 +165,40 @@ class Admin(commands.Cog): # noinspection PyTypeChecker await self._removerole(ctx, user, rolename) else: - await self.complain(ctx, USER_HIERARCHY_ISSUE) + await self.complain(ctx, user_hierarchy_issue()) @commands.group() @commands.guild_only() @checks.admin_or_permissions(manage_roles=True) async def editrole(self, ctx: commands.Context): - """Edits roles settings""" + """Edit role settings.""" pass @editrole.command(name="colour", aliases=["color"]) async def editrole_colour( self, ctx: commands.Context, role: discord.Role, value: discord.Colour ): - """Edits a role's colour + """Edit a role's colour. Use double quotes if the role contains spaces. Colour must be in hexadecimal format. - \"http://www.w3schools.com/colors/colors_picker.asp\" + [Online colour picker](http://www.w3schools.com/colors/colors_picker.asp) + Examples: - !editrole colour \"The Transistor\" #ff0000 - !editrole colour Test #ff9900""" + `[p]editrole colour "The Transistor" #ff0000` + `[p]editrole colour Test #ff9900` + """ author = ctx.author reason = "{}({}) changed the colour of role '{}'".format(author.name, author.id, role.name) if not self.pass_user_hierarchy_check(ctx, role): - await self.complain(ctx, USER_HIERARCHY_ISSUE) + await self.complain(ctx, user_hierarchy_issue()) return try: await role.edit(reason=reason, color=value) except discord.Forbidden: - await self.complain(ctx, GENERIC_FORBIDDEN) + await self.complain(ctx, generic_forbidden()) else: log.info(reason) await ctx.send(_("Done.")) @@ -199,11 +206,13 @@ class Admin(commands.Cog): @editrole.command(name="name") @checks.admin_or_permissions(administrator=True) async def edit_role_name(self, ctx: commands.Context, role: discord.Role, *, name: str): - """Edits a role's name + """Edit a role's name. Use double quotes if the role or the name contain spaces. + Examples: - !editrole name \"The Transistor\" Test""" + `[p]editrole name \"The Transistor\" Test` + """ author = ctx.message.author old_name = role.name reason = "{}({}) changed the name of role '{}' to '{}'".format( @@ -211,13 +220,13 @@ class Admin(commands.Cog): ) if not self.pass_user_hierarchy_check(ctx, role): - await self.complain(ctx, USER_HIERARCHY_ISSUE) + await self.complain(ctx, user_hierarchy_issue()) return try: await role.edit(reason=reason, name=name) except discord.Forbidden: - await self.complain(ctx, GENERIC_FORBIDDEN) + await self.complain(ctx, generic_forbidden()) else: log.info(reason) await ctx.send(_("Done.")) @@ -225,9 +234,7 @@ class Admin(commands.Cog): @commands.group(invoke_without_command=True) @checks.is_owner() async def announce(self, ctx: commands.Context, *, message: str): - """ - Announces a message to all servers the bot is in. - """ + """Announce a message to all servers the bot is in.""" if not self.is_announcing(): announcer = Announcer(ctx, message, config=self.conf) announcer.start() @@ -237,14 +244,12 @@ class Admin(commands.Cog): await ctx.send(_("The announcement has begun.")) else: prefix = ctx.prefix - await self.complain(ctx, RUNNING_ANNOUNCEMENT, prefix=prefix) + await self.complain(ctx, running_announcement(), prefix=prefix) @announce.command(name="cancel") @checks.is_owner() async def announce_cancel(self, ctx): - """ - Cancels a running announce. - """ + """Cancel a running announce.""" try: self.__current_announcer.cancel() except AttributeError: @@ -256,9 +261,7 @@ class Admin(commands.Cog): @commands.guild_only() @checks.guildowner_or_permissions(administrator=True) async def announce_channel(self, ctx, *, channel: discord.TextChannel = None): - """ - Changes the channel on which the bot makes announcements. - """ + """Change the channel to which the bot makes announcements.""" if channel is None: channel = ctx.channel await self.conf.guild(ctx.guild).announce_channel.set(channel.id) @@ -271,9 +274,7 @@ class Admin(commands.Cog): @commands.guild_only() @checks.guildowner_or_permissions(administrator=True) async def announce_ignore(self, ctx): - """ - Toggles whether the announcements will ignore the current server. - """ + """Toggle announcements being enabled this server.""" ignored = await self.conf.guild(ctx.guild).announce_ignore() await self.conf.guild(ctx.guild).announce_ignore.set(not ignored) @@ -309,8 +310,9 @@ class Admin(commands.Cog): @commands.guild_only() @commands.group(invoke_without_command=True) async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole): - """ - Add a role to yourself that server admins have configured as user settable. + """Add a role to yourself. + + Server admins must have configured the role as user settable. NOTE: The role is case sensitive! """ @@ -319,8 +321,7 @@ class Admin(commands.Cog): @selfrole.command(name="remove") async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole): - """ - Removes a selfrole from yourself. + """Remove a selfrole from yourself. NOTE: The role is case sensitive! """ @@ -330,8 +331,7 @@ class Admin(commands.Cog): @selfrole.command(name="add") @checks.admin_or_permissions(manage_roles=True) async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role): - """ - Add a role to the list of available selfroles. + """Add a role to the list of available selfroles. NOTE: The role is case sensitive! """ @@ -344,8 +344,7 @@ class Admin(commands.Cog): @selfrole.command(name="delete") @checks.admin_or_permissions(manage_roles=True) async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole): - """ - Removes a role from the list of available selfroles. + """Remove a role from the list of available selfroles. NOTE: The role is case sensitive! """ @@ -362,7 +361,7 @@ class Admin(commands.Cog): selfroles = await self._valid_selfroles(ctx.guild) fmt_selfroles = "\n".join(["+ " + r.name for r in selfroles]) - msg = _("Available Selfroles:\n{selfroles}").format(selfroles=fmt_selfroles) + msg = _("Available Selfroles: {selfroles}").format(selfroles=fmt_selfroles) await ctx.send(box(msg, "diff")) async def _serverlock_check(self, guild: discord.Guild) -> bool: @@ -379,9 +378,7 @@ class Admin(commands.Cog): @commands.command() @checks.is_owner() async def serverlock(self, ctx: commands.Context): - """ - Locks a bot to its current servers only. - """ + """Lock a bot to its current servers only.""" serverlocked = await self.conf.serverlocked() await self.conf.serverlocked.set(not serverlocked) diff --git a/redbot/cogs/admin/announcer.py b/redbot/cogs/admin/announcer.py index 8c02b6e10..154eacb48 100644 --- a/redbot/cogs/admin/announcer.py +++ b/redbot/cogs/admin/announcer.py @@ -2,6 +2,9 @@ import asyncio import discord from redbot.core import commands +from redbot.core.i18n import Translator + +_ = Translator("Announcer", __file__) class Announcer: @@ -63,7 +66,9 @@ class Announcer: try: await channel.send(self.message) except discord.Forbidden: - await bot_owner.send("I could not announce to server: {}".format(g.id)) + await bot_owner.send( + _("I could not announce to server: {server.id}").format(server=g) + ) await asyncio.sleep(0.5) self.active = False diff --git a/redbot/cogs/admin/converters.py b/redbot/cogs/admin/converters.py index f507d975f..073511d90 100644 --- a/redbot/cogs/admin/converters.py +++ b/redbot/cogs/admin/converters.py @@ -1,5 +1,8 @@ import discord from redbot.core import commands +from redbot.core.i18n import Translator + +_ = Translator("AdminConverters", __file__) class MemberDefaultAuthor(commands.Converter): @@ -19,7 +22,7 @@ class SelfRole(commands.Converter): async def convert(self, ctx: commands.Context, arg: str) -> discord.Role: admin = ctx.command.instance if admin is None: - raise commands.BadArgument("Admin is not loaded.") + raise commands.BadArgument(_("The Admin cog is not loaded.")) conf = admin.conf selfroles = await conf.guild(ctx.guild).selfroles() @@ -28,5 +31,5 @@ class SelfRole(commands.Converter): role = await role_converter.convert(ctx, arg) if role.id not in selfroles: - raise commands.BadArgument("The provided role is not a valid selfrole.") + raise commands.BadArgument(_("The provided role is not a valid selfrole.")) return role diff --git a/redbot/cogs/alias/alias.py b/redbot/cogs/alias/alias.py index 6b3596e72..e0ccdef1e 100644 --- a/redbot/cogs/alias/alias.py +++ b/redbot/cogs/alias/alias.py @@ -15,15 +15,14 @@ _ = Translator("Alias", __file__) @cog_i18n(_) class Alias(commands.Cog): - """ - Alias - - Aliases are per server shortcuts for commands. They - can act as both a lambda (storing arguments for repeated use) - or as simply a shortcut to saying "x y z". - + """Create aliases for commands. + + Aliases are alternative names shortcuts for commands. They + can act as both a lambda (storing arguments for repeated use) + or as simply a shortcut to saying "x y z". + When run, aliases will accept any additional arguments - and append them to the stored alias + and append them to the stored alias. """ default_global_settings = {"entries": []} @@ -114,9 +113,8 @@ class Alias(commands.Cog): return False async def get_prefix(self, message: discord.Message) -> str: - """ - Tries to determine what prefix is used in a message object. - Looks to identify from longest prefix to smallest. + """Tries to determine what prefix is used in a message object. + Looks to identify from longest prefix to smallest. Will raise ValueError if no prefix is found. :param message: Message object @@ -177,23 +175,19 @@ class Alias(commands.Cog): @commands.group() @commands.guild_only() async def alias(self, ctx: commands.Context): - """Manage per-server aliases for commands""" + """Manage per-server aliases for commands.""" pass @alias.group(name="global") async def global_(self, ctx: commands.Context): - """ - Manage global aliases. - """ + """Manage global aliases.""" pass @checks.mod_or_permissions(manage_guild=True) @alias.command(name="add") @commands.guild_only() async def _add_alias(self, ctx: commands.Context, alias_name: str, *, command): - """ - Add an alias for a command. - """ + """Add an alias for a command.""" # region Alias Add Validity Checking is_command = self.is_command(alias_name) if is_command: @@ -242,9 +236,7 @@ class Alias(commands.Cog): @checks.is_owner() @global_.command(name="add") async def _add_global_alias(self, ctx: commands.Context, alias_name: str, *, command): - """ - Add a global alias for a command. - """ + """Add a global alias for a command.""" # region Alias Add Validity Checking is_command = self.is_command(alias_name) if is_command: @@ -292,7 +284,7 @@ class Alias(commands.Cog): @alias.command(name="help") @commands.guild_only() async def _help_alias(self, ctx: commands.Context, alias_name: str): - """Tries to execute help for the base command of the alias""" + """Try to execute help for the base command of the alias.""" is_alias, alias = await self.is_alias(ctx.guild, alias_name=alias_name) if is_alias: base_cmd = alias.command[0] @@ -308,7 +300,7 @@ class Alias(commands.Cog): @alias.command(name="show") @commands.guild_only() async def _show_alias(self, ctx: commands.Context, alias_name: str): - """Shows what command the alias executes.""" + """Show what command the alias executes.""" is_alias, alias = await self.is_alias(ctx.guild, alias_name) if is_alias: @@ -324,14 +316,12 @@ class Alias(commands.Cog): @alias.command(name="del") @commands.guild_only() async def _del_alias(self, ctx: commands.Context, alias_name: str): - """ - Deletes an existing alias on this server. - """ + """Delete an existing alias on this server.""" aliases = await self.unloaded_aliases(ctx.guild) try: next(aliases) except StopIteration: - await ctx.send(_("There are no aliases on this guild.")) + await ctx.send(_("There are no aliases on this server.")) return if await self.delete_alias(ctx, alias_name): @@ -344,9 +334,7 @@ class Alias(commands.Cog): @checks.is_owner() @global_.command(name="del") async def _del_global_alias(self, ctx: commands.Context, alias_name: str): - """ - Deletes an existing global alias. - """ + """Delete an existing global alias.""" aliases = await self.unloaded_global_aliases() try: next(aliases) @@ -364,9 +352,7 @@ class Alias(commands.Cog): @alias.command(name="list") @commands.guild_only() async def _list_alias(self, ctx: commands.Context): - """ - Lists the available aliases on this server. - """ + """List the available aliases on this server.""" names = [_("Aliases:")] + sorted( ["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))] ) @@ -377,9 +363,7 @@ class Alias(commands.Cog): @global_.command(name="list") async def _list_global_alias(self, ctx: commands.Context): - """ - Lists the available global aliases on this bot. - """ + """List the available global aliases on this bot.""" names = [_("Aliases:")] + sorted( ["+ " + a.name for a in await self.unloaded_global_aliases()] ) diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py index dd17228d8..d91eeaf0c 100644 --- a/redbot/cogs/audio/audio.py +++ b/redbot/cogs/audio/audio.py @@ -34,6 +34,7 @@ __author__ = ["aikaterna", "billy/bollo/ati"] @cog_i18n(_) class Audio(commands.Cog): + """Play audio through voice channels.""" def __init__(self, bot): super().__init__() self.bot = bot @@ -146,7 +147,7 @@ class Audio(commands.Cog): dur = lavalink.utils.format_time(player.current.length) embed = discord.Embed( colour=(await self._get_embed_colour(notify_channel)), - title="Now Playing", + title=_("Now Playing"), description=description, ) embed.set_footer( @@ -174,7 +175,7 @@ class Audio(commands.Cog): if playing_servers > 1: await self.bot.change_presence( activity=discord.Activity( - name="music in {} servers".format(playing_servers), + name=_("music in {num} servers").format(num=playing_servers), type=discord.ActivityType.playing, ) ) @@ -184,7 +185,7 @@ class Audio(commands.Cog): if notify_channel: notify_channel = self.bot.get_channel(notify_channel) embed = discord.Embed( - colour=(await self._get_embed_colour(notify_channel)), title="Queue ended." + colour=(await self._get_embed_colour(notify_channel)), title=_("Queue ended.") ) await notify_channel.send(embed=embed) @@ -213,12 +214,12 @@ class Audio(commands.Cog): message_channel = self.bot.get_channel(message_channel) embed = discord.Embed( colour=(await self._get_embed_colour(message_channel)), - title="Track Error", + title=_("Track Error"), description="{}\n**[{}]({})**".format( extra, player.current.title, player.current.uri ), ) - embed.set_footer(text="Skipping...") + embed.set_footer(text=_("Skipping...")) await message_channel.send(embed=embed) await player.skip() @@ -231,11 +232,14 @@ class Audio(commands.Cog): @audioset.command() @checks.admin_or_permissions(manage_roles=True) async def dj(self, ctx): - """Toggle DJ mode (users need a role to use audio commands).""" + """Toggle DJ mode. + + DJ mode allows users with the DJ role to use audio commands. + """ dj_role_id = await self.config.guild(ctx.guild).dj_role() if dj_role_id is None and ctx.guild.get_role(dj_role_id): await self._embed_msg( - ctx, "Please set a role to use with DJ mode. Enter the role name or ID now." + ctx, _("Please set a role to use with DJ mode. Enter the role name or ID now.") ) try: @@ -254,16 +258,19 @@ class Audio(commands.Cog): async def emptydisconnect(self, ctx, seconds: int): """Auto-disconnection after x seconds while stopped. 0 to disable.""" if seconds < 0: - return await self._embed_msg(ctx, "Can't be less than zero.") + return await self._embed_msg(ctx, _("Can't be less than zero.")) if seconds < 10 and seconds > 0: seconds = 10 if seconds == 0: enabled = False - await self._embed_msg(ctx, "Empty disconnect disabled.") + await self._embed_msg(ctx, _("Empty disconnect disabled.")) else: enabled = True await self._embed_msg( - ctx, "Empty disconnect timer set to {}.".format(self._dynamic_time(seconds)) + ctx, + _("Empty disconnect timer set to {num_seconds}.").format( + num_seconds=self._dynamic_time(seconds) + ), ) await self.config.guild(ctx.guild).emptydc_timer.set(seconds) @@ -272,7 +279,7 @@ class Audio(commands.Cog): @audioset.command() @checks.admin_or_permissions(manage_roles=True) async def role(self, ctx, role_name: discord.Role): - """Sets the role to use for DJ mode.""" + """Set the role to use for DJ mode.""" await self.config.guild(ctx.guild).dj_role.set(role_name.id) dj_role_obj = ctx.guild.get_role(await self.config.guild(ctx.guild).dj_role()) await self._embed_msg(ctx, "DJ role set to: {}.".format(dj_role_obj.name)) @@ -280,18 +287,18 @@ class Audio(commands.Cog): @audioset.command() @checks.mod_or_permissions(administrator=True) async def jukebox(self, ctx, price: int): - """Set a price for queueing songs for non-mods. 0 to disable.""" + """Set a price for queueing tracks for non-mods. 0 to disable.""" if price < 0: - return await self._embed_msg(ctx, "Can't be less than zero.") + return await self._embed_msg(ctx, _("Can't be less than zero.")) if price == 0: jukebox = False - await self._embed_msg(ctx, "Jukebox mode disabled.") + await self._embed_msg(ctx, _("Jukebox mode disabled.")) else: jukebox = True await self._embed_msg( ctx, - "Track queueing command price set to {} {}.".format( - price, await bank.get_currency_name(ctx.guild) + _("Track queueing command price set to {price} {currency}.").format( + price=price, currency=await bank.get_currency_name(ctx.guild) ), ) @@ -301,10 +308,12 @@ class Audio(commands.Cog): @audioset.command() @checks.mod_or_permissions(manage_messages=True) async def notify(self, ctx): - """Toggle song announcement and other bot messages.""" + """Toggle track announcement and other bot messages.""" notify = await self.config.guild(ctx.guild).notify() await self.config.guild(ctx.guild).notify.set(not notify) - await self._embed_msg(ctx, "Verbose mode on: {}.".format(not notify)) + await self._embed_msg( + ctx, _("Verbose mode on: {true_or_false}.").format(true_or_false=not notify) + ) @audioset.command() async def settings(self, ctx): @@ -321,32 +330,34 @@ class Audio(commands.Cog): jarbuild = redbot.core.__version__ vote_percent = data["vote_percent"] - msg = "```ini\n" "----Server Settings----\n" + msg = _("```ini\n----Server Settings----\n") if emptydc_enabled: - msg += "Disconnect timer: [{0}]\n".format(self._dynamic_time(emptydc_timer)) + msg += _("Disconnect timer: [{num_seconds}]\n").format( + num_seconds=self._dynamic_time(emptydc_timer) + ) if dj_enabled: - msg += "DJ Role: [{}]\n".format(dj_role_obj.name) + msg += _("DJ Role: [{role.name}]\n").format(role=dj_role_obj) if jukebox: - msg += "Jukebox: [{0}]\n".format(jukebox) - msg += "Command price: [{0}]\n".format(jukebox_price) - msg += ( + msg += _("Jukebox: [{jukebox_name}]\n").format(jukebox_name=jukebox) + msg += _("Command price: [{jukebox_price}]\n").format(jukebox_price=jukebox_price) + msg += _( "Repeat: [{repeat}]\n" "Shuffle: [{shuffle}]\n" "Song notify msgs: [{notify}]\n" - "Songs as status: [{status}]\n".format(**global_data, **data) - ) + "Songs as status: [{status}]\n" + ).format(**global_data, **data) if thumbnail: msg += "Thumbnails: [{0}]\n".format(thumbnail) if vote_percent > 0: - msg += ( - "Vote skip: [{vote_enabled}]\n" "Skip percentage: [{vote_percent}%]\n" + msg += _( + "Vote skip: [{vote_enabled}]\nSkip percentage: [{vote_percent}%]\n" ).format(**data) - msg += ( + msg += _( "---Lavalink Settings---\n" - "Cog version: [{}]\n" - "Jar build: [{}]\n" + "Cog version: [{version}]\n" + "Jar build: [{jarbuild}]\n" "External server: [{use_external_lavalink}]```" - ).format(__version__, jarbuild, **global_data) + ).format(version=__version__, jarbuild=jarbuild, **global_data) embed = discord.Embed(colour=await ctx.embed_colour(), description=msg) return await ctx.send(embed=embed) @@ -362,19 +373,21 @@ class Audio(commands.Cog): @audioset.command() @checks.mod_or_permissions(administrator=True) async def vote(self, ctx, percent: int): - """Percentage needed for non-mods to skip songs. 0 to disable.""" + """Percentage needed for non-mods to skip tracks. 0 to disable.""" if percent < 0: - return await self._embed_msg(ctx, "Can't be less than zero.") + return await self._embed_msg(ctx, _("Can't be less than zero.")) elif percent > 100: percent = 100 if percent == 0: enabled = False await self._embed_msg( - ctx, "Voting disabled. All users can use queue management commands." + ctx, _("Voting disabled. All users can use queue management commands.") ) else: enabled = True - await self._embed_msg(ctx, "Vote percentage set to {}%.".format(percent)) + await self._embed_msg( + ctx, _("Vote percentage set to {percent}%.").format(percent=percent) + ) await self.config.guild(ctx.guild).vote_percent.set(percent) await self.config.guild(ctx.guild).vote_enabled.set(enabled) @@ -382,10 +395,12 @@ class Audio(commands.Cog): @checks.is_owner() @audioset.command() async def status(self, ctx): - """Enables/disables songs' titles as status.""" + """Enable/disable tracks' titles as status.""" status = await self.config.status() await self.config.status.set(not status) - await self._embed_msg(ctx, "Song titles as status: {}.".format(not status)) + await self._embed_msg( + ctx, _("Song titles as status: {true_or_false}.").format(true_or_false=not status) + ) @commands.command() @commands.guild_only() @@ -424,16 +439,16 @@ class Audio(commands.Cog): except AttributeError: server_list.append( "{} [`{}`]: **{}**".format( - p.channel.guild.name, connect_dur, "Nothing playing." + p.channel.guild.name, connect_dur, _("Nothing playing.") ) ) if server_num == 0: - servers = "Not connected anywhere." + servers = _("Not connected anywhere.") else: servers = "\n".join(server_list) embed = discord.Embed( colour=await ctx.embed_colour(), - title="Connected in {} servers:".format(server_num), + title=_("Connected in {num} servers:").format(num=server_num), description=servers, ) await ctx.send(embed=embed) @@ -441,21 +456,23 @@ class Audio(commands.Cog): @commands.command() @commands.guild_only() async def bump(self, ctx, index: int): - """Bump a song number to the top of the queue.""" + """Bump a track number to the top of the queue.""" dj_enabled = await self.config.guild(ctx.guild).dj_enabled() if not self._player_check(ctx): - return await self._embed_msg(ctx, "Nothing playing.") + return await self._embed_msg(ctx, _("Nothing playing.")) player = lavalink.get_player(ctx.guild.id) if ( not ctx.author.voice or ctx.author.voice.channel != player.channel ) and not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You must be in the voice channel to bump a song.") + return await self._embed_msg( + ctx, _("You must be in the voice channel to bump a track.") + ) if dj_enabled: if not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You need the DJ role to bump songs.") + return await self._embed_msg(ctx, _("You need the DJ role to bump tracks.")) if index > len(player.queue) or index < 1: return await self._embed_msg( - ctx, "Song number must be greater than 1 and within the queue limit." + ctx, _("Song number must be greater than 1 and within the queue limit.") ) bump_index = index - 1 @@ -469,7 +486,9 @@ class Audio(commands.Cog): removed_title = "{} - {}".format(removed.author, removed.title) else: removed_title = removed.title - await self._embed_msg(ctx, "Moved {} to the top of the queue.".format(removed_title)) + await self._embed_msg( + ctx, _("Moved {track} to the top of the queue.").format(track=removed_title) + ) @commands.command(aliases=["dc"]) @commands.guild_only() @@ -642,12 +661,12 @@ class Audio(commands.Cog): else: return True - @commands.command(aliases=["np", "n", "song"]) + @commands.command(aliases=["np", "n", "song", "track"]) @commands.guild_only() async def now(self, ctx): """Now playing.""" if not self._player_check(ctx): - return await self._embed_msg(ctx, "Nothing playing.") + return await self._embed_msg(ctx, _("Nothing playing.")) expected = ("⏮", "⏹", "⏸", "⏭") emoji = {"prev": "⏮", "stop": "⏹", "pause": "⏸", "next": "⏭"} player = lavalink.get_player(ctx.guild.id) @@ -660,34 +679,22 @@ class Audio(commands.Cog): dur = lavalink.utils.format_time(player.current.length) if "localtracks" in player.current.uri: if not player.current.title == "Unknown title": - song = "**{} - {}**\n{}\nRequested by: **{}**\n\n{}`{}`/`{}`".format( - player.current.author, - player.current.title, - player.current.uri.replace("localtracks/", ""), - player.current.requester, - arrow, - pos, - dur, - ) + song = "**{track.author} - {track.title}**\n{uri}\n" else: - song = "{}\nRequested by: **{}**\n\n{}`{}`/`{}`".format( - player.current.uri.replace("localtracks/", ""), - player.current.requester, - arrow, - pos, - dur, - ) + song = "{uri}\n" else: - song = "**[{}]({})**\nRequested by: **{}**\n\n{}`{}`/`{}`".format( - player.current.title, - player.current.uri, - player.current.requester, - arrow, - pos, - dur, - ) + song = "**[{track.title}]({track.uri})**\n" + song += _("Requested by: **{track.requester}**") + song += "\n\n{arrow}`{pos}`/`{dur}`" + song = song.format( + track=player.current, + uri=player.current.uri.replace("localtracks/", ""), + arrow=arrow, + pos=pos, + dur=dur, + ) else: - song = "Nothing." + song = _("Nothing.") if player.fetch("np_message") is not None: try: @@ -696,7 +703,7 @@ class Audio(commands.Cog): pass embed = discord.Embed( - colour=await ctx.embed_colour(), title="Now Playing", description=song + colour=await ctx.embed_colour(), title=_("Now Playing"), description=song ) if await self.config.guild(ctx.guild).thumbnail() and player.current: if player.current.thumbnail: @@ -749,19 +756,19 @@ class Audio(commands.Cog): """Pause and resume.""" dj_enabled = await self.config.guild(ctx.guild).dj_enabled() if not self._player_check(ctx): - return await self._embed_msg(ctx, "Nothing playing.") + return await self._embed_msg(ctx, _("Nothing playing.")) player = lavalink.get_player(ctx.guild.id) if ( not ctx.author.voice or ctx.author.voice.channel != player.channel ) and not await self._can_instaskip(ctx, ctx.author): return await self._embed_msg( - ctx, "You must be in the voice channel to pause the music." + ctx, _("You must be in the voice channel to pause the music.") ) if dj_enabled: if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone( ctx, ctx.author ): - return await self._embed_msg(ctx, "You need the DJ role to pause songs.") + return await self._embed_msg(ctx, _("You need the DJ role to pause tracks.")) command = ctx.invoked_with if not player.current: @@ -775,29 +782,29 @@ class Audio(commands.Cog): if player.current and not player.paused and command != "resume": await player.pause() embed = discord.Embed( - colour=await ctx.embed_colour(), title="Track Paused", description=description + colour=await ctx.embed_colour(), title=_("Track Paused"), description=description ) return await ctx.send(embed=embed) if player.paused and command != "pause": await player.pause(False) embed = discord.Embed( - colour=await ctx.embed_colour(), title="Track Resumed", description=description + colour=await ctx.embed_colour(), title=_("Track Resumed"), description=description ) return await ctx.send(embed=embed) if player.paused and command == "pause": - return await self._embed_msg(ctx, "Track is paused.") + return await self._embed_msg(ctx, _("Track is paused.")) if player.current and command == "resume": - return await self._embed_msg(ctx, "Track is playing.") - await self._embed_msg(ctx, "Nothing playing.") + return await self._embed_msg(ctx, _("Track is playing.")) + await self._embed_msg(ctx, _("Nothing playing.")) @commands.command() @commands.guild_only() async def percent(self, ctx): """Queue percentage.""" if not self._player_check(ctx): - return await self._embed_msg(ctx, "Nothing playing.") + return await self._embed_msg(ctx, _("Nothing playing.")) player = lavalink.get_player(ctx.guild.id) queue_tracks = player.queue requesters = {"total": 0, "users": {}} @@ -821,7 +828,7 @@ class Audio(commands.Cog): ) await _usercount(req_username) except AttributeError: - return await self._embed_msg(ctx, "Nothing in the queue.") + return await self._embed_msg(ctx, _("Nothing in the queue.")) for req_username in requesters["users"]: percentage = float(requesters["users"][req_username]["songcount"]) / float( @@ -843,7 +850,7 @@ class Audio(commands.Cog): queue_user_list = "\n".join(queue_user) embed = discord.Embed( colour=await ctx.embed_colour(), - title="Queued and playing songs:", + title=_("Queued and playing tracks:"), description=queue_user_list, ) await ctx.send(embed=embed) @@ -851,7 +858,7 @@ class Audio(commands.Cog): @commands.command() @commands.guild_only() async def play(self, ctx, *, query): - """Play a URL or search for a song.""" + """Play a URL or search for a track.""" dj_enabled = await self.config.guild(ctx.guild).dj_enabled() jukebox_price = await self.config.guild(ctx.guild).jukebox_price() shuffle = await self.config.guild(ctx.guild).shuffle() @@ -861,16 +868,16 @@ class Audio(commands.Cog): ctx.author.voice.channel ): return await self._embed_msg( - ctx, "I don't have permission to connect to your channel." + ctx, _("I don't have permission to connect to your channel.") ) await lavalink.connect(ctx.author.voice.channel) player = lavalink.get_player(ctx.guild.id) player.store("connect", datetime.datetime.utcnow()) except AttributeError: - return await self._embed_msg(ctx, "Connect to a voice channel first.") + return await self._embed_msg(ctx, _("Connect to a voice channel first.")) if dj_enabled: if not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You need the DJ role to queue songs.") + return await self._embed_msg(ctx, _("You need the DJ role to queue tracks.")) player = lavalink.get_player(ctx.guild.id) player.store("channel", ctx.channel.id) player.store("guild", ctx.guild.id) @@ -879,13 +886,13 @@ class Audio(commands.Cog): not ctx.author.voice or ctx.author.voice.channel != player.channel ) and not await self._can_instaskip(ctx, ctx.author): return await self._embed_msg( - ctx, "You must be in the voice channel to use the play command." + ctx, _("You must be in the voice channel to use the play command.") ) if not await self._currency_check(ctx, jukebox_price): return if not query: - return await self._embed_msg(ctx, "No songs to play.") + return await self._embed_msg(ctx, _("No tracks to play.")) query = query.strip("<>") if query.startswith("localtrack:"): @@ -899,7 +906,7 @@ class Audio(commands.Cog): tracks = await player.get_tracks(query) if not tracks: - return await self._embed_msg(ctx, "Nothing found.") + return await self._embed_msg(ctx, _("Nothing found.")) queue_duration = await self._queue_duration(ctx) queue_total_duration = lavalink.utils.format_time(queue_duration) @@ -910,14 +917,14 @@ class Audio(commands.Cog): player.add(ctx.author, track) embed = discord.Embed( colour=await ctx.embed_colour(), - title="Playlist Enqueued", - description="Added {} tracks to the queue.".format(len(tracks)), + title=_("Playlist Enqueued"), + description=_("Added {num} tracks to the queue.").format(num=len(tracks)), ) if not shuffle and queue_duration > 0: embed.set_footer( - text="{} until start of playlist playback: starts at #{} in queue".format( - queue_total_duration, before_queue_length + 1 - ) + text=_( + "{time} until start of playlist playback: starts at #{position} in queue" + ).format(time=queue_total_duration, position=before_queue_length + 1) ) if not player.current: await player.play() @@ -937,16 +944,16 @@ class Audio(commands.Cog): else: description = "**[{}]({})**".format(single_track.title, single_track.uri) embed = discord.Embed( - colour=await ctx.embed_colour(), title="Track Enqueued", description=description + colour=await ctx.embed_colour(), title=_("Track Enqueued"), description=description ) if not shuffle and queue_duration > 0: embed.set_footer( - text="{} until track playback: #{} in queue".format( - queue_total_duration, before_queue_length + 1 + text=_("{time} until track playback: #{position} in queue").format( + time=queue_total_duration, position=before_queue_length + 1 ) ) elif queue_duration > 0: - embed.set_footer(text="#{} in queue".format(len(player.queue))) + embed.set_footer(text=_("#{position} in queue").format(position=len(player.queue))) if not player.current: await player.play() await ctx.send(embed=embed) @@ -959,7 +966,10 @@ class Audio(commands.Cog): @playlist.command(name="append") async def _playlist_append(self, ctx, playlist_name, *url): - """Add a song URL, playlist link, or quick search to the end of a saved playlist.""" + """Add a track URL, playlist link, or quick search to a playlist. + + The track(s) will be appended to the end of the playlist. + """ if not await self._playlist_check(ctx): return async with self.config.guild(ctx.guild).playlists() as playlists: @@ -967,7 +977,9 @@ class Audio(commands.Cog): if playlists[playlist_name][ "author" ] != ctx.author.id and not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You are not the author of that playlist.") + return await self._embed_msg( + ctx, _("You are not the author of that playlist.") + ) player = lavalink.get_player(ctx.guild.id) to_append = await self._playlist_tracks(ctx, player, url) if not to_append: @@ -983,16 +995,22 @@ class Audio(commands.Cog): else: playlists[playlist_name]["tracks"] = to_append except KeyError: - return await self._embed_msg(ctx, "No playlist with that name.") + return await self._embed_msg(ctx, _("No playlist with that name.")) if playlists[playlist_name]["playlist_url"] is not None: playlists[playlist_name]["playlist_url"] = None if len(to_append) == 1: track_title = to_append[0]["info"]["title"] return await self._embed_msg( - ctx, "{} appended to {}.".format(track_title, playlist_name) + ctx, + _("{track} appended to {playlist}.").format( + track=track_title, playlist=playlist_name + ), ) await self._embed_msg( - ctx, "{} tracks appended to {}.".format(len(to_append), playlist_name) + ctx, + _("{num} tracks appended to {playlist}.").format( + num=len(to_append), playlist=playlist_name + ), ) @playlist.command(name="create") @@ -1001,17 +1019,17 @@ class Audio(commands.Cog): dj_enabled = await self.config.guild(ctx.guild).dj_enabled() if dj_enabled: if not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You need the DJ role to save playlists.") + return await self._embed_msg(ctx, _("You need the DJ role to save playlists.")) async with self.config.guild(ctx.guild).playlists() as playlists: if playlist_name in playlists: return await self._embed_msg( - ctx, "Playlist name already exists, try again with a different name." + ctx, _("Playlist name already exists, try again with a different name.") ) playlist_name = playlist_name.split(" ")[0].strip('"') playlist_list = self._to_json(ctx, None, None) async with self.config.guild(ctx.guild).playlists() as playlists: playlists[playlist_name] = playlist_list - await self._embed_msg(ctx, "Empty playlist {} created.".format(playlist_name)) + await self._embed_msg(ctx, _("Empty playlist {name} created.").format(name=playlist_name)) @playlist.command(name="delete") async def _playlist_delete(self, ctx, playlist_name): @@ -1021,11 +1039,13 @@ class Audio(commands.Cog): if playlists[playlist_name][ "author" ] != ctx.author.id and not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You are not the author of that playlist.") + return await self._embed_msg( + ctx, _("You are not the author of that playlist.") + ) del playlists[playlist_name] except KeyError: - return await self._embed_msg(ctx, "No playlist with that name.") - await self._embed_msg(ctx, "{} playlist deleted.".format(playlist_name)) + return await self._embed_msg(ctx, _("No playlist with that name.")) + await self._embed_msg(ctx, _("{name} playlist deleted.").format(name=playlist_name)) @playlist.command(name="info") async def _playlist_info(self, ctx, playlist_name): @@ -1034,7 +1054,7 @@ class Audio(commands.Cog): try: author_id = playlists[playlist_name]["author"] except KeyError: - return await self._embed_msg(ctx, "No playlist with that name.") + return await self._embed_msg(ctx, _("No playlist with that name.")) author_obj = self.bot.get_user(author_id) playlist_url = playlists[playlist_name]["playlist_url"] try: @@ -1042,15 +1062,17 @@ class Audio(commands.Cog): except TypeError: track_len = 0 if playlist_url is None: - playlist_url = "**Custom playlist.**" + playlist_url = _("**Custom playlist.**") else: - playlist_url = "URL: <{}>".format(playlist_url) + playlist_url = _("URL: <{url}>").format(url=playlist_url) embed = discord.Embed( colour=await ctx.embed_colour(), - title="Playlist info for {}:".format(playlist_name), - description="Author: **{}**\n{}".format(author_obj, playlist_url), + title=_("Playlist info for {playlist_name}:").format(playlist_name=playlist_name), + description=_("Author: **{author_name}**\n{url}").format( + author_name=author_obj, url=playlist_url + ), ) - embed.set_footer(text="{} track(s)".format(track_len)) + embed.set_footer(text=_("{num} track(s)").format(num=track_len)) await ctx.send(embed=embed) @playlist.command(name="list") @@ -1095,7 +1117,7 @@ class Audio(commands.Cog): plist += "`{}.` {}".format(item_idx, playlist_info) embed = discord.Embed( colour=await ctx.embed_colour(), - title="Playlists for {}:".format(ctx.guild.name), + title=_("Playlists for {server_name}:").format(server_name=ctx.guild.name), description=plist, ) embed.set_footer( @@ -1110,14 +1132,14 @@ class Audio(commands.Cog): dj_enabled = await self.config.guild(ctx.guild).dj_enabled() if dj_enabled: if not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You need the DJ role to save playlists.") + return await self._embed_msg(ctx, _("You need the DJ role to save playlists.")) async with self.config.guild(ctx.guild).playlists() as playlists: if playlist_name in playlists: return await self._embed_msg( - ctx, "Playlist name already exists, try again with a different name." + ctx, _("Playlist name already exists, try again with a different name.") ) if not self._player_check(ctx): - return await self._embed_msg(ctx, "Nothing playing.") + return await self._embed_msg(ctx, _("Nothing playing.")) player = lavalink.get_player(ctx.guild.id) tracklist = [] np_song = self._track_creator(player, "np") @@ -1127,7 +1149,7 @@ class Audio(commands.Cog): track_obj = self._track_creator(player, queue_idx) tracklist.append(track_obj) if not playlist_name: - await self._embed_msg(ctx, "Please enter a name for this playlist.") + await self._embed_msg(ctx, _("Please enter a name for this playlist.")) try: playlist_name_msg = await ctx.bot.wait_for( @@ -1137,56 +1159,63 @@ class Audio(commands.Cog): ) playlist_name = playlist_name_msg.content.split(" ")[0].strip('"') if len(playlist_name) > 20: - return await self._embed_msg(ctx, "Try the command again with a shorter name.") + return await self._embed_msg( + ctx, _("Try the command again with a shorter name.") + ) if playlist_name in playlists: return await self._embed_msg( - ctx, "Playlist name already exists, try again with a different name." + ctx, _("Playlist name already exists, try again with a different name.") ) except asyncio.TimeoutError: - return await self._embed_msg(ctx, "No playlist name entered, try again later.") + return await self._embed_msg(ctx, _("No playlist name entered, try again later.")) playlist_list = self._to_json(ctx, None, tracklist) async with self.config.guild(ctx.guild).playlists() as playlists: playlist_name = playlist_name.split(" ")[0].strip('"') playlists[playlist_name] = playlist_list await self._embed_msg( ctx, - "Playlist {} saved from current queue: {} tracks added.".format( - playlist_name.split(" ")[0].strip('"'), len(tracklist) + _("Playlist {name} saved from current queue: {num} tracks added.").format( + name=playlist_name.split(" ")[0].strip('"'), num=len(tracklist) ), ) @playlist.command(name="remove") async def _playlist_remove(self, ctx, playlist_name, url): - """Remove a song from a playlist by url.""" + """Remove a track from a playlist by url.""" async with self.config.guild(ctx.guild).playlists() as playlists: try: if playlists[playlist_name][ "author" ] != ctx.author.id and not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You are not the author of that playlist.") + return await self._embed_msg( + ctx, _("You are not the author of that playlist.") + ) except KeyError: - return await self._embed_msg(ctx, "No playlist with that name.") + return await self._embed_msg(ctx, _("No playlist with that name.")) track_list = playlists[playlist_name]["tracks"] clean_list = [track for track in track_list if not url == track["info"]["uri"]] if len(playlists[playlist_name]["tracks"]) == len(clean_list): - return await self._embed_msg(ctx, "URL not in playlist.") + return await self._embed_msg(ctx, _("URL not in playlist.")) del_count = len(playlists[playlist_name]["tracks"]) - len(clean_list) if not clean_list: del playlists[playlist_name] - return await self._embed_msg(ctx, "No songs left, removing playlist.") + return await self._embed_msg(ctx, _("No tracks left, removing playlist.")) playlists[playlist_name]["tracks"] = clean_list if playlists[playlist_name]["playlist_url"] is not None: playlists[playlist_name]["playlist_url"] = None if del_count > 1: await self._embed_msg( ctx, - "{} entries have been removed from the {} playlist.".format( - del_count, playlist_name + _("{num} entries have been removed from the {playlist_name} playlist.").format( + num=del_count, playlist_name=playlist_name ), ) else: await self._embed_msg( - ctx, "The track has been removed from the {} playlist.".format(playlist_name) + ctx, + _("The track has been removed from the {playlist_name} playlist.").format( + playlist_name=playlist_name + ), ) @playlist.command(name="save") @@ -1203,7 +1232,9 @@ class Audio(commands.Cog): playlists[playlist_name] = playlist_list return await self._embed_msg( ctx, - "Playlist {} saved: {} tracks added.".format(playlist_name, len(tracklist)), + _("Playlist {name} saved: {num} tracks added.").format( + name=playlist_name, num=len(tracklist) + ), ) @playlist.command(name="start") @@ -1221,8 +1252,8 @@ class Audio(commands.Cog): track_count = track_count + 1 embed = discord.Embed( colour=await ctx.embed_colour(), - title="Playlist Enqueued", - description="Added {} tracks to the queue.".format(track_count), + title=_("Playlist Enqueued"), + description=_("Added {num} tracks to the queue.").format(num=track_count), ) await ctx.send(embed=embed) if not player.current: @@ -1230,7 +1261,7 @@ class Audio(commands.Cog): except TypeError: await ctx.invoke(self.play, query=playlists[playlist_name]["playlist_url"]) except KeyError: - await self._embed_msg(ctx, "That playlist doesn't exist.") + await self._embed_msg(ctx, _("That playlist doesn't exist.")) @checks.is_owner() @playlist.command(name="upload") @@ -1240,7 +1271,8 @@ class Audio(commands.Cog): return player = lavalink.get_player(ctx.guild.id) await self._embed_msg( - ctx, "Please upload the playlist file. Any other message will cancel this operation." + ctx, + _("Please upload the playlist file. Any other message will cancel this operation."), ) try: @@ -1248,20 +1280,20 @@ class Audio(commands.Cog): "message", timeout=30.0, check=MessagePredicate.same_context(ctx) ) except asyncio.TimeoutError: - return await self._embed_msg(ctx, "No file detected, try again later.") + return await self._embed_msg(ctx, _("No file detected, try again later.")) try: file_url = file_message.attachments[0].url except IndexError: - return await self._embed_msg(ctx, "Upload canceled.") + return await self._embed_msg(ctx, _("Upload cancelled.")) v2_playlist_name = (file_url.split("/")[6]).split(".")[0] file_suffix = file_url.rsplit(".", 1)[1] if file_suffix != "txt": - return await self._embed_msg(ctx, "Only playlist files can be uploaded.") + return await self._embed_msg(ctx, _("Only playlist files can be uploaded.")) try: async with self.session.request("GET", file_url) as r: v2_playlist = await r.json(content_type="text/plain") except UnicodeDecodeError: - return await self._embed_msg(ctx, "Not a valid playlist file.") + return await self._embed_msg(ctx, _("Not a valid playlist file.")) try: v2_playlist_url = v2_playlist["link"] except KeyError: @@ -1277,12 +1309,12 @@ class Audio(commands.Cog): try: if v3_playlists[v2_playlist_name]: return await self._embed_msg( - ctx, "A playlist already exists with this name." + ctx, _("A playlist already exists with this name.") ) except KeyError: pass embed1 = discord.Embed( - colour=await ctx.embed_colour(), title="Please wait, adding tracks..." + colour=await ctx.embed_colour(), title=_("Please wait, adding tracks...") ) playlist_msg = await ctx.send(embed=embed1) for song_url in v2_playlist["playlist"]: @@ -1296,26 +1328,28 @@ class Audio(commands.Cog): if track_count % 5 == 0: embed2 = discord.Embed( colour=await ctx.embed_colour(), - title="Loading track {}/{}...".format( - track_count, len(v2_playlist["playlist"]) + title=_("Loading track {num}/{total}...").format( + num=track_count, total=len(v2_playlist["playlist"]) ), ) await playlist_msg.edit(embed=embed2) if not track_list: - return await self._embed_msg(ctx, "No tracks found.") + return await self._embed_msg(ctx, _("No tracks found.")) playlist_list = self._to_json(ctx, v2_playlist_url, track_list) async with self.config.guild(ctx.guild).playlists() as v3_playlists: v3_playlists[v2_playlist_name] = playlist_list if len(v2_playlist["playlist"]) != track_count: bad_tracks = len(v2_playlist["playlist"]) - track_count - msg = ( - "Added {} tracks from the {} playlist. {} track(s) could not " - "be loaded.".format(track_count, v2_playlist_name, bad_tracks) - ) + msg = _( + "Added {num} tracks from the {playlist_name} playlist. {num_bad} track(s) " + "could not be loaded." + ).format(num=track_count, playlist_name=v2_playlist_name, num_bad=bad_tracks) else: - msg = "Added {} tracks from the {} playlist.".format(track_count, v2_playlist_name) + msg = _("Added {num} tracks from the {playlist_name} playlist.").format( + num=track_count, playlist_name=v2_playlist_name + ) embed3 = discord.Embed( - colour=await ctx.embed_colour(), title="Playlist Saved", description=msg + colour=await ctx.embed_colour(), title=_("Playlist Saved"), description=msg ) await playlist_msg.edit(embed=embed3) else: @@ -1326,7 +1360,7 @@ class Audio(commands.Cog): jukebox_price = await self.config.guild(ctx.guild).jukebox_price() if dj_enabled: if not await self._can_instaskip(ctx, ctx.author): - await self._embed_msg(ctx, "You need the DJ role to use playlists.") + await self._embed_msg(ctx, _("You need the DJ role to use playlists.")) return False if not self._player_check(ctx): try: @@ -1334,13 +1368,13 @@ class Audio(commands.Cog): ctx.author.voice.channel ): return await self._embed_msg( - ctx, "I don't have permission to connect to your channel." + ctx, _("I don't have permission to connect to your channel.") ) await lavalink.connect(ctx.author.voice.channel) player = lavalink.get_player(ctx.guild.id) player.store("connect", datetime.datetime.utcnow()) except AttributeError: - await self._embed_msg(ctx, "Connect to a voice channel first.") + await self._embed_msg(ctx, _("Connect to a voice channel first.")) return False player = lavalink.get_player(ctx.guild.id) player.store("channel", ctx.channel.id) @@ -1349,7 +1383,7 @@ class Audio(commands.Cog): not ctx.author.voice or ctx.author.voice.channel != player.channel ) and not await self._can_instaskip(ctx, ctx.author): await self._embed_msg( - ctx, "You must be in the voice channel to use the playlist command." + ctx, _("You must be in the voice channel to use the playlist command.") ) return False if not await self._currency_check(ctx, jukebox_price): @@ -1367,7 +1401,7 @@ class Audio(commands.Cog): search = True tracks = await player.get_tracks(query) if not tracks: - return await self._embed_msg(ctx, "Nothing found.") + return await self._embed_msg(ctx, _("Nothing found.")) tracklist = [] if not search: for track in tracks: @@ -1381,9 +1415,9 @@ class Audio(commands.Cog): @commands.command() @commands.guild_only() async def prev(self, ctx): - """Skips to the start of the previously played track.""" + """Skip to the start of the previously played track.""" if not self._player_check(ctx): - return await self._embed_msg(ctx, "Nothing playing.") + return await self._embed_msg(ctx, _("Nothing playing.")) dj_enabled = await self.config.guild(ctx.guild).dj_enabled() player = lavalink.get_player(ctx.guild.id) shuffle = await self.config.guild(ctx.guild).shuffle() @@ -1391,17 +1425,17 @@ class Audio(commands.Cog): if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone( ctx, ctx.author ): - return await self._embed_msg(ctx, "You need the DJ role to skip songs.") + return await self._embed_msg(ctx, _("You need the DJ role to skip tracks.")) if ( not ctx.author.voice or ctx.author.voice.channel != player.channel ) and not await self._can_instaskip(ctx, ctx.author): return await self._embed_msg( - ctx, "You must be in the voice channel to skip the music." + ctx, _("You must be in the voice channel to skip the music.") ) if shuffle: - return await self._embed_msg(ctx, "Turn shuffle off to use this command.") + return await self._embed_msg(ctx, _("Turn shuffle off to use this command.")) if player.fetch("prev_song") is None: - return await self._embed_msg(ctx, "No previous track.") + return await self._embed_msg(ctx, _("No previous track.")) else: last_track = await player.get_tracks(player.fetch("prev_song")) player.add(player.fetch("prev_requester"), last_track[0]) @@ -1417,21 +1451,21 @@ class Audio(commands.Cog): else: description = "**[{}]({})**".format(player.current.title, player.current.uri) embed = discord.Embed( - colour=await ctx.embed_colour(), title="Replaying Track", description=description + colour=await ctx.embed_colour(), title=_("Replaying Track"), description=description ) await ctx.send(embed=embed) @commands.command(aliases=["q"]) @commands.guild_only() async def queue(self, ctx, *, page="1"): - """Lists the queue. + """List the queue. Use [p]queue search to search the queue.""" if not self._player_check(ctx): - return await self._embed_msg(ctx, "There's nothing in the queue.") + return await self._embed_msg(ctx, _("There's nothing in the queue.")) player = lavalink.get_player(ctx.guild.id) if not player.queue: - return await self._embed_msg(ctx, "There's nothing in the queue.") + return await self._embed_msg(ctx, _("There's nothing in the queue.")) if not page.isdigit(): if page.startswith("search "): return await self._queue_search(ctx=ctx, search_words=page.replace("search ", "")) @@ -1458,7 +1492,7 @@ class Audio(commands.Cog): try: arrow = await self._draw_time(ctx) except AttributeError: - return await self._embed_msg(ctx, "There's nothing in the queue.") + return await self._embed_msg(ctx, _("There's nothing in the queue.")) pos = lavalink.utils.format_time(player.position) if player.current.is_stream: @@ -1467,9 +1501,7 @@ class Audio(commands.Cog): dur = lavalink.utils.format_time(player.current.length) if player.current.is_stream: - queue_list += "**Currently livestreaming:** **[{}]({})**\nRequested by: **{}**\n\n{}`{}`/`{}`\n\n".format( - player.current.title, player.current.uri, player.current.requester, arrow, pos, dur - ) + queue_list += _("**Currently livestreaming:**") elif "localtracks" in player.current.uri: if not player.current.title == "Unknown title": @@ -1491,9 +1523,10 @@ class Audio(commands.Cog): dur, ) else: - queue_list += "Playing: **[{}]({})**\nRequested by: **{}**\n\n{}`{}`/`{}`\n\n".format( - player.current.title, player.current.uri, player.current.requester, arrow, pos, dur - ) + queue_list += _("Playing:") + queue_list += " **[{current.title}]({current.uri})**\n".format(current=player.current) + queue_list += _("Requested by: **{user}**").format(user=player.current.requester) + queue_list += "\n\n{arrow}`{pos}`/`{dur}`\n\n".format(arrow=arrow, pos=pos, dur=dur) for i, track in enumerate( player.queue[queue_idx_start:queue_idx_end], start=queue_idx_start @@ -1515,9 +1548,10 @@ class Audio(commands.Cog): track_idx, track.author, track_title, req_user ) else: - queue_list += "`{}.` **[{}]({})**, requested by **{}**\n".format( - track_idx, track_title, track.uri, req_user - ) + queue_list += "`{idx}.` **[{title}]({uri})**, ".format( + idx=track_idx, title=track_title, uri=track.uri + ) + queue_list += _("requested by **{user}**\n").format(user=req_user) embed = discord.Embed( colour=await ctx.embed_colour(), @@ -1528,13 +1562,18 @@ class Audio(commands.Cog): embed.set_thumbnail(url=player.current.thumbnail) queue_duration = await self._queue_duration(ctx) queue_total_duration = lavalink.utils.format_time(queue_duration) - text = "Page {}/{} | {} tracks, {} remaining".format( - page_num, queue_num_pages, len(player.queue) + 1, queue_total_duration + text = _( + "Page {page_num}/{total_pages} | {num_tracks} tracks, {num_remaining} remaining" + ).format( + page_num=page_num, + total_pages=queue_num_pages, + num_tracks=len(player.queue) + 1, + num_remaining=queue_total_duration, ) if repeat: - text += " | Repeat: \N{WHITE HEAVY CHECK MARK}" + text += " | " + _("Repeat") + ": \N{WHITE HEAVY CHECK MARK}" if shuffle: - text += " | Shuffle: \N{WHITE HEAVY CHECK MARK}" + text += " | " + _("Shuffle") + ": \N{WHITE HEAVY CHECK MARK}" embed.set_footer(text=text) return embed @@ -1601,13 +1640,13 @@ class Audio(commands.Cog): @commands.command() @commands.guild_only() async def repeat(self, ctx): - """Toggles repeat.""" + """Toggle repeat.""" dj_enabled = await self.config.guild(ctx.guild).dj_enabled() if dj_enabled: if not await self._can_instaskip(ctx, ctx.author) and not await self._has_dj_role( ctx, ctx.author ): - return await self._embed_msg(ctx, "You need the DJ role to toggle repeat.") + return await self._embed_msg(ctx, _("You need the DJ role to toggle repeat.")) repeat = await self.config.guild(ctx.guild).repeat() await self.config.guild(ctx.guild).repeat.set(not repeat) repeat = await self.config.guild(ctx.guild).repeat() @@ -1618,32 +1657,34 @@ class Audio(commands.Cog): not ctx.author.voice or ctx.author.voice.channel != player.channel ) and not await self._can_instaskip(ctx, ctx.author): return await self._embed_msg( - ctx, "You must be in the voice channel to toggle repeat." + ctx, _("You must be in the voice channel to toggle repeat.") ) - await self._embed_msg(ctx, "Repeat songs: {}.".format(repeat)) + await self._embed_msg( + ctx, _("Repeat tracks: {true_or_false}.").format(true_or_false=repeat) + ) @commands.command() @commands.guild_only() async def remove(self, ctx, index: int): - """Remove a specific song number from the queue.""" + """Remove a specific track number from the queue.""" dj_enabled = await self.config.guild(ctx.guild).dj_enabled() if not self._player_check(ctx): - return await self._embed_msg(ctx, "Nothing playing.") + return await self._embed_msg(ctx, _("Nothing playing.")) player = lavalink.get_player(ctx.guild.id) if not player.queue: - return await self._embed_msg(ctx, "Nothing queued.") + return await self._embed_msg(ctx, _("Nothing queued.")) if dj_enabled: if not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You need the DJ role to remove songs.") + return await self._embed_msg(ctx, _("You need the DJ role to remove tracks.")) if ( not ctx.author.voice or ctx.author.voice.channel != player.channel ) and not await self._can_instaskip(ctx, ctx.author): return await self._embed_msg( - ctx, "You must be in the voice channel to manage the queue." + ctx, _("You must be in the voice channel to manage the queue.") ) if index > len(player.queue) or index < 1: return await self._embed_msg( - ctx, "Song number must be greater than 1 and within the queue limit." + ctx, _("Song number must be greater than 1 and within the queue limit.") ) index -= 1 removed = player.queue.pop(index) @@ -1654,14 +1695,17 @@ class Audio(commands.Cog): removed_title = "{} - {}".format(removed.author, removed.title) else: removed_title = removed.title - await self._embed_msg(ctx, "Removed {} from the queue.".format(removed_title)) + await self._embed_msg( + ctx, _("Removed {track} from the queue.").format(track=removed_title) + ) @commands.command() @commands.guild_only() async def search(self, ctx, *, query): - """Pick a song with a search. - Use [p]search list to queue all songs found on YouTube. - [p]search sc will search SoundCloud instead of YouTube. + """Pick a track with a search. + + Use `[p]search list ` to queue all tracks found on YouTube. + `[p]search sc ` will search SoundCloud instead of YouTube. """ async def _search_menu( @@ -1695,13 +1739,13 @@ class Audio(commands.Cog): ctx.author.voice.channel ): return await self._embed_msg( - ctx, "I don't have permission to connect to your channel." + ctx, _("I don't have permission to connect to your channel.") ) await lavalink.connect(ctx.author.voice.channel) player = lavalink.get_player(ctx.guild.id) player.store("connect", datetime.datetime.utcnow()) except AttributeError: - return await self._embed_msg(ctx, "Connect to a voice channel first.") + return await self._embed_msg(ctx, _("Connect to a voice channel first.")) player = lavalink.get_player(ctx.guild.id) shuffle = await self.config.guild(ctx.guild).shuffle() player.store("channel", ctx.channel.id) @@ -1709,7 +1753,9 @@ class Audio(commands.Cog): if ( not ctx.author.voice or ctx.author.voice.channel != player.channel ) and not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You must be in the voice channel to enqueue songs.") + return await self._embed_msg( + ctx, _("You must be in the voice channel to enqueue tracks.") + ) await self._data_check(ctx) if not isinstance(query, list): @@ -1722,17 +1768,17 @@ class Audio(commands.Cog): query = query.replace("folder:", "") tracks = await self._folder_tracks(ctx, player, query) if not tracks: - return await self._embed_msg(ctx, "Nothing found.") + return await self._embed_msg(ctx, _("Nothing found.")) songembed = discord.Embed( colour=await ctx.embed_colour(), - title="Queued {} track(s).".format(len(tracks)), + title=_("Queued {num} track(s).").format(num=len(tracks)), ) queue_duration = await self._queue_duration(ctx) queue_total_duration = lavalink.utils.format_time(queue_duration) if not shuffle and queue_duration > 0: songembed.set_footer( - text="{} until start of search playback: starts at #{} in queue".format( - queue_total_duration, (len(player.queue) + 1) + text=_("{time} until start of search playback: starts at #{position} in queue").format( + time=queue_total_duration, position=len(player.queue) + 1 ) ) for track in tracks: @@ -1759,7 +1805,7 @@ class Audio(commands.Cog): else: tracks = await player.get_tracks(query) if not tracks: - return await self._embed_msg(ctx, "Nothing found.") + return await self._embed_msg(ctx, _("Nothing found.")) else: tracks = query @@ -1829,18 +1875,20 @@ class Audio(commands.Cog): return await ctx.invoke(self.play, query=("localtrack:{}".format(search_choice))) embed = discord.Embed( - colour=await ctx.embed_colour(), title="Track Enqueued", description=description + colour=await ctx.embed_colour(), title=_("Track Enqueued"), description=description ) queue_duration = await self._queue_duration(ctx) queue_total_duration = lavalink.utils.format_time(queue_duration) if not shuffle and queue_duration > 0: embed.set_footer( - text="{} until track playback: #{} in queue".format( - queue_total_duration, (len(player.queue) + 1) + text=_("{time} until track playback: #{position} in queue").format( + time=queue_total_duration, position=len(player.queue) + 1 + ) + ) + elif queue_duration > 0: + embed.set_footer( + text=_("#{position} in queue").format(position=len(player.queue) + 1) ) - ) - elif queue_duration > 0: - embed.set_footer(text="#{} in queue".format(len(player.queue) + 1)) player.add(ctx.author, search_choice) if not player.current: @@ -1888,64 +1936,76 @@ class Audio(commands.Cog): folder = False try: title_check = tracks[0].uri - title = "Tracks Found:" - footer = "search results" + title = _("Tracks Found:") + footer = _("search results") except AttributeError: if folder: - title = "Folders Found:" - footer = "local folders" + title = _("Folders Found:") + footer = _("local folders") else: - title = "Files Found:" - footer = "local tracks" + title = _("Files Found:") + footer = _("local tracks") embed = discord.Embed( colour=await ctx.embed_colour(), title=title, description=search_list ) embed.set_footer( - text="Page {}/{} | {} {}".format(page_num, search_num_pages, len(tracks), footer) + text=( + _("Page {page_num}/{total_pages}") + " | {num_results} {footer}" + ).format( + page_num=page_num, + total_pages=search_num_pages, + num_results=len(tracks), + footer=footer + ) ) return embed @commands.command() @commands.guild_only() async def seek(self, ctx, seconds: int = 30): - """Seeks ahead or behind on a track by seconds.""" + """Seek ahead or behind on a track by seconds.""" dj_enabled = await self.config.guild(ctx.guild).dj_enabled() if not self._player_check(ctx): - return await self._embed_msg(ctx, "Nothing playing.") + return await self._embed_msg(ctx, _("Nothing playing.")) player = lavalink.get_player(ctx.guild.id) if ( not ctx.author.voice or ctx.author.voice.channel != player.channel ) and not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You must be in the voice channel to use seek.") + return await self._embed_msg(ctx, _("You must be in the voice channel to use seek.")) if dj_enabled: if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone( ctx, ctx.author ): - return await self._embed_msg(ctx, "You need the DJ role to use seek.") + return await self._embed_msg(ctx, _("You need the DJ role to use seek.")) if player.current: if player.current.is_stream: - return await self._embed_msg(ctx, "Can't seek on a stream.") + return await self._embed_msg(ctx, _("Can't seek on a stream.")) else: time_sec = seconds * 1000 seek = player.position + time_sec if seek <= 0: - await self._embed_msg(ctx, "Moved {}s to 00:00:00".format(seconds)) + await self._embed_msg( + ctx, _("Moved {num_seconds}s to 00:00:00").format(num_seconds=seconds) + ) else: await self._embed_msg( - ctx, "Moved {}s to {}".format(seconds, lavalink.utils.format_time(seek)) + ctx, + _("Moved {num_seconds}s to {time}").format( + num_seconds=seconds, time=lavalink.utils.format_time(seek) + ), ) return await player.seek(seek) else: - await self._embed_msg(ctx, "Nothing playing.") + await self._embed_msg(ctx, _("Nothing playing.")) @commands.command() @commands.guild_only() async def shuffle(self, ctx): - """Toggles shuffle.""" + """Toggle shuffle.""" dj_enabled = await self.config.guild(ctx.guild).dj_enabled() if dj_enabled: if not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You need the DJ role to toggle shuffle.") + return await self._embed_msg(ctx, _("You need the DJ role to toggle shuffle.")) shuffle = await self.config.guild(ctx.guild).shuffle() await self.config.guild(ctx.guild).shuffle.set(not shuffle) shuffle = await self.config.guild(ctx.guild).shuffle() @@ -1956,9 +2016,11 @@ class Audio(commands.Cog): not ctx.author.voice or ctx.author.voice.channel != player.channel ) and not await self._can_instaskip(ctx, ctx.author): return await self._embed_msg( - ctx, "You must be in the voice channel to toggle shuffle." + ctx, _("You must be in the voice channel to toggle shuffle.") ) - await self._embed_msg(ctx, "Shuffle songs: {}.".format(shuffle)) + await self._embed_msg( + ctx, _("Shuffle tracks: {true_or_false}.").format(true_or_false=shuffle) + ) @commands.command() @commands.guild_only() @@ -1980,27 +2042,27 @@ class Audio(commands.Cog): async def skip(self, ctx): """Skips to the next track.""" if not self._player_check(ctx): - return await self._embed_msg(ctx, "Nothing playing.") + return await self._embed_msg(ctx, _("Nothing playing.")) player = lavalink.get_player(ctx.guild.id) if ( not ctx.author.voice or ctx.author.voice.channel != player.channel ) and not await self._can_instaskip(ctx, ctx.author): return await self._embed_msg( - ctx, "You must be in the voice channel to skip the music." + ctx, _("You must be in the voice channel to skip the music.") ) dj_enabled = await self.config.guild(ctx.guild).dj_enabled() vote_enabled = await self.config.guild(ctx.guild).vote_enabled() if dj_enabled and not vote_enabled and not await self._can_instaskip(ctx, ctx.author): if not await self._is_alone(ctx, ctx.author): - return await self._embed_msg(ctx, "You need the DJ role to skip songs.") + return await self._embed_msg(ctx, _("You need the DJ role to skip tracks.")) if vote_enabled: if not await self._can_instaskip(ctx, ctx.author): if ctx.author.id in self.skip_votes[ctx.message.guild]: self.skip_votes[ctx.message.guild].remove(ctx.author.id) - reply = "I removed your vote to skip." + reply = _("I removed your vote to skip.") else: self.skip_votes[ctx.message.guild].append(ctx.author.id) - reply = "You voted to skip." + reply = _("You voted to skip.") num_votes = len(self.skip_votes[ctx.message.guild]) vote_mods = [] @@ -2013,11 +2075,15 @@ class Audio(commands.Cog): percent = await self.config.guild(ctx.guild).vote_percent() if vote >= percent: self.skip_votes[ctx.message.guild] = [] - await self._embed_msg(ctx, "Vote threshold met.") + await self._embed_msg(ctx, _("Vote threshold met.")) return await self._skip_action(ctx) else: - reply += " Votes: %d/%d" % (num_votes, num_members) - reply += " (%d%% out of %d%% needed)" % (vote, percent) + reply += _(" Votes: {num_votes}/{num_members}").format( + num_votes=num_votes, num_members=num_members + ) + reply += _(" ({cur_percent}% out of {required_percent}% needed)").format( + cur_percent=vote, required_percent=percent + ) return await self._embed_msg(ctx, reply) else: return await self._skip_action(ctx) @@ -2085,18 +2151,24 @@ class Audio(commands.Cog): try: pos, dur = player.position, player.current.length except AttributeError: - return await self._embed_msg(ctx, "There's nothing in the queue.") + return await self._embed_msg(ctx, _("There's nothing in the queue.")) time_remain = lavalink.utils.format_time(dur - pos) if player.current.is_stream: embed = discord.Embed( - colour=await ctx.embed_colour(), title="There's nothing in the queue." + colour=await ctx.embed_colour(), title=_("There's nothing in the queue.") + ) + embed.set_footer( + text=_("Currently livestreaming {track}").format(track=player.current.title) ) - embed.set_footer(text="Currently livestreaming {}".format(player.current.title)) else: embed = discord.Embed( - colour=await ctx.embed_colour(), title="There's nothing in the queue." + colour=await ctx.embed_colour(), title=_("There's nothing in the queue.") + ) + embed.set_footer( + text=_("{time} left on {track}").format( + time=time_remain, track=player.current.title + ) ) - embed.set_footer(text="{} left on {}".format(time_remain, player.current.title)) return await ctx.send(embed=embed) if "localtracks" in player.current.uri: @@ -2111,7 +2183,7 @@ class Audio(commands.Cog): else: description = "**[{}]({})**".format(player.current.title, player.current.uri) embed = discord.Embed( - colour=await ctx.embed_colour(), title="Track Skipped", description=description + colour=await ctx.embed_colour(), title=_("Track Skipped"), description=description ) await ctx.send(embed=embed) await player.skip() @@ -2119,30 +2191,30 @@ class Audio(commands.Cog): @commands.command(aliases=["s"]) @commands.guild_only() async def stop(self, ctx): - """Stops playback and clears the queue.""" + """Stop playback and clear the queue.""" dj_enabled = await self.config.guild(ctx.guild).dj_enabled() vote_enabled = await self.config.guild(ctx.guild).vote_enabled() if not self._player_check(ctx): - return await self._embed_msg(ctx, "Nothing playing.") + return await self._embed_msg(ctx, _("Nothing playing.")) player = lavalink.get_player(ctx.guild.id) if ( not ctx.author.voice or ctx.author.voice.channel != player.channel ) and not await self._can_instaskip(ctx, ctx.author): return await self._embed_msg( - ctx, "You must be in the voice channel to stop the music." + ctx, _("You must be in the voice channel to stop the music.") ) if vote_enabled or vote_enabled and dj_enabled: if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone( ctx, ctx.author ): return await self._embed_msg( - ctx, "There are other people listening - vote to skip instead." + ctx, _("There are other people listening - vote to skip instead.") ) if dj_enabled and not vote_enabled: if not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You need the DJ role to stop the music.") + return await self._embed_msg(ctx, _("You need the DJ role to stop the music.")) if player.is_playing: - await self._embed_msg(ctx, "Stopping...") + await self._embed_msg(ctx, _("Stopping...")) await player.stop() player.store("prev_requester", None) player.store("prev_song", None) @@ -2152,17 +2224,17 @@ class Audio(commands.Cog): @commands.command() @commands.guild_only() async def volume(self, ctx, vol: int = None): - """Sets the volume, 1% - 150%.""" + """Set the volume, 1% - 150%.""" dj_enabled = await self.config.guild(ctx.guild).dj_enabled() if not vol: vol = await self.config.guild(ctx.guild).volume() embed = discord.Embed( colour=await ctx.embed_colour(), - title="Current Volume:", + title=_("Current Volume:"), description=str(vol) + "%", ) if not self._player_check(ctx): - embed.set_footer(text="Nothing playing.") + embed.set_footer(text=_("Nothing playing.")) return await ctx.send(embed=embed) if self._player_check(ctx): player = lavalink.get_player(ctx.guild.id) @@ -2170,13 +2242,13 @@ class Audio(commands.Cog): not ctx.author.voice or ctx.author.voice.channel != player.channel ) and not await self._can_instaskip(ctx, ctx.author): return await self._embed_msg( - ctx, "You must be in the voice channel to change the volume." + ctx, _("You must be in the voice channel to change the volume.") ) if dj_enabled: if not await self._can_instaskip(ctx, ctx.author) and not await self._has_dj_role( ctx, ctx.author ): - return await self._embed_msg(ctx, "You need the DJ role to change the volume.") + return await self._embed_msg(ctx, _("You need the DJ role to change the volume.")) if vol < 0: vol = 0 if vol > 150: @@ -2189,10 +2261,10 @@ class Audio(commands.Cog): if self._player_check(ctx): await lavalink.get_player(ctx.guild.id).set_volume(vol) embed = discord.Embed( - colour=await ctx.embed_colour(), title="Volume:", description=str(vol) + "%" + colour=await ctx.embed_colour(), title=_("Volume:"), description=str(vol) + "%" ) if not self._player_check(ctx): - embed.set_footer(text="Nothing playing.") + embed.set_footer(text=_("Nothing playing.")) await ctx.send(embed=embed) @commands.group(aliases=["llset"]) @@ -2204,7 +2276,7 @@ class Audio(commands.Cog): @llsetup.command() async def external(self, ctx): - """Toggles using external lavalink servers.""" + """Toggle using external lavalink servers.""" external = await self.config.use_external_lavalink() await self.config.use_external_lavalink.set(not external) if external: @@ -2214,12 +2286,17 @@ class Audio(commands.Cog): await self.config.ws_port.set(2332) embed = discord.Embed( colour=await ctx.embed_colour(), - title="External lavalink server: {}.".format(not external), + title=_("External lavalink server: {true_or_false}.").format( + true_or_false=not external + ), ) - embed.set_footer(text="Defaults reset.") + embed.set_footer(text=_("Defaults reset.")) return await ctx.send(embed=embed) else: - await self._embed_msg(ctx, "External lavalink server: {}.".format(not external)) + await self._embed_msg( + ctx, + _("External lavalink server: {true_or_false}.").format(true_or_false=not external), + ) @llsetup.command() async def host(self, ctx, host): @@ -2227,12 +2304,13 @@ class Audio(commands.Cog): await self.config.host.set(host) if await self._check_external(): embed = discord.Embed( - colour=await ctx.embed_colour(), title="Host set to {}.".format(host) + colour=await ctx.embed_colour(), + title=_("Host set to {host}.").format(host=host), ) - embed.set_footer(text="External lavalink server set to True.") + embed.set_footer(text=_("External lavalink server set to True.")) await ctx.send(embed=embed) else: - await self._embed_msg(ctx, "Host set to {}.".format(host)) + await self._embed_msg(ctx, _("Host set to {host}.").format(host=host)) @llsetup.command() async def password(self, ctx, password): @@ -2241,12 +2319,14 @@ class Audio(commands.Cog): if await self._check_external(): embed = discord.Embed( colour=await ctx.embed_colour(), - title="Server password set to {}.".format(password), + title=_("Server password set to {password}.").format(password=password), ) - embed.set_footer(text="External lavalink server set to True.") + embed.set_footer(text=_("External lavalink server set to True.")) await ctx.send(embed=embed) else: - await self._embed_msg(ctx, "Server password set to {}.".format(password)) + await self._embed_msg( + ctx, _("Server password set to {password}.").format(password=password) + ) @llsetup.command() async def restport(self, ctx, rest_port: int): @@ -2254,12 +2334,13 @@ class Audio(commands.Cog): await self.config.rest_port.set(rest_port) if await self._check_external(): embed = discord.Embed( - colour=await ctx.embed_colour(), title="REST port set to {}.".format(rest_port) + colour=await ctx.embed_colour(), + title=_("REST port set to {port}.").format(port=rest_port), ) - embed.set_footer(text="External lavalink server set to True.") + embed.set_footer(text=_("External lavalink server set to True.")) await ctx.send(embed=embed) else: - await self._embed_msg(ctx, "REST port set to {}.".format(rest_port)) + await self._embed_msg(ctx, _("REST port set to {port}.").format(port=rest_port)) @llsetup.command() async def wsport(self, ctx, ws_port: int): @@ -2267,12 +2348,12 @@ class Audio(commands.Cog): await self.config.ws_port.set(ws_port) if await self._check_external(): embed = discord.Embed( - colour=await ctx.embed_colour(), title="Websocket port set to {}.".format(ws_port) + colour=await ctx.embed_colour(), title=_("Websocket port set to {}.").format(ws_port) ) - embed.set_footer(text="External lavalink server set to True.") + embed.set_footer(text=_("External lavalink server set to True.")) await ctx.send(embed=embed) else: - await self._embed_msg(ctx, "Websocket port set to {}.".format(ws_port)) + await self._embed_msg(ctx, _("Websocket port set to {port}.").format(port=ws_port)) async def _check_external(self): external = await self.config.use_external_lavalink() @@ -2298,7 +2379,10 @@ class Audio(commands.Cog): except ValueError: credits_name = await bank.get_currency_name(ctx.guild) await self._embed_msg( - ctx, "Not enough {} ({} required).".format(credits_name, jukebox_price) + ctx, + _("Not enough {currency} ({required_credits} required).").format( + currency=credits_name, required_credits=jukebox_price + ), ) return False else: From 0c3d8af8f4f768482bb2db1ca89df8e15c21fb52 Mon Sep 17 00:00:00 2001 From: Toby Harradine Date: Thu, 16 Aug 2018 23:51:45 +1000 Subject: [PATCH 3/5] [i18n] Pass over bank, cleanup, customcom, dataconverter, downloader Signed-off-by: Toby Harradine --- redbot/cogs/admin/admin.py | 5 +- redbot/cogs/bank/bank.py | 14 ++-- redbot/cogs/cleanup/cleanup.py | 39 +++++----- redbot/cogs/customcom/customcom.py | 70 +++++++++--------- redbot/cogs/dataconverter/dataconverter.py | 19 ++--- redbot/cogs/downloader/checks.py | 11 ++- redbot/cogs/downloader/converters.py | 4 +- redbot/cogs/downloader/downloader.py | 82 +++++++++------------- redbot/cogs/downloader/repo_manager.py | 10 ++- 9 files changed, 123 insertions(+), 131 deletions(-) diff --git a/redbot/cogs/admin/admin.py b/redbot/cogs/admin/admin.py index 35eb7a964..a8bb090c9 100644 --- a/redbot/cogs/admin/admin.py +++ b/redbot/cogs/admin/admin.py @@ -1,11 +1,10 @@ +import logging from typing import Tuple import discord from redbot.core import Config, checks, commands - -import logging - +from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.chat_formatting import box from .announcer import Announcer from .converters import MemberDefaultAuthor, SelfRole diff --git a/redbot/cogs/bank/bank.py b/redbot/cogs/bank/bank.py index 6969fe30d..7fd79170c 100644 --- a/redbot/cogs/bank/bank.py +++ b/redbot/cogs/bank/bank.py @@ -67,7 +67,7 @@ class Bank(commands.Cog): @checks.guildowner_or_permissions(administrator=True) @commands.group(autohelp=True) async def bankset(self, ctx: commands.Context): - """Base command for bank settings""" + """Base command for bank settings.""" if ctx.invoked_subcommand is None: if await bank.is_global(): bank_name = await bank._conf.bank_name() @@ -91,9 +91,11 @@ class Bank(commands.Cog): @bankset.command(name="toggleglobal") @checks.is_owner() async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool = False): - """Toggles whether the bank is global or not - If the bank is global, it will become per-server - If the bank is per-server, it will become global""" + """Toggle whether the bank is global or not. + + If the bank is global, it will become per-server. + If the bank is per-server, it will become global. + """ cur_setting = await bank.is_global() word = _("per-server") if cur_setting else _("global") @@ -111,14 +113,14 @@ class Bank(commands.Cog): @bankset.command(name="bankname") @check_global_setting_guildowner() async def bankset_bankname(self, ctx: commands.Context, *, name: str): - """Set the bank's name""" + """Set the bank's name.""" await bank.set_bank_name(name, ctx.guild) await ctx.send(_("Bank name has been set to: {name}").format(name=name)) @bankset.command(name="creditsname") @check_global_setting_guildowner() async def bankset_creditsname(self, ctx: commands.Context, *, name: str): - """Set the name for the bank's currency""" + """Set the name for the bank's currency.""" await bank.set_currency_name(name, ctx.guild) await ctx.send(_("Currency name has been set to: {name}").format(name=name)) diff --git a/redbot/cogs/cleanup/cleanup.py b/redbot/cogs/cleanup/cleanup.py index 94eab2364..0ff24fdd5 100644 --- a/redbot/cogs/cleanup/cleanup.py +++ b/redbot/cogs/cleanup/cleanup.py @@ -16,7 +16,7 @@ _ = Translator("Cleanup", __file__) @cog_i18n(_) class Cleanup(commands.Cog): - """Commands for cleaning messages""" + """Commands for cleaning messages.""" def __init__(self, bot: Red): super().__init__() @@ -104,7 +104,7 @@ class Cleanup(commands.Cog): @commands.group() @checks.mod_or_permissions(manage_messages=True) async def cleanup(self, ctx: commands.Context): - """Deletes messages.""" + """Delete messages.""" pass @cleanup.command() @@ -112,16 +112,17 @@ class Cleanup(commands.Cog): async def text( self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False ): - """Deletes last X messages matching the specified text. + """Delete the last X messages matching the specified text. Example: - cleanup text \"test\" 5 + `[p]cleanup text "test" 5` - Remember to use double quotes.""" + Remember to use double quotes. + """ channel = ctx.channel if not channel.permissions_for(ctx.guild.me).manage_messages: - await ctx.send("I need the Manage Messages permission to do this.") + await ctx.send(_("I need the Manage Messages permission to do this.")) return author = ctx.author @@ -159,14 +160,15 @@ class Cleanup(commands.Cog): async def user( self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False ): - """Deletes last X messages from specified user. + """Delete the last X messages from a specified user. Examples: - cleanup user @\u200bTwentysix 2 - cleanup user Red 6""" + `[p]cleanup user @\u200bTwentysix 2` + `[p]cleanup user Red 6` + """ channel = ctx.channel if not channel.permissions_for(ctx.guild.me).manage_messages: - await ctx.send("I need the Manage Messages permission to do this.") + await ctx.send(_("I need the Manage Messages permission to do this.")) return member = None @@ -214,7 +216,7 @@ class Cleanup(commands.Cog): @cleanup.command() @commands.guild_only() async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False): - """Deletes all messages after specified message. + """Delete all messages after a specified message. To get a message id, enable developer mode in Discord's settings, 'appearance' tab. Then right click a message @@ -223,7 +225,7 @@ class Cleanup(commands.Cog): channel = ctx.channel if not channel.permissions_for(ctx.guild.me).manage_messages: - await ctx.send("I need the Manage Messages permission to do this.") + await ctx.send(_("I need the Manage Messages permission to do this.")) return author = ctx.author @@ -280,14 +282,15 @@ class Cleanup(commands.Cog): @cleanup.command() @commands.guild_only() async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False): - """Deletes last X messages. + """Delete the last X messages. Example: - cleanup messages 26""" + `[p]cleanup messages 26` + """ channel = ctx.channel if not channel.permissions_for(ctx.guild.me).manage_messages: - await ctx.send("I need the Manage Messages permission to do this.") + await ctx.send(_("I need the Manage Messages permission to do this.")) return author = ctx.author @@ -311,11 +314,11 @@ class Cleanup(commands.Cog): @cleanup.command(name="bot") @commands.guild_only() async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False): - """Cleans up command messages and messages from the bot.""" + """Clean up command messages and messages from the bot.""" channel = ctx.channel if not channel.permissions_for(ctx.guild.me).manage_messages: - await ctx.send("I need the Manage Messages permission to do this.") + await ctx.send(_("I need the Manage Messages permission to do this.")) return author = ctx.message.author @@ -369,7 +372,7 @@ class Cleanup(commands.Cog): match_pattern: str = None, delete_pinned: bool = False, ): - """Cleans up messages owned by the bot. + """Clean up messages owned by the bot. By default, all messages are cleaned. If a third argument is specified, it is used for pattern matching: If it begins with r( and ends with ), diff --git a/redbot/cogs/customcom/customcom.py b/redbot/cogs/customcom/customcom.py index 870cf8923..f2d6165d2 100644 --- a/redbot/cogs/customcom/customcom.py +++ b/redbot/cogs/customcom/customcom.py @@ -54,7 +54,7 @@ class CommandObj: intro = _( "Welcome to the interactive random {cc} maker!\n" "Every message you send will be added as one of the random " - "responses to choose from once this {} is " + "responses to choose from once this {cc} is " "triggered. To exit this interactive menu, type `{quit}`" ).format(cc="customcommand", quit="exit()") await ctx.send(intro) @@ -196,30 +196,26 @@ class CustomCommands(commands.Cog): @commands.group(aliases=["cc"]) @commands.guild_only() async def customcom(self, ctx: commands.Context): - """Custom commands management""" + """Custom commands management.""" pass - @customcom.group(name="add") + @customcom.group(name="create", aliases=["add"]) @checks.mod_or_permissions(administrator=True) - async def cc_add(self, ctx: commands.Context): - """ - Adds a new custom command + async def cc_create(self, ctx: commands.Context): + """Create custom commands. - CCs can be enhanced with arguments: - https://red-discordbot.readthedocs.io/en/v3-develop/cog_customcom.html + CCs can be enhanced with arguments, see the guide + [here](https://red-discordbot.readthedocs.io/en/v3-develop/cog_customcom.html). """ pass - @cc_add.command(name="random") + @cc_create.command(name="random") @checks.mod_or_permissions(administrator=True) - async def cc_add_random(self, ctx: commands.Context, command: str.lower): - """ - Create a CC where it will randomly choose a response! + async def cc_create_random(self, ctx: commands.Context, command: str.lower): + """Create a CC where it will randomly choose a response! - Note: This is interactive + Note: This command is interactive. """ - responses = [] - responses = await self.commandobj.get_responses(ctx=ctx) try: await self.commandobj.create(ctx=ctx, command=command, response=responses) @@ -233,16 +229,16 @@ class CustomCommands(commands.Cog): # await ctx.send(str(responses)) - @cc_add.command(name="simple") + @cc_create.command(name="simple") @checks.mod_or_permissions(administrator=True) - async def cc_add_simple(self, ctx, command: str.lower, *, text: str): - """Adds a simple custom command + async def cc_create_simple(self, ctx, command: str.lower, *, text: str): + """Add a simple custom command. Example: - [p]customcom add simple yourcommand Text you want + - `[p]customcom create simple yourcommand Text you want` """ if command in self.bot.all_commands: - await ctx.send(_("That command is already a standard command.")) + await ctx.send(_("There already exists a bot command with the same name.")) return try: await self.commandobj.create(ctx=ctx, command=command, response=text) @@ -261,13 +257,14 @@ class CustomCommands(commands.Cog): async def cc_cooldown( self, ctx, command: str.lower, cooldown: int = None, *, per: str.lower = "member" ): - """ - Sets, edits, or views cooldowns for a custom command + """Set, edit, or view the cooldown for a custom command. + + You may set cooldowns per member, channel, or guild. Multiple + cooldowns may be set. All cooldowns must be cooled to call the + custom command. - You may set cooldowns per member, channel, or guild. - Multiple cooldowns may be set. All cooldowns must be cooled to call the custom command. Example: - [p]customcom cooldown yourcommand 30 + - `[p]customcom cooldown yourcommand 30` """ if cooldown is None: try: @@ -294,17 +291,18 @@ class CustomCommands(commands.Cog): except NotFound: await ctx.send( _("That command doesn't exist. Use `{command}` to add it.").format( - command="{}customcom add".format(ctx.prefix) + command="{}customcom create".format(ctx.prefix) ) ) @customcom.command(name="delete") @checks.mod_or_permissions(administrator=True) async def cc_delete(self, ctx, command: str.lower): - """Deletes a custom command - + """Delete a custom command +. Example: - [p]customcom delete yourcommand""" + - `[p]customcom delete yourcommand` + """ try: await self.commandobj.delete(ctx=ctx, command=command) await ctx.send(_("Custom command successfully deleted.")) @@ -314,18 +312,20 @@ class CustomCommands(commands.Cog): @customcom.command(name="edit") @checks.mod_or_permissions(administrator=True) async def cc_edit(self, ctx, command: str.lower, *, text: str = None): - """Edits a custom command's response + """Edit a custom command. Example: - [p]customcom edit yourcommand Text you want + - `[p]customcom edit yourcommand Text you want` """ + command = command.lower() + try: await self.commandobj.edit(ctx=ctx, command=command, response=text) await ctx.send(_("Custom command successfully edited.")) except NotFound: await ctx.send( - _("That command doesn't exist. Use `{}` to add it.").format( - "{}customcom add".format(ctx.prefix) + _("That command doesn't exist. Use `{command}` to add it.").format( + command="{}customcom create".format(ctx.prefix) ) ) except ArgParseError as e: @@ -333,7 +333,7 @@ class CustomCommands(commands.Cog): @customcom.command(name="list") async def cc_list(self, ctx): - """Shows custom commands list""" + """List all available custom commands.""" response = await CommandObj.get_commands(self.config.guild(ctx.guild)) @@ -342,7 +342,7 @@ class CustomCommands(commands.Cog): _( "There are no custom commands in this server." " Use `{command}` to start adding some." - ).format(command="{}customcom add".format(ctx.prefix)) + ).format(command="{}customcom create".format(ctx.prefix)) ) return diff --git a/redbot/cogs/dataconverter/dataconverter.py b/redbot/cogs/dataconverter/dataconverter.py index 93696800b..ee17f7a68 100644 --- a/redbot/cogs/dataconverter/dataconverter.py +++ b/redbot/cogs/dataconverter/dataconverter.py @@ -13,9 +13,7 @@ _ = Translator("DataConverter", __file__) @cog_i18n(_) class DataConverter(commands.Cog): - """ - Cog for importing Red v2 Data - """ + """Import Red V2 data to your V3 instance.""" def __init__(self, bot: Red): super().__init__() @@ -24,13 +22,10 @@ class DataConverter(commands.Cog): @checks.is_owner() @commands.command(name="convertdata") async def dataconversioncommand(self, ctx: commands.Context, v2path: str): - """ - Interactive prompt for importing data from Red v2 + """Interactive prompt for importing data from Red V2. - Takes the path where the v2 install is - - Overwrites values which have entries in both v2 and v3, - use with caution. + Takes the path where the V2 install is, and overwrites + values which have entries in both V2 and v3; use with caution. """ resolver = SpecResolver(Path(v2path.strip())) @@ -54,7 +49,7 @@ class DataConverter(commands.Cog): "message", check=MessagePredicate.same_context(ctx), timeout=60 ) except asyncio.TimeoutError: - return await ctx.send(_("Try this again when you are more ready")) + return await ctx.send(_("Try this again when you are ready.")) else: if message.content.strip().lower() in ["quit", "exit", "-1", "q", "cancel"]: return await ctx.tick() @@ -72,7 +67,7 @@ class DataConverter(commands.Cog): else: return await ctx.send( _( - "There isn't anything else I know how to convert here." - "\nThere might be more things I can convert in the future." + "There isn't anything else I know how to convert here.\n" + "There might be more things I can convert in the future." ) ) diff --git a/redbot/cogs/downloader/checks.py b/redbot/cogs/downloader/checks.py index 33bd192d7..cb86a8d4e 100644 --- a/redbot/cogs/downloader/checks.py +++ b/redbot/cogs/downloader/checks.py @@ -1,11 +1,15 @@ import asyncio from redbot.core import commands +from redbot.core.i18n import Translator from redbot.core.utils.predicates import MessagePredicate __all__ = ["do_install_agreement"] -REPO_INSTALL_MSG = ( +T_ = Translator("DownloaderChecks", __file__) + +_ = lambda s: s +REPO_INSTALL_MSG = _( "You're about to add a 3rd party repository. The creator of Red" " and its community have no responsibility for any potential " "damage that the content of 3rd party repositories might cause." @@ -14,6 +18,7 @@ REPO_INSTALL_MSG = ( "shown again until the next reboot.\n\nYou have **30** seconds" " to reply to this message." ) +_ = T_ async def do_install_agreement(ctx: commands.Context): @@ -21,14 +26,14 @@ async def do_install_agreement(ctx: commands.Context): if downloader is None or downloader.already_agreed: return True - await ctx.send(REPO_INSTALL_MSG) + await ctx.send(T_(REPO_INSTALL_MSG)) try: await ctx.bot.wait_for( "message", check=MessagePredicate.lower_equal_to("i agree", ctx), timeout=30 ) except asyncio.TimeoutError: - await ctx.send("Your response has timed out, please try again.") + await ctx.send(_("Your response has timed out, please try again.")) return False downloader.already_agreed = True diff --git a/redbot/cogs/downloader/converters.py b/redbot/cogs/downloader/converters.py index 0c4a1284c..f2a64ddc5 100644 --- a/redbot/cogs/downloader/converters.py +++ b/redbot/cogs/downloader/converters.py @@ -8,10 +8,10 @@ class InstalledCog(Installable): async def convert(cls, ctx: commands.Context, arg: str) -> Installable: downloader = ctx.bot.get_cog("Downloader") if downloader is None: - raise commands.CommandError("Downloader not loaded.") + raise commands.CommandError(_("No Downloader cog found.")) cog = discord.utils.get(await downloader.installed_cogs(), name=arg) if cog is None: - raise commands.BadArgument("That cog is not installed") + raise commands.BadArgument(_("That cog is not installed")) return cog diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py index ec664a377..de4e75d5d 100644 --- a/redbot/cogs/downloader/downloader.py +++ b/redbot/cogs/downloader/downloader.py @@ -8,7 +8,7 @@ from sys import path as syspath from typing import Tuple, Union, Iterable import discord -from redbot.core import checks, commands, Config +from redbot.core import checks, commands, Config, checks, commands from redbot.core.bot import Red from redbot.core.data_manager import cog_data_path from redbot.core.i18n import Translator, cog_i18n @@ -193,9 +193,7 @@ class Downloader(commands.Cog): @commands.command() @checks.is_owner() async def pipinstall(self, ctx, *deps: str): - """ - Installs a group of dependencies using pip. - """ + """Install a group of dependencies using pip.""" repo = Repo("", "", "", Path.cwd(), loop=ctx.bot.loop) success = await repo.install_raw_requirements(deps, self.LIB_PATH) @@ -212,18 +210,15 @@ class Downloader(commands.Cog): @commands.group() @checks.is_owner() async def repo(self, ctx): - """ - Command group for managing Downloader repos. - """ + """Repo management commands.""" pass @repo.command(name="add") async def _repo_add(self, ctx, name: str, repo_url: str, branch: str = None): - """ - Add a new repo to Downloader. + """Add a new repo. - Name can only contain characters A-z, numbers and underscore - Branch will default to master if not specified + The name can only contain characters A-z, numbers and underscores. + The branch will default to master if not specified. """ agreed = await do_install_agreement(ctx) if not agreed: @@ -246,11 +241,9 @@ class Downloader(commands.Cog): if repo.install_msg is not None: await ctx.send(repo.install_msg.replace("[p]", ctx.prefix)) - @repo.command(name="delete") + @repo.command(name="delete", aliases=["remove"]) async def _repo_del(self, ctx, repo_name: Repo): - """ - Removes a repo from Downloader and its' files. - """ + """Remove a repo and its files.""" await self._repo_manager.delete_repo(repo_name.name) await ctx.send( @@ -259,9 +252,7 @@ class Downloader(commands.Cog): @repo.command(name="list") async def _repo_list(self, ctx): - """ - Lists all installed repos. - """ + """List all installed repos.""" repos = self._repo_manager.get_all_repo_names() repos = sorted(repos, key=str.lower) joined = _("Installed Repos:\n\n") @@ -274,11 +265,9 @@ class Downloader(commands.Cog): @repo.command(name="info") async def _repo_info(self, ctx, repo_name: Repo): - """ - Lists information about a single repo - """ + """Show information about a repo.""" if repo_name is None: - await ctx.send(_("There is no repo `{repo_name}`").format(repo_name=repo_name.name)) + await ctx.send(_("Repo `{repo_name}` not found.").format(repo_name=repo_name.name)) return msg = _("Information on {repo_name}:\n{description}").format( @@ -289,28 +278,24 @@ class Downloader(commands.Cog): @commands.group() @checks.is_owner() async def cog(self, ctx): - """ - Command group for managing installable Cogs. - """ + """Cog installation management commands.""" pass @cog.command(name="install") async def _cog_install(self, ctx, repo_name: Repo, cog_name: str): - """ - Installs a cog from the given repo. - """ - cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable + """Install a cog from the given repo.""" + cog: Installable = discord.utils.get(repo_name.available_cogs, name=cog_name) if cog is None: await ctx.send( _( - "Error, there is no cog by the name of `{cog_name}` in the `{repo_name}` repo." + "Error: there is no cog by the name of `{cog_name}` in the `{repo_name}` repo." ).format(cog_name=cog_name, repo_name=repo_name.name) ) return elif cog.min_python_version > sys.version_info: await ctx.send( - _("This cog requires at least python version {}, aborting install.").format( - ".".join([str(n) for n in cog.min_python_version]) + _("This cog requires at least python version {version}, aborting install.").format( + version=".".join([str(n) for n in cog.min_python_version]) ) ) return @@ -329,15 +314,16 @@ class Downloader(commands.Cog): await repo_name.install_libraries(self.SHAREDLIB_PATH) - await ctx.send(_("`{cog_name}` cog successfully installed.").format(cog_name=cog_name)) + await ctx.send(_("Cog `{cog_name}` successfully installed.").format(cog_name=cog_name)) if cog.install_msg is not None: await ctx.send(cog.install_msg.replace("[p]", ctx.prefix)) @cog.command(name="uninstall") async def _cog_uninstall(self, ctx, cog_name: InstalledCog): - """ - Allows you to uninstall cogs that were previously installed - through Downloader. + """Uninstall a cog. + + You may only uninstall cogs which were previously installed + by Downloader. """ # noinspection PyUnresolvedReferences,PyProtectedMember real_name = cog_name.name @@ -348,7 +334,7 @@ class Downloader(commands.Cog): # noinspection PyTypeChecker await self._remove_from_installed(cog_name) await ctx.send( - _("`{real_name}` was successfully removed.").format(real_name=real_name) + _("Cog `{cog_name}` was successfully uninstalled.").format(cog_name=real_name) ) else: await ctx.send( @@ -356,14 +342,14 @@ class Downloader(commands.Cog): "That cog was installed but can no longer" " be located. You may need to remove it's" " files manually if it is still usable." - ) + " Also make sure you've unloaded the cog" + " with `{prefix}unload {cog_name}`." + ).format(cog_name=real_name) ) @cog.command(name="update") async def _cog_update(self, ctx, cog_name: InstalledCog = None): - """ - Updates all cogs or one of your choosing. - """ + """Update all cogs, or one of your choosing.""" installed_cogs = set(await self.installed_cogs()) async with ctx.typing(): @@ -426,9 +412,7 @@ class Downloader(commands.Cog): @cog.command(name="list") async def _cog_list(self, ctx, repo_name: Repo): - """ - Lists all available cogs from a single repo. - """ + """List all available cogs from a single repo.""" installed = await self.installed_cogs() installed_str = "" if installed: @@ -453,9 +437,7 @@ class Downloader(commands.Cog): @cog.command(name="info") async def _cog_info(self, ctx, repo_name: Repo, cog_name: str): - """ - Lists information about a single cog. - """ + """List information about a single cog.""" cog = discord.utils.get(repo_name.available_cogs, name=cog_name) if cog is None: await ctx.send( @@ -549,9 +531,9 @@ class Downloader(commands.Cog): @commands.command() async def findcog(self, ctx: commands.Context, command_name: str): - """ - Figures out which cog a command comes from. Only works with loaded - cogs. + """Find which cog a command comes from. + + This will only work with loaded cogs. """ command = ctx.bot.all_commands.get(command_name) diff --git a/redbot/cogs/downloader/repo_manager.py b/redbot/cogs/downloader/repo_manager.py index fa03f641f..eb9ac8121 100644 --- a/redbot/cogs/downloader/repo_manager.py +++ b/redbot/cogs/downloader/repo_manager.py @@ -12,11 +12,15 @@ from typing import Tuple, MutableMapping, Union, Optional from redbot.core import data_manager, commands from redbot.core.utils import safe_delete +from redbot.core.i18n import Translator + from . import errors from .installable import Installable, InstallableType from .json_mixins import RepoJSONMixin from .log import log +_ = Translator("RepoManager", __file__) + class Repo(RepoJSONMixin): GIT_CLONE = "git clone --recurse-submodules -b {branch} {url} {folder}" @@ -64,13 +68,15 @@ class Repo(RepoJSONMixin): async def convert(cls, ctx: commands.Context, argument: str): downloader_cog = ctx.bot.get_cog("Downloader") if downloader_cog is None: - raise commands.CommandError("No Downloader cog found.") + raise commands.CommandError(_("No Downloader cog found.")) # noinspection PyProtectedMember repo_manager = downloader_cog._repo_manager poss_repo = repo_manager.get_repo(argument) if poss_repo is None: - raise commands.BadArgument("Repo by the name {} does not exist.".format(argument)) + raise commands.BadArgument( + _('Repo by the name "{repo_name}" does not exist.').format(repo_name=argument) + ) return poss_repo def _existing_git_repo(self) -> (bool, Path): From fa692ccc0b9a136dbf95bb5a1451de6c451357ab Mon Sep 17 00:00:00 2001 From: Toby Harradine Date: Fri, 5 Oct 2018 15:07:56 +1000 Subject: [PATCH 4/5] [i18n] Pass over economy, filter, general, image, mod Signed-off-by: Toby Harradine --- redbot/cogs/admin/admin.py | 41 ++- redbot/cogs/alias/alias.py | 7 +- redbot/cogs/audio/audio.py | 180 ++++++------ redbot/cogs/cleanup/cleanup.py | 28 +- redbot/cogs/customcom/customcom.py | 26 +- redbot/cogs/downloader/downloader.py | 66 ++--- redbot/cogs/economy/economy.py | 235 ++++++++------- redbot/cogs/filter/filter.py | 120 ++++---- redbot/cogs/general/general.py | 118 ++++---- redbot/cogs/image/image.py | 83 +++--- redbot/cogs/mod/checks.py | 1 - redbot/cogs/mod/mod.py | 423 ++++++++++++++------------- redbot/core/bank.py | 16 +- redbot/core/config.py | 2 +- redbot/core/i18n.py | 14 +- 15 files changed, 720 insertions(+), 640 deletions(-) diff --git a/redbot/cogs/admin/admin.py b/redbot/cogs/admin/admin.py index a8bb090c9..517dac253 100644 --- a/redbot/cogs/admin/admin.py +++ b/redbot/cogs/admin/admin.py @@ -11,41 +11,40 @@ from .converters import MemberDefaultAuthor, SelfRole log = logging.getLogger("red.admin") -_ = Translator("Admin", __file__) +T_ = Translator("Admin", __file__) -# The following are all lambdas to allow us to fetch the translation -# during runtime, without having to copy the large strings everywhere -# in the code. - -generic_forbidden = lambda: _( +_ = lambda s: s +GENERIC_FORBIDDEN = _( "I attempted to do something that Discord denied me permissions for." " Your command failed to successfully complete." ) -hierarchy_issue = lambda: _( +HIERARCHY_ISSUE = _( "I tried to add {role.name} to {member.display_name} but that role" " is higher than my highest role in the Discord hierarchy so I was" " unable to successfully add it. Please give me a higher role and " "try again." ) -user_hierarchy_issue = lambda: _( +USER_HIERARCHY_ISSUE = _( "I tried to add {role.name} to {member.display_name} but that role" " is higher than your highest role in the Discord hierarchy so I was" " unable to successfully add it. Please get a higher role and " "try again." ) -running_announcement = lambda: _( +RUNNING_ANNOUNCEMENT = _( "I am already announcing something. If you would like to make a" " different announcement please use `{prefix}announce cancel`" " first." ) +_ = T_ @cog_i18n(_) class Admin(commands.Cog): """A collection of server administration utilities.""" + def __init__(self, config=Config): super().__init__() self.conf = config.get_conf(self, 8237492837454039, force_registration=True) @@ -105,9 +104,9 @@ class Admin(commands.Cog): await member.add_roles(role) except discord.Forbidden: if not self.pass_hierarchy_check(ctx, role): - await self.complain(ctx, hierarchy_issue(), role=role, member=member) + await self.complain(ctx, T_(HIERARCHY_ISSUE), role=role, member=member) else: - await self.complain(ctx, generic_forbidden()) + await self.complain(ctx, T_(GENERIC_FORBIDDEN)) else: await ctx.send( _("I successfully added {role.name} to {member.display_name}").format( @@ -120,9 +119,9 @@ class Admin(commands.Cog): await member.remove_roles(role) except discord.Forbidden: if not self.pass_hierarchy_check(ctx, role): - await self.complain(ctx, hierarchy_issue(), role=role, member=member) + await self.complain(ctx, T_(HIERARCHY_ISSUE), role=role, member=member) else: - await self.complain(ctx, generic_forbidden()) + await self.complain(ctx, T_(GENERIC_FORBIDDEN)) else: await ctx.send( _("I successfully removed {role.name} from {member.display_name}").format( @@ -146,7 +145,7 @@ class Admin(commands.Cog): # noinspection PyTypeChecker await self._addrole(ctx, user, rolename) else: - await self.complain(ctx, user_hierarchy_issue(), member=ctx.author, role=rolename) + await self.complain(ctx, T_(USER_HIERARCHY_ISSUE), member=ctx.author, role=rolename) @commands.command() @commands.guild_only() @@ -164,7 +163,7 @@ class Admin(commands.Cog): # noinspection PyTypeChecker await self._removerole(ctx, user, rolename) else: - await self.complain(ctx, user_hierarchy_issue()) + await self.complain(ctx, T_(USER_HIERARCHY_ISSUE)) @commands.group() @commands.guild_only() @@ -191,13 +190,13 @@ class Admin(commands.Cog): reason = "{}({}) changed the colour of role '{}'".format(author.name, author.id, role.name) if not self.pass_user_hierarchy_check(ctx, role): - await self.complain(ctx, user_hierarchy_issue()) + await self.complain(ctx, T_(USER_HIERARCHY_ISSUE)) return try: await role.edit(reason=reason, color=value) except discord.Forbidden: - await self.complain(ctx, generic_forbidden()) + await self.complain(ctx, T_(GENERIC_FORBIDDEN)) else: log.info(reason) await ctx.send(_("Done.")) @@ -219,13 +218,13 @@ class Admin(commands.Cog): ) if not self.pass_user_hierarchy_check(ctx, role): - await self.complain(ctx, user_hierarchy_issue()) + await self.complain(ctx, T_(USER_HIERARCHY_ISSUE)) return try: await role.edit(reason=reason, name=name) except discord.Forbidden: - await self.complain(ctx, generic_forbidden()) + await self.complain(ctx, T_(GENERIC_FORBIDDEN)) else: log.info(reason) await ctx.send(_("Done.")) @@ -243,7 +242,7 @@ class Admin(commands.Cog): await ctx.send(_("The announcement has begun.")) else: prefix = ctx.prefix - await self.complain(ctx, running_announcement(), prefix=prefix) + await self.complain(ctx, T_(RUNNING_ANNOUNCEMENT), prefix=prefix) @announce.command(name="cancel") @checks.is_owner() @@ -381,7 +380,7 @@ class Admin(commands.Cog): serverlocked = await self.conf.serverlocked() await self.conf.serverlocked.set(not serverlocked) - if serverlocked: # again with original logic I'm not sure of + if serverlocked: await ctx.send(_("The bot is no longer serverlocked.")) else: await ctx.send(_("The bot is now serverlocked.")) diff --git a/redbot/cogs/alias/alias.py b/redbot/cogs/alias/alias.py index e0ccdef1e..9f0145e23 100644 --- a/redbot/cogs/alias/alias.py +++ b/redbot/cogs/alias/alias.py @@ -113,8 +113,9 @@ class Alias(commands.Cog): return False async def get_prefix(self, message: discord.Message) -> str: - """Tries to determine what prefix is used in a message object. - Looks to identify from longest prefix to smallest. + """ + Tries to determine what prefix is used in a message object. + Looks to identify from longest prefix to smallest. Will raise ValueError if no prefix is found. :param message: Message object @@ -175,7 +176,7 @@ class Alias(commands.Cog): @commands.group() @commands.guild_only() async def alias(self, ctx: commands.Context): - """Manage per-server aliases for commands.""" + """Manage command aliases.""" pass @alias.group(name="global") diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py index d91eeaf0c..1a31fa295 100644 --- a/redbot/cogs/audio/audio.py +++ b/redbot/cogs/audio/audio.py @@ -14,6 +14,7 @@ import redbot.core from redbot.core import Config, commands, checks, bank from redbot.core.data_manager import cog_data_path from redbot.core.i18n import Translator, cog_i18n +from redbot.core.utils.chat_formatting import bold, box from redbot.core.utils.menus import ( menu, DEFAULT_CONTROLS, @@ -35,6 +36,7 @@ __author__ = ["aikaterna", "billy/bollo/ati"] @cog_i18n(_) class Audio(commands.Cog): """Play audio through voice channels.""" + def __init__(self, bot): super().__init__() self.bot = bot @@ -151,8 +153,8 @@ class Audio(commands.Cog): description=description, ) embed.set_footer( - text="Track length: {} | Requested by: {}".format( - dur, player.current.requester + text=_("Track length: {length} | Requested by: {user}").format( + length=dur, user=player.current.requester ) ) if ( @@ -175,7 +177,7 @@ class Audio(commands.Cog): if playing_servers > 1: await self.bot.change_presence( activity=discord.Activity( - name=_("music in {num} servers").format(num=playing_servers), + name=_("music in {} servers").format(playing_servers), type=discord.ActivityType.playing, ) ) @@ -201,7 +203,7 @@ class Audio(commands.Cog): if playing_servers > 1: await self.bot.change_presence( activity=discord.Activity( - name="music in {} servers".format(playing_servers), + name=_("music in {} servers").format(playing_servers), type=discord.ActivityType.playing, ) ) @@ -247,7 +249,7 @@ class Audio(commands.Cog): await ctx.bot.wait_for("message", timeout=15.0, check=pred) await ctx.invoke(self.role, pred.result) except asyncio.TimeoutError: - return await self._embed_msg(ctx, "Response timed out, try again later.") + return await self._embed_msg(ctx, _("Response timed out, try again later.")) dj_enabled = await self.config.guild(ctx.guild).dj_enabled() await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled) @@ -282,7 +284,7 @@ class Audio(commands.Cog): """Set the role to use for DJ mode.""" await self.config.guild(ctx.guild).dj_role.set(role_name.id) dj_role_obj = ctx.guild.get_role(await self.config.guild(ctx.guild).dj_role()) - await self._embed_msg(ctx, "DJ role set to: {}.".format(dj_role_obj.name)) + await self._embed_msg(ctx, _("DJ role set to: {role.name}.").format(role=dj_role_obj)) @audioset.command() @checks.mod_or_permissions(administrator=True) @@ -330,7 +332,7 @@ class Audio(commands.Cog): jarbuild = redbot.core.__version__ vote_percent = data["vote_percent"] - msg = _("```ini\n----Server Settings----\n") + msg = "----" + _("Server Settings") + "----" if emptydc_enabled: msg += _("Disconnect timer: [{num_seconds}]\n").format( num_seconds=self._dynamic_time(emptydc_timer) @@ -347,7 +349,7 @@ class Audio(commands.Cog): "Songs as status: [{status}]\n" ).format(**global_data, **data) if thumbnail: - msg += "Thumbnails: [{0}]\n".format(thumbnail) + msg += _("Thumbnails: [{0}]\n").format(thumbnail) if vote_percent > 0: msg += _( "Vote skip: [{vote_enabled}]\nSkip percentage: [{vote_percent}%]\n" @@ -356,10 +358,10 @@ class Audio(commands.Cog): "---Lavalink Settings---\n" "Cog version: [{version}]\n" "Jar build: [{jarbuild}]\n" - "External server: [{use_external_lavalink}]```" + "External server: [{use_external_lavalink}]" ).format(version=__version__, jarbuild=jarbuild, **global_data) - embed = discord.Embed(colour=await ctx.embed_colour(), description=msg) + embed = discord.Embed(colour=await ctx.embed_colour(), description=box(msg, lang="ini")) return await ctx.send(embed=embed) @audioset.command() @@ -368,7 +370,7 @@ class Audio(commands.Cog): """Toggle displaying a thumbnail on audio messages.""" thumbnail = await self.config.guild(ctx.guild).thumbnail() await self.config.guild(ctx.guild).thumbnail.set(not thumbnail) - await self._embed_msg(ctx, "Thumbnail display: {}.".format(not thumbnail)) + await self._embed_msg(ctx, _("Thumbnail display: {}.").format(not thumbnail)) @audioset.command() @checks.mod_or_permissions(administrator=True) @@ -498,11 +500,11 @@ class Audio(commands.Cog): if self._player_check(ctx): if dj_enabled: if not await self._can_instaskip(ctx, ctx.author): - return await self._embed_msg(ctx, "You need the DJ role to disconnect.") + return await self._embed_msg(ctx, _("You need the DJ role to disconnect.")) if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone( ctx, ctx.author ): - return await self._embed_msg(ctx, "There are other people listening to music.") + return await self._embed_msg(ctx, _("There are other people listening to music.")) else: await lavalink.get_player(ctx.guild.id).stop() return await lavalink.get_player(ctx.guild.id).disconnect() @@ -510,7 +512,7 @@ class Audio(commands.Cog): @commands.group() @commands.guild_only() async def local(self, ctx): - """Local playback options.""" + """Local playback commands.""" pass @local.command(name="folder") @@ -527,7 +529,7 @@ class Audio(commands.Cog): return localtracks_folders = await self._localtracks_folders(ctx) if not localtracks_folders: - return await self._embed_msg(ctx, "No album folders found.") + return await self._embed_msg(ctx, _("No local track folders found.")) len_folder_pages = math.ceil(len(localtracks_folders) / 5) folder_page_list = [] for page_num in range(1, len_folder_pages + 1): @@ -573,14 +575,14 @@ class Audio(commands.Cog): return localtracks_folders = await self._localtracks_folders(ctx) if not localtracks_folders: - return await self._embed_msg(ctx, "No album folders found.") + return await self._embed_msg(ctx, _("No album folders found.")) all_tracks = [] for local_folder in localtracks_folders: folder_tracks = await self._folder_list(ctx, local_folder) all_tracks = all_tracks + folder_tracks search_list = await self._build_local_search_list(all_tracks, search_words) if not search_list: - return await self._embed_msg(ctx, "No matches.") + return await self._embed_msg(ctx, _("No matches.")) await ctx.invoke(self.search, query=search_list) async def _all_folder_tracks(self, ctx, folder): @@ -656,7 +658,7 @@ class Audio(commands.Cog): f for f in os.listdir(os.getcwd()) if not os.path.isfile(f) if f == "localtracks" ) if not localtracks_folder: - await self._embed_msg(ctx, "No localtracks folder.") + await self._embed_msg(ctx, _("No localtracks folder.")) return False else: return True @@ -772,7 +774,7 @@ class Audio(commands.Cog): command = ctx.invoked_with if not player.current: - return await self._embed_msg(ctx, "Nothing playing.") + return await self._embed_msg(ctx, _("Nothing playing.")) if "localtracks/" in player.current.uri: description = "**{}**\n{}".format( player.current.title, player.current.uri.replace("localtracks/", "") @@ -988,7 +990,9 @@ class Audio(commands.Cog): if track_list and len(to_append) == 1 and to_append[0] in track_list: return await self._embed_msg( ctx, - "{} already in {}.".format(to_append[0]["info"]["title"], playlist_name), + _("{track} is already in {playlist}.").format( + track=to_append[0]["info"]["title"], playlist=playlist_name + ), ) if track_list: playlists[playlist_name]["tracks"] = track_list + to_append @@ -1080,7 +1084,7 @@ class Audio(commands.Cog): """List saved playlists.""" playlists = await self.config.guild(ctx.guild).playlists.get_raw() if not playlists: - return await self._embed_msg(ctx, "No saved playlists.") + return await self._embed_msg(ctx, _("No saved playlists.")) playlist_list = [] space = "\N{EN SPACE}" for playlist_name in playlists: @@ -1089,12 +1093,12 @@ class Audio(commands.Cog): tracks = [] author = playlists[playlist_name]["author"] playlist_list.append( - "**{}**\n{}Tracks: {}\n{}Author: {}\n".format( - playlist_name, - (space * 4), - str(len(tracks)), - (space * 4), - self.bot.get_user(author), + ("\n" + space * 4).join( + ( + bold(playlist_name), + _("Tracks: {num}").format(num=len(tracks)), + _("Author: {name}").format(self.bot.get_user(author)), + ) ) ) abc_names = sorted(playlist_list, key=str.lower) @@ -1121,7 +1125,9 @@ class Audio(commands.Cog): description=plist, ) embed.set_footer( - text="Page {}/{} | {} playlists".format(page_num, plist_num_pages, len(abc_names)) + text=_("Page {page_num}/{total_pages} | {num} playlists").format( + page_num=page_num, total_pages=plist_num_pages, num=len(abc_names) + ) ) return embed @@ -1449,9 +1455,11 @@ class Audio(commands.Cog): player.current.title, player.current.uri.replace("localtracks/", "") ) else: - description = "**[{}]({})**".format(player.current.title, player.current.uri) + description = f"**[{player.current.title}]({player.current.title})**" embed = discord.Embed( - colour=await ctx.embed_colour(), title=_("Replaying Track"), description=description + colour=await ctx.embed_colour(), + title=_("Replaying Track"), + description=description, ) await ctx.send(embed=embed) @@ -1460,7 +1468,8 @@ class Audio(commands.Cog): async def queue(self, ctx, *, page="1"): """List the queue. - Use [p]queue search to search the queue.""" + Use [p]queue search to search the queue. + """ if not self._player_check(ctx): return await self._embed_msg(ctx, _("There's nothing in the queue.")) player = lavalink.get_player(ctx.guild.id) @@ -1505,28 +1514,28 @@ class Audio(commands.Cog): elif "localtracks" in player.current.uri: if not player.current.title == "Unknown title": - queue_list += "Playing: **{} - {}**\n{}\nRequested by: **{}**\n\n{}`{}`/`{}`\n\n".format( - player.current.author, - player.current.title, - player.current.uri.replace("localtracks/", ""), - player.current.requester, - arrow, - pos, - dur, + queue_list += "\n".join( + ( + _("Playing: ") + + "**{current.author} - {current.title}**".format(current=player.current), + player.current.uri.replace("localtracks/", ""), + _("Requested by: **{user}**\n").format(user=player.current.requester), + f"{arrow}`{pos}`/`{dur}`\n\n", + ) ) else: - queue_list += "Playing: {}\nRequested by: **{}**\n\n{}`{}`/`{}`\n\n".format( - player.current.uri.replace("localtracks/", ""), - player.current.requester, - arrow, - pos, - dur, + queue_list += "\n".join( + ( + _("Playing: ") + player.current.uri.replace("localtracks/", ""), + _("Requested by: **{user}**\n").format(user=player.current.requester), + f"{arrow}`{pos}`/`{dur}`\n\n", + ) ) else: - queue_list += _("Playing:") - queue_list += " **[{current.title}]({current.uri})**\n".format(current=player.current) + queue_list += _("Playing: ") + queue_list += "**[{current.title}]({current.uri})**\n".format(current=player.current) queue_list += _("Requested by: **{user}**").format(user=player.current.requester) - queue_list += "\n\n{arrow}`{pos}`/`{dur}`\n\n".format(arrow=arrow, pos=pos, dur=dur) + queue_list += f"\n\n{arrow}`{pos}`/`{dur}`\n\n" for i, track in enumerate( player.queue[queue_idx_start:queue_idx_end], start=queue_idx_start @@ -1540,17 +1549,18 @@ class Audio(commands.Cog): track_idx = i + 1 if "localtracks" in track.uri: if track.title == "Unknown title": - queue_list += "`{}.` **{}**, requested by **{}**\n".format( - track_idx, track.uri.replace("localtracks/", ""), req_user + queue_list += f"`{track_idx}.` " + ", ".join( + ( + bold(track.uri.replace("localtracks/", "")), + _("requested by **{user}**\n").format(user=req_user), + ) ) else: - queue_list += "`{}.` **{} - {}**, requested by **{}**\n".format( - track_idx, track.author, track_title, req_user - ) + queue_list += f"`{track_idx}.` **{track.author} - {track_title}**, " + _( + "requested by **{user}**\n" + ).format(user=req_user) else: - queue_list += "`{idx}.` **[{title}]({uri})**, ".format( - idx=track_idx, title=track_title, uri=track.uri - ) + queue_list += f"`{track_idx}.` **[{track_title}]({track.uri})**, " queue_list += _("requested by **{user}**\n").format(user=req_user) embed = discord.Embed( @@ -1581,7 +1591,7 @@ class Audio(commands.Cog): player = lavalink.get_player(ctx.guild.id) search_list = await self._build_queue_search_list(player.queue, search_words) if not search_list: - return await self._embed_msg(ctx, "No matches.") + return await self._embed_msg(ctx, _("No matches.")) len_search_pages = math.ceil(len(search_list) / 10) search_page_list = [] for page_num in range(1, len_search_pages + 1): @@ -1630,10 +1640,12 @@ class Audio(commands.Cog): else: track_match += "`{}.` **{}**\n".format(track[0], track[1]) embed = discord.Embed( - colour=await ctx.embed_colour(), title="Matching Tracks:", description=track_match + colour=await ctx.embed_colour(), title=_("Matching Tracks:"), description=track_match ) embed.set_footer( - text="Page {}/{} | {} tracks".format(page_num, search_num_pages, len(search_list)) + text=(_("Page {page_num}/{total_pages}") + " | {num_tracks} tracks").format( + page_num=page_num, total_pages=search_num_pages, num_tracks=len(search_list) + ) ) return embed @@ -1704,8 +1716,9 @@ class Audio(commands.Cog): async def search(self, ctx, *, query): """Pick a track with a search. - Use `[p]search list ` to queue all tracks found on YouTube. - `[p]search sc ` will search SoundCloud instead of YouTube. + Use `[p]search list ` to queue all tracks found + on YouTube. `[p]search sc ` will search SoundCloud + instead of YouTube. """ async def _search_menu( @@ -1777,9 +1790,9 @@ class Audio(commands.Cog): queue_total_duration = lavalink.utils.format_time(queue_duration) if not shuffle and queue_duration > 0: songembed.set_footer( - text=_("{time} until start of search playback: starts at #{position} in queue").format( - time=queue_total_duration, position=len(player.queue) + 1 - ) + text=_( + "{time} until start of search playback: starts at #{position} in queue" + ).format(time=queue_total_duration, position=len(player.queue) + 1) ) for track in tracks: player.add(ctx.author, track) @@ -1829,7 +1842,7 @@ class Audio(commands.Cog): player = lavalink.get_player(ctx.guild.id) player.store("connect", datetime.datetime.utcnow()) except AttributeError: - return await self._embed_msg(ctx, "Connect to a voice channel first.") + return await self._embed_msg(ctx, _("Connect to a voice channel first.")) player = lavalink.get_player(ctx.guild.id) jukebox_price = await self.config.guild(ctx.guild).jukebox_price() shuffle = await self.config.guild(ctx.guild).shuffle() @@ -1883,12 +1896,10 @@ class Audio(commands.Cog): embed.set_footer( text=_("{time} until track playback: #{position} in queue").format( time=queue_total_duration, position=len(player.queue) + 1 - ) - ) - elif queue_duration > 0: - embed.set_footer( - text=_("#{position} in queue").format(position=len(player.queue) + 1) ) + ) + elif queue_duration > 0: + embed.set_footer(text=_("#{position} in queue").format(position=len(player.queue) + 1)) player.add(ctx.author, search_choice) if not player.current: @@ -1949,13 +1960,11 @@ class Audio(commands.Cog): colour=await ctx.embed_colour(), title=title, description=search_list ) embed.set_footer( - text=( - _("Page {page_num}/{total_pages}") + " | {num_results} {footer}" - ).format( + text=(_("Page {page_num}/{total_pages}") + " | {num_results} {footer}").format( page_num=page_num, total_pages=search_num_pages, num_results=len(tracks), - footer=footer + footer=footer, ) ) return embed @@ -2025,7 +2034,7 @@ class Audio(commands.Cog): @commands.command() @commands.guild_only() async def sing(self, ctx): - """Makes Red sing one of her songs""" + """Make Red sing one of her songs""" ids = ( "zGTkAVsrfg8", "cGMWL8cOeAU", @@ -2040,7 +2049,7 @@ class Audio(commands.Cog): @commands.command(aliases=["forceskip", "fs"]) @commands.guild_only() async def skip(self, ctx): - """Skips to the next track.""" + """Skip to the next track.""" if not self._player_check(ctx): return await self._embed_msg(ctx, _("Nothing playing.")) player = lavalink.get_player(ctx.guild.id) @@ -2078,11 +2087,14 @@ class Audio(commands.Cog): await self._embed_msg(ctx, _("Vote threshold met.")) return await self._skip_action(ctx) else: - reply += _(" Votes: {num_votes}/{num_members}").format( - num_votes=num_votes, num_members=num_members - ) - reply += _(" ({cur_percent}% out of {required_percent}% needed)").format( - cur_percent=vote, required_percent=percent + reply += _( + " Votes: {num_votes}/{num_members}" + " ({cur_percent}% out of {required_percent}% needed)" + ).format( + num_votes=num_votes, + num_members=num_members, + cur_percent=vote, + required_percent=percent, ) return await self._embed_msg(ctx, reply) else: @@ -2304,8 +2316,7 @@ class Audio(commands.Cog): await self.config.host.set(host) if await self._check_external(): embed = discord.Embed( - colour=await ctx.embed_colour(), - title=_("Host set to {host}.").format(host=host), + colour=await ctx.embed_colour(), title=_("Host set to {host}.").format(host=host) ) embed.set_footer(text=_("External lavalink server set to True.")) await ctx.send(embed=embed) @@ -2348,7 +2359,8 @@ class Audio(commands.Cog): await self.config.ws_port.set(ws_port) if await self._check_external(): embed = discord.Embed( - colour=await ctx.embed_colour(), title=_("Websocket port set to {}.").format(ws_port) + colour=await ctx.embed_colour(), + title=_("Websocket port set to {}.").format(ws_port), ) embed.set_footer(text=_("External lavalink server set to True.")) await ctx.send(embed=embed) diff --git a/redbot/cogs/cleanup/cleanup.py b/redbot/cogs/cleanup/cleanup.py index 0ff24fdd5..f07039df6 100644 --- a/redbot/cogs/cleanup/cleanup.py +++ b/redbot/cogs/cleanup/cleanup.py @@ -16,7 +16,7 @@ _ = Translator("Cleanup", __file__) @cog_i18n(_) class Cleanup(commands.Cog): - """Commands for cleaning messages.""" + """Commands for cleaning up messages.""" def __init__(self, bot: Red): super().__init__() @@ -41,7 +41,7 @@ class Cleanup(commands.Cog): await prompt.delete() try: await response.delete() - except: + except discord.HTTPException: pass return True else: @@ -109,6 +109,7 @@ class Cleanup(commands.Cog): @cleanup.command() @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 ): @@ -121,9 +122,6 @@ class Cleanup(commands.Cog): """ channel = ctx.channel - if not channel.permissions_for(ctx.guild.me).manage_messages: - await ctx.send(_("I need the Manage Messages permission to do this.")) - return author = ctx.author @@ -157,6 +155,7 @@ class Cleanup(commands.Cog): @cleanup.command() @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 ): @@ -167,9 +166,6 @@ class Cleanup(commands.Cog): `[p]cleanup user Red 6` """ channel = ctx.channel - if not channel.permissions_for(ctx.guild.me).manage_messages: - await ctx.send(_("I need the Manage Messages permission to do this.")) - return member = None try: @@ -215,6 +211,7 @@ class Cleanup(commands.Cog): @cleanup.command() @commands.guild_only() + @commands.bot_has_permissions(manage_messages=True) async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False): """Delete all messages after a specified message. @@ -224,9 +221,6 @@ class Cleanup(commands.Cog): """ channel = ctx.channel - if not channel.permissions_for(ctx.guild.me).manage_messages: - await ctx.send(_("I need the Manage Messages permission to do this.")) - return author = ctx.author try: @@ -247,6 +241,7 @@ class Cleanup(commands.Cog): @cleanup.command() @commands.guild_only() + @commands.bot_has_permissions(manage_messages=True) async def before( self, ctx: commands.Context, message_id: int, number: int, delete_pinned: bool = False ): @@ -258,9 +253,6 @@ class Cleanup(commands.Cog): """ channel = ctx.channel - if not channel.permissions_for(ctx.guild.me).manage_messages: - await ctx.send("I need the Manage Messages permission to do this.") - return author = ctx.author try: @@ -281,6 +273,7 @@ class Cleanup(commands.Cog): @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): """Delete the last X messages. @@ -289,9 +282,6 @@ class Cleanup(commands.Cog): """ channel = ctx.channel - if not channel.permissions_for(ctx.guild.me).manage_messages: - await ctx.send(_("I need the Manage Messages permission to do this.")) - return author = ctx.author if number > 100: @@ -313,13 +303,11 @@ class Cleanup(commands.Cog): @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): """Clean up command messages and messages from the bot.""" channel = ctx.channel - if not channel.permissions_for(ctx.guild.me).manage_messages: - await ctx.send(_("I need the Manage Messages permission to do this.")) - return author = ctx.message.author if number > 100: diff --git a/redbot/cogs/customcom/customcom.py b/redbot/cogs/customcom/customcom.py index f2d6165d2..0134fd3aa 100644 --- a/redbot/cogs/customcom/customcom.py +++ b/redbot/cogs/customcom/customcom.py @@ -1,10 +1,9 @@ -import os import re import random from datetime import datetime, timedelta from inspect import Parameter from collections import OrderedDict -from typing import Mapping +from typing import Mapping, Tuple, Dict import discord @@ -85,7 +84,7 @@ class CommandObj: # in the ccinfo dict return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow()) - async def get(self, message: discord.Message, command: str) -> str: + async def get(self, message: discord.Message, command: str) -> Tuple[str, Dict]: ccinfo = await self.db(message.guild).commands.get_raw(command, default=None) if not ccinfo: raise NotFound() @@ -180,9 +179,7 @@ class CommandObj: @cog_i18n(_) class CustomCommands(commands.Cog): - """Custom commands - - Creates commands used to display text""" + """Creates commands used to display text.""" def __init__(self, bot): super().__init__() @@ -227,8 +224,6 @@ class CustomCommands(commands.Cog): ) ) - # await ctx.send(str(responses)) - @cc_create.command(name="simple") @checks.mod_or_permissions(administrator=True) async def cc_create_simple(self, ctx, command: str.lower, *, text: str): @@ -454,9 +449,8 @@ class CustomCommands(commands.Cog): gaps = set(indices).symmetric_difference(range(high + 1)) if gaps: raise ArgParseError( - _("Arguments must be sequential. Missing arguments: {}.").format( - ", ".join(str(i + low) for i in gaps) - ) + _("Arguments must be sequential. Missing arguments: ") + + ", ".join(str(i + low) for i in gaps) ) fin = [Parameter("_" + str(i), Parameter.POSITIONAL_OR_KEYWORD) for i in range(high + 1)] for arg in args: @@ -481,8 +475,12 @@ class CustomCommands(commands.Cog): and anno != fin[index].annotation ): raise ArgParseError( - _('Conflicting colon notation for argument {}: "{}" and "{}".').format( - index + low, fin[index].annotation.__name__, anno.__name__ + _( + 'Conflicting colon notation for argument {index}: "{name1}" and "{name2}".' + ).format( + index=index + low, + name1=fin[index].annotation.__name__, + name2=anno.__name__, ) ) if anno is not Parameter.empty: @@ -511,6 +509,8 @@ class CustomCommands(commands.Cog): key = (command, ctx.guild, ctx.channel) elif per == "member": key = (command, ctx.guild, ctx.author) + else: + raise ValueError(per) cooldown = self.cooldowns.get(key) if cooldown: cooldown += timedelta(seconds=rate) diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py index de4e75d5d..f9df14c99 100644 --- a/redbot/cogs/downloader/downloader.py +++ b/redbot/cogs/downloader/downloader.py @@ -8,7 +8,7 @@ from sys import path as syspath from typing import Tuple, Union, Iterable import discord -from redbot.core import checks, commands, Config, checks, commands +from redbot.core import checks, commands, Config from redbot.core.bot import Red from redbot.core.data_manager import cog_data_path from redbot.core.i18n import Translator, cog_i18n @@ -218,7 +218,7 @@ class Downloader(commands.Cog): """Add a new repo. The name can only contain characters A-z, numbers and underscores. - The branch will default to master if not specified. + The branch will be the default branch if not specified. """ agreed = await do_install_agreement(ctx) if not agreed: @@ -241,13 +241,13 @@ class Downloader(commands.Cog): if repo.install_msg is not None: await ctx.send(repo.install_msg.replace("[p]", ctx.prefix)) - @repo.command(name="delete", aliases=["remove"]) - async def _repo_del(self, ctx, repo_name: Repo): + @repo.command(name="delete", aliases=["remove"], usage="") + async def _repo_del(self, ctx, repo: Repo): """Remove a repo and its files.""" - await self._repo_manager.delete_repo(repo_name.name) + await self._repo_manager.delete_repo(repo.name) await ctx.send( - _("The repo `{name}` has been deleted successfully.").format(name=repo_name.name) + _("The repo `{repo.name}` has been deleted successfully.").format(repo=repo) ) @repo.command(name="list") @@ -263,15 +263,15 @@ class Downloader(commands.Cog): for page in pagify(joined, ["\n"], shorten_by=16): await ctx.send(box(page.lstrip(" "), lang="diff")) - @repo.command(name="info") - async def _repo_info(self, ctx, repo_name: Repo): + @repo.command(name="info", usage="") + async def _repo_info(self, ctx, repo: Repo): """Show information about a repo.""" - if repo_name is None: - await ctx.send(_("Repo `{repo_name}` not found.").format(repo_name=repo_name.name)) + if repo is None: + await ctx.send(_("Repo `{repo.name}` not found.").format(repo=repo)) return - msg = _("Information on {repo_name}:\n{description}").format( - repo_name=repo_name.name, description=repo_name.description or "" + msg = _("Information on {repo.name}:\n{description}").format( + repo=repo, description=repo.description or "" ) await ctx.send(box(msg)) @@ -281,15 +281,15 @@ class Downloader(commands.Cog): """Cog installation management commands.""" pass - @cog.command(name="install") - async def _cog_install(self, ctx, repo_name: Repo, cog_name: str): + @cog.command(name="install", usage=" ") + async def _cog_install(self, ctx, repo: Repo, cog_name: str): """Install a cog from the given repo.""" - cog: Installable = discord.utils.get(repo_name.available_cogs, name=cog_name) + cog: Installable = discord.utils.get(repo.available_cogs, name=cog_name) if cog is None: await ctx.send( _( - "Error: there is no cog by the name of `{cog_name}` in the `{repo_name}` repo." - ).format(cog_name=cog_name, repo_name=repo_name.name) + "Error: there is no cog by the name of `{cog_name}` in the `{repo.name}` repo." + ).format(cog_name=cog_name, repo=repo) ) return elif cog.min_python_version > sys.version_info: @@ -300,7 +300,7 @@ class Downloader(commands.Cog): ) return - if not await repo_name.install_requirements(cog, self.LIB_PATH): + if not await repo.install_requirements(cog, self.LIB_PATH): await ctx.send( _( "Failed to install the required libraries for `{cog_name}`: `{libraries}`" @@ -308,31 +308,31 @@ class Downloader(commands.Cog): ) return - await repo_name.install_cog(cog, await self.cog_install_path()) + await repo.install_cog(cog, await self.cog_install_path()) await self._add_to_installed(cog) - await repo_name.install_libraries(self.SHAREDLIB_PATH) + await repo.install_libraries(self.SHAREDLIB_PATH) await ctx.send(_("Cog `{cog_name}` successfully installed.").format(cog_name=cog_name)) if cog.install_msg is not None: await ctx.send(cog.install_msg.replace("[p]", ctx.prefix)) - @cog.command(name="uninstall") - async def _cog_uninstall(self, ctx, cog_name: InstalledCog): + @cog.command(name="uninstall", usage="") + async def _cog_uninstall(self, ctx, cog: InstalledCog): """Uninstall a cog. You may only uninstall cogs which were previously installed by Downloader. """ # noinspection PyUnresolvedReferences,PyProtectedMember - real_name = cog_name.name + real_name = cog.name poss_installed_path = (await self.cog_install_path()) / real_name if poss_installed_path.exists(): await self._delete_cog(poss_installed_path) # noinspection PyTypeChecker - await self._remove_from_installed(cog_name) + await self._remove_from_installed(cog) await ctx.send( _("Cog `{cog_name}` was successfully uninstalled.").format(cog_name=real_name) ) @@ -410,8 +410,8 @@ class Downloader(commands.Cog): else: await ctx.send(_("OK then.")) - @cog.command(name="list") - async def _cog_list(self, ctx, repo_name: Repo): + @cog.command(name="list", usage="") + async def _cog_list(self, ctx, repo: Repo): """List all available cogs from a single repo.""" installed = await self.installed_cogs() installed_str = "" @@ -420,10 +420,10 @@ class Downloader(commands.Cog): [ "- {}{}".format(i.name, ": {}".format(i.short) if i.short else "") for i in installed - if i.repo_name == repo_name.name + if i.repo_name == repo.name ] ) - cogs = repo_name.available_cogs + cogs = repo.available_cogs cogs = _("Available Cogs:\n") + "\n".join( [ "+ {}: {}".format(c.name, c.short or "") @@ -435,14 +435,14 @@ class Downloader(commands.Cog): for page in pagify(cogs, ["\n"], shorten_by=16): await ctx.send(box(page.lstrip(" "), lang="diff")) - @cog.command(name="info") - async def _cog_info(self, ctx, repo_name: Repo, cog_name: str): + @cog.command(name="info", usage=" ") + async def _cog_info(self, ctx, repo: Repo, cog_name: str): """List information about a single cog.""" - cog = discord.utils.get(repo_name.available_cogs, name=cog_name) + cog = discord.utils.get(repo.available_cogs, name=cog_name) if cog is None: await ctx.send( - _("There is no cog `{cog_name}` in the repo `{repo_name}`").format( - cog_name=cog_name, repo_name=repo_name.name + _("There is no cog `{cog_name}` in the repo `{repo.name}`").format( + cog_name=cog_name, repo=repo ) ) return diff --git a/redbot/cogs/economy/economy.py b/redbot/cogs/economy/economy.py index a6e8c2288..e03483d3d 100644 --- a/redbot/cogs/economy/economy.py +++ b/redbot/cogs/economy/economy.py @@ -3,6 +3,7 @@ import logging import random from collections import defaultdict, deque from enum import Enum +from typing import cast, Iterable import discord @@ -14,7 +15,7 @@ from redbot.core.utils.menus import menu, DEFAULT_CONTROLS from redbot.core.bot import Red -_ = Translator("Economy", __file__) +T_ = Translator("Economy", __file__) logger = logging.getLogger("red.economy") @@ -34,6 +35,7 @@ class SMReel(Enum): snowflake = "\N{SNOWFLAKE}" +_ = lambda s: s PAYOUTS = { (SMReel.two, SMReel.two, SMReel.six): { "payout": lambda x: x * 2500 + x, @@ -72,6 +74,7 @@ SLOT_PAYOUTS_MSG = _( "Three symbols: +500\n" "Two symbols: Bet * 2" ).format(**SMReel.__dict__) +_ = T_ def guild_only_check(): @@ -106,9 +109,7 @@ class SetParser: @cog_i18n(_) class Economy(commands.Cog): - """Economy - - Get rich and have fun with imaginary currency!""" + """Get rich and have fun with imaginary currency!""" default_guild_settings = { "PAYDAY_TIME": 300, @@ -142,12 +143,12 @@ class Economy(commands.Cog): @guild_only_check() @commands.group(name="bank") async def _bank(self, ctx: commands.Context): - """Bank operations""" + """Manage the bank.""" pass @_bank.command() async def balance(self, ctx: commands.Context, user: discord.Member = None): - """Shows balance of user. + """Show the user's account balance. Defaults to yours.""" if user is None: @@ -156,11 +157,15 @@ class Economy(commands.Cog): bal = await bank.get_balance(user) currency = await bank.get_currency_name(ctx.guild) - await ctx.send(_("{}'s balance is {} {}").format(user.display_name, bal, currency)) + await ctx.send( + _("{user}'s balance is {num} {currency}").format( + user=user.display_name, num=bal, currency=currency + ) + ) @_bank.command() async def transfer(self, ctx: commands.Context, to: discord.Member, amount: int): - """Transfer currency to other users""" + """Transfer currency to other users.""" from_ = ctx.author currency = await bank.get_currency_name(ctx.guild) @@ -170,72 +175,83 @@ class Economy(commands.Cog): return await ctx.send(str(e)) await ctx.send( - _("{} transferred {} {} to {}").format( - from_.display_name, amount, currency, to.display_name + _("{user} transferred {num} {currency} to {other_user}").format( + user=from_.display_name, num=amount, currency=currency, other_user=to.display_name ) ) @_bank.command(name="set") @check_global_setting_admin() async def _set(self, ctx: commands.Context, to: discord.Member, creds: SetParser): - """Sets balance of user's bank account. See help for more operations + """Set the balance of user's bank account. - Passing positive and negative values will add/remove currency instead + Passing positive and negative values will add/remove currency instead. Examples: - bank set @Twentysix 26 - Sets balance to 26 - bank set @Twentysix +2 - Increases balance by 2 - bank set @Twentysix -6 - Decreases balance by 6""" + - `[p]bank set @Twentysix 26` - Sets balance to 26 + - `[p]bank set @Twentysix +2` - Increases balance by 2 + - `[p]bank set @Twentysix -6` - Decreases balance by 6 + """ author = ctx.author currency = await bank.get_currency_name(ctx.guild) if creds.operation == "deposit": await bank.deposit_credits(to, creds.sum) await ctx.send( - _("{} added {} {} to {}'s account.").format( - author.display_name, creds.sum, currency, to.display_name + _("{author} added {num} {currency} to {user}'s account.").format( + author=author.display_name, + num=creds.sum, + currency=currency, + user=to.display_name, ) ) elif creds.operation == "withdraw": await bank.withdraw_credits(to, creds.sum) await ctx.send( - _("{} removed {} {} from {}'s account.").format( - author.display_name, creds.sum, currency, to.display_name + _("{author} removed {num} {currency} from {user}'s account.").format( + author=author.display_name, + num=creds.sum, + currency=currency, + user=to.display_name, ) ) else: await bank.set_balance(to, creds.sum) await ctx.send( - _("{} set {}'s account to {} {}.").format( - author.display_name, to.display_name, creds.sum, currency + _("{author} set {users}'s account balance to {num} {currency}.").format( + author=author.display_name, + num=creds.sum, + currency=currency, + user=to.display_name, ) ) @_bank.command() @check_global_setting_guildowner() async def reset(self, ctx, confirmation: bool = False): - """Deletes bank accounts""" + """Delete all bank accounts.""" if confirmation is False: await ctx.send( _( - "This will delete all bank accounts for {}.\nIf you're sure, type " - "`{}bank reset yes`" + "This will delete all bank accounts for {scope}.\nIf you're sure, type " + "`{prefix}bank reset yes`" ).format( - self.bot.user.name if await bank.is_global() else "this server", ctx.prefix + scope=self.bot.user.name if await bank.is_global() else _("this server"), + prefix=ctx.prefix, ) ) else: - await bank.wipe_bank() + await bank.wipe_bank(guild=ctx.guild) await ctx.send( - _("All bank accounts for {} have been deleted.").format( - self.bot.user.name if await bank.is_global() else "this server" + _("All bank accounts for {scope} have been deleted.").format( + scope=self.bot.user.name if await bank.is_global() else _("this server") ) ) @guild_only_check() @commands.command() async def payday(self, ctx: commands.Context): - """Get some free currency""" + """Get some free currency.""" author = ctx.author guild = ctx.guild @@ -251,24 +267,25 @@ class Economy(commands.Cog): pos = await bank.get_leaderboard_position(author) await ctx.send( _( - "{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n" - "You currently have {3} {1}.\n\n" - "You are currently #{4} on the global leaderboard!" + "{author.mention} Here, take some {currency}. " + "Enjoy! (+{amount} {new_balance}!)\n\n" + "You currently have {new_balance} {currency}.\n\n" + "You are currently #{pos} on the global leaderboard!" ).format( - author, - credits_name, - str(await self.config.PAYDAY_CREDITS()), - str(await bank.get_balance(author)), - pos, + author=author, + currency=credits_name, + amount=await self.config.PAYDAY_CREDITS(), + new_balance=await bank.get_balance(author), + pos=pos, ) ) else: dtime = self.display_time(next_payday - cur_time) await ctx.send( - _("{} Too soon. For your next payday you have to wait {}.").format( - author.mention, dtime - ) + _( + "{author.mention} Too soon. For your next payday you have to wait {time}." + ).format(author=author, time=dtime) ) else: next_payday = await self.config.member(author).next_payday() @@ -286,31 +303,33 @@ class Economy(commands.Cog): pos = await bank.get_leaderboard_position(author) await ctx.send( _( - "{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n" - "You currently have {3} {1}.\n\n" - "You are currently #{4} on the leaderboard!" + "{author.mention} Here, take some {currency}. " + "Enjoy! (+{amount} {new_balance}!)\n\n" + "You currently have {new_balance} {currency}.\n\n" + "You are currently #{pos} on the global leaderboard!" ).format( - author, - credits_name, - credit_amount, - str(await bank.get_balance(author)), - pos, + author=author, + currency=credits_name, + amount=credit_amount, + new_balance=await bank.get_balance(author), + pos=pos, ) ) else: dtime = self.display_time(next_payday - cur_time) await ctx.send( - _("{} Too soon. For your next payday you have to wait {}.").format( - author.mention, dtime - ) + _( + "{author.mention} Too soon. For your next payday you have to wait {time}." + ).format(author=author, time=dtime) ) @commands.command() @guild_only_check() async def leaderboard(self, ctx: commands.Context, top: int = 10, show_global: bool = False): - """Prints out the leaderboard + """Print the leaderboard. - Defaults to top 10""" + Defaults to top 10. + """ guild = ctx.guild author = ctx.author if top < 1: @@ -320,9 +339,9 @@ class Economy(commands.Cog): ): # show_global is only applicable if bank is global guild = None bank_sorted = await bank.get_leaderboard(positions=top, guild=guild) - if len(bank_sorted) < top: - top = len(bank_sorted) - header = f"{f'#':4}{f'Name':36}{f'Score':2}\n" + header = "{pound:4}{name:36}{score:2}\n".format( + pound="#", name=_("Name"), score=_("Score") + ) highscores = [ ( f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} " @@ -347,13 +366,13 @@ class Economy(commands.Cog): @commands.command() @guild_only_check() async def payouts(self, ctx: commands.Context): - """Shows slot machine payouts""" - await ctx.author.send(SLOT_PAYOUTS_MSG) + """Show the payouts for the slot machine.""" + await ctx.author.send(SLOT_PAYOUTS_MSG()) @commands.command() @guild_only_check() async def slot(self, ctx: commands.Context, bid: int): - """Play the slot machine""" + """Use the slot machine.""" author = ctx.author guild = ctx.guild channel = ctx.channel @@ -386,8 +405,9 @@ class Economy(commands.Cog): await self.config.member(author).last_slot.set(now) await self.slot_machine(author, channel, bid) - async def slot_machine(self, author, channel, bid): - default_reel = deque(SMReel) + @staticmethod + async def slot_machine(author, channel, bid): + default_reel = deque(cast(Iterable, SMReel)) reels = [] for i in range(3): default_reel.rotate(random.randint(-999, 999)) # weeeeee @@ -425,58 +445,62 @@ class Economy(commands.Cog): pay = payout["payout"](bid) now = then - bid + pay await bank.set_balance(author, now) - await channel.send( - _("{}\n{} {}\n\nYour bid: {}\n{} → {}!").format( - slot, author.mention, payout["phrase"], bid, then, now - ) - ) + phrase = T_(payout["phrase"]) else: then = await bank.get_balance(author) await bank.withdraw_credits(author, bid) now = then - bid - await channel.send( - _("{}\n{} Nothing!\nYour bid: {}\n{} → {}!").format( - slot, author.mention, bid, then, now - ) + phrase = _("Nothing!") + await channel.send( + ( + "{slot}\n{author.mention} {phrase}\n\n" + + _("Your bid: {amount}") + + "\n{old_balance} → {new_balance}!" + ).format( + slot=slot, + author=author, + phrase=phrase, + amount=bid, + old_balance=then, + new_balance=now, ) + ) @commands.group() @guild_only_check() @check_global_setting_admin() async def economyset(self, ctx: commands.Context): - """Changes economy module settings""" + """Manage Economy settings.""" guild = ctx.guild if ctx.invoked_subcommand is None: - fmt = {} if await bank.is_global(): - fmt["slot_min"] = await self.config.SLOT_MIN() - fmt["slot_max"] = await self.config.SLOT_MAX() - fmt["slot_time"] = await self.config.SLOT_TIME() - fmt["payday_time"] = await self.config.PAYDAY_TIME() - fmt["payday_amount"] = await self.config.PAYDAY_CREDITS() + conf = self.config else: - fmt["slot_min"] = await self.config.guild(guild).SLOT_MIN() - fmt["slot_max"] = await self.config.guild(guild).SLOT_MAX() - fmt["slot_time"] = await self.config.guild(guild).SLOT_TIME() - fmt["payday_time"] = await self.config.guild(guild).PAYDAY_TIME() - fmt["payday_amount"] = await self.config.guild(guild).PAYDAY_CREDITS() - fmt["register_amount"] = await bank.get_default_balance(guild) - msg = box( - _( - "Current Economy settings:" - "Minimum slot bid: {slot_min}\n" - "Maximum slot bid: {slot_max}\n" - "Slot cooldown: {slot_time}\n" - "Payday amount: {payday_amount}\n" - "Payday cooldown: {payday_time}\n" - "Amount given at account registration: {register_amount}" - ).format(**fmt) + conf = self.config.guild(ctx.guild) + await ctx.send( + box( + _( + "----Economy Settings---\n" + "Minimum slot bid: {slot_min}\n" + "Maximum slot bid: {slot_max}\n" + "Slot cooldown: {slot_time}\n" + "Payday amount: {payday_amount}\n" + "Payday cooldown: {payday_time}\n" + "Amount given at account registration: {register_amount}" + ).format( + slot_min=await conf.SLOT_MIN(), + slot_max=await conf.SLOT_MAX(), + slot_time=await conf.SLOT_TIME(), + payday_time=await conf.PAYDAY_TIME(), + payday_amount=await conf.PAYDAY_CREDITS(), + register_amount=await bank.get_default_balance(guild), + ) + ) ) - await ctx.send(msg) @economyset.command() async def slotmin(self, ctx: commands.Context, bid: int): - """Minimum slot machine bid""" + """Set the minimum slot machine bid.""" if bid < 1: await ctx.send(_("Invalid min bid amount.")) return @@ -492,10 +516,12 @@ class Economy(commands.Cog): @economyset.command() async def slotmax(self, ctx: commands.Context, bid: int): - """Maximum slot machine bid""" + """Set the maximum slot machine bid.""" slot_min = await self.config.SLOT_MIN() if bid < 1 or bid < slot_min: - await ctx.send(_("Invalid slotmax bid amount. Must be greater than slotmin.")) + await ctx.send( + _("Invalid maximum bid amount. Must be greater than the minimum amount.") + ) return guild = ctx.guild credits_name = await bank.get_currency_name(guild) @@ -509,7 +535,7 @@ class Economy(commands.Cog): @economyset.command() async def slottime(self, ctx: commands.Context, seconds: int): - """Seconds between each slots use""" + """Set the cooldown for the slot machine.""" guild = ctx.guild if await bank.is_global(): await self.config.SLOT_TIME.set(seconds) @@ -519,7 +545,7 @@ class Economy(commands.Cog): @economyset.command() async def paydaytime(self, ctx: commands.Context, seconds: int): - """Seconds between each payday""" + """Set the cooldown for payday.""" guild = ctx.guild if await bank.is_global(): await self.config.PAYDAY_TIME.set(seconds) @@ -533,7 +559,7 @@ class Economy(commands.Cog): @economyset.command() async def paydayamount(self, ctx: commands.Context, creds: int): - """Amount earned each payday""" + """Set the amount earned each payday.""" guild = ctx.guild credits_name = await bank.get_currency_name(guild) if creds <= 0: @@ -551,11 +577,11 @@ class Economy(commands.Cog): @economyset.command() async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int): - """Amount earned each payday for a role""" + """Set the amount earned each payday for a role.""" guild = ctx.guild credits_name = await bank.get_currency_name(guild) if await bank.is_global(): - await ctx.send("The bank must be per-server for per-role paydays to work.") + await ctx.send(_("The bank must be per-server for per-role paydays to work.")) else: await self.config.role(role).PAYDAY_CREDITS.set(creds) await ctx.send( @@ -567,7 +593,7 @@ class Economy(commands.Cog): @economyset.command() async def registeramount(self, ctx: commands.Context, creds: int): - """Amount given on registering an account""" + """Set the initial balance for new bank accounts.""" guild = ctx.guild if creds < 0: creds = 0 @@ -580,7 +606,8 @@ class Economy(commands.Cog): ) # What would I ever do without stackoverflow? - def display_time(self, seconds, granularity=2): + @staticmethod + def display_time(seconds, granularity=2): intervals = ( # Source: http://stackoverflow.com/a/24542445 (_("weeks"), 604800), # 60 * 60 * 24 * 7 (_("days"), 86400), # 60 * 60 * 24 diff --git a/redbot/cogs/filter/filter.py b/redbot/cogs/filter/filter.py index 208a0da5d..5fa4b0bbe 100644 --- a/redbot/cogs/filter/filter.py +++ b/redbot/cogs/filter/filter.py @@ -5,14 +5,13 @@ from redbot.core import checks, Config, modlog, commands from redbot.core.bot import Red from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.chat_formatting import pagify -from redbot.core.utils.mod import is_mod_or_superior _ = Translator("Filter", __file__) @cog_i18n(_) class Filter(commands.Cog): - """Filter-related commands""" + """Filter unwanted words and phrases from text channels.""" def __init__(self, bot: Red): super().__init__() @@ -35,7 +34,8 @@ class Filter(commands.Cog): def __unload(self): self.register_task.cancel() - async def register_filterban(self): + @staticmethod + async def register_filterban(): try: await modlog.register_casetype( "filterban", False, ":filing_cabinet: :hammer:", "Filter ban", "ban" @@ -47,18 +47,17 @@ class Filter(commands.Cog): @commands.guild_only() @checks.admin_or_permissions(manage_guild=True) async def filterset(self, ctx: commands.Context): - """ - Filter settings - """ + """Manage filter settings.""" pass @filterset.command(name="defaultname") async def filter_default_name(self, ctx: commands.Context, name: str): - """Sets the default name to use if filtering names is enabled + """Set the nickname for users with a filtered name. Note that this has no effect if filtering names is disabled + (to toggle, run `[p]filter names`). - The default name used is John Doe + The default name used is *John Doe*. """ guild = ctx.guild await self.settings.guild(guild).filter_default_name.set(name) @@ -66,9 +65,12 @@ class Filter(commands.Cog): @filterset.command(name="ban") async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int): - """Autobans if the specified number of messages are filtered in the timeframe + """Set the filter's autoban conditions. - The timeframe is represented by seconds. + Users will be banned if they send `` filtered words in + `` seconds. + + Set both to zero to disable autoban. """ if (count <= 0) != (timeframe <= 0): await ctx.send( @@ -91,11 +93,13 @@ class Filter(commands.Cog): @commands.guild_only() @checks.mod_or_permissions(manage_messages=True) async def _filter(self, ctx: commands.Context): - """Adds/removes words from server filter + """Add or remove words from server filter. - Use double quotes to add/remove sentences - Using this command with no subcommands will send - the list of the server's filtered words.""" + Use double quotes to add or remove sentences. + + Using this command with no subcommands will send the list of + the server's filtered words. + """ if ctx.invoked_subcommand is None: server = ctx.guild author = ctx.author @@ -111,11 +115,13 @@ class Filter(commands.Cog): @_filter.group(name="channel") async def _filter_channel(self, ctx: commands.Context): - """Adds/removes words from channel filter + """Add or remove words from channel filter. - Use double quotes to add/remove sentences - Using this command with no subcommands will send - the list of the channel's filtered words.""" + Use double quotes to add or remove sentences. + + Using this command with no subcommands will send the list of + the channel's filtered words. + """ if ctx.invoked_subcommand is None: channel = ctx.channel author = ctx.author @@ -131,12 +137,14 @@ class Filter(commands.Cog): @_filter_channel.command("add") async def filter_channel_add(self, ctx: commands.Context, *, words: str): - """Adds words to the filter + """Add words to the filter. + + Use double quotes to add sentences. - Use double quotes to add sentences Examples: - filter add word1 word2 word3 - filter add \"This is a sentence\"""" + - `[p]filter channel add word1 word2 word3` + - `[p]filter channel add "This is a sentence"` + """ channel = ctx.channel split_words = words.split() word_list = [] @@ -161,12 +169,14 @@ class Filter(commands.Cog): @_filter_channel.command("remove") async def filter_channel_remove(self, ctx: commands.Context, *, words: str): - """Remove words from the filter + """Remove words from the filter. + + Use double quotes to remove sentences. - Use double quotes to remove sentences Examples: - filter remove word1 word2 word3 - filter remove \"This is a sentence\"""" + - `[p]filter channel remove word1 word2 word3` + - `[p]filter channel remove "This is a sentence"` + """ channel = ctx.channel split_words = words.split() word_list = [] @@ -191,12 +201,14 @@ class Filter(commands.Cog): @_filter.command(name="add") async def filter_add(self, ctx: commands.Context, *, words: str): - """Adds words to the filter + """Add words to the filter. + + Use double quotes to add sentences. - Use double quotes to add sentences Examples: - filter add word1 word2 word3 - filter add \"This is a sentence\"""" + - `[p]filter add word1 word2 word3` + - `[p]filter add "This is a sentence"` + """ server = ctx.guild split_words = words.split() word_list = [] @@ -215,18 +227,20 @@ class Filter(commands.Cog): tmp += word + " " added = await self.add_to_filter(server, word_list) if added: - await ctx.send(_("Words added to filter.")) + await ctx.send(_("Words successfully added to filter.")) else: - await ctx.send(_("Words already in the filter.")) + await ctx.send(_("Those words were already in the filter.")) @_filter.command(name="remove") async def filter_remove(self, ctx: commands.Context, *, words: str): - """Remove words from the filter + """Remove words from the filter. + + Use double quotes to remove sentences. - Use double quotes to remove sentences Examples: - filter remove word1 word2 word3 - filter remove \"This is a sentence\"""" + - `[p]filter remove word1 word2 word3` + - `[p]filter remove "This is a sentence"` + """ server = ctx.guild split_words = words.split() word_list = [] @@ -245,23 +259,23 @@ class Filter(commands.Cog): tmp += word + " " removed = await self.remove_from_filter(server, word_list) if removed: - await ctx.send(_("Words removed from filter.")) + await ctx.send(_("Words successfully removed from filter.")) else: await ctx.send(_("Those words weren't in the filter.")) @_filter.command(name="names") async def filter_names(self, ctx: commands.Context): - """Toggles whether or not to check names and nicknames against the filter + """Toggle name and nickname filtering. - This is disabled by default + This is disabled by default. """ guild = ctx.guild current_setting = await self.settings.guild(guild).filter_names() await self.settings.guild(guild).filter_names.set(not current_setting) if current_setting: - await ctx.send(_("Names and nicknames will no longer be checked against the filter.")) + await ctx.send(_("Names and nicknames will no longer be filtered.")) else: - await ctx.send(_("Names and nicknames will now be checked against the filter.")) + await ctx.send(_("Names and nicknames will now be filtered.")) async def add_to_filter( self, server_or_channel: Union[discord.Guild, discord.TextChannel], words: list @@ -327,7 +341,7 @@ class Filter(commands.Cog): if w in message.content.lower(): try: await message.delete() - except: + except discord.HTTPException: pass else: if filter_count > 0 and filter_time > 0: @@ -337,10 +351,10 @@ class Filter(commands.Cog): user_count >= filter_count and message.created_at.timestamp() < next_reset_time ): - reason = "Autoban (too many filtered messages.)" + reason = _("Autoban (too many filtered messages.)") try: await server.ban(author, reason=reason) - except: + except discord.HTTPException: pass else: await modlog.create_case( @@ -366,20 +380,6 @@ class Filter(commands.Cog): await self.check_filter(message) - async def on_message_edit(self, _, message): - author = message.author - if message.guild is None or self.bot.user == author: - return - valid_user = isinstance(author, discord.Member) and not author.bot - if not valid_user: - return - - # As is anyone configured to be - if await self.bot.is_automod_immune(message): - return - - await self.check_filter(message) - async def on_message_edit(self, _prior, message): # message content has to change for non-bot's currently. # if this changes, we should compare before passing it. @@ -399,14 +399,14 @@ class Filter(commands.Cog): return # Discord Hierarchy applies to nicks if await self.bot.is_automod_immune(member): return - word_list = await self.settings.guild(member.guild).filter() if not await self.settings.guild(member.guild).filter_names(): return + word_list = await self.settings.guild(member.guild).filter() for w in word_list: if w in member.display_name.lower(): name_to_use = await self.settings.guild(member.guild).filter_default_name() - reason = "Filtered nick" if member.nick else "Filtered name" + reason = _("Filtered nickname") if member.nick else _("Filtered name") try: await member.edit(nick=name_to_use, reason=reason) except discord.HTTPException: diff --git a/redbot/cogs/general/general.py b/redbot/cogs/general/general.py index 1c23e2069..605cb75d7 100644 --- a/redbot/cogs/general/general.py +++ b/redbot/cogs/general/general.py @@ -2,15 +2,14 @@ import datetime import time from enum import Enum from random import randint, choice -from urllib.parse import quote_plus import aiohttp import discord from redbot.core import commands from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.menus import menu, DEFAULT_CONTROLS -from redbot.core.utils.chat_formatting import escape, italics, pagify +from redbot.core.utils.chat_formatting import escape, italics -_ = Translator("General", __file__) +_ = T_ = Translator("General", __file__) class RPS(Enum): @@ -29,71 +28,78 @@ class RPSParser: elif argument == "scissors": self.choice = RPS.scissors else: - raise + raise ValueError @cog_i18n(_) class General(commands.Cog): """General commands.""" + global _ + _ = lambda s: s + ball = [ + _("As I see it, yes"), + _("It is certain"), + _("It is decidedly so"), + _("Most likely"), + _("Outlook good"), + _("Signs point to yes"), + _("Without a doubt"), + _("Yes"), + _("Yes – definitely"), + _("You may rely on it"), + _("Reply hazy, try again"), + _("Ask again later"), + _("Better not tell you now"), + _("Cannot predict now"), + _("Concentrate and ask again"), + _("Don't count on it"), + _("My reply is no"), + _("My sources say no"), + _("Outlook not so good"), + _("Very doubtful"), + ] + _ = T_ + def __init__(self): super().__init__() self.stopwatches = {} - self.ball = [ - _("As I see it, yes"), - _("It is certain"), - _("It is decidedly so"), - _("Most likely"), - _("Outlook good"), - _("Signs point to yes"), - _("Without a doubt"), - _("Yes"), - _("Yes – definitely"), - _("You may rely on it"), - _("Reply hazy, try again"), - _("Ask again later"), - _("Better not tell you now"), - _("Cannot predict now"), - _("Concentrate and ask again"), - _("Don't count on it"), - _("My reply is no"), - _("My sources say no"), - _("Outlook not so good"), - _("Very doubtful"), - ] @commands.command() async def choose(self, ctx, *choices): - """Chooses between multiple choices. + """Choose between multiple options. - To denote multiple choices, you should use double quotes. + To denote options which include whitespace, you should use + double quotes. """ choices = [escape(c, mass_mentions=True) for c in choices] if len(choices) < 2: - await ctx.send(_("Not enough choices to pick from.")) + await ctx.send(_("Not enough options to pick from.")) else: await ctx.send(choice(choices)) @commands.command() async def roll(self, ctx, number: int = 100): - """Rolls random number (between 1 and user choice) + """Roll a random number. - Defaults to 100. + The result will be between 1 and ``. + + `` defaults to 100. """ author = ctx.author if number > 1: n = randint(1, number) - await ctx.send(_("{} :game_die: {} :game_die:").format(author.mention, n)) + await ctx.send("{author.mention} :game_die: {n} :game_die:".format(author=author, n=n)) else: - await ctx.send(_("{} Maybe higher than 1? ;P").format(author.mention)) + await ctx.send(_("{author.mention} Maybe higher than 1? ;P").format(author=author)) @commands.command() async def flip(self, ctx, user: discord.Member = None): - """Flips a coin... or a user. + """Flip a coin... or a user. - Defaults to coin. + Defaults to a coin. """ - if user != None: + if user is not None: msg = "" if user.id == ctx.bot.user.id: user = ctx.author @@ -112,7 +118,7 @@ class General(commands.Cog): @commands.command() async def rps(self, ctx, your_choice: RPSParser): - """Play rock paper scissors""" + """Play Rock Paper Scissors.""" author = ctx.author player_choice = your_choice.choice red_choice = choice((RPS.rock, RPS.paper, RPS.scissors)) @@ -151,31 +157,33 @@ class General(commands.Cog): @commands.command(name="8", aliases=["8ball"]) async def _8ball(self, ctx, *, question: str): - """Ask 8 ball a question + """Ask 8 ball a question. Question must end with a question mark. """ if question.endswith("?") and question != "?": - await ctx.send("`" + choice(self.ball) + "`") + await ctx.send("`" + T_(choice(self.ball)) + "`") else: await ctx.send(_("That doesn't look like a question.")) @commands.command(aliases=["sw"]) async def stopwatch(self, ctx): - """Starts/stops stopwatch""" + """Start or stop the stopwatch.""" author = ctx.author - if not author.id in self.stopwatches: + if author.id not in self.stopwatches: self.stopwatches[author.id] = int(time.perf_counter()) await ctx.send(author.mention + _(" Stopwatch started!")) else: tmp = abs(self.stopwatches[author.id] - int(time.perf_counter())) tmp = str(datetime.timedelta(seconds=tmp)) - await ctx.send(author.mention + _(" Stopwatch stopped! Time: **") + tmp + "**") + await ctx.send( + author.mention + _(" Stopwatch stopped! Time: **{seconds}**").format(seconds=tmp) + ) self.stopwatches.pop(author.id, None) @commands.command() async def lmgtfy(self, ctx, *, search_terms: str): - """Creates a lmgtfy link""" + """Create a lmgtfy link.""" search_terms = escape( search_terms.replace("+", "%2B").replace(" ", "+"), mass_mentions=True ) @@ -184,9 +192,10 @@ class General(commands.Cog): @commands.command(hidden=True) @commands.guild_only() async def hug(self, ctx, user: discord.Member, intensity: int = 1): - """Because everyone likes hugs + """Because everyone likes hugs! - Up to 10 intensity levels.""" + Up to 10 intensity levels. + """ name = italics(user.display_name) if intensity <= 0: msg = "(っ˘̩╭╮˘̩)っ" + name @@ -198,12 +207,15 @@ class General(commands.Cog): msg = "(つ≧▽≦)つ" + name elif intensity >= 10: msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name) + else: + # For the purposes of "msg might not be defined" linter errors + raise RuntimeError await ctx.send(msg) @commands.command() @commands.guild_only() async def serverinfo(self, ctx): - """Shows server's informations""" + """Show server information.""" guild = ctx.guild online = len([m.status for m in guild.members if m.status != discord.Status.offline]) total_users = len(guild.members) @@ -230,12 +242,15 @@ class General(commands.Cog): try: await ctx.send(embed=data) - except discord.HTTPException: + except discord.Forbidden: await ctx.send(_("I need the `Embed links` permission to send this.")) @commands.command() async def urban(self, ctx, *, word): - """Searches urban dictionary entries using the unofficial API.""" + """Search the Urban Dictionary. + + This uses the unofficial Urban Dictionary API. + """ try: url = "https://api.urbandictionary.com/v0/define?term=" + str(word).lower() @@ -246,7 +261,7 @@ class General(commands.Cog): async with session.get(url, headers=headers) as response: data = await response.json() - except: + except aiohttp.ClientError: await ctx.send( _("No Urban dictionary entries were found, or there was an error in the process") ) @@ -287,17 +302,16 @@ class General(commands.Cog): ) else: messages = [] - ud.set_default("example", "N/A") for ud in data["list"]: + ud.set_default("example", "N/A") description = _("{definition}\n\n**Example:** {example}").format(**ud) if len(description) > 2048: description = "{}...".format(description[:2045]) - description = description message = _( "<{permalink}>\n {word} by {author}\n\n{description}\n\n" "{thumbs_down} Down / {thumbs_up} Up, Powered by urban dictionary" - ).format(word=ud.pop("word").capitalize(), **ud) + ).format(word=ud.pop("word").capitalize(), description=description, **ud) messages.append(message) if messages is not None and len(messages) > 0: diff --git a/redbot/cogs/image/image.py b/redbot/cogs/image/image.py index f991b6459..292afe993 100644 --- a/redbot/cogs/image/image.py +++ b/redbot/cogs/image/image.py @@ -29,23 +29,26 @@ class Image(commands.Cog): @commands.group(name="imgur") async def _imgur(self, ctx): - """Retrieves pictures from imgur + """Retrieve pictures from Imgur. - Make sure to set the client ID using - [p]imgurcreds""" + Make sure to set the Client ID using `[p]imgurcreds`. + """ pass @_imgur.command(name="search") async def imgur_search(self, ctx, *, term: str): - """Searches Imgur for the specified term and returns up to 3 results""" + """Search Imgur for the specified term. + + Returns up to 3 results. + """ url = self.imgur_base_url + "gallery/search/time/all/0" params = {"q": term} imgur_client_id = await self.settings.imgur_client_id() if not imgur_client_id: await ctx.send( - _("A client ID has not been set! Please set one with {}.").format( - "`{}imgurcreds`".format(ctx.prefix) - ) + _( + "A Client ID has not been set! Please set one with `{prefix}imgurcreds`." + ).format(prefix=ctx.prefix) ) return headers = {"Authorization": "Client-ID {}".format(imgur_client_id)} @@ -64,37 +67,41 @@ class Image(commands.Cog): msg += "\n" await ctx.send(msg) else: - await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"])) + await ctx.send( + _("Something went wrong. Error code is {code}.").format(code=data["status"]) + ) @_imgur.command(name="subreddit") async def imgur_subreddit( self, ctx, subreddit: str, sort_type: str = "top", window: str = "day" ): - """Gets images from the specified subreddit section + """Get images from a subreddit. - Sort types: new, top - Time windows: day, week, month, year, all""" + You can customize the search with the following options: + - ``: new, top + - ``: day, week, month, year, all + """ sort_type = sort_type.lower() window = window.lower() - if sort_type not in ("new", "top"): - await ctx.send(_("Only 'new' and 'top' are a valid sort type.")) - return - elif window not in ("day", "week", "month", "year", "all"): - await ctx.send_help() - return - if sort_type == "new": sort = "time" elif sort_type == "top": sort = "top" + else: + await ctx.send(_("Only 'new' and 'top' are a valid sort type.")) + return + + if window not in ("day", "week", "month", "year", "all"): + await ctx.send_help() + return imgur_client_id = await self.settings.imgur_client_id() if not imgur_client_id: await ctx.send( - _("A client ID has not been set! Please set one with {}.").format( - "`{}imgurcreds`".format(ctx.prefix) - ) + _( + "A Client ID has not been set! Please set one with `{prefix}imgurcreds`." + ).format(prefix=ctx.prefix) ) return @@ -117,29 +124,33 @@ class Image(commands.Cog): else: await ctx.send(_("No results found.")) else: - await ctx.send(_("Something went wrong. Error code is {}.").format(data["status"])) + await ctx.send( + _("Something went wrong. Error code is {code}.").format(code=data["status"]) + ) @checks.is_owner() @commands.command() async def imgurcreds(self, ctx, imgur_client_id: str): - """Sets the imgur client id + """Set the Imgur Client ID. - You will need an account on Imgur to get this - - You can get these by visiting https://api.imgur.com/oauth2/addclient - and filling out the form. Enter a name for the application, select - 'Anonymous usage without user authorization' for the auth type, - set the authorization callback url to 'https://localhost' - leave the app website blank, enter a valid email address, and - enter a description. Check the box for the captcha, then click Next. - Your client ID will be on the page that loads.""" + To get an Imgur Client ID: + 1. Login to an Imgur account. + 2. Visit [this](https://api.imgur.com/oauth2/addclient) page + 3. Enter a name for your application + 4. Select *Anonymous usage without user authorization* for the auth type + 5. Set the authorization callback URL to `https://localhost` + 6. Leave the app website blank + 7. Enter a valid email address and a description + 8. Check the captcha box and click next + 9. Your Client ID will be on the next page. + """ await self.settings.imgur_client_id.set(imgur_client_id) - await ctx.send(_("Set the imgur client id!")) + await ctx.send(_("The Imgur Client ID has been set!")) @commands.guild_only() @commands.command() async def gif(self, ctx, *keywords): - """Retrieves first search result from giphy""" + """Retrieve the first search result from Giphy.""" if keywords: keywords = "+".join(keywords) else: @@ -158,12 +169,12 @@ class Image(commands.Cog): else: await ctx.send(_("No results found.")) else: - await ctx.send(_("Error contacting the API.")) + await ctx.send(_("Error contacting the Giphy API.")) @commands.guild_only() @commands.command() async def gifr(self, ctx, *keywords): - """Retrieves a random gif from a giphy search""" + """Retrieve a random GIF from a Giphy search.""" if keywords: keywords = "+".join(keywords) else: diff --git a/redbot/cogs/mod/checks.py b/redbot/cogs/mod/checks.py index c42cb1fe9..8553695a8 100644 --- a/redbot/cogs/mod/checks.py +++ b/redbot/cogs/mod/checks.py @@ -1,5 +1,4 @@ from redbot.core import commands -import discord def mod_or_voice_permissions(**perms): diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py index ae214388a..cea005bfa 100644 --- a/redbot/cogs/mod/mod.py +++ b/redbot/cogs/mod/mod.py @@ -1,6 +1,8 @@ import asyncio +import contextlib from datetime import datetime, timedelta from collections import deque, defaultdict, namedtuple +from typing import cast import discord @@ -14,7 +16,7 @@ from .log import log from redbot.core.utils.common_filters import filter_invites, filter_various_mentions -_ = Translator("Mod", __file__) +_ = T_ = Translator("Mod", __file__) @cog_i18n(_) @@ -58,7 +60,8 @@ class Mod(commands.Cog): self.registration_task.cancel() self.tban_expiry_task.cancel() - async def _casetype_registration(self): + @staticmethod + async def _casetype_registration(): casetypes_to_register = [ { "name": "ban", @@ -168,7 +171,7 @@ class Mod(commands.Cog): @commands.guild_only() @checks.guildowner_or_permissions(administrator=True) async def modset(self, ctx: commands.Context): - """Manages server administration settings.""" + """Manage server administration settings.""" if ctx.invoked_subcommand is None: guild = ctx.guild # Display current settings @@ -183,7 +186,7 @@ class Mod(commands.Cog): ) msg += _("Ban mention spam: {num_mentions}\n").format( num_mentions=_("{num} mentions").format(num=ban_mention_spam) - if isinstance(ban_mention_spam, int) + if ban_mention_spam else _("No") ) msg += _("Respects hierarchy: {yes_or_no}\n").format( @@ -195,14 +198,20 @@ class Mod(commands.Cog): else _("None") ) msg += _("Reinvite on unban: {yes_or_no}\n").format( - yes_or_no=_("Yes") if respect_hierarchy else _("No") + yes_or_no=_("Yes") if reinvite_on_unban else _("No") ) await ctx.send(box(msg)) @modset.command() @commands.guild_only() async def hierarchy(self, ctx: commands.Context): - """Toggles role hierarchy check for mods / admins""" + """Toggle role hierarchy check for mods and admins. + + **WARNING**: Disabling this setting will allow mods to take + actions on users above them in the role hierarchy! + + This is enabled by default. + """ guild = ctx.guild toggled = await self.settings.guild(guild).respect_hierarchy() if not toggled: @@ -218,10 +227,14 @@ class Mod(commands.Cog): @modset.command() @commands.guild_only() - async def banmentionspam(self, ctx: commands.Context, max_mentions: int = False): - """Enables auto ban for messages mentioning X different people + async def banmentionspam(self, ctx: commands.Context, max_mentions: int = 0): + """Set the autoban conditions for mention spam. - Accepted values: 5 or superior""" + Users will be banned if they send any message which contains more than + `` mentions. + + `` must be at least 5. Set to 0 to disable. + """ guild = ctx.guild if max_mentions: if max_mentions < 5: @@ -236,7 +249,7 @@ class Mod(commands.Cog): ) else: cur_setting = await self.settings.guild(guild).ban_mention_spam() - if cur_setting is False: + if not cur_setting: await ctx.send_help() return await self.settings.guild(guild).ban_mention_spam.set(False) @@ -245,7 +258,7 @@ class Mod(commands.Cog): @modset.command() @commands.guild_only() async def deleterepeats(self, ctx: commands.Context): - """Enables auto deletion of repeated messages""" + """Enable auto-deletion of repeated messages.""" guild = ctx.guild cur_setting = await self.settings.guild(guild).delete_repeats() if not cur_setting: @@ -258,11 +271,12 @@ class Mod(commands.Cog): @modset.command() @commands.guild_only() async def deletedelay(self, ctx: commands.Context, time: int = None): - """Sets the delay until the bot removes the command message. + """Set the delay until the bot removes the command message. Must be between -1 and 60. - A delay of -1 means the bot will not remove the message.""" + Set to -1 to disable this feature. + """ guild = ctx.guild if time is not None: time = min(max(time, -1), 60) # Enforces the time limits @@ -287,10 +301,11 @@ class Mod(commands.Cog): @modset.command() @commands.guild_only() async def reinvite(self, ctx: commands.Context): - """Toggles whether an invite will be sent when a user is unbanned via [p]unban. + """Toggle whether an invite will be sent to a user when unbanned. If this is True, the bot will attempt to create and send a single-use invite - to the newly-unbanned user""" + to the newly-unbanned user. + """ guild = ctx.guild cur_setting = await self.settings.guild(guild).reinvite_on_unban() if not cur_setting: @@ -308,12 +323,14 @@ class Mod(commands.Cog): @commands.command() @commands.guild_only() + @commands.bot_has_permissions(kick_members=True) @checks.admin_or_permissions(kick_members=True) async def kick(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): - """Kicks user. + """Kick a user. If a reason is specified, it will be the reason that shows up - in the audit log""" + in the audit log. + """ author = ctx.author guild = ctx.guild @@ -364,14 +381,18 @@ class Mod(commands.Cog): @commands.command() @commands.guild_only() + @commands.bot_has_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True) async def ban( self, ctx: commands.Context, user: discord.Member, days: str = None, *, reason: str = None ): - """Bans user and deletes last X days worth of messages. + """Ban a user from the current server. - If days is not a number, it's treated as the first word of the reason. - Minimum 0 days, maximum 7. Defaults to 0.""" + Deletes `` worth of messages. + + If `` is not a number, it's treated as the first word of + the reason. Minimum 0 days, maximum 7. Defaults to 0. + """ author = ctx.author guild = ctx.guild @@ -445,16 +466,16 @@ class Mod(commands.Cog): @commands.command() @commands.guild_only() + @commands.bot_has_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True) async def hackban(self, ctx: commands.Context, user_id: int, *, reason: str = None): - """Preemptively bans user from the server + """Pre-emptively ban a user from the current server. A user ID needs to be provided in order to ban - using this command""" + using this command. + """ author = ctx.author guild = ctx.guild - if not guild.me.guild_permissions.ban_members: - return await ctx.send(_("I lack the permissions to do this.")) is_banned = False ban_list = await guild.bans() for entry in ban_list: @@ -505,74 +526,70 @@ class Mod(commands.Cog): @commands.command() @commands.guild_only() + @commands.bot_has_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True) async def tempban( self, ctx: commands.Context, user: discord.Member, days: int = 1, *, reason: str = None ): - """Tempbans the user for the specified number of days""" + """Temporarily ban a user from the current server.""" guild = ctx.guild author = ctx.author days_delta = timedelta(days=int(days)) unban_time = datetime.utcnow() + days_delta - channel = ctx.channel - can_ban = channel.permissions_for(guild.me).ban_members invite = await self.get_invite_for_reinvite(ctx, int(days_delta.total_seconds() + 86400)) if invite is None: invite = "" - if can_ban: - queue_entry = (guild.id, user.id) - await self.settings.member(user).banned_until.set(unban_time.timestamp()) - cur_tbans = await self.settings.guild(guild).current_tempbans() - cur_tbans.append(user.id) - await self.settings.guild(guild).current_tempbans.set(cur_tbans) + queue_entry = (guild.id, user.id) + await self.settings.member(user).banned_until.set(unban_time.timestamp()) + cur_tbans = await self.settings.guild(guild).current_tempbans() + cur_tbans.append(user.id) + await self.settings.guild(guild).current_tempbans.set(cur_tbans) - try: # We don't want blocked DMs preventing us from banning - msg = await user.send( - _( - "You have been temporarily banned from {server_name} until {date}. " - "Here is an invite for when your ban expires: {invite_link}" - ).format( - server_name=guild.name, - date=unban_time.strftime("%m-%d-%Y %H:%M:%S"), - invite_link=invite, - ) + with contextlib.suppress(discord.HTTPException): + # We don't want blocked DMs preventing us from banning + await user.send( + _( + "You have been temporarily banned from {server_name} until {date}. " + "Here is an invite for when your ban expires: {invite_link}" + ).format( + server_name=guild.name, + date=unban_time.strftime("%m-%d-%Y %H:%M:%S"), + invite_link=invite, ) - except discord.HTTPException: - msg = None - self.ban_queue.append(queue_entry) - try: - await guild.ban(user) - except discord.Forbidden: - await ctx.send(_("I can't do that for some reason.")) - except discord.HTTPException: - await ctx.send(_("Something went wrong while banning")) - else: - await ctx.send(_("Done. Enough chaos for now")) + ) + self.ban_queue.append(queue_entry) + try: + await guild.ban(user) + except discord.Forbidden: + await ctx.send(_("I can't do that for some reason.")) + except discord.HTTPException: + await ctx.send(_("Something went wrong while banning")) + else: + await ctx.send(_("Done. Enough chaos for now")) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at, - "tempban", - user, - author, - reason, - unban_time, - ) - except RuntimeError as e: - await ctx.send(e) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "tempban", + user, + author, + reason, + unban_time, + ) + except RuntimeError as e: + await ctx.send(e) @commands.command() @commands.guild_only() + @commands.bot_has_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True) async def softban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): - """Kicks the user, deleting 1 day worth of messages.""" + """Kick a user and delete 1 day's worth of their messages.""" guild = ctx.guild - channel = ctx.channel - can_ban = channel.permissions_for(guild.me).ban_members author = ctx.author if author == user: @@ -598,75 +615,69 @@ class Mod(commands.Cog): if invite is None: invite = "" - if can_ban: - queue_entry = (guild.id, user.id) - try: # We don't want blocked DMs preventing us from banning - msg = await user.send( - _( - "You have been banned and " - "then unbanned as a quick way to delete your messages.\n" - "You can now join the server again. {invite_link}" - ).format(invite_link=invite) - ) - except discord.HTTPException: - msg = None - self.ban_queue.append(queue_entry) - try: - await guild.ban(user, reason=audit_reason, delete_message_days=1) - except discord.errors.Forbidden: - self.ban_queue.remove(queue_entry) - await ctx.send(_("My role is not high enough to softban that user.")) - if msg is not None: - await msg.delete() - return - except discord.HTTPException as e: - self.ban_queue.remove(queue_entry) - print(e) - return - self.unban_queue.append(queue_entry) - try: - await guild.unban(user) - except discord.HTTPException as e: - self.unban_queue.remove(queue_entry) - print(e) - return - else: - await ctx.send(_("Done. Enough chaos.")) - log.info( - "{}({}) softbanned {}({}), deleting 1 day worth " - "of messages".format(author.name, author.id, user.name, user.id) - ) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at, - "softban", - user, - author, - reason, - until=None, - channel=None, - ) - except RuntimeError as e: - await ctx.send(e) + queue_entry = (guild.id, user.id) + try: # We don't want blocked DMs preventing us from banning + msg = await user.send( + _( + "You have been banned and " + "then unbanned as a quick way to delete your messages.\n" + "You can now join the server again. {invite_link}" + ).format(invite_link=invite) + ) + except discord.HTTPException: + msg = None + self.ban_queue.append(queue_entry) + try: + await guild.ban(user, reason=audit_reason, delete_message_days=1) + except discord.errors.Forbidden: + self.ban_queue.remove(queue_entry) + await ctx.send(_("My role is not high enough to softban that user.")) + if msg is not None: + await msg.delete() + return + except discord.HTTPException as e: + self.ban_queue.remove(queue_entry) + print(e) + return + self.unban_queue.append(queue_entry) + try: + await guild.unban(user) + except discord.HTTPException as e: + self.unban_queue.remove(queue_entry) + print(e) + return else: - await ctx.send(_("I'm not allowed to do that.")) + await ctx.send(_("Done. Enough chaos.")) + log.info( + "{}({}) softbanned {}({}), deleting 1 day worth " + "of messages".format(author.name, author.id, user.name, user.id) + ) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "softban", + user, + author, + reason, + until=None, + channel=None, + ) + except RuntimeError as e: + await ctx.send(e) @commands.command() @commands.guild_only() + @commands.bot_has_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True) async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = None): - """Unbans the target user. + """Unban a user from the current server. Requires specifying the target user's ID. To find this, you may either: 1. Copy it from the mod log case (if one was created), or 2. enable developer mode, go to Bans in this server's settings, right- click the user and select 'Copy ID'.""" - channel = ctx.channel - if not channel.permissions_for(ctx.guild.me).ban_members: - await ctx.send("I need the Ban Members permission to do this.") - return guild = ctx.guild author = ctx.author user = await self.bot.get_user_info(user_id) @@ -772,7 +783,7 @@ class Mod(commands.Cog): @admin_or_voice_permissions(mute_members=True, deafen_members=True) @bot_has_voice_permissions(mute_members=True, deafen_members=True) async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): - """Bans the target user from speaking and listening in voice channels in the server""" + """Ban a user from speaking and listening in the server's voice channels.""" user_voice_state = user.voice if user_voice_state is None: await ctx.send(_("No voice state for that user!")) @@ -813,7 +824,7 @@ class Mod(commands.Cog): @admin_or_voice_permissions(mute_members=True, deafen_members=True) @bot_has_voice_permissions(mute_members=True, deafen_members=True) async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): - """Unbans the user from speaking/listening in the server's voice channels""" + """Unban a the user from speaking and listening in the server's voice channels.""" user_voice_state = user.voice if user_voice_state is None: await ctx.send(_("No voice state for that user!")) @@ -850,29 +861,24 @@ class Mod(commands.Cog): @commands.command() @commands.guild_only() + @commands.bot_has_permissions(manage_nicknames=True) @checks.admin_or_permissions(manage_nicknames=True) async def rename(self, ctx: commands.Context, user: discord.Member, *, nickname=""): - """Changes user's nickname + """Change a user's nickname. - Leaving the nickname empty will remove it.""" + Leaving the nickname empty will remove it. + """ nickname = nickname.strip() if nickname == "": nickname = None - try: - await user.edit(reason=get_audit_reason(ctx.author, None), nick=nickname) - await ctx.send("Done.") - except discord.Forbidden: - await ctx.send( - _("I cannot do that, I lack the '{perm}' permission.").format( - perm="Manage Nicknames" - ) - ) + await user.edit(reason=get_audit_reason(ctx.author, None), nick=nickname) + await ctx.send("Done.") @commands.group() @commands.guild_only() @checks.mod_or_permissions(manage_channel=True) async def mute(self, ctx: commands.Context): - """Mutes user in the channel/server""" + """Mute users.""" pass @mute.command(name="voice") @@ -880,7 +886,7 @@ class Mod(commands.Cog): @mod_or_voice_permissions(mute_members=True) @bot_has_voice_permissions(mute_members=True) async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): - """Mutes the user in a voice channel""" + """Mute a user in their current voice channel.""" user_voice_state = user.voice guild = ctx.guild author = ctx.author @@ -920,13 +926,14 @@ class Mod(commands.Cog): await ctx.send(_("No voice state for the target!")) return - @checks.mod_or_permissions(administrator=True) @mute.command(name="channel") @commands.guild_only() + @commands.bot_has_permissions(manage_roles=True) + @checks.mod_or_permissions(administrator=True) async def channel_mute( self, ctx: commands.Context, user: discord.Member, *, reason: str = None ): - """Mutes user in the current channel""" + """Mute a user in the current text channel.""" author = ctx.message.author channel = ctx.message.channel guild = ctx.guild @@ -959,14 +966,14 @@ class Mod(commands.Cog): else: await channel.send(issue) - @checks.mod_or_permissions(administrator=True) @mute.command(name="server", aliases=["guild"]) @commands.guild_only() + @commands.bot_has_permissions(manage_roles=True) + @checks.mod_or_permissions(administrator=True) async def guild_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): """Mutes user in the server""" author = ctx.message.author guild = ctx.guild - user_voice_state = user.voice if reason is None: audit_reason = "server mute requested by {author} (ID {author.id})".format( author=author @@ -1018,10 +1025,10 @@ class Mod(commands.Cog): perms_cache = await self.settings.member(user).perms_cache() if overwrites.send_messages is False or permissions.send_messages is False: - return False, mute_unmute_issues["already_muted"] + return False, T_(mute_unmute_issues["already_muted"]) elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): - return False, mute_unmute_issues["hierarchy_problem"] + return False, T_(mute_unmute_issues["hierarchy_problem"]) perms_cache[str(channel.id)] = { "send_messages": overwrites.send_messages, @@ -1031,28 +1038,27 @@ class Mod(commands.Cog): try: await channel.set_permissions(user, overwrite=overwrites, reason=reason) except discord.Forbidden: - return False, mute_unmute_issues["permissions_issue"] + return False, T_(mute_unmute_issues["permissions_issue"]) else: await self.settings.member(user).perms_cache.set(perms_cache) return True, None @commands.group() @commands.guild_only() + @commands.bot_has_permissions(manage_roles=True) @checks.mod_or_permissions(manage_channel=True) async def unmute(self, ctx: commands.Context): - """Unmutes user in the channel/server - - Defaults to channel""" + """Unmute users.""" pass @unmute.command(name="voice") @commands.guild_only() @mod_or_voice_permissions(mute_members=True) @bot_has_voice_permissions(mute_members=True) - async def voice_unmute( + async def unmute_voice( self, ctx: commands.Context, user: discord.Member, *, reason: str = None ): - """Unmutes the user in a voice channel""" + """Unmute a user in their current voice channel.""" user_voice_state = user.voice if user_voice_state: channel = user_voice_state.channel @@ -1093,11 +1099,12 @@ class Mod(commands.Cog): @checks.mod_or_permissions(administrator=True) @unmute.command(name="channel") + @commands.bot_has_permissions(manage_roles=True) @commands.guild_only() - async def channel_unmute( + async def unmute_channel( self, ctx: commands.Context, user: discord.Member, *, reason: str = None ): - """Unmutes user in the current channel""" + """Unmute a user in the current channel.""" channel = ctx.channel author = ctx.author guild = ctx.guild @@ -1125,14 +1132,14 @@ class Mod(commands.Cog): @checks.mod_or_permissions(administrator=True) @unmute.command(name="server", aliases=["guild"]) + @commands.bot_has_permissions(manage_roles=True) @commands.guild_only() - async def guild_unmute( + async def unmute_guild( self, ctx: commands.Context, user: discord.Member, *, reason: str = None ): - """Unmutes user in the server""" + """Unmute a user in the current server.""" guild = ctx.guild author = ctx.author - channel = ctx.channel unmute_success = [] for channel in guild.channels: @@ -1172,10 +1179,10 @@ class Mod(commands.Cog): perms_cache = await self.settings.member(user).perms_cache() if overwrites.send_messages or permissions.send_messages: - return False, mute_unmute_issues["already_unmuted"] + return False, T_(mute_unmute_issues["already_unmuted"]) elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): - return False, mute_unmute_issues["hierarchy_problem"] + return False, T_(mute_unmute_issues["hierarchy_problem"]) if channel.id in perms_cache: old_values = perms_cache[channel.id] @@ -1190,9 +1197,11 @@ class Mod(commands.Cog): if not is_empty: await channel.set_permissions(user, overwrite=overwrites) else: - await channel.set_permissions(user, overwrite=None) + await channel.set_permissions( + user, overwrite=cast(discord.PermissionOverwrite, None) + ) except discord.Forbidden: - return False, mute_unmute_issues["permissions_issue"] + return False, T_(mute_unmute_issues["permissions_issue"]) else: try: del perms_cache[channel.id] @@ -1206,15 +1215,16 @@ class Mod(commands.Cog): @commands.guild_only() @checks.admin_or_permissions(manage_channels=True) async def ignore(self, ctx: commands.Context): - """Adds servers/channels to ignorelist""" + """Add servers or channels to the ignore list.""" if ctx.invoked_subcommand is None: await ctx.send(await self.count_ignored()) @ignore.command(name="channel") async def ignore_channel(self, ctx: commands.Context, channel: discord.TextChannel = None): - """Ignores channel + """Ignore commands in the channel. - Defaults to current one""" + Defaults to the current channel. + """ if not channel: channel = ctx.channel if not await self.settings.channel(channel).ignored(): @@ -1226,7 +1236,7 @@ class Mod(commands.Cog): @ignore.command(name="server", aliases=["guild"]) @checks.admin_or_permissions(manage_guild=True) async def ignore_guild(self, ctx: commands.Context): - """Ignores current server""" + """Ignore commands in the current server.""" guild = ctx.guild if not await self.settings.guild(guild).ignored(): await self.settings.guild(guild).ignored.set(True) @@ -1238,15 +1248,16 @@ class Mod(commands.Cog): @commands.guild_only() @checks.admin_or_permissions(manage_channels=True) async def unignore(self, ctx: commands.Context): - """Removes servers/channels from ignorelist""" + """Remove servers or channels from the ignore list.""" if ctx.invoked_subcommand is None: await ctx.send(await self.count_ignored()) @unignore.command(name="channel") async def unignore_channel(self, ctx: commands.Context, channel: discord.TextChannel = None): - """Removes channel from ignore list + """Remove a channel from ignore the list. - Defaults to current one""" + Defaults to the current channel. + """ if not channel: channel = ctx.channel @@ -1259,7 +1270,7 @@ class Mod(commands.Cog): @unignore.command(name="server", aliases=["guild"]) @checks.admin_or_permissions(manage_guild=True) async def unignore_guild(self, ctx: commands.Context): - """Removes current guild from ignore list""" + """Remove the current server from the ignore list.""" guild = ctx.message.guild if await self.settings.guild(guild).ignored(): await self.settings.guild(guild).ignored.set(False) @@ -1284,7 +1295,8 @@ class Mod(commands.Cog): """Global check to see if a channel or server is ignored. Any users who have permission to use the `ignore` or `unignore` commands - surpass the check.""" + surpass the check. + """ perms = ctx.channel.permissions_for(ctx.author) surpass_ignore = ( isinstance(ctx.channel, discord.abc.PrivateChannel) @@ -1300,14 +1312,15 @@ class Mod(commands.Cog): @commands.command() @commands.guild_only() + @commands.bot_has_permissions(embed_links=True) async def userinfo(self, ctx, *, user: discord.Member = None): - """Shows information for a user. + """Show information about a user. This includes fields for status, discord join date, server join date, voice state and previous names/nicknames. - If the user has none of roles, previous names or previous - nicknames, these fields will be omitted. + If the user has no roles, previous names or previous nicknames, + these fields will be omitted. """ author = ctx.author guild = ctx.guild @@ -1383,14 +1396,11 @@ class Mod(commands.Cog): else: data.set_author(name=name) - try: - await ctx.send(embed=data) - except discord.HTTPException: - await ctx.send(_("I need the `Embed links` permission to send this.")) + await ctx.send(embed=data) @commands.command() async def names(self, ctx: commands.Context, user: discord.Member): - """Show previous names/nicknames of a user""" + """Show previous names and nicknames of a user.""" names, nicks = await self.get_names_and_nicks(user) msg = "" if names: @@ -1433,7 +1443,7 @@ class Mod(commands.Cog): queue_entry = (guild.id, user.id) self.unban_queue.append(queue_entry) try: - await guild.unban(user, reason="Tempban finished") + await guild.unban(user, reason=_("Tempban finished")) guild_tempbans.remove(uid) except discord.Forbidden: self.unban_queue.remove(queue_entry) @@ -1463,12 +1473,12 @@ class Mod(commands.Cog): guild = message.guild author = message.author - if await self.settings.guild(guild).ban_mention_spam(): - max_mentions = await self.settings.guild(guild).ban_mention_spam() + max_mentions = await self.settings.guild(guild).ban_mention_spam() + if max_mentions: mentions = set(message.mentions) if len(mentions) >= max_mentions: try: - await guild.ban(author, reason="Mention spam (Autoban)") + await guild.ban(author, reason=_("Mention spam (Autoban)")) except discord.HTTPException: log.info( "Failed to ban member for mention spam in server {}.".format(guild.id) @@ -1482,7 +1492,7 @@ class Mod(commands.Cog): "ban", author, guild.me, - "Mention spam (Autoban)", + _("Mention spam (Autoban)"), until=None, channel=None, ) @@ -1495,6 +1505,7 @@ class Mod(commands.Cog): async def on_command_completion(self, ctx: commands.Context): await self._delete_delay(ctx) + # noinspection PyUnusedLocal async def on_command_error(self, ctx: commands.Context, error): await self._delete_delay(ctx) @@ -1511,11 +1522,9 @@ class Mod(commands.Cog): return async def _delete_helper(m): - try: + with contextlib.suppress(discord.HTTPException): await m.delete() log.debug("Deleted command msg {}".format(m.id)) - except: - pass # We don't really care if it fails or not await asyncio.sleep(delay) await _delete_helper(message) @@ -1537,7 +1546,7 @@ class Mod(commands.Cog): return deleted = await self.check_duplicates(message) if not deleted: - deleted = await self.check_mention_spam(message) + await self.check_mention_spam(message) async def on_member_ban(self, guild: discord.Guild, member: discord.Member): if (guild.id, member.id) in self.ban_queue: @@ -1577,7 +1586,8 @@ class Mod(commands.Cog): except RuntimeError as e: print(e) - async def on_modlog_case_create(self, case: modlog.Case): + @staticmethod + async def on_modlog_case_create(case: modlog.Case): """ An event for modlog case creation """ @@ -1592,7 +1602,8 @@ class Mod(commands.Cog): msg = await mod_channel.send(case_content) await case.edit({"message": msg}) - async def on_modlog_case_edit(self, case: modlog.Case): + @staticmethod + async def on_modlog_case_edit(case: modlog.Case): """ Event for modlog case edits """ @@ -1605,7 +1616,10 @@ class Mod(commands.Cog): else: await case.message.edit(content=case_content) - async def get_audit_entry_info(self, guild: discord.Guild, action: int, target): + @classmethod + async def get_audit_entry_info( + cls, guild: discord.Guild, action: discord.AuditLogAction, target + ): """Get info about an audit log entry. Parameters @@ -1625,14 +1639,15 @@ class Mod(commands.Cog): if the audit log entry could not be found. """ try: - entry = await self.get_audit_log_entry(guild, action=action, target=target) + entry = await cls.get_audit_log_entry(guild, action=action, target=target) except discord.HTTPException: entry = None if entry is None: return None, None, None return entry.user, entry.reason, entry.created_at - async def get_audit_log_entry(self, guild: discord.Guild, action: int, target): + @staticmethod + async def get_audit_log_entry(guild: discord.Guild, action: discord.AuditLogAction, target): """Get an audit log entry. Any exceptions encountered when looking through the audit log will be @@ -1686,12 +1701,16 @@ class Mod(commands.Cog): return [p for p in iter(overwrites)] == [p for p in iter(discord.PermissionOverwrite())] +_ = lambda s: s mute_unmute_issues = { - "already_muted": "That user can't send messages in this channel.", - "already_unmuted": "That user isn't muted in this channel!", - "hierarchy_problem": "I cannot let you do that. You are not higher than " - "the user in the role hierarchy.", - "permissions_issue": "Failed to mute user. I need the manage roles " - "permission and the user I'm muting must be " - "lower than myself in the role hierarchy.", + "already_muted": _("That user can't send messages in this channel."), + "already_unmuted": _("That user isn't muted in this channel!"), + "hierarchy_problem": _( + "I cannot let you do that. You are not higher than " "the user in the role hierarchy." + ), + "permissions_issue": _( + "Failed to mute user. I need the manage roles " + "permission and the user I'm muting must be " + "lower than myself in the role hierarchy." + ), } diff --git a/redbot/core/bank.py b/redbot/core/bank.py index 2bedae9a7..840721147 100644 --- a/redbot/core/bank.py +++ b/redbot/core/bank.py @@ -1,6 +1,6 @@ import datetime import os -from typing import Union, List +from typing import Union, List, Optional import discord @@ -296,12 +296,20 @@ async def transfer_credits(from_: discord.Member, to: discord.Member, amount: in return await deposit_credits(to, amount) -async def wipe_bank(): - """Delete all accounts from the bank.""" +async def wipe_bank(guild: Optional[discord.Guild] = None) -> None: + """Delete all accounts from the bank. + + Parameters + ---------- + guild : discord.Guild + The guild to clear accounts for. If unsupplied and the bank is + per-server, all accounts in every guild will be wiped. + + """ if await is_global(): await _conf.clear_all_users() else: - await _conf.clear_all_members() + await _conf.clear_all_members(guild) async def get_leaderboard(positions: int = None, guild: discord.Guild = None) -> List[tuple]: diff --git a/redbot/core/config.py b/redbot/core/config.py index bd3226115..b9b7a0a76 100644 --- a/redbot/core/config.py +++ b/redbot/core/config.py @@ -838,7 +838,7 @@ class Config: """ return self._get_base_group(self.ROLE, role.id) - def user(self, user: discord.User) -> Group: + def user(self, user: discord.abc.User) -> Group: """Returns a `Group` for the given user. Parameters diff --git a/redbot/core/i18n.py b/redbot/core/i18n.py index 035fb43d3..5223131e9 100644 --- a/redbot/core/i18n.py +++ b/redbot/core/i18n.py @@ -1,5 +1,7 @@ +import os import re from pathlib import Path +from typing import Callable, Union from . import commands @@ -113,9 +115,9 @@ def _normalize(string, remove_newline=False): ends_with_space = s[-1] in " \n\t\r" if remove_newline: newline_re = re.compile("[\r\n]+") - s = " ".join(filter(bool, newline_re.split(s))) - s = " ".join(filter(bool, s.split("\t"))) - s = " ".join(filter(bool, s.split(" "))) + s = " ".join(filter(None, newline_re.split(s))) + s = " ".join(filter(None, s.split("\t"))) + s = " ".join(filter(None, s.split(" "))) if starts_with_space: s = " " + s if ends_with_space: @@ -149,10 +151,10 @@ def get_locale_path(cog_folder: Path, extension: str) -> Path: return cog_folder / "locales" / "{}.{}".format(get_locale(), extension) -class Translator: +class Translator(Callable[[str], str]): """Function to get translated strings at runtime.""" - def __init__(self, name, file_location): + def __init__(self, name: str, file_location: Union[str, Path, os.PathLike]): """ Initializes an internationalization object. @@ -173,7 +175,7 @@ class Translator: self.load_translations() - def __call__(self, untranslated: str): + def __call__(self, untranslated: str) -> str: """Translate the given string. This will look for the string in the translator's :code:`.pot` file, From 443f2ca556a25de440562f502a8362ce9b31bdf7 Mon Sep 17 00:00:00 2001 From: Toby Harradine Date: Fri, 5 Oct 2018 17:37:26 +1000 Subject: [PATCH 5/5] [i18n] Pass over modlog, permissions, reports, streams, trivia, warnings Signed-off-by: Toby Harradine --- redbot/cogs/mod/mod.py | 16 +- redbot/cogs/modlog/modlog.py | 73 +++--- redbot/cogs/permissions/converters.py | 57 +++-- redbot/cogs/permissions/permissions.py | 19 +- redbot/cogs/reports/reports.py | 45 ++-- redbot/cogs/streams/streams.py | 308 +++++++++++++------------ redbot/cogs/trivia/session.py | 71 +++--- redbot/cogs/trivia/trivia.py | 206 +++++++++-------- redbot/cogs/warnings/warnings.py | 117 ++++++---- redbot/core/utils/tunnel.py | 21 +- 10 files changed, 498 insertions(+), 435 deletions(-) diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py index cea005bfa..5c4b6c1f6 100644 --- a/redbot/cogs/mod/mod.py +++ b/redbot/cogs/mod/mod.py @@ -386,7 +386,7 @@ class Mod(commands.Cog): async def ban( self, ctx: commands.Context, user: discord.Member, days: str = None, *, reason: str = None ): - """Ban a user from the current server. + """Ban a user from this server. Deletes `` worth of messages. @@ -469,7 +469,7 @@ class Mod(commands.Cog): @commands.bot_has_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True) async def hackban(self, ctx: commands.Context, user_id: int, *, reason: str = None): - """Pre-emptively ban a user from the current server. + """Pre-emptively ban a user from this server. A user ID needs to be provided in order to ban using this command. @@ -531,7 +531,7 @@ class Mod(commands.Cog): async def tempban( self, ctx: commands.Context, user: discord.Member, days: int = 1, *, reason: str = None ): - """Temporarily ban a user from the current server.""" + """Temporarily ban a user from this server.""" guild = ctx.guild author = ctx.author days_delta = timedelta(days=int(days)) @@ -672,7 +672,7 @@ class Mod(commands.Cog): @commands.bot_has_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True) async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = None): - """Unban a user from the current server. + """Unban a user from this server. Requires specifying the target user's ID. To find this, you may either: 1. Copy it from the mod log case (if one was created), or @@ -1104,7 +1104,7 @@ class Mod(commands.Cog): async def unmute_channel( self, ctx: commands.Context, user: discord.Member, *, reason: str = None ): - """Unmute a user in the current channel.""" + """Unmute a user in this channel.""" channel = ctx.channel author = ctx.author guild = ctx.guild @@ -1137,7 +1137,7 @@ class Mod(commands.Cog): async def unmute_guild( self, ctx: commands.Context, user: discord.Member, *, reason: str = None ): - """Unmute a user in the current server.""" + """Unmute a user in this server.""" guild = ctx.guild author = ctx.author @@ -1236,7 +1236,7 @@ class Mod(commands.Cog): @ignore.command(name="server", aliases=["guild"]) @checks.admin_or_permissions(manage_guild=True) async def ignore_guild(self, ctx: commands.Context): - """Ignore commands in the current server.""" + """Ignore commands in this server.""" guild = ctx.guild if not await self.settings.guild(guild).ignored(): await self.settings.guild(guild).ignored.set(True) @@ -1270,7 +1270,7 @@ class Mod(commands.Cog): @unignore.command(name="server", aliases=["guild"]) @checks.admin_or_permissions(manage_guild=True) async def unignore_guild(self, ctx: commands.Context): - """Remove the current server from the ignore list.""" + """Remove this server from the ignore list.""" guild = ctx.message.guild if await self.settings.guild(guild).ignored(): await self.settings.guild(guild).ignored.set(False) diff --git a/redbot/cogs/modlog/modlog.py b/redbot/cogs/modlog/modlog.py index 5cdf5c894..aa0b40b8c 100644 --- a/redbot/cogs/modlog/modlog.py +++ b/redbot/cogs/modlog/modlog.py @@ -1,3 +1,5 @@ +from typing import Optional + import discord from redbot.core import checks, modlog, commands @@ -10,7 +12,7 @@ _ = Translator("ModLog", __file__) @cog_i18n(_) class ModLog(commands.Cog): - """Log for mod actions""" + """Manage log channels for moderation actions.""" def __init__(self, bot: Red): super().__init__() @@ -19,23 +21,28 @@ class ModLog(commands.Cog): @commands.group() @checks.guildowner_or_permissions(administrator=True) async def modlogset(self, ctx: commands.Context): - """Settings for the mod log""" + """Manage modlog settings.""" pass @modlogset.command() @commands.guild_only() async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None): - """Sets a channel as mod log + """Set a channel as the modlog. - Leaving the channel parameter empty will deactivate it""" + Omit `` to disable the modlog. + """ guild = ctx.guild if channel: if channel.permissions_for(guild.me).send_messages: await modlog.set_modlog_channel(guild, channel) - await ctx.send(_("Mod events will be sent to {}").format(channel.mention)) + await ctx.send( + _("Mod events will be sent to {channel}").format(channel=channel.mention) + ) else: await ctx.send( - _("I do not have permissions to send messages in {}!").format(channel.mention) + _("I do not have permissions to send messages in {channel}!").format( + channel=channel.mention + ) ) else: try: @@ -49,39 +56,36 @@ class ModLog(commands.Cog): @modlogset.command(name="cases") @commands.guild_only() async def set_cases(self, ctx: commands.Context, action: str = None): - """Enables or disables case creation for each type of mod action""" + """Enable or disable case creation for a mod action.""" guild = ctx.guild if action is None: # No args given casetypes = await modlog.get_all_casetypes(guild) await ctx.send_help() - title = _("Current settings:") - msg = "" + lines = [] for ct in casetypes: - enabled = await ct.is_enabled() - value = "enabled" if enabled else "disabled" - msg += "%s : %s\n" % (ct.name, value) + enabled = "enabled" if await ct.is_enabled() else "disabled" + lines.append(f"{ct.name} : {enabled}") - msg = title + "\n" + box(msg) - await ctx.send(msg) + await ctx.send(_("Current settings:\n") + box("\n".join(lines))) return + casetype = await modlog.get_casetype(action, guild) if not casetype: await ctx.send(_("That action is not registered")) else: - enabled = await casetype.is_enabled() - await casetype.set_enabled(True if not enabled else False) - - msg = _("Case creation for {} actions is now {}.").format( - action, "enabled" if not enabled else "disabled" + await casetype.set_enabled(not enabled) + await ctx.send( + _("Case creation for {action_name} actions is now {enabled}.").format( + action_name=action, enabled="enabled" if not enabled else "disabled" + ) ) - await ctx.send(msg) @modlogset.command() @commands.guild_only() async def resetcases(self, ctx: commands.Context): - """Resets modlog's cases""" + """Reset all modlog cases in this server.""" guild = ctx.guild await modlog.reset_cases(guild) await ctx.send(_("Cases have been reset.")) @@ -89,7 +93,7 @@ class ModLog(commands.Cog): @commands.command() @commands.guild_only() async def case(self, ctx: commands.Context, number: int): - """Shows the specified case""" + """Show the specified case.""" try: case = await modlog.get_case(number, ctx.guild, self.bot) except RuntimeError: @@ -101,24 +105,21 @@ class ModLog(commands.Cog): else: await ctx.send(await case.message_content(embed=False)) - @commands.command(usage="[case] ") + @commands.command() @commands.guild_only() - async def reason(self, ctx: commands.Context, *, reason: str): - """Lets you specify a reason for mod-log's cases - + async def reason(self, ctx: commands.Context, case: Optional[int], *, reason: str): + """Specify a reason for a modlog case. + Please note that you can only edit cases you are - the owner of unless you are a mod/admin or the server owner. - - If no number is specified, the latest case will be used.""" + the owner of unless you are a mod, admin or server owner. + + If no case number is specified, the latest case will be used. + """ author = ctx.author guild = ctx.guild - potential_case = reason.split()[0] - if potential_case.isdigit(): - case = int(potential_case) - reason = reason.replace(potential_case, "") - else: - case = str(int(await modlog.get_next_case_number(guild)) - 1) - # latest case + if case is None: + # get the latest case + case = int(await modlog.get_next_case_number(guild)) - 1 try: case_before = await modlog.get_case(case, guild, self.bot) except RuntimeError: diff --git a/redbot/cogs/permissions/converters.py b/redbot/cogs/permissions/converters.py index 0cfaddbc8..1b1668dc6 100644 --- a/redbot/cogs/permissions/converters.py +++ b/redbot/cogs/permissions/converters.py @@ -1,5 +1,9 @@ -from typing import NamedTuple, Union, Optional +from typing import NamedTuple, Union, Optional, cast, Type + from redbot.core import commands +from redbot.core.i18n import Translator + +_ = Translator("PermissionsConverters", __file__) class CogOrCommand(NamedTuple): @@ -18,39 +22,34 @@ class CogOrCommand(NamedTuple): return cls(type="COMMAND", name=cmd.qualified_name, obj=cmd) raise commands.BadArgument( - 'Cog or command "{arg}" not found. Please note that this is case sensitive.' - "".format(arg=arg) + _( + 'Cog or command "{name}" not found. Please note that this is case sensitive.' + ).format(name=arg) ) -class RuleType: +def RuleType(arg: str) -> bool: + if arg.lower() in ("allow", "whitelist", "allowed"): + return True + if arg.lower() in ("deny", "blacklist", "denied"): + return False - # noinspection PyUnusedLocal - @classmethod - async def convert(cls, ctx: commands.Context, arg: str) -> bool: - if arg.lower() in ("allow", "whitelist", "allowed"): - return True - if arg.lower() in ("deny", "blacklist", "denied"): - return False - - raise commands.BadArgument( - '"{arg}" is not a valid rule. Valid rules are "allow" or "deny"'.format(arg=arg) - ) + raise commands.BadArgument( + _('"{arg}" is not a valid rule. Valid rules are "allow" or "deny"').format(arg=arg) + ) -class ClearableRuleType: +def ClearableRuleType(arg: str) -> Optional[bool]: + if arg.lower() in ("allow", "whitelist", "allowed"): + return True + if arg.lower() in ("deny", "blacklist", "denied"): + return False + if arg.lower() in ("clear", "reset"): + return None - # noinspection PyUnusedLocal - @classmethod - async def convert(cls, ctx: commands.Context, arg: str) -> Optional[bool]: - if arg.lower() in ("allow", "whitelist", "allowed"): - return True - if arg.lower() in ("deny", "blacklist", "denied"): - return False - if arg.lower() in ("clear", "reset"): - return None - - raise commands.BadArgument( + raise commands.BadArgument( + _( '"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to ' - "remove the rule".format(arg=arg) - ) + "remove the rule" + ).format(arg=arg) + ) diff --git a/redbot/cogs/permissions/permissions.py b/redbot/cogs/permissions/permissions.py index 48346b8d2..f0bbd13d8 100644 --- a/redbot/cogs/permissions/permissions.py +++ b/redbot/cogs/permissions/permissions.py @@ -2,7 +2,7 @@ import asyncio import io import textwrap from copy import copy -from typing import Union, Optional, Dict, List, Tuple, Any, Iterator, ItemsView +from typing import Union, Optional, Dict, List, Tuple, Any, Iterator, ItemsView, cast import discord import yaml @@ -287,9 +287,11 @@ class Permissions(commands.Cog): `` is the user, channel, role or server the rule is for. """ - # noinspection PyTypeChecker await self._add_rule( - rule=allow_or_deny, cog_or_cmd=cog_or_command, model_id=who_or_what.id, guild_id=0 + rule=cast(bool, allow_or_deny), + cog_or_cmd=cog_or_command, + model_id=who_or_what.id, + guild_id=0, ) await ctx.send(_("Rule added.")) @@ -312,9 +314,8 @@ class Permissions(commands.Cog): `` is the user, channel or role the rule is for. """ - # noinspection PyTypeChecker await self._add_rule( - rule=allow_or_deny, + rule=cast(bool, allow_or_deny), cog_or_cmd=cog_or_command, model_id=who_or_what.id, guild_id=ctx.guild.id, @@ -381,9 +382,10 @@ class Permissions(commands.Cog): `` is the cog or command to set the default rule for. This is case sensitive. """ - # noinspection PyTypeChecker await self._set_default_rule( - rule=allow_or_deny, cog_or_cmd=cog_or_command, guild_id=ctx.guild.id + rule=cast(Optional[bool], allow_or_deny), + cog_or_cmd=cog_or_command, + guild_id=ctx.guild.id, ) await ctx.send(_("Default set.")) @@ -403,9 +405,8 @@ class Permissions(commands.Cog): `` is the cog or command to set the default rule for. This is case sensitive. """ - # noinspection PyTypeChecker await self._set_default_rule( - rule=allow_or_deny, cog_or_cmd=cog_or_command, guild_id=GLOBAL + rule=cast(Optional[bool], allow_or_deny), cog_or_cmd=cog_or_command, guild_id=GLOBAL ) await ctx.send(_("Default set.")) diff --git a/redbot/cogs/reports/reports.py b/redbot/cogs/reports/reports.py index 08cfc1d8a..61b000897 100644 --- a/redbot/cogs/reports/reports.py +++ b/redbot/cogs/reports/reports.py @@ -1,6 +1,6 @@ import logging import asyncio -from typing import Union +from typing import Union, List from datetime import timedelta from copy import copy import contextlib @@ -60,23 +60,20 @@ class Reports(commands.Cog): @commands.guild_only() @commands.group(name="reportset") async def reportset(self, ctx: commands.Context): - """ - Settings for the report system. - """ + """Manage Reports.""" pass @checks.admin_or_permissions(manage_guild=True) @reportset.command(name="output") - async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel): - """Set the channel where reports will show up""" + async def reportset_output(self, ctx: commands.Context, channel: discord.TextChannel): + """Set the channel where reports will be sent.""" await self.config.guild(ctx.guild).output_channel.set(channel.id) await ctx.send(_("The report channel has been set.")) @checks.admin_or_permissions(manage_guild=True) @reportset.command(name="toggle", aliases=["toggleactive"]) - async def report_toggle(self, ctx: commands.Context): - """Enables or Disables reporting for the server""" - + async def reportset_toggle(self, ctx: commands.Context): + """Enable or Disable reporting for this server.""" active = await self.config.guild(ctx.guild).active() active = not active await self.config.guild(ctx.guild).active.set(active) @@ -168,7 +165,7 @@ class Reports(commands.Cog): if channel is None: return None - files = await Tunnel.files_from_attatch(msg) + files: List[discord.File] = await Tunnel.files_from_attatch(msg) ticket_number = await self.config.guild(guild).next_ticket() await self.config.guild(guild).next_ticket.set(ticket_number + 1) @@ -204,11 +201,10 @@ class Reports(commands.Cog): @commands.group(name="report", invoke_without_command=True) async def report(self, ctx: commands.Context, *, _report: str = ""): - """ - Send a report. + """Send a report. Use without arguments for interactive reporting, or do - [p]report to use it non-interactively. + `[p]report ` to use it non-interactively. """ author = ctx.author guild = ctx.guild @@ -323,9 +319,8 @@ class Reports(commands.Cog): @checks.mod_or_permissions(manage_members=True) @report.command(name="interact") async def response(self, ctx, ticket_number: int): - """ - Open a message tunnel. - + """Open a message tunnel. + This tunnel will forward things you say in this channel to the ticket opener's direct messages. @@ -354,8 +349,7 @@ class Reports(commands.Cog): ) big_topic = _( - "{who} opened a 2-way communication " - "about ticket number {ticketnum}. Anything you say or upload here " + " Anything you say or upload here " "(8MB file size limitation on uploads) " "will be forwarded to them until the communication is closed.\n" "You can close a communication at any point by reacting with " @@ -364,8 +358,12 @@ class Reports(commands.Cog): "\N{WHITE HEAVY CHECK MARK}.\n" "Tunnels are not persistent across bot restarts." ) - topic = big_topic.format( - ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild) + topic = ( + _( + "A moderator in the server `{guild.name}` has opened a 2-way communication about " + "ticket number {ticket_number}." + ).format(guild=guild, ticket_number=ticket_number) + + big_topic ) try: m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True) @@ -373,4 +371,9 @@ class Reports(commands.Cog): await ctx.send(_("That user has DMs disabled.")) else: self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m} - await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number)) + await ctx.send( + _( + "You have opened a 2-way communication about ticket number {ticket_number}." + ).format(ticket_number=ticket_number) + + big_topic + ) diff --git a/redbot/cogs/streams/streams.py b/redbot/cogs/streams/streams.py index 381ee3cb6..901ca6803 100644 --- a/redbot/cogs/streams/streams.py +++ b/redbot/cogs/streams/streams.py @@ -1,3 +1,5 @@ +import contextlib + import discord from redbot.core import Config, checks, commands from redbot.core.utils.chat_formatting import pagify @@ -22,7 +24,7 @@ from .errors import ( StreamsError, InvalidTwitchCredentials, ) -from . import streamtypes as StreamClasses +from . import streamtypes as _streamtypes from collections import defaultdict import asyncio import re @@ -76,14 +78,14 @@ class Streams(commands.Cog): @commands.command() async def twitch(self, ctx: commands.Context, channel_name: str): - """Checks if a Twitch channel is live""" + """Check if a Twitch channel is live.""" token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None) stream = TwitchStream(name=channel_name, token=token) await self.check_online(ctx, stream) @commands.command() async def youtube(self, ctx: commands.Context, channel_id_or_name: str): - """Checks if a Youtube channel is live""" + """Check if a YouTube channel is live.""" apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None) is_name = self.check_name_or_id(channel_id_or_name) if is_name: @@ -94,23 +96,24 @@ class Streams(commands.Cog): @commands.command() async def hitbox(self, ctx: commands.Context, channel_name: str): - """Checks if a Hitbox channel is live""" + """Check if a Hitbox channel is live.""" stream = HitboxStream(name=channel_name) await self.check_online(ctx, stream) @commands.command() async def mixer(self, ctx: commands.Context, channel_name: str): - """Checks if a Mixer channel is live""" + """Check if a Mixer channel is live.""" stream = MixerStream(name=channel_name) await self.check_online(ctx, stream) @commands.command() async def picarto(self, ctx: commands.Context, channel_name: str): - """Checks if a Picarto channel is live""" + """Check if a Picarto channel is live.""" stream = PicartoStream(name=channel_name) await self.check_online(ctx, stream) - async def check_online(self, ctx: commands.Context, stream): + @staticmethod + async def check_online(ctx: commands.Context, stream): try: embed = await stream.is_online() except OfflineStream: @@ -119,15 +122,17 @@ class Streams(commands.Cog): await ctx.send(_("That channel doesn't seem to exist.")) except InvalidTwitchCredentials: await ctx.send( - _("The twitch token is either invalid or has not been set. See `{}`.").format( - "{}streamset twitchtoken".format(ctx.prefix) - ) + _( + "The Twitch token is either invalid or has not been set. See " + "`{prefix}streamset twitchtoken`." + ).format(prefix=ctx.prefix) ) except InvalidYoutubeCredentials: await ctx.send( - _("Your Youtube API key is either invalid or has not been set. See {}.").format( - "`{}streamset youtubekey`".format(ctx.prefix) - ) + _( + "The YouTube API key is either invalid or has not been set. See " + "`{prefix}streamset youtubekey`." + ).format(prefix=ctx.prefix) ) except APIError: await ctx.send( @@ -140,11 +145,12 @@ class Streams(commands.Cog): @commands.guild_only() @checks.mod() async def streamalert(self, ctx: commands.Context): + """Manage automated stream alerts.""" pass @streamalert.group(name="twitch", invoke_without_command=True) async def _twitch(self, ctx: commands.Context, channel_name: str = None): - """Twitch stream alerts""" + """Manage Twitch stream notifications.""" if channel_name is not None: await ctx.invoke(self.twitch_alert_channel, channel_name) else: @@ -152,7 +158,7 @@ class Streams(commands.Cog): @_twitch.command(name="channel") async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str): - """Sets a Twitch alert notification in the channel""" + """Toggle alerts in this channel for a Twitch stream.""" if re.fullmatch(r"<#\d+>", channel_name): await ctx.send("Please supply the name of a *Twitch* channel, not a Discord channel.") return @@ -160,33 +166,39 @@ class Streams(commands.Cog): @_twitch.command(name="community") async def twitch_alert_community(self, ctx: commands.Context, community: str): - """Sets an alert notification in the channel for the specified twitch community.""" + """Toggle alerts in this channel for a Twitch community.""" await self.community_alert(ctx, TwitchCommunity, community.lower()) @streamalert.command(name="youtube") async def youtube_alert(self, ctx: commands.Context, channel_name_or_id: str): - """Sets a Youtube alert notification in the channel""" + """Toggle alerts in this channel for a YouTube stream.""" await self.stream_alert(ctx, YoutubeStream, channel_name_or_id) @streamalert.command(name="hitbox") async def hitbox_alert(self, ctx: commands.Context, channel_name: str): - """Sets a Hitbox alert notification in the channel""" + """Toggle alerts in this channel for a Hitbox stream.""" await self.stream_alert(ctx, HitboxStream, channel_name) @streamalert.command(name="mixer") async def mixer_alert(self, ctx: commands.Context, channel_name: str): - """Sets a Mixer alert notification in the channel""" + """Toggle alerts in this channel for a Mixer stream.""" await self.stream_alert(ctx, MixerStream, channel_name) @streamalert.command(name="picarto") async def picarto_alert(self, ctx: commands.Context, channel_name: str): - """Sets a Picarto alert notification in the channel""" + """Toggle alerts in this channel for a Picarto stream.""" await self.stream_alert(ctx, PicartoStream, channel_name) - @streamalert.command(name="stop") + @streamalert.command(name="stop", usage="[disable_all=No]") async def streamalert_stop(self, ctx: commands.Context, _all: bool = False): - """Stops all stream notifications in the channel - Adding 'yes' will disable all notifications in the server""" + """Disable all stream alerts in this channel or server. + + `[p]streamalert stop` will disable this channel's stream + alerts. + + Do `[p]streamalert stop yes` to disable all stream alerts in + this server. + """ streams = self.streams.copy() local_channel_ids = [c.id for c in ctx.guild.channels] to_remove = [] @@ -208,9 +220,10 @@ class Streams(commands.Cog): self.streams = streams await self.save_streams() - msg = _("All the alerts in the {} have been disabled.").format( - "server" if _all else "channel" - ) + if _all: + msg = _("All the stream alerts in this server have been disabled.") + else: + msg = _("All the stream alerts in this channel have been disabled.") await ctx.send(msg) @@ -250,16 +263,18 @@ class Streams(commands.Cog): exists = await self.check_exists(stream) except InvalidTwitchCredentials: await ctx.send( - _("Your twitch token is either invalid or has not been set. See {}.").format( - "`{}streamset twitchtoken`".format(ctx.prefix) - ) + _( + "The Twitch token is either invalid or has not been set. See " + "`{prefix}streamset twitchtoken`." + ).format(prefix=ctx.prefix) ) return except InvalidYoutubeCredentials: await ctx.send( _( - "Your Youtube API key is either invalid or has not been set. See {}." - ).format("`{}streamset youtubekey`".format(ctx.prefix)) + "The YouTube API key is either invalid or has not been set. See " + "`{prefix}streamset youtubekey`." + ).format(prefix=ctx.prefix) ) return except APIError: @@ -283,9 +298,10 @@ class Streams(commands.Cog): await community.get_community_streams() except InvalidTwitchCredentials: await ctx.send( - _("The twitch token is either invalid or has not been set. See {}.").format( - "`{}streamset twitchtoken`".format(ctx.prefix) - ) + _( + "The Twitch token is either invalid or has not been set. See " + "`{prefix}streamset twitchtoken`." + ).format(prefix=ctx.prefix) ) return except CommunityNotFound: @@ -309,14 +325,15 @@ class Streams(commands.Cog): @streamset.command() @checks.is_owner() async def twitchtoken(self, ctx: commands.Context, token: str): - """Set the Client ID for twitch. + """Set the Client ID for Twitch. + To do this, follow these steps: - 1. Go to this page: https://dev.twitch.tv/dashboard/apps. - 2. Click *Register Your Application* - 3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and - select an Application Category of your choosing. - 4. Click *Register*, and on the following page, copy the Client ID. - 5. Paste the Client ID into this command. Done! + 1. Go to this page: https://dev.twitch.tv/dashboard/apps. + 2. Click *Register Your Application* + 3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and + select an Application Category of your choosing. + 4. Click *Register*, and on the following page, copy the Client ID. + 5. Paste the Client ID into this command. Done! """ await self.db.tokens.set_raw("TwitchStream", value=token) await self.db.tokens.set_raw("TwitchCommunity", value=token) @@ -325,64 +342,59 @@ class Streams(commands.Cog): @streamset.command() @checks.is_owner() async def youtubekey(self, ctx: commands.Context, key: str): - """Sets the API key for Youtube. + """Set the API key for YouTube. + To get one, do the following: 1. Create a project (see https://support.google.com/googleapi/answer/6251787 for details) - 2. Enable the Youtube Data API v3 (see https://support.google.com/googleapi/answer/6158841 for instructions) - 3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for instructions) + 2. Enable the YouTube Data API v3 (see https://support.google.com/googleapi/answer/6158841 + for instructions) + 3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for + instructions) 4. Copy your API key and paste it into this command. Done! """ await self.db.tokens.set_raw("YoutubeStream", value=key) - await ctx.send(_("Youtube key set.")) + await ctx.send(_("YouTube key set.")) @streamset.group() @commands.guild_only() async def mention(self, ctx: commands.Context): - """Sets mentions for alerts.""" + """Manage mention settings for stream alerts.""" pass @mention.command(aliases=["everyone"]) @commands.guild_only() async def all(self, ctx: commands.Context): - """Toggles everyone mention""" + """Toggle the `@\u200beveryone` mention.""" guild = ctx.guild current_setting = await self.db.guild(guild).mention_everyone() if current_setting: await self.db.guild(guild).mention_everyone.set(False) - await ctx.send( - _("{} will no longer be mentioned when a stream or community is live").format( - "@\u200beveryone" - ) - ) + await ctx.send(_("`@\u200beveryone` will no longer be mentioned for stream alerts.")) else: await self.db.guild(guild).mention_everyone.set(True) await ctx.send( - _("When a stream or community " "is live, {} will be mentioned.").format( - "@\u200beveryone" - ) + _("When a stream or community is live, `@\u200beveryone` will be mentioned.") ) @mention.command(aliases=["here"]) @commands.guild_only() async def online(self, ctx: commands.Context): - """Toggles here mention""" + """Toggle the `@\u200bhere` mention.""" guild = ctx.guild current_setting = await self.db.guild(guild).mention_here() if current_setting: await self.db.guild(guild).mention_here.set(False) - await ctx.send(_("{} will no longer be mentioned for an alert.").format("@\u200bhere")) + await ctx.send(_("`@\u200bhere` will no longer be mentioned for stream alerts.")) else: await self.db.guild(guild).mention_here.set(True) await ctx.send( - _("When a stream or community " "is live, {} will be mentioned.").format( - "@\u200bhere" - ) + _("When a stream or community is live, `@\u200bhere` will be mentioned.") ) @mention.command() @commands.guild_only() async def role(self, ctx: commands.Context, *, role: discord.Role): - """Toggles role mention""" + """Toggle a role mention.""" current_setting = await self.db.role(role).mention() if not role.mentionable: await ctx.send("That role is not mentionable!") @@ -390,27 +402,27 @@ class Streams(commands.Cog): if current_setting: await self.db.role(role).mention.set(False) await ctx.send( - _("{} will no longer be mentioned for an alert.").format( - "@\u200b{}".format(role.name) + _("`@\u200b{role.name}` will no longer be mentioned for stream alerts.").format( + role=role ) ) else: await self.db.role(role).mention.set(True) await ctx.send( - _("When a stream or community " "is live, {} will be mentioned." "").format( - "@\u200b{}".format(role.name) - ) + _( + "When a stream or community is live, `@\u200b{role.name}` will be mentioned." + ).format(role=role) ) @streamset.command() @commands.guild_only() async def autodelete(self, ctx: commands.Context, on_off: bool): - """Toggles automatic deletion of notifications for streams that go offline""" + """Toggle alert deletion for when streams go offline.""" await self.db.guild(ctx.guild).autodelete.set(on_off) if on_off: - await ctx.send("The notifications will be deleted once streams go offline.") + await ctx.send(_("The notifications will be deleted once streams go offline.")) else: - await ctx.send("Notifications will never be deleted.") + await ctx.send(_("Notifications will no longer be deleted.")) async def add_or_remove(self, ctx: commands.Context, stream): if ctx.channel.id not in stream.channels: @@ -418,18 +430,18 @@ class Streams(commands.Cog): if stream not in self.streams: self.streams.append(stream) await ctx.send( - _("I'll now send a notification in this channel when {} is live.").format( - stream.name - ) + _( + "I'll now send a notification in this channel when {stream.name} is live." + ).format(stream=stream) ) else: stream.channels.remove(ctx.channel.id) if not stream.channels: self.streams.remove(stream) await ctx.send( - _("I won't send notifications about {} in this channel anymore.").format( - stream.name - ) + _( + "I won't send notifications about {stream.name} in this channel anymore." + ).format(stream=stream) ) await self.save_streams() @@ -442,9 +454,8 @@ class Streams(commands.Cog): await ctx.send( _( "I'll send a notification in this channel when a " - "channel is live in the {} community." - "" - ).format(community.name) + "channel is live in the {community.name} community." + ).format(community=community) ) else: community.channels.remove(ctx.channel.id) @@ -453,9 +464,8 @@ class Streams(commands.Cog): await ctx.send( _( "I won't send notifications about channels streaming " - "in the {} community in this channel anymore." - "" - ).format(community.name) + "in the {community.name} community in this channel anymore." + ).format(community=community) ) await self.save_communities() @@ -481,7 +491,8 @@ class Streams(commands.Cog): if community.type == _class.__name__ and community.name.lower() == name.lower(): return community - async def check_exists(self, stream): + @staticmethod + async def check_exists(stream): try: await stream.is_online() except OfflineStream: @@ -506,40 +517,36 @@ class Streams(commands.Cog): async def check_streams(self): for stream in self.streams: - try: - embed = await stream.is_online() - except OfflineStream: - if not stream._messages_cache: - continue - for message in stream._messages_cache: - try: - autodelete = await self.db.guild(message.guild).autodelete() - if autodelete: - await message.delete() - except: - pass - stream._messages_cache.clear() - await self.save_streams() - except: - pass - else: - if stream._messages_cache: - continue - for channel_id in stream.channels: - channel = self.bot.get_channel(channel_id) - mention_str = await self._get_mention_str(channel.guild) + with contextlib.suppress(Exception): + try: + embed = await stream.is_online() + except OfflineStream: + if not stream._messages_cache: + continue + for message in stream._messages_cache: + with contextlib.suppress(Exception): + autodelete = await self.db.guild(message.guild).autodelete() + if autodelete: + await message.delete() + stream._messages_cache.clear() + await self.save_streams() + else: + if stream._messages_cache: + continue + for channel_id in stream.channels: + channel = self.bot.get_channel(channel_id) + mention_str = await self._get_mention_str(channel.guild) - if mention_str: - content = "{}, {} is live!".format(mention_str, stream.name) - else: - content = "{} is live!".format(stream.name) + if mention_str: + content = _("{mention}, {stream.name} is live!").format( + mention=mention_str, stream=stream + ) + else: + content = _("{stream.name} is live!").format(stream=stream.name) - try: m = await channel.send(content, embed=embed) stream._messages_cache.append(m) await self.save_streams() - except: - pass async def _get_mention_str(self, guild: discord.Guild): settings = self.db.guild(guild) @@ -555,45 +562,46 @@ class Streams(commands.Cog): async def check_communities(self): for community in self.communities: - try: - stream_list = await community.get_community_streams() - except CommunityNotFound: - print(_("The Community {} was not found!").format(community.name)) - continue - except OfflineCommunity: - if not community._messages_cache: + with contextlib.suppress(Exception): + try: + stream_list = await community.get_community_streams() + except CommunityNotFound: + print( + _("The Community {community.name} was not found!").format( + community=community + ) + ) continue - for message in community._messages_cache: - try: - autodelete = await self.db.guild(message.guild).autodelete() - if autodelete: - await message.delete() - except: - pass - community._messages_cache.clear() - await self.save_communities() - except: - pass - else: - for channel in community.channels: - chn = self.bot.get_channel(channel) - streams = await self.filter_streams(stream_list, chn) - emb = await community.make_embed(streams) - chn_msg = [m for m in community._messages_cache if m.channel == chn] - if not chn_msg: - mentions = await self._get_mention_str(chn.guild) - if mentions: - msg = await chn.send(mentions, embed=emb) + except OfflineCommunity: + if not community._messages_cache: + continue + for message in community._messages_cache: + with contextlib.suppress(Exception): + autodelete = await self.db.guild(message.guild).autodelete() + if autodelete: + await message.delete() + community._messages_cache.clear() + await self.save_communities() + else: + for channel in community.channels: + chn = self.bot.get_channel(channel) + streams = await self.filter_streams(stream_list, chn) + emb = await community.make_embed(streams) + chn_msg = [m for m in community._messages_cache if m.channel == chn] + if not chn_msg: + mentions = await self._get_mention_str(chn.guild) + if mentions: + msg = await chn.send(mentions, embed=emb) + else: + msg = await chn.send(embed=emb) + community._messages_cache.append(msg) + await self.save_communities() else: - msg = await chn.send(embed=emb) - community._messages_cache.append(msg) - await self.save_communities() - else: - chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0] - community._messages_cache.remove(chn_msg) - await chn_msg.edit(embed=emb) - community._messages_cache.append(chn_msg) - await self.save_communities() + chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0] + community._messages_cache.remove(chn_msg) + await chn_msg.edit(embed=emb) + community._messages_cache.append(chn_msg) + await self.save_communities() async def filter_streams(self, streams: list, channel: discord.TextChannel) -> list: filtered = [] @@ -611,7 +619,7 @@ class Streams(commands.Cog): streams = [] for raw_stream in await self.db.streams(): - _class = getattr(StreamClasses, raw_stream["type"], None) + _class = getattr(_streamtypes, raw_stream["type"], None) if not _class: continue raw_msg_cache = raw_stream["messages"] @@ -631,7 +639,7 @@ class Streams(commands.Cog): communities = [] for raw_community in await self.db.communities(): - _class = getattr(StreamClasses, raw_community["type"], None) + _class = getattr(_streamtypes, raw_community["type"], None) if not _class: continue raw_msg_cache = raw_community["messages"] diff --git a/redbot/cogs/trivia/session.py b/redbot/cogs/trivia/session.py index 0dd34d959..cc6e2e142 100644 --- a/redbot/cogs/trivia/session.py +++ b/redbot/cogs/trivia/session.py @@ -4,20 +4,30 @@ import time import random from collections import Counter import discord -from redbot.core.bank import deposit_credits -from redbot.core.utils.chat_formatting import box +from redbot.core import bank +from redbot.core.i18n import Translator +from redbot.core.utils.chat_formatting import box, bold, humanize_list from redbot.core.utils.common_filters import normalize_smartquotes from .log import LOG __all__ = ["TriviaSession"] -_REVEAL_MESSAGES = ("I know this one! {}!", "Easy: {}.", "Oh really? It's {} of course.") -_FAIL_MESSAGES = ( - "To the next one I guess...", - "Moving on...", - "I'm sure you'll know the answer of the next one.", - "\N{PENSIVE FACE} Next one.", +T_ = Translator("TriviaSession", __file__) + + +_ = lambda s: s +_REVEAL_MESSAGES = ( + _("I know this one! {answer}!"), + _("Easy: {answer}."), + _("Oh really? It's {answer} of course."), ) +_FAIL_MESSAGES = ( + _("To the next one I guess..."), + _("Moving on..."), + _("I'm sure you'll know the answer of the next one."), + _("\N{PENSIVE FACE} Next one."), +) +_ = T_ class TriviaSession: @@ -104,7 +114,7 @@ class TriviaSession: async with self.ctx.typing(): await asyncio.sleep(3) self.count += 1 - msg = "**Question number {}!**\n\n{}".format(self.count, question) + msg = bold(_("**Question number {num}!").format(num=self.count)) + "\n\n" + question await self.ctx.send(msg) continue_ = await self.wait_for_answer(answers, delay, timeout) if continue_ is False: @@ -113,7 +123,7 @@ class TriviaSession: await self.end_game() break else: - await self.ctx.send("There are no more questions!") + await self.ctx.send(_("There are no more questions!")) await self.end_game() async def _send_startup_msg(self): @@ -121,20 +131,13 @@ class TriviaSession: for idx, tup in enumerate(self.settings["lists"].items()): name, author = tup if author: - title = "{} (by {})".format(name, author) + title = _("{trivia_list} (by {author})").format(trivia_list=name, author=author) else: title = name list_names.append(title) - num_lists = len(list_names) - if num_lists > 2: - # at least 3 lists, join all but last with comma - msg = ", ".join(list_names[: num_lists - 1]) - # join onto last with "and" - msg = " and ".join((msg, list_names[num_lists - 1])) - else: - # either 1 or 2 lists, join together with "and" - msg = " and ".join(list_names) - await self.ctx.send("Starting Trivia: " + msg) + await self.ctx.send( + _("Starting Trivia: {list_names}").format(list_names=humanize_list(list_names)) + ) def _iter_questions(self): """Iterate over questions and answers for this session. @@ -179,20 +182,20 @@ class TriviaSession: ) except asyncio.TimeoutError: if time.time() - self._last_response >= timeout: - await self.ctx.send("Guys...? Well, I guess I'll stop then.") + await self.ctx.send(_("Guys...? Well, I guess I'll stop then.")) self.stop() return False if self.settings["reveal_answer"]: - reply = random.choice(_REVEAL_MESSAGES).format(answers[0]) + reply = T_(random.choice(_REVEAL_MESSAGES)).format(answer=answers[0]) else: - reply = random.choice(_FAIL_MESSAGES) + reply = T_(random.choice(_FAIL_MESSAGES)) if self.settings["bot_plays"]: - reply += " **+1** for me!" + reply += _(" **+1** for me!") self.scores[self.ctx.guild.me] += 1 await self.ctx.send(reply) else: self.scores[message.author] += 1 - reply = "You got it {}! **+1** to you!".format(message.author.display_name) + reply = _("You got it {user}! **+1** to you!").format(user=message.author.display_name) await self.ctx.send(reply) return True @@ -282,10 +285,16 @@ class TriviaSession: amount = int(multiplier * score) if amount > 0: LOG.debug("Paying trivia winner: %d credits --> %s", amount, str(winner)) - await deposit_credits(winner, int(multiplier * score)) + await bank.deposit_credits(winner, int(multiplier * score)) await self.ctx.send( - "Congratulations, {0}, you have received {1} credits" - " for coming first.".format(winner.display_name, amount) + _( + "Congratulations, {user}, you have received {num} {currency}" + " for coming first." + ).format( + user=winner.display_name, + num=amount, + currency=await bank.get_currency_name(self.ctx.guild), + ) ) @@ -313,9 +322,9 @@ def _parse_answers(answers): for answer in answers: if isinstance(answer, bool): if answer is True: - ret.extend(["True", "Yes"]) + ret.extend(["True", "Yes", _("Yes")]) else: - ret.extend(["False", "No"]) + ret.extend(["False", "No", _("No")]) else: ret.append(str(answer)) # Uniquify list diff --git a/redbot/cogs/trivia/trivia.py b/redbot/cogs/trivia/trivia.py index 55ce6337a..b04aa1141 100644 --- a/redbot/cogs/trivia/trivia.py +++ b/redbot/cogs/trivia/trivia.py @@ -7,7 +7,8 @@ import discord from redbot.core import commands from redbot.core import Config, checks from redbot.core.data_manager import cog_data_path -from redbot.core.utils.chat_formatting import box, pagify +from redbot.core.i18n import Translator, cog_i18n +from redbot.core.utils.chat_formatting import box, pagify, bold from redbot.cogs.bank import check_global_setting_admin from .log import LOG from .session import TriviaSession @@ -16,6 +17,8 @@ __all__ = ["Trivia", "UNIQUE_ID", "get_core_lists"] UNIQUE_ID = 0xB3C0E453 +_ = Translator("Trivia", __file__) + class InvalidListError(Exception): """A Trivia list file is in invalid format.""" @@ -23,6 +26,7 @@ class InvalidListError(Exception): pass +@cog_i18n(_) class Trivia(commands.Cog): """Play trivia with friends!""" @@ -47,20 +51,21 @@ class Trivia(commands.Cog): @commands.guild_only() @checks.mod_or_permissions(administrator=True) async def triviaset(self, ctx: commands.Context): - """Manage trivia settings.""" + """Manage Trivia settings.""" if ctx.invoked_subcommand is None: settings = self.conf.guild(ctx.guild) settings_dict = await settings.all() msg = box( - "**Current settings**\n" - "Bot gains points: {bot_plays}\n" - "Answer time limit: {delay} seconds\n" - "Lack of response timeout: {timeout} seconds\n" - "Points to win: {max_score}\n" - "Reveal answer on timeout: {reveal_answer}\n" - "Payout multiplier: {payout_multiplier}\n" - "Allow lists to override settings: {allow_override}" - "".format(**settings_dict), + _( + "**Current settings**\n" + "Bot gains points: {bot_plays}\n" + "Answer time limit: {delay} seconds\n" + "Lack of response timeout: {timeout} seconds\n" + "Points to win: {max_score}\n" + "Reveal answer on timeout: {reveal_answer}\n" + "Payout multiplier: {payout_multiplier}\n" + "Allow lists to override settings: {allow_override}" + ).format(**settings_dict), lang="py", ) await ctx.send(msg) @@ -69,33 +74,34 @@ class Trivia(commands.Cog): async def triviaset_max_score(self, ctx: commands.Context, score: int): """Set the total points required to win.""" if score < 0: - await ctx.send("Score must be greater than 0.") + await ctx.send(_("Score must be greater than 0.")) return settings = self.conf.guild(ctx.guild) await settings.max_score.set(score) - await ctx.send("Done. Points required to win set to {}.".format(score)) + await ctx.send(_("Done. Points required to win set to {num}.").format(num=score)) @triviaset.command(name="timelimit") async def triviaset_timelimit(self, ctx: commands.Context, seconds: float): """Set the maximum seconds permitted to answer a question.""" if seconds < 4.0: - await ctx.send("Must be at least 4 seconds.") + await ctx.send(_("Must be at least 4 seconds.")) return settings = self.conf.guild(ctx.guild) await settings.delay.set(seconds) - await ctx.send("Done. Maximum seconds to answer set to {}.".format(seconds)) + await ctx.send(_("Done. Maximum seconds to answer set to {num}.").format(num=seconds)) @triviaset.command(name="stopafter") async def triviaset_stopafter(self, ctx: commands.Context, seconds: float): """Set how long until trivia stops due to no response.""" settings = self.conf.guild(ctx.guild) if seconds < await settings.delay(): - await ctx.send("Must be larger than the answer time limit.") + await ctx.send(_("Must be larger than the answer time limit.")) return await settings.timeout.set(seconds) await ctx.send( - "Done. Trivia sessions will now time out after {}" - " seconds of no responses.".format(seconds) + _( + "Done. Trivia sessions will now time out after {num} seconds of no responses." + ).format(num=seconds) ) @triviaset.command(name="override") @@ -103,46 +109,46 @@ class Trivia(commands.Cog): """Allow/disallow trivia lists to override settings.""" settings = self.conf.guild(ctx.guild) await settings.allow_override.set(enabled) - enabled = "now" if enabled else "no longer" - await ctx.send( - "Done. Trivia lists can {} override the trivia settings" - " for this server.".format(enabled) - ) + if enabled: + await ctx.send( + _( + "Done. Trivia lists can now override the trivia settings for this server." + ).format(now=enabled) + ) + else: + await ctx.send( + _( + "Done. Trivia lists can no longer override the trivia settings for this " + "server." + ).format(now=enabled) + ) - @triviaset.command(name="botplays") - async def trivaset_bot_plays(self, ctx: commands.Context, true_or_false: bool): + @triviaset.command(name="botplays", usage="") + async def trivaset_bot_plays(self, ctx: commands.Context, enabled: bool): """Set whether or not the bot gains points. If enabled, the bot will gain a point if no one guesses correctly. """ settings = self.conf.guild(ctx.guild) - await settings.bot_plays.set(true_or_false) - await ctx.send( - "Done. " - + ( - "I'll gain a point if users don't answer in time." - if true_or_false - else "Alright, I won't embarass you at trivia anymore." - ) - ) + await settings.bot_plays.set(enabled) + if enabled: + await ctx.send(_("Done. I'll now gain a point if users don't answer in time.")) + else: + await ctx.send(_("Alright, I won't embarass you at trivia anymore.")) - @triviaset.command(name="revealanswer") - async def trivaset_reveal_answer(self, ctx: commands.Context, true_or_false: bool): + @triviaset.command(name="revealanswer", usage="") + async def trivaset_reveal_answer(self, ctx: commands.Context, enabled: bool): """Set whether or not the answer is revealed. If enabled, the bot will reveal the answer if no one guesses correctly in time. """ settings = self.conf.guild(ctx.guild) - await settings.reveal_answer.set(true_or_false) - await ctx.send( - "Done. " - + ( - "I'll reveal the answer if no one knows it." - if true_or_false - else "I won't reveal the answer to the questions anymore." - ) - ) + await settings.reveal_answer.set(enabled) + if enabled: + await ctx.send(_("Done. I'll reveal the answer if no one knows it.")) + else: + await ctx.send(_("Alright, I won't reveal the answer to the questions anymore.")) @triviaset.command(name="payout") @check_global_setting_admin() @@ -158,13 +164,13 @@ class Trivia(commands.Cog): """ settings = self.conf.guild(ctx.guild) if multiplier < 0: - await ctx.send("Multiplier must be at least 0.") + await ctx.send(_("Multiplier must be at least 0.")) return await settings.payout_multiplier.set(multiplier) - if not multiplier: - await ctx.send("Done. I will no longer reward the winner with a payout.") - return - await ctx.send("Done. Payout multiplier set to {}.".format(multiplier)) + if multiplier: + await ctx.send(_("Done. Payout multiplier set to {num}.").format(num=multiplier)) + else: + await ctx.send(_("Done. I will no longer reward the winner with a payout.")) @commands.group(invoke_without_command=True) @commands.guild_only() @@ -180,7 +186,7 @@ class Trivia(commands.Cog): categories = [c.lower() for c in categories] session = self._get_trivia_session(ctx.channel) if session is not None: - await ctx.send("There is already an ongoing trivia session in this channel.") + await ctx.send(_("There is already an ongoing trivia session in this channel.")) return trivia_dict = {} authors = [] @@ -191,15 +197,17 @@ class Trivia(commands.Cog): dict_ = self.get_trivia_list(category) except FileNotFoundError: await ctx.send( - "Invalid category `{0}`. See `{1}trivia list`" - " for a list of trivia categories." - "".format(category, ctx.prefix) + _( + "Invalid category `{name}`. See `{prefix}trivia list` for a list of " + "trivia categories." + ).format(name=category, prefix=ctx.prefix) ) except InvalidListError: await ctx.send( - "There was an error parsing the trivia list for" - " the `{}` category. It may be formatted" - " incorrectly.".format(category) + _( + "There was an error parsing the trivia list for the `{name}` category. It " + "may be formatted incorrectly." + ).format(name=category) ) else: trivia_dict.update(dict_) @@ -208,7 +216,7 @@ class Trivia(commands.Cog): return if not trivia_dict: await ctx.send( - "The trivia list was parsed successfully, however it appears to be empty!" + _("The trivia list was parsed successfully, however it appears to be empty!") ) return settings = await self.conf.guild(ctx.guild).all() @@ -225,7 +233,7 @@ class Trivia(commands.Cog): """Stop an ongoing trivia session.""" session = self._get_trivia_session(ctx.channel) if session is None: - await ctx.send("There is no ongoing trivia session in this channel.") + await ctx.send(_("There is no ongoing trivia session in this channel.")) return author = ctx.author auth_checks = ( @@ -238,20 +246,28 @@ class Trivia(commands.Cog): if any(auth_checks): await session.end_game() session.force_stop() - await ctx.send("Trivia stopped.") + await ctx.send(_("Trivia stopped.")) else: - await ctx.send("You are not allowed to do that.") + await ctx.send(_("You are not allowed to do that.")) @trivia.command(name="list") async def trivia_list(self, ctx: commands.Context): """List available trivia categories.""" lists = set(p.stem for p in self._all_lists()) - - msg = box("**Available trivia lists**\n\n{}".format(", ".join(sorted(lists)))) - if len(msg) > 1000: - await ctx.author.send(msg) - return - await ctx.send(msg) + if await ctx.embed_requested(): + await ctx.send( + embed=discord.Embed( + title=_("Available trivia lists"), + colour=await ctx.embed_colour(), + description=", ".join(sorted(lists)), + ) + ) + else: + msg = box(bold(_("Available trivia lists")) + "\n\n" + ", ".join(sorted(lists))) + if len(msg) > 1000: + await ctx.author.send(msg) + else: + await ctx.send(msg) @trivia.group(name="leaderboard", aliases=["lboard"], autohelp=False) async def trivia_leaderboard(self, ctx: commands.Context): @@ -273,19 +289,21 @@ class Trivia(commands.Cog): ): """Leaderboard for this server. - can be any of the following fields: - - wins : total wins - - avg : average score - - total : total correct answers + `` can be any of the following fields: + - `wins` : total wins + - `avg` : average score + - `total` : total correct answers + - `games` : total games played - is the number of ranks to show on the leaderboard. + `` is the number of ranks to show on the leaderboard. """ key = self._get_sort_key(sort_by) if key is None: await ctx.send( - "Unknown field `{}`, see `{}help trivia " - "leaderboard server` for valid fields to sort by." - "".format(sort_by, ctx.prefix) + _( + "Unknown field `{field_name}`, see `{prefix}help trivia leaderboard server` " + "for valid fields to sort by." + ).format(field_name=sort_by, prefix=ctx.prefix) ) return guild = ctx.guild @@ -300,20 +318,21 @@ class Trivia(commands.Cog): ): """Global trivia leaderboard. - can be any of the following fields: - - wins : total wins - - avg : average score - - total : total correct answers from all sessions - - games : total games played + `` can be any of the following fields: + - `wins` : total wins + - `avg` : average score + - `total` : total correct answers from all sessions + - `games` : total games played - is the number of ranks to show on the leaderboard. + `` is the number of ranks to show on the leaderboard. """ key = self._get_sort_key(sort_by) if key is None: await ctx.send( - "Unknown field `{}`, see `{}help trivia " - "leaderboard global` for valid fields to sort by." - "".format(sort_by, ctx.prefix) + _( + "Unknown field `{field_name}`, see `{prefix}help trivia leaderboard server` " + "for valid fields to sort by." + ).format(field_name=sort_by, prefix=ctx.prefix) ) return data = await self.conf.all_members() @@ -365,7 +384,7 @@ class Trivia(commands.Cog): """ if not data: - await ctx.send("There are no scores on record!") + await ctx.send(_("There are no scores on record!")) return leaderboard = self._get_leaderboard(data, key, top) ret = [] @@ -386,7 +405,7 @@ class Trivia(commands.Cog): try: priority.remove(key) except ValueError: - raise ValueError("{} is not a valid key.".format(key)) + raise ValueError(f"{key} is not a valid key.") # Put key last in reverse priority priority.append(key) items = data.items() @@ -395,16 +414,15 @@ class Trivia(commands.Cog): max_name_len = max(map(lambda m: len(str(m)), data.keys())) # Headers headers = ( - "Rank", - "Member{}".format(" " * (max_name_len - 6)), - "Wins", - "Games Played", - "Total Score", - "Average Score", + _("Rank"), + _("Member") + " " * (max_name_len - 6), + _("Wins"), + _("Games Played"), + _("Total Score"), + _("Average Score"), ) - lines = [" | ".join(headers)] + lines = [" | ".join(headers), " | ".join(("-" * len(h) for h in headers))] # Header underlines - lines.append(" | ".join(("-" * len(h) for h in headers))) for rank, tup in enumerate(items, 1): member, m_data = tup # Align fields to header width diff --git a/redbot/cogs/warnings/warnings.py b/redbot/cogs/warnings/warnings.py index 77937b645..1b545591b 100644 --- a/redbot/cogs/warnings/warnings.py +++ b/redbot/cogs/warnings/warnings.py @@ -22,7 +22,7 @@ _ = Translator("Warnings", __file__) @cog_i18n(_) class Warnings(commands.Cog): - """A warning system for Red""" + """Warn misbehaving users and take automated actions.""" default_guild = {"actions": [], "reasons": {}, "allow_custom_reasons": False} @@ -48,31 +48,42 @@ class Warnings(commands.Cog): @commands.guild_only() @checks.guildowner_or_permissions(administrator=True) async def warningset(self, ctx: commands.Context): - """Warning settings""" + """Manage settings for Warnings.""" pass @warningset.command() @commands.guild_only() async def allowcustomreasons(self, ctx: commands.Context, allowed: bool): - """Enable or Disable custom reasons for a warning""" + """Enable or disable custom reasons for a warning.""" guild = ctx.guild await self.config.guild(guild).allow_custom_reasons.set(allowed) - await ctx.send( - _("Custom reasons have been {}.").format(_("enabled") if allowed else _("disabled")) - ) + if allowed: + await ctx.send(_("Custom reasons have been enabled.")) + else: + await ctx.send(_("Custom reasons have been disabled.")) @commands.group() @commands.guild_only() @checks.guildowner_or_permissions(administrator=True) async def warnaction(self, ctx: commands.Context): - """Action management""" + """Manage automated actions for Warnings. + + Actions are essentially command macros. Any command can be run + when the action is initially triggered, and/or when the action + is lifted. + + Actions must be given a name and a points threshold. When a + user is warned enough so that their points go over this + threshold, the action will be executed. + """ pass @warnaction.command(name="add") @commands.guild_only() async def action_add(self, ctx: commands.Context, name: str, points: int): - """Create an action to be taken at a specified point count - Duplicate action names are not allowed + """Create an automated action. + + Duplicate action names are not allowed. """ guild = ctx.guild @@ -103,7 +114,7 @@ class Warnings(commands.Cog): @warnaction.command(name="del") @commands.guild_only() async def action_del(self, ctx: commands.Context, action_name: str): - """Delete the point count action with the specified name""" + """Delete the action with the specified name.""" guild = ctx.guild guild_settings = self.config.guild(guild) async with guild_settings.actions() as registered_actions: @@ -116,23 +127,29 @@ class Warnings(commands.Cog): registered_actions.remove(to_remove) await ctx.tick() else: - await ctx.send(_("No action named {} exists!").format(action_name)) + await ctx.send(_("No action named {name} exists!").format(name=action_name)) @commands.group() @commands.guild_only() @checks.guildowner_or_permissions(administrator=True) async def warnreason(self, ctx: commands.Context): - """Add reasons for warnings""" + """Manage warning reasons. + + Reasons must be given a name, description and points value. The + name of the reason must be given when a user is warned. + """ pass - @warnreason.command(name="add") + @warnreason.command(name="create", aliases=["add"]) @commands.guild_only() - async def reason_add(self, ctx: commands.Context, name: str, points: int, *, description: str): - """Add a reason to be available for warnings""" + async def reason_create( + self, ctx: commands.Context, name: str, points: int, *, description: str + ): + """Create a warning reason.""" guild = ctx.guild if name.lower() == "custom": - await ctx.send("That cannot be used as a reason name!") + await ctx.send(_("*Custom* cannot be used as a reason name!")) return to_add = {"points": points, "description": description} completed = {name.lower(): to_add} @@ -142,12 +159,12 @@ class Warnings(commands.Cog): async with guild_settings.reasons() as registered_reasons: registered_reasons.update(completed) - await ctx.send(_("That reason has been registered.")) + await ctx.send(_("The new reason has been registered.")) - @warnreason.command(name="del") + @warnreason.command(name="del", aliases=["remove"]) @commands.guild_only() async def reason_del(self, ctx: commands.Context, reason_name: str): - """Delete the reason with the specified name""" + """Delete a warning reason.""" guild = ctx.guild guild_settings = self.config.guild(guild) async with guild_settings.reasons() as registered_reasons: @@ -160,7 +177,7 @@ class Warnings(commands.Cog): @commands.guild_only() @checks.admin_or_permissions(ban_members=True) async def reasonlist(self, ctx: commands.Context): - """List all configured reasons for warnings""" + """List all configured reasons for Warnings.""" guild = ctx.guild guild_settings = self.config.guild(guild) msg_list = [] @@ -174,9 +191,9 @@ class Warnings(commands.Cog): msg_list.append(em) else: msg_list.append( - "Name: {}\nPoints: {}\nDescription: {}".format( - r, v["points"], v["description"] - ) + _( + "Name: {reason_name}\nPoints: {points}\nDescription: {description}" + ).format(reason_name=r, **v) ) if msg_list: await menu(ctx, msg_list, DEFAULT_CONTROLS) @@ -187,7 +204,7 @@ class Warnings(commands.Cog): @commands.guild_only() @checks.admin_or_permissions(ban_members=True) async def actionlist(self, ctx: commands.Context): - """List the actions to be taken at specific point values""" + """List all configured automated actions for Warnings.""" guild = ctx.guild guild_settings = self.config.guild(guild) msg_list = [] @@ -201,10 +218,10 @@ class Warnings(commands.Cog): msg_list.append(em) else: msg_list.append( - "Name: {}\nPoints: {}\nExceed command: {}\n" - "Drop command: {}".format( - r["action_name"], r["points"], r["exceed_command"], r["drop_command"] - ) + _( + "Name: {action_name}\nPoints: {points}\n" + "Exceed command: {exceed_command}\nDrop command: {drop_command}" + ).format(**r) ) if msg_list: await menu(ctx, msg_list, DEFAULT_CONTROLS) @@ -215,8 +232,10 @@ class Warnings(commands.Cog): @commands.guild_only() @checks.admin_or_permissions(ban_members=True) async def warn(self, ctx: commands.Context, user: discord.Member, reason: str): - """Warn the user for the specified reason - Reason must be a registered reason, or "custom" if custom reasons are allowed + """Warn the user for the specified reason. + + `` must be a registered reason name, or *custom* if + custom reasons are enabled. """ if user == ctx.author: await ctx.send(_("You cannot warn yourself.")) @@ -226,9 +245,9 @@ class Warnings(commands.Cog): if not custom_allowed: await ctx.send( _( - "Custom reasons are not allowed! Please see {} for " + "Custom reasons are not allowed! Please see `{prefix}reasonlist` for " "a complete list of valid reasons." - ).format("`{}reasonlist`".format(ctx.prefix)) + ).format(prefix=ctx.prefix) ) return reason_type = await self.custom_warning_reason(ctx) @@ -272,9 +291,7 @@ class Warnings(commands.Cog): await warning_points_add_check(self.config, ctx, user, current_point_count) try: em = discord.Embed( - title=_("Warning from {mod_name}#{mod_discrim}").format( - mod_name=ctx.author.display_name, mod_discrim=ctx.author.discriminator - ), + title=_("Warning from {user}").format(user=ctx.author), description=reason_type["description"], ) em.add_field(name=_("Points"), value=str(reason_type["points"])) @@ -286,19 +303,17 @@ class Warnings(commands.Cog): ) except discord.HTTPException: pass - await ctx.send( - _("User {user_name}#{user_discrim} has been warned.").format( - user_name=user.display_name, user_discrim=user.discriminator - ) - ) + await ctx.send(_("User {user} has been warned.").format(user=user)) @commands.command() @commands.guild_only() async def warnings(self, ctx: commands.Context, userid: int = None): - """Show warnings for the specified user. - If userid is None, show warnings for the person running the command + """List the warnings for the specified user. + + Emit `` to see your own warnings. + Note that showing warnings for users other than yourself requires - appropriate permissions + appropriate permissions. """ if userid is None: user = ctx.author @@ -326,18 +341,24 @@ class Warnings(commands.Cog): ) if mod is None: mod = await self.bot.get_user_info(user_warnings[key]["mod"]) - msg += "{} point warning {} issued by {} for {}\n".format( - user_warnings[key]["points"], key, mod, user_warnings[key]["description"] + msg += _( + "{num_points} point warning {reason_name} issued by {user} for " + "{description}\n" + ).format( + num_points=user_warnings[key]["points"], + reason_name=key, + user=mod, + description=user_warnings[key]["description"], ) await ctx.send_interactive( - pagify(msg, shorten_by=58), box_lang="Warnings for {}".format(user) + pagify(msg, shorten_by=58), box_lang=_("Warnings for {user}").format(user=user) ) @commands.command() @commands.guild_only() @checks.admin_or_permissions(ban_members=True) async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str): - """Removes the specified warning from the user specified""" + """Remove a warning from a user.""" if user_id == ctx.author.id: await ctx.send(_("You cannot remove warnings from yourself.")) return @@ -351,7 +372,7 @@ class Warnings(commands.Cog): await warning_points_remove_check(self.config, ctx, member, current_point_count) async with member_settings.warnings() as user_warnings: if warn_id not in user_warnings.keys(): - await ctx.send("That warning doesn't exist!") + await ctx.send(_("That warning doesn't exist!")) return else: current_point_count -= user_warnings[warn_id]["points"] diff --git a/redbot/core/utils/tunnel.py b/redbot/core/utils/tunnel.py index 32684a721..5b757543d 100644 --- a/redbot/core/utils/tunnel.py +++ b/redbot/core/utils/tunnel.py @@ -4,7 +4,7 @@ from redbot.core.utils.chat_formatting import pagify import io import sys import weakref -from typing import List +from typing import List, Optional from .common_filters import filter_mass_mentions _instances = weakref.WeakValueDictionary({}) @@ -86,7 +86,11 @@ class Tunnel(metaclass=TunnelMeta): @staticmethod async def message_forwarder( - *, destination: discord.abc.Messageable, content: str = None, embed=None, files=[] + *, + destination: discord.abc.Messageable, + content: str = None, + embed=None, + files: Optional[List[discord.File]] = None ) -> List[discord.Message]: """ This does the actual sending, use this instead of a full tunnel @@ -95,19 +99,19 @@ class Tunnel(metaclass=TunnelMeta): Parameters ---------- - destination: `discord.abc.Messageable` + destination: discord.abc.Messageable Where to send - content: `str` + content: str The message content - embed: `discord.Embed` + embed: discord.Embed The embed to send - files: `list` of `discord.File` + files: Optional[List[discord.File]] A list of files to send. Returns ------- - list of `discord.Message` - The `discord.Message`\ (s) sent as a result + List[discord.Message] + The messages sent as a result. Raises ------ @@ -117,7 +121,6 @@ class Tunnel(metaclass=TunnelMeta): see `discord.abc.Messageable.send` """ rets = [] - files = files if files else None if content: for page in pagify(content): rets.append(await destination.send(page, files=files, embed=embed))