From fa692ccc0b9a136dbf95bb5a1451de6c451357ab Mon Sep 17 00:00:00 2001 From: Toby Harradine Date: Fri, 5 Oct 2018 15:07:56 +1000 Subject: [PATCH] [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,