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