aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRapptz <[email protected]>2020-12-09 20:15:35 -0500
committerRapptz <[email protected]>2020-12-09 20:15:35 -0500
commit44dc7a8e0208b8e5bbec568accab9ab4de818300 (patch)
treeb725764729f6d07709105c3811eef8602076b8d0
parentRevert Message.edit logic that deals with allowed_mentions (diff)
downloaddiscord.py-44dc7a8e0208b8e5bbec568accab9ab4de818300.tar.xz
discord.py-44dc7a8e0208b8e5bbec568accab9ab4de818300.zip
Add support for editing and deleting webhook messages.
Fix #6058
-rw-r--r--discord/channel.py6
-rw-r--r--discord/guild.py4
-rw-r--r--discord/webhook.py239
-rw-r--r--docs/api.rst7
4 files changed, 241 insertions, 15 deletions
diff --git a/discord/channel.py b/discord/channel.py
index 7568b404..9343b72c 100644
--- a/discord/channel.py
+++ b/discord/channel.py
@@ -34,7 +34,6 @@ from .mixins import Hashable
from . import utils
from .asset import Asset
from .errors import ClientException, NoMoreItems, InvalidArgument
-from .webhook import Webhook
__all__ = (
'TextChannel',
@@ -221,7 +220,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
A value of `0` disables slowmode. The maximum value possible is `21600`.
type: :class:`ChannelType`
Change the type of this text channel. Currently, only conversion between
- :attr:`ChannelType.text` and :attr:`ChannelType.news` is supported. This
+ :attr:`ChannelType.text` and :attr:`ChannelType.news` is supported. This
is only available to guilds that contain ``NEWS`` in :attr:`Guild.features`.
reason: Optional[:class:`str`]
The reason for editing this channel. Shows up on the audit log.
@@ -429,6 +428,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
The webhooks for this channel.
"""
+ from .webhook import Webhook
data = await self._state.http.channel_webhooks(self.id)
return [Webhook.from_state(d, state=self._state) for d in data]
@@ -465,6 +465,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
The created webhook.
"""
+ from .webhook import Webhook
if avatar is not None:
avatar = utils._bytes_to_base64_data(avatar)
@@ -512,6 +513,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
if not isinstance(destination, TextChannel):
raise InvalidArgument('Expected TextChannel received {0.__name__}'.format(type(destination)))
+ from .webhook import Webhook
data = await self._state.http.follow_webhook(self.id, webhook_channel_id=destination.id, reason=reason)
return Webhook._as_follower(data, channel=destination, user=self._state.user)
diff --git a/discord/guild.py b/discord/guild.py
index 1f78de42..1a1917a8 100644
--- a/discord/guild.py
+++ b/discord/guild.py
@@ -41,7 +41,6 @@ from .mixins import Hashable
from .user import User
from .invite import Invite
from .iterators import AuditLogIterator, MemberIterator
-from .webhook import Webhook
from .widget import Widget
from .asset import Asset
from .flags import SystemChannelFlags
@@ -482,7 +481,7 @@ class Guild(Hashable):
@property
def public_updates_channel(self):
"""Optional[:class:`TextChannel`]: Return's the guild's channel where admins and
- moderators of the guilds receive notices from Discord. The guild must be a
+ moderators of the guilds receive notices from Discord. The guild must be a
Community guild.
If no channel is set, then this returns ``None``.
@@ -1482,6 +1481,7 @@ class Guild(Hashable):
The webhooks for this guild.
"""
+ from .webhook import Webhook
data = await self._state.http.guild_webhooks(self.id)
return [Webhook.from_state(d, state=self._state) for d in data]
diff --git a/discord/webhook.py b/discord/webhook.py
index bd280230..db328dfb 100644
--- a/discord/webhook.py
+++ b/discord/webhook.py
@@ -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)
diff --git a/docs/api.rst b/docs/api.rst
index 892e175e..ef3f5de5 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -2090,9 +2090,9 @@ Certain utilities make working with async iterators easier, detailed below.
Collects items into chunks of up to a given maximum size.
Another :class:`AsyncIterator` is returned which collects items into
:class:`list`\s of a given size. The maximum chunk size must be a positive integer.
-
+
.. versionadded:: 1.6
-
+
Collecting groups of users: ::
async for leader, *users in reaction.users().chunk(3):
@@ -2544,6 +2544,9 @@ discord.py offers support for creating, editing, and executing webhooks through
.. autoclass:: Webhook
:members:
+.. autoclass:: WebhookMessage
+ :members:
+
Adapters
~~~~~~~~~