[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
: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
********************

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ from discord.ext import commands
class MemberDefaultAuthor(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> discord.Member:
member_converter = commands.MemberConverter()
try:
@ -16,6 +17,7 @@ class MemberDefaultAuthor(commands.Converter):
class SelfRole(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> discord.Role:
admin = ctx.command.instance
if admin is None:
@ -28,6 +30,5 @@ class SelfRole(commands.Converter):
role = await role_converter.convert(ctx, arg)
if role.id not in selfroles:
raise commands.BadArgument("The provided role is not a valid"
" selfrole.")
raise commands.BadArgument("The provided role is not a valid" " selfrole.")
return role

View File

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

View File

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

View File

@ -5,8 +5,10 @@ from redbot.core import commands
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__()
self.has_real_data = False
self.name = name
@ -43,13 +45,12 @@ class AliasEntry:
"creator": creator,
"guild": guild,
"global": self.global_,
"uses": self.uses
"uses": self.uses,
}
@classmethod
def from_json(cls, data: dict, bot: commands.Bot=None):
ret = cls(data["name"], data["command"],
data["creator"], global_=data["global"])
def from_json(cls, data: dict, bot: commands.Bot = None):
ret = cls(data["name"], data["command"], data["creator"], global_=data["global"])
if bot:
ret.has_real_data = True

View File

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

View File

@ -9,9 +9,10 @@ from redbot.core.data_manager import cog_data_path
import redbot.core
LAVALINK_DOWNLOAD_URL = (
"https://github.com/Cog-Creators/Red-DiscordBot/"
"releases/download/{}/Lavalink.jar"
).format(redbot.core.__version__)
"https://github.com/Cog-Creators/Red-DiscordBot/" "releases/download/{}/Lavalink.jar"
).format(
redbot.core.__version__
)
LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio")
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):
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:
while True:
chunk = await resp.content.read(512)

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -5,7 +5,7 @@ from subprocess import Popen, DEVNULL, PIPE
import os
import logging
log = logging.getLogger('red.audio.manager')
log = logging.getLogger("red.audio.manager")
proc = None
SHUTDOWN = asyncio.Event()
@ -13,7 +13,8 @@ SHUTDOWN = asyncio.Event()
def has_java_error(pid):
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()
@ -29,14 +30,14 @@ async def monitor_lavalink_server(loop):
log.info("Restarting Lavalink jar.")
await start_lavalink_server(loop)
else:
log.error("Your Java is borked. Please find the hs_err_pid{}.log file"
" in the Audio data folder and report this issue.".format(
proc.pid
))
log.error(
"Your Java is borked. Please find the hs_err_pid{}.log file"
" in the Audio data folder and report this issue.".format(proc.pid)
)
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:
return False
@ -48,20 +49,18 @@ async def get_java_version(loop):
"""
This assumes we've already checked that java exists.
"""
proc = Popen(
shlex.split("java -version", posix=os.name == 'posix'),
stdout=PIPE, stderr=PIPE
)
proc = Popen(shlex.split("java -version", posix=os.name == "posix"), stdout=PIPE, stderr=PIPE)
_, 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_string = version_line[version_start + 1:-1]
major, minor = version_string.split('.')[:2]
major, minor = version_string.split(".")[:2]
return int(major), int(minor)
async def start_lavalink_server(loop):
java_available, java_version = await has_java(loop)
if not java_available:
@ -72,13 +71,15 @@ async def start_lavalink_server(loop):
extra_flags = "-Dsun.zip.disableMemoryMapping=true"
from . import LAVALINK_DOWNLOAD_DIR, LAVALINK_JAR_FILE
start_cmd = "java {} -jar {}".format(extra_flags, LAVALINK_JAR_FILE.resolve())
global proc
proc = Popen(
shlex.split(start_cmd, posix=os.name == 'posix'),
shlex.split(start_cmd, posix=os.name == "posix"),
cwd=str(LAVALINK_DOWNLOAD_DIR),
stdout=DEVNULL, stderr=DEVNULL
stdout=DEVNULL,
stderr=DEVNULL,
)
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
_ = Translator('Bank', __file__)
_ = Translator("Bank", __file__)
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
either the guildowner or has the administrator permission.
"""
async def pred(ctx: commands.Context):
author = ctx.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
either a bot admin or has the manage_guild permission.
"""
async def pred(ctx: commands.Context):
author = ctx.author
if await ctx.bot.is_owner(author):
@ -73,19 +75,23 @@ class Bank:
currency_name = await bank._conf.guild(ctx.guild).currency()
default_balance = await bank._conf.guild(ctx.guild).default_balance()
settings = (_(
"Bank settings:\n\n"
"Bank name: {}\n"
"Currency: {}\n"
"Default balance: {}"
"").format(bank_name, currency_name, default_balance)
settings = (
_(
"Bank settings:\n\n"
"Bank name: {}\n"
"Currency: {}\n"
"Default balance: {}"
""
).format(
bank_name, currency_name, default_balance
)
)
await ctx.send(box(settings))
await ctx.send_help()
@bankset.command(name="toggleglobal")
@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
If the bank is global, it will become per-server
If the bank is per-server, it will become global"""
@ -94,8 +100,10 @@ class Bank:
word = _("per-server") if cur_setting else _("global")
if confirm is False:
await ctx.send(
_("This will toggle the bank to be {}, deleting all accounts "
"in the process! If you're sure, type `{}`").format(
_(
"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)
)
)

View File

@ -1,6 +1,7 @@
class BankError(Exception):
pass
class BankNotGlobal(BankError):
pass
@ -34,4 +35,4 @@ class NegativeValue(BankError):
class SameSenderAndReceiver(BankError):
pass
pass

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,49 +16,49 @@ class SpecResolver(object):
self.v2path = path
self.resolved = set()
self.available_core_conversions = {
'Bank Accounts': {
'cfg': ('Bank', None, 384734293238749),
'file': self.v2path / 'data' / 'economy' / 'bank.json',
'converter': self.bank_accounts_conv_spec
"Bank Accounts": {
"cfg": ("Bank", None, 384734293238749),
"file": self.v2path / "data" / "economy" / "bank.json",
"converter": self.bank_accounts_conv_spec,
},
'Economy Settings': {
'cfg': ('Economy', 'config', 1256844281),
'file': self.v2path / 'data' / 'economy' / 'settings.json',
'converter': self.economy_conv_spec
"Economy Settings": {
"cfg": ("Economy", "config", 1256844281),
"file": self.v2path / "data" / "economy" / "settings.json",
"converter": self.economy_conv_spec,
},
'Mod Log Cases': {
'cfg': ('ModLog', None, 1354799444),
'file': self.v2path / 'data' / 'mod' / 'modlog.json',
'converter': None # prevents from showing as available
"Mod Log Cases": {
"cfg": ("ModLog", None, 1354799444),
"file": self.v2path / "data" / "mod" / "modlog.json",
"converter": None, # prevents from showing as available
},
'Filter': {
'cfg': ('Filter', 'settings', 4766951341),
'file': self.v2path / 'data' / 'mod' / 'filter.json',
'converter': self.filter_conv_spec
"Filter": {
"cfg": ("Filter", "settings", 4766951341),
"file": self.v2path / "data" / "mod" / "filter.json",
"converter": self.filter_conv_spec,
},
'Past Names': {
'cfg': ('Mod', 'settings', 4961522000),
'file': self.v2path / 'data' / 'mod' / 'past_names.json',
'converter': self.past_names_conv_spec
"Past Names": {
"cfg": ("Mod", "settings", 4961522000),
"file": self.v2path / "data" / "mod" / "past_names.json",
"converter": self.past_names_conv_spec,
},
'Past Nicknames': {
'cfg': ('Mod', 'settings', 4961522000),
'file': self.v2path / 'data' / 'mod' / 'past_nicknames.json',
'converter': self.past_nicknames_conv_spec
"Past Nicknames": {
"cfg": ("Mod", "settings", 4961522000),
"file": self.v2path / "data" / "mod" / "past_nicknames.json",
"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
def available(self):
return sorted(
k for k, v in self.available_core_conversions.items()
if v['file'].is_file() and v['converter'] is not None
and k not in self.resolved
k
for k, v in self.available_core_conversions.items()
if v["file"].is_file() and v["converter"] is not None and k not in self.resolved
)
def unpack(self, parent_key, parent_value):
@ -75,15 +75,8 @@ class SpecResolver(object):
"""Flatten a nested dictionary structure"""
dictionary = {(key,): value for key, value in dictionary.items()}
while True:
dictionary = dict(
chain.from_iterable(
starmap(self.unpack, dictionary.items())
)
)
if not any(
isinstance(value, dict)
for value in dictionary.values()
):
dictionary = dict(chain.from_iterable(starmap(self.unpack, dictionary.items())))
if not any(isinstance(value, dict) for value in dictionary.values()):
break
return dictionary
@ -97,11 +90,8 @@ class SpecResolver(object):
outerkey, innerkey = tuple(k[:-1]), (k[-1],)
if outerkey not in ret:
ret[outerkey] = {}
if innerkey[0] == 'created_at':
x = int(
datetime.strptime(
v, "%Y-%m-%d %H:%M:%S").timestamp()
)
if innerkey[0] == "created_at":
x = int(datetime.strptime(v, "%Y-%m-%d %H:%M:%S").timestamp())
ret[outerkey].update({innerkey: x})
else:
ret[outerkey].update({innerkey: v})
@ -121,16 +111,10 @@ class SpecResolver(object):
raise NotImplementedError("This one isn't ready yet")
def filter_conv_spec(self, data: dict):
return {
(Config.GUILD, k): {('filter',): v}
for k, v in data.items()
}
return {(Config.GUILD, k): {("filter",): v} for k, v in data.items()}
def past_names_conv_spec(self, data: dict):
return {
(Config.USER, k): {('past_names',): v}
for k, v in data.items()
}
return {(Config.USER, k): {("past_names",): v} for k, v in data.items()}
def past_nicknames_conv_spec(self, data: dict):
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))
ret = {}
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:
ret[outerkey] = {}
ccinfo = {
'author': {
'id': 42,
'name': 'Converted from a v2 instance'
},
'command': k[-1],
'created_at': '{:%d/%m/%Y %H:%M:%S}'.format(datetime.utcnow()),
'editors': [],
'response': v
"author": {"id": 42, "name": "Converted from a v2 instance"},
"command": k[-1],
"created_at": "{:%d/%m/%Y %H:%M:%S}".format(datetime.utcnow()),
"editors": [],
"response": v,
}
ret[outerkey].update({innerkey: ccinfo})
return ret
@ -168,8 +149,8 @@ class SpecResolver(object):
raise NotImplementedError("No Conversion Specs for this")
info = self.available_core_conversions[prettyname]
filepath, converter = info['file'], info['converter']
(cogname, attr, _id) = info['cfg']
filepath, converter = info["file"], info["converter"]
(cogname, attr, _id) = info["cfg"]
try:
config = getattr(bot.get_cog(cogname), attr)
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.core.utils.chat_formatting import box
_ = Translator('DataConverter', __file__)
_ = Translator("DataConverter", __file__)
@cog_i18n(_)
@ -34,13 +34,14 @@ class DataConverter:
if not resolver.available:
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 "
"installation path?")
_(
"There don't seem to be any data files I know how to "
"handle here. Are you sure you gave me the base "
"installation path?"
)
)
while resolver.available:
menu = _("Please select a set of data to import by number"
", or 'exit' to exit")
menu = _("Please select a set of data to import by number" ", or 'exit' to exit")
for index, entry in enumerate(resolver.available, 1):
menu += "\n{}. {}".format(index, entry)
@ -50,24 +51,17 @@ class DataConverter:
return m.channel == ctx.channel and m.author == ctx.author
try:
message = await self.bot.wait_for(
'message', check=pred, timeout=60
)
message = await self.bot.wait_for("message", check=pred, timeout=60)
except asyncio.TimeoutError:
return await ctx.send(
_('Try this again when you are more ready'))
return await ctx.send(_("Try this again when you are more ready"))
else:
if message.content.strip().lower() in [
'quit', 'exit', '-1', 'q', 'cancel'
]:
if message.content.strip().lower() in ["quit", "exit", "-1", "q", "cancel"]:
return await ctx.tick()
try:
message = int(message.content.strip())
to_conv = resolver.available[message - 1]
except (ValueError, IndexError):
await ctx.send(
_("That wasn't a valid choice.")
)
await ctx.send(_("That wasn't a valid choice."))
continue
else:
async with ctx.typing():
@ -76,6 +70,8 @@ class DataConverter:
await menu_message.delete()
else:
return await ctx.send(
_("There isn't anything else I know how to convert here."
"\nThere might be more things I can convert in the future.")
_(
"There isn't anything else I know how to convert here."
"\nThere might be more things I can convert in the future."
)
)

View File

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

View File

@ -3,7 +3,7 @@ import asyncio
import discord
from discord.ext import commands
__all__ = ["install_agreement", ]
__all__ = ["install_agreement"]
REPO_INSTALL_MSG = (
"You're about to add a 3rd party repository. The creator of Red"
@ -17,29 +17,28 @@ REPO_INSTALL_MSG = (
def install_agreement():
async def pred(ctx: commands.Context):
downloader = ctx.command.instance
if downloader is None:
return True
elif downloader.already_agreed:
return True
elif ctx.invoked_subcommand is None or \
isinstance(ctx.invoked_subcommand, commands.Group):
elif ctx.invoked_subcommand is None or isinstance(ctx.invoked_subcommand, commands.Group):
return True
def does_agree(msg: discord.Message):
return ctx.author == msg.author and \
ctx.channel == msg.channel and \
msg.content == "I agree"
return ctx.author == msg.author and ctx.channel == msg.channel and msg.content == "I agree"
await ctx.send(REPO_INSTALL_MSG)
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:
await ctx.send("Your response has timed out, please try again.")
return False
downloader.already_agreed = True
return True
return commands.check(pred)

View File

@ -5,6 +5,7 @@ from .installable import Installable
class InstalledCog(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> Installable:
downloader = ctx.bot.get_cog("Downloader")
if downloader is None:
@ -12,8 +13,6 @@ class InstalledCog(commands.Converter):
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
if cog is None:
raise commands.BadArgument(
"That cog is not installed"
)
raise commands.BadArgument("That cog is not installed")
return cog

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ class RepoJSONMixin:
return
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)
except json.JSONDecodeError:
return
@ -34,4 +34,4 @@ class RepoJSONMixin:
self.author = info.get("author")
self.install_msg = info.get("install_msg")
self.short = info.get("short")
self.description = info.get("description")
self.description = info.get("description")

View File

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

View File

@ -1,3 +1,3 @@
import logging
log = logging.getLogger("red.downloader")
log = logging.getLogger("red.downloader")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,9 +13,7 @@ GIPHY_API_KEY = "dc6zaTOxFJmzC"
@cog_i18n(_)
class Image:
"""Image related commands."""
default_global = {
"imgur_client_id": None
}
default_global = {"imgur_client_id": None}
def __init__(self, bot):
self.bot = bot
@ -45,7 +43,9 @@ class Image:
if not imgur_client_id:
await ctx.send(
_("A client ID has not been set! Please set one with {}").format(
"`{}imgurcreds`".format(ctx.prefix)))
"`{}imgurcreds`".format(ctx.prefix)
)
)
return
headers = {"Authorization": "Client-ID {}".format(imgur_client_id)}
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"]))
@_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
Sort types: new, top
@ -90,7 +92,9 @@ class Image:
if not imgur_client_id:
await ctx.send(
_("A client ID has not been set! Please set one with {}").format(
"`{}imgurcreds`".format(ctx.prefix)))
"`{}imgurcreds`".format(ctx.prefix)
)
)
return
links = []
@ -139,8 +143,10 @@ class Image:
await ctx.send_help()
return
url = ("http://api.giphy.com/v1/gifs/search?&api_key={}&q={}"
"".format(GIPHY_API_KEY, keywords))
url = (
"http://api.giphy.com/v1/gifs/search?&api_key={}&q={}"
"".format(GIPHY_API_KEY, keywords)
)
async with self.session.get(url) as r:
result = await r.json()
@ -161,8 +167,10 @@ class Image:
await ctx.send_help()
return
url = ("http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}"
"".format(GIPHY_API_KEY, keywords))
url = (
"http://api.giphy.com/v1/gifs/random?&api_key={}&tag={}"
"".format(GIPHY_API_KEY, keywords)
)
async with self.session.get(url) as r:
result = await r.json()

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -27,4 +27,4 @@ class OfflineStream(StreamsError):
class OfflineCommunity(StreamsError):
pass
pass

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,9 @@ from redbot.core.i18n import Translator
_ = 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"""
guild = ctx.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)
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_settings = config.guild(guild)
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)
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.content = command_str.format(user=user.mention, prefix=realctx.prefix)
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:
com = bot.get_command(userinput)
if com is None:
userinput = ' '.join(userinput.split(' ')[:-1])
userinput = " ".join(userinput.split(" ")[:-1])
if len(userinput) == 0:
break
if com is None:
@ -63,8 +69,9 @@ def get_command_from_input(bot, userinput: str):
check_str = inspect.getsource(checks.is_owner)
if any(inspect.getsource(x) in check_str for x in com.checks):
# command the user specified has the is_owner check
return None, _("That command requires bot owner. I can't "
"allow you to use that for an action")
return None, _(
"That command requires bot owner. I can't " "allow you to use that for an action"
)
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
the points threshold for the action"""
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 "
"actually trying to run the command, except don't put a prefix and "
"use {user} in place of any user/member arguments\n\n"
"WARNING: The command entered will be run without regard to checks or cooldowns. "
"Commands requiring bot owner are not allowed for security reasons.\n\n"
"Please wait 15 seconds before entering your response.")
_(
"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 "
"actually trying to run the command, except don't put a prefix and "
"use {user} in place of any user/member arguments\n\n"
"WARNING: The command entered will be run without regard to checks or cooldowns. "
"Commands requiring bot owner are not allowed for security reasons.\n\n"
"Please wait 15 seconds before entering your response."
)
)
await asyncio.sleep(15)
@ -110,15 +119,17 @@ async def get_command_for_dropping_points(ctx: commands.Context):
when the user exceeded the threshold
"""
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 "
"intended to be used for reversal of the action taken when the user "
"exceeded the action's point value\nEnter it exactly as you would "
"if you were actually trying to run the command, except don't put a prefix "
"and use {user} in place of any user/member arguments\n\n"
"WARNING: The command entered will be run without regard to checks or cooldowns. "
"Commands requiring bot owner are not allowed for security reasons.\n\n"
"Please wait 15 seconds before entering your response.")
_(
"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 "
"intended to be used for reversal of the action taken when the user "
"exceeded the action's point value\nEnter it exactly as you would "
"if you were actually trying to run the command, except don't put a prefix "
"and use {user} in place of any user/member arguments\n\n"
"WARNING: The command entered will be run without regard to checks or cooldowns. "
"Commands requiring bot owner are not allowed for security reasons.\n\n"
"Please wait 15 seconds before entering your response."
)
)
await asyncio.sleep(15)

View File

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

View File

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

View File

@ -4,16 +4,15 @@ __all__ = ["Config", "__version__"]
class VersionInfo:
def __init__(self, major, minor, micro, releaselevel, serial):
self._levels = ['alpha', 'beta', 'final']
self._levels = ["alpha", "beta", "final"]
self.major = major
self.minor = minor
self.micro = micro
if releaselevel not in self._levels:
raise TypeError("'releaselevel' must be one of: {}".format(
', '.join(self._levels)
))
raise TypeError("'releaselevel' must be one of: {}".format(", ".join(self._levels)))
self.releaselevel = releaselevel
self.serial = serial
@ -21,8 +20,9 @@ class VersionInfo:
def __lt__(self, other):
my_index = self._levels.index(self.releaselevel)
other_index = self._levels.index(other.releaselevel)
return (self.major, self.minor, self.micro, my_index, self.serial) < \
(other.major, other.minor, other.micro, other_index, other.serial)
return (self.major, self.minor, self.micro, my_index, self.serial) < (
other.major, other.minor, other.micro, other_index, other.serial
)
def __repr__(self):
return "VersionInfo(major={}, minor={}, micro={}, releaselevel={}, serial={})".format(
@ -32,5 +32,6 @@ class VersionInfo:
def to_json(self):
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
__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
__all__ = ["Account", "get_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"]
__all__ = [
"Account",
"get_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 = {
"is_global": False,
"bank_name": "Twentysix bank",
"currency": "credits",
"default_balance": 100
"default_balance": 100,
}
_DEFAULT_GUILD = {
"bank_name": "Twentysix bank",
"currency": "credits",
"default_balance": 100
}
_DEFAULT_GUILD = {"bank_name": "Twentysix bank", "currency": "credits", "default_balance": 100}
_DEFAULT_MEMBER = {
"name": "",
"balance": 0,
"created_at": 0
}
_DEFAULT_MEMBER = {"name": "", "balance": 0, "created_at": 0}
_DEFAULT_USER = _DEFAULT_MEMBER
@ -50,9 +57,9 @@ def _register_defaults():
_conf.register_member(**_DEFAULT_MEMBER)
_conf.register_user(**_DEFAULT_USER)
if not os.environ.get('BUILDING_DOCS'):
_conf = Config.get_conf(
None, 384734293238749, cog_name="Bank", force_registration=True)
if not os.environ.get("BUILDING_DOCS"):
_conf = Config.get_conf(None, 384734293238749, cog_name="Bank", force_registration=True)
_register_defaults()
@ -285,7 +292,7 @@ async def wipe_bank():
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
@ -319,14 +326,16 @@ async def get_leaderboard(positions: int=None, guild: discord.Guild=None) -> Lis
if guild is None:
raise TypeError("Expected a guild, got NoneType object instead!")
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:
return sorted_acc
else:
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
@ -387,13 +396,13 @@ async def get_account(member: Union[discord.Member, discord.User]) -> Account:
if acc_data == {}:
acc_data = default
acc_data['name'] = member.display_name
acc_data["name"] = member.display_name
try:
acc_data['balance'] = await get_default_balance(member.guild)
acc_data["balance"] = await get_default_balance(member.guild)
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)
@ -444,7 +453,7 @@ async def set_global(global_: bool) -> bool:
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.
Parameters
@ -472,7 +481,7 @@ async def get_bank_name(guild: discord.Guild=None) -> str:
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.
Parameters
@ -499,12 +508,13 @@ async def set_bank_name(name: str, guild: discord.Guild=None) -> str:
elif guild is not None:
await _conf.guild(guild).bank_name.set(name)
else:
raise RuntimeError("Guild must be provided if setting the name of a guild"
"-specific bank.")
raise RuntimeError(
"Guild must be provided if setting the name of a guild" "-specific bank."
)
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.
Parameters
@ -532,7 +542,7 @@ async def get_currency_name(guild: discord.Guild=None) -> str:
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.
Parameters
@ -559,12 +569,13 @@ async def set_currency_name(name: str, guild: discord.Guild=None) -> str:
elif guild is not None:
await _conf.guild(guild).currency.set(name)
else:
raise RuntimeError("Guild must be provided if setting the currency"
" name of a guild-specific bank.")
raise RuntimeError(
"Guild must be provided if setting the currency" " name of a guild-specific bank."
)
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.
Parameters
@ -592,7 +603,7 @@ async def get_default_balance(guild: discord.Guild=None) -> int:
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.
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
from discord.voice_client import VoiceClient
VoiceClient.warn_nacl = False
from .cog_manager import CogManager
from . import (
Config,
i18n,
commands,
rpc
)
from . import Config, i18n, commands, rpc
from .help_formatter import Help, help as help_
from .sentry import SentryManager
from .utils import TYPE_CHECKING
@ -32,6 +28,7 @@ if TYPE_CHECKING:
# noinspection PyUnresolvedReferences
class RpcMethodMixin:
async def rpc__cogs(self, request):
return list(self.cogs.keys())
@ -48,7 +45,8 @@ class RedBase(BotBase, RpcMethodMixin):
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.db = Config.get_core_conf(force_registration=True)
self._co_owners = cli_flags.co_owner
@ -62,22 +60,15 @@ class RedBase(BotBase, RpcMethodMixin):
whitelist=[],
blacklist=[],
enable_sentry=None,
locale='en',
embeds=True
locale="en",
embeds=True,
)
self.db.register_guild(
prefix=[],
whitelist=[],
blacklist=[],
admin_role=None,
mod_role=None,
embeds=None
prefix=[], whitelist=[], blacklist=[], admin_role=None, mod_role=None, embeds=None
)
self.db.register_user(
embeds=None
)
self.db.register_user(embeds=None)
async def prefix_manager(bot, message):
if not cli_flags.prefix:
@ -88,9 +79,13 @@ class RedBase(BotBase, RpcMethodMixin):
return global_prefix
server_prefix = await bot.db.guild(message.guild).prefix()
if cli_flags.mentionable:
return when_mentioned_or(*server_prefix)(bot, message) \
if server_prefix else \
when_mentioned_or(*global_prefix)(bot, message)
return when_mentioned_or(*server_prefix)(
bot, message
) if server_prefix else when_mentioned_or(
*global_prefix
)(
bot, message
)
else:
return server_prefix if server_prefix else global_prefix
@ -109,13 +104,13 @@ class RedBase(BotBase, RpcMethodMixin):
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()
super().__init__(formatter=Help(), **kwargs)
self.remove_command('help')
self.remove_command("help")
self.add_command(help_)
@ -124,7 +119,7 @@ class RedBase(BotBase, RpcMethodMixin):
def enable_sentry(self):
"""Enable Sentry logging for Red."""
if self._sentry_mgr is None:
sentry_log = logging.getLogger('red.sentry')
sentry_log = logging.getLogger("red.sentry")
sentry_log.setLevel(logging.WARNING)
self._sentry_mgr = SentryManager(sentry_log)
self._sentry_mgr.enable()
@ -143,7 +138,7 @@ class RedBase(BotBase, RpcMethodMixin):
:return:
"""
indict['owner_id'] = await self.db.owner()
indict["owner_id"] = await self.db.owner()
i18n.set_locale(await self.db.locale())
async def embed_requested(self, channel, user, command=None) -> bool:
@ -164,8 +159,9 @@ class RedBase(BotBase, RpcMethodMixin):
bool
:code:`True` if an embed is requested
"""
if isinstance(channel, discord.abc.PrivateChannel) or (
command and command == self.get_command("help")
if (
isinstance(channel, discord.abc.PrivateChannel)
or (command and command == self.get_command("help"))
):
user_setting = await self.db.user(user).embeds()
if user_setting is not None:
@ -214,14 +210,14 @@ class RedBase(BotBase, RpcMethodMixin):
curr_pkgs.remove(pkg_name)
async def load_extension(self, spec: ModuleSpec):
name = spec.name.split('.')[-1]
name = spec.name.split(".")[-1]
if name in self.extensions:
return
lib = spec.loader.load_module()
if not hasattr(lib, 'setup'):
if not hasattr(lib, "setup"):
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):
await lib.setup(self)
@ -262,7 +258,7 @@ class RedBase(BotBase, RpcMethodMixin):
del event_list[index]
try:
func = getattr(lib, 'teardown')
func = getattr(lib, "teardown")
except AttributeError:
pass
else:
@ -279,19 +275,20 @@ class RedBase(BotBase, RpcMethodMixin):
if m.startswith(pkg_name):
del sys.modules[m]
if pkg_name.startswith('redbot.cogs'):
del sys.modules['redbot.cogs'].__dict__[name]
if pkg_name.startswith("redbot.cogs"):
del sys.modules["redbot.cogs"].__dict__[name]
def register_rpc_methods(self):
rpc.add_method('bot', self.rpc__cogs)
rpc.add_method('bot', self.rpc__extensions)
rpc.add_method("bot", self.rpc__cogs)
rpc.add_method("bot", self.rpc__extensions)
class Red(RedBase, discord.AutoShardedClient):
"""
You're welcome Caleb.
"""
async def shutdown(self, *, restart: bool=False):
async def shutdown(self, *, restart: bool = False):
"""Gracefully quit Red.
The program will exit with code :code:`0` by default.
@ -314,4 +311,4 @@ class Red(RedBase, discord.AutoShardedClient):
class ExitCodes(Enum):
CRITICAL = 1
SHUTDOWN = 0
RESTART = 26
RESTART = 26

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,9 +14,15 @@ from .utils import TYPE_CHECKING
if TYPE_CHECKING:
from . import Config
__all__ = ['load_basic_configuration', 'cog_data_path', 'core_data_path',
'load_bundled_data', 'bundled_data_path', 'storage_details',
'storage_type']
__all__ = [
"load_basic_configuration",
"cog_data_path",
"core_data_path",
"load_bundled_data",
"bundled_data_path",
"storage_details",
"storage_type",
]
log = logging.getLogger("red.data_manager")
@ -25,20 +31,16 @@ basic_config = None
instance_name = None
basic_config_default = {
"DATA_PATH": None,
"COG_PATH_APPEND": "cogs",
"CORE_PATH_APPEND": "core"
}
basic_config_default = {"DATA_PATH": None, "COG_PATH_APPEND": "cogs", "CORE_PATH_APPEND": "core"}
config_dir = None
appdir = appdirs.AppDirs("Red-DiscordBot")
if sys.platform == 'linux':
if sys.platform == "linux":
if 0 < os.getuid() < 1000:
config_dir = Path(appdir.site_data_dir)
if not 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):
@ -67,20 +69,23 @@ def load_basic_configuration(instance_name_: str):
config = jsonio._load_json()
basic_config = config[instance_name]
except (FileNotFoundError, KeyError):
print("You need to configure the bot instance using `redbot-setup`"
" prior to running the bot.")
print(
"You need to configure the bot instance using `redbot-setup`"
" prior to running the bot."
)
sys.exit(1)
def _base_data_path() -> Path:
if basic_config is None:
raise RuntimeError("You must load the basic config before you"
" can get the base data path.")
path = basic_config['DATA_PATH']
raise RuntimeError(
"You must load the basic config before you" " can get the base data path."
)
path = basic_config["DATA_PATH"]
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
which to store your own cog's data please pass in an instance
of your cog class.
@ -104,9 +109,10 @@ def cog_data_path(cog_instance=None, raw_name: str=None) -> Path:
try:
base_data_path = Path(_base_data_path())
except RuntimeError as e:
raise RuntimeError("You must load the basic config before you"
" can get the cog data path.") from e
cog_path = base_data_path / basic_config['COG_PATH_APPEND']
raise RuntimeError(
"You must load the basic config before you" " can get the cog data path."
) from e
cog_path = base_data_path / basic_config["COG_PATH_APPEND"]
if raw_name is not None:
cog_path = cog_path / raw_name
@ -121,9 +127,10 @@ def core_data_path() -> Path:
try:
base_data_path = Path(_base_data_path())
except RuntimeError as e:
raise RuntimeError("You must load the basic config before you"
" can get the core data path.") from e
core_path = base_data_path / basic_config['CORE_PATH_APPEND']
raise RuntimeError(
"You must load the basic config before you" " can get the core data path."
) from e
core_path = base_data_path / basic_config["CORE_PATH_APPEND"]
core_path.mkdir(exist_ok=True, parents=True)
return core_path.resolve()
@ -145,15 +152,13 @@ def _find_data_files(init_location: str) -> (Path, List[Path]):
if not init_file.is_file():
return []
package_folder = init_file.parent.resolve() / 'data'
package_folder = init_file.parent.resolve() / "data"
if not package_folder.is_dir():
return []
all_files = list(package_folder.rglob("*"))
return package_folder, [p.resolve()
for p in all_files
if p.is_file()]
return package_folder, [p.resolve() 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):
@ -181,27 +186,24 @@ def _compare_and_copy(to_copy: List[Path], bundled_data_dir: Path, cog_data_dir:
yield block
block = afile.read(blocksize)
lookup = {p: cog_data_dir.joinpath(p.relative_to(bundled_data_dir))
for p in to_copy}
lookup = {p: cog_data_dir.joinpath(p.relative_to(bundled_data_dir)) for p in to_copy}
for orig, poss_existing in lookup.items():
if not poss_existing.is_file():
poss_existing.parent.mkdir(exist_ok=True, parents=True)
exists_checksum = None
else:
exists_checksum = hash_bytestr_iter(file_as_blockiter(
poss_existing.open('rb')), hashlib.sha256())
exists_checksum = hash_bytestr_iter(
file_as_blockiter(poss_existing.open("rb")), hashlib.sha256()
)
orig_checksum = ...
if exists_checksum is not None:
orig_checksum = hash_bytestr_iter(file_as_blockiter(
orig.open('rb')), hashlib.sha256())
orig_checksum = hash_bytestr_iter(file_as_blockiter(orig.open("rb")), hashlib.sha256())
if exists_checksum != orig_checksum:
shutil.copy(str(orig), str(poss_existing))
log.debug("Copying {} to {}".format(
orig, poss_existing
))
log.debug("Copying {} to {}".format(orig, poss_existing))
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)
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)
@ -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.
"""
bundled_path = cog_data_path(cog_instance) / 'bundled_data'
bundled_path = cog_data_path(cog_instance) / "bundled_data"
if not bundled_path.is_dir():
raise FileNotFoundError("No such directory {}".format(
bundled_path
))
raise FileNotFoundError("No such directory {}".format(bundled_path))
return bundled_path
@ -282,9 +282,9 @@ def storage_type() -> str:
str
"""
try:
return basic_config['STORAGE_TYPE']
return basic_config["STORAGE_TYPE"]
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:
@ -297,6 +297,6 @@ def storage_details() -> dict:
dict
"""
try:
return basic_config['STORAGE_DETAILS']
return basic_config["STORAGE_DETAILS"]
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 .i18n import Translator
from .utils.chat_formatting import box, pagify
"""
Notice:
@ -32,11 +33,11 @@ class Dev:
def cleanup_code(content):
"""Automatically removes code blocks from the code."""
# remove ```py\n```
if content.startswith('```') and content.endswith('```'):
return '\n'.join(content.split('\n')[1:-1])
if content.startswith("```") and content.endswith("```"):
return "\n".join(content.split("\n")[1:-1])
# remove `foo`
return content.strip('` \n')
return content.strip("` \n")
@staticmethod
def get_syntax_error(e):
@ -45,11 +46,10 @@ class Dev:
Returns a string representation of the error formatted as a codeblock.
"""
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(
'{0.text}{1:>{0.offset}}\n{2}: {0}'
''.format(e, '^', type(e).__name__),
lang="py")
"{0.text}{1:>{0.offset}}\n{2}: {0}" "".format(e, "^", type(e).__name__), lang="py"
)
@staticmethod
def get_pages(msg: str):
@ -90,15 +90,15 @@ class Dev:
_ - The result of the last dev command.
"""
env = {
'bot': ctx.bot,
'ctx': ctx,
'channel': ctx.channel,
'author': ctx.author,
'guild': ctx.guild,
'message': ctx.message,
'discord': discord,
'commands': commands,
'_': self._last_result
"bot": ctx.bot,
"ctx": ctx,
"channel": ctx.channel,
"author": ctx.author,
"guild": ctx.guild,
"message": ctx.message,
"discord": discord,
"commands": commands,
"_": self._last_result,
}
code = self.cleanup_code(code)
@ -109,8 +109,7 @@ class Dev:
await ctx.send(self.get_syntax_error(e))
return
except Exception as e:
await ctx.send(
box('{}: {!s}'.format(type(e).__name__, e), lang='py'))
await ctx.send(box("{}: {!s}".format(type(e).__name__, e), lang="py"))
return
if asyncio.iscoroutine(result):
@ -122,7 +121,7 @@ class Dev:
await ctx.send_interactive(self.get_pages(result), box_lang="py")
@commands.command(name='eval')
@commands.command(name="eval")
@checks.is_owner()
async def _eval(self, ctx, *, body: str):
"""Execute asynchronous code.
@ -145,28 +144,28 @@ class Dev:
_ - The result of the last dev command.
"""
env = {
'bot': ctx.bot,
'ctx': ctx,
'channel': ctx.channel,
'author': ctx.author,
'guild': ctx.guild,
'message': ctx.message,
'discord': discord,
'commands': commands,
'_': self._last_result
"bot": ctx.bot,
"ctx": ctx,
"channel": ctx.channel,
"author": ctx.author,
"guild": ctx.guild,
"message": ctx.message,
"discord": discord,
"commands": commands,
"_": self._last_result,
}
body = self.cleanup_code(body)
stdout = io.StringIO()
to_compile = 'async def func():\n%s' % textwrap.indent(body, ' ')
to_compile = "async def func():\n%s" % textwrap.indent(body, " ")
try:
exec(to_compile, env)
except SyntaxError as e:
return await ctx.send(self.get_syntax_error(e))
func = env['func']
func = env["func"]
result = None
try:
with redirect_stdout(stdout):
@ -199,43 +198,43 @@ class Dev:
async function.
"""
variables = {
'ctx': ctx,
'bot': ctx.bot,
'message': ctx.message,
'guild': ctx.guild,
'channel': ctx.channel,
'author': ctx.author,
'_': None,
"ctx": ctx,
"bot": ctx.bot,
"message": ctx.message,
"guild": ctx.guild,
"channel": ctx.channel,
"author": ctx.author,
"_": None,
}
if ctx.channel.id in self.sessions:
await ctx.send(_('Already running a REPL session in this channel. '
'Exit it with `quit`.'))
await ctx.send(
_("Already running a REPL session in this channel. " "Exit it with `quit`.")
)
return
self.sessions.add(ctx.channel.id)
await ctx.send(_('Enter code to execute or evaluate.'
' `exit()` or `quit` to exit.'))
await ctx.send(_("Enter code to execute or evaluate." " `exit()` or `quit` to exit."))
msg_check = lambda m: (m.author == ctx.author and
m.channel == ctx.channel and
m.content.startswith('`'))
msg_check = lambda m: (
m.author == ctx.author and m.channel == ctx.channel and m.content.startswith("`")
)
while True:
response = await ctx.bot.wait_for("message", check=msg_check)
cleaned = self.cleanup_code(response.content)
if cleaned in ('quit', 'exit', 'exit()'):
await ctx.send('Exiting.')
if cleaned in ("quit", "exit", "exit()"):
await ctx.send("Exiting.")
self.sessions.remove(ctx.channel.id)
return
executor = exec
if cleaned.count('\n') == 0:
if cleaned.count("\n") == 0:
# single statement, potentially 'eval'
try:
code = compile(cleaned, '<repl session>', 'eval')
code = compile(cleaned, "<repl session>", "eval")
except SyntaxError:
pass
else:
@ -243,12 +242,12 @@ class Dev:
if executor is exec:
try:
code = compile(cleaned, '<repl session>', 'exec')
code = compile(cleaned, "<repl session>", "exec")
except SyntaxError as e:
await ctx.send(self.get_syntax_error(e))
continue
variables['message'] = response
variables["message"] = response
stdout = io.StringIO()
@ -266,7 +265,7 @@ class Dev:
value = stdout.getvalue()
if result is not None:
msg = "{}{}".format(value, result)
variables['_'] = result
variables["_"] = result
elif value:
msg = "{}".format(value)
@ -277,7 +276,7 @@ class Dev:
except discord.Forbidden:
pass
except discord.HTTPException as e:
await ctx.send(_('Unexpected error: `{}`').format(e))
await ctx.send(_("Unexpected error: `{}`").format(e))
@commands.command()
@checks.is_owner()
@ -290,7 +289,7 @@ class Dev:
msg.author = user
msg.content = ctx.prefix + command
ctx.bot.dispatch('message', msg)
ctx.bot.dispatch("message", msg)
@commands.command(name="mockmsg")
@checks.is_owner()

View File

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

View File

@ -2,6 +2,7 @@ __all__ = ["BaseDriver"]
class BaseDriver:
def __init__(self, cog_name, identifier):
self.cog_name = cog_name
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`.
"""
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)
self.file_name = file_name_override
if data_path_override:
self.data_path = data_path_override
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)

View File

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

View File

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

View File

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

View File

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

View File

@ -11,13 +11,14 @@ from pathlib import Path
log = logging.getLogger("red")
PRETTY = {"indent": 4, "sort_keys": True, "separators": (',', ' : ')}
MINIFIED = {"sort_keys": True, "separators": (',', ':')}
PRETTY = {"indent": 4, "sort_keys": True, "separators": (",", " : ")}
MINIFIED = {"sort_keys": True, "separators": (",", ":")}
class JsonIO:
"""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.
"""
@ -43,7 +44,7 @@ class JsonIO:
# noinspection PyUnresolvedReferences
def _load_json(self):
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)
return data

View File

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

View File

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

View File

@ -10,8 +10,8 @@ from .utils import TYPE_CHECKING, NewType
if TYPE_CHECKING:
from .bot import Red
log = logging.getLogger('red.rpc')
JsonSerializable = NewType('JsonSerializable', dict)
log = logging.getLogger("red.rpc")
JsonSerializable = NewType("JsonSerializable", dict)
_rpc = JsonRpc(logger=log)
@ -22,13 +22,13 @@ async def initialize(bot: "Red"):
global _rpc_server
app = Application(loop=bot.loop)
app.router.add_route('*', '/rpc', _rpc)
app.router.add_route("*", "/rpc", _rpc)
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):
@ -77,10 +77,7 @@ def add_method(prefix, method):
method
MUST BE A COROUTINE OR OBJECT.
"""
_rpc.add_methods(
('', method),
prefix=prefix
)
_rpc.add_methods(("", method), prefix=prefix)
def clean_up():

View File

@ -12,11 +12,13 @@ class SentryManager:
def __init__(self, logger: logging.Logger):
self.client = Client(
dsn=("https://62402161d4cd4ef18f83b16f3e22a020:9310ef55a502442598203205a84da2bb@"
"sentry.io/253983"),
dsn=(
"https://62402161d4cd4ef18f83b16f3e22a020:9310ef55a502442598203205a84da2bb@"
"sentry.io/253983"
),
release=__version__,
include_paths=['redbot'],
enable_breadcrumbs=False
include_paths=["redbot"],
enable_breadcrumbs=False,
)
self.handler = SentryHandler(self.client)
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
import os
@ -12,6 +12,7 @@ except ImportError:
try:
from typing import NewType
except ImportError:
def NewType(name, tp):
return type(name, (tp,), {})

View File

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

View File

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

View File

@ -28,7 +28,7 @@ class DataConverter:
The file isn't valid JSON
"""
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)
except (FileNotFoundError, json.JSONDecodeError):
raise

View File

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

View File

@ -8,8 +8,7 @@ from redbot.core import Config
from redbot.core.bot import Red
async def mass_purge(messages: List[discord.Message],
channel: discord.TextChannel):
async def mass_purge(messages: List[discord.Message], channel: discord.TextChannel):
"""Bulk delete messages from a channel.
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.
"""
return \
"Action requested by {} (ID {}). Reason: {}".format(author, author.id, reason) if reason else \
"Action requested by {} (ID {}).".format(author, author.id)
return "Action requested by {} (ID {}). Reason: {}".format(
author, author.id, reason
) if reason else "Action requested by {} (ID {}).".format(
author, author.id
)
async def is_allowed_by_hierarchy(bot: Red,
settings: Config,
guild: discord.Guild,
mod: discord.Member,
user: discord.Member):
async def is_allowed_by_hierarchy(
bot: Red, settings: Config, guild: discord.Guild, mod: discord.Member, user: discord.Member
):
if not await settings.guild(guild).respect_hierarchy():
return True
is_special = mod == guild.owner or await bot.is_owner(mod)
return mod.top_role.position > user.top_role.position or is_special
async def is_mod_or_superior(
bot: Red, obj: Union[discord.Message, discord.Member, discord.Role]):
async def is_mod_or_superior(bot: Red, obj: Union[discord.Message, discord.Member, discord.Role]):
"""Check if an object has mod or superior permissions.
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):
pass
else:
raise TypeError('Only messages, members or roles may be passed')
raise TypeError("Only messages, members or roles may be passed")
server = obj.guild
admin_role_id = await bot.db.guild(server).admin_role()
@ -168,26 +166,27 @@ def strfdelta(delta: timedelta):
"""
s = []
if delta.days:
ds = '%i day' % delta.days
ds = "%i day" % delta.days
if delta.days > 1:
ds += 's'
ds += "s"
s.append(ds)
hrs, rem = divmod(delta.seconds, 60*60)
hrs, rem = divmod(delta.seconds, 60 * 60)
if hrs:
hs = '%i hr' % hrs
hs = "%i hr" % hrs
if hrs > 1:
hs += 's'
hs += "s"
s.append(hs)
mins, secs = divmod(rem, 60)
if mins:
s.append('%i min' % mins)
s.append("%i min" % mins)
if secs:
s.append('%i sec' % secs)
return ' '.join(s)
s.append("%i sec" % secs)
return " ".join(s)
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.
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):
pass
else:
raise TypeError('Only messages, members or roles may be passed')
raise TypeError("Only messages, members or roles may be passed")
server = obj.guild
admin_role_id = await bot.db.guild(server).admin_role()

View File

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

View File

@ -8,7 +8,14 @@ import asyncio
import pkg_resources
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.cli import confirm
@ -18,9 +25,9 @@ if sys.platform == "linux":
PYTHON_OK = sys.version_info >= (3, 5)
INTERACTIVE_MODE = not len(sys.argv) > 1 # CLI flags = non-interactive
INTRO = ("==========================\n"
"Red Discord Bot - Launcher\n"
"==========================\n")
INTRO = (
"==========================\n" "Red Discord Bot - Launcher\n" "==========================\n"
)
IS_WINDOWS = os.name == "nt"
IS_MAC = sys.platform == "darwin"
@ -31,35 +38,35 @@ def parse_cli_args():
description="Red - Discord Bot's launcher (V3)", allow_abbrev=False
)
instances = load_existing_config()
parser.add_argument("instancename", metavar="instancename", type=str,
nargs="?", help="The instance to run", choices=list(instances.keys()))
parser.add_argument("--start", "-s",
help="Starts Red",
action="store_true")
parser.add_argument("--auto-restart",
help="Autorestarts Red in case of issues",
action="store_true")
parser.add_argument("--update",
help="Updates Red",
action="store_true")
parser.add_argument("--update-dev",
help="Updates Red from the Github repo",
action="store_true")
parser.add_argument("--voice",
help="Installs extra 'voice' when updating",
action="store_true")
parser.add_argument("--docs",
help="Installs extra 'docs' when updating",
action="store_true")
parser.add_argument("--test",
help="Installs extra 'test' when updating",
action="store_true")
parser.add_argument("--mongo",
help="Installs extra 'mongo' when updating",
action="store_true")
parser.add_argument("--debuginfo",
help="Prints basic debug info that would be useful for support",
action="store_true")
parser.add_argument(
"instancename",
metavar="instancename",
type=str,
nargs="?",
help="The instance to run",
choices=list(instances.keys()),
)
parser.add_argument("--start", "-s", help="Starts Red", action="store_true")
parser.add_argument(
"--auto-restart", help="Autorestarts Red in case of issues", action="store_true"
)
parser.add_argument("--update", help="Updates Red", action="store_true")
parser.add_argument(
"--update-dev", help="Updates Red from the Github repo", action="store_true"
)
parser.add_argument(
"--voice", help="Installs extra 'voice' when updating", action="store_true"
)
parser.add_argument("--docs", help="Installs extra 'docs' when updating", action="store_true")
parser.add_argument("--test", help="Installs extra 'test' when updating", action="store_true")
parser.add_argument(
"--mongo", help="Installs extra 'mongo' when updating", action="store_true"
)
parser.add_argument(
"--debuginfo",
help="Prints basic debug info that would be useful for support",
action="store_true",
)
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:
package += "[{}]".format(", ".join(egg_l))
if reinstall:
code = subprocess.call([
interpreter, "-m",
"pip", "install", "-U", "-I",
"--force-reinstall", "--no-cache-dir",
"--process-dependency-links",
package
])
code = subprocess.call(
[
interpreter,
"-m",
"pip",
"install",
"-U",
"-I",
"--force-reinstall",
"--no-cache-dir",
"--process-dependency-links",
package,
]
)
else:
code = subprocess.call([
interpreter, "-m",
"pip", "install", "-U",
"--process-dependency-links",
package
])
code = subprocess.call(
[interpreter, "-m", "pip", "install", "-U", "--process-dependency-links", package]
)
if code == 0:
print("Red has been updated")
else:
@ -123,7 +134,7 @@ def update_red(dev=False, reinstall=False, voice=False, mongo=False, docs=False,
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:
print("Starting {}...".format(selected_instance))
cmd_list = ["redbot", selected_instance]
@ -153,12 +164,15 @@ def cli_flag_getter():
if choice == "y":
print(
"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()
for p in prefixes:
flags.append("-p {}".format(p))
print("Would you like to disable console input? Please note that features "
"requiring console interaction may fail to work (y/n)")
print(
"Would you like to disable console input? Please note that features "
"requiring console interaction may fail to work (y/n)"
)
choice = user_choice()
if choice == "y":
flags.append("--no-prompt")
@ -169,9 +183,11 @@ def cli_flag_getter():
print("Is this a selfbot? (y/n)")
choice = user_choice()
if choice == "y":
print("Please note that selfbots are not allowed by Discord. See"
"https://support.discordapp.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-"
"for more information.")
print(
"Please note that selfbots are not allowed by Discord. See"
"https://support.discordapp.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-"
"for more information."
)
flags.append("--self-bot")
print("Does this token belong to a user account rather than a bot account? (y/n)")
choice = user_choice()
@ -185,7 +201,9 @@ def cli_flag_getter():
choice = user_choice()
if choice == "y":
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()
if choice == "y":
flags.append("--dev")
@ -218,8 +236,8 @@ def instance_menu():
name_num_map = {}
for name in list(instances.keys()):
print("{}. {}\n".format(counter+1, name))
name_num_map[str(counter+1)] = name
print("{}. {}\n".format(counter + 1, name))
name_num_map[str(counter + 1)] = name
counter += 1
while True:
@ -229,7 +247,7 @@ def instance_menu():
except ValueError:
print("Invalid input! Please enter a number corresponding to an instance.")
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")
else:
return name_num_map[str(selection)]
@ -237,13 +255,15 @@ def instance_menu():
async def reset_red():
instances = load_existing_config()
if not instances:
print("No instance to delete.\n")
return
print("WARNING: You are about to remove ALL Red instances on this computer.")
print("If you want to reset data of only one instance, "
"please select option 5 in the launcher.")
print(
"If you want to reset data of only one instance, "
"please select option 5 in the launcher."
)
await asyncio.sleep(2)
print("\nIf you continue you will remove these instanes.\n")
for instance in list(instances.keys()):
@ -254,7 +274,7 @@ async def reset_red():
if response != "I agree":
print("Cancelling...")
return
if confirm("\nDo you want to create a backup for an instance? (y/n) "):
for index, instance in instances.items():
print("\nRemoving {}...".format(index))
@ -264,7 +284,7 @@ async def reset_red():
for index, instance in instances.items():
await remove_instance(index, instance)
print("All instances have been removed.")
def clear_screen():
if IS_WINDOWS:
@ -290,7 +310,7 @@ def extras_selector():
return selected
def development_choice(reinstall = False):
def development_choice(reinstall=False):
while True:
print("\n")
print("Do you want to install stable or development version?")
@ -301,18 +321,22 @@ def development_choice(reinstall = False):
selected = extras_selector()
if choice == "1":
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,
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
elif choice == "2":
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,
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
@ -332,12 +356,17 @@ def debug_info():
os_info = distro.linux_distribution()
osver = "{} {}".format(os_info[0], os_info[1]).strip()
user_who_ran = getpass.getuser()
info = "Debug Info for Red\n\n" +\
"Python version: {}\n".format(pyver) +\
"Red version: {}\n".format(redver) +\
"OS version: {}\n".format(osver) +\
"System arch: {}\n".format(platform.machine()) +\
"User: {}\n".format(user_who_ran)
info = "Debug Info for Red\n\n" + "Python version: {}\n".format(
pyver
) + "Red version: {}\n".format(
redver
) + "OS version: {}\n".format(
osver
) + "System arch: {}\n".format(
platform.machine()
) + "User: {}\n".format(
user_who_ran
)
print(info)
exit(0)
@ -385,7 +414,9 @@ def main_menu():
loop = asyncio.get_event_loop()
clear_screen()
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("3. Factory reset (discard code changes, reset all data)")
print("\n")
@ -411,27 +442,20 @@ def main_menu():
def main():
if not PYTHON_OK:
raise RuntimeError(
"Red requires Python 3.5 or greater. "
"Please install the correct version!"
"Red requires Python 3.5 or greater. " "Please install the correct version!"
)
if args.debuginfo: # Check first since the function triggers an exit
debug_info()
if args.update and args.update_dev: # Conflicting args, so error out
raise RuntimeError(
"\nUpdate requested but conflicting arguments provided.\n\n"
"Please try again using only one of --update or --update-dev"
)
if args.update:
update_red(
voice=args.voice, docs=args.docs,
test=args.test, mongo=args.mongo
)
update_red(voice=args.voice, docs=args.docs, test=args.test, mongo=args.mongo)
elif args.update_dev:
update_red(
dev=True, voice=args.voice, docs=args.docs,
test=args.test, mongo=args.mongo
)
update_red(dev=True, voice=args.voice, docs=args.docs, test=args.test, mongo=args.mongo)
if INTERACTIVE_MODE:
main_menu()

View File

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