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

View File

@ -2,6 +2,9 @@ import asyncio
import discord import discord
from redbot.core import commands from redbot.core import commands
from redbot.core.i18n import Translator
_ = Translator("Announcer", __file__)
class Announcer: class Announcer:
@ -63,7 +66,9 @@ class Announcer:
try: try:
await channel.send(self.message) await channel.send(self.message)
except discord.Forbidden: 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) await asyncio.sleep(0.5)
self.active = False self.active = False

View File

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

View File

@ -15,15 +15,14 @@ _ = Translator("Alias", __file__)
@cog_i18n(_) @cog_i18n(_)
class Alias(commands.Cog): class Alias(commands.Cog):
""" """Create aliases for commands.
Alias
Aliases are per server shortcuts for commands. They Aliases are alternative names shortcuts for commands. They
can act as both a lambda (storing arguments for repeated use) can act as both a lambda (storing arguments for repeated use)
or as simply a shortcut to saying "x y z". or as simply a shortcut to saying "x y z".
When run, aliases will accept any additional arguments 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": []} default_global_settings = {"entries": []}
@ -177,32 +176,28 @@ class Alias(commands.Cog):
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
async def alias(self, ctx: commands.Context): async def alias(self, ctx: commands.Context):
"""Manage per-server aliases for commands""" """Manage command aliases."""
pass pass
@alias.group(name="global") @alias.group(name="global")
async def global_(self, ctx: commands.Context): async def global_(self, ctx: commands.Context):
""" """Manage global aliases."""
Manage global aliases.
"""
pass pass
@checks.mod_or_permissions(manage_guild=True) @checks.mod_or_permissions(manage_guild=True)
@alias.command(name="add") @alias.command(name="add")
@commands.guild_only() @commands.guild_only()
async def _add_alias(self, ctx: commands.Context, alias_name: str, *, command): 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 # region Alias Add Validity Checking
is_command = self.is_command(alias_name) is_command = self.is_command(alias_name)
if is_command: if is_command:
await ctx.send( await ctx.send(
_( _(
"You attempted to create a new alias" "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." " name is already a command on this bot."
).format(alias_name) ).format(name=alias_name)
) )
return return
@ -211,9 +206,9 @@ class Alias(commands.Cog):
await ctx.send( await ctx.send(
_( _(
"You attempted to create a new alias" "You attempted to create a new alias"
" with the name {} but that" " with the name {name} but that"
" alias already exists on this server." " alias already exists on this server."
).format(alias_name) ).format(name=alias_name)
) )
return return
@ -222,10 +217,10 @@ class Alias(commands.Cog):
await ctx.send( await ctx.send(
_( _(
"You attempted to create a new alias" "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" " name is an invalid alias name. Alias"
" names may not contain spaces." " names may not contain spaces."
).format(alias_name) ).format(name=alias_name)
) )
return return
# endregion # endregion
@ -235,23 +230,23 @@ class Alias(commands.Cog):
await self.add_alias(ctx, alias_name, command) 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() @checks.is_owner()
@global_.command(name="add") @global_.command(name="add")
async def _add_global_alias(self, ctx: commands.Context, alias_name: str, *, command): 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 # region Alias Add Validity Checking
is_command = self.is_command(alias_name) is_command = self.is_command(alias_name)
if is_command: if is_command:
await ctx.send( await ctx.send(
_( _(
"You attempted to create a new global alias" "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." " name is already a command on this bot."
).format(alias_name) ).format(name=alias_name)
) )
return return
@ -260,9 +255,9 @@ class Alias(commands.Cog):
await ctx.send( await ctx.send(
_( _(
"You attempted to create a new global alias" "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." " alias already exists on this server."
).format(alias_name) ).format(name=alias_name)
) )
return return
@ -271,10 +266,10 @@ class Alias(commands.Cog):
await ctx.send( await ctx.send(
_( _(
"You attempted to create a new global alias" "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" " name is an invalid alias name. Alias"
" names may not contain spaces." " names may not contain spaces."
).format(alias_name) ).format(name=alias_name)
) )
return return
# endregion # endregion
@ -282,63 +277,65 @@ class Alias(commands.Cog):
await self.add_alias(ctx, alias_name, command, global_=True) await self.add_alias(ctx, alias_name, command, global_=True)
await ctx.send( 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") @alias.command(name="help")
@commands.guild_only() @commands.guild_only()
async def _help_alias(self, ctx: commands.Context, alias_name: str): 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) is_alias, alias = await self.is_alias(ctx.guild, alias_name=alias_name)
if is_alias: if is_alias:
base_cmd = alias.command[0] base_cmd = alias.command[0]
new_msg = copy(ctx.message) 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) await self.bot.process_commands(new_msg)
else: else:
ctx.send(_("No such alias exists.")) await ctx.send(_("No such alias exists."))
@alias.command(name="show") @alias.command(name="show")
@commands.guild_only() @commands.guild_only()
async def _show_alias(self, ctx: commands.Context, alias_name: str): 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) is_alias, alias = await self.is_alias(ctx.guild, alias_name)
if is_alias: if is_alias:
await ctx.send( 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: 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) @checks.mod_or_permissions(manage_guild=True)
@alias.command(name="del") @alias.command(name="del")
@commands.guild_only() @commands.guild_only()
async def _del_alias(self, ctx: commands.Context, alias_name: str): async def _del_alias(self, ctx: commands.Context, alias_name: str):
""" """Delete an existing alias on this server."""
Deletes an existing alias on this server.
"""
aliases = await self.unloaded_aliases(ctx.guild) aliases = await self.unloaded_aliases(ctx.guild)
try: try:
next(aliases) next(aliases)
except StopIteration: except StopIteration:
await ctx.send(_("There are no aliases on this guild.")) await ctx.send(_("There are no aliases on this server."))
return return
if await self.delete_alias(ctx, alias_name): if await self.delete_alias(ctx, alias_name):
await ctx.send( 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: 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() @checks.is_owner()
@global_.command(name="del") @global_.command(name="del")
async def _del_global_alias(self, ctx: commands.Context, alias_name: str): async def _del_global_alias(self, ctx: commands.Context, alias_name: str):
""" """Delete an existing global alias."""
Deletes an existing global alias.
"""
aliases = await self.unloaded_global_aliases() aliases = await self.unloaded_global_aliases()
try: try:
next(aliases) next(aliases)
@ -348,17 +345,15 @@ class Alias(commands.Cog):
if await self.delete_alias(ctx, alias_name, global_=True): if await self.delete_alias(ctx, alias_name, global_=True):
await ctx.send( 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: 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") @alias.command(name="list")
@commands.guild_only() @commands.guild_only()
async def _list_alias(self, ctx: commands.Context): async def _list_alias(self, ctx: commands.Context):
""" """List the available aliases on this server."""
Lists the available aliases on this server.
"""
names = [_("Aliases:")] + sorted( names = [_("Aliases:")] + sorted(
["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))] ["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))]
) )
@ -369,9 +364,7 @@ class Alias(commands.Cog):
@global_.command(name="list") @global_.command(name="list")
async def _list_global_alias(self, ctx: commands.Context): async def _list_global_alias(self, ctx: commands.Context):
""" """List the available global aliases on this bot."""
Lists the available global aliases on this bot.
"""
names = [_("Aliases:")] + sorted( names = [_("Aliases:")] + sorted(
["+ " + a.name for a in await self.unloaded_global_aliases()] ["+ " + 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) @checks.guildowner_or_permissions(administrator=True)
@commands.group(autohelp=True) @commands.group(autohelp=True)
async def bankset(self, ctx: commands.Context): async def bankset(self, ctx: commands.Context):
"""Base command for bank settings""" """Base command for bank settings."""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
if await bank.is_global(): if await bank.is_global():
bank_name = await bank._conf.bank_name() 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() default_balance = await bank._conf.guild(ctx.guild).default_balance()
settings = _( settings = _(
"Bank settings:\n\nBank name: {}\nCurrency: {}\nDefault balance: {}" "Bank settings:\n\nBank name: {bank_name}\nCurrency: {currency_name}\n"
).format(bank_name, currency_name, default_balance) "Default balance: {default_balance}"
).format(
bank_name=bank_name, currency_name=currency_name, default_balance=default_balance
)
await ctx.send(box(settings)) await ctx.send(box(settings))
@bankset.command(name="toggleglobal") @bankset.command(name="toggleglobal")
@checks.is_owner() @checks.is_owner()
async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool = False): async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool = False):
"""Toggles whether the bank is global or not """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""" 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() cur_setting = await bank.is_global()
word = _("per-server") if cur_setting else _("global") word = _("per-server") if cur_setting else _("global")
if confirm is False: if confirm is False:
await ctx.send( await ctx.send(
_( _(
"This will toggle the bank to be {}, deleting all accounts " "This will toggle the bank to be {banktype}, deleting all accounts "
"in the process! If you're sure, type `{}`" "in the process! If you're sure, type `{command}`"
).format(word, "{}bankset toggleglobal yes".format(ctx.prefix)) ).format(banktype=word, command="{}bankset toggleglobal yes".format(ctx.prefix))
) )
else: else:
await bank.set_global(not cur_setting) 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") @bankset.command(name="bankname")
@check_global_setting_guildowner() @check_global_setting_guildowner()
async def bankset_bankname(self, ctx: commands.Context, *, name: str): 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 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") @bankset.command(name="creditsname")
@check_global_setting_guildowner() @check_global_setting_guildowner()
async def bankset_creditsname(self, ctx: commands.Context, *, name: str): 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 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 # ENDSECTION

View File

@ -16,7 +16,7 @@ _ = Translator("Cleanup", __file__)
@cog_i18n(_) @cog_i18n(_)
class Cleanup(commands.Cog): class Cleanup(commands.Cog):
"""Commands for cleaning messages""" """Commands for cleaning up messages."""
def __init__(self, bot: Red): def __init__(self, bot: Red):
super().__init__() super().__init__()
@ -33,7 +33,7 @@ class Cleanup(commands.Cog):
""" """
prompt = await ctx.send( 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)) response = await ctx.bot.wait_for("message", check=MessagePredicate.same_context(ctx))
@ -41,7 +41,7 @@ class Cleanup(commands.Cog):
await prompt.delete() await prompt.delete()
try: try:
await response.delete() await response.delete()
except: except discord.HTTPException:
pass pass
return True return True
else: else:
@ -104,25 +104,24 @@ class Cleanup(commands.Cog):
@commands.group() @commands.group()
@checks.mod_or_permissions(manage_messages=True) @checks.mod_or_permissions(manage_messages=True)
async def cleanup(self, ctx: commands.Context): async def cleanup(self, ctx: commands.Context):
"""Deletes messages.""" """Delete messages."""
pass pass
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def text( async def text(
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
): ):
"""Deletes last X messages matching the specified text. """Delete the last X messages matching the specified text.
Example: Example:
cleanup text \"test\" 5 `[p]cleanup text "test" 5`
Remember to use double quotes.""" Remember to use double quotes.
"""
channel = ctx.channel channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).manage_messages:
await ctx.send("I need the Manage Messages permission to do this.")
return
author = ctx.author author = ctx.author
@ -156,18 +155,17 @@ class Cleanup(commands.Cog):
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def user( async def user(
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
): ):
"""Deletes last X messages from specified user. """Delete the last X messages from a specified user.
Examples: Examples:
cleanup user @\u200bTwentysix 2 `[p]cleanup user @\u200bTwentysix 2`
cleanup user Red 6""" `[p]cleanup user Red 6`
"""
channel = ctx.channel channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).manage_messages:
await ctx.send("I need the Manage Messages permission to do this.")
return
member = None member = None
try: try:
@ -213,8 +211,9 @@ class Cleanup(commands.Cog):
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False): async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool = False):
"""Deletes all messages after specified message. """Delete all messages after a specified message.
To get a message id, enable developer mode in Discord's To get a message id, enable developer mode in Discord's
settings, 'appearance' tab. Then right click a message settings, 'appearance' tab. Then right click a message
@ -222,9 +221,6 @@ class Cleanup(commands.Cog):
""" """
channel = ctx.channel channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).manage_messages:
await ctx.send("I need the Manage Messages permission to do this.")
return
author = ctx.author author = ctx.author
try: try:
@ -245,6 +241,7 @@ class Cleanup(commands.Cog):
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def before( async def before(
self, ctx: commands.Context, message_id: int, number: int, delete_pinned: bool = False self, ctx: commands.Context, message_id: int, number: int, delete_pinned: bool = False
): ):
@ -256,9 +253,6 @@ class Cleanup(commands.Cog):
""" """
channel = ctx.channel channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).manage_messages:
await ctx.send("I need the Manage Messages permission to do this.")
return
author = ctx.author author = ctx.author
try: try:
@ -279,16 +273,15 @@ class Cleanup(commands.Cog):
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False): async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
"""Deletes last X messages. """Delete the last X messages.
Example: Example:
cleanup messages 26""" `[p]cleanup messages 26`
"""
channel = ctx.channel channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).manage_messages:
await ctx.send("I need the Manage Messages permission to do this.")
return
author = ctx.author author = ctx.author
if number > 100: if number > 100:
@ -310,13 +303,11 @@ class Cleanup(commands.Cog):
@cleanup.command(name="bot") @cleanup.command(name="bot")
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True)
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False): async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool = False):
"""Cleans up command messages and messages from the bot.""" """Clean up command messages and messages from the bot."""
channel = ctx.channel channel = ctx.channel
if not channel.permissions_for(ctx.guild.me).manage_messages:
await ctx.send("I need the Manage Messages permission to do this.")
return
author = ctx.message.author author = ctx.message.author
if number > 100: if number > 100:
@ -369,7 +360,7 @@ class Cleanup(commands.Cog):
match_pattern: str = None, match_pattern: str = None,
delete_pinned: bool = False, 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, 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 ), 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 re
import random import random
from datetime import datetime, timedelta from datetime import datetime, timedelta
from inspect import Parameter from inspect import Parameter
from collections import OrderedDict from collections import OrderedDict
from typing import Mapping from typing import Mapping, Tuple, Dict
import discord import discord
@ -52,11 +51,11 @@ class CommandObj:
async def get_responses(self, ctx): async def get_responses(self, ctx):
intro = _( 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 " "Every message you send will be added as one of the random "
"responses to choose from once this {} is " "responses to choose from once this {cc} is "
"triggered. To exit this interactive menu, type `{}`" "triggered. To exit this interactive menu, type `{quit}`"
).format("customcommand", "customcommand", "exit()") ).format(cc="customcommand", quit="exit()")
await ctx.send(intro) await ctx.send(intro)
responses = [] responses = []
@ -85,7 +84,7 @@ class CommandObj:
# in the ccinfo dict # in the ccinfo dict
return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow()) return "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow())
async def get(self, message: discord.Message, command: str) -> str: async def get(self, message: discord.Message, command: str) -> Tuple[str, Dict]:
ccinfo = await self.db(message.guild).commands.get_raw(command, default=None) ccinfo = await self.db(message.guild).commands.get_raw(command, default=None)
if not ccinfo: if not ccinfo:
raise NotFound() raise NotFound()
@ -180,9 +179,7 @@ class CommandObj:
@cog_i18n(_) @cog_i18n(_)
class CustomCommands(commands.Cog): class CustomCommands(commands.Cog):
"""Custom commands """Creates commands used to display text."""
Creates commands used to display text"""
def __init__(self, bot): def __init__(self, bot):
super().__init__() super().__init__()
@ -196,61 +193,55 @@ class CustomCommands(commands.Cog):
@commands.group(aliases=["cc"]) @commands.group(aliases=["cc"])
@commands.guild_only() @commands.guild_only()
async def customcom(self, ctx: commands.Context): async def customcom(self, ctx: commands.Context):
"""Custom commands management""" """Custom commands management."""
pass pass
@customcom.group(name="add") @customcom.group(name="create", aliases=["add"])
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def cc_add(self, ctx: commands.Context): async def cc_create(self, ctx: commands.Context):
""" """Create custom commands.
Adds a new custom command
CCs can be enhanced with arguments: CCs can be enhanced with arguments, see the guide
https://red-discordbot.readthedocs.io/en/v3-develop/cog_customcom.html [here](https://red-discordbot.readthedocs.io/en/v3-develop/cog_customcom.html).
""" """
pass pass
@cc_add.command(name="random") @cc_create.command(name="random")
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def cc_add_random(self, ctx: commands.Context, command: str.lower): async def cc_create_random(self, ctx: commands.Context, command: str.lower):
""" """Create a CC where it will randomly choose a response!
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) responses = await self.commandobj.get_responses(ctx=ctx)
try: try:
await self.commandobj.create(ctx=ctx, command=command, response=responses) await self.commandobj.create(ctx=ctx, command=command, response=responses)
await ctx.send(_("Custom command successfully added.")) await ctx.send(_("Custom command successfully added."))
except AlreadyExists: except AlreadyExists:
await ctx.send( await ctx.send(
_("This command already exists. Use `{}` to edit it.").format( _("This command already exists. Use `{command}` to edit it.").format(
"{}customcom edit".format(ctx.prefix) command="{}customcom edit".format(ctx.prefix)
) )
) )
# await ctx.send(str(responses)) @cc_create.command(name="simple")
@cc_add.command(name="simple")
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def cc_add_simple(self, ctx, command: str.lower, *, text: str): async def cc_create_simple(self, ctx, command: str.lower, *, text: str):
"""Adds a simple custom command """Add a simple custom command.
Example: Example:
[p]customcom add simple yourcommand Text you want - `[p]customcom create simple yourcommand Text you want`
""" """
if command in self.bot.all_commands: 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 return
try: try:
await self.commandobj.create(ctx=ctx, command=command, response=text) await self.commandobj.create(ctx=ctx, command=command, response=text)
await ctx.send(_("Custom command successfully added.")) await ctx.send(_("Custom command successfully added."))
except AlreadyExists: except AlreadyExists:
await ctx.send( await ctx.send(
_("This command already exists. Use `{}` to edit it.").format( _("This command already exists. Use `{command}` to edit it.").format(
"{}customcom edit".format(ctx.prefix) command="{}customcom edit".format(ctx.prefix)
) )
) )
except ArgParseError as e: except ArgParseError as e:
@ -261,13 +252,14 @@ class CustomCommands(commands.Cog):
async def cc_cooldown( async def cc_cooldown(
self, ctx, command: str.lower, cooldown: int = None, *, per: str.lower = "member" self, ctx, command: str.lower, cooldown: int = None, *, per: str.lower = "member"
): ):
""" """Set, edit, or view the cooldown for a custom command.
Sets, edits, or views cooldowns 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: Example:
[p]customcom cooldown yourcommand 30 - `[p]customcom cooldown yourcommand 30`
""" """
if cooldown is None: if cooldown is None:
try: try:
@ -293,18 +285,19 @@ class CustomCommands(commands.Cog):
await ctx.send(_("Custom command cooldown successfully edited.")) await ctx.send(_("Custom command cooldown successfully edited."))
except NotFound: except NotFound:
await ctx.send( await ctx.send(
_("That command doesn't exist. Use `{}` to add it.").format( _("That command doesn't exist. Use `{command}` to add it.").format(
"{}customcom add".format(ctx.prefix) command="{}customcom create".format(ctx.prefix)
) )
) )
@customcom.command(name="delete") @customcom.command(name="delete")
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def cc_delete(self, ctx, command: str.lower): async def cc_delete(self, ctx, command: str.lower):
"""Deletes a custom command """Delete a custom command
.
Example: Example:
[p]customcom delete yourcommand""" - `[p]customcom delete yourcommand`
"""
try: try:
await self.commandobj.delete(ctx=ctx, command=command) await self.commandobj.delete(ctx=ctx, command=command)
await ctx.send(_("Custom command successfully deleted.")) await ctx.send(_("Custom command successfully deleted."))
@ -314,18 +307,20 @@ class CustomCommands(commands.Cog):
@customcom.command(name="edit") @customcom.command(name="edit")
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def cc_edit(self, ctx, command: str.lower, *, text: str = None): async def cc_edit(self, ctx, command: str.lower, *, text: str = None):
"""Edits a custom command's response """Edit a custom command.
Example: Example:
[p]customcom edit yourcommand Text you want - `[p]customcom edit yourcommand Text you want`
""" """
command = command.lower()
try: try:
await self.commandobj.edit(ctx=ctx, command=command, response=text) await self.commandobj.edit(ctx=ctx, command=command, response=text)
await ctx.send(_("Custom command successfully edited.")) await ctx.send(_("Custom command successfully edited."))
except NotFound: except NotFound:
await ctx.send( await ctx.send(
_("That command doesn't exist. Use `{}` to add it.").format( _("That command doesn't exist. Use `{command}` to add it.").format(
"{}customcom add".format(ctx.prefix) command="{}customcom create".format(ctx.prefix)
) )
) )
except ArgParseError as e: except ArgParseError as e:
@ -333,7 +328,7 @@ class CustomCommands(commands.Cog):
@customcom.command(name="list") @customcom.command(name="list")
async def cc_list(self, ctx): 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)) response = await CommandObj.get_commands(self.config.guild(ctx.guild))
@ -341,8 +336,8 @@ class CustomCommands(commands.Cog):
await ctx.send( await ctx.send(
_( _(
"There are no custom commands in this server." "There are no custom commands in this server."
" Use `{}` to start adding some." " Use `{command}` to start adding some."
).format("{}customcom add".format(ctx.prefix)) ).format(command="{}customcom create".format(ctx.prefix))
) )
return return
@ -454,9 +449,8 @@ class CustomCommands(commands.Cog):
gaps = set(indices).symmetric_difference(range(high + 1)) gaps = set(indices).symmetric_difference(range(high + 1))
if gaps: if gaps:
raise ArgParseError( raise ArgParseError(
_("Arguments must be sequential. Missing arguments: {}.").format( _("Arguments must be sequential. Missing arguments: ")
", ".join(str(i + low) for i in gaps) + ", ".join(str(i + low) for i in gaps)
)
) )
fin = [Parameter("_" + str(i), Parameter.POSITIONAL_OR_KEYWORD) for i in range(high + 1)] fin = [Parameter("_" + str(i), Parameter.POSITIONAL_OR_KEYWORD) for i in range(high + 1)]
for arg in args: for arg in args:
@ -481,8 +475,12 @@ class CustomCommands(commands.Cog):
and anno != fin[index].annotation and anno != fin[index].annotation
): ):
raise ArgParseError( raise ArgParseError(
_('Conflicting colon notation for argument {}: "{}" and "{}".').format( _(
index + low, fin[index].annotation.__name__, anno.__name__ 'Conflicting colon notation for argument {index}: "{name1}" and "{name2}".'
).format(
index=index + low,
name1=fin[index].annotation.__name__,
name2=anno.__name__,
) )
) )
if anno is not Parameter.empty: if anno is not Parameter.empty:
@ -511,6 +509,8 @@ class CustomCommands(commands.Cog):
key = (command, ctx.guild, ctx.channel) key = (command, ctx.guild, ctx.channel)
elif per == "member": elif per == "member":
key = (command, ctx.guild, ctx.author) key = (command, ctx.guild, ctx.author)
else:
raise ValueError(per)
cooldown = self.cooldowns.get(key) cooldown = self.cooldowns.get(key)
if cooldown: if cooldown:
cooldown += timedelta(seconds=rate) cooldown += timedelta(seconds=rate)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import logging import logging
import asyncio import asyncio
from typing import Union from typing import Union, List
from datetime import timedelta from datetime import timedelta
from copy import copy from copy import copy
import contextlib import contextlib
@ -60,23 +60,20 @@ class Reports(commands.Cog):
@commands.guild_only() @commands.guild_only()
@commands.group(name="reportset") @commands.group(name="reportset")
async def reportset(self, ctx: commands.Context): async def reportset(self, ctx: commands.Context):
""" """Manage Reports."""
Settings for the report system.
"""
pass pass
@checks.admin_or_permissions(manage_guild=True) @checks.admin_or_permissions(manage_guild=True)
@reportset.command(name="output") @reportset.command(name="output")
async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel): async def reportset_output(self, ctx: commands.Context, channel: discord.TextChannel):
"""Set the channel where reports will show up""" """Set the channel where reports will be sent."""
await self.config.guild(ctx.guild).output_channel.set(channel.id) await self.config.guild(ctx.guild).output_channel.set(channel.id)
await ctx.send(_("The report channel has been set.")) await ctx.send(_("The report channel has been set."))
@checks.admin_or_permissions(manage_guild=True) @checks.admin_or_permissions(manage_guild=True)
@reportset.command(name="toggle", aliases=["toggleactive"]) @reportset.command(name="toggle", aliases=["toggleactive"])
async def report_toggle(self, ctx: commands.Context): async def reportset_toggle(self, ctx: commands.Context):
"""Enables or Disables reporting for the server""" """Enable or Disable reporting for this server."""
active = await self.config.guild(ctx.guild).active() active = await self.config.guild(ctx.guild).active()
active = not active active = not active
await self.config.guild(ctx.guild).active.set(active) await self.config.guild(ctx.guild).active.set(active)
@ -168,7 +165,7 @@ class Reports(commands.Cog):
if channel is None: if channel is None:
return 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() ticket_number = await self.config.guild(guild).next_ticket()
await self.config.guild(guild).next_ticket.set(ticket_number + 1) 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) @commands.group(name="report", invoke_without_command=True)
async def report(self, ctx: commands.Context, *, _report: str = ""): async def report(self, ctx: commands.Context, *, _report: str = ""):
""" """Send a report.
Send a report.
Use without arguments for interactive reporting, or do 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 author = ctx.author
guild = ctx.guild guild = ctx.guild
@ -323,8 +319,7 @@ class Reports(commands.Cog):
@checks.mod_or_permissions(manage_members=True) @checks.mod_or_permissions(manage_members=True)
@report.command(name="interact") @report.command(name="interact")
async def response(self, ctx, ticket_number: int): 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 This tunnel will forward things you say in this channel
to the ticket opener's direct messages. to the ticket opener's direct messages.
@ -354,8 +349,7 @@ class Reports(commands.Cog):
) )
big_topic = _( big_topic = _(
"{who} opened a 2-way communication " " Anything you say or upload here "
"about ticket number {ticketnum}. Anything you say or upload here "
"(8MB file size limitation on uploads) " "(8MB file size limitation on uploads) "
"will be forwarded to them until the communication is closed.\n" "will be forwarded to them until the communication is closed.\n"
"You can close a communication at any point by reacting with " "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" "\N{WHITE HEAVY CHECK MARK}.\n"
"Tunnels are not persistent across bot restarts." "Tunnels are not persistent across bot restarts."
) )
topic = big_topic.format( topic = (
ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild) _(
"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: try:
m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True) 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.")) await ctx.send(_("That user has DMs disabled."))
else: else:
self.tunnel_store[(guild, ticket_number)] = {"tun": tun, "msgs": m} 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 import discord
from redbot.core import Config, checks, commands from redbot.core import Config, checks, commands
from redbot.core.utils.chat_formatting import pagify from redbot.core.utils.chat_formatting import pagify
@ -22,7 +24,7 @@ from .errors import (
StreamsError, StreamsError,
InvalidTwitchCredentials, InvalidTwitchCredentials,
) )
from . import streamtypes as StreamClasses from . import streamtypes as _streamtypes
from collections import defaultdict from collections import defaultdict
import asyncio import asyncio
import re import re
@ -76,14 +78,14 @@ class Streams(commands.Cog):
@commands.command() @commands.command()
async def twitch(self, ctx: commands.Context, channel_name: str): 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) token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
stream = TwitchStream(name=channel_name, token=token) stream = TwitchStream(name=channel_name, token=token)
await self.check_online(ctx, stream) await self.check_online(ctx, stream)
@commands.command() @commands.command()
async def youtube(self, ctx: commands.Context, channel_id_or_name: str): 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) apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None)
is_name = self.check_name_or_id(channel_id_or_name) is_name = self.check_name_or_id(channel_id_or_name)
if is_name: if is_name:
@ -94,23 +96,24 @@ class Streams(commands.Cog):
@commands.command() @commands.command()
async def hitbox(self, ctx: commands.Context, channel_name: str): 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) stream = HitboxStream(name=channel_name)
await self.check_online(ctx, stream) await self.check_online(ctx, stream)
@commands.command() @commands.command()
async def mixer(self, ctx: commands.Context, channel_name: str): 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) stream = MixerStream(name=channel_name)
await self.check_online(ctx, stream) await self.check_online(ctx, stream)
@commands.command() @commands.command()
async def picarto(self, ctx: commands.Context, channel_name: str): 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) stream = PicartoStream(name=channel_name)
await self.check_online(ctx, stream) 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: try:
embed = await stream.is_online() embed = await stream.is_online()
except OfflineStream: except OfflineStream:
@ -119,15 +122,17 @@ class Streams(commands.Cog):
await ctx.send(_("That channel doesn't seem to exist.")) await ctx.send(_("That channel doesn't seem to exist."))
except InvalidTwitchCredentials: except InvalidTwitchCredentials:
await ctx.send( 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: except InvalidYoutubeCredentials:
await ctx.send( 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: except APIError:
await ctx.send( await ctx.send(
@ -140,11 +145,12 @@ class Streams(commands.Cog):
@commands.guild_only() @commands.guild_only()
@checks.mod() @checks.mod()
async def streamalert(self, ctx: commands.Context): async def streamalert(self, ctx: commands.Context):
"""Manage automated stream alerts."""
pass pass
@streamalert.group(name="twitch", invoke_without_command=True) @streamalert.group(name="twitch", invoke_without_command=True)
async def _twitch(self, ctx: commands.Context, channel_name: str = None): async def _twitch(self, ctx: commands.Context, channel_name: str = None):
"""Twitch stream alerts""" """Manage Twitch stream notifications."""
if channel_name is not None: if channel_name is not None:
await ctx.invoke(self.twitch_alert_channel, channel_name) await ctx.invoke(self.twitch_alert_channel, channel_name)
else: else:
@ -152,7 +158,7 @@ class Streams(commands.Cog):
@_twitch.command(name="channel") @_twitch.command(name="channel")
async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str): 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): if re.fullmatch(r"<#\d+>", channel_name):
await ctx.send("Please supply the name of a *Twitch* channel, not a Discord channel.") await ctx.send("Please supply the name of a *Twitch* channel, not a Discord channel.")
return return
@ -160,33 +166,39 @@ class Streams(commands.Cog):
@_twitch.command(name="community") @_twitch.command(name="community")
async def twitch_alert_community(self, ctx: commands.Context, community: str): 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()) await self.community_alert(ctx, TwitchCommunity, community.lower())
@streamalert.command(name="youtube") @streamalert.command(name="youtube")
async def youtube_alert(self, ctx: commands.Context, channel_name_or_id: str): 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) await self.stream_alert(ctx, YoutubeStream, channel_name_or_id)
@streamalert.command(name="hitbox") @streamalert.command(name="hitbox")
async def hitbox_alert(self, ctx: commands.Context, channel_name: str): 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) await self.stream_alert(ctx, HitboxStream, channel_name)
@streamalert.command(name="mixer") @streamalert.command(name="mixer")
async def mixer_alert(self, ctx: commands.Context, channel_name: str): 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) await self.stream_alert(ctx, MixerStream, channel_name)
@streamalert.command(name="picarto") @streamalert.command(name="picarto")
async def picarto_alert(self, ctx: commands.Context, channel_name: str): 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) 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): async def streamalert_stop(self, ctx: commands.Context, _all: bool = False):
"""Stops all stream notifications in the channel """Disable all stream alerts in this channel or server.
Adding 'yes' will disable all notifications in the 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() streams = self.streams.copy()
local_channel_ids = [c.id for c in ctx.guild.channels] local_channel_ids = [c.id for c in ctx.guild.channels]
to_remove = [] to_remove = []
@ -208,9 +220,10 @@ class Streams(commands.Cog):
self.streams = streams self.streams = streams
await self.save_streams() await self.save_streams()
msg = _("All the alerts in the {} have been disabled.").format( if _all:
"server" if _all else "channel" 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) await ctx.send(msg)
@ -250,16 +263,18 @@ class Streams(commands.Cog):
exists = await self.check_exists(stream) exists = await self.check_exists(stream)
except InvalidTwitchCredentials: except InvalidTwitchCredentials:
await ctx.send( 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 return
except InvalidYoutubeCredentials: except InvalidYoutubeCredentials:
await ctx.send( await ctx.send(
_( _(
"Your Youtube API key is either invalid or has not been set. See {}." "The YouTube API key is either invalid or has not been set. See "
).format("`{}streamset youtubekey`".format(ctx.prefix)) "`{prefix}streamset youtubekey`."
).format(prefix=ctx.prefix)
) )
return return
except APIError: except APIError:
@ -283,9 +298,10 @@ class Streams(commands.Cog):
await community.get_community_streams() await community.get_community_streams()
except InvalidTwitchCredentials: except InvalidTwitchCredentials:
await ctx.send( 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 return
except CommunityNotFound: except CommunityNotFound:
@ -309,14 +325,15 @@ class Streams(commands.Cog):
@streamset.command() @streamset.command()
@checks.is_owner() @checks.is_owner()
async def twitchtoken(self, ctx: commands.Context, token: str): 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: To do this, follow these steps:
1. Go to this page: https://dev.twitch.tv/dashboard/apps. 1. Go to this page: https://dev.twitch.tv/dashboard/apps.
2. Click *Register Your Application* 2. Click *Register Your Application*
3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and 3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and
select an Application Category of your choosing. select an Application Category of your choosing.
4. Click *Register*, and on the following page, copy the Client ID. 4. Click *Register*, and on the following page, copy the Client ID.
5. Paste the Client ID into this command. Done! 5. Paste the Client ID into this command. Done!
""" """
await self.db.tokens.set_raw("TwitchStream", value=token) await self.db.tokens.set_raw("TwitchStream", value=token)
await self.db.tokens.set_raw("TwitchCommunity", value=token) await self.db.tokens.set_raw("TwitchCommunity", value=token)
@ -325,64 +342,59 @@ class Streams(commands.Cog):
@streamset.command() @streamset.command()
@checks.is_owner() @checks.is_owner()
async def youtubekey(self, ctx: commands.Context, key: str): 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: To get one, do the following:
1. Create a project (see https://support.google.com/googleapi/answer/6251787 for details) 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) 2. Enable the YouTube Data API v3 (see https://support.google.com/googleapi/answer/6158841
3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for instructions) 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! 4. Copy your API key and paste it into this command. Done!
""" """
await self.db.tokens.set_raw("YoutubeStream", value=key) await self.db.tokens.set_raw("YoutubeStream", value=key)
await ctx.send(_("Youtube key set.")) await ctx.send(_("YouTube key set."))
@streamset.group() @streamset.group()
@commands.guild_only() @commands.guild_only()
async def mention(self, ctx: commands.Context): async def mention(self, ctx: commands.Context):
"""Sets mentions for alerts.""" """Manage mention settings for stream alerts."""
pass pass
@mention.command(aliases=["everyone"]) @mention.command(aliases=["everyone"])
@commands.guild_only() @commands.guild_only()
async def all(self, ctx: commands.Context): async def all(self, ctx: commands.Context):
"""Toggles everyone mention""" """Toggle the `@\u200beveryone` mention."""
guild = ctx.guild guild = ctx.guild
current_setting = await self.db.guild(guild).mention_everyone() current_setting = await self.db.guild(guild).mention_everyone()
if current_setting: if current_setting:
await self.db.guild(guild).mention_everyone.set(False) await self.db.guild(guild).mention_everyone.set(False)
await ctx.send( await ctx.send(_("`@\u200beveryone` will no longer be mentioned for stream alerts."))
_("{} will no longer be mentioned when a stream or community is live").format(
"@\u200beveryone"
)
)
else: else:
await self.db.guild(guild).mention_everyone.set(True) await self.db.guild(guild).mention_everyone.set(True)
await ctx.send( await ctx.send(
_("When a stream or community " "is live, {} will be mentioned.").format( _("When a stream or community is live, `@\u200beveryone` will be mentioned.")
"@\u200beveryone"
)
) )
@mention.command(aliases=["here"]) @mention.command(aliases=["here"])
@commands.guild_only() @commands.guild_only()
async def online(self, ctx: commands.Context): async def online(self, ctx: commands.Context):
"""Toggles here mention""" """Toggle the `@\u200bhere` mention."""
guild = ctx.guild guild = ctx.guild
current_setting = await self.db.guild(guild).mention_here() current_setting = await self.db.guild(guild).mention_here()
if current_setting: if current_setting:
await self.db.guild(guild).mention_here.set(False) 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: else:
await self.db.guild(guild).mention_here.set(True) await self.db.guild(guild).mention_here.set(True)
await ctx.send( await ctx.send(
_("When a stream or community " "is live, {} will be mentioned.").format( _("When a stream or community is live, `@\u200bhere` will be mentioned.")
"@\u200bhere"
)
) )
@mention.command() @mention.command()
@commands.guild_only() @commands.guild_only()
async def role(self, ctx: commands.Context, *, role: discord.Role): 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() current_setting = await self.db.role(role).mention()
if not role.mentionable: if not role.mentionable:
await ctx.send("That role is not mentionable!") await ctx.send("That role is not mentionable!")
@ -390,27 +402,27 @@ class Streams(commands.Cog):
if current_setting: if current_setting:
await self.db.role(role).mention.set(False) await self.db.role(role).mention.set(False)
await ctx.send( await ctx.send(
_("{} will no longer be mentioned for an alert.").format( _("`@\u200b{role.name}` will no longer be mentioned for stream alerts.").format(
"@\u200b{}".format(role.name) role=role
) )
) )
else: else:
await self.db.role(role).mention.set(True) await self.db.role(role).mention.set(True)
await ctx.send( 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() @streamset.command()
@commands.guild_only() @commands.guild_only()
async def autodelete(self, ctx: commands.Context, on_off: bool): 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) await self.db.guild(ctx.guild).autodelete.set(on_off)
if 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: 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): async def add_or_remove(self, ctx: commands.Context, stream):
if ctx.channel.id not in stream.channels: if ctx.channel.id not in stream.channels:
@ -418,18 +430,18 @@ class Streams(commands.Cog):
if stream not in self.streams: if stream not in self.streams:
self.streams.append(stream) self.streams.append(stream)
await ctx.send( 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: else:
stream.channels.remove(ctx.channel.id) stream.channels.remove(ctx.channel.id)
if not stream.channels: if not stream.channels:
self.streams.remove(stream) self.streams.remove(stream)
await ctx.send( 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() await self.save_streams()
@ -442,9 +454,8 @@ class Streams(commands.Cog):
await ctx.send( await ctx.send(
_( _(
"I'll send a notification in this channel when a " "I'll send a notification in this channel when a "
"channel is live in the {} community." "channel is live in the {community.name} community."
"" ).format(community=community)
).format(community.name)
) )
else: else:
community.channels.remove(ctx.channel.id) community.channels.remove(ctx.channel.id)
@ -453,9 +464,8 @@ class Streams(commands.Cog):
await ctx.send( await ctx.send(
_( _(
"I won't send notifications about channels streaming " "I won't send notifications about channels streaming "
"in the {} community in this channel anymore." "in the {community.name} community in this channel anymore."
"" ).format(community=community)
).format(community.name)
) )
await self.save_communities() await self.save_communities()
@ -481,7 +491,8 @@ class Streams(commands.Cog):
if community.type == _class.__name__ and community.name.lower() == name.lower(): if community.type == _class.__name__ and community.name.lower() == name.lower():
return community return community
async def check_exists(self, stream): @staticmethod
async def check_exists(stream):
try: try:
await stream.is_online() await stream.is_online()
except OfflineStream: except OfflineStream:
@ -506,40 +517,36 @@ class Streams(commands.Cog):
async def check_streams(self): async def check_streams(self):
for stream in self.streams: for stream in self.streams:
try: with contextlib.suppress(Exception):
embed = await stream.is_online() try:
except OfflineStream: embed = await stream.is_online()
if not stream._messages_cache: except OfflineStream:
continue if not stream._messages_cache:
for message in stream._messages_cache: continue
try: for message in stream._messages_cache:
autodelete = await self.db.guild(message.guild).autodelete() with contextlib.suppress(Exception):
if autodelete: autodelete = await self.db.guild(message.guild).autodelete()
await message.delete() if autodelete:
except: await message.delete()
pass stream._messages_cache.clear()
stream._messages_cache.clear() await self.save_streams()
await self.save_streams() else:
except: if stream._messages_cache:
pass continue
else: for channel_id in stream.channels:
if stream._messages_cache: channel = self.bot.get_channel(channel_id)
continue mention_str = await self._get_mention_str(channel.guild)
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: if mention_str:
content = "{}, {} is live!".format(mention_str, stream.name) content = _("{mention}, {stream.name} is live!").format(
else: mention=mention_str, stream=stream
content = "{} is live!".format(stream.name) )
else:
content = _("{stream.name} is live!").format(stream=stream.name)
try:
m = await channel.send(content, embed=embed) m = await channel.send(content, embed=embed)
stream._messages_cache.append(m) stream._messages_cache.append(m)
await self.save_streams() await self.save_streams()
except:
pass
async def _get_mention_str(self, guild: discord.Guild): async def _get_mention_str(self, guild: discord.Guild):
settings = self.db.guild(guild) settings = self.db.guild(guild)
@ -555,45 +562,46 @@ class Streams(commands.Cog):
async def check_communities(self): async def check_communities(self):
for community in self.communities: for community in self.communities:
try: with contextlib.suppress(Exception):
stream_list = await community.get_community_streams() try:
except CommunityNotFound: stream_list = await community.get_community_streams()
print(_("The Community {} was not found!").format(community.name)) except CommunityNotFound:
continue print(
except OfflineCommunity: _("The Community {community.name} was not found!").format(
if not community._messages_cache: community=community
)
)
continue continue
for message in community._messages_cache: except OfflineCommunity:
try: if not community._messages_cache:
autodelete = await self.db.guild(message.guild).autodelete() continue
if autodelete: for message in community._messages_cache:
await message.delete() with contextlib.suppress(Exception):
except: autodelete = await self.db.guild(message.guild).autodelete()
pass if autodelete:
community._messages_cache.clear() await message.delete()
await self.save_communities() community._messages_cache.clear()
except: await self.save_communities()
pass else:
else: for channel in community.channels:
for channel in community.channels: chn = self.bot.get_channel(channel)
chn = self.bot.get_channel(channel) streams = await self.filter_streams(stream_list, chn)
streams = await self.filter_streams(stream_list, chn) emb = await community.make_embed(streams)
emb = await community.make_embed(streams) chn_msg = [m for m in community._messages_cache if m.channel == chn]
chn_msg = [m for m in community._messages_cache if m.channel == chn] if not chn_msg:
if not chn_msg: mentions = await self._get_mention_str(chn.guild)
mentions = await self._get_mention_str(chn.guild) if mentions:
if mentions: msg = await chn.send(mentions, embed=emb)
msg = await chn.send(mentions, embed=emb) else:
msg = await chn.send(embed=emb)
community._messages_cache.append(msg)
await self.save_communities()
else: else:
msg = await chn.send(embed=emb) chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
community._messages_cache.append(msg) community._messages_cache.remove(chn_msg)
await self.save_communities() await chn_msg.edit(embed=emb)
else: community._messages_cache.append(chn_msg)
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0] await self.save_communities()
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: async def filter_streams(self, streams: list, channel: discord.TextChannel) -> list:
filtered = [] filtered = []
@ -611,7 +619,7 @@ class Streams(commands.Cog):
streams = [] streams = []
for raw_stream in await self.db.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: if not _class:
continue continue
raw_msg_cache = raw_stream["messages"] raw_msg_cache = raw_stream["messages"]
@ -631,7 +639,7 @@ class Streams(commands.Cog):
communities = [] communities = []
for raw_community in await self.db.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: if not _class:
continue continue
raw_msg_cache = raw_community["messages"] raw_msg_cache = raw_community["messages"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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