[V3] Update code standards (black code format pass) (#1650)

* ran black: code formatter against `redbot/` with `-l 99`

* badge
This commit is contained in:
Michael H 2018-05-14 15:33:24 -04:00 committed by Will
parent e7476edd68
commit b88b5a2601
90 changed files with 3629 additions and 3223 deletions

View File

@ -14,6 +14,11 @@
:target: https://www.patreon.com/Red_Devs :target: https://www.patreon.com/Red_Devs
:alt: Patreon :alt: Patreon
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
:alt: Code style: black
******************** ********************
Red - Discord Bot v3 Red - Discord Bot v3
******************** ********************

View File

@ -5,7 +5,9 @@ import discord
# Let's do all the dumb version checking in one place. # Let's do all the dumb version checking in one place.
if discord.version_info.major < 1: if discord.version_info.major < 1:
print("You are not running the rewritten version of discord.py.\n\n" print(
"In order to use Red v3 you MUST be running d.py version" "You are not running the rewritten version of discord.py.\n\n"
" >= 1.0.0.") "In order to use Red v3 you MUST be running d.py version"
" >= 1.0.0."
)
sys.exit(1) sys.exit(1)

View File

@ -40,24 +40,25 @@ def init_loggers(cli_flags):
logger = logging.getLogger("red") logger = logging.getLogger("red")
red_format = logging.Formatter( red_format = logging.Formatter(
'%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: ' "%(asctime)s %(levelname)s %(module)s %(funcName)s %(lineno)d: " "%(message)s",
'%(message)s', datefmt="[%d/%m/%Y %H:%M]",
datefmt="[%d/%m/%Y %H:%M]") )
stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(red_format) stdout_handler.setFormatter(red_format)
if cli_flags.debug: if cli_flags.debug:
os.environ['PYTHONASYNCIODEBUG'] = '1' os.environ["PYTHONASYNCIODEBUG"] = "1"
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
else: else:
logger.setLevel(logging.WARNING) logger.setLevel(logging.WARNING)
from redbot.core.data_manager import core_data_path from redbot.core.data_manager import core_data_path
logfile_path = core_data_path() / 'red.log'
logfile_path = core_data_path() / "red.log"
fhandler = logging.handlers.RotatingFileHandler( fhandler = logging.handlers.RotatingFileHandler(
filename=str(logfile_path), encoding='utf-8', mode='a', filename=str(logfile_path), encoding="utf-8", mode="a", maxBytes=10 ** 7, backupCount=5
maxBytes=10**7, backupCount=5) )
fhandler.setFormatter(red_format) fhandler.setFormatter(red_format)
logger.addHandler(fhandler) logger.addHandler(fhandler)
@ -76,15 +77,17 @@ async def _get_prefix_and_token(red, indict):
:param indict: :param indict:
:return: :return:
""" """
indict['token'] = await red.db.token() indict["token"] = await red.db.token()
indict['prefix'] = await red.db.prefix() indict["prefix"] = await red.db.prefix()
indict['enable_sentry'] = await red.db.enable_sentry() indict["enable_sentry"] = await red.db.enable_sentry()
def list_instances(): def list_instances():
if not config_file.exists(): if not config_file.exists():
print("No instances have been configured! Configure one " print(
"using `redbot-setup` before trying to run the bot!") "No instances have been configured! Configure one "
"using `redbot-setup` before trying to run the bot!"
)
sys.exit(1) sys.exit(1)
else: else:
data = JsonIO(config_file)._load_json() data = JsonIO(config_file)._load_json()
@ -118,29 +121,30 @@ def main():
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
tmp_data = {} tmp_data = {}
loop.run_until_complete(_get_prefix_and_token(red, tmp_data)) loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
token = os.environ.get("RED_TOKEN", tmp_data['token']) token = os.environ.get("RED_TOKEN", tmp_data["token"])
prefix = cli_flags.prefix or tmp_data['prefix'] prefix = cli_flags.prefix or tmp_data["prefix"]
if token is None or not prefix: if token is None or not prefix:
if cli_flags.no_prompt is False: if cli_flags.no_prompt is False:
new_token = interactive_config(red, token_set=bool(token), new_token = interactive_config(red, token_set=bool(token), prefix_set=bool(prefix))
prefix_set=bool(prefix))
if new_token: if new_token:
token = new_token token = new_token
else: else:
log.critical("Token and prefix must be set in order to login.") log.critical("Token and prefix must be set in order to login.")
sys.exit(1) sys.exit(1)
loop.run_until_complete(_get_prefix_and_token(red, tmp_data)) loop.run_until_complete(_get_prefix_and_token(red, tmp_data))
if tmp_data['enable_sentry']: if tmp_data["enable_sentry"]:
red.enable_sentry() red.enable_sentry()
cleanup_tasks = True cleanup_tasks = True
try: try:
loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot)) loop.run_until_complete(red.start(token, bot=not cli_flags.not_bot))
except discord.LoginFailure: except discord.LoginFailure:
cleanup_tasks = False # No login happened, no need for this cleanup_tasks = False # No login happened, no need for this
log.critical("This token doesn't seem to be valid. If it belongs to " log.critical(
"a user account, remember that the --not-bot flag " "This token doesn't seem to be valid. If it belongs to "
"must be used. For self-bot functionalities instead, " "a user account, remember that the --not-bot flag "
"--self-bot") "must be used. For self-bot functionalities instead, "
"--self-bot"
)
db_token = red.db.token() db_token = red.db.token()
if db_token and not cli_flags.no_prompt: if db_token and not cli_flags.no_prompt:
print("\nDo you want to reset the token? (y/n)") print("\nDo you want to reset the token? (y/n)")
@ -159,12 +163,11 @@ def main():
rpc.clean_up() rpc.clean_up()
if cleanup_tasks: if cleanup_tasks:
pending = asyncio.Task.all_tasks(loop=red.loop) pending = asyncio.Task.all_tasks(loop=red.loop)
gathered = asyncio.gather( gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
*pending, loop=red.loop, return_exceptions=True)
gathered.cancel() gathered.cancel()
sys.exit(red._shutdown_mode.value) sys.exit(red._shutdown_mode.value)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -40,18 +40,16 @@ RUNNING_ANNOUNCEMENT = (
class Admin: class Admin:
def __init__(self, config=Config):
self.conf = config.get_conf(self, 8237492837454039,
force_registration=True)
self.conf.register_global( def __init__(self, config=Config):
serverlocked=False self.conf = config.get_conf(self, 8237492837454039, force_registration=True)
)
self.conf.register_global(serverlocked=False)
self.conf.register_guild( self.conf.register_guild(
announce_ignore=False, announce_ignore=False,
announce_channel=None, # Integer ID announce_channel=None, # Integer ID
selfroles=[] # List of integer ID's selfroles=[], # List of integer ID's
) )
self.__current_announcer = None self.__current_announcer = None
@ -63,8 +61,7 @@ class Admin:
pass pass
@staticmethod @staticmethod
async def complain(ctx: commands.Context, message: str, async def complain(ctx: commands.Context, message: str, **kwargs):
**kwargs):
await ctx.send(message.format(**kwargs)) await ctx.send(message.format(**kwargs))
def is_announcing(self) -> bool: def is_announcing(self) -> bool:
@ -78,8 +75,7 @@ class Admin:
return self.__current_announcer.active or False return self.__current_announcer.active or False
@staticmethod @staticmethod
def pass_heirarchy_check(ctx: commands.Context, def pass_heirarchy_check(ctx: commands.Context, role: discord.Role) -> bool:
role: discord.Role) -> bool:
""" """
Determines if the bot has a higher role than the given one. Determines if the bot has a higher role than the given one.
:param ctx: :param ctx:
@ -89,8 +85,7 @@ class Admin:
return ctx.guild.me.top_role > role return ctx.guild.me.top_role > role
@staticmethod @staticmethod
def pass_user_heirarchy_check(ctx: commands.Context, def pass_user_heirarchy_check(ctx: commands.Context, role: discord.Role) -> bool:
role: discord.Role) -> bool:
""" """
Determines if a user is allowed to add/remove/edit the given role. Determines if a user is allowed to add/remove/edit the given role.
:param ctx: :param ctx:
@ -99,43 +94,40 @@ class Admin:
""" """
return ctx.author.top_role > role return ctx.author.top_role > role
async def _addrole(self, ctx: commands.Context, member: discord.Member, async def _addrole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
role: discord.Role):
try: try:
await member.add_roles(role) await member.add_roles(role)
except discord.Forbidden: except discord.Forbidden:
if not self.pass_heirarchy_check(ctx, role): if not self.pass_heirarchy_check(ctx, role):
await self.complain(ctx, HIERARCHY_ISSUE, role=role, await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member)
member=member)
else: else:
await self.complain(ctx, GENERIC_FORBIDDEN) await self.complain(ctx, GENERIC_FORBIDDEN)
else: else:
await ctx.send("I successfully added {role.name} to" await ctx.send(
" {member.display_name}".format( "I successfully added {role.name} to"
role=role, member=member " {member.display_name}".format(role=role, member=member)
)) )
async def _removerole(self, ctx: commands.Context, member: discord.Member, async def _removerole(self, ctx: commands.Context, member: discord.Member, role: discord.Role):
role: discord.Role):
try: try:
await member.remove_roles(role) await member.remove_roles(role)
except discord.Forbidden: except discord.Forbidden:
if not self.pass_heirarchy_check(ctx, role): if not self.pass_heirarchy_check(ctx, role):
await self.complain(ctx, HIERARCHY_ISSUE, role=role, await self.complain(ctx, HIERARCHY_ISSUE, role=role, member=member)
member=member)
else: else:
await self.complain(ctx, GENERIC_FORBIDDEN) await self.complain(ctx, GENERIC_FORBIDDEN)
else: else:
await ctx.send("I successfully removed {role.name} from" await ctx.send(
" {member.display_name}".format( "I successfully removed {role.name} from"
role=role, member=member " {member.display_name}".format(role=role, member=member)
)) )
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(manage_roles=True) @checks.admin_or_permissions(manage_roles=True)
async def addrole(self, ctx: commands.Context, rolename: discord.Role, *, async def addrole(
user: MemberDefaultAuthor=None): self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
):
""" """
Adds a role to a user. If user is left blank it defaults to the Adds a role to a user. If user is left blank it defaults to the
author of the command. author of the command.
@ -151,8 +143,9 @@ class Admin:
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(manage_roles=True) @checks.admin_or_permissions(manage_roles=True)
async def removerole(self, ctx: commands.Context, rolename: discord.Role, *, async def removerole(
user: MemberDefaultAuthor=None): self, ctx: commands.Context, rolename: discord.Role, *, user: MemberDefaultAuthor = None
):
""" """
Removes a role from a user. If user is left blank it defaults to the Removes a role from a user. If user is left blank it defaults to the
author of the command. author of the command.
@ -173,9 +166,10 @@ class Admin:
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
@editrole.command(name="colour", aliases=["color", ]) @editrole.command(name="colour", aliases=["color"])
async def editrole_colour(self, ctx: commands.Context, role: discord.Role, async def editrole_colour(
value: discord.Colour): self, ctx: commands.Context, role: discord.Role, value: discord.Colour
):
"""Edits a role's colour """Edits a role's colour
Use double quotes if the role contains spaces. Use double quotes if the role contains spaces.
@ -185,8 +179,7 @@ class Admin:
!editrole colour \"The Transistor\" #ff0000 !editrole colour \"The Transistor\" #ff0000
!editrole colour Test #ff9900""" !editrole colour Test #ff9900"""
author = ctx.author author = ctx.author
reason = "{}({}) changed the colour of role '{}'".format( reason = "{}({}) changed the colour of role '{}'".format(author.name, author.id, role.name)
author.name, author.id, role.name)
if not self.pass_user_heirarchy_check(ctx, role): if not self.pass_user_heirarchy_check(ctx, role):
await self.complain(ctx, USER_HIERARCHY_ISSUE) await self.complain(ctx, USER_HIERARCHY_ISSUE)
@ -211,7 +204,8 @@ class Admin:
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(
author.name, author.id, old_name, name) author.name, author.id, old_name, name
)
if not self.pass_user_heirarchy_check(ctx, role): if not self.pass_user_heirarchy_check(ctx, role):
await self.complain(ctx, USER_HIERARCHY_ISSUE) await self.complain(ctx, USER_HIERARCHY_ISSUE)
@ -240,8 +234,7 @@ class Admin:
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, await self.complain(ctx, RUNNING_ANNOUNCEMENT, prefix=prefix)
prefix=prefix)
@announce.command(name="cancel") @announce.command(name="cancel")
@checks.is_owner() @checks.is_owner()
@ -259,7 +252,7 @@ class Admin:
@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):
""" """
Changes the channel on which the bot makes announcements. Changes the channel on which the bot makes announcements.
""" """
@ -267,14 +260,12 @@ class Admin:
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( await ctx.send("The announcement channel has been set to {}".format(channel.mention))
channel.mention
))
@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, *, guild: discord.Guild=None): async def announce_ignore(self, ctx, *, guild: discord.Guild = None):
""" """
Toggles whether the announcements will ignore the given server. Toggles whether the announcements will ignore the given server.
Defaults to the current server if none is provided. Defaults to the current server if none is provided.
@ -287,9 +278,7 @@ class Admin:
verb = "will" if ignored else "will not" verb = "will" if ignored else "will not"
await ctx.send("The server {} {} receive announcements.".format( await ctx.send("The server {} {} receive announcements.".format(guild.name, verb))
guild.name, verb
))
async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]: async def _valid_selfroles(self, guild: discord.Guild) -> Tuple[discord.Role]:
""" """
@ -384,8 +373,10 @@ class Admin:
await ctx.send("The bot {} serverlocked.".format(verb)) await ctx.send("The bot {} serverlocked.".format(verb))
# region Event Handlers # region Event Handlers
async def on_guild_join(self, guild: discord.Guild): async def on_guild_join(self, guild: discord.Guild):
if await self._serverlock_check(guild): if await self._serverlock_check(guild):
return return
# endregion # endregion

View File

@ -5,9 +5,8 @@ from discord.ext import commands
class Announcer: class Announcer:
def __init__(self, ctx: commands.Context,
message: str, def __init__(self, ctx: commands.Context, message: str, config=None):
config=None):
""" """
:param ctx: :param ctx:
:param message: :param message:
@ -65,10 +64,7 @@ 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( await bot_owner.send("I could not announce to server: {}".format(g.id))
g.id
))
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
self.active = False self.active = False

View File

@ -3,6 +3,7 @@ from discord.ext import commands
class MemberDefaultAuthor(commands.Converter): class MemberDefaultAuthor(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> discord.Member: async def convert(self, ctx: commands.Context, arg: str) -> discord.Member:
member_converter = commands.MemberConverter() member_converter = commands.MemberConverter()
try: try:
@ -16,6 +17,7 @@ class MemberDefaultAuthor(commands.Converter):
class SelfRole(commands.Converter): 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:
@ -28,6 +30,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" raise commands.BadArgument("The provided role is not a valid" " selfrole.")
" selfrole.")
return role return role

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../admin.py"]
'../admin.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -26,14 +26,9 @@ class Alias:
and append them to the stored alias and append them to the stored alias
""" """
default_global_settings = { default_global_settings = {"entries": []}
"entries": []
}
default_guild_settings = { default_guild_settings = {"enabled": False, "entries": []} # Going to be a list of dicts
"enabled": False,
"entries": [] # Going to be a list of dicts
}
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
@ -49,14 +44,17 @@ class Alias:
return (AliasEntry.from_json(d) for d in (await self._aliases.entries())) return (AliasEntry.from_json(d) for d in (await self._aliases.entries()))
async def loaded_aliases(self, guild: discord.Guild) -> Generator[AliasEntry, None, None]: async def loaded_aliases(self, guild: discord.Guild) -> Generator[AliasEntry, None, None]:
return (AliasEntry.from_json(d, bot=self.bot) return (
for d in (await self._aliases.guild(guild).entries())) AliasEntry.from_json(d, bot=self.bot)
for d in (await self._aliases.guild(guild).entries())
)
async def loaded_global_aliases(self) -> Generator[AliasEntry, None, None]: async def loaded_global_aliases(self) -> Generator[AliasEntry, None, None]:
return (AliasEntry.from_json(d, bot=self.bot) for d in (await self._aliases.entries())) return (AliasEntry.from_json(d, bot=self.bot) for d in (await self._aliases.entries()))
async def is_alias(self, guild: discord.Guild, alias_name: str, async def is_alias(
server_aliases: Iterable[AliasEntry]=()) -> (bool, AliasEntry): self, guild: discord.Guild, alias_name: str, server_aliases: Iterable[AliasEntry] = ()
) -> (bool, AliasEntry):
if not server_aliases: if not server_aliases:
server_aliases = await self.unloaded_aliases(guild) server_aliases = await self.unloaded_aliases(guild)
@ -76,10 +74,11 @@ class Alias:
@staticmethod @staticmethod
def is_valid_alias_name(alias_name: str) -> bool: def is_valid_alias_name(alias_name: str) -> bool:
return not bool(search(r'\s', alias_name)) and alias_name.isprintable() return not bool(search(r"\s", alias_name)) and alias_name.isprintable()
async def add_alias(self, ctx: commands.Context, alias_name: str, async def add_alias(
command: Tuple[str], global_: bool=False) -> AliasEntry: self, ctx: commands.Context, alias_name: str, command: Tuple[str], global_: bool = False
) -> AliasEntry:
alias = AliasEntry(alias_name, command, ctx.author, global_=global_) alias = AliasEntry(alias_name, command, ctx.author, global_=global_)
if global_: if global_:
@ -93,8 +92,9 @@ class Alias:
return alias return alias
async def delete_alias(self, ctx: commands.Context, alias_name: str, async def delete_alias(
global_: bool=False) -> bool: self, ctx: commands.Context, alias_name: str, global_: bool = False
) -> bool:
if global_: if global_:
settings = self._aliases settings = self._aliases
else: else:
@ -120,16 +120,15 @@ class Alias:
""" """
content = message.content content = message.content
prefix_list = await self.bot.command_prefix(self.bot, message) prefix_list = await self.bot.command_prefix(self.bot, message)
prefixes = sorted(prefix_list, prefixes = sorted(prefix_list, key=lambda pfx: len(pfx), reverse=True)
key=lambda pfx: len(pfx),
reverse=True)
for p in prefixes: for p in prefixes:
if content.startswith(p): if content.startswith(p):
return p return p
raise ValueError(_("No prefix found.")) raise ValueError(_("No prefix found."))
def get_extra_args_from_alias(self, message: discord.Message, prefix: str, def get_extra_args_from_alias(
alias: AliasEntry) -> str: self, message: discord.Message, prefix: str, alias: AliasEntry
) -> str:
""" """
When an alias is executed by a user in chat this function tries When an alias is executed by a user in chat this function tries
to get any extra arguments passed in with the call. to get any extra arguments passed in with the call.
@ -143,8 +142,9 @@ class Alias:
extra = message.content[known_content_length:].strip() extra = message.content[known_content_length:].strip()
return extra return extra
async def maybe_call_alias(self, message: discord.Message, async def maybe_call_alias(
aliases: Iterable[AliasEntry]=None): self, message: discord.Message, aliases: Iterable[AliasEntry] = None
):
try: try:
prefix = await self.get_prefix(message) prefix = await self.get_prefix(message)
except ValueError: except ValueError:
@ -155,13 +155,14 @@ class Alias:
except IndexError: except IndexError:
return False return False
is_alias, alias = await self.is_alias(message.guild, potential_alias, server_aliases=aliases) is_alias, alias = await self.is_alias(
message.guild, potential_alias, server_aliases=aliases
)
if is_alias: if is_alias:
await self.call_alias(message, prefix, alias) await self.call_alias(message, prefix, alias)
async def call_alias(self, message: discord.Message, prefix: str, async def call_alias(self, message: discord.Message, prefix: str, alias: AliasEntry):
alias: AliasEntry):
new_message = copy(message) new_message = copy(message)
args = self.get_extra_args_from_alias(message, prefix, alias) args = self.get_extra_args_from_alias(message, prefix, alias)
@ -181,83 +182,118 @@ class Alias:
""" """
Manage global aliases. Manage global aliases.
""" """
if ctx.invoked_subcommand is None or \ if ctx.invoked_subcommand is None or isinstance(ctx.invoked_subcommand, commands.Group):
isinstance(ctx.invoked_subcommand, commands.Group):
await ctx.send_help() await ctx.send_help()
@alias.command(name="add") @alias.command(name="add")
@commands.guild_only() @commands.guild_only()
async def _add_alias(self, ctx: commands.Context, async def _add_alias(self, ctx: commands.Context, alias_name: str, *, command):
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(_("You attempted to create a new alias" await ctx.send(
" with the name {} but that" _(
" name is already a command on this bot.").format(alias_name)) "You attempted to create a new alias"
" with the name {} but that"
" name is already a command on this bot."
).format(
alias_name
)
)
return return
is_alias, something_useless = await self.is_alias(ctx.guild, alias_name) is_alias, something_useless = await self.is_alias(ctx.guild, alias_name)
if is_alias: if is_alias:
await ctx.send(_("You attempted to create a new alias" await ctx.send(
" with the name {} but that" _(
" alias already exists on this server.").format(alias_name)) "You attempted to create a new alias"
" with the name {} but that"
" alias already exists on this server."
).format(
alias_name
)
)
return return
is_valid_name = self.is_valid_alias_name(alias_name) is_valid_name = self.is_valid_alias_name(alias_name)
if not is_valid_name: if not is_valid_name:
await ctx.send(_("You attempted to create a new alias" await ctx.send(
" with the name {} but that" _(
" name is an invalid alias name. Alias" "You attempted to create a new alias"
" names may not contain spaces.").format(alias_name)) " with the name {} but that"
" name is an invalid alias name. Alias"
" names may not contain spaces."
).format(
alias_name
)
)
return return
# endregion # endregion
# At this point we know we need to make a new alias # At this point we know we need to make a new alias
# and that the alias name is valid. # and that the alias name is valid.
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 `{}`" await ctx.send(
" has been created.").format(alias_name)) _("A new alias with the trigger `{}`" " has been created.").format(alias_name)
)
@global_.command(name="add") @global_.command(name="add")
async def _add_global_alias(self, ctx: commands.Context, async def _add_global_alias(self, ctx: commands.Context, alias_name: str, *, command):
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(_("You attempted to create a new global alias" await ctx.send(
" with the name {} but that" _(
" name is already a command on this bot.").format(alias_name)) "You attempted to create a new global alias"
" with the name {} but that"
" name is already a command on this bot."
).format(
alias_name
)
)
return return
is_alias, something_useless = await self.is_alias(ctx.guild, alias_name) is_alias, something_useless = await self.is_alias(ctx.guild, alias_name)
if is_alias: if is_alias:
await ctx.send(_("You attempted to create a new global alias" await ctx.send(
" with the name {} but that" _(
" alias already exists on this server.").format(alias_name)) "You attempted to create a new global alias"
" with the name {} but that"
" alias already exists on this server."
).format(
alias_name
)
)
return return
is_valid_name = self.is_valid_alias_name(alias_name) is_valid_name = self.is_valid_alias_name(alias_name)
if not is_valid_name: if not is_valid_name:
await ctx.send(_("You attempted to create a new global alias" await ctx.send(
" with the name {} but that" _(
" name is an invalid alias name. Alias" "You attempted to create a new global alias"
" names may not contain spaces.").format(alias_name)) " with the name {} but that"
" name is an invalid alias name. Alias"
" names may not contain spaces."
).format(
alias_name
)
)
return return
# endregion # endregion
await self.add_alias(ctx, alias_name, command, global_=True) await self.add_alias(ctx, alias_name, command, global_=True)
await ctx.send(_("A new global alias with the trigger `{}`" await ctx.send(
" has been created.").format(alias_name)) _("A new global alias with the trigger `{}`" " has been created.").format(alias_name)
)
@alias.command(name="help") @alias.command(name="help")
@commands.guild_only() @commands.guild_only()
@ -280,8 +316,11 @@ class Alias:
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(_("The `{}` alias will execute the" await ctx.send(
" command `{}`").format(alias_name, alias.command)) _("The `{}` alias will execute the" " command `{}`").format(
alias_name, 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 `{}`").format(alias_name))
@ -299,8 +338,9 @@ class Alias:
return return
if await self.delete_alias(ctx, alias_name): if await self.delete_alias(ctx, alias_name):
await ctx.send(_("Alias with the name `{}` was successfully" await ctx.send(
" deleted.").format(alias_name)) _("Alias with the name `{}` was successfully" " deleted.").format(alias_name)
)
else: else:
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name)) await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
@ -317,8 +357,9 @@ class Alias:
return return
if await self.delete_alias(ctx, alias_name, global_=True): if await self.delete_alias(ctx, alias_name, global_=True):
await ctx.send(_("Alias with the name `{}` was successfully" await ctx.send(
" deleted.").format(alias_name)) _("Alias with the name `{}` was successfully" " deleted.").format(alias_name)
)
else: else:
await ctx.send(_("Alias with name `{}` was not found.").format(alias_name)) await ctx.send(_("Alias with name `{}` was not found.").format(alias_name))
@ -328,7 +369,9 @@ class Alias:
""" """
Lists the available aliases on this server. Lists the available aliases on this server.
""" """
names = [_("Aliases:"), ] + sorted(["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))]) names = [_("Aliases:")] + sorted(
["+ " + a.name for a in (await self.unloaded_aliases(ctx.guild))]
)
if len(names) == 0: if len(names) == 0:
await ctx.send(_("There are no aliases on this server.")) await ctx.send(_("There are no aliases on this server."))
else: else:
@ -339,7 +382,9 @@ class Alias:
""" """
Lists the available global aliases on this bot. Lists the available global aliases on this bot.
""" """
names = [_("Aliases:"), ] + sorted(["+ " + a.name for a in await self.unloaded_global_aliases()]) names = [_("Aliases:")] + sorted(
["+ " + a.name for a in await self.unloaded_global_aliases()]
)
if len(names) == 0: if len(names) == 0:
await ctx.send(_("There are no aliases on this server.")) await ctx.send(_("There are no aliases on this server."))
else: else:

View File

@ -5,8 +5,10 @@ from redbot.core import commands
class AliasEntry: class AliasEntry:
def __init__(self, name: str, command: Tuple[str],
creator: discord.Member, global_: bool=False): def __init__(
self, name: str, command: Tuple[str], creator: discord.Member, global_: bool = False
):
super().__init__() super().__init__()
self.has_real_data = False self.has_real_data = False
self.name = name self.name = name
@ -43,13 +45,12 @@ class AliasEntry:
"creator": creator, "creator": creator,
"guild": guild, "guild": guild,
"global": self.global_, "global": self.global_,
"uses": self.uses "uses": self.uses,
} }
@classmethod @classmethod
def from_json(cls, data: dict, bot: commands.Bot=None): def from_json(cls, data: dict, bot: commands.Bot = None):
ret = cls(data["name"], data["command"], ret = cls(data["name"], data["command"], data["creator"], global_=data["global"])
data["creator"], global_=data["global"])
if bot: if bot:
ret.has_real_data = True ret.has_real_data = True

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../alias.py"]
'../alias.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -9,9 +9,10 @@ from redbot.core.data_manager import cog_data_path
import redbot.core import redbot.core
LAVALINK_DOWNLOAD_URL = ( LAVALINK_DOWNLOAD_URL = (
"https://github.com/Cog-Creators/Red-DiscordBot/" "https://github.com/Cog-Creators/Red-DiscordBot/" "releases/download/{}/Lavalink.jar"
"releases/download/{}/Lavalink.jar" ).format(
).format(redbot.core.__version__) redbot.core.__version__
)
LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio") LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio")
LAVALINK_JAR_FILE = LAVALINK_DOWNLOAD_DIR / "Lavalink.jar" LAVALINK_JAR_FILE = LAVALINK_DOWNLOAD_DIR / "Lavalink.jar"
@ -21,7 +22,7 @@ BUNDLED_APP_YML_FILE = Path(__file__).parent / "application.yml"
async def download_lavalink(session): async def download_lavalink(session):
with LAVALINK_JAR_FILE.open(mode='wb') as f: with LAVALINK_JAR_FILE.open(mode="wb") as f:
async with session.get(LAVALINK_DOWNLOAD_URL) as resp: async with session.get(LAVALINK_DOWNLOAD_URL) as resp:
while True: while True:
chunk = await resp.content.read(512) chunk = await resp.content.read(512)

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../audio.py"]
'../audio.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -5,7 +5,7 @@ from subprocess import Popen, DEVNULL, PIPE
import os import os
import logging import logging
log = logging.getLogger('red.audio.manager') log = logging.getLogger("red.audio.manager")
proc = None proc = None
SHUTDOWN = asyncio.Event() SHUTDOWN = asyncio.Event()
@ -13,7 +13,8 @@ SHUTDOWN = asyncio.Event()
def has_java_error(pid): def has_java_error(pid):
from . import LAVALINK_DOWNLOAD_DIR from . import LAVALINK_DOWNLOAD_DIR
poss_error_file = LAVALINK_DOWNLOAD_DIR / 'hs_err_pid{}.log'.format(pid)
poss_error_file = LAVALINK_DOWNLOAD_DIR / "hs_err_pid{}.log".format(pid)
return poss_error_file.exists() return poss_error_file.exists()
@ -29,14 +30,14 @@ async def monitor_lavalink_server(loop):
log.info("Restarting Lavalink jar.") log.info("Restarting Lavalink jar.")
await start_lavalink_server(loop) await start_lavalink_server(loop)
else: else:
log.error("Your Java is borked. Please find the hs_err_pid{}.log file" log.error(
" in the Audio data folder and report this issue.".format( "Your Java is borked. Please find the hs_err_pid{}.log file"
proc.pid " in the Audio data folder and report this issue.".format(proc.pid)
)) )
async def has_java(loop): async def has_java(loop):
java_available = shutil.which('java') is not None java_available = shutil.which("java") is not None
if not java_available: if not java_available:
return False return False
@ -48,20 +49,18 @@ async def get_java_version(loop):
""" """
This assumes we've already checked that java exists. This assumes we've already checked that java exists.
""" """
proc = Popen( proc = Popen(shlex.split("java -version", posix=os.name == "posix"), stdout=PIPE, stderr=PIPE)
shlex.split("java -version", posix=os.name == 'posix'),
stdout=PIPE, stderr=PIPE
)
_, err = proc.communicate() _, err = proc.communicate()
version_info = str(err, encoding='utf-8') version_info = str(err, encoding="utf-8")
version_line = version_info.split('\n')[0] version_line = version_info.split("\n")[0]
version_start = version_line.find('"') version_start = version_line.find('"')
version_string = version_line[version_start + 1:-1] version_string = version_line[version_start + 1:-1]
major, minor = version_string.split('.')[:2] major, minor = version_string.split(".")[:2]
return int(major), int(minor) return int(major), int(minor)
async def start_lavalink_server(loop): async def start_lavalink_server(loop):
java_available, java_version = await has_java(loop) java_available, java_version = await has_java(loop)
if not java_available: if not java_available:
@ -72,13 +71,15 @@ async def start_lavalink_server(loop):
extra_flags = "-Dsun.zip.disableMemoryMapping=true" extra_flags = "-Dsun.zip.disableMemoryMapping=true"
from . import LAVALINK_DOWNLOAD_DIR, LAVALINK_JAR_FILE from . import LAVALINK_DOWNLOAD_DIR, LAVALINK_JAR_FILE
start_cmd = "java {} -jar {}".format(extra_flags, LAVALINK_JAR_FILE.resolve()) start_cmd = "java {} -jar {}".format(extra_flags, LAVALINK_JAR_FILE.resolve())
global proc global proc
proc = Popen( proc = Popen(
shlex.split(start_cmd, posix=os.name == 'posix'), shlex.split(start_cmd, posix=os.name == "posix"),
cwd=str(LAVALINK_DOWNLOAD_DIR), cwd=str(LAVALINK_DOWNLOAD_DIR),
stdout=DEVNULL, stderr=DEVNULL stdout=DEVNULL,
stderr=DEVNULL,
) )
log.info("Lavalink jar started. PID: {}".format(proc.pid)) log.info("Lavalink jar started. PID: {}".format(proc.pid))

View File

@ -6,7 +6,7 @@ from redbot.core.i18n import Translator, cog_i18n
from redbot.core.bot import Red # Only used for type hints from redbot.core.bot import Red # Only used for type hints
_ = Translator('Bank', __file__) _ = Translator("Bank", __file__)
def check_global_setting_guildowner(): def check_global_setting_guildowner():
@ -14,6 +14,7 @@ def check_global_setting_guildowner():
Command decorator. If the bank is not global, it checks if the author is Command decorator. If the bank is not global, it checks if the author is
either the guildowner or has the administrator permission. either the guildowner or has the administrator permission.
""" """
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
author = ctx.author author = ctx.author
if await ctx.bot.is_owner(author): if await ctx.bot.is_owner(author):
@ -32,6 +33,7 @@ def check_global_setting_admin():
Command decorator. If the bank is not global, it checks if the author is Command decorator. If the bank is not global, it checks if the author is
either a bot admin or has the manage_guild permission. either a bot admin or has the manage_guild permission.
""" """
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
author = ctx.author author = ctx.author
if await ctx.bot.is_owner(author): if await ctx.bot.is_owner(author):
@ -73,19 +75,23 @@ class Bank:
currency_name = await bank._conf.guild(ctx.guild).currency() currency_name = await bank._conf.guild(ctx.guild).currency()
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\n" _(
"Bank name: {}\n" "Bank settings:\n\n"
"Currency: {}\n" "Bank name: {}\n"
"Default balance: {}" "Currency: {}\n"
"").format(bank_name, currency_name, default_balance) "Default balance: {}"
""
).format(
bank_name, currency_name, default_balance
)
) )
await ctx.send(box(settings)) await ctx.send(box(settings))
await ctx.send_help() await ctx.send_help()
@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 """Toggles whether the bank is global or not
If the bank is global, it will become per-server If the bank is global, it will become per-server
If the bank is per-server, it will become global""" If the bank is per-server, it will become global"""
@ -94,8 +100,10 @@ class Bank:
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 " _(
"in the process! If you're sure, type `{}`").format( "This will toggle the bank to be {}, deleting all accounts "
"in the process! If you're sure, type `{}`"
).format(
word, "{}bankset toggleglobal yes".format(ctx.prefix) word, "{}bankset toggleglobal yes".format(ctx.prefix)
) )
) )

View File

@ -1,6 +1,7 @@
class BankError(Exception): class BankError(Exception):
pass pass
class BankNotGlobal(BankError): class BankNotGlobal(BankError):
pass pass

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../bank.py"]
'../bank.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -27,13 +27,16 @@ class Cleanup:
Tries its best to cleanup after itself if the response is positive. Tries its best to cleanup after itself if the response is positive.
""" """
def author_check(message): def author_check(message):
return message.author == ctx.author return message.author == ctx.author
prompt = await ctx.send(_('Are you sure you want to delete {} messages? (y/n)').format(number)) prompt = await ctx.send(
response = await ctx.bot.wait_for('message', check=author_check) _("Are you sure you want to delete {} messages? (y/n)").format(number)
)
response = await ctx.bot.wait_for("message", check=author_check)
if response.content.lower().startswith('y'): if response.content.lower().startswith("y"):
await prompt.delete() await prompt.delete()
try: try:
await response.delete() await response.delete()
@ -41,14 +44,19 @@ class Cleanup:
pass pass
return True return True
else: else:
await ctx.send(_('Cancelled.')) await ctx.send(_("Cancelled."))
return False return False
@staticmethod @staticmethod
async def get_messages_for_deletion( async def get_messages_for_deletion(
ctx: commands.Context, channel: discord.TextChannel, number, ctx: commands.Context,
check=lambda x: True, limit=100, before=None, after=None, channel: discord.TextChannel,
delete_pinned=False number,
check=lambda x: True,
limit=100,
before=None,
after=None,
delete_pinned=False,
) -> list: ) -> list:
""" """
Gets a list of messages meeting the requirements to be deleted. Gets a list of messages meeting the requirements to be deleted.
@ -65,9 +73,7 @@ class Cleanup:
while not too_old and len(to_delete) - 1 < number: while not too_old and len(to_delete) - 1 < number:
message = None message = None
async for message in channel.history(limit=limit, async for message in channel.history(limit=limit, before=before, after=after):
before=before,
after=after):
if ( if (
(not number or len(to_delete) - 1 < number) (not number or len(to_delete) - 1 < number)
and check(message) and check(message)
@ -96,7 +102,9 @@ class Cleanup:
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True) @commands.bot_has_permissions(manage_messages=True)
async def text(self, ctx: commands.Context, text: str, number: int, delete_pinned: bool=False): async def text(
self, ctx: commands.Context, text: str, number: int, delete_pinned: bool = False
):
"""Deletes last X messages matching the specified text. """Deletes last X messages matching the specified text.
Example: Example:
@ -122,12 +130,18 @@ class Cleanup:
return False return False
to_delete = await self.get_messages_for_deletion( to_delete = await self.get_messages_for_deletion(
ctx, channel, number, check=check, limit=1000, before=ctx.message, ctx,
delete_pinned=delete_pinned) channel,
number,
check=check,
limit=1000,
before=ctx.message,
delete_pinned=delete_pinned,
)
reason = "{}({}) deleted {} messages "\ reason = "{}({}) deleted {} messages " " containing '{}' in channel {}.".format(
" containing '{}' in channel {}.".format(author.name, author.name, author.id, len(to_delete), text, channel.id
author.id, len(to_delete), text, channel.id) )
log.info(reason) log.info(reason)
if is_bot: if is_bot:
@ -138,7 +152,9 @@ class Cleanup:
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True) @commands.bot_has_permissions(manage_messages=True)
async def user(self, ctx: commands.Context, user: str, number: int, delete_pinned: bool=False): async def user(
self, ctx: commands.Context, user: str, number: int, delete_pinned: bool = False
):
"""Deletes last X messages from specified user. """Deletes last X messages from specified user.
Examples: Examples:
@ -174,13 +190,17 @@ class Cleanup:
return False return False
to_delete = await self.get_messages_for_deletion( to_delete = await self.get_messages_for_deletion(
ctx, channel, number, check=check, limit=1000, before=ctx.message, ctx,
delete_pinned=delete_pinned channel,
number,
check=check,
limit=1000,
before=ctx.message,
delete_pinned=delete_pinned,
)
reason = "{}({}) deleted {} messages " " made by {}({}) in channel {}." "".format(
author.name, author.id, len(to_delete), member or "???", _id, channel.name
) )
reason = "{}({}) deleted {} messages "\
" made by {}({}) in channel {}."\
"".format(author.name, author.id, len(to_delete),
member or '???', _id, channel.name)
log.info(reason) log.info(reason)
if is_bot: if is_bot:
@ -192,7 +212,7 @@ class Cleanup:
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True) @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. """Deletes all messages after specified message.
To get a message id, enable developer mode in Discord's To get a message id, enable developer mode in Discord's
@ -207,8 +227,7 @@ class Cleanup:
is_bot = self.bot.user.bot is_bot = self.bot.user.bot
if not is_bot: if not is_bot:
await ctx.send(_("This command can only be used on bots with " await ctx.send(_("This command can only be used on bots with " "bot accounts."))
"bot accounts."))
return return
after = await channel.get_message(message_id) after = await channel.get_message(message_id)
@ -221,9 +240,9 @@ class Cleanup:
ctx, channel, 0, limit=None, after=after, delete_pinned=delete_pinned ctx, channel, 0, limit=None, after=after, delete_pinned=delete_pinned
) )
reason = "{}({}) deleted {} messages in channel {}."\ reason = "{}({}) deleted {} messages in channel {}." "".format(
"".format(author.name, author.id, author.name, author.id, len(to_delete), channel.name
len(to_delete), channel.name) )
log.info(reason) log.info(reason)
await mass_purge(to_delete, channel) await mass_purge(to_delete, channel)
@ -231,7 +250,7 @@ class Cleanup:
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True) @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. """Deletes last X messages.
Example: Example:
@ -248,14 +267,13 @@ class Cleanup:
return return
to_delete = await self.get_messages_for_deletion( to_delete = await self.get_messages_for_deletion(
ctx, channel, number, limit=1000, before=ctx.message, ctx, channel, number, limit=1000, before=ctx.message, delete_pinned=delete_pinned
delete_pinned=delete_pinned
) )
to_delete.append(ctx.message) to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages in channel {}."\ reason = "{}({}) deleted {} messages in channel {}." "".format(
"".format(author.name, author.id, author.name, author.id, number, channel.name
number, channel.name) )
log.info(reason) log.info(reason)
if is_bot: if is_bot:
@ -263,10 +281,10 @@ class Cleanup:
else: else:
await slow_deletion(to_delete) await slow_deletion(to_delete)
@cleanup.command(name='bot') @cleanup.command(name="bot")
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True) @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.""" """Cleans up command messages and messages from the bot."""
channel = ctx.message.channel channel = ctx.message.channel
@ -278,13 +296,13 @@ class Cleanup:
if not cont: if not cont:
return return
prefixes = await self.bot.get_prefix(ctx.message) # This returns all server prefixes prefixes = await self.bot.get_prefix(ctx.message) # This returns all server prefixes
if isinstance(prefixes, str): if isinstance(prefixes, str):
prefixes = [prefixes] prefixes = [prefixes]
# In case some idiot sets a null prefix # In case some idiot sets a null prefix
if '' in prefixes: if "" in prefixes:
prefixes.remove('') prefixes.remove("")
def check(m): def check(m):
if m.author.id == self.bot.user.id: if m.author.id == self.bot.user.id:
@ -293,20 +311,24 @@ class Cleanup:
return True return True
p = discord.utils.find(m.content.startswith, prefixes) p = discord.utils.find(m.content.startswith, prefixes)
if p and len(p) > 0: if p and len(p) > 0:
cmd_name = m.content[len(p):].split(' ')[0] cmd_name = m.content[len(p):].split(" ")[0]
return bool(self.bot.get_command(cmd_name)) return bool(self.bot.get_command(cmd_name))
return False return False
to_delete = await self.get_messages_for_deletion( to_delete = await self.get_messages_for_deletion(
ctx, channel, number, check=check, limit=1000, before=ctx.message, ctx,
delete_pinned=delete_pinned channel,
number,
check=check,
limit=1000,
before=ctx.message,
delete_pinned=delete_pinned,
) )
to_delete.append(ctx.message) to_delete.append(ctx.message)
reason = "{}({}) deleted {} "\ reason = "{}({}) deleted {} " " command messages in channel {}." "".format(
" command messages in channel {}."\ author.name, author.id, len(to_delete), channel.name
"".format(author.name, author.id, len(to_delete), )
channel.name)
log.info(reason) log.info(reason)
if is_bot: if is_bot:
@ -314,10 +336,14 @@ class Cleanup:
else: else:
await slow_deletion(to_delete) await slow_deletion(to_delete)
@cleanup.command(name='self') @cleanup.command(name="self")
async def cleanup_self( async def cleanup_self(
self, ctx: commands.Context, number: int, self,
match_pattern: str = None, delete_pinned: bool=False): ctx: commands.Context,
number: int,
match_pattern: str = None,
delete_pinned: bool = False,
):
"""Cleans up messages owned by the bot. """Cleans 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,
@ -343,8 +369,7 @@ class Cleanup:
me = ctx.guild.me me = ctx.guild.me
can_mass_purge = channel.permissions_for(me).manage_messages can_mass_purge = channel.permissions_for(me).manage_messages
use_re = (match_pattern and match_pattern.startswith('r(') and use_re = (match_pattern and match_pattern.startswith("r(") and match_pattern.endswith(")"))
match_pattern.endswith(')'))
if use_re: if use_re:
match_pattern = match_pattern[1:] # strip 'r' match_pattern = match_pattern[1:] # strip 'r'
@ -352,10 +377,14 @@ class Cleanup:
def content_match(c): def content_match(c):
return bool(match_re.match(c)) return bool(match_re.match(c))
elif match_pattern: elif match_pattern:
def content_match(c): def content_match(c):
return match_pattern in c return match_pattern in c
else: else:
def content_match(_): def content_match(_):
return True return True
@ -367,8 +396,13 @@ class Cleanup:
return False return False
to_delete = await self.get_messages_for_deletion( to_delete = await self.get_messages_for_deletion(
ctx, channel, number, check=check, limit=1000, before=ctx.message, ctx,
delete_pinned=delete_pinned channel,
number,
check=check,
limit=1000,
before=ctx.message,
delete_pinned=delete_pinned,
) )
# Selfbot convenience, delete trigger message # Selfbot convenience, delete trigger message
@ -376,14 +410,13 @@ class Cleanup:
to_delete.append(ctx.message) to_delete.append(ctx.message)
if channel.name: if channel.name:
channel_name = 'channel ' + channel.name channel_name = "channel " + channel.name
else: else:
channel_name = str(channel) channel_name = str(channel)
reason = "{}({}) deleted {} messages "\ reason = "{}({}) deleted {} messages " "sent by the bot in {}." "".format(
"sent by the bot in {}."\ author.name, author.id, len(to_delete), channel_name
"".format(author.name, author.id, len(to_delete), )
channel_name)
log.info(reason) log.info(reason)
if is_bot and can_mass_purge: if is_bot and can_mass_purge:

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../cleanup.py"]
'../cleanup.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -27,8 +27,8 @@ class AlreadyExists(CCError):
class CommandObj: class CommandObj:
def __init__(self, **kwargs): def __init__(self, **kwargs):
config = kwargs.get('config') config = kwargs.get("config")
self.bot = kwargs.get('bot') self.bot = kwargs.get("bot")
self.db = config.guild self.db = config.guild
@staticmethod @staticmethod
@ -40,22 +40,27 @@ class CommandObj:
return customcommands return customcommands
async def get_responses(self, ctx): async def get_responses(self, ctx):
intro = (_("Welcome to the interactive random {} maker!\n" intro = (
"Every message you send will be added as one of the random " _(
"response to choose from once this {} is " "Welcome to the interactive random {} maker!\n"
"triggered. To exit this interactive menu, type `{}`").format( "Every message you send will be added as one of the random "
"customcommand", "customcommand", "exit()" "response to choose from once this {} is "
)) "triggered. To exit this interactive menu, type `{}`"
).format(
"customcommand", "customcommand", "exit()"
)
)
await ctx.send(intro) await ctx.send(intro)
def check(m): def check(m):
return m.channel == ctx.channel and m.author == ctx.message.author return m.channel == ctx.channel and m.author == ctx.message.author
responses = [] responses = []
while True: while True:
await ctx.send(_("Add a random response:")) await ctx.send(_("Add a random response:"))
msg = await self.bot.wait_for('message', check=check) msg = await self.bot.wait_for("message", check=check)
if msg.content.lower() == 'exit()': if msg.content.lower() == "exit()":
break break
else: else:
responses.append(msg.content) responses.append(msg.content)
@ -64,44 +69,31 @@ class CommandObj:
def get_now(self) -> str: def get_now(self) -> str:
# Get current time as a string, for 'created_at' and 'edited_at' fields # Get current time as a string, for 'created_at' and 'edited_at' fields
# 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, async def get(self, message: discord.Message, command: str) -> str:
message: discord.Message,
command: str) -> str:
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
else: else:
return ccinfo['response'] return ccinfo["response"]
async def create(self, async def create(self, ctx: commands.Context, command: str, response):
ctx: commands.Context,
command: str,
response):
"""Create a customcommand""" """Create a customcommand"""
# Check if this command is already registered as a customcommand # Check if this command is already registered as a customcommand
if await self.db(ctx.guild).commands.get_raw(command, default=None): if await self.db(ctx.guild).commands.get_raw(command, default=None):
raise AlreadyExists() raise AlreadyExists()
author = ctx.message.author author = ctx.message.author
ccinfo = { ccinfo = {
'author': { "author": {"id": author.id, "name": author.name},
'id': author.id, "command": command,
'name': author.name "created_at": self.get_now(),
}, "editors": [],
'command': command, "response": response,
'created_at': self.get_now(),
'editors': [],
'response': response
} }
await self.db(ctx.guild).commands.set_raw( await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
command, value=ccinfo)
async def edit(self, async def edit(self, ctx: commands.Context, command: str, response: None):
ctx: commands.Context,
command: str,
response: None):
"""Edit an already existing custom command""" """Edit an already existing custom command"""
# Check if this command is registered # Check if this command is registered
if not await self.db(ctx.guild).commands.get_raw(command, default=None): if not await self.db(ctx.guild).commands.get_raw(command, default=None):
@ -114,41 +106,31 @@ class CommandObj:
return m.channel == ctx.channel and m.author == ctx.message.author return m.channel == ctx.channel and m.author == ctx.message.author
if not response: if not response:
await ctx.send( await ctx.send(_("Do you want to create a 'randomized' cc? {}").format("y/n"))
_("Do you want to create a 'randomized' cc? {}").format("y/n")
)
msg = await self.bot.wait_for('message', check=check) msg = await self.bot.wait_for("message", check=check)
if msg.content.lower() == 'y': if msg.content.lower() == "y":
response = await self.get_responses(ctx=ctx) response = await self.get_responses(ctx=ctx)
else: else:
await ctx.send(_("What response do you want?")) await ctx.send(_("What response do you want?"))
response = (await self.bot.wait_for( response = (await self.bot.wait_for("message", check=check)).content
'message', check=check)
).content
ccinfo['response'] = response ccinfo["response"] = response
ccinfo['edited_at'] = self.get_now() ccinfo["edited_at"] = self.get_now()
if author.id not in ccinfo['editors']: if author.id not in ccinfo["editors"]:
# Add the person who invoked the `edit` coroutine to the list of # Add the person who invoked the `edit` coroutine to the list of
# editors, if the person is not yet in there # editors, if the person is not yet in there
ccinfo['editors'].append( ccinfo["editors"].append(author.id)
author.id
)
await self.db(ctx.guild).commands.set_raw( await self.db(ctx.guild).commands.set_raw(command, value=ccinfo)
command, value=ccinfo)
async def delete(self, async def delete(self, ctx: commands.Context, command: str):
ctx: commands.Context,
command: str):
"""Delete an already exisiting custom command""" """Delete an already exisiting custom command"""
# Check if this command is registered # Check if this command is registered
if not await self.db(ctx.guild).commands.get_raw(command, default=None): if not await self.db(ctx.guild).commands.get_raw(command, default=None):
raise NotFound() raise NotFound()
await self.db(ctx.guild).commands.set_raw( await self.db(ctx.guild).commands.set_raw(command, value=None)
command, value=None)
@cog_i18n(_) @cog_i18n(_)
@ -159,24 +141,20 @@ class CustomCommands:
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.key = 414589031223512 self.key = 414589031223512
self.config = Config.get_conf(self, self.config = Config.get_conf(self, self.key)
self.key)
self.config.register_guild(commands={}) self.config.register_guild(commands={})
self.commandobj = CommandObj(config=self.config, self.commandobj = CommandObj(config=self.config, bot=self.bot)
bot=self.bot)
@commands.group(aliases=["cc"], no_pm=True) @commands.group(aliases=["cc"], no_pm=True)
@commands.guild_only() @commands.guild_only()
async def customcom(self, async def customcom(self, ctx: commands.Context):
ctx: commands.Context):
"""Custom commands management""" """Custom commands management"""
if not ctx.invoked_subcommand: if not ctx.invoked_subcommand:
await ctx.send_help() await ctx.send_help()
@customcom.group(name="add") @customcom.group(name="add")
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def cc_add(self, async def cc_add(self, ctx: commands.Context):
ctx: commands.Context):
""" """
CCs can be enhanced with arguments: CCs can be enhanced with arguments:
@ -192,15 +170,12 @@ class CustomCommands:
{server} message.guild {server} message.guild
""" """
if not ctx.invoked_subcommand or isinstance(ctx.invoked_subcommand, if not ctx.invoked_subcommand or isinstance(ctx.invoked_subcommand, commands.Group):
commands.Group):
await ctx.send_help() await ctx.send_help()
@cc_add.command(name='random') @cc_add.command(name="random")
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
async def cc_add_random(self, async def cc_add_random(self, ctx: commands.Context, command: str):
ctx: commands.Context,
command: str):
""" """
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 is interactive
@ -210,26 +185,20 @@ class CustomCommands:
responses = await self.commandobj.get_responses(ctx=ctx) responses = await self.commandobj.get_responses(ctx=ctx)
try: try:
await self.commandobj.create(ctx=ctx, await self.commandobj.create(ctx=ctx, command=command, response=responses)
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 " _("This command already exists. Use " "`{}` to edit it.").format(
"`{}` to edit it.").format(
"{}customcom edit".format(ctx.prefix) "{}customcom edit".format(ctx.prefix)
)) )
)
# await ctx.send(str(responses)) # await ctx.send(str(responses))
@cc_add.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, async def cc_add_simple(self, ctx, command: str, *, text):
ctx,
command: str,
*,
text):
"""Adds a simple custom command """Adds a simple custom command
Example: Example:
[p]customcom add simple yourcommand Text you want [p]customcom add simple yourcommand Text you want
@ -240,24 +209,18 @@ class CustomCommands:
await ctx.send(_("That command is already a standard command.")) await ctx.send(_("That command is already a standard command."))
return return
try: try:
await self.commandobj.create(ctx=ctx, await self.commandobj.create(ctx=ctx, command=command, response=text)
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 " _("This command already exists. Use " "`{}` to edit it.").format(
"`{}` to edit it.").format(
"{}customcom edit".format(ctx.prefix) "{}customcom edit".format(ctx.prefix)
)) )
)
@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, async def cc_edit(self, ctx, command: str, *, text=None):
ctx,
command: str,
*,
text=None):
"""Edits a custom command """Edits a custom command
Example: Example:
[p]customcom edit yourcommand Text you want [p]customcom edit yourcommand Text you want
@ -266,61 +229,57 @@ class CustomCommands:
command = command.lower() command = command.lower()
try: try:
await self.commandobj.edit(ctx=ctx, await self.commandobj.edit(ctx=ctx, command=command, response=text)
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 " _("That command doesn't exist. Use " "`{}` to add it.").format(
"`{}` to add it.").format(
"{}customcom add".format(ctx.prefix) "{}customcom add".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, async def cc_delete(self, ctx, command: str):
ctx,
command: str):
"""Deletes a custom command """Deletes a custom command
Example: Example:
[p]customcom delete yourcommand""" [p]customcom delete yourcommand"""
guild = ctx.message.guild guild = ctx.message.guild
command = command.lower() command = command.lower()
try: try:
await self.commandobj.delete(ctx=ctx, await self.commandobj.delete(ctx=ctx, command=command)
command=command)
await ctx.send(_("Custom command successfully deleted.")) await ctx.send(_("Custom command successfully deleted."))
except NotFound: except NotFound:
await ctx.send(_("That command doesn't exist.")) await ctx.send(_("That command doesn't exist."))
@customcom.command(name="list") @customcom.command(name="list")
async def cc_list(self, async def cc_list(self, ctx):
ctx):
"""Shows custom commands list""" """Shows custom commands list"""
response = await CommandObj.get_commands(self.config.guild(ctx.guild)) response = await CommandObj.get_commands(self.config.guild(ctx.guild))
if not response: if not response:
await ctx.send(_( await ctx.send(
"There are no custom commands in this server." _(
" Use `{}` to start adding some.").format( "There are no custom commands in this server."
" Use `{}` to start adding some."
).format(
"{}customcom add".format(ctx.prefix) "{}customcom add".format(ctx.prefix)
)) )
)
return return
results = [] results = []
for command, body in response.items(): for command, body in response.items():
responses = body['response'] responses = body["response"]
if isinstance(responses, list): if isinstance(responses, list):
result = ", ".join(responses) result = ", ".join(responses)
elif isinstance(responses, str): elif isinstance(responses, str):
result = responses result = responses
else: else:
continue continue
results.append("{command:<15} : {result}".format(command=command, results.append("{command:<15} : {result}".format(command=command, result=result))
result=result))
commands = "\n".join(results) commands = "\n".join(results)
@ -330,14 +289,13 @@ class CustomCommands:
for page in pagify(commands, delims=[" ", "\n"]): for page in pagify(commands, delims=[" ", "\n"]):
await ctx.author.send(box(page)) await ctx.author.send(box(page))
async def on_message(self, async def on_message(self, message):
message):
is_private = isinstance(message.channel, discord.abc.PrivateChannel) is_private = isinstance(message.channel, discord.abc.PrivateChannel)
if len(message.content) < 2 or is_private: if len(message.content) < 2 or is_private:
return return
guild = message.guild guild = message.guild
prefixes = await self.bot.db.guild(guild).get_raw('prefix', default=[]) prefixes = await self.bot.db.guild(guild).get_raw("prefix", default=[])
if len(prefixes) < 1: if len(prefixes) < 1:
def_prefixes = await self.bot.get_prefix(message) def_prefixes = await self.bot.get_prefix(message)
@ -358,8 +316,7 @@ class CustomCommands:
if user_allowed: if user_allowed:
cmd = message.content[len(prefix):] cmd = message.content[len(prefix):]
try: try:
c = await self.commandobj.get(message=message, c = await self.commandobj.get(message=message, command=cmd)
command=cmd)
if isinstance(c, list): if isinstance(c, list):
command = random.choice(c) command = random.choice(c)
elif isinstance(c, str): elif isinstance(c, str):
@ -371,18 +328,14 @@ class CustomCommands:
response = self.format_cc(command, message) response = self.format_cc(command, message)
await message.channel.send(response) await message.channel.send(response)
def format_cc(self, def format_cc(self, command, message) -> str:
command,
message) -> str:
results = re.findall("\{([^}]+)\}", command) results = re.findall("\{([^}]+)\}", command)
for result in results: for result in results:
param = self.transform_parameter(result, message) param = self.transform_parameter(result, message)
command = command.replace("{" + result + "}", param) command = command.replace("{" + result + "}", param)
return command return command
def transform_parameter(self, def transform_parameter(self, result, message) -> str:
result,
message) -> str:
""" """
For security reasons only specific objects are allowed For security reasons only specific objects are allowed
Internals are ignored Internals are ignored
@ -393,7 +346,7 @@ class CustomCommands:
"author": message.author, "author": message.author,
"channel": message.channel, "channel": message.channel,
"guild": message.guild, "guild": message.guild,
"server": message.guild "server": message.guild,
} }
if result in objects: if result in objects:
return str(objects[result]) return str(objects[result])

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../customcom.py"]
'../customcom.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -16,49 +16,49 @@ class SpecResolver(object):
self.v2path = path self.v2path = path
self.resolved = set() self.resolved = set()
self.available_core_conversions = { self.available_core_conversions = {
'Bank Accounts': { "Bank Accounts": {
'cfg': ('Bank', None, 384734293238749), "cfg": ("Bank", None, 384734293238749),
'file': self.v2path / 'data' / 'economy' / 'bank.json', "file": self.v2path / "data" / "economy" / "bank.json",
'converter': self.bank_accounts_conv_spec "converter": self.bank_accounts_conv_spec,
}, },
'Economy Settings': { "Economy Settings": {
'cfg': ('Economy', 'config', 1256844281), "cfg": ("Economy", "config", 1256844281),
'file': self.v2path / 'data' / 'economy' / 'settings.json', "file": self.v2path / "data" / "economy" / "settings.json",
'converter': self.economy_conv_spec "converter": self.economy_conv_spec,
}, },
'Mod Log Cases': { "Mod Log Cases": {
'cfg': ('ModLog', None, 1354799444), "cfg": ("ModLog", None, 1354799444),
'file': self.v2path / 'data' / 'mod' / 'modlog.json', "file": self.v2path / "data" / "mod" / "modlog.json",
'converter': None # prevents from showing as available "converter": None, # prevents from showing as available
}, },
'Filter': { "Filter": {
'cfg': ('Filter', 'settings', 4766951341), "cfg": ("Filter", "settings", 4766951341),
'file': self.v2path / 'data' / 'mod' / 'filter.json', "file": self.v2path / "data" / "mod" / "filter.json",
'converter': self.filter_conv_spec "converter": self.filter_conv_spec,
}, },
'Past Names': { "Past Names": {
'cfg': ('Mod', 'settings', 4961522000), "cfg": ("Mod", "settings", 4961522000),
'file': self.v2path / 'data' / 'mod' / 'past_names.json', "file": self.v2path / "data" / "mod" / "past_names.json",
'converter': self.past_names_conv_spec "converter": self.past_names_conv_spec,
}, },
'Past Nicknames': { "Past Nicknames": {
'cfg': ('Mod', 'settings', 4961522000), "cfg": ("Mod", "settings", 4961522000),
'file': self.v2path / 'data' / 'mod' / 'past_nicknames.json', "file": self.v2path / "data" / "mod" / "past_nicknames.json",
'converter': self.past_nicknames_conv_spec "converter": self.past_nicknames_conv_spec,
},
"Custom Commands": {
"cfg": ("CustomCommands", "config", 414589031223512),
"file": self.v2path / "data" / "customcom" / "commands.json",
"converter": self.customcom_conv_spec,
}, },
'Custom Commands': {
'cfg': ('CustomCommands', 'config', 414589031223512),
'file': self.v2path / 'data' / 'customcom' / 'commands.json',
'converter': self.customcom_conv_spec
}
} }
@property @property
def available(self): def available(self):
return sorted( return sorted(
k for k, v in self.available_core_conversions.items() k
if v['file'].is_file() and v['converter'] is not None for k, v in self.available_core_conversions.items()
and k not in self.resolved if v["file"].is_file() and v["converter"] is not None and k not in self.resolved
) )
def unpack(self, parent_key, parent_value): def unpack(self, parent_key, parent_value):
@ -75,15 +75,8 @@ class SpecResolver(object):
"""Flatten a nested dictionary structure""" """Flatten a nested dictionary structure"""
dictionary = {(key,): value for key, value in dictionary.items()} dictionary = {(key,): value for key, value in dictionary.items()}
while True: while True:
dictionary = dict( dictionary = dict(chain.from_iterable(starmap(self.unpack, dictionary.items())))
chain.from_iterable( if not any(isinstance(value, dict) for value in dictionary.values()):
starmap(self.unpack, dictionary.items())
)
)
if not any(
isinstance(value, dict)
for value in dictionary.values()
):
break break
return dictionary return dictionary
@ -97,11 +90,8 @@ class SpecResolver(object):
outerkey, innerkey = tuple(k[:-1]), (k[-1],) outerkey, innerkey = tuple(k[:-1]), (k[-1],)
if outerkey not in ret: if outerkey not in ret:
ret[outerkey] = {} ret[outerkey] = {}
if innerkey[0] == 'created_at': if innerkey[0] == "created_at":
x = int( x = int(datetime.strptime(v, "%Y-%m-%d %H:%M:%S").timestamp())
datetime.strptime(
v, "%Y-%m-%d %H:%M:%S").timestamp()
)
ret[outerkey].update({innerkey: x}) ret[outerkey].update({innerkey: x})
else: else:
ret[outerkey].update({innerkey: v}) ret[outerkey].update({innerkey: v})
@ -121,16 +111,10 @@ class SpecResolver(object):
raise NotImplementedError("This one isn't ready yet") raise NotImplementedError("This one isn't ready yet")
def filter_conv_spec(self, data: dict): def filter_conv_spec(self, data: dict):
return { return {(Config.GUILD, k): {("filter",): v} for k, v in data.items()}
(Config.GUILD, k): {('filter',): v}
for k, v in data.items()
}
def past_names_conv_spec(self, data: dict): def past_names_conv_spec(self, data: dict):
return { return {(Config.USER, k): {("past_names",): v} for k, v in data.items()}
(Config.USER, k): {('past_names',): v}
for k, v in data.items()
}
def past_nicknames_conv_spec(self, data: dict): def past_nicknames_conv_spec(self, data: dict):
flatscoped = self.apply_scope(Config.MEMBER, self.flatten_dict(data)) flatscoped = self.apply_scope(Config.MEMBER, self.flatten_dict(data))
@ -146,19 +130,16 @@ class SpecResolver(object):
flatscoped = self.apply_scope(Config.GUILD, self.flatten_dict(data)) flatscoped = self.apply_scope(Config.GUILD, self.flatten_dict(data))
ret = {} ret = {}
for k, v in flatscoped.items(): for k, v in flatscoped.items():
outerkey, innerkey = (*k[:-1],), ('commands', k[-1]) outerkey, innerkey = (*k[:-1],), ("commands", k[-1])
if outerkey not in ret: if outerkey not in ret:
ret[outerkey] = {} ret[outerkey] = {}
ccinfo = { ccinfo = {
'author': { "author": {"id": 42, "name": "Converted from a v2 instance"},
'id': 42, "command": k[-1],
'name': 'Converted from a v2 instance' "created_at": "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow()),
}, "editors": [],
'command': k[-1], "response": v,
'created_at': '{:%d/%m/%Y %H:%M:%S}'.format(datetime.utcnow()),
'editors': [],
'response': v
} }
ret[outerkey].update({innerkey: ccinfo}) ret[outerkey].update({innerkey: ccinfo})
return ret return ret
@ -168,8 +149,8 @@ class SpecResolver(object):
raise NotImplementedError("No Conversion Specs for this") raise NotImplementedError("No Conversion Specs for this")
info = self.available_core_conversions[prettyname] info = self.available_core_conversions[prettyname]
filepath, converter = info['file'], info['converter'] filepath, converter = info["file"], info["converter"]
(cogname, attr, _id) = info['cfg'] (cogname, attr, _id) = info["cfg"]
try: try:
config = getattr(bot.get_cog(cogname), attr) config = getattr(bot.get_cog(cogname), attr)
except (TypeError, AttributeError): except (TypeError, AttributeError):

View File

@ -7,7 +7,7 @@ from redbot.core.i18n import Translator, cog_i18n
from redbot.cogs.dataconverter.core_specs import SpecResolver from redbot.cogs.dataconverter.core_specs import SpecResolver
from redbot.core.utils.chat_formatting import box from redbot.core.utils.chat_formatting import box
_ = Translator('DataConverter', __file__) _ = Translator("DataConverter", __file__)
@cog_i18n(_) @cog_i18n(_)
@ -34,13 +34,14 @@ class DataConverter:
if not resolver.available: if not resolver.available:
return await ctx.send( return await ctx.send(
_("There don't seem to be any data files I know how to " _(
"handle here. Are you sure you gave me the base " "There don't seem to be any data files I know how to "
"installation path?") "handle here. Are you sure you gave me the base "
"installation path?"
)
) )
while resolver.available: while resolver.available:
menu = _("Please select a set of data to import by number" menu = _("Please select a set of data to import by number" ", or 'exit' to exit")
", or 'exit' to exit")
for index, entry in enumerate(resolver.available, 1): for index, entry in enumerate(resolver.available, 1):
menu += "\n{}. {}".format(index, entry) menu += "\n{}. {}".format(index, entry)
@ -50,24 +51,17 @@ class DataConverter:
return m.channel == ctx.channel and m.author == ctx.author return m.channel == ctx.channel and m.author == ctx.author
try: try:
message = await self.bot.wait_for( message = await self.bot.wait_for("message", check=pred, timeout=60)
'message', check=pred, timeout=60
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
return await ctx.send( return await ctx.send(_("Try this again when you are more ready"))
_('Try this again when you are more ready'))
else: else:
if message.content.strip().lower() in [ if message.content.strip().lower() in ["quit", "exit", "-1", "q", "cancel"]:
'quit', 'exit', '-1', 'q', 'cancel'
]:
return await ctx.tick() return await ctx.tick()
try: try:
message = int(message.content.strip()) message = int(message.content.strip())
to_conv = resolver.available[message - 1] to_conv = resolver.available[message - 1]
except (ValueError, IndexError): except (ValueError, IndexError):
await ctx.send( await ctx.send(_("That wasn't a valid choice."))
_("That wasn't a valid choice.")
)
continue continue
else: else:
async with ctx.typing(): async with ctx.typing():
@ -76,6 +70,8 @@ class DataConverter:
await menu_message.delete() await menu_message.delete()
else: else:
return await ctx.send( return await ctx.send(
_("There isn't anything else I know how to convert here." _(
"\nThere might be more things I can convert in the future.") "There isn't anything else I know how to convert here."
"\nThere might be more things I can convert in the future."
)
) )

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../dataconverter.py"]
'../dataconverter.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -3,7 +3,7 @@ import asyncio
import discord import discord
from discord.ext import commands from discord.ext import commands
__all__ = ["install_agreement", ] __all__ = ["install_agreement"]
REPO_INSTALL_MSG = ( 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"
@ -17,29 +17,28 @@ REPO_INSTALL_MSG = (
def install_agreement(): def install_agreement():
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
downloader = ctx.command.instance downloader = ctx.command.instance
if downloader is None: if downloader is None:
return True return True
elif downloader.already_agreed: elif downloader.already_agreed:
return True return True
elif ctx.invoked_subcommand is None or \ elif ctx.invoked_subcommand is None or isinstance(ctx.invoked_subcommand, commands.Group):
isinstance(ctx.invoked_subcommand, commands.Group):
return True return True
def does_agree(msg: discord.Message): def does_agree(msg: discord.Message):
return ctx.author == msg.author and \ return ctx.author == msg.author and ctx.channel == msg.channel and msg.content == "I agree"
ctx.channel == msg.channel and \
msg.content == "I agree"
await ctx.send(REPO_INSTALL_MSG) await ctx.send(REPO_INSTALL_MSG)
try: try:
await ctx.bot.wait_for('message', check=does_agree, timeout=30) await ctx.bot.wait_for("message", check=does_agree, 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
return True return True
return commands.check(pred) return commands.check(pred)

View File

@ -5,6 +5,7 @@ from .installable import Installable
class InstalledCog(commands.Converter): class InstalledCog(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> Installable: async def convert(self, 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:
@ -12,8 +13,6 @@ class InstalledCog(commands.Converter):
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( raise commands.BadArgument("That cog is not installed")
"That cog is not installed"
)
return cog return cog

View File

@ -22,20 +22,18 @@ from .installable import Installable
from .log import log from .log import log
from .repo_manager import RepoManager, Repo from .repo_manager import RepoManager, Repo
_ = Translator('Downloader', __file__) _ = Translator("Downloader", __file__)
@cog_i18n(_) @cog_i18n(_)
class Downloader: class Downloader:
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
self.conf = Config.get_conf(self, identifier=998240343, self.conf = Config.get_conf(self, identifier=998240343, force_registration=True)
force_registration=True)
self.conf.register_global( self.conf.register_global(installed=[])
installed=[]
)
self.already_agreed = False self.already_agreed = False
@ -46,7 +44,7 @@ class Downloader:
self.LIB_PATH.mkdir(parents=True, exist_ok=True) self.LIB_PATH.mkdir(parents=True, exist_ok=True)
self.SHAREDLIB_PATH.mkdir(parents=True, exist_ok=True) self.SHAREDLIB_PATH.mkdir(parents=True, exist_ok=True)
if not self.SHAREDLIB_INIT.exists(): if not self.SHAREDLIB_INIT.exists():
with self.SHAREDLIB_INIT.open(mode='w', encoding='utf-8') as _: with self.SHAREDLIB_INIT.open(mode="w", encoding="utf-8") as _:
pass pass
if str(self.LIB_PATH) not in syspath: if str(self.LIB_PATH) not in syspath:
@ -170,7 +168,7 @@ class Downloader:
for repo, reqs in has_reqs: for repo, reqs in has_reqs:
for req in reqs: for req in reqs:
# noinspection PyTypeChecker # noinspection PyTypeChecker
ret = ret and await repo.install_raw_requirements([req, ], self.LIB_PATH) ret = ret and await repo.install_raw_requirements([req], self.LIB_PATH)
return ret return ret
@staticmethod @staticmethod
@ -200,8 +198,12 @@ class Downloader:
if success: if success:
await ctx.send(_("Libraries installed.")) await ctx.send(_("Libraries installed."))
else: else:
await ctx.send(_("Some libraries failed to install. Please check" await ctx.send(
" your logs for a complete list.")) _(
"Some libraries failed to install. Please check"
" your logs for a complete list."
)
)
@commands.group() @commands.group()
@checks.is_owner() @checks.is_owner()
@ -214,7 +216,7 @@ class Downloader:
@repo.command(name="add") @repo.command(name="add")
@install_agreement() @install_agreement()
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 to Downloader. Add a new repo to Downloader.
@ -223,11 +225,7 @@ class Downloader:
""" """
try: try:
# noinspection PyTypeChecker # noinspection PyTypeChecker
repo = await self._repo_manager.add_repo( repo = await self._repo_manager.add_repo(name=name, url=repo_url, branch=branch)
name=name,
url=repo_url,
branch=branch
)
except ExistingGitRepo: except ExistingGitRepo:
await ctx.send(_("That git repo has already been added under another name.")) await ctx.send(_("That git repo has already been added under another name."))
except CloningError: except CloningError:
@ -275,20 +273,28 @@ class Downloader:
""" """
cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable cog = discord.utils.get(repo_name.available_cogs, name=cog_name) # type: Installable
if cog is None: if cog is None:
await ctx.send(_("Error, there is no cog by the name of" await ctx.send(
" `{}` in the `{}` repo.").format(cog_name, repo_name.name)) _("Error, there is no cog by the name of" " `{}` in the `{}` repo.").format(
cog_name, repo_name.name
)
)
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( _(
'.'.join([str(n) for n in cog.min_python_version]) "This cog requires at least python version {}, aborting install.".format(
".".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_name.install_requirements(cog, self.LIB_PATH):
await ctx.send(_("Failed to install the required libraries for" await ctx.send(
" `{}`: `{}`").format(cog.name, cog.requirements)) _("Failed to install the required libraries for" " `{}`: `{}`").format(
cog.name, cog.requirements
)
)
return return
await repo_name.install_cog(cog, await self.cog_install_path()) await repo_name.install_cog(cog, await self.cog_install_path())
@ -317,12 +323,16 @@ class Downloader:
await self._remove_from_installed(cog_name) await self._remove_from_installed(cog_name)
await ctx.send(_("`{}` was successfully removed.").format(real_name)) await ctx.send(_("`{}` was successfully removed.").format(real_name))
else: else:
await ctx.send(_("That cog was installed but can no longer" await ctx.send(
" be located. You may need to remove it's" _(
" files manually if it is still usable.")) "That cog was installed but can no longer"
" be located. You may need to remove it's"
" files manually if it is still usable."
)
)
@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):
""" """
Updates all cogs or one of your choosing. Updates all cogs or one of your choosing.
""" """
@ -358,7 +368,8 @@ class Downloader:
""" """
cogs = repo_name.available_cogs cogs = repo_name.available_cogs
cogs = _("Available Cogs:\n") + "\n".join( cogs = _("Available Cogs:\n") + "\n".join(
["+ {}: {}".format(c.name, c.short or "") for c in cogs]) ["+ {}: {}".format(c.name, c.short or "") for c in cogs]
)
await ctx.send(box(cogs, lang="diff")) await ctx.send(box(cogs, lang="diff"))
@ -369,9 +380,9 @@ class Downloader:
""" """
cog = discord.utils.get(repo_name.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(_("There is no cog `{}` in the repo `{}`").format( await ctx.send(
cog_name, repo_name.name _("There is no cog `{}` in the repo `{}`").format(cog_name, repo_name.name)
)) )
return return
msg = _("Information on {}:\n{}").format(cog.name, cog.description or "") msg = _("Information on {}:\n{}").format(cog.name, cog.description or "")
@ -397,8 +408,9 @@ class Downloader:
return True, installable return True, installable
return False, None return False, None
def format_findcog_info(self, command_name: str, def format_findcog_info(
cog_installable: Union[Installable, object]=None) -> str: self, command_name: str, cog_installable: Union[Installable, object] = None
) -> str:
"""Format a cog's info for output to discord. """Format a cog's info for output to discord.
Parameters Parameters
@ -444,7 +456,7 @@ class Downloader:
The name of the cog according to Downloader.. The name of the cog according to Downloader..
""" """
splitted = instance.__module__.split('.') splitted = instance.__module__.split(".")
return splitted[-2] return splitted[-2]
@commands.command() @commands.command()

View File

@ -1,6 +1,16 @@
__all__ = ["DownloaderException", "GitException", "InvalidRepoName", "ExistingGitRepo", __all__ = [
"MissingGitRepo", "CloningError", "CurrentHashError", "HardResetError", "DownloaderException",
"UpdateError", "GitDiffError", "PipError"] "GitException",
"InvalidRepoName",
"ExistingGitRepo",
"MissingGitRepo",
"CloningError",
"CurrentHashError",
"HardResetError",
"UpdateError",
"GitDiffError",
"PipError",
]
class DownloaderException(Exception): class DownloaderException(Exception):

View File

@ -56,6 +56,7 @@ class Installable(RepoJSONMixin):
:class:`InstallationType`. :class:`InstallationType`.
""" """
def __init__(self, location: Path): def __init__(self, location: Path):
"""Base installable initializer. """Base installable initializer.
@ -114,13 +115,9 @@ class Installable(RepoJSONMixin):
# noinspection PyBroadException # noinspection PyBroadException
try: try:
copy_func( copy_func(src=str(self._location), dst=str(target_dir / self._location.stem))
src=str(self._location),
dst=str(target_dir / self._location.stem)
)
except: except:
log.exception("Error occurred when copying path:" log.exception("Error occurred when copying path:" " {}".format(self._location))
" {}".format(self._location))
return False return False
return True return True
@ -130,7 +127,7 @@ class Installable(RepoJSONMixin):
if self._info_file.exists(): if self._info_file.exists():
self._process_info_file() self._process_info_file()
def _process_info_file(self, info_file_path: Path=None) -> MutableMapping[str, Any]: def _process_info_file(self, info_file_path: Path = None) -> MutableMapping[str, Any]:
""" """
Processes an information file. Loads dependencies among other Processes an information file. Loads dependencies among other
information into this object. information into this object.
@ -144,13 +141,14 @@ class Installable(RepoJSONMixin):
raise ValueError("No valid information file path was found.") raise ValueError("No valid information file path was found.")
info = {} info = {}
with info_file_path.open(encoding='utf-8') as f: with info_file_path.open(encoding="utf-8") as f:
try: try:
info = json.load(f) info = json.load(f)
except json.JSONDecodeError: except json.JSONDecodeError:
info = {} info = {}
log.exception("Invalid JSON information file at path:" log.exception(
" {}".format(info_file_path)) "Invalid JSON information file at path:" " {}".format(info_file_path)
)
else: else:
self._info = info self._info = info
@ -167,7 +165,7 @@ class Installable(RepoJSONMixin):
self.bot_version = bot_version self.bot_version = bot_version
try: try:
min_python_version = tuple(info.get('min_python_version', [3, 5, 1])) min_python_version = tuple(info.get("min_python_version", [3, 5, 1]))
except ValueError: except ValueError:
min_python_version = self.min_python_version min_python_version = self.min_python_version
self.min_python_version = min_python_version self.min_python_version = min_python_version
@ -200,15 +198,12 @@ class Installable(RepoJSONMixin):
return info return info
def to_json(self): def to_json(self):
return { return {"repo_name": self.repo_name, "cog_name": self.name}
"repo_name": self.repo_name,
"cog_name": self.name
}
@classmethod @classmethod
def from_json(cls, data: dict, repo_mgr: "RepoManager"): def from_json(cls, data: dict, repo_mgr: "RepoManager"):
repo_name = data['repo_name'] repo_name = data["repo_name"]
cog_name = data['cog_name'] cog_name = data["cog_name"]
repo = repo_mgr.get_repo(repo_name) repo = repo_mgr.get_repo(repo_name)
if repo is not None: if repo is not None:

View File

@ -24,7 +24,7 @@ class RepoJSONMixin:
return return
try: try:
with self._info_file.open(encoding='utf-8') as f: with self._info_file.open(encoding="utf-8") as f:
info = json.load(f) info = json.load(f)
except json.JSONDecodeError: except json.JSONDecodeError:
return return

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../downloader.py"]
'../downloader.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -27,16 +27,23 @@ class Repo(RepoJSONMixin):
GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}" GIT_LATEST_COMMIT = "git -C {path} rev-parse {branch}"
GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q" GIT_HARD_RESET = "git -C {path} reset --hard origin/{branch} -q"
GIT_PULL = "git -C {path} pull -q --ff-only" GIT_PULL = "git -C {path} pull -q --ff-only"
GIT_DIFF_FILE_STATUS = ("git -C {path} diff --no-commit-id --name-status" GIT_DIFF_FILE_STATUS = (
" {old_hash} {new_hash}") "git -C {path} diff --no-commit-id --name-status" " {old_hash} {new_hash}"
GIT_LOG = ("git -C {path} log --relative-date --reverse {old_hash}.." )
" {relative_file_path}") GIT_LOG = ("git -C {path} log --relative-date --reverse {old_hash}.." " {relative_file_path}")
GIT_DISCOVER_REMOTE_URL = "git -C {path} config --get remote.origin.url" GIT_DISCOVER_REMOTE_URL = "git -C {path} config --get remote.origin.url"
PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}" PIP_INSTALL = "{python} -m pip install -U -t {target_dir} {reqs}"
def __init__(self, name: str, url: str, branch: str, folder_path: Path, def __init__(
available_modules: Tuple[Installable]=(), loop: asyncio.AbstractEventLoop=None): self,
name: str,
url: str,
branch: str,
folder_path: Path,
available_modules: Tuple[Installable] = (),
loop: asyncio.AbstractEventLoop = None,
):
self.url = url self.url = url
self.branch = branch self.branch = branch
@ -71,11 +78,12 @@ class Repo(RepoJSONMixin):
return poss_repo return poss_repo
def _existing_git_repo(self) -> (bool, Path): def _existing_git_repo(self) -> (bool, Path):
git_path = self.folder_path / '.git' git_path = self.folder_path / ".git"
return git_path.exists(), git_path return git_path.exists(), git_path
async def _get_file_update_statuses( async def _get_file_update_statuses(
self, old_hash: str, new_hash: str) -> MutableMapping[str, str]: self, old_hash: str, new_hash: str
) -> MutableMapping[str, str]:
""" """
Gets the file update status letters for each changed file between Gets the file update status letters for each changed file between
the two hashes. the two hashes.
@ -85,29 +93,25 @@ class Repo(RepoJSONMixin):
""" """
p = await self._run( p = await self._run(
self.GIT_DIFF_FILE_STATUS.format( self.GIT_DIFF_FILE_STATUS.format(
path=self.folder_path, path=self.folder_path, old_hash=old_hash, new_hash=new_hash
old_hash=old_hash,
new_hash=new_hash
) )
) )
if p.returncode != 0: if p.returncode != 0:
raise GitDiffError("Git diff failed for repo at path:" raise GitDiffError("Git diff failed for repo at path:" " {}".format(self.folder_path))
" {}".format(self.folder_path))
stdout = p.stdout.strip().decode().split('\n') stdout = p.stdout.strip().decode().split("\n")
ret = {} ret = {}
for filename in stdout: for filename in stdout:
# TODO: filter these filenames by ones in self.available_modules # TODO: filter these filenames by ones in self.available_modules
status, _, filepath = filename.partition('\t') status, _, filepath = filename.partition("\t")
ret[filepath] = status ret[filepath] = status
return ret return ret
async def _get_commit_notes(self, old_commit_hash: str, async def _get_commit_notes(self, old_commit_hash: str, relative_file_path: str) -> str:
relative_file_path: str) -> str:
""" """
Gets the commit notes from git log. Gets the commit notes from git log.
:param old_commit_hash: Point in time to start getting messages :param old_commit_hash: Point in time to start getting messages
@ -119,13 +123,15 @@ class Repo(RepoJSONMixin):
self.GIT_LOG.format( self.GIT_LOG.format(
path=self.folder_path, path=self.folder_path,
old_hash=old_commit_hash, old_hash=old_commit_hash,
relative_file_path=relative_file_path relative_file_path=relative_file_path,
) )
) )
if p.returncode != 0: if p.returncode != 0:
raise GitException("An exception occurred while executing git log on" raise GitException(
" this repo: {}".format(self.folder_path)) "An exception occurred while executing git log on"
" this repo: {}".format(self.folder_path)
)
return p.stdout.decode().strip() return p.stdout.decode().strip()
@ -146,10 +152,8 @@ class Repo(RepoJSONMixin):
Installable(location=name) Installable(location=name)
) )
""" """
for file_finder, name, is_pkg in pkgutil.walk_packages(path=[str(self.folder_path), ]): for file_finder, name, is_pkg in pkgutil.walk_packages(path=[str(self.folder_path)]):
curr_modules.append( curr_modules.append(Installable(location=self.folder_path / name))
Installable(location=self.folder_path / name)
)
self.available_modules = curr_modules self.available_modules = curr_modules
# noinspection PyTypeChecker # noinspection PyTypeChecker
@ -157,12 +161,11 @@ class Repo(RepoJSONMixin):
async def _run(self, *args, **kwargs): async def _run(self, *args, **kwargs):
env = os.environ.copy() env = os.environ.copy()
env['GIT_TERMINAL_PROMPT'] = '0' env["GIT_TERMINAL_PROMPT"] = "0"
kwargs['env'] = env kwargs["env"] = env
async with self._repo_lock: async with self._repo_lock:
return await self._loop.run_in_executor( return await self._loop.run_in_executor(
self._executor, self._executor, functools.partial(sp_run, *args, stdout=PIPE, **kwargs)
functools.partial(sp_run, *args, stdout=PIPE, **kwargs)
) )
async def clone(self) -> Tuple[str]: async def clone(self) -> Tuple[str]:
@ -176,24 +179,17 @@ class Repo(RepoJSONMixin):
""" """
exists, path = self._existing_git_repo() exists, path = self._existing_git_repo()
if exists: if exists:
raise ExistingGitRepo( raise ExistingGitRepo("A git repo already exists at path: {}".format(path))
"A git repo already exists at path: {}".format(path)
)
if self.branch is not None: if self.branch is not None:
p = await self._run( p = await self._run(
self.GIT_CLONE.format( self.GIT_CLONE.format(
branch=self.branch, branch=self.branch, url=self.url, folder=self.folder_path
url=self.url,
folder=self.folder_path
).split() ).split()
) )
else: else:
p = await self._run( p = await self._run(
self.GIT_CLONE_NO_BRANCH.format( self.GIT_CLONE_NO_BRANCH.format(url=self.url, folder=self.folder_path).split()
url=self.url,
folder=self.folder_path
).split()
) )
if p.returncode != 0: if p.returncode != 0:
@ -217,23 +213,18 @@ class Repo(RepoJSONMixin):
""" """
exists, _ = self._existing_git_repo() exists, _ = self._existing_git_repo()
if not exists: if not exists:
raise MissingGitRepo( raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
"A git repo does not exist at path: {}".format(self.folder_path)
)
p = await self._run( p = await self._run(self.GIT_CURRENT_BRANCH.format(path=self.folder_path).split())
self.GIT_CURRENT_BRANCH.format(
path=self.folder_path
).split()
)
if p.returncode != 0: if p.returncode != 0:
raise GitException("Could not determine current branch" raise GitException(
" at path: {}".format(self.folder_path)) "Could not determine current branch" " at path: {}".format(self.folder_path)
)
return p.stdout.decode().strip() return p.stdout.decode().strip()
async def current_commit(self, branch: str=None) -> str: async def current_commit(self, branch: str = None) -> str:
"""Determine the current commit hash of the repo. """Determine the current commit hash of the repo.
Parameters Parameters
@ -252,15 +243,10 @@ class Repo(RepoJSONMixin):
exists, _ = self._existing_git_repo() exists, _ = self._existing_git_repo()
if not exists: if not exists:
raise MissingGitRepo( raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
"A git repo does not exist at path: {}".format(self.folder_path)
)
p = await self._run( p = await self._run(
self.GIT_LATEST_COMMIT.format( self.GIT_LATEST_COMMIT.format(path=self.folder_path, branch=branch).split()
path=self.folder_path,
branch=branch
).split()
) )
if p.returncode != 0: if p.returncode != 0:
@ -268,7 +254,7 @@ class Repo(RepoJSONMixin):
return p.stdout.decode().strip() return p.stdout.decode().strip()
async def current_url(self, folder: Path=None) -> str: async def current_url(self, folder: Path = None) -> str:
""" """
Discovers the FETCH URL for a Git repo. Discovers the FETCH URL for a Git repo.
@ -290,18 +276,14 @@ class Repo(RepoJSONMixin):
if folder is None: if folder is None:
folder = self.folder_path folder = self.folder_path
p = await self._run( p = await self._run(Repo.GIT_DISCOVER_REMOTE_URL.format(path=folder).split())
Repo.GIT_DISCOVER_REMOTE_URL.format(
path=folder
).split()
)
if p.returncode != 0: if p.returncode != 0:
raise RuntimeError("Unable to discover a repo URL.") raise RuntimeError("Unable to discover a repo URL.")
return p.stdout.decode().strip() return p.stdout.decode().strip()
async def hard_reset(self, branch: str=None) -> None: async def hard_reset(self, branch: str = None) -> None:
"""Perform a hard reset on the current repo. """Perform a hard reset on the current repo.
Parameters Parameters
@ -315,21 +297,18 @@ class Repo(RepoJSONMixin):
exists, _ = self._existing_git_repo() exists, _ = self._existing_git_repo()
if not exists: if not exists:
raise MissingGitRepo( raise MissingGitRepo("A git repo does not exist at path: {}".format(self.folder_path))
"A git repo does not exist at path: {}".format(self.folder_path)
)
p = await self._run( p = await self._run(
self.GIT_HARD_RESET.format( self.GIT_HARD_RESET.format(path=self.folder_path, branch=branch).split()
path=self.folder_path,
branch=branch
).split()
) )
if p.returncode != 0: if p.returncode != 0:
raise HardResetError("Some error occurred when trying to" raise HardResetError(
" execute a hard reset on the repo at" "Some error occurred when trying to"
" the following path: {}".format(self.folder_path)) " execute a hard reset on the repo at"
" the following path: {}".format(self.folder_path)
)
async def update(self) -> (str, str): async def update(self) -> (str, str):
"""Update the current branch of this repo. """Update the current branch of this repo.
@ -345,15 +324,13 @@ class Repo(RepoJSONMixin):
await self.hard_reset(branch=curr_branch) await self.hard_reset(branch=curr_branch)
p = await self._run( p = await self._run(self.GIT_PULL.format(path=self.folder_path).split())
self.GIT_PULL.format(
path=self.folder_path
).split()
)
if p.returncode != 0: if p.returncode != 0:
raise UpdateError("Git pull returned a non zero exit code" raise UpdateError(
" for the repo located at path: {}".format(self.folder_path)) "Git pull returned a non zero exit code"
" for the repo located at path: {}".format(self.folder_path)
)
new_commit = await self.current_commit(branch=curr_branch) new_commit = await self.current_commit(branch=curr_branch)
@ -389,7 +366,9 @@ class Repo(RepoJSONMixin):
return await cog.copy_to(target_dir=target_dir) return await cog.copy_to(target_dir=target_dir)
async def install_libraries(self, target_dir: Path, libraries: Tuple[Installable]=()) -> bool: async def install_libraries(
self, target_dir: Path, libraries: Tuple[Installable] = ()
) -> bool:
"""Install shared libraries to the target directory. """Install shared libraries to the target directory.
If :code:`libraries` is not specified, all shared libraries in the repo If :code:`libraries` is not specified, all shared libraries in the repo
@ -469,16 +448,16 @@ class Repo(RepoJSONMixin):
p = await self._run( p = await self._run(
self.PIP_INSTALL.format( self.PIP_INSTALL.format(
python=executable, python=executable, target_dir=target_dir, reqs=" ".join(requirements)
target_dir=target_dir,
reqs=" ".join(requirements)
).split() ).split()
) )
if p.returncode != 0: if p.returncode != 0:
log.error("Something went wrong when installing" log.error(
" the following requirements:" "Something went wrong when installing"
" {}".format(", ".join(requirements))) " the following requirements:"
" {}".format(", ".join(requirements))
)
return False return False
return True return True
@ -490,8 +469,7 @@ class Repo(RepoJSONMixin):
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
return tuple( return tuple(
[m for m in self.available_modules [m for m in self.available_modules if m.type == InstallableType.COG and not m.hidden]
if m.type == InstallableType.COG and not m.hidden]
) )
@property @property
@ -501,8 +479,7 @@ class Repo(RepoJSONMixin):
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
return tuple( return tuple(
[m for m in self.available_modules [m for m in self.available_modules if m.type == InstallableType.SHARED_LIBRARY]
if m.type == InstallableType.SHARED_LIBRARY]
) )
@classmethod @classmethod
@ -515,6 +492,7 @@ class Repo(RepoJSONMixin):
class RepoManager: class RepoManager:
def __init__(self, downloader_config: Config): def __init__(self, downloader_config: Config):
self.downloader_config = downloader_config self.downloader_config = downloader_config
@ -526,7 +504,7 @@ class RepoManager:
@property @property
def repos_folder(self) -> Path: def repos_folder(self) -> Path:
data_folder = data_manager.cog_data_path(self) data_folder = data_manager.cog_data_path(self)
return data_folder / 'repos' return data_folder / "repos"
def does_repo_exist(self, name: str) -> bool: def does_repo_exist(self, name: str) -> bool:
return name in self._repos return name in self._repos
@ -537,7 +515,7 @@ class RepoManager:
raise InvalidRepoName("Not a valid Python variable name.") raise InvalidRepoName("Not a valid Python variable name.")
return name.lower() return name.lower()
async def add_repo(self, url: str, name: str, branch: str="master") -> Repo: async def add_repo(self, url: str, name: str, branch: str = "master") -> Repo:
"""Add and clone a git repository. """Add and clone a git repository.
Parameters Parameters
@ -557,13 +535,11 @@ class RepoManager:
""" """
if self.does_repo_exist(name): if self.does_repo_exist(name):
raise InvalidRepoName( raise InvalidRepoName(
"That repo name you provided already exists." "That repo name you provided already exists." " Please choose another."
" Please choose another."
) )
# noinspection PyTypeChecker # noinspection PyTypeChecker
r = Repo(url=url, name=name, branch=branch, r = Repo(url=url, name=name, branch=branch, folder_path=self.repos_folder / name)
folder_path=self.repos_folder / name)
await r.clone() await r.clone()
self._repos[name] = r self._repos[name] = r

View File

@ -36,45 +36,44 @@ class SMReel(Enum):
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,
"phrase": _("JACKPOT! 226! Your bid has been multiplied * 2500!") "phrase": _("JACKPOT! 226! Your bid has been multiplied * 2500!"),
}, },
(SMReel.flc, SMReel.flc, SMReel.flc): { (SMReel.flc, SMReel.flc, SMReel.flc): {
"payout": lambda x: x + 1000, "payout": lambda x: x + 1000, "phrase": _("4LC! +1000!")
"phrase": _("4LC! +1000!")
}, },
(SMReel.cherries, SMReel.cherries, SMReel.cherries): { (SMReel.cherries, SMReel.cherries, SMReel.cherries): {
"payout": lambda x: x + 800, "payout": lambda x: x + 800, "phrase": _("Three cherries! +800!")
"phrase": _("Three cherries! +800!")
}, },
(SMReel.two, SMReel.six): { (SMReel.two, SMReel.six): {
"payout": lambda x: x * 4 + x, "payout": lambda x: x * 4 + x, "phrase": _("2 6! Your bid has been multiplied * 4!")
"phrase": _("2 6! Your bid has been multiplied * 4!")
}, },
(SMReel.cherries, SMReel.cherries): { (SMReel.cherries, SMReel.cherries): {
"payout": lambda x: x * 3 + x, "payout": lambda x: x * 3 + x,
"phrase": _("Two cherries! Your bid has been multiplied * 3!") "phrase": _("Two cherries! Your bid has been multiplied * 3!"),
},
"3 symbols": {
"payout": lambda x: x + 500,
"phrase": _("Three symbols! +500!")
}, },
"3 symbols": {"payout": lambda x: x + 500, "phrase": _("Three symbols! +500!")},
"2 symbols": { "2 symbols": {
"payout": lambda x: x * 2 + x, "payout": lambda x: x * 2 + x,
"phrase": _("Two consecutive symbols! Your bid has been multiplied * 2!") "phrase": _("Two consecutive symbols! Your bid has been multiplied * 2!"),
}, },
} }
SLOT_PAYOUTS_MSG = _("Slot machine payouts:\n" SLOT_PAYOUTS_MSG = _(
"{two.value} {two.value} {six.value} Bet * 2500\n" "Slot machine payouts:\n"
"{flc.value} {flc.value} {flc.value} +1000\n" "{two.value} {two.value} {six.value} Bet * 2500\n"
"{cherries.value} {cherries.value} {cherries.value} +800\n" "{flc.value} {flc.value} {flc.value} +1000\n"
"{two.value} {six.value} Bet * 4\n" "{cherries.value} {cherries.value} {cherries.value} +800\n"
"{cherries.value} {cherries.value} Bet * 3\n\n" "{two.value} {six.value} Bet * 4\n"
"Three symbols: +500\n" "{cherries.value} {cherries.value} Bet * 3\n\n"
"Two symbols: Bet * 2").format(**SMReel.__dict__) "Three symbols: +500\n"
"Two symbols: Bet * 2"
).format(
**SMReel.__dict__
)
def guild_only_check(): def guild_only_check():
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
if await bank.is_global(): if await bank.is_global():
return True return True
@ -82,10 +81,12 @@ def guild_only_check():
return True return True
else: else:
return False return False
return commands.check(pred) return commands.check(pred)
class SetParser: class SetParser:
def __init__(self, argument): def __init__(self, argument):
allowed = ("+", "-") allowed = ("+", "-")
self.sum = int(argument) self.sum = int(argument)
@ -115,19 +116,14 @@ class Economy:
"SLOT_MIN": 5, "SLOT_MIN": 5,
"SLOT_MAX": 100, "SLOT_MAX": 100,
"SLOT_TIME": 0, "SLOT_TIME": 0,
"REGISTER_CREDITS": 0 "REGISTER_CREDITS": 0,
} }
default_global_settings = default_guild_settings default_global_settings = default_guild_settings
default_member_settings = { default_member_settings = {"next_payday": 0, "last_slot": 0}
"next_payday": 0,
"last_slot": 0
}
default_role_settings = { default_role_settings = {"PAYDAY_CREDITS": 0}
"PAYDAY_CREDITS": 0
}
default_user_settings = default_member_settings default_user_settings = default_member_settings
@ -159,8 +155,7 @@ class Economy:
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( await ctx.send(_("{}'s balance is {} {}").format(user.display_name, bal, currency))
user.display_name, bal, 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):
@ -173,9 +168,11 @@ class Economy:
except ValueError as e: except ValueError as e:
await ctx.send(str(e)) await ctx.send(str(e))
await ctx.send(_("{} transferred {} {} to {}").format( await ctx.send(
from_.display_name, amount, currency, to.display_name _("{} transferred {} {} to {}").format(
)) from_.display_name, amount, currency, to.display_name
)
)
@_bank.command(name="set") @_bank.command(name="set")
@check_global_setting_admin() @check_global_setting_admin()
@ -193,19 +190,25 @@ class Economy:
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(_("{} added {} {} to {}'s account.").format( await ctx.send(
author.display_name, creds.sum, currency, to.display_name _("{} added {} {} to {}'s account.").format(
)) author.display_name, creds.sum, currency, 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(_("{} removed {} {} from {}'s account.").format( await ctx.send(
author.display_name, creds.sum, currency, to.display_name _("{} removed {} {} from {}'s account.").format(
)) author.display_name, creds.sum, currency, to.display_name
)
)
else: else:
await bank.set_balance(to, creds.sum) await bank.set_balance(to, creds.sum)
await ctx.send(_("{} set {}'s account to {} {}.").format( await ctx.send(
author.display_name, to.display_name, creds.sum, currency _("{} set {}'s account to {} {}.").format(
)) author.display_name, to.display_name, creds.sum, currency
)
)
@_bank.command() @_bank.command()
@guild_only_check() @guild_only_check()
@ -214,19 +217,20 @@ class Economy:
"""Deletes bank accounts""" """Deletes 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 " _(
"`{}bank reset yes`").format( "This will delete all bank accounts for {}.\nIf you're sure, type "
self.bot.user.name if await bank.is_global() else "this server", "`{}bank reset yes`"
ctx.prefix ).format(
self.bot.user.name if await bank.is_global() else "this server", ctx.prefix
) )
) )
else: else:
await bank.wipe_bank() await bank.wipe_bank()
await ctx.send(_("All bank accounts for {} have been " await ctx.send(
"deleted.").format( _("All bank accounts for {} have been " "deleted.").format(
self.bot.user.name if await bank.is_global() else "this server" self.bot.user.name if await bank.is_global() else "this server"
) )
) )
@commands.command() @commands.command()
@guild_only_check() @guild_only_check()
@ -245,50 +249,65 @@ class Economy:
await self.config.user(author).next_payday.set(next_payday) await self.config.user(author).next_payday.set(next_payday)
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" _(
"You currently have {3} {1}.\n\n" "{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
"You are currently #{4} on the leaderboard!" "You currently have {3} {1}.\n\n"
).format( "You are currently #{4} on the leaderboard!"
author, credits_name, str(await self.config.PAYDAY_CREDITS()), ).format(
str(await bank.get_balance(author)), pos author,
)) credits_name,
str(await self.config.PAYDAY_CREDITS()),
str(await bank.get_balance(author)),
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" _("{} Too soon. For your next payday you have to" " wait {}.").format(
" wait {}.").format(author.mention, dtime) author.mention, dtime
)
) )
else: else:
next_payday = await self.config.member(author).next_payday() next_payday = await self.config.member(author).next_payday()
if cur_time >= next_payday: if cur_time >= next_payday:
credit_amount = await self.config.guild(guild).PAYDAY_CREDITS() credit_amount = await self.config.guild(guild).PAYDAY_CREDITS()
for role in author.roles: for role in author.roles:
role_credits = await self.config.role(role).PAYDAY_CREDITS() # Nice variable name role_credits = await self.config.role(
role
).PAYDAY_CREDITS() # Nice variable name
if role_credits > credit_amount: if role_credits > credit_amount:
credit_amount = role_credits credit_amount = role_credits
await bank.deposit_credits(author, credit_amount) await bank.deposit_credits(author, credit_amount)
next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME() next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME()
await self.config.member(author).next_payday.set(next_payday) await self.config.member(author).next_payday.set(next_payday)
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" _(
"You currently have {3} {1}.\n\n" "{0.mention} Here, take some {1}. Enjoy! (+{2} {1}!)\n\n"
"You are currently #{4} on the leaderboard!" "You currently have {3} {1}.\n\n"
).format( "You are currently #{4} on the leaderboard!"
author, credits_name, credit_amount, ).format(
str(await bank.get_balance(author)), pos author,
)) credits_name,
credit_amount,
str(await bank.get_balance(author)),
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" _("{} Too soon. For your next payday you have to" " wait {}.").format(
" wait {}.").format(author.mention, dtime)) author.mention, 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 """Prints out the leaderboard
Defaults to top 10""" Defaults to top 10"""
@ -296,7 +315,9 @@ class Economy:
guild = ctx.guild guild = ctx.guild
if top < 1: if top < 1:
top = 10 top = 10
if await bank.is_global() and show_global: # show_global is only applicable if bank is global if (
await bank.is_global() and show_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: if len(bank_sorted) < top:
@ -310,8 +331,12 @@ class Economy:
balance = acc[1]["balance"] balance = acc[1]["balance"]
balwidth = 2 balwidth = 2
highscore += "{pos: <{poswidth}} {name: <{namewidth}s} {balance: >{balwidth}}\n".format( highscore += "{pos: <{poswidth}} {name: <{namewidth}s} {balance: >{balwidth}}\n".format(
pos=pos, poswidth=poswidth, name=name, namewidth=namewidth, pos=pos,
balance=balance, balwidth=balwidth poswidth=poswidth,
name=name,
namewidth=namewidth,
balance=balance,
balwidth=balwidth,
) )
if highscore != "": if highscore != "":
for page in pagify(highscore, shorten_by=12): for page in pagify(highscore, shorten_by=12):
@ -337,7 +362,11 @@ class Economy:
slot_time = await self.config.SLOT_TIME() slot_time = await self.config.SLOT_TIME()
last_slot = await self.config.user(author).last_slot() last_slot = await self.config.user(author).last_slot()
else: else:
valid_bid = await self.config.guild(guild).SLOT_MIN() <= bid <= await self.config.guild(guild).SLOT_MAX() valid_bid = await self.config.guild(
guild
).SLOT_MIN() <= bid <= await self.config.guild(
guild
).SLOT_MAX()
slot_time = await self.config.guild(guild).SLOT_TIME() slot_time = await self.config.guild(guild).SLOT_TIME()
last_slot = await self.config.member(author).last_slot() last_slot = await self.config.member(author).last_slot()
now = calendar.timegm(ctx.message.created_at.utctimetuple()) now = calendar.timegm(ctx.message.created_at.utctimetuple())
@ -364,9 +393,11 @@ class Economy:
default_reel.rotate(random.randint(-999, 999)) # weeeeee default_reel.rotate(random.randint(-999, 999)) # weeeeee
new_reel = deque(default_reel, maxlen=3) # we need only 3 symbols new_reel = deque(default_reel, maxlen=3) # we need only 3 symbols
reels.append(new_reel) # for each reel reels.append(new_reel) # for each reel
rows = ((reels[0][0], reels[1][0], reels[2][0]), rows = (
(reels[0][1], reels[1][1], reels[2][1]), (reels[0][0], reels[1][0], reels[2][0]),
(reels[0][2], reels[1][2], reels[2][2])) (reels[0][1], reels[1][1], reels[2][1]),
(reels[0][2], reels[1][2], reels[2][2]),
)
slot = "~~\n~~" # Mobile friendly slot = "~~\n~~" # Mobile friendly
for i, row in enumerate(rows): # Let's build the slot to show for i, row in enumerate(rows): # Let's build the slot to show
@ -378,8 +409,7 @@ class Economy:
payout = PAYOUTS.get(rows[1]) payout = PAYOUTS.get(rows[1])
if not payout: if not payout:
# Checks for two-consecutive-symbols special rewards # Checks for two-consecutive-symbols special rewards
payout = PAYOUTS.get((rows[1][0], rows[1][1]), payout = PAYOUTS.get((rows[1][0], rows[1][1]), PAYOUTS.get((rows[1][1], rows[1][2])))
PAYOUTS.get((rows[1][1], rows[1][2])))
if not payout: if not payout:
# Still nothing. Let's check for 3 generic same symbols # Still nothing. Let's check for 3 generic same symbols
# or 2 consecutive symbols # or 2 consecutive symbols
@ -395,15 +425,20 @@ class Economy:
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(_("{}\n{} {}\n\nYour bid: {}\n{}{}!" await channel.send(
"").format(slot, author.mention, _("{}\n{} {}\n\nYour bid: {}\n{}{}!" "").format(
payout["phrase"], bid, then, now)) 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(_("{}\n{} Nothing!\nYour bid: {}\n{}{}!" await channel.send(
"").format(slot, author.mention, bid, then, now)) _("{}\n{} Nothing!\nYour bid: {}\n{}{}!" "").format(
slot, author.mention, bid, then, now
)
)
@commands.group() @commands.group()
@guild_only_check() @guild_only_check()
@ -427,17 +462,18 @@ class Economy:
payday_amount = await self.config.guild(guild).PAYDAY_CREDITS() payday_amount = await self.config.guild(guild).PAYDAY_CREDITS()
register_amount = await bank.get_default_balance(guild) register_amount = await bank.get_default_balance(guild)
msg = box( msg = box(
_("Minimum slot bid: {}\n" _(
"Maximum slot bid: {}\n" "Minimum slot bid: {}\n"
"Slot cooldown: {}\n" "Maximum slot bid: {}\n"
"Payday amount: {}\n" "Slot cooldown: {}\n"
"Payday cooldown: {}\n" "Payday amount: {}\n"
"Amount given at account registration: {}" "Payday cooldown: {}\n"
"").format( "Amount given at account registration: {}"
slot_min, slot_max, slot_time, ""
payday_amount, payday_time, register_amount ).format(
slot_min, slot_max, slot_time, payday_amount, payday_time, register_amount
), ),
_("Current Economy settings:") _("Current Economy settings:"),
) )
await ctx.send(msg) await ctx.send(msg)
@ -445,7 +481,7 @@ class Economy:
async def slotmin(self, ctx: commands.Context, bid: int): async def slotmin(self, ctx: commands.Context, bid: int):
"""Minimum slot machine bid""" """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
guild = ctx.guild guild = ctx.guild
if await bank.is_global(): if await bank.is_global():
@ -460,8 +496,7 @@ class Economy:
"""Maximum slot machine bid""" """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' await ctx.send(_("Invalid slotmax bid amount. Must be greater" " than slotmin."))
' than slotmin.'))
return return
guild = ctx.guild guild = ctx.guild
credits_name = await bank.get_currency_name(guild) credits_name = await bank.get_currency_name(guild)
@ -489,8 +524,11 @@ class Economy:
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(_("Value modified. At least {} seconds must pass " await ctx.send(
"between each payday.").format(seconds)) _("Value modified. At least {} seconds must pass " "between each payday.").format(
seconds
)
)
@economyset.command() @economyset.command()
async def paydayamount(self, ctx: commands.Context, creds: int): async def paydayamount(self, ctx: commands.Context, creds: int):
@ -504,8 +542,7 @@ class Economy:
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 {} {}." await ctx.send(_("Every payday will now give {} {}." "").format(creds, credits_name))
"").format(creds, 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):
@ -516,8 +553,11 @@ class Economy:
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(_("Every payday will now give {} {} to people with the role {}." await ctx.send(
"").format(creds, credits_name, role.name)) _("Every payday will now give {} {} to people with the role {}." "").format(
creds, credits_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):
@ -527,17 +567,18 @@ class Economy:
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(_("Registering an account will now give {} {}." await ctx.send(
"").format(creds, credits_name)) _("Registering an account will now give {} {}." "").format(creds, credits_name)
)
# What would I ever do without stackoverflow? # What would I ever do without stackoverflow?
def display_time(self, seconds, granularity=2): def display_time(self, 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
(_('hours'), 3600), # 60 * 60 (_("hours"), 3600), # 60 * 60
(_('minutes'), 60), (_("minutes"), 60),
(_('seconds'), 1), (_("seconds"), 1),
) )
result = [] result = []
@ -547,6 +588,6 @@ class Economy:
if value: if value:
seconds -= value * count seconds -= value * count
if value == 1: if value == 1:
name = name.rstrip('s') name = name.rstrip("s")
result.append("{} {}".format(value, name)) result.append("{} {}".format(value, name))
return ', '.join(result[:granularity]) return ", ".join(result[:granularity])

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../economy.py"]
'../economy.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -21,12 +21,9 @@ class Filter:
"filterban_count": 0, "filterban_count": 0,
"filterban_time": 0, "filterban_time": 0,
"filter_names": False, "filter_names": False,
"filter_default_name": "John Doe" "filter_default_name": "John Doe",
}
default_member_settings = {
"filter_count": 0,
"next_reset_time": 0
} }
default_member_settings = {"filter_count": 0, "next_reset_time": 0}
self.settings.register_guild(**default_guild_settings) self.settings.register_guild(**default_guild_settings)
self.settings.register_member(**default_member_settings) self.settings.register_member(**default_member_settings)
self.register_task = self.bot.loop.create_task(self.register_filterban()) self.register_task = self.bot.loop.create_task(self.register_filterban())
@ -37,8 +34,7 @@ class Filter:
async def register_filterban(self): async def register_filterban(self):
try: try:
await modlog.register_casetype( await modlog.register_casetype(
"filterban", False, ":filing_cabinet: :hammer:", "filterban", False, ":filing_cabinet: :hammer:", "Filter ban", "ban"
"Filter ban", "ban"
) )
except RuntimeError: except RuntimeError:
pass pass
@ -79,13 +75,12 @@ class Filter:
word_list = [] word_list = []
tmp = "" tmp = ""
for word in split_words: for word in split_words:
if not word.startswith("\"")\ if not word.startswith('"') and not word.endswith('"') and not tmp:
and not word.endswith("\"") and not tmp:
word_list.append(word) word_list.append(word)
else: else:
if word.startswith("\""): if word.startswith('"'):
tmp += word[1:] tmp += word[1:]
elif word.endswith("\""): elif word.endswith('"'):
tmp += word[:-1] tmp += word[:-1]
word_list.append(tmp) word_list.append(tmp)
tmp = "" tmp = ""
@ -110,13 +105,12 @@ class Filter:
word_list = [] word_list = []
tmp = "" tmp = ""
for word in split_words: for word in split_words:
if not word.startswith("\"")\ if not word.startswith('"') and not word.endswith('"') and not tmp:
and not word.endswith("\"") and not tmp:
word_list.append(word) word_list.append(word)
else: else:
if word.startswith("\""): if word.startswith('"'):
tmp += word[1:] tmp += word[1:]
elif word.endswith("\""): elif word.endswith('"'):
tmp += word[:-1] tmp += word[:-1]
word_list.append(tmp) word_list.append(tmp)
tmp = "" tmp = ""
@ -139,14 +133,10 @@ class Filter:
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( await ctx.send(
_("Names and nicknames will no longer be " _("Names and nicknames will no longer be " "checked against the filter")
"checked against the filter")
) )
else: else:
await ctx.send( await ctx.send(_("Names and nicknames will now be checked against " "the filter"))
_("Names and nicknames will now be checked against "
"the filter")
)
@_filter.command(name="defaultname") @_filter.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):
@ -160,17 +150,17 @@ class Filter:
await ctx.send(_("The name to use on filtered names has been set")) await ctx.send(_("The name to use on filtered names has been set"))
@_filter.command(name="ban") @_filter.command(name="ban")
async def filter_ban( async def filter_ban(self, ctx: commands.Context, count: int, timeframe: int):
self, ctx: commands.Context, count: int, timeframe: int):
""" """
Sets up an autoban if the specified number of messages are Sets up an autoban if the specified number of messages are
filtered in the specified amount of time (in seconds) filtered in the specified amount of time (in seconds)
""" """
if (count <= 0) != (timeframe <= 0): if (count <= 0) != (timeframe <= 0):
await ctx.send( await ctx.send(
_("Count and timeframe either both need to be 0 " _(
"or both need to be greater than 0!" "Count and timeframe either both need to be 0 "
) "or both need to be greater than 0!"
)
) )
return return
elif count == 0 and timeframe == 0: elif count == 0 and timeframe == 0:
@ -213,9 +203,7 @@ class Filter:
if filter_count > 0 and filter_time > 0: if filter_count > 0 and filter_time > 0:
if message.created_at.timestamp() >= next_reset_time: if message.created_at.timestamp() >= next_reset_time:
next_reset_time = message.created_at.timestamp() + filter_time next_reset_time = message.created_at.timestamp() + filter_time
await self.settings.member(author).next_reset_time.set( await self.settings.member(author).next_reset_time.set(next_reset_time)
next_reset_time
)
if user_count > 0: if user_count > 0:
user_count = 0 user_count = 0
await self.settings.member(author).filter_count.set(user_count) await self.settings.member(author).filter_count.set(user_count)
@ -231,8 +219,10 @@ class Filter:
if filter_count > 0 and filter_time > 0: if filter_count > 0 and filter_time > 0:
user_count += 1 user_count += 1
await self.settings.member(author).filter_count.set(user_count) await self.settings.member(author).filter_count.set(user_count)
if user_count >= filter_count and \ if (
message.created_at.timestamp() < next_reset_time: user_count >= filter_count
and message.created_at.timestamp() < next_reset_time
):
reason = "Autoban (too many filtered messages)" reason = "Autoban (too many filtered messages)"
try: try:
await server.ban(author, reason=reason) await server.ban(author, reason=reason)
@ -240,8 +230,13 @@ class Filter:
pass pass
else: else:
await modlog.create_case( await modlog.create_case(
self.bot, server, message.created_at, self.bot,
"filterban", author, server.me, reason server,
message.created_at,
"filterban",
author,
server.me,
reason,
) )
async def on_message(self, message: discord.Message): async def on_message(self, message: discord.Message):
@ -323,4 +318,3 @@ class Filter:
except: except:
pass pass
break break

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../filter.py"]
'../filter.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -15,12 +15,13 @@ _ = Translator("General", __file__)
class RPS(Enum): class RPS(Enum):
rock = "\N{MOYAI}" rock = "\N{MOYAI}"
paper = "\N{PAGE FACING UP}" paper = "\N{PAGE FACING UP}"
scissors = "\N{BLACK SCISSORS}" scissors = "\N{BLACK SCISSORS}"
class RPSParser: class RPSParser:
def __init__(self, argument): def __init__(self, argument):
argument = argument.lower() argument = argument.lower()
if argument == "rock": if argument == "rock":
@ -40,13 +41,26 @@ class General:
def __init__(self): def __init__(self):
self.stopwatches = {} self.stopwatches = {}
self.ball = [ self.ball = [
_("As I see it, yes"), _("It is certain"), _("It is decidedly so"), _("As I see it, yes"),
_("Most likely"), _("Outlook good"), _("Signs point to yes"), _("It is certain"),
_("Without a doubt"), _("Yes"), _("Yes definitely"), _("You may rely on it"), _("It is decidedly so"),
_("Reply hazy, try again"), _("Ask again later"), _("Most likely"),
_("Better not tell you now"), _("Cannot predict now"), _("Outlook good"),
_("Concentrate and ask again"), _("Don't count on it"), _("My reply is no"), _("Signs point to yes"),
_("My sources say no"), _("Outlook not so good"), _("Very doubtful") _("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()
@ -57,12 +71,12 @@ class General:
""" """
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 choices 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) """Rolls random number (between 1 and user choice)
Defaults to 100. Defaults to 100.
@ -70,14 +84,12 @@ class General:
author = ctx.author author = ctx.author
if number > 1: if number > 1:
n = randint(1, number) n = randint(1, number)
await ctx.send( await ctx.send(_("{} :game_die: {} :game_die:").format(author.mention, n))
_("{} :game_die: {} :game_die:").format(author.mention, n)
)
else: else:
await ctx.send(_("{} Maybe higher than 1? ;P").format(author.mention)) await ctx.send(_("{} Maybe higher than 1? ;P").format(author.mention))
@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. """Flips a coin... or a user.
Defaults to coin. Defaults to coin.
@ -86,8 +98,7 @@ class General:
msg = "" msg = ""
if user.id == ctx.bot.user.id: if user.id == ctx.bot.user.id:
user = ctx.author user = ctx.author
msg = _("Nice try. You think this is funny?\n" msg = _("Nice try. You think this is funny?\n" "How about *this* instead:\n\n")
"How about *this* instead:\n\n")
char = "abcdefghijklmnopqrstuvwxyz" char = "abcdefghijklmnopqrstuvwxyz"
tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz" tran = "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz"
table = str.maketrans(char, tran) table = str.maketrans(char, tran)
@ -98,45 +109,37 @@ class General:
name = name.translate(table) name = name.translate(table)
await ctx.send(msg + "(╯°□°)╯︵ " + name[::-1]) await ctx.send(msg + "(╯°□°)╯︵ " + name[::-1])
else: else:
await ctx.send( await ctx.send(_("*flips a coin and... ") + choice([_("HEADS!*"), _("TAILS!*")]))
_("*flips a coin and... ") + choice([_("HEADS!*"), _("TAILS!*")])
)
@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))
cond = { cond = {
(RPS.rock, RPS.paper) : False, (RPS.rock, RPS.paper): False,
(RPS.rock, RPS.scissors) : True, (RPS.rock, RPS.scissors): True,
(RPS.paper, RPS.rock) : True, (RPS.paper, RPS.rock): True,
(RPS.paper, RPS.scissors) : False, (RPS.paper, RPS.scissors): False,
(RPS.scissors, RPS.rock) : False, (RPS.scissors, RPS.rock): False,
(RPS.scissors, RPS.paper) : True (RPS.scissors, RPS.paper): True,
} }
if red_choice == player_choice: if red_choice == player_choice:
outcome = None # Tie outcome = None # Tie
else: else:
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( await ctx.send(_("{} You win {}!").format(red_choice.value, author.mention))
red_choice.value, author.mention
))
elif outcome is False: elif outcome is False:
await ctx.send(_("{} You lose {}!").format( await ctx.send(_("{} You lose {}!").format(red_choice.value, author.mention))
red_choice.value, author.mention
))
else: else:
await ctx.send(_("{} We're square {}!").format( await ctx.send(_("{} We're square {}!").format(red_choice.value, author.mention))
red_choice.value, author.mention
))
@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.
@ -160,14 +163,14 @@ class General:
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""" """Creates a lmgtfy link"""
search_terms = escape(search_terms.replace(" ", "+"), mass_mentions=True) search_terms = escape(search_terms.replace(" ", "+"), mass_mentions=True)
await ctx.send("https://lmgtfy.com/?q={}".format(search_terms)) await ctx.send("https://lmgtfy.com/?q={}".format(search_terms))
@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."""
@ -186,7 +189,7 @@ class General:
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
async def userinfo(self, ctx, *, user: discord.Member=None): async def userinfo(self, ctx, *, user: discord.Member = None):
"""Shows users's informations""" """Shows users's informations"""
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
@ -196,8 +199,7 @@ class General:
# A special case for a special someone :^) # A special case for a special someone :^)
special_date = datetime.datetime(2016, 1, 10, 6, 8, 4, 443000) special_date = datetime.datetime(2016, 1, 10, 6, 8, 4, 443000)
is_special = (user.id == 96130341705637888 and is_special = (user.id == 96130341705637888 and guild.id == 133049272517001216)
guild.id == 133049272517001216)
roles = sorted(user.roles)[1:] roles = sorted(user.roles)[1:]
@ -206,8 +208,7 @@ class General:
since_joined = (ctx.message.created_at - joined_at).days since_joined = (ctx.message.created_at - joined_at).days
user_joined = joined_at.strftime("%d %b %Y %H:%M") user_joined = joined_at.strftime("%d %b %Y %H:%M")
user_created = user.created_at.strftime("%d %b %Y %H:%M") user_created = user.created_at.strftime("%d %b %Y %H:%M")
member_number = sorted(guild.members, member_number = sorted(guild.members, key=lambda m: m.joined_at).index(user) + 1
key=lambda m: m.joined_at).index(user) + 1
created_on = _("{}\n({} days ago)").format(user_created, since_created) created_on = _("{}\n({} days ago)").format(user_created, since_created)
joined_on = _("{}\n({} days ago)").format(user_joined, since_joined) joined_on = _("{}\n({} days ago)").format(user_joined, since_joined)
@ -233,15 +234,14 @@ class General:
data.add_field(name=_("Joined Discord on"), value=created_on) data.add_field(name=_("Joined Discord on"), value=created_on)
data.add_field(name=_("Joined this server on"), value=joined_on) data.add_field(name=_("Joined this server on"), value=joined_on)
data.add_field(name=_("Roles"), value=roles, inline=False) data.add_field(name=_("Roles"), value=roles, inline=False)
data.set_footer(text=_("Member #{} | User ID: {}" data.set_footer(text=_("Member #{} | User ID: {}" "").format(member_number, user.id))
"").format(member_number, user.id))
name = str(user) name = str(user)
name = " ~ ".join((name, user.nick)) if user.nick else name name = " ~ ".join((name, user.nick)) if user.nick else name
if user.avatar: if user.avatar:
avatar = user.avatar_url avatar = user.avatar_url
avatar = avatar.replace('webp', 'png') avatar = avatar.replace("webp", "png")
data.set_author(name=name, url=avatar) data.set_author(name=name, url=avatar)
data.set_thumbnail(url=avatar) data.set_thumbnail(url=avatar)
else: else:
@ -250,31 +250,34 @@ class General:
try: try:
await ctx.send(embed=data) await ctx.send(embed=data)
except discord.HTTPException: except discord.HTTPException:
await ctx.send(_("I need the `Embed links` permission " await ctx.send(_("I need the `Embed links` permission " "to send this."))
"to send this."))
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
async def serverinfo(self, ctx): async def serverinfo(self, ctx):
"""Shows server's informations""" """Shows server's informations"""
guild = ctx.guild guild = ctx.guild
online = len([m.status for m in guild.members online = len(
if m.status == discord.Status.online or [
m.status == discord.Status.idle]) m.status
for m in guild.members
if m.status == discord.Status.online or m.status == discord.Status.idle
]
)
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!" created_at = (
"").format(guild.created_at.strftime("%d %b %Y %H:%M"), _("Since {}. That's over {} days ago!" "").format(
passed)) guild.created_at.strftime("%d %b %Y %H:%M"), passed
)
)
colour = ''.join([choice('0123456789ABCDEF') for x in range(6)]) colour = "".join([choice("0123456789ABCDEF") for x in range(6)])
colour = randint(0, 0xFFFFFF) colour = randint(0, 0xFFFFFF)
data = discord.Embed( data = discord.Embed(description=created_at, colour=discord.Colour(value=colour))
description=created_at,
colour=discord.Colour(value=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="{}/{}".format(online, total_users))
data.add_field(name=_("Text Channels"), value=text_channels) data.add_field(name=_("Text Channels"), value=text_channels)
@ -292,16 +295,16 @@ class General:
try: try:
await ctx.send(embed=data) await ctx.send(embed=data)
except discord.HTTPException: except discord.HTTPException:
await ctx.send(_("I need the `Embed links` permission " await ctx.send(_("I need the `Embed links` permission " "to send this."))
"to send this."))
@commands.command() @commands.command()
async def urban(self, ctx, *, search_terms: str, definition_number: int=1): async def urban(self, ctx, *, search_terms: str, definition_number: int = 1):
"""Urban Dictionary search """Urban Dictionary search
Definition number must be between 1 and 10""" Definition number must be between 1 and 10"""
def encode(s): def encode(s):
return quote_plus(s, encoding='utf-8', errors='replace') return quote_plus(s, encoding="utf-8", errors="replace")
# definition_number is just there to show up in the help # definition_number is just there to show up in the help
# all this mess is to avoid forcing double quotes on the user # all this mess is to avoid forcing double quotes on the user
@ -313,8 +316,8 @@ class General:
search_terms = search_terms[:-1] search_terms = search_terms[:-1]
else: else:
pos = 0 pos = 0
if pos not in range(0, 11): # API only provides the if pos not in range(0, 11): # API only provides the
pos = 0 # top 10 definitions pos = 0 # top 10 definitions
except ValueError: except ValueError:
pos = 0 pos = 0
@ -326,18 +329,19 @@ class General:
result = await r.json() result = await r.json()
item_list = result["list"] item_list = result["list"]
if item_list: if item_list:
definition = item_list[pos]['definition'] definition = item_list[pos]["definition"]
example = item_list[pos]['example'] example = item_list[pos]["example"]
defs = len(item_list) defs = len(item_list)
msg = ("**Definition #{} out of {}:\n**{}\n\n" msg = (
"**Example:\n**{}".format(pos+1, defs, definition, "**Definition #{} out of {}:\n**{}\n\n"
example)) "**Example:\n**{}".format(pos + 1, defs, definition, example)
)
msg = pagify(msg, ["\n"]) msg = pagify(msg, ["\n"])
for page in msg: for page in msg:
await ctx.send(page) await ctx.send(page)
else: else:
await ctx.send(_("Your search terms gave no results.")) await ctx.send(_("Your search terms gave no results."))
except IndexError: except IndexError:
await ctx.send(_("There is no definition #{}").format(pos+1)) await ctx.send(_("There is no definition #{}").format(pos + 1))
except: except:
await ctx.send(_("Error.")) await ctx.send(_("Error."))

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../general.py"]
'../general.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -13,9 +13,7 @@ GIPHY_API_KEY = "dc6zaTOxFJmzC"
@cog_i18n(_) @cog_i18n(_)
class Image: class Image:
"""Image related commands.""" """Image related commands."""
default_global = { default_global = {"imgur_client_id": None}
"imgur_client_id": None
}
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@ -45,7 +43,9 @@ class Image:
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( _("A client ID has not been set! Please set one with {}").format(
"`{}imgurcreds`".format(ctx.prefix))) "`{}imgurcreds`".format(ctx.prefix)
)
)
return return
headers = {"Authorization": "Client-ID {}".format(imgur_client_id)} headers = {"Authorization": "Client-ID {}".format(imgur_client_id)}
async with self.session.get(url, headers=headers, params=params) as search_get: async with self.session.get(url, headers=headers, params=params) as search_get:
@ -66,7 +66,9 @@ class Image:
await ctx.send(_("Something went wrong. Error code is {}").format(data["status"])) await ctx.send(_("Something went wrong. Error code is {}").format(data["status"]))
@_imgur.command(name="subreddit") @_imgur.command(name="subreddit")
async def imgur_subreddit(self, ctx, subreddit: str, sort_type: str="top", window: str="day"): async def imgur_subreddit(
self, ctx, subreddit: str, sort_type: str = "top", window: str = "day"
):
"""Gets images from the specified subreddit section """Gets images from the specified subreddit section
Sort types: new, top Sort types: new, top
@ -90,7 +92,9 @@ class Image:
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( _("A client ID has not been set! Please set one with {}").format(
"`{}imgurcreds`".format(ctx.prefix))) "`{}imgurcreds`".format(ctx.prefix)
)
)
return return
links = [] links = []
@ -139,8 +143,10 @@ class Image:
await ctx.send_help() await ctx.send_help()
return return
url = ("http://api.giphy.com/v1/gifs/search?&api_key={}&q={}" url = (
"".format(GIPHY_API_KEY, keywords)) "http://api.giphy.com/v1/gifs/search?&api_key={}&q={}"
"".format(GIPHY_API_KEY, keywords)
)
async with self.session.get(url) as r: async with self.session.get(url) as r:
result = await r.json() result = await r.json()
@ -161,8 +167,10 @@ class Image:
await ctx.send_help() await ctx.send_help()
return return
url = ("http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}" url = (
"".format(GIPHY_API_KEY, keywords)) "http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}"
"".format(GIPHY_API_KEY, keywords)
)
async with self.session.get(url) as r: async with self.session.get(url) as r:
result = await r.json() result = await r.json()

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../image.py"]
'../image.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -3,6 +3,7 @@ import discord
def mod_or_voice_permissions(**perms): def mod_or_voice_permissions(**perms):
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
@ -23,10 +24,12 @@ def mod_or_voice_permissions(**perms):
return False return False
else: else:
return True return True
return commands.check(pred) return commands.check(pred)
def admin_or_voice_permissions(**perms): def admin_or_voice_permissions(**perms):
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
@ -42,10 +45,12 @@ def admin_or_voice_permissions(**perms):
return False return False
else: else:
return True return True
return commands.check(pred) return commands.check(pred)
def bot_has_voice_permissions(**perms): def bot_has_voice_permissions(**perms):
async def pred(ctx: commands.Context): async def pred(ctx: commands.Context):
guild = ctx.guild guild = ctx.guild
for vc in guild.voice_channels: for vc in guild.voice_channels:
@ -55,4 +60,5 @@ def bot_has_voice_permissions(**perms):
return False return False
else: else:
return True return True
return commands.check(pred) return commands.check(pred)

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../mod.py"]
'../mod.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../modlog.py"]
'../modlog.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -5,7 +5,7 @@ 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 box from redbot.core.utils.chat_formatting import box
_ = Translator('ModLog', __file__) _ = Translator("ModLog", __file__)
@cog_i18n(_) @cog_i18n(_)
@ -32,15 +32,12 @@ class ModLog:
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( await ctx.send(_("Mod events will be sent to {}").format(channel.mention))
_("Mod events will be sent to {}").format(
channel.mention
)
)
else: else:
await ctx.send( await ctx.send(
_("I do not have permissions to " _("I do not have permissions to " "send messages in {}!").format(
"send messages in {}!").format(channel.mention) channel.mention
)
) )
else: else:
try: try:
@ -51,7 +48,7 @@ class ModLog:
await modlog.set_modlog_channel(guild, None) await modlog.set_modlog_channel(guild, None)
await ctx.send(_("Mod log deactivated.")) await ctx.send(_("Mod log deactivated."))
@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""" """Enables or disables case creation for each type of mod action"""
@ -64,8 +61,8 @@ class ModLog:
msg = "" msg = ""
for ct in casetypes: for ct in casetypes:
enabled = await ct.is_enabled() enabled = await ct.is_enabled()
value = 'enabled' if enabled else 'disabled' value = "enabled" if enabled else "disabled"
msg += '%s : %s\n' % (ct.name, value) msg += "%s : %s\n" % (ct.name, value)
msg = title + "\n" + box(msg) msg = title + "\n" + box(msg)
await ctx.send(msg) await ctx.send(msg)
@ -79,8 +76,8 @@ class ModLog:
await casetype.set_enabled(True if not enabled else False) await casetype.set_enabled(True if not enabled else False)
msg = ( msg = (
_('Case creation for {} actions is now {}.').format( _("Case creation for {} actions is now {}.").format(
action, 'enabled' if not enabled else 'disabled' action, "enabled" if not enabled else "disabled"
) )
) )
await ctx.send(msg) await ctx.send(msg)
@ -133,8 +130,10 @@ class ModLog:
if audit_type: if audit_type:
audit_case = None audit_case = None
async for entry in guild.audit_logs(action=audit_type): async for entry in guild.audit_logs(action=audit_type):
if entry.target.id == case_before.user.id and \ if (
entry.action == audit_type: entry.target.id == case_before.user.id
and entry.action == audit_type
):
audit_case = entry audit_case = entry
break break
if audit_case: if audit_case:
@ -145,9 +144,7 @@ class ModLog:
if not (is_guild_owner or is_case_author or author_is_mod): if not (is_guild_owner or is_case_author or author_is_mod):
await ctx.send(_("You are not authorized to modify that case!")) await ctx.send(_("You are not authorized to modify that case!"))
return return
to_modify = { to_modify = {"reason": reason}
"reason": reason,
}
if case_before.moderator != author: if case_before.moderator != author:
to_modify["amended_by"] = author to_modify["amended_by"] = author
to_modify["modified_at"] = ctx.message.created_at.timestamp() to_modify["modified_at"] = ctx.message.created_at.timestamp()

View File

@ -22,15 +22,9 @@ log = logging.getLogger("red.reports")
@cog_i18n(_) @cog_i18n(_)
class Reports: class Reports:
default_guild_settings = { default_guild_settings = {"output_channel": None, "active": False, "next_ticket": 1}
"output_channel": None,
"active": False,
"next_ticket": 1
}
default_report = { default_report = {"report": {}}
'report': {}
}
# This can be made configureable later if it # This can be made configureable later if it
# becomes an issue. # becomes an issue.
@ -42,15 +36,14 @@ class Reports:
(timedelta(seconds=5), 1), (timedelta(seconds=5), 1),
(timedelta(minutes=5), 3), (timedelta(minutes=5), 3),
(timedelta(hours=1), 10), (timedelta(hours=1), 10),
(timedelta(days=1), 24) (timedelta(days=1), 24),
] ]
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
self.config = Config.get_conf( self.config = Config.get_conf(self, 78631113035100160, force_registration=True)
self, 78631113035100160, force_registration=True)
self.config.register_guild(**self.default_guild_settings) self.config.register_guild(**self.default_guild_settings)
self.config.register_custom('REPORT', **self.default_report) self.config.register_custom("REPORT", **self.default_report)
self.antispam = {} self.antispam = {}
self.user_cache = [] self.user_cache = []
self.tunnel_store = {} self.tunnel_store = {}
@ -59,9 +52,7 @@ class Reports:
@property @property
def tunnels(self): def tunnels(self):
return [ return [x["tun"] for x in self.tunnel_store.values()]
x['tun'] for x in self.tunnel_store.values()
]
@checks.admin_or_permissions(manage_guild=True) @checks.admin_or_permissions(manage_guild=True)
@commands.guild_only() @commands.guild_only()
@ -99,9 +90,7 @@ class Reports:
admin_role = discord.utils.get( admin_role = discord.utils.get(
guild.roles, id=await self.bot.db.guild(guild).admin_role() guild.roles, id=await self.bot.db.guild(guild).admin_role()
) )
mod_role = discord.utils.get( mod_role = discord.utils.get(guild.roles, id=await self.bot.db.guild(guild).mod_role())
guild.roles, id=await self.bot.db.guild(guild).mod_role()
)
ret |= any(r in m.roles for r in (mod_role, admin_role)) ret |= any(r in m.roles for r in (mod_role, admin_role))
if perms: if perms:
ret |= m.guild_permissions >= perms ret |= m.guild_permissions >= perms
@ -111,10 +100,13 @@ class Reports:
return ret return ret
async def discover_guild( async def discover_guild(
self, author: discord.User, *, self,
mod: bool=False, author: discord.User,
permissions: Union[discord.Permissions, dict]=None, *,
prompt: str=""): mod: bool = False,
permissions: Union[discord.Permissions, dict] = None,
prompt: str = ""
):
""" """
discovers which of shared guilds between the bot discovers which of shared guilds between the bot
and provided user based on conditions (mod or permissions is an or) and provided user based on conditions (mod or permissions is an or)
@ -151,13 +143,9 @@ class Reports:
return m.author == author and m.channel == dm.channel return m.author == author and m.channel == dm.channel
try: try:
message = await self.bot.wait_for( message = await self.bot.wait_for("message", check=pred, timeout=45)
'message', check=pred, timeout=45
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
await author.send( await author.send(_("You took too long to select. Try again later."))
_("You took too long to select. Try again later.")
)
return None return None
try: try:
@ -187,35 +175,31 @@ class Reports:
if await self.bot.embed_requested(channel, author): if await self.bot.embed_requested(channel, author):
em = discord.Embed(description=report) em = discord.Embed(description=report)
em.set_author( em.set_author(
name=_('Report from {0.display_name}').format(author), name=_("Report from {0.display_name}").format(author), icon_url=author.avatar_url
icon_url=author.avatar_url
) )
em.set_footer(text=_("Report #{}").format(ticket_number)) em.set_footer(text=_("Report #{}").format(ticket_number))
send_content = None send_content = None
else: else:
em = None em = None
send_content = _( send_content = _("Report from {author.mention} (Ticket #{number})").format(
'Report from {author.mention} (Ticket #{number})' author=author, number=ticket_number
).format(author=author, number=ticket_number) )
send_content += "\n" + report send_content += "\n" + report
try: try:
await Tunnel.message_forwarder( await Tunnel.message_forwarder(
destination=channel, destination=channel, content=send_content, embed=em, files=files
content=send_content,
embed=em,
files=files
) )
except (discord.Forbidden, discord.HTTPException): except (discord.Forbidden, discord.HTTPException):
return None return None
await self.config.custom('REPORT', guild.id, ticket_number).report.set( await self.config.custom("REPORT", guild.id, ticket_number).report.set(
{'user_id': author.id, 'report': report} {"user_id": author.id, "report": report}
) )
return ticket_number return ticket_number
@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 = ""):
""" """
Follow the prompts to make a report Follow the prompts to make a report
@ -226,8 +210,7 @@ class Reports:
guild = ctx.guild guild = ctx.guild
if guild is None: if guild is None:
guild = await self.discover_guild( guild = await self.discover_guild(
author, author, prompt=_("Select a server to make a report in by number.")
prompt=_("Select a server to make a report in by number.")
) )
else: else:
try: try:
@ -238,24 +221,23 @@ class Reports:
return return
g_active = await self.config.guild(guild).active() g_active = await self.config.guild(guild).active()
if not g_active: if not g_active:
return await author.send( return await author.send(_("Reporting has not been enabled for this server"))
_("Reporting has not been enabled for this server")
)
if guild.id not in self.antispam: if guild.id not in self.antispam:
self.antispam[guild.id] = {} self.antispam[guild.id] = {}
if author.id not in self.antispam[guild.id]: if author.id not in self.antispam[guild.id]:
self.antispam[guild.id][author.id] = AntiSpam(self.intervals) self.antispam[guild.id][author.id] = AntiSpam(self.intervals)
if self.antispam[guild.id][author.id].spammy: if self.antispam[guild.id][author.id].spammy:
return await author.send( return await author.send(
_("You've sent a few too many of these recently. " _(
"Contact a server admin to resolve this, or try again " "You've sent a few too many of these recently. "
"later.") "Contact a server admin to resolve this, or try again "
"later."
)
) )
if author.id in self.user_cache: if author.id in self.user_cache:
return await author.send( return await author.send(
_("Finish making your prior report " _("Finish making your prior report " "before making an additional one")
"before making an additional one")
) )
if ctx.guild: if ctx.guild:
@ -273,13 +255,13 @@ class Reports:
else: else:
try: try:
dm = await author.send( dm = await author.send(
_("Please respond to this message with your Report." _(
"\nYour report should be a single message") "Please respond to this message with your Report."
"\nYour report should be a single message"
)
) )
except discord.Forbidden: except discord.Forbidden:
await ctx.send( await ctx.send(_("This requires DMs enabled."))
_("This requires DMs enabled.")
)
self.user_cache.remove(author.id) self.user_cache.remove(author.id)
return return
@ -287,25 +269,17 @@ class Reports:
return m.author == author and m.channel == dm.channel return m.author == author and m.channel == dm.channel
try: try:
message = await self.bot.wait_for( message = await self.bot.wait_for("message", check=pred, timeout=180)
'message', check=pred, timeout=180
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
await author.send( await author.send(_("You took too long. Try again later."))
_("You took too long. Try again later.")
)
else: else:
val = await self.send_report(message, guild) val = await self.send_report(message, guild)
with contextlib.suppress(discord.Forbidden, discord.HTTPException): with contextlib.suppress(discord.Forbidden, discord.HTTPException):
if val is None: if val is None:
await author.send( await author.send(_("There was an error sending your report."))
_("There was an error sending your report.")
)
else: else:
await author.send( await author.send(_("Your report was submitted. (Ticket #{})").format(val))
_("Your report was submitted. (Ticket #{})").format(val)
)
self.antispam[guild.id][author.id].stamp() self.antispam[guild.id][author.id].stamp()
self.user_cache.remove(author.id) self.user_cache.remove(author.id)
@ -318,18 +292,14 @@ class Reports:
return return
_id = payload.message_id _id = payload.message_id
t = next(filter( t = next(filter(lambda x: _id in x[1]["msgs"], self.tunnel_store.items()), None)
lambda x: _id in x[1]['msgs'],
self.tunnel_store.items()
), None)
if t is None: if t is None:
return return
tun = t[1]['tun'] tun = t[1]["tun"]
if payload.user_id in [x.id for x in tun.members]: if payload.user_id in [x.id for x in tun.members]:
await tun.react_close( await tun.react_close(
uid=payload.user_id, uid=payload.user_id, message=_("{closer} has closed the correspondence")
message=_("{closer} has closed the correspondence")
) )
self.tunnel_store.pop(t[0], None) self.tunnel_store.pop(t[0], None)
@ -337,12 +307,12 @@ class Reports:
for k, v in self.tunnel_store.items(): for k, v in self.tunnel_store.items():
topic = _("Re: ticket# {1} in {0.name}").format(*k) topic = _("Re: ticket# {1} in {0.name}").format(*k)
# Tunnels won't forward unintended messages, this is safe # Tunnels won't forward unintended messages, this is safe
msgs = await v['tun'].communicate(message=message, topic=topic) msgs = await v["tun"].communicate(message=message, topic=topic)
if msgs: if msgs:
self.tunnel_store[k]['msgs'] = msgs self.tunnel_store[k]["msgs"] = msgs
@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):
""" """
opens a message tunnel between things you say in this channel opens a message tunnel between things you say in this channel
@ -353,27 +323,24 @@ class Reports:
# note, mod_or_permissions is an implicit guild_only # note, mod_or_permissions is an implicit guild_only
guild = ctx.guild guild = ctx.guild
rec = await self.config.custom( rec = await self.config.custom("REPORT", guild.id, ticket_number).report()
'REPORT', guild.id, ticket_number).report()
try: try:
user = guild.get_member(rec.get('user_id')) user = guild.get_member(rec.get("user_id"))
except KeyError: except KeyError:
return await ctx.send( return await ctx.send(_("That ticket doesn't seem to exist"))
_("That ticket doesn't seem to exist")
)
if user is None: if user is None:
return await ctx.send( return await ctx.send(_("That user isn't here anymore."))
_("That user isn't here anymore.")
)
tun = Tunnel(recipient=user, origin=ctx.channel, sender=ctx.author) tun = Tunnel(recipient=user, origin=ctx.channel, sender=ctx.author)
if tun is None: if tun is None:
return await ctx.send( return await ctx.send(
_("Either you or the user you are trying to reach already " _(
"has an open communication.") "Either you or the user you are trying to reach already "
"has an open communication."
)
) )
big_topic = _( big_topic = _(
@ -387,18 +354,13 @@ class Reports:
"\nTunnels are not persistent across bot restarts." "\nTunnels are not persistent across bot restarts."
) )
topic = big_topic.format( topic = big_topic.format(
ticketnum=ticket_number, ticketnum=ticket_number, who=_("A moderator in `{guild.name}` has").format(guild=guild)
who=_("A moderator in `{guild.name}` has").format(guild=guild)
) )
try: try:
m = await tun.communicate( m = await tun.communicate(message=ctx.message, topic=topic, skip_message_content=True)
message=ctx.message, topic=topic, skip_message_content=True
)
except discord.Forbidden: except discord.Forbidden:
await ctx.send(_("User has disabled DMs.")) await ctx.send(_("User has disabled DMs."))
tun.close() tun.close()
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( await ctx.send(big_topic.format(who=_("You have"), ticketnum=ticket_number))
big_topic.format(who=_("You have"), ticketnum=ticket_number)
)

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../mod.py"]
'../mod.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -3,9 +3,24 @@ from redbot.core import Config, checks, commands
from redbot.core.utils.chat_formatting import pagify from redbot.core.utils.chat_formatting import pagify
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 .streamtypes import TwitchStream, HitboxStream, MixerStream, PicartoStream, TwitchCommunity, YoutubeStream from .streamtypes import (
from .errors import (OfflineStream, StreamNotFound, APIError, InvalidYoutubeCredentials, TwitchStream,
CommunityNotFound, OfflineCommunity, StreamsError, InvalidTwitchCredentials) HitboxStream,
MixerStream,
PicartoStream,
TwitchCommunity,
YoutubeStream,
)
from .errors import (
OfflineStream,
StreamNotFound,
APIError,
InvalidYoutubeCredentials,
CommunityNotFound,
OfflineCommunity,
StreamsError,
InvalidTwitchCredentials,
)
from . import streamtypes as StreamClasses from . import streamtypes as StreamClasses
from collections import defaultdict from collections import defaultdict
import asyncio import asyncio
@ -20,21 +35,11 @@ _ = Translator("Streams", __file__)
@cog_i18n(_) @cog_i18n(_)
class Streams: class Streams:
global_defaults = { global_defaults = {"tokens": {}, "streams": [], "communities": []}
"tokens": {},
"streams": [],
"communities": []
}
guild_defaults = { guild_defaults = {"autodelete": False, "mention_everyone": False, "mention_here": False}
"autodelete": False,
"mention_everyone": False,
"mention_here": False
}
role_defaults = { role_defaults = {"mention": False}
"mention": False
}
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.db = Config.get_conf(self, 26262626) self.db = Config.get_conf(self, 26262626)
@ -67,8 +72,7 @@ class Streams:
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 streaming""" """Checks if a Twitch channel is streaming"""
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, stream = TwitchStream(name=channel_name, token=token)
token=token)
await self.check_online(ctx, stream) await self.check_online(ctx, stream)
@commands.command() @commands.command()
@ -110,14 +114,21 @@ class Streams:
except StreamNotFound: except StreamNotFound:
await ctx.send(_("The channel doesn't seem to exist.")) await ctx.send(_("The channel doesn't seem to exist."))
except InvalidTwitchCredentials: except InvalidTwitchCredentials:
await ctx.send(_("The twitch token is either invalid or has not been set. " await ctx.send(
"See `{}`.").format("{}streamset twitchtoken".format(ctx.prefix))) _("The twitch token is either invalid or has not been set. " "See `{}`.").format(
"{}streamset twitchtoken".format(ctx.prefix)
)
)
except InvalidYoutubeCredentials: except InvalidYoutubeCredentials:
await ctx.send(_("The Youtube API key is either invalid or has not been set. " await ctx.send(
"See {}.").format("`{}streamset youtubekey`".format(ctx.prefix))) _("The Youtube API key is either invalid or has not been set. " "See {}.").format(
"`{}streamset youtubekey`".format(ctx.prefix)
)
)
except APIError: except APIError:
await ctx.send(_("Something went wrong whilst trying to contact the " await ctx.send(
"stream service's API.")) _("Something went wrong whilst trying to contact the " "stream service's API.")
)
else: else:
await ctx.send(embed=embed) await ctx.send(embed=embed)
@ -166,7 +177,7 @@ class Streams:
await self.stream_alert(ctx, PicartoStream, channel_name) await self.stream_alert(ctx, PicartoStream, channel_name)
@streamalert.command(name="stop") @streamalert.command(name="stop")
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 """Stops all stream notifications in the channel
Adding 'yes' will disable all notifications in the server""" Adding 'yes' will disable all notifications in the server"""
@ -191,8 +202,9 @@ class Streams:
self.streams = streams self.streams = streams
await self.save_streams() await self.save_streams()
msg = _("All {}'s stream alerts have been disabled." msg = _("All {}'s stream alerts have been disabled." "").format(
"").format("server" if _all else "channel") "server" if _all else "channel"
)
await ctx.send(msg) await ctx.send(msg)
@ -226,23 +238,29 @@ class Streams:
if is_yt and not self.check_name_or_id(channel_name): if is_yt and not self.check_name_or_id(channel_name):
stream = _class(id=channel_name, token=token) stream = _class(id=channel_name, token=token)
else: else:
stream = _class(name=channel_name, stream = _class(name=channel_name, token=token)
token=token)
try: try:
exists = await self.check_exists(stream) exists = await self.check_exists(stream)
except InvalidTwitchCredentials: except InvalidTwitchCredentials:
await ctx.send( await ctx.send(
_("The twitch token is either invalid or has not been set. " _("The twitch token is either invalid or has not been set. " "See {}.").format(
"See {}.").format("`{}streamset twitchtoken`".format(ctx.prefix))) "`{}streamset twitchtoken`".format(ctx.prefix)
)
)
return return
except InvalidYoutubeCredentials: except InvalidYoutubeCredentials:
await ctx.send(_("The Youtube API key is either invalid or has not been set. " await ctx.send(
"See {}.").format("`{}streamset youtubekey`".format(ctx.prefix))) _(
"The Youtube API key is either invalid or has not been set. " "See {}."
).format(
"`{}streamset youtubekey`".format(ctx.prefix)
)
)
return return
except APIError: except APIError:
await ctx.send( await ctx.send(
_("Something went wrong whilst trying to contact the " _("Something went wrong whilst trying to contact the " "stream service's API.")
"stream service's API.")) )
return return
else: else:
if not exists: if not exists:
@ -260,16 +278,18 @@ class Streams:
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. " _("The twitch token is either invalid or has not been set. " "See {}.").format(
"See {}.").format("`{}streamset twitchtoken`".format(ctx.prefix))) "`{}streamset twitchtoken`".format(ctx.prefix)
)
)
return return
except CommunityNotFound: except CommunityNotFound:
await ctx.send(_("That community doesn't seem to exist.")) await ctx.send(_("That community doesn't seem to exist."))
return return
except APIError: except APIError:
await ctx.send( await ctx.send(
_("Something went wrong whilst trying to contact the " _("Something went wrong whilst trying to contact the " "stream service's API.")
"stream service's API.")) )
return return
except OfflineCommunity: except OfflineCommunity:
pass pass
@ -331,12 +351,21 @@ class Streams:
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(_("{} will no longer be mentioned " await ctx.send(
"for a stream alert.").format("@\u200beveryone")) _("{} will no longer be mentioned " "for a stream alert.").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(_("When a stream configured for stream alerts " await ctx.send(
"comes online, {} will be mentioned").format("@\u200beveryone")) _(
"When a stream configured for stream alerts "
"comes online, {} will be mentioned"
).format(
"@\u200beveryone"
)
)
@mention.command(aliases=["here"]) @mention.command(aliases=["here"])
@commands.guild_only() @commands.guild_only()
@ -346,12 +375,19 @@ class Streams:
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 " await ctx.send(
"for a stream alert.").format("@\u200bhere")) _("{} will no longer be mentioned " "for a stream alert.").format("@\u200bhere")
)
else: else:
await self.db.guild(guild).mention_here.set(True) await self.db.guild(guild).mention_here.set(True)
await ctx.send(_("When a stream configured for stream alerts " await ctx.send(
"comes online, {} will be mentioned").format("@\u200bhere")) _(
"When a stream configured for stream alerts "
"comes online, {} will be mentioned"
).format(
"@\u200bhere"
)
)
@mention.command() @mention.command()
@commands.guild_only() @commands.guild_only()
@ -363,13 +399,22 @@ class Streams:
return return
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(_("{} will no longer be mentioned " await ctx.send(
"for a stream alert").format("@\u200b{}".format(role.name))) _("{} will no longer be mentioned " "for a stream alert").format(
"@\u200b{}".format(role.name)
)
)
else: else:
await self.db.role(role).mention.set(True) await self.db.role(role).mention.set(True)
await ctx.send(_("When a stream configured for stream alerts " await ctx.send(
"comes online, {} will be mentioned" _(
"").format("@\u200b{}".format(role.name))) "When a stream configured for stream alerts "
"comes online, {} will be mentioned"
""
).format(
"@\u200b{}".format(role.name)
)
)
@streamset.command() @streamset.command()
@commands.guild_only() @commands.guild_only()
@ -377,8 +422,7 @@ class Streams:
"""Toggles automatic deletion of notifications for streams that go offline""" """Toggles automatic deletion of notifications for streams that 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 " await ctx.send("The notifications will be deleted once " "streams go offline.")
"streams go offline.")
else: else:
await ctx.send("Notifications will never be deleted.") await ctx.send("Notifications will never be deleted.")
@ -387,14 +431,20 @@ class Streams:
stream.channels.append(ctx.channel.id) stream.channels.append(ctx.channel.id)
if stream not in self.streams: if stream not in self.streams:
self.streams.append(stream) self.streams.append(stream)
await ctx.send(_("I'll send a notification in this channel when {} " await ctx.send(
"is online.").format(stream.name)) _("I'll send a notification in this channel when {} " "is online.").format(
stream.name
)
)
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(_("I won't send notifications about {} in this " await ctx.send(
"channel anymore.").format(stream.name)) _("I won't send notifications about {} in this " "channel anymore.").format(
stream.name
)
)
await self.save_streams() await self.save_streams()
@ -403,16 +453,28 @@ class Streams:
community.channels.append(ctx.channel.id) community.channels.append(ctx.channel.id)
if community not in self.communities: if community not in self.communities:
self.communities.append(community) self.communities.append(community)
await ctx.send(_("I'll send a notification in this channel when a " await ctx.send(
"channel is streaming to the {} community" _(
"").format(community.name)) "I'll send a notification in this channel when a "
"channel is streaming to the {} community"
""
).format(
community.name
)
)
else: else:
community.channels.remove(ctx.channel.id) community.channels.remove(ctx.channel.id)
if not community.channels: if not community.channels:
self.communities.remove(community) self.communities.remove(community)
await ctx.send(_("I won't send notifications about channels streaming " await ctx.send(
"to the {} community in this channel anymore" _(
"").format(community.name)) "I won't send notifications about channels streaming "
"to the {} community in this channel anymore"
""
).format(
community.name
)
)
await self.save_communities() await self.save_communities()
def get_stream(self, _class, name): def get_stream(self, _class, name):
@ -499,13 +561,13 @@ class Streams:
settings = self.db.guild(guild) settings = self.db.guild(guild)
mentions = [] mentions = []
if await settings.mention_everyone(): if await settings.mention_everyone():
mentions.append('@everyone') mentions.append("@everyone")
if await settings.mention_here(): if await settings.mention_here():
mentions.append('@here') mentions.append("@here")
for role in guild.roles: for role in guild.roles:
if await self.db.role(role).mention(): if await self.db.role(role).mention():
mentions.append(role.mention) mentions.append(role.mention)
return ' '.join(mentions) return " ".join(mentions)
async def check_communities(self): async def check_communities(self):
for community in self.communities: for community in self.communities:
@ -579,8 +641,7 @@ class Streams:
# Fast dedupe below # Fast dedupe below
seen = set() seen = set()
seen_add = seen.add seen_add = seen.add
return [x for x in streams return [x for x in streams if not (x.name.lower() in seen or seen_add(x.name.lower()))]
if not (x.name.lower() in seen or seen_add(x.name.lower()))]
# return streams # return streams
@ -604,8 +665,7 @@ class Streams:
# Fast dedupe below # Fast dedupe below
seen = set() seen = set()
seen_add = seen.add seen_add = seen.add
return [x for x in communities return [x for x in communities if not (x.name.lower() in seen or seen_add(x.name.lower()))]
if not (x.name.lower() in seen or seen_add(x.name.lower()))]
# return communities # return communities
async def save_streams(self): async def save_streams(self):

View File

@ -1,5 +1,12 @@
from .errors import StreamNotFound, APIError, OfflineStream, CommunityNotFound, OfflineCommunity, \ from .errors import (
InvalidYoutubeCredentials, InvalidTwitchCredentials StreamNotFound,
APIError,
OfflineStream,
CommunityNotFound,
OfflineCommunity,
InvalidYoutubeCredentials,
InvalidTwitchCredentials,
)
from random import choice, sample from random import choice, sample
from string import ascii_letters from string import ascii_letters
import discord import discord
@ -23,6 +30,7 @@ def rnd(url):
class TwitchCommunity: class TwitchCommunity:
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.name = kwargs.pop("name") self.name = kwargs.pop("name")
self.id = kwargs.pop("id", None) self.id = kwargs.pop("id", None)
@ -32,15 +40,12 @@ class TwitchCommunity:
self.type = self.__class__.__name__ self.type = self.__class__.__name__
async def get_community_id(self): async def get_community_id(self):
headers = { headers = {"Accept": "application/vnd.twitchtv.v5+json", "Client-ID": str(self._token)}
"Accept": "application/vnd.twitchtv.v5+json", params = {"name": self.name}
"Client-ID": str(self._token)
}
params = {
"name": self.name
}
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(TWITCH_COMMUNITIES_ENDPOINT, headers=headers, params=params) as r: async with session.get(
TWITCH_COMMUNITIES_ENDPOINT, headers=headers, params=params
) as r:
data = await r.json() data = await r.json()
if r.status == 200: if r.status == 200:
return data["_id"] return data["_id"]
@ -57,14 +62,8 @@ class TwitchCommunity:
self.id = await self.get_community_id() self.id = await self.get_community_id()
except CommunityNotFound: except CommunityNotFound:
raise raise
headers = { headers = {"Accept": "application/vnd.twitchtv.v5+json", "Client-ID": str(self._token)}
"Accept": "application/vnd.twitchtv.v5+json", params = {"community_id": self.id, "limit": 100}
"Client-ID": str(self._token)
}
params = {
"community_id": self.id,
"limit": 100
}
url = TWITCH_BASE_URL + "/kraken/streams" url = TWITCH_BASE_URL + "/kraken/streams"
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers, params=params) as r: async with session.get(url, headers=headers, params=params) as r:
@ -82,14 +81,11 @@ class TwitchCommunity:
raise APIError() raise APIError()
async def make_embed(self, streams: list) -> discord.Embed: async def make_embed(self, streams: list) -> discord.Embed:
headers = { headers = {"Accept": "application/vnd.twitchtv.v5+json", "Client-ID": str(self._token)}
"Accept": "application/vnd.twitchtv.v5+json",
"Client-ID": str(self._token)
}
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get( async with session.get(
"{}/{}".format(TWITCH_COMMUNITIES_ENDPOINT, self.id), "{}/{}".format(TWITCH_COMMUNITIES_ENDPOINT, self.id), headers=headers
headers=headers) as r: ) as r:
data = await r.json() data = await r.json()
avatar = data["avatar_image_url"] avatar = data["avatar_image_url"]
@ -102,9 +98,7 @@ class TwitchCommunity:
else: else:
stream_list = streams stream_list = streams
for stream in stream_list: for stream in stream_list:
name = "[{}]({})".format( name = "[{}]({})".format(stream["channel"]["display_name"], stream["channel"]["url"])
stream["channel"]["display_name"], stream["channel"]["url"]
)
embed.add_field(name=stream["channel"]["status"], value=name, inline=False) embed.add_field(name=stream["channel"]["status"], value=name, inline=False)
embed.color = 0x6441A4 embed.color = 0x6441A4
@ -125,10 +119,11 @@ class TwitchCommunity:
class Stream: class Stream:
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.name = kwargs.pop("name", None) self.name = kwargs.pop("name", None)
self.channels = kwargs.pop("channels", []) self.channels = kwargs.pop("channels", [])
#self.already_online = kwargs.pop("already_online", False) # self.already_online = kwargs.pop("already_online", False)
self._messages_cache = kwargs.pop("_messages_cache", []) self._messages_cache = kwargs.pop("_messages_cache", [])
self.type = self.__class__.__name__ self.type = self.__class__.__name__
@ -153,6 +148,7 @@ class Stream:
class YoutubeStream(Stream): class YoutubeStream(Stream):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.id = kwargs.pop("id", None) self.id = kwargs.pop("id", None)
self._token = kwargs.pop("token", None) self._token = kwargs.pop("token", None)
@ -167,7 +163,7 @@ class YoutubeStream(Stream):
"part": "snippet", "part": "snippet",
"channelId": self.id, "channelId": self.id,
"type": "video", "type": "video",
"eventType": "live" "eventType": "live",
} }
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as r: async with session.get(url, params=params) as r:
@ -176,11 +172,7 @@ class YoutubeStream(Stream):
raise OfflineStream() raise OfflineStream()
elif "items" in data: elif "items" in data:
vid_id = data["items"][0]["id"]["videoId"] vid_id = data["items"][0]["id"]["videoId"]
params = { params = {"key": self._token, "id": vid_id, "part": "snippet"}
"key": self._token,
"id": vid_id,
"part": "snippet"
}
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(YOUTUBE_VIDEOS_ENDPOINT, params=params) as r: async with session.get(YOUTUBE_VIDEOS_ENDPOINT, params=params) as r:
data = await r.json() data = await r.json()
@ -199,17 +191,16 @@ class YoutubeStream(Stream):
return embed return embed
async def fetch_id(self): async def fetch_id(self):
params = { params = {"key": self._token, "forUsername": self.name, "part": "id"}
"key": self._token,
"forUsername": self.name,
"part": "id"
}
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(YOUTUBE_CHANNELS_ENDPOINT, params=params) as r: async with session.get(YOUTUBE_CHANNELS_ENDPOINT, params=params) as r:
data = await r.json() data = await r.json()
if "error" in data and data["error"]["code"] == 400 and\ if (
data["error"]["errors"][0]["reason"] == "keyInvalid": "error" in data
and data["error"]["code"] == 400
and data["error"]["errors"][0]["reason"] == "keyInvalid"
):
raise InvalidYoutubeCredentials() raise InvalidYoutubeCredentials()
elif "items" in data and len(data["items"]) == 0: elif "items" in data and len(data["items"]) == 0:
raise StreamNotFound() raise StreamNotFound()
@ -222,6 +213,7 @@ class YoutubeStream(Stream):
class TwitchStream(Stream): class TwitchStream(Stream):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.id = kwargs.pop("id", None) self.id = kwargs.pop("id", None)
self._token = kwargs.pop("token", None) self._token = kwargs.pop("token", None)
@ -232,19 +224,16 @@ class TwitchStream(Stream):
self.id = await self.fetch_id() self.id = await self.fetch_id()
url = TWITCH_STREAMS_ENDPOINT + self.id url = TWITCH_STREAMS_ENDPOINT + self.id
header = { header = {"Client-ID": str(self._token), "Accept": "application/vnd.twitchtv.v5+json"}
'Client-ID': str(self._token),
'Accept': 'application/vnd.twitchtv.v5+json'
}
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url, headers=header) as r: async with session.get(url, headers=header) as r:
data = await r.json(encoding='utf-8') data = await r.json(encoding="utf-8")
if r.status == 200: if r.status == 200:
if data["stream"] is None: if data["stream"] is None:
#self.already_online = False # self.already_online = False
raise OfflineStream() raise OfflineStream()
#self.already_online = True # self.already_online = True
# In case of rename # In case of rename
self.name = data["stream"]["channel"]["name"] self.name = data["stream"]["channel"]["name"]
return self.make_embed(data) return self.make_embed(data)
@ -256,10 +245,7 @@ class TwitchStream(Stream):
raise APIError() raise APIError()
async def fetch_id(self): async def fetch_id(self):
header = { header = {"Client-ID": str(self._token), "Accept": "application/vnd.twitchtv.v5+json"}
'Client-ID': str(self._token),
'Accept': 'application/vnd.twitchtv.v5+json'
}
url = TWITCH_ID_ENDPOINT + self.name url = TWITCH_ID_ENDPOINT + self.name
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
@ -280,8 +266,7 @@ class TwitchStream(Stream):
url = channel["url"] url = channel["url"]
logo = channel["logo"] logo = channel["logo"]
if logo is None: if logo is None:
logo = ("https://static-cdn.jtvnw.net/" logo = ("https://static-cdn.jtvnw.net/" "jtv_user_pictures/xarth/404_user_70x70.png")
"jtv_user_pictures/xarth/404_user_70x70.png")
status = channel["status"] status = channel["status"]
if not status: if not status:
status = "Untitled broadcast" status = "Untitled broadcast"
@ -303,21 +288,22 @@ class TwitchStream(Stream):
class HitboxStream(Stream): class HitboxStream(Stream):
async def is_online(self): async def is_online(self):
url = "https://api.hitbox.tv/media/live/" + self.name url = "https://api.hitbox.tv/media/live/" + self.name
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url) as r: async with session.get(url) as r:
#data = await r.json(encoding='utf-8') # data = await r.json(encoding='utf-8')
data = await r.text() data = await r.text()
data = json.loads(data, strict=False) data = json.loads(data, strict=False)
if "livestream" not in data: if "livestream" not in data:
raise StreamNotFound() raise StreamNotFound()
elif data["livestream"][0]["media_is_live"] == "0": elif data["livestream"][0]["media_is_live"] == "0":
#self.already_online = False # self.already_online = False
raise OfflineStream() raise OfflineStream()
elif data["livestream"][0]["media_is_live"] == "1": elif data["livestream"][0]["media_is_live"] == "1":
#self.already_online = True # self.already_online = True
return self.make_embed(data) return self.make_embed(data)
raise APIError() raise APIError()
@ -340,20 +326,21 @@ class HitboxStream(Stream):
class MixerStream(Stream): class MixerStream(Stream):
async def is_online(self): async def is_online(self):
url = "https://mixer.com/api/v1/channels/" + self.name url = "https://mixer.com/api/v1/channels/" + self.name
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url) as r: async with session.get(url) as r:
#data = await r.json(encoding='utf-8') # data = await r.json(encoding='utf-8')
data = await r.text(encoding='utf-8') data = await r.text(encoding="utf-8")
if r.status == 200: if r.status == 200:
data = json.loads(data, strict=False) data = json.loads(data, strict=False)
if data["online"] is True: if data["online"] is True:
#self.already_online = True # self.already_online = True
return self.make_embed(data) return self.make_embed(data)
else: else:
#self.already_online = False # self.already_online = False
raise OfflineStream() raise OfflineStream()
elif r.status == 404: elif r.status == 404:
raise StreamNotFound() raise StreamNotFound()
@ -361,8 +348,7 @@ class MixerStream(Stream):
raise APIError() raise APIError()
def make_embed(self, data): def make_embed(self, data):
default_avatar = ("https://mixer.com/_latest/assets/images/main/" default_avatar = ("https://mixer.com/_latest/assets/images/main/" "avatars/default.jpg")
"avatars/default.jpg")
user = data["user"] user = data["user"]
url = "https://mixer.com/" + data["token"] url = "https://mixer.com/" + data["token"]
embed = discord.Embed(title=data["name"], url=url) embed = discord.Embed(title=data["name"], url=url)
@ -382,19 +368,20 @@ class MixerStream(Stream):
class PicartoStream(Stream): class PicartoStream(Stream):
async def is_online(self): async def is_online(self):
url = "https://api.picarto.tv/v1/channel/name/" + self.name url = "https://api.picarto.tv/v1/channel/name/" + self.name
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url) as r: async with session.get(url) as r:
data = await r.text(encoding='utf-8') data = await r.text(encoding="utf-8")
if r.status == 200: if r.status == 200:
data = json.loads(data) data = json.loads(data)
if data["online"] is True: if data["online"] is True:
#self.already_online = True # self.already_online = True
return self.make_embed(data) return self.make_embed(data)
else: else:
#self.already_online = False # self.already_online = False
raise OfflineStream() raise OfflineStream()
elif r.status == 404: elif r.status == 404:
raise StreamNotFound() raise StreamNotFound()
@ -402,8 +389,9 @@ class PicartoStream(Stream):
raise APIError() raise APIError()
def make_embed(self, data): def make_embed(self, data):
avatar = rnd("https://picarto.tv/user_data/usrimg/{}/dsdefault.jpg" avatar = rnd(
"".format(data["name"].lower())) "https://picarto.tv/user_data/usrimg/{}/dsdefault.jpg" "".format(data["name"].lower())
)
url = "https://picarto.tv/" + data["name"] url = "https://picarto.tv/" + data["name"]
thumbnail = data["thumbnails"]["web"] thumbnail = data["thumbnails"]["web"]
embed = discord.Embed(title=data["title"], url=url) embed = discord.Embed(title=data["title"], url=url)
@ -424,6 +412,5 @@ class PicartoStream(Stream):
data["adult"] = "" data["adult"] = ""
embed.color = 0x4C90F3 embed.color = 0x4C90F3
embed.set_footer(text="{adult}Category: {category} | Tags: {tags}" embed.set_footer(text="{adult}Category: {category} | Tags: {tags}" "".format(**data))
"".format(**data))
return embed return embed

View File

@ -1,14 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../mod.py"]
'../mod.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -10,11 +10,13 @@ from .log import LOG
__all__ = ["TriviaSession"] __all__ = ["TriviaSession"]
_REVEAL_MESSAGES = ("I know this one! {}!", "Easy: {}.", _REVEAL_MESSAGES = ("I know this one! {}!", "Easy: {}.", "Oh really? It's {} of course.")
"Oh really? It's {} of course.") _FAIL_MESSAGES = (
_FAIL_MESSAGES = ("To the next one I guess...", "Moving on...", "To the next one I guess...",
"I'm sure you'll know the answer of the next one.", "Moving on...",
"\N{PENSIVE FACE} Next one.") "I'm sure you'll know the answer of the next one.",
"\N{PENSIVE FACE} Next one.",
)
class TriviaSession(): class TriviaSession():
@ -49,10 +51,7 @@ class TriviaSession():
""" """
def __init__(self, def __init__(self, ctx, question_list: dict, settings: dict):
ctx,
question_list: dict,
settings: dict):
self.ctx = ctx self.ctx = ctx
list_ = list(question_list.items()) list_ = list(question_list.items())
random.shuffle(list_) random.shuffle(list_)
@ -128,9 +127,9 @@ class TriviaSession():
num_lists = len(list_names) num_lists = len(list_names)
if num_lists > 2: if num_lists > 2:
# at least 3 lists, join all but last with comma # at least 3 lists, join all but last with comma
msg = ", ".join(list_names[:num_lists-1]) msg = ", ".join(list_names[:num_lists - 1])
# join onto last with "and" # join onto last with "and"
msg = " and ".join((msg, list_names[num_lists-1])) msg = " and ".join((msg, list_names[num_lists - 1]))
else: else:
# either 1 or 2 lists, join together with "and" # either 1 or 2 lists, join together with "and"
msg = " and ".join(list_names) msg = " and ".join(list_names)
@ -150,10 +149,7 @@ class TriviaSession():
answers = _parse_answers(answers) answers = _parse_answers(answers)
yield question, answers yield question, answers
async def wait_for_answer(self, async def wait_for_answer(self, answers, delay: float, timeout: float):
answers,
delay: float,
timeout: float):
"""Wait for a correct answer, and then respond. """Wait for a correct answer, and then respond.
Scores are also updated in this method. Scores are also updated in this method.
@ -178,7 +174,8 @@ class TriviaSession():
""" """
try: try:
message = await self.ctx.bot.wait_for( message = await self.ctx.bot.wait_for(
"message", check=self.check_answer(answers), timeout=delay) "message", check=self.check_answer(answers), timeout=delay
)
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.")
@ -194,8 +191,7 @@ class TriviaSession():
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( reply = "You got it {}! **+1** to you!".format(message.author.display_name)
message.author.display_name)
await self.ctx.send(reply) await self.ctx.send(reply)
return True return True
@ -218,9 +214,11 @@ class TriviaSession():
""" """
answers = tuple(s.lower() for s in answers) answers = tuple(s.lower() for s in answers)
def _pred(message: discord.Message): def _pred(message: discord.Message):
early_exit = (message.channel != self.ctx.channel early_exit = (
or message.author == self.ctx.guild.me) message.channel != self.ctx.channel or message.author == self.ctx.guild.me
)
if early_exit: if early_exit:
return False return False
@ -260,8 +258,7 @@ class TriviaSession():
"""Cancel whichever tasks this session is running.""" """Cancel whichever tasks this session is running."""
self._task.cancel() self._task.cancel()
channel = self.ctx.channel channel = self.ctx.channel
LOG.debug("Force stopping trivia session; #%s in %s", channel, LOG.debug("Force stopping trivia session; #%s in %s", channel, channel.guild.id)
channel.guild.id)
async def pay_winner(self, multiplier: float): async def pay_winner(self, multiplier: float):
"""Pay the winner of this trivia session. """Pay the winner of this trivia session.
@ -275,8 +272,7 @@ class TriviaSession():
paid. paid.
""" """
(winner, score) = next((tup for tup in self.scores.most_common(1)), (winner, score) = next((tup for tup in self.scores.most_common(1)), (None, None))
(None, None))
me_ = self.ctx.guild.me me_ = self.ctx.guild.me
if winner is not None and winner != me_ and score > 0: if winner is not None and winner != me_ and score > 0:
contestants = list(self.scores.keys()) contestants = list(self.scores.keys())
@ -285,13 +281,12 @@ class TriviaSession():
if len(contestants) >= 3: if len(contestants) >= 3:
amount = int(multiplier * score) amount = int(multiplier * score)
if amount > 0: if amount > 0:
LOG.debug("Paying trivia winner: %d credits --> %s", LOG.debug("Paying trivia winner: %d credits --> %s", amount, str(winner))
amount, str(winner))
await deposit_credits(winner, int(multiplier * score)) await deposit_credits(winner, int(multiplier * score))
await self.ctx.send( await self.ctx.send(
"Congratulations, {0}, you have received {1} credits" "Congratulations, {0}, you have received {1} credits"
" for coming first.".format(winner.display_name, " for coming first.".format(winner.display_name, amount)
amount)) )
def _parse_answers(answers): def _parse_answers(answers):

View File

@ -26,8 +26,7 @@ class Trivia:
def __init__(self): def __init__(self):
self.trivia_sessions = [] self.trivia_sessions = []
self.conf = Config.get_conf( self.conf = Config.get_conf(self, identifier=UNIQUE_ID, force_registration=True)
self, identifier=UNIQUE_ID, force_registration=True)
self.conf.register_guild( self.conf.register_guild(
max_score=10, max_score=10,
@ -36,10 +35,10 @@ class Trivia:
bot_plays=False, bot_plays=False,
reveal_answer=True, reveal_answer=True,
payout_multiplier=0.0, payout_multiplier=0.0,
allow_override=True) allow_override=True,
)
self.conf.register_member( self.conf.register_member(wins=0, games=0, total_score=0)
wins=0, games=0, total_score=0)
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@ -60,7 +59,8 @@ class Trivia:
"Payout multiplier: {payout_multiplier}\n" "Payout multiplier: {payout_multiplier}\n"
"Allow lists to override settings: {allow_override}" "Allow lists to override settings: {allow_override}"
"".format(**settings_dict), "".format(**settings_dict),
lang="py") lang="py",
)
await ctx.send(msg) await ctx.send(msg)
@triviaset.command(name="maxscore") @triviaset.command(name="maxscore")
@ -81,8 +81,7 @@ class Trivia:
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 {}." await ctx.send("Done. Maximum seconds to answer set to {}." "".format(seconds))
"".format(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):
@ -92,38 +91,41 @@ class Trivia:
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("Done. Trivia sessions will now time out after {}" await ctx.send(
" seconds of no responses.".format(seconds)) "Done. Trivia sessions will now time out after {}"
" seconds of no responses.".format(seconds)
)
@triviaset.command(name="override") @triviaset.command(name="override")
async def triviaset_allowoverride(self, async def triviaset_allowoverride(self, ctx: commands.Context, enabled: bool):
ctx: commands.Context,
enabled: bool):
"""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" enabled = "now" if enabled else "no longer"
await ctx.send("Done. Trivia lists can {} override the trivia settings" await ctx.send(
" for this server.".format(enabled)) "Done. Trivia lists can {} override the trivia settings"
" for this server.".format(enabled)
)
@triviaset.command(name="botplays") @triviaset.command(name="botplays")
async def trivaset_bot_plays(self, async def trivaset_bot_plays(self, ctx: commands.Context, true_or_false: bool):
ctx: commands.Context,
true_or_false: 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(true_or_false)
await ctx.send("Done. " + ( await ctx.send(
"I'll gain a point if users don't answer in time." if true_or_false "Done. "
else "Alright, I won't embarass you at trivia anymore.")) + (
"I'll gain a point if users don't answer in time."
if true_or_false
else "Alright, I won't embarass you at trivia anymore."
)
)
@triviaset.command(name="revealanswer") @triviaset.command(name="revealanswer")
async def trivaset_reveal_answer(self, async def trivaset_reveal_answer(self, ctx: commands.Context, true_or_false: bool):
ctx: commands.Context,
true_or_false: 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
@ -131,15 +133,18 @@ class Trivia:
""" """
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(true_or_false)
await ctx.send("Done. " + ( await ctx.send(
"I'll reveal the answer if no one knows it." if true_or_false else "Done. "
"I won't reveal the answer to the questions anymore.")) + (
"I'll reveal the answer if no one knows it."
if true_or_false
else "I won't reveal the answer to the questions anymore."
)
)
@triviaset.command(name="payout") @triviaset.command(name="payout")
@check_global_setting_admin() @check_global_setting_admin()
async def triviaset_payout_multiplier(self, async def triviaset_payout_multiplier(self, ctx: commands.Context, multiplier: float):
ctx: commands.Context,
multiplier: float):
"""Set the payout multiplier. """Set the payout multiplier.
This can be any positive decimal number. If a user wins trivia when at This can be any positive decimal number. If a user wins trivia when at
@ -155,8 +160,7 @@ class Trivia:
return return
await settings.payout_multiplier.set(multiplier) await settings.payout_multiplier.set(multiplier)
if not multiplier: if not multiplier:
await ctx.send("Done. I will no longer reward the winner with a" await ctx.send("Done. I will no longer reward the winner with a" " payout.")
" payout.")
return return
await ctx.send("Done. Payout multiplier set to {}.".format(multiplier)) await ctx.send("Done. Payout multiplier set to {}.".format(multiplier))
@ -174,8 +178,7 @@ class Trivia:
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( await ctx.send("There is already an ongoing trivia session in this channel.")
"There is already an ongoing trivia session in this channel.")
return return
trivia_dict = {} trivia_dict = {}
authors = [] authors = []
@ -185,21 +188,26 @@ class Trivia:
try: try:
dict_ = self.get_trivia_list(category) dict_ = self.get_trivia_list(category)
except FileNotFoundError: except FileNotFoundError:
await ctx.send("Invalid category `{0}`. See `{1}trivia list`" await ctx.send(
" for a list of trivia categories." "Invalid category `{0}`. See `{1}trivia list`"
"".format(category, ctx.prefix)) " for a list of trivia categories."
"".format(category, ctx.prefix)
)
except InvalidListError: except InvalidListError:
await ctx.send("There was an error parsing the trivia list for" await ctx.send(
" the `{}` category. It may be formatted" "There was an error parsing the trivia list for"
" incorrectly.".format(category)) " the `{}` category. It may be formatted"
" incorrectly.".format(category)
)
else: else:
trivia_dict.update(dict_) trivia_dict.update(dict_)
authors.append(trivia_dict.pop("AUTHOR", None)) authors.append(trivia_dict.pop("AUTHOR", None))
continue continue
return return
if not trivia_dict: if not trivia_dict:
await ctx.send("The trivia list was parsed successfully, however" await ctx.send(
" 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()
config = trivia_dict.pop("CONFIG", None) config = trivia_dict.pop("CONFIG", None)
@ -215,13 +223,16 @@ class Trivia:
"""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( await ctx.send("There is no ongoing trivia session in this channel.")
"There is no ongoing trivia session in this channel.")
return return
author = ctx.author author = ctx.author
auth_checks = (await ctx.bot.is_owner(author), await auth_checks = (
ctx.bot.is_mod(author), await ctx.bot.is_admin(author), await ctx.bot.is_owner(author),
author == ctx.guild.owner, author == session.ctx.author) await ctx.bot.is_mod(author),
await ctx.bot.is_admin(author),
author == ctx.guild.owner,
author == session.ctx.author,
)
if any(auth_checks): if any(auth_checks):
await session.end_game() await session.end_game()
session.force_stop() session.force_stop()
@ -234,8 +245,7 @@ class Trivia:
"""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())
msg = box("**Available trivia lists**\n\n{}" msg = box("**Available trivia lists**\n\n{}" "".format(", ".join(sorted(lists))))
"".format(", ".join(sorted(lists))))
if len(msg) > 1000: if len(msg) > 1000:
await ctx.author.send(msg) await ctx.author.send(msg)
return return
@ -256,10 +266,9 @@ class Trivia:
@trivia_leaderboard.command(name="server") @trivia_leaderboard.command(name="server")
@commands.guild_only() @commands.guild_only()
async def trivia_leaderboard_server(self, async def trivia_leaderboard_server(
ctx: commands.Context, self, ctx: commands.Context, sort_by: str = "wins", top: int = 10
sort_by: str="wins", ):
top: int=10):
"""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:
@ -271,9 +280,11 @@ class Trivia:
""" """
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("Unknown field `{}`, see `{}help trivia " await ctx.send(
"leaderboard server` for valid fields to sort by." "Unknown field `{}`, see `{}help trivia "
"".format(sort_by, ctx.prefix)) "leaderboard server` for valid fields to sort by."
"".format(sort_by, ctx.prefix)
)
return return
guild = ctx.guild guild = ctx.guild
data = await self.conf.all_members(guild) data = await self.conf.all_members(guild)
@ -282,10 +293,9 @@ class Trivia:
await self.send_leaderboard(ctx, data, key, top) await self.send_leaderboard(ctx, data, key, top)
@trivia_leaderboard.command(name="global") @trivia_leaderboard.command(name="global")
async def trivia_leaderboard_global(self, async def trivia_leaderboard_global(
ctx: commands.Context, self, ctx: commands.Context, sort_by: str = "wins", top: int = 10
sort_by: str="wins", ):
top: int=10):
"""Global trivia leaderboard. """Global trivia leaderboard.
<sort_by> can be any of the following fields: <sort_by> can be any of the following fields:
@ -298,9 +308,11 @@ class Trivia:
""" """
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("Unknown field `{}`, see `{}help trivia " await ctx.send(
"leaderboard global` for valid fields to sort by." "Unknown field `{}`, see `{}help trivia "
"".format(sort_by, ctx.prefix)) "leaderboard global` for valid fields to sort by."
"".format(sort_by, ctx.prefix)
)
return return
data = await self.conf.all_members() data = await self.conf.all_members()
collated_data = {} collated_data = {}
@ -327,11 +339,7 @@ class Trivia:
elif key in ("total", "score", "answers", "correct"): elif key in ("total", "score", "answers", "correct"):
return "total_score" return "total_score"
async def send_leaderboard(self, async def send_leaderboard(self, ctx: commands.Context, data: dict, key: str, top: int):
ctx: commands.Context,
data: dict,
key: str,
top: int):
"""Send the leaderboard from the given data. """Send the leaderboard from the given data.
Parameters Parameters
@ -382,23 +390,34 @@ class Trivia:
items = sorted(items, key=lambda t: t[1][key], reverse=True) items = sorted(items, key=lambda t: t[1][key], reverse=True)
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 = ("Rank", "Member{}".format(" " * (max_name_len - 6)), "Wins", headers = (
"Games Played", "Total Score", "Average Score") "Rank",
"Member{}".format(" " * (max_name_len - 6)),
"Wins",
"Games Played",
"Total Score",
"Average Score",
)
lines = [" | ".join(headers)] lines = [" | ".join(headers)]
# Header underlines # Header underlines
lines.append(" | ".join(("-" * len(h) for h in headers))) 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
fields = tuple(map(str, (rank, fields = tuple(
member, map(
m_data["wins"], str,
m_data["games"], (
m_data["total_score"], rank,
round(m_data["average_score"], 2)))) member,
padding = [ m_data["wins"],
" " * (len(h) - len(f)) for h, f in zip(headers, fields) m_data["games"],
] m_data["total_score"],
round(m_data["average_score"], 2),
),
)
)
padding = [" " * (len(h) - len(f)) for h, f in zip(headers, fields)]
fields = tuple(f + padding[i] for i, f in enumerate(fields)) fields = tuple(f + padding[i] for i, f in enumerate(fields))
lines.append(" | ".join(fields).format(member=member, **m_data)) lines.append(" | ".join(fields).format(member=member, **m_data))
if rank == top: if rank == top:
@ -418,8 +437,7 @@ class Trivia:
""" """
channel = session.ctx.channel channel = session.ctx.channel
LOG.debug("Ending trivia session; #%s in %s", channel, LOG.debug("Ending trivia session; #%s in %s", channel, channel.guild.id)
channel.guild.id)
if session in self.trivia_sessions: if session in self.trivia_sessions:
self.trivia_sessions.remove(session) self.trivia_sessions.remove(session)
if session.scores: if session.scores:
@ -462,10 +480,9 @@ class Trivia:
try: try:
path = next(p for p in self._all_lists() if p.stem == category) path = next(p for p in self._all_lists() if p.stem == category)
except StopIteration: except StopIteration:
raise FileNotFoundError("Could not find the `{}` category" raise FileNotFoundError("Could not find the `{}` category" "".format(category))
"".format(category))
with path.open(encoding='utf-8') as file: with path.open(encoding="utf-8") as file:
try: try:
dict_ = yaml.load(file) dict_ = yaml.load(file)
except yaml.error.YAMLError as exc: except yaml.error.YAMLError as exc:
@ -473,14 +490,13 @@ class Trivia:
else: else:
return dict_ return dict_
def _get_trivia_session(self, def _get_trivia_session(self, channel: discord.TextChannel) -> TriviaSession:
channel: discord.TextChannel) -> TriviaSession: return next(
return next((session for session in self.trivia_sessions (session for session in self.trivia_sessions if session.ctx.channel == channel), None
if session.ctx.channel == channel), None) )
def _all_lists(self): def _all_lists(self):
personal_lists = tuple(p.resolve() personal_lists = tuple(p.resolve() for p in cog_data_path(self).glob("*.yaml"))
for p in cog_data_path(self).glob("*.yaml"))
return personal_lists + tuple(ext_trivia.lists()) return personal_lists + tuple(ext_trivia.lists())

View File

@ -9,7 +9,9 @@ from redbot.core.i18n import Translator
_ = Translator("Warnings", __file__) _ = Translator("Warnings", __file__)
async def warning_points_add_check(config: Config, ctx: commands.Context, user: discord.Member, points: int): async def warning_points_add_check(
config: Config, ctx: commands.Context, user: discord.Member, points: int
):
"""Handles any action that needs to be taken or not based on the points""" """Handles any action that needs to be taken or not based on the points"""
guild = ctx.guild guild = ctx.guild
guild_settings = config.guild(guild) guild_settings = config.guild(guild)
@ -24,7 +26,9 @@ async def warning_points_add_check(config: Config, ctx: commands.Context, user:
await create_and_invoke_context(ctx, act["exceed_command"], user) await create_and_invoke_context(ctx, act["exceed_command"], user)
async def warning_points_remove_check(config: Config, ctx: commands.Context, user: discord.Member, points: int): async def warning_points_remove_check(
config: Config, ctx: commands.Context, user: discord.Member, points: int
):
guild = ctx.guild guild = ctx.guild
guild_settings = config.guild(guild) guild_settings = config.guild(guild)
act = {} act = {}
@ -38,7 +42,9 @@ async def warning_points_remove_check(config: Config, ctx: commands.Context, use
await create_and_invoke_context(ctx, act["drop_command"], user) await create_and_invoke_context(ctx, act["drop_command"], user)
async def create_and_invoke_context(realctx: commands.Context, command_str: str, user: discord.Member): async def create_and_invoke_context(
realctx: commands.Context, command_str: str, user: discord.Member
):
m = copy(realctx.message) m = copy(realctx.message)
m.content = command_str.format(user=user.mention, prefix=realctx.prefix) m.content = command_str.format(user=user.mention, prefix=realctx.prefix)
fctx = await realctx.bot.get_context(m, cls=commands.Context) fctx = await realctx.bot.get_context(m, cls=commands.Context)
@ -54,7 +60,7 @@ def get_command_from_input(bot, userinput: str):
while com is None: while com is None:
com = bot.get_command(userinput) com = bot.get_command(userinput)
if com is None: if com is None:
userinput = ' '.join(userinput.split(' ')[:-1]) userinput = " ".join(userinput.split(" ")[:-1])
if len(userinput) == 0: if len(userinput) == 0:
break break
if com is None: if com is None:
@ -63,8 +69,9 @@ def get_command_from_input(bot, userinput: str):
check_str = inspect.getsource(checks.is_owner) check_str = inspect.getsource(checks.is_owner)
if any(inspect.getsource(x) in check_str for x in com.checks): if any(inspect.getsource(x) in check_str for x in com.checks):
# command the user specified has the is_owner check # command the user specified has the is_owner check
return None, _("That command requires bot owner. I can't " return None, _(
"allow you to use that for an action") "That command requires bot owner. I can't " "allow you to use that for an action"
)
return "{prefix}" + orig, None return "{prefix}" + orig, None
@ -72,13 +79,15 @@ async def get_command_for_exceeded_points(ctx: commands.Context):
"""Gets the command to be executed when the user is at or exceeding """Gets the command to be executed when the user is at or exceeding
the points threshold for the action""" the points threshold for the action"""
await ctx.send( await ctx.send(
_("Enter the command to be run when the user exceeds the points for " _(
"this action to occur.\nEnter it exactly as you would if you were " "Enter the command to be run when the user exceeds the points for "
"actually trying to run the command, except don't put a prefix and " "this action to occur.\nEnter it exactly as you would if you were "
"use {user} in place of any user/member arguments\n\n" "actually trying to run the command, except don't put a prefix and "
"WARNING: The command entered will be run without regard to checks or cooldowns. " "use {user} in place of any user/member arguments\n\n"
"Commands requiring bot owner are not allowed for security reasons.\n\n" "WARNING: The command entered will be run without regard to checks or cooldowns. "
"Please wait 15 seconds before entering your response.") "Commands requiring bot owner are not allowed for security reasons.\n\n"
"Please wait 15 seconds before entering your response."
)
) )
await asyncio.sleep(15) await asyncio.sleep(15)
@ -110,15 +119,17 @@ async def get_command_for_dropping_points(ctx: commands.Context):
when the user exceeded the threshold when the user exceeded the threshold
""" """
await ctx.send( await ctx.send(
_("Enter the command to be run when the user returns to a value below " _(
"the points for this action to occur. Please note that this is " "Enter the command to be run when the user returns to a value below "
"intended to be used for reversal of the action taken when the user " "the points for this action to occur. Please note that this is "
"exceeded the action's point value\nEnter it exactly as you would " "intended to be used for reversal of the action taken when the user "
"if you were actually trying to run the command, except don't put a prefix " "exceeded the action's point value\nEnter it exactly as you would "
"and use {user} in place of any user/member arguments\n\n" "if you were actually trying to run the command, except don't put a prefix "
"WARNING: The command entered will be run without regard to checks or cooldowns. " "and use {user} in place of any user/member arguments\n\n"
"Commands requiring bot owner are not allowed for security reasons.\n\n" "WARNING: The command entered will be run without regard to checks or cooldowns. "
"Please wait 15 seconds before entering your response.") "Commands requiring bot owner are not allowed for security reasons.\n\n"
"Please wait 15 seconds before entering your response."
)
) )
await asyncio.sleep(15) await asyncio.sleep(15)

View File

@ -1,15 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../warnings.py", "../helpers.py"]
'../warnings.py',
'../helpers.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -3,8 +3,12 @@ from collections import namedtuple
import discord import discord
import asyncio import asyncio
from redbot.cogs.warnings.helpers import warning_points_add_check, get_command_for_exceeded_points, \ from redbot.cogs.warnings.helpers import (
get_command_for_dropping_points, warning_points_remove_check warning_points_add_check,
get_command_for_exceeded_points,
get_command_for_dropping_points,
warning_points_remove_check,
)
from redbot.core import Config, modlog, checks, commands from redbot.core import Config, modlog, checks, 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
@ -18,17 +22,9 @@ _ = Translator("Warnings", __file__)
class Warnings: class Warnings:
"""A warning system for Red""" """A warning system for Red"""
default_guild = { default_guild = {"actions": [], "reasons": {}, "allow_custom_reasons": False}
"actions": [],
"reasons": {},
"allow_custom_reasons": False
}
default_member = { default_member = {"total_points": 0, "status": "", "warnings": {}}
"total_points": 0,
"status": "",
"warnings": {}
}
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.config = Config.get_conf(self, identifier=5757575755) self.config = Config.get_conf(self, identifier=5757575755)
@ -41,9 +37,7 @@ class Warnings:
@staticmethod @staticmethod
async def register_warningtype(): async def register_warningtype():
try: try:
await modlog.register_casetype( await modlog.register_casetype("warning", True, "\N{WARNING SIGN}", "Warning", None)
"warning", True, "\N{WARNING SIGN}", "Warning", None
)
except RuntimeError: except RuntimeError:
pass pass
@ -105,7 +99,7 @@ class Warnings:
"action_name": name, "action_name": name,
"points": points, "points": points,
"exceed_command": exceed_command, "exceed_command": exceed_command,
"drop_command": drop_command "drop_command": drop_command,
} }
# Have all details for the action, now save the action # Have all details for the action, now save the action
@ -138,9 +132,7 @@ class Warnings:
registered_actions.remove(to_remove) registered_actions.remove(to_remove)
await ctx.tick() await ctx.tick()
else: else:
await ctx.send( await ctx.send(_("No action named {} exists!").format(action_name))
_("No action named {} exists!").format(action_name)
)
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@ -159,13 +151,8 @@ class Warnings:
if name.lower() == "custom": if name.lower() == "custom":
await ctx.send("That cannot be used as a reason name!") await ctx.send("That cannot be used as a reason name!")
return return
to_add = { to_add = {"points": points, "description": description}
"points": points, completed = {name.lower(): to_add}
"description": description
}
completed = {
name.lower(): to_add
}
guild_settings = self.config.guild(guild) guild_settings = self.config.guild(guild)
@ -219,8 +206,7 @@ class Warnings:
msg_list.append( msg_list.append(
"Name: {}\nPoints: {}\nExceed command: {}\n" "Name: {}\nPoints: {}\nExceed command: {}\n"
"Drop command: {}".format( "Drop command: {}".format(
r["action_name"], r["points"], r["exceed_command"], r["action_name"], r["points"], r["exceed_command"], r["drop_command"]
r["drop_command"]
) )
) )
if msg_list: if msg_list:
@ -262,7 +248,7 @@ class Warnings:
str(ctx.message.id): { str(ctx.message.id): {
"points": reason_type["points"], "points": reason_type["points"],
"description": reason_type["description"], "description": reason_type["description"],
"mod": ctx.author.id "mod": ctx.author.id,
} }
} }
async with member_settings.warnings() as user_warnings: async with member_settings.warnings() as user_warnings:
@ -275,7 +261,7 @@ class Warnings:
@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. """Show warnings for the specified user.
If userid is None, show warnings for the person running the command If userid is None, show warnings for the person running the command
Note that showing warnings for users other than yourself requires Note that showing warnings for users other than yourself requires
@ -285,10 +271,7 @@ class Warnings:
else: else:
if not await is_admin_or_superior(self.bot, ctx.author): if not await is_admin_or_superior(self.bot, ctx.author):
await ctx.send( await ctx.send(
warning( warning(_("You are not allowed to check " "warnings for other users!"))
_("You are not allowed to check "
"warnings for other users!")
)
) )
return return
else: else:
@ -305,22 +288,14 @@ class Warnings:
mod = ctx.guild.get_member(user_warnings[key]["mod"]) mod = ctx.guild.get_member(user_warnings[key]["mod"])
if mod is None: if mod is None:
mod = discord.utils.get( mod = discord.utils.get(
self.bot.get_all_members(), self.bot.get_all_members(), id=user_warnings[key]["mod"]
id=user_warnings[key]["mod"]
) )
if mod is None: if mod is None:
mod = await self.bot.get_user_info( mod = await self.bot.get_user_info(user_warnings[key]["mod"])
user_warnings[key]["mod"]
)
msg += "{} point warning {} issued by {} for {}\n".format( msg += "{} point warning {} issued by {} for {}\n".format(
user_warnings[key]["points"], user_warnings[key]["points"], key, mod, user_warnings[key]["description"]
key,
mod,
user_warnings[key]["description"]
) )
await ctx.send_interactive( await ctx.send_interactive(pagify(msg), box_lang="Warnings for {}".format(user))
pagify(msg), box_lang="Warnings for {}".format(user)
)
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@ -348,10 +323,7 @@ class Warnings:
@staticmethod @staticmethod
async def custom_warning_reason(ctx: commands.Context): async def custom_warning_reason(ctx: commands.Context):
"""Handles getting description and points for custom reasons""" """Handles getting description and points for custom reasons"""
to_add = { to_add = {"points": 0, "description": ""}
"points": 0,
"description": ""
}
def same_author_check(m): def same_author_check(m):
return m.author == ctx.author return m.author == ctx.author

View File

@ -4,16 +4,15 @@ __all__ = ["Config", "__version__"]
class VersionInfo: class VersionInfo:
def __init__(self, major, minor, micro, releaselevel, serial): def __init__(self, major, minor, micro, releaselevel, serial):
self._levels = ['alpha', 'beta', 'final'] self._levels = ["alpha", "beta", "final"]
self.major = major self.major = major
self.minor = minor self.minor = minor
self.micro = micro self.micro = micro
if releaselevel not in self._levels: if releaselevel not in self._levels:
raise TypeError("'releaselevel' must be one of: {}".format( raise TypeError("'releaselevel' must be one of: {}".format(", ".join(self._levels)))
', '.join(self._levels)
))
self.releaselevel = releaselevel self.releaselevel = releaselevel
self.serial = serial self.serial = serial
@ -21,8 +20,9 @@ class VersionInfo:
def __lt__(self, other): def __lt__(self, other):
my_index = self._levels.index(self.releaselevel) my_index = self._levels.index(self.releaselevel)
other_index = self._levels.index(other.releaselevel) other_index = self._levels.index(other.releaselevel)
return (self.major, self.minor, self.micro, my_index, self.serial) < \ return (self.major, self.minor, self.micro, my_index, self.serial) < (
(other.major, other.minor, other.micro, other_index, other.serial) other.major, other.minor, other.micro, other_index, other.serial
)
def __repr__(self): def __repr__(self):
return "VersionInfo(major={}, minor={}, micro={}, releaselevel={}, serial={})".format( return "VersionInfo(major={}, minor={}, micro={}, releaselevel={}, serial={})".format(
@ -32,5 +32,6 @@ class VersionInfo:
def to_json(self): def to_json(self):
return [self.major, self.minor, self.micro, self.releaselevel, self.serial] return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
__version__ = "3.0.0b14" __version__ = "3.0.0b14"
version_info = VersionInfo(3, 0, 0, 'beta', 14) version_info = VersionInfo(3, 0, 0, "beta", 14)

View File

@ -6,29 +6,36 @@ import discord
from redbot.core import Config from redbot.core import Config
__all__ = ["Account", "get_balance", "set_balance", "withdraw_credits", "deposit_credits", __all__ = [
"can_spend", "transfer_credits", "wipe_bank", "get_account", "is_global", "Account",
"set_global", "get_bank_name", "set_bank_name", "get_currency_name", "get_balance",
"set_currency_name", "get_default_balance", "set_default_balance"] "set_balance",
"withdraw_credits",
"deposit_credits",
"can_spend",
"transfer_credits",
"wipe_bank",
"get_account",
"is_global",
"set_global",
"get_bank_name",
"set_bank_name",
"get_currency_name",
"set_currency_name",
"get_default_balance",
"set_default_balance",
]
_DEFAULT_GLOBAL = { _DEFAULT_GLOBAL = {
"is_global": False, "is_global": False,
"bank_name": "Twentysix bank", "bank_name": "Twentysix bank",
"currency": "credits", "currency": "credits",
"default_balance": 100 "default_balance": 100,
} }
_DEFAULT_GUILD = { _DEFAULT_GUILD = {"bank_name": "Twentysix bank", "currency": "credits", "default_balance": 100}
"bank_name": "Twentysix bank",
"currency": "credits",
"default_balance": 100
}
_DEFAULT_MEMBER = { _DEFAULT_MEMBER = {"name": "", "balance": 0, "created_at": 0}
"name": "",
"balance": 0,
"created_at": 0
}
_DEFAULT_USER = _DEFAULT_MEMBER _DEFAULT_USER = _DEFAULT_MEMBER
@ -50,9 +57,9 @@ def _register_defaults():
_conf.register_member(**_DEFAULT_MEMBER) _conf.register_member(**_DEFAULT_MEMBER)
_conf.register_user(**_DEFAULT_USER) _conf.register_user(**_DEFAULT_USER)
if not os.environ.get('BUILDING_DOCS'):
_conf = Config.get_conf( if not os.environ.get("BUILDING_DOCS"):
None, 384734293238749, cog_name="Bank", force_registration=True) _conf = Config.get_conf(None, 384734293238749, cog_name="Bank", force_registration=True)
_register_defaults() _register_defaults()
@ -285,7 +292,7 @@ async def wipe_bank():
await _conf.clear_all_members() await _conf.clear_all_members()
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]:
""" """
Gets the bank's leaderboard Gets the bank's leaderboard
@ -319,14 +326,16 @@ async def get_leaderboard(positions: int=None, guild: discord.Guild=None) -> Lis
if guild is None: if guild is None:
raise TypeError("Expected a guild, got NoneType object instead!") raise TypeError("Expected a guild, got NoneType object instead!")
raw_accounts = await _conf.all_members(guild) raw_accounts = await _conf.all_members(guild)
sorted_acc = sorted(raw_accounts.items(), key=lambda x: x[1]['balance'], reverse=True) sorted_acc = sorted(raw_accounts.items(), key=lambda x: x[1]["balance"], reverse=True)
if positions is None: if positions is None:
return sorted_acc return sorted_acc
else: else:
return sorted_acc[:positions] return sorted_acc[:positions]
async def get_leaderboard_position(member: Union[discord.User, discord.Member]) -> Union[int, None]: async def get_leaderboard_position(
member: Union[discord.User, discord.Member]
) -> Union[int, None]:
""" """
Get the leaderboard position for the specified user Get the leaderboard position for the specified user
@ -387,13 +396,13 @@ async def get_account(member: Union[discord.Member, discord.User]) -> Account:
if acc_data == {}: if acc_data == {}:
acc_data = default acc_data = default
acc_data['name'] = member.display_name acc_data["name"] = member.display_name
try: try:
acc_data['balance'] = await get_default_balance(member.guild) acc_data["balance"] = await get_default_balance(member.guild)
except AttributeError: except AttributeError:
acc_data['balance'] = await get_default_balance() acc_data["balance"] = await get_default_balance()
acc_data['created_at'] = _decode_time(acc_data['created_at']) acc_data["created_at"] = _decode_time(acc_data["created_at"])
return Account(**acc_data) return Account(**acc_data)
@ -444,7 +453,7 @@ async def set_global(global_: bool) -> bool:
return global_ return global_
async def get_bank_name(guild: discord.Guild=None) -> str: async def get_bank_name(guild: discord.Guild = None) -> str:
"""Get the current bank name. """Get the current bank name.
Parameters Parameters
@ -472,7 +481,7 @@ async def get_bank_name(guild: discord.Guild=None) -> str:
raise RuntimeError("Guild parameter is required and missing.") raise RuntimeError("Guild parameter is required and missing.")
async def set_bank_name(name: str, guild: discord.Guild=None) -> str: async def set_bank_name(name: str, guild: discord.Guild = None) -> str:
"""Set the bank name. """Set the bank name.
Parameters Parameters
@ -499,12 +508,13 @@ async def set_bank_name(name: str, guild: discord.Guild=None) -> str:
elif guild is not None: elif guild is not None:
await _conf.guild(guild).bank_name.set(name) await _conf.guild(guild).bank_name.set(name)
else: else:
raise RuntimeError("Guild must be provided if setting the name of a guild" raise RuntimeError(
"-specific bank.") "Guild must be provided if setting the name of a guild" "-specific bank."
)
return name return name
async def get_currency_name(guild: discord.Guild=None) -> str: async def get_currency_name(guild: discord.Guild = None) -> str:
"""Get the currency name of the bank. """Get the currency name of the bank.
Parameters Parameters
@ -532,7 +542,7 @@ async def get_currency_name(guild: discord.Guild=None) -> str:
raise RuntimeError("Guild must be provided.") raise RuntimeError("Guild must be provided.")
async def set_currency_name(name: str, guild: discord.Guild=None) -> str: async def set_currency_name(name: str, guild: discord.Guild = None) -> str:
"""Set the currency name for the bank. """Set the currency name for the bank.
Parameters Parameters
@ -559,12 +569,13 @@ async def set_currency_name(name: str, guild: discord.Guild=None) -> str:
elif guild is not None: elif guild is not None:
await _conf.guild(guild).currency.set(name) await _conf.guild(guild).currency.set(name)
else: else:
raise RuntimeError("Guild must be provided if setting the currency" raise RuntimeError(
" name of a guild-specific bank.") "Guild must be provided if setting the currency" " name of a guild-specific bank."
)
return name return name
async def get_default_balance(guild: discord.Guild=None) -> int: async def get_default_balance(guild: discord.Guild = None) -> int:
"""Get the current default balance amount. """Get the current default balance amount.
Parameters Parameters
@ -592,7 +603,7 @@ async def get_default_balance(guild: discord.Guild=None) -> int:
raise RuntimeError("Guild is missing and required!") raise RuntimeError("Guild is missing and required!")
async def set_default_balance(amount: int, guild: discord.Guild=None) -> int: async def set_default_balance(amount: int, guild: discord.Guild = None) -> int:
"""Set the default balance amount. """Set the default balance amount.
Parameters Parameters

View File

@ -14,15 +14,11 @@ from discord.ext.commands import when_mentioned_or
# This supresses the PyNaCl warning that isn't relevant here # This supresses the PyNaCl warning that isn't relevant here
from discord.voice_client import VoiceClient from discord.voice_client import VoiceClient
VoiceClient.warn_nacl = False VoiceClient.warn_nacl = False
from .cog_manager import CogManager from .cog_manager import CogManager
from . import ( from . import Config, i18n, commands, rpc
Config,
i18n,
commands,
rpc
)
from .help_formatter import Help, help as help_ from .help_formatter import Help, help as help_
from .sentry import SentryManager from .sentry import SentryManager
from .utils import TYPE_CHECKING from .utils import TYPE_CHECKING
@ -32,6 +28,7 @@ if TYPE_CHECKING:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
class RpcMethodMixin: class RpcMethodMixin:
async def rpc__cogs(self, request): async def rpc__cogs(self, request):
return list(self.cogs.keys()) return list(self.cogs.keys())
@ -48,7 +45,8 @@ class RedBase(BotBase, RpcMethodMixin):
Selfbots should inherit from this mixin along with `discord.Client`. Selfbots should inherit from this mixin along with `discord.Client`.
""" """
def __init__(self, cli_flags, bot_dir: Path=Path.cwd(), **kwargs):
def __init__(self, cli_flags, bot_dir: Path = Path.cwd(), **kwargs):
self._shutdown_mode = ExitCodes.CRITICAL self._shutdown_mode = ExitCodes.CRITICAL
self.db = Config.get_core_conf(force_registration=True) self.db = Config.get_core_conf(force_registration=True)
self._co_owners = cli_flags.co_owner self._co_owners = cli_flags.co_owner
@ -62,22 +60,15 @@ class RedBase(BotBase, RpcMethodMixin):
whitelist=[], whitelist=[],
blacklist=[], blacklist=[],
enable_sentry=None, enable_sentry=None,
locale='en', locale="en",
embeds=True embeds=True,
) )
self.db.register_guild( self.db.register_guild(
prefix=[], prefix=[], whitelist=[], blacklist=[], admin_role=None, mod_role=None, embeds=None
whitelist=[],
blacklist=[],
admin_role=None,
mod_role=None,
embeds=None
) )
self.db.register_user( self.db.register_user(embeds=None)
embeds=None
)
async def prefix_manager(bot, message): async def prefix_manager(bot, message):
if not cli_flags.prefix: if not cli_flags.prefix:
@ -88,9 +79,13 @@ class RedBase(BotBase, RpcMethodMixin):
return global_prefix return global_prefix
server_prefix = await bot.db.guild(message.guild).prefix() server_prefix = await bot.db.guild(message.guild).prefix()
if cli_flags.mentionable: if cli_flags.mentionable:
return when_mentioned_or(*server_prefix)(bot, message) \ return when_mentioned_or(*server_prefix)(
if server_prefix else \ bot, message
when_mentioned_or(*global_prefix)(bot, message) ) if server_prefix else when_mentioned_or(
*global_prefix
)(
bot, message
)
else: else:
return server_prefix if server_prefix else global_prefix return server_prefix if server_prefix else global_prefix
@ -109,13 +104,13 @@ class RedBase(BotBase, RpcMethodMixin):
self.main_dir = bot_dir self.main_dir = bot_dir
self.cog_mgr = CogManager(paths=(str(self.main_dir / 'cogs'),)) self.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))
self.register_rpc_methods() self.register_rpc_methods()
super().__init__(formatter=Help(), **kwargs) super().__init__(formatter=Help(), **kwargs)
self.remove_command('help') self.remove_command("help")
self.add_command(help_) self.add_command(help_)
@ -124,7 +119,7 @@ class RedBase(BotBase, RpcMethodMixin):
def enable_sentry(self): def enable_sentry(self):
"""Enable Sentry logging for Red.""" """Enable Sentry logging for Red."""
if self._sentry_mgr is None: if self._sentry_mgr is None:
sentry_log = logging.getLogger('red.sentry') sentry_log = logging.getLogger("red.sentry")
sentry_log.setLevel(logging.WARNING) sentry_log.setLevel(logging.WARNING)
self._sentry_mgr = SentryManager(sentry_log) self._sentry_mgr = SentryManager(sentry_log)
self._sentry_mgr.enable() self._sentry_mgr.enable()
@ -143,7 +138,7 @@ class RedBase(BotBase, RpcMethodMixin):
:return: :return:
""" """
indict['owner_id'] = await self.db.owner() indict["owner_id"] = await self.db.owner()
i18n.set_locale(await self.db.locale()) i18n.set_locale(await self.db.locale())
async def embed_requested(self, channel, user, command=None) -> bool: async def embed_requested(self, channel, user, command=None) -> bool:
@ -164,8 +159,9 @@ class RedBase(BotBase, RpcMethodMixin):
bool bool
:code:`True` if an embed is requested :code:`True` if an embed is requested
""" """
if isinstance(channel, discord.abc.PrivateChannel) or ( if (
command and command == self.get_command("help") isinstance(channel, discord.abc.PrivateChannel)
or (command and command == self.get_command("help"))
): ):
user_setting = await self.db.user(user).embeds() user_setting = await self.db.user(user).embeds()
if user_setting is not None: if user_setting is not None:
@ -214,14 +210,14 @@ class RedBase(BotBase, RpcMethodMixin):
curr_pkgs.remove(pkg_name) curr_pkgs.remove(pkg_name)
async def load_extension(self, spec: ModuleSpec): async def load_extension(self, spec: ModuleSpec):
name = spec.name.split('.')[-1] name = spec.name.split(".")[-1]
if name in self.extensions: if name in self.extensions:
return return
lib = spec.loader.load_module() lib = spec.loader.load_module()
if not hasattr(lib, 'setup'): if not hasattr(lib, "setup"):
del lib del lib
raise discord.ClientException('extension does not have a setup function') raise discord.ClientException("extension does not have a setup function")
if asyncio.iscoroutinefunction(lib.setup): if asyncio.iscoroutinefunction(lib.setup):
await lib.setup(self) await lib.setup(self)
@ -262,7 +258,7 @@ class RedBase(BotBase, RpcMethodMixin):
del event_list[index] del event_list[index]
try: try:
func = getattr(lib, 'teardown') func = getattr(lib, "teardown")
except AttributeError: except AttributeError:
pass pass
else: else:
@ -279,19 +275,20 @@ class RedBase(BotBase, RpcMethodMixin):
if m.startswith(pkg_name): if m.startswith(pkg_name):
del sys.modules[m] del sys.modules[m]
if pkg_name.startswith('redbot.cogs'): if pkg_name.startswith("redbot.cogs"):
del sys.modules['redbot.cogs'].__dict__[name] del sys.modules["redbot.cogs"].__dict__[name]
def register_rpc_methods(self): def register_rpc_methods(self):
rpc.add_method('bot', self.rpc__cogs) rpc.add_method("bot", self.rpc__cogs)
rpc.add_method('bot', self.rpc__extensions) rpc.add_method("bot", self.rpc__extensions)
class Red(RedBase, discord.AutoShardedClient): class Red(RedBase, discord.AutoShardedClient):
""" """
You're welcome Caleb. You're welcome Caleb.
""" """
async def shutdown(self, *, restart: bool=False):
async def shutdown(self, *, restart: bool = False):
"""Gracefully quit Red. """Gracefully quit Red.
The program will exit with code :code:`0` by default. The program will exit with code :code:`0` by default.
@ -314,4 +311,4 @@ class Red(RedBase, discord.AutoShardedClient):
class ExitCodes(Enum): class ExitCodes(Enum):
CRITICAL = 1 CRITICAL = 1
SHUTDOWN = 0 SHUTDOWN = 0
RESTART = 26 RESTART = 26

View File

@ -5,23 +5,22 @@ from discord.ext import commands
async def check_overrides(ctx, *, level): async def check_overrides(ctx, *, level):
if await ctx.bot.is_owner(ctx.author): if await ctx.bot.is_owner(ctx.author):
return True return True
perm_cog = ctx.bot.get_cog('Permissions') perm_cog = ctx.bot.get_cog("Permissions")
if not perm_cog or ctx.cog == perm_cog: if not perm_cog or ctx.cog == perm_cog:
return None return None
# don't break if someone loaded a cog named # don't break if someone loaded a cog named
# permissions that doesn't implement this # permissions that doesn't implement this
func = getattr(perm_cog, 'check_overrides', None) func = getattr(perm_cog, "check_overrides", None)
val = None if func is None else await func(ctx, level) val = None if func is None else await func(ctx, level)
return val return val
def is_owner(**kwargs): def is_owner(**kwargs):
async def check(ctx): async def check(ctx):
override = await check_overrides(ctx, level='owner') override = await check_overrides(ctx, level="owner")
return ( return (override if override is not None else await ctx.bot.is_owner(ctx.author, **kwargs))
override if override is not None
else await ctx.bot.is_owner(ctx.author, **kwargs)
)
return commands.check(check) return commands.check(check)
@ -32,10 +31,7 @@ async def check_permissions(ctx, perms):
return False return False
resolved = ctx.channel.permissions_for(ctx.author) resolved = ctx.channel.permissions_for(ctx.author)
return all( return all(getattr(resolved, name, None) == value for name, value in perms.items())
getattr(resolved, name, None) == value
for name, value in perms.items()
)
async def is_mod_or_superior(ctx): async def is_mod_or_superior(ctx):
@ -75,47 +71,49 @@ async def is_admin_or_superior(ctx):
def mod_or_permissions(**perms): def mod_or_permissions(**perms):
async def predicate(ctx): async def predicate(ctx):
override = await check_overrides(ctx, level='mod') override = await check_overrides(ctx, level="mod")
return ( return (
override if override is not None override
else await check_permissions(ctx, perms) if override is not None
or await is_mod_or_superior(ctx) else await check_permissions(ctx, perms) or await is_mod_or_superior(ctx)
) )
return commands.check(predicate) return commands.check(predicate)
def admin_or_permissions(**perms): def admin_or_permissions(**perms):
async def predicate(ctx): async def predicate(ctx):
override = await check_overrides(ctx, level='admin') override = await check_overrides(ctx, level="admin")
return ( return (
override if override is not None override
else await check_permissions(ctx, perms) if override is not None
or await is_admin_or_superior(ctx) else await check_permissions(ctx, perms) or await is_admin_or_superior(ctx)
) )
return commands.check(predicate) return commands.check(predicate)
def bot_in_a_guild(**kwargs): def bot_in_a_guild(**kwargs):
async def predicate(ctx): async def predicate(ctx):
return len(ctx.bot.guilds) > 0 return len(ctx.bot.guilds) > 0
return commands.check(predicate) return commands.check(predicate)
def guildowner_or_permissions(**perms): def guildowner_or_permissions(**perms):
async def predicate(ctx): async def predicate(ctx):
has_perms_or_is_owner = await check_permissions(ctx, perms) has_perms_or_is_owner = await check_permissions(ctx, perms)
if ctx.guild is None: if ctx.guild is None:
return has_perms_or_is_owner return has_perms_or_is_owner
is_guild_owner = ctx.author == ctx.guild.owner is_guild_owner = ctx.author == ctx.guild.owner
override = await check_overrides(ctx, level='guildowner') override = await check_overrides(ctx, level="guildowner")
return ( return (override if override is not None else is_guild_owner or has_perms_or_is_owner)
override if override is not None
else is_guild_owner or has_perms_or_is_owner
)
return commands.check(predicate) return commands.check(predicate)

View File

@ -26,16 +26,17 @@ def interactive_config(red, token_set, prefix_set):
if not prefix_set: if not prefix_set:
prefix = "" prefix = ""
print("\nPick a prefix. A prefix is what you type before a " print(
"command. Example:\n" "\nPick a prefix. A prefix is what you type before a "
"!help\n^ The exclamation mark is the prefix in this case.\n" "command. Example:\n"
"Can be multiple characters. You will be able to change it " "!help\n^ The exclamation mark is the prefix in this case.\n"
"later and add more of them.\nChoose your prefix:\n") "Can be multiple characters. You will be able to change it "
"later and add more of them.\nChoose your prefix:\n"
)
while not prefix: while not prefix:
prefix = input("Prefix> ") prefix = input("Prefix> ")
if len(prefix) > 10: if len(prefix) > 10:
print("Your prefix seems overly long. Are you sure it " print("Your prefix seems overly long. Are you sure it " "is correct? (y/n)")
"is correct? (y/n)")
if not confirm("> "): if not confirm("> "):
prefix = "" prefix = ""
if prefix: if prefix:
@ -48,12 +49,14 @@ def interactive_config(red, token_set, prefix_set):
def ask_sentry(red: Red): def ask_sentry(red: Red):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
print("\nThank you for installing Red V3 beta! The current version\n" print(
" is not suited for production use and is aimed at testing\n" "\nThank you for installing Red V3 beta! The current version\n"
" the current and upcoming featureset, that's why we will\n" " is not suited for production use and is aimed at testing\n"
" also collect the fatal error logs to help us fix any new\n" " the current and upcoming featureset, that's why we will\n"
" found issues in a timely manner. If you wish to opt in\n" " also collect the fatal error logs to help us fix any new\n"
" the process please type \"yes\":\n") " found issues in a timely manner. If you wish to opt in\n"
' the process please type "yes":\n'
)
if not confirm("> "): if not confirm("> "):
loop.run_until_complete(red.db.enable_sentry.set(False)) loop.run_until_complete(red.db.enable_sentry.set(False))
else: else:
@ -62,64 +65,82 @@ def ask_sentry(red: Red):
def parse_cli_flags(args): def parse_cli_flags(args):
parser = argparse.ArgumentParser(description="Red - Discord Bot", parser = argparse.ArgumentParser(
usage="redbot <instance_name> [arguments]") description="Red - Discord Bot", usage="redbot <instance_name> [arguments]"
parser.add_argument("--version", "-V", action="store_true", )
help="Show Red's current version") parser.add_argument("--version", "-V", action="store_true", help="Show Red's current version")
parser.add_argument("--list-instances", action="store_true", parser.add_argument(
help="List all instance names setup " "--list-instances",
"with 'redbot-setup'") action="store_true",
parser.add_argument("--owner", type=int, help="List all instance names setup " "with 'redbot-setup'",
help="ID of the owner. Only who hosts " )
"Red should be owner, this has " parser.add_argument(
"serious security implications if misused.") "--owner",
parser.add_argument("--co-owner", type=int, default=[], nargs="*", type=int,
help="ID of a co-owner. Only people who have access " help="ID of the owner. Only who hosts "
"to the system that is hosting Red should be " "Red should be owner, this has "
"co-owners, as this gives them complete access " "serious security implications if misused.",
"to the system's data. This has serious " )
"security implications if misused. Can be " parser.add_argument(
"multiple.") "--co-owner",
parser.add_argument("--prefix", "-p", action="append", type=int,
help="Global prefix. Can be multiple") default=[],
parser.add_argument("--no-prompt", action="store_true", nargs="*",
help="Disables console inputs. Features requiring " help="ID of a co-owner. Only people who have access "
"console interaction could be disabled as a " "to the system that is hosting Red should be "
"result") "co-owners, as this gives them complete access "
parser.add_argument("--no-cogs", "to the system's data. This has serious "
action="store_true", "security implications if misused. Can be "
help="Starts Red with no cogs loaded, only core") "multiple.",
parser.add_argument("--load-cogs", type=str, nargs="*", )
help="Force loading specified cogs from the installed packages. " parser.add_argument("--prefix", "-p", action="append", help="Global prefix. Can be multiple")
"Can be used with the --no-cogs flag to load these cogs exclusively.") parser.add_argument(
parser.add_argument("--self-bot", "--no-prompt",
action='store_true', action="store_true",
help="Specifies if Red should log in as selfbot") help="Disables console inputs. Features requiring "
parser.add_argument("--not-bot", "console interaction could be disabled as a "
action='store_true', "result",
help="Specifies if the token used belongs to a bot " )
"account.") parser.add_argument(
parser.add_argument("--dry-run", "--no-cogs", action="store_true", help="Starts Red with no cogs loaded, only core"
action="store_true", )
help="Makes Red quit with code 0 just before the " parser.add_argument(
"login. This is useful for testing the boot " "--load-cogs",
"process.") type=str,
parser.add_argument("--debug", nargs="*",
action="store_true", help="Force loading specified cogs from the installed packages. "
help="Sets the loggers level as debug") "Can be used with the --no-cogs flag to load these cogs exclusively.",
parser.add_argument("--dev", )
action="store_true", parser.add_argument(
help="Enables developer mode") "--self-bot", action="store_true", help="Specifies if Red should log in as selfbot"
parser.add_argument("--mentionable", )
action="store_true", parser.add_argument(
help="Allows mentioning the bot as an alternative " "--not-bot",
"to using the bot prefix") action="store_true",
parser.add_argument("--rpc", help="Specifies if the token used belongs to a bot " "account.",
action="store_true", )
help="Enables the built-in RPC server. Please read the docs" parser.add_argument(
"prior to enabling this!") "--dry-run",
parser.add_argument("instance_name", nargs="?", action="store_true",
help="Name of the bot instance created during `redbot-setup`.") help="Makes Red quit with code 0 just before the "
"login. This is useful for testing the boot "
"process.",
)
parser.add_argument("--debug", action="store_true", help="Sets the loggers level as debug")
parser.add_argument("--dev", action="store_true", help="Enables developer mode")
parser.add_argument(
"--mentionable",
action="store_true",
help="Allows mentioning the bot as an alternative " "to using the bot prefix",
)
parser.add_argument(
"--rpc",
action="store_true",
help="Enables the built-in RPC server. Please read the docs" "prior to enabling this!",
)
parser.add_argument(
"instance_name", nargs="?", help="Name of the bot instance created during `redbot-setup`."
)
args = parser.parse_args(args) args = parser.parse_args(args)
@ -129,4 +150,3 @@ def parse_cli_flags(args):
args.prefix = [] args.prefix = []
return args return args

View File

@ -34,14 +34,12 @@ class CogManager:
install new cogs to, the default being the :code:`cogs/` folder in the root install new cogs to, the default being the :code:`cogs/` folder in the root
bot directory. bot directory.
""" """
def __init__(self, paths: Tuple[str]=()):
def __init__(self, paths: Tuple[str] = ()):
self.conf = Config.get_conf(self, 2938473984732, True) self.conf = Config.get_conf(self, 2938473984732, True)
tmp_cog_install_path = cog_data_path(self) / "cogs" tmp_cog_install_path = cog_data_path(self) / "cogs"
tmp_cog_install_path.mkdir(parents=True, exist_ok=True) tmp_cog_install_path.mkdir(parents=True, exist_ok=True)
self.conf.register_global( self.conf.register_global(paths=(), install_path=str(tmp_cog_install_path))
paths=(),
install_path=str(tmp_cog_install_path)
)
self._paths = [Path(p) for p in paths] self._paths = [Path(p) for p in paths]
@ -158,7 +156,7 @@ class CogManager:
if path == await self.install_path(): if path == await self.install_path():
raise ValueError("Cannot add the install path as an additional path.") raise ValueError("Cannot add the install path as an additional path.")
all_paths = _deduplicate(await self.paths() + (path, )) all_paths = _deduplicate(await self.paths() + (path,))
# noinspection PyTypeChecker # noinspection PyTypeChecker
await self.set_paths(all_paths) await self.set_paths(all_paths)
@ -225,8 +223,10 @@ class CogManager:
if spec: if spec:
return spec return spec
raise RuntimeError("No 3rd party module by the name of '{}' was found" raise RuntimeError(
" in any available path.".format(name)) "No 3rd party module by the name of '{}' was found"
" in any available path.".format(name)
)
async def _find_core_cog(self, name: str) -> ModuleSpec: async def _find_core_cog(self, name: str) -> ModuleSpec:
""" """
@ -247,10 +247,11 @@ class CogManager:
""" """
real_name = ".{}".format(name) real_name = ".{}".format(name)
try: try:
mod = import_module(real_name, package='redbot.cogs') mod = import_module(real_name, package="redbot.cogs")
except ImportError as e: except ImportError as e:
raise RuntimeError("No core cog by the name of '{}' could" raise RuntimeError(
"be found.".format(name)) from e "No core cog by the name of '{}' could" "be found.".format(name)
) from e
return mod.__spec__ return mod.__spec__
# noinspection PyUnreachableCode # noinspection PyUnreachableCode
@ -284,7 +285,7 @@ class CogManager:
async def available_modules(self) -> List[str]: async def available_modules(self) -> List[str]:
"""Finds the names of all available modules to load. """Finds the names of all available modules to load.
""" """
paths = (await self.install_path(), ) + await self.paths() paths = (await self.install_path(),) + await self.paths()
paths = [str(p) for p in paths] paths = [str(p) for p in paths]
ret = [] ret = []
@ -341,8 +342,9 @@ class CogManagerUI:
Add a path to the list of available cog paths. Add a path to the list of available cog paths.
""" """
if not path.is_dir(): if not path.is_dir():
await ctx.send(_("That path does not exist or does not" await ctx.send(
" point to a valid directory.")) _("That path does not exist or does not" " point to a valid directory.")
)
return return
try: try:
@ -398,7 +400,7 @@ class CogManagerUI:
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
async def installpath(self, ctx: commands.Context, path: Path=None): async def installpath(self, ctx: commands.Context, path: Path = None):
""" """
Returns the current install path or sets it if one is provided. Returns the current install path or sets it if one is provided.
The provided path must be absolute or relative to the bot's The provided path must be absolute or relative to the bot's
@ -416,8 +418,9 @@ class CogManagerUI:
return return
install_path = await ctx.bot.cog_mgr.install_path() install_path = await ctx.bot.cog_mgr.install_path()
await ctx.send(_("The bot will install new cogs to the `{}`" await ctx.send(
" directory.").format(install_path)) _("The bot will install new cogs to the `{}`" " directory.").format(install_path)
)
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
@ -435,22 +438,20 @@ class CogManagerUI:
unloaded = sorted(list(unloaded), key=str.lower) unloaded = sorted(list(unloaded), key=str.lower)
if await ctx.embed_requested(): if await ctx.embed_requested():
loaded = ('**{} loaded:**\n').format(len(loaded)) + ", ".join(loaded) loaded = ("**{} loaded:**\n").format(len(loaded)) + ", ".join(loaded)
unloaded = ('**{} unloaded:**\n').format(len(unloaded)) + ", ".join(unloaded) unloaded = ("**{} unloaded:**\n").format(len(unloaded)) + ", ".join(unloaded)
for page in pagify(loaded, delims=[', ', '\n'], page_length=1800): for page in pagify(loaded, delims=[", ", "\n"], page_length=1800):
e = discord.Embed(description=page, e = discord.Embed(description=page, colour=discord.Colour.dark_green())
colour=discord.Colour.dark_green())
await ctx.send(embed=e) await ctx.send(embed=e)
for page in pagify(unloaded, delims=[', ', '\n'], page_length=1800): for page in pagify(unloaded, delims=[", ", "\n"], page_length=1800):
e = discord.Embed(description=page, e = discord.Embed(description=page, colour=discord.Colour.dark_red())
colour=discord.Colour.dark_red())
await ctx.send(embed=e) await ctx.send(embed=e)
else: else:
loaded_count = '**{} loaded:**\n'.format(len(loaded)) loaded_count = "**{} loaded:**\n".format(len(loaded))
loaded = ", ".join(loaded) loaded = ", ".join(loaded)
unloaded_count = '**{} unloaded:**\n'.format(len(unloaded)) unloaded_count = "**{} unloaded:**\n".format(len(unloaded))
unloaded = ", ".join(unloaded) unloaded = ", ".join(unloaded)
loaded_count_sent = False loaded_count_sent = False
unloaded_count_sent = False unloaded_count_sent = False

View File

@ -20,7 +20,7 @@ class Command(commands.Command):
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._help_override = kwargs.pop('help_override', None) self._help_override = kwargs.pop("help_override", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.translator = kwargs.pop("i18n", None) self.translator = kwargs.pop("i18n", None)
@ -40,7 +40,7 @@ class Command(commands.Command):
translator = self.translator translator = self.translator
command_doc = self.callback.__doc__ command_doc = self.callback.__doc__
if command_doc is None: if command_doc is None:
return '' return ""
return inspect.cleandoc(translator(command_doc)) return inspect.cleandoc(translator(command_doc))
@help.setter @help.setter
@ -60,6 +60,7 @@ class Group(Command, commands.Group):
# decorators # decorators
def command(name=None, cls=Command, **attrs): def command(name=None, cls=Command, **attrs):
"""A decorator which transforms an async function into a `Command`. """A decorator which transforms an async function into a `Command`.

View File

@ -59,10 +59,9 @@ class Context(commands.Context):
else: else:
return True return True
async def send_interactive(self, async def send_interactive(
messages: Iterable[str], self, messages: Iterable[str], box_lang: str = None, timeout: int = 15
box_lang: str=None, ) -> List[discord.Message]:
timeout: int=15) -> List[discord.Message]:
"""Send multiple messages interactively. """Send multiple messages interactively.
The user will be prompted for whether or not they would like to view The user will be prompted for whether or not they would like to view
@ -84,9 +83,9 @@ class Context(commands.Context):
messages = tuple(messages) messages = tuple(messages)
ret = [] ret = []
more_check = lambda m: (m.author == self.author and more_check = lambda m: (
m.channel == self.channel and m.author == self.author and m.channel == self.channel and m.content.lower() == "more"
m.content.lower() == "more") )
for idx, page in enumerate(messages, 1): for idx, page in enumerate(messages, 1):
if box_lang is None: if box_lang is None:
@ -105,10 +104,10 @@ class Context(commands.Context):
query = await self.send( query = await self.send(
"There {} still {} message{} remaining. " "There {} still {} message{} remaining. "
"Type `more` to continue." "Type `more` to continue."
"".format(is_are, n_remaining, plural)) "".format(is_are, n_remaining, plural)
)
try: try:
resp = await self.bot.wait_for( resp = await self.bot.wait_for("message", check=more_check, timeout=timeout)
'message', check=more_check, timeout=timeout)
except asyncio.TimeoutError: except asyncio.TimeoutError:
await query.delete() await query.delete()
break break
@ -134,9 +133,7 @@ class Context(commands.Context):
""" """
if self.guild and not self.channel.permissions_for(self.guild.me).embed_links: if self.guild and not self.channel.permissions_for(self.guild.me).embed_links:
return False return False
return await self.bot.embed_requested( return await self.bot.embed_requested(self.channel, self.author, command=self.command)
self.channel, self.author, command=self.command
)
async def maybe_send_embed(self, message: str) -> discord.Message: async def maybe_send_embed(self, message: str) -> discord.Message:
""" """

View File

@ -38,9 +38,11 @@ class _ValueCtxManager:
async def __aenter__(self): async def __aenter__(self):
self.raw_value = await self self.raw_value = await self
if not isinstance(self.raw_value, (list, dict)): if not isinstance(self.raw_value, (list, dict)):
raise TypeError("Type of retrieved value must be mutable (i.e. " raise TypeError(
"list or dict) in order to use a config value as " "Type of retrieved value must be mutable (i.e. "
"a context manager.") "list or dict) in order to use a config value as "
"a context manager."
)
return self.raw_value return self.raw_value
async def __aexit__(self, *exc_info): async def __aexit__(self, *exc_info):
@ -61,6 +63,7 @@ class Value:
A reference to `Config.driver`. A reference to `Config.driver`.
""" """
def __init__(self, identifiers: Tuple[str], default_value, driver): def __init__(self, identifiers: Tuple[str], default_value, driver):
self._identifiers = identifiers self._identifiers = identifiers
self.default = default_value self.default = default_value
@ -168,10 +171,10 @@ class Group(Value):
A reference to `Config.driver`. A reference to `Config.driver`.
""" """
def __init__(self, identifiers: Tuple[str],
defaults: dict, def __init__(
driver, self, identifiers: Tuple[str], defaults: dict, driver, force_registration: bool = False
force_registration: bool=False): ):
self._defaults = defaults self._defaults = defaults
self.force_registration = force_registration self.force_registration = force_registration
self.driver = driver self.driver = driver
@ -209,31 +212,22 @@ class Group(Value):
""" """
is_group = self.is_group(item) is_group = self.is_group(item)
is_value = not is_group and self.is_value(item) is_value = not is_group and self.is_value(item)
new_identifiers = self.identifiers + (item, ) new_identifiers = self.identifiers + (item,)
if is_group: if is_group:
return Group( return Group(
identifiers=new_identifiers, identifiers=new_identifiers,
defaults=self._defaults[item], defaults=self._defaults[item],
driver=self.driver, driver=self.driver,
force_registration=self.force_registration force_registration=self.force_registration,
) )
elif is_value: elif is_value:
return Value( return Value(
identifiers=new_identifiers, identifiers=new_identifiers, default_value=self._defaults[item], driver=self.driver
default_value=self._defaults[item],
driver=self.driver
) )
elif self.force_registration: elif self.force_registration:
raise AttributeError( raise AttributeError("'{}' is not a valid registered Group " "or value.".format(item))
"'{}' is not a valid registered Group "
"or value.".format(item)
)
else: else:
return Value( return Value(identifiers=new_identifiers, default_value=None, driver=self.driver)
identifiers=new_identifiers,
default_value=None,
driver=self.driver
)
def is_group(self, item: str) -> bool: def is_group(self, item: str) -> bool:
"""A helper method for `__getattr__`. Most developers will have no need """A helper method for `__getattr__`. Most developers will have no need
@ -385,9 +379,7 @@ class Group(Value):
async def set(self, value): async def set(self, value):
if not isinstance(value, dict): if not isinstance(value, dict):
raise ValueError( raise ValueError("You may only set the value of a group to be a dict.")
"You may only set the value of a group to be a dict."
)
await super().set(value) await super().set(value)
async def set_raw(self, *nested_path: str, value): async def set_raw(self, *nested_path: str, value):
@ -456,10 +448,14 @@ class Config:
USER = "USER" USER = "USER"
MEMBER = "MEMBER" MEMBER = "MEMBER"
def __init__(self, cog_name: str, unique_identifier: str, def __init__(
driver: "BaseDriver", self,
force_registration: bool=False, cog_name: str,
defaults: dict=None): unique_identifier: str,
driver: "BaseDriver",
force_registration: bool = False,
defaults: dict = None,
):
self.cog_name = cog_name self.cog_name = cog_name
self.unique_identifier = unique_identifier self.unique_identifier = unique_identifier
@ -472,8 +468,7 @@ class Config:
return deepcopy(self._defaults) return deepcopy(self._defaults)
@classmethod @classmethod
def get_conf(cls, cog_instance, identifier: int, def get_conf(cls, cog_instance, identifier: int, force_registration=False, cog_name=None):
force_registration=False, cog_name=None):
"""Get a Config instance for your cog. """Get a Config instance for your cog.
.. warning:: .. warning::
@ -519,20 +514,24 @@ class Config:
log.debug("Basic config: \n\n{}".format(basic_config)) log.debug("Basic config: \n\n{}".format(basic_config))
driver_name = basic_config.get('STORAGE_TYPE', 'JSON') driver_name = basic_config.get("STORAGE_TYPE", "JSON")
driver_details = basic_config.get('STORAGE_DETAILS', {}) driver_details = basic_config.get("STORAGE_DETAILS", {})
log.debug("Using driver: '{}'".format(driver_name)) log.debug("Using driver: '{}'".format(driver_name))
driver = get_driver(driver_name, cog_name, uuid, data_path_override=cog_path_override, driver = get_driver(
**driver_details) driver_name, cog_name, uuid, data_path_override=cog_path_override, **driver_details
conf = cls(cog_name=cog_name, unique_identifier=uuid, )
force_registration=force_registration, conf = cls(
driver=driver) cog_name=cog_name,
unique_identifier=uuid,
force_registration=force_registration,
driver=driver,
)
return conf return conf
@classmethod @classmethod
def get_core_conf(cls, force_registration: bool=False): def get_core_conf(cls, force_registration: bool = False):
"""Get a Config instance for a core module. """Get a Config instance for a core module.
All core modules that require a config instance should use this All core modules that require a config instance should use this
@ -549,14 +548,18 @@ class Config:
# We have to import this here otherwise we have a circular dependency # We have to import this here otherwise we have a circular dependency
from .data_manager import basic_config from .data_manager import basic_config
driver_name = basic_config.get('STORAGE_TYPE', 'JSON') driver_name = basic_config.get("STORAGE_TYPE", "JSON")
driver_details = basic_config.get('STORAGE_DETAILS', {}) driver_details = basic_config.get("STORAGE_DETAILS", {})
driver = get_driver(driver_name, "Core", '0', data_path_override=core_path, driver = get_driver(
**driver_details) driver_name, "Core", "0", data_path_override=core_path, **driver_details
conf = cls(cog_name="Core", driver=driver, )
unique_identifier='0', conf = cls(
force_registration=force_registration) cog_name="Core",
driver=driver,
unique_identifier="0",
force_registration=force_registration,
)
return conf return conf
def __getattr__(self, item: str) -> Union[Group, Value]: def __getattr__(self, item: str) -> Union[Group, Value]:
@ -593,7 +596,7 @@ class Config:
""" """
ret = {} ret = {}
partial = ret partial = ret
splitted = key.split('__') splitted = key.split("__")
for i, k in enumerate(splitted, start=1): for i, k in enumerate(splitted, start=1):
if not k.isidentifier(): if not k.isidentifier():
raise RuntimeError("'{}' is an invalid config key.".format(k)) raise RuntimeError("'{}' is an invalid config key.".format(k))
@ -621,8 +624,9 @@ class Config:
existing_is_dict = isinstance(_partial[k], dict) existing_is_dict = isinstance(_partial[k], dict)
if val_is_dict != existing_is_dict: if val_is_dict != existing_is_dict:
# != is XOR # != is XOR
raise KeyError("You cannot register a Group and a Value under" raise KeyError(
" the same name.") "You cannot register a Group and a Value under" " the same name."
)
if val_is_dict: if val_is_dict:
Config._update_defaults(v, _partial=_partial[k]) Config._update_defaults(v, _partial=_partial[k])
else: else:
@ -736,7 +740,7 @@ class Config:
identifiers=(key, *identifiers), identifiers=(key, *identifiers),
defaults=self.defaults.get(key, {}), defaults=self.defaults.get(key, {}),
driver=self.driver, driver=self.driver,
force_registration=self.force_registration force_registration=self.force_registration,
) )
def guild(self, guild: discord.Guild) -> Group: def guild(self, guild: discord.Guild) -> Group:
@ -935,7 +939,7 @@ class Config:
ret[int(member_id)] = new_member_data ret[int(member_id)] = new_member_data
return ret return ret
async def all_members(self, guild: discord.Guild=None) -> dict: async def all_members(self, guild: discord.Guild = None) -> dict:
"""Get data for all members. """Get data for all members.
If :code:`guild` is specified, only the data for the members of that If :code:`guild` is specified, only the data for the members of that
@ -965,8 +969,7 @@ class Config:
group = self._get_base_group(self.MEMBER) group = self._get_base_group(self.MEMBER)
dict_ = await group() dict_ = await group()
for guild_id, guild_data in dict_.items(): for guild_id, guild_data in dict_.items():
ret[int(guild_id)] = self._all_members_from_guild( ret[int(guild_id)] = self._all_members_from_guild(group, guild_data)
group, guild_data)
else: else:
group = self._get_base_group(self.MEMBER, guild.id) group = self._get_base_group(self.MEMBER, guild.id)
guild_data = await group() guild_data = await group()
@ -992,9 +995,7 @@ class Config:
""" """
if not scopes: if not scopes:
group = Group(identifiers=[], group = Group(identifiers=[], defaults={}, driver=self.driver)
defaults={},
driver=self.driver)
else: else:
group = self._get_base_group(*scopes) group = self._get_base_group(*scopes)
await group.clear() await group.clear()
@ -1046,7 +1047,7 @@ class Config:
""" """
await self._clear_scope(self.USER) await self._clear_scope(self.USER)
async def clear_all_members(self, guild: discord.Guild=None): async def clear_all_members(self, guild: discord.Guild = None):
"""Clear all member data. """Clear all member data.
This resets all specified member data to its registered defaults. This resets all specified member data to its registered defaults.

View File

@ -32,10 +32,12 @@ __all__ = ["Core"]
log = logging.getLogger("red") log = logging.getLogger("red")
OWNER_DISCLAIMER = ("⚠ **Only** the person who is hosting Red should be " OWNER_DISCLAIMER = (
"owner. **This has SERIOUS security implications. The " "⚠ **Only** the person who is hosting Red should be "
"owner can access any data that is present on the host " "owner. **This has SERIOUS security implications. The "
"system.** ⚠") "owner can access any data that is present on the host "
"system.** ⚠"
)
_ = i18n.Translator("Core", __file__) _ = i18n.Translator("Core", __file__)
@ -44,12 +46,13 @@ _ = i18n.Translator("Core", __file__)
@i18n.cog_i18n(_) @i18n.cog_i18n(_)
class Core: class Core:
"""Commands related to core functions""" """Commands related to core functions"""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot # type: Red self.bot = bot # type: Red
rpc.add_method('core', self.rpc_load) rpc.add_method("core", self.rpc_load)
rpc.add_method('core', self.rpc_unload) rpc.add_method("core", self.rpc_unload)
rpc.add_method('core', self.rpc_reload) rpc.add_method("core", self.rpc_reload)
@commands.command(hidden=True) @commands.command(hidden=True)
async def ping(self, ctx): async def ping(self, ctx):
@ -72,15 +75,13 @@ class Core:
since = datetime.datetime(2016, 1, 2, 0, 0) since = datetime.datetime(2016, 1, 2, 0, 0)
days_since = (datetime.datetime.utcnow() - since).days days_since = (datetime.datetime.utcnow() - since).days
dpy_version = "[{}]({})".format(discord.__version__, dpy_repo) dpy_version = "[{}]({})".format(discord.__version__, dpy_repo)
python_version = "[{}.{}.{}]({})".format( python_version = "[{}.{}.{}]({})".format(*sys.version_info[:3], python_url)
*sys.version_info[:3], python_url
)
red_version = "[{}]({})".format(__version__, red_pypi) red_version = "[{}]({})".format(__version__, red_pypi)
app_info = await self.bot.application_info() app_info = await self.bot.application_info()
owner = app_info.owner owner = app_info.owner
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get('{}/json'.format(red_pypi)) as r: async with session.get("{}/json".format(red_pypi)) as r:
data = await r.json() data = await r.json()
outdated = StrictVersion(data["info"]["version"]) > StrictVersion(__version__) outdated = StrictVersion(data["info"]["version"]) > StrictVersion(__version__)
about = ( about = (
@ -89,7 +90,8 @@ class Core:
"Red is backed by a passionate community who contributes and " "Red is backed by a passionate community who contributes and "
"creates content for everyone to enjoy. [Join us today]({}) " "creates content for everyone to enjoy. [Join us today]({}) "
"and help us improve!\n\n" "and help us improve!\n\n"
"".format(red_repo, author_repo, org_repo, support_server_url)) "".format(red_repo, author_repo, org_repo, support_server_url)
)
embed = discord.Embed(color=discord.Color.red()) embed = discord.Embed(color=discord.Color.red())
embed.add_field(name="Instance owned by", value=str(owner)) embed.add_field(name="Instance owned by", value=str(owner))
@ -97,14 +99,14 @@ class Core:
embed.add_field(name="discord.py", value=dpy_version) embed.add_field(name="discord.py", value=dpy_version)
embed.add_field(name="Red version", value=red_version) embed.add_field(name="Red version", value=red_version)
if outdated: if outdated:
embed.add_field(name="Outdated", value="Yes, {} is available".format( embed.add_field(
data["info"]["version"] name="Outdated", value="Yes, {} is available".format(data["info"]["version"])
)
) )
embed.add_field(name="About Red", value=about, inline=False) embed.add_field(name="About Red", value=about, inline=False)
embed.set_footer(text="Bringing joy since 02 Jan 2016 (over " embed.set_footer(
"{} days ago!)".format(days_since)) text="Bringing joy since 02 Jan 2016 (over " "{} days ago!)".format(days_since)
)
try: try:
await ctx.send(embed=embed) await ctx.send(embed=embed)
except discord.HTTPException: except discord.HTTPException:
@ -115,11 +117,7 @@ class Core:
"""Shows Red's uptime""" """Shows Red's uptime"""
since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S") since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
passed = self.get_bot_uptime() passed = self.get_bot_uptime()
await ctx.send( await ctx.send("Been up for: **{}** (since {} UTC)".format(passed, since))
"Been up for: **{}** (since {} UTC)".format(
passed, since
)
)
def get_bot_uptime(self, *, brief=False): def get_bot_uptime(self, *, brief=False):
# Courtesy of Danny # Courtesy of Danny
@ -131,13 +129,13 @@ class Core:
if not brief: if not brief:
if days: if days:
fmt = '{d} days, {h} hours, {m} minutes, and {s} seconds' fmt = "{d} days, {h} hours, {m} minutes, and {s} seconds"
else: else:
fmt = '{h} hours, {m} minutes, and {s} seconds' fmt = "{h} hours, {m} minutes, and {s} seconds"
else: else:
fmt = '{h}h {m}m {s}s' fmt = "{h}h {m}m {s}s"
if days: if days:
fmt = '{d}d ' + fmt fmt = "{d}d " + fmt
return fmt.format(d=days, h=hours, m=minutes, s=seconds) return fmt.format(d=days, h=hours, m=minutes, s=seconds)
@ -176,14 +174,12 @@ class Core:
current = await self.bot.db.embeds() current = await self.bot.db.embeds()
await self.bot.db.embeds.set(not current) await self.bot.db.embeds.set(not current)
await ctx.send( await ctx.send(
_("Embeds are now {} by default.").format( _("Embeds are now {} by default.").format("disabled" if current else "enabled")
"disabled" if current else "enabled"
)
) )
@embedset.command(name="guild") @embedset.command(name="guild")
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def embedset_guild(self, ctx: commands.Context, enabled: bool=None): async def embedset_guild(self, ctx: commands.Context, enabled: bool = None):
""" """
Toggle the guild's embed setting. Toggle the guild's embed setting.
@ -197,18 +193,14 @@ class Core:
""" """
await self.bot.db.guild(ctx.guild).embeds.set(enabled) await self.bot.db.guild(ctx.guild).embeds.set(enabled)
if enabled is None: if enabled is None:
await ctx.send( await ctx.send(_("Embeds will now fall back to the global setting."))
_("Embeds will now fall back to the global setting.")
)
else: else:
await ctx.send( await ctx.send(
_("Embeds are now {} for this guild.").format( _("Embeds are now {} for this guild.").format("enabled" if enabled else "disabled")
"enabled" if enabled else "disabled"
)
) )
@embedset.command(name="user") @embedset.command(name="user")
async def embedset_user(self, ctx: commands.Context, enabled: bool=None): async def embedset_user(self, ctx: commands.Context, enabled: bool = None):
""" """
Toggle the user's embed setting. Toggle the user's embed setting.
@ -222,19 +214,15 @@ class Core:
""" """
await self.bot.db.user(ctx.author).embeds.set(enabled) await self.bot.db.user(ctx.author).embeds.set(enabled)
if enabled is None: if enabled is None:
await ctx.send( await ctx.send(_("Embeds will now fall back to the global setting."))
_("Embeds will now fall back to the global setting.")
)
else: else:
await ctx.send( await ctx.send(
_("Embeds are now {} for you.").format( _("Embeds are now {} for you.").format("enabled" if enabled else "disabled")
"enabled" if enabled else "disabled"
)
) )
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
async def traceback(self, ctx, public: bool=False): async def traceback(self, ctx, public: bool = False):
"""Sends to the owner the last command exception that has occurred """Sends to the owner the last command exception that has occurred
If public (yes is specified), it will be sent to the chat instead""" If public (yes is specified), it will be sent to the chat instead"""
@ -267,8 +255,7 @@ class Core:
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
await ctx.send("Are you sure you want me to leave this server?" await ctx.send("Are you sure you want me to leave this server?" " Type yes to confirm.")
" Type yes to confirm.")
def conf_check(m): def conf_check(m):
return m.author == author return m.author == author
@ -285,15 +272,14 @@ class Core:
async def servers(self, ctx): async def servers(self, ctx):
"""Lists and allows to leave servers""" """Lists and allows to leave servers"""
owner = ctx.author owner = ctx.author
guilds = sorted(list(self.bot.guilds), guilds = sorted(list(self.bot.guilds), key=lambda s: s.name.lower())
key=lambda s: s.name.lower())
msg = "" msg = ""
for i, server in enumerate(guilds, 1): for i, server in enumerate(guilds, 1):
msg += "{}: {}\n".format(i, server.name) msg += "{}: {}\n".format(i, server.name)
msg += "\nTo leave a server, just type its number." msg += "\nTo leave a server, just type its number."
for page in pagify(msg, ['\n']): for page in pagify(msg, ["\n"]):
await ctx.send(page) await ctx.send(page)
def msg_check(m): def msg_check(m):
@ -343,7 +329,7 @@ class Core:
loaded_packages = [] loaded_packages = []
notfound_packages = [] notfound_packages = []
cognames = [c.strip() for c in cog_name.split(' ')] cognames = [c.strip() for c in cog_name.split(" ")]
cogspecs = [] cogspecs = []
for c in cognames: for c in cognames:
@ -352,20 +338,22 @@ class Core:
cogspecs.append((spec, c)) cogspecs.append((spec, c))
except RuntimeError: except RuntimeError:
notfound_packages.append(inline(c)) notfound_packages.append(inline(c))
#await ctx.send(_("No module named '{}' was found in any" # await ctx.send(_("No module named '{}' was found in any"
# " cog path.").format(c)) # " cog path.").format(c))
if len(cogspecs) > 0: if len(cogspecs) > 0:
for spec, name in cogspecs: for spec, name in cogspecs:
try: try:
await ctx.bot.load_extension(spec) await ctx.bot.load_extension(spec)
except Exception as e: except Exception as e:
log.exception("Package loading failed", exc_info=e) log.exception("Package loading failed", exc_info=e)
exception_log = ("Exception in command '{}'\n" exception_log = (
"".format(ctx.command.qualified_name)) "Exception in command '{}'\n" "".format(ctx.command.qualified_name)
exception_log += "".join(traceback.format_exception(type(e), )
e, e.__traceback__)) exception_log += "".join(
traceback.format_exception(type(e), e, e.__traceback__)
)
self.bot._last_exception = exception_log self.bot._last_exception = exception_log
failed_packages.append(inline(name)) failed_packages.append(inline(name))
else: else:
@ -378,21 +366,23 @@ class Core:
await ctx.send(_(formed)) await ctx.send(_(formed))
if failed_packages: if failed_packages:
fmt = ("Failed to load package{plural} {packs}. Check your console or " fmt = (
"logs for details.") "Failed to load package{plural} {packs}. Check your console or "
"logs for details."
)
formed = self.get_package_strings(failed_packages, fmt) formed = self.get_package_strings(failed_packages, fmt)
await ctx.send(_(formed)) await ctx.send(_(formed))
if notfound_packages: if notfound_packages:
fmt = 'The package{plural} {packs} {other} not found in any cog path.' fmt = "The package{plural} {packs} {other} not found in any cog path."
formed = self.get_package_strings(notfound_packages, fmt, ('was', 'were')) formed = self.get_package_strings(notfound_packages, fmt, ("was", "were"))
await ctx.send(_(formed)) await ctx.send(_(formed))
@commands.group() @commands.group()
@checks.is_owner() @checks.is_owner()
async def unload(self, ctx, *, cog_name: str): async def unload(self, ctx, *, cog_name: str):
"""Unloads packages""" """Unloads packages"""
cognames = [c.strip() for c in cog_name.split(' ')] cognames = [c.strip() for c in cog_name.split(" ")]
failed_packages = [] failed_packages = []
unloaded_packages = [] unloaded_packages = []
@ -406,12 +396,12 @@ class Core:
if unloaded_packages: if unloaded_packages:
fmt = "Package{plural} {packs} {other} unloaded." fmt = "Package{plural} {packs} {other} unloaded."
formed = self.get_package_strings(unloaded_packages, fmt, ('was', 'were')) formed = self.get_package_strings(unloaded_packages, fmt, ("was", "were"))
await ctx.send(_(formed)) await ctx.send(_(formed))
if failed_packages: if failed_packages:
fmt = "The package{plural} {packs} {other} not loaded." fmt = "The package{plural} {packs} {other} not loaded."
formed = self.get_package_strings(failed_packages, fmt, ('is', 'are')) formed = self.get_package_strings(failed_packages, fmt, ("is", "are"))
await ctx.send(_(formed)) await ctx.send(_(formed))
@commands.command(name="reload") @commands.command(name="reload")
@ -419,7 +409,7 @@ class Core:
async def _reload(self, ctx, *, cog_name: str): async def _reload(self, ctx, *, cog_name: str):
"""Reloads packages""" """Reloads packages"""
cognames = [c.strip() for c in cog_name.split(' ')] cognames = [c.strip() for c in cog_name.split(" ")]
for c in cognames: for c in cognames:
ctx.bot.unload_extension(c) ctx.bot.unload_extension(c)
@ -444,50 +434,46 @@ class Core:
except Exception as e: except Exception as e:
log.exception("Package reloading failed", exc_info=e) log.exception("Package reloading failed", exc_info=e)
exception_log = ("Exception in command '{}'\n" exception_log = (
"".format(ctx.command.qualified_name)) "Exception in command '{}'\n" "".format(ctx.command.qualified_name)
exception_log += "".join(traceback.format_exception(type(e), )
e, e.__traceback__)) exception_log += "".join(traceback.format_exception(type(e), e, e.__traceback__))
self.bot._last_exception = exception_log self.bot._last_exception = exception_log
failed_packages.append(inline(name)) failed_packages.append(inline(name))
if loaded_packages: if loaded_packages:
fmt = "Package{plural} {packs} {other} reloaded." fmt = "Package{plural} {packs} {other} reloaded."
formed = self.get_package_strings(loaded_packages, fmt, ('was', 'were')) formed = self.get_package_strings(loaded_packages, fmt, ("was", "were"))
await ctx.send(_(formed)) await ctx.send(_(formed))
if failed_packages: if failed_packages:
fmt = ("Failed to reload package{plural} {packs}. Check your " fmt = ("Failed to reload package{plural} {packs}. Check your " "logs for details")
"logs for details")
formed = self.get_package_strings(failed_packages, fmt) formed = self.get_package_strings(failed_packages, fmt)
await ctx.send(_(formed)) await ctx.send(_(formed))
if notfound_packages: if notfound_packages:
fmt = 'The package{plural} {packs} {other} not found in any cog path.' fmt = "The package{plural} {packs} {other} not found in any cog path."
formed = self.get_package_strings(notfound_packages, fmt, ('was', 'were')) formed = self.get_package_strings(notfound_packages, fmt, ("was", "were"))
await ctx.send(_(formed)) await ctx.send(_(formed))
def get_package_strings(self, packages: list, fmt: str, other: tuple=None): def get_package_strings(self, packages: list, fmt: str, other: tuple = None):
""" """
Gets the strings needed for the load, unload and reload commands Gets the strings needed for the load, unload and reload commands
""" """
if other is None: if other is None:
other = ('', '') other = ("", "")
plural = 's' if len(packages) > 1 else '' plural = "s" if len(packages) > 1 else ""
use_and, other = ('', other[0]) if len(packages) == 1 else (' and ', other[1]) use_and, other = ("", other[0]) if len(packages) == 1 else (" and ", other[1])
packages_string = ', '.join(packages[:-1]) + use_and + packages[-1] packages_string = ", ".join(packages[:-1]) + use_and + packages[-1]
form = {'plural': plural, form = {"plural": plural, "packs": packages_string, "other": other}
'packs' : packages_string,
'other' : other
}
final_string = fmt.format(**form) final_string = fmt.format(**form)
return final_string return final_string
@commands.command(name="shutdown") @commands.command(name="shutdown")
@checks.is_owner() @checks.is_owner()
async def _shutdown(self, ctx, silently: bool=False): async def _shutdown(self, ctx, silently: bool = False):
"""Shuts down the bot""" """Shuts down the bot"""
wave = "\N{WAVING HAND SIGN}" wave = "\N{WAVING HAND SIGN}"
skin = "\N{EMOJI MODIFIER FITZPATRICK TYPE-3}" skin = "\N{EMOJI MODIFIER FITZPATRICK TYPE-3}"
@ -500,7 +486,7 @@ class Core:
@commands.command(name="restart") @commands.command(name="restart")
@checks.is_owner() @checks.is_owner()
async def _restart(self, ctx, silently: bool=False): async def _restart(self, ctx, silently: bool = False):
"""Attempts to restart Red """Attempts to restart Red
Makes Red quit with exit code 26 Makes Red quit with exit code 26
@ -515,7 +501,7 @@ class Core:
def cleanup_and_refresh_modules(self, module_name: str): def cleanup_and_refresh_modules(self, module_name: str):
"""Interally reloads modules so that changes are detected""" """Interally reloads modules so that changes are detected"""
splitted = module_name.split('.') splitted = module_name.split(".")
def maybe_reload(new_name): def maybe_reload(new_name):
try: try:
@ -553,9 +539,11 @@ class Core:
"Mod role: {}\n" "Mod role: {}\n"
"Locale: {}" "Locale: {}"
"".format( "".format(
ctx.bot.user.name, " ".join(prefixes), ctx.bot.user.name,
" ".join(prefixes),
admin_role.name if admin_role else "Not set", admin_role.name if admin_role else "Not set",
mod_role.name if mod_role else "Not set", locale mod_role.name if mod_role else "Not set",
locale,
) )
) )
await ctx.send(box(settings)) await ctx.send(box(settings))
@ -588,9 +576,13 @@ class Core:
try: try:
await ctx.bot.user.edit(avatar=data) await ctx.bot.user.edit(avatar=data)
except discord.HTTPException: except discord.HTTPException:
await ctx.send(_("Failed. Remember that you can edit my avatar " await ctx.send(
"up to two times a hour. The URL must be a " _(
"direct link to a JPG / PNG.")) "Failed. Remember that you can edit my avatar "
"up to two times a hour. The URL must be a "
"direct link to a JPG / PNG."
)
)
except discord.InvalidArgument: except discord.InvalidArgument:
await ctx.send(_("JPG / PNG format only.")) await ctx.send(_("JPG / PNG format only."))
else: else:
@ -599,26 +591,24 @@ class Core:
@_set.command(name="game") @_set.command(name="game")
@checks.bot_in_a_guild() @checks.bot_in_a_guild()
@checks.is_owner() @checks.is_owner()
async def _game(self, ctx, *, game: str=None): async def _game(self, ctx, *, game: str = None):
"""Sets Red's playing status""" """Sets Red's playing status"""
if game: if game:
game = discord.Game(name=game) game = discord.Game(name=game)
else: else:
game = None game = None
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 \ status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
else discord.Status.online
await ctx.bot.change_presence(status=status, activity=game) await ctx.bot.change_presence(status=status, activity=game)
await ctx.send(_("Game set.")) await ctx.send(_("Game set."))
@_set.command(name="listening") @_set.command(name="listening")
@checks.bot_in_a_guild() @checks.bot_in_a_guild()
@checks.is_owner() @checks.is_owner()
async def _listening(self, ctx, *, listening: str=None): async def _listening(self, ctx, *, listening: str = None):
"""Sets Red's listening status""" """Sets Red's listening status"""
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 \ status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
else discord.Status.online
if listening: if listening:
activity = discord.Activity(name=listening, type=discord.ActivityType.listening) activity = discord.Activity(name=listening, type=discord.ActivityType.listening)
else: else:
@ -629,11 +619,10 @@ class Core:
@_set.command(name="watching") @_set.command(name="watching")
@checks.bot_in_a_guild() @checks.bot_in_a_guild()
@checks.is_owner() @checks.is_owner()
async def _watching(self, ctx, *, watching: str=None): async def _watching(self, ctx, *, watching: str = None):
"""Sets Red's watching status""" """Sets Red's watching status"""
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 \ status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else discord.Status.online
else discord.Status.online
if watching: if watching:
activity = discord.Activity(name=watching, type=discord.ActivityType.watching) activity = discord.Activity(name=watching, type=discord.ActivityType.watching)
else: else:
@ -658,7 +647,7 @@ class Core:
"online": discord.Status.online, "online": discord.Status.online,
"idle": discord.Status.idle, "idle": discord.Status.idle,
"dnd": discord.Status.dnd, "dnd": discord.Status.dnd,
"invisible": discord.Status.invisible "invisible": discord.Status.invisible,
} }
game = ctx.bot.guilds[0].me.activity if len(ctx.bot.guilds) > 0 else None game = ctx.bot.guilds[0].me.activity if len(ctx.bot.guilds) > 0 else None
@ -677,8 +666,7 @@ class Core:
"""Sets Red's streaming status """Sets Red's streaming status
Leaving both streamer and stream_title empty will clear it.""" Leaving both streamer and stream_title empty will clear it."""
status = ctx.bot.guilds[0].me.status \ status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 else None
if len(ctx.bot.guilds) > 0 else None
if stream_title: if stream_title:
stream_title = stream_title.strip() stream_title = stream_title.strip()
@ -700,23 +688,28 @@ class Core:
try: try:
await ctx.bot.user.edit(username=username) await ctx.bot.user.edit(username=username)
except discord.HTTPException: except discord.HTTPException:
await ctx.send(_("Failed to change name. Remember that you can " await ctx.send(
"only do it up to 2 times an hour. Use " _(
"nicknames if you need frequent changes. " "Failed to change name. Remember that you can "
"`{}set nickname`").format(ctx.prefix)) "only do it up to 2 times an hour. Use "
"nicknames if you need frequent changes. "
"`{}set nickname`"
).format(
ctx.prefix
)
)
else: else:
await ctx.send(_("Done.")) await ctx.send(_("Done."))
@_set.command(name="nickname") @_set.command(name="nickname")
@checks.admin() @checks.admin()
@commands.guild_only() @commands.guild_only()
async def _nickname(self, ctx, *, nickname: str=None): async def _nickname(self, ctx, *, nickname: str = None):
"""Sets Red's nickname""" """Sets Red's nickname"""
try: try:
await ctx.guild.me.edit(nick=nickname) await ctx.guild.me.edit(nick=nickname)
except discord.Forbidden: except discord.Forbidden:
await ctx.send(_("I lack the permissions to change my own " await ctx.send(_("I lack the permissions to change my own " "nickname."))
"nickname."))
else: else:
await ctx.send("Done.") await ctx.send("Done.")
@ -748,6 +741,7 @@ class Core:
@commands.cooldown(1, 60 * 10, commands.BucketType.default) @commands.cooldown(1, 60 * 10, commands.BucketType.default)
async def owner(self, ctx): async def owner(self, ctx):
"""Sets Red's main owner""" """Sets Red's main owner"""
def check(m): def check(m):
return m.author == ctx.author and m.channel == ctx.channel return m.author == ctx.author and m.channel == ctx.channel
@ -759,20 +753,22 @@ class Core:
for i in range(length): for i in range(length):
token += random.choice(chars) token += random.choice(chars)
log.info("{0} ({0.id}) requested to be set as owner." log.info("{0} ({0.id}) requested to be set as owner." "".format(ctx.author))
"".format(ctx.author))
print(_("\nVerification token:")) print(_("\nVerification token:"))
print(token) print(token)
await ctx.send(_("Remember:\n") + OWNER_DISCLAIMER) await ctx.send(_("Remember:\n") + OWNER_DISCLAIMER)
await asyncio.sleep(5) await asyncio.sleep(5)
await ctx.send(_("I have printed a one-time token in the console. " await ctx.send(
"Copy and paste it here to confirm you are the owner.")) _(
"I have printed a one-time token in the console. "
"Copy and paste it here to confirm you are the owner."
)
)
try: try:
message = await ctx.bot.wait_for("message", check=check, message = await ctx.bot.wait_for("message", check=check, timeout=60)
timeout=60)
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.owner.reset_cooldown(ctx) self.owner.reset_cooldown(ctx)
await ctx.send(_("The set owner request has timed out.")) await ctx.send(_("The set owner request has timed out."))
@ -798,10 +794,15 @@ class Core:
pass pass
await ctx.send( await ctx.send(
_("Please use that command in DM. Since users probably saw your token," _(
" it is recommended to reset it right now. Go to the following link and" "Please use that command in DM. Since users probably saw your token,"
" select `Reveal Token` and `Generate a new token?`." " it is recommended to reset it right now. Go to the following link and"
"\n\nhttps://discordapp.com/developers/applications/me/{}").format(self.bot.user.id)) " select `Reveal Token` and `Generate a new token?`."
"\n\nhttps://discordapp.com/developers/applications/me/{}"
).format(
self.bot.user.id
)
)
return return
await ctx.bot.db.token.set(token) await ctx.bot.db.token.set(token)
@ -854,9 +855,7 @@ class Core:
locale_list = sorted(set([loc.stem for loc in list(red_path.glob("**/*.po"))])) locale_list = sorted(set([loc.stem for loc in list(red_path.glob("**/*.po"))]))
pages = pagify("\n".join(locale_list)) pages = pagify("\n".join(locale_list))
await ctx.send_interactive( await ctx.send_interactive(pages, box_lang="Available Locales:")
pages, box_lang="Available Locales:"
)
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
@ -864,9 +863,11 @@ class Core:
"""Creates a backup of all data for the instance.""" """Creates a backup of all data for the instance."""
from redbot.core.data_manager import basic_config, instance_name from redbot.core.data_manager import basic_config, instance_name
from redbot.core.drivers.red_json import JSON from redbot.core.drivers.red_json import JSON
data_dir = Path(basic_config["DATA_PATH"]) data_dir = Path(basic_config["DATA_PATH"])
if basic_config["STORAGE_TYPE"] == "MongoDB": if basic_config["STORAGE_TYPE"] == "MongoDB":
from redbot.core.drivers.red_mongo import Mongo from redbot.core.drivers.red_mongo import Mongo
m = Mongo("Core", **basic_config["STORAGE_DETAILS"]) m = Mongo("Core", **basic_config["STORAGE_DETAILS"])
db = m.db db = m.db
collection_names = await db.collection_names(include_system_collections=False) collection_names = await db.collection_names(include_system_collections=False)
@ -891,9 +892,9 @@ class Core:
os.chdir(str(data_dir.parent)) os.chdir(str(data_dir.parent))
with tarfile.open(str(backup_file), "w:gz") as tar: with tarfile.open(str(backup_file), "w:gz") as tar:
tar.add(data_dir.stem) tar.add(data_dir.stem)
await ctx.send(_("A backup has been made of this instance. It is at {}.").format( await ctx.send(
backup_file _("A backup has been made of this instance. It is at {}.").format(backup_file)
)) )
else: else:
await ctx.send(_("That directory doesn't seem to exist...")) await ctx.send(_("That directory doesn't seem to exist..."))
@ -902,8 +903,7 @@ class Core:
async def contact(self, ctx, *, message: str): async def contact(self, ctx, *, message: str):
"""Sends a message to the owner""" """Sends a message to the owner"""
guild = ctx.message.guild guild = ctx.message.guild
owner = discord.utils.get(ctx.bot.get_all_members(), owner = discord.utils.get(ctx.bot.get_all_members(), id=ctx.bot.owner_id)
id=ctx.bot.owner_id)
author = ctx.message.author author = ctx.message.author
footer = _("User ID: {}").format(author.id) footer = _("User ID: {}").format(author.id)
@ -916,12 +916,11 @@ class Core:
# We need to grab the DM command prefix (global) # We need to grab the DM command prefix (global)
# Since it can also be set through cli flags, bot.db is not a reliable # Since it can also be set through cli flags, bot.db is not a reliable
# source. So we'll just mock a DM message instead. # source. So we'll just mock a DM message instead.
fake_message = namedtuple('Message', 'guild') fake_message = namedtuple("Message", "guild")
prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None)) prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))
prefix = prefixes[0] prefix = prefixes[0]
content = _("Use `{}dm {} <text>` to reply to this user" content = _("Use `{}dm {} <text>` to reply to this user" "").format(prefix, author.id)
"").format(prefix, author.id)
description = _("Sent by {} {}").format(author, source) description = _("Sent by {} {}").format(author, source)
@ -941,21 +940,21 @@ class Core:
try: try:
await owner.send(content, embed=e) await owner.send(content, embed=e)
except discord.InvalidArgument: except discord.InvalidArgument:
await ctx.send(_("I cannot send your message, I'm unable to find " await ctx.send(
"my owner... *sigh*")) _("I cannot send your message, I'm unable to find " "my owner... *sigh*")
)
except: except:
await ctx.send(_("I'm unable to deliver your message. Sorry.")) await ctx.send(_("I'm unable to deliver your message. Sorry."))
else: else:
await ctx.send(_("Your message has been sent.")) await ctx.send(_("Your message has been sent."))
else: else:
msg_text = ( msg_text = ("{}\nMessage:\n\n{}\n{}".format(description, message, footer))
"{}\nMessage:\n\n{}\n{}".format(description, message, footer)
)
try: try:
await owner.send("{}\n{}".format(content, box(msg_text))) await owner.send("{}\n{}".format(content, box(msg_text)))
except discord.InvalidArgument: except discord.InvalidArgument:
await ctx.send(_("I cannot send your message, I'm unable to find " await ctx.send(
"my owner... *sigh*")) _("I cannot send your message, I'm unable to find " "my owner... *sigh*")
)
except: except:
await ctx.send(_("I'm unable to deliver your message. Sorry.")) await ctx.send(_("I'm unable to deliver your message. Sorry."))
else: else:
@ -970,15 +969,18 @@ class Core:
To get a user id enable 'developer mode' in Discord's To get a user id enable 'developer mode' in Discord's
settings, 'appearance' tab. Then right click a user settings, 'appearance' tab. Then right click a user
and copy their id""" and copy their id"""
destination = discord.utils.get(ctx.bot.get_all_members(), destination = discord.utils.get(ctx.bot.get_all_members(), id=user_id)
id=user_id)
if destination is None: if destination is None:
await ctx.send(_("Invalid ID or user not found. You can only " await ctx.send(
"send messages to people I share a server " _(
"with.")) "Invalid ID or user not found. You can only "
"send messages to people I share a server "
"with."
)
)
return return
fake_message = namedtuple('Message', 'guild') fake_message = namedtuple("Message", "guild")
prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None)) prefixes = await ctx.bot.command_prefix(ctx.bot, fake_message(guild=None))
prefix = prefixes[0] prefix = prefixes[0]
description = _("Owner of {}").format(ctx.bot.user) description = _("Owner of {}").format(ctx.bot.user)
@ -995,8 +997,9 @@ class Core:
try: try:
await destination.send(embed=e) await destination.send(embed=e)
except: except:
await ctx.send(_("Sorry, I couldn't deliver your message " await ctx.send(
"to {}").format(destination)) _("Sorry, I couldn't deliver your message " "to {}").format(destination)
)
else: else:
await ctx.send(_("Message delivered to {}").format(destination)) await ctx.send(_("Message delivered to {}").format(destination))
else: else:
@ -1004,8 +1007,9 @@ class Core:
try: try:
await destination.send("{}\n{}".format(box(response), content)) await destination.send("{}\n{}".format(box(response), content))
except: except:
await ctx.send(_("Sorry, I couldn't deliver your message " await ctx.send(
"to {}").format(destination)) _("Sorry, I couldn't deliver your message " "to {}").format(destination)
)
else: else:
await ctx.send(_("Message delivered to {}").format(destination)) await ctx.send(_("Message delivered to {}").format(destination))
@ -1018,7 +1022,7 @@ class Core:
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
@whitelist.command(name='add') @whitelist.command(name="add")
async def whitelist_add(self, ctx, user: discord.User): async def whitelist_add(self, ctx, user: discord.User):
""" """
Adds a user to the whitelist. Adds a user to the whitelist.
@ -1029,7 +1033,7 @@ class Core:
await ctx.send(_("User added to whitelist.")) await ctx.send(_("User added to whitelist."))
@whitelist.command(name='list') @whitelist.command(name="list")
async def whitelist_list(self, ctx): async def whitelist_list(self, ctx):
""" """
Lists whitelisted users. Lists whitelisted users.
@ -1043,7 +1047,7 @@ class Core:
for page in pagify(msg): for page in pagify(msg):
await ctx.send(box(page)) await ctx.send(box(page))
@whitelist.command(name='remove') @whitelist.command(name="remove")
async def whitelist_remove(self, ctx, user: discord.User): async def whitelist_remove(self, ctx, user: discord.User):
""" """
Removes user from whitelist. Removes user from whitelist.
@ -1060,7 +1064,7 @@ class Core:
else: else:
await ctx.send(_("User was not in the whitelist.")) await ctx.send(_("User was not in the whitelist."))
@whitelist.command(name='clear') @whitelist.command(name="clear")
async def whitelist_clear(self, ctx): async def whitelist_clear(self, ctx):
""" """
Clears the whitelist. Clears the whitelist.
@ -1077,7 +1081,7 @@ class Core:
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
@blacklist.command(name='add') @blacklist.command(name="add")
async def blacklist_add(self, ctx, user: discord.User): async def blacklist_add(self, ctx, user: discord.User):
""" """
Adds a user to the blacklist. Adds a user to the blacklist.
@ -1092,7 +1096,7 @@ class Core:
await ctx.send(_("User added to blacklist.")) await ctx.send(_("User added to blacklist."))
@blacklist.command(name='list') @blacklist.command(name="list")
async def blacklist_list(self, ctx): async def blacklist_list(self, ctx):
""" """
Lists blacklisted users. Lists blacklisted users.
@ -1106,7 +1110,7 @@ class Core:
for page in pagify(msg): for page in pagify(msg):
await ctx.send(box(page)) await ctx.send(box(page))
@blacklist.command(name='remove') @blacklist.command(name="remove")
async def blacklist_remove(self, ctx, user: discord.User): async def blacklist_remove(self, ctx, user: discord.User):
""" """
Removes user from blacklist. Removes user from blacklist.
@ -1123,7 +1127,7 @@ class Core:
else: else:
await ctx.send(_("User was not in the blacklist.")) await ctx.send(_("User was not in the blacklist."))
@blacklist.command(name='clear') @blacklist.command(name="clear")
async def blacklist_clear(self, ctx): async def blacklist_clear(self, ctx):
""" """
Clears the blacklist. Clears the blacklist.

View File

@ -14,9 +14,15 @@ from .utils import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from . import Config from . import Config
__all__ = ['load_basic_configuration', 'cog_data_path', 'core_data_path', __all__ = [
'load_bundled_data', 'bundled_data_path', 'storage_details', "load_basic_configuration",
'storage_type'] "cog_data_path",
"core_data_path",
"load_bundled_data",
"bundled_data_path",
"storage_details",
"storage_type",
]
log = logging.getLogger("red.data_manager") log = logging.getLogger("red.data_manager")
@ -25,20 +31,16 @@ basic_config = None
instance_name = None instance_name = None
basic_config_default = { basic_config_default = {"DATA_PATH": None, "COG_PATH_APPEND": "cogs", "CORE_PATH_APPEND": "core"}
"DATA_PATH": None,
"COG_PATH_APPEND": "cogs",
"CORE_PATH_APPEND": "core"
}
config_dir = None config_dir = None
appdir = appdirs.AppDirs("Red-DiscordBot") appdir = appdirs.AppDirs("Red-DiscordBot")
if sys.platform == 'linux': if sys.platform == "linux":
if 0 < os.getuid() < 1000: if 0 < os.getuid() < 1000:
config_dir = Path(appdir.site_data_dir) config_dir = Path(appdir.site_data_dir)
if not config_dir: if not config_dir:
config_dir = Path(appdir.user_config_dir) config_dir = Path(appdir.user_config_dir)
config_file = config_dir / 'config.json' config_file = config_dir / "config.json"
def load_basic_configuration(instance_name_: str): def load_basic_configuration(instance_name_: str):
@ -67,20 +69,23 @@ def load_basic_configuration(instance_name_: str):
config = jsonio._load_json() config = jsonio._load_json()
basic_config = config[instance_name] basic_config = config[instance_name]
except (FileNotFoundError, KeyError): except (FileNotFoundError, KeyError):
print("You need to configure the bot instance using `redbot-setup`" print(
" prior to running the bot.") "You need to configure the bot instance using `redbot-setup`"
" prior to running the bot."
)
sys.exit(1) sys.exit(1)
def _base_data_path() -> Path: def _base_data_path() -> Path:
if basic_config is None: if basic_config is None:
raise RuntimeError("You must load the basic config before you" raise RuntimeError(
" can get the base data path.") "You must load the basic config before you" " can get the base data path."
path = basic_config['DATA_PATH'] )
path = basic_config["DATA_PATH"]
return Path(path).resolve() return Path(path).resolve()
def cog_data_path(cog_instance=None, raw_name: str=None) -> Path: def cog_data_path(cog_instance=None, raw_name: str = None) -> Path:
"""Gets the base cog data path. If you want to get the folder with """Gets the base cog data path. If you want to get the folder with
which to store your own cog's data please pass in an instance which to store your own cog's data please pass in an instance
of your cog class. of your cog class.
@ -104,9 +109,10 @@ def cog_data_path(cog_instance=None, raw_name: str=None) -> Path:
try: try:
base_data_path = Path(_base_data_path()) base_data_path = Path(_base_data_path())
except RuntimeError as e: except RuntimeError as e:
raise RuntimeError("You must load the basic config before you" raise RuntimeError(
" can get the cog data path.") from e "You must load the basic config before you" " can get the cog data path."
cog_path = base_data_path / basic_config['COG_PATH_APPEND'] ) from e
cog_path = base_data_path / basic_config["COG_PATH_APPEND"]
if raw_name is not None: if raw_name is not None:
cog_path = cog_path / raw_name cog_path = cog_path / raw_name
@ -121,9 +127,10 @@ def core_data_path() -> Path:
try: try:
base_data_path = Path(_base_data_path()) base_data_path = Path(_base_data_path())
except RuntimeError as e: except RuntimeError as e:
raise RuntimeError("You must load the basic config before you" raise RuntimeError(
" can get the core data path.") from e "You must load the basic config before you" " can get the core data path."
core_path = base_data_path / basic_config['CORE_PATH_APPEND'] ) from e
core_path = base_data_path / basic_config["CORE_PATH_APPEND"]
core_path.mkdir(exist_ok=True, parents=True) core_path.mkdir(exist_ok=True, parents=True)
return core_path.resolve() return core_path.resolve()
@ -145,15 +152,13 @@ def _find_data_files(init_location: str) -> (Path, List[Path]):
if not init_file.is_file(): if not init_file.is_file():
return [] return []
package_folder = init_file.parent.resolve() / 'data' package_folder = init_file.parent.resolve() / "data"
if not package_folder.is_dir(): if not package_folder.is_dir():
return [] return []
all_files = list(package_folder.rglob("*")) all_files = list(package_folder.rglob("*"))
return package_folder, [p.resolve() return package_folder, [p.resolve() for p in all_files if p.is_file()]
for p in all_files
if p.is_file()]
def _compare_and_copy(to_copy: List[Path], bundled_data_dir: Path, cog_data_dir: Path): def _compare_and_copy(to_copy: List[Path], bundled_data_dir: Path, cog_data_dir: Path):
@ -181,27 +186,24 @@ def _compare_and_copy(to_copy: List[Path], bundled_data_dir: Path, cog_data_dir:
yield block yield block
block = afile.read(blocksize) block = afile.read(blocksize)
lookup = {p: cog_data_dir.joinpath(p.relative_to(bundled_data_dir)) lookup = {p: cog_data_dir.joinpath(p.relative_to(bundled_data_dir)) for p in to_copy}
for p in to_copy}
for orig, poss_existing in lookup.items(): for orig, poss_existing in lookup.items():
if not poss_existing.is_file(): if not poss_existing.is_file():
poss_existing.parent.mkdir(exist_ok=True, parents=True) poss_existing.parent.mkdir(exist_ok=True, parents=True)
exists_checksum = None exists_checksum = None
else: else:
exists_checksum = hash_bytestr_iter(file_as_blockiter( exists_checksum = hash_bytestr_iter(
poss_existing.open('rb')), hashlib.sha256()) file_as_blockiter(poss_existing.open("rb")), hashlib.sha256()
)
orig_checksum = ... orig_checksum = ...
if exists_checksum is not None: if exists_checksum is not None:
orig_checksum = hash_bytestr_iter(file_as_blockiter( orig_checksum = hash_bytestr_iter(file_as_blockiter(orig.open("rb")), hashlib.sha256())
orig.open('rb')), hashlib.sha256())
if exists_checksum != orig_checksum: if exists_checksum != orig_checksum:
shutil.copy(str(orig), str(poss_existing)) shutil.copy(str(orig), str(poss_existing))
log.debug("Copying {} to {}".format( log.debug("Copying {} to {}".format(orig, poss_existing))
orig, poss_existing
))
def load_bundled_data(cog_instance, init_location: str): def load_bundled_data(cog_instance, init_location: str):
@ -233,7 +235,7 @@ def load_bundled_data(cog_instance, init_location: str):
""" """
bundled_data_folder, to_copy = _find_data_files(init_location) bundled_data_folder, to_copy = _find_data_files(init_location)
cog_data_folder = cog_data_path(cog_instance) / 'bundled_data' cog_data_folder = cog_data_path(cog_instance) / "bundled_data"
_compare_and_copy(to_copy, bundled_data_folder, cog_data_folder) _compare_and_copy(to_copy, bundled_data_folder, cog_data_folder)
@ -264,12 +266,10 @@ def bundled_data_path(cog_instance) -> Path:
If no bundled data folder exists or if it hasn't been loaded yet. If no bundled data folder exists or if it hasn't been loaded yet.
""" """
bundled_path = cog_data_path(cog_instance) / 'bundled_data' bundled_path = cog_data_path(cog_instance) / "bundled_data"
if not bundled_path.is_dir(): if not bundled_path.is_dir():
raise FileNotFoundError("No such directory {}".format( raise FileNotFoundError("No such directory {}".format(bundled_path))
bundled_path
))
return bundled_path return bundled_path
@ -282,9 +282,9 @@ def storage_type() -> str:
str str
""" """
try: try:
return basic_config['STORAGE_TYPE'] return basic_config["STORAGE_TYPE"]
except KeyError as e: except KeyError as e:
raise RuntimeError('Bot basic config has not been loaded yet.') from e raise RuntimeError("Bot basic config has not been loaded yet.") from e
def storage_details() -> dict: def storage_details() -> dict:
@ -297,6 +297,6 @@ def storage_details() -> dict:
dict dict
""" """
try: try:
return basic_config['STORAGE_DETAILS'] return basic_config["STORAGE_DETAILS"]
except KeyError as e: except KeyError as e:
raise RuntimeError('Bot basic config has not been loaded yet.') from e raise RuntimeError("Bot basic config has not been loaded yet.") from e

View File

@ -10,6 +10,7 @@ import discord
from . import checks, commands from . import checks, commands
from .i18n import Translator from .i18n import Translator
from .utils.chat_formatting import box, pagify from .utils.chat_formatting import box, pagify
""" """
Notice: Notice:
@ -32,11 +33,11 @@ class Dev:
def cleanup_code(content): def cleanup_code(content):
"""Automatically removes code blocks from the code.""" """Automatically removes code blocks from the code."""
# remove ```py\n``` # remove ```py\n```
if content.startswith('```') and content.endswith('```'): if content.startswith("```") and content.endswith("```"):
return '\n'.join(content.split('\n')[1:-1]) return "\n".join(content.split("\n")[1:-1])
# remove `foo` # remove `foo`
return content.strip('` \n') return content.strip("` \n")
@staticmethod @staticmethod
def get_syntax_error(e): def get_syntax_error(e):
@ -45,11 +46,10 @@ class Dev:
Returns a string representation of the error formatted as a codeblock. Returns a string representation of the error formatted as a codeblock.
""" """
if e.text is None: if e.text is None:
return box('{0.__class__.__name__}: {0}'.format(e), lang="py") return box("{0.__class__.__name__}: {0}".format(e), lang="py")
return box( return box(
'{0.text}{1:>{0.offset}}\n{2}: {0}' "{0.text}{1:>{0.offset}}\n{2}: {0}" "".format(e, "^", type(e).__name__), lang="py"
''.format(e, '^', type(e).__name__), )
lang="py")
@staticmethod @staticmethod
def get_pages(msg: str): def get_pages(msg: str):
@ -90,15 +90,15 @@ class Dev:
_ - The result of the last dev command. _ - The result of the last dev command.
""" """
env = { env = {
'bot': ctx.bot, "bot": ctx.bot,
'ctx': ctx, "ctx": ctx,
'channel': ctx.channel, "channel": ctx.channel,
'author': ctx.author, "author": ctx.author,
'guild': ctx.guild, "guild": ctx.guild,
'message': ctx.message, "message": ctx.message,
'discord': discord, "discord": discord,
'commands': commands, "commands": commands,
'_': self._last_result "_": self._last_result,
} }
code = self.cleanup_code(code) code = self.cleanup_code(code)
@ -109,8 +109,7 @@ class Dev:
await ctx.send(self.get_syntax_error(e)) await ctx.send(self.get_syntax_error(e))
return return
except Exception as e: except Exception as e:
await ctx.send( await ctx.send(box("{}: {!s}".format(type(e).__name__, e), lang="py"))
box('{}: {!s}'.format(type(e).__name__, e), lang='py'))
return return
if asyncio.iscoroutine(result): if asyncio.iscoroutine(result):
@ -122,7 +121,7 @@ class Dev:
await ctx.send_interactive(self.get_pages(result), box_lang="py") await ctx.send_interactive(self.get_pages(result), box_lang="py")
@commands.command(name='eval') @commands.command(name="eval")
@checks.is_owner() @checks.is_owner()
async def _eval(self, ctx, *, body: str): async def _eval(self, ctx, *, body: str):
"""Execute asynchronous code. """Execute asynchronous code.
@ -145,28 +144,28 @@ class Dev:
_ - The result of the last dev command. _ - The result of the last dev command.
""" """
env = { env = {
'bot': ctx.bot, "bot": ctx.bot,
'ctx': ctx, "ctx": ctx,
'channel': ctx.channel, "channel": ctx.channel,
'author': ctx.author, "author": ctx.author,
'guild': ctx.guild, "guild": ctx.guild,
'message': ctx.message, "message": ctx.message,
'discord': discord, "discord": discord,
'commands': commands, "commands": commands,
'_': self._last_result "_": self._last_result,
} }
body = self.cleanup_code(body) body = self.cleanup_code(body)
stdout = io.StringIO() stdout = io.StringIO()
to_compile = 'async def func():\n%s' % textwrap.indent(body, ' ') to_compile = "async def func():\n%s" % textwrap.indent(body, " ")
try: try:
exec(to_compile, env) exec(to_compile, env)
except SyntaxError as e: except SyntaxError as e:
return await ctx.send(self.get_syntax_error(e)) return await ctx.send(self.get_syntax_error(e))
func = env['func'] func = env["func"]
result = None result = None
try: try:
with redirect_stdout(stdout): with redirect_stdout(stdout):
@ -199,43 +198,43 @@ class Dev:
async function. async function.
""" """
variables = { variables = {
'ctx': ctx, "ctx": ctx,
'bot': ctx.bot, "bot": ctx.bot,
'message': ctx.message, "message": ctx.message,
'guild': ctx.guild, "guild": ctx.guild,
'channel': ctx.channel, "channel": ctx.channel,
'author': ctx.author, "author": ctx.author,
'_': None, "_": None,
} }
if ctx.channel.id in self.sessions: if ctx.channel.id in self.sessions:
await ctx.send(_('Already running a REPL session in this channel. ' await ctx.send(
'Exit it with `quit`.')) _("Already running a REPL session in this channel. " "Exit it with `quit`.")
)
return return
self.sessions.add(ctx.channel.id) self.sessions.add(ctx.channel.id)
await ctx.send(_('Enter code to execute or evaluate.' await ctx.send(_("Enter code to execute or evaluate." " `exit()` or `quit` to exit."))
' `exit()` or `quit` to exit.'))
msg_check = lambda m: (m.author == ctx.author and msg_check = lambda m: (
m.channel == ctx.channel and m.author == ctx.author and m.channel == ctx.channel and m.content.startswith("`")
m.content.startswith('`')) )
while True: while True:
response = await ctx.bot.wait_for("message", check=msg_check) response = await ctx.bot.wait_for("message", check=msg_check)
cleaned = self.cleanup_code(response.content) cleaned = self.cleanup_code(response.content)
if cleaned in ('quit', 'exit', 'exit()'): if cleaned in ("quit", "exit", "exit()"):
await ctx.send('Exiting.') await ctx.send("Exiting.")
self.sessions.remove(ctx.channel.id) self.sessions.remove(ctx.channel.id)
return return
executor = exec executor = exec
if cleaned.count('\n') == 0: if cleaned.count("\n") == 0:
# single statement, potentially 'eval' # single statement, potentially 'eval'
try: try:
code = compile(cleaned, '<repl session>', 'eval') code = compile(cleaned, "<repl session>", "eval")
except SyntaxError: except SyntaxError:
pass pass
else: else:
@ -243,12 +242,12 @@ class Dev:
if executor is exec: if executor is exec:
try: try:
code = compile(cleaned, '<repl session>', 'exec') code = compile(cleaned, "<repl session>", "exec")
except SyntaxError as e: except SyntaxError as e:
await ctx.send(self.get_syntax_error(e)) await ctx.send(self.get_syntax_error(e))
continue continue
variables['message'] = response variables["message"] = response
stdout = io.StringIO() stdout = io.StringIO()
@ -266,7 +265,7 @@ class Dev:
value = stdout.getvalue() value = stdout.getvalue()
if result is not None: if result is not None:
msg = "{}{}".format(value, result) msg = "{}{}".format(value, result)
variables['_'] = result variables["_"] = result
elif value: elif value:
msg = "{}".format(value) msg = "{}".format(value)
@ -277,7 +276,7 @@ class Dev:
except discord.Forbidden: except discord.Forbidden:
pass pass
except discord.HTTPException as e: except discord.HTTPException as e:
await ctx.send(_('Unexpected error: `{}`').format(e)) await ctx.send(_("Unexpected error: `{}`").format(e))
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
@ -290,7 +289,7 @@ class Dev:
msg.author = user msg.author = user
msg.content = ctx.prefix + command msg.content = ctx.prefix + command
ctx.bot.dispatch('message', msg) ctx.bot.dispatch("message", msg)
@commands.command(name="mockmsg") @commands.command(name="mockmsg")
@checks.is_owner() @checks.is_owner()

View File

@ -24,8 +24,10 @@ def get_driver(type, *args, **kwargs):
""" """
if type == "JSON": if type == "JSON":
from .red_json import JSON from .red_json import JSON
return JSON(*args, **kwargs) return JSON(*args, **kwargs)
elif type == "MongoDB": elif type == "MongoDB":
from .red_mongo import Mongo from .red_mongo import Mongo
return Mongo(*args, **kwargs) return Mongo(*args, **kwargs)
raise RuntimeError("Invalid driver type: '{}'".format(type)) raise RuntimeError("Invalid driver type: '{}'".format(type))

View File

@ -2,6 +2,7 @@ __all__ = ["BaseDriver"]
class BaseDriver: class BaseDriver:
def __init__(self, cog_name, identifier): def __init__(self, cog_name, identifier):
self.cog_name = cog_name self.cog_name = cog_name
self.unique_cog_identifier = identifier self.unique_cog_identifier = identifier

View File

@ -44,14 +44,21 @@ class JSON(BaseDriver):
The path in which to store the file indicated by :py:attr:`file_name`. The path in which to store the file indicated by :py:attr:`file_name`.
""" """
def __init__(self, cog_name, identifier, *, data_path_override: Path=None,
file_name_override: str="settings.json"): def __init__(
self,
cog_name,
identifier,
*,
data_path_override: Path = None,
file_name_override: str = "settings.json"
):
super().__init__(cog_name, identifier) super().__init__(cog_name, identifier)
self.file_name = file_name_override self.file_name = file_name_override
if data_path_override: if data_path_override:
self.data_path = data_path_override self.data_path = data_path_override
else: else:
self.data_path = Path.cwd() / 'cogs' / '.data' / self.cog_name self.data_path = Path.cwd() / "cogs" / ".data" / self.cog_name
self.data_path.mkdir(parents=True, exist_ok=True) self.data_path.mkdir(parents=True, exist_ok=True)

View File

@ -8,21 +8,16 @@ _conn = None
def _initialize(**kwargs): def _initialize(**kwargs):
host = kwargs['HOST'] host = kwargs["HOST"]
port = kwargs['PORT'] port = kwargs["PORT"]
admin_user = kwargs['USERNAME'] admin_user = kwargs["USERNAME"]
admin_pass = kwargs['PASSWORD'] admin_pass = kwargs["PASSWORD"]
db_name = kwargs.get('DB_NAME', 'default_db') db_name = kwargs.get("DB_NAME", "default_db")
if admin_user is not None and admin_pass is not None: if admin_user is not None and admin_pass is not None:
url = "mongodb://{}:{}@{}:{}/{}".format( url = "mongodb://{}:{}@{}:{}/{}".format(admin_user, admin_pass, host, port, db_name)
admin_user, admin_pass, host, port,
db_name
)
else: else:
url = "mongodb://{}:{}/{}".format( url = "mongodb://{}:{}/{}".format(host, port, db_name)
host, port, db_name
)
global _conn global _conn
_conn = motor.motor_asyncio.AsyncIOMotorClient(url) _conn = motor.motor_asyncio.AsyncIOMotorClient(url)
@ -32,6 +27,7 @@ class Mongo(BaseDriver):
""" """
Subclass of :py:class:`.red_base.BaseDriver`. Subclass of :py:class:`.red_base.BaseDriver`.
""" """
def __init__(self, cog_name, identifier, **kwargs): def __init__(self, cog_name, identifier, **kwargs):
super().__init__(cog_name, identifier) super().__init__(cog_name, identifier)
@ -75,45 +71,40 @@ class Mongo(BaseDriver):
async def get(self, *identifiers: str): async def get(self, *identifiers: str):
mongo_collection = self.get_collection() mongo_collection = self.get_collection()
dot_identifiers = '.'.join(identifiers) dot_identifiers = ".".join(identifiers)
partial = await mongo_collection.find_one( partial = await mongo_collection.find_one(
filter={'_id': self.unique_cog_identifier}, filter={"_id": self.unique_cog_identifier}, projection={dot_identifiers: True}
projection={dot_identifiers: True}
) )
if partial is None: if partial is None:
raise KeyError("No matching document was found and Config expects" raise KeyError("No matching document was found and Config expects" " a KeyError.")
" a KeyError.")
for i in identifiers: for i in identifiers:
partial = partial[i] partial = partial[i]
return partial return partial
async def set(self, *identifiers: str, value=None): async def set(self, *identifiers: str, value=None):
dot_identifiers = '.'.join(identifiers) dot_identifiers = ".".join(identifiers)
mongo_collection = self.get_collection() mongo_collection = self.get_collection()
await mongo_collection.update_one( await mongo_collection.update_one(
{'_id': self.unique_cog_identifier}, {"_id": self.unique_cog_identifier},
update={"$set": {dot_identifiers: value}}, update={"$set": {dot_identifiers: value}},
upsert=True upsert=True,
) )
async def clear(self, *identifiers: str): async def clear(self, *identifiers: str):
dot_identifiers = '.'.join(identifiers) dot_identifiers = ".".join(identifiers)
mongo_collection = self.get_collection() mongo_collection = self.get_collection()
if len(identifiers) > 0: if len(identifiers) > 0:
await mongo_collection.update_one( await mongo_collection.update_one(
{'_id': self.unique_cog_identifier}, {"_id": self.unique_cog_identifier}, update={"$unset": {dot_identifiers: 1}}
update={"$unset": {dot_identifiers: 1}}
) )
else: else:
await mongo_collection.delete_one( await mongo_collection.delete_one({"_id": self.unique_cog_identifier})
{'_id': self.unique_cog_identifier}
)
def get_config_details(): def get_config_details():
@ -129,10 +120,10 @@ def get_config_details():
admin_uname = admin_password = None admin_uname = admin_password = None
ret = { ret = {
'HOST': host, "HOST": host,
'PORT': port, "PORT": port,
'USERNAME': admin_uname, "USERNAME": admin_uname,
'PASSWORD': admin_password, "PASSWORD": admin_password,
'DB_NAME': db_name "DB_NAME": db_name,
} }
return ret return ret

View File

@ -44,8 +44,8 @@ def should_log_sentry(exception) -> bool:
tb_frame = tb.tb_frame tb_frame = tb.tb_frame
tb = tb.tb_next tb = tb.tb_next
module = tb_frame.f_globals.get('__name__') module = tb_frame.f_globals.get("__name__")
return module.startswith('redbot') return module.startswith("redbot")
def init_events(bot, cli_flags): def init_events(bot, cli_flags):
@ -77,8 +77,7 @@ def init_events(bot, cli_flags):
spec = await bot.cog_mgr.find_cog(package) spec = await bot.cog_mgr.find_cog(package)
await bot.load_extension(spec) await bot.load_extension(spec)
except Exception as e: except Exception as e:
log.exception("Failed to load package {}".format(package), log.exception("Failed to load package {}".format(package), exc_info=e)
exc_info=e)
await bot.remove_loaded_package(package) await bot.remove_loaded_package(package)
to_remove.append(package) to_remove.append(package)
for package in to_remove: for package in to_remove:
@ -104,18 +103,21 @@ def init_events(bot, cli_flags):
red_pkg = pkg_resources.get_distribution("Red-DiscordBot") red_pkg = pkg_resources.get_distribution("Red-DiscordBot")
dpy_version = discord.__version__ dpy_version = discord.__version__
INFO = [str(bot.user), "Prefixes: {}".format(', '.join(prefixes)), INFO = [
'Language: {}'.format(lang), str(bot.user),
"Red Bot Version: {}".format(red_version), "Prefixes: {}".format(", ".join(prefixes)),
"Discord.py Version: {}".format(dpy_version), "Language: {}".format(lang),
"Shards: {}".format(bot.shard_count)] "Red Bot Version: {}".format(red_version),
"Discord.py Version: {}".format(dpy_version),
"Shards: {}".format(bot.shard_count),
]
if guilds: if guilds:
INFO.extend(("Servers: {}".format(guilds), "Users: {}".format(users))) INFO.extend(("Servers: {}".format(guilds), "Users: {}".format(users)))
else: else:
print("Ready. I'm not in any server yet!") print("Ready. I'm not in any server yet!")
INFO.append('{} cogs with {} commands'.format(len(bot.cogs), len(bot.commands))) INFO.append("{} cogs with {} commands".format(len(bot.cogs), len(bot.commands)))
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r: async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r:
@ -139,11 +141,7 @@ def init_events(bot, cli_flags):
sentry = await bot.db.enable_sentry() sentry = await bot.db.enable_sentry()
mongo_enabled = storage_type() != "JSON" mongo_enabled = storage_type() != "JSON"
reqs_installed = { reqs_installed = {"voice": None, "docs": None, "test": None}
"voice": None,
"docs": None,
"test": None
}
for key in reqs_installed.keys(): for key in reqs_installed.keys():
reqs = [x.name for x in red_pkg._dep_map[key]] reqs = [x.name for x in red_pkg._dep_map[key]]
try: try:
@ -158,7 +156,7 @@ def init_events(bot, cli_flags):
("MongoDB", mongo_enabled), ("MongoDB", mongo_enabled),
("Voice", reqs_installed["voice"]), ("Voice", reqs_installed["voice"]),
("Docs", reqs_installed["docs"]), ("Docs", reqs_installed["docs"]),
("Tests", reqs_installed["test"]) ("Tests", reqs_installed["test"]),
) )
on_symbol, off_symbol, ascii_border = _get_startup_screen_specs() on_symbol, off_symbol, ascii_border = _get_startup_screen_specs()
@ -201,21 +199,25 @@ def init_events(bot, cli_flags):
await ctx.send(msg) await ctx.send(msg)
return return
""" """
log.exception("Exception in command '{}'" log.exception(
"".format(ctx.command.qualified_name), "Exception in command '{}'" "".format(ctx.command.qualified_name),
exc_info=error.original) exc_info=error.original,
)
if should_log_sentry(error): if should_log_sentry(error):
sentry_log.exception("Exception in command '{}'" sentry_log.exception(
"".format(ctx.command.qualified_name), "Exception in command '{}'" "".format(ctx.command.qualified_name),
exc_info=error.original) exc_info=error.original,
)
message = ("Error in command '{}'. Check your console or " message = (
"logs for details." "Error in command '{}'. Check your console or "
"".format(ctx.command.qualified_name)) "logs for details."
exception_log = ("Exception in command '{}'\n" "".format(ctx.command.qualified_name)
"".format(ctx.command.qualified_name)) )
exception_log += "".join(traceback.format_exception(type(error), exception_log = ("Exception in command '{}'\n" "".format(ctx.command.qualified_name))
error, error.__traceback__)) exception_log += "".join(
traceback.format_exception(type(error), error, error.__traceback__)
)
bot._last_exception = exception_log bot._last_exception = exception_log
if not hasattr(ctx.cog, "_{0.command.cog_name}__error".format(ctx)): if not hasattr(ctx.cog, "_{0.command.cog_name}__error".format(ctx)):
await ctx.send(inline(message)) await ctx.send(inline(message))
@ -226,9 +228,9 @@ def init_events(bot, cli_flags):
elif isinstance(error, commands.NoPrivateMessage): elif isinstance(error, commands.NoPrivateMessage):
await ctx.send("That command is not available in DMs.") await ctx.send("That command is not available in DMs.")
elif isinstance(error, commands.CommandOnCooldown): elif isinstance(error, commands.CommandOnCooldown):
await ctx.send("This command is on cooldown. " await ctx.send(
"Try again in {:.2f}s" "This command is on cooldown. " "Try again in {:.2f}s" "".format(error.retry_after)
"".format(error.retry_after)) )
else: else:
log.exception(type(error).__name__, exc_info=error) log.exception(type(error).__name__, exc_info=error)
try: try:
@ -237,8 +239,7 @@ def init_events(bot, cli_flags):
sentry_error = error sentry_error = error
if should_log_sentry(sentry_error): if should_log_sentry(sentry_error):
sentry_log.exception("Unhandled command error.", sentry_log.exception("Unhandled command error.", exc_info=sentry_error)
exc_info=sentry_error)
@bot.event @bot.event
async def on_message(message): async def on_message(message):
@ -253,6 +254,7 @@ def init_events(bot, cli_flags):
async def on_command(command): async def on_command(command):
bot.counter["processed_commands"] += 1 bot.counter["processed_commands"] += 1
def _get_startup_screen_specs(): def _get_startup_screen_specs():
"""Get specs for displaying the startup screen on stdout. """Get specs for displaying the startup screen on stdout.
@ -278,11 +280,10 @@ def _get_startup_screen_specs():
off_symbol = "X" off_symbol = "X"
try: try:
encoder('┌┐└┘─│') # border symbols encoder("┌┐└┘─│") # border symbols
except UnicodeEncodeError: except UnicodeEncodeError:
ascii_border = True ascii_border = True
else: else:
ascii_border = False ascii_border = False
return on_symbol, off_symbol, ascii_border return on_symbol, off_symbol, ascii_border

View File

@ -38,16 +38,13 @@ import traceback
from . import commands from . import commands
EMPTY_STRING = u'\u200b' EMPTY_STRING = u"\u200b"
_mentions_transforms = { _mentions_transforms = {"@everyone": "@\u200beveryone", "@here": "@\u200bhere"}
'@everyone': '@\u200beveryone',
'@here': '@\u200bhere'
}
_mention_pattern = re.compile('|'.join(_mentions_transforms.keys())) _mention_pattern = re.compile("|".join(_mentions_transforms.keys()))
EmbedField = namedtuple('EmbedField', 'name value inline') EmbedField = namedtuple("EmbedField", "name value inline")
class Help(formatter.HelpFormatter): class Help(formatter.HelpFormatter):
@ -71,7 +68,7 @@ class Help(formatter.HelpFormatter):
@property @property
def avatar(self): def avatar(self):
return self.context.bot.user.avatar_url_as(format='png') return self.context.bot.user.avatar_url_as(format="png")
@property @property
def color(self): def color(self):
@ -94,48 +91,41 @@ class Help(formatter.HelpFormatter):
if self.pm_check(self.context): if self.pm_check(self.context):
name = self.context.bot.user.name name = self.context.bot.user.name
else: else:
name = self.me.display_name if not '' else self.context.bot.user.name name = self.me.display_name if not "" else self.context.bot.user.name
author = { author = {"name": "{0} Help Manual".format(name), "icon_url": self.avatar}
'name': '{0} Help Manual'.format(name),
'icon_url': self.avatar
}
return author return author
def _add_subcommands(self, cmds): def _add_subcommands(self, cmds):
entries = '' entries = ""
for name, command in cmds: for name, command in cmds:
if name in command.aliases: if name in command.aliases:
# skip aliases # skip aliases
continue continue
if self.is_cog() or self.is_bot(): if self.is_cog() or self.is_bot():
name = '{0}{1}'.format(self.clean_prefix, name) name = "{0}{1}".format(self.clean_prefix, name)
entries += '**{0}** {1}\n'.format(name, command.short_doc) entries += "**{0}** {1}\n".format(name, command.short_doc)
return entries return entries
def get_ending_note(self): def get_ending_note(self):
# command_name = self.context.invoked_with # command_name = self.context.invoked_with
return "Type {0}help <command> for more info on a command.\n" \ return "Type {0}help <command> for more info on a command.\n" "You can also type {0}help <category> for more info on a category.".format(
"You can also type {0}help <category> for more info on a category.".format(self.clean_prefix) self.clean_prefix
)
async def format(self) -> dict: async def format(self) -> dict:
"""Formats command for output. """Formats command for output.
Returns a dict used to build embed""" Returns a dict used to build embed"""
emb = { emb = {
'embed': { "embed": {"title": "", "description": ""},
'title': '', "footer": {"text": self.get_ending_note()},
'description': '', "fields": [],
},
'footer': {
'text': self.get_ending_note()
},
'fields': []
} }
if self.is_cog(): if self.is_cog():
translator = getattr(self.command, '__translator__', lambda s: s) translator = getattr(self.command, "__translator__", lambda s: s)
description = ( description = (
inspect.cleandoc(translator(self.command.__doc__)) inspect.cleandoc(translator(self.command.__doc__))
if self.command.__doc__ if self.command.__doc__
@ -144,27 +134,27 @@ class Help(formatter.HelpFormatter):
else: else:
description = self.command.description description = self.command.description
if not description == '' and description is not None: if not description == "" and description is not None:
description = '*{0}*'.format(description) description = "*{0}*".format(description)
if description: if description:
# <description> portion # <description> portion
emb['embed']['description'] = description[:2046] emb["embed"]["description"] = description[:2046]
if isinstance(self.command, discord.ext.commands.core.Command): if isinstance(self.command, discord.ext.commands.core.Command):
# <signature portion> # <signature portion>
emb['embed']['title'] = emb['embed']['description'] emb["embed"]["title"] = emb["embed"]["description"]
emb['embed']['description'] = '`Syntax: {0}`'.format(self.get_command_signature()) emb["embed"]["description"] = "`Syntax: {0}`".format(self.get_command_signature())
# <long doc> section # <long doc> section
if self.command.help: if self.command.help:
splitted = self.command.help.split('\n\n') splitted = self.command.help.split("\n\n")
name = '__{0}__'.format(splitted[0]) name = "__{0}__".format(splitted[0])
value = '\n\n'.join(splitted[1:]).replace('[p]', self.clean_prefix) value = "\n\n".join(splitted[1:]).replace("[p]", self.clean_prefix)
if value == '': if value == "":
value = EMPTY_STRING value = EMPTY_STRING
field = EmbedField(name[:252], value[:1024], False) field = EmbedField(name[:252], value[:1024], False)
emb['fields'].append(field) emb["fields"].append(field)
# end it here if it's just a regular command # end it here if it's just a regular command
if not self.has_subcommands(): if not self.has_subcommands():
@ -173,7 +163,7 @@ class Help(formatter.HelpFormatter):
def category(tup): def category(tup):
# Turn get cog (Category) name from cog/list tuples # Turn get cog (Category) name from cog/list tuples
cog = tup[1].cog_name cog = tup[1].cog_name
return '**__{0}:__**'.format(cog) if cog is not None else '**__\u200bNo Category:__**' return "**__{0}:__**".format(cog) if cog is not None else "**__\u200bNo Category:__**"
# Get subcommands for bot or category # Get subcommands for bot or category
filtered = await self.filter_command_list() filtered = await self.filter_command_list()
@ -185,18 +175,21 @@ class Help(formatter.HelpFormatter):
commands_ = sorted(commands_) commands_ = sorted(commands_)
if len(commands_) > 0: if len(commands_) > 0:
field = EmbedField(category, self._add_subcommands(commands_), False) field = EmbedField(category, self._add_subcommands(commands_), False)
emb['fields'].append(field) emb["fields"].append(field)
else: else:
# Get list of commands for category # Get list of commands for category
filtered = sorted(filtered) filtered = sorted(filtered)
if filtered: if filtered:
field = EmbedField( field = EmbedField(
'**__Commands:__**' if not self.is_bot() and self.is_cog() else '**__Subcommands:__**', "**__Commands:__**"
if not self.is_bot() and self.is_cog()
else "**__Subcommands:__**",
self._add_subcommands(filtered), # May need paginated self._add_subcommands(filtered), # May need paginated
False) False,
)
emb['fields'].append(field) emb["fields"].append(field)
return emb return emb
@ -214,7 +207,7 @@ class Help(formatter.HelpFormatter):
return ret return ret
async def format_help_for(self, ctx, command_or_bot, reason: str=None): async def format_help_for(self, ctx, command_or_bot, reason: str = None):
"""Formats the help page and handles the actual heavy lifting of how ### WTF HAPPENED? """Formats the help page and handles the actual heavy lifting of how ### WTF HAPPENED?
the help command looks like. To change the behaviour, override the the help command looks like. To change the behaviour, override the
:meth:`~.HelpFormatter.format` method. :meth:`~.HelpFormatter.format` method.
@ -237,16 +230,18 @@ class Help(formatter.HelpFormatter):
emb = await self.format() emb = await self.format()
if reason: if reason:
emb['embed']['title'] = "{0}".format(reason) emb["embed"]["title"] = "{0}".format(reason)
ret = [] ret = []
field_groups = self.group_fields(emb['fields']) field_groups = self.group_fields(emb["fields"])
for i, group in enumerate(field_groups, 1): for i, group in enumerate(field_groups, 1):
embed = discord.Embed(color=self.color, **emb['embed']) embed = discord.Embed(color=self.color, **emb["embed"])
if len(field_groups) > 1: if len(field_groups) > 1:
description = "{} *- Page {} of {}*".format(embed.description, i, len(field_groups)) description = "{} *- Page {} of {}*".format(
embed.description, i, len(field_groups)
)
embed.description = description embed.description = description
embed.set_author(**self.author) embed.set_author(**self.author)
@ -254,7 +249,7 @@ class Help(formatter.HelpFormatter):
for field in group: for field in group:
embed.add_field(**field._asdict()) embed.add_field(**field._asdict())
embed.set_footer(**emb['footer']) embed.set_footer(**emb["footer"])
ret.append(embed) ret.append(embed)
@ -275,18 +270,18 @@ class Help(formatter.HelpFormatter):
embed = self.simple_embed( embed = self.simple_embed(
ctx, ctx,
title=ctx.bot.command_not_found.format(cmd), title=ctx.bot.command_not_found.format(cmd),
description='Commands are case sensitive. Please check your spelling and try again', description="Commands are case sensitive. Please check your spelling and try again",
color=color) color=color,
)
return embed return embed
def cmd_has_no_subcommands(self, ctx, cmd, color=None): def cmd_has_no_subcommands(self, ctx, cmd, color=None):
embed = self.simple_embed( embed = self.simple_embed(
ctx, ctx, title=ctx.bot.command_has_no_subcommands.format(cmd), color=color
title=ctx.bot.command_has_no_subcommands.format(cmd),
color=color
) )
return embed return embed
@commands.command() @commands.command()
async def help(ctx, *cmds: str): async def help(ctx, *cmds: str):
"""Shows help documentation. """Shows help documentation.
@ -297,7 +292,8 @@ async def help(ctx, *cmds: str):
destination = ctx.author if ctx.bot.pm_help else ctx destination = ctx.author if ctx.bot.pm_help else ctx
def repl(obj): def repl(obj):
return _mentions_transforms.get(obj.group(0), '') return _mentions_transforms.get(obj.group(0), "")
use_embeds = await ctx.embed_requested() use_embeds = await ctx.embed_requested()
f = formatter.HelpFormatter() f = formatter.HelpFormatter()
# help by itself just lists our own commands. # help by itself just lists our own commands.
@ -316,12 +312,9 @@ async def help(ctx, *cmds: str):
command = ctx.bot.all_commands.get(name) command = ctx.bot.all_commands.get(name)
if command is None: if command is None:
if use_embeds: if use_embeds:
await destination.send( await destination.send(embed=ctx.bot.formatter.cmd_not_found(ctx, name))
embed=ctx.bot.formatter.cmd_not_found(ctx, name))
else: else:
await destination.send( await destination.send(ctx.bot.command_not_found.format(name))
ctx.bot.command_not_found.format(name)
)
return return
if use_embeds: if use_embeds:
embeds = await ctx.bot.formatter.format_help_for(ctx, command) embeds = await ctx.bot.formatter.format_help_for(ctx, command)
@ -332,12 +325,9 @@ async def help(ctx, *cmds: str):
command = ctx.bot.all_commands.get(name) command = ctx.bot.all_commands.get(name)
if command is None: if command is None:
if use_embeds: if use_embeds:
await destination.send( await destination.send(embed=ctx.bot.formatter.cmd_not_found(ctx, name))
embed=ctx.bot.formatter.cmd_not_found(ctx, name))
else: else:
await destination.send( await destination.send(ctx.bot.command_not_found.format(name))
ctx.bot.command_not_found.format(name)
)
return return
for key in cmds[1:]: for key in cmds[1:]:
@ -346,12 +336,9 @@ async def help(ctx, *cmds: str):
command = command.all_commands.get(key) command = command.all_commands.get(key)
if command is None: if command is None:
if use_embeds: if use_embeds:
await destination.send( await destination.send(embed=ctx.bot.formatter.cmd_not_found(ctx, key))
embed=ctx.bot.formatter.cmd_not_found(ctx, key))
else: else:
await destination.send( await destination.send(ctx.bot.command_not_found.format(key))
ctx.bot.command_not_found.format(key)
)
return return
except AttributeError: except AttributeError:
if use_embeds: if use_embeds:
@ -359,11 +346,11 @@ async def help(ctx, *cmds: str):
embed=ctx.bot.formatter.simple_embed( embed=ctx.bot.formatter.simple_embed(
ctx, ctx,
title='Command "{0.name}" has no subcommands.'.format(command), title='Command "{0.name}" has no subcommands.'.format(command),
color=ctx.bot.formatter.color)) color=ctx.bot.formatter.color,
else: )
await destination.send(
ctx.bot.command_has_no_subcommands.format(command)
) )
else:
await destination.send(ctx.bot.command_has_no_subcommands.format(command))
return return
if use_embeds: if use_embeds:
embeds = await ctx.bot.formatter.format_help_for(ctx, command) embeds = await ctx.bot.formatter.format_help_for(ctx, command)
@ -391,5 +378,5 @@ async def help(ctx, *cmds: str):
@help.error @help.error
async def help_error(ctx, error): async def help_error(ctx, error):
destination = ctx.author if ctx.bot.pm_help else ctx destination = ctx.author if ctx.bot.pm_help else ctx
await destination.send('{0.__name__}: {1}'.format(type(error), error)) await destination.send("{0.__name__}: {1}".format(type(error), error))
traceback.print_tb(error.original.__traceback__, file=sys.stderr) traceback.print_tb(error.original.__traceback__, file=sys.stderr)

View File

@ -3,10 +3,9 @@ from pathlib import Path
from . import commands from . import commands
__all__ = ['get_locale', 'set_locale', 'reload_locales', 'cog_i18n', __all__ = ["get_locale", "set_locale", "reload_locales", "cog_i18n", "Translator"]
'Translator']
_current_locale = 'en_us' _current_locale = "en_us"
WAITING_FOR_MSGID = 1 WAITING_FOR_MSGID = 1
IN_MSGID = 2 IN_MSGID = 2
@ -54,8 +53,8 @@ def _parse(translation_file):
if line.startswith(MSGID): if line.startswith(MSGID):
# Don't check if step is WAITING_FOR_MSGID # Don't check if step is WAITING_FOR_MSGID
untranslated = '' untranslated = ""
translated = '' translated = ""
data = line[len(MSGID):-1] data = line[len(MSGID):-1]
if len(data) == 0: # Multiline mode if len(data) == 0: # Multiline mode
step = IN_MSGID step = IN_MSGID
@ -63,10 +62,9 @@ def _parse(translation_file):
untranslated += data untranslated += data
step = WAITING_FOR_MSGSTR step = WAITING_FOR_MSGSTR
elif step is IN_MSGID and line.startswith('"') and \ elif step is IN_MSGID and line.startswith('"') and line.endswith('"'):
line.endswith('"'):
untranslated += line[1:-1] untranslated += line[1:-1]
elif step is IN_MSGID and untranslated == '': # Empty MSGID elif step is IN_MSGID and untranslated == "": # Empty MSGID
step = WAITING_FOR_MSGID step = WAITING_FOR_MSGID
elif step is IN_MSGID: # the MSGID is finished elif step is IN_MSGID: # the MSGID is finished
step = WAITING_FOR_MSGSTR step = WAITING_FOR_MSGSTR
@ -79,16 +77,15 @@ def _parse(translation_file):
translations |= {(untranslated, data)} translations |= {(untranslated, data)}
step = WAITING_FOR_MSGID step = WAITING_FOR_MSGID
elif step is IN_MSGSTR and line.startswith('"') and \ elif step is IN_MSGSTR and line.startswith('"') and line.endswith('"'):
line.endswith('"'):
translated += line[1:-1] translated += line[1:-1]
elif step is IN_MSGSTR: # the MSGSTR is finished elif step is IN_MSGSTR: # the MSGSTR is finished
step = WAITING_FOR_MSGID step = WAITING_FOR_MSGID
if translated == '': if translated == "":
translated = untranslated translated = untranslated
translations |= {(untranslated, translated)} translations |= {(untranslated, translated)}
if step is IN_MSGSTR: if step is IN_MSGSTR:
if translated == '': if translated == "":
translated = untranslated translated = untranslated
translations |= {(untranslated, translated)} translations |= {(untranslated, translated)}
return translations return translations
@ -107,33 +104,34 @@ def _normalize(string, remove_newline=False):
:param remove_newline: :param remove_newline:
:return: :return:
""" """
def normalize_whitespace(s): def normalize_whitespace(s):
"""Normalizes the whitespace in a string; \s+ becomes one space.""" """Normalizes the whitespace in a string; \s+ becomes one space."""
if not s: if not s:
return str(s) # not the same reference return str(s) # not the same reference
starts_with_space = (s[0] in ' \n\t\r') starts_with_space = (s[0] in " \n\t\r")
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(bool, newline_re.split(s)))
s = ' '.join(filter(bool, s.split('\t'))) s = " ".join(filter(bool, s.split("\t")))
s = ' '.join(filter(bool, s.split(' '))) s = " ".join(filter(bool, s.split(" ")))
if starts_with_space: if starts_with_space:
s = ' ' + s s = " " + s
if ends_with_space: if ends_with_space:
s += ' ' s += " "
return s return s
if string is None: if string is None:
return "" return ""
string = string.replace('\\n\\n', '\n\n') string = string.replace("\\n\\n", "\n\n")
string = string.replace('\\n', ' ') string = string.replace("\\n", " ")
string = string.replace('\\"', '"') string = string.replace('\\"', '"')
string = string.replace("\'", "'") string = string.replace("'", "'")
string = normalize_whitespace(string) string = normalize_whitespace(string)
string = string.strip('\n') string = string.strip("\n")
string = string.strip('\t') string = string.strip("\t")
return string return string
@ -148,7 +146,7 @@ def get_locale_path(cog_folder: Path, extension: str) -> Path:
:return: :return:
Path of possible localization file, it may not exist. Path of possible localization file, it may not exist.
""" """
return cog_folder / 'locales' / "{}.{}".format(get_locale(), extension) return cog_folder / "locales" / "{}.{}".format(get_locale(), extension)
class Translator: class Translator:
@ -193,13 +191,13 @@ class Translator:
""" """
self.translations = {} self.translations = {}
translation_file = None translation_file = None
locale_path = get_locale_path(self.cog_folder, 'po') locale_path = get_locale_path(self.cog_folder, "po")
try: try:
try: try:
translation_file = locale_path.open('ru', encoding='utf-8') translation_file = locale_path.open("ru", encoding="utf-8")
except ValueError: # We are using Windows except ValueError: # We are using Windows
translation_file = locale_path.open('r', encoding='utf-8') translation_file = locale_path.open("r", encoding="utf-8")
self._parse(translation_file) self._parse(translation_file)
except (IOError, FileNotFoundError): # The translation is unavailable except (IOError, FileNotFoundError): # The translation is unavailable
pass pass
@ -221,6 +219,7 @@ class Translator:
def cog_i18n(translator: Translator): def cog_i18n(translator: Translator):
"""Get a class decorator to link the translator to this cog.""" """Get a class decorator to link the translator to this cog."""
def decorator(cog_class: type): def decorator(cog_class: type):
cog_class.__translator__ = translator cog_class.__translator__ = translator
for name, attr in cog_class.__dict__.items(): for name, attr in cog_class.__dict__.items():
@ -228,4 +227,5 @@ def cog_i18n(translator: Translator):
attr.translator = translator attr.translator = translator
setattr(cog_class, name, attr) setattr(cog_class, name, attr)
return cog_class return cog_class
return decorator return decorator

View File

@ -11,13 +11,14 @@ from pathlib import Path
log = logging.getLogger("red") log = logging.getLogger("red")
PRETTY = {"indent": 4, "sort_keys": True, "separators": (',', ' : ')} PRETTY = {"indent": 4, "sort_keys": True, "separators": (",", " : ")}
MINIFIED = {"sort_keys": True, "separators": (',', ':')} MINIFIED = {"sort_keys": True, "separators": (",", ":")}
class JsonIO: class JsonIO:
"""Basic functions for atomic saving / loading of json files""" """Basic functions for atomic saving / loading of json files"""
def __init__(self, path: Path=Path.cwd()):
def __init__(self, path: Path = Path.cwd()):
""" """
:param path: Full path to file. :param path: Full path to file.
""" """
@ -43,7 +44,7 @@ class JsonIO:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
def _load_json(self): def _load_json(self):
log.debug("Reading file {}".format(self.path)) log.debug("Reading file {}".format(self.path))
with self.path.open(encoding='utf-8', mode="r") as f: with self.path.open(encoding="utf-8", mode="r") as f:
data = json.load(f) data = json.load(f)
return data return data

View File

@ -1,16 +1,10 @@
import subprocess import subprocess
TO_TRANSLATE = [ TO_TRANSLATE = ["../cog_manager.py", "../core_commands.py", "../dev_commands.py"]
'../cog_manager.py',
'../core_commands.py',
'../dev_commands.py'
]
def regen_messages(): def regen_messages():
subprocess.run( subprocess.run(["pygettext", "-n"] + TO_TRANSLATE)
['pygettext', '-n'] + TO_TRANSLATE
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -8,21 +8,24 @@ from redbot.core import Config
from redbot.core.bot import Red from redbot.core.bot import Red
__all__ = [ __all__ = [
"Case", "CaseType", "get_next_case_number", "get_case", "get_all_cases", "Case",
"create_case", "get_casetype", "get_all_casetypes", "register_casetype", "CaseType",
"register_casetypes", "get_modlog_channel", "set_modlog_channel", "get_next_case_number",
"reset_cases" "get_case",
"get_all_cases",
"create_case",
"get_casetype",
"get_all_casetypes",
"register_casetype",
"register_casetypes",
"get_modlog_channel",
"set_modlog_channel",
"reset_cases",
] ]
_DEFAULT_GLOBAL = { _DEFAULT_GLOBAL = {"casetypes": {}}
"casetypes": {}
}
_DEFAULT_GUILD = { _DEFAULT_GUILD = {"mod_log": None, "cases": {}, "casetypes": {}}
"mod_log": None,
"cases": {},
"casetypes": {}
}
def _register_defaults(): def _register_defaults():
@ -30,8 +33,8 @@ def _register_defaults():
_conf.register_guild(**_DEFAULT_GUILD) _conf.register_guild(**_DEFAULT_GUILD)
if not os.environ.get('BUILDING_DOCS'): if not os.environ.get("BUILDING_DOCS"):
_conf = Config.get_conf(None, 1354799444, cog_name='ModLog') _conf = Config.get_conf(None, 1354799444, cog_name="ModLog")
_register_defaults() _register_defaults()
@ -39,11 +42,20 @@ class Case:
"""A single mod log case""" """A single mod log case"""
def __init__( def __init__(
self, guild: discord.Guild, created_at: int, action_type: str, self,
user: discord.User, moderator: discord.Member, case_number: int, guild: discord.Guild,
reason: str=None, until: int=None, created_at: int,
channel: discord.TextChannel=None, amended_by: discord.Member=None, action_type: str,
modified_at: int=None, message: discord.Message=None): user: discord.User,
moderator: discord.Member,
case_number: int,
reason: str = None,
until: int = None,
channel: discord.TextChannel = None,
amended_by: discord.Member = None,
modified_at: int = None,
message: discord.Message = None,
):
self.guild = guild self.guild = guild
self.created_at = created_at self.created_at = created_at
self.action_type = action_type self.action_type = action_type
@ -82,11 +94,9 @@ class Case:
else: else:
await self.message.edit(case_content) await self.message.edit(case_content)
await _conf.guild(self.guild).cases.set_raw( await _conf.guild(self.guild).cases.set_raw(str(self.case_number), value=self.to_json())
str(self.case_number), value=self.to_json()
)
async def message_content(self, embed: bool=True): async def message_content(self, embed: bool = True):
""" """
Format a case message Format a case message
@ -102,22 +112,18 @@ class Case:
""" """
casetype = await get_casetype(self.action_type) casetype = await get_casetype(self.action_type)
title = "{}".format("Case #{} | {} {}".format( title = "{}".format(
self.case_number, casetype.case_str, casetype.image)) "Case #{} | {} {}".format(self.case_number, casetype.case_str, casetype.image)
)
if self.reason: if self.reason:
reason = "**Reason:** {}".format(self.reason) reason = "**Reason:** {}".format(self.reason)
else: else:
reason = \ reason = "**Reason:** Use `[p]reason {} <reason>` to add it".format(self.case_number)
"**Reason:** Use `[p]reason {} <reason>` to add it".format(
self.case_number
)
if self.moderator is not None: if self.moderator is not None:
moderator = "{}#{} ({})\n".format( moderator = "{}#{} ({})\n".format(
self.moderator.name, self.moderator.name, self.moderator.discriminator, self.moderator.id
self.moderator.discriminator,
self.moderator.id
) )
else: else:
moderator = "Unknown" moderator = "Unknown"
@ -126,7 +132,7 @@ class Case:
if self.until: if self.until:
start = datetime.fromtimestamp(self.created_at) start = datetime.fromtimestamp(self.created_at)
end = datetime.fromtimestamp(self.until) end = datetime.fromtimestamp(self.until)
end_fmt = end.strftime('%Y-%m-%d %H:%M:%S') end_fmt = end.strftime("%Y-%m-%d %H:%M:%S")
duration = end - start duration = end - start
dur_fmt = _strfdelta(duration) dur_fmt = _strfdelta(duration)
until = end_fmt until = end_fmt
@ -135,21 +141,16 @@ class Case:
amended_by = None amended_by = None
if self.amended_by: if self.amended_by:
amended_by = "{}#{} ({})".format( amended_by = "{}#{} ({})".format(
self.amended_by.name, self.amended_by.name, self.amended_by.discriminator, self.amended_by.id
self.amended_by.discriminator,
self.amended_by.id
) )
last_modified = None last_modified = None
if self.modified_at: if self.modified_at:
last_modified = "{}".format( last_modified = "{}".format(
datetime.fromtimestamp( datetime.fromtimestamp(self.modified_at).strftime("%Y-%m-%d %H:%M:%S")
self.modified_at
).strftime('%Y-%m-%d %H:%M:%S')
) )
user = "{}#{} ({})\n".format( user = "{}#{} ({})\n".format(self.user.name, self.user.discriminator, self.user.id)
self.user.name, self.user.discriminator, self.user.id)
if embed: if embed:
emb = discord.Embed(title=title, description=reason) emb = discord.Embed(title=title, description=reason)
@ -208,7 +209,7 @@ class Case:
"channel": self.channel.id if hasattr(self.channel, "id") else None, "channel": self.channel.id if hasattr(self.channel, "id") else None,
"amended_by": self.amended_by.id if hasattr(self.amended_by, "id") else None, "amended_by": self.amended_by.id if hasattr(self.amended_by, "id") else None,
"modified_at": self.modified_at, "modified_at": self.modified_at,
"message": self.message.id if hasattr(self.message, "id") else None "message": self.message.id if hasattr(self.message, "id") else None,
} }
return data return data
@ -239,11 +240,18 @@ class Case:
amended_by = guild.get_member(data["amended_by"]) amended_by = guild.get_member(data["amended_by"])
case_guild = bot.get_guild(data["guild"]) case_guild = bot.get_guild(data["guild"])
return cls( return cls(
guild=case_guild, created_at=data["created_at"], guild=case_guild,
action_type=data["action_type"], user=user, moderator=moderator, created_at=data["created_at"],
case_number=data["case_number"], reason=data["reason"], action_type=data["action_type"],
until=data["until"], channel=channel, amended_by=amended_by, user=user,
modified_at=data["modified_at"], message=message moderator=moderator,
case_number=data["case_number"],
reason=data["reason"],
until=data["until"],
channel=channel,
amended_by=amended_by,
modified_at=data["modified_at"],
message=message,
) )
@ -266,9 +274,16 @@ class CaseType:
The action type of the action as it would appear in the The action type of the action as it would appear in the
audit log audit log
""" """
def __init__( def __init__(
self, name: str, default_setting: bool, image: str, self,
case_str: str, audit_type: str=None, guild: discord.Guild=None): name: str,
default_setting: bool,
image: str,
case_str: str,
audit_type: str = None,
guild: discord.Guild = None,
):
self.name = name self.name = name
self.default_setting = default_setting self.default_setting = default_setting
self.image = image self.image = image
@ -282,7 +297,7 @@ class CaseType:
"default_setting": self.default_setting, "default_setting": self.default_setting,
"image": self.image, "image": self.image,
"case_str": self.case_str, "case_str": self.case_str,
"audit_type": self.audit_type "audit_type": self.audit_type,
} }
await _conf.casetypes.set_raw(self.name, value=data) await _conf.casetypes.set_raw(self.name, value=data)
@ -302,7 +317,8 @@ class CaseType:
if not self.guild: if not self.guild:
return False return False
return await _conf.guild(self.guild).casetypes.get_raw( return await _conf.guild(self.guild).casetypes.get_raw(
self.name, default=self.default_setting) self.name, default=self.default_setting
)
async def set_enabled(self, enabled: bool): async def set_enabled(self, enabled: bool):
""" """
@ -348,16 +364,11 @@ async def get_next_case_number(guild: discord.Guild) -> str:
The next case number The next case number
""" """
cases = sorted( cases = sorted((await _conf.guild(guild).get_raw("cases")), key=lambda x: int(x), reverse=True)
(await _conf.guild(guild).get_raw("cases")),
key=lambda x: int(x),
reverse=True
)
return str(int(cases[0]) + 1) if cases else "1" return str(int(cases[0]) + 1) if cases else "1"
async def get_case(case_number: int, guild: discord.Guild, async def get_case(case_number: int, guild: discord.Guild, bot: Red) -> Case:
bot: Red) -> Case:
""" """
Gets the case with the associated case number Gets the case with the associated case number
@ -384,9 +395,7 @@ async def get_case(case_number: int, guild: discord.Guild,
try: try:
case = await _conf.guild(guild).cases.get_raw(str(case_number)) case = await _conf.guild(guild).cases.get_raw(str(case_number))
except KeyError as e: except KeyError as e:
raise RuntimeError( raise RuntimeError("That case does not exist for guild {}".format(guild.name)) from e
"That case does not exist for guild {}".format(guild.name)
) from e
mod_channel = await get_modlog_channel(guild) mod_channel = await get_modlog_channel(guild)
return await Case.from_json(mod_channel, bot, case) return await Case.from_json(mod_channel, bot, case)
@ -416,11 +425,17 @@ async def get_all_cases(guild: discord.Guild, bot: Red) -> List[Case]:
return case_list return case_list
async def create_case(bot: Red, guild: discord.Guild, created_at: datetime, action_type: str, async def create_case(
user: Union[discord.User, discord.Member], bot: Red,
moderator: discord.Member=None, reason: str=None, guild: discord.Guild,
until: datetime=None, channel: discord.TextChannel=None created_at: datetime,
) -> Union[Case, None]: action_type: str,
user: Union[discord.User, discord.Member],
moderator: discord.Member = None,
reason: str = None,
until: datetime = None,
channel: discord.TextChannel = None,
) -> Union[Case, None]:
""" """
Creates a new case Creates a new case
@ -463,9 +478,7 @@ async def create_case(bot: Red, guild: discord.Guild, created_at: datetime, acti
try: try:
mod_channel = await get_modlog_channel(guild) mod_channel = await get_modlog_channel(guild)
except RuntimeError: except RuntimeError:
raise RuntimeError( raise RuntimeError("No mod log channel set for guild {}".format(guild.name))
"No mod log channel set for guild {}".format(guild.name)
)
case_type = await get_casetype(action_type, guild) case_type = await get_casetype(action_type, guild)
if case_type is None: if case_type is None:
return None return None
@ -475,9 +488,20 @@ async def create_case(bot: Red, guild: discord.Guild, created_at: datetime, acti
next_case_number = int(await get_next_case_number(guild)) next_case_number = int(await get_next_case_number(guild))
case = Case(guild, int(created_at.timestamp()), action_type, user, moderator, case = Case(
next_case_number, reason, int(until.timestamp()) if until else None, guild,
channel, amended_by=None, modified_at=None, message=None) int(created_at.timestamp()),
action_type,
user,
moderator,
next_case_number,
reason,
int(until.timestamp()) if until else None,
channel,
amended_by=None,
modified_at=None,
message=None,
)
if hasattr(mod_channel, "send"): # Not going to be the case for tests if hasattr(mod_channel, "send"): # Not going to be the case for tests
use_embeds = await bot.embed_requested(mod_channel, guild.me) use_embeds = await bot.embed_requested(mod_channel, guild.me)
case_content = await case.message_content(use_embeds) case_content = await case.message_content(use_embeds)
@ -490,7 +514,7 @@ async def create_case(bot: Red, guild: discord.Guild, created_at: datetime, acti
return case return case
async def get_casetype(name: str, guild: discord.Guild=None) -> Union[CaseType, None]: async def get_casetype(name: str, guild: discord.Guild = None) -> Union[CaseType, None]:
""" """
Gets the case type Gets the case type
@ -516,7 +540,7 @@ async def get_casetype(name: str, guild: discord.Guild=None) -> Union[CaseType,
return None return None
async def get_all_casetypes(guild: discord.Guild=None) -> List[CaseType]: async def get_all_casetypes(guild: discord.Guild = None) -> List[CaseType]:
""" """
Get all currently registered case types Get all currently registered case types
@ -538,8 +562,8 @@ async def get_all_casetypes(guild: discord.Guild=None) -> List[CaseType]:
async def register_casetype( async def register_casetype(
name: str, default_setting: bool, name: str, default_setting: bool, image: str, case_str: str, audit_type: str = None
image: str, case_str: str, audit_type: str=None) -> CaseType: ) -> CaseType:
""" """
Registers a case type. If the case type exists and Registers a case type. If the case type exists and
there are differences between the values passed and there are differences between the values passed and
@ -665,8 +689,7 @@ async def register_casetypes(new_types: List[dict]) -> List[CaseType]:
return type_list return type_list
async def get_modlog_channel(guild: discord.Guild async def get_modlog_channel(guild: discord.Guild) -> Union[discord.TextChannel, None]:
) -> Union[discord.TextChannel, None]:
""" """
Get the current modlog channel Get the current modlog channel
@ -695,8 +718,9 @@ async def get_modlog_channel(guild: discord.Guild
return channel return channel
async def set_modlog_channel(guild: discord.Guild, async def set_modlog_channel(
channel: Union[discord.TextChannel, None]) -> bool: guild: discord.Guild, channel: Union[discord.TextChannel, None]
) -> bool:
""" """
Changes the modlog channel Changes the modlog channel
@ -713,9 +737,7 @@ async def set_modlog_channel(guild: discord.Guild,
`True` if successful `True` if successful
""" """
await _conf.guild(guild).mod_log.set( await _conf.guild(guild).mod_log.set(channel.id if hasattr(channel, "id") else None)
channel.id if hasattr(channel, "id") else None
)
return True return True
@ -741,19 +763,19 @@ async def reset_cases(guild: discord.Guild) -> bool:
def _strfdelta(delta): def _strfdelta(delta):
s = [] s = []
if delta.days: if delta.days:
ds = '%i day' % delta.days ds = "%i day" % delta.days
if delta.days > 1: if delta.days > 1:
ds += 's' ds += "s"
s.append(ds) s.append(ds)
hrs, rem = divmod(delta.seconds, 60*60) hrs, rem = divmod(delta.seconds, 60 * 60)
if hrs: if hrs:
hs = '%i hr' % hrs hs = "%i hr" % hrs
if hrs > 1: if hrs > 1:
hs += 's' hs += "s"
s.append(hs) s.append(hs)
mins, secs = divmod(rem, 60) mins, secs = divmod(rem, 60)
if mins: if mins:
s.append('%i min' % mins) s.append("%i min" % mins)
if secs: if secs:
s.append('%i sec' % secs) s.append("%i sec" % secs)
return ' '.join(s) return " ".join(s)

View File

@ -10,8 +10,8 @@ from .utils import TYPE_CHECKING, NewType
if TYPE_CHECKING: if TYPE_CHECKING:
from .bot import Red from .bot import Red
log = logging.getLogger('red.rpc') log = logging.getLogger("red.rpc")
JsonSerializable = NewType('JsonSerializable', dict) JsonSerializable = NewType("JsonSerializable", dict)
_rpc = JsonRpc(logger=log) _rpc = JsonRpc(logger=log)
@ -22,13 +22,13 @@ async def initialize(bot: "Red"):
global _rpc_server global _rpc_server
app = Application(loop=bot.loop) app = Application(loop=bot.loop)
app.router.add_route('*', '/rpc', _rpc) app.router.add_route("*", "/rpc", _rpc)
handler = app.make_handler() handler = app.make_handler()
_rpc_server = await bot.loop.create_server(handler, '127.0.0.1', 6133) _rpc_server = await bot.loop.create_server(handler, "127.0.0.1", 6133)
log.debug('Created RPC _rpc_server listener.') log.debug("Created RPC _rpc_server listener.")
def add_topic(topic_name: str): def add_topic(topic_name: str):
@ -77,10 +77,7 @@ def add_method(prefix, method):
method method
MUST BE A COROUTINE OR OBJECT. MUST BE A COROUTINE OR OBJECT.
""" """
_rpc.add_methods( _rpc.add_methods(("", method), prefix=prefix)
('', method),
prefix=prefix
)
def clean_up(): def clean_up():

View File

@ -12,11 +12,13 @@ class SentryManager:
def __init__(self, logger: logging.Logger): def __init__(self, logger: logging.Logger):
self.client = Client( self.client = Client(
dsn=("https://62402161d4cd4ef18f83b16f3e22a020:9310ef55a502442598203205a84da2bb@" dsn=(
"sentry.io/253983"), "https://62402161d4cd4ef18f83b16f3e22a020:9310ef55a502442598203205a84da2bb@"
"sentry.io/253983"
),
release=__version__, release=__version__,
include_paths=['redbot'], include_paths=["redbot"],
enable_breadcrumbs=False enable_breadcrumbs=False,
) )
self.handler = SentryHandler(self.client) self.handler = SentryHandler(self.client)
self.logger = logger self.logger = logger

View File

@ -1,4 +1,4 @@
__all__ = ['TYPE_CHECKING', 'NewType', 'safe_delete'] __all__ = ["TYPE_CHECKING", "NewType", "safe_delete"]
from pathlib import Path from pathlib import Path
import os import os
@ -12,6 +12,7 @@ except ImportError:
try: try:
from typing import NewType from typing import NewType
except ImportError: except ImportError:
def NewType(name, tp): def NewType(name, tp):
return type(name, (tp,), {}) return type(name, (tp,), {})

View File

@ -3,7 +3,7 @@ from typing import Tuple, List
from collections import namedtuple from collections import namedtuple
Interval = Tuple[timedelta, int] Interval = Tuple[timedelta, int]
AntiSpamInterval = namedtuple('AntiSpamInterval', ['period', 'frequency']) AntiSpamInterval = namedtuple("AntiSpamInterval", ["period", "frequency"])
class AntiSpam: class AntiSpam:
@ -26,21 +26,18 @@ class AntiSpam:
(timedelta(seconds=5), 3), (timedelta(seconds=5), 3),
(timedelta(minutes=1), 5), (timedelta(minutes=1), 5),
(timedelta(hours=1), 10), (timedelta(hours=1), 10),
(timedelta(days=1), 24) (timedelta(days=1), 24),
] ]
def __init__(self, intervals: List[Interval]): def __init__(self, intervals: List[Interval]):
self.__event_timestamps = [] self.__event_timestamps = []
_itvs = intervals if intervals else self.default_intervals _itvs = intervals if intervals else self.default_intervals
self.__intervals = [ self.__intervals = [AntiSpamInterval(*x) for x in _itvs]
AntiSpamInterval(*x) for x in _itvs
]
self.__discard_after = max([x.period for x in self.__intervals]) self.__discard_after = max([x.period for x in self.__intervals])
def __interval_check(self, interval: AntiSpamInterval): def __interval_check(self, interval: AntiSpamInterval):
return len( return len(
[t for t in self.__event_timestamps [t for t in self.__event_timestamps if (t + interval.period) > datetime.utcnow()]
if (t + interval.period) > datetime.utcnow()]
) >= interval.frequency ) >= interval.frequency
@property @property
@ -57,6 +54,5 @@ class AntiSpam:
""" """
self.__event_timestamps.append(datetime.utcnow()) self.__event_timestamps.append(datetime.utcnow())
self.__event_timestamps = [ self.__event_timestamps = [
t for t in self.__event_timestamps t for t in self.__event_timestamps if t + self.__discard_after > datetime.utcnow()
if t + self.__discard_after > datetime.utcnow()
] ]

View File

@ -1,6 +1,7 @@
import itertools import itertools
from typing import Sequence, Iterator from typing import Sequence, Iterator
def error(text: str) -> str: def error(text: str) -> str:
"""Get text prefixed with an error emoji. """Get text prefixed with an error emoji.
@ -66,7 +67,7 @@ def bold(text: str) -> str:
return "**{}**".format(text) return "**{}**".format(text)
def box(text: str, lang: str="") -> str: def box(text: str, lang: str = "") -> str:
"""Get the given text in a code block. """Get the given text in a code block.
Parameters Parameters
@ -120,7 +121,7 @@ def italics(text: str) -> str:
return "*{}*".format(text) return "*{}*".format(text)
def bordered(*columns: Sequence[str], ascii_border: bool=False) -> str: def bordered(*columns: Sequence[str], ascii_border: bool = False) -> str:
"""Get two blocks of text in a borders. """Get two blocks of text in a borders.
Note Note
@ -141,18 +142,18 @@ def bordered(*columns: Sequence[str], ascii_border: bool=False) -> str:
""" """
borders = { borders = {
'TL': '-' if ascii_border else '', # Top-left "TL": "-" if ascii_border else "", # Top-left
'TR': '-' if ascii_border else '', # Top-right "TR": "-" if ascii_border else "", # Top-right
'BL': '-' if ascii_border else '', # Bottom-left "BL": "-" if ascii_border else "", # Bottom-left
'BR': '-' if ascii_border else '', # Bottom-right "BR": "-" if ascii_border else "", # Bottom-right
'HZ': '-' if ascii_border else '', # Horizontal "HZ": "-" if ascii_border else "", # Horizontal
'VT': '|' if ascii_border else '', # Vertical "VT": "|" if ascii_border else "", # Vertical
} }
sep = ' ' * 4 # Separator between boxes sep = " " * 4 # Separator between boxes
widths = tuple(max(len(row) for row in column) + 9 for column in columns) # width of each col widths = tuple(max(len(row) for row in column) + 9 for column in columns) # width of each col
colsdone = [False] * len(columns) # whether or not each column is done colsdone = [False] * len(columns) # whether or not each column is done
lines = [sep.join('{TL}' + '{HZ}'*width + '{TR}' for width in widths)] lines = [sep.join("{TL}" + "{HZ}" * width + "{TR}" for width in widths)]
for line in itertools.zip_longest(*columns): for line in itertools.zip_longest(*columns):
row = [] row = []
@ -162,36 +163,38 @@ def bordered(*columns: Sequence[str], ascii_border: bool=False) -> str:
if column is None: if column is None:
if not done: if not done:
# bottom border of column # bottom border of column
column = '{HZ}' * width column = "{HZ}" * width
row.append('{BL}' + column + '{BR}') row.append("{BL}" + column + "{BR}")
colsdone[colidx] = True # mark column as done colsdone[colidx] = True # mark column as done
else: else:
# leave empty # leave empty
row.append(' ' * (width + 2)) row.append(" " * (width + 2))
else: else:
column += ' ' * (width - len(column)) # append padded spaces column += " " * (width - len(column)) # append padded spaces
row.append('{VT}' + column + '{VT}') row.append("{VT}" + column + "{VT}")
lines.append(sep.join(row)) lines.append(sep.join(row))
final_row = [] final_row = []
for width, done in zip(widths, colsdone): for width, done in zip(widths, colsdone):
if not done: if not done:
final_row.append('{BL}' + '{HZ}' * width + '{BR}') final_row.append("{BL}" + "{HZ}" * width + "{BR}")
else: else:
final_row.append(' ' * (width + 2)) final_row.append(" " * (width + 2))
lines.append(sep.join(final_row)) lines.append(sep.join(final_row))
return "\n".join(lines).format(**borders) return "\n".join(lines).format(**borders)
def pagify(text: str, def pagify(
delims: Sequence[str]=["\n"], text: str,
*, delims: Sequence[str] = ["\n"],
priority: bool=False, *,
escape_mass_mentions: bool=True, priority: bool = False,
shorten_by: int=8, escape_mass_mentions: bool = True,
page_length: int=2000) -> Iterator[str]: shorten_by: int = 8,
page_length: int = 2000
) -> Iterator[str]:
"""Generate multiple pages from the given text. """Generate multiple pages from the given text.
Note Note
@ -232,10 +235,10 @@ def pagify(text: str,
while len(in_text) > page_length: while len(in_text) > page_length:
this_page_len = page_length this_page_len = page_length
if escape_mass_mentions: if escape_mass_mentions:
this_page_len -= (in_text.count("@here", 0, page_length) + this_page_len -= (
in_text.count("@everyone", 0, page_length)) in_text.count("@here", 0, page_length) + in_text.count("@everyone", 0, page_length)
closest_delim = (in_text.rfind(d, 1, this_page_len) )
for d in delims) closest_delim = (in_text.rfind(d, 1, this_page_len) for d in delims)
if priority: if priority:
closest_delim = next((x for x in closest_delim if x > 0), -1) closest_delim = next((x for x in closest_delim if x > 0), -1)
else: else:
@ -290,8 +293,7 @@ def underline(text: str) -> str:
return "__{}__".format(text) return "__{}__".format(text)
def escape(text: str, *, mass_mentions: bool=False, def escape(text: str, *, mass_mentions: bool = False, formatting: bool = False) -> str:
formatting: bool=False) -> str:
"""Get text with all mass mentions or markdown escaped. """Get text with all mass mentions or markdown escaped.
Parameters Parameters
@ -313,8 +315,7 @@ def escape(text: str, *, mass_mentions: bool=False,
text = text.replace("@everyone", "@\u200beveryone") text = text.replace("@everyone", "@\u200beveryone")
text = text.replace("@here", "@\u200bhere") text = text.replace("@here", "@\u200bhere")
if formatting: if formatting:
text = (text.replace("`", "\\`") text = (
.replace("*", "\\*") text.replace("`", "\\`").replace("*", "\\*").replace("_", "\\_").replace("~", "\\~")
.replace("_", "\\_") )
.replace("~", "\\~"))
return text return text

View File

@ -28,7 +28,7 @@ class DataConverter:
The file isn't valid JSON The file isn't valid JSON
""" """
try: try:
with file_path.open(mode='r', encoding='utf-8') as f: with file_path.open(mode="r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError): except (FileNotFoundError, json.JSONDecodeError):
raise raise

View File

@ -10,10 +10,14 @@ import discord
from redbot.core import commands from redbot.core import commands
async def menu(ctx: commands.Context, pages: list, async def menu(
controls: dict, ctx: commands.Context,
message: discord.Message=None, page: int=0, pages: list,
timeout: float=30.0): controls: dict,
message: discord.Message = None,
page: int = 0,
timeout: float = 30.0,
):
""" """
An emoji-based menu An emoji-based menu
@ -48,8 +52,10 @@ async def menu(ctx: commands.Context, pages: list,
RuntimeError RuntimeError
If either of the notes above are violated If either of the notes above are violated
""" """
if not all(isinstance(x, discord.Embed) for x in pages) and\ if (
not all(isinstance(x, str) for x in pages): not all(isinstance(x, discord.Embed) for x in pages)
and not all(isinstance(x, str) for x in pages)
):
raise RuntimeError("All pages must be of the same type") raise RuntimeError("All pages must be of the same type")
for key, value in controls.items(): for key, value in controls.items():
if not asyncio.iscoroutinefunction(value): if not asyncio.iscoroutinefunction(value):
@ -70,15 +76,10 @@ async def menu(ctx: commands.Context, pages: list,
await message.edit(content=current_page) await message.edit(content=current_page)
def react_check(r, u): def react_check(r, u):
return u == ctx.author and r.message.id == message.id and \ return u == ctx.author and r.message.id == message.id and str(r.emoji) in controls.keys()
str(r.emoji) in controls.keys()
try: try:
react, user = await ctx.bot.wait_for( react, user = await ctx.bot.wait_for("reaction_add", check=react_check, timeout=timeout)
"reaction_add",
check=react_check,
timeout=timeout
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
try: try:
await message.clear_reactions() await message.clear_reactions()
@ -87,14 +88,18 @@ async def menu(ctx: commands.Context, pages: list,
await message.remove_reaction(key, ctx.bot.user) await message.remove_reaction(key, ctx.bot.user)
return None return None
return await controls[react.emoji](ctx, pages, controls, return await controls[react.emoji](ctx, pages, controls, message, page, timeout, react.emoji)
message, page,
timeout, react.emoji)
async def next_page(ctx: commands.Context, pages: list, async def next_page(
controls: dict, message: discord.Message, page: int, ctx: commands.Context,
timeout: float, emoji: str): pages: list,
controls: dict,
message: discord.Message,
page: int,
timeout: float,
emoji: str,
):
perms = message.channel.permissions_for(ctx.guild.me) perms = message.channel.permissions_for(ctx.guild.me)
if perms.manage_messages: # Can manage messages, so remove react if perms.manage_messages: # Can manage messages, so remove react
try: try:
@ -105,13 +110,18 @@ async def next_page(ctx: commands.Context, pages: list,
page = 0 # Loop around to the first item page = 0 # Loop around to the first item
else: else:
page = page + 1 page = page + 1
return await menu(ctx, pages, controls, message=message, return await menu(ctx, pages, controls, message=message, page=page, timeout=timeout)
page=page, timeout=timeout)
async def prev_page(ctx: commands.Context, pages: list, async def prev_page(
controls: dict, message: discord.Message, page: int, ctx: commands.Context,
timeout: float, emoji: str): pages: list,
controls: dict,
message: discord.Message,
page: int,
timeout: float,
emoji: str,
):
perms = message.channel.permissions_for(ctx.guild.me) perms = message.channel.permissions_for(ctx.guild.me)
if perms.manage_messages: # Can manage messages, so remove react if perms.manage_messages: # Can manage messages, so remove react
try: try:
@ -122,20 +132,21 @@ async def prev_page(ctx: commands.Context, pages: list,
next_page = len(pages) - 1 # Loop around to the last item next_page = len(pages) - 1 # Loop around to the last item
else: else:
next_page = page - 1 next_page = page - 1
return await menu(ctx, pages, controls, message=message, return await menu(ctx, pages, controls, message=message, page=next_page, timeout=timeout)
page=next_page, timeout=timeout)
async def close_menu(ctx: commands.Context, pages: list, async def close_menu(
controls: dict, message: discord.Message, page: int, ctx: commands.Context,
timeout: float, emoji: str): pages: list,
controls: dict,
message: discord.Message,
page: int,
timeout: float,
emoji: str,
):
if message: if message:
await message.delete() await message.delete()
return None return None
DEFAULT_CONTROLS = { DEFAULT_CONTROLS = {"": prev_page, "": close_menu, "": next_page}
"": prev_page,
"": close_menu,
"": next_page
}

View File

@ -8,8 +8,7 @@ from redbot.core import Config
from redbot.core.bot import Red from redbot.core.bot import Red
async def mass_purge(messages: List[discord.Message], async def mass_purge(messages: List[discord.Message], channel: discord.TextChannel):
channel: discord.TextChannel):
"""Bulk delete messages from a channel. """Bulk delete messages from a channel.
If more than 100 messages are supplied, the bot will delete 100 messages at If more than 100 messages are supplied, the bot will delete 100 messages at
@ -80,24 +79,23 @@ def get_audit_reason(author: discord.Member, reason: str = None):
The formatted audit log reason. The formatted audit log reason.
""" """
return \ return "Action requested by {} (ID {}). Reason: {}".format(
"Action requested by {} (ID {}). Reason: {}".format(author, author.id, reason) if reason else \ author, author.id, reason
"Action requested by {} (ID {}).".format(author, author.id) ) if reason else "Action requested by {} (ID {}).".format(
author, author.id
)
async def is_allowed_by_hierarchy(bot: Red, async def is_allowed_by_hierarchy(
settings: Config, bot: Red, settings: Config, guild: discord.Guild, mod: discord.Member, user: discord.Member
guild: discord.Guild, ):
mod: discord.Member,
user: discord.Member):
if not await settings.guild(guild).respect_hierarchy(): if not await settings.guild(guild).respect_hierarchy():
return True return True
is_special = mod == guild.owner or await bot.is_owner(mod) is_special = mod == guild.owner or await bot.is_owner(mod)
return mod.top_role.position > user.top_role.position or is_special return mod.top_role.position > user.top_role.position or is_special
async def is_mod_or_superior( async def is_mod_or_superior(bot: Red, obj: Union[discord.Message, discord.Member, discord.Role]):
bot: Red, obj: Union[discord.Message, discord.Member, discord.Role]):
"""Check if an object has mod or superior permissions. """Check if an object has mod or superior permissions.
If a message is passed, its author's permissions are checked. If a role is If a message is passed, its author's permissions are checked. If a role is
@ -129,7 +127,7 @@ async def is_mod_or_superior(
elif isinstance(obj, discord.Role): elif isinstance(obj, discord.Role):
pass pass
else: else:
raise TypeError('Only messages, members or roles may be passed') raise TypeError("Only messages, members or roles may be passed")
server = obj.guild server = obj.guild
admin_role_id = await bot.db.guild(server).admin_role() admin_role_id = await bot.db.guild(server).admin_role()
@ -168,26 +166,27 @@ def strfdelta(delta: timedelta):
""" """
s = [] s = []
if delta.days: if delta.days:
ds = '%i day' % delta.days ds = "%i day" % delta.days
if delta.days > 1: if delta.days > 1:
ds += 's' ds += "s"
s.append(ds) s.append(ds)
hrs, rem = divmod(delta.seconds, 60*60) hrs, rem = divmod(delta.seconds, 60 * 60)
if hrs: if hrs:
hs = '%i hr' % hrs hs = "%i hr" % hrs
if hrs > 1: if hrs > 1:
hs += 's' hs += "s"
s.append(hs) s.append(hs)
mins, secs = divmod(rem, 60) mins, secs = divmod(rem, 60)
if mins: if mins:
s.append('%i min' % mins) s.append("%i min" % mins)
if secs: if secs:
s.append('%i sec' % secs) s.append("%i sec" % secs)
return ' '.join(s) return " ".join(s)
async def is_admin_or_superior( async def is_admin_or_superior(
bot: Red, obj: Union[discord.Message, discord.Member, discord.Role]): bot: Red, obj: Union[discord.Message, discord.Member, discord.Role]
):
"""Same as `is_mod_or_superior` except for admin permissions. """Same as `is_mod_or_superior` except for admin permissions.
If a message is passed, its author's permissions are checked. If a role is If a message is passed, its author's permissions are checked. If a role is
@ -219,7 +218,7 @@ async def is_admin_or_superior(
elif isinstance(obj, discord.Role): elif isinstance(obj, discord.Role):
pass pass
else: else:
raise TypeError('Only messages, members or roles may be passed') raise TypeError("Only messages, members or roles may be passed")
server = obj.guild server = obj.guild
admin_role_id = await bot.db.guild(server).admin_role() admin_role_id = await bot.db.guild(server).admin_role()

View File

@ -16,10 +16,7 @@ class TunnelMeta(type):
""" """
def __call__(cls, *args, **kwargs): def __call__(cls, *args, **kwargs):
lockout_tuple = ( lockout_tuple = ((kwargs.get("sender"), kwargs.get("origin")), kwargs.get("recipient"))
(kwargs.get('sender'), kwargs.get('origin')),
kwargs.get('recipient')
)
if lockout_tuple in _instances: if lockout_tuple in _instances:
return _instances[lockout_tuple] return _instances[lockout_tuple]
@ -30,13 +27,8 @@ class TunnelMeta(type):
while True: while True:
try: try:
if not ( if not (
any( any(lockout_tuple[0] == x[0] for x in _instances.keys())
lockout_tuple[0] == x[0] or any(lockout_tuple[1] == x[1] for x in _instances.keys())
for x in _instances.keys()
) or any(
lockout_tuple[1] == x[1]
for x in _instances.keys()
)
): ):
# if this isn't temporarily stored, the weakref dict # if this isn't temporarily stored, the weakref dict
# will discard this before the return statement, # will discard this before the return statement,
@ -70,10 +62,9 @@ class Tunnel(metaclass=TunnelMeta):
The user on the other end of the tunnel The user on the other end of the tunnel
""" """
def __init__(self, *, def __init__(
sender: discord.Member, self, *, sender: discord.Member, origin: discord.TextChannel, recipient: discord.User
origin: discord.TextChannel, ):
recipient: discord.User):
self.sender = sender self.sender = sender
self.origin = origin self.origin = origin
self.recipient = recipient self.recipient = recipient
@ -81,11 +72,8 @@ class Tunnel(metaclass=TunnelMeta):
async def react_close(self, *, uid: int, message: str): async def react_close(self, *, uid: int, message: str):
send_to = self.origin if uid == self.sender.id else self.sender send_to = self.origin if uid == self.sender.id else self.sender
closer = next(filter( closer = next(filter(lambda x: x.id == uid, (self.sender, self.recipient)), None)
lambda x: x.id == uid, (self.sender, self.recipient)), None) await send_to.send(message.format(closer=closer))
await send_to.send(
message.format(closer=closer)
)
@property @property
def members(self): def members(self):
@ -97,8 +85,8 @@ class Tunnel(metaclass=TunnelMeta):
@staticmethod @staticmethod
async def message_forwarder( async def message_forwarder(
*, destination: discord.abc.Messageable, *, destination: discord.abc.Messageable, content: str = None, embed=None, files=[]
content: str=None, embed=None, files=[]) -> 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
if you are using command initiated reactions instead of persistent if you are using command initiated reactions instead of persistent
@ -131,18 +119,13 @@ class Tunnel(metaclass=TunnelMeta):
files = files if files else None files = files if files else None
if content: if content:
for page in pagify(content): for page in pagify(content):
rets.append( rets.append(await destination.send(page, files=files, embed=embed))
await destination.send(
page, files=files, embed=embed)
)
if files: if files:
del files del files
if embed: if embed:
del embed del embed
elif embed or files: elif embed or files:
rets.append( rets.append(await destination.send(files=files, embed=embed))
await destination.send(files=files, embed=embed)
)
return rets return rets
@staticmethod @staticmethod
@ -172,15 +155,12 @@ class Tunnel(metaclass=TunnelMeta):
size += sys.getsizeof(_fp) size += sys.getsizeof(_fp)
if size > max_size: if size > max_size:
return [] return []
files.append( files.append(discord.File(_fp, filename=a.filename))
discord.File(_fp, filename=a.filename)
)
return files return files
async def communicate(self, *, async def communicate(
message: discord.Message, self, *, message: discord.Message, topic: str = None, skip_message_content: bool = False
topic: str=None, ):
skip_message_content: bool=False):
""" """
Forwards a message. Forwards a message.
@ -208,18 +188,15 @@ class Tunnel(metaclass=TunnelMeta):
the bot can't upload at the origin channel the bot can't upload at the origin channel
or can't add reactions there. or can't add reactions there.
""" """
if message.channel == self.origin \ if message.channel == self.origin and message.author == self.sender:
and message.author == self.sender:
send_to = self.recipient send_to = self.recipient
elif message.author == self.recipient \ elif message.author == self.recipient and isinstance(message.channel, discord.DMChannel):
and isinstance(message.channel, discord.DMChannel):
send_to = self.origin send_to = self.origin
else: else:
return None return None
if not skip_message_content: if not skip_message_content:
content = "\n".join((topic, message.content)) if topic \ content = "\n".join((topic, message.content)) if topic else message.content
else message.content
else: else:
content = topic content = topic
@ -234,11 +211,7 @@ class Tunnel(metaclass=TunnelMeta):
else: else:
attach = [] attach = []
rets = await self.message_forwarder( rets = await self.message_forwarder(destination=send_to, content=content, files=attach)
destination=send_to,
content=content,
files=attach
)
await message.add_reaction("\N{WHITE HEAVY CHECK MARK}") await message.add_reaction("\N{WHITE HEAVY CHECK MARK}")
await message.add_reaction("\N{NEGATIVE SQUARED CROSS MARK}") await message.add_reaction("\N{NEGATIVE SQUARED CROSS MARK}")

View File

@ -8,7 +8,14 @@ import asyncio
import pkg_resources import pkg_resources
from pathlib import Path from pathlib import Path
from redbot.setup import basic_setup, load_existing_config, remove_instance, remove_instance_interaction, create_backup, save_config from redbot.setup import (
basic_setup,
load_existing_config,
remove_instance,
remove_instance_interaction,
create_backup,
save_config,
)
from redbot.core.utils import safe_delete from redbot.core.utils import safe_delete
from redbot.core.cli import confirm from redbot.core.cli import confirm
@ -18,9 +25,9 @@ if sys.platform == "linux":
PYTHON_OK = sys.version_info >= (3, 5) PYTHON_OK = sys.version_info >= (3, 5)
INTERACTIVE_MODE = not len(sys.argv) > 1 # CLI flags = non-interactive INTERACTIVE_MODE = not len(sys.argv) > 1 # CLI flags = non-interactive
INTRO = ("==========================\n" INTRO = (
"Red Discord Bot - Launcher\n" "==========================\n" "Red Discord Bot - Launcher\n" "==========================\n"
"==========================\n") )
IS_WINDOWS = os.name == "nt" IS_WINDOWS = os.name == "nt"
IS_MAC = sys.platform == "darwin" IS_MAC = sys.platform == "darwin"
@ -31,35 +38,35 @@ def parse_cli_args():
description="Red - Discord Bot's launcher (V3)", allow_abbrev=False description="Red - Discord Bot's launcher (V3)", allow_abbrev=False
) )
instances = load_existing_config() instances = load_existing_config()
parser.add_argument("instancename", metavar="instancename", type=str, parser.add_argument(
nargs="?", help="The instance to run", choices=list(instances.keys())) "instancename",
parser.add_argument("--start", "-s", metavar="instancename",
help="Starts Red", type=str,
action="store_true") nargs="?",
parser.add_argument("--auto-restart", help="The instance to run",
help="Autorestarts Red in case of issues", choices=list(instances.keys()),
action="store_true") )
parser.add_argument("--update", parser.add_argument("--start", "-s", help="Starts Red", action="store_true")
help="Updates Red", parser.add_argument(
action="store_true") "--auto-restart", help="Autorestarts Red in case of issues", action="store_true"
parser.add_argument("--update-dev", )
help="Updates Red from the Github repo", parser.add_argument("--update", help="Updates Red", action="store_true")
action="store_true") parser.add_argument(
parser.add_argument("--voice", "--update-dev", help="Updates Red from the Github repo", action="store_true"
help="Installs extra 'voice' when updating", )
action="store_true") parser.add_argument(
parser.add_argument("--docs", "--voice", help="Installs extra 'voice' when updating", action="store_true"
help="Installs extra 'docs' when updating", )
action="store_true") parser.add_argument("--docs", help="Installs extra 'docs' when updating", action="store_true")
parser.add_argument("--test", parser.add_argument("--test", help="Installs extra 'test' when updating", action="store_true")
help="Installs extra 'test' when updating", parser.add_argument(
action="store_true") "--mongo", help="Installs extra 'mongo' when updating", action="store_true"
parser.add_argument("--mongo", )
help="Installs extra 'mongo' when updating", parser.add_argument(
action="store_true") "--debuginfo",
parser.add_argument("--debuginfo", help="Prints basic debug info that would be useful for support",
help="Prints basic debug info that would be useful for support", action="store_true",
action="store_true") )
return parser.parse_known_args() return parser.parse_known_args()
@ -97,20 +104,24 @@ def update_red(dev=False, reinstall=False, voice=False, mongo=False, docs=False,
if egg_l: if egg_l:
package += "[{}]".format(", ".join(egg_l)) package += "[{}]".format(", ".join(egg_l))
if reinstall: if reinstall:
code = subprocess.call([ code = subprocess.call(
interpreter, "-m", [
"pip", "install", "-U", "-I", interpreter,
"--force-reinstall", "--no-cache-dir", "-m",
"--process-dependency-links", "pip",
package "install",
]) "-U",
"-I",
"--force-reinstall",
"--no-cache-dir",
"--process-dependency-links",
package,
]
)
else: else:
code = subprocess.call([ code = subprocess.call(
interpreter, "-m", [interpreter, "-m", "pip", "install", "-U", "--process-dependency-links", package]
"pip", "install", "-U", )
"--process-dependency-links",
package
])
if code == 0: if code == 0:
print("Red has been updated") print("Red has been updated")
else: else:
@ -123,7 +134,7 @@ def update_red(dev=False, reinstall=False, voice=False, mongo=False, docs=False,
os.rename(new_name, old_name) os.rename(new_name, old_name)
def run_red(selected_instance, autorestart: bool=False, cliflags=None): def run_red(selected_instance, autorestart: bool = False, cliflags=None):
while True: while True:
print("Starting {}...".format(selected_instance)) print("Starting {}...".format(selected_instance))
cmd_list = ["redbot", selected_instance] cmd_list = ["redbot", selected_instance]
@ -153,12 +164,15 @@ def cli_flag_getter():
if choice == "y": if choice == "y":
print( print(
"Enter the prefixes, separated by a space (please note " "Enter the prefixes, separated by a space (please note "
"that prefixes containing a space will need to be added with [p]set prefix)") "that prefixes containing a space will need to be added with [p]set prefix)"
)
prefixes = user_choice().split() prefixes = user_choice().split()
for p in prefixes: for p in prefixes:
flags.append("-p {}".format(p)) flags.append("-p {}".format(p))
print("Would you like to disable console input? Please note that features " print(
"requiring console interaction may fail to work (y/n)") "Would you like to disable console input? Please note that features "
"requiring console interaction may fail to work (y/n)"
)
choice = user_choice() choice = user_choice()
if choice == "y": if choice == "y":
flags.append("--no-prompt") flags.append("--no-prompt")
@ -169,9 +183,11 @@ def cli_flag_getter():
print("Is this a selfbot? (y/n)") print("Is this a selfbot? (y/n)")
choice = user_choice() choice = user_choice()
if choice == "y": if choice == "y":
print("Please note that selfbots are not allowed by Discord. See" print(
"https://support.discordapp.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-" "Please note that selfbots are not allowed by Discord. See"
"for more information.") "https://support.discordapp.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-"
"for more information."
)
flags.append("--self-bot") flags.append("--self-bot")
print("Does this token belong to a user account rather than a bot account? (y/n)") print("Does this token belong to a user account rather than a bot account? (y/n)")
choice = user_choice() choice = user_choice()
@ -185,7 +201,9 @@ def cli_flag_getter():
choice = user_choice() choice = user_choice()
if choice == "y": if choice == "y":
flags.append("--debug") flags.append("--debug")
print("Do you want the Dev cog loaded (thus enabling commands such as debug and repl)? (y/n)") print(
"Do you want the Dev cog loaded (thus enabling commands such as debug and repl)? (y/n)"
)
choice = user_choice() choice = user_choice()
if choice == "y": if choice == "y":
flags.append("--dev") flags.append("--dev")
@ -218,8 +236,8 @@ def instance_menu():
name_num_map = {} name_num_map = {}
for name in list(instances.keys()): for name in list(instances.keys()):
print("{}. {}\n".format(counter+1, name)) print("{}. {}\n".format(counter + 1, name))
name_num_map[str(counter+1)] = name name_num_map[str(counter + 1)] = name
counter += 1 counter += 1
while True: while True:
@ -229,7 +247,7 @@ def instance_menu():
except ValueError: except ValueError:
print("Invalid input! Please enter a number corresponding to an instance.") print("Invalid input! Please enter a number corresponding to an instance.")
else: else:
if selection not in list(range(1, counter+1)): if selection not in list(range(1, counter + 1)):
print("Invalid selection! Please try again") print("Invalid selection! Please try again")
else: else:
return name_num_map[str(selection)] return name_num_map[str(selection)]
@ -242,8 +260,10 @@ async def reset_red():
print("No instance to delete.\n") print("No instance to delete.\n")
return return
print("WARNING: You are about to remove ALL Red instances on this computer.") print("WARNING: You are about to remove ALL Red instances on this computer.")
print("If you want to reset data of only one instance, " print(
"please select option 5 in the launcher.") "If you want to reset data of only one instance, "
"please select option 5 in the launcher."
)
await asyncio.sleep(2) await asyncio.sleep(2)
print("\nIf you continue you will remove these instanes.\n") print("\nIf you continue you will remove these instanes.\n")
for instance in list(instances.keys()): for instance in list(instances.keys()):
@ -290,7 +310,7 @@ def extras_selector():
return selected return selected
def development_choice(reinstall = False): def development_choice(reinstall=False):
while True: while True:
print("\n") print("\n")
print("Do you want to install stable or development version?") print("Do you want to install stable or development version?")
@ -301,18 +321,22 @@ def development_choice(reinstall = False):
selected = extras_selector() selected = extras_selector()
if choice == "1": if choice == "1":
update_red( update_red(
dev=False, reinstall=reinstall, voice=True if "voice" in selected else False, dev=False,
reinstall=reinstall,
voice=True if "voice" in selected else False,
docs=True if "docs" in selected else False, docs=True if "docs" in selected else False,
test=True if "test" in selected else False, test=True if "test" in selected else False,
mongo=True if "mongo" in selected else False mongo=True if "mongo" in selected else False,
) )
break break
elif choice == "2": elif choice == "2":
update_red( update_red(
dev=True, reinstall=reinstall, voice=True if "voice" in selected else False, dev=True,
reinstall=reinstall,
voice=True if "voice" in selected else False,
docs=True if "docs" in selected else False, docs=True if "docs" in selected else False,
test=True if "test" in selected else False, test=True if "test" in selected else False,
mongo=True if "mongo" in selected else False mongo=True if "mongo" in selected else False,
) )
break break
@ -332,12 +356,17 @@ def debug_info():
os_info = distro.linux_distribution() os_info = distro.linux_distribution()
osver = "{} {}".format(os_info[0], os_info[1]).strip() osver = "{} {}".format(os_info[0], os_info[1]).strip()
user_who_ran = getpass.getuser() user_who_ran = getpass.getuser()
info = "Debug Info for Red\n\n" +\ info = "Debug Info for Red\n\n" + "Python version: {}\n".format(
"Python version: {}\n".format(pyver) +\ pyver
"Red version: {}\n".format(redver) +\ ) + "Red version: {}\n".format(
"OS version: {}\n".format(osver) +\ redver
"System arch: {}\n".format(platform.machine()) +\ ) + "OS version: {}\n".format(
"User: {}\n".format(user_who_ran) osver
) + "System arch: {}\n".format(
platform.machine()
) + "User: {}\n".format(
user_who_ran
)
print(info) print(info)
exit(0) exit(0)
@ -385,7 +414,9 @@ def main_menu():
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
clear_screen() clear_screen()
print("==== Reinstall Red ====") print("==== Reinstall Red ====")
print("1. Reinstall Red requirements (discard code changes, keep data and 3rd party cogs)") print(
"1. Reinstall Red requirements (discard code changes, keep data and 3rd party cogs)"
)
print("2. Reset all data") print("2. Reset all data")
print("3. Factory reset (discard code changes, reset all data)") print("3. Factory reset (discard code changes, reset all data)")
print("\n") print("\n")
@ -411,8 +442,7 @@ def main_menu():
def main(): def main():
if not PYTHON_OK: if not PYTHON_OK:
raise RuntimeError( raise RuntimeError(
"Red requires Python 3.5 or greater. " "Red requires Python 3.5 or greater. " "Please install the correct version!"
"Please install the correct version!"
) )
if args.debuginfo: # Check first since the function triggers an exit if args.debuginfo: # Check first since the function triggers an exit
debug_info() debug_info()
@ -423,15 +453,9 @@ def main():
"Please try again using only one of --update or --update-dev" "Please try again using only one of --update or --update-dev"
) )
if args.update: if args.update:
update_red( update_red(voice=args.voice, docs=args.docs, test=args.test, mongo=args.mongo)
voice=args.voice, docs=args.docs,
test=args.test, mongo=args.mongo
)
elif args.update_dev: elif args.update_dev:
update_red( update_red(dev=True, voice=args.voice, docs=args.docs, test=args.test, mongo=args.mongo)
dev=True, voice=args.voice, docs=args.docs,
test=args.test, mongo=args.mongo
)
if INTERACTIVE_MODE: if INTERACTIVE_MODE:
main_menu() main_menu()

View File

@ -20,7 +20,7 @@ from redbot.core.drivers.red_json import JSON
config_dir = None config_dir = None
appdir = appdirs.AppDirs("Red-DiscordBot") appdir = appdirs.AppDirs("Red-DiscordBot")
if sys.platform == 'linux': if sys.platform == "linux":
if 0 < os.getuid() < 1000: if 0 < os.getuid() < 1000:
config_dir = Path(appdir.site_data_dir) config_dir = Path(appdir.site_data_dir)
if not config_dir: if not config_dir:
@ -28,27 +28,17 @@ if not config_dir:
try: try:
config_dir.mkdir(parents=True, exist_ok=True) config_dir.mkdir(parents=True, exist_ok=True)
except PermissionError: except PermissionError:
print( print("You don't have permission to write to " "'{}'\nExiting...".format(config_dir))
"You don't have permission to write to "
"'{}'\nExiting...".format(config_dir))
sys.exit(1) sys.exit(1)
config_file = config_dir / 'config.json' config_file = config_dir / "config.json"
def parse_cli_args(): def parse_cli_args():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(description="Red - Discord Bot's instance manager (V3)")
description="Red - Discord Bot's instance manager (V3)"
)
parser.add_argument( parser.add_argument(
"--delete", "-d", "--delete", "-d", help="Interactively delete an instance", action="store_true"
help="Interactively delete an instance",
action="store_true"
)
parser.add_argument(
"--edit", "-e",
help="Interactively edit an instance",
action="store_true"
) )
parser.add_argument("--edit", "-e", help="Interactively edit an instance", action="store_true")
return parser.parse_known_args() return parser.parse_known_args()
@ -79,18 +69,20 @@ def save_config(name, data, remove=False):
def get_data_dir(): def get_data_dir():
default_data_dir = Path(appdir.user_data_dir) default_data_dir = Path(appdir.user_data_dir)
print("Hello! Before we begin the full configuration process we need to" print(
" gather some initial information about where you'd like us" "Hello! Before we begin the full configuration process we need to"
" to store your bot's data. We've attempted to figure out a" " gather some initial information about where you'd like us"
" sane default data location which is printed below. If you don't" " to store your bot's data. We've attempted to figure out a"
" want to change this default please press [ENTER], otherwise" " sane default data location which is printed below. If you don't"
" input your desired data location.") " want to change this default please press [ENTER], otherwise"
" input your desired data location."
)
print() print()
print("Default: {}".format(default_data_dir)) print("Default: {}".format(default_data_dir))
new_path = input('> ') new_path = input("> ")
if new_path != '': if new_path != "":
new_path = Path(new_path) new_path = Path(new_path)
default_data_dir = new_path default_data_dir = new_path
@ -98,13 +90,14 @@ def get_data_dir():
try: try:
default_data_dir.mkdir(parents=True, exist_ok=True) default_data_dir.mkdir(parents=True, exist_ok=True)
except OSError: except OSError:
print("We were unable to create your chosen directory." print(
" You may need to restart this process with admin" "We were unable to create your chosen directory."
" privileges.") " You may need to restart this process with admin"
" privileges."
)
sys.exit(1) sys.exit(1)
print("You have chosen {} to be your data directory." print("You have chosen {} to be your data directory." "".format(default_data_dir))
"".format(default_data_dir))
if not confirm("Please confirm (y/n):"): if not confirm("Please confirm (y/n):"):
print("Please start the process over.") print("Please start the process over.")
sys.exit(0) sys.exit(0)
@ -112,10 +105,7 @@ def get_data_dir():
def get_storage_type(): def get_storage_type():
storage_dict = { storage_dict = {1: "JSON", 2: "MongoDB"}
1: "JSON",
2: "MongoDB"
}
storage = None storage = None
while storage is None: while storage is None:
print() print()
@ -137,8 +127,10 @@ def get_name():
name = "" name = ""
while len(name) == 0: while len(name) == 0:
print() print()
print("Please enter a name for your instance, this name cannot include spaces" print(
" and it will be used to run your bot from here on out.") "Please enter a name for your instance, this name cannot include spaces"
" and it will be used to run your bot from here on out."
)
name = input("> ") name = input("> ")
if " " in name: if " " in name:
name = "" name = ""
@ -154,41 +146,40 @@ def basic_setup():
default_data_dir = get_data_dir() default_data_dir = get_data_dir()
default_dirs = deepcopy(basic_config_default) default_dirs = deepcopy(basic_config_default)
default_dirs['DATA_PATH'] = str(default_data_dir.resolve()) default_dirs["DATA_PATH"] = str(default_data_dir.resolve())
storage = get_storage_type() storage = get_storage_type()
storage_dict = { storage_dict = {1: "JSON", 2: "MongoDB"}
1: "JSON", default_dirs["STORAGE_TYPE"] = storage_dict.get(storage, 1)
2: "MongoDB"
}
default_dirs['STORAGE_TYPE'] = storage_dict.get(storage, 1)
if storage_dict.get(storage, 1) == "MongoDB": if storage_dict.get(storage, 1) == "MongoDB":
from redbot.core.drivers.red_mongo import get_config_details from redbot.core.drivers.red_mongo import get_config_details
default_dirs['STORAGE_DETAILS'] = get_config_details()
default_dirs["STORAGE_DETAILS"] = get_config_details()
else: else:
default_dirs['STORAGE_DETAILS'] = {} default_dirs["STORAGE_DETAILS"] = {}
name = get_name() name = get_name()
save_config(name, default_dirs) save_config(name, default_dirs)
print() print()
print("Your basic configuration has been saved. Please run `redbot <name>` to" print(
" continue your setup process and to run the bot.") "Your basic configuration has been saved. Please run `redbot <name>` to"
" continue your setup process and to run the bot."
)
async def json_to_mongo(current_data_dir: Path, storage_details: dict): async def json_to_mongo(current_data_dir: Path, storage_details: dict):
from redbot.core.drivers.red_mongo import Mongo from redbot.core.drivers.red_mongo import Mongo
core_data_file = list(current_data_dir.glob("core/settings.json"))[0] core_data_file = list(current_data_dir.glob("core/settings.json"))[0]
m = Mongo("Core", "0", **storage_details) m = Mongo("Core", "0", **storage_details)
with core_data_file.open(mode="r") as f: with core_data_file.open(mode="r") as f:
core_data = json.loads(f.read()) core_data = json.loads(f.read())
collection = m.get_collection() collection = m.get_collection()
await collection.update_one( await collection.update_one(
{'_id': m.unique_cog_identifier}, {"_id": m.unique_cog_identifier}, update={"$set": core_data["0"]}, upsert=True
update={"$set": core_data["0"]},
upsert=True
) )
for p in current_data_dir.glob("cogs/**/settings.json"): for p in current_data_dir.glob("cogs/**/settings.json"):
with p.open(mode="r") as f: with p.open(mode="r") as f:
@ -200,14 +191,13 @@ async def json_to_mongo(current_data_dir: Path, storage_details: dict):
cog_c = cog_m.get_collection() cog_c = cog_m.get_collection()
for ident in list(cog_data.keys()): for ident in list(cog_data.keys()):
await cog_c.update_one( await cog_c.update_one(
{"_id": cog_m.unique_cog_identifier}, {"_id": cog_m.unique_cog_identifier}, update={"$set": cog_data[cog_i]}, upsert=True
update={"$set": cog_data[cog_i]},
upsert=True
) )
async def mongo_to_json(current_data_dir: Path, storage_details: dict): async def mongo_to_json(current_data_dir: Path, storage_details: dict):
from redbot.core.drivers.red_mongo import Mongo from redbot.core.drivers.red_mongo import Mongo
m = Mongo("Core", "0", **storage_details) m = Mongo("Core", "0", **storage_details)
db = m.db db = m.db
collection_names = await db.collection_names(include_system_collections=False) collection_names = await db.collection_names(include_system_collections=False)
@ -250,9 +240,7 @@ async def edit_instance():
default_dirs = deepcopy(basic_config_default) default_dirs = deepcopy(basic_config_default)
current_data_dir = Path(instance_data["DATA_PATH"]) current_data_dir = Path(instance_data["DATA_PATH"])
print( print("You have selected '{}' as the instance to modify.".format(selected))
"You have selected '{}' as the instance to modify.".format(selected)
)
if not confirm("Please confirm (y/n):"): if not confirm("Please confirm (y/n):"):
print("Ok, we will not continue then.") print("Ok, we will not continue then.")
return return
@ -273,13 +261,11 @@ async def edit_instance():
if confirm("Would you like to change the storage type? (y/n):"): if confirm("Would you like to change the storage type? (y/n):"):
storage = get_storage_type() storage = get_storage_type()
storage_dict = { storage_dict = {1: "JSON", 2: "MongoDB"}
1: "JSON",
2: "MongoDB"
}
default_dirs["STORAGE_TYPE"] = storage_dict[storage] default_dirs["STORAGE_TYPE"] = storage_dict[storage]
if storage_dict.get(storage, 1) == "MongoDB": if storage_dict.get(storage, 1) == "MongoDB":
from redbot.core.drivers.red_mongo import get_config_details from redbot.core.drivers.red_mongo import get_config_details
storage_details = get_config_details() storage_details = get_config_details()
default_dirs["STORAGE_DETAILS"] = storage_details default_dirs["STORAGE_DETAILS"] = storage_details
@ -297,9 +283,7 @@ async def edit_instance():
save_config(selected, {}, remove=True) save_config(selected, {}, remove=True)
save_config(name, default_dirs) save_config(name, default_dirs)
print( print("Your basic configuration has been edited")
"Your basic configuration has been edited"
)
async def create_backup(selected, instance_data): async def create_backup(selected, instance_data):
@ -317,9 +301,7 @@ async def create_backup(selected, instance_data):
os.chdir(str(pth.parent)) os.chdir(str(pth.parent))
with tarfile.open(str(backup_file), "w:gz") as tar: with tarfile.open(str(backup_file), "w:gz") as tar:
tar.add(pth.stem) tar.add(pth.stem)
print("A backup of {} has been made. It is at {}".format( print("A backup of {} has been made. It is at {}".format(selected, backup_file))
selected, backup_file
))
else: else:
print("Backing up the instance's data...") print("Backing up the instance's data...")
@ -333,11 +315,7 @@ async def create_backup(selected, instance_data):
os.chdir(str(pth.parent)) # str is used here because 3.5 support os.chdir(str(pth.parent)) # str is used here because 3.5 support
with tarfile.open(str(backup_file), "w:gz") as tar: with tarfile.open(str(backup_file), "w:gz") as tar:
tar.add(pth.stem) # add all files in that directory tar.add(pth.stem) # add all files in that directory
print( print("A backup of {} has been made. It is at {}".format(selected, backup_file))
"A backup of {} has been made. It is at {}".format(
selected, backup_file
)
)
async def remove_instance(selected, instance_data): async def remove_instance(selected, instance_data):
@ -390,6 +368,7 @@ def main():
else: else:
basic_setup() basic_setup()
args, _ = parse_cli_args() args, _ = parse_cli_args()
if __name__ == "__main__": if __name__ == "__main__":