|
36 | 36 | DiscordThreadId, |
37 | 37 | InteractionResponseType, |
38 | 38 | ) |
39 | | -from chat_sdk.emoji import convert_emoji_placeholders |
| 39 | +from chat_sdk.emoji import convert_emoji_placeholders, get_emoji, resolve_emoji_from_gchat |
40 | 40 | from chat_sdk.logger import ConsoleLogger, Logger |
41 | | -from chat_sdk.shared.adapter_utils import extract_card |
| 41 | +from chat_sdk.shared.adapter_utils import extract_card, extract_files |
42 | 42 | from chat_sdk.shared.errors import NetworkError, ValidationError |
43 | 43 | from chat_sdk.types import ( |
44 | 44 | ActionEvent, |
|
50 | 50 | EmojiValue, |
51 | 51 | FetchOptions, |
52 | 52 | FetchResult, |
| 53 | + FileUpload, |
53 | 54 | FormattedContent, |
54 | 55 | Message, |
55 | 56 | MessageMetadata, |
@@ -677,13 +678,21 @@ async def _handle_forwarded_reaction( |
677 | 678 | emoji_id = emoji_data.get("id") |
678 | 679 | raw_emoji = f"<:{emoji_name}:{emoji_id}>" if emoji_id else emoji_name |
679 | 680 |
|
| 681 | + # Normalize emoji through the emoji resolver |
| 682 | + if emoji_name and not emoji_id: |
| 683 | + # Standard unicode emoji -- resolve through gchat (unicode) resolver |
| 684 | + normalized = resolve_emoji_from_gchat(emoji_name) |
| 685 | + else: |
| 686 | + # Custom emoji -- use custom:{id} key or raw name |
| 687 | + normalized = get_emoji(f"custom:{emoji_id}" if emoji_id else emoji_name) |
| 688 | + |
680 | 689 | self._chat.process_reaction( |
681 | 690 | ReactionEvent( |
682 | 691 | adapter=self, |
683 | 692 | thread=None, |
684 | 693 | thread_id=thread_id, |
685 | 694 | message_id=data.get("message_id", ""), |
686 | | - emoji=EmojiValue(name=emoji_name), |
| 695 | + emoji=normalized, |
687 | 696 | raw_emoji=raw_emoji, |
688 | 697 | added=added, |
689 | 698 | user=Author( |
@@ -730,20 +739,59 @@ async def post_message( |
730 | 739 | if components: |
731 | 740 | payload["components"] = components |
732 | 741 |
|
| 742 | + # --- Handle file attachments via multipart/form-data --- |
| 743 | + files = extract_files(message) |
| 744 | + |
| 745 | + # --- Resolve deferred slash-command interaction if pending --- |
| 746 | + req_ctx = self._request_context.get() |
| 747 | + slash_ctx = req_ctx.slash_command if req_ctx else None |
| 748 | + if slash_ctx and not slash_ctx.initial_response_sent: |
| 749 | + slash_ctx.initial_response_sent = True |
| 750 | + self._logger.debug( |
| 751 | + "Discord API: PATCH deferred interaction response", |
| 752 | + { |
| 753 | + "channelId": channel_id, |
| 754 | + "contentLength": len(payload.get("content", "")), |
| 755 | + "embedCount": len(embeds), |
| 756 | + "componentCount": len(components), |
| 757 | + "fileCount": len(files), |
| 758 | + }, |
| 759 | + ) |
| 760 | + |
| 761 | + result = await self._discord_fetch( |
| 762 | + f"/webhooks/{self._application_id}/{slash_ctx.interaction_token}/messages/@original", |
| 763 | + "PATCH", |
| 764 | + payload, |
| 765 | + files=files or None, |
| 766 | + ) |
| 767 | + |
| 768 | + self._logger.debug( |
| 769 | + "Discord API: PATCH deferred interaction response completed", |
| 770 | + {"messageId": result.get("id") if result else None}, |
| 771 | + ) |
| 772 | + |
| 773 | + return RawMessage( |
| 774 | + id=(result or {}).get("id", ""), |
| 775 | + thread_id=thread_id, |
| 776 | + raw=result or {}, |
| 777 | + ) |
| 778 | + |
733 | 779 | self._logger.debug( |
734 | 780 | "Discord API: POST message", |
735 | 781 | { |
736 | 782 | "channelId": channel_id, |
737 | 783 | "contentLength": len(payload.get("content", "")), |
738 | 784 | "embedCount": len(embeds), |
739 | 785 | "componentCount": len(components), |
| 786 | + "fileCount": len(files), |
740 | 787 | }, |
741 | 788 | ) |
742 | 789 |
|
743 | 790 | result = await self._discord_fetch( |
744 | 791 | f"/channels/{channel_id}/messages", |
745 | 792 | "POST", |
746 | 793 | payload, |
| 794 | + files=files or None, |
747 | 795 | ) |
748 | 796 |
|
749 | 797 | self._logger.debug( |
@@ -1255,25 +1303,48 @@ async def _discord_fetch( |
1255 | 1303 | path: str, |
1256 | 1304 | method: str, |
1257 | 1305 | body: Any = None, |
| 1306 | + files: list[FileUpload] | None = None, |
1258 | 1307 | ) -> Any: |
1259 | | - """Make a request to the Discord API using aiohttp (lazy import).""" |
| 1308 | + """Make a request to the Discord API using aiohttp (lazy import). |
| 1309 | +
|
| 1310 | + When *files* is provided the request uses ``multipart/form-data`` |
| 1311 | + with a ``payload_json`` field for the JSON body and one field per |
| 1312 | + file attachment, matching the Discord API multipart upload spec. |
| 1313 | + """ |
1260 | 1314 | import aiohttp # lazy import |
1261 | 1315 |
|
1262 | 1316 | url = f"{DISCORD_API_BASE}{path}" |
1263 | 1317 | headers: dict[str, str] = { |
1264 | 1318 | "Authorization": f"Bot {self._bot_token}", |
1265 | 1319 | } |
1266 | 1320 |
|
1267 | | - if body is not None: |
1268 | | - headers["Content-Type"] = "application/json" |
| 1321 | + # Build request kwargs depending on whether we have file uploads |
| 1322 | + request_kwargs: dict[str, Any] = {} |
| 1323 | + if files: |
| 1324 | + # Multipart form-data with payload_json + file parts |
| 1325 | + form = aiohttp.FormData() |
| 1326 | + form.add_field("payload_json", json.dumps(body or {}), content_type="application/json") |
| 1327 | + for idx, file in enumerate(files): |
| 1328 | + form.add_field( |
| 1329 | + f"files[{idx}]", |
| 1330 | + file.data, |
| 1331 | + filename=file.filename, |
| 1332 | + content_type=file.mime_type or "application/octet-stream", |
| 1333 | + ) |
| 1334 | + request_kwargs["data"] = form |
| 1335 | + # Do NOT set Content-Type header -- aiohttp sets the multipart boundary |
| 1336 | + else: |
| 1337 | + if body is not None: |
| 1338 | + headers["Content-Type"] = "application/json" |
| 1339 | + request_kwargs["json"] = body |
1269 | 1340 |
|
1270 | 1341 | async with ( |
1271 | 1342 | aiohttp.ClientSession() as session, |
1272 | 1343 | session.request( |
1273 | 1344 | method, |
1274 | 1345 | url, |
1275 | 1346 | headers=headers, |
1276 | | - json=body if body is not None else None, |
| 1347 | + **request_kwargs, |
1277 | 1348 | ) as response, |
1278 | 1349 | ): |
1279 | 1350 | if not response.ok: |
|
0 commit comments