jack1142 febca8ccbb
Migration to discord.py 2.0 (#5600)
* Temporarily set d.py to use latest git revision

* Remove `bot` param to Client.start

* Switch to aware datetimes

A lot of this is removing `.replace(...)` which while not technically
needed, simplifies the code base. There's only a few changes that are
actually necessary here.

* Update to work with new Asset design

* [threads] Update core ModLog API to support threads

- Added proper support for passing `Thread` to `channel`
  when creating/editing case
- Added `parent_channel_id` attribute to Modlog API's Case
    - Added `parent_channel` property that tries to get parent channel
- Updated case's content to show both thread and parent information

* [threads] Disallow usage of threads in some of the commands

- announceset channel
- filter channel clear
- filter channel add
- filter channel remove
- GlobalUniqueObjectFinder converter
    - permissions addglobalrule
    - permissions removeglobalrule
    - permissions removeserverrule
    - Permissions cog does not perform any validation for IDs
      when setting through YAML so that has not been touched
- streamalert twitch/youtube/picarto
- embedset channel
- set ownernotifications adddestination

* [threads] Handle threads in Red's permissions system (Requires)

- Made permissions system apply rules of (only) parent in threads

* [threads] Update embed_requested to support threads

- Threads don't have their own embed settings and inherit from parent

* [threads] Update Red.message_eligible_as_command to support threads

* [threads] Properly handle invocation of [p](un)mutechannel in threads

Usage of a (un)mutechannel will mute/unmute user in the parent channel
if it's invoked in a thread.

* [threads] Update Filter cog to properly handle threads

- `[p]filter channel list` in a threads sends list for parent channel
- Checking for filter hits for a message in a thread checks its parent
  channel's word list. There's no separate word list for threads.

* [threads] Support threads in Audio cog

- Handle threads being notify channels
- Update type hint for `is_query_allowed()`

* [threads] Update type hints and documentation to reflect thread support

- Documented that `{channel}` in CCs might be a thread
- Allowed (documented) usage of threads with `Config.channel()`
    - Separate thread scope is still in the picture though
      if it were to be done, it's going to be in separate in PR
- GuildContext.channel might be Thread

* Use less costy channel check in customcom's on_message_without_command

This isn't needed for d.py 2.0 but whatever...

* Update for in-place edits

* Embed's bool changed behavior, I'm hoping it doesn't affect us

* Address User.permissions_in() removal

* Swap VerificationLevel.extreme with VerificationLevel.highest

* Change to keyword-only parameters

* Change of `Guild.vanity_invite()` return type

* avatar -> display_avatar

* Fix metaclass shenanigans with Converter

* Update Red.add_cog() to be inline with `dpy_commands.Bot.add_cog()`

This means adding `override` keyword-only parameter and causing
small breakage by swapping RuntimeError with discord.ClientException.

* Address all DEP-WARNs

* Remove Context.clean_prefix and use upstream implementation instead

* Remove commands.Literal and use upstream implementation instead

Honestly, this was a rather bad implementation anyway...

Breaking but actually not really - it was provisional.

* Update Command.callback's setter

Support for functools.partial is now built into d.py

* Add new perms in HUMANIZED_PERM mapping (some from d.py 1.7 it seems)

BTW, that should really be in core instead of what we have now...

* Remove the part of do_conversion that has not worked for a long while

* Stop wrapping BadArgument in ConversionFailure

This is breaking but it's best to resolve it like this.

The functionality of ConversionFailure can be replicated with
Context.current_parameter and Context.current_argument.

* Add custom errors for int and float converters

* Remove Command.__call__ as it's now implemented in d.py

* Get rid of _dpy_reimplements

These were reimplemented for the purpose of typing
so it is no longer needed now that d.py is type hinted.

* Add return to Red.remove_cog

* Ensure we don't delete messages that differ only by used sticker

* discord.InvalidArgument->ValueError

* Move from raw <t:...> syntax to discord.utils.format_dt()

* Address AsyncIter removal

* Swap to pos-only for params that are pos-only in upstream

* Update for changes to Command.params

* [threads] Support threads in ignore checks and allow ignoring them

- Updated `[p](un)ignore channel` to accept threads
- Updated `[p]ignore list` to list ignored threads
- Updated logic in `Red.ignored_channel_or_guild()`

Ignores for guild channels now work as follows (only changes for threads):
- if channel is not a thread:
    - check if user has manage channels perm in channel
      and allow command usage if so
    - check if channel is ignored and disallow command usage if so
    - allow command usage if none of the conditions above happened
- if channel is a thread:
    - check if user has manage channels perm in parent channel
      and allow command usage if so
    - check if parent channel is ignored and disallow command usage
      if so
    - check if user has manage thread perm in parent channel
      and allow command usage if so
    - check if thread is ignored and disallow command usage if so
    - allow command usage if none of the conditions above happened

* [partial] Raise TypeError when channel is of PartialMessageable type

- Red.embed_requested
- Red.ignored_channel_or_guild

* [partial] Discard command messages when channel is PartialMessageable

* [threads] Add utilities for checking appropriate perms in both channels & threads

* [threads] Update code to use can_react_in() and @bot_can_react()

* [threads] Update code to use can_send_messages_in

* [threads] Add send_messages_in_threads perm to mute role and overrides

* [threads] Update code to use (bot/user)_can_manage_channel

* [threads] Update [p]diagnoseissues to work with threads

* Type hint fix

* [threads] Patch vendored discord.ext.menus to check proper perms in threads

I guess we've reached time when we have to patch the lib we vendor...

* Make docs generation work with non-final d.py releases

* Update discord.utils.oauth_url() usage

* Swap usage of discord.Embed.Empty/discord.embeds.EmptyEmbed to None

* Update usage of Guild.member_count to work with `None`

* Switch from Guild.vanity_invite() to Guild.vanity_url

* Update startup process to work with d.py's new asynchronous startup

* Use setup_hook() for pre-connect actions

* Update core's add_cog, remove_cog, and load_extension methods

* Update all setup functions to async and add awaits to bot.add_cog calls

* Modernize cogs by using async cog_load and cog_unload

* Address StoreChannel removal

* [partial] Disallow passing PartialMessageable to Case.channel

* [partial] Update cogs and utils to work better with PartialMessageable

- Ignore messages with PartialMessageable channel in CustomCommands cog
- In Filter cog, don't pass channel to modlog.create_case()
  if it's PartialMessageable
- In Trivia cog, only compare channel IDs
- Make `.utils.menus.menu()` work for messages
  with PartialMessageable channel
- Make checks in `.utils.tunnel.Tunnel.communicate()` more rigid

* Add few missing DEP-WARNs
2022-04-03 03:21:20 +02:00

356 lines
11 KiB
Python

from __future__ import annotations
import asyncio
import contextlib
import os
import re
from typing import Iterable, List, Union, Optional, TYPE_CHECKING
import discord
from discord.ext.commands import Context as DPYContext
from .requires import PermState
from ..utils.chat_formatting import box
from ..utils.predicates import MessagePredicate
from ..utils import can_user_react_in, common_filters
if TYPE_CHECKING:
from .commands import Command
from ..bot import Red
TICK = "\N{WHITE HEAVY CHECK MARK}"
__all__ = ["Context", "GuildContext", "DMContext"]
class Context(DPYContext):
"""Command invocation context for Red.
All context passed into commands will be of this type.
This class inherits from `discord.ext.commands.Context`.
Attributes
----------
assume_yes: bool
Whether or not interactive checks should
be skipped and assumed to be confirmed.
This is intended for allowing automation of tasks.
An example of this would be scheduled commands
not requiring interaction if the cog developer
checks this value prior to confirming something interactively.
Depending on the potential impact of a command,
it may still be appropriate not to use this setting.
permission_state: PermState
The permission state the current context is in.
"""
command: "Command"
invoked_subcommand: "Optional[Command]"
bot: "Red"
def __init__(self, **attrs):
self.assume_yes = attrs.pop("assume_yes", False)
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`, optional
A function which is used to filter the ``content`` before
it is sent.
This must take a single `str` as an argument, and return
the processed `str`. When `None` is passed, ``content`` won't be touched.
Defaults to `None`.
**kwargs
See `discord.ext.commands.Context.send`.
Returns
-------
discord.Message
The message that was sent.
"""
_filter = kwargs.pop("filter", None)
if _filter and content:
content = _filter(str(content))
return await super().send(content=content, **kwargs)
async def send_help(self, command=None):
"""Send the command help message."""
# This allows people to manually use this similarly
# to the upstream d.py version, while retaining our use.
command = command or self.command
await self.bot.send_help_for(self, command)
async def tick(self, *, message: Optional[str] = None) -> bool:
"""Add a tick reaction to the command message.
Keyword Arguments
-----------------
message : str, optional
The message to send if adding the reaction doesn't succeed.
Returns
-------
bool
:code:`True` if adding the reaction succeeded.
"""
return await self.react_quietly(TICK, message=message)
async def react_quietly(
self,
reaction: Union[discord.Emoji, discord.Reaction, discord.PartialEmoji, str],
*,
message: Optional[str] = None,
) -> bool:
"""Adds a reaction to the command message.
Parameters
----------
reaction : Union[discord.Emoji, discord.Reaction, discord.PartialEmoji, str]
The emoji to react with.
Keyword Arguments
-----------------
message : str, optional
The message to send if adding the reaction doesn't succeed.
Returns
-------
bool
:code:`True` if adding the reaction succeeded.
"""
try:
if not can_user_react_in(self.me, self.channel):
raise RuntimeError
await self.message.add_reaction(reaction)
except (RuntimeError, discord.HTTPException):
if message is not None:
await self.send(message)
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 = []
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=MessagePredicate.lower_equal_to("more", self),
timeout=timeout,
)
except asyncio.TimeoutError:
with contextlib.suppress(discord.HTTPException):
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 channel is a DM
with contextlib.suppress(discord.HTTPException):
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
"""
return await self.bot.get_embed_color(self)
@property
def embed_color(self):
# Rather than double awaiting.
return self.embed_colour
async def embed_requested(self):
"""
Short-hand for calling bot.embed_requested with permission checks.
Equivalent to:
.. code:: python
await ctx.bot.embed_requested(ctx)
Returns
-------
bool:
:code:`True` if an embed is requested
"""
return await self.bot.embed_requested(self)
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`
ValueError
when the message's length is not between 1 and 2000 characters.
"""
if not message or len(message) > 2000:
raise ValueError("Message length must be between 1 and 2000")
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,
allowed_mentions=discord.AllowedMentions(everyone=False, roles=False, users=False),
)
@property
def me(self) -> Union[discord.ClientUser, discord.Member]:
"""
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
if TYPE_CHECKING or os.getenv("BUILDING_DOCS", False):
class DMContext(Context):
"""
At runtime, this will still be a normal context object.
This lies about some type narrowing for type analysis in commands
using a dm_only decorator.
It is only correct to use when those types are already narrowed
"""
@property
def author(self) -> discord.User:
...
@property
def channel(self) -> discord.DMChannel:
...
@property
def guild(self) -> None:
...
@property
def me(self) -> discord.ClientUser:
...
class GuildContext(Context):
"""
At runtime, this will still be a normal context object.
This lies about some type narrowing for type analysis in commands
using a guild_only decorator.
It is only correct to use when those types are already narrowed
"""
@property
def author(self) -> discord.Member:
...
@property
def channel(self) -> Union[discord.TextChannel, discord.Thread]:
...
@property
def guild(self) -> discord.Guild:
...
@property
def me(self) -> discord.Member:
...
else:
GuildContext = Context
DMContext = Context