Merge branch 'V3/release/3.0.0' into V3/develop

# Conflicts:
#	redbot/cogs/audio/audio.py
This commit is contained in:
Toby Harradine 2018-12-21 13:37:32 +11:00
commit bdcb69ad37
22 changed files with 422 additions and 204 deletions

View File

@ -1285,6 +1285,9 @@ class Audio(commands.Cog):
url_check = self._url_check(track["info"]["uri"])
if not url_check:
continue
if track["info"]["uri"].startswith("localtracks/"):
if not os.path.isfile(track["info"]["uri"]):
continue
player.add(author_obj, lavalink.rest_api.Track(data=track))
track_count = track_count + 1
embed = discord.Embed(
@ -2005,6 +2008,7 @@ class Audio(commands.Cog):
async def seek(self, ctx, seconds: int = 30):
"""Seek ahead or behind on a track by seconds."""
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
if not self._player_check(ctx):
return await self._embed_msg(ctx, _("Nothing playing."))
player = lavalink.get_player(ctx.guild.id)
@ -2017,6 +2021,13 @@ class Audio(commands.Cog):
ctx, ctx.author
):
return await self._embed_msg(ctx, _("You need the DJ role to use seek."))
if vote_enabled:
if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(
ctx, ctx.author
):
return await self._embed_msg(
ctx, _("There are other people listening - vote to skip instead.")
)
if player.current:
if player.current.is_stream:
return await self._embed_msg(ctx, _("Can't seek on a stream."))

View File

@ -71,13 +71,19 @@ async def get_java_version(loop) -> _JavaVersion:
# ... version "MAJOR.MINOR.PATCH[_BUILD]" ...
# ...
# We only care about the major and minor parts though.
version_line_re = re.compile(r'version "(?P<major>\d+).(?P<minor>\d+).\d+(?:_\d+)?"')
version_line_re = re.compile(
r'version "(?P<major>\d+).(?P<minor>\d+).\d+(?:_\d+)?(?:-[A-Za-z0-9]+)?"'
)
short_version_re = re.compile(r'version "(?P<major>\d+)"')
lines = version_info.splitlines()
for line in lines:
match = version_line_re.search(line)
short_match = short_version_re.search(line)
if match:
return int(match["major"]), int(match["minor"])
elif short_match:
return int(short_match["major"]), 0
raise RuntimeError(
"The output of `java -version` was unexpected. Please report this issue on Red's "

View File

@ -133,8 +133,6 @@ class Cleanup(commands.Cog):
def check(m):
if text in m.content:
return True
elif m == ctx.message:
return True
else:
return False
@ -145,6 +143,7 @@ class Cleanup(commands.Cog):
before=ctx.message,
delete_pinned=delete_pinned,
)
to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format(
author.name, author.id, len(to_delete), text, channel.id
@ -188,8 +187,6 @@ class Cleanup(commands.Cog):
def check(m):
if m.author.id == _id:
return True
elif m == ctx.message:
return True
else:
return False
@ -200,6 +197,8 @@ class Cleanup(commands.Cog):
before=ctx.message,
delete_pinned=delete_pinned,
)
to_delete.append(ctx.message)
reason = (
"{}({}) deleted {} messages "
" made by {}({}) in channel {}."
@ -231,6 +230,7 @@ class Cleanup(commands.Cog):
to_delete = await self.get_messages_for_deletion(
channel=channel, number=None, after=after, delete_pinned=delete_pinned
)
to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages in channel {}.".format(
author.name, author.id, len(to_delete), channel.name
@ -263,6 +263,7 @@ class Cleanup(commands.Cog):
to_delete = await self.get_messages_for_deletion(
channel=channel, number=number, before=before, delete_pinned=delete_pinned
)
to_delete.append(ctx.message)
reason = "{}({}) deleted {} messages in channel {}.".format(
author.name, author.id, len(to_delete), channel.name

View File

@ -502,7 +502,7 @@ class Downloader(commands.Cog):
if isinstance(cog_installable, Installable):
made_by = ", ".join(cog_installable.author) or _("Missing from info.json")
repo = self._repo_manager.get_repo(cog_installable.repo_name)
repo_url = repo.url
repo_url = _("Missing from installed repos") if repo is None else repo.url
cog_name = cog_installable.name
else:
made_by = "26 & co."

View File

@ -388,7 +388,7 @@ class Economy(commands.Cog):
@guild_only_check()
async def payouts(self, ctx: commands.Context):
"""Show the payouts for the slot machine."""
await ctx.author.send(SLOT_PAYOUTS_MSG())
await ctx.author.send(SLOT_PAYOUTS_MSG)
@commands.command()
@guild_only_check()

View File

@ -28,7 +28,7 @@ class RPSParser:
elif argument == "scissors":
self.choice = RPS.scissors
else:
raise ValueError
self.choice = None
@cog_i18n(_)
@ -121,6 +121,8 @@ class General(commands.Cog):
"""Play Rock Paper Scissors."""
author = ctx.author
player_choice = your_choice.choice
if not player_choice:
return await ctx.send("This isn't a valid option. Try rock, paper, or scissors.")
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
cond = {
(RPS.rock, RPS.paper): False,
@ -263,12 +265,13 @@ class General(commands.Cog):
except aiohttp.ClientError:
await ctx.send(
_("No Urban dictionary entries were found, or there was an error in the process")
_("No Urban Dictionary entries were found, or there was an error in the process.")
)
return
if data.get("error") != 404:
if not data["list"]:
return await ctx.send(_("No Urban Dictionary entries were found."))
if await ctx.embed_requested():
# a list of embeds
embeds = []
@ -303,14 +306,14 @@ class General(commands.Cog):
else:
messages = []
for ud in data["list"]:
ud.set_default("example", "N/A")
ud.setdefault("example", "N/A")
description = _("{definition}\n\n**Example:** {example}").format(**ud)
if len(description) > 2048:
description = "{}...".format(description[:2045])
message = _(
"<{permalink}>\n {word} by {author}\n\n{description}\n\n"
"{thumbs_down} Down / {thumbs_up} Up, Powered by urban dictionary"
"{thumbs_down} Down / {thumbs_up} Up, Powered by Urban Dictionary."
).format(word=ud.pop("word").capitalize(), description=description, **ud)
messages.append(message)
@ -325,6 +328,5 @@ class General(commands.Cog):
)
else:
await ctx.send(
_("No Urban dictionary entries were found, or there was an error in the process.")
_("No Urban Dictionary entries were found, or there was an error in the process.")
)
return

View File

@ -311,13 +311,15 @@ class Mod(commands.Cog):
if not cur_setting:
await self.settings.guild(guild).reinvite_on_unban.set(True)
await ctx.send(
_("Users unbanned with {command} will be reinvited.").format(f"{ctx.prefix}unban")
_("Users unbanned with {command} will be reinvited.").format(
command=f"{ctx.prefix}unban"
)
)
else:
await self.settings.guild(guild).reinvite_on_unban.set(False)
await ctx.send(
_("Users unbanned with {command} will not be reinvited.").format(
f"{ctx.prefix}unban"
command=f"{ctx.prefix}unban"
)
)
@ -864,20 +866,46 @@ class Mod(commands.Cog):
@commands.guild_only()
@commands.bot_has_permissions(manage_nicknames=True)
@checks.admin_or_permissions(manage_nicknames=True)
async def rename(self, ctx: commands.Context, user: discord.Member, *, nickname=""):
async def rename(self, ctx: commands.Context, user: discord.Member, *, nickname: str = ""):
"""Change a user's nickname.
Leaving the nickname empty will remove it.
"""
nickname = nickname.strip()
if nickname == "":
me = cast(discord.Member, ctx.me)
if not nickname:
nickname = None
elif not 2 <= len(nickname) <= 32:
await ctx.send(_("Nicknames must be between 2 and 32 characters long."))
return
if not (
(me.guild_permissions.manage_nicknames or me.guild_permissions.administrator)
and me.top_role > user.top_role
and user != ctx.guild.owner
):
await ctx.send(
_(
"I do not have permission to rename that member. They may be higher than or "
"equal to me in the role hierarchy."
)
)
else:
try:
await user.edit(reason=get_audit_reason(ctx.author, None), nick=nickname)
await ctx.send("Done.")
except discord.Forbidden:
# Just in case we missed something in the permissions check above
await ctx.send(_("I do not have permission to rename that member."))
except discord.HTTPException as exc:
if exc.status == 400: # BAD REQUEST
await ctx.send(_("That nickname is invalid."))
else:
await ctx.send(_("An unexpected error has occured."))
else:
await ctx.send(_("Done."))
@commands.group()
@commands.guild_only()
@checks.mod_or_permissions(manage_channel=True)
@checks.mod_or_permissions(manage_channels=True)
async def mute(self, ctx: commands.Context):
"""Mute users."""
pass
@ -1033,7 +1061,7 @@ class Mod(commands.Cog):
@commands.group()
@commands.guild_only()
@commands.bot_has_permissions(manage_roles=True)
@checks.mod_or_permissions(manage_channel=True)
@checks.mod_or_permissions(manage_channels=True)
async def unmute(self, ctx: commands.Context):
"""Unmute users."""
pass
@ -1306,8 +1334,8 @@ class Mod(commands.Cog):
user = author
# A special case for a special someone :^)
special_date = datetime(2016, 1, 10, 6, 8, 4, 443000)
is_special = user.id == 96130341705637888 and guild.id == 133049272517001216
special_date = datetime(2016, 1, 10, 6, 8, 4, 443_000)
is_special = user.id == 96_130_341_705_637_888 and guild.id == 133_049_272_517_001_216
roles = sorted(user.roles)[1:]
names, nicks = await self.get_names_and_nicks(user)
@ -1567,8 +1595,9 @@ class Mod(commands.Cog):
"""
An event for modlog case creation
"""
try:
mod_channel = await modlog.get_modlog_channel(case.guild)
if mod_channel is None:
except RuntimeError:
return
use_embeds = await case.bot.embed_requested(mod_channel, case.guild.me)
case_content = await case.message_content(use_embeds)

View File

@ -1,10 +1,133 @@
from typing import NamedTuple, Union, Optional, cast, Type
import itertools
import re
from typing import NamedTuple, Union, Optional
import discord
from redbot.core import commands
from redbot.core.i18n import Translator
_ = Translator("PermissionsConverters", __file__)
MENTION_RE = re.compile(r"^<?(?:(?:@[!&]?)?|#)(\d{15,21})>?$")
def _match_id(arg: str) -> Optional[int]:
m = MENTION_RE.match(arg)
if m:
return int(m.group(1))
class GlobalUniqueObjectFinder(commands.Converter):
async def convert(
self, ctx: commands.Context, arg: str
) -> Union[discord.Guild, discord.abc.GuildChannel, discord.abc.User, discord.Role]:
bot: commands.Bot = ctx.bot
_id = _match_id(arg)
if _id is not None:
guild: discord.Guild = bot.get_guild(_id)
if guild is not None:
return guild
channel: discord.abc.GuildChannel = bot.get_channel(_id)
if channel is not None:
return channel
user: discord.User = bot.get_user(_id)
if user is not None:
return user
for guild in bot.guilds:
role: discord.Role = guild.get_role(_id)
if role is not None:
return role
objects = itertools.chain(
bot.get_all_channels(),
bot.users,
bot.guilds,
*(filter(lambda r: not r.is_default(), guild.roles) for guild in bot.guilds),
)
maybe_matches = []
for obj in objects:
if obj.name == arg or str(obj) == arg:
maybe_matches.append(obj)
if ctx.guild is not None:
for member in ctx.guild.members:
if member.nick == arg and not any(obj.id == member.id for obj in maybe_matches):
maybe_matches.append(member)
if not maybe_matches:
raise commands.BadArgument(
_(
'"{arg}" was not found. It must be the ID, mention, or name of a server, '
"channel, user or role which the bot can see."
).format(arg=arg)
)
elif len(maybe_matches) == 1:
return maybe_matches[0]
else:
raise commands.BadArgument(
_(
'"{arg}" does not refer to a unique server, channel, user or role. Please use '
"the ID for whatever/whoever you're trying to specify, or mention it/them."
).format(arg=arg)
)
class GuildUniqueObjectFinder(commands.Converter):
async def convert(
self, ctx: commands.Context, arg: str
) -> Union[discord.abc.GuildChannel, discord.Member, discord.Role]:
guild: discord.Guild = ctx.guild
_id = _match_id(arg)
if _id is not None:
channel: discord.abc.GuildChannel = guild.get_channel(_id)
if channel is not None:
return channel
member: discord.Member = guild.get_member(_id)
if member is not None:
return member
role: discord.Role = guild.get_role(_id)
if role is not None and not role.is_default():
return role
objects = itertools.chain(
guild.channels, guild.members, filter(lambda r: not r.is_default(), guild.roles)
)
maybe_matches = []
for obj in objects:
if obj.name == arg or str(obj) == arg:
maybe_matches.append(obj)
try:
if obj.nick == arg:
maybe_matches.append(obj)
except AttributeError:
pass
if not maybe_matches:
raise commands.BadArgument(
_(
'"{arg}" was not found. It must be the ID, mention, or name of a channel, '
"user or role in this server."
).format(arg=arg)
)
elif len(maybe_matches) == 1:
return maybe_matches[0]
else:
raise commands.BadArgument(
_(
'"{arg}" does not refer to a unique channel, user or role. Please use the ID '
"for whatever/whoever you're trying to specify, or mention it/them."
).format(arg=arg)
)
class CogOrCommand(NamedTuple):
type: str

View File

@ -14,7 +14,13 @@ from redbot.core.utils.chat_formatting import box
from redbot.core.utils.menus import start_adding_reactions
from redbot.core.utils.predicates import ReactionPredicate, MessagePredicate
from .converters import CogOrCommand, RuleType, ClearableRuleType
from .converters import (
CogOrCommand,
RuleType,
ClearableRuleType,
GuildUniqueObjectFinder,
GlobalUniqueObjectFinder,
)
_ = Translator("Permissions", __file__)
@ -142,23 +148,20 @@ class Permissions(commands.Cog):
if not command:
return await ctx.send_help()
message = copy(ctx.message)
message.author = user
message.content = "{}{}".format(ctx.prefix, command)
fake_message = copy(ctx.message)
fake_message.author = user
fake_message.content = "{}{}".format(ctx.prefix, command)
com = ctx.bot.get_command(command)
if com is None:
out = _("No such command")
else:
fake_context = await ctx.bot.get_context(fake_message)
try:
testcontext = await ctx.bot.get_context(message, cls=commands.Context)
to_check = [*reversed(com.parents)] + [com]
can = False
for cmd in to_check:
can = await cmd.can_run(testcontext)
if can is False:
break
except commands.CheckFailure:
can = await com.can_run(
fake_context, check_all_parents=True, change_permission_state=False
)
except commands.CommandError:
can = False
out = (
@ -275,7 +278,7 @@ class Permissions(commands.Cog):
ctx: commands.Context,
allow_or_deny: RuleType,
cog_or_command: CogOrCommand,
who_or_what: commands.GlobalPermissionModel,
who_or_what: GlobalUniqueObjectFinder,
):
"""Add a global rule to a command.
@ -303,7 +306,7 @@ class Permissions(commands.Cog):
ctx: commands.Context,
allow_or_deny: RuleType,
cog_or_command: CogOrCommand,
who_or_what: commands.GuildPermissionModel,
who_or_what: GuildUniqueObjectFinder,
):
"""Add a rule to a command in this server.
@ -328,7 +331,7 @@ class Permissions(commands.Cog):
self,
ctx: commands.Context,
cog_or_command: CogOrCommand,
who_or_what: commands.GlobalPermissionModel,
who_or_what: GlobalUniqueObjectFinder,
):
"""Remove a global rule from a command.
@ -351,7 +354,7 @@ class Permissions(commands.Cog):
ctx: commands.Context,
cog_or_command: CogOrCommand,
*,
who_or_what: commands.GuildPermissionModel,
who_or_what: GuildUniqueObjectFinder,
):
"""Remove a server rule from a command.

View File

@ -316,7 +316,7 @@ class Reports(commands.Cog):
self.tunnel_store[k]["msgs"] = msgs
@commands.guild_only()
@checks.mod_or_permissions(manage_members=True)
@checks.mod_or_permissions(manage_roles=True)
@report.command(name="interact")
async def response(self, ctx, ticket_number: int):
"""Open a message tunnel.

View File

@ -28,7 +28,7 @@ from . import streamtypes as _streamtypes
from collections import defaultdict
import asyncio
import re
from typing import Optional, List
from typing import Optional, List, Tuple
CHECK_DELAY = 60
@ -320,6 +320,7 @@ class Streams(commands.Cog):
@commands.group()
@checks.mod()
async def streamset(self, ctx: commands.Context):
"""Set tokens for accessing streams."""
pass
@streamset.command()
@ -396,9 +397,6 @@ class Streams(commands.Cog):
async def role(self, ctx: commands.Context, *, role: discord.Role):
"""Toggle a role mention."""
current_setting = await self.db.role(role).mention()
if not role.mentionable:
await ctx.send("That role is not mentionable!")
return
if current_setting:
await self.db.role(role).mention.set(False)
await ctx.send(
@ -408,11 +406,17 @@ class Streams(commands.Cog):
)
else:
await self.db.role(role).mention.set(True)
await ctx.send(
_(
msg = _(
"When a stream or community is live, `@\u200b{role.name}` will be mentioned."
).format(role=role)
if not role.mentionable:
msg += " " + _(
"Since the role is not mentionable, it will be momentarily made mentionable "
"when announcing a streamalert. Please make sure I have the correct "
"permissions to manage this role, or else members of this role won't receive "
"a notification."
)
await ctx.send(msg)
@streamset.command()
@commands.guild_only()
@ -535,30 +539,46 @@ class Streams(commands.Cog):
continue
for channel_id in stream.channels:
channel = self.bot.get_channel(channel_id)
mention_str = await self._get_mention_str(channel.guild)
mention_str, edited_roles = await self._get_mention_str(channel.guild)
if mention_str:
content = _("{mention}, {stream.name} is live!").format(
mention=mention_str, stream=stream
)
else:
content = _("{stream.name} is live!").format(stream=stream.name)
content = _("{stream.name} is live!").format(stream=stream)
m = await channel.send(content, embed=embed)
stream._messages_cache.append(m)
if edited_roles:
for role in edited_roles:
await role.edit(mentionable=False)
await self.save_streams()
async def _get_mention_str(self, guild: discord.Guild):
async def _get_mention_str(self, guild: discord.Guild) -> Tuple[str, List[discord.Role]]:
"""Returns a 2-tuple with the string containing the mentions, and a list of
all roles which need to have their `mentionable` property set back to False.
"""
settings = self.db.guild(guild)
mentions = []
edited_roles = []
if await settings.mention_everyone():
mentions.append("@everyone")
if await settings.mention_here():
mentions.append("@here")
can_manage_roles = guild.me.guild_permissions.manage_roles
for role in guild.roles:
if await self.db.role(role).mention():
if can_manage_roles and not role.mentionable:
try:
await role.edit(mentionable=True)
except discord.Forbidden:
# Might still be unable to edit role based on hierarchy
pass
else:
edited_roles.append(role)
mentions.append(role.mention)
return " ".join(mentions)
return " ".join(mentions), edited_roles
async def check_communities(self):
for community in self.communities:
@ -589,12 +609,15 @@ class Streams(commands.Cog):
emb = await community.make_embed(streams)
chn_msg = [m for m in community._messages_cache if m.channel == chn]
if not chn_msg:
mentions = await self._get_mention_str(chn.guild)
mentions, roles = await self._get_mention_str(chn.guild)
if mentions:
msg = await chn.send(mentions, embed=emb)
else:
msg = await chn.send(embed=emb)
community._messages_cache.append(msg)
if roles:
for role in roles:
await role.edit(mentionable=False)
await self.save_communities()
else:
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]

View File

@ -114,7 +114,7 @@ class TriviaSession:
async with self.ctx.typing():
await asyncio.sleep(3)
self.count += 1
msg = bold(_("**Question number {num}!").format(num=self.count)) + "\n\n" + question
msg = bold(_("Question number {num}!").format(num=self.count)) + "\n\n" + question
await self.ctx.send(msg)
continue_ = await self.wait_for_answer(answers, delay, timeout)
if continue_ is False:

View File

@ -111,16 +111,14 @@ class Trivia(commands.Cog):
await settings.allow_override.set(enabled)
if enabled:
await ctx.send(
_(
"Done. Trivia lists can now override the trivia settings for this server."
).format(now=enabled)
_("Done. Trivia lists can now override the trivia settings for this server.")
)
else:
await ctx.send(
_(
"Done. Trivia lists can no longer override the trivia settings for this "
"server."
).format(now=enabled)
)
)
@triviaset.command(name="botplays", usage="<true_or_false>")
@ -506,7 +504,7 @@ class Trivia(commands.Cog):
with path.open(encoding="utf-8") as file:
try:
dict_ = yaml.load(file)
dict_ = yaml.safe_load(file)
except yaml.error.YAMLError as exc:
raise InvalidListError("YAML parsing failed.") from exc
else:

View File

@ -186,13 +186,23 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
async def is_admin(self, member: discord.Member):
"""Checks if a member is an admin of their guild."""
admin_role = await self.db.guild(member.guild).admin_role()
return any(role.id == admin_role for role in member.roles)
try:
if any(role.id == admin_role for role in member.roles):
return True
except AttributeError: # someone passed a webhook to this
pass
return False
async def is_mod(self, member: discord.Member):
"""Checks if a member is a mod or admin of their guild."""
mod_role = await self.db.guild(member.guild).mod_role()
admin_role = await self.db.guild(member.guild).admin_role()
return any(role.id in (mod_role, admin_role) for role in member.roles)
try:
if any(role.id in (mod_role, admin_role) for role in member.roles):
return True
except AttributeError: # someone passed a webhook to this
pass
return False
async def get_context(self, message, *, cls=commands.Context):
return await super().get_context(message, cls=cls)
@ -334,7 +344,13 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
ids_to_check = [to_check.id]
else:
author = getattr(to_check, "author", to_check)
try:
ids_to_check = [r.id for r in author.roles]
except AttributeError:
# webhook messages are a user not member,
# cheaper than isinstance
return True # webhooks require significant permissions to enable.
else:
ids_to_check.append(author.id)
immune_ids = await self.db.guild(guild).autoimmune_ids()

View File

@ -157,12 +157,31 @@ class Command(CogCommandMixin, commands.Command):
cmd = cmd.parent
return sorted(entries, key=lambda x: len(x.qualified_name), reverse=True)
async def can_run(self, ctx: "Context") -> bool:
# noinspection PyMethodOverriding
async def can_run(
self,
ctx: "Context",
*,
check_all_parents: bool = False,
change_permission_state: bool = False,
) -> bool:
"""Check if this command can be run in the given context.
This function first checks if the command can be run using
discord.py's method `discord.ext.commands.Command.can_run`,
then will return the result of `Requires.verify`.
Keyword Arguments
-----------------
check_all_parents : bool
If ``True``, this will check permissions for all of this
command's parents and its cog as well as the command
itself. Defaults to ``False``.
change_permission_state : bool
Whether or not the permission state should be changed as
a result of this call. For most cases this should be
``False``. Defaults to ``False``.
"""
ret = await super().can_run(ctx)
if ret is False:
@ -171,8 +190,21 @@ class Command(CogCommandMixin, commands.Command):
# This is so contexts invoking other commands can be checked with
# this command as well
original_command = ctx.command
original_state = ctx.permission_state
ctx.command = self
if check_all_parents is True:
# Since we're starting from the beginning, we should reset the state to normal
ctx.permission_state = PermState.NORMAL
for parent in reversed(self.parents):
try:
result = await parent.can_run(ctx, change_permission_state=True)
except commands.CommandError:
result = False
if result is False:
return False
if self.parent is None and self.instance is not None:
# For top-level commands, we need to check the cog's requires too
ret = await self.instance.requires.verify(ctx)
@ -183,6 +215,17 @@ class Command(CogCommandMixin, commands.Command):
return await self.requires.verify(ctx)
finally:
ctx.command = original_command
if not change_permission_state:
ctx.permission_state = original_state
async def _verify_checks(self, ctx):
if not self.enabled:
raise commands.DisabledCommand(f"{self.name} command is disabled")
if not (await self.can_run(ctx, change_permission_state=True)):
raise commands.CheckFailure(
f"The check functions for command {self.qualified_name} failed."
)
async def do_conversion(
self, ctx: "Context", converter, argument: str, param: inspect.Parameter
@ -238,7 +281,9 @@ class Command(CogCommandMixin, commands.Command):
if cmd.hidden:
return False
try:
can_run = await self.can_run(ctx)
can_run = await self.can_run(
ctx, check_all_parents=True, change_permission_state=False
)
except commands.CheckFailure:
return False
else:

View File

@ -281,12 +281,14 @@ class Requires:
if isinstance(user_perms, dict):
self.user_perms: Optional[discord.Permissions] = discord.Permissions.none()
_validate_perms_dict(user_perms)
self.user_perms.update(**user_perms)
else:
self.user_perms = user_perms
if isinstance(bot_perms, dict):
self.bot_perms: discord.Permissions = discord.Permissions.none()
_validate_perms_dict(bot_perms)
self.bot_perms.update(**bot_perms)
else:
self.bot_perms = bot_perms
@ -311,6 +313,7 @@ class Requires:
if user_perms is None:
func.requires.user_perms = None
else:
_validate_perms_dict(user_perms)
func.requires.user_perms.update(**user_perms)
return func
@ -449,7 +452,20 @@ class Requires:
should_invoke = await self._verify_user(ctx)
elif isinstance(next_state, dict):
# NORMAL to PASSIVE_ALLOW; should we proceed as normal or transition?
next_state = next_state[await self._verify_user(ctx)]
# We must check what would happen normally, if no explicit rules were set.
default_rule = PermState.NORMAL
if ctx.guild is not None:
default_rule = self.get_default_guild_rule(guild_id=ctx.guild.id)
if default_rule is PermState.NORMAL:
default_rule = self.default_global_rule
if default_rule == PermState.ACTIVE_DENY:
would_invoke = False
elif default_rule == PermState.ACTIVE_ALLOW:
would_invoke = True
else:
would_invoke = await self._verify_user(ctx)
next_state = next_state[would_invoke]
ctx.permission_state = next_state
return should_invoke
@ -588,6 +604,7 @@ def bot_has_permissions(**perms: bool):
if asyncio.iscoroutinefunction(func):
func.__requires_bot_perms__ = perms
else:
_validate_perms_dict(perms)
func.requires.bot_perms.update(**perms)
return func
@ -599,6 +616,8 @@ def has_permissions(**perms: bool):
This check can be overridden by rules.
"""
if perms is None:
raise TypeError("Must provide at least one keyword argument to has_permissions")
return Requires.get_decorator(None, perms)
@ -670,3 +689,20 @@ class _IntKeyDict(Dict[int, _T]):
if not isinstance(key, int):
raise TypeError("Keys must be of type `int`")
return super().__setitem__(key, value)
def _validate_perms_dict(perms: Dict[str, bool]) -> None:
for perm, value in perms.items():
try:
attr = getattr(discord.Permissions, perm)
except AttributeError:
attr = None
if attr is None or not isinstance(attr, property):
# We reject invalid permissions
raise TypeError(f"Unknown permission name '{perm}'")
if value is not True:
# We reject any permission not specified as 'True', since this is the only value which
# makes practical sense.
raise TypeError(f"Permission {perm} may only be specified as 'True', not {value}")

View File

@ -468,7 +468,7 @@ class Core(commands.Cog, CoreLogic):
pred = MessagePredicate.yes_or_no(ctx)
try:
await self.bot.wait_for("message", check=MessagePredicate.yes_or_no(ctx))
await self.bot.wait_for("message", check=pred)
except asyncio.TimeoutError:
await ctx.send("Response timed out.")
return
@ -1729,7 +1729,7 @@ class Core(commands.Cog, CoreLogic):
await ctx.tick()
@commands.guild_only()
@checks.guildowner_or_permissions(manage_server=True)
@checks.guildowner_or_permissions(manage_guild=True)
@commands.group(name="autoimmune")
async def autoimmune_group(self, ctx: commands.Context):
"""

View File

@ -1,15 +1,15 @@
import sys
import os
from pathlib import Path
from typing import List
from copy import deepcopy
import hashlib
import shutil
import inspect
import logging
import os
import sys
import tempfile
from copy import deepcopy
from pathlib import Path
import appdirs
import tempfile
from discord.utils import deprecated
from . import commands
from .json_io import JsonIO
__all__ = [
@ -153,124 +153,28 @@ def core_data_path() -> Path:
return core_path.resolve()
def _find_data_files(init_location: str) -> (Path, List[Path]):
"""
Discovers all files in the bundled data folder of an installed cog.
Parameters
----------
init_location
Returns
-------
(pathlib.Path, list of pathlib.Path)
"""
init_file = Path(init_location)
if not init_file.is_file():
return []
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()]
def _compare_and_copy(to_copy: List[Path], bundled_data_dir: Path, cog_data_dir: Path):
"""
Filters out files from ``to_copy`` that already exist, and are the
same, in ``data_dir``. The files that are different are copied into
``data_dir``.
Parameters
----------
to_copy : list of pathlib.Path
bundled_data_dir : pathlib.Path
cog_data_dir : pathlib.Path
"""
def hash_bytestr_iter(bytesiter, hasher, ashexstr=False):
for block in bytesiter:
hasher.update(block)
return hasher.hexdigest() if ashexstr else hasher.digest()
def file_as_blockiter(afile, blocksize=65536):
with afile:
block = afile.read(blocksize)
while len(block) > 0:
yield block
block = afile.read(blocksize)
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()
)
orig_checksum = ...
if exists_checksum is not None:
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))
# noinspection PyUnusedLocal
@deprecated("bundled_data_path() without calling this function")
def load_bundled_data(cog_instance, init_location: str):
pass
def bundled_data_path(cog_instance: commands.Cog) -> Path:
"""
This function copies (and overwrites) data from the ``data/`` folder
of the installed cog.
Get the path to the "data" directory bundled with this cog.
The bundled data folder must be located alongside the ``.py`` file
which contains the cog class.
.. important::
This function MUST be called from the ``setup()`` function of your
cog.
Examples
--------
>>> from redbot.core import data_manager
>>>
>>> def setup(bot):
>>> cog = MyCog()
>>> data_manager.load_bundled_data(cog, __file__)
>>> bot.add_cog(cog)
Parameters
----------
cog_instance
An instance of your cog class.
init_location : str
The ``__file__`` attribute of the file where your ``setup()``
function exists.
"""
bundled_data_folder, to_copy = _find_data_files(init_location)
cog_data_folder = cog_data_path(cog_instance) / "bundled_data"
_compare_and_copy(to_copy, bundled_data_folder, cog_data_folder)
def bundled_data_path(cog_instance) -> Path:
"""
The "data" directory that has been copied from installed cogs.
.. important::
You should *NEVER* write to this directory. Data manager will
overwrite files in this directory each time `load_bundled_data`
is called. You should instead write to the directory provided by
`cog_data_path`.
You should *NEVER* write to this directory.
Parameters
----------
cog_instance
An instance of your cog. If calling from a command or method of
your cog, this should be ``self``.
Returns
-------
@ -280,10 +184,10 @@ def bundled_data_path(cog_instance) -> Path:
Raises
------
FileNotFoundError
If no bundled data folder exists or if it hasn't been loaded yet.
"""
If no bundled data folder exists.
bundled_path = cog_data_path(cog_instance) / "bundled_data"
"""
bundled_path = Path(inspect.getfile(cog_instance.__class__)).parent / "data"
if not bundled_path.is_dir():
raise FileNotFoundError("No such directory {}".format(bundled_path))

View File

@ -23,6 +23,7 @@ discord.py 1.0.0a
This help formatter contains work by Rapptz (Danny) and SirThane#1780.
"""
import contextlib
from collections import namedtuple
from typing import List, Optional, Union
@ -224,8 +225,8 @@ class Help(dpy_formatter.HelpFormatter):
return ret
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?
async def format_help_for(self, ctx, command_or_bot, reason: str = ""):
"""Formats the help page and handles the actual heavy lifting of how
the help command looks like. To change the behaviour, override the
:meth:`~.HelpFormatter.format` method.
@ -244,10 +245,24 @@ class Help(dpy_formatter.HelpFormatter):
"""
self.context = ctx
self.command = command_or_bot
# We want the permission state to be set as if the author had run the command he is
# requesting help for. This is so the subcommands shown in the help menu correctly reflect
# any permission rules set.
if isinstance(self.command, commands.Command):
with contextlib.suppress(commands.CommandError):
await self.command.can_run(
self.context, check_all_parents=True, change_permission_state=True
)
elif isinstance(self.command, commands.Cog):
with contextlib.suppress(commands.CommandError):
# Cog's don't have a `can_run` method, so we use the `Requires` object directly.
await self.command.requires.verify(self.context)
emb = await self.format()
if reason:
emb["embed"]["title"] = "{0}".format(reason)
emb["embed"]["title"] = reason
ret = []

View File

@ -3,6 +3,7 @@ import json
import os
import asyncio
import logging
from copy import deepcopy
from uuid import uuid4
# This is basically our old DataIO and just a base for much more elaborate classes
@ -69,7 +70,11 @@ class JsonIO:
async def _threadsafe_save_json(self, data, settings=PRETTY):
loop = asyncio.get_event_loop()
func = functools.partial(self._save_json, data, settings)
# the deepcopy is needed here. otherwise,
# the dict can change during serialization
# and this will break the encoder.
data_copy = deepcopy(data)
func = functools.partial(self._save_json, data_copy, settings)
async with self._lock:
await loop.run_in_executor(None, func)

View File

@ -666,29 +666,30 @@ 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) -> discord.TextChannel:
"""
Get the current modlog channel
Get the current modlog channel.
Parameters
----------
guild: `discord.Guild`
The guild to get the modlog channel for
The guild to get the modlog channel for.
Returns
-------
`discord.TextChannel` or `None`
The channel object representing the modlog channel
`discord.TextChannel`
The channel object representing the modlog channel.
Raises
------
RuntimeError
If the modlog channel is not found
If the modlog channel is not found.
"""
if hasattr(guild, "get_channel"):
channel = guild.get_channel(await _conf.guild(guild).mod_log())
else:
# For unit tests only
channel = await _conf.guild(guild).mod_log()
if channel is None:
raise RuntimeError("Failed to get the mod log channel!")

View File

@ -10,7 +10,7 @@ def test_trivia_lists():
for l in list_names:
with l.open() as f:
try:
dict_ = yaml.load(f)
dict_ = yaml.safe_load(f)
except yaml.error.YAMLError as e:
problem_lists.append((l.stem, "YAML error:\n{!s}".format(e)))
else: