Repository: Pycord-Development/pycord

Issue:
Can't create Forum threads with a file
### Summary

Trying to pass `file` or `files` into `ForumChannel.create_thread` raises a `HTTPException` due to using the wrong method.

### Reproduction Steps

1) Attempt to create a forum thread with either the `file` or `files` parameter

### Minimal Reproducible Code

```python
forum_channel_id = ...  # Whatever your ID is
path = "..."  # some image path
forum_channel = bot.get_channel(forum_channel_id)
await forum_channel.create_thread(name="A Thread", file=discord.File(path))
```


### Expected Results

The thread is created with the file included.

### Actual Results

```py
Traceback (most recent call last):
  File "/home/container/.local/lib/python3.10/site-packages/discord/channel.py", line 1278, in create_thread
    data = await state.http.send_files(
  File "/home/container/.local/lib/python3.10/site-packages/discord/http.py", line 366, in request
    raise HTTPException(response, data)
discord.errors.HTTPException: 400 Bad Request (error code: 50008): Cannot send messages in a non-text channel
```

### Intents

all

### System Information

- Python v3.10.0-final
- py-cord v2.4.0-final
- aiohttp v3.8.1
- system info: Windows 10 10.0.19045

### Checklist

- [X] I have searched the open issues for duplicates.
- [X] I have shown the entire traceback, if possible.
- [X] I have removed my token from display, if visible.

### Additional Context

This happens because when `file` or `files` is passed in, [we use the separate `http.send_files` method](https://github.com/Pycord-Development/pycord/blob/master/discord/channel.py#L1277) which directs *to the forum channel ID*, not the thread ID.
```py
        if file is not None:
            ...
            try:
                data = await state.http.send_files(self.id, files=[file], ... )  # ForumChannel ID
            finally:
                file.close()

        elif files is not None:
            ...
            try:
                data = await state.http.send_files(self.id,  files=files, ... )  # ForumChannel ID
            finally:
                for f in files:
                    f.close()
        else:
            data = await state.http.start_forum_thread(...)  # No files arg
```
This method is only used in the default `Messageable.send` and isn't really made for this purpose as it's a `send` method, not for creating threads. [The thread creation endpoint supports files directly](https://discord.com/developers/docs/resources/channel#start-thread-in-forum-channel) as part of its message payload, so if I had to guess this was just an oversight that hadn't been considered. I've created this issue for posterity and may look into fixing it myself, but if anyone else wants to go for it then be my guest.


Discussion:
May I add, same thing happens when a webhook tries creating a forum thread with files attached. It seems that the `thread_name` entry tries to get included in the `payload` dict, but when this happens, `payload` equals to None and throws this error.
```
File "C:\Users\Stickfab\AppData\Local\Programs\Python\Python310\lib\site-packages\discord\webhook\async_.py", line 1745, in send
    data = await adapter.execute_webhook(
  File "C:\Users\Stickfab\AppData\Local\Programs\Python\Python310\lib\site-packages\discord\webhook\async_.py", line 344, in execute_webhook
    payload["thread_name"] = thread_name
TypeError: 'NoneType' object does not support item assignment
```

Upon further investigation, it seems that the code turns `payload` to None when files are included in the message, which contradicts the fact that it needs a payload to create a thread.
```py
# /discord/webhook/async_.py ; lines 610-631
def handle_message_parameters(...) -> ExecuteWebhookParameters:
    ...
    payload = {}
```
```py
# /discord/webhook/async_.py ; lines 668-670
if files:
        multipart.append({"name": "payload_json", "value": utils._to_json(payload)})
        payload = None
```
```py
# /discord/webhook/async_.py ; lines 1726-1757
params = handle_message_parameters(...)     # in this case, since it has files, params.payload is None
...
data = await adapter.execute_webhook(
            ...
            payload=params.payload,
            ...
        )
```

```py
# /discord/webhook/async_.py ; lines 324-344
def execute_webhook(...) -> Response[MessagePayload | None]:
        params = {"wait": int(wait)}
        if thread_id:
            params["thread_id"] = thread_id

        if thread_name:
            payload["thread_name"] = thread_name     # payload is still None, and throws the "TypeError: 'NoneType' object does not support item assignment" exception
```
Mirroring @honzajavorek's  workaround from their previous [discussion](https://github.com/Pycord-Development/pycord/discussions/1948) post here for convenience:

Editing forum threads to add a file upload, like so, is working as expected

```python
thread = await channel.create_thread(...)
message = await thread.fetch_message(thread.id)
await message.edit(file=File(path))
```

Resolving patch:
diff --git a/discord/channel.py b/discord/channel.py
--- a/discord/channel.py
+++ b/discord/channel.py
@@ -1274,26 +1274,7 @@ async def create_thread(
         if file is not None and files is not None:
             raise InvalidArgument("cannot pass both file and files parameter to send()")
 
-        if file is not None:
-            if not isinstance(file, File):
-                raise InvalidArgument("file parameter must be File")
-
-            try:
-                data = await state.http.send_files(
-                    self.id,
-                    files=[file],
-                    allowed_mentions=allowed_mentions,
-                    content=message_content,
-                    embed=embed,
-                    embeds=embeds,
-                    nonce=nonce,
-                    stickers=stickers,
-                    components=components,
-                )
-            finally:
-                file.close()
-
-        elif files is not None:
+        if files is not None:
             if len(files) > 10:
                 raise InvalidArgument(
                     "files parameter must be a list of up to 10 elements"
@@ -1301,26 +1282,17 @@ async def create_thread(
             elif not all(isinstance(file, File) for file in files):
                 raise InvalidArgument("files parameter must be a list of File")
 
-            try:
-                data = await state.http.send_files(
-                    self.id,
-                    files=files,
-                    content=message_content,
-                    embed=embed,
-                    embeds=embeds,
-                    nonce=nonce,
-                    allowed_mentions=allowed_mentions,
-                    stickers=stickers,
-                    components=components,
-                )
-            finally:
-                for f in files:
-                    f.close()
-        else:
+        if file is not None:
+            if not isinstance(file, File):
+                raise InvalidArgument("file parameter must be File")
+            files = [file]
+
+        try:
             data = await state.http.start_forum_thread(
                 self.id,
                 content=message_content,
                 name=name,
+                files=files,
                 embed=embed,
                 embeds=embeds,
                 nonce=nonce,
@@ -1333,6 +1305,11 @@ async def create_thread(
                 applied_tags=applied_tags,
                 reason=reason,
             )
+        finally:
+            if files is not None:
+                for f in files:
+                    f.close()
+
         ret = Thread(guild=self.guild, state=self._state, data=data)
         msg = ret.get_partial_message(data["last_message_id"])
         if view:
diff --git a/discord/http.py b/discord/http.py
--- a/discord/http.py
+++ b/discord/http.py
@@ -1170,6 +1170,7 @@ def start_forum_thread(
         invitable: bool = True,
         applied_tags: SnowflakeList | None = None,
         reason: str | None = None,
+        files: Sequence[File] | None = None,
         embed: embed.Embed | None = None,
         embeds: list[embed.Embed] | None = None,
         nonce: str | None = None,
@@ -1177,7 +1178,7 @@ def start_forum_thread(
         stickers: list[sticker.StickerItem] | None = None,
         components: list[components.Component] | None = None,
     ) -> Response[threads.Thread]:
-        payload = {
+        payload: dict[str, Any] = {
             "name": name,
             "auto_archive_duration": auto_archive_duration,
             "invitable": invitable,
@@ -1208,12 +1209,38 @@ def start_forum_thread(
 
         if rate_limit_per_user:
             payload["rate_limit_per_user"] = rate_limit_per_user
+
         # TODO: Once supported by API, remove has_message=true query parameter
         route = Route(
             "POST",
             "/channels/{channel_id}/threads?has_message=true",
             channel_id=channel_id,
         )
+
+        if files:
+            form = [{"name": "payload_json"}]
+
+            attachments = []
+            for index, file in enumerate(files):
+                attachments.append(
+                    {
+                        "id": index,
+                        "filename": file.filename,
+                        "description": file.description,
+                    }
+                )
+                form.append(
+                    {
+                        "name": f"files[{index}]",
+                        "value": file.fp,
+                        "filename": file.filename,
+                        "content_type": "application/octet-stream",
+                    }
+                )
+
+            payload["attachments"] = attachments
+            form[0]["value"] = utils._to_json(payload)
+            return self.request(route, form=form, reason=reason)
         return self.request(route, json=payload, reason=reason)
 
     def join_thread(self, channel_id: Snowflake) -> Response[None]:


Test patch: