Skip to content

Commit 12c78c2

Browse files
committed
feat: add work objects support
1 parent e9e64d8 commit 12c78c2

File tree

7 files changed

+293
-12
lines changed

7 files changed

+293
-12
lines changed

integration_tests/web/test_message_metadata.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import unittest
55

66
from integration_tests.env_variable_names import SLACK_SDK_TEST_BOT_TOKEN
7-
from slack_sdk.models.metadata import Metadata
7+
from slack_sdk.models.metadata import Metadata, EventAndEntityMetadata, EntityMetadata, EntityType, ExternalRef
88
from slack_sdk.web import WebClient
99

1010

@@ -125,3 +125,24 @@ def test_publishing_message_metadata_using_models(self):
125125
)
126126
self.assertIsNone(scheduled.get("error"))
127127
self.assertIsNotNone(scheduled.get("message").get("metadata"))
128+
129+
def test_publishing_message_entity_metadata_using_models(self):
130+
131+
payload = { "attributes": { "title": { "text": "Work References" }, "product_name": "We reference only", "metadata_last_modified": 1760999279, "full_size_preview": { "is_supported": True } }, "fields": { "due_date": { "value": "2026-06-06", "type": "slack#/types/date", "edit": { "enabled": True } }, "created_by": { "type": "slack#/types/user", "user": { "user_id": "U014KLZE350" }, "edit": { "enabled": True } }, "description": { "value": "Initial task work object for test test test", "format": "markdown", "long": True, "edit": { "enabled": True, "text": { "min_length": 1, "max_length": 100 } } }, "date_created": { "value": 1760629278 }, "date_updated": { "value": 1760999279 } }, "custom_fields": [ { "label": "img", "key": "img", "type": "slack#/types/image", "image_url": "https://cdn.shopify.com/s/files/1/0156/3796/files/shutterstock_54266797_large.jpg?v=1549042211" }, { "label": "Reference tasks", "key": "ref-tasks", "type": "slack#/types/entity_ref", "entity_ref": { "entity_url": "https://app-one-app-two-auth-dev.tinyspeck.com/admin/slack/workobject/129/change/", "external_ref": { "id": "129" }, "title": "Radiant task", "display_type": "tasks", "icon": { "alt_text": "img", "url": "https://avatars.slack-edge.com/2024-09-05/7707480927360_791cc0c5cdf5b6720b21_512.png" } } }, { "label": "All related references", "key": "related-refs", "type": "array", "item_type": "slack#/types/entity_ref", "value": [ { "entity_ref": { "entity_url": "https://app-one-app-two-auth-dev.tinyspeck.com/admin/slack/workobject/131/change/", "external_ref": { "id": "131" }, "title": "Work object planner", "icon": { "alt_text": "img", "url": "https://avatars.slack-edge.com/2024-09-05/7707480927360_791cc0c5cdf5b6720b21_512.png" } } }, { "entity_ref": { "entity_url": "https://app-one-app-two-auth-dev.tinyspeck.com/admin/slack/workobject/133/change/", "external_ref": { "id": "133" }, "title": "Test" } }, { "entity_ref": { "entity_url": "https://app-one-app-two-auth-dev.tinyspeck.com/admin/slack/workobject/142/change/", "external_ref": { "id": "142" }, "title": "Test" } } ] } ] }
132+
133+
client: WebClient = WebClient(token=self.bot_token,base_url='https://dev2378.slack.com/api/')
134+
new_message = client.chat_postMessage(
135+
channel="C014KLZN9M0",
136+
text="dbl check message with metadata",
137+
metadata=EventAndEntityMetadata(
138+
entities=[
139+
EntityMetadata(
140+
entity_type=EntityType.TASK,
141+
external_ref=ExternalRef(id="abc123"),
142+
url="https://myappdomain.com",
143+
entity_payload=payload,
144+
)]),
145+
)
146+
147+
self.assertIsNone(new_message.get("error"))
148+
self.assertIsNone(new_message.get("warning"))

slack_sdk/models/metadata/__init__.py

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Dict, Any
1+
from typing import Dict, Any, Union
2+
from enum import StrEnum
23
from slack_sdk.models.basic_objects import JsonObject
34

45

@@ -28,3 +29,180 @@ def __str__(self):
2829

2930
def __repr__(self):
3031
return self.__str__()
32+
33+
34+
## Work object entity metadata
35+
36+
class EntityType(StrEnum):
37+
FILE = "slack#/entities/file"
38+
TASK = "slack#/entities/task"
39+
ITEM = "slack#/entities/item"
40+
INCIDENT = "slack#/entities/incident"
41+
CONTENT_ITEM = "slack#/entities/content_item"
42+
43+
44+
class ExternalRef(JsonObject):
45+
attributes = {
46+
"id",
47+
"type",
48+
}
49+
def __init__(
50+
self,
51+
id: str,
52+
type: str = None,
53+
**kwargs,
54+
):
55+
self.id = id
56+
self.type = type
57+
self.additional_attributes = kwargs
58+
59+
def __str__(self):
60+
return str(self.get_non_null_attributes())
61+
62+
def __repr__(self):
63+
return self.__str__()
64+
65+
class Title(JsonObject):
66+
attributes = {
67+
"text",
68+
"edit",
69+
}
70+
def __init__(
71+
self,
72+
text: str,
73+
edit: Dict[str, Any] = None, # TODO EntityEditSupport
74+
**kwargs,
75+
):
76+
self.text = text
77+
self.edit = edit
78+
self.additional_attributes = kwargs
79+
80+
def __str__(self):
81+
return str(self.get_non_null_attributes())
82+
83+
def __repr__(self):
84+
return self.__str__()
85+
86+
87+
class Attributes(JsonObject):
88+
attributes = {
89+
"title",
90+
"display_type",
91+
"display_id",
92+
"product_icon"
93+
"product_name"
94+
"locale"
95+
"full_size_preview"
96+
"metadata_last_modified"
97+
}
98+
def __init__(
99+
self,
100+
title: Title,
101+
display_type: str = None,
102+
display_id: str = None,
103+
product_icon: Dict[str, Any] = None,
104+
product_name: str = None,
105+
locale: str = None,
106+
full_size_preview: str = None,
107+
metadata_last_modified: int = None,
108+
**kwargs,
109+
):
110+
self.title = title
111+
self.display_type = display_type
112+
self.display_id = display_id
113+
self.product_icon = product_icon
114+
self.product_name = product_name
115+
self.locale = locale
116+
self.full_size_preview = full_size_preview
117+
self.metadata_last_modified = metadata_last_modified
118+
self.additional_attributes = kwargs
119+
120+
def __str__(self):
121+
return str(self.get_non_null_attributes())
122+
123+
def __repr__(self):
124+
return self.__str__()
125+
126+
127+
class EntityPayload(JsonObject):
128+
attributes = {
129+
"attributes"
130+
}
131+
def __init__(
132+
self,
133+
attributes: Attributes,
134+
**kwargs,
135+
):
136+
self.attributes = attributes
137+
# TODO
138+
self.additional_attributes = kwargs
139+
140+
def __str__(self):
141+
return str(self.get_non_null_attributes())
142+
143+
def __repr__(self):
144+
return self.__str__()
145+
146+
147+
class EntityMetadata(JsonObject):
148+
"""Work object entity metadata
149+
150+
https://docs.slack.dev/messaging/work-objects/
151+
"""
152+
153+
attributes = {
154+
"entity_type",
155+
"external_ref",
156+
"url",
157+
"app_unfurl_url",
158+
"entity_payload",
159+
}
160+
161+
def __init__(
162+
self,
163+
entity_type: Union[str, EntityType],
164+
external_ref: Union[Dict[str, Any], ExternalRef],
165+
url: str,
166+
entity_payload: Union[Dict[str, Any], EntityPayload],
167+
app_unfurl_url: str = None,
168+
**kwargs,
169+
):
170+
self.entity_type = entity_type
171+
self.external_ref = external_ref
172+
self.url = url
173+
self.app_unfurl_url = app_unfurl_url
174+
self.entity_payload = entity_payload
175+
self.additional_attributes = kwargs
176+
177+
def __str__(self):
178+
return str(self.get_non_null_attributes())
179+
180+
def __repr__(self):
181+
return self.__str__()
182+
183+
184+
class EventAndEntityMetadata(JsonObject):
185+
"""Message metadata
186+
187+
https://docs.slack.dev/messaging/message-metadata/
188+
"""
189+
190+
attributes = {"event_type", "event_payload", "entities"}
191+
192+
def __init__(
193+
self,
194+
event_type: str = None,
195+
event_payload: Dict[str, Any] = None,
196+
entities: list[EntityMetadata] = None,
197+
**kwargs,
198+
):
199+
self.event_type = event_type
200+
self.event_payload = event_payload
201+
self.entities = entities
202+
self.additional_attributes = kwargs
203+
204+
def __str__(self):
205+
return str(self.get_non_null_attributes())
206+
207+
def __repr__(self):
208+
return self.__str__()

slack_sdk/web/async_client.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
from ..models.attachments import Attachment
2424
from ..models.blocks import Block
25-
from ..models.metadata import Metadata
25+
from ..models.metadata import Metadata, EventAndEntityMetadata
2626
from .async_base_client import AsyncBaseClient, AsyncSlackResponse
2727
from .internal_utils import (
2828
_parse_web_class_objects,
@@ -2769,7 +2769,7 @@ async def chat_postMessage(
27692769
link_names: Optional[bool] = None,
27702770
username: Optional[str] = None,
27712771
parse: Optional[str] = None, # none, full
2772-
metadata: Optional[Union[Dict, Metadata]] = None,
2772+
metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None,
27732773
markdown_text: Optional[str] = None,
27742774
**kwargs,
27752775
) -> AsyncSlackResponse:
@@ -2998,6 +2998,7 @@ async def chat_unfurl(
29982998
source: Optional[str] = None,
29992999
unfurl_id: Optional[str] = None,
30003000
unfurls: Optional[Dict[str, Dict]] = None, # or user_auth_*
3001+
metadata: Optional[dict] = None,
30013002
user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
30023003
user_auth_message: Optional[str] = None,
30033004
user_auth_required: Optional[bool] = None,
@@ -3014,6 +3015,7 @@ async def chat_unfurl(
30143015
"source": source,
30153016
"unfurl_id": unfurl_id,
30163017
"unfurls": unfurls,
3018+
"metadata": metadata,
30173019
"user_auth_blocks": user_auth_blocks,
30183020
"user_auth_message": user_auth_message,
30193021
"user_auth_required": user_auth_required,
@@ -3068,6 +3070,7 @@ async def chat_update(
30683070
kwargs = _remove_none_values(kwargs)
30693071
_warn_if_message_text_content_is_missing("chat.update", kwargs)
30703072
# NOTE: intentionally using json over params for API methods using blocks/attachments
3073+
print("**** entering .....")
30713074
return await self.api_call("chat.update", json=kwargs)
30723075

30733076
async def conversations_acceptSharedInvite(
@@ -3645,6 +3648,29 @@ async def emoji_list(
36453648
kwargs.update({"include_categories": include_categories})
36463649
return await self.api_call("emoji.list", http_verb="GET", params=kwargs)
36473650

3651+
async def entity_presentDetails(
3652+
self,
3653+
metadata: Optional[dict] = None,
3654+
trigger_id: str = None,
3655+
user_auth_required: Optional[bool] = None,
3656+
user_auth_url: Optional[str] = None,
3657+
error: Optional[Dict[str, str]] = None,
3658+
**kwargs,
3659+
) -> AsyncSlackResponse:
3660+
"""Provides entity details for the flexpane.
3661+
https://docs.slack.dev/reference/methods/entity.presentDetails/
3662+
"""
3663+
kwargs.update({"trigger_id": trigger_id})
3664+
if metadata is not None:
3665+
kwargs.update({"metadata": metadata})
3666+
if user_auth_required is not None:
3667+
kwargs.update({"user_auth_required": user_auth_required})
3668+
if user_auth_url is not None:
3669+
kwargs.update({"user_auth_url": user_auth_url})
3670+
if error is not None:
3671+
kwargs.update({"error": error})
3672+
return await self.api_call("entity.presentDetails", json=kwargs)
3673+
36483674
async def files_comments_delete(
36493675
self,
36503676
*,

slack_sdk/web/base_client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ def _perform_urllib_http_request(self, *, url: str, args: Dict[str, Dict[str, An
348348
Returns:
349349
dict {status: int, headers: Headers, body: str}
350350
"""
351+
351352
headers = args["headers"]
352353
body: Optional[Union[bytes, str]] = None
353354
if args["json"]:

slack_sdk/web/client.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from ..models.attachments import Attachment
1414
from ..models.blocks import Block
15-
from ..models.metadata import Metadata
15+
from ..models.metadata import Metadata, EventAndEntityMetadata
1616
from .base_client import BaseClient, SlackResponse
1717
from .internal_utils import (
1818
_parse_web_class_objects,
@@ -2759,7 +2759,7 @@ def chat_postMessage(
27592759
link_names: Optional[bool] = None,
27602760
username: Optional[str] = None,
27612761
parse: Optional[str] = None, # none, full
2762-
metadata: Optional[Union[Dict, Metadata]] = None,
2762+
metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None,
27632763
markdown_text: Optional[str] = None,
27642764
**kwargs,
27652765
) -> SlackResponse:
@@ -2988,6 +2988,7 @@ def chat_unfurl(
29882988
source: Optional[str] = None,
29892989
unfurl_id: Optional[str] = None,
29902990
unfurls: Optional[Dict[str, Dict]] = None, # or user_auth_*
2991+
metadata: Optional[dict] = None,
29912992
user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
29922993
user_auth_message: Optional[str] = None,
29932994
user_auth_required: Optional[bool] = None,
@@ -3004,6 +3005,7 @@ def chat_unfurl(
30043005
"source": source,
30053006
"unfurl_id": unfurl_id,
30063007
"unfurls": unfurls,
3008+
"metadata": metadata,
30073009
"user_auth_blocks": user_auth_blocks,
30083010
"user_auth_message": user_auth_message,
30093011
"user_auth_required": user_auth_required,
@@ -3058,6 +3060,7 @@ def chat_update(
30583060
kwargs = _remove_none_values(kwargs)
30593061
_warn_if_message_text_content_is_missing("chat.update", kwargs)
30603062
# NOTE: intentionally using json over params for API methods using blocks/attachments
3063+
print("**** entering .....")
30613064
return self.api_call("chat.update", json=kwargs)
30623065

30633066
def conversations_acceptSharedInvite(
@@ -3635,6 +3638,29 @@ def emoji_list(
36353638
kwargs.update({"include_categories": include_categories})
36363639
return self.api_call("emoji.list", http_verb="GET", params=kwargs)
36373640

3641+
def entity_presentDetails(
3642+
self,
3643+
metadata: Optional[dict] = None,
3644+
trigger_id: str = None,
3645+
user_auth_required: Optional[bool] = None,
3646+
user_auth_url: Optional[str] = None,
3647+
error: Optional[Dict[str, str]] = None,
3648+
**kwargs,
3649+
) -> SlackResponse:
3650+
"""Provides entity details for the flexpane.
3651+
https://docs.slack.dev/reference/methods/entity.presentDetails/
3652+
"""
3653+
kwargs.update({"trigger_id": trigger_id})
3654+
if metadata is not None:
3655+
kwargs.update({"metadata": metadata})
3656+
if user_auth_required is not None:
3657+
kwargs.update({"user_auth_required": user_auth_required})
3658+
if user_auth_url is not None:
3659+
kwargs.update({"user_auth_url": user_auth_url})
3660+
if error is not None:
3661+
kwargs.update({"error": error})
3662+
return self.api_call("entity.presentDetails", json=kwargs)
3663+
36383664
def files_comments_delete(
36393665
self,
36403666
*,

0 commit comments

Comments
 (0)