mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
API changes: - Cogs must now inherit from `commands.Cog` (see #2151 for discussion and more details) - All functions which are not decorators in the `redbot.core.checks` module are now deprecated in favour of their counterparts in `redbot.core.utils.mod`. This is to make this module more consistent and end the confusing naming convention. - `redbot.core.checks.check_overrides` function is now gone, overrideable checks can now be created with the `@commands.permissions_check` decorator - Command, Group, Cog and Context have some new attributes and methods, but they are for internal use so shouldn't concern cog creators (unless they're making a permissions cog!). - `__permissions_check_before` and `__permissions_check_after` have been replaced: A cog method named `__permissions_hook` will be evaluated as permissions hooks in the same way `__permissions_check_before` previously was. Permissions hooks can also be added/removed/verified through the new `*_permissions_hook()` methods on the bot object, and they will be verified even when permissions is unloaded. - New utility method `redbot.core.utils.chat_formatting.humanize_list` - New dependency [`schema`](https://github.com/keleshev/schema) User-facing changes: - When a `@bot_has_permissions` check fails, the bot will respond saying what permissions were actually missing. - All YAML-related `[p]permissions` subcommands now reside under the `[p]permissions acl` sub-group (tbh I still think the whole cog has too many top-level commands) - The YAML schema for these commands has been changed - A rule cannot be set as allow and deny at the same time (previously this would just default to allow) Documentation: - New documentation for `redbot.core.commands.requires` and `redbot.core.checks` modules - Renewed documentation for the permissions cog - `sphinx.ext.doctest` is now enabled Note: standard discord.py checks will still behave exactly the same way, in fact they are checked before `Requires` is looked at, so they are not overrideable. Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
262 lines
8.1 KiB
Python
262 lines
8.1 KiB
Python
|
|
import asyncio
|
|
from typing import Iterable, List
|
|
import discord
|
|
from discord.ext import commands
|
|
|
|
from .requires import PermState
|
|
from ..utils.chat_formatting import box
|
|
from ..utils import common_filters
|
|
|
|
TICK = "\N{WHITE HEAVY CHECK MARK}"
|
|
|
|
__all__ = ["Context"]
|
|
|
|
|
|
class Context(commands.Context):
|
|
"""Command invocation context for Red.
|
|
|
|
All context passed into commands will be of this type.
|
|
|
|
This class inherits from `discord.ext.commands.Context`.
|
|
"""
|
|
|
|
def __init__(self, **attrs):
|
|
super().__init__(**attrs)
|
|
self.permission_state: PermState = PermState.NORMAL
|
|
|
|
async def send(self, content=None, **kwargs):
|
|
"""Sends a message to the destination with the content given.
|
|
|
|
This acts the same as `discord.ext.commands.Context.send`, with
|
|
one added keyword argument as detailed below in *Other Parameters*.
|
|
|
|
Parameters
|
|
----------
|
|
content : str
|
|
The content of the message to send.
|
|
|
|
Other Parameters
|
|
----------------
|
|
filter : Callable[`str`] -> `str`
|
|
A function which is used to sanitize the ``content`` before
|
|
it is sent. Defaults to
|
|
:func:`~redbot.core.utils.common_filters.filter_mass_mentions`.
|
|
This must take a single `str` as an argument, and return
|
|
the sanitized `str`.
|
|
\*\*kwargs
|
|
See `discord.ext.commands.Context.send`.
|
|
|
|
Returns
|
|
-------
|
|
discord.Message
|
|
The message that was sent.
|
|
|
|
"""
|
|
|
|
_filter = kwargs.pop("filter", common_filters.filter_mass_mentions)
|
|
|
|
if _filter and content:
|
|
content = _filter(str(content))
|
|
|
|
return await super().send(content=content, **kwargs)
|
|
|
|
async def send_help(self) -> List[discord.Message]:
|
|
"""Send the command help message.
|
|
|
|
Returns
|
|
-------
|
|
`list` of `discord.Message`
|
|
A list of help messages which were sent to the user.
|
|
|
|
"""
|
|
command = self.invoked_subcommand or self.command
|
|
embed_wanted = await self.bot.embed_requested(
|
|
self.channel, self.author, command=self.bot.get_command("help")
|
|
)
|
|
if self.guild and not self.channel.permissions_for(self.guild.me).embed_links:
|
|
embed_wanted = False
|
|
|
|
ret = []
|
|
destination = self
|
|
if embed_wanted:
|
|
embeds = await self.bot.formatter.format_help_for(self, command)
|
|
for embed in embeds:
|
|
try:
|
|
m = await destination.send(embed=embed)
|
|
except discord.HTTPException:
|
|
destination = self.author
|
|
m = await destination.send(embed=embed)
|
|
ret.append(m)
|
|
else:
|
|
f = commands.HelpFormatter()
|
|
msgs = await f.format_help_for(self, command)
|
|
for msg in msgs:
|
|
try:
|
|
m = await destination.send(msg)
|
|
except discord.HTTPException:
|
|
destination = self.author
|
|
m = await destination.send(msg)
|
|
ret.append(m)
|
|
|
|
return ret
|
|
|
|
async def tick(self) -> bool:
|
|
"""Add a tick reaction to the command message.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
:code:`True` if adding the reaction succeeded.
|
|
|
|
"""
|
|
try:
|
|
await self.message.add_reaction(TICK)
|
|
except discord.HTTPException:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
async def send_interactive(
|
|
self, messages: Iterable[str], box_lang: str = None, timeout: int = 15
|
|
) -> List[discord.Message]:
|
|
"""Send multiple messages interactively.
|
|
|
|
The user will be prompted for whether or not they would like to view
|
|
the next message, one at a time. They will also be notified of how
|
|
many messages are remaining on each prompt.
|
|
|
|
Parameters
|
|
----------
|
|
messages : `iterable` of `str`
|
|
The messages to send.
|
|
box_lang : str
|
|
If specified, each message will be contained within a codeblock of
|
|
this language.
|
|
timeout : int
|
|
How long the user has to respond to the prompt before it times out.
|
|
After timing out, the bot deletes its prompt message.
|
|
|
|
"""
|
|
messages = tuple(messages)
|
|
ret = []
|
|
|
|
more_check = lambda m: (
|
|
m.author == self.author and m.channel == self.channel and m.content.lower() == "more"
|
|
)
|
|
|
|
for idx, page in enumerate(messages, 1):
|
|
if box_lang is None:
|
|
msg = await self.send(page)
|
|
else:
|
|
msg = await self.send(box(page, lang=box_lang))
|
|
ret.append(msg)
|
|
n_remaining = len(messages) - idx
|
|
if n_remaining > 0:
|
|
if n_remaining == 1:
|
|
plural = ""
|
|
is_are = "is"
|
|
else:
|
|
plural = "s"
|
|
is_are = "are"
|
|
query = await self.send(
|
|
"There {} still {} message{} remaining. "
|
|
"Type `more` to continue."
|
|
"".format(is_are, n_remaining, plural)
|
|
)
|
|
try:
|
|
resp = await self.bot.wait_for("message", check=more_check, timeout=timeout)
|
|
except asyncio.TimeoutError:
|
|
await query.delete()
|
|
break
|
|
else:
|
|
try:
|
|
await self.channel.delete_messages((query, resp))
|
|
except (discord.HTTPException, AttributeError):
|
|
# In case the bot can't delete other users' messages,
|
|
# or is not a bot account
|
|
# or chanel is a DM
|
|
await query.delete()
|
|
return ret
|
|
|
|
async def embed_colour(self):
|
|
"""
|
|
Helper function to get the colour for an embed.
|
|
|
|
Returns
|
|
-------
|
|
discord.Colour:
|
|
The colour to be used
|
|
"""
|
|
if self.guild and await self.bot.db.guild(self.guild).use_bot_color():
|
|
return self.guild.me.color
|
|
else:
|
|
return self.bot.color
|
|
|
|
@property
|
|
def embed_color(self):
|
|
# Rather than double awaiting.
|
|
return self.embed_colour
|
|
|
|
async def embed_requested(self):
|
|
"""
|
|
Simple helper to call bot.embed_requested
|
|
with logic around if embed permissions are available
|
|
|
|
Returns
|
|
-------
|
|
bool:
|
|
:code:`True` if an embed is requested
|
|
"""
|
|
if self.guild and not self.channel.permissions_for(self.guild.me).embed_links:
|
|
return False
|
|
return await self.bot.embed_requested(self.channel, self.author, command=self.command)
|
|
|
|
async def maybe_send_embed(self, message: str) -> discord.Message:
|
|
"""
|
|
Simple helper to send a simple message to context
|
|
without manually checking ctx.embed_requested
|
|
This should only be used for simple messages.
|
|
|
|
Parameters
|
|
----------
|
|
message: `str`
|
|
The string to send
|
|
|
|
Returns
|
|
-------
|
|
discord.Message:
|
|
the message which was sent
|
|
|
|
Raises
|
|
------
|
|
discord.Forbidden
|
|
see `discord.abc.Messageable.send`
|
|
discord.HTTPException
|
|
see `discord.abc.Messageable.send`
|
|
"""
|
|
|
|
if await self.embed_requested():
|
|
return await self.send(
|
|
embed=discord.Embed(description=message, color=(await self.embed_colour()))
|
|
)
|
|
else:
|
|
return await self.send(message)
|
|
|
|
@property
|
|
def clean_prefix(self) -> str:
|
|
"""str: The command prefix, but a mention prefix is displayed nicer."""
|
|
me = self.me
|
|
return self.prefix.replace(me.mention, f"@{me.display_name}")
|
|
|
|
@property
|
|
def me(self) -> discord.abc.User:
|
|
"""discord.abc.User: The bot member or user object.
|
|
|
|
If the context is DM, this will be a `discord.User` object.
|
|
"""
|
|
if self.guild is not None:
|
|
return self.guild.me
|
|
else:
|
|
return self.bot.user
|