Merge branch V3/feature/i18n_pass into V3/develop (#2024)

[i18n] Improves the coverage and quality of extracted user-facing string literals in the `redbot.core` package and makes them less ambiguous for the translator.
This commit is contained in:
Toby Harradine 2018-10-06 09:58:45 +10:00 committed by GitHub
commit 7b15ad5989
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1910 additions and 1622 deletions

View File

@ -1,44 +1,50 @@
import logging
from typing import Tuple
import discord
from redbot.core import Config, checks, commands
import logging
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box
from .announcer import Announcer
from .converters import MemberDefaultAuthor, SelfRole
log = logging.getLogger("red.admin")
GENERIC_FORBIDDEN = (
T_ = Translator("Admin", __file__)
_ = lambda s: s
GENERIC_FORBIDDEN = _(
"I attempted to do something that Discord denied me permissions for."
" Your command failed to successfully complete."
)
HIERARCHY_ISSUE = (
HIERARCHY_ISSUE = _(
"I tried to add {role.name} to {member.display_name} but that role"
" is higher than my highest role in the Discord hierarchy so I was"
" unable to successfully add it. Please give me a higher role and "
"try again."
)
USER_HIERARCHY_ISSUE = (
USER_HIERARCHY_ISSUE = _(
"I tried to add {role.name} to {member.display_name} but that role"
" is higher than your highest role in the Discord hierarchy so I was"
" unable to successfully add it. Please get a higher role and "
"try again."
)
RUNNING_ANNOUNCEMENT = (
RUNNING_ANNOUNCEMENT = _(
"I am already announcing something. If you would like to make a"
" different announcement please use `{prefix}announce cancel`"
" first."
)
_ = T_
@cog_i18n(_)
class Admin(commands.Cog):
"""A collection of server administration utilities."""
def __init__(self, config=Config):
super().__init__()
self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
@ -98,13 +104,14 @@ class Admin(commands.Cog):
await member.add_roles(role)
except discord.Forbidden:
if not self.pass_hierarchy_check(ctx, role):
await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member)
await self.complain(ctx, T_(HIERARCHY_ISSUE), role=role, member=member)
else:
await self.complain(ctx, GENERIC_FORBIDDEN)
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
else:
await ctx.send(
"I successfully added {role.name} to"
" {member.display_name}".format(role=role, member=member)
_("I successfully added {role.name} to {member.display_name}").format(
role=role, member=member
)
)
async def _removerole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
@ -112,13 +119,14 @@ class Admin(commands.Cog):
await member.remove_roles(role)
except discord.Forbidden:
if not self.pass_hierarchy_check(ctx, role):
await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member)
await self.complain(ctx, T_(HIERARCHY_ISSUE), role=role, member=member)
else:
await self.complain(ctx, GENERIC_FORBIDDEN)
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
else:
await ctx.send(
"I successfully removed {role.name} from"
" {member.display_name}".format(role=role, member=member)
_("I successfully removed {role.name} from {member.display_name}").format(
role=role, member=member
)
)
@commands.command()
@ -127,8 +135,8 @@ class Admin(commands.Cog):
async def addrole(
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
):
"""
Adds a role to a user.
"""Add a role to a user.
If user is left blank it defaults to the author of the command.
"""
if user is None:
@ -137,7 +145,7 @@ class Admin(commands.Cog):
# noinspection PyTypeChecker
await self._addrole(ctx, user, rolename)
else:
await self.complain(ctx, USER_HIERARCHY_ISSUE, member=ctx.author, role=rolename)
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE), member=ctx.author, role=rolename)
@commands.command()
@commands.guild_only()
@ -145,8 +153,8 @@ class Admin(commands.Cog):
async def removerole(
self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
):
"""
Removes a role from a user.
"""Remove a role from a user.
If user is left blank it defaults to the author of the command.
"""
if user is None:
@ -155,50 +163,54 @@ class Admin(commands.Cog):
# noinspection PyTypeChecker
await self._removerole(ctx, user, rolename)
else:
await self.complain(ctx, USER_HIERARCHY_ISSUE)
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
@commands.group()
@commands.guild_only()
@checks.admin_or_permissions(manage_roles=True)
async def editrole(self, ctx: commands.Context):
"""Edits roles settings"""
"""Edit role settings."""
pass
@editrole.command(name="colour", aliases=["color"])
async def editrole_colour(
self, ctx: commands.Context, role: discord.Role, value: discord.Colour
):
"""Edits a role's colour
"""Edit a role's colour.
Use double quotes if the role contains spaces.
Colour must be in hexadecimal format.
\"http://www.w3schools.com/colors/colors_picker.asp\"
[Online colour picker](http://www.w3schools.com/colors/colors_picker.asp)
Examples:
!editrole colour \"The Transistor\" #ff0000
!editrole colour Test #ff9900"""
`[p]editrole colour "The Transistor" #ff0000`
`[p]editrole colour Test #ff9900`
"""
author = ctx.author
reason = "{}({}) changed the colour of role '{}'".format(author.name, author.id, role.name)
if not self.pass_user_hierarchy_check(ctx, role):
await self.complain(ctx, USER_HIERARCHY_ISSUE)
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
return
try:
await role.edit(reason=reason, color=value)
except discord.Forbidden:
await self.complain(ctx, GENERIC_FORBIDDEN)
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
else:
log.info(reason)
await ctx.send("Done.")
await ctx.send(_("Done."))
@editrole.command(name="name")
@checks.admin_or_permissions(administrator=True)
async def edit_role_name(self, ctx: commands.Context, role: discord.Role, *, name: str):
"""Edits a role's name
"""Edit a role's name.
Use double quotes if the role or the name contain spaces.
Examples:
!editrole name \"The Transistor\" Test"""
`[p]editrole name \"The Transistor\" Test`
"""
author = ctx.message.author
old_name = role.name
reason = "{}({}) changed the name of role '{}' to '{}'".format(
@ -206,73 +218,74 @@ class Admin(commands.Cog):
)
if not self.pass_user_hierarchy_check(ctx, role):
await self.complain(ctx, USER_HIERARCHY_ISSUE)
await self.complain(ctx, T_(USER_HIERARCHY_ISSUE))
return
try:
await role.edit(reason=reason, name=name)
except discord.Forbidden:
await self.complain(ctx, GENERIC_FORBIDDEN)
await self.complain(ctx, T_(GENERIC_FORBIDDEN))
else:
log.info(reason)
await ctx.send("Done.")
await ctx.send(_("Done."))
@commands.group(invoke_without_command=True)
@checks.is_owner()
async def announce(self, ctx: commands.Context, *, message: str):
"""
Announces a message to all servers the bot is in.
"""
"""Announce a message to all servers the bot is in."""
if not self.is_announcing():
announcer = Announcer(ctx, message, config=self.conf)
announcer.start()
self.__current_announcer = announcer
await ctx.send("The announcement has begun.")
await ctx.send(_("The announcement has begun."))
else:
prefix = ctx.prefix
await self.complain(ctx, RUNNING_ANNOUNCEMENT, prefix=prefix)
await self.complain(ctx, T_(RUNNING_ANNOUNCEMENT), prefix=prefix)
@announce.command(name="cancel")
@checks.is_owner()
async def announce_cancel(self, ctx):
"""
Cancels a running announce.
"""
"""Cancel a running announce."""
try:
self.__current_announcer.cancel()
except AttributeError:
pass
await ctx.send("The current announcement has been cancelled.")
await ctx.send(_("The current announcement has been cancelled."))
@announce.command(name="channel")
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def announce_channel(self, ctx, *, channel: discord.TextChannel = None):
"""
Changes the channel on which the bot makes announcements.
"""
"""Change the channel to which the bot makes announcements."""
if channel is None:
channel = ctx.channel
await self.conf.guild(ctx.guild).announce_channel.set(channel.id)
await ctx.send("The announcement channel has been set to {}".format(channel.mention))
await ctx.send(
_("The announcement channel has been set to {channel.mention}").format(channel=channel)
)
@announce.command(name="ignore")
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def announce_ignore(self, ctx):
"""
Toggles whether the announcements will ignore the current server.
"""
"""Toggle announcements being enabled this server."""
ignored = await self.conf.guild(ctx.guild).announce_ignore()
await self.conf.guild(ctx.guild).announce_ignore.set(not ignored)
verb = "will" if ignored else "will not"
await ctx.send(f"The server {ctx.guild.name} {verb} receive announcements.")
if ignored: # Keeping original logic....
await ctx.send(
_("The server {guild.name} will receive announcements.").format(guild=ctx.guild)
)
else:
await ctx.send(
_("The server {guild.name} will not receive announcements.").format(
guild=ctx.guild
)
)
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
"""
@ -295,8 +308,9 @@ class Admin(commands.Cog):
@commands.guild_only()
@commands.group(invoke_without_command=True)
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
"""
Add a role to yourself that server admins have configured as user settable.
"""Add a role to yourself.
Server admins must have configured the role as user settable.
NOTE: The role is case sensitive!
"""
@ -305,8 +319,7 @@ class Admin(commands.Cog):
@selfrole.command(name="remove")
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
"""
Removes a selfrole from yourself.
"""Remove a selfrole from yourself.
NOTE: The role is case sensitive!
"""
@ -316,8 +329,7 @@ class Admin(commands.Cog):
@selfrole.command(name="add")
@checks.admin_or_permissions(manage_roles=True)
async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role):
"""
Add a role to the list of available selfroles.
"""Add a role to the list of available selfroles.
NOTE: The role is case sensitive!
"""
@ -325,20 +337,19 @@ class Admin(commands.Cog):
if role.id not in curr_selfroles:
curr_selfroles.append(role.id)
await ctx.send("The selfroles list has been successfully modified.")
await ctx.send(_("The selfroles list has been successfully modified."))
@selfrole.command(name="delete")
@checks.admin_or_permissions(manage_roles=True)
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
"""
Removes a role from the list of available selfroles.
"""Remove a role from the list of available selfroles.
NOTE: The role is case sensitive!
"""
async with self.conf.guild(ctx.guild).selfroles() as curr_selfroles:
curr_selfroles.remove(role.id)
await ctx.send("The selfroles list has been successfully modified.")
await ctx.send(_("The selfroles list has been successfully modified."))
@selfrole.command(name="list")
async def selfrole_list(self, ctx: commands.Context):
@ -348,7 +359,7 @@ class Admin(commands.Cog):
selfroles = await self._valid_selfroles(ctx.guild)
fmt_selfroles = "\n".join(["+ " + r.name for r in selfroles])
msg = "Available Selfroles:\n{}".format(fmt_selfroles)
msg = _("Available Selfroles: {selfroles}").format(selfroles=fmt_selfroles)
await ctx.send(box(msg, "diff"))
async def _serverlock_check(self, guild: discord.Guild) -> bool:
@ -365,15 +376,14 @@ class Admin(commands.Cog):
@commands.command()
@checks.is_owner()
async def serverlock(self, ctx: commands.Context):
"""
Locks a bot to its current servers only.
"""
"""Lock a bot to its current servers only."""
serverlocked = await self.conf.serverlocked()
await self.conf.serverlocked.set(not serverlocked)
verb = "is now" if not serverlocked else "is no longer"
await ctx.send("The bot {} serverlocked.".format(verb))
if serverlocked:
await ctx.send(_("The bot is no longer serverlocked."))
else:
await ctx.send(_("The bot is now serverlocked."))
# region Event Handlers
async def on_guild_join(self, guild: discord.Guild):

View File

@ -2,6 +2,9 @@ import asyncio
import discord
from redbot.core import commands
from redbot.core.i18n import Translator
_ = Translator("Announcer", __file__)
class Announcer:
@ -63,7 +66,9 @@ class Announcer:
try:
await channel.send(self.message)
except discord.Forbidden:
await bot_owner.send("I could not announce to server: {}".format(g.id))
await bot_owner.send(
_("I could not announce to server: {server.id}").format(server=g)
)
await asyncio.sleep(0.5)
self.active = False

View File

@ -1,5 +1,8 @@
import discord
from redbot.core import commands
from redbot.core.i18n import Translator
_ = Translator("AdminConverters", __file__)
class MemberDefaultAuthor(commands.Converter):
@ -19,7 +22,7 @@ class SelfRole(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> discord.Role:
admin = ctx.command.instance
if admin is None:
raise commands.BadArgument("Admin is not loaded.")
raise commands.BadArgument(_("The Admin cog is not loaded."))
conf = admin.conf
selfroles = await conf.guild(ctx.guild).selfroles()
@ -28,5 +31,5 @@ class SelfRole(commands.Converter):
role = await role_converter.convert(ctx, arg)
if role.id not in selfroles:
raise commands.BadArgument("The provided role is not a valid selfrole.")
raise commands.BadArgument(_("The provided role is not a valid selfrole."))
return role

View File

@ -15,15 +15,14 @@ _ = Translator("Alias", __file__)
@cog_i18n(_)
class Alias(commands.Cog):
"""
Alias
Aliases are per server shortcuts for commands. They
can act as both a lambda (storing arguments for repeated use)
or as simply a shortcut to saying "x y z".
"""Create aliases for commands.
Aliases are alternative names shortcuts for commands. They
can act as both a lambda (storing arguments for repeated use)
or as simply a shortcut to saying "x y z".
When run, aliases will accept any additional arguments
and append them to the stored alias
and append them to the stored alias.
"""
default_global_settings = {"entries": []}
@ -177,32 +176,28 @@ class Alias(commands.Cog):
@commands.group()
@commands.guild_only()
async def alias(self, ctx: commands.Context):
"""Manage per-server aliases for commands"""
"""Manage command aliases."""
pass
@alias.group(name="global")
async def global_(self, ctx: commands.Context):
"""
Manage global aliases.
"""
"""Manage global aliases."""
pass
@checks.mod_or_permissions(manage_guild=True)
@alias.command(name="add")
@commands.guild_only()
async def _add_alias(self, ctx: commands.Context, alias_name: str, *, command):
"""
Add an alias for a command.
"""
"""Add an alias for a command."""
# region Alias Add Validity Checking
is_command = self.is_command(alias_name)
if is_command:
await ctx.send(
_(
"You attempted to create a new alias"
" with the name {} but that"
" with the name {name} but that"
" name is already a command on this bot."
).format(alias_name)
).format(name=alias_name)
)
return
@ -211,9 +206,9 @@ class Alias(commands.Cog):
await ctx.send(
_(
"You attempted to create a new alias"
" with the name {} but that"
" with the name {name} but that"
" alias already exists on this server."
).format(alias_name)
).format(name=alias_name)
)
return
@ -222,10 +217,10 @@ class Alias(commands.Cog):
await ctx.send(
_(
"You attempted to create a new alias"
" with the name {} but that"
" with the name {name} but that"
" name is an invalid alias name. Alias"
" names may not contain spaces."
).format(alias_name)
).format(name=alias_name)
)
return
# endregion
@ -235,23 +230,23 @@ class Alias(commands.Cog):
await self.add_alias(ctx, alias_name, command)
await ctx.send(_("A new alias with the trigger `{}` has been created.").format(alias_name))
await ctx.send(
_("A new alias with the trigger `{name}` has been created.").format(name=alias_name)
)
@checks.is_owner()
@global_.command(name="add")
async def _add_global_alias(self, ctx: commands.Context, alias_name: str, *, command):
"""
Add a global alias for a command.
"""
"""Add a global alias for a command."""
# region Alias Add Validity Checking
is_command = self.is_command(alias_name)
if is_command:
await ctx.send(
_(
"You attempted to create a new global alias"
" with the name {} but that"
" with the name {name} but that"
" name is already a command on this bot."
).format(alias_name)
).format(name=alias_name)
)
return
@ -260,9 +255,9 @@ class Alias(commands.Cog):
await ctx.send(
_(
"You attempted to create a new global alias"
" with the name {} but that"
" with the name {name} but that"
" alias already exists on this server."
).format(alias_name)
).format(name=alias_name)
)
return
@ -271,10 +266,10 @@ class Alias(commands.Cog):
await ctx.send(
_(
"You attempted to create a new global alias"
" with the name {} but that"
" with the name {name} but that"
" name is an invalid alias name. Alias"
" names may not contain spaces."
).format(alias_name)
).format(name=alias_name)
)
return
# endregion
@ -282,63 +277,65 @@ class Alias(commands.Cog):
await self.add_alias(ctx, alias_name, command, global_=True)
await ctx.send(
_("A new global alias with the trigger `{}` has been created.").format(alias_name)
_("A new global alias with the trigger `{name}` has been created.").format(
name=alias_name
)
)
@alias.command(name="help")
@commands.guild_only()
async def _help_alias(self, ctx: commands.Context, alias_name: str):
"""Tries to execute help for the base command of the alias"""
"""Try to execute help for the base command of the alias."""
is_alias, alias = await self.is_alias(ctx.guild, alias_name=alias_name)
if is_alias:
base_cmd = alias.command[0]
new_msg = copy(ctx.message)
new_msg.content = "{}help {}".format(ctx.prefix, base_cmd)
new_msg.content = _("{prefix}help {command}").format(
prefix=ctx.prefix, command=base_cmd
)
await self.bot.process_commands(new_msg)
else:
ctx.send(_("No such alias exists."))
await ctx.send(_("No such alias exists."))
@alias.command(name="show")
@commands.guild_only()
async def _show_alias(self, ctx: commands.Context, alias_name: str):
"""Shows what command the alias executes."""
"""Show what command the alias executes."""
is_alias, alias = await self.is_alias(ctx.guild, alias_name)
if is_alias:
await ctx.send(
_("The `{}` alias will execute the command `{}`").format(alias_name, alias.command)
_("The `{alias_name}` alias will execute the command `{command}`").format(
alias_name=alias_name, command=alias.command
)
)
else:
await ctx.send(_("There is no alias with the name `{}`").format(alias_name))
await ctx.send(_("There is no alias with the name `{name}`").format(name=alias_name))
@checks.mod_or_permissions(manage_guild=True)
@alias.command(name="del")
@commands.guild_only()
async def _del_alias(self, ctx: commands.Context, alias_name: str):
"""
Deletes an existing alias on this server.
"""
"""Delete an existing alias on this server."""
aliases = await self.unloaded_aliases(ctx.guild)
try:
next(aliases)
except StopIteration:
await ctx.send(_("There are no aliases on this guild."))
await ctx.send(_("There are no aliases on this server."))
return
if await self.delete_alias(ctx, alias_name):
await ctx.send(
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
_("Alias with the name `{name}` was successfully deleted.").format(name=alias_name)
)
else:
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
await ctx.send(_("Alias with name `{name}` was not found.").format(name=alias_name))
@checks.is_owner()
@global_.command(name="del")
async def _del_global_alias(self, ctx: commands.Context, alias_name: str):
"""
Deletes an existing global alias.
"""
"""Delete an existing global alias."""
aliases = await self.unloaded_global_aliases()
try:
next(aliases)
@ -348,17 +345,15 @@ class Alias(commands.Cog):
if await self.delete_alias(ctx, alias_name, global_=True):
await ctx.send(
_("Alias with the name `{}` was successfully deleted.").format(alias_name)
_("Alias with the name `{name}` was successfully deleted.").format(name=alias_name)
)
else:
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
await ctx.send(_("Alias with name `{name}` was not found.").format(name=alias_name))
@alias.command(name="list")
@commands.guild_only()
async def _list_alias(self, ctx: commands.Context):
"""
Lists the available aliases on this server.
"""
"""List the available aliases on this server."""
names = [_("Aliases:")] + sorted(
["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))]
)
@ -369,9 +364,7 @@ class Alias(commands.Cog):
@global_.command(name="list")
async def _list_global_alias(self, ctx: commands.Context):
"""
Lists the available global aliases on this bot.
"""
"""List the available global aliases on this bot."""
names = [_("Aliases:")] + sorted(
["+ " + a.name for a in await self.unloaded_global_aliases()]
)

File diff suppressed because it is too large Load Diff

View File

@ -67,7 +67,7 @@ class Bank(commands.Cog):
@checks.guildowner_or_permissions(administrator=True)
@commands.group(autohelp=True)
async def bankset(self, ctx: commands.Context):
"""Base command for bank settings"""
"""Base command for bank settings."""
if ctx.invoked_subcommand is None:
if await bank.is_global():
bank_name = await bank._conf.bank_name()
@ -81,42 +81,47 @@ class Bank(commands.Cog):
default_balance = await bank._conf.guild(ctx.guild).default_balance()
settings = _(
"Bank settings:\n\nBank name: {}\nCurrency: {}\nDefault balance: {}"
).format(bank_name, currency_name, default_balance)
"Bank settings:\n\nBank name: {bank_name}\nCurrency: {currency_name}\n"
"Default balance: {default_balance}"
).format(
bank_name=bank_name, currency_name=currency_name, default_balance=default_balance
)
await ctx.send(box(settings))
@bankset.command(name="toggleglobal")
@checks.is_owner()
async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool = False):
"""Toggles whether the bank is global or not
If the bank is global, it will become per-server
If the bank is per-server, it will become global"""
"""Toggle whether the bank is global or not.
If the bank is global, it will become per-server.
If the bank is per-server, it will become global.
"""
cur_setting = await bank.is_global()
word = _("per-server") if cur_setting else _("global")
if confirm is False:
await ctx.send(
_(
"This will toggle the bank to be {}, deleting all accounts "
"in the process! If you're sure, type `{}`"
).format(word, "{}bankset toggleglobal yes".format(ctx.prefix))
"This will toggle the bank to be {banktype}, deleting all accounts "
"in the process! If you're sure, type `{command}`"
).format(banktype=word, command="{}bankset toggleglobal yes".format(ctx.prefix))
)
else:
await bank.set_global(not cur_setting)
await ctx.send(_("The bank is now {}.").format(word))
await ctx.send(_("The bank is now {banktype}.").format(banktype=word))
@bankset.command(name="bankname")
@check_global_setting_guildowner()
async def bankset_bankname(self, ctx: commands.Context, *, name: str):
"""Set the bank's name"""
"""Set the bank's name."""
await bank.set_bank_name(name, ctx.guild)
await ctx.send(_("Bank's name has been set to {}").format(name))
await ctx.send(_("Bank name has been set to: {name}").format(name=name))
@bankset.command(name="creditsname")
@check_global_setting_guildowner()
async def bankset_creditsname(self, ctx: commands.Context, *, name: str):
"""Set the name for the bank's currency"""
"""Set the name for the bank's currency."""
await bank.set_currency_name(name, ctx.guild)
await ctx.send(_("Currency name has been set to {}").format(name))
await ctx.send(_("Currency name has been set to: {name}").format(name=name))
# ENDSECTION

View File

@ -16,7 +16,7 @@ _ = Translator("Cleanup", __file__)
@cog_i18n(_)
class Cleanup(commands.Cog):
"""Commands for cleaning messages"""
"""Commands for cleaning up messages."""
def __init__(self, bot: Red):
super().__init__()
@ -33,7 +33,7 @@ class Cleanup(commands.Cog):
"""
prompt = await ctx.send(
_("Are you sure you want to delete {} messages? (y/n)").format(number)
_("Are you sure you want to delete {number} messages? (y/n)").format(number=number)
)
response = await ctx.bot.wait_for("message", check=MessagePredicate.same_context(ctx))
@ -41,7 +41,7 @@ class Cleanup(commands.Cog):
await prompt.delete()
try:
await response.delete()
except:
except discord.HTTPException:
pass
return True
else:
@ -104,25 +104,24 @@ class Cleanup(commands.Cog):
@commands.group()
@checks.mod_or_permissions(manage_messages=True)
async def cleanup(self, ctx: commands.Context):
"""Deletes messages."""
"""Delete messages."""
pass
@cleanup.command()
@commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def text(
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
):
"""Deletes last X messages matching the specified text.
"""Delete the last X messages matching the specified text.
Example:
cleanup text \"test\" 5
`[p]cleanup text "test" 5`
Remember to use double quotes."""
Remember to use double quotes.
"""
channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).manage_messages:
await ctx.send("I need the Manage Messages permission to do this.")
return
author = ctx.author
@ -156,18 +155,17 @@ class Cleanup(commands.Cog):
@cleanup.command()
@commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def user(
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
):
"""Deletes last X messages from specified user.
"""Delete the last X messages from a specified user.
Examples:
cleanup user @\u200bTwentysix 2
cleanup user Red 6"""
`[p]cleanup user @\u200bTwentysix 2`
`[p]cleanup user Red 6`
"""
channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).manage_messages:
await ctx.send("I need the Manage Messages permission to do this.")
return
member = None
try:
@ -213,8 +211,9 @@ class Cleanup(commands.Cog):
@cleanup.command()
@commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
"""Deletes all messages after specified message.
"""Delete all messages after a specified message.
To get a message id, enable developer mode in Discord's
settings, 'appearance' tab. Then right click a message
@ -222,9 +221,6 @@ class Cleanup(commands.Cog):
"""
channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).manage_messages:
await ctx.send("I need the Manage Messages permission to do this.")
return
author = ctx.author
try:
@ -245,6 +241,7 @@ class Cleanup(commands.Cog):
@cleanup.command()
@commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def before(
self, ctx: commands.Context, message_id: int, number: int, delete_pinned: bool = False
):
@ -256,9 +253,6 @@ class Cleanup(commands.Cog):
"""
channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).manage_messages:
await ctx.send("I need the Manage Messages permission to do this.")
return
author = ctx.author
try:
@ -279,16 +273,15 @@ class Cleanup(commands.Cog):
@cleanup.command()
@commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
"""Deletes last X messages.
"""Delete the last X messages.
Example:
cleanup messages 26"""
`[p]cleanup messages 26`
"""
channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).manage_messages:
await ctx.send("I need the Manage Messages permission to do this.")
return
author = ctx.author
if number > 100:
@ -310,13 +303,11 @@ class Cleanup(commands.Cog):
@cleanup.command(name="bot")
@commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
"""Cleans up command messages and messages from the bot."""
"""Clean up command messages and messages from the bot."""
channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).manage_messages:
await ctx.send("I need the Manage Messages permission to do this.")
return
author = ctx.message.author
if number > 100:
@ -369,7 +360,7 @@ class Cleanup(commands.Cog):
match_pattern: str = None,
delete_pinned: bool = False,
):
"""Cleans up messages owned by the bot.
"""Clean up messages owned by the bot.
By default, all messages are cleaned. If a third argument is specified,
it is used for pattern matching: If it begins with r( and ends with ),

View File

@ -1,10 +1,9 @@
import os
import re
import random
from datetime import datetime, timedelta
from inspect import Parameter
from collections import OrderedDict
from typing import Mapping
from typing import Mapping, Tuple, Dict
import discord
@ -52,11 +51,11 @@ class CommandObj:
async def get_responses(self, ctx):
intro = _(
"Welcome to the interactive random {} maker!\n"
"Welcome to the interactive random {cc} maker!\n"
"Every message you send will be added as one of the random "
"responses to choose from once this {} is "
"triggered. To exit this interactive menu, type `{}`"
).format("customcommand", "customcommand", "exit()")
"responses to choose from once this {cc} is "
"triggered. To exit this interactive menu, type `{quit}`"
).format(cc="customcommand", quit="exit()")
await ctx.send(intro)
responses = []
@ -85,7 +84,7 @@ class CommandObj:
# in the ccinfo dict
return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow())
async def get(self, message: discord.Message, command: str) -> str:
async def get(self, message: discord.Message, command: str) -> Tuple[str, Dict]:
ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
if not ccinfo:
raise NotFound()
@ -180,9 +179,7 @@ class CommandObj:
@cog_i18n(_)
class CustomCommands(commands.Cog):
"""Custom commands
Creates commands used to display text"""
"""Creates commands used to display text."""
def __init__(self, bot):
super().__init__()
@ -196,61 +193,55 @@ class CustomCommands(commands.Cog):
@commands.group(aliases=["cc"])
@commands.guild_only()
async def customcom(self, ctx: commands.Context):
"""Custom commands management"""
"""Custom commands management."""
pass
@customcom.group(name="add")
@customcom.group(name="create", aliases=["add"])
@checks.mod_or_permissions(administrator=True)
async def cc_add(self, ctx: commands.Context):
"""
Adds a new custom command
async def cc_create(self, ctx: commands.Context):
"""Create custom commands.
CCs can be enhanced with arguments:
https://red-discordbot.readthedocs.io/en/v3-develop/cog_customcom.html
CCs can be enhanced with arguments, see the guide
[here](https://red-discordbot.readthedocs.io/en/v3-develop/cog_customcom.html).
"""
pass
@cc_add.command(name="random")
@cc_create.command(name="random")
@checks.mod_or_permissions(administrator=True)
async def cc_add_random(self, ctx: commands.Context, command: str.lower):
"""
Create a CC where it will randomly choose a response!
async def cc_create_random(self, ctx: commands.Context, command: str.lower):
"""Create a CC where it will randomly choose a response!
Note: This is interactive
Note: This command is interactive.
"""
responses = []
responses = await self.commandobj.get_responses(ctx=ctx)
try:
await self.commandobj.create(ctx=ctx, command=command, response=responses)
await ctx.send(_("Custom command successfully added."))
except AlreadyExists:
await ctx.send(
_("This command already exists. Use `{}` to edit it.").format(
"{}customcom edit".format(ctx.prefix)
_("This command already exists. Use `{command}` to edit it.").format(
command="{}customcom edit".format(ctx.prefix)
)
)
# await ctx.send(str(responses))
@cc_add.command(name="simple")
@cc_create.command(name="simple")
@checks.mod_or_permissions(administrator=True)
async def cc_add_simple(self, ctx, command: str.lower, *, text: str):
"""Adds a simple custom command
async def cc_create_simple(self, ctx, command: str.lower, *, text: str):
"""Add a simple custom command.
Example:
[p]customcom add simple yourcommand Text you want
- `[p]customcom create simple yourcommand Text you want`
"""
if command in self.bot.all_commands:
await ctx.send(_("That command is already a standard command."))
await ctx.send(_("There already exists a bot command with the same name."))
return
try:
await self.commandobj.create(ctx=ctx, command=command, response=text)
await ctx.send(_("Custom command successfully added."))
except AlreadyExists:
await ctx.send(
_("This command already exists. Use `{}` to edit it.").format(
"{}customcom edit".format(ctx.prefix)
_("This command already exists. Use `{command}` to edit it.").format(
command="{}customcom edit".format(ctx.prefix)
)
)
except ArgParseError as e:
@ -261,13 +252,14 @@ class CustomCommands(commands.Cog):
async def cc_cooldown(
self, ctx, command: str.lower, cooldown: int = None, *, per: str.lower = "member"
):
"""
Sets, edits, or views cooldowns for a custom command
"""Set, edit, or view the cooldown for a custom command.
You may set cooldowns per member, channel, or guild. Multiple
cooldowns may be set. All cooldowns must be cooled to call the
custom command.
You may set cooldowns per member, channel, or guild.
Multiple cooldowns may be set. All cooldowns must be cooled to call the custom command.
Example:
[p]customcom cooldown yourcommand 30
- `[p]customcom cooldown yourcommand 30`
"""
if cooldown is None:
try:
@ -293,18 +285,19 @@ class CustomCommands(commands.Cog):
await ctx.send(_("Custom command cooldown successfully edited."))
except NotFound:
await ctx.send(
_("That command doesn't exist. Use `{}` to add it.").format(
"{}customcom add".format(ctx.prefix)
_("That command doesn't exist. Use `{command}` to add it.").format(
command="{}customcom create".format(ctx.prefix)
)
)
@customcom.command(name="delete")
@checks.mod_or_permissions(administrator=True)
async def cc_delete(self, ctx, command: str.lower):
"""Deletes a custom command
"""Delete a custom command
.
Example:
[p]customcom delete yourcommand"""
- `[p]customcom delete yourcommand`
"""
try:
await self.commandobj.delete(ctx=ctx, command=command)
await ctx.send(_("Custom command successfully deleted."))
@ -314,18 +307,20 @@ class CustomCommands(commands.Cog):
@customcom.command(name="edit")
@checks.mod_or_permissions(administrator=True)
async def cc_edit(self, ctx, command: str.lower, *, text: str = None):
"""Edits a custom command's response
"""Edit a custom command.
Example:
[p]customcom edit yourcommand Text you want
- `[p]customcom edit yourcommand Text you want`
"""
command = command.lower()
try:
await self.commandobj.edit(ctx=ctx, command=command, response=text)
await ctx.send(_("Custom command successfully edited."))
except NotFound:
await ctx.send(
_("That command doesn't exist. Use `{}` to add it.").format(
"{}customcom add".format(ctx.prefix)
_("That command doesn't exist. Use `{command}` to add it.").format(
command="{}customcom create".format(ctx.prefix)
)
)
except ArgParseError as e:
@ -333,7 +328,7 @@ class CustomCommands(commands.Cog):
@customcom.command(name="list")
async def cc_list(self, ctx):
"""Shows custom commands list"""
"""List all available custom commands."""
response = await CommandObj.get_commands(self.config.guild(ctx.guild))
@ -341,8 +336,8 @@ class CustomCommands(commands.Cog):
await ctx.send(
_(
"There are no custom commands in this server."
" Use `{}` to start adding some."
).format("{}customcom add".format(ctx.prefix))
" Use `{command}` to start adding some."
).format(command="{}customcom create".format(ctx.prefix))
)
return
@ -454,9 +449,8 @@ class CustomCommands(commands.Cog):
gaps = set(indices).symmetric_difference(range(high + 1))
if gaps:
raise ArgParseError(
_("Arguments must be sequential. Missing arguments: {}.").format(
", ".join(str(i + low) for i in gaps)
)
_("Arguments must be sequential. Missing arguments: ")
+ ", ".join(str(i + low) for i in gaps)
)
fin = [Parameter("_" + str(i), Parameter.POSITIONAL_OR_KEYWORD) for i in range(high + 1)]
for arg in args:
@ -481,8 +475,12 @@ class CustomCommands(commands.Cog):
and anno != fin[index].annotation
):
raise ArgParseError(
_('Conflicting colon notation for argument {}: "{}" and "{}".').format(
index + low, fin[index].annotation.__name__, anno.__name__
_(
'Conflicting colon notation for argument {index}: "{name1}" and "{name2}".'
).format(
index=index + low,
name1=fin[index].annotation.__name__,
name2=anno.__name__,
)
)
if anno is not Parameter.empty:
@ -511,6 +509,8 @@ class CustomCommands(commands.Cog):
key = (command, ctx.guild, ctx.channel)
elif per == "member":
key = (command, ctx.guild, ctx.author)
else:
raise ValueError(per)
cooldown = self.cooldowns.get(key)
if cooldown:
cooldown += timedelta(seconds=rate)

View File

@ -13,9 +13,7 @@ _ = Translator("DataConverter", __file__)
@cog_i18n(_)
class DataConverter(commands.Cog):
"""
Cog for importing Red v2 Data
"""
"""Import Red V2 data to your V3 instance."""
def __init__(self, bot: Red):
super().__init__()
@ -24,13 +22,10 @@ class DataConverter(commands.Cog):
@checks.is_owner()
@commands.command(name="convertdata")
async def dataconversioncommand(self, ctx: commands.Context, v2path: str):
"""
Interactive prompt for importing data from Red v2
"""Interactive prompt for importing data from Red V2.
Takes the path where the v2 install is
Overwrites values which have entries in both v2 and v3,
use with caution.
Takes the path where the V2 install is, and overwrites
values which have entries in both V2 and v3; use with caution.
"""
resolver = SpecResolver(Path(v2path.strip()))
@ -54,7 +49,7 @@ class DataConverter(commands.Cog):
"message", check=MessagePredicate.same_context(ctx), timeout=60
)
except asyncio.TimeoutError:
return await ctx.send(_("Try this again when you are more ready"))
return await ctx.send(_("Try this again when you are ready."))
else:
if message.content.strip().lower() in ["quit", "exit", "-1", "q", "cancel"]:
return await ctx.tick()
@ -72,7 +67,7 @@ class DataConverter(commands.Cog):
else:
return await ctx.send(
_(
"There isn't anything else I know how to convert here."
"\nThere might be more things I can convert in the future."
"There isn't anything else I know how to convert here.\n"
"There might be more things I can convert in the future."
)
)

View File

@ -1,11 +1,15 @@
import asyncio
from redbot.core import commands
from redbot.core.i18n import Translator
from redbot.core.utils.predicates import MessagePredicate
__all__ = ["do_install_agreement"]
REPO_INSTALL_MSG = (
T_ = Translator("DownloaderChecks", __file__)
_ = lambda s: s
REPO_INSTALL_MSG = _(
"You're about to add a 3rd party repository. The creator of Red"
" and its community have no responsibility for any potential "
"damage that the content of 3rd party repositories might cause."
@ -14,6 +18,7 @@ REPO_INSTALL_MSG = (
"shown again until the next reboot.\n\nYou have **30** seconds"
" to reply to this message."
)
_ = T_
async def do_install_agreement(ctx: commands.Context):
@ -21,14 +26,14 @@ async def do_install_agreement(ctx: commands.Context):
if downloader is None or downloader.already_agreed:
return True
await ctx.send(REPO_INSTALL_MSG)
await ctx.send(T_(REPO_INSTALL_MSG))
try:
await ctx.bot.wait_for(
"message", check=MessagePredicate.lower_equal_to("i agree", ctx), timeout=30
)
except asyncio.TimeoutError:
await ctx.send("Your response has timed out, please try again.")
await ctx.send(_("Your response has timed out, please try again."))
return False
downloader.already_agreed = True

View File

@ -8,10 +8,10 @@ class InstalledCog(Installable):
async def convert(cls, ctx: commands.Context, arg: str) -> Installable:
downloader = ctx.bot.get_cog("Downloader")
if downloader is None:
raise commands.CommandError("Downloader not loaded.")
raise commands.CommandError(_("No Downloader cog found."))
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
if cog is None:
raise commands.BadArgument("That cog is not installed")
raise commands.BadArgument(_("That cog is not installed"))
return cog

View File

@ -193,9 +193,7 @@ class Downloader(commands.Cog):
@commands.command()
@checks.is_owner()
async def pipinstall(self, ctx, *deps: str):
"""
Installs a group of dependencies using pip.
"""
"""Install a group of dependencies using pip."""
repo = Repo("", "", "", Path.cwd(), loop=ctx.bot.loop)
success = await repo.install_raw_requirements(deps, self.LIB_PATH)
@ -212,18 +210,15 @@ class Downloader(commands.Cog):
@commands.group()
@checks.is_owner()
async def repo(self, ctx):
"""
Command group for managing Downloader repos.
"""
"""Repo management commands."""
pass
@repo.command(name="add")
async def _repo_add(self, ctx, name: str, repo_url: str, branch: str = None):
"""
Add a new repo to Downloader.
"""Add a new repo.
Name can only contain characters A-z, numbers and underscore
Branch will default to master if not specified
The name can only contain characters A-z, numbers and underscores.
The branch will be the default branch if not specified.
"""
agreed = await do_install_agreement(ctx)
if not agreed:
@ -242,24 +237,22 @@ class Downloader(commands.Cog):
exc_info=err,
)
else:
await ctx.send(_("Repo `{}` successfully added.").format(name))
await ctx.send(_("Repo `{name}` successfully added.").format(name=name))
if repo.install_msg is not None:
await ctx.send(repo.install_msg.replace("[p]", ctx.prefix))
@repo.command(name="delete")
async def _repo_del(self, ctx, repo_name: Repo):
"""
Removes a repo from Downloader and its' files.
"""
await self._repo_manager.delete_repo(repo_name.name)
@repo.command(name="delete", aliases=["remove"], usage="<repo_name>")
async def _repo_del(self, ctx, repo: Repo):
"""Remove a repo and its files."""
await self._repo_manager.delete_repo(repo.name)
await ctx.send(_("The repo `{}` has been deleted successfully.").format(repo_name.name))
await ctx.send(
_("The repo `{repo.name}` has been deleted successfully.").format(repo=repo)
)
@repo.command(name="list")
async def _repo_list(self, ctx):
"""
Lists all installed repos.
"""
"""List all installed repos."""
repos = self._repo_manager.get_all_repo_names()
repos = sorted(repos, key=str.lower)
joined = _("Installed Repos:\n\n")
@ -270,94 +263,93 @@ class Downloader(commands.Cog):
for page in pagify(joined, ["\n"], shorten_by=16):
await ctx.send(box(page.lstrip(" "), lang="diff"))
@repo.command(name="info")
async def _repo_info(self, ctx, repo_name: Repo):
"""
Lists information about a single repo
"""
if repo_name is None:
await ctx.send(_("There is no repo `{}`").format(repo_name.name))
@repo.command(name="info", usage="<repo_name>")
async def _repo_info(self, ctx, repo: Repo):
"""Show information about a repo."""
if repo is None:
await ctx.send(_("Repo `{repo.name}` not found.").format(repo=repo))
return
msg = _("Information on {}:\n{}").format(repo_name.name, repo_name.description or "")
msg = _("Information on {repo.name}:\n{description}").format(
repo=repo, description=repo.description or ""
)
await ctx.send(box(msg))
@commands.group()
@checks.is_owner()
async def cog(self, ctx):
"""
Command group for managing installable Cogs.
"""
"""Cog installation management commands."""
pass
@cog.command(name="install")
async def _cog_install(self, ctx, repo_name: Repo, cog_name: str):
"""
Installs a cog from the given repo.
"""
cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable
@cog.command(name="install", usage="<repo_name> <cog_name>")
async def _cog_install(self, ctx, repo: Repo, cog_name: str):
"""Install a cog from the given repo."""
cog: Installable = discord.utils.get(repo.available_cogs, name=cog_name)
if cog is None:
await ctx.send(
_("Error, there is no cog by the name of `{}` in the `{}` repo.").format(
cog_name, repo_name.name
)
_(
"Error: there is no cog by the name of `{cog_name}` in the `{repo.name}` repo."
).format(cog_name=cog_name, repo=repo)
)
return
elif cog.min_python_version > sys.version_info:
await ctx.send(
_("This cog requires at least python version {}, aborting install.").format(
".".join([str(n) for n in cog.min_python_version])
_("This cog requires at least python version {version}, aborting install.").format(
version=".".join([str(n) for n in cog.min_python_version])
)
)
return
if not await repo_name.install_requirements(cog, self.LIB_PATH):
if not await repo.install_requirements(cog, self.LIB_PATH):
await ctx.send(
_("Failed to install the required libraries for `{}`: `{}`").format(
cog.name, cog.requirements
)
_(
"Failed to install the required libraries for `{cog_name}`: `{libraries}`"
).format(cog_name=cog.name, libraries=cog.requirements)
)
return
await repo_name.install_cog(cog, await self.cog_install_path())
await repo.install_cog(cog, await self.cog_install_path())
await self._add_to_installed(cog)
await repo_name.install_libraries(self.SHAREDLIB_PATH)
await repo.install_libraries(self.SHAREDLIB_PATH)
await ctx.send(_("`{}` cog successfully installed.").format(cog_name))
await ctx.send(_("Cog `{cog_name}` successfully installed.").format(cog_name=cog_name))
if cog.install_msg is not None:
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
@cog.command(name="uninstall")
async def _cog_uninstall(self, ctx, cog_name: InstalledCog):
"""
Allows you to uninstall cogs that were previously installed
through Downloader.
@cog.command(name="uninstall", usage="<cog_name>")
async def _cog_uninstall(self, ctx, cog: InstalledCog):
"""Uninstall a cog.
You may only uninstall cogs which were previously installed
by Downloader.
"""
# noinspection PyUnresolvedReferences,PyProtectedMember
real_name = cog_name.name
real_name = cog.name
poss_installed_path = (await self.cog_install_path()) / real_name
if poss_installed_path.exists():
await self._delete_cog(poss_installed_path)
# noinspection PyTypeChecker
await self._remove_from_installed(cog_name)
await ctx.send(_("`{}` was successfully removed.").format(real_name))
await self._remove_from_installed(cog)
await ctx.send(
_("Cog `{cog_name}` was successfully uninstalled.").format(cog_name=real_name)
)
else:
await ctx.send(
_(
"That cog was installed but can no longer"
" be located. You may need to remove it's"
" files manually if it is still usable."
)
" Also make sure you've unloaded the cog"
" with `{prefix}unload {cog_name}`."
).format(cog_name=real_name)
)
@cog.command(name="update")
async def _cog_update(self, ctx, cog_name: InstalledCog = None):
"""
Updates all cogs or one of your choosing.
"""
"""Update all cogs, or one of your choosing."""
installed_cogs = set(await self.installed_cogs())
async with ctx.typing():
@ -418,11 +410,9 @@ class Downloader(commands.Cog):
else:
await ctx.send(_("OK then."))
@cog.command(name="list")
async def _cog_list(self, ctx, repo_name: Repo):
"""
Lists all available cogs from a single repo.
"""
@cog.command(name="list", usage="<repo_name>")
async def _cog_list(self, ctx, repo: Repo):
"""List all available cogs from a single repo."""
installed = await self.installed_cogs()
installed_str = ""
if installed:
@ -430,10 +420,10 @@ class Downloader(commands.Cog):
[
"- {}{}".format(i.name, ": {}".format(i.short) if i.short else "")
for i in installed
if i.repo_name == repo_name.name
if i.repo_name == repo.name
]
)
cogs = repo_name.available_cogs
cogs = repo.available_cogs
cogs = _("Available Cogs:\n") + "\n".join(
[
"+ {}: {}".format(c.name, c.short or "")
@ -445,20 +435,24 @@ class Downloader(commands.Cog):
for page in pagify(cogs, ["\n"], shorten_by=16):
await ctx.send(box(page.lstrip(" "), lang="diff"))
@cog.command(name="info")
async def _cog_info(self, ctx, repo_name: Repo, cog_name: str):
"""
Lists information about a single cog.
"""
cog = discord.utils.get(repo_name.available_cogs, name=cog_name)
@cog.command(name="info", usage="<repo_name> <cog_name>")
async def _cog_info(self, ctx, repo: Repo, cog_name: str):
"""List information about a single cog."""
cog = discord.utils.get(repo.available_cogs, name=cog_name)
if cog is None:
await ctx.send(
_("There is no cog `{}` in the repo `{}`").format(cog_name, repo_name.name)
_("There is no cog `{cog_name}` in the repo `{repo.name}`").format(
cog_name=cog_name, repo=repo
)
)
return
msg = _("Information on {}:\n{}\n\nRequirements: {}").format(
cog.name, cog.description or "", ", ".join(cog.requirements) or "None"
msg = _(
"Information on {cog_name}:\n{description}\n\nRequirements: {requirements}"
).format(
cog_name=cog.name,
description=cog.description or "",
requirements=", ".join(cog.requirements) or "None",
)
await ctx.send(box(msg))
@ -512,9 +506,9 @@ class Downloader(commands.Cog):
repo_url = "https://github.com/Cog-Creators/Red-DiscordBot"
cog_name = cog_installable.__class__.__name__
msg = _("Command: {}\nMade by: {}\nRepo: {}\nCog name: {}")
msg = _("Command: {command}\nMade by: {author}\nRepo: {repo}\nCog name: {cog}")
return msg.format(command_name, made_by, repo_url, cog_name)
return msg.format(command=command_name, author=made_by, repo=repo_url, cog=cog_name)
def cog_name_from_instance(self, instance: object) -> str:
"""Determines the cog name that Downloader knows from the cog instance.
@ -537,9 +531,9 @@ class Downloader(commands.Cog):
@commands.command()
async def findcog(self, ctx: commands.Context, command_name: str):
"""
Figures out which cog a command comes from. Only works with loaded
cogs.
"""Find which cog a command comes from.
This will only work with loaded cogs.
"""
command = ctx.bot.all_commands.get(command_name)

View File

@ -12,11 +12,15 @@ from typing import Tuple, MutableMapping, Union, Optional
from redbot.core import data_manager, commands
from redbot.core.utils import safe_delete
from redbot.core.i18n import Translator
from . import errors
from .installable import Installable, InstallableType
from .json_mixins import RepoJSONMixin
from .log import log
_ = Translator("RepoManager", __file__)
class Repo(RepoJSONMixin):
GIT_CLONE = "git clone --recurse-submodules -b {branch} {url} {folder}"
@ -64,13 +68,15 @@ class Repo(RepoJSONMixin):
async def convert(cls, ctx: commands.Context, argument: str):
downloader_cog = ctx.bot.get_cog("Downloader")
if downloader_cog is None:
raise commands.CommandError("No Downloader cog found.")
raise commands.CommandError(_("No Downloader cog found."))
# noinspection PyProtectedMember
repo_manager = downloader_cog._repo_manager
poss_repo = repo_manager.get_repo(argument)
if poss_repo is None:
raise commands.BadArgument("Repo by the name {} does not exist.".format(argument))
raise commands.BadArgument(
_('Repo by the name "{repo_name}" does not exist.').format(repo_name=argument)
)
return poss_repo
def _existing_git_repo(self) -> (bool, Path):

View File

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

View File

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

View File

@ -2,15 +2,14 @@ import datetime
import time
from enum import Enum
from random import randint, choice
from urllib.parse import quote_plus
import aiohttp
import discord
from redbot.core import commands
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
from redbot.core.utils.chat_formatting import escape, italics, pagify
from redbot.core.utils.chat_formatting import escape, italics
_ = Translator("General", __file__)
_ = T_ = Translator("General", __file__)
class RPS(Enum):
@ -29,71 +28,78 @@ class RPSParser:
elif argument == "scissors":
self.choice = RPS.scissors
else:
raise
raise ValueError
@cog_i18n(_)
class General(commands.Cog):
"""General commands."""
global _
_ = lambda s: s
ball = [
_("As I see it, yes"),
_("It is certain"),
_("It is decidedly so"),
_("Most likely"),
_("Outlook good"),
_("Signs point to yes"),
_("Without a doubt"),
_("Yes"),
_("Yes definitely"),
_("You may rely on it"),
_("Reply hazy, try again"),
_("Ask again later"),
_("Better not tell you now"),
_("Cannot predict now"),
_("Concentrate and ask again"),
_("Don't count on it"),
_("My reply is no"),
_("My sources say no"),
_("Outlook not so good"),
_("Very doubtful"),
]
_ = T_
def __init__(self):
super().__init__()
self.stopwatches = {}
self.ball = [
_("As I see it, yes"),
_("It is certain"),
_("It is decidedly so"),
_("Most likely"),
_("Outlook good"),
_("Signs point to yes"),
_("Without a doubt"),
_("Yes"),
_("Yes definitely"),
_("You may rely on it"),
_("Reply hazy, try again"),
_("Ask again later"),
_("Better not tell you now"),
_("Cannot predict now"),
_("Concentrate and ask again"),
_("Don't count on it"),
_("My reply is no"),
_("My sources say no"),
_("Outlook not so good"),
_("Very doubtful"),
]
@commands.command()
async def choose(self, ctx, *choices):
"""Chooses between multiple choices.
"""Choose between multiple options.
To denote multiple choices, you should use double quotes.
To denote options which include whitespace, you should use
double quotes.
"""
choices = [escape(c, mass_mentions=True) for c in choices]
if len(choices) < 2:
await ctx.send(_("Not enough choices to pick from."))
await ctx.send(_("Not enough options to pick from."))
else:
await ctx.send(choice(choices))
@commands.command()
async def roll(self, ctx, number: int = 100):
"""Rolls random number (between 1 and user choice)
"""Roll a random number.
Defaults to 100.
The result will be between 1 and `<number>`.
`<number>` defaults to 100.
"""
author = ctx.author
if number > 1:
n = randint(1, number)
await ctx.send(_("{} :game_die: {} :game_die:").format(author.mention, n))
await ctx.send("{author.mention} :game_die: {n} :game_die:".format(author=author, n=n))
else:
await ctx.send(_("{} Maybe higher than 1? ;P").format(author.mention))
await ctx.send(_("{author.mention} Maybe higher than 1? ;P").format(author=author))
@commands.command()
async def flip(self, ctx, user: discord.Member = None):
"""Flips a coin... or a user.
"""Flip a coin... or a user.
Defaults to coin.
Defaults to a coin.
"""
if user != None:
if user is not None:
msg = ""
if user.id == ctx.bot.user.id:
user = ctx.author
@ -112,7 +118,7 @@ class General(commands.Cog):
@commands.command()
async def rps(self, ctx, your_choice: RPSParser):
"""Play rock paper scissors"""
"""Play Rock Paper Scissors."""
author = ctx.author
player_choice = your_choice.choice
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
@ -131,39 +137,53 @@ class General(commands.Cog):
outcome = cond[(player_choice, red_choice)]
if outcome is True:
await ctx.send(_("{} You win {}!").format(red_choice.value, author.mention))
await ctx.send(
_("{choice} You win {author.mention}!").format(
choice=red_choice.value, author=author
)
)
elif outcome is False:
await ctx.send(_("{} You lose {}!").format(red_choice.value, author.mention))
await ctx.send(
_("{choice} You lose {author.mention}!").format(
choice=red_choice.value, author=author
)
)
else:
await ctx.send(_("{} We're square {}!").format(red_choice.value, author.mention))
await ctx.send(
_("{choice} We're square {author.mention}!").format(
choice=red_choice.value, author=author
)
)
@commands.command(name="8", aliases=["8ball"])
async def _8ball(self, ctx, *, question: str):
"""Ask 8 ball a question
"""Ask 8 ball a question.
Question must end with a question mark.
"""
if question.endswith("?") and question != "?":
await ctx.send("`" + choice(self.ball) + "`")
await ctx.send("`" + T_(choice(self.ball)) + "`")
else:
await ctx.send(_("That doesn't look like a question."))
@commands.command(aliases=["sw"])
async def stopwatch(self, ctx):
"""Starts/stops stopwatch"""
"""Start or stop the stopwatch."""
author = ctx.author
if not author.id in self.stopwatches:
if author.id not in self.stopwatches:
self.stopwatches[author.id] = int(time.perf_counter())
await ctx.send(author.mention + _(" Stopwatch started!"))
else:
tmp = abs(self.stopwatches[author.id] - int(time.perf_counter()))
tmp = str(datetime.timedelta(seconds=tmp))
await ctx.send(author.mention + _(" Stopwatch stopped! Time: **") + tmp + "**")
await ctx.send(
author.mention + _(" Stopwatch stopped! Time: **{seconds}**").format(seconds=tmp)
)
self.stopwatches.pop(author.id, None)
@commands.command()
async def lmgtfy(self, ctx, *, search_terms: str):
"""Creates a lmgtfy link"""
"""Create a lmgtfy link."""
search_terms = escape(
search_terms.replace("+", "%2B").replace(" ", "+"), mass_mentions=True
)
@ -172,9 +192,10 @@ class General(commands.Cog):
@commands.command(hidden=True)
@commands.guild_only()
async def hug(self, ctx, user: discord.Member, intensity: int = 1):
"""Because everyone likes hugs
"""Because everyone likes hugs!
Up to 10 intensity levels."""
Up to 10 intensity levels.
"""
name = italics(user.display_name)
if intensity <= 0:
msg = "(っ˘̩╭╮˘̩)っ" + name
@ -186,24 +207,27 @@ class General(commands.Cog):
msg = "(つ≧▽≦)つ" + name
elif intensity >= 10:
msg = "(づ ̄ ³ ̄)づ{} ⊂(´・ω・`⊂)".format(name)
else:
# For the purposes of "msg might not be defined" linter errors
raise RuntimeError
await ctx.send(msg)
@commands.command()
@commands.guild_only()
async def serverinfo(self, ctx):
"""Shows server's informations"""
"""Show server information."""
guild = ctx.guild
online = len([m.status for m in guild.members if m.status != discord.Status.offline])
total_users = len(guild.members)
text_channels = len(guild.text_channels)
voice_channels = len(guild.voice_channels)
passed = (ctx.message.created_at - guild.created_at).days
created_at = _("Since {}. That's over {} days ago!").format(
guild.created_at.strftime("%d %b %Y %H:%M"), passed
created_at = _("Since {date}. That's over {num} days ago!").format(
date=guild.created_at.strftime("%d %b %Y %H:%M"), num=passed
)
data = discord.Embed(description=created_at, colour=(await ctx.embed_colour()))
data.add_field(name=_("Region"), value=str(guild.region))
data.add_field(name=_("Users"), value="{}/{}".format(online, total_users))
data.add_field(name=_("Users"), value=f"{online}/{total_users}")
data.add_field(name=_("Text Channels"), value=str(text_channels))
data.add_field(name=_("Voice Channels"), value=str(voice_channels))
data.add_field(name=_("Roles"), value=str(len(guild.roles)))
@ -218,12 +242,15 @@ class General(commands.Cog):
try:
await ctx.send(embed=data)
except discord.HTTPException:
except discord.Forbidden:
await ctx.send(_("I need the `Embed links` permission to send this."))
@commands.command()
async def urban(self, ctx, *, word):
"""Searches urban dictionary entries using the unofficial api"""
"""Search the Urban Dictionary.
This uses the unofficial Urban Dictionary API.
"""
try:
url = "https://api.urbandictionary.com/v0/define?term=" + str(word).lower()
@ -234,10 +261,11 @@ class General(commands.Cog):
async with session.get(url, headers=headers) as response:
data = await response.json()
except:
except aiohttp.ClientError:
await ctx.send(
_("No Urban dictionary entries were found or there was an error in the process")
_("No Urban dictionary entries were found, or there was an error in the process")
)
return
if data.get("error") != 404:
@ -246,20 +274,20 @@ class General(commands.Cog):
embeds = []
for ud in data["list"]:
embed = discord.Embed()
embed.title = _("{} by {}").format(ud["word"].capitalize(), ud["author"])
embed.title = _("{word} by {author}").format(
word=ud["word"].capitalize(), author=ud["author"]
)
embed.url = ud["permalink"]
description = "{} \n \n **Example : ** {}".format(
ud["definition"], ud.get("example", "N/A")
)
description = _("{definition}\n\n**Example:** {example}").format(**ud)
if len(description) > 2048:
description = "{}...".format(description[:2045])
embed.description = description
embed.set_footer(
text=_("{} Down / {} Up , Powered by urban dictionary").format(
ud["thumbs_down"], ud["thumbs_up"]
)
text=_(
"{thumbs_down} Down / {thumbs_up} Up, Powered by Urban Dictionary."
).format(**ud)
)
embeds.append(embed)
@ -275,24 +303,15 @@ class General(commands.Cog):
else:
messages = []
for ud in data["list"]:
description = _("{} \n \n **Example : ** {}").format(
ud["definition"], ud.get("example", "N/A")
)
ud.set_default("example", "N/A")
description = _("{definition}\n\n**Example:** {example}").format(**ud)
if len(description) > 2048:
description = "{}...".format(description[:2045])
description = description
message = _(
"<{}> \n {} by {} \n \n {} \n \n {} Down / {} Up, Powered by urban "
"dictionary"
).format(
ud["permalink"],
ud["word"].capitalize(),
ud["author"],
description,
ud["thumbs_down"],
ud["thumbs_up"],
)
"<{permalink}>\n {word} by {author}\n\n{description}\n\n"
"{thumbs_down} Down / {thumbs_up} Up, Powered by urban dictionary"
).format(word=ud.pop("word").capitalize(), description=description, **ud)
messages.append(message)
if messages is not None and len(messages) > 0:
@ -306,6 +325,6 @@ class General(commands.Cog):
)
else:
await ctx.send(
_("No Urban dictionary entries were found or there was an error in the process")
_("No Urban dictionary entries were found, or there was an error in the process.")
)
return

View File

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

View File

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

View File

@ -1,6 +1,8 @@
import asyncio
import contextlib
from datetime import datetime, timedelta
from collections import deque, defaultdict, namedtuple
from typing import cast
import discord
@ -14,7 +16,7 @@ from .log import log
from redbot.core.utils.common_filters import filter_invites, filter_various_mentions
_ = Translator("Mod", __file__)
_ = T_ = Translator("Mod", __file__)
@cog_i18n(_)
@ -58,7 +60,8 @@ class Mod(commands.Cog):
self.registration_task.cancel()
self.tban_expiry_task.cancel()
async def _casetype_registration(self):
@staticmethod
async def _casetype_registration():
casetypes_to_register = [
{
"name": "ban",
@ -168,7 +171,7 @@ class Mod(commands.Cog):
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def modset(self, ctx: commands.Context):
"""Manages server administration settings."""
"""Manage server administration settings."""
if ctx.invoked_subcommand is None:
guild = ctx.guild
# Display current settings
@ -178,23 +181,37 @@ class Mod(commands.Cog):
delete_delay = await self.settings.guild(guild).delete_delay()
reinvite_on_unban = await self.settings.guild(guild).reinvite_on_unban()
msg = ""
msg += "Delete repeats: {}\n".format("Yes" if delete_repeats else "No")
msg += "Ban mention spam: {}\n".format(
"{} mentions".format(ban_mention_spam)
if isinstance(ban_mention_spam, int)
else "No"
msg += _("Delete repeats: {yes_or_no}\n").format(
yes_or_no=_("Yes") if delete_repeats else _("No")
)
msg += "Respects hierarchy: {}\n".format("Yes" if respect_hierarchy else "No")
msg += "Delete delay: {}\n".format(
"{} seconds".format(delete_delay) if delete_delay != -1 else "None"
msg += _("Ban mention spam: {num_mentions}\n").format(
num_mentions=_("{num} mentions").format(num=ban_mention_spam)
if ban_mention_spam
else _("No")
)
msg += _("Respects hierarchy: {yes_or_no}\n").format(
yes_or_no=_("Yes") if respect_hierarchy else _("No")
)
msg += _("Delete delay: {num_seconds}\n").format(
num_seconds=_("{num} seconds").format(delete_delay)
if delete_delay != -1
else _("None")
)
msg += _("Reinvite on unban: {yes_or_no}\n").format(
yes_or_no=_("Yes") if reinvite_on_unban else _("No")
)
msg += "Reinvite on unban: {}".format("Yes" if reinvite_on_unban else "No")
await ctx.send(box(msg))
@modset.command()
@commands.guild_only()
async def hierarchy(self, ctx: commands.Context):
"""Toggles role hierarchy check for mods / admins"""
"""Toggle role hierarchy check for mods and admins.
**WARNING**: Disabling this setting will allow mods to take
actions on users above them in the role hierarchy!
This is enabled by default.
"""
guild = ctx.guild
toggled = await self.settings.guild(guild).respect_hierarchy()
if not toggled:
@ -210,10 +227,14 @@ class Mod(commands.Cog):
@modset.command()
@commands.guild_only()
async def banmentionspam(self, ctx: commands.Context, max_mentions: int = False):
"""Enables auto ban for messages mentioning X different people
async def banmentionspam(self, ctx: commands.Context, max_mentions: int = 0):
"""Set the autoban conditions for mention spam.
Accepted values: 5 or superior"""
Users will be banned if they send any message which contains more than
`<max_mentions>` mentions.
`<max_mentions>` must be at least 5. Set to 0 to disable.
"""
guild = ctx.guild
if max_mentions:
if max_mentions < 5:
@ -222,13 +243,13 @@ class Mod(commands.Cog):
await ctx.send(
_(
"Autoban for mention spam enabled. "
"Anyone mentioning {} or more different people "
"Anyone mentioning {max_mentions} or more different people "
"in a single message will be autobanned."
).format(max_mentions)
).format(max_mentions=max_mentions)
)
else:
cur_setting = await self.settings.guild(guild).ban_mention_spam()
if cur_setting is False:
if not cur_setting:
await ctx.send_help()
return
await self.settings.guild(guild).ban_mention_spam.set(False)
@ -237,7 +258,7 @@ class Mod(commands.Cog):
@modset.command()
@commands.guild_only()
async def deleterepeats(self, ctx: commands.Context):
"""Enables auto deletion of repeated messages"""
"""Enable auto-deletion of repeated messages."""
guild = ctx.guild
cur_setting = await self.settings.guild(guild).delete_repeats()
if not cur_setting:
@ -250,11 +271,12 @@ class Mod(commands.Cog):
@modset.command()
@commands.guild_only()
async def deletedelay(self, ctx: commands.Context, time: int = None):
"""Sets the delay until the bot removes the command message.
"""Set the delay until the bot removes the command message.
Must be between -1 and 60.
A delay of -1 means the bot will not remove the message."""
Set to -1 to disable this feature.
"""
guild = ctx.guild
if time is not None:
time = min(max(time, -1), 60) # Enforces the time limits
@ -262,16 +284,16 @@ class Mod(commands.Cog):
if time == -1:
await ctx.send(_("Command deleting disabled."))
else:
await ctx.send(_("Delete delay set to {} seconds.").format(time))
await ctx.send(_("Delete delay set to {num} seconds.").format(num=time))
else:
delay = await self.settings.guild(guild).delete_delay()
if delay != -1:
await ctx.send(
_(
"Bot will delete command messages after"
" {} seconds. Set this value to -1 to"
" {num} seconds. Set this value to -1 to"
" stop deleting messages"
).format(delay)
).format(num=delay)
)
else:
await ctx.send(_("I will not delete command messages."))
@ -279,33 +301,44 @@ class Mod(commands.Cog):
@modset.command()
@commands.guild_only()
async def reinvite(self, ctx: commands.Context):
"""Toggles whether an invite will be sent when a user is unbanned via [p]unban.
"""Toggle whether an invite will be sent to a user when unbanned.
If this is True, the bot will attempt to create and send a single-use invite
to the newly-unbanned user"""
to the newly-unbanned user.
"""
guild = ctx.guild
cur_setting = await self.settings.guild(guild).reinvite_on_unban()
if not cur_setting:
await self.settings.guild(guild).reinvite_on_unban.set(True)
await ctx.send(_("Users unbanned with {} will be reinvited.").format("[p]unban"))
await ctx.send(
_("Users unbanned with {command} will be reinvited.").format(f"{ctx.prefix}unban")
)
else:
await self.settings.guild(guild).reinvite_on_unban.set(False)
await ctx.send(_("Users unbanned with {} will not be reinvited.").format("[p]unban"))
await ctx.send(
_("Users unbanned with {command} will not be reinvited.").format(
f"{ctx.prefix}unban"
)
)
@commands.command()
@commands.guild_only()
@commands.bot_has_permissions(kick_members=True)
@checks.admin_or_permissions(kick_members=True)
async def kick(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Kicks user.
"""Kick a user.
If a reason is specified, it will be the reason that shows up
in the audit log"""
in the audit log.
"""
author = ctx.author
guild = ctx.guild
if author == user:
await ctx.send(
_("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}")
_("I cannot let you do that. Self-harm is bad {emoji}").format(
emoji="\N{PENSIVE FACE}"
)
)
return
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
@ -348,14 +381,18 @@ class Mod(commands.Cog):
@commands.command()
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True)
async def ban(
self, ctx: commands.Context, user: discord.Member, days: str = None, *, reason: str = None
):
"""Bans user and deletes last X days worth of messages.
"""Ban a user from this server.
If days is not a number, it's treated as the first word of the reason.
Minimum 0 days, maximum 7. Defaults to 0."""
Deletes `<days>` worth of messages.
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
guild = ctx.guild
@ -429,16 +466,16 @@ class Mod(commands.Cog):
@commands.command()
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True)
async def hackban(self, ctx: commands.Context, user_id: int, *, reason: str = None):
"""Preemptively bans user from the server
"""Pre-emptively ban a user from this server.
A user ID needs to be provided in order to ban
using this command"""
using this command.
"""
author = ctx.author
guild = ctx.guild
if not guild.me.guild_permissions.ban_members:
return await ctx.send(_("I lack the permissions to do this."))
is_banned = False
ban_list = await guild.bans()
for entry in ban_list:
@ -489,75 +526,77 @@ class Mod(commands.Cog):
@commands.command()
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True)
async def tempban(
self, ctx: commands.Context, user: discord.Member, days: int = 1, *, reason: str = None
):
"""Tempbans the user for the specified number of days"""
"""Temporarily ban a user from this server."""
guild = ctx.guild
author = ctx.author
days_delta = timedelta(days=int(days))
unban_time = datetime.utcnow() + days_delta
channel = ctx.channel
can_ban = channel.permissions_for(guild.me).ban_members
invite = await self.get_invite_for_reinvite(ctx, int(days_delta.total_seconds() + 86400))
if invite is None:
invite = ""
if can_ban:
queue_entry = (guild.id, user.id)
await self.settings.member(user).banned_until.set(unban_time.timestamp())
cur_tbans = await self.settings.guild(guild).current_tempbans()
cur_tbans.append(user.id)
await self.settings.guild(guild).current_tempbans.set(cur_tbans)
queue_entry = (guild.id, user.id)
await self.settings.member(user).banned_until.set(unban_time.timestamp())
cur_tbans = await self.settings.guild(guild).current_tempbans()
cur_tbans.append(user.id)
await self.settings.guild(guild).current_tempbans.set(cur_tbans)
try: # We don't want blocked DMs preventing us from banning
msg = await user.send(
_(
"You have been temporarily banned from {} until {}. "
"Here is an invite for when your ban expires: {}"
).format(guild.name, unban_time.strftime("%m-%d-%Y %H:%M:%S"), invite)
with contextlib.suppress(discord.HTTPException):
# We don't want blocked DMs preventing us from banning
await user.send(
_(
"You have been temporarily banned from {server_name} until {date}. "
"Here is an invite for when your ban expires: {invite_link}"
).format(
server_name=guild.name,
date=unban_time.strftime("%m-%d-%Y %H:%M:%S"),
invite_link=invite,
)
except discord.HTTPException:
msg = None
self.ban_queue.append(queue_entry)
try:
await guild.ban(user)
except discord.Forbidden:
await ctx.send(_("I can't do that for some reason."))
except discord.HTTPException:
await ctx.send(_("Something went wrong while banning"))
else:
await ctx.send(_("Done. Enough chaos for now"))
)
self.ban_queue.append(queue_entry)
try:
await guild.ban(user)
except discord.Forbidden:
await ctx.send(_("I can't do that for some reason."))
except discord.HTTPException:
await ctx.send(_("Something went wrong while banning"))
else:
await ctx.send(_("Done. Enough chaos for now"))
try:
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at,
"tempban",
user,
author,
reason,
unban_time,
)
except RuntimeError as e:
await ctx.send(e)
try:
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at,
"tempban",
user,
author,
reason,
unban_time,
)
except RuntimeError as e:
await ctx.send(e)
@commands.command()
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True)
async def softban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Kicks the user, deleting 1 day worth of messages."""
"""Kick a user and delete 1 day's worth of their messages."""
guild = ctx.guild
channel = ctx.channel
can_ban = channel.permissions_for(guild.me).ban_members
author = ctx.author
if author == user:
await ctx.send(
_("I cannot let you do that. Self-harm is bad {}").format("\N{PENSIVE FACE}")
_("I cannot let you do that. Self-harm is bad {emoji}").format(
emoji="\N{PENSIVE FACE}"
)
)
return
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
@ -576,75 +615,69 @@ class Mod(commands.Cog):
if invite is None:
invite = ""
if can_ban:
queue_entry = (guild.id, user.id)
try: # We don't want blocked DMs preventing us from banning
msg = await user.send(
_(
"You have been banned and "
"then unbanned as a quick way to delete your messages.\n"
"You can now join the server again. {}"
).format(invite)
)
except discord.HTTPException:
msg = None
self.ban_queue.append(queue_entry)
try:
await guild.ban(user, reason=audit_reason, delete_message_days=1)
except discord.errors.Forbidden:
self.ban_queue.remove(queue_entry)
await ctx.send(_("My role is not high enough to softban that user."))
if msg is not None:
await msg.delete()
return
except discord.HTTPException as e:
self.ban_queue.remove(queue_entry)
print(e)
return
self.unban_queue.append(queue_entry)
try:
await guild.unban(user)
except discord.HTTPException as e:
self.unban_queue.remove(queue_entry)
print(e)
return
else:
await ctx.send(_("Done. Enough chaos."))
log.info(
"{}({}) softbanned {}({}), deleting 1 day worth "
"of messages".format(author.name, author.id, user.name, user.id)
)
try:
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at,
"softban",
user,
author,
reason,
until=None,
channel=None,
)
except RuntimeError as e:
await ctx.send(e)
queue_entry = (guild.id, user.id)
try: # We don't want blocked DMs preventing us from banning
msg = await user.send(
_(
"You have been banned and "
"then unbanned as a quick way to delete your messages.\n"
"You can now join the server again. {invite_link}"
).format(invite_link=invite)
)
except discord.HTTPException:
msg = None
self.ban_queue.append(queue_entry)
try:
await guild.ban(user, reason=audit_reason, delete_message_days=1)
except discord.errors.Forbidden:
self.ban_queue.remove(queue_entry)
await ctx.send(_("My role is not high enough to softban that user."))
if msg is not None:
await msg.delete()
return
except discord.HTTPException as e:
self.ban_queue.remove(queue_entry)
print(e)
return
self.unban_queue.append(queue_entry)
try:
await guild.unban(user)
except discord.HTTPException as e:
self.unban_queue.remove(queue_entry)
print(e)
return
else:
await ctx.send(_("I'm not allowed to do that."))
await ctx.send(_("Done. Enough chaos."))
log.info(
"{}({}) softbanned {}({}), deleting 1 day worth "
"of messages".format(author.name, author.id, user.name, user.id)
)
try:
await modlog.create_case(
self.bot,
guild,
ctx.message.created_at,
"softban",
user,
author,
reason,
until=None,
channel=None,
)
except RuntimeError as e:
await ctx.send(e)
@commands.command()
@commands.guild_only()
@commands.bot_has_permissions(ban_members=True)
@checks.admin_or_permissions(ban_members=True)
async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = None):
"""Unbans the target user.
"""Unban a user from this server.
Requires specifying the target user's ID. To find this, you may either:
1. Copy it from the mod log case (if one was created), or
2. enable developer mode, go to Bans in this server's settings, right-
click the user and select 'Copy ID'."""
channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).ban_members:
await ctx.send("I need the Ban Members permission to do this.")
return
guild = ctx.guild
author = ctx.author
user = await self.bot.get_user_info(user_id)
@ -687,26 +720,26 @@ class Mod(commands.Cog):
invite = await self.get_invite_for_reinvite(ctx)
if invite:
try:
user.send(
await user.send(
_(
"You've been unbanned from {}.\n"
"Here is an invite for that server: {}"
).format(guild.name, invite.url)
"You've been unbanned from {server}.\n"
"Here is an invite for that server: {invite_link}"
).format(server=guild.name, invite_link=invite.url)
)
except discord.Forbidden:
await ctx.send(
_(
"I failed to send an invite to that user. "
"Perhaps you may be able to send it for me?\n"
"Here's the invite link: {}"
).format(invite.url)
"Here's the invite link: {invite_link}"
).format(invite_link=invite.url)
)
except discord.HTTPException:
await ctx.send(
_(
"Something went wrong when attempting to send that user"
"an invite. Here's the link so you can try: {}"
).format(invite.url)
"an invite. Here's the link so you can try: {invite_link}"
).format(invite_link=invite.url)
)
@staticmethod
@ -750,7 +783,7 @@ class Mod(commands.Cog):
@admin_or_voice_permissions(mute_members=True, deafen_members=True)
@bot_has_voice_permissions(mute_members=True, deafen_members=True)
async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Bans the target user from speaking and listening in voice channels in the server"""
"""Ban a user from speaking and listening in the server's voice channels."""
user_voice_state = user.voice
if user_voice_state is None:
await ctx.send(_("No voice state for that user!"))
@ -791,7 +824,7 @@ class Mod(commands.Cog):
@admin_or_voice_permissions(mute_members=True, deafen_members=True)
@bot_has_voice_permissions(mute_members=True, deafen_members=True)
async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Unbans the user from speaking/listening in the server's voice channels"""
"""Unban a the user from speaking and listening in the server's voice channels."""
user_voice_state = user.voice
if user_voice_state is None:
await ctx.send(_("No voice state for that user!"))
@ -828,27 +861,24 @@ class Mod(commands.Cog):
@commands.command()
@commands.guild_only()
@commands.bot_has_permissions(manage_nicknames=True)
@checks.admin_or_permissions(manage_nicknames=True)
async def rename(self, ctx: commands.Context, user: discord.Member, *, nickname=""):
"""Changes user's nickname
"""Change a user's nickname.
Leaving the nickname empty will remove it."""
Leaving the nickname empty will remove it.
"""
nickname = nickname.strip()
if nickname == "":
nickname = None
try:
await user.edit(reason=get_audit_reason(ctx.author, None), nick=nickname)
await ctx.send("Done.")
except discord.Forbidden:
await ctx.send(
_("I cannot do that, I lack the '{}' permission.").format("Manage Nicknames")
)
await user.edit(reason=get_audit_reason(ctx.author, None), nick=nickname)
await ctx.send("Done.")
@commands.group()
@commands.guild_only()
@checks.mod_or_permissions(manage_channel=True)
async def mute(self, ctx: commands.Context):
"""Mutes user in the channel/server"""
"""Mute users."""
pass
@mute.command(name="voice")
@ -856,7 +886,7 @@ class Mod(commands.Cog):
@mod_or_voice_permissions(mute_members=True)
@bot_has_voice_permissions(mute_members=True)
async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Mutes the user in a voice channel"""
"""Mute a user in their current voice channel."""
user_voice_state = user.voice
guild = ctx.guild
author = ctx.author
@ -868,9 +898,7 @@ class Mod(commands.Cog):
audit_reason = get_audit_reason(ctx.author, reason)
await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason)
await ctx.send(
_("Muted {}#{} in channel {}").format(
user.name, user.discriminator, channel.name
)
_("Muted {user} in channel {channel.name}").format(user, channel=channel)
)
try:
await modlog.create_case(
@ -888,7 +916,9 @@ class Mod(commands.Cog):
await ctx.send(e)
return
elif channel.permissions_for(user).speak is False:
await ctx.send(_("That user is already muted in {}!").format(channel.name))
await ctx.send(
_("That user is already muted in {channel}!").format(channel=channel.name)
)
return
else:
await ctx.send(_("That user is not in a voice channel right now!"))
@ -896,22 +926,23 @@ class Mod(commands.Cog):
await ctx.send(_("No voice state for the target!"))
return
@checks.mod_or_permissions(administrator=True)
@mute.command(name="channel")
@commands.guild_only()
@commands.bot_has_permissions(manage_roles=True)
@checks.mod_or_permissions(administrator=True)
async def channel_mute(
self, ctx: commands.Context, user: discord.Member, *, reason: str = None
):
"""Mutes user in the current channel"""
"""Mute a user in the current text channel."""
author = ctx.message.author
channel = ctx.message.channel
guild = ctx.guild
if reason is None:
audit_reason = "Channel mute requested by {} (ID {})".format(author, author.id)
audit_reason = "Channel mute requested by {a} (ID {a.id})".format(a=author)
else:
audit_reason = "Channel mute requested by {} (ID {}). Reason: {}".format(
author, author.id, reason
audit_reason = "Channel mute requested by {a} (ID {a.id}). Reason: {r}".format(
a=author, r=reason
)
success, issue = await self.mute_user(guild, channel, author, user, audit_reason)
@ -935,20 +966,22 @@ class Mod(commands.Cog):
else:
await channel.send(issue)
@checks.mod_or_permissions(administrator=True)
@mute.command(name="server", aliases=["guild"])
@commands.guild_only()
@commands.bot_has_permissions(manage_roles=True)
@checks.mod_or_permissions(administrator=True)
async def guild_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Mutes user in the server"""
author = ctx.message.author
guild = ctx.guild
user_voice_state = user.voice
if reason is None:
audit_reason = "server mute requested by {} (ID {})".format(author, author.id)
else:
audit_reason = "server mute requested by {} (ID {}). Reason: {}".format(
author, author.id, reason
audit_reason = "server mute requested by {author} (ID {author.id})".format(
author=author
)
else:
audit_reason = (
"server mute requested by {author} (ID {author.id}). Reason: {reason}"
).format(author=author, reason=reason)
mute_success = []
for channel in guild.channels:
@ -992,10 +1025,10 @@ class Mod(commands.Cog):
perms_cache = await self.settings.member(user).perms_cache()
if overwrites.send_messages is False or permissions.send_messages is False:
return False, mute_unmute_issues["already_muted"]
return False, T_(mute_unmute_issues["already_muted"])
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
return False, mute_unmute_issues["hierarchy_problem"]
return False, T_(mute_unmute_issues["hierarchy_problem"])
perms_cache[str(channel.id)] = {
"send_messages": overwrites.send_messages,
@ -1005,28 +1038,27 @@ class Mod(commands.Cog):
try:
await channel.set_permissions(user, overwrite=overwrites, reason=reason)
except discord.Forbidden:
return False, mute_unmute_issues["permissions_issue"]
return False, T_(mute_unmute_issues["permissions_issue"])
else:
await self.settings.member(user).perms_cache.set(perms_cache)
return True, None
@commands.group()
@commands.guild_only()
@commands.bot_has_permissions(manage_roles=True)
@checks.mod_or_permissions(manage_channel=True)
async def unmute(self, ctx: commands.Context):
"""Unmutes user in the channel/server
Defaults to channel"""
"""Unmute users."""
pass
@unmute.command(name="voice")
@commands.guild_only()
@mod_or_voice_permissions(mute_members=True)
@bot_has_voice_permissions(mute_members=True)
async def voice_unmute(
async def unmute_voice(
self, ctx: commands.Context, user: discord.Member, *, reason: str = None
):
"""Unmutes the user in a voice channel"""
"""Unmute a user in their current voice channel."""
user_voice_state = user.voice
if user_voice_state:
channel = user_voice_state.channel
@ -1067,11 +1099,12 @@ class Mod(commands.Cog):
@checks.mod_or_permissions(administrator=True)
@unmute.command(name="channel")
@commands.bot_has_permissions(manage_roles=True)
@commands.guild_only()
async def channel_unmute(
async def unmute_channel(
self, ctx: commands.Context, user: discord.Member, *, reason: str = None
):
"""Unmutes user in the current channel"""
"""Unmute a user in this channel."""
channel = ctx.channel
author = ctx.author
guild = ctx.guild
@ -1099,14 +1132,14 @@ class Mod(commands.Cog):
@checks.mod_or_permissions(administrator=True)
@unmute.command(name="server", aliases=["guild"])
@commands.bot_has_permissions(manage_roles=True)
@commands.guild_only()
async def guild_unmute(
async def unmute_guild(
self, ctx: commands.Context, user: discord.Member, *, reason: str = None
):
"""Unmutes user in the server"""
"""Unmute a user in this server."""
guild = ctx.guild
author = ctx.author
channel = ctx.channel
unmute_success = []
for channel in guild.channels:
@ -1146,10 +1179,10 @@ class Mod(commands.Cog):
perms_cache = await self.settings.member(user).perms_cache()
if overwrites.send_messages or permissions.send_messages:
return False, mute_unmute_issues["already_unmuted"]
return False, T_(mute_unmute_issues["already_unmuted"])
elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user):
return False, mute_unmute_issues["hierarchy_problem"]
return False, T_(mute_unmute_issues["hierarchy_problem"])
if channel.id in perms_cache:
old_values = perms_cache[channel.id]
@ -1164,9 +1197,11 @@ class Mod(commands.Cog):
if not is_empty:
await channel.set_permissions(user, overwrite=overwrites)
else:
await channel.set_permissions(user, overwrite=None)
await channel.set_permissions(
user, overwrite=cast(discord.PermissionOverwrite, None)
)
except discord.Forbidden:
return False, mute_unmute_issues["permissions_issue"]
return False, T_(mute_unmute_issues["permissions_issue"])
else:
try:
del perms_cache[channel.id]
@ -1180,15 +1215,16 @@ class Mod(commands.Cog):
@commands.guild_only()
@checks.admin_or_permissions(manage_channels=True)
async def ignore(self, ctx: commands.Context):
"""Adds servers/channels to ignorelist"""
"""Add servers or channels to the ignore list."""
if ctx.invoked_subcommand is None:
await ctx.send(await self.count_ignored())
@ignore.command(name="channel")
async def ignore_channel(self, ctx: commands.Context, channel: discord.TextChannel = None):
"""Ignores channel
"""Ignore commands in the channel.
Defaults to current one"""
Defaults to the current channel.
"""
if not channel:
channel = ctx.channel
if not await self.settings.channel(channel).ignored():
@ -1200,7 +1236,7 @@ class Mod(commands.Cog):
@ignore.command(name="server", aliases=["guild"])
@checks.admin_or_permissions(manage_guild=True)
async def ignore_guild(self, ctx: commands.Context):
"""Ignores current server"""
"""Ignore commands in this server."""
guild = ctx.guild
if not await self.settings.guild(guild).ignored():
await self.settings.guild(guild).ignored.set(True)
@ -1212,15 +1248,16 @@ class Mod(commands.Cog):
@commands.guild_only()
@checks.admin_or_permissions(manage_channels=True)
async def unignore(self, ctx: commands.Context):
"""Removes servers/channels from ignorelist"""
"""Remove servers or channels from the ignore list."""
if ctx.invoked_subcommand is None:
await ctx.send(await self.count_ignored())
@unignore.command(name="channel")
async def unignore_channel(self, ctx: commands.Context, channel: discord.TextChannel = None):
"""Removes channel from ignore list
"""Remove a channel from ignore the list.
Defaults to current one"""
Defaults to the current channel.
"""
if not channel:
channel = ctx.channel
@ -1233,7 +1270,7 @@ class Mod(commands.Cog):
@unignore.command(name="server", aliases=["guild"])
@checks.admin_or_permissions(manage_guild=True)
async def unignore_guild(self, ctx: commands.Context):
"""Removes current guild from ignore list"""
"""Remove this server from the ignore list."""
guild = ctx.message.guild
if await self.settings.guild(guild).ignored():
await self.settings.guild(guild).ignored.set(False)
@ -1258,7 +1295,8 @@ class Mod(commands.Cog):
"""Global check to see if a channel or server is ignored.
Any users who have permission to use the `ignore` or `unignore` commands
surpass the check."""
surpass the check.
"""
perms = ctx.channel.permissions_for(ctx.author)
surpass_ignore = (
isinstance(ctx.channel, discord.abc.PrivateChannel)
@ -1274,14 +1312,15 @@ class Mod(commands.Cog):
@commands.command()
@commands.guild_only()
@commands.bot_has_permissions(embed_links=True)
async def userinfo(self, ctx, *, user: discord.Member = None):
"""Shows information for a user.
"""Show information about a user.
This includes fields for status, discord join date, server
join date, voice state and previous names/nicknames.
If the user has none of roles, previous names or previous
nicknames, these fields will be omitted.
If the user has no roles, previous names or previous nicknames,
these fields will be omitted.
"""
author = ctx.author
guild = ctx.guild
@ -1357,14 +1396,11 @@ class Mod(commands.Cog):
else:
data.set_author(name=name)
try:
await ctx.send(embed=data)
except discord.HTTPException:
await ctx.send(_("I need the `Embed links` permission to send this."))
await ctx.send(embed=data)
@commands.command()
async def names(self, ctx: commands.Context, user: discord.Member):
"""Show previous names/nicknames of a user"""
"""Show previous names and nicknames of a user."""
names, nicks = await self.get_names_and_nicks(user)
msg = ""
if names:
@ -1407,7 +1443,7 @@ class Mod(commands.Cog):
queue_entry = (guild.id, user.id)
self.unban_queue.append(queue_entry)
try:
await guild.unban(user, reason="Tempban finished")
await guild.unban(user, reason=_("Tempban finished"))
guild_tempbans.remove(uid)
except discord.Forbidden:
self.unban_queue.remove(queue_entry)
@ -1437,12 +1473,12 @@ class Mod(commands.Cog):
guild = message.guild
author = message.author
if await self.settings.guild(guild).ban_mention_spam():
max_mentions = await self.settings.guild(guild).ban_mention_spam()
max_mentions = await self.settings.guild(guild).ban_mention_spam()
if max_mentions:
mentions = set(message.mentions)
if len(mentions) >= max_mentions:
try:
await guild.ban(author, reason="Mention spam (Autoban)")
await guild.ban(author, reason=_("Mention spam (Autoban)"))
except discord.HTTPException:
log.info(
"Failed to ban member for mention spam in server {}.".format(guild.id)
@ -1456,7 +1492,7 @@ class Mod(commands.Cog):
"ban",
author,
guild.me,
"Mention spam (Autoban)",
_("Mention spam (Autoban)"),
until=None,
channel=None,
)
@ -1469,6 +1505,7 @@ class Mod(commands.Cog):
async def on_command_completion(self, ctx: commands.Context):
await self._delete_delay(ctx)
# noinspection PyUnusedLocal
async def on_command_error(self, ctx: commands.Context, error):
await self._delete_delay(ctx)
@ -1485,11 +1522,9 @@ class Mod(commands.Cog):
return
async def _delete_helper(m):
try:
with contextlib.suppress(discord.HTTPException):
await m.delete()
log.debug("Deleted command msg {}".format(m.id))
except:
pass # We don't really care if it fails or not
await asyncio.sleep(delay)
await _delete_helper(message)
@ -1511,7 +1546,7 @@ class Mod(commands.Cog):
return
deleted = await self.check_duplicates(message)
if not deleted:
deleted = await self.check_mention_spam(message)
await self.check_mention_spam(message)
async def on_member_ban(self, guild: discord.Guild, member: discord.Member):
if (guild.id, member.id) in self.ban_queue:
@ -1551,7 +1586,8 @@ class Mod(commands.Cog):
except RuntimeError as e:
print(e)
async def on_modlog_case_create(self, case: modlog.Case):
@staticmethod
async def on_modlog_case_create(case: modlog.Case):
"""
An event for modlog case creation
"""
@ -1566,7 +1602,8 @@ class Mod(commands.Cog):
msg = await mod_channel.send(case_content)
await case.edit({"message": msg})
async def on_modlog_case_edit(self, case: modlog.Case):
@staticmethod
async def on_modlog_case_edit(case: modlog.Case):
"""
Event for modlog case edits
"""
@ -1579,7 +1616,10 @@ class Mod(commands.Cog):
else:
await case.message.edit(content=case_content)
async def get_audit_entry_info(self, guild: discord.Guild, action: int, target):
@classmethod
async def get_audit_entry_info(
cls, guild: discord.Guild, action: discord.AuditLogAction, target
):
"""Get info about an audit log entry.
Parameters
@ -1599,14 +1639,15 @@ class Mod(commands.Cog):
if the audit log entry could not be found.
"""
try:
entry = await self.get_audit_log_entry(guild, action=action, target=target)
entry = await cls.get_audit_log_entry(guild, action=action, target=target)
except discord.HTTPException:
entry = None
if entry is None:
return None, None, None
return entry.user, entry.reason, entry.created_at
async def get_audit_log_entry(self, guild: discord.Guild, action: int, target):
@staticmethod
async def get_audit_log_entry(guild: discord.Guild, action: discord.AuditLogAction, target):
"""Get an audit log entry.
Any exceptions encountered when looking through the audit log will be
@ -1660,12 +1701,16 @@ class Mod(commands.Cog):
return [p for p in iter(overwrites)] == [p for p in iter(discord.PermissionOverwrite())]
_ = lambda s: s
mute_unmute_issues = {
"already_muted": "That user can't send messages in this channel.",
"already_unmuted": "That user isn't muted in this channel!",
"hierarchy_problem": "I cannot let you do that. You are not higher than "
"the user in the role hierarchy.",
"permissions_issue": "Failed to mute user. I need the manage roles "
"permission and the user I'm muting must be "
"lower than myself in the role hierarchy.",
"already_muted": _("That user can't send messages in this channel."),
"already_unmuted": _("That user isn't muted in this channel!"),
"hierarchy_problem": _(
"I cannot let you do that. You are not higher than " "the user in the role hierarchy."
),
"permissions_issue": _(
"Failed to mute user. I need the manage roles "
"permission and the user I'm muting must be "
"lower than myself in the role hierarchy."
),
}

View File

@ -1,3 +1,5 @@
from typing import Optional
import discord
from redbot.core import checks, modlog, commands
@ -10,7 +12,7 @@ _ = Translator("ModLog", __file__)
@cog_i18n(_)
class ModLog(commands.Cog):
"""Log for mod actions"""
"""Manage log channels for moderation actions."""
def __init__(self, bot: Red):
super().__init__()
@ -19,23 +21,28 @@ class ModLog(commands.Cog):
@commands.group()
@checks.guildowner_or_permissions(administrator=True)
async def modlogset(self, ctx: commands.Context):
"""Settings for the mod log"""
"""Manage modlog settings."""
pass
@modlogset.command()
@commands.guild_only()
async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None):
"""Sets a channel as mod log
"""Set a channel as the modlog.
Leaving the channel parameter empty will deactivate it"""
Omit `<channel>` to disable the modlog.
"""
guild = ctx.guild
if channel:
if channel.permissions_for(guild.me).send_messages:
await modlog.set_modlog_channel(guild, channel)
await ctx.send(_("Mod events will be sent to {}").format(channel.mention))
await ctx.send(
_("Mod events will be sent to {channel}").format(channel=channel.mention)
)
else:
await ctx.send(
_("I do not have permissions to send messages in {}!").format(channel.mention)
_("I do not have permissions to send messages in {channel}!").format(
channel=channel.mention
)
)
else:
try:
@ -49,39 +56,36 @@ class ModLog(commands.Cog):
@modlogset.command(name="cases")
@commands.guild_only()
async def set_cases(self, ctx: commands.Context, action: str = None):
"""Enables or disables case creation for each type of mod action"""
"""Enable or disable case creation for a mod action."""
guild = ctx.guild
if action is None: # No args given
casetypes = await modlog.get_all_casetypes(guild)
await ctx.send_help()
title = _("Current settings:")
msg = ""
lines = []
for ct in casetypes:
enabled = await ct.is_enabled()
value = "enabled" if enabled else "disabled"
msg += "%s : %s\n" % (ct.name, value)
enabled = "enabled" if await ct.is_enabled() else "disabled"
lines.append(f"{ct.name} : {enabled}")
msg = title + "\n" + box(msg)
await ctx.send(msg)
await ctx.send(_("Current settings:\n") + box("\n".join(lines)))
return
casetype = await modlog.get_casetype(action, guild)
if not casetype:
await ctx.send(_("That action is not registered"))
else:
enabled = await casetype.is_enabled()
await casetype.set_enabled(True if not enabled else False)
msg = _("Case creation for {} actions is now {}.").format(
action, "enabled" if not enabled else "disabled"
await casetype.set_enabled(not enabled)
await ctx.send(
_("Case creation for {action_name} actions is now {enabled}.").format(
action_name=action, enabled="enabled" if not enabled else "disabled"
)
)
await ctx.send(msg)
@modlogset.command()
@commands.guild_only()
async def resetcases(self, ctx: commands.Context):
"""Resets modlog's cases"""
"""Reset all modlog cases in this server."""
guild = ctx.guild
await modlog.reset_cases(guild)
await ctx.send(_("Cases have been reset."))
@ -89,7 +93,7 @@ class ModLog(commands.Cog):
@commands.command()
@commands.guild_only()
async def case(self, ctx: commands.Context, number: int):
"""Shows the specified case"""
"""Show the specified case."""
try:
case = await modlog.get_case(number, ctx.guild, self.bot)
except RuntimeError:
@ -101,24 +105,21 @@ class ModLog(commands.Cog):
else:
await ctx.send(await case.message_content(embed=False))
@commands.command(usage="[case] <reason>")
@commands.command()
@commands.guild_only()
async def reason(self, ctx: commands.Context, *, reason: str):
"""Lets you specify a reason for mod-log's cases
async def reason(self, ctx: commands.Context, case: Optional[int], *, reason: str):
"""Specify a reason for a modlog case.
Please note that you can only edit cases you are
the owner of unless you are a mod/admin or the server owner.
If no number is specified, the latest case will be used."""
the owner of unless you are a mod, admin or server owner.
If no case number is specified, the latest case will be used.
"""
author = ctx.author
guild = ctx.guild
potential_case = reason.split()[0]
if potential_case.isdigit():
case = int(potential_case)
reason = reason.replace(potential_case, "")
else:
case = str(int(await modlog.get_next_case_number(guild)) - 1)
# latest case
if case is None:
# get the latest case
case = int(await modlog.get_next_case_number(guild)) - 1
try:
case_before = await modlog.get_case(case, guild, self.bot)
except RuntimeError:

View File

@ -1,5 +1,9 @@
from typing import NamedTuple, Union, Optional
from typing import NamedTuple, Union, Optional, cast, Type
from redbot.core import commands
from redbot.core.i18n import Translator
_ = Translator("PermissionsConverters", __file__)
class CogOrCommand(NamedTuple):
@ -18,39 +22,34 @@ class CogOrCommand(NamedTuple):
return cls(type="COMMAND", name=cmd.qualified_name, obj=cmd)
raise commands.BadArgument(
'Cog or command "{arg}" not found. Please note that this is case sensitive.'
"".format(arg=arg)
_(
'Cog or command "{name}" not found. Please note that this is case sensitive.'
).format(name=arg)
)
class RuleType:
def RuleType(arg: str) -> bool:
if arg.lower() in ("allow", "whitelist", "allowed"):
return True
if arg.lower() in ("deny", "blacklist", "denied"):
return False
# noinspection PyUnusedLocal
@classmethod
async def convert(cls, ctx: commands.Context, arg: str) -> bool:
if arg.lower() in ("allow", "whitelist", "allowed"):
return True
if arg.lower() in ("deny", "blacklist", "denied"):
return False
raise commands.BadArgument(
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny"'.format(arg=arg)
)
raise commands.BadArgument(
_('"{arg}" is not a valid rule. Valid rules are "allow" or "deny"').format(arg=arg)
)
class ClearableRuleType:
def ClearableRuleType(arg: str) -> Optional[bool]:
if arg.lower() in ("allow", "whitelist", "allowed"):
return True
if arg.lower() in ("deny", "blacklist", "denied"):
return False
if arg.lower() in ("clear", "reset"):
return None
# noinspection PyUnusedLocal
@classmethod
async def convert(cls, ctx: commands.Context, arg: str) -> Optional[bool]:
if arg.lower() in ("allow", "whitelist", "allowed"):
return True
if arg.lower() in ("deny", "blacklist", "denied"):
return False
if arg.lower() in ("clear", "reset"):
return None
raise commands.BadArgument(
raise commands.BadArgument(
_(
'"{arg}" is not a valid rule. Valid rules are "allow" or "deny", or "clear" to '
"remove the rule".format(arg=arg)
)
"remove the rule"
).format(arg=arg)
)

View File

@ -2,7 +2,7 @@ import asyncio
import io
import textwrap
from copy import copy
from typing import Union, Optional, Dict, List, Tuple, Any, Iterator, ItemsView
from typing import Union, Optional, Dict, List, Tuple, Any, Iterator, ItemsView, cast
import discord
import yaml
@ -287,9 +287,11 @@ class Permissions(commands.Cog):
`<who_or_what>` is the user, channel, role or server the rule
is for.
"""
# noinspection PyTypeChecker
await self._add_rule(
rule=allow_or_deny, cog_or_cmd=cog_or_command, model_id=who_or_what.id, guild_id=0
rule=cast(bool, allow_or_deny),
cog_or_cmd=cog_or_command,
model_id=who_or_what.id,
guild_id=0,
)
await ctx.send(_("Rule added."))
@ -312,9 +314,8 @@ class Permissions(commands.Cog):
`<who_or_what>` is the user, channel or role the rule is for.
"""
# noinspection PyTypeChecker
await self._add_rule(
rule=allow_or_deny,
rule=cast(bool, allow_or_deny),
cog_or_cmd=cog_or_command,
model_id=who_or_what.id,
guild_id=ctx.guild.id,
@ -381,9 +382,10 @@ class Permissions(commands.Cog):
`<cog_or_command>` is the cog or command to set the default
rule for. This is case sensitive.
"""
# noinspection PyTypeChecker
await self._set_default_rule(
rule=allow_or_deny, cog_or_cmd=cog_or_command, guild_id=ctx.guild.id
rule=cast(Optional[bool], allow_or_deny),
cog_or_cmd=cog_or_command,
guild_id=ctx.guild.id,
)
await ctx.send(_("Default set."))
@ -403,9 +405,8 @@ class Permissions(commands.Cog):
`<cog_or_command>` is the cog or command to set the default
rule for. This is case sensitive.
"""
# noinspection PyTypeChecker
await self._set_default_rule(
rule=allow_or_deny, cog_or_cmd=cog_or_command, guild_id=GLOBAL
rule=cast(Optional[bool], allow_or_deny), cog_or_cmd=cog_or_command, guild_id=GLOBAL
)
await ctx.send(_("Default set."))

View File

@ -1,6 +1,6 @@
import logging
import asyncio
from typing import Union
from typing import Union, List
from datetime import timedelta
from copy import copy
import contextlib
@ -60,23 +60,20 @@ class Reports(commands.Cog):
@commands.guild_only()
@commands.group(name="reportset")
async def reportset(self, ctx: commands.Context):
"""
Settings for the report system.
"""
"""Manage Reports."""
pass
@checks.admin_or_permissions(manage_guild=True)
@reportset.command(name="output")
async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel):
"""Set the channel where reports will show up"""
async def reportset_output(self, ctx: commands.Context, channel: discord.TextChannel):
"""Set the channel where reports will be sent."""
await self.config.guild(ctx.guild).output_channel.set(channel.id)
await ctx.send(_("The report channel has been set."))
@checks.admin_or_permissions(manage_guild=True)
@reportset.command(name="toggle", aliases=["toggleactive"])
async def report_toggle(self, ctx: commands.Context):
"""Enables or Disables reporting for the server"""
async def reportset_toggle(self, ctx: commands.Context):
"""Enable or Disable reporting for this server."""
active = await self.config.guild(ctx.guild).active()
active = not active
await self.config.guild(ctx.guild).active.set(active)
@ -168,7 +165,7 @@ class Reports(commands.Cog):
if channel is None:
return None
files = await Tunnel.files_from_attatch(msg)
files: List[discord.File] = await Tunnel.files_from_attatch(msg)
ticket_number = await self.config.guild(guild).next_ticket()
await self.config.guild(guild).next_ticket.set(ticket_number + 1)
@ -204,11 +201,10 @@ class Reports(commands.Cog):
@commands.group(name="report", invoke_without_command=True)
async def report(self, ctx: commands.Context, *, _report: str = ""):
"""
Send a report.
"""Send a report.
Use without arguments for interactive reporting, or do
[p]report <text> to use it non-interactively.
`[p]report <text>` to use it non-interactively.
"""
author = ctx.author
guild = ctx.guild
@ -323,9 +319,8 @@ class Reports(commands.Cog):
@checks.mod_or_permissions(manage_members=True)
@report.command(name="interact")
async def response(self, ctx, ticket_number: int):
"""
Open a message tunnel.
"""Open a message tunnel.
This tunnel will forward things you say in this channel
to the ticket opener's direct messages.
@ -354,8 +349,7 @@ class Reports(commands.Cog):
)
big_topic = _(
"{who} opened a 2-way communication "
"about ticket number {ticketnum}. Anything you say or upload here "
" Anything you say or upload here "
"(8MB file size limitation on uploads) "
"will be forwarded to them until the communication is closed.\n"
"You can close a communication at any point by reacting with "
@ -364,8 +358,12 @@ class Reports(commands.Cog):
"\N{WHITE HEAVY CHECK MARK}.\n"
"Tunnels are not persistent across bot restarts."
)
topic = big_topic.format(
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
topic = (
_(
"A moderator in the server `{guild.name}` has opened a 2-way communication about "
"ticket number {ticket_number}."
).format(guild=guild, ticket_number=ticket_number)
+ big_topic
)
try:
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
@ -373,4 +371,9 @@ class Reports(commands.Cog):
await ctx.send(_("That user has DMs disabled."))
else:
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m}
await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))
await ctx.send(
_(
"You have opened a 2-way communication about ticket number {ticket_number}."
).format(ticket_number=ticket_number)
+ big_topic
)

View File

@ -1,3 +1,5 @@
import contextlib
import discord
from redbot.core import Config, checks, commands
from redbot.core.utils.chat_formatting import pagify
@ -22,7 +24,7 @@ from .errors import (
StreamsError,
InvalidTwitchCredentials,
)
from . import streamtypes as StreamClasses
from . import streamtypes as _streamtypes
from collections import defaultdict
import asyncio
import re
@ -76,14 +78,14 @@ class Streams(commands.Cog):
@commands.command()
async def twitch(self, ctx: commands.Context, channel_name: str):
"""Checks if a Twitch channel is live"""
"""Check if a Twitch channel is live."""
token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
stream = TwitchStream(name=channel_name, token=token)
await self.check_online(ctx, stream)
@commands.command()
async def youtube(self, ctx: commands.Context, channel_id_or_name: str):
"""Checks if a Youtube channel is live"""
"""Check if a YouTube channel is live."""
apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None)
is_name = self.check_name_or_id(channel_id_or_name)
if is_name:
@ -94,23 +96,24 @@ class Streams(commands.Cog):
@commands.command()
async def hitbox(self, ctx: commands.Context, channel_name: str):
"""Checks if a Hitbox channel is live"""
"""Check if a Hitbox channel is live."""
stream = HitboxStream(name=channel_name)
await self.check_online(ctx, stream)
@commands.command()
async def mixer(self, ctx: commands.Context, channel_name: str):
"""Checks if a Mixer channel is live"""
"""Check if a Mixer channel is live."""
stream = MixerStream(name=channel_name)
await self.check_online(ctx, stream)
@commands.command()
async def picarto(self, ctx: commands.Context, channel_name: str):
"""Checks if a Picarto channel is live"""
"""Check if a Picarto channel is live."""
stream = PicartoStream(name=channel_name)
await self.check_online(ctx, stream)
async def check_online(self, ctx: commands.Context, stream):
@staticmethod
async def check_online(ctx: commands.Context, stream):
try:
embed = await stream.is_online()
except OfflineStream:
@ -119,15 +122,17 @@ class Streams(commands.Cog):
await ctx.send(_("That channel doesn't seem to exist."))
except InvalidTwitchCredentials:
await ctx.send(
_("The twitch token is either invalid or has not been set. See `{}`.").format(
"{}streamset twitchtoken".format(ctx.prefix)
)
_(
"The Twitch token is either invalid or has not been set. See "
"`{prefix}streamset twitchtoken`."
).format(prefix=ctx.prefix)
)
except InvalidYoutubeCredentials:
await ctx.send(
_("Your Youtube API key is either invalid or has not been set. See {}.").format(
"`{}streamset youtubekey`".format(ctx.prefix)
)
_(
"The YouTube API key is either invalid or has not been set. See "
"`{prefix}streamset youtubekey`."
).format(prefix=ctx.prefix)
)
except APIError:
await ctx.send(
@ -140,11 +145,12 @@ class Streams(commands.Cog):
@commands.guild_only()
@checks.mod()
async def streamalert(self, ctx: commands.Context):
"""Manage automated stream alerts."""
pass
@streamalert.group(name="twitch", invoke_without_command=True)
async def _twitch(self, ctx: commands.Context, channel_name: str = None):
"""Twitch stream alerts"""
"""Manage Twitch stream notifications."""
if channel_name is not None:
await ctx.invoke(self.twitch_alert_channel, channel_name)
else:
@ -152,7 +158,7 @@ class Streams(commands.Cog):
@_twitch.command(name="channel")
async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str):
"""Sets a Twitch alert notification in the channel"""
"""Toggle alerts in this channel for a Twitch stream."""
if re.fullmatch(r"<#\d+>", channel_name):
await ctx.send("Please supply the name of a *Twitch* channel, not a Discord channel.")
return
@ -160,33 +166,39 @@ class Streams(commands.Cog):
@_twitch.command(name="community")
async def twitch_alert_community(self, ctx: commands.Context, community: str):
"""Sets an alert notification in the channel for the specified twitch community."""
"""Toggle alerts in this channel for a Twitch community."""
await self.community_alert(ctx, TwitchCommunity, community.lower())
@streamalert.command(name="youtube")
async def youtube_alert(self, ctx: commands.Context, channel_name_or_id: str):
"""Sets a Youtube alert notification in the channel"""
"""Toggle alerts in this channel for a YouTube stream."""
await self.stream_alert(ctx, YoutubeStream, channel_name_or_id)
@streamalert.command(name="hitbox")
async def hitbox_alert(self, ctx: commands.Context, channel_name: str):
"""Sets a Hitbox alert notification in the channel"""
"""Toggle alerts in this channel for a Hitbox stream."""
await self.stream_alert(ctx, HitboxStream, channel_name)
@streamalert.command(name="mixer")
async def mixer_alert(self, ctx: commands.Context, channel_name: str):
"""Sets a Mixer alert notification in the channel"""
"""Toggle alerts in this channel for a Mixer stream."""
await self.stream_alert(ctx, MixerStream, channel_name)
@streamalert.command(name="picarto")
async def picarto_alert(self, ctx: commands.Context, channel_name: str):
"""Sets a Picarto alert notification in the channel"""
"""Toggle alerts in this channel for a Picarto stream."""
await self.stream_alert(ctx, PicartoStream, channel_name)
@streamalert.command(name="stop")
@streamalert.command(name="stop", usage="[disable_all=No]")
async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
"""Stops all stream notifications in the channel
Adding 'yes' will disable all notifications in the server"""
"""Disable all stream alerts in this channel or server.
`[p]streamalert stop` will disable this channel's stream
alerts.
Do `[p]streamalert stop yes` to disable all stream alerts in
this server.
"""
streams = self.streams.copy()
local_channel_ids = [c.id for c in ctx.guild.channels]
to_remove = []
@ -208,9 +220,10 @@ class Streams(commands.Cog):
self.streams = streams
await self.save_streams()
msg = _("All the alerts in the {} have been disabled.").format(
"server" if _all else "channel"
)
if _all:
msg = _("All the stream alerts in this server have been disabled.")
else:
msg = _("All the stream alerts in this channel have been disabled.")
await ctx.send(msg)
@ -250,16 +263,18 @@ class Streams(commands.Cog):
exists = await self.check_exists(stream)
except InvalidTwitchCredentials:
await ctx.send(
_("Your twitch token is either invalid or has not been set. See {}.").format(
"`{}streamset twitchtoken`".format(ctx.prefix)
)
_(
"The Twitch token is either invalid or has not been set. See "
"`{prefix}streamset twitchtoken`."
).format(prefix=ctx.prefix)
)
return
except InvalidYoutubeCredentials:
await ctx.send(
_(
"Your Youtube API key is either invalid or has not been set. See {}."
).format("`{}streamset youtubekey`".format(ctx.prefix))
"The YouTube API key is either invalid or has not been set. See "
"`{prefix}streamset youtubekey`."
).format(prefix=ctx.prefix)
)
return
except APIError:
@ -283,9 +298,10 @@ class Streams(commands.Cog):
await community.get_community_streams()
except InvalidTwitchCredentials:
await ctx.send(
_("The twitch token is either invalid or has not been set. See {}.").format(
"`{}streamset twitchtoken`".format(ctx.prefix)
)
_(
"The Twitch token is either invalid or has not been set. See "
"`{prefix}streamset twitchtoken`."
).format(prefix=ctx.prefix)
)
return
except CommunityNotFound:
@ -309,14 +325,15 @@ class Streams(commands.Cog):
@streamset.command()
@checks.is_owner()
async def twitchtoken(self, ctx: commands.Context, token: str):
"""Set the Client ID for twitch.
"""Set the Client ID for Twitch.
To do this, follow these steps:
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
2. Click *Register Your Application*
3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and
select an Application Category of your choosing.
4. Click *Register*, and on the following page, copy the Client ID.
5. Paste the Client ID into this command. Done!
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
2. Click *Register Your Application*
3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and
select an Application Category of your choosing.
4. Click *Register*, and on the following page, copy the Client ID.
5. Paste the Client ID into this command. Done!
"""
await self.db.tokens.set_raw("TwitchStream", value=token)
await self.db.tokens.set_raw("TwitchCommunity", value=token)
@ -325,64 +342,59 @@ class Streams(commands.Cog):
@streamset.command()
@checks.is_owner()
async def youtubekey(self, ctx: commands.Context, key: str):
"""Sets the API key for Youtube.
"""Set the API key for YouTube.
To get one, do the following:
1. Create a project (see https://support.google.com/googleapi/answer/6251787 for details)
2. Enable the Youtube Data API v3 (see https://support.google.com/googleapi/answer/6158841 for instructions)
3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for instructions)
2. Enable the YouTube Data API v3 (see https://support.google.com/googleapi/answer/6158841
for instructions)
3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for
instructions)
4. Copy your API key and paste it into this command. Done!
"""
await self.db.tokens.set_raw("YoutubeStream", value=key)
await ctx.send(_("Youtube key set."))
await ctx.send(_("YouTube key set."))
@streamset.group()
@commands.guild_only()
async def mention(self, ctx: commands.Context):
"""Sets mentions for alerts."""
"""Manage mention settings for stream alerts."""
pass
@mention.command(aliases=["everyone"])
@commands.guild_only()
async def all(self, ctx: commands.Context):
"""Toggles everyone mention"""
"""Toggle the `@\u200beveryone` mention."""
guild = ctx.guild
current_setting = await self.db.guild(guild).mention_everyone()
if current_setting:
await self.db.guild(guild).mention_everyone.set(False)
await ctx.send(
_("{} will no longer be mentioned when a stream or community is live").format(
"@\u200beveryone"
)
)
await ctx.send(_("`@\u200beveryone` will no longer be mentioned for stream alerts."))
else:
await self.db.guild(guild).mention_everyone.set(True)
await ctx.send(
_("When a stream or community " "is live, {} will be mentioned.").format(
"@\u200beveryone"
)
_("When a stream or community is live, `@\u200beveryone` will be mentioned.")
)
@mention.command(aliases=["here"])
@commands.guild_only()
async def online(self, ctx: commands.Context):
"""Toggles here mention"""
"""Toggle the `@\u200bhere` mention."""
guild = ctx.guild
current_setting = await self.db.guild(guild).mention_here()
if current_setting:
await self.db.guild(guild).mention_here.set(False)
await ctx.send(_("{} will no longer be mentioned for an alert.").format("@\u200bhere"))
await ctx.send(_("`@\u200bhere` will no longer be mentioned for stream alerts."))
else:
await self.db.guild(guild).mention_here.set(True)
await ctx.send(
_("When a stream or community " "is live, {} will be mentioned.").format(
"@\u200bhere"
)
_("When a stream or community is live, `@\u200bhere` will be mentioned.")
)
@mention.command()
@commands.guild_only()
async def role(self, ctx: commands.Context, *, role: discord.Role):
"""Toggles role mention"""
"""Toggle a role mention."""
current_setting = await self.db.role(role).mention()
if not role.mentionable:
await ctx.send("That role is not mentionable!")
@ -390,27 +402,27 @@ class Streams(commands.Cog):
if current_setting:
await self.db.role(role).mention.set(False)
await ctx.send(
_("{} will no longer be mentioned for an alert.").format(
"@\u200b{}".format(role.name)
_("`@\u200b{role.name}` will no longer be mentioned for stream alerts.").format(
role=role
)
)
else:
await self.db.role(role).mention.set(True)
await ctx.send(
_("When a stream or community " "is live, {} will be mentioned." "").format(
"@\u200b{}".format(role.name)
)
_(
"When a stream or community is live, `@\u200b{role.name}` will be mentioned."
).format(role=role)
)
@streamset.command()
@commands.guild_only()
async def autodelete(self, ctx: commands.Context, on_off: bool):
"""Toggles automatic deletion of notifications for streams that go offline"""
"""Toggle alert deletion for when streams go offline."""
await self.db.guild(ctx.guild).autodelete.set(on_off)
if on_off:
await ctx.send("The notifications will be deleted once streams go offline.")
await ctx.send(_("The notifications will be deleted once streams go offline."))
else:
await ctx.send("Notifications will never be deleted.")
await ctx.send(_("Notifications will no longer be deleted."))
async def add_or_remove(self, ctx: commands.Context, stream):
if ctx.channel.id not in stream.channels:
@ -418,18 +430,18 @@ class Streams(commands.Cog):
if stream not in self.streams:
self.streams.append(stream)
await ctx.send(
_("I'll now send a notification in this channel when {} is live.").format(
stream.name
)
_(
"I'll now send a notification in this channel when {stream.name} is live."
).format(stream=stream)
)
else:
stream.channels.remove(ctx.channel.id)
if not stream.channels:
self.streams.remove(stream)
await ctx.send(
_("I won't send notifications about {} in this channel anymore.").format(
stream.name
)
_(
"I won't send notifications about {stream.name} in this channel anymore."
).format(stream=stream)
)
await self.save_streams()
@ -442,9 +454,8 @@ class Streams(commands.Cog):
await ctx.send(
_(
"I'll send a notification in this channel when a "
"channel is live in the {} community."
""
).format(community.name)
"channel is live in the {community.name} community."
).format(community=community)
)
else:
community.channels.remove(ctx.channel.id)
@ -453,9 +464,8 @@ class Streams(commands.Cog):
await ctx.send(
_(
"I won't send notifications about channels streaming "
"in the {} community in this channel anymore."
""
).format(community.name)
"in the {community.name} community in this channel anymore."
).format(community=community)
)
await self.save_communities()
@ -481,7 +491,8 @@ class Streams(commands.Cog):
if community.type == _class.__name__ and community.name.lower() == name.lower():
return community
async def check_exists(self, stream):
@staticmethod
async def check_exists(stream):
try:
await stream.is_online()
except OfflineStream:
@ -506,40 +517,36 @@ class Streams(commands.Cog):
async def check_streams(self):
for stream in self.streams:
try:
embed = await stream.is_online()
except OfflineStream:
if not stream._messages_cache:
continue
for message in stream._messages_cache:
try:
autodelete = await self.db.guild(message.guild).autodelete()
if autodelete:
await message.delete()
except:
pass
stream._messages_cache.clear()
await self.save_streams()
except:
pass
else:
if stream._messages_cache:
continue
for channel_id in stream.channels:
channel = self.bot.get_channel(channel_id)
mention_str = await self._get_mention_str(channel.guild)
with contextlib.suppress(Exception):
try:
embed = await stream.is_online()
except OfflineStream:
if not stream._messages_cache:
continue
for message in stream._messages_cache:
with contextlib.suppress(Exception):
autodelete = await self.db.guild(message.guild).autodelete()
if autodelete:
await message.delete()
stream._messages_cache.clear()
await self.save_streams()
else:
if stream._messages_cache:
continue
for channel_id in stream.channels:
channel = self.bot.get_channel(channel_id)
mention_str = await self._get_mention_str(channel.guild)
if mention_str:
content = "{}, {} is live!".format(mention_str, stream.name)
else:
content = "{} is live!".format(stream.name)
if mention_str:
content = _("{mention}, {stream.name} is live!").format(
mention=mention_str, stream=stream
)
else:
content = _("{stream.name} is live!").format(stream=stream.name)
try:
m = await channel.send(content, embed=embed)
stream._messages_cache.append(m)
await self.save_streams()
except:
pass
async def _get_mention_str(self, guild: discord.Guild):
settings = self.db.guild(guild)
@ -555,45 +562,46 @@ class Streams(commands.Cog):
async def check_communities(self):
for community in self.communities:
try:
stream_list = await community.get_community_streams()
except CommunityNotFound:
print(_("The Community {} was not found!").format(community.name))
continue
except OfflineCommunity:
if not community._messages_cache:
with contextlib.suppress(Exception):
try:
stream_list = await community.get_community_streams()
except CommunityNotFound:
print(
_("The Community {community.name} was not found!").format(
community=community
)
)
continue
for message in community._messages_cache:
try:
autodelete = await self.db.guild(message.guild).autodelete()
if autodelete:
await message.delete()
except:
pass
community._messages_cache.clear()
await self.save_communities()
except:
pass
else:
for channel in community.channels:
chn = self.bot.get_channel(channel)
streams = await self.filter_streams(stream_list, chn)
emb = await community.make_embed(streams)
chn_msg = [m for m in community._messages_cache if m.channel == chn]
if not chn_msg:
mentions = await self._get_mention_str(chn.guild)
if mentions:
msg = await chn.send(mentions, embed=emb)
except OfflineCommunity:
if not community._messages_cache:
continue
for message in community._messages_cache:
with contextlib.suppress(Exception):
autodelete = await self.db.guild(message.guild).autodelete()
if autodelete:
await message.delete()
community._messages_cache.clear()
await self.save_communities()
else:
for channel in community.channels:
chn = self.bot.get_channel(channel)
streams = await self.filter_streams(stream_list, chn)
emb = await community.make_embed(streams)
chn_msg = [m for m in community._messages_cache if m.channel == chn]
if not chn_msg:
mentions = await self._get_mention_str(chn.guild)
if mentions:
msg = await chn.send(mentions, embed=emb)
else:
msg = await chn.send(embed=emb)
community._messages_cache.append(msg)
await self.save_communities()
else:
msg = await chn.send(embed=emb)
community._messages_cache.append(msg)
await self.save_communities()
else:
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
community._messages_cache.remove(chn_msg)
await chn_msg.edit(embed=emb)
community._messages_cache.append(chn_msg)
await self.save_communities()
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
community._messages_cache.remove(chn_msg)
await chn_msg.edit(embed=emb)
community._messages_cache.append(chn_msg)
await self.save_communities()
async def filter_streams(self, streams: list, channel: discord.TextChannel) -> list:
filtered = []
@ -611,7 +619,7 @@ class Streams(commands.Cog):
streams = []
for raw_stream in await self.db.streams():
_class = getattr(StreamClasses, raw_stream["type"], None)
_class = getattr(_streamtypes, raw_stream["type"], None)
if not _class:
continue
raw_msg_cache = raw_stream["messages"]
@ -631,7 +639,7 @@ class Streams(commands.Cog):
communities = []
for raw_community in await self.db.communities():
_class = getattr(StreamClasses, raw_community["type"], None)
_class = getattr(_streamtypes, raw_community["type"], None)
if not _class:
continue
raw_msg_cache = raw_community["messages"]

View File

@ -4,20 +4,30 @@ import time
import random
from collections import Counter
import discord
from redbot.core.bank import deposit_credits
from redbot.core.utils.chat_formatting import box
from redbot.core import bank
from redbot.core.i18n import Translator
from redbot.core.utils.chat_formatting import box, bold, humanize_list
from redbot.core.utils.common_filters import normalize_smartquotes
from .log import LOG
__all__ = ["TriviaSession"]
_REVEAL_MESSAGES = ("I know this one! {}!", "Easy: {}.", "Oh really? It's {} of course.")
_FAIL_MESSAGES = (
"To the next one I guess...",
"Moving on...",
"I'm sure you'll know the answer of the next one.",
"\N{PENSIVE FACE} Next one.",
T_ = Translator("TriviaSession", __file__)
_ = lambda s: s
_REVEAL_MESSAGES = (
_("I know this one! {answer}!"),
_("Easy: {answer}."),
_("Oh really? It's {answer} of course."),
)
_FAIL_MESSAGES = (
_("To the next one I guess..."),
_("Moving on..."),
_("I'm sure you'll know the answer of the next one."),
_("\N{PENSIVE FACE} Next one."),
)
_ = T_
class TriviaSession:
@ -104,7 +114,7 @@ class TriviaSession:
async with self.ctx.typing():
await asyncio.sleep(3)
self.count += 1
msg = "**Question number {}!**\n\n{}".format(self.count, question)
msg = bold(_("**Question number {num}!").format(num=self.count)) + "\n\n" + question
await self.ctx.send(msg)
continue_ = await self.wait_for_answer(answers, delay, timeout)
if continue_ is False:
@ -113,7 +123,7 @@ class TriviaSession:
await self.end_game()
break
else:
await self.ctx.send("There are no more questions!")
await self.ctx.send(_("There are no more questions!"))
await self.end_game()
async def _send_startup_msg(self):
@ -121,20 +131,13 @@ class TriviaSession:
for idx, tup in enumerate(self.settings["lists"].items()):
name, author = tup
if author:
title = "{} (by {})".format(name, author)
title = _("{trivia_list} (by {author})").format(trivia_list=name, author=author)
else:
title = name
list_names.append(title)
num_lists = len(list_names)
if num_lists > 2:
# at least 3 lists, join all but last with comma
msg = ", ".join(list_names[: num_lists - 1])
# join onto last with "and"
msg = " and ".join((msg, list_names[num_lists - 1]))
else:
# either 1 or 2 lists, join together with "and"
msg = " and ".join(list_names)
await self.ctx.send("Starting Trivia: " + msg)
await self.ctx.send(
_("Starting Trivia: {list_names}").format(list_names=humanize_list(list_names))
)
def _iter_questions(self):
"""Iterate over questions and answers for this session.
@ -179,20 +182,20 @@ class TriviaSession:
)
except asyncio.TimeoutError:
if time.time() - self._last_response >= timeout:
await self.ctx.send("Guys...? Well, I guess I'll stop then.")
await self.ctx.send(_("Guys...? Well, I guess I'll stop then."))
self.stop()
return False
if self.settings["reveal_answer"]:
reply = random.choice(_REVEAL_MESSAGES).format(answers[0])
reply = T_(random.choice(_REVEAL_MESSAGES)).format(answer=answers[0])
else:
reply = random.choice(_FAIL_MESSAGES)
reply = T_(random.choice(_FAIL_MESSAGES))
if self.settings["bot_plays"]:
reply += " **+1** for me!"
reply += _(" **+1** for me!")
self.scores[self.ctx.guild.me] += 1
await self.ctx.send(reply)
else:
self.scores[message.author] += 1
reply = "You got it {}! **+1** to you!".format(message.author.display_name)
reply = _("You got it {user}! **+1** to you!").format(user=message.author.display_name)
await self.ctx.send(reply)
return True
@ -282,10 +285,16 @@ class TriviaSession:
amount = int(multiplier * score)
if amount > 0:
LOG.debug("Paying trivia winner: %d credits --> %s", amount, str(winner))
await deposit_credits(winner, int(multiplier * score))
await bank.deposit_credits(winner, int(multiplier * score))
await self.ctx.send(
"Congratulations, {0}, you have received {1} credits"
" for coming first.".format(winner.display_name, amount)
_(
"Congratulations, {user}, you have received {num} {currency}"
" for coming first."
).format(
user=winner.display_name,
num=amount,
currency=await bank.get_currency_name(self.ctx.guild),
)
)
@ -313,9 +322,9 @@ def _parse_answers(answers):
for answer in answers:
if isinstance(answer, bool):
if answer is True:
ret.extend(["True", "Yes"])
ret.extend(["True", "Yes", _("Yes")])
else:
ret.extend(["False", "No"])
ret.extend(["False", "No", _("No")])
else:
ret.append(str(answer))
# Uniquify list

View File

@ -7,7 +7,8 @@ import discord
from redbot.core import commands
from redbot.core import Config, checks
from redbot.core.data_manager import cog_data_path
from redbot.core.utils.chat_formatting import box, pagify
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box, pagify, bold
from redbot.cogs.bank import check_global_setting_admin
from .log import LOG
from .session import TriviaSession
@ -16,6 +17,8 @@ __all__ = ["Trivia", "UNIQUE_ID", "get_core_lists"]
UNIQUE_ID = 0xB3C0E453
_ = Translator("Trivia", __file__)
class InvalidListError(Exception):
"""A Trivia list file is in invalid format."""
@ -23,6 +26,7 @@ class InvalidListError(Exception):
pass
@cog_i18n(_)
class Trivia(commands.Cog):
"""Play trivia with friends!"""
@ -47,20 +51,21 @@ class Trivia(commands.Cog):
@commands.guild_only()
@checks.mod_or_permissions(administrator=True)
async def triviaset(self, ctx: commands.Context):
"""Manage trivia settings."""
"""Manage Trivia settings."""
if ctx.invoked_subcommand is None:
settings = self.conf.guild(ctx.guild)
settings_dict = await settings.all()
msg = box(
"**Current settings**\n"
"Bot gains points: {bot_plays}\n"
"Answer time limit: {delay} seconds\n"
"Lack of response timeout: {timeout} seconds\n"
"Points to win: {max_score}\n"
"Reveal answer on timeout: {reveal_answer}\n"
"Payout multiplier: {payout_multiplier}\n"
"Allow lists to override settings: {allow_override}"
"".format(**settings_dict),
_(
"**Current settings**\n"
"Bot gains points: {bot_plays}\n"
"Answer time limit: {delay} seconds\n"
"Lack of response timeout: {timeout} seconds\n"
"Points to win: {max_score}\n"
"Reveal answer on timeout: {reveal_answer}\n"
"Payout multiplier: {payout_multiplier}\n"
"Allow lists to override settings: {allow_override}"
).format(**settings_dict),
lang="py",
)
await ctx.send(msg)
@ -69,33 +74,34 @@ class Trivia(commands.Cog):
async def triviaset_max_score(self, ctx: commands.Context, score: int):
"""Set the total points required to win."""
if score < 0:
await ctx.send("Score must be greater than 0.")
await ctx.send(_("Score must be greater than 0."))
return
settings = self.conf.guild(ctx.guild)
await settings.max_score.set(score)
await ctx.send("Done. Points required to win set to {}.".format(score))
await ctx.send(_("Done. Points required to win set to {num}.").format(num=score))
@triviaset.command(name="timelimit")
async def triviaset_timelimit(self, ctx: commands.Context, seconds: float):
"""Set the maximum seconds permitted to answer a question."""
if seconds < 4.0:
await ctx.send("Must be at least 4 seconds.")
await ctx.send(_("Must be at least 4 seconds."))
return
settings = self.conf.guild(ctx.guild)
await settings.delay.set(seconds)
await ctx.send("Done. Maximum seconds to answer set to {}.".format(seconds))
await ctx.send(_("Done. Maximum seconds to answer set to {num}.").format(num=seconds))
@triviaset.command(name="stopafter")
async def triviaset_stopafter(self, ctx: commands.Context, seconds: float):
"""Set how long until trivia stops due to no response."""
settings = self.conf.guild(ctx.guild)
if seconds < await settings.delay():
await ctx.send("Must be larger than the answer time limit.")
await ctx.send(_("Must be larger than the answer time limit."))
return
await settings.timeout.set(seconds)
await ctx.send(
"Done. Trivia sessions will now time out after {}"
" seconds of no responses.".format(seconds)
_(
"Done. Trivia sessions will now time out after {num} seconds of no responses."
).format(num=seconds)
)
@triviaset.command(name="override")
@ -103,46 +109,46 @@ class Trivia(commands.Cog):
"""Allow/disallow trivia lists to override settings."""
settings = self.conf.guild(ctx.guild)
await settings.allow_override.set(enabled)
enabled = "now" if enabled else "no longer"
await ctx.send(
"Done. Trivia lists can {} override the trivia settings"
" for this server.".format(enabled)
)
if enabled:
await ctx.send(
_(
"Done. Trivia lists can now override the trivia settings for this server."
).format(now=enabled)
)
else:
await ctx.send(
_(
"Done. Trivia lists can no longer override the trivia settings for this "
"server."
).format(now=enabled)
)
@triviaset.command(name="botplays")
async def trivaset_bot_plays(self, ctx: commands.Context, true_or_false: bool):
@triviaset.command(name="botplays", usage="<true_or_false>")
async def trivaset_bot_plays(self, ctx: commands.Context, enabled: bool):
"""Set whether or not the bot gains points.
If enabled, the bot will gain a point if no one guesses correctly.
"""
settings = self.conf.guild(ctx.guild)
await settings.bot_plays.set(true_or_false)
await ctx.send(
"Done. "
+ (
"I'll gain a point if users don't answer in time."
if true_or_false
else "Alright, I won't embarass you at trivia anymore."
)
)
await settings.bot_plays.set(enabled)
if enabled:
await ctx.send(_("Done. I'll now gain a point if users don't answer in time."))
else:
await ctx.send(_("Alright, I won't embarass you at trivia anymore."))
@triviaset.command(name="revealanswer")
async def trivaset_reveal_answer(self, ctx: commands.Context, true_or_false: bool):
@triviaset.command(name="revealanswer", usage="<true_or_false>")
async def trivaset_reveal_answer(self, ctx: commands.Context, enabled: bool):
"""Set whether or not the answer is revealed.
If enabled, the bot will reveal the answer if no one guesses correctly
in time.
"""
settings = self.conf.guild(ctx.guild)
await settings.reveal_answer.set(true_or_false)
await ctx.send(
"Done. "
+ (
"I'll reveal the answer if no one knows it."
if true_or_false
else "I won't reveal the answer to the questions anymore."
)
)
await settings.reveal_answer.set(enabled)
if enabled:
await ctx.send(_("Done. I'll reveal the answer if no one knows it."))
else:
await ctx.send(_("Alright, I won't reveal the answer to the questions anymore."))
@triviaset.command(name="payout")
@check_global_setting_admin()
@ -158,13 +164,13 @@ class Trivia(commands.Cog):
"""
settings = self.conf.guild(ctx.guild)
if multiplier < 0:
await ctx.send("Multiplier must be at least 0.")
await ctx.send(_("Multiplier must be at least 0."))
return
await settings.payout_multiplier.set(multiplier)
if not multiplier:
await ctx.send("Done. I will no longer reward the winner with a payout.")
return
await ctx.send("Done. Payout multiplier set to {}.".format(multiplier))
if multiplier:
await ctx.send(_("Done. Payout multiplier set to {num}.").format(num=multiplier))
else:
await ctx.send(_("Done. I will no longer reward the winner with a payout."))
@commands.group(invoke_without_command=True)
@commands.guild_only()
@ -180,7 +186,7 @@ class Trivia(commands.Cog):
categories = [c.lower() for c in categories]
session = self._get_trivia_session(ctx.channel)
if session is not None:
await ctx.send("There is already an ongoing trivia session in this channel.")
await ctx.send(_("There is already an ongoing trivia session in this channel."))
return
trivia_dict = {}
authors = []
@ -191,15 +197,17 @@ class Trivia(commands.Cog):
dict_ = self.get_trivia_list(category)
except FileNotFoundError:
await ctx.send(
"Invalid category `{0}`. See `{1}trivia list`"
" for a list of trivia categories."
"".format(category, ctx.prefix)
_(
"Invalid category `{name}`. See `{prefix}trivia list` for a list of "
"trivia categories."
).format(name=category, prefix=ctx.prefix)
)
except InvalidListError:
await ctx.send(
"There was an error parsing the trivia list for"
" the `{}` category. It may be formatted"
" incorrectly.".format(category)
_(
"There was an error parsing the trivia list for the `{name}` category. It "
"may be formatted incorrectly."
).format(name=category)
)
else:
trivia_dict.update(dict_)
@ -208,7 +216,7 @@ class Trivia(commands.Cog):
return
if not trivia_dict:
await ctx.send(
"The trivia list was parsed successfully, however it appears to be empty!"
_("The trivia list was parsed successfully, however it appears to be empty!")
)
return
settings = await self.conf.guild(ctx.guild).all()
@ -225,7 +233,7 @@ class Trivia(commands.Cog):
"""Stop an ongoing trivia session."""
session = self._get_trivia_session(ctx.channel)
if session is None:
await ctx.send("There is no ongoing trivia session in this channel.")
await ctx.send(_("There is no ongoing trivia session in this channel."))
return
author = ctx.author
auth_checks = (
@ -238,20 +246,28 @@ class Trivia(commands.Cog):
if any(auth_checks):
await session.end_game()
session.force_stop()
await ctx.send("Trivia stopped.")
await ctx.send(_("Trivia stopped."))
else:
await ctx.send("You are not allowed to do that.")
await ctx.send(_("You are not allowed to do that."))
@trivia.command(name="list")
async def trivia_list(self, ctx: commands.Context):
"""List available trivia categories."""
lists = set(p.stem for p in self._all_lists())
msg = box("**Available trivia lists**\n\n{}".format(", ".join(sorted(lists))))
if len(msg) > 1000:
await ctx.author.send(msg)
return
await ctx.send(msg)
if await ctx.embed_requested():
await ctx.send(
embed=discord.Embed(
title=_("Available trivia lists"),
colour=await ctx.embed_colour(),
description=", ".join(sorted(lists)),
)
)
else:
msg = box(bold(_("Available trivia lists")) + "\n\n" + ", ".join(sorted(lists)))
if len(msg) > 1000:
await ctx.author.send(msg)
else:
await ctx.send(msg)
@trivia.group(name="leaderboard", aliases=["lboard"], autohelp=False)
async def trivia_leaderboard(self, ctx: commands.Context):
@ -273,19 +289,21 @@ class Trivia(commands.Cog):
):
"""Leaderboard for this server.
<sort_by> can be any of the following fields:
- wins : total wins
- avg : average score
- total : total correct answers
`<sort_by>` can be any of the following fields:
- `wins` : total wins
- `avg` : average score
- `total` : total correct answers
- `games` : total games played
<top> is the number of ranks to show on the leaderboard.
`<top>` is the number of ranks to show on the leaderboard.
"""
key = self._get_sort_key(sort_by)
if key is None:
await ctx.send(
"Unknown field `{}`, see `{}help trivia "
"leaderboard server` for valid fields to sort by."
"".format(sort_by, ctx.prefix)
_(
"Unknown field `{field_name}`, see `{prefix}help trivia leaderboard server` "
"for valid fields to sort by."
).format(field_name=sort_by, prefix=ctx.prefix)
)
return
guild = ctx.guild
@ -300,20 +318,21 @@ class Trivia(commands.Cog):
):
"""Global trivia leaderboard.
<sort_by> can be any of the following fields:
- wins : total wins
- avg : average score
- total : total correct answers from all sessions
- games : total games played
`<sort_by>` can be any of the following fields:
- `wins` : total wins
- `avg` : average score
- `total` : total correct answers from all sessions
- `games` : total games played
<top> is the number of ranks to show on the leaderboard.
`<top>` is the number of ranks to show on the leaderboard.
"""
key = self._get_sort_key(sort_by)
if key is None:
await ctx.send(
"Unknown field `{}`, see `{}help trivia "
"leaderboard global` for valid fields to sort by."
"".format(sort_by, ctx.prefix)
_(
"Unknown field `{field_name}`, see `{prefix}help trivia leaderboard server` "
"for valid fields to sort by."
).format(field_name=sort_by, prefix=ctx.prefix)
)
return
data = await self.conf.all_members()
@ -365,7 +384,7 @@ class Trivia(commands.Cog):
"""
if not data:
await ctx.send("There are no scores on record!")
await ctx.send(_("There are no scores on record!"))
return
leaderboard = self._get_leaderboard(data, key, top)
ret = []
@ -386,7 +405,7 @@ class Trivia(commands.Cog):
try:
priority.remove(key)
except ValueError:
raise ValueError("{} is not a valid key.".format(key))
raise ValueError(f"{key} is not a valid key.")
# Put key last in reverse priority
priority.append(key)
items = data.items()
@ -395,16 +414,15 @@ class Trivia(commands.Cog):
max_name_len = max(map(lambda m: len(str(m)), data.keys()))
# Headers
headers = (
"Rank",
"Member{}".format(" " * (max_name_len - 6)),
"Wins",
"Games Played",
"Total Score",
"Average Score",
_("Rank"),
_("Member") + " " * (max_name_len - 6),
_("Wins"),
_("Games Played"),
_("Total Score"),
_("Average Score"),
)
lines = [" | ".join(headers)]
lines = [" | ".join(headers), " | ".join(("-" * len(h) for h in headers))]
# Header underlines
lines.append(" | ".join(("-" * len(h) for h in headers)))
for rank, tup in enumerate(items, 1):
member, m_data = tup
# Align fields to header width

View File

@ -22,7 +22,7 @@ _ = Translator("Warnings", __file__)
@cog_i18n(_)
class Warnings(commands.Cog):
"""A warning system for Red"""
"""Warn misbehaving users and take automated actions."""
default_guild = {"actions": [], "reasons": {}, "allow_custom_reasons": False}
@ -48,31 +48,42 @@ class Warnings(commands.Cog):
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def warningset(self, ctx: commands.Context):
"""Warning settings"""
"""Manage settings for Warnings."""
pass
@warningset.command()
@commands.guild_only()
async def allowcustomreasons(self, ctx: commands.Context, allowed: bool):
"""Enable or Disable custom reasons for a warning"""
"""Enable or disable custom reasons for a warning."""
guild = ctx.guild
await self.config.guild(guild).allow_custom_reasons.set(allowed)
await ctx.send(
_("Custom reasons have been {}.").format(_("enabled") if allowed else _("disabled"))
)
if allowed:
await ctx.send(_("Custom reasons have been enabled."))
else:
await ctx.send(_("Custom reasons have been disabled."))
@commands.group()
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def warnaction(self, ctx: commands.Context):
"""Action management"""
"""Manage automated actions for Warnings.
Actions are essentially command macros. Any command can be run
when the action is initially triggered, and/or when the action
is lifted.
Actions must be given a name and a points threshold. When a
user is warned enough so that their points go over this
threshold, the action will be executed.
"""
pass
@warnaction.command(name="add")
@commands.guild_only()
async def action_add(self, ctx: commands.Context, name: str, points: int):
"""Create an action to be taken at a specified point count
Duplicate action names are not allowed
"""Create an automated action.
Duplicate action names are not allowed.
"""
guild = ctx.guild
@ -103,7 +114,7 @@ class Warnings(commands.Cog):
@warnaction.command(name="del")
@commands.guild_only()
async def action_del(self, ctx: commands.Context, action_name: str):
"""Delete the point count action with the specified name"""
"""Delete the action with the specified name."""
guild = ctx.guild
guild_settings = self.config.guild(guild)
async with guild_settings.actions() as registered_actions:
@ -116,23 +127,29 @@ class Warnings(commands.Cog):
registered_actions.remove(to_remove)
await ctx.tick()
else:
await ctx.send(_("No action named {} exists!").format(action_name))
await ctx.send(_("No action named {name} exists!").format(name=action_name))
@commands.group()
@commands.guild_only()
@checks.guildowner_or_permissions(administrator=True)
async def warnreason(self, ctx: commands.Context):
"""Add reasons for warnings"""
"""Manage warning reasons.
Reasons must be given a name, description and points value. The
name of the reason must be given when a user is warned.
"""
pass
@warnreason.command(name="add")
@warnreason.command(name="create", aliases=["add"])
@commands.guild_only()
async def reason_add(self, ctx: commands.Context, name: str, points: int, *, description: str):
"""Add a reason to be available for warnings"""
async def reason_create(
self, ctx: commands.Context, name: str, points: int, *, description: str
):
"""Create a warning reason."""
guild = ctx.guild
if name.lower() == "custom":
await ctx.send("That cannot be used as a reason name!")
await ctx.send(_("*Custom* cannot be used as a reason name!"))
return
to_add = {"points": points, "description": description}
completed = {name.lower(): to_add}
@ -142,12 +159,12 @@ class Warnings(commands.Cog):
async with guild_settings.reasons() as registered_reasons:
registered_reasons.update(completed)
await ctx.send(_("That reason has been registered."))
await ctx.send(_("The new reason has been registered."))
@warnreason.command(name="del")
@warnreason.command(name="del", aliases=["remove"])
@commands.guild_only()
async def reason_del(self, ctx: commands.Context, reason_name: str):
"""Delete the reason with the specified name"""
"""Delete a warning reason."""
guild = ctx.guild
guild_settings = self.config.guild(guild)
async with guild_settings.reasons() as registered_reasons:
@ -160,7 +177,7 @@ class Warnings(commands.Cog):
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
async def reasonlist(self, ctx: commands.Context):
"""List all configured reasons for warnings"""
"""List all configured reasons for Warnings."""
guild = ctx.guild
guild_settings = self.config.guild(guild)
msg_list = []
@ -174,9 +191,9 @@ class Warnings(commands.Cog):
msg_list.append(em)
else:
msg_list.append(
"Name: {}\nPoints: {}\nDescription: {}".format(
r, v["points"], v["description"]
)
_(
"Name: {reason_name}\nPoints: {points}\nDescription: {description}"
).format(reason_name=r, **v)
)
if msg_list:
await menu(ctx, msg_list, DEFAULT_CONTROLS)
@ -187,7 +204,7 @@ class Warnings(commands.Cog):
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
async def actionlist(self, ctx: commands.Context):
"""List the actions to be taken at specific point values"""
"""List all configured automated actions for Warnings."""
guild = ctx.guild
guild_settings = self.config.guild(guild)
msg_list = []
@ -201,10 +218,10 @@ class Warnings(commands.Cog):
msg_list.append(em)
else:
msg_list.append(
"Name: {}\nPoints: {}\nExceed command: {}\n"
"Drop command: {}".format(
r["action_name"], r["points"], r["exceed_command"], r["drop_command"]
)
_(
"Name: {action_name}\nPoints: {points}\n"
"Exceed command: {exceed_command}\nDrop command: {drop_command}"
).format(**r)
)
if msg_list:
await menu(ctx, msg_list, DEFAULT_CONTROLS)
@ -215,8 +232,10 @@ class Warnings(commands.Cog):
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
async def warn(self, ctx: commands.Context, user: discord.Member, reason: str):
"""Warn the user for the specified reason
Reason must be a registered reason, or "custom" if custom reasons are allowed
"""Warn the user for the specified reason.
`<reason>` must be a registered reason name, or *custom* if
custom reasons are enabled.
"""
if user == ctx.author:
await ctx.send(_("You cannot warn yourself."))
@ -226,9 +245,9 @@ class Warnings(commands.Cog):
if not custom_allowed:
await ctx.send(
_(
"Custom reasons are not allowed! Please see {} for "
"Custom reasons are not allowed! Please see `{prefix}reasonlist` for "
"a complete list of valid reasons."
).format("`{}reasonlist`".format(ctx.prefix))
).format(prefix=ctx.prefix)
)
return
reason_type = await self.custom_warning_reason(ctx)
@ -272,9 +291,7 @@ class Warnings(commands.Cog):
await warning_points_add_check(self.config, ctx, user, current_point_count)
try:
em = discord.Embed(
title=_("Warning from {mod_name}#{mod_discrim}").format(
mod_name=ctx.author.display_name, mod_discrim=ctx.author.discriminator
),
title=_("Warning from {user}").format(user=ctx.author),
description=reason_type["description"],
)
em.add_field(name=_("Points"), value=str(reason_type["points"]))
@ -286,19 +303,17 @@ class Warnings(commands.Cog):
)
except discord.HTTPException:
pass
await ctx.send(
_("User {user_name}#{user_discrim} has been warned.").format(
user_name=user.display_name, user_discrim=user.discriminator
)
)
await ctx.send(_("User {user} has been warned.").format(user=user))
@commands.command()
@commands.guild_only()
async def warnings(self, ctx: commands.Context, userid: int = None):
"""Show warnings for the specified user.
If userid is None, show warnings for the person running the command
"""List the warnings for the specified user.
Emit `<userid>` to see your own warnings.
Note that showing warnings for users other than yourself requires
appropriate permissions
appropriate permissions.
"""
if userid is None:
user = ctx.author
@ -326,18 +341,24 @@ class Warnings(commands.Cog):
)
if mod is None:
mod = await self.bot.get_user_info(user_warnings[key]["mod"])
msg += "{} point warning {} issued by {} for {}\n".format(
user_warnings[key]["points"], key, mod, user_warnings[key]["description"]
msg += _(
"{num_points} point warning {reason_name} issued by {user} for "
"{description}\n"
).format(
num_points=user_warnings[key]["points"],
reason_name=key,
user=mod,
description=user_warnings[key]["description"],
)
await ctx.send_interactive(
pagify(msg, shorten_by=58), box_lang="Warnings for {}".format(user)
pagify(msg, shorten_by=58), box_lang=_("Warnings for {user}").format(user=user)
)
@commands.command()
@commands.guild_only()
@checks.admin_or_permissions(ban_members=True)
async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str):
"""Removes the specified warning from the user specified"""
"""Remove a warning from a user."""
if user_id == ctx.author.id:
await ctx.send(_("You cannot remove warnings from yourself."))
return
@ -351,7 +372,7 @@ class Warnings(commands.Cog):
await warning_points_remove_check(self.config, ctx, member, current_point_count)
async with member_settings.warnings() as user_warnings:
if warn_id not in user_warnings.keys():
await ctx.send("That warning doesn't exist!")
await ctx.send(_("That warning doesn't exist!"))
return
else:
current_point_count -= user_warnings[warn_id]["points"]

View File

@ -1,6 +1,6 @@
import datetime
import os
from typing import Union, List
from typing import Union, List, Optional
import discord
@ -296,12 +296,20 @@ async def transfer_credits(from_: discord.Member, to: discord.Member, amount: in
return await deposit_credits(to, amount)
async def wipe_bank():
"""Delete all accounts from the bank."""
async def wipe_bank(guild: Optional[discord.Guild] = None) -> None:
"""Delete all accounts from the bank.
Parameters
----------
guild : discord.Guild
The guild to clear accounts for. If unsupplied and the bank is
per-server, all accounts in every guild will be wiped.
"""
if await is_global():
await _conf.clear_all_users()
else:
await _conf.clear_all_members()
await _conf.clear_all_members(guild)
async def get_leaderboard(positions: int = None, guild: discord.Guild = None) -> List[tuple]:

View File

@ -838,7 +838,7 @@ class Config:
"""
return self._get_base_group(self.ROLE, role.id)
def user(self, user: discord.User) -> Group:
def user(self, user: discord.abc.User) -> Group:
"""Returns a `Group` for the given user.
Parameters

View File

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

View File

@ -4,7 +4,7 @@ from redbot.core.utils.chat_formatting import pagify
import io
import sys
import weakref
from typing import List
from typing import List, Optional
from .common_filters import filter_mass_mentions
_instances = weakref.WeakValueDictionary({})
@ -86,7 +86,11 @@ class Tunnel(metaclass=TunnelMeta):
@staticmethod
async def message_forwarder(
*, destination: discord.abc.Messageable, content: str = None, embed=None, files=[]
*,
destination: discord.abc.Messageable,
content: str = None,
embed=None,
files: Optional[List[discord.File]] = None
) -> List[discord.Message]:
"""
This does the actual sending, use this instead of a full tunnel
@ -95,19 +99,19 @@ class Tunnel(metaclass=TunnelMeta):
Parameters
----------
destination: `discord.abc.Messageable`
destination: discord.abc.Messageable
Where to send
content: `str`
content: str
The message content
embed: `discord.Embed`
embed: discord.Embed
The embed to send
files: `list` of `discord.File`
files: Optional[List[discord.File]]
A list of files to send.
Returns
-------
list of `discord.Message`
The `discord.Message`\ (s) sent as a result
List[discord.Message]
The messages sent as a result.
Raises
------
@ -117,7 +121,6 @@ class Tunnel(metaclass=TunnelMeta):
see `discord.abc.Messageable.send`
"""
rets = []
files = files if files else None
if content:
for page in pagify(content):
rets.append(await destination.send(page, files=files, embed=embed))