[i18n] Pass over economy, filter, general, image, mod

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
This commit is contained in:
Toby Harradine 2018-10-05 15:07:56 +10:00
parent 0c3d8af8f4
commit fa692ccc0b
15 changed files with 720 additions and 640 deletions

View File

@ -11,41 +11,40 @@ from .converters import MemberDefaultAuthor, SelfRole
log = logging.getLogger("red.admin") log = logging.getLogger("red.admin")
_ = Translator("Admin", __file__) T_ = Translator("Admin", __file__)
# The following are all lambdas to allow us to fetch the translation _ = lambda s: s
# during runtime, without having to copy the large strings everywhere GENERIC_FORBIDDEN = _(
# in the code.
generic_forbidden = lambda: _(
"I attempted to do something that Discord denied me permissions for." "I attempted to do something that Discord denied me permissions for."
" Your command failed to successfully complete." " Your command failed to successfully complete."
) )
hierarchy_issue = lambda: _( HIERARCHY_ISSUE = _(
"I tried to add {role.name} to {member.display_name} but that role" "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" " 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 " " unable to successfully add it. Please give me a higher role and "
"try again." "try again."
) )
user_hierarchy_issue = lambda: _( USER_HIERARCHY_ISSUE = _(
"I tried to add {role.name} to {member.display_name} but that role" "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" " is higher than your highest role in the Discord hierarchy so I was"
" unable to successfully add it. Please get a higher role and " " unable to successfully add it. Please get a higher role and "
"try again." "try again."
) )
running_announcement = lambda: _( RUNNING_ANNOUNCEMENT = _(
"I am already announcing something. If you would like to make a" "I am already announcing something. If you would like to make a"
" different announcement please use `{prefix}announce cancel`" " different announcement please use `{prefix}announce cancel`"
" first." " first."
) )
_ = T_
@cog_i18n(_) @cog_i18n(_)
class Admin(commands.Cog): class Admin(commands.Cog):
"""A collection of server administration utilities.""" """A collection of server administration utilities."""
def __init__(self, config=Config): def __init__(self, config=Config):
super().__init__() super().__init__()
self.conf = config.get_conf(self, 8237492837454039, force_registration=True) self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
@ -105,9 +104,9 @@ class Admin(commands.Cog):
await member.add_roles(role) await member.add_roles(role)
except discord.Forbidden: except discord.Forbidden:
if not self.pass_hierarchy_check(ctx, role): 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: else:
await self.complain(ctx, generic_forbidden()) await self.complain(ctx, T_(GENERIC_FORBIDDEN))
else: else:
await ctx.send( await ctx.send(
_("I successfully added {role.name} to {member.display_name}").format( _("I successfully added {role.name} to {member.display_name}").format(
@ -120,9 +119,9 @@ class Admin(commands.Cog):
await member.remove_roles(role) await member.remove_roles(role)
except discord.Forbidden: except discord.Forbidden:
if not self.pass_hierarchy_check(ctx, role): 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: else:
await self.complain(ctx, generic_forbidden()) await self.complain(ctx, T_(GENERIC_FORBIDDEN))
else: else:
await ctx.send( await ctx.send(
_("I successfully removed {role.name} from {member.display_name}").format( _("I successfully removed {role.name} from {member.display_name}").format(
@ -146,7 +145,7 @@ class Admin(commands.Cog):
# noinspection PyTypeChecker # noinspection PyTypeChecker
await self._addrole(ctx, user, rolename) await self._addrole(ctx, user, rolename)
else: 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.command()
@commands.guild_only() @commands.guild_only()
@ -164,7 +163,7 @@ class Admin(commands.Cog):
# noinspection PyTypeChecker # noinspection PyTypeChecker
await self._removerole(ctx, user, rolename) await self._removerole(ctx, user, rolename)
else: else:
await self.complain(ctx, user_hierarchy_issue()) await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@ -191,13 +190,13 @@ class Admin(commands.Cog):
reason = "{}({}) changed the colour of role '{}'".format(author.name, author.id, role.name) reason = "{}({}) changed the colour of role '{}'".format(author.name, author.id, role.name)
if not self.pass_user_hierarchy_check(ctx, role): 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 return
try: try:
await role.edit(reason=reason, color=value) await role.edit(reason=reason, color=value)
except discord.Forbidden: except discord.Forbidden:
await self.complain(ctx, generic_forbidden()) await self.complain(ctx, T_(GENERIC_FORBIDDEN))
else: else:
log.info(reason) log.info(reason)
await ctx.send(_("Done.")) await ctx.send(_("Done."))
@ -219,13 +218,13 @@ class Admin(commands.Cog):
) )
if not self.pass_user_hierarchy_check(ctx, role): 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 return
try: try:
await role.edit(reason=reason, name=name) await role.edit(reason=reason, name=name)
except discord.Forbidden: except discord.Forbidden:
await self.complain(ctx, generic_forbidden()) await self.complain(ctx, T_(GENERIC_FORBIDDEN))
else: else:
log.info(reason) log.info(reason)
await ctx.send(_("Done.")) await ctx.send(_("Done."))
@ -243,7 +242,7 @@ class Admin(commands.Cog):
await ctx.send(_("The announcement has begun.")) await ctx.send(_("The announcement has begun."))
else: else:
prefix = ctx.prefix prefix = ctx.prefix
await self.complain(ctx, running_announcement(), prefix=prefix) await self.complain(ctx, T_(RUNNING_ANNOUNCEMENT), prefix=prefix)
@announce.command(name="cancel") @announce.command(name="cancel")
@checks.is_owner() @checks.is_owner()
@ -381,7 +380,7 @@ class Admin(commands.Cog):
serverlocked = await self.conf.serverlocked() serverlocked = await self.conf.serverlocked()
await self.conf.serverlocked.set(not 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.")) await ctx.send(_("The bot is no longer serverlocked."))
else: else:
await ctx.send(_("The bot is now serverlocked.")) await ctx.send(_("The bot is now serverlocked."))

View File

@ -113,7 +113,8 @@ class Alias(commands.Cog):
return False return False
async def get_prefix(self, message: discord.Message) -> str: async def get_prefix(self, message: discord.Message) -> str:
"""Tries to determine what prefix is used in a message object. """
Tries to determine what prefix is used in a message object.
Looks to identify from longest prefix to smallest. Looks to identify from longest prefix to smallest.
Will raise ValueError if no prefix is found. Will raise ValueError if no prefix is found.
@ -175,7 +176,7 @@ class Alias(commands.Cog):
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
async def alias(self, ctx: commands.Context): async def alias(self, ctx: commands.Context):
"""Manage per-server aliases for commands.""" """Manage command aliases."""
pass pass
@alias.group(name="global") @alias.group(name="global")

View File

@ -14,6 +14,7 @@ import redbot.core
from redbot.core import Config, commands, checks, bank from redbot.core import Config, commands, checks, bank
from redbot.core.data_manager import cog_data_path from redbot.core.data_manager import cog_data_path
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import bold, box
from redbot.core.utils.menus import ( from redbot.core.utils.menus import (
menu, menu,
DEFAULT_CONTROLS, DEFAULT_CONTROLS,
@ -35,6 +36,7 @@ __author__ = ["aikaterna", "billy/bollo/ati"]
@cog_i18n(_) @cog_i18n(_)
class Audio(commands.Cog): class Audio(commands.Cog):
"""Play audio through voice channels.""" """Play audio through voice channels."""
def __init__(self, bot): def __init__(self, bot):
super().__init__() super().__init__()
self.bot = bot self.bot = bot
@ -151,8 +153,8 @@ class Audio(commands.Cog):
description=description, description=description,
) )
embed.set_footer( embed.set_footer(
text="Track length: {} | Requested by: {}".format( text=_("Track length: {length} | Requested by: {user}").format(
dur, player.current.requester length=dur, user=player.current.requester
) )
) )
if ( if (
@ -175,7 +177,7 @@ class Audio(commands.Cog):
if playing_servers > 1: if playing_servers > 1:
await self.bot.change_presence( await self.bot.change_presence(
activity=discord.Activity( activity=discord.Activity(
name=_("music in {num} servers").format(num=playing_servers), name=_("music in {} servers").format(playing_servers),
type=discord.ActivityType.playing, type=discord.ActivityType.playing,
) )
) )
@ -201,7 +203,7 @@ class Audio(commands.Cog):
if playing_servers > 1: if playing_servers > 1:
await self.bot.change_presence( await self.bot.change_presence(
activity=discord.Activity( activity=discord.Activity(
name="music in {} servers".format(playing_servers), name=_("music in {} servers").format(playing_servers),
type=discord.ActivityType.playing, 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.bot.wait_for("message", timeout=15.0, check=pred)
await ctx.invoke(self.role, pred.result) await ctx.invoke(self.role, pred.result)
except asyncio.TimeoutError: 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() dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
await self.config.guild(ctx.guild).dj_enabled.set(not 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.""" """Set the role to use for DJ mode."""
await self.config.guild(ctx.guild).dj_role.set(role_name.id) 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()) 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() @audioset.command()
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@ -330,7 +332,7 @@ class Audio(commands.Cog):
jarbuild = redbot.core.__version__ jarbuild = redbot.core.__version__
vote_percent = data["vote_percent"] vote_percent = data["vote_percent"]
msg = _("```ini\n----Server Settings----\n") msg = "----" + _("Server Settings") + "----"
if emptydc_enabled: if emptydc_enabled:
msg += _("Disconnect timer: [{num_seconds}]\n").format( msg += _("Disconnect timer: [{num_seconds}]\n").format(
num_seconds=self._dynamic_time(emptydc_timer) num_seconds=self._dynamic_time(emptydc_timer)
@ -347,7 +349,7 @@ class Audio(commands.Cog):
"Songs as status: [{status}]\n" "Songs as status: [{status}]\n"
).format(**global_data, **data) ).format(**global_data, **data)
if thumbnail: if thumbnail:
msg += "Thumbnails: [{0}]\n".format(thumbnail) msg += _("Thumbnails: [{0}]\n").format(thumbnail)
if vote_percent > 0: if vote_percent > 0:
msg += _( msg += _(
"Vote skip: [{vote_enabled}]\nSkip percentage: [{vote_percent}%]\n" "Vote skip: [{vote_enabled}]\nSkip percentage: [{vote_percent}%]\n"
@ -356,10 +358,10 @@ class Audio(commands.Cog):
"---Lavalink Settings---\n" "---Lavalink Settings---\n"
"Cog version: [{version}]\n" "Cog version: [{version}]\n"
"Jar build: [{jarbuild}]\n" "Jar build: [{jarbuild}]\n"
"External server: [{use_external_lavalink}]```" "External server: [{use_external_lavalink}]"
).format(version=__version__, jarbuild=jarbuild, **global_data) ).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) return await ctx.send(embed=embed)
@audioset.command() @audioset.command()
@ -368,7 +370,7 @@ class Audio(commands.Cog):
"""Toggle displaying a thumbnail on audio messages.""" """Toggle displaying a thumbnail on audio messages."""
thumbnail = await self.config.guild(ctx.guild).thumbnail() thumbnail = await self.config.guild(ctx.guild).thumbnail()
await self.config.guild(ctx.guild).thumbnail.set(not 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() @audioset.command()
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@ -498,11 +500,11 @@ class Audio(commands.Cog):
if self._player_check(ctx): if self._player_check(ctx):
if dj_enabled: if dj_enabled:
if not await self._can_instaskip(ctx, ctx.author): 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( if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(
ctx, ctx.author 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: else:
await lavalink.get_player(ctx.guild.id).stop() await lavalink.get_player(ctx.guild.id).stop()
return await lavalink.get_player(ctx.guild.id).disconnect() return await lavalink.get_player(ctx.guild.id).disconnect()
@ -510,7 +512,7 @@ class Audio(commands.Cog):
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
async def local(self, ctx): async def local(self, ctx):
"""Local playback options.""" """Local playback commands."""
pass pass
@local.command(name="folder") @local.command(name="folder")
@ -527,7 +529,7 @@ class Audio(commands.Cog):
return return
localtracks_folders = await self._localtracks_folders(ctx) localtracks_folders = await self._localtracks_folders(ctx)
if not localtracks_folders: 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) len_folder_pages = math.ceil(len(localtracks_folders) / 5)
folder_page_list = [] folder_page_list = []
for page_num in range(1, len_folder_pages + 1): for page_num in range(1, len_folder_pages + 1):
@ -573,14 +575,14 @@ class Audio(commands.Cog):
return return
localtracks_folders = await self._localtracks_folders(ctx) localtracks_folders = await self._localtracks_folders(ctx)
if not localtracks_folders: 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 = [] all_tracks = []
for local_folder in localtracks_folders: for local_folder in localtracks_folders:
folder_tracks = await self._folder_list(ctx, local_folder) folder_tracks = await self._folder_list(ctx, local_folder)
all_tracks = all_tracks + folder_tracks all_tracks = all_tracks + folder_tracks
search_list = await self._build_local_search_list(all_tracks, search_words) search_list = await self._build_local_search_list(all_tracks, search_words)
if not search_list: 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) await ctx.invoke(self.search, query=search_list)
async def _all_folder_tracks(self, ctx, folder): 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" f for f in os.listdir(os.getcwd()) if not os.path.isfile(f) if f == "localtracks"
) )
if not localtracks_folder: if not localtracks_folder:
await self._embed_msg(ctx, "No localtracks folder.") await self._embed_msg(ctx, _("No localtracks folder."))
return False return False
else: else:
return True return True
@ -772,7 +774,7 @@ class Audio(commands.Cog):
command = ctx.invoked_with command = ctx.invoked_with
if not player.current: 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: if "localtracks/" in player.current.uri:
description = "**{}**\n{}".format( description = "**{}**\n{}".format(
player.current.title, player.current.uri.replace("localtracks/", "") 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: if track_list and len(to_append) == 1 and to_append[0] in track_list:
return await self._embed_msg( return await self._embed_msg(
ctx, 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: if track_list:
playlists[playlist_name]["tracks"] = track_list + to_append playlists[playlist_name]["tracks"] = track_list + to_append
@ -1080,7 +1084,7 @@ class Audio(commands.Cog):
"""List saved playlists.""" """List saved playlists."""
playlists = await self.config.guild(ctx.guild).playlists.get_raw() playlists = await self.config.guild(ctx.guild).playlists.get_raw()
if not playlists: if not playlists:
return await self._embed_msg(ctx, "No saved playlists.") return await self._embed_msg(ctx, _("No saved playlists."))
playlist_list = [] playlist_list = []
space = "\N{EN SPACE}" space = "\N{EN SPACE}"
for playlist_name in playlists: for playlist_name in playlists:
@ -1089,12 +1093,12 @@ class Audio(commands.Cog):
tracks = [] tracks = []
author = playlists[playlist_name]["author"] author = playlists[playlist_name]["author"]
playlist_list.append( playlist_list.append(
"**{}**\n{}Tracks: {}\n{}Author: {}\n".format( ("\n" + space * 4).join(
playlist_name, (
(space * 4), bold(playlist_name),
str(len(tracks)), _("Tracks: {num}").format(num=len(tracks)),
(space * 4), _("Author: {name}").format(self.bot.get_user(author)),
self.bot.get_user(author), )
) )
) )
abc_names = sorted(playlist_list, key=str.lower) abc_names = sorted(playlist_list, key=str.lower)
@ -1121,7 +1125,9 @@ class Audio(commands.Cog):
description=plist, description=plist,
) )
embed.set_footer( 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 return embed
@ -1449,9 +1455,11 @@ class Audio(commands.Cog):
player.current.title, player.current.uri.replace("localtracks/", "") player.current.title, player.current.uri.replace("localtracks/", "")
) )
else: else:
description = "**[{}]({})**".format(player.current.title, player.current.uri) description = f"**[{player.current.title}]({player.current.title})**"
embed = discord.Embed( 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) await ctx.send(embed=embed)
@ -1460,7 +1468,8 @@ class Audio(commands.Cog):
async def queue(self, ctx, *, page="1"): async def queue(self, ctx, *, page="1"):
"""List the queue. """List the queue.
Use [p]queue search <search terms> to search the queue.""" Use [p]queue search <search terms> to search the queue.
"""
if not self._player_check(ctx): if not self._player_check(ctx):
return await self._embed_msg(ctx, _("There's nothing in the queue.")) return await self._embed_msg(ctx, _("There's nothing in the queue."))
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
@ -1505,28 +1514,28 @@ class Audio(commands.Cog):
elif "localtracks" in player.current.uri: elif "localtracks" in player.current.uri:
if not player.current.title == "Unknown title": if not player.current.title == "Unknown title":
queue_list += "Playing: **{} - {}**\n{}\nRequested by: **{}**\n\n{}`{}`/`{}`\n\n".format( queue_list += "\n".join(
player.current.author, (
player.current.title, _("Playing: ")
+ "**{current.author} - {current.title}**".format(current=player.current),
player.current.uri.replace("localtracks/", ""), player.current.uri.replace("localtracks/", ""),
player.current.requester, _("Requested by: **{user}**\n").format(user=player.current.requester),
arrow, f"{arrow}`{pos}`/`{dur}`\n\n",
pos, )
dur,
) )
else: else:
queue_list += "Playing: {}\nRequested by: **{}**\n\n{}`{}`/`{}`\n\n".format( queue_list += "\n".join(
player.current.uri.replace("localtracks/", ""), (
player.current.requester, _("Playing: ") + player.current.uri.replace("localtracks/", ""),
arrow, _("Requested by: **{user}**\n").format(user=player.current.requester),
pos, f"{arrow}`{pos}`/`{dur}`\n\n",
dur, )
) )
else: else:
queue_list += _("Playing:") queue_list += _("Playing: ")
queue_list += " **[{current.title}]({current.uri})**\n".format(current=player.current) queue_list += "**[{current.title}]({current.uri})**\n".format(current=player.current)
queue_list += _("Requested by: **{user}**").format(user=player.current.requester) 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( for i, track in enumerate(
player.queue[queue_idx_start:queue_idx_end], start=queue_idx_start player.queue[queue_idx_start:queue_idx_end], start=queue_idx_start
@ -1540,17 +1549,18 @@ class Audio(commands.Cog):
track_idx = i + 1 track_idx = i + 1
if "localtracks" in track.uri: if "localtracks" in track.uri:
if track.title == "Unknown title": if track.title == "Unknown title":
queue_list += "`{}.` **{}**, requested by **{}**\n".format( queue_list += f"`{track_idx}.` " + ", ".join(
track_idx, track.uri.replace("localtracks/", ""), req_user (
bold(track.uri.replace("localtracks/", "")),
_("requested by **{user}**\n").format(user=req_user),
)
) )
else: else:
queue_list += "`{}.` **{} - {}**, requested by **{}**\n".format( queue_list += f"`{track_idx}.` **{track.author} - {track_title}**, " + _(
track_idx, track.author, track_title, req_user "requested by **{user}**\n"
) ).format(user=req_user)
else: else:
queue_list += "`{idx}.` **[{title}]({uri})**, ".format( queue_list += f"`{track_idx}.` **[{track_title}]({track.uri})**, "
idx=track_idx, title=track_title, uri=track.uri
)
queue_list += _("requested by **{user}**\n").format(user=req_user) queue_list += _("requested by **{user}**\n").format(user=req_user)
embed = discord.Embed( embed = discord.Embed(
@ -1581,7 +1591,7 @@ class Audio(commands.Cog):
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
search_list = await self._build_queue_search_list(player.queue, search_words) search_list = await self._build_queue_search_list(player.queue, search_words)
if not search_list: 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) len_search_pages = math.ceil(len(search_list) / 10)
search_page_list = [] search_page_list = []
for page_num in range(1, len_search_pages + 1): for page_num in range(1, len_search_pages + 1):
@ -1630,10 +1640,12 @@ class Audio(commands.Cog):
else: else:
track_match += "`{}.` **{}**\n".format(track[0], track[1]) track_match += "`{}.` **{}**\n".format(track[0], track[1])
embed = discord.Embed( 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( 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 return embed
@ -1704,8 +1716,9 @@ class Audio(commands.Cog):
async def search(self, ctx, *, query): async def search(self, ctx, *, query):
"""Pick a track with a search. """Pick a track with a search.
Use `[p]search list <search term>` to queue all tracks found on YouTube. Use `[p]search list <search term>` to queue all tracks found
`[p]search sc <search term>` will search SoundCloud instead of YouTube. on YouTube. `[p]search sc <search term>` will search SoundCloud
instead of YouTube.
""" """
async def _search_menu( async def _search_menu(
@ -1777,9 +1790,9 @@ class Audio(commands.Cog):
queue_total_duration = lavalink.utils.format_time(queue_duration) queue_total_duration = lavalink.utils.format_time(queue_duration)
if not shuffle and queue_duration > 0: if not shuffle and queue_duration > 0:
songembed.set_footer( songembed.set_footer(
text=_("{time} until start of search playback: starts at #{position} in queue").format( text=_(
time=queue_total_duration, position=len(player.queue) + 1 "{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: for track in tracks:
player.add(ctx.author, track) player.add(ctx.author, track)
@ -1829,7 +1842,7 @@ class Audio(commands.Cog):
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
player.store("connect", datetime.datetime.utcnow()) player.store("connect", datetime.datetime.utcnow())
except AttributeError: 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) player = lavalink.get_player(ctx.guild.id)
jukebox_price = await self.config.guild(ctx.guild).jukebox_price() jukebox_price = await self.config.guild(ctx.guild).jukebox_price()
shuffle = await self.config.guild(ctx.guild).shuffle() shuffle = await self.config.guild(ctx.guild).shuffle()
@ -1886,9 +1899,7 @@ class Audio(commands.Cog):
) )
) )
elif queue_duration > 0: elif queue_duration > 0:
embed.set_footer( embed.set_footer(text=_("#{position} in queue").format(position=len(player.queue) + 1))
text=_("#{position} in queue").format(position=len(player.queue) + 1)
)
player.add(ctx.author, search_choice) player.add(ctx.author, search_choice)
if not player.current: if not player.current:
@ -1949,13 +1960,11 @@ class Audio(commands.Cog):
colour=await ctx.embed_colour(), title=title, description=search_list colour=await ctx.embed_colour(), title=title, description=search_list
) )
embed.set_footer( embed.set_footer(
text=( text=(_("Page {page_num}/{total_pages}") + " | {num_results} {footer}").format(
_("Page {page_num}/{total_pages}") + " | {num_results} {footer}"
).format(
page_num=page_num, page_num=page_num,
total_pages=search_num_pages, total_pages=search_num_pages,
num_results=len(tracks), num_results=len(tracks),
footer=footer footer=footer,
) )
) )
return embed return embed
@ -2025,7 +2034,7 @@ class Audio(commands.Cog):
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
async def sing(self, ctx): async def sing(self, ctx):
"""Makes Red sing one of her songs""" """Make Red sing one of her songs"""
ids = ( ids = (
"zGTkAVsrfg8", "zGTkAVsrfg8",
"cGMWL8cOeAU", "cGMWL8cOeAU",
@ -2040,7 +2049,7 @@ class Audio(commands.Cog):
@commands.command(aliases=["forceskip", "fs"]) @commands.command(aliases=["forceskip", "fs"])
@commands.guild_only() @commands.guild_only()
async def skip(self, ctx): async def skip(self, ctx):
"""Skips to the next track.""" """Skip to the next track."""
if not self._player_check(ctx): if not self._player_check(ctx):
return await self._embed_msg(ctx, _("Nothing playing.")) return await self._embed_msg(ctx, _("Nothing playing."))
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
@ -2078,11 +2087,14 @@ class Audio(commands.Cog):
await self._embed_msg(ctx, _("Vote threshold met.")) await self._embed_msg(ctx, _("Vote threshold met."))
return await self._skip_action(ctx) return await self._skip_action(ctx)
else: else:
reply += _(" Votes: {num_votes}/{num_members}").format( reply += _(
num_votes=num_votes, num_members=num_members " Votes: {num_votes}/{num_members}"
) " ({cur_percent}% out of {required_percent}% needed)"
reply += _(" ({cur_percent}% out of {required_percent}% needed)").format( ).format(
cur_percent=vote, required_percent=percent num_votes=num_votes,
num_members=num_members,
cur_percent=vote,
required_percent=percent,
) )
return await self._embed_msg(ctx, reply) return await self._embed_msg(ctx, reply)
else: else:
@ -2304,8 +2316,7 @@ class Audio(commands.Cog):
await self.config.host.set(host) await self.config.host.set(host)
if await self._check_external(): if await self._check_external():
embed = discord.Embed( embed = discord.Embed(
colour=await ctx.embed_colour(), colour=await ctx.embed_colour(), title=_("Host set to {host}.").format(host=host)
title=_("Host set to {host}.").format(host=host),
) )
embed.set_footer(text=_("External lavalink server set to True.")) embed.set_footer(text=_("External lavalink server set to True."))
await ctx.send(embed=embed) await ctx.send(embed=embed)
@ -2348,7 +2359,8 @@ class Audio(commands.Cog):
await self.config.ws_port.set(ws_port) await self.config.ws_port.set(ws_port)
if await self._check_external(): if await self._check_external():
embed = discord.Embed( embed = discord.Embed(
colour=await ctx.embed_colour(), title=_("Websocket port set to {}.").format(ws_port) colour=await ctx.embed_colour(),
title=_("Websocket port set to {}.").format(ws_port),
) )
embed.set_footer(text=_("External lavalink server set to True.")) embed.set_footer(text=_("External lavalink server set to True."))
await ctx.send(embed=embed) await ctx.send(embed=embed)

View File

@ -16,7 +16,7 @@ _ = Translator("Cleanup", __file__)
@cog_i18n(_) @cog_i18n(_)
class Cleanup(commands.Cog): class Cleanup(commands.Cog):
"""Commands for cleaning messages.""" """Commands for cleaning up messages."""
def __init__(self, bot: Red): def __init__(self, bot: Red):
super().__init__() super().__init__()
@ -41,7 +41,7 @@ class Cleanup(commands.Cog):
await prompt.delete() await prompt.delete()
try: try:
await response.delete() await response.delete()
except: except discord.HTTPException:
pass pass
return True return True
else: else:
@ -109,6 +109,7 @@ class Cleanup(commands.Cog):
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def text( async def text(
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
): ):
@ -121,9 +122,6 @@ class Cleanup(commands.Cog):
""" """
channel = ctx.channel 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 author = ctx.author
@ -157,6 +155,7 @@ class Cleanup(commands.Cog):
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def user( async def user(
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False 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` `[p]cleanup user Red 6`
""" """
channel = ctx.channel 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 member = None
try: try:
@ -215,6 +211,7 @@ class Cleanup(commands.Cog):
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False): async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
"""Delete all messages after a specified message. """Delete all messages after a specified message.
@ -224,9 +221,6 @@ class Cleanup(commands.Cog):
""" """
channel = ctx.channel 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 author = ctx.author
try: try:
@ -247,6 +241,7 @@ class Cleanup(commands.Cog):
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def before( async def before(
self, ctx: commands.Context, message_id: int, number: int, delete_pinned: bool = False self, ctx: commands.Context, message_id: int, number: int, delete_pinned: bool = False
): ):
@ -258,9 +253,6 @@ class Cleanup(commands.Cog):
""" """
channel = ctx.channel 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 author = ctx.author
try: try:
@ -281,6 +273,7 @@ class Cleanup(commands.Cog):
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False): async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
"""Delete the last X messages. """Delete the last X messages.
@ -289,9 +282,6 @@ class Cleanup(commands.Cog):
""" """
channel = ctx.channel 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 author = ctx.author
if number > 100: if number > 100:
@ -313,13 +303,11 @@ class Cleanup(commands.Cog):
@cleanup.command(name="bot") @cleanup.command(name="bot")
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False): async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
"""Clean up command messages and messages from the bot.""" """Clean up command messages and messages from the bot."""
channel = ctx.channel 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 author = ctx.message.author
if number > 100: if number > 100:

View File

@ -1,10 +1,9 @@
import os
import re import re
import random import random
from datetime import datetime, timedelta from datetime import datetime, timedelta
from inspect import Parameter from inspect import Parameter
from collections import OrderedDict from collections import OrderedDict
from typing import Mapping from typing import Mapping, Tuple, Dict
import discord import discord
@ -85,7 +84,7 @@ class CommandObj:
# in the ccinfo dict # in the ccinfo dict
return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow()) 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) ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
if not ccinfo: if not ccinfo:
raise NotFound() raise NotFound()
@ -180,9 +179,7 @@ class CommandObj:
@cog_i18n(_) @cog_i18n(_)
class CustomCommands(commands.Cog): class CustomCommands(commands.Cog):
"""Custom commands """Creates commands used to display text."""
Creates commands used to display text"""
def __init__(self, bot): def __init__(self, bot):
super().__init__() super().__init__()
@ -227,8 +224,6 @@ class CustomCommands(commands.Cog):
) )
) )
# await ctx.send(str(responses))
@cc_create.command(name="simple") @cc_create.command(name="simple")
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def cc_create_simple(self, ctx, command: str.lower, *, text: str): 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)) gaps = set(indices).symmetric_difference(range(high + 1))
if gaps: if gaps:
raise ArgParseError( raise ArgParseError(
_("Arguments must be sequential. Missing arguments: {}.").format( _("Arguments must be sequential. Missing arguments: ")
", ".join(str(i + low) for i in gaps) + ", ".join(str(i + low) for i in gaps)
)
) )
fin = [Parameter("_" + str(i), Parameter.POSITIONAL_OR_KEYWORD) for i in range(high + 1)] fin = [Parameter("_" + str(i), Parameter.POSITIONAL_OR_KEYWORD) for i in range(high + 1)]
for arg in args: for arg in args:
@ -481,8 +475,12 @@ class CustomCommands(commands.Cog):
and anno != fin[index].annotation and anno != fin[index].annotation
): ):
raise ArgParseError( 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: if anno is not Parameter.empty:
@ -511,6 +509,8 @@ class CustomCommands(commands.Cog):
key = (command, ctx.guild, ctx.channel) key = (command, ctx.guild, ctx.channel)
elif per == "member": elif per == "member":
key = (command, ctx.guild, ctx.author) key = (command, ctx.guild, ctx.author)
else:
raise ValueError(per)
cooldown = self.cooldowns.get(key) cooldown = self.cooldowns.get(key)
if cooldown: if cooldown:
cooldown += timedelta(seconds=rate) cooldown += timedelta(seconds=rate)

View File

@ -8,7 +8,7 @@ from sys import path as syspath
from typing import Tuple, Union, Iterable from typing import Tuple, Union, Iterable
import discord 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.bot import Red
from redbot.core.data_manager import cog_data_path from redbot.core.data_manager import cog_data_path
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
@ -218,7 +218,7 @@ class Downloader(commands.Cog):
"""Add a new repo. """Add a new repo.
The name can only contain characters A-z, numbers and underscores. 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) agreed = await do_install_agreement(ctx)
if not agreed: if not agreed:
@ -241,13 +241,13 @@ class Downloader(commands.Cog):
if repo.install_msg is not None: if repo.install_msg is not None:
await ctx.send(repo.install_msg.replace("[p]", ctx.prefix)) await ctx.send(repo.install_msg.replace("[p]", ctx.prefix))
@repo.command(name="delete", aliases=["remove"]) @repo.command(name="delete", aliases=["remove"], usage="<repo_name>")
async def _repo_del(self, ctx, repo_name: Repo): async def _repo_del(self, ctx, repo: Repo):
"""Remove a repo and its files.""" """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( 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") @repo.command(name="list")
@ -263,15 +263,15 @@ class Downloader(commands.Cog):
for page in pagify(joined, ["\n"], shorten_by=16): for page in pagify(joined, ["\n"], shorten_by=16):
await ctx.send(box(page.lstrip(" "), lang="diff")) await ctx.send(box(page.lstrip(" "), lang="diff"))
@repo.command(name="info") @repo.command(name="info", usage="<repo_name>")
async def _repo_info(self, ctx, repo_name: Repo): async def _repo_info(self, ctx, repo: Repo):
"""Show information about a repo.""" """Show information about a repo."""
if repo_name is None: if repo is None:
await ctx.send(_("Repo `{repo_name}` not found.").format(repo_name=repo_name.name)) await ctx.send(_("Repo `{repo.name}` not found.").format(repo=repo))
return return
msg = _("Information on {repo_name}:\n{description}").format( msg = _("Information on {repo.name}:\n{description}").format(
repo_name=repo_name.name, description=repo_name.description or "" repo=repo, description=repo.description or ""
) )
await ctx.send(box(msg)) await ctx.send(box(msg))
@ -281,15 +281,15 @@ class Downloader(commands.Cog):
"""Cog installation management commands.""" """Cog installation management commands."""
pass pass
@cog.command(name="install") @cog.command(name="install", usage="<repo_name> <cog_name>")
async def _cog_install(self, ctx, repo_name: Repo, cog_name: str): async def _cog_install(self, ctx, repo: Repo, cog_name: str):
"""Install a cog from the given repo.""" """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: if cog is None:
await ctx.send( await ctx.send(
_( _(
"Error: there is no cog by the name of `{cog_name}` in the `{repo_name}` repo." "Error: there is no cog by the name of `{cog_name}` in the `{repo.name}` repo."
).format(cog_name=cog_name, repo_name=repo_name.name) ).format(cog_name=cog_name, repo=repo)
) )
return return
elif cog.min_python_version > sys.version_info: elif cog.min_python_version > sys.version_info:
@ -300,7 +300,7 @@ class Downloader(commands.Cog):
) )
return 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( await ctx.send(
_( _(
"Failed to install the required libraries for `{cog_name}`: `{libraries}`" "Failed to install the required libraries for `{cog_name}`: `{libraries}`"
@ -308,31 +308,31 @@ class Downloader(commands.Cog):
) )
return 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 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)) await ctx.send(_("Cog `{cog_name}` successfully installed.").format(cog_name=cog_name))
if cog.install_msg is not None: if cog.install_msg is not None:
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix)) await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
@cog.command(name="uninstall") @cog.command(name="uninstall", usage="<cog_name>")
async def _cog_uninstall(self, ctx, cog_name: InstalledCog): async def _cog_uninstall(self, ctx, cog: InstalledCog):
"""Uninstall a cog. """Uninstall a cog.
You may only uninstall cogs which were previously installed You may only uninstall cogs which were previously installed
by Downloader. by Downloader.
""" """
# noinspection PyUnresolvedReferences,PyProtectedMember # noinspection PyUnresolvedReferences,PyProtectedMember
real_name = cog_name.name real_name = cog.name
poss_installed_path = (await self.cog_install_path()) / real_name poss_installed_path = (await self.cog_install_path()) / real_name
if poss_installed_path.exists(): if poss_installed_path.exists():
await self._delete_cog(poss_installed_path) await self._delete_cog(poss_installed_path)
# noinspection PyTypeChecker # noinspection PyTypeChecker
await self._remove_from_installed(cog_name) await self._remove_from_installed(cog)
await ctx.send( await ctx.send(
_("Cog `{cog_name}` was successfully uninstalled.").format(cog_name=real_name) _("Cog `{cog_name}` was successfully uninstalled.").format(cog_name=real_name)
) )
@ -410,8 +410,8 @@ class Downloader(commands.Cog):
else: else:
await ctx.send(_("OK then.")) await ctx.send(_("OK then."))
@cog.command(name="list") @cog.command(name="list", usage="<repo_name>")
async def _cog_list(self, ctx, repo_name: Repo): async def _cog_list(self, ctx, repo: Repo):
"""List all available cogs from a single repo.""" """List all available cogs from a single repo."""
installed = await self.installed_cogs() installed = await self.installed_cogs()
installed_str = "" installed_str = ""
@ -420,10 +420,10 @@ class Downloader(commands.Cog):
[ [
"- {}{}".format(i.name, ": {}".format(i.short) if i.short else "") "- {}{}".format(i.name, ": {}".format(i.short) if i.short else "")
for i in installed 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( cogs = _("Available Cogs:\n") + "\n".join(
[ [
"+ {}: {}".format(c.name, c.short or "") "+ {}: {}".format(c.name, c.short or "")
@ -435,14 +435,14 @@ class Downloader(commands.Cog):
for page in pagify(cogs, ["\n"], shorten_by=16): for page in pagify(cogs, ["\n"], shorten_by=16):
await ctx.send(box(page.lstrip(" "), lang="diff")) await ctx.send(box(page.lstrip(" "), lang="diff"))
@cog.command(name="info") @cog.command(name="info", usage="<repo_name> <cog_name>")
async def _cog_info(self, ctx, repo_name: Repo, cog_name: str): async def _cog_info(self, ctx, repo: Repo, cog_name: str):
"""List information about a single cog.""" """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: if cog is None:
await ctx.send( await ctx.send(
_("There is no cog `{cog_name}` in the repo `{repo_name}`").format( _("There is no cog `{cog_name}` in the repo `{repo.name}`").format(
cog_name=cog_name, repo_name=repo_name.name cog_name=cog_name, repo=repo
) )
) )
return return

View File

@ -3,6 +3,7 @@ import logging
import random import random
from collections import defaultdict, deque from collections import defaultdict, deque
from enum import Enum from enum import Enum
from typing import cast, Iterable
import discord import discord
@ -14,7 +15,7 @@ from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
from redbot.core.bot import Red from redbot.core.bot import Red
_ = Translator("Economy", __file__) T_ = Translator("Economy", __file__)
logger = logging.getLogger("red.economy") logger = logging.getLogger("red.economy")
@ -34,6 +35,7 @@ class SMReel(Enum):
snowflake = "\N{SNOWFLAKE}" snowflake = "\N{SNOWFLAKE}"
_ = lambda s: s
PAYOUTS = { PAYOUTS = {
(SMReel.two, SMReel.two, SMReel.six): { (SMReel.two, SMReel.two, SMReel.six): {
"payout": lambda x: x * 2500 + x, "payout": lambda x: x * 2500 + x,
@ -72,6 +74,7 @@ SLOT_PAYOUTS_MSG = _(
"Three symbols: +500\n" "Three symbols: +500\n"
"Two symbols: Bet * 2" "Two symbols: Bet * 2"
).format(**SMReel.__dict__) ).format(**SMReel.__dict__)
_ = T_
def guild_only_check(): def guild_only_check():
@ -106,9 +109,7 @@ class SetParser:
@cog_i18n(_) @cog_i18n(_)
class Economy(commands.Cog): class Economy(commands.Cog):
"""Economy """Get rich and have fun with imaginary currency!"""
Get rich and have fun with imaginary currency!"""
default_guild_settings = { default_guild_settings = {
"PAYDAY_TIME": 300, "PAYDAY_TIME": 300,
@ -142,12 +143,12 @@ class Economy(commands.Cog):
@guild_only_check() @guild_only_check()
@commands.group(name="bank") @commands.group(name="bank")
async def _bank(self, ctx: commands.Context): async def _bank(self, ctx: commands.Context):
"""Bank operations""" """Manage the bank."""
pass pass
@_bank.command() @_bank.command()
async def balance(self, ctx: commands.Context, user: discord.Member = None): async def balance(self, ctx: commands.Context, user: discord.Member = None):
"""Shows balance of user. """Show the user's account balance.
Defaults to yours.""" Defaults to yours."""
if user is None: if user is None:
@ -156,11 +157,15 @@ class Economy(commands.Cog):
bal = await bank.get_balance(user) bal = await bank.get_balance(user)
currency = await bank.get_currency_name(ctx.guild) 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() @_bank.command()
async def transfer(self, ctx: commands.Context, to: discord.Member, amount: int): 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 from_ = ctx.author
currency = await bank.get_currency_name(ctx.guild) currency = await bank.get_currency_name(ctx.guild)
@ -170,72 +175,83 @@ class Economy(commands.Cog):
return await ctx.send(str(e)) return await ctx.send(str(e))
await ctx.send( await ctx.send(
_("{} transferred {} {} to {}").format( _("{user} transferred {num} {currency} to {other_user}").format(
from_.display_name, amount, currency, to.display_name user=from_.display_name, num=amount, currency=currency, other_user=to.display_name
) )
) )
@_bank.command(name="set") @_bank.command(name="set")
@check_global_setting_admin() @check_global_setting_admin()
async def _set(self, ctx: commands.Context, to: discord.Member, creds: SetParser): 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: Examples:
bank set @Twentysix 26 - Sets balance to 26 - `[p]bank set @Twentysix 26` - Sets balance to 26
bank set @Twentysix +2 - Increases balance by 2 - `[p]bank set @Twentysix +2` - Increases balance by 2
bank set @Twentysix -6 - Decreases balance by 6""" - `[p]bank set @Twentysix -6` - Decreases balance by 6
"""
author = ctx.author author = ctx.author
currency = await bank.get_currency_name(ctx.guild) currency = await bank.get_currency_name(ctx.guild)
if creds.operation == "deposit": if creds.operation == "deposit":
await bank.deposit_credits(to, creds.sum) await bank.deposit_credits(to, creds.sum)
await ctx.send( await ctx.send(
_("{} added {} {} to {}'s account.").format( _("{author} added {num} {currency} to {user}'s account.").format(
author.display_name, creds.sum, currency, to.display_name author=author.display_name,
num=creds.sum,
currency=currency,
user=to.display_name,
) )
) )
elif creds.operation == "withdraw": elif creds.operation == "withdraw":
await bank.withdraw_credits(to, creds.sum) await bank.withdraw_credits(to, creds.sum)
await ctx.send( await ctx.send(
_("{} removed {} {} from {}'s account.").format( _("{author} removed {num} {currency} from {user}'s account.").format(
author.display_name, creds.sum, currency, to.display_name author=author.display_name,
num=creds.sum,
currency=currency,
user=to.display_name,
) )
) )
else: else:
await bank.set_balance(to, creds.sum) await bank.set_balance(to, creds.sum)
await ctx.send( await ctx.send(
_("{} set {}'s account to {} {}.").format( _("{author} set {users}'s account balance to {num} {currency}.").format(
author.display_name, to.display_name, creds.sum, currency author=author.display_name,
num=creds.sum,
currency=currency,
user=to.display_name,
) )
) )
@_bank.command() @_bank.command()
@check_global_setting_guildowner() @check_global_setting_guildowner()
async def reset(self, ctx, confirmation: bool = False): async def reset(self, ctx, confirmation: bool = False):
"""Deletes bank accounts""" """Delete all bank accounts."""
if confirmation is False: if confirmation is False:
await ctx.send( await ctx.send(
_( _(
"This will delete all bank accounts for {}.\nIf you're sure, type " "This will delete all bank accounts for {scope}.\nIf you're sure, type "
"`{}bank reset yes`" "`{prefix}bank reset yes`"
).format( ).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: else:
await bank.wipe_bank() await bank.wipe_bank(guild=ctx.guild)
await ctx.send( await ctx.send(
_("All bank accounts for {} have been deleted.").format( _("All bank accounts for {scope} have been deleted.").format(
self.bot.user.name if await bank.is_global() else "this server" scope=self.bot.user.name if await bank.is_global() else _("this server")
) )
) )
@guild_only_check() @guild_only_check()
@commands.command() @commands.command()
async def payday(self, ctx: commands.Context): async def payday(self, ctx: commands.Context):
"""Get some free currency""" """Get some free currency."""
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
@ -251,24 +267,25 @@ class Economy(commands.Cog):
pos = await bank.get_leaderboard_position(author) pos = await bank.get_leaderboard_position(author)
await ctx.send( await ctx.send(
_( _(
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n" "{author.mention} Here, take some {currency}. "
"You currently have {3} {1}.\n\n" "Enjoy! (+{amount} {new_balance}!)\n\n"
"You are currently #{4} on the global leaderboard!" "You currently have {new_balance} {currency}.\n\n"
"You are currently #{pos} on the global leaderboard!"
).format( ).format(
author, author=author,
credits_name, currency=credits_name,
str(await self.config.PAYDAY_CREDITS()), amount=await self.config.PAYDAY_CREDITS(),
str(await bank.get_balance(author)), new_balance=await bank.get_balance(author),
pos, pos=pos,
) )
) )
else: else:
dtime = self.display_time(next_payday - cur_time) dtime = self.display_time(next_payday - cur_time)
await ctx.send( 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: else:
next_payday = await self.config.member(author).next_payday() next_payday = await self.config.member(author).next_payday()
@ -286,31 +303,33 @@ class Economy(commands.Cog):
pos = await bank.get_leaderboard_position(author) pos = await bank.get_leaderboard_position(author)
await ctx.send( await ctx.send(
_( _(
"{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n" "{author.mention} Here, take some {currency}. "
"You currently have {3} {1}.\n\n" "Enjoy! (+{amount} {new_balance}!)\n\n"
"You are currently #{4} on the leaderboard!" "You currently have {new_balance} {currency}.\n\n"
"You are currently #{pos} on the global leaderboard!"
).format( ).format(
author, author=author,
credits_name, currency=credits_name,
credit_amount, amount=credit_amount,
str(await bank.get_balance(author)), new_balance=await bank.get_balance(author),
pos, pos=pos,
) )
) )
else: else:
dtime = self.display_time(next_payday - cur_time) dtime = self.display_time(next_payday - cur_time)
await ctx.send( 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() @commands.command()
@guild_only_check() @guild_only_check()
async def leaderboard(self, ctx: commands.Context, top: int = 10, show_global: bool = False): 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 guild = ctx.guild
author = ctx.author author = ctx.author
if top < 1: if top < 1:
@ -320,9 +339,9 @@ class Economy(commands.Cog):
): # show_global is only applicable if bank is global ): # show_global is only applicable if bank is global
guild = None guild = None
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild) bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
if len(bank_sorted) < top: header = "{pound:4}{name:36}{score:2}\n".format(
top = len(bank_sorted) pound="#", name=_("Name"), score=_("Score")
header = f"{f'#':4}{f'Name':36}{f'Score':2}\n" )
highscores = [ highscores = [
( (
f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} " f"{f'{pos}.': <{3 if pos < 10 else 2}} {acc[1]['name']: <{35}s} "
@ -347,13 +366,13 @@ class Economy(commands.Cog):
@commands.command() @commands.command()
@guild_only_check() @guild_only_check()
async def payouts(self, ctx: commands.Context): async def payouts(self, ctx: commands.Context):
"""Shows slot machine payouts""" """Show the payouts for the slot machine."""
await ctx.author.send(SLOT_PAYOUTS_MSG) await ctx.author.send(SLOT_PAYOUTS_MSG())
@commands.command() @commands.command()
@guild_only_check() @guild_only_check()
async def slot(self, ctx: commands.Context, bid: int): async def slot(self, ctx: commands.Context, bid: int):
"""Play the slot machine""" """Use the slot machine."""
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
channel = ctx.channel channel = ctx.channel
@ -386,8 +405,9 @@ class Economy(commands.Cog):
await self.config.member(author).last_slot.set(now) await self.config.member(author).last_slot.set(now)
await self.slot_machine(author, channel, bid) await self.slot_machine(author, channel, bid)
async def slot_machine(self, author, channel, bid): @staticmethod
default_reel = deque(SMReel) async def slot_machine(author, channel, bid):
default_reel = deque(cast(Iterable, SMReel))
reels = [] reels = []
for i in range(3): for i in range(3):
default_reel.rotate(random.randint(-999, 999)) # weeeeee default_reel.rotate(random.randint(-999, 999)) # weeeeee
@ -425,18 +445,24 @@ class Economy(commands.Cog):
pay = payout["payout"](bid) pay = payout["payout"](bid)
now = then - bid + pay now = then - bid + pay
await bank.set_balance(author, now) await bank.set_balance(author, now)
await channel.send( phrase = T_(payout["phrase"])
_("{}\n{} {}\n\nYour bid: {}\n{}{}!").format(
slot, author.mention, payout["phrase"], bid, then, now
)
)
else: else:
then = await bank.get_balance(author) then = await bank.get_balance(author)
await bank.withdraw_credits(author, bid) await bank.withdraw_credits(author, bid)
now = then - bid now = then - bid
phrase = _("Nothing!")
await channel.send( await channel.send(
_("{}\n{} Nothing!\nYour bid: {}\n{}{}!").format( (
slot, author.mention, bid, then, now "{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,
) )
) )
@ -444,39 +470,37 @@ class Economy(commands.Cog):
@guild_only_check() @guild_only_check()
@check_global_setting_admin() @check_global_setting_admin()
async def economyset(self, ctx: commands.Context): async def economyset(self, ctx: commands.Context):
"""Changes economy module settings""" """Manage Economy settings."""
guild = ctx.guild guild = ctx.guild
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
fmt = {}
if await bank.is_global(): if await bank.is_global():
fmt["slot_min"] = await self.config.SLOT_MIN() conf = self.config
fmt["slot_max"] = await self.config.SLOT_MAX()
fmt["slot_time"] = await self.config.SLOT_TIME()
fmt["payday_time"] = await self.config.PAYDAY_TIME()
fmt["payday_amount"] = await self.config.PAYDAY_CREDITS()
else: else:
fmt["slot_min"] = await self.config.guild(guild).SLOT_MIN() conf = self.config.guild(ctx.guild)
fmt["slot_max"] = await self.config.guild(guild).SLOT_MAX() await ctx.send(
fmt["slot_time"] = await self.config.guild(guild).SLOT_TIME() box(
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:" "----Economy Settings---\n"
"Minimum slot bid: {slot_min}\n" "Minimum slot bid: {slot_min}\n"
"Maximum slot bid: {slot_max}\n" "Maximum slot bid: {slot_max}\n"
"Slot cooldown: {slot_time}\n" "Slot cooldown: {slot_time}\n"
"Payday amount: {payday_amount}\n" "Payday amount: {payday_amount}\n"
"Payday cooldown: {payday_time}\n" "Payday cooldown: {payday_time}\n"
"Amount given at account registration: {register_amount}" "Amount given at account registration: {register_amount}"
).format(**fmt) ).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() @economyset.command()
async def slotmin(self, ctx: commands.Context, bid: int): async def slotmin(self, ctx: commands.Context, bid: int):
"""Minimum slot machine bid""" """Set the minimum slot machine bid."""
if bid < 1: if bid < 1:
await ctx.send(_("Invalid min bid amount.")) await ctx.send(_("Invalid min bid amount."))
return return
@ -492,10 +516,12 @@ class Economy(commands.Cog):
@economyset.command() @economyset.command()
async def slotmax(self, ctx: commands.Context, bid: int): 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() slot_min = await self.config.SLOT_MIN()
if bid < 1 or bid < 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 return
guild = ctx.guild guild = ctx.guild
credits_name = await bank.get_currency_name(guild) credits_name = await bank.get_currency_name(guild)
@ -509,7 +535,7 @@ class Economy(commands.Cog):
@economyset.command() @economyset.command()
async def slottime(self, ctx: commands.Context, seconds: int): async def slottime(self, ctx: commands.Context, seconds: int):
"""Seconds between each slots use""" """Set the cooldown for the slot machine."""
guild = ctx.guild guild = ctx.guild
if await bank.is_global(): if await bank.is_global():
await self.config.SLOT_TIME.set(seconds) await self.config.SLOT_TIME.set(seconds)
@ -519,7 +545,7 @@ class Economy(commands.Cog):
@economyset.command() @economyset.command()
async def paydaytime(self, ctx: commands.Context, seconds: int): async def paydaytime(self, ctx: commands.Context, seconds: int):
"""Seconds between each payday""" """Set the cooldown for payday."""
guild = ctx.guild guild = ctx.guild
if await bank.is_global(): if await bank.is_global():
await self.config.PAYDAY_TIME.set(seconds) await self.config.PAYDAY_TIME.set(seconds)
@ -533,7 +559,7 @@ class Economy(commands.Cog):
@economyset.command() @economyset.command()
async def paydayamount(self, ctx: commands.Context, creds: int): async def paydayamount(self, ctx: commands.Context, creds: int):
"""Amount earned each payday""" """Set the amount earned each payday."""
guild = ctx.guild guild = ctx.guild
credits_name = await bank.get_currency_name(guild) credits_name = await bank.get_currency_name(guild)
if creds <= 0: if creds <= 0:
@ -551,11 +577,11 @@ class Economy(commands.Cog):
@economyset.command() @economyset.command()
async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int): 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 guild = ctx.guild
credits_name = await bank.get_currency_name(guild) credits_name = await bank.get_currency_name(guild)
if await bank.is_global(): 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: else:
await self.config.role(role).PAYDAY_CREDITS.set(creds) await self.config.role(role).PAYDAY_CREDITS.set(creds)
await ctx.send( await ctx.send(
@ -567,7 +593,7 @@ class Economy(commands.Cog):
@economyset.command() @economyset.command()
async def registeramount(self, ctx: commands.Context, creds: int): 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 guild = ctx.guild
if creds < 0: if creds < 0:
creds = 0 creds = 0
@ -580,7 +606,8 @@ class Economy(commands.Cog):
) )
# What would I ever do without stackoverflow? # 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 intervals = ( # Source: http://stackoverflow.com/a/24542445
(_("weeks"), 604800), # 60 * 60 * 24 * 7 (_("weeks"), 604800), # 60 * 60 * 24 * 7
(_("days"), 86400), # 60 * 60 * 24 (_("days"), 86400), # 60 * 60 * 24

View File

@ -5,14 +5,13 @@ from redbot.core import checks, Config, modlog, commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import pagify from redbot.core.utils.chat_formatting import pagify
from redbot.core.utils.mod import is_mod_or_superior
_ = Translator("Filter", __file__) _ = Translator("Filter", __file__)
@cog_i18n(_) @cog_i18n(_)
class Filter(commands.Cog): class Filter(commands.Cog):
"""Filter-related commands""" """Filter unwanted words and phrases from text channels."""
def __init__(self, bot: Red): def __init__(self, bot: Red):
super().__init__() super().__init__()
@ -35,7 +34,8 @@ class Filter(commands.Cog):
def __unload(self): def __unload(self):
self.register_task.cancel() self.register_task.cancel()
async def register_filterban(self): @staticmethod
async def register_filterban():
try: try:
await modlog.register_casetype( await modlog.register_casetype(
"filterban", False, ":filing_cabinet: :hammer:", "Filter ban", "ban" "filterban", False, ":filing_cabinet: :hammer:", "Filter ban", "ban"
@ -47,18 +47,17 @@ class Filter(commands.Cog):
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(manage_guild=True) @checks.admin_or_permissions(manage_guild=True)
async def filterset(self, ctx: commands.Context): async def filterset(self, ctx: commands.Context):
""" """Manage filter settings."""
Filter settings
"""
pass pass
@filterset.command(name="defaultname") @filterset.command(name="defaultname")
async def filter_default_name(self, ctx: commands.Context, name: str): 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 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 guild = ctx.guild
await self.settings.guild(guild).filter_default_name.set(name) await self.settings.guild(guild).filter_default_name.set(name)
@ -66,9 +65,12 @@ class Filter(commands.Cog):
@filterset.command(name="ban") @filterset.command(name="ban")
async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int): 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 `<count>` filtered words in
`<timeframe>` seconds.
Set both to zero to disable autoban.
""" """
if (count <= 0) != (timeframe <= 0): if (count <= 0) != (timeframe <= 0):
await ctx.send( await ctx.send(
@ -91,11 +93,13 @@ class Filter(commands.Cog):
@commands.guild_only() @commands.guild_only()
@checks.mod_or_permissions(manage_messages=True) @checks.mod_or_permissions(manage_messages=True)
async def _filter(self, ctx: commands.Context): 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 Use double quotes to add or remove sentences.
Using this command with no subcommands will send
the list of the server's filtered words.""" Using this command with no subcommands will send the list of
the server's filtered words.
"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
server = ctx.guild server = ctx.guild
author = ctx.author author = ctx.author
@ -111,11 +115,13 @@ class Filter(commands.Cog):
@_filter.group(name="channel") @_filter.group(name="channel")
async def _filter_channel(self, ctx: commands.Context): 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 Use double quotes to add or remove sentences.
Using this command with no subcommands will send
the list of the channel's filtered words.""" Using this command with no subcommands will send the list of
the channel's filtered words.
"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
channel = ctx.channel channel = ctx.channel
author = ctx.author author = ctx.author
@ -131,12 +137,14 @@ class Filter(commands.Cog):
@_filter_channel.command("add") @_filter_channel.command("add")
async def filter_channel_add(self, ctx: commands.Context, *, words: str): 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: Examples:
filter add word1 word2 word3 - `[p]filter channel add word1 word2 word3`
filter add \"This is a sentence\"""" - `[p]filter channel add "This is a sentence"`
"""
channel = ctx.channel channel = ctx.channel
split_words = words.split() split_words = words.split()
word_list = [] word_list = []
@ -161,12 +169,14 @@ class Filter(commands.Cog):
@_filter_channel.command("remove") @_filter_channel.command("remove")
async def filter_channel_remove(self, ctx: commands.Context, *, words: str): 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: Examples:
filter remove word1 word2 word3 - `[p]filter channel remove word1 word2 word3`
filter remove \"This is a sentence\"""" - `[p]filter channel remove "This is a sentence"`
"""
channel = ctx.channel channel = ctx.channel
split_words = words.split() split_words = words.split()
word_list = [] word_list = []
@ -191,12 +201,14 @@ class Filter(commands.Cog):
@_filter.command(name="add") @_filter.command(name="add")
async def filter_add(self, ctx: commands.Context, *, words: str): 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: Examples:
filter add word1 word2 word3 - `[p]filter add word1 word2 word3`
filter add \"This is a sentence\"""" - `[p]filter add "This is a sentence"`
"""
server = ctx.guild server = ctx.guild
split_words = words.split() split_words = words.split()
word_list = [] word_list = []
@ -215,18 +227,20 @@ class Filter(commands.Cog):
tmp += word + " " tmp += word + " "
added = await self.add_to_filter(server, word_list) added = await self.add_to_filter(server, word_list)
if added: if added:
await ctx.send(_("Words added to filter.")) await ctx.send(_("Words successfully added to filter."))
else: else:
await ctx.send(_("Words already in the filter.")) await ctx.send(_("Those words were already in the filter."))
@_filter.command(name="remove") @_filter.command(name="remove")
async def filter_remove(self, ctx: commands.Context, *, words: str): 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: Examples:
filter remove word1 word2 word3 - `[p]filter remove word1 word2 word3`
filter remove \"This is a sentence\"""" - `[p]filter remove "This is a sentence"`
"""
server = ctx.guild server = ctx.guild
split_words = words.split() split_words = words.split()
word_list = [] word_list = []
@ -245,23 +259,23 @@ class Filter(commands.Cog):
tmp += word + " " tmp += word + " "
removed = await self.remove_from_filter(server, word_list) removed = await self.remove_from_filter(server, word_list)
if removed: if removed:
await ctx.send(_("Words removed from filter.")) await ctx.send(_("Words successfully removed from filter."))
else: else:
await ctx.send(_("Those words weren't in the filter.")) await ctx.send(_("Those words weren't in the filter."))
@_filter.command(name="names") @_filter.command(name="names")
async def filter_names(self, ctx: commands.Context): 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 guild = ctx.guild
current_setting = await self.settings.guild(guild).filter_names() current_setting = await self.settings.guild(guild).filter_names()
await self.settings.guild(guild).filter_names.set(not current_setting) await self.settings.guild(guild).filter_names.set(not current_setting)
if 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: 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( async def add_to_filter(
self, server_or_channel: Union[discord.Guild, discord.TextChannel], words: list 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(): if w in message.content.lower():
try: try:
await message.delete() await message.delete()
except: except discord.HTTPException:
pass pass
else: else:
if filter_count > 0 and filter_time > 0: if filter_count > 0 and filter_time > 0:
@ -337,10 +351,10 @@ class Filter(commands.Cog):
user_count >= filter_count user_count >= filter_count
and message.created_at.timestamp() < next_reset_time and message.created_at.timestamp() < next_reset_time
): ):
reason = "Autoban (too many filtered messages.)" reason = _("Autoban (too many filtered messages.)")
try: try:
await server.ban(author, reason=reason) await server.ban(author, reason=reason)
except: except discord.HTTPException:
pass pass
else: else:
await modlog.create_case( await modlog.create_case(
@ -366,20 +380,6 @@ class Filter(commands.Cog):
await self.check_filter(message) 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): async def on_message_edit(self, _prior, message):
# message content has to change for non-bot's currently. # message content has to change for non-bot's currently.
# if this changes, we should compare before passing it. # if this changes, we should compare before passing it.
@ -399,14 +399,14 @@ class Filter(commands.Cog):
return # Discord Hierarchy applies to nicks return # Discord Hierarchy applies to nicks
if await self.bot.is_automod_immune(member): if await self.bot.is_automod_immune(member):
return return
word_list = await self.settings.guild(member.guild).filter()
if not await self.settings.guild(member.guild).filter_names(): if not await self.settings.guild(member.guild).filter_names():
return return
word_list = await self.settings.guild(member.guild).filter()
for w in word_list: for w in word_list:
if w in member.display_name.lower(): if w in member.display_name.lower():
name_to_use = await self.settings.guild(member.guild).filter_default_name() 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: try:
await member.edit(nick=name_to_use, reason=reason) await member.edit(nick=name_to_use, reason=reason)
except discord.HTTPException: except discord.HTTPException:

View File

@ -2,15 +2,14 @@ import datetime
import time import time
from enum import Enum from enum import Enum
from random import randint, choice from random import randint, choice
from urllib.parse import quote_plus
import aiohttp import aiohttp
import discord import discord
from redbot.core import commands from redbot.core import commands
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS 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): class RPS(Enum):
@ -29,17 +28,16 @@ class RPSParser:
elif argument == "scissors": elif argument == "scissors":
self.choice = RPS.scissors self.choice = RPS.scissors
else: else:
raise raise ValueError
@cog_i18n(_) @cog_i18n(_)
class General(commands.Cog): class General(commands.Cog):
"""General commands.""" """General commands."""
def __init__(self): global _
super().__init__() _ = lambda s: s
self.stopwatches = {} ball = [
self.ball = [
_("As I see it, yes"), _("As I see it, yes"),
_("It is certain"), _("It is certain"),
_("It is decidedly so"), _("It is decidedly so"),
@ -61,39 +59,47 @@ class General(commands.Cog):
_("Outlook not so good"), _("Outlook not so good"),
_("Very doubtful"), _("Very doubtful"),
] ]
_ = T_
def __init__(self):
super().__init__()
self.stopwatches = {}
@commands.command() @commands.command()
async def choose(self, ctx, *choices): 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] choices = [escape(c, mass_mentions=True) for c in choices]
if len(choices) < 2: if len(choices) < 2:
await ctx.send(_("Not enough choices to pick from.")) await ctx.send(_("Not enough options to pick from."))
else: else:
await ctx.send(choice(choices)) await ctx.send(choice(choices))
@commands.command() @commands.command()
async def roll(self, ctx, number: int = 100): 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 `<number>`.
`<number>` defaults to 100.
""" """
author = ctx.author author = ctx.author
if number > 1: if number > 1:
n = randint(1, number) 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: 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() @commands.command()
async def flip(self, ctx, user: discord.Member = None): 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 = "" msg = ""
if user.id == ctx.bot.user.id: if user.id == ctx.bot.user.id:
user = ctx.author user = ctx.author
@ -112,7 +118,7 @@ class General(commands.Cog):
@commands.command() @commands.command()
async def rps(self, ctx, your_choice: RPSParser): async def rps(self, ctx, your_choice: RPSParser):
"""Play rock paper scissors""" """Play Rock Paper Scissors."""
author = ctx.author author = ctx.author
player_choice = your_choice.choice player_choice = your_choice.choice
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors)) red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
@ -151,31 +157,33 @@ class General(commands.Cog):
@commands.command(name="8", aliases=["8ball"]) @commands.command(name="8", aliases=["8ball"])
async def _8ball(self, ctx, *, question: str): async def _8ball(self, ctx, *, question: str):
"""Ask 8 ball a question """Ask 8 ball a question.
Question must end with a question mark. Question must end with a question mark.
""" """
if question.endswith("?") and question != "?": if question.endswith("?") and question != "?":
await ctx.send("`" + choice(self.ball) + "`") await ctx.send("`" + T_(choice(self.ball)) + "`")
else: else:
await ctx.send(_("That doesn't look like a question.")) await ctx.send(_("That doesn't look like a question."))
@commands.command(aliases=["sw"]) @commands.command(aliases=["sw"])
async def stopwatch(self, ctx): async def stopwatch(self, ctx):
"""Starts/stops stopwatch""" """Start or stop the stopwatch."""
author = ctx.author 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()) self.stopwatches[author.id] = int(time.perf_counter())
await ctx.send(author.mention + _(" Stopwatch started!")) await ctx.send(author.mention + _(" Stopwatch started!"))
else: else:
tmp = abs(self.stopwatches[author.id] - int(time.perf_counter())) tmp = abs(self.stopwatches[author.id] - int(time.perf_counter()))
tmp = str(datetime.timedelta(seconds=tmp)) 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) self.stopwatches.pop(author.id, None)
@commands.command() @commands.command()
async def lmgtfy(self, ctx, *, search_terms: str): async def lmgtfy(self, ctx, *, search_terms: str):
"""Creates a lmgtfy link""" """Create a lmgtfy link."""
search_terms = escape( search_terms = escape(
search_terms.replace("+", "%2B").replace(" ", "+"), mass_mentions=True search_terms.replace("+", "%2B").replace(" ", "+"), mass_mentions=True
) )
@ -184,9 +192,10 @@ class General(commands.Cog):
@commands.command(hidden=True) @commands.command(hidden=True)
@commands.guild_only() @commands.guild_only()
async def hug(self, ctx, user: discord.Member, intensity: int = 1): 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) name = italics(user.display_name)
if intensity <= 0: if intensity <= 0:
msg = "(っ˘̩╭╮˘̩)っ" + name msg = "(っ˘̩╭╮˘̩)っ" + name
@ -198,12 +207,15 @@ class General(commands.Cog):
msg = "(つ≧▽≦)つ" + name msg = "(つ≧▽≦)つ" + name
elif intensity >= 10: elif intensity >= 10:
msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name) msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name)
else:
# For the purposes of "msg might not be defined" linter errors
raise RuntimeError
await ctx.send(msg) await ctx.send(msg)
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
async def serverinfo(self, ctx): async def serverinfo(self, ctx):
"""Shows server's informations""" """Show server information."""
guild = ctx.guild guild = ctx.guild
online = len([m.status for m in guild.members if m.status != discord.Status.offline]) online = len([m.status for m in guild.members if m.status != discord.Status.offline])
total_users = len(guild.members) total_users = len(guild.members)
@ -230,12 +242,15 @@ class General(commands.Cog):
try: try:
await ctx.send(embed=data) await ctx.send(embed=data)
except discord.HTTPException: except discord.Forbidden:
await ctx.send(_("I need the `Embed links` permission to send this.")) await ctx.send(_("I need the `Embed links` permission to send this."))
@commands.command() @commands.command()
async def urban(self, ctx, *, word): 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: try:
url = "https://api.urbandictionary.com/v0/define?term=" + str(word).lower() 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: async with session.get(url, headers=headers) as response:
data = await response.json() data = await response.json()
except: except aiohttp.ClientError:
await ctx.send( await ctx.send(
_("No Urban dictionary entries were found, or there was an error in the process") _("No Urban dictionary entries were found, or there was an error in the process")
) )
@ -287,17 +302,16 @@ class General(commands.Cog):
) )
else: else:
messages = [] messages = []
ud.set_default("example", "N/A")
for ud in data["list"]: for ud in data["list"]:
ud.set_default("example", "N/A")
description = _("{definition}\n\n**Example:** {example}").format(**ud) description = _("{definition}\n\n**Example:** {example}").format(**ud)
if len(description) > 2048: if len(description) > 2048:
description = "{}...".format(description[:2045]) description = "{}...".format(description[:2045])
description = description
message = _( message = _(
"<{permalink}>\n {word} by {author}\n\n{description}\n\n" "<{permalink}>\n {word} by {author}\n\n{description}\n\n"
"{thumbs_down} Down / {thumbs_up} Up, Powered by urban dictionary" "{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) messages.append(message)
if messages is not None and len(messages) > 0: if messages is not None and len(messages) > 0:

View File

@ -29,23 +29,26 @@ class Image(commands.Cog):
@commands.group(name="imgur") @commands.group(name="imgur")
async def _imgur(self, ctx): async def _imgur(self, ctx):
"""Retrieves pictures from imgur """Retrieve pictures from Imgur.
Make sure to set the client ID using Make sure to set the Client ID using `[p]imgurcreds`.
[p]imgurcreds""" """
pass pass
@_imgur.command(name="search") @_imgur.command(name="search")
async def imgur_search(self, ctx, *, term: str): 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" url = self.imgur_base_url + "gallery/search/time/all/0"
params = {"q": term} params = {"q": term}
imgur_client_id = await self.settings.imgur_client_id() imgur_client_id = await self.settings.imgur_client_id()
if not imgur_client_id: if not imgur_client_id:
await ctx.send( 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 return
headers = {"Authorization": "Client-ID {}".format(imgur_client_id)} headers = {"Authorization": "Client-ID {}".format(imgur_client_id)}
@ -64,37 +67,41 @@ class Image(commands.Cog):
msg += "\n" msg += "\n"
await ctx.send(msg) await ctx.send(msg)
else: 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") @_imgur.command(name="subreddit")
async def imgur_subreddit( async def imgur_subreddit(
self, ctx, subreddit: str, sort_type: str = "top", window: str = "day" 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 You can customize the search with the following options:
Time windows: day, week, month, year, all""" - `<sort_type>`: new, top
- `<window>`: day, week, month, year, all
"""
sort_type = sort_type.lower() sort_type = sort_type.lower()
window = window.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": if sort_type == "new":
sort = "time" sort = "time"
elif sort_type == "top": elif sort_type == "top":
sort = "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() imgur_client_id = await self.settings.imgur_client_id()
if not imgur_client_id: if not imgur_client_id:
await ctx.send( 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 return
@ -117,29 +124,33 @@ class Image(commands.Cog):
else: else:
await ctx.send(_("No results found.")) await ctx.send(_("No results found."))
else: 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() @checks.is_owner()
@commands.command() @commands.command()
async def imgurcreds(self, ctx, imgur_client_id: str): 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 To get an Imgur Client ID:
1. Login to an Imgur account.
You can get these by visiting https://api.imgur.com/oauth2/addclient 2. Visit [this](https://api.imgur.com/oauth2/addclient) page
and filling out the form. Enter a name for the application, select 3. Enter a name for your application
'Anonymous usage without user authorization' for the auth type, 4. Select *Anonymous usage without user authorization* for the auth type
set the authorization callback url to 'https://localhost' 5. Set the authorization callback URL to `https://localhost`
leave the app website blank, enter a valid email address, and 6. Leave the app website blank
enter a description. Check the box for the captcha, then click Next. 7. Enter a valid email address and a description
Your client ID will be on the page that loads.""" 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 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.guild_only()
@commands.command() @commands.command()
async def gif(self, ctx, *keywords): async def gif(self, ctx, *keywords):
"""Retrieves first search result from giphy""" """Retrieve the first search result from Giphy."""
if keywords: if keywords:
keywords = "+".join(keywords) keywords = "+".join(keywords)
else: else:
@ -158,12 +169,12 @@ class Image(commands.Cog):
else: else:
await ctx.send(_("No results found.")) await ctx.send(_("No results found."))
else: else:
await ctx.send(_("Error contacting the API.")) await ctx.send(_("Error contacting the Giphy API."))
@commands.guild_only() @commands.guild_only()
@commands.command() @commands.command()
async def gifr(self, ctx, *keywords): async def gifr(self, ctx, *keywords):
"""Retrieves a random gif from a giphy search""" """Retrieve a random GIF from a Giphy search."""
if keywords: if keywords:
keywords = "+".join(keywords) keywords = "+".join(keywords)
else: else:

View File

@ -1,5 +1,4 @@
from redbot.core import commands from redbot.core import commands
import discord
def mod_or_voice_permissions(**perms): def mod_or_voice_permissions(**perms):

View File

@ -1,6 +1,8 @@
import asyncio import asyncio
import contextlib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from collections import deque, defaultdict, namedtuple from collections import deque, defaultdict, namedtuple
from typing import cast
import discord import discord
@ -14,7 +16,7 @@ from .log import log
from redbot.core.utils.common_filters import filter_invites, filter_various_mentions from redbot.core.utils.common_filters import filter_invites, filter_various_mentions
_ = Translator("Mod", __file__) _ = T_ = Translator("Mod", __file__)
@cog_i18n(_) @cog_i18n(_)
@ -58,7 +60,8 @@ class Mod(commands.Cog):
self.registration_task.cancel() self.registration_task.cancel()
self.tban_expiry_task.cancel() self.tban_expiry_task.cancel()
async def _casetype_registration(self): @staticmethod
async def _casetype_registration():
casetypes_to_register = [ casetypes_to_register = [
{ {
"name": "ban", "name": "ban",
@ -168,7 +171,7 @@ class Mod(commands.Cog):
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def modset(self, ctx: commands.Context): async def modset(self, ctx: commands.Context):
"""Manages server administration settings.""" """Manage server administration settings."""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
guild = ctx.guild guild = ctx.guild
# Display current settings # Display current settings
@ -183,7 +186,7 @@ class Mod(commands.Cog):
) )
msg += _("Ban mention spam: {num_mentions}\n").format( msg += _("Ban mention spam: {num_mentions}\n").format(
num_mentions=_("{num} mentions").format(num=ban_mention_spam) num_mentions=_("{num} mentions").format(num=ban_mention_spam)
if isinstance(ban_mention_spam, int) if ban_mention_spam
else _("No") else _("No")
) )
msg += _("Respects hierarchy: {yes_or_no}\n").format( msg += _("Respects hierarchy: {yes_or_no}\n").format(
@ -195,14 +198,20 @@ class Mod(commands.Cog):
else _("None") else _("None")
) )
msg += _("Reinvite on unban: {yes_or_no}\n").format( 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)) await ctx.send(box(msg))
@modset.command() @modset.command()
@commands.guild_only() @commands.guild_only()
async def hierarchy(self, ctx: commands.Context): 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 guild = ctx.guild
toggled = await self.settings.guild(guild).respect_hierarchy() toggled = await self.settings.guild(guild).respect_hierarchy()
if not toggled: if not toggled:
@ -218,10 +227,14 @@ class Mod(commands.Cog):
@modset.command() @modset.command()
@commands.guild_only() @commands.guild_only()
async def banmentionspam(self, ctx: commands.Context, max_mentions: int = False): async def banmentionspam(self, ctx: commands.Context, max_mentions: int = 0):
"""Enables auto ban for messages mentioning X different people """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
`<max_mentions>` mentions.
`<max_mentions>` must be at least 5. Set to 0 to disable.
"""
guild = ctx.guild guild = ctx.guild
if max_mentions: if max_mentions:
if max_mentions < 5: if max_mentions < 5:
@ -236,7 +249,7 @@ class Mod(commands.Cog):
) )
else: else:
cur_setting = await self.settings.guild(guild).ban_mention_spam() cur_setting = await self.settings.guild(guild).ban_mention_spam()
if cur_setting is False: if not cur_setting:
await ctx.send_help() await ctx.send_help()
return return
await self.settings.guild(guild).ban_mention_spam.set(False) await self.settings.guild(guild).ban_mention_spam.set(False)
@ -245,7 +258,7 @@ class Mod(commands.Cog):
@modset.command() @modset.command()
@commands.guild_only() @commands.guild_only()
async def deleterepeats(self, ctx: commands.Context): async def deleterepeats(self, ctx: commands.Context):
"""Enables auto deletion of repeated messages""" """Enable auto-deletion of repeated messages."""
guild = ctx.guild guild = ctx.guild
cur_setting = await self.settings.guild(guild).delete_repeats() cur_setting = await self.settings.guild(guild).delete_repeats()
if not cur_setting: if not cur_setting:
@ -258,11 +271,12 @@ class Mod(commands.Cog):
@modset.command() @modset.command()
@commands.guild_only() @commands.guild_only()
async def deletedelay(self, ctx: commands.Context, time: int = None): 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. 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 guild = ctx.guild
if time is not None: if time is not None:
time = min(max(time, -1), 60) # Enforces the time limits time = min(max(time, -1), 60) # Enforces the time limits
@ -287,10 +301,11 @@ class Mod(commands.Cog):
@modset.command() @modset.command()
@commands.guild_only() @commands.guild_only()
async def reinvite(self, ctx: commands.Context): 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 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 guild = ctx.guild
cur_setting = await self.settings.guild(guild).reinvite_on_unban() cur_setting = await self.settings.guild(guild).reinvite_on_unban()
if not cur_setting: if not cur_setting:
@ -308,12 +323,14 @@ class Mod(commands.Cog):
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(kick_members=True)
@checks.admin_or_permissions(kick_members=True) @checks.admin_or_permissions(kick_members=True)
async def kick(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): 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 If a reason is specified, it will be the reason that shows up
in the audit log""" in the audit log.
"""
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
@ -364,14 +381,18 @@ class Mod(commands.Cog):
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True)
async def ban( async def ban(
self, ctx: commands.Context, user: discord.Member, days: str = None, *, reason: str = None 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. Deletes `<days>` worth of messages.
Minimum 0 days, maximum 7. Defaults to 0."""
If `<days>` 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 author = ctx.author
guild = ctx.guild guild = ctx.guild
@ -445,16 +466,16 @@ class Mod(commands.Cog):
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True)
async def hackban(self, ctx: commands.Context, user_id: int, *, reason: str = None): async def hackban(self, ctx: commands.Context, user_id: int, *, reason: str = None):
"""Preemptively bans user from the server """Pre-emptively ban a user from the current server.
A user ID needs to be provided in order to ban A user ID needs to be provided in order to ban
using this command""" using this command.
"""
author = ctx.author author = ctx.author
guild = ctx.guild 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 is_banned = False
ban_list = await guild.bans() ban_list = await guild.bans()
for entry in ban_list: for entry in ban_list:
@ -505,31 +526,30 @@ class Mod(commands.Cog):
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True)
async def tempban( async def tempban(
self, ctx: commands.Context, user: discord.Member, days: int = 1, *, reason: str = None 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 guild = ctx.guild
author = ctx.author author = ctx.author
days_delta = timedelta(days=int(days)) days_delta = timedelta(days=int(days))
unban_time = datetime.utcnow() + days_delta 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)) invite = await self.get_invite_for_reinvite(ctx, int(days_delta.total_seconds() + 86400))
if invite is None: if invite is None:
invite = "" invite = ""
if can_ban:
queue_entry = (guild.id, user.id) queue_entry = (guild.id, user.id)
await self.settings.member(user).banned_until.set(unban_time.timestamp()) await self.settings.member(user).banned_until.set(unban_time.timestamp())
cur_tbans = await self.settings.guild(guild).current_tempbans() cur_tbans = await self.settings.guild(guild).current_tempbans()
cur_tbans.append(user.id) cur_tbans.append(user.id)
await self.settings.guild(guild).current_tempbans.set(cur_tbans) await self.settings.guild(guild).current_tempbans.set(cur_tbans)
try: # We don't want blocked DMs preventing us from banning with contextlib.suppress(discord.HTTPException):
msg = await user.send( # We don't want blocked DMs preventing us from banning
await user.send(
_( _(
"You have been temporarily banned from {server_name} until {date}. " "You have been temporarily banned from {server_name} until {date}. "
"Here is an invite for when your ban expires: {invite_link}" "Here is an invite for when your ban expires: {invite_link}"
@ -539,8 +559,6 @@ class Mod(commands.Cog):
invite_link=invite, invite_link=invite,
) )
) )
except discord.HTTPException:
msg = None
self.ban_queue.append(queue_entry) self.ban_queue.append(queue_entry)
try: try:
await guild.ban(user) await guild.ban(user)
@ -567,12 +585,11 @@ class Mod(commands.Cog):
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True)
async def softban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): async def softban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Kicks the user, deleting 1 day worth of messages.""" """Kick a user and delete 1 day's worth of their messages."""
guild = ctx.guild guild = ctx.guild
channel = ctx.channel
can_ban = channel.permissions_for(guild.me).ban_members
author = ctx.author author = ctx.author
if author == user: if author == user:
@ -598,7 +615,6 @@ class Mod(commands.Cog):
if invite is None: if invite is None:
invite = "" invite = ""
if can_ban:
queue_entry = (guild.id, user.id) queue_entry = (guild.id, user.id)
try: # We don't want blocked DMs preventing us from banning try: # We don't want blocked DMs preventing us from banning
msg = await user.send( msg = await user.send(
@ -650,23 +666,18 @@ class Mod(commands.Cog):
) )
except RuntimeError as e: except RuntimeError as e:
await ctx.send(e) await ctx.send(e)
else:
await ctx.send(_("I'm not allowed to do that."))
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True)
async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = None): 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: 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 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- 2. enable developer mode, go to Bans in this server's settings, right-
click the user and select 'Copy ID'.""" 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 guild = ctx.guild
author = ctx.author author = ctx.author
user = await self.bot.get_user_info(user_id) 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) @admin_or_voice_permissions(mute_members=True, deafen_members=True)
@bot_has_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): 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 user_voice_state = user.voice
if user_voice_state is None: if user_voice_state is None:
await ctx.send(_("No voice state for that user!")) 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) @admin_or_voice_permissions(mute_members=True, deafen_members=True)
@bot_has_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): 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 user_voice_state = user.voice
if user_voice_state is None: if user_voice_state is None:
await ctx.send(_("No voice state for that user!")) await ctx.send(_("No voice state for that user!"))
@ -850,29 +861,24 @@ class Mod(commands.Cog):
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_nicknames=True)
@checks.admin_or_permissions(manage_nicknames=True) @checks.admin_or_permissions(manage_nicknames=True)
async def rename(self, ctx: commands.Context, user: discord.Member, *, nickname=""): 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() nickname = nickname.strip()
if nickname == "": if nickname == "":
nickname = None nickname = None
try:
await user.edit(reason=get_audit_reason(ctx.author, None), nick=nickname) await user.edit(reason=get_audit_reason(ctx.author, None), nick=nickname)
await ctx.send("Done.") await ctx.send("Done.")
except discord.Forbidden:
await ctx.send(
_("I cannot do that, I lack the '{perm}' permission.").format(
perm="Manage Nicknames"
)
)
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@checks.mod_or_permissions(manage_channel=True) @checks.mod_or_permissions(manage_channel=True)
async def mute(self, ctx: commands.Context): async def mute(self, ctx: commands.Context):
"""Mutes user in the channel/server""" """Mute users."""
pass pass
@mute.command(name="voice") @mute.command(name="voice")
@ -880,7 +886,7 @@ class Mod(commands.Cog):
@mod_or_voice_permissions(mute_members=True) @mod_or_voice_permissions(mute_members=True)
@bot_has_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): 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 user_voice_state = user.voice
guild = ctx.guild guild = ctx.guild
author = ctx.author author = ctx.author
@ -920,13 +926,14 @@ class Mod(commands.Cog):
await ctx.send(_("No voice state for the target!")) await ctx.send(_("No voice state for the target!"))
return return
@checks.mod_or_permissions(administrator=True)
@mute.command(name="channel") @mute.command(name="channel")
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_roles=True)
@checks.mod_or_permissions(administrator=True)
async def channel_mute( async def channel_mute(
self, ctx: commands.Context, user: discord.Member, *, reason: str = None 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 author = ctx.message.author
channel = ctx.message.channel channel = ctx.message.channel
guild = ctx.guild guild = ctx.guild
@ -959,14 +966,14 @@ class Mod(commands.Cog):
else: else:
await channel.send(issue) await channel.send(issue)
@checks.mod_or_permissions(administrator=True)
@mute.command(name="server", aliases=["guild"]) @mute.command(name="server", aliases=["guild"])
@commands.guild_only() @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): async def guild_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Mutes user in the server""" """Mutes user in the server"""
author = ctx.message.author author = ctx.message.author
guild = ctx.guild guild = ctx.guild
user_voice_state = user.voice
if reason is None: if reason is None:
audit_reason = "server mute requested by {author} (ID {author.id})".format( audit_reason = "server mute requested by {author} (ID {author.id})".format(
author=author author=author
@ -1018,10 +1025,10 @@ class Mod(commands.Cog):
perms_cache = await self.settings.member(user).perms_cache() perms_cache = await self.settings.member(user).perms_cache()
if overwrites.send_messages is False or permissions.send_messages is False: 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): 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)] = { perms_cache[str(channel.id)] = {
"send_messages": overwrites.send_messages, "send_messages": overwrites.send_messages,
@ -1031,28 +1038,27 @@ class Mod(commands.Cog):
try: try:
await channel.set_permissions(user, overwrite=overwrites, reason=reason) await channel.set_permissions(user, overwrite=overwrites, reason=reason)
except discord.Forbidden: except discord.Forbidden:
return False, mute_unmute_issues["permissions_issue"] return False, T_(mute_unmute_issues["permissions_issue"])
else: else:
await self.settings.member(user).perms_cache.set(perms_cache) await self.settings.member(user).perms_cache.set(perms_cache)
return True, None return True, None
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_roles=True)
@checks.mod_or_permissions(manage_channel=True) @checks.mod_or_permissions(manage_channel=True)
async def unmute(self, ctx: commands.Context): async def unmute(self, ctx: commands.Context):
"""Unmutes user in the channel/server """Unmute users."""
Defaults to channel"""
pass pass
@unmute.command(name="voice") @unmute.command(name="voice")
@commands.guild_only() @commands.guild_only()
@mod_or_voice_permissions(mute_members=True) @mod_or_voice_permissions(mute_members=True)
@bot_has_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 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 user_voice_state = user.voice
if user_voice_state: if user_voice_state:
channel = user_voice_state.channel channel = user_voice_state.channel
@ -1093,11 +1099,12 @@ class Mod(commands.Cog):
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@unmute.command(name="channel") @unmute.command(name="channel")
@commands.bot_has_permissions(manage_roles=True)
@commands.guild_only() @commands.guild_only()
async def channel_unmute( async def unmute_channel(
self, ctx: commands.Context, user: discord.Member, *, reason: str = None 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 channel = ctx.channel
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
@ -1125,14 +1132,14 @@ class Mod(commands.Cog):
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@unmute.command(name="server", aliases=["guild"]) @unmute.command(name="server", aliases=["guild"])
@commands.bot_has_permissions(manage_roles=True)
@commands.guild_only() @commands.guild_only()
async def guild_unmute( async def unmute_guild(
self, ctx: commands.Context, user: discord.Member, *, reason: str = None 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 guild = ctx.guild
author = ctx.author author = ctx.author
channel = ctx.channel
unmute_success = [] unmute_success = []
for channel in guild.channels: for channel in guild.channels:
@ -1172,10 +1179,10 @@ class Mod(commands.Cog):
perms_cache = await self.settings.member(user).perms_cache() perms_cache = await self.settings.member(user).perms_cache()
if overwrites.send_messages or permissions.send_messages: 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): 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: if channel.id in perms_cache:
old_values = perms_cache[channel.id] old_values = perms_cache[channel.id]
@ -1190,9 +1197,11 @@ class Mod(commands.Cog):
if not is_empty: if not is_empty:
await channel.set_permissions(user, overwrite=overwrites) await channel.set_permissions(user, overwrite=overwrites)
else: else:
await channel.set_permissions(user, overwrite=None) await channel.set_permissions(
user, overwrite=cast(discord.PermissionOverwrite, None)
)
except discord.Forbidden: except discord.Forbidden:
return False, mute_unmute_issues["permissions_issue"] return False, T_(mute_unmute_issues["permissions_issue"])
else: else:
try: try:
del perms_cache[channel.id] del perms_cache[channel.id]
@ -1206,15 +1215,16 @@ class Mod(commands.Cog):
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(manage_channels=True) @checks.admin_or_permissions(manage_channels=True)
async def ignore(self, ctx: commands.Context): 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: if ctx.invoked_subcommand is None:
await ctx.send(await self.count_ignored()) await ctx.send(await self.count_ignored())
@ignore.command(name="channel") @ignore.command(name="channel")
async def ignore_channel(self, ctx: commands.Context, channel: discord.TextChannel = None): 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: if not channel:
channel = ctx.channel channel = ctx.channel
if not await self.settings.channel(channel).ignored(): if not await self.settings.channel(channel).ignored():
@ -1226,7 +1236,7 @@ class Mod(commands.Cog):
@ignore.command(name="server", aliases=["guild"]) @ignore.command(name="server", aliases=["guild"])
@checks.admin_or_permissions(manage_guild=True) @checks.admin_or_permissions(manage_guild=True)
async def ignore_guild(self, ctx: commands.Context): async def ignore_guild(self, ctx: commands.Context):
"""Ignores current server""" """Ignore commands in the current server."""
guild = ctx.guild guild = ctx.guild
if not await self.settings.guild(guild).ignored(): if not await self.settings.guild(guild).ignored():
await self.settings.guild(guild).ignored.set(True) await self.settings.guild(guild).ignored.set(True)
@ -1238,15 +1248,16 @@ class Mod(commands.Cog):
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(manage_channels=True) @checks.admin_or_permissions(manage_channels=True)
async def unignore(self, ctx: commands.Context): 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: if ctx.invoked_subcommand is None:
await ctx.send(await self.count_ignored()) await ctx.send(await self.count_ignored())
@unignore.command(name="channel") @unignore.command(name="channel")
async def unignore_channel(self, ctx: commands.Context, channel: discord.TextChannel = None): 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: if not channel:
channel = ctx.channel channel = ctx.channel
@ -1259,7 +1270,7 @@ class Mod(commands.Cog):
@unignore.command(name="server", aliases=["guild"]) @unignore.command(name="server", aliases=["guild"])
@checks.admin_or_permissions(manage_guild=True) @checks.admin_or_permissions(manage_guild=True)
async def unignore_guild(self, ctx: commands.Context): 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 guild = ctx.message.guild
if await self.settings.guild(guild).ignored(): if await self.settings.guild(guild).ignored():
await self.settings.guild(guild).ignored.set(False) 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. """Global check to see if a channel or server is ignored.
Any users who have permission to use the `ignore` or `unignore` commands 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) perms = ctx.channel.permissions_for(ctx.author)
surpass_ignore = ( surpass_ignore = (
isinstance(ctx.channel, discord.abc.PrivateChannel) isinstance(ctx.channel, discord.abc.PrivateChannel)
@ -1300,14 +1312,15 @@ class Mod(commands.Cog):
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(embed_links=True)
async def userinfo(self, ctx, *, user: discord.Member = None): 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 This includes fields for status, discord join date, server
join date, voice state and previous names/nicknames. join date, voice state and previous names/nicknames.
If the user has none of roles, previous names or previous If the user has no roles, previous names or previous nicknames,
nicknames, these fields will be omitted. these fields will be omitted.
""" """
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
@ -1383,14 +1396,11 @@ class Mod(commands.Cog):
else: else:
data.set_author(name=name) data.set_author(name=name)
try:
await ctx.send(embed=data) await ctx.send(embed=data)
except discord.HTTPException:
await ctx.send(_("I need the `Embed links` permission to send this."))
@commands.command() @commands.command()
async def names(self, ctx: commands.Context, user: discord.Member): 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) names, nicks = await self.get_names_and_nicks(user)
msg = "" msg = ""
if names: if names:
@ -1433,7 +1443,7 @@ class Mod(commands.Cog):
queue_entry = (guild.id, user.id) queue_entry = (guild.id, user.id)
self.unban_queue.append(queue_entry) self.unban_queue.append(queue_entry)
try: try:
await guild.unban(user, reason="Tempban finished") await guild.unban(user, reason=_("Tempban finished"))
guild_tempbans.remove(uid) guild_tempbans.remove(uid)
except discord.Forbidden: except discord.Forbidden:
self.unban_queue.remove(queue_entry) self.unban_queue.remove(queue_entry)
@ -1463,12 +1473,12 @@ class Mod(commands.Cog):
guild = message.guild guild = message.guild
author = message.author 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) mentions = set(message.mentions)
if len(mentions) >= max_mentions: if len(mentions) >= max_mentions:
try: try:
await guild.ban(author, reason="Mention spam (Autoban)") await guild.ban(author, reason=_("Mention spam (Autoban)"))
except discord.HTTPException: except discord.HTTPException:
log.info( log.info(
"Failed to ban member for mention spam in server {}.".format(guild.id) "Failed to ban member for mention spam in server {}.".format(guild.id)
@ -1482,7 +1492,7 @@ class Mod(commands.Cog):
"ban", "ban",
author, author,
guild.me, guild.me,
"Mention spam (Autoban)", _("Mention spam (Autoban)"),
until=None, until=None,
channel=None, channel=None,
) )
@ -1495,6 +1505,7 @@ class Mod(commands.Cog):
async def on_command_completion(self, ctx: commands.Context): async def on_command_completion(self, ctx: commands.Context):
await self._delete_delay(ctx) await self._delete_delay(ctx)
# noinspection PyUnusedLocal
async def on_command_error(self, ctx: commands.Context, error): async def on_command_error(self, ctx: commands.Context, error):
await self._delete_delay(ctx) await self._delete_delay(ctx)
@ -1511,11 +1522,9 @@ class Mod(commands.Cog):
return return
async def _delete_helper(m): async def _delete_helper(m):
try: with contextlib.suppress(discord.HTTPException):
await m.delete() await m.delete()
log.debug("Deleted command msg {}".format(m.id)) 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 asyncio.sleep(delay)
await _delete_helper(message) await _delete_helper(message)
@ -1537,7 +1546,7 @@ class Mod(commands.Cog):
return return
deleted = await self.check_duplicates(message) deleted = await self.check_duplicates(message)
if not deleted: 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): async def on_member_ban(self, guild: discord.Guild, member: discord.Member):
if (guild.id, member.id) in self.ban_queue: if (guild.id, member.id) in self.ban_queue:
@ -1577,7 +1586,8 @@ class Mod(commands.Cog):
except RuntimeError as e: except RuntimeError as e:
print(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 An event for modlog case creation
""" """
@ -1592,7 +1602,8 @@ class Mod(commands.Cog):
msg = await mod_channel.send(case_content) msg = await mod_channel.send(case_content)
await case.edit({"message": msg}) 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 Event for modlog case edits
""" """
@ -1605,7 +1616,10 @@ class Mod(commands.Cog):
else: else:
await case.message.edit(content=case_content) 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. """Get info about an audit log entry.
Parameters Parameters
@ -1625,14 +1639,15 @@ class Mod(commands.Cog):
if the audit log entry could not be found. if the audit log entry could not be found.
""" """
try: 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: except discord.HTTPException:
entry = None entry = None
if entry is None: if entry is None:
return None, None, None return None, None, None
return entry.user, entry.reason, entry.created_at 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. """Get an audit log entry.
Any exceptions encountered when looking through the audit log will be 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())] return [p for p in iter(overwrites)] == [p for p in iter(discord.PermissionOverwrite())]
_ = lambda s: s
mute_unmute_issues = { mute_unmute_issues = {
"already_muted": "That user can't send messages in this channel.", "already_muted": _("That user can't send messages in this channel."),
"already_unmuted": "That user isn't muted 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 " "hierarchy_problem": _(
"the user in the role hierarchy.", "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 " ),
"permissions_issue": _(
"Failed to mute user. I need the manage roles "
"permission and the user I'm muting must be " "permission and the user I'm muting must be "
"lower than myself in the role hierarchy.", "lower than myself in the role hierarchy."
),
} }

View File

@ -1,6 +1,6 @@
import datetime import datetime
import os import os
from typing import Union, List from typing import Union, List, Optional
import discord import discord
@ -296,12 +296,20 @@ async def transfer_credits(from_: discord.Member, to: discord.Member, amount: in
return await deposit_credits(to, amount) return await deposit_credits(to, amount)
async def wipe_bank(): async def wipe_bank(guild: Optional[discord.Guild] = None) -> None:
"""Delete all accounts from the bank.""" """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(): if await is_global():
await _conf.clear_all_users() await _conf.clear_all_users()
else: 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]: async def get_leaderboard(positions: int = None, guild: discord.Guild = None) -> List[tuple]:

View File

@ -838,7 +838,7 @@ class Config:
""" """
return self._get_base_group(self.ROLE, role.id) 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. """Returns a `Group` for the given user.
Parameters Parameters

View File

@ -1,5 +1,7 @@
import os
import re import re
from pathlib import Path from pathlib import Path
from typing import Callable, Union
from . import commands from . import commands
@ -113,9 +115,9 @@ def _normalize(string, remove_newline=False):
ends_with_space = s[-1] in " \n\t\r" ends_with_space = s[-1] in " \n\t\r"
if remove_newline: if remove_newline:
newline_re = re.compile("[\r\n]+") newline_re = re.compile("[\r\n]+")
s = " ".join(filter(bool, newline_re.split(s))) s = " ".join(filter(None, newline_re.split(s)))
s = " ".join(filter(bool, s.split("\t"))) s = " ".join(filter(None, s.split("\t")))
s = " ".join(filter(bool, s.split(" "))) s = " ".join(filter(None, s.split(" ")))
if starts_with_space: if starts_with_space:
s = " " + s s = " " + s
if ends_with_space: 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) return cog_folder / "locales" / "{}.{}".format(get_locale(), extension)
class Translator: class Translator(Callable[[str], str]):
"""Function to get translated strings at runtime.""" """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. Initializes an internationalization object.
@ -173,7 +175,7 @@ class Translator:
self.load_translations() self.load_translations()
def __call__(self, untranslated: str): def __call__(self, untranslated: str) -> str:
"""Translate the given string. """Translate the given string.
This will look for the string in the translator's :code:`.pot` file, This will look for the string in the translator's :code:`.pot` file,