mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 11:18:54 -05:00
Merge branch 'V3/release/3.0.0' into V3/develop
# Conflicts: # redbot/cogs/audio/audio.py
This commit is contained in:
commit
bdcb69ad37
@ -1285,6 +1285,9 @@ class Audio(commands.Cog):
|
|||||||
url_check = self._url_check(track["info"]["uri"])
|
url_check = self._url_check(track["info"]["uri"])
|
||||||
if not url_check:
|
if not url_check:
|
||||||
continue
|
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))
|
player.add(author_obj, lavalink.rest_api.Track(data=track))
|
||||||
track_count = track_count + 1
|
track_count = track_count + 1
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
@ -2005,6 +2008,7 @@ class Audio(commands.Cog):
|
|||||||
async def seek(self, ctx, seconds: int = 30):
|
async def seek(self, ctx, seconds: int = 30):
|
||||||
"""Seek ahead or behind on a track by seconds."""
|
"""Seek ahead or behind on a track by seconds."""
|
||||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
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):
|
if not self._player_check(ctx):
|
||||||
return await self._embed_msg(ctx, _("Nothing playing."))
|
return await self._embed_msg(ctx, _("Nothing playing."))
|
||||||
player = lavalink.get_player(ctx.guild.id)
|
player = lavalink.get_player(ctx.guild.id)
|
||||||
@ -2017,6 +2021,13 @@ class Audio(commands.Cog):
|
|||||||
ctx, ctx.author
|
ctx, ctx.author
|
||||||
):
|
):
|
||||||
return await self._embed_msg(ctx, _("You need the DJ role to use seek."))
|
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:
|
||||||
if player.current.is_stream:
|
if player.current.is_stream:
|
||||||
return await self._embed_msg(ctx, _("Can't seek on a stream."))
|
return await self._embed_msg(ctx, _("Can't seek on a stream."))
|
||||||
|
|||||||
@ -71,13 +71,19 @@ async def get_java_version(loop) -> _JavaVersion:
|
|||||||
# ... version "MAJOR.MINOR.PATCH[_BUILD]" ...
|
# ... version "MAJOR.MINOR.PATCH[_BUILD]" ...
|
||||||
# ...
|
# ...
|
||||||
# We only care about the major and minor parts though.
|
# 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()
|
lines = version_info.splitlines()
|
||||||
for line in lines:
|
for line in lines:
|
||||||
match = version_line_re.search(line)
|
match = version_line_re.search(line)
|
||||||
|
short_match = short_version_re.search(line)
|
||||||
if match:
|
if match:
|
||||||
return int(match["major"]), int(match["minor"])
|
return int(match["major"]), int(match["minor"])
|
||||||
|
elif short_match:
|
||||||
|
return int(short_match["major"]), 0
|
||||||
|
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"The output of `java -version` was unexpected. Please report this issue on Red's "
|
"The output of `java -version` was unexpected. Please report this issue on Red's "
|
||||||
|
|||||||
@ -133,8 +133,6 @@ class Cleanup(commands.Cog):
|
|||||||
def check(m):
|
def check(m):
|
||||||
if text in m.content:
|
if text in m.content:
|
||||||
return True
|
return True
|
||||||
elif m == ctx.message:
|
|
||||||
return True
|
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -145,6 +143,7 @@ class Cleanup(commands.Cog):
|
|||||||
before=ctx.message,
|
before=ctx.message,
|
||||||
delete_pinned=delete_pinned,
|
delete_pinned=delete_pinned,
|
||||||
)
|
)
|
||||||
|
to_delete.append(ctx.message)
|
||||||
|
|
||||||
reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format(
|
reason = "{}({}) deleted {} messages containing '{}' in channel {}.".format(
|
||||||
author.name, author.id, len(to_delete), text, channel.id
|
author.name, author.id, len(to_delete), text, channel.id
|
||||||
@ -188,8 +187,6 @@ class Cleanup(commands.Cog):
|
|||||||
def check(m):
|
def check(m):
|
||||||
if m.author.id == _id:
|
if m.author.id == _id:
|
||||||
return True
|
return True
|
||||||
elif m == ctx.message:
|
|
||||||
return True
|
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -200,6 +197,8 @@ class Cleanup(commands.Cog):
|
|||||||
before=ctx.message,
|
before=ctx.message,
|
||||||
delete_pinned=delete_pinned,
|
delete_pinned=delete_pinned,
|
||||||
)
|
)
|
||||||
|
to_delete.append(ctx.message)
|
||||||
|
|
||||||
reason = (
|
reason = (
|
||||||
"{}({}) deleted {} messages "
|
"{}({}) deleted {} messages "
|
||||||
" made by {}({}) in channel {}."
|
" made by {}({}) in channel {}."
|
||||||
@ -231,6 +230,7 @@ class Cleanup(commands.Cog):
|
|||||||
to_delete = await self.get_messages_for_deletion(
|
to_delete = await self.get_messages_for_deletion(
|
||||||
channel=channel, number=None, after=after, delete_pinned=delete_pinned
|
channel=channel, number=None, after=after, delete_pinned=delete_pinned
|
||||||
)
|
)
|
||||||
|
to_delete.append(ctx.message)
|
||||||
|
|
||||||
reason = "{}({}) deleted {} messages in channel {}.".format(
|
reason = "{}({}) deleted {} messages in channel {}.".format(
|
||||||
author.name, author.id, len(to_delete), channel.name
|
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(
|
to_delete = await self.get_messages_for_deletion(
|
||||||
channel=channel, number=number, before=before, delete_pinned=delete_pinned
|
channel=channel, number=number, before=before, delete_pinned=delete_pinned
|
||||||
)
|
)
|
||||||
|
to_delete.append(ctx.message)
|
||||||
|
|
||||||
reason = "{}({}) deleted {} messages in channel {}.".format(
|
reason = "{}({}) deleted {} messages in channel {}.".format(
|
||||||
author.name, author.id, len(to_delete), channel.name
|
author.name, author.id, len(to_delete), channel.name
|
||||||
|
|||||||
@ -502,7 +502,7 @@ class Downloader(commands.Cog):
|
|||||||
if isinstance(cog_installable, Installable):
|
if isinstance(cog_installable, Installable):
|
||||||
made_by = ", ".join(cog_installable.author) or _("Missing from info.json")
|
made_by = ", ".join(cog_installable.author) or _("Missing from info.json")
|
||||||
repo = self._repo_manager.get_repo(cog_installable.repo_name)
|
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
|
cog_name = cog_installable.name
|
||||||
else:
|
else:
|
||||||
made_by = "26 & co."
|
made_by = "26 & co."
|
||||||
|
|||||||
@ -388,7 +388,7 @@ class Economy(commands.Cog):
|
|||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
async def payouts(self, ctx: commands.Context):
|
async def payouts(self, ctx: commands.Context):
|
||||||
"""Show the payouts for the slot machine."""
|
"""Show the payouts for the slot machine."""
|
||||||
await ctx.author.send(SLOT_PAYOUTS_MSG())
|
await ctx.author.send(SLOT_PAYOUTS_MSG)
|
||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@guild_only_check()
|
@guild_only_check()
|
||||||
|
|||||||
@ -28,7 +28,7 @@ class RPSParser:
|
|||||||
elif argument == "scissors":
|
elif argument == "scissors":
|
||||||
self.choice = RPS.scissors
|
self.choice = RPS.scissors
|
||||||
else:
|
else:
|
||||||
raise ValueError
|
self.choice = None
|
||||||
|
|
||||||
|
|
||||||
@cog_i18n(_)
|
@cog_i18n(_)
|
||||||
@ -121,6 +121,8 @@ class General(commands.Cog):
|
|||||||
"""Play Rock Paper Scissors."""
|
"""Play Rock Paper Scissors."""
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
player_choice = your_choice.choice
|
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))
|
red_choice = choice((RPS.rock, RPS.paper, RPS.scissors))
|
||||||
cond = {
|
cond = {
|
||||||
(RPS.rock, RPS.paper): False,
|
(RPS.rock, RPS.paper): False,
|
||||||
@ -263,12 +265,13 @@ class General(commands.Cog):
|
|||||||
|
|
||||||
except aiohttp.ClientError:
|
except aiohttp.ClientError:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("No Urban dictionary entries were found, or there was an error in the process")
|
_("No Urban Dictionary entries were found, or there was an error in the process.")
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if data.get("error") != 404:
|
if data.get("error") != 404:
|
||||||
|
if not data["list"]:
|
||||||
|
return await ctx.send(_("No Urban Dictionary entries were found."))
|
||||||
if await ctx.embed_requested():
|
if await ctx.embed_requested():
|
||||||
# a list of embeds
|
# a list of embeds
|
||||||
embeds = []
|
embeds = []
|
||||||
@ -303,14 +306,14 @@ class General(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
messages = []
|
messages = []
|
||||||
for ud in data["list"]:
|
for ud in data["list"]:
|
||||||
ud.set_default("example", "N/A")
|
ud.setdefault("example", "N/A")
|
||||||
description = _("{definition}\n\n**Example:** {example}").format(**ud)
|
description = _("{definition}\n\n**Example:** {example}").format(**ud)
|
||||||
if len(description) > 2048:
|
if len(description) > 2048:
|
||||||
description = "{}...".format(description[:2045])
|
description = "{}...".format(description[:2045])
|
||||||
|
|
||||||
message = _(
|
message = _(
|
||||||
"<{permalink}>\n {word} by {author}\n\n{description}\n\n"
|
"<{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)
|
).format(word=ud.pop("word").capitalize(), description=description, **ud)
|
||||||
messages.append(message)
|
messages.append(message)
|
||||||
|
|
||||||
@ -325,6 +328,5 @@ class General(commands.Cog):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("No Urban dictionary entries were found, or there was an error in the process.")
|
_("No Urban Dictionary entries were found, or there was an error in the process.")
|
||||||
)
|
)
|
||||||
return
|
|
||||||
|
|||||||
@ -311,13 +311,15 @@ class Mod(commands.Cog):
|
|||||||
if not cur_setting:
|
if not cur_setting:
|
||||||
await self.settings.guild(guild).reinvite_on_unban.set(True)
|
await self.settings.guild(guild).reinvite_on_unban.set(True)
|
||||||
await ctx.send(
|
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:
|
else:
|
||||||
await self.settings.guild(guild).reinvite_on_unban.set(False)
|
await self.settings.guild(guild).reinvite_on_unban.set(False)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_("Users unbanned with {command} will not be reinvited.").format(
|
_("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.guild_only()
|
||||||
@commands.bot_has_permissions(manage_nicknames=True)
|
@commands.bot_has_permissions(manage_nicknames=True)
|
||||||
@checks.admin_or_permissions(manage_nicknames=True)
|
@checks.admin_or_permissions(manage_nicknames=True)
|
||||||
async def rename(self, ctx: commands.Context, user: discord.Member, *, nickname=""):
|
async def rename(self, ctx: commands.Context, user: discord.Member, *, nickname: str = ""):
|
||||||
"""Change a user's nickname.
|
"""Change a user's nickname.
|
||||||
|
|
||||||
Leaving the nickname empty will remove it.
|
Leaving the nickname empty will remove it.
|
||||||
"""
|
"""
|
||||||
nickname = nickname.strip()
|
nickname = nickname.strip()
|
||||||
if nickname == "":
|
me = cast(discord.Member, ctx.me)
|
||||||
|
if not nickname:
|
||||||
nickname = None
|
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 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.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.mod_or_permissions(manage_channel=True)
|
@checks.mod_or_permissions(manage_channels=True)
|
||||||
async def mute(self, ctx: commands.Context):
|
async def mute(self, ctx: commands.Context):
|
||||||
"""Mute users."""
|
"""Mute users."""
|
||||||
pass
|
pass
|
||||||
@ -1033,7 +1061,7 @@ class Mod(commands.Cog):
|
|||||||
@commands.group()
|
@commands.group()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.bot_has_permissions(manage_roles=True)
|
@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):
|
async def unmute(self, ctx: commands.Context):
|
||||||
"""Unmute users."""
|
"""Unmute users."""
|
||||||
pass
|
pass
|
||||||
@ -1306,8 +1334,8 @@ class Mod(commands.Cog):
|
|||||||
user = author
|
user = author
|
||||||
|
|
||||||
# A special case for a special someone :^)
|
# A special case for a special someone :^)
|
||||||
special_date = datetime(2016, 1, 10, 6, 8, 4, 443000)
|
special_date = datetime(2016, 1, 10, 6, 8, 4, 443_000)
|
||||||
is_special = user.id == 96130341705637888 and guild.id == 133049272517001216
|
is_special = user.id == 96_130_341_705_637_888 and guild.id == 133_049_272_517_001_216
|
||||||
|
|
||||||
roles = sorted(user.roles)[1:]
|
roles = sorted(user.roles)[1:]
|
||||||
names, nicks = await self.get_names_and_nicks(user)
|
names, nicks = await self.get_names_and_nicks(user)
|
||||||
@ -1567,8 +1595,9 @@ class Mod(commands.Cog):
|
|||||||
"""
|
"""
|
||||||
An event for modlog case creation
|
An event for modlog case creation
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
mod_channel = await modlog.get_modlog_channel(case.guild)
|
mod_channel = await modlog.get_modlog_channel(case.guild)
|
||||||
if mod_channel is None:
|
except RuntimeError:
|
||||||
return
|
return
|
||||||
use_embeds = await case.bot.embed_requested(mod_channel, case.guild.me)
|
use_embeds = await case.bot.embed_requested(mod_channel, case.guild.me)
|
||||||
case_content = await case.message_content(use_embeds)
|
case_content = await case.message_content(use_embeds)
|
||||||
|
|||||||
@ -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 import commands
|
||||||
from redbot.core.i18n import Translator
|
from redbot.core.i18n import Translator
|
||||||
|
|
||||||
_ = Translator("PermissionsConverters", __file__)
|
_ = 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):
|
class CogOrCommand(NamedTuple):
|
||||||
type: str
|
type: str
|
||||||
|
|||||||
@ -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.menus import start_adding_reactions
|
||||||
from redbot.core.utils.predicates import ReactionPredicate, MessagePredicate
|
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__)
|
_ = Translator("Permissions", __file__)
|
||||||
|
|
||||||
@ -142,23 +148,20 @@ class Permissions(commands.Cog):
|
|||||||
if not command:
|
if not command:
|
||||||
return await ctx.send_help()
|
return await ctx.send_help()
|
||||||
|
|
||||||
message = copy(ctx.message)
|
fake_message = copy(ctx.message)
|
||||||
message.author = user
|
fake_message.author = user
|
||||||
message.content = "{}{}".format(ctx.prefix, command)
|
fake_message.content = "{}{}".format(ctx.prefix, command)
|
||||||
|
|
||||||
com = ctx.bot.get_command(command)
|
com = ctx.bot.get_command(command)
|
||||||
if com is None:
|
if com is None:
|
||||||
out = _("No such command")
|
out = _("No such command")
|
||||||
else:
|
else:
|
||||||
|
fake_context = await ctx.bot.get_context(fake_message)
|
||||||
try:
|
try:
|
||||||
testcontext = await ctx.bot.get_context(message, cls=commands.Context)
|
can = await com.can_run(
|
||||||
to_check = [*reversed(com.parents)] + [com]
|
fake_context, check_all_parents=True, change_permission_state=False
|
||||||
can = False
|
)
|
||||||
for cmd in to_check:
|
except commands.CommandError:
|
||||||
can = await cmd.can_run(testcontext)
|
|
||||||
if can is False:
|
|
||||||
break
|
|
||||||
except commands.CheckFailure:
|
|
||||||
can = False
|
can = False
|
||||||
|
|
||||||
out = (
|
out = (
|
||||||
@ -275,7 +278,7 @@ class Permissions(commands.Cog):
|
|||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
allow_or_deny: RuleType,
|
allow_or_deny: RuleType,
|
||||||
cog_or_command: CogOrCommand,
|
cog_or_command: CogOrCommand,
|
||||||
who_or_what: commands.GlobalPermissionModel,
|
who_or_what: GlobalUniqueObjectFinder,
|
||||||
):
|
):
|
||||||
"""Add a global rule to a command.
|
"""Add a global rule to a command.
|
||||||
|
|
||||||
@ -303,7 +306,7 @@ class Permissions(commands.Cog):
|
|||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
allow_or_deny: RuleType,
|
allow_or_deny: RuleType,
|
||||||
cog_or_command: CogOrCommand,
|
cog_or_command: CogOrCommand,
|
||||||
who_or_what: commands.GuildPermissionModel,
|
who_or_what: GuildUniqueObjectFinder,
|
||||||
):
|
):
|
||||||
"""Add a rule to a command in this server.
|
"""Add a rule to a command in this server.
|
||||||
|
|
||||||
@ -328,7 +331,7 @@ class Permissions(commands.Cog):
|
|||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
cog_or_command: CogOrCommand,
|
cog_or_command: CogOrCommand,
|
||||||
who_or_what: commands.GlobalPermissionModel,
|
who_or_what: GlobalUniqueObjectFinder,
|
||||||
):
|
):
|
||||||
"""Remove a global rule from a command.
|
"""Remove a global rule from a command.
|
||||||
|
|
||||||
@ -351,7 +354,7 @@ class Permissions(commands.Cog):
|
|||||||
ctx: commands.Context,
|
ctx: commands.Context,
|
||||||
cog_or_command: CogOrCommand,
|
cog_or_command: CogOrCommand,
|
||||||
*,
|
*,
|
||||||
who_or_what: commands.GuildPermissionModel,
|
who_or_what: GuildUniqueObjectFinder,
|
||||||
):
|
):
|
||||||
"""Remove a server rule from a command.
|
"""Remove a server rule from a command.
|
||||||
|
|
||||||
|
|||||||
@ -316,7 +316,7 @@ class Reports(commands.Cog):
|
|||||||
self.tunnel_store[k]["msgs"] = msgs
|
self.tunnel_store[k]["msgs"] = msgs
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.mod_or_permissions(manage_members=True)
|
@checks.mod_or_permissions(manage_roles=True)
|
||||||
@report.command(name="interact")
|
@report.command(name="interact")
|
||||||
async def response(self, ctx, ticket_number: int):
|
async def response(self, ctx, ticket_number: int):
|
||||||
"""Open a message tunnel.
|
"""Open a message tunnel.
|
||||||
|
|||||||
@ -28,7 +28,7 @@ from . import streamtypes as _streamtypes
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
import re
|
||||||
from typing import Optional, List
|
from typing import Optional, List, Tuple
|
||||||
|
|
||||||
CHECK_DELAY = 60
|
CHECK_DELAY = 60
|
||||||
|
|
||||||
@ -320,6 +320,7 @@ class Streams(commands.Cog):
|
|||||||
@commands.group()
|
@commands.group()
|
||||||
@checks.mod()
|
@checks.mod()
|
||||||
async def streamset(self, ctx: commands.Context):
|
async def streamset(self, ctx: commands.Context):
|
||||||
|
"""Set tokens for accessing streams."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@streamset.command()
|
@streamset.command()
|
||||||
@ -396,9 +397,6 @@ class Streams(commands.Cog):
|
|||||||
async def role(self, ctx: commands.Context, *, role: discord.Role):
|
async def role(self, ctx: commands.Context, *, role: discord.Role):
|
||||||
"""Toggle a role mention."""
|
"""Toggle a role mention."""
|
||||||
current_setting = await self.db.role(role).mention()
|
current_setting = await self.db.role(role).mention()
|
||||||
if not role.mentionable:
|
|
||||||
await ctx.send("That role is not mentionable!")
|
|
||||||
return
|
|
||||||
if current_setting:
|
if current_setting:
|
||||||
await self.db.role(role).mention.set(False)
|
await self.db.role(role).mention.set(False)
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
@ -408,11 +406,17 @@ class Streams(commands.Cog):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await self.db.role(role).mention.set(True)
|
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."
|
"When a stream or community is live, `@\u200b{role.name}` will be mentioned."
|
||||||
).format(role=role)
|
).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()
|
@streamset.command()
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@ -535,30 +539,46 @@ class Streams(commands.Cog):
|
|||||||
continue
|
continue
|
||||||
for channel_id in stream.channels:
|
for channel_id in stream.channels:
|
||||||
channel = self.bot.get_channel(channel_id)
|
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:
|
if mention_str:
|
||||||
content = _("{mention}, {stream.name} is live!").format(
|
content = _("{mention}, {stream.name} is live!").format(
|
||||||
mention=mention_str, stream=stream
|
mention=mention_str, stream=stream
|
||||||
)
|
)
|
||||||
else:
|
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)
|
m = await channel.send(content, embed=embed)
|
||||||
stream._messages_cache.append(m)
|
stream._messages_cache.append(m)
|
||||||
|
if edited_roles:
|
||||||
|
for role in edited_roles:
|
||||||
|
await role.edit(mentionable=False)
|
||||||
await self.save_streams()
|
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)
|
settings = self.db.guild(guild)
|
||||||
mentions = []
|
mentions = []
|
||||||
|
edited_roles = []
|
||||||
if await settings.mention_everyone():
|
if await settings.mention_everyone():
|
||||||
mentions.append("@everyone")
|
mentions.append("@everyone")
|
||||||
if await settings.mention_here():
|
if await settings.mention_here():
|
||||||
mentions.append("@here")
|
mentions.append("@here")
|
||||||
|
can_manage_roles = guild.me.guild_permissions.manage_roles
|
||||||
for role in guild.roles:
|
for role in guild.roles:
|
||||||
if await self.db.role(role).mention():
|
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)
|
mentions.append(role.mention)
|
||||||
return " ".join(mentions)
|
return " ".join(mentions), edited_roles
|
||||||
|
|
||||||
async def check_communities(self):
|
async def check_communities(self):
|
||||||
for community in self.communities:
|
for community in self.communities:
|
||||||
@ -589,12 +609,15 @@ class Streams(commands.Cog):
|
|||||||
emb = await community.make_embed(streams)
|
emb = await community.make_embed(streams)
|
||||||
chn_msg = [m for m in community._messages_cache if m.channel == chn]
|
chn_msg = [m for m in community._messages_cache if m.channel == chn]
|
||||||
if not chn_msg:
|
if not chn_msg:
|
||||||
mentions = await self._get_mention_str(chn.guild)
|
mentions, roles = await self._get_mention_str(chn.guild)
|
||||||
if mentions:
|
if mentions:
|
||||||
msg = await chn.send(mentions, embed=emb)
|
msg = await chn.send(mentions, embed=emb)
|
||||||
else:
|
else:
|
||||||
msg = await chn.send(embed=emb)
|
msg = await chn.send(embed=emb)
|
||||||
community._messages_cache.append(msg)
|
community._messages_cache.append(msg)
|
||||||
|
if roles:
|
||||||
|
for role in roles:
|
||||||
|
await role.edit(mentionable=False)
|
||||||
await self.save_communities()
|
await self.save_communities()
|
||||||
else:
|
else:
|
||||||
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
|
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
|
||||||
|
|||||||
@ -114,7 +114,7 @@ class TriviaSession:
|
|||||||
async with self.ctx.typing():
|
async with self.ctx.typing():
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
self.count += 1
|
self.count += 1
|
||||||
msg = 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)
|
await self.ctx.send(msg)
|
||||||
continue_ = await self.wait_for_answer(answers, delay, timeout)
|
continue_ = await self.wait_for_answer(answers, delay, timeout)
|
||||||
if continue_ is False:
|
if continue_ is False:
|
||||||
|
|||||||
@ -111,16 +111,14 @@ class Trivia(commands.Cog):
|
|||||||
await settings.allow_override.set(enabled)
|
await settings.allow_override.set(enabled)
|
||||||
if enabled:
|
if enabled:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_("Done. Trivia lists can now override the trivia settings for this server.")
|
||||||
"Done. Trivia lists can now override the trivia settings for this server."
|
|
||||||
).format(now=enabled)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
_(
|
_(
|
||||||
"Done. Trivia lists can no longer override the trivia settings for this "
|
"Done. Trivia lists can no longer override the trivia settings for this "
|
||||||
"server."
|
"server."
|
||||||
).format(now=enabled)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@triviaset.command(name="botplays", usage="<true_or_false>")
|
@triviaset.command(name="botplays", usage="<true_or_false>")
|
||||||
@ -506,7 +504,7 @@ class Trivia(commands.Cog):
|
|||||||
|
|
||||||
with path.open(encoding="utf-8") as file:
|
with path.open(encoding="utf-8") as file:
|
||||||
try:
|
try:
|
||||||
dict_ = yaml.load(file)
|
dict_ = yaml.safe_load(file)
|
||||||
except yaml.error.YAMLError as exc:
|
except yaml.error.YAMLError as exc:
|
||||||
raise InvalidListError("YAML parsing failed.") from exc
|
raise InvalidListError("YAML parsing failed.") from exc
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -186,13 +186,23 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
|
|||||||
async def is_admin(self, member: discord.Member):
|
async def is_admin(self, member: discord.Member):
|
||||||
"""Checks if a member is an admin of their guild."""
|
"""Checks if a member is an admin of their guild."""
|
||||||
admin_role = await self.db.guild(member.guild).admin_role()
|
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):
|
async def is_mod(self, member: discord.Member):
|
||||||
"""Checks if a member is a mod or admin of their guild."""
|
"""Checks if a member is a mod or admin of their guild."""
|
||||||
mod_role = await self.db.guild(member.guild).mod_role()
|
mod_role = await self.db.guild(member.guild).mod_role()
|
||||||
admin_role = await self.db.guild(member.guild).admin_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):
|
async def get_context(self, message, *, cls=commands.Context):
|
||||||
return await super().get_context(message, cls=cls)
|
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]
|
ids_to_check = [to_check.id]
|
||||||
else:
|
else:
|
||||||
author = getattr(to_check, "author", to_check)
|
author = getattr(to_check, "author", to_check)
|
||||||
|
try:
|
||||||
ids_to_check = [r.id for r in author.roles]
|
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)
|
ids_to_check.append(author.id)
|
||||||
|
|
||||||
immune_ids = await self.db.guild(guild).autoimmune_ids()
|
immune_ids = await self.db.guild(guild).autoimmune_ids()
|
||||||
|
|||||||
@ -157,12 +157,31 @@ class Command(CogCommandMixin, commands.Command):
|
|||||||
cmd = cmd.parent
|
cmd = cmd.parent
|
||||||
return sorted(entries, key=lambda x: len(x.qualified_name), reverse=True)
|
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.
|
"""Check if this command can be run in the given context.
|
||||||
|
|
||||||
This function first checks if the command can be run using
|
This function first checks if the command can be run using
|
||||||
discord.py's method `discord.ext.commands.Command.can_run`,
|
discord.py's method `discord.ext.commands.Command.can_run`,
|
||||||
then will return the result of `Requires.verify`.
|
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)
|
ret = await super().can_run(ctx)
|
||||||
if ret is False:
|
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 is so contexts invoking other commands can be checked with
|
||||||
# this command as well
|
# this command as well
|
||||||
original_command = ctx.command
|
original_command = ctx.command
|
||||||
|
original_state = ctx.permission_state
|
||||||
ctx.command = self
|
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:
|
if self.parent is None and self.instance is not None:
|
||||||
# For top-level commands, we need to check the cog's requires too
|
# For top-level commands, we need to check the cog's requires too
|
||||||
ret = await self.instance.requires.verify(ctx)
|
ret = await self.instance.requires.verify(ctx)
|
||||||
@ -183,6 +215,17 @@ class Command(CogCommandMixin, commands.Command):
|
|||||||
return await self.requires.verify(ctx)
|
return await self.requires.verify(ctx)
|
||||||
finally:
|
finally:
|
||||||
ctx.command = original_command
|
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(
|
async def do_conversion(
|
||||||
self, ctx: "Context", converter, argument: str, param: inspect.Parameter
|
self, ctx: "Context", converter, argument: str, param: inspect.Parameter
|
||||||
@ -238,7 +281,9 @@ class Command(CogCommandMixin, commands.Command):
|
|||||||
if cmd.hidden:
|
if cmd.hidden:
|
||||||
return False
|
return False
|
||||||
try:
|
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:
|
except commands.CheckFailure:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -281,12 +281,14 @@ class Requires:
|
|||||||
|
|
||||||
if isinstance(user_perms, dict):
|
if isinstance(user_perms, dict):
|
||||||
self.user_perms: Optional[discord.Permissions] = discord.Permissions.none()
|
self.user_perms: Optional[discord.Permissions] = discord.Permissions.none()
|
||||||
|
_validate_perms_dict(user_perms)
|
||||||
self.user_perms.update(**user_perms)
|
self.user_perms.update(**user_perms)
|
||||||
else:
|
else:
|
||||||
self.user_perms = user_perms
|
self.user_perms = user_perms
|
||||||
|
|
||||||
if isinstance(bot_perms, dict):
|
if isinstance(bot_perms, dict):
|
||||||
self.bot_perms: discord.Permissions = discord.Permissions.none()
|
self.bot_perms: discord.Permissions = discord.Permissions.none()
|
||||||
|
_validate_perms_dict(bot_perms)
|
||||||
self.bot_perms.update(**bot_perms)
|
self.bot_perms.update(**bot_perms)
|
||||||
else:
|
else:
|
||||||
self.bot_perms = bot_perms
|
self.bot_perms = bot_perms
|
||||||
@ -311,6 +313,7 @@ class Requires:
|
|||||||
if user_perms is None:
|
if user_perms is None:
|
||||||
func.requires.user_perms = None
|
func.requires.user_perms = None
|
||||||
else:
|
else:
|
||||||
|
_validate_perms_dict(user_perms)
|
||||||
func.requires.user_perms.update(**user_perms)
|
func.requires.user_perms.update(**user_perms)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
@ -449,7 +452,20 @@ class Requires:
|
|||||||
should_invoke = await self._verify_user(ctx)
|
should_invoke = await self._verify_user(ctx)
|
||||||
elif isinstance(next_state, dict):
|
elif isinstance(next_state, dict):
|
||||||
# NORMAL to PASSIVE_ALLOW; should we proceed as normal or transition?
|
# 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
|
ctx.permission_state = next_state
|
||||||
return should_invoke
|
return should_invoke
|
||||||
@ -588,6 +604,7 @@ def bot_has_permissions(**perms: bool):
|
|||||||
if asyncio.iscoroutinefunction(func):
|
if asyncio.iscoroutinefunction(func):
|
||||||
func.__requires_bot_perms__ = perms
|
func.__requires_bot_perms__ = perms
|
||||||
else:
|
else:
|
||||||
|
_validate_perms_dict(perms)
|
||||||
func.requires.bot_perms.update(**perms)
|
func.requires.bot_perms.update(**perms)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
@ -599,6 +616,8 @@ def has_permissions(**perms: bool):
|
|||||||
|
|
||||||
This check can be overridden by rules.
|
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)
|
return Requires.get_decorator(None, perms)
|
||||||
|
|
||||||
|
|
||||||
@ -670,3 +689,20 @@ class _IntKeyDict(Dict[int, _T]):
|
|||||||
if not isinstance(key, int):
|
if not isinstance(key, int):
|
||||||
raise TypeError("Keys must be of type `int`")
|
raise TypeError("Keys must be of type `int`")
|
||||||
return super().__setitem__(key, value)
|
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}")
|
||||||
|
|||||||
@ -468,7 +468,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
|
|
||||||
pred = MessagePredicate.yes_or_no(ctx)
|
pred = MessagePredicate.yes_or_no(ctx)
|
||||||
try:
|
try:
|
||||||
await self.bot.wait_for("message", check=MessagePredicate.yes_or_no(ctx))
|
await self.bot.wait_for("message", check=pred)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await ctx.send("Response timed out.")
|
await ctx.send("Response timed out.")
|
||||||
return
|
return
|
||||||
@ -1729,7 +1729,7 @@ class Core(commands.Cog, CoreLogic):
|
|||||||
await ctx.tick()
|
await ctx.tick()
|
||||||
|
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@checks.guildowner_or_permissions(manage_server=True)
|
@checks.guildowner_or_permissions(manage_guild=True)
|
||||||
@commands.group(name="autoimmune")
|
@commands.group(name="autoimmune")
|
||||||
async def autoimmune_group(self, ctx: commands.Context):
|
async def autoimmune_group(self, ctx: commands.Context):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import sys
|
import inspect
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import List
|
|
||||||
from copy import deepcopy
|
|
||||||
import hashlib
|
|
||||||
import shutil
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from copy import deepcopy
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import appdirs
|
import appdirs
|
||||||
import tempfile
|
from discord.utils import deprecated
|
||||||
|
|
||||||
|
from . import commands
|
||||||
from .json_io import JsonIO
|
from .json_io import JsonIO
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -153,124 +153,28 @@ def core_data_path() -> Path:
|
|||||||
return core_path.resolve()
|
return core_path.resolve()
|
||||||
|
|
||||||
|
|
||||||
def _find_data_files(init_location: str) -> (Path, List[Path]):
|
# noinspection PyUnusedLocal
|
||||||
"""
|
@deprecated("bundled_data_path() without calling this function")
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
def load_bundled_data(cog_instance, init_location: str):
|
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
|
Get the path to the "data" directory bundled with this cog.
|
||||||
of the installed cog.
|
|
||||||
|
The bundled data folder must be located alongside the ``.py`` file
|
||||||
|
which contains the cog class.
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
|
|
||||||
This function MUST be called from the ``setup()`` function of your
|
You should *NEVER* write to this directory.
|
||||||
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`.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
cog_instance
|
cog_instance
|
||||||
|
An instance of your cog. If calling from a command or method of
|
||||||
|
your cog, this should be ``self``.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@ -280,10 +184,10 @@ def bundled_data_path(cog_instance) -> Path:
|
|||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
FileNotFoundError
|
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():
|
if not bundled_path.is_dir():
|
||||||
raise FileNotFoundError("No such directory {}".format(bundled_path))
|
raise FileNotFoundError("No such directory {}".format(bundled_path))
|
||||||
|
|||||||
@ -23,6 +23,7 @@ discord.py 1.0.0a
|
|||||||
|
|
||||||
This help formatter contains work by Rapptz (Danny) and SirThane#1780.
|
This help formatter contains work by Rapptz (Danny) and SirThane#1780.
|
||||||
"""
|
"""
|
||||||
|
import contextlib
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
@ -224,8 +225,8 @@ class Help(dpy_formatter.HelpFormatter):
|
|||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def format_help_for(self, ctx, command_or_bot, reason: str = None):
|
async def format_help_for(self, ctx, command_or_bot, reason: str = ""):
|
||||||
"""Formats the help page and handles the actual heavy lifting of how ### WTF HAPPENED?
|
"""Formats the help page and handles the actual heavy lifting of how
|
||||||
the help command looks like. To change the behaviour, override the
|
the help command looks like. To change the behaviour, override the
|
||||||
:meth:`~.HelpFormatter.format` method.
|
:meth:`~.HelpFormatter.format` method.
|
||||||
|
|
||||||
@ -244,10 +245,24 @@ class Help(dpy_formatter.HelpFormatter):
|
|||||||
"""
|
"""
|
||||||
self.context = ctx
|
self.context = ctx
|
||||||
self.command = command_or_bot
|
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()
|
emb = await self.format()
|
||||||
|
|
||||||
if reason:
|
if reason:
|
||||||
emb["embed"]["title"] = "{0}".format(reason)
|
emb["embed"]["title"] = reason
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
# This is basically our old DataIO and just a base for much more elaborate classes
|
# 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):
|
async def _threadsafe_save_json(self, data, settings=PRETTY):
|
||||||
loop = asyncio.get_event_loop()
|
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:
|
async with self._lock:
|
||||||
await loop.run_in_executor(None, func)
|
await loop.run_in_executor(None, func)
|
||||||
|
|
||||||
|
|||||||
@ -666,29 +666,30 @@ async def register_casetypes(new_types: List[dict]) -> List[CaseType]:
|
|||||||
return type_list
|
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
|
Parameters
|
||||||
----------
|
----------
|
||||||
guild: `discord.Guild`
|
guild: `discord.Guild`
|
||||||
The guild to get the modlog channel for
|
The guild to get the modlog channel for.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
`discord.TextChannel` or `None`
|
`discord.TextChannel`
|
||||||
The channel object representing the modlog channel
|
The channel object representing the modlog channel.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
RuntimeError
|
RuntimeError
|
||||||
If the modlog channel is not found
|
If the modlog channel is not found.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if hasattr(guild, "get_channel"):
|
if hasattr(guild, "get_channel"):
|
||||||
channel = guild.get_channel(await _conf.guild(guild).mod_log())
|
channel = guild.get_channel(await _conf.guild(guild).mod_log())
|
||||||
else:
|
else:
|
||||||
|
# For unit tests only
|
||||||
channel = await _conf.guild(guild).mod_log()
|
channel = await _conf.guild(guild).mod_log()
|
||||||
if channel is None:
|
if channel is None:
|
||||||
raise RuntimeError("Failed to get the mod log channel!")
|
raise RuntimeError("Failed to get the mod log channel!")
|
||||||
|
|||||||
@ -10,7 +10,7 @@ def test_trivia_lists():
|
|||||||
for l in list_names:
|
for l in list_names:
|
||||||
with l.open() as f:
|
with l.open() as f:
|
||||||
try:
|
try:
|
||||||
dict_ = yaml.load(f)
|
dict_ = yaml.safe_load(f)
|
||||||
except yaml.error.YAMLError as e:
|
except yaml.error.YAMLError as e:
|
||||||
problem_lists.append((l.stem, "YAML error:\n{!s}".format(e)))
|
problem_lists.append((l.stem, "YAML error:\n{!s}".format(e)))
|
||||||
else:
|
else:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user