diff --git a/integration_tests/web/test_message_metadata.py b/integration_tests/web/test_message_metadata.py new file mode 100644 index 000000000..3676446bf --- /dev/null +++ b/integration_tests/web/test_message_metadata.py @@ -0,0 +1,128 @@ +import logging +import os +import time +import unittest + +from integration_tests.env_variable_names import SLACK_SDK_TEST_BOT_TOKEN +from slack_sdk.models.metadata import Metadata +from slack_sdk.web import WebClient + + +class TestWebClient(unittest.TestCase): + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + + def tearDown(self): + pass + + def test_publishing_message_metadata(self): + client: WebClient = WebClient(token=self.bot_token) + new_message = client.chat_postMessage( + channel='#random', + text="message with metadata", + metadata={ + "event_type": "procurement-task", + "event_payload": { + "id": "11111", + "amount": 5000, + "tags": ["foo", "bar", "baz"] + }, + } + ) + self.assertIsNone(new_message.get("error")) + self.assertIsNotNone(new_message.get("message").get("metadata")) + + history = client.conversations_history( + channel=new_message.get("channel"), + limit=1, + include_all_metadata=True, + ) + self.assertIsNone(history.get("error")) + self.assertIsNotNone(history.get("messages")[0].get("metadata")) + + modification = client.chat_update( + channel=new_message.get("channel"), + ts=new_message.get("ts"), + text="message with metadata (modified)", + metadata={ + "event_type": "procurement-task", + "event_payload": { + "id": "11111", + "amount": 6000, + }, + } + ) + self.assertIsNone(modification.get("error")) + self.assertIsNotNone(modification.get("message").get("metadata")) + + scheduled = client.chat_scheduleMessage( + channel=new_message.get("channel"), + post_at=int(time.time()) + 30, + text="message with metadata (scheduled)", + metadata={ + "event_type": "procurement-task", + "event_payload": { + "id": "11111", + "amount": 10, + }, + } + ) + self.assertIsNone(scheduled.get("error")) + self.assertIsNotNone(scheduled.get("message").get("metadata")) + + def test_publishing_message_metadata_using_models(self): + client: WebClient = WebClient(token=self.bot_token) + new_message = client.chat_postMessage( + channel='#random', + text="message with metadata", + metadata=Metadata( + event_type="procurement-task", + event_payload={ + "id": "11111", + "amount": 5000, + "tags": ["foo", "bar", "baz"] + } + ) + ) + self.assertIsNone(new_message.get("error")) + self.assertIsNotNone(new_message.get("message").get("metadata")) + + history = client.conversations_history( + channel=new_message.get("channel"), + limit=1, + include_all_metadata=True, + ) + self.assertIsNone(history.get("error")) + self.assertIsNotNone(history.get("messages")[0].get("metadata")) + + modification = client.chat_update( + channel=new_message.get("channel"), + ts=new_message.get("ts"), + text="message with metadata (modified)", + metadata=Metadata( + event_type="procurement-task", + event_payload={ + "id": "11111", + "amount": 6000, + } + ) + ) + self.assertIsNone(modification.get("error")) + self.assertIsNotNone(modification.get("message").get("metadata")) + + scheduled = client.chat_scheduleMessage( + channel=new_message.get("channel"), + post_at=int(time.time()) + 30, + text="message with metadata (scheduled)", + metadata=Metadata( + event_type="procurement-task", + event_payload={ + "id": "11111", + "amount": 10, + } + ) + ) + self.assertIsNone(scheduled.get("error")) + self.assertIsNotNone(scheduled.get("message").get("metadata")) diff --git a/slack_sdk/models/metadata/__init__.py b/slack_sdk/models/metadata/__init__.py new file mode 100644 index 000000000..5a1b7a9e3 --- /dev/null +++ b/slack_sdk/models/metadata/__init__.py @@ -0,0 +1,30 @@ +from typing import Dict, Any +from slack_sdk.models.basic_objects import JsonObject + + +class Metadata(JsonObject): + """Message metadata + + https://api.slack.com/metadata + """ + + attributes = { + "event_type", + "event_payload", + } + + def __init__( + self, + event_type: str, + event_payload: Dict[str, Any], + **kwargs, + ): + self.event_type = event_type + self.event_payload = event_payload + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() diff --git a/slack_sdk/web/async_client.py b/slack_sdk/web/async_client.py index dbe3b8b46..c3ca205fb 100644 --- a/slack_sdk/web/async_client.py +++ b/slack_sdk/web/async_client.py @@ -24,6 +24,7 @@ ) from ..models.attachments import Attachment from ..models.blocks import Block +from ..models.metadata import Metadata class AsyncWebClient(AsyncBaseClient): @@ -2098,6 +2099,7 @@ async def chat_postMessage( link_names: Optional[bool] = None, username: Optional[str] = None, parse: Optional[str] = None, # none, full + metadata: Optional[Union[Dict, Metadata]] = None, **kwargs, ) -> AsyncSlackResponse: """Sends a message to a channel. @@ -2122,6 +2124,7 @@ async def chat_postMessage( "link_names": link_names, "username": username, "parse": parse, + "metadata": metadata, } ) _parse_web_class_objects(kwargs) @@ -2145,6 +2148,7 @@ async def chat_scheduleMessage( unfurl_links: Optional[bool] = None, unfurl_media: Optional[bool] = None, link_names: Optional[bool] = None, + metadata: Optional[Union[Dict, Metadata]] = None, **kwargs, ) -> AsyncSlackResponse: """Schedules a message. @@ -2164,6 +2168,7 @@ async def chat_scheduleMessage( "unfurl_links": unfurl_links, "unfurl_media": unfurl_media, "link_names": link_names, + "metadata": metadata, } ) _parse_web_class_objects(kwargs) @@ -2215,6 +2220,7 @@ async def chat_update( link_names: Optional[bool] = None, parse: Optional[str] = None, # none, full reply_broadcast: Optional[bool] = None, + metadata: Optional[Union[Dict, Metadata]] = None, **kwargs, ) -> AsyncSlackResponse: """Updates a message in a channel. @@ -2231,6 +2237,7 @@ async def chat_update( "link_names": link_names, "parse": parse, "reply_broadcast": reply_broadcast, + "metadata": metadata, } ) if isinstance(file_ids, (list, Tuple)): @@ -2375,6 +2382,7 @@ async def conversations_history( channel: str, cursor: Optional[str] = None, inclusive: Optional[bool] = None, + include_all_metadata: Optional[bool] = None, latest: Optional[str] = None, limit: Optional[int] = None, oldest: Optional[str] = None, @@ -2388,6 +2396,7 @@ async def conversations_history( "channel": channel, "cursor": cursor, "inclusive": inclusive, + "include_all_metadata": include_all_metadata, "limit": limit, "latest": latest, "oldest": oldest, diff --git a/slack_sdk/web/client.py b/slack_sdk/web/client.py index 23301355b..9d26c6894 100644 --- a/slack_sdk/web/client.py +++ b/slack_sdk/web/client.py @@ -15,6 +15,7 @@ ) from ..models.attachments import Attachment from ..models.blocks import Block +from ..models.metadata import Metadata class WebClient(BaseClient): @@ -2047,6 +2048,7 @@ def chat_postMessage( link_names: Optional[bool] = None, username: Optional[str] = None, parse: Optional[str] = None, # none, full + metadata: Optional[Union[Dict, Metadata]] = None, **kwargs, ) -> SlackResponse: """Sends a message to a channel. @@ -2071,6 +2073,7 @@ def chat_postMessage( "link_names": link_names, "username": username, "parse": parse, + "metadata": metadata, } ) _parse_web_class_objects(kwargs) @@ -2094,6 +2097,7 @@ def chat_scheduleMessage( unfurl_links: Optional[bool] = None, unfurl_media: Optional[bool] = None, link_names: Optional[bool] = None, + metadata: Optional[Union[Dict, Metadata]] = None, **kwargs, ) -> SlackResponse: """Schedules a message. @@ -2113,6 +2117,7 @@ def chat_scheduleMessage( "unfurl_links": unfurl_links, "unfurl_media": unfurl_media, "link_names": link_names, + "metadata": metadata, } ) _parse_web_class_objects(kwargs) @@ -2164,6 +2169,7 @@ def chat_update( link_names: Optional[bool] = None, parse: Optional[str] = None, # none, full reply_broadcast: Optional[bool] = None, + metadata: Optional[Union[Dict, Metadata]] = None, **kwargs, ) -> SlackResponse: """Updates a message in a channel. @@ -2180,6 +2186,7 @@ def chat_update( "link_names": link_names, "parse": parse, "reply_broadcast": reply_broadcast, + "metadata": metadata, } ) if isinstance(file_ids, (list, Tuple)): @@ -2324,6 +2331,7 @@ def conversations_history( channel: str, cursor: Optional[str] = None, inclusive: Optional[bool] = None, + include_all_metadata: Optional[bool] = None, latest: Optional[str] = None, limit: Optional[int] = None, oldest: Optional[str] = None, @@ -2337,6 +2345,7 @@ def conversations_history( "channel": channel, "cursor": cursor, "inclusive": inclusive, + "include_all_metadata": include_all_metadata, "limit": limit, "latest": latest, "oldest": oldest, diff --git a/slack_sdk/web/internal_utils.py b/slack_sdk/web/internal_utils.py index e19a2bfde..115478923 100644 --- a/slack_sdk/web/internal_utils.py +++ b/slack_sdk/web/internal_utils.py @@ -11,6 +11,7 @@ from slack_sdk.errors import SlackRequestError from slack_sdk.models.attachments import Attachment from slack_sdk.models.blocks import Block +from slack_sdk.models.metadata import Metadata def convert_bool_to_0_or_1( @@ -180,11 +181,13 @@ def _build_req_args( def _parse_web_class_objects(kwargs) -> None: - def to_dict(obj: Union[Dict, Block, Attachment]): + def to_dict(obj: Union[Dict, Block, Attachment, Metadata]): if isinstance(obj, Block): return obj.to_dict() if isinstance(obj, Attachment): return obj.to_dict() + if isinstance(obj, Metadata): + return obj.to_dict() return obj blocks = kwargs.get("blocks", None) @@ -197,6 +200,10 @@ def to_dict(obj: Union[Dict, Block, Attachment]): dict_attachments = [to_dict(a) for a in attachments] kwargs.update({"attachments": dict_attachments}) + metadata = kwargs.get("metadata", None) + if metadata is not None and isinstance(metadata, Metadata): + kwargs.update({"metadata": to_dict(metadata)}) + def _update_call_participants( kwargs, users: Union[str, Sequence[Dict[str, str]]] diff --git a/slack_sdk/web/legacy_client.py b/slack_sdk/web/legacy_client.py index 4ce006902..626b1a927 100644 --- a/slack_sdk/web/legacy_client.py +++ b/slack_sdk/web/legacy_client.py @@ -26,6 +26,7 @@ ) from ..models.attachments import Attachment from ..models.blocks import Block +from ..models.metadata import Metadata class LegacyWebClient(LegacyBaseClient): @@ -2058,6 +2059,7 @@ def chat_postMessage( link_names: Optional[bool] = None, username: Optional[str] = None, parse: Optional[str] = None, # none, full + metadata: Optional[Union[Dict, Metadata]] = None, **kwargs, ) -> Union[Future, SlackResponse]: """Sends a message to a channel. @@ -2082,6 +2084,7 @@ def chat_postMessage( "link_names": link_names, "username": username, "parse": parse, + "metadata": metadata, } ) _parse_web_class_objects(kwargs) @@ -2105,6 +2108,7 @@ def chat_scheduleMessage( unfurl_links: Optional[bool] = None, unfurl_media: Optional[bool] = None, link_names: Optional[bool] = None, + metadata: Optional[Union[Dict, Metadata]] = None, **kwargs, ) -> Union[Future, SlackResponse]: """Schedules a message. @@ -2124,6 +2128,7 @@ def chat_scheduleMessage( "unfurl_links": unfurl_links, "unfurl_media": unfurl_media, "link_names": link_names, + "metadata": metadata, } ) _parse_web_class_objects(kwargs) @@ -2175,6 +2180,7 @@ def chat_update( link_names: Optional[bool] = None, parse: Optional[str] = None, # none, full reply_broadcast: Optional[bool] = None, + metadata: Optional[Union[Dict, Metadata]] = None, **kwargs, ) -> Union[Future, SlackResponse]: """Updates a message in a channel. @@ -2191,6 +2197,7 @@ def chat_update( "link_names": link_names, "parse": parse, "reply_broadcast": reply_broadcast, + "metadata": metadata, } ) if isinstance(file_ids, (list, Tuple)): @@ -2335,6 +2342,7 @@ def conversations_history( channel: str, cursor: Optional[str] = None, inclusive: Optional[bool] = None, + include_all_metadata: Optional[bool] = None, latest: Optional[str] = None, limit: Optional[int] = None, oldest: Optional[str] = None, @@ -2348,6 +2356,7 @@ def conversations_history( "channel": channel, "cursor": cursor, "inclusive": inclusive, + "include_all_metadata": include_all_metadata, "limit": limit, "latest": latest, "oldest": oldest, diff --git a/tests/slack_sdk/web/test_web_client.py b/tests/slack_sdk/web/test_web_client.py index 8dd94ac00..6737b95b9 100644 --- a/tests/slack_sdk/web/test_web_client.py +++ b/tests/slack_sdk/web/test_web_client.py @@ -1,9 +1,11 @@ import re import socket import unittest +import time import slack_sdk.errors as err from slack_sdk import WebClient +from slack_sdk.models.metadata import Metadata from tests.slack_sdk.web.mock_web_api_server import ( setup_mock_web_api_server, cleanup_mock_web_api_server, @@ -177,3 +179,54 @@ def test_default_team_id(self): client = WebClient(base_url="http://localhost:8888", team_id="T_DEFAULT") resp = client.users_list(token="xoxb-users_list_pagination") self.assertIsNone(resp["error"]) + + def test_message_metadata(self): + client = self.client + new_message = client.chat_postMessage( + channel="#random", + text="message with metadata", + metadata=Metadata( + event_type="procurement-task", + event_payload={ + "id": "11111", + "amount": 5000, + "tags": ["foo", "bar", "baz"], + }, + ), + ) + self.assertIsNone(new_message.get("error")) + + history = client.conversations_history( + channel=new_message.get("channel"), + limit=1, + include_all_metadata=True, + ) + self.assertIsNone(history.get("error")) + + modification = client.chat_update( + channel=new_message.get("channel"), + ts=new_message.get("ts"), + text="message with metadata (modified)", + metadata=Metadata( + event_type="procurement-task", + event_payload={ + "id": "11111", + "amount": 6000, + }, + ), + ) + self.assertIsNone(modification.get("error")) + + scheduled = client.chat_scheduleMessage( + channel=new_message.get("channel"), + post_at=int(time.time()) + 30, + text="message with metadata (scheduled)", + metadata=Metadata( + event_type="procurement-task", + event_payload={ + "id": "11111", + "amount": 10, + }, + ), + ) + self.assertIsNone(scheduled.get("error"))