Browse Source

Add support for editing and deleting webhook messages.

Fix #6058
pull/6146/head
Rapptz 4 years ago
parent
commit
44dc7a8e02
  1. 6
      discord/channel.py
  2. 4
      discord/guild.py
  3. 239
      discord/webhook.py
  4. 7
      docs/api.rst

6
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)

4
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]

239
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)

7
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
~~~~~~~~~

Loading…
Cancel
Save