Add support for editing and deleting webhook messages.

Fix #6058
This commit is contained in:
Rapptz
2020-12-09 20:15:35 -05:00
parent b00aaab0b2
commit 44dc7a8e02
4 changed files with 241 additions and 15 deletions

View File

@ -35,6 +35,7 @@ import aiohttp
from . import utils
from .errors import InvalidArgument, HTTPException, Forbidden, NotFound, DiscordServerError
from .message import Message
from .enums import try_enum, WebhookType
from .user import BaseUser, User
from .asset import Asset
@ -45,6 +46,7 @@ __all__ = (
'AsyncWebhookAdapter',
'RequestsWebhookAdapter',
'Webhook',
'WebhookMessage',
)
log = logging.getLogger(__name__)
@ -66,6 +68,9 @@ class WebhookAdapter:
self._request_url = '{0.BASE}/webhooks/{1}/{2}'.format(self, webhook.id, webhook.token)
self.webhook = webhook
def is_async(self):
return False
def request(self, verb, url, payload=None, multipart=None):
"""Actually does the request.
@ -94,6 +99,12 @@ class WebhookAdapter:
def edit_webhook(self, *, reason=None, **payload):
return self.request('PATCH', self._request_url, payload=payload, reason=reason)
def edit_webhook_message(self, message_id, payload):
return self.request('PATCH', '{}/messages/{}'.format(self._request_url, message_id), payload=payload)
def delete_webhook_message(self, message_id):
return self.request('DELETE', '{}/messages/{}'.format(self._request_url, message_id))
def handle_execution_response(self, data, *, wait):
"""Transforms the webhook execution response into something
more meaningful.
@ -178,6 +189,9 @@ class AsyncWebhookAdapter(WebhookAdapter):
self.session = session
self.loop = asyncio.get_event_loop()
def is_async(self):
return True
async def request(self, verb, url, payload=None, multipart=None, *, files=None, reason=None):
headers = {}
data = None
@ -253,8 +267,9 @@ class AsyncWebhookAdapter(WebhookAdapter):
return data
# transform into Message object
from .message import Message
return Message(data=data, state=self.webhook._state, channel=self.webhook.channel)
# Make sure to coerce the state to the partial one to allow message edits/delete
state = _PartialWebhookState(self, self.webhook)
return WebhookMessage(data=data, state=state, channel=self.webhook.channel)
class RequestsWebhookAdapter(WebhookAdapter):
"""A webhook adapter suited for use with ``requests``.
@ -356,8 +371,9 @@ class RequestsWebhookAdapter(WebhookAdapter):
return response
# transform into Message object
from .message import Message
return Message(data=response, state=self.webhook._state, channel=self.webhook.channel)
# Make sure to coerce the state to the partial one to allow message edits/delete
state = _PartialWebhookState(self, self.webhook)
return WebhookMessage(data=response, state=state, channel=self.webhook.channel)
class _FriendlyHttpAttributeErrorHelper:
__slots__ = ()
@ -366,9 +382,10 @@ class _FriendlyHttpAttributeErrorHelper:
raise AttributeError('PartialWebhookState does not support http methods.')
class _PartialWebhookState:
__slots__ = ('loop',)
__slots__ = ('loop', 'parent')
def __init__(self, adapter):
def __init__(self, adapter, parent):
self.parent = parent
# Fetch the loop from the adapter if it's there
try:
self.loop = adapter.loop
@ -394,6 +411,98 @@ class _PartialWebhookState:
def __getattr__(self, attr):
raise AttributeError('PartialWebhookState does not support {0!r}.'.format(attr))
class WebhookMessage(Message):
"""Represents a message sent from your webhook.
This allows you to edit or delete a message sent by your
webhook.
This inherits from :class:`discord.Message` with changes to
:meth:`edit` and :meth:`delete` to work.
.. versionadded:: 1.6
"""
def edit(self, **fields):
"""|maybecoro|
Edits the message.
The content must be able to be transformed into a string via ``str(content)``.
.. versionadded:: 1.6
Parameters
------------
content: Optional[:class:`str`]
The content to edit the message with or ``None`` to clear it.
embeds: List[:class:`Embed`]
A list of embeds to edit the message with.
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Edited a message that is not yours.
InvalidArgument
You specified both ``embed`` and ``embeds`` or the length of
``embeds`` was invalid or there was no token associated with
this webhook.
"""
return self._state.parent.edit_message(self.id, **fields)
def _delete_delay_sync(self, delay):
time.sleep(delay)
return self._state.parent.delete_message(self.id)
async def _delete_delay_async(self, delay):
async def inner_call():
await asyncio.sleep(delay)
try:
await self._state.parent.delete_message(self.id)
except HTTPException:
pass
asyncio.ensure_future(inner_call(), loop=self._state.loop)
return await asyncio.sleep(0)
def delete(self, *, delay=None):
"""|coro|
Deletes the message.
Parameters
-----------
delay: Optional[:class:`float`]
If provided, the number of seconds to wait before deleting the message.
If this is a coroutine, the waiting is done in the background and deletion failures
are ignored. If this is not a coroutine then the delay blocks the thread.
Raises
------
Forbidden
You do not have proper permissions to delete the message.
NotFound
The message was deleted already
HTTPException
Deleting the message failed.
"""
if delay is not None:
if self._state.parent._adapter.is_async():
return self._delete_delay_async(delay)
else:
return self._delete_delay_sync(delay)
return self._state.parent.delete_message(self.id)
class Webhook(Hashable):
"""Represents a Discord webhook.
@ -488,7 +597,7 @@ class Webhook(Hashable):
self.name = data.get('name')
self.avatar = data.get('avatar')
self.token = data.get('token')
self._state = state or _PartialWebhookState(adapter)
self._state = state or _PartialWebhookState(adapter, self)
self._adapter = adapter
self._adapter._prepare(self)
@ -785,7 +894,7 @@ class Webhook(Hashable):
wait: :class:`bool`
Whether the server should wait before sending a response. This essentially
means that the return type of this function changes from ``None`` to
a :class:`Message` if set to ``True``.
a :class:`WebhookMessage` if set to ``True``.
username: :class:`str`
The username to send with this message. If no username is provided
then the default username for the webhook is used.
@ -825,7 +934,7 @@ class Webhook(Hashable):
Returns
---------
Optional[:class:`Message`]
Optional[:class:`WebhookMessage`]
The message that was sent.
"""
@ -869,3 +978,115 @@ class Webhook(Hashable):
def execute(self, *args, **kwargs):
"""An alias for :meth:`~.Webhook.send`."""
return self.send(*args, **kwargs)
def edit_message(self, message_id, **fields):
"""|maybecoro|
Edits a message owned by this webhook.
This is a lower level interface to :meth:`WebhookMessage.edit` in case
you only have an ID.
.. versionadded:: 1.6
Parameters
------------
message_id: :class:`int`
The message ID to edit.
content: Optional[:class:`str`]
The content to edit the message with or ``None`` to clear it.
embeds: List[:class:`Embed`]
A list of embeds to edit the message with.
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Edited a message that is not yours.
InvalidArgument
You specified both ``embed`` and ``embeds`` or the length of
``embeds`` was invalid or there was no token associated with
this webhook.
"""
payload = {}
if self.token is None:
raise InvalidArgument('This webhook does not have a token associated with it')
try:
content = fields['content']
except KeyError:
pass
else:
if content is not None:
content = str(content)
payload['content'] = content
# Check if the embeds interface is being used
try:
embeds = fields['embeds']
except KeyError:
# Nope
pass
else:
if embeds is None or len(embeds) > 10:
raise InvalidArgument('embeds has a maximum of 10 elements')
payload['embeds'] = [e.to_dict() for e in embeds]
try:
embed = fields['embed']
except KeyError:
pass
else:
if 'embeds' in payload:
raise InvalidArgument('Cannot mix embed and embeds keyword arguments')
if embed is None:
payload['embeds'] = []
else:
payload['embeds'] = [embed.to_dict()]
allowed_mentions = fields.pop('allowed_mentions', None)
previous_mentions = getattr(self._state, 'allowed_mentions', None)
if allowed_mentions:
if previous_mentions is not None:
payload['allowed_mentions'] = previous_mentions.merge(allowed_mentions).to_dict()
else:
payload['allowed_mentions'] = allowed_mentions.to_dict()
elif previous_mentions is not None:
payload['allowed_mentions'] = previous_mentions.to_dict()
return self._adapter.edit_webhook_message(message_id, payload=payload)
def delete_message(self, message_id):
"""|maybecoro|
Deletes a message owned by this webhook.
This is a lower level interface to :meth:`WebhookMessage.delete` in case
you only have an ID.
.. versionadded:: 1.6
Parameters
------------
message_id: :class:`int`
The message ID to edit.
Raises
-------
HTTPException
Deleting the message failed.
Forbidden
Deleted a message that is not yours.
"""
return self._adapter.delete_webhook_message(message_id)