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
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
await user.edit(reason=get_audit_reason(ctx.author, None), nick=nickname)
await ctx.send("Done.")
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)
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
"""
mod_channel = await modlog.get_modlog_channel(case.guild)
if mod_channel is None:
try:
mod_channel = await modlog.get_modlog_channel(case.guild)
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(
_(
"When a stream or community is live, `@\u200b{role.name}` will be mentioned."
).format(role=role)
)
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: