Browse Source

Add support for editing message attachments

pull/7494/head
Josh 3 years ago
committed by GitHub
parent
commit
dede5539ee
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      discord/file.py
  2. 53
      discord/http.py
  3. 105
      discord/interactions.py
  4. 68
      discord/message.py
  5. 138
      discord/webhook/async_.py
  6. 89
      discord/webhook/sync.py

13
discord/file.py

@ -23,7 +23,7 @@ DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Optional, Union
from typing import Any, Dict, Optional, Union
import os
import io
@ -126,3 +126,14 @@ class File:
self.fp.close = self._closer
if self._owner:
self._closer()
def to_dict(self, index: int) -> Dict[str, Any]:
payload = {
'id': index,
'filename': self.filename,
}
if self.description is not None:
payload['description'] = self.description
return payload

53
discord/http.py

@ -52,13 +52,13 @@ import aiohttp
from .errors import HTTPException, Forbidden, NotFound, LoginFailure, DiscordServerError, GatewayNotFound, InvalidArgument
from .gateway import DiscordClientWebSocketResponse
from .file import File
from . import __version__, utils
from .utils import MISSING
_log = logging.getLogger(__name__)
if TYPE_CHECKING:
from .file import File
from .ui.view import View
from .embeds import Embed
from .mentions import AllowedMentions
@ -147,7 +147,7 @@ def handle_message_parameters(
files: List[File] = MISSING,
embed: Optional[Embed] = MISSING,
embeds: List[Embed] = MISSING,
attachments: List[Attachment] = MISSING,
attachments: List[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
allowed_mentions: Optional[AllowedMentions] = MISSING,
message_reference: Optional[message.MessageReference] = MISSING,
@ -160,6 +160,12 @@ def handle_message_parameters(
if embeds is not MISSING and embed is not MISSING:
raise TypeError('Cannot mix embed and embeds keyword arguments.')
if file is not MISSING:
files = [file]
if attachments is not MISSING and files is not MISSING:
raise TypeError('Cannot mix attachments and files keyword arguments.')
payload = {}
if embeds is not MISSING:
if len(embeds) > 10:
@ -190,11 +196,6 @@ def handle_message_parameters(
if message_reference is not MISSING:
payload['message_reference'] = message_reference
if attachments is not MISSING:
# Note: This will be overwritten if file or files is provided
# However, right now this is only passed via Message.edit not Messageable.send
payload['attachments'] = [a.to_dict() for a in attachments]
if stickers is not MISSING:
if stickers is not None:
payload['sticker_ids'] = stickers
@ -224,26 +225,25 @@ def handle_message_parameters(
except KeyError:
pass
multipart = []
if file is not MISSING:
files = [file]
if attachments is MISSING:
attachments = files # type: ignore
else:
files = [a for a in attachments if isinstance(a, File)]
if files:
for index, file in enumerate(files):
attachments_payload = []
for index, file in enumerate(files):
attachment = {
'id': index,
'filename': file.filename,
}
if file.description is not None:
attachment['description'] = file.description
attachments_payload.append(attachment)
if attachments is not MISSING:
file_index = 0
attachments_payload = []
for attachment in attachments:
if isinstance(attachment, File):
attachments_payload.append(attachment.to_dict(file_index))
file_index += 1
else:
attachments_payload.append(attachment.to_dict())
payload['attachments'] = attachments_payload
payload['attachments'] = attachments_payload
multipart = []
if files:
multipart.append({'name': 'payload_json', 'value': utils._to_json(payload)})
payload = None
for index, file in enumerate(files):
@ -596,7 +596,10 @@ class HTTPClient:
def edit_message(self, channel_id: Snowflake, message_id: Snowflake, *, params: MultipartParameters) -> Response[message.Message]:
r = Route('PATCH', '/channels/{channel_id}/messages/{message_id}', channel_id=channel_id, message_id=message_id)
return self.request(r, json=params.payload)
if params.files:
return self.request(r, files=params.files, form=params.multipart)
else:
return self.request(r, json=params.payload)
def add_reaction(self, channel_id: Snowflake, message_id: Snowflake, emoji: str) -> Response[None]:
r = Route(

105
discord/interactions.py

@ -259,8 +259,7 @@ class Interaction:
content: Optional[str] = MISSING,
embeds: List[Embed] = MISSING,
embed: Optional[Embed] = MISSING,
file: File = MISSING,
files: List[File] = MISSING,
attachments: List[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
) -> InteractionMessage:
@ -283,11 +282,14 @@ class Interaction:
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
file: :class:`File`
The file to upload. This cannot be mixed with ``files`` parameter.
files: List[:class:`File`]
A list of files to send with the content. This cannot be mixed with the
``file`` parameter.
attachments: List[Union[:class:`Attachment`, :class:`File`]]
A list of attachments to keep in the message as well as new files to upload. If ``[]`` is passed
then all attachments are removed.
.. note::
New files will always appear after current attachments.
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
@ -302,7 +304,7 @@ class Interaction:
Forbidden
Edited a message that is not yours.
TypeError
You specified both ``embed`` and ``embeds`` or ``file`` and ``files``
You specified both ``embed`` and ``embeds``
ValueError
The length of ``embeds`` was invalid.
@ -315,8 +317,7 @@ class Interaction:
previous_mentions: Optional[AllowedMentions] = self._state.allowed_mentions
params = handle_message_parameters(
content=content,
file=file,
files=files,
attachments=attachments,
embed=embed,
embeds=embeds,
view=view,
@ -549,7 +550,7 @@ class InteractionResponse:
content: Optional[Any] = MISSING,
embed: Optional[Embed] = MISSING,
embeds: List[Embed] = MISSING,
attachments: List[Attachment] = MISSING,
attachments: List[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
allowed_mentions: Optional[AllowedMentions] = MISSING,
) -> None:
@ -567,9 +568,14 @@ class InteractionResponse:
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
attachments: List[:class:`Attachment`]
A list of attachments to keep in the message. If ``[]`` is passed
attachments: List[Union[:class:`Attachment`, :class:`File`]]
A list of attachments to keep in the message as well as new files to upload. If ``[]`` is passed
then all attachments are removed.
.. note::
New files will always appear after current attachments.
view: Optional[:class:`~discord.ui.View`]
The updated view to update this message with. If ``None`` is passed then
the view is removed.
@ -667,8 +673,7 @@ class InteractionMessage(Message):
content: Optional[str] = MISSING,
embeds: List[Embed] = MISSING,
embed: Optional[Embed] = MISSING,
file: File = MISSING,
files: List[File] = MISSING,
attachments: List[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
) -> InteractionMessage:
@ -685,11 +690,14 @@ class InteractionMessage(Message):
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
file: :class:`File`
The file to upload. This cannot be mixed with ``files`` parameter.
files: List[:class:`File`]
A list of files to send with the content. This cannot be mixed with the
``file`` parameter.
attachments: List[Union[:class:`Attachment`, :class:`File`]]
A list of attachments to keep in the message as well as new files to upload. If ``[]`` is passed
then all attachments are removed.
.. note::
New files will always appear after current attachments.
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
@ -704,7 +712,7 @@ class InteractionMessage(Message):
Forbidden
Edited a message that is not yours.
TypeError
You specified both ``embed`` and ``embeds`` or ``file`` and ``files``
You specified both ``embed`` and ``embeds``
ValueError
The length of ``embeds`` was invalid.
@ -717,11 +725,62 @@ class InteractionMessage(Message):
content=content,
embeds=embeds,
embed=embed,
file=file,
files=files,
attachments=attachments,
view=view,
allowed_mentions=allowed_mentions,
)
async def add_files(self, *files: File) -> InteractionMessage:
r"""|coro|
Adds new files to the end of the message attachments.
.. versionadded:: 2.0
Parameters
-----------
\*files: :class:`File`
New files to add to the message.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Tried to edit a message that isn't yours.
Returns
---------
:class:`InteractionMessage`
The newly edited message.
"""
return await self.edit(attachments=[*self.attachments, *files])
async def remove_attachments(self, *attachments: Attachment) -> InteractionMessage:
r"""|coro|
Removes attachments from the message.
.. versionadded:: 2.0
Parameters
-----------
\*attachments: :class:`Attachment`
Attachments to remove from the message.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Tried to edit a message that isn't yours.
Returns
---------
:class:`InteractionMessage`
The newly edited message.
"""
return await self.edit(attachments=[a for a in self.attachments if a not in attachments])
async def delete(self, *, delay: Optional[float] = None) -> None:
"""|coro|

68
discord/message.py

@ -1176,7 +1176,7 @@ class Message(Hashable):
*,
content: Optional[str] = ...,
embed: Optional[Embed] = ...,
attachments: List[Attachment] = ...,
attachments: List[Union[Attachment, File]] = ...,
suppress: bool = ...,
delete_after: Optional[float] = ...,
allowed_mentions: Optional[AllowedMentions] = ...,
@ -1190,7 +1190,7 @@ class Message(Hashable):
*,
content: Optional[str] = ...,
embeds: List[Embed] = ...,
attachments: List[Attachment] = ...,
attachments: List[Union[Attachment, File]] = ...,
suppress: bool = ...,
delete_after: Optional[float] = ...,
allowed_mentions: Optional[AllowedMentions] = ...,
@ -1203,7 +1203,7 @@ class Message(Hashable):
content: Optional[str] = MISSING,
embed: Optional[Embed] = MISSING,
embeds: List[Embed] = MISSING,
attachments: List[Attachment] = MISSING,
attachments: List[Union[Attachment, File]] = MISSING,
suppress: bool = MISSING,
delete_after: Optional[float] = None,
allowed_mentions: Optional[AllowedMentions] = MISSING,
@ -1231,10 +1231,14 @@ class Message(Hashable):
To remove all embeds ``[]`` should be passed.
.. versionadded:: 2.0
attachments: List[:class:`Attachment`]
A list of attachments to keep in the message. If ``[]`` is passed
attachments: List[Union[:class:`Attachment`, :class:`File`]]
A list of attachments to keep in the message as well as new files to upload. If ``[]`` is passed
then all attachments are removed.
.. note::
New files will always appear after current attachments.
.. versionadded:: 2.0
suppress: :class:`bool`
Whether to suppress embeds for the message. This removes
@ -1299,6 +1303,58 @@ class Message(Hashable):
return message
async def add_files(self, *files: File) -> Message:
r"""|coro|
Adds new files to the end of the message attachments.
.. versionadded:: 2.0
Parameters
-----------
\*files: :class:`File`
New files to add to the message.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Tried to edit a message that isn't yours.
Returns
--------
:class:`Message`
The newly edited message.
"""
return await self.edit(attachments=[*self.attachments, *files])
async def remove_attachments(self, *attachments: Attachment) -> Message:
r"""|coro|
Removes attachments from the message.
.. versionadded:: 2.0
Parameters
-----------
\*attachments: :class:`Attachment`
Attachments to remove from the message.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Tried to edit a message that isn't yours.
Returns
--------
:class:`Message`
The newly edited message.
"""
return await self.edit(attachments=[a for a in self.attachments if a not in attachments])
async def publish(self) -> None:
"""|coro|
@ -1643,6 +1699,8 @@ class PartialMessage(Hashable):
jump_url: str = Message.jump_url # type: ignore
edit = Message.edit
add_files = Message.add_files
remove_attachments = Message.remove_attachments
delete = Message.delete
publish = Message.publish
pin = Message.pin

138
discord/webhook/async_.py

@ -30,7 +30,7 @@ import json
import re
from urllib.parse import quote as urlquote
from typing import Any, Dict, List, Literal, NamedTuple, Optional, TYPE_CHECKING, Tuple, Union, overload
from typing import Any, Dict, List, Literal, Optional, TYPE_CHECKING, Tuple, Union, overload
from contextvars import ContextVar
import weakref
@ -434,7 +434,7 @@ def interaction_message_response_params(
files: List[File] = MISSING,
embed: Optional[Embed] = MISSING,
embeds: List[Embed] = MISSING,
attachments: List[Attachment] = MISSING,
attachments: List[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
allowed_mentions: Optional[AllowedMentions] = MISSING,
previous_allowed_mentions: Optional[AllowedMentions] = None,
@ -444,6 +444,12 @@ def interaction_message_response_params(
if embeds is not MISSING and embed is not MISSING:
raise TypeError('Cannot mix embed and embeds keyword arguments.')
if file is not MISSING:
files = [file]
if attachments is not MISSING and files is not MISSING:
raise TypeError('Cannot mix attachments and files keyword arguments.')
data: Optional[Dict[str, Any]] = {
'tts': tts,
}
@ -471,11 +477,6 @@ def interaction_message_response_params(
else:
data['components'] = []
if attachments is not MISSING:
# Note: This will be overwritten if file or files is provided
# However, right now this is only passed via edit not send
data['attachments'] = [a.to_dict() for a in attachments]
if flags is not MISSING:
data['flags'] = flags.value
@ -487,26 +488,25 @@ def interaction_message_response_params(
elif previous_allowed_mentions is not None:
data['allowed_mentions'] = previous_allowed_mentions.to_dict()
multipart = []
if file is not MISSING:
files = [file]
if files:
for index, file in enumerate(files):
attachments_payload = []
for index, file in enumerate(files):
attachment = {
'id': index,
'filename': file.filename,
}
if file.description is not None:
attachment['description'] = file.description
if attachments is MISSING:
attachments = files # type: ignore
else:
files = [a for a in attachments if isinstance(a, File)]
attachments_payload.append(attachment)
if attachments is not MISSING:
file_index = 0
attachments_payload = []
for attachment in attachments:
if isinstance(attachment, File):
attachments_payload.append(attachment.to_dict(file_index))
file_index += 1
else:
attachments_payload.append(attachment.to_dict())
data['attachments'] = attachments_payload
data['attachments'] = attachments_payload
multipart = []
if files:
data = {'type': type, 'data': data}
multipart.append({'name': 'payload_json', 'value': utils._to_json(data)})
data = None
@ -656,8 +656,7 @@ class WebhookMessage(Message):
content: Optional[str] = MISSING,
embeds: List[Embed] = MISSING,
embed: Optional[Embed] = MISSING,
file: File = MISSING,
files: List[File] = MISSING,
attachments: List[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
) -> WebhookMessage:
@ -679,13 +678,13 @@ class WebhookMessage(Message):
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
file: :class:`File`
The file to upload. This cannot be mixed with ``files`` parameter.
attachments: List[Union[:class:`Attachment`, :class:`File`]]
A list of attachments to keep in the message as well as new files to upload. If ``[]`` is passed
then all attachments are removed.
.. note::
.. versionadded:: 2.0
files: List[:class:`File`]
A list of files to send with the content. This cannot be mixed with the
``file`` parameter.
New files will always appear after current attachments.
.. versionadded:: 2.0
allowed_mentions: :class:`AllowedMentions`
@ -704,7 +703,7 @@ class WebhookMessage(Message):
Forbidden
Edited a message that is not yours.
TypeError
You specified both ``embed`` and ``embeds`` or ``file`` and ``files``
You specified both ``embed`` and ``embeds``
ValueError
The length of ``embeds`` was invalid
InvalidArgument
@ -720,12 +719,63 @@ class WebhookMessage(Message):
content=content,
embeds=embeds,
embed=embed,
file=file,
files=files,
attachments=attachments,
view=view,
allowed_mentions=allowed_mentions,
)
async def add_files(self, *files: File) -> WebhookMessage:
r"""|coro|
Adds new files to the end of the message attachments.
.. versionadded:: 2.0
Parameters
-----------
\*files: :class:`File`
New files to add to the message.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Tried to edit a message that isn't yours.
Returns
--------
:class:`WebhookMessage`
The newly edited message.
"""
return await self.edit(attachments=[*self.attachments, *files])
async def remove_attachments(self, *attachments: Attachment) -> WebhookMessage:
r"""|coro|
Removes attachments from the message.
.. versionadded:: 2.0
Parameters
-----------
\*attachments: :class:`Attachment`
Attachments to remove from the message.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Tried to edit a message that isn't yours.
Returns
--------
:class:`WebhookMessage`
The newly edited message.
"""
return await self.edit(attachments=[a for a in self.attachments if a not in attachments])
async def delete(self, *, delay: Optional[float] = None) -> None:
"""|coro|
@ -1470,8 +1520,7 @@ class Webhook(BaseWebhook):
content: Optional[str] = MISSING,
embeds: List[Embed] = MISSING,
embed: Optional[Embed] = MISSING,
file: File = MISSING,
files: List[File] = MISSING,
attachments: List[Union[Attachment, File]] = MISSING,
view: Optional[View] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
) -> WebhookMessage:
@ -1498,13 +1547,9 @@ class Webhook(BaseWebhook):
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
file: :class:`File`
The file to upload. This cannot be mixed with ``files`` parameter.
.. versionadded:: 2.0
files: List[:class:`File`]
A list of files to send with the content. This cannot be mixed with the
``file`` parameter.
attachments: List[Union[:class:`Attachment`, :class:`File`]]
A list of attachments to keep in the message as well as new files to upload. If ``[]`` is passed
then all attachments are removed.
.. versionadded:: 2.0
allowed_mentions: :class:`AllowedMentions`
@ -1524,7 +1569,7 @@ class Webhook(BaseWebhook):
Forbidden
Edited a message that is not yours.
TypeError
You specified both ``embed`` and ``embeds`` or ``file`` and ``files``
You specified both ``embed`` and ``embeds``
ValueError
The length of ``embeds`` was invalid
InvalidArgument
@ -1549,8 +1594,7 @@ class Webhook(BaseWebhook):
previous_mentions: Optional[AllowedMentions] = getattr(self._state, 'allowed_mentions', None)
params = handle_message_parameters(
content=content,
file=file,
files=files,
attachments=attachments,
embed=embed,
embeds=embeds,
view=view,

89
discord/webhook/sync.py

@ -59,6 +59,7 @@ if TYPE_CHECKING:
from ..file import File
from ..embeds import Embed
from ..mentions import AllowedMentions
from ..message import Attachment
from ..types.webhook import (
Webhook as WebhookPayload,
)
@ -381,8 +382,7 @@ class SyncWebhookMessage(Message):
content: Optional[str] = MISSING,
embeds: List[Embed] = MISSING,
embed: Optional[Embed] = MISSING,
file: File = MISSING,
files: List[File] = MISSING,
attachments: List[Union[Attachment, File]] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
) -> SyncWebhookMessage:
"""Edits the message.
@ -396,11 +396,15 @@ class SyncWebhookMessage(Message):
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
file: :class:`File`
The file to upload. This cannot be mixed with ``files`` parameter.
files: List[:class:`File`]
A list of files to send with the content. This cannot be mixed with the
``file`` parameter.
attachments: List[Union[:class:`Attachment`, :class:`File`]]
A list of attachments to keep in the message as well as new files to upload. If ``[]`` is passed
then all attachments are removed.
.. note::
New files will always appear after current attachments.
.. versionadded:: 2.0
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
@ -412,7 +416,7 @@ class SyncWebhookMessage(Message):
Forbidden
Edited a message that is not yours.
TypeError
You specified both ``embed`` and ``embeds`` or ``file`` and ``files``
You specified both ``embed`` and ``embeds``
ValueError
The length of ``embeds`` was invalid
InvalidArgument
@ -428,11 +432,58 @@ class SyncWebhookMessage(Message):
content=content,
embeds=embeds,
embed=embed,
file=file,
files=files,
attachments=attachments,
allowed_mentions=allowed_mentions,
)
def add_files(self, *files: File) -> SyncWebhookMessage:
r"""Adds new files to the end of the message attachments.
.. versionadded:: 2.0
Parameters
-----------
\*files: :class:`File`
New files to add to the message.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Tried to edit a message that isn't yours.
Returns
--------
:class:`SyncWebhookMessage`
The newly edited message.
"""
return self.edit(attachments=[*self.attachments, *files])
def remove_attachments(self, *attachments: Attachment) -> SyncWebhookMessage:
r"""Removes attachments from the message.
.. versionadded:: 2.0
Parameters
-----------
\*attachments: :class:`Attachment`
Attachments to remove from the message.
Raises
-------
HTTPException
Editing the message failed.
Forbidden
Tried to edit a message that isn't yours.
Returns
--------
:class:`SyncWebhookMessage`
The newly edited message.
"""
return self.edit(attachments=[a for a in self.attachments if a not in attachments])
def delete(self, *, delay: Optional[float] = None) -> None:
"""Deletes the message.
@ -966,8 +1017,7 @@ class SyncWebhook(BaseWebhook):
content: Optional[str] = MISSING,
embeds: List[Embed] = MISSING,
embed: Optional[Embed] = MISSING,
file: File = MISSING,
files: List[File] = MISSING,
attachments: List[Union[Attachment, File]] = MISSING,
allowed_mentions: Optional[AllowedMentions] = None,
) -> SyncWebhookMessage:
"""Edits a message owned by this webhook.
@ -988,11 +1038,11 @@ class SyncWebhook(BaseWebhook):
embed: Optional[:class:`Embed`]
The embed to edit the message with. ``None`` suppresses the embeds.
This should not be mixed with the ``embeds`` parameter.
file: :class:`File`
The file to upload. This cannot be mixed with ``files`` parameter.
files: List[:class:`File`]
A list of files to send with the content. This cannot be mixed with the
``file`` parameter.
attachments: List[Union[:class:`Attachment`, :class:`File`]]
A list of attachments to keep in the message as well as new files to upload. If ``[]`` is passed
then all attachments are removed.
.. versionadded:: 2.0
allowed_mentions: :class:`AllowedMentions`
Controls the mentions being processed in this message.
See :meth:`.abc.Messageable.send` for more information.
@ -1004,7 +1054,7 @@ class SyncWebhook(BaseWebhook):
Forbidden
Edited a message that is not yours.
TypeError
You specified both ``embed`` and ``embeds`` or ``file`` and ``files``
You specified both ``embed`` and ``embeds``
ValueError
The length of ``embeds`` was invalid
InvalidArgument
@ -1017,8 +1067,7 @@ class SyncWebhook(BaseWebhook):
previous_mentions: Optional[AllowedMentions] = getattr(self._state, 'allowed_mentions', None)
params = handle_message_parameters(
content=content,
file=file,
files=files,
attachments=attachments,
embed=embed,
embeds=embeds,
allowed_mentions=allowed_mentions,

Loading…
Cancel
Save