diff --git a/docs/framework_commands.rst b/docs/framework_commands.rst new file mode 100644 index 000000000..57f62b6d9 --- /dev/null +++ b/docs/framework_commands.rst @@ -0,0 +1,21 @@ +.. red commands module documentation + +================ +Commands Package +================ + +This package acts almost identically to ``discord.ext.commands``; i.e. they both have the same +attributes. Some of these attributes, however, have been slightly modified, as outlined below. + +.. autofunction:: redbot.core.commands.command + +.. autofunction:: redbot.core.commands.group + +.. autoclass:: redbot.core.commands.Command + :members: + +.. autoclass:: redbot.core.commands.Group + :members: + +.. autoclass:: redbot.core.commands.Context + :members: diff --git a/docs/framework_context.rst b/docs/framework_context.rst deleted file mode 100644 index 10a8229e4..000000000 --- a/docs/framework_context.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. red invocation context documentation - -========================== -Command Invocation Context -========================== - -.. automodule:: redbot.core.context - -.. autoclass:: redbot.core.RedContext - :members: diff --git a/docs/framework_i18n.rst b/docs/framework_i18n.rst index 590d57305..86b166d4d 100644 --- a/docs/framework_i18n.rst +++ b/docs/framework_i18n.rst @@ -13,11 +13,12 @@ Basic Usage .. code-block:: python - from discord.ext import commands - from redbot.core.i18n import CogI18n + from redbot.core import commands + from redbot.core.i18n import Translator, cog_i18n - _ = CogI18n("ExampleCog", __file__) + _ = Translator("ExampleCog", __file__) + @cog_i18n(_) class ExampleCog: """description""" @@ -39,16 +40,19 @@ In a command prompt in your cog's package (where yourcog.py is), create a directory called "locales". Then do one of the following: -Windows: :code:`python \Tools\i18n\pygettext.py -n -p locales` +Windows: :code:`python \Tools\i18n\pygettext.py -D -n -p locales` Mac: ? -Linux: :code:`pygettext3 -n -p locales` +Linux: :code:`pygettext3 -D -n -p locales` -This will generate a messages.pot file with strings to be translated +This will generate a messages.pot file with strings to be translated, including +docstrings. ------------- API Reference ------------- -.. automodule:: redbot.core.i18n \ No newline at end of file +.. automodule:: redbot.core.i18n + :members: + :special-members: __call__ diff --git a/docs/index.rst b/docs/index.rst index bd5b6d0f6..068a8e773 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -37,12 +37,12 @@ Welcome to Red - Discord Bot's documentation! framework_bot framework_cogmanager framework_config - framework_context framework_datamanager framework_downloader framework_events framework_i18n framework_modlog + framework_commands framework_rpc framework_utils diff --git a/redbot/cogs/alias/alias.py b/redbot/cogs/alias/alias.py index 801f7dc2f..f6d4b239a 100644 --- a/redbot/cogs/alias/alias.py +++ b/redbot/cogs/alias/alias.py @@ -3,17 +3,17 @@ from re import search from typing import Generator, Tuple, Iterable import discord -from redbot.core import Config -from redbot.core.i18n import CogI18n +from redbot.core import Config, commands +from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.chat_formatting import box -from discord.ext import commands from redbot.core.bot import Red from .alias_entry import AliasEntry -_ = CogI18n("Alias", __file__) +_ = Translator("Alias", __file__) +@cog_i18n(_) class Alias: """ Alias diff --git a/redbot/cogs/alias/alias_entry.py b/redbot/cogs/alias/alias_entry.py index ec3c373c0..880b55a15 100644 --- a/redbot/cogs/alias/alias_entry.py +++ b/redbot/cogs/alias/alias_entry.py @@ -1,7 +1,7 @@ from typing import Tuple -from discord.ext import commands import discord +from redbot.core import commands class AliasEntry: diff --git a/redbot/cogs/audio/audio.py b/redbot/cogs/audio/audio.py index 7439230fa..1ccfd3928 100644 --- a/redbot/cogs/audio/audio.py +++ b/redbot/cogs/audio/audio.py @@ -7,15 +7,19 @@ import lavalink import math import re import redbot.core -from discord.ext import commands from redbot.core import Config, checks, bank +from redbot.core import commands +from redbot.core.i18n import Translator, cog_i18n +_ = Translator("Audio", __file__) from .manager import shutdown_lavalink_server __version__ = "0.0.5a" __author__ = ["aikaterna", "billy/bollo/ati"] + +@cog_i18n(_) class Audio: def __init__(self, bot): self.bot = bot diff --git a/redbot/cogs/bank/bank.py b/redbot/cogs/bank/bank.py index feee170b8..83d7a94dd 100644 --- a/redbot/cogs/bank/bank.py +++ b/redbot/cogs/bank/bank.py @@ -1,13 +1,12 @@ import discord from redbot.core.utils.chat_formatting import box -from redbot.core import checks, bank -from redbot.core.i18n import CogI18n -from discord.ext import commands +from redbot.core import checks, bank, commands +from redbot.core.i18n import Translator, cog_i18n from redbot.core.bot import Red # Only used for type hints -_ = CogI18n('Bank', __file__) +_ = Translator('Bank', __file__) def check_global_setting_guildowner(): @@ -48,6 +47,7 @@ def check_global_setting_admin(): return commands.check(pred) +@cog_i18n(_) class Bank: """Bank""" diff --git a/redbot/cogs/cleanup/cleanup.py b/redbot/cogs/cleanup/cleanup.py index 80e5082a7..cca65f565 100644 --- a/redbot/cogs/cleanup/cleanup.py +++ b/redbot/cogs/cleanup/cleanup.py @@ -1,17 +1,17 @@ import re import discord -from discord.ext import commands -from redbot.core import checks, RedContext +from redbot.core import checks, commands from redbot.core.bot import Red -from redbot.core.i18n import CogI18n +from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.mod import slow_deletion, mass_purge from redbot.cogs.mod.log import log -_ = CogI18n("Cleanup", __file__) +_ = Translator("Cleanup", __file__) +@cog_i18n(_) class Cleanup: """Commands for cleaning messages""" @@ -19,7 +19,7 @@ class Cleanup: self.bot = bot @staticmethod - async def check_100_plus(ctx: RedContext, number: int) -> bool: + async def check_100_plus(ctx: commands.Context, number: int) -> bool: """ Called when trying to delete more than 100 messages at once @@ -39,7 +39,7 @@ class Cleanup: @staticmethod async def get_messages_for_deletion( - ctx: RedContext, channel: discord.TextChannel, number, + ctx: commands.Context, channel: discord.TextChannel, number, check=lambda x: True, limit=100, before=None, after=None ) -> list: """ @@ -75,7 +75,7 @@ class Cleanup: @commands.group() @checks.mod_or_permissions(manage_messages=True) - async def cleanup(self, ctx: RedContext): + async def cleanup(self, ctx: commands.Context): """Deletes messages.""" if ctx.invoked_subcommand is None: await ctx.send_help() @@ -83,7 +83,7 @@ class Cleanup: @cleanup.command() @commands.guild_only() @commands.bot_has_permissions(manage_messages=True) - async def text(self, ctx: RedContext, text: str, number: int): + async def text(self, ctx: commands.Context, text: str, number: int): """Deletes last X messages matching the specified text. Example: @@ -94,12 +94,12 @@ class Cleanup: channel = ctx.channel author = ctx.author is_bot = self.bot.user.bot - + if number > 100: cont = await self.check_100_plus(ctx, number) if not cont: return - + def check(m): if text in m.content: return True @@ -124,7 +124,7 @@ class Cleanup: @cleanup.command() @commands.guild_only() @commands.bot_has_permissions(manage_messages=True) - async def user(self, ctx: RedContext, user: str, number: int): + async def user(self, ctx: commands.Context, user: str, number: int): """Deletes last X messages from specified user. Examples: @@ -176,7 +176,7 @@ class Cleanup: @cleanup.command() @commands.guild_only() @commands.bot_has_permissions(manage_messages=True) - async def after(self, ctx: RedContext, message_id: int): + async def after(self, ctx: commands.Context, message_id: int): """Deletes all messages after specified message. To get a message id, enable developer mode in Discord's @@ -215,7 +215,7 @@ class Cleanup: @cleanup.command() @commands.guild_only() @commands.bot_has_permissions(manage_messages=True) - async def messages(self, ctx: RedContext, number: int): + async def messages(self, ctx: commands.Context, number: int): """Deletes last X messages. Example: @@ -225,7 +225,7 @@ class Cleanup: author = ctx.author is_bot = self.bot.user.bot - + if number > 100: cont = await self.check_100_plus(ctx, number) if not cont: @@ -248,7 +248,7 @@ class Cleanup: @cleanup.command(name='bot') @commands.guild_only() @commands.bot_has_permissions(manage_messages=True) - async def cleanup_bot(self, ctx: RedContext, number: int): + async def cleanup_bot(self, ctx: commands.Context, number: int): """Cleans up command messages and messages from the bot.""" channel = ctx.message.channel @@ -295,7 +295,7 @@ class Cleanup: await slow_deletion(to_delete) @cleanup.command(name='self') - async def cleanup_self(self, ctx: RedContext, number: int, match_pattern: str = None): + async def cleanup_self(self, ctx: commands.Context, number: int, match_pattern: str = None): """Cleans up messages owned by the bot. By default, all messages are cleaned. If a third argument is specified, diff --git a/redbot/cogs/customcom/customcom.py b/redbot/cogs/customcom/customcom.py index cc0bc9904..73adac26d 100644 --- a/redbot/cogs/customcom/customcom.py +++ b/redbot/cogs/customcom/customcom.py @@ -4,13 +4,12 @@ import random from datetime import datetime import discord -from discord.ext import commands -from redbot.core import Config, checks +from redbot.core import Config, checks, commands from redbot.core.utils.chat_formatting import box, pagify -from redbot.core.i18n import CogI18n +from redbot.core.i18n import Translator, cog_i18n -_ = CogI18n("CustomCommands", __file__) +_ = Translator("CustomCommands", __file__) class CCError(Exception): @@ -152,6 +151,7 @@ class CommandObj: command, value=None) +@cog_i18n(_) class CustomCommands: """Custom commands Creates commands used to display text""" diff --git a/redbot/cogs/dataconverter/dataconverter.py b/redbot/cogs/dataconverter/dataconverter.py index 37de1d9c7..322302df7 100644 --- a/redbot/cogs/dataconverter/dataconverter.py +++ b/redbot/cogs/dataconverter/dataconverter.py @@ -1,17 +1,16 @@ from pathlib import Path import asyncio -from discord.ext import commands - -from redbot.core import checks, RedContext +from redbot.core import checks, commands from redbot.core.bot import Red -from redbot.core.i18n import CogI18n +from redbot.core.i18n import Translator, cog_i18n from redbot.cogs.dataconverter.core_specs import SpecResolver from redbot.core.utils.chat_formatting import box -_ = CogI18n('DataConverter', __file__) +_ = Translator('DataConverter', __file__) +@cog_i18n(_) class DataConverter: """ Cog for importing Red v2 Data @@ -22,7 +21,7 @@ class DataConverter: @checks.is_owner() @commands.command(name="convertdata") - async def dataconversioncommand(self, ctx: RedContext, v2path: str): + async def dataconversioncommand(self, ctx: commands.Context, v2path: str): """ Interactive prompt for importing data from Red v2 diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py index a599639c3..dc84a63a8 100644 --- a/redbot/cogs/downloader/downloader.py +++ b/redbot/cogs/downloader/downloader.py @@ -10,9 +10,9 @@ import sys from redbot.core import Config from redbot.core import checks from redbot.core.data_manager import cog_data_path -from redbot.core.i18n import CogI18n +from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.chat_formatting import box, pagify -from discord.ext import commands +from redbot.core import commands from redbot.core.bot import Red from .checks import install_agreement @@ -22,9 +22,10 @@ from .installable import Installable from .log import log from .repo_manager import RepoManager, Repo -_ = CogI18n('Downloader', __file__) +_ = Translator('Downloader', __file__) +@cog_i18n(_) class Downloader: def __init__(self, bot: Red): self.bot = bot diff --git a/redbot/cogs/economy/economy.py b/redbot/cogs/economy/economy.py index 88251fd22..5a9e1f5e1 100644 --- a/redbot/cogs/economy/economy.py +++ b/redbot/cogs/economy/economy.py @@ -7,14 +7,13 @@ from enum import Enum import discord from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin -from redbot.core import Config, bank -from redbot.core.i18n import CogI18n +from redbot.core import Config, bank, commands +from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.chat_formatting import pagify, box -from discord.ext import commands from redbot.core.bot import Red -_ = CogI18n("Economy", __file__) +_ = Translator("Economy", __file__) logger = logging.getLogger("red.economy") @@ -104,6 +103,7 @@ class SetParser: raise RuntimeError +@cog_i18n(_) class Economy: """Economy diff --git a/redbot/cogs/filter/filter.py b/redbot/cogs/filter/filter.py index b5fe1e3cb..2b03e6d8a 100644 --- a/redbot/cogs/filter/filter.py +++ b/redbot/cogs/filter/filter.py @@ -1,15 +1,15 @@ import discord -from discord.ext import commands -from redbot.core import checks, Config, modlog, RedContext +from redbot.core import checks, Config, modlog, commands from redbot.core.bot import Red -from redbot.core.i18n import CogI18n +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 -_ = CogI18n("Filter", __file__) +_ = Translator("Filter", __file__) +@cog_i18n(_) class Filter: """Filter-related commands""" @@ -46,7 +46,7 @@ class Filter: @commands.group(name="filter") @commands.guild_only() @checks.mod_or_permissions(manage_messages=True) - async def _filter(self, ctx: RedContext): + async def _filter(self, ctx: commands.Context): """Adds/removes words from filter Use double quotes to add/remove sentences @@ -129,7 +129,7 @@ class Filter: await ctx.send(_("Those words weren't in the filter.")) @_filter.command(name="names") - async def filter_names(self, ctx: RedContext): + async def filter_names(self, ctx: commands.Context): """ Toggles whether or not to check names and nicknames against the filter This is disabled by default @@ -149,7 +149,7 @@ class Filter: ) @_filter.command(name="defaultname") - async def filter_default_name(self, ctx: RedContext, name: str): + async def filter_default_name(self, ctx: commands.Context, name: str): """ Sets the default name to use if filtering names is enabled Note that this has no effect if filtering names is disabled diff --git a/redbot/cogs/general/general.py b/redbot/cogs/general/general.py index 88d3671ca..a4f01ce16 100644 --- a/redbot/cogs/general/general.py +++ b/redbot/cogs/general/general.py @@ -6,12 +6,12 @@ from urllib.parse import quote_plus import aiohttp import discord -from redbot.core.i18n import CogI18n -from discord.ext import commands +from redbot.core import commands +from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.chat_formatting import escape, italics, pagify -_ = CogI18n("General", __file__) +_ = Translator("General", __file__) class RPS(Enum): @@ -33,6 +33,7 @@ class RPSParser: raise +@cog_i18n(_) class General: """General commands.""" diff --git a/redbot/cogs/image/image.py b/redbot/cogs/image/image.py index 6bacdf5fd..d52d9d29a 100644 --- a/redbot/cogs/image/image.py +++ b/redbot/cogs/image/image.py @@ -1,16 +1,16 @@ from random import shuffle import aiohttp -from discord.ext import commands -from redbot.core.i18n import CogI18n -from redbot.core import checks, Config +from redbot.core.i18n import Translator, cog_i18n +from redbot.core import checks, Config, commands -_ = CogI18n("Image", __file__) +_ = Translator("Image", __file__) GIPHY_API_KEY = "dc6zaTOxFJmzC" +@cog_i18n(_) class Image: """Image related commands.""" default_global = { diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py index b8014e0cf..8e2155274 100644 --- a/redbot/cogs/mod/mod.py +++ b/redbot/cogs/mod/mod.py @@ -3,21 +3,20 @@ from datetime import datetime, timedelta from collections import deque, defaultdict, namedtuple import discord -from discord.ext import commands -from redbot.core import checks, Config, modlog, RedContext +from redbot.core import checks, Config, modlog, commands from redbot.core.bot import Red -from redbot.core.i18n import CogI18n +from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.chat_formatting import box, escape from .checks import mod_or_voice_permissions, admin_or_voice_permissions, bot_has_voice_permissions from redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, \ get_audit_reason from .log import log -_ = CogI18n("Mod", __file__) - +_ = Translator("Mod", __file__) +@cog_i18n(_) class Mod: """Moderation tools.""" @@ -174,7 +173,7 @@ class Mod: @commands.group() @commands.guild_only() @checks.guildowner_or_permissions(administrator=True) - async def modset(self, ctx: RedContext): + async def modset(self, ctx: commands.Context): """Manages server administration settings.""" if ctx.invoked_subcommand is None: guild = ctx.guild @@ -200,7 +199,7 @@ class Mod: @modset.command() @commands.guild_only() - async def hierarchy(self, ctx: RedContext): + async def hierarchy(self, ctx: commands.Context): """Toggles role hierarchy check for mods / admins""" guild = ctx.guild toggled = await self.settings.guild(guild).respect_hierarchy() @@ -215,7 +214,7 @@ class Mod: @modset.command() @commands.guild_only() - async def banmentionspam(self, ctx: RedContext, max_mentions: int=False): + async def banmentionspam(self, ctx: commands.Context, max_mentions: int=False): """Enables auto ban for messages mentioning X different people Accepted values: 5 or superior""" @@ -240,7 +239,7 @@ class Mod: @modset.command() @commands.guild_only() - async def deleterepeats(self, ctx: RedContext): + async def deleterepeats(self, ctx: commands.Context): """Enables auto deletion of repeated messages""" guild = ctx.guild cur_setting = await self.settings.guild(guild).delete_repeats() @@ -254,7 +253,7 @@ class Mod: @modset.command() @commands.guild_only() - async def deletedelay(self, ctx: RedContext, time: int=None): + async def deletedelay(self, ctx: commands.Context, time: int=None): """Sets the delay until the bot removes the command message. Must be between -1 and 60. @@ -281,7 +280,7 @@ class Mod: @modset.command() @commands.guild_only() - async def reinvite(self, ctx: RedContext): + async def reinvite(self, ctx: commands.Context): """Toggles whether an invite will be sent when a user is unbanned via [p]unban. If this is True, the bot will attempt to create and send a single-use invite @@ -298,7 +297,7 @@ class Mod: @commands.command() @commands.guild_only() @checks.admin_or_permissions(kick_members=True) - async def kick(self, ctx: RedContext, user: discord.Member, *, reason: str = None): + async def kick(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): """Kicks user. If a reason is specified, it will be the reason that shows up @@ -338,7 +337,7 @@ class Mod: @commands.command() @commands.guild_only() @checks.admin_or_permissions(ban_members=True) - async def ban(self, ctx: RedContext, user: discord.Member, days: str = None, *, reason: str = None): + 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. If days is not a number, it's treated as the first word of the reason. @@ -399,7 +398,7 @@ class Mod: @commands.command() @commands.guild_only() @checks.admin_or_permissions(ban_members=True) - async def hackban(self, ctx: RedContext, user_id: int, *, reason: str = None): + async def hackban(self, ctx: commands.Context, user_id: int, *, reason: str = None): """Preemptively bans user from the server A user ID needs to be provided in order to ban @@ -452,7 +451,7 @@ class Mod: @commands.command() @commands.guild_only() @checks.admin_or_permissions(ban_members=True) - async def tempban(self, ctx: RedContext, user: discord.Member, days: int=1, *, reason: str=None): + 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""" guild = ctx.guild author = ctx.author @@ -500,7 +499,7 @@ class Mod: @commands.command() @commands.guild_only() @checks.admin_or_permissions(ban_members=True) - async def softban(self, ctx: RedContext, user: discord.Member, *, reason: str = None): + async def softban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): """Kicks the user, deleting 1 day worth of messages.""" guild = ctx.guild channel = ctx.channel @@ -579,7 +578,7 @@ class Mod: @commands.guild_only() @checks.admin_or_permissions(ban_members=True) @commands.bot_has_permissions(ban_members=True) - async def unban(self, ctx: RedContext, user_id: int, *, reason: str = None): + async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = None): """Unbans the target user. Requires specifying the target user's ID. To find this, you may either: @@ -637,7 +636,7 @@ class Mod: .format(invite.url)) @staticmethod - async def get_invite_for_reinvite(ctx: RedContext, max_age: int=86400): + async def get_invite_for_reinvite(ctx: commands.Context, max_age: int=86400): """Handles the reinvite logic for getting an invite to send the newly unbanned user :returns: :class:`Invite`""" @@ -672,7 +671,7 @@ class Mod: @commands.guild_only() @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: RedContext, user: discord.Member, *, reason: str=None): + 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""" user_voice_state = user.voice if user_voice_state is None: @@ -709,7 +708,7 @@ class Mod: @commands.guild_only() @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: RedContext, user: discord.Member, *, reason: str=None): + 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""" user_voice_state = user.voice if user_voice_state is None: @@ -743,7 +742,7 @@ class Mod: @commands.command() @commands.guild_only() @checks.admin_or_permissions(manage_nicknames=True) - async def rename(self, ctx: RedContext, user: discord.Member, *, nickname=""): + async def rename(self, ctx: commands.Context, user: discord.Member, *, nickname=""): """Changes user's nickname Leaving the nickname empty will remove it.""" @@ -763,7 +762,7 @@ class Mod: @commands.group() @commands.guild_only() @checks.mod_or_permissions(manage_channel=True) - async def mute(self, ctx: RedContext): + async def mute(self, ctx: commands.Context): """Mutes user in the channel/server""" if ctx.invoked_subcommand is None: await ctx.send_help() @@ -772,7 +771,7 @@ class Mod: @commands.guild_only() @mod_or_voice_permissions(mute_members=True) @bot_has_voice_permissions(mute_members=True) - async def voice_mute(self, ctx: RedContext, user: discord.Member, + async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): """Mutes the user in a voice channel""" user_voice_state = user.voice @@ -811,7 +810,7 @@ class Mod: @checks.mod_or_permissions(administrator=True) @mute.command(name="channel") @commands.guild_only() - async def channel_mute(self, ctx: RedContext, user: discord.Member, *, reason: str = None): + async def channel_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): """Mutes user in the current channel""" author = ctx.message.author channel = ctx.message.channel @@ -839,7 +838,7 @@ class Mod: @checks.mod_or_permissions(administrator=True) @mute.command(name="server", aliases=["guild"]) @commands.guild_only() - async def guild_mute(self, ctx: RedContext, user: discord.Member, *, reason: str = None): + 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 @@ -902,7 +901,7 @@ class Mod: @commands.group() @commands.guild_only() @checks.mod_or_permissions(manage_channel=True) - async def unmute(self, ctx: RedContext): + async def unmute(self, ctx: commands.Context): """Unmutes user in the channel/server Defaults to channel""" @@ -913,7 +912,7 @@ class Mod: @commands.guild_only() @mod_or_voice_permissions(mute_members=True) @bot_has_voice_permissions(mute_members=True) - async def voice_unmute(self, ctx: RedContext, user: discord.Member, *, reason: str = None): + async def voice_unmute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): """Unmutes the user in a voice channel""" user_voice_state = user.voice if user_voice_state: @@ -947,7 +946,7 @@ class Mod: @checks.mod_or_permissions(administrator=True) @unmute.command(name="channel") @commands.guild_only() - async def channel_unmute(self, ctx: RedContext, user: discord.Member, *, reason: str=None): + async def channel_unmute(self, ctx: commands.Context, user: discord.Member, *, reason: str=None): """Unmutes user in the current channel""" channel = ctx.channel author = ctx.author @@ -970,7 +969,7 @@ class Mod: @checks.mod_or_permissions(administrator=True) @unmute.command(name="server", aliases=["guild"]) @commands.guild_only() - async def guild_unmute(self, ctx: RedContext, user: discord.Member, *, reason: str=None): + async def guild_unmute(self, ctx: commands.Context, user: discord.Member, *, reason: str=None): """Unmutes user in the server""" guild = ctx.guild author = ctx.author @@ -1038,14 +1037,14 @@ class Mod: @commands.group() @commands.guild_only() @checks.admin_or_permissions(manage_channels=True) - async def ignore(self, ctx: RedContext): + async def ignore(self, ctx: commands.Context): """Adds servers/channels to ignorelist""" if ctx.invoked_subcommand is None: await ctx.send_help() await ctx.send(await self.count_ignored()) @ignore.command(name="channel") - async def ignore_channel(self, ctx: RedContext, channel: discord.TextChannel=None): + async def ignore_channel(self, ctx: commands.Context, channel: discord.TextChannel=None): """Ignores channel Defaults to current one""" @@ -1058,7 +1057,8 @@ class Mod: await ctx.send(_("Channel already in ignore list.")) @ignore.command(name="server", aliases=["guild"]) - async def ignore_guild(self, ctx: RedContext): + @commands.has_permissions(manage_guild=True) + async def ignore_guild(self, ctx: commands.Context): """Ignores current server""" guild = ctx.guild if not await self.settings.guild(guild).ignored(): @@ -1070,14 +1070,14 @@ class Mod: @commands.group() @commands.guild_only() @checks.admin_or_permissions(manage_channels=True) - async def unignore(self, ctx: RedContext): + async def unignore(self, ctx: commands.Context): """Removes servers/channels from ignorelist""" if ctx.invoked_subcommand is None: await ctx.send_help() await ctx.send(await self.count_ignored()) @unignore.command(name="channel") - async def unignore_channel(self, ctx: RedContext, channel: discord.TextChannel=None): + async def unignore_channel(self, ctx: commands.Context, channel: discord.TextChannel=None): """Removes channel from ignore list Defaults to current one""" @@ -1091,7 +1091,8 @@ class Mod: await ctx.send(_("That channel is not in the ignore list.")) @unignore.command(name="server", aliases=["guild"]) - async def unignore_guild(self, ctx: RedContext): + @commands.has_permissions(manage_guild=True) + async def unignore_guild(self, ctx: commands.Context): """Removes current guild from ignore list""" guild = ctx.message.guild if await self.settings.guild(guild).ignored(): @@ -1131,11 +1132,8 @@ class Mod: chann_ignored and not perms.manage_channels) @commands.command() - async def names(self, ctx: RedContext, user: discord.Member): + async def names(self, ctx: commands.Context, user: discord.Member): """Show previous names/nicknames of a user""" - async with self.settings.user(user).past_names() as name_list: - while None in name_list: # clean out null entries from a bug - name_list.remove(None) names = await self.settings.user(user).past_names() nicks = await self.settings.member(user).past_nicks() msg = "" @@ -1225,7 +1223,7 @@ class Mod: return True return False - async def on_command(self, ctx: RedContext): + async def on_command(self, ctx: commands.Context): """Currently used for: * delete delay""" guild = ctx.guild @@ -1359,15 +1357,13 @@ class Mod: if entry.target == target: return entry - async def on_member_update(self, before: discord.Member, after: discord.Member): + async def on_member_update(self, before, after): if before.name != after.name: async with self.settings.user(before).past_names() as name_list: - while None in name_list: # clean out null entries from a bug - name_list.remove(None) - if after.name in name_list: + if after.nick in name_list: # Ensure order is maintained without duplicates occuring - name_list.remove(after.name) - name_list.append(after.name) + name_list.remove(after.nick) + name_list.append(after.nick) while len(name_list) > 20: name_list.pop(0) diff --git a/redbot/cogs/modlog/modlog.py b/redbot/cogs/modlog/modlog.py index 69319381a..c42747a49 100644 --- a/redbot/cogs/modlog/modlog.py +++ b/redbot/cogs/modlog/modlog.py @@ -1,14 +1,14 @@ import discord -from discord.ext import commands -from redbot.core import checks, modlog, RedContext +from redbot.core import checks, modlog, commands from redbot.core.bot import Red -from redbot.core.i18n import CogI18n +from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.chat_formatting import box -_ = CogI18n('ModLog', __file__) +_ = Translator('ModLog', __file__) +@cog_i18n(_) class ModLog: """Log for mod actions""" @@ -17,14 +17,14 @@ class ModLog: @commands.group() @checks.guildowner_or_permissions(administrator=True) - async def modlogset(self, ctx: RedContext): + async def modlogset(self, ctx: commands.Context): """Settings for the mod log""" if ctx.invoked_subcommand is None: await ctx.send_help() @modlogset.command() @commands.guild_only() - async def modlog(self, ctx: RedContext, channel: discord.TextChannel = None): + async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None): """Sets a channel as mod log Leaving the channel parameter empty will deactivate it""" @@ -53,7 +53,7 @@ class ModLog: @modlogset.command(name='cases') @commands.guild_only() - async def set_cases(self, ctx: RedContext, action: str = None): + async def set_cases(self, ctx: commands.Context, action: str = None): """Enables or disables case creation for each type of mod action""" guild = ctx.guild @@ -87,7 +87,7 @@ class ModLog: @modlogset.command() @commands.guild_only() - async def resetcases(self, ctx: RedContext): + async def resetcases(self, ctx: commands.Context): """Resets modlog's cases""" guild = ctx.guild await modlog.reset_cases(guild) @@ -95,7 +95,7 @@ class ModLog: @commands.command() @commands.guild_only() - async def case(self, ctx: RedContext, number: int): + async def case(self, ctx: commands.Context, number: int): """Shows the specified case""" try: case = await modlog.get_case(number, ctx.guild, self.bot) @@ -107,7 +107,7 @@ class ModLog: @commands.command() @commands.guild_only() - async def reason(self, ctx: RedContext, case: int, *, reason: str = ""): + async def reason(self, ctx: commands.Context, case: int, *, reason: str = ""): """Lets you specify a reason for mod-log's cases Please note that you can only edit cases you are the owner of unless you are a mod/admin or the server owner""" diff --git a/redbot/cogs/reports/reports.py b/redbot/cogs/reports/reports.py index 337636c13..d9f4fd290 100644 --- a/redbot/cogs/reports/reports.py +++ b/redbot/cogs/reports/reports.py @@ -5,21 +5,21 @@ from datetime import timedelta from copy import copy import contextlib import discord -from discord.ext import commands -from redbot.core import Config, checks, RedContext +from redbot.core import Config, checks, commands from redbot.core.utils.chat_formatting import pagify, box from redbot.core.utils.antispam import AntiSpam from redbot.core.bot import Red -from redbot.core.i18n import CogI18n +from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.tunnel import Tunnel -_ = CogI18n("Reports", __file__) +_ = Translator("Reports", __file__) log = logging.getLogger("red.reports") +@cog_i18n(_) class Reports: default_guild_settings = { @@ -66,7 +66,7 @@ class Reports: @checks.admin_or_permissions(manage_guild=True) @commands.guild_only() @commands.group(name="reportset") - async def reportset(self, ctx: RedContext): + async def reportset(self, ctx: commands.Context): """ settings for reports """ @@ -74,14 +74,14 @@ class Reports: @checks.admin_or_permissions(manage_guild=True) @reportset.command(name="output") - async def setoutput(self, ctx: RedContext, channel: discord.TextChannel): + async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel): """sets the output channel""" await self.config.guild(ctx.guild).output_channel.set(channel.id) await ctx.send(_("Report Channel Set.")) @checks.admin_or_permissions(manage_guild=True) @reportset.command(name="toggleactive") - async def report_toggle(self, ctx: RedContext): + async def report_toggle(self, ctx: commands.Context): """Toggles whether the Reporting tool is enabled or not""" active = await self.config.guild(ctx.guild).active() @@ -136,7 +136,6 @@ class Reports: shared_guilds.append(guild) if len(shared_guilds) == 0: raise ValueError("No Qualifying Shared Guilds") - return if len(shared_guilds) == 1: return shared_guilds[0] output = "" @@ -216,7 +215,7 @@ class Reports: return ticket_number @commands.group(name="report", invoke_without_command=True) - async def report(self, ctx: RedContext, *, _report: str=""): + async def report(self, ctx: commands.Context, *, _report: str=""): """ Follow the prompts to make a report diff --git a/redbot/cogs/streams/streams.py b/redbot/cogs/streams/streams.py index c51a280a1..e87ce8ab4 100644 --- a/redbot/cogs/streams/streams.py +++ b/redbot/cogs/streams/streams.py @@ -1,9 +1,8 @@ import discord -from discord.ext import commands -from redbot.core import Config, checks, RedContext +from redbot.core import Config, checks, commands from redbot.core.utils.chat_formatting import pagify from redbot.core.bot import Red -from redbot.core.i18n import CogI18n +from redbot.core.i18n import Translator, cog_i18n from .streamtypes import TwitchStream, HitboxStream, MixerStream, PicartoStream, TwitchCommunity, YoutubeStream from .errors import (OfflineStream, StreamNotFound, APIError, InvalidYoutubeCredentials, CommunityNotFound, OfflineCommunity, StreamsError, InvalidTwitchCredentials) @@ -15,9 +14,10 @@ import re CHECK_DELAY = 60 -_ = CogI18n("Streams", __file__) +_ = Translator("Streams", __file__) +@cog_i18n(_) class Streams: global_defaults = { @@ -64,7 +64,7 @@ class Streams: self.task = self.bot.loop.create_task(self._stream_alerts()) @commands.command() - async def twitch(self, ctx, channel_name: str): + async def twitch(self, ctx: commands.Context, channel_name: str): """Checks if a Twitch channel is streaming""" token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None) stream = TwitchStream(name=channel_name, @@ -72,7 +72,7 @@ class Streams: await self.check_online(ctx, stream) @commands.command() - async def youtube(self, ctx, channel_id_or_name: str): + async def youtube(self, ctx: commands.Context, channel_id_or_name: str): """ Checks if a Youtube channel is streaming """ @@ -85,24 +85,24 @@ class Streams: await self.check_online(ctx, stream) @commands.command() - async def hitbox(self, ctx, channel_name: str): + async def hitbox(self, ctx: commands.Context, channel_name: str): """Checks if a Hitbox channel is streaming""" stream = HitboxStream(name=channel_name) await self.check_online(ctx, stream) @commands.command() - async def mixer(self, ctx, channel_name: str): + async def mixer(self, ctx: commands.Context, channel_name: str): """Checks if a Mixer channel is streaming""" stream = MixerStream(name=channel_name) await self.check_online(ctx, stream) @commands.command() - async def picarto(self, ctx, channel_name: str): + async def picarto(self, ctx: commands.Context, channel_name: str): """Checks if a Picarto channel is streaming""" stream = PicartoStream(name=channel_name) await self.check_online(ctx, stream) - async def check_online(self, ctx, stream): + async def check_online(self, ctx: commands.Context, stream): try: embed = await stream.is_online() except OfflineStream: @@ -124,49 +124,49 @@ class Streams: @commands.group() @commands.guild_only() @checks.mod() - async def streamalert(self, ctx): + async def streamalert(self, ctx: commands.Context): if ctx.invoked_subcommand is None: await ctx.send_help() @streamalert.group(name="twitch") - async def _twitch(self, ctx): + async def _twitch(self, ctx: commands.Context): """Twitch stream alerts""" if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self._twitch: await ctx.send_help() @_twitch.command(name="channel") - async def twitch_alert_channel(self, ctx: RedContext, channel_name: str): + async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str): """Sets a Twitch stream alert notification in the channel""" await self.stream_alert(ctx, TwitchStream, channel_name.lower()) @_twitch.command(name="community") - async def twitch_alert_community(self, ctx: RedContext, community: str): + async def twitch_alert_community(self, ctx: commands.Context, community: str): """Sets a Twitch stream alert notification in the channel for the specified community.""" await self.community_alert(ctx, TwitchCommunity, community.lower()) @streamalert.command(name="youtube") - async def youtube_alert(self, ctx: RedContext, channel_name_or_id: str): + async def youtube_alert(self, ctx: commands.Context, channel_name_or_id: str): """Sets a Youtube stream alert notification in the channel""" await self.stream_alert(ctx, YoutubeStream, channel_name_or_id) @streamalert.command(name="hitbox") - async def hitbox_alert(self, ctx, channel_name: str): + async def hitbox_alert(self, ctx: commands.Context, channel_name: str): """Sets a Hitbox stream alert notification in the channel""" await self.stream_alert(ctx, HitboxStream, channel_name) @streamalert.command(name="mixer") - async def mixer_alert(self, ctx, channel_name: str): + async def mixer_alert(self, ctx: commands.Context, channel_name: str): """Sets a Mixer stream alert notification in the channel""" await self.stream_alert(ctx, MixerStream, channel_name) @streamalert.command(name="picarto") - async def picarto_alert(self, ctx, channel_name: str): + async def picarto_alert(self, ctx: commands.Context, channel_name: str): """Sets a Picarto stream alert notification in the channel""" await self.stream_alert(ctx, PicartoStream, channel_name) @streamalert.command(name="stop") - async def streamalert_stop(self, ctx, _all: bool=False): + 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""" @@ -197,7 +197,7 @@ class Streams: await ctx.send(msg) @streamalert.command(name="list") - async def streamalert_list(self, ctx): + async def streamalert_list(self, ctx: commands.Context): streams_list = defaultdict(list) guild_channels_ids = [c.id for c in ctx.guild.channels] msg = _("Active stream alerts:\n\n") @@ -218,7 +218,7 @@ class Streams: for page in pagify(msg): await ctx.send(page) - async def stream_alert(self, ctx, _class, channel_name): + async def stream_alert(self, ctx: commands.Context, _class, channel_name): stream = self.get_stream(_class, channel_name) if not stream: token = await self.db.tokens.get_raw(_class.__name__, default=None) @@ -251,7 +251,7 @@ class Streams: await self.add_or_remove(ctx, stream) - async def community_alert(self, ctx, _class, community_name): + async def community_alert(self, ctx: commands.Context, _class, community_name): community = self.get_community(_class, community_name) if not community: token = await self.db.tokens.get_raw(_class.__name__, default=None) @@ -278,13 +278,13 @@ class Streams: @commands.group() @checks.mod() - async def streamset(self, ctx): + async def streamset(self, ctx: commands.Context): if ctx.invoked_subcommand is None: await ctx.send_help() @streamset.command() @checks.is_owner() - async def twitchtoken(self, ctx, token: str): + async def twitchtoken(self, ctx: commands.Context, token: str): """Set the Client ID for twitch. To do this, follow these steps: @@ -302,7 +302,7 @@ class Streams: @streamset.command() @checks.is_owner() - async def youtubekey(self, ctx: RedContext, key: str): + async def youtubekey(self, ctx: commands.Context, key: str): """Sets the API key for Youtube. To get one, do the following: @@ -318,14 +318,14 @@ class Streams: @streamset.group() @commands.guild_only() - async def mention(self, ctx): + async def mention(self, ctx: commands.Context): """Sets mentions for stream alerts.""" if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.mention: await ctx.send_help() @mention.command(aliases=["everyone"]) @commands.guild_only() - async def all(self, ctx): + async def all(self, ctx: commands.Context): """Toggles everyone mention""" guild = ctx.guild current_setting = await self.db.guild(guild).mention_everyone() @@ -340,7 +340,7 @@ class Streams: @mention.command(aliases=["here"]) @commands.guild_only() - async def online(self, ctx): + async def online(self, ctx: commands.Context): """Toggles here mention""" guild = ctx.guild current_setting = await self.db.guild(guild).mention_here() @@ -355,7 +355,7 @@ class Streams: @mention.command() @commands.guild_only() - async def role(self, ctx, *, role: discord.Role): + async def role(self, ctx: commands.Context, *, role: discord.Role): """Toggles role mention""" current_setting = await self.db.role(role).mention() if not role.mentionable: @@ -373,7 +373,7 @@ class Streams: @streamset.command() @commands.guild_only() - async def autodelete(self, ctx, on_off: bool): + async def autodelete(self, ctx: commands.Context, on_off: bool): """Toggles automatic deletion of notifications for streams that go offline""" await self.db.guild(ctx.guild).autodelete.set(on_off) if on_off: @@ -382,7 +382,7 @@ class Streams: else: await ctx.send("Notifications will never be deleted.") - async def add_or_remove(self, ctx, stream): + async def add_or_remove(self, ctx: commands.Context, stream): if ctx.channel.id not in stream.channels: stream.channels.append(ctx.channel.id) if stream not in self.streams: @@ -398,7 +398,7 @@ class Streams: await self.save_streams() - async def add_or_remove_community(self, ctx, community): + async def add_or_remove_community(self, ctx: commands.Context, community): if ctx.channel.id not in community.channels: community.channels.append(ctx.channel.id) if community not in self.communities: diff --git a/redbot/cogs/warnings/helpers.py b/redbot/cogs/warnings/helpers.py index b885230ee..6bc6664b8 100644 --- a/redbot/cogs/warnings/helpers.py +++ b/redbot/cogs/warnings/helpers.py @@ -1,16 +1,15 @@ from copy import copy -from discord.ext import commands import asyncio import inspect import discord -from redbot.core import RedContext, Config, checks -from redbot.core.i18n import CogI18n +from redbot.core import Config, checks, commands +from redbot.core.i18n import Translator -_ = CogI18n("Warnings", __file__) +_ = Translator("Warnings", __file__) -async def warning_points_add_check(config: Config, ctx: RedContext, user: discord.Member, points: int): +async def warning_points_add_check(config: Config, ctx: commands.Context, user: discord.Member, points: int): """Handles any action that needs to be taken or not based on the points""" guild = ctx.guild guild_settings = config.guild(guild) @@ -25,7 +24,7 @@ async def warning_points_add_check(config: Config, ctx: RedContext, user: discor await create_and_invoke_context(ctx, act["exceed_command"], user) -async def warning_points_remove_check(config: Config, ctx: RedContext, user: discord.Member, points: int): +async def warning_points_remove_check(config: Config, ctx: commands.Context, user: discord.Member, points: int): guild = ctx.guild guild_settings = config.guild(guild) act = {} @@ -39,10 +38,10 @@ async def warning_points_remove_check(config: Config, ctx: RedContext, user: dis await create_and_invoke_context(ctx, act["drop_command"], user) -async def create_and_invoke_context(realctx: RedContext, command_str: str, user: discord.Member): +async def create_and_invoke_context(realctx: commands.Context, command_str: str, user: discord.Member): m = copy(realctx.message) m.content = command_str.format(user=user.mention, prefix=realctx.prefix) - fctx = await realctx.bot.get_context(m, cls=RedContext) + fctx = await realctx.bot.get_context(m, cls=commands.Context) try: await realctx.bot.invoke(fctx) except (commands.CheckFailure, commands.CommandOnCooldown): @@ -69,7 +68,7 @@ def get_command_from_input(bot, userinput: str): return "{prefix}" + orig, None -async def get_command_for_exceeded_points(ctx: RedContext): +async def get_command_for_exceeded_points(ctx: commands.Context): """Gets the command to be executed when the user is at or exceeding the points threshold for the action""" await ctx.send( @@ -102,7 +101,7 @@ async def get_command_for_exceeded_points(ctx: RedContext): return command -async def get_command_for_dropping_points(ctx: RedContext): +async def get_command_for_dropping_points(ctx: commands.Context): """ Gets the command to be executed when the user drops below the points threshold diff --git a/redbot/cogs/warnings/warnings.py b/redbot/cogs/warnings/warnings.py index d1edbbc7f..bf3a1f5f9 100644 --- a/redbot/cogs/warnings/warnings.py +++ b/redbot/cogs/warnings/warnings.py @@ -1,21 +1,20 @@ from collections import namedtuple -from discord.ext import commands import discord import asyncio from redbot.cogs.warnings.helpers import warning_points_add_check, get_command_for_exceeded_points, \ get_command_for_dropping_points, warning_points_remove_check -from redbot.core import Config, modlog, checks +from redbot.core import Config, modlog, checks, commands from redbot.core.bot import Red -from redbot.core.context import RedContext -from redbot.core.i18n import CogI18n +from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.mod import is_admin_or_superior from redbot.core.utils.chat_formatting import warning, pagify -_ = CogI18n("Warnings", __file__) +_ = Translator("Warnings", __file__) +@cog_i18n(_) class Warnings: """A warning system for Red""" @@ -51,14 +50,14 @@ class Warnings: @commands.group() @commands.guild_only() @checks.guildowner_or_permissions(administrator=True) - async def warningset(self, ctx: RedContext): + async def warningset(self, ctx: commands.Context): """Warning settings""" if ctx.invoked_subcommand is None: await ctx.send_help() @warningset.command() @commands.guild_only() - async def allowcustomreasons(self, ctx: RedContext, allowed: bool): + async def allowcustomreasons(self, ctx: commands.Context, allowed: bool): """Allow or disallow custom reasons for a warning""" guild = ctx.guild await self.config.guild(guild).allow_custom_reasons.set(allowed) @@ -69,14 +68,14 @@ class Warnings: @commands.group() @commands.guild_only() @checks.guildowner_or_permissions(administrator=True) - async def warnaction(self, ctx: RedContext): + async def warnaction(self, ctx: commands.Context): """Action management""" if ctx.invoked_subcommand is None: await ctx.send_help() @warnaction.command(name="add") @commands.guild_only() - async def action_add(self, ctx: RedContext, name: str, points: int): + 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""" guild = ctx.guild @@ -125,7 +124,7 @@ class Warnings: @warnaction.command(name="del") @commands.guild_only() - async def action_del(self, ctx: RedContext, action_name: str): + async def action_del(self, ctx: commands.Context, action_name: str): """Delete the point count action with the specified name""" guild = ctx.guild guild_settings = self.config.guild(guild) @@ -146,14 +145,14 @@ class Warnings: @commands.group() @commands.guild_only() @checks.guildowner_or_permissions(administrator=True) - async def warnreason(self, ctx: RedContext): + async def warnreason(self, ctx: commands.Context): """Add reasons for warnings""" if ctx.invoked_subcommand is None: await ctx.send_help() @warnreason.command(name="add") @commands.guild_only() - async def reason_add(self, ctx: RedContext, name: str, points: int, *, description: str): + async def reason_add(self, ctx: commands.Context, name: str, points: int, *, description: str): """Add a reason to be available for warnings""" guild = ctx.guild @@ -177,7 +176,7 @@ class Warnings: @warnreason.command(name="del") @commands.guild_only() - async def reason_del(self, ctx: RedContext, reason_name: str): + async def reason_del(self, ctx: commands.Context, reason_name: str): """Delete the reason with the specified name""" guild = ctx.guild guild_settings = self.config.guild(guild) @@ -190,7 +189,7 @@ class Warnings: @commands.command() @commands.guild_only() @checks.admin_or_permissions(ban_members=True) - async def reasonlist(self, ctx: RedContext): + async def reasonlist(self, ctx: commands.Context): """List all configured reasons for warnings""" guild = ctx.guild guild_settings = self.config.guild(guild) @@ -210,7 +209,7 @@ class Warnings: @commands.command() @commands.guild_only() @checks.admin_or_permissions(ban_members=True) - async def actionlist(self, ctx: RedContext): + async def actionlist(self, ctx: commands.Context): """List the actions to be taken at specific point values""" guild = ctx.guild guild_settings = self.config.guild(guild) @@ -232,7 +231,7 @@ class Warnings: @commands.command() @commands.guild_only() @checks.admin_or_permissions(ban_members=True) - async def warn(self, ctx: RedContext, user: discord.Member, reason: str): + 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""" reason_type = {} @@ -276,7 +275,7 @@ class Warnings: @commands.command() @commands.guild_only() - async def warnings(self, ctx: RedContext, userid: int=None): + 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 Note that showing warnings for users other than yourself requires @@ -326,7 +325,7 @@ class Warnings: @commands.command() @commands.guild_only() @checks.admin_or_permissions(ban_members=True) - async def unwarn(self, ctx: RedContext, user_id: int, warn_id: str): + async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str): """Removes the specified warning from the user specified""" guild = ctx.guild member = guild.get_member(user_id) @@ -347,7 +346,7 @@ class Warnings: await ctx.tick() @staticmethod - async def custom_warning_reason(ctx: RedContext): + async def custom_warning_reason(ctx: commands.Context): """Handles getting description and points for custom reasons""" to_add = { "points": 0, diff --git a/redbot/core/__init__.py b/redbot/core/__init__.py index 7d08730d4..56b1fcf4f 100644 --- a/redbot/core/__init__.py +++ b/redbot/core/__init__.py @@ -1,7 +1,6 @@ from .config import Config -from .context import RedContext -__all__ = ["Config", "RedContext", "__version__"] +__all__ = ["Config", "__version__"] class VersionInfo: diff --git a/redbot/core/bot.py b/redbot/core/bot.py index 82efebeda..d4eaf62c6 100644 --- a/redbot/core/bot.py +++ b/redbot/core/bot.py @@ -20,7 +20,7 @@ from .cog_manager import CogManager from . import ( Config, i18n, - RedContext, + commands, rpc ) from .help_formatter import Help, help as help_ @@ -193,7 +193,7 @@ class RedBase(BotBase, RpcMethodMixin): admin_role = await self.db.guild(member.guild).admin_role() return any(role.id in (mod_role, admin_role) for role in member.roles) - async def get_context(self, message, *, cls=RedContext): + async def get_context(self, message, *, cls=commands.Context): return await super().get_context(message, cls=cls) def list_packages(self): diff --git a/redbot/core/cog_manager.py b/redbot/core/cog_manager.py index 5f518e001..f504ac5de 100644 --- a/redbot/core/cog_manager.py +++ b/redbot/core/cog_manager.py @@ -8,11 +8,10 @@ from typing import Tuple, Union, List import redbot.cogs import discord -from . import checks +from . import checks, commands from .config import Config -from .i18n import CogI18n +from .i18n import Translator, cog_i18n from .data_manager import cog_data_path -from discord.ext import commands from .utils.chat_formatting import box, pagify @@ -303,10 +302,13 @@ class CogManager: invalidate_caches() -_ = CogI18n("CogManagerUI", __file__) +_ = Translator("CogManagerUI", __file__) +@cog_i18n(_) class CogManagerUI: + """Commands to interface with Red's cog manager.""" + async def visible_paths(self, ctx): install_path = await ctx.bot.cog_mgr.install_path() cog_paths = await ctx.bot.cog_mgr.paths() diff --git a/redbot/core/commands/__init__.py b/redbot/core/commands/__init__.py new file mode 100644 index 000000000..77ab1b024 --- /dev/null +++ b/redbot/core/commands/__init__.py @@ -0,0 +1,4 @@ + +from discord.ext.commands import * +from .commands import * +from .context import * diff --git a/redbot/core/commands/commands.py b/redbot/core/commands/commands.py new file mode 100644 index 000000000..bfcd87e65 --- /dev/null +++ b/redbot/core/commands/commands.py @@ -0,0 +1,74 @@ +"""Module for command helpers and classes. + +This module contains extended classes and functions which are intended to +replace those from the `discord.ext.commands` module. +""" +import inspect + +from discord.ext import commands + + +__all__ = ["Command", "Group", "command", "group"] + + +class Command(commands.Command): + """Command class for Red. + + This should not be created directly, and instead via the decorator. + + This class inherits from `discord.ext.commands.Command`. + """ + + def __init__(self, *args, **kwargs): + self._help_override = kwargs.pop('help_override', None) + super().__init__(*args, **kwargs) + self.translator = kwargs.pop("i18n", None) + + @property + def help(self): + """Help string for this command. + + If the :code:`help` kwarg was passed into the decorator, it will + default to that. If not, it will attempt to translate the docstring + of the command's callback function. + """ + if self._help_override is not None: + return self._help_override + if self.translator is None: + translator = lambda s: s + else: + translator = self.translator + return inspect.cleandoc(translator(self.callback.__doc__)) + + @help.setter + def help(self, value): + # We don't want our help property to be overwritten, namely by super() + pass + + +class Group(Command, commands.Group): + """Group command class for Red. + + This class inherits from `discord.ext.commands.Group`, with `Command` mixed + in. + """ + pass + + +# decorators + +def command(name=None, cls=Command, **attrs): + """A decorator which transforms an async function into a `Command`. + + Same interface as `discord.ext.commands.command`. + """ + attrs["help_override"] = attrs.pop("help", None) + return commands.command(name, cls, **attrs) + + +def group(name=None, **attrs): + """A decorator which transforms an async function into a `Group`. + + Same interface as `discord.ext.commands.group`. + """ + return command(name, cls=Group, **attrs) diff --git a/redbot/core/context.py b/redbot/core/commands/context.py similarity index 95% rename from redbot/core/context.py rename to redbot/core/commands/context.py index 94886a65e..c9674c363 100644 --- a/redbot/core/context.py +++ b/redbot/core/commands/context.py @@ -1,26 +1,23 @@ -""" -The purpose of this module is to allow for Red to further customise the command -invocation context provided by discord.py. -""" + import asyncio from typing import Iterable, List - import discord from discord.ext import commands from redbot.core.utils.chat_formatting import box -__all__ = ["RedContext"] TICK = "\N{WHITE HEAVY CHECK MARK}" +__all__ = ["Context"] -class RedContext(commands.Context): + +class Context(commands.Context): """Command invocation context for Red. All context passed into commands will be of this type. - This class inherits from `commands.Context `. + This class inherits from `discord.ext.commands.Context`. """ async def send_help(self) -> List[discord.Message]: diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py index 68098ae17..f84560798 100644 --- a/redbot/core/core_commands.py +++ b/redbot/core/core_commands.py @@ -16,13 +16,12 @@ from distutils.version import StrictVersion import aiohttp import discord import pkg_resources -from discord.ext import commands from redbot.core import __version__ from redbot.core import checks from redbot.core import i18n from redbot.core import rpc -from redbot.core.context import RedContext +from redbot.core import commands from .utils import TYPE_CHECKING from .utils.chat_formatting import pagify, box, inline @@ -39,9 +38,10 @@ OWNER_DISCLAIMER = ("⚠ **Only** the person who is hosting Red should be " "system.** ⚠") -_ = i18n.CogI18n("Core", __file__) +_ = i18n.Translator("Core", __file__) +@i18n.cog_i18n(_) class Core: """Commands related to core functions""" def __init__(self, bot): @@ -52,7 +52,7 @@ class Core: rpc.add_method('core', self.rpc_reload) @commands.command() - async def info(self, ctx: RedContext): + async def info(self, ctx: commands.Context): """Shows info about Red""" author_repo = "https://github.com/Twentysix26" org_repo = "https://github.com/Cog-Creators" @@ -103,7 +103,7 @@ class Core: await ctx.send("I need the `Embed links` permission to send this") @commands.command() - async def uptime(self, ctx: RedContext): + async def uptime(self, ctx: commands.Context): """Shows Red's uptime""" since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S") passed = self.get_bot_uptime() @@ -112,7 +112,7 @@ class Core: passed, since ) ) - + def get_bot_uptime(self, *, brief=False): # Courtesy of Danny now = datetime.datetime.utcnow() @@ -134,7 +134,7 @@ class Core: return fmt.format(d=days, h=hours, m=minutes, s=seconds) @commands.group() - async def embedset(self, ctx: RedContext): + async def embedset(self, ctx: commands.Context): """ Commands for toggling embeds on or off. @@ -157,7 +157,7 @@ class Core: @embedset.command(name="global") @checks.is_owner() - async def embedset_global(self, ctx: RedContext): + async def embedset_global(self, ctx: commands.Context): """ Toggle the global embed setting. @@ -175,7 +175,7 @@ class Core: @embedset.command(name="guild") @checks.guildowner_or_permissions(administrator=True) - async def embedset_guild(self, ctx: RedContext, enabled: bool=None): + async def embedset_guild(self, ctx: commands.Context, enabled: bool=None): """ Toggle the guild's embed setting. @@ -200,7 +200,7 @@ class Core: ) @embedset.command(name="user") - async def embedset_user(self, ctx: RedContext, enabled: bool=None): + async def embedset_user(self, ctx: commands.Context, enabled: bool=None): """ Toggle the user's embed setting. @@ -412,7 +412,7 @@ class Core: """Reloads packages""" cognames = [c.strip() for c in cog_name.split(' ')] - + for c in cognames: ctx.bot.unload_extension(c) @@ -428,7 +428,7 @@ class Core: except RuntimeError: notfound_packages.append(inline(c)) - for spec, name in cogspecs: + for spec, name in cogspecs: try: self.cleanup_and_refresh_modules(spec.name) await ctx.bot.load_extension(spec) @@ -489,7 +489,7 @@ class Core: except: pass await ctx.bot.shutdown() - + @commands.command(name="restart") @checks.is_owner() async def _restart(self, ctx, silently: bool=False): @@ -776,26 +776,26 @@ class Core: await ctx.send(_("You have been set as owner.")) else: await ctx.send(_("Invalid token.")) - + @_set.command() @checks.is_owner() async def token(self, ctx, token: str): """Change bot token.""" if not isinstance(ctx.channel, discord.DMChannel): - + try: await ctx.message.delete() except discord.Forbidden: pass - + await ctx.send( _("Please use that command in DM. Since users probably saw your token," " it is recommended to reset it right now. Go to the following link and" " select `Reveal Token` and `Generate a new token?`." "\n\nhttps://discordapp.com/developers/applications/me/{}").format(self.bot.user.id)) return - + await ctx.bot.db.token.set(token) await ctx.send("Token set. Restart me.") @@ -834,7 +834,7 @@ class Core: @commands.command() @checks.is_owner() - async def listlocales(self, ctx: RedContext): + async def listlocales(self, ctx: commands.Context): """ Lists all available locales @@ -1051,7 +1051,7 @@ class Core: await ctx.send(_("User has been removed from whitelist.")) else: await ctx.send(_("User was not in the whitelist.")) - + @whitelist.command(name='clear') async def whitelist_clear(self, ctx): """ diff --git a/redbot/core/dev_commands.py b/redbot/core/dev_commands.py index 8ec038bf0..5c40522e1 100644 --- a/redbot/core/dev_commands.py +++ b/redbot/core/dev_commands.py @@ -7,9 +7,8 @@ from contextlib import redirect_stdout from copy import copy import discord -from discord.ext import commands -from . import checks -from .i18n import CogI18n +from . import checks, commands +from .i18n import Translator from .utils.chat_formatting import box, pagify """ Notice: @@ -19,7 +18,7 @@ Notice: https://github.com/Rapptz/RoboDanny/blob/master/cogs/repl.py """ -_ = CogI18n("Dev", __file__) +_ = Translator("Dev", __file__) class Dev: diff --git a/redbot/core/global_checks.py b/redbot/core/global_checks.py index a82b30ed6..2ef57166b 100644 --- a/redbot/core/global_checks.py +++ b/redbot/core/global_checks.py @@ -1,5 +1,5 @@ """The checks in this module run on every command.""" -from discord.ext import commands +from . import commands def init_global_checks(bot): diff --git a/redbot/core/help_formatter.py b/redbot/core/help_formatter.py index 24649c7ad..b0d2984cb 100644 --- a/redbot/core/help_formatter.py +++ b/redbot/core/help_formatter.py @@ -28,7 +28,6 @@ from collections import namedtuple from typing import List import discord -from discord.ext import commands from discord.ext.commands import formatter import inspect import itertools @@ -36,6 +35,8 @@ import re import sys import traceback +from . import commands + EMPTY_STRING = u'\u200b' @@ -133,7 +134,12 @@ class Help(formatter.HelpFormatter): 'fields': [] } - description = self.command.description if not self.is_cog() else inspect.getdoc(self.command) + if self.is_cog(): + translator = getattr(self.command, '__translator__', lambda s: s) + description = inspect.cleandoc(translator(self.command.__doc__)) + else: + description = self.command.description + if not description == '' and description is not None: description = '*{0}*'.format(description) diff --git a/redbot/core/i18n.py b/redbot/core/i18n.py index 62f4ab50f..c4e700e8e 100644 --- a/redbot/core/i18n.py +++ b/redbot/core/i18n.py @@ -1,7 +1,10 @@ import re from pathlib import Path -__all__ = ['get_locale', 'set_locale', 'reload_locales', 'CogI18n'] +from . import commands + +__all__ = ['get_locale', 'set_locale', 'reload_locales', 'cog_i18n', + 'Translator'] _current_locale = 'en_us' @@ -13,7 +16,7 @@ IN_MSGSTR = 4 MSGID = 'msgid "' MSGSTR = 'msgstr "' -_i18n_cogs = {} +_translators = [] def get_locale(): @@ -27,8 +30,8 @@ def set_locale(locale): def reload_locales(): - for cog_name, i18n in _i18n_cogs.items(): - i18n.load_translations() + for translator in _translators: + translator.load_translations() def _parse(translation_file): @@ -145,25 +148,36 @@ def get_locale_path(cog_folder: Path, extension: str) -> Path: return cog_folder / 'locales' / "{}.{}".format(get_locale(), extension) -class CogI18n: +class Translator: + """Function to get translated strings at runtime.""" + def __init__(self, name, file_location): """ - Initializes the internationalization object for a given cog. + Initializes an internationalization object. - :param name: Your cog name. - :param file_location: + Parameters + ---------- + name : str + Your cog name. + file_location : `str` or `pathlib.Path` This should always be ``__file__`` otherwise your localizations will not load. + """ self.cog_folder = Path(file_location).resolve().parent self.cog_name = name self.translations = {} - _i18n_cogs.update({self.cog_name: self}) + _translators.append(self) self.load_translations() def __call__(self, untranslated: str): + """Translate the given string. + + This will look for the string in the translator's :code:`.pot` file, + with respect to the current locale. + """ normalized_untranslated = _normalize(untranslated, True) try: return self.translations[normalized_untranslated] @@ -172,7 +186,7 @@ class CogI18n: def load_translations(self): """ - Loads the current translations for this cog. + Loads the current translations. """ self.translations = {} translation_file = None @@ -201,3 +215,14 @@ class CogI18n: if translated: self.translations.update({untranslated: translated}) + +def cog_i18n(translator: Translator): + """Get a class decorator to link the translator to this cog.""" + def decorator(cog_class: type): + cog_class.__translator__ = translator + for name, attr in cog_class.__dict__.items(): + if isinstance(attr, (commands.Group, commands.Command)): + attr.translator = translator + setattr(cog_class, name, attr) + return cog_class + return decorator diff --git a/redbot/core/locales/debugging.po b/redbot/core/locales/debugging.po new file mode 100644 index 000000000..e4e0a3c56 --- /dev/null +++ b/redbot/core/locales/debugging.po @@ -0,0 +1,197 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ORGANIZATION +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2017-12-06 11:27+1100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=cp1252\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: pygettext.py 1.5\n" + + +#: ../cog_manager.py:21 +#, docstring +msgid "" +"Directory manager for Red's cogs.\n" +"\n" +" This module allows you to load cogs from multiple directories and even from\n" +" outside the bot directory. You may also set a directory for downloader to\n" +" install new cogs to, the default being the :code:`cogs/` folder in the root\n" +" bot directory.\n" +" " +msgstr "" + +#: ../cog_manager.py:40 +#, docstring +msgid "" +"Get all currently valid path directories.\n" +"\n" +" Returns\n" +" -------\n" +" `tuple` of `pathlib.Path`\n" +" All valid cog paths.\n" +"\n" +" " +msgstr "" + +#: ../cog_manager.py:64 +#, docstring +msgid "" +"Get the install path for 3rd party cogs.\n" +"\n" +" Returns\n" +" -------\n" +" pathlib.Path\n" +" The path to the directory where 3rd party cogs are stored.\n" +"\n" +" " +msgstr "" + +#: ../cog_manager.py:273 +#, docstring +msgid "" +"Finds the names of all available modules to load.\n" +" " +msgstr "" + +#: ../cog_manager.py:285 +#, docstring +msgid "" +"Re-evaluate modules in the py cache.\n" +"\n" +" This is an alias for an importlib internal and should be called\n" +" any time that a new module has been installed to a cog directory.\n" +" " +msgstr "" + +#: ../cog_manager.py:298 +#, docstring +msgid "" +"Commands to interface with Red's cog manager." +msgstr "" +"(TRANSLATED) Commands to interface with Red's cog manager." + +#: ../cog_manager.py:302 +#, docstring +msgid "" +"\n" +" Lists current cog paths in order of priority." +" " +msgstr "" +"\n" +" (TRANSLATED) Lists current cog paths in order of priority." +" " + +#: ../cog_manager.py:321 +#, docstring +msgid "" +"\n" +" Add a path to the list of available cog paths." +" " +msgstr "" +"\n" +" (TRANSLATED) Add a path to the list of available cog paths." +" " + +#: ../cog_manager.py:340 +#, docstring +msgid "" +"\n" +" Removes a path from the available cog paths given the path_number" +" from !paths" +" " +msgstr "" +"\n" +" (TRANSLATED) Removes a path from the available cog paths given the path_number" +" from !paths" +" " + +#: ../cog_manager.py:357 +#, docstring +msgid "" +"\n" +" Reorders paths internally to allow discovery of different cogs." +" " +msgstr "" +"\n" +" (TRANSLATED) Reorders paths internally to allow discovery of different cogs." +" " + +#: ../cog_manager.py:383 +#, docstring +msgid "" +"\n" +" Returns the current install path or sets it if one is provided." +" The provided path must be absolute or relative to the bot's" +" directory and it must already exist." +"\n" +" No installed cogs will be transferred in the process." +" " +msgstr "" +"\n" +" (TRANSLATED) Returns the current install path or sets it if one is provided." +" The provided path must be absolute or relative to the bot's" +" directory and it must already exist." +"\n" +" No installed cogs will be transferred in the process." +" " + +#: ../cog_manager.py:406 +#, docstring +msgid "" +"\n" +" Lists all loaded and available cogs." +" " +msgstr "" +"\n" +" (TRANSLATED) Lists all loaded and available cogs." +" " + +#: ../cog_manager.py:309 +msgid "" +"Install Path: {}\n" +"\n" +msgstr "" + +#: ../cog_manager.py:325 +msgid "That path is does not exist or does not point to a valid directory." +msgstr "" + +#: ../cog_manager.py:334 +msgid "Path successfully added." +msgstr "" + +#: ../cog_manager.py:347 +msgid "That is an invalid path number." +msgstr "" + +#: ../cog_manager.py:351 +msgid "Path successfully removed." +msgstr "" + +#: ../cog_manager.py:367 +msgid "Invalid 'from' index." +msgstr "" + +#: ../cog_manager.py:373 +msgid "Invalid 'to' index." +msgstr "" + +#: ../cog_manager.py:377 +msgid "Paths reordered." +msgstr "" + +#: ../cog_manager.py:395 +msgid "That path does not exist." +msgstr "" + +#: ../cog_manager.py:399 +msgid "The bot will install new cogs to the `{}` directory." +msgstr "" + diff --git a/redbot/core/utils/menus.py b/redbot/core/utils/menus.py index b08146014..8ac4afa6e 100644 --- a/redbot/core/utils/menus.py +++ b/redbot/core/utils/menus.py @@ -7,10 +7,10 @@ Ported to Red V3 by Palm__ (https://github.com/palmtree5) import asyncio import discord -from redbot.core import RedContext +from redbot.core import commands -async def menu(ctx: RedContext, pages: list, +async def menu(ctx: commands.Context, pages: list, controls: dict, message: discord.Message=None, page: int=0, timeout: float=30.0): @@ -28,7 +28,7 @@ async def menu(ctx: RedContext, pages: list, Parameters ---------- - ctx: RedContext + ctx: commands.Context The command context pages: `list` of `str` or `discord.Embed` The pages of the menu. @@ -92,7 +92,7 @@ async def menu(ctx: RedContext, pages: list, timeout, react.emoji) -async def next_page(ctx: RedContext, pages: list, +async def next_page(ctx: commands.Context, pages: list, controls: dict, message: discord.Message, page: int, timeout: float, emoji: str): perms = message.channel.permissions_for(ctx.guild.me) @@ -109,7 +109,7 @@ async def next_page(ctx: RedContext, pages: list, page=page, timeout=timeout) -async def prev_page(ctx: RedContext, pages: list, +async def prev_page(ctx: commands.Context, pages: list, controls: dict, message: discord.Message, page: int, timeout: float, emoji: str): perms = message.channel.permissions_for(ctx.guild.me) @@ -126,7 +126,7 @@ async def prev_page(ctx: RedContext, pages: list, page=next_page, timeout=timeout) -async def close_menu(ctx: RedContext, pages: list, +async def close_menu(ctx: commands.Context, pages: list, controls: dict, message: discord.Message, page: int, timeout: float, emoji: str): if message: