Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Uploading/Attaching files to channel messages #653

Merged
merged 2 commits into from
Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 27 additions & 3 deletions interactions/api/http/message.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import List, Optional, Union

from aiohttp import MultipartWriter

from ...api.cache import Cache, Item
from ..models.message import Embed, Message
from ..models.misc import Snowflake
from ..models.misc import MISSING, File, Snowflake
from .request import _Request
from .route import Route

Expand Down Expand Up @@ -56,16 +58,38 @@ async def send_message(

return await self.create_message(payload, channel_id)

async def create_message(self, payload: dict, channel_id: int) -> dict:
async def create_message(
self, payload: dict, channel_id: int, files: Optional[List[File]] = MISSING
) -> dict:
"""
Send a message to the specified channel.

:param payload: Dictionary contents of a message. (i.e. message payload)
:param channel_id: Channel snowflake ID.
:param files: An optional list of files to send attached to the message.
:return dict: Dictionary representing a message (?)
"""

data = None
if files is not MISSING and len(files) > 0:

data = MultipartWriter("form-data")
part = data.append_json(payload)
part.set_content_disposition("form-data", name="payload_json")
payload = None

for id, file in enumerate(files):
part = data.append(
file._fp,
)
part.set_content_disposition(
"form-data", name="files[" + str(id) + "]", filename=file._filename
)

request = await self._req.request(
Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id), json=payload
Route("POST", "/channels/{channel_id}/messages", channel_id=channel_id),
json=payload,
data=data,
)
if request.get("id"):
self.cache.messages.add(Item(id=request["id"], value=Message(**request)))
Expand Down
4 changes: 2 additions & 2 deletions interactions/api/http/message.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ from typing import List, Optional, Union

from ...api.cache import Cache
from ..models.message import Embed, Message
from ..models.misc import Snowflake
from ..models.misc import Snowflake, File
from .request import _Request


Expand All @@ -23,7 +23,7 @@ class _MessageRequest:
allowed_mentions=None, # don't know type
message_reference: Optional[Message] = None,
) -> dict: ...
async def create_message(self, payload: dict, channel_id: int) -> dict: ...
async def create_message(self, payload: dict, channel_id: int, files: Optional[List[File]]) -> dict: ...
async def get_message(self, channel_id: int, message_id: int) -> Optional[dict]: ...
async def delete_message(
self, channel_id: int, message_id: int, reason: Optional[str] = None
Expand Down
1 change: 1 addition & 0 deletions interactions/api/http/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ async def request(self, route: Route, **kwargs) -> Optional[Any]:
"""

kwargs["headers"] = {**self._headers, **kwargs.get("headers", {})}

if kwargs.get("json"):
kwargs["headers"]["Content-Type"] = "application/json"

Expand Down
22 changes: 16 additions & 6 deletions interactions/api/models/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from enum import IntEnum
from typing import Callable, List, Optional, Union

from .misc import MISSING, DictSerializerMixin, Overwrite, Snowflake
from .misc import MISSING, DictSerializerMixin, File, Overwrite, Snowflake


class ChannelType(IntEnum):
Expand Down Expand Up @@ -199,7 +199,7 @@ async def send(
content: Optional[str] = MISSING,
*,
tts: Optional[bool] = MISSING,
# attachments: Optional[List[Any]] = None, # TODO: post-v4: Replace with own file type.
files: Optional[Union[File, List[File]]] = MISSING,
embeds: Optional[Union["Embed", List["Embed"]]] = MISSING, # noqa
allowed_mentions: Optional["MessageInteraction"] = MISSING, # noqa
components: Optional[
Expand All @@ -220,6 +220,8 @@ async def send(
:type content: Optional[str]
:param tts?: Whether the message utilizes the text-to-speech Discord programme or not.
:type tts: Optional[bool]
:param files?: A file or list of files to be attached to the message.
:type files: Optional[Union[File, List[File]]]
:param embeds?: An embed, or list of embeds for the message.
:type embeds: Optional[Union[Embed, List[Embed]]]
:param allowed_mentions?: The message interactions/mention limits that the message can refer to.
Expand Down Expand Up @@ -251,18 +253,26 @@ async def send(
else:
_components = _build_components(components=components)

# TODO: post-v4: Add attachments into Message obj.
if not files or files is MISSING:
_files = []
elif isinstance(files, list):
_files = [file._json_payload(id) for id, file in enumerate(files)]
else:
_files = [files._json_payload(0)]
files = [files]

payload = Message(
content=_content,
tts=_tts,
# file=file,
# attachments=_attachments,
attachments=_files,
embeds=_embeds,
allowed_mentions=_allowed_mentions,
components=_components,
)

res = await self._client.create_message(channel_id=int(self.id), payload=payload._json)
res = await self._client.create_message(
channel_id=int(self.id), payload=payload._json, files=files
)
return Message(**res, _client=self._client)

async def delete(self) -> None:
Expand Down
4 changes: 2 additions & 2 deletions interactions/api/models/channel.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ from typing import List, Optional, Union, Callable
from .guild import Invite, InviteTargetType
from .message import Message, Embed, MessageInteraction
from ...models.component import ActionRow, Button, SelectMenu
from .misc import DictSerializerMixin, Overwrite, Snowflake, MISSING
from .misc import DictSerializerMixin, Overwrite, Snowflake, MISSING, File
from .user import User
from ..http.client import HTTPClient

Expand Down Expand Up @@ -77,7 +77,7 @@ class Channel(DictSerializerMixin):
content: Optional[str] = MISSING,
*,
tts: Optional[bool] = MISSING,
# attachments: Optional[List[Any]] = None, # TODO: post-v4: Replace with own file type.
files: Optional[Union[File, List[File]]] = MISSING,
embeds: Optional[Union[Embed, List[Embed]]] = MISSING,
allowed_mentions: Optional[MessageInteraction] = MISSING,
components: Optional[
Expand Down
40 changes: 39 additions & 1 deletion interactions/api/models/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
# TODO: Reorganise mixins to its own thing, currently placed here because circular import sucks.
# also, it should be serialiser* but idk, fl0w'd say something if I left it like that. /shrug
import datetime
from io import IOBase
from logging import Logger
from math import floor
from typing import Union
from os.path import basename
from typing import Optional, Union

from interactions.base import get_logger

Expand Down Expand Up @@ -232,3 +234,39 @@ class MISSING:
"""A pseudosentinel based from an empty object. This does violate PEP, but, I don't care."""

...


class File(object):
"""
A File object to be sent as an attachment along with a message.

If an fp is not given, this will try to open & send a local file at the location
specified in the 'filename' parameter.

.. note::
If a description is not given the file's basename is used instead.
"""

def __init__(
self, filename: str, fp: Optional[IOBase] = MISSING, description: Optional[str] = MISSING
):

if not isinstance(filename, str):
raise TypeError(
"File's first parameter 'filename' must be a string, not " + str(type(filename))
)

if not fp or fp is MISSING:
self._fp = open(filename, "rb")
else:
self._fp = fp

self._filename = basename(filename)

if not description or description is MISSING:
self._description = self._filename
else:
self._description = description

def _json_payload(self, id):
return {"id": id, "description": self._description, "filename": self._filename}
14 changes: 13 additions & 1 deletion interactions/api/models/misc.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
from datetime import datetime
from typing import Optional, Union

from io import IOBase

log: logging.Logger

Expand Down Expand Up @@ -62,3 +62,15 @@ class Format:
def stylize(self, format: str, **kwargs) -> str: ...

class MISSING: ...

class File(object):
_filename: str
_fp: IOBase
_description: str
def __init__(
self,
filename: str,
fp: Optional[IOBase] = MISSING,
description: Optional[str] = MISSING
) -> None: ...
def _json_payload(self, id) -> dict: ...