From 7953a72c742a2f390c7fcc8cb04da4dbaab52d3f Mon Sep 17 00:00:00 2001 From: Elaine Vegeris Date: Mon, 17 Nov 2025 17:03:38 -0500 Subject: [PATCH 1/3] add work objects sample --- messaging/README.md | 9 +++ messaging/work-objects/.gitignore | 6 ++ messaging/work-objects/README.md | 33 +++++++++ messaging/work-objects/manifest.json | 41 +++++++++++ messaging/work-objects/requirements.txt | 5 ++ messaging/work-objects/src/__init__.py | 0 messaging/work-objects/src/app.py | 88 ++++++++++++++++++++++++ messaging/work-objects/src/metadata.py | 51 ++++++++++++++ messaging/work-objects/tests/__init__.py | 0 9 files changed, 233 insertions(+) create mode 100644 messaging/README.md create mode 100644 messaging/work-objects/.gitignore create mode 100644 messaging/work-objects/README.md create mode 100644 messaging/work-objects/manifest.json create mode 100644 messaging/work-objects/requirements.txt create mode 100644 messaging/work-objects/src/__init__.py create mode 100644 messaging/work-objects/src/app.py create mode 100644 messaging/work-objects/src/metadata.py create mode 100644 messaging/work-objects/tests/__init__.py diff --git a/messaging/README.md b/messaging/README.md new file mode 100644 index 0000000..312b65e --- /dev/null +++ b/messaging/README.md @@ -0,0 +1,9 @@ +# Messaging + +Core Slack functionality! + +Read the [docs](https://docs.slack.dev/messaging/) to learn about messaging in Slack. + +## What's on display + +- **[Work Objects](https://docs.slack.dev/messaging/work-objects/)**: Preview and interact with your app data in Slack. \ No newline at end of file diff --git a/messaging/work-objects/.gitignore b/messaging/work-objects/.gitignore new file mode 100644 index 0000000..917bb1e --- /dev/null +++ b/messaging/work-objects/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +.mypy_cache +.pytest_cache +.ruff_cache +.venv +.slack \ No newline at end of file diff --git a/messaging/work-objects/README.md b/messaging/work-objects/README.md new file mode 100644 index 0000000..17f940e --- /dev/null +++ b/messaging/work-objects/README.md @@ -0,0 +1,33 @@ +# Work Objects Showcase + +Your app can respond to links being shared in Slack with Work Object metadata to display a link preview. Users can click the preview to view authenticated data in the flexpane. If supported, users can edit app data directly from the flexpane. + +You can also [post](https://docs.slack.dev/messaging/work-objects/#implementation-notifications) Work Object metadata directly with `chat.postMessage`, without a link being shared. + +Read the [docs](https://docs.slack.dev/messaging/work-objects/) to learn more! + +## Running locally + +### 1. Setup environment variables + +```zsh +# Replace with your tokens +export SLACK_BOT_TOKEN= +export SLACK_APP_TOKEN= +``` + +### 2. Setup your local project + +```zsh +# Setup virtual environment +python3 -m venv .venv +source .venv/bin/activate + +# Install the dependencies +pip install -r requirements.txt +``` + +### 3. Start the server +```zsh +python3 src/app.py +``` diff --git a/messaging/work-objects/manifest.json b/messaging/work-objects/manifest.json new file mode 100644 index 0000000..838c0ed --- /dev/null +++ b/messaging/work-objects/manifest.json @@ -0,0 +1,41 @@ +{ + "display_information": { + "name": "Work Objects for Bolt Python App" + }, + "features": { + "rich_previews": { + "is_active": true, + "entity_types": [ + "slack#/entities/task" + ] + }, + "unfurl_domains": [ + "tmpdomain.com" + ], + "app_home": { + "home_tab_enabled": false, + "messages_tab_enabled": true, + "messages_tab_read_only_enabled": false + }, + "bot_user": { + "display_name": "WorkObjectsApp", + "always_online": true + } + }, + "oauth_config": { + "scopes": { + "bot": ["channels:history", "chat:write", "im:history", "links:read", "links:write"] + } + }, + "settings": { + "event_subscriptions": { + "bot_events": ["link_shared", "entity_details_requested"] + }, + "interactivity": { + "is_enabled": true + }, + "org_deploy_enabled": true, + "socket_mode_enabled": true, + "token_rotation_enabled": false + } +} \ No newline at end of file diff --git a/messaging/work-objects/requirements.txt b/messaging/work-objects/requirements.txt new file mode 100644 index 0000000..ba7dc97 --- /dev/null +++ b/messaging/work-objects/requirements.txt @@ -0,0 +1,5 @@ +mypy==1.18.2 +pytest==9.0.1 +ruff==0.14.5 +#slack_sdk==3.39.0 +slack-cli-hooks<1.0.0 \ No newline at end of file diff --git a/messaging/work-objects/src/__init__.py b/messaging/work-objects/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/messaging/work-objects/src/app.py b/messaging/work-objects/src/app.py new file mode 100644 index 0000000..0cea0e0 --- /dev/null +++ b/messaging/work-objects/src/app.py @@ -0,0 +1,88 @@ +import os +import logging +from slack_bolt import App +from slack_bolt.adapter.socket_mode import SocketModeHandler +from slack_sdk import WebClient +from slack_sdk.models.metadata import EventAndEntityMetadata +from metadata import sample_entities + + +# Initializes your app with your bot token +app = App(token=os.environ.get("SLACK_BOT_TOKEN")) + + +# Listens for the link_shared event +# https://docs.slack.dev/reference/events/link_shared/ +@app.event("link_shared") +def link_shared_callback(event: dict, client: WebClient, logger: logging.Logger): + try: + for link in event["links"]: + entity = sample_entities[link["url"]] + if entity is not None: + client.chat_unfurl( + channel=event["channel"], + ts=event["message_ts"], + metadata=EventAndEntityMetadata(entities=[entity]), + ) + else: + logger.info("No entity found with URL " + link) + except Exception as e: + logger.error( + f"An error occurred while handling the entity_details_requested event: {type(e).__name__} - {str(e)}", + exc_info=e, + ) + + +# Listens for the entity_details_requested event, which is sent when a user opens the flexpane +# https://docs.slack.dev/reference/events/entity_details_requested/ +@app.event("entity_details_requested") +def entity_details_callback(event: dict, client: WebClient, logger: logging.Logger): + try: + entity_url = event["entity_url"] + entity = sample_entities[entity_url] + if entity is not None: + client.entity_presentDetails( + trigger_id=event["trigger_id"], metadata=entity + ) + else: + logger.info("No entity found with URL " + entity_url) + except Exception as e: + logger.error( + f"An error occurred while handling the entity_details_requested event: {type(e).__name__} - {str(e)}", + exc_info=e, + ) + + +# Listens for the view_submission event sent when the user submits edits in the flexpane +# https://docs.slack.dev/tools/bolt-js/concepts/view-submissions/ +# https://docs.slack.dev/messaging/work-objects/#editing-view-submission +@app.view_submission("work-object-edit") +def work_object_edit_callback(view: dict, body: dict, client: WebClient, logger: logging.Logger, ack): + try: + ack() + + entity_url = view["entity_url"] + entity = sample_entities[entity_url] + + attributes = entity.entity_payload.entity_attributes + attributes.title.text = view["state"]["values"]["title"]["title.input"]["value"] + entity.entity_payload.entity_attributes = attributes + + entity.entity_payload.fields.description.value = view["state"]["values"]["description"]["description.input"]["value"] + + if entity is not None: + client.entity_presentDetails( + trigger_id=body["trigger_id"], metadata=entity + ) + else: + logger.info("No entity found with URL " + entity_url) + except Exception as e: + logger.error( + f"An error occurred while handling the entity_details_requested event: {type(e).__name__} - {str(e)}", + exc_info=e, + ) + + +# Start your app +if __name__ == "__main__": + SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start() diff --git a/messaging/work-objects/src/metadata.py b/messaging/work-objects/src/metadata.py new file mode 100644 index 0000000..14db30c --- /dev/null +++ b/messaging/work-objects/src/metadata.py @@ -0,0 +1,51 @@ +from slack_sdk.models.metadata import ( + EntityMetadata, + EntityType, + ExternalRef, + EntityPayload, + EntityAttributes, + EntityTitle, + TaskEntityFields, + EntityStringField, + EntityTitle, + EntityAttributes, + TaskEntityFields, + EntityStringField, + EntityEditSupport, + EntityCustomField, + ExternalRef, +) + +# Update the URL here to match your app's domain +sample_task_unfurl_url = "https://wo-python-nov2025.com/task" + +sample_entities = { + sample_task_unfurl_url: EntityMetadata( + entity_type="slack#/entities/task", + url=sample_task_unfurl_url, + app_unfurl_url=sample_task_unfurl_url, + external_ref=ExternalRef(id="sample_task_id"), + entity_payload=EntityPayload( + attributes=EntityAttributes( + title=EntityTitle( + text="My Title", + edit=EntityEditSupport(enabled=True), + ) + ), + fields=TaskEntityFields( + description=EntityStringField( + value="Hello World!", + edit=EntityEditSupport(enabled=True) + ) + ), + custom_fields=[ + EntityCustomField( + key="hello-world", + label="hello-world", + value="hello-world", + type="string", + ) + ], + ), + ) +} diff --git a/messaging/work-objects/tests/__init__.py b/messaging/work-objects/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 696bea3ef0d1a46b098720614446f75a57f70ff5 Mon Sep 17 00:00:00 2001 From: Elaine Vegeris Date: Wed, 19 Nov 2025 20:22:28 -0500 Subject: [PATCH 2/3] update --- messaging/work-objects/README.md | 4 +++ messaging/work-objects/src/metadata.py | 35 ++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/messaging/work-objects/README.md b/messaging/work-objects/README.md index 17f940e..95a333d 100644 --- a/messaging/work-objects/README.md +++ b/messaging/work-objects/README.md @@ -8,6 +8,10 @@ Read the [docs](https://docs.slack.dev/messaging/work-objects/) to learn more! ## Running locally +### 0. Create an app + +Create an app and add `myappdomain.com`as an [app unfurl domain](https://docs.slack.dev/messaging/unfurling-links-in-messages/#configuring_domains) (or update the unfurl URLs in `metadata.py` with your domain). Also enable [Work Object Previews](https://docs.slack.dev/messaging/work-objects/#implementation) for your app. + ### 1. Setup environment variables ```zsh diff --git a/messaging/work-objects/src/metadata.py b/messaging/work-objects/src/metadata.py index 14db30c..52ee72f 100644 --- a/messaging/work-objects/src/metadata.py +++ b/messaging/work-objects/src/metadata.py @@ -14,10 +14,12 @@ EntityEditSupport, EntityCustomField, ExternalRef, + EntityFullSizePreview ) # Update the URL here to match your app's domain -sample_task_unfurl_url = "https://wo-python-nov2025.com/task" +sample_task_unfurl_url = "https://myappdomain.com/task" +sample_file_unfurl_url = "https://myappdomain.com/file" sample_entities = { sample_task_unfurl_url: EntityMetadata( @@ -28,7 +30,36 @@ entity_payload=EntityPayload( attributes=EntityAttributes( title=EntityTitle( - text="My Title", + text="My Task", + edit=EntityEditSupport(enabled=True), + ) + ), + fields=TaskEntityFields( + description=EntityStringField( + value="Hello World!", + edit=EntityEditSupport(enabled=True) + ) + ), + custom_fields=[ + EntityCustomField( + key="hello-world", + label="hello-world", + value="hello-world", + type="string", + ) + ], + ), + ), + sample_file_unfurl_url: EntityMetadata( + entity_type="slack#/entities/file", + url=sample_file_unfurl_url, + app_unfurl_url=sample_file_unfurl_url, + external_ref=ExternalRef(id="sample_task_id"), + entity_payload=EntityPayload( + attributes=EntityAttributes( + full_size_preview=EntityFullSizePreview(is_supported=True, preview_url="https://muertoscoffeeco.com/cdn/shop/articles/Why-Roast-Coffee-scaled_1000x.jpg?v=1592234291"), + title=EntityTitle( + text="My File", edit=EntityEditSupport(enabled=True), ) ), From babac6c895463d2767a52302b289cc413857b8d0 Mon Sep 17 00:00:00 2001 From: Elaine Vegeris Date: Wed, 3 Dec 2025 16:12:24 -0500 Subject: [PATCH 3/3] update --- messaging/work-objects/.gitignore | 2 +- messaging/work-objects/README.md | 31 +++++++++++--------- messaging/work-objects/{src => }/app.py | 23 ++++++++++----- messaging/work-objects/manifest.json | 22 +++++--------- messaging/work-objects/{src => }/metadata.py | 4 +-- messaging/work-objects/requirements.txt | 3 +- messaging/work-objects/src/__init__.py | 0 7 files changed, 45 insertions(+), 40 deletions(-) rename messaging/work-objects/{src => }/app.py (77%) rename messaging/work-objects/{src => }/metadata.py (95%) delete mode 100644 messaging/work-objects/src/__init__.py diff --git a/messaging/work-objects/.gitignore b/messaging/work-objects/.gitignore index 917bb1e..84e785d 100644 --- a/messaging/work-objects/.gitignore +++ b/messaging/work-objects/.gitignore @@ -3,4 +3,4 @@ __pycache__ .pytest_cache .ruff_cache .venv -.slack \ No newline at end of file +.slack diff --git a/messaging/work-objects/README.md b/messaging/work-objects/README.md index 95a333d..636dfe3 100644 --- a/messaging/work-objects/README.md +++ b/messaging/work-objects/README.md @@ -6,32 +6,35 @@ You can also [post](https://docs.slack.dev/messaging/work-objects/#implementatio Read the [docs](https://docs.slack.dev/messaging/work-objects/) to learn more! -## Running locally +## Setup -### 0. Create an app +### Option 1: Create a new app with the Slack CLI -Create an app and add `myappdomain.com`as an [app unfurl domain](https://docs.slack.dev/messaging/unfurling-links-in-messages/#configuring_domains) (or update the unfurl URLs in `metadata.py` with your domain). Also enable [Work Object Previews](https://docs.slack.dev/messaging/work-objects/#implementation) for your app. +```bash +slack init && slack run +``` + +### Option 2: Create a Slack App in the UI and copy over your tokens -### 1. Setup environment variables +Create an app on [https://api.slack.com/apps](https://api.slack.com/apps) from the app manifest in this repo (`manifest.json`). -```zsh -# Replace with your tokens +Configure environment variables: +```bash +# OAuth & Permissions → Bot User OAuth Token export SLACK_BOT_TOKEN= -export SLACK_APP_TOKEN= +# Basic Information → App-Level Tokens (create one with the `connections:write` scope) +export SLACK_APP_TOKEN= ``` -### 2. Setup your local project - -```zsh +Install dependencies and run the app: +```bash # Setup virtual environment python3 -m venv .venv source .venv/bin/activate # Install the dependencies pip install -r requirements.txt -``` -### 3. Start the server -```zsh -python3 src/app.py +# Start the server +python3 app.py ``` diff --git a/messaging/work-objects/src/app.py b/messaging/work-objects/app.py similarity index 77% rename from messaging/work-objects/src/app.py rename to messaging/work-objects/app.py index 0cea0e0..9916483 100644 --- a/messaging/work-objects/src/app.py +++ b/messaging/work-objects/app.py @@ -1,10 +1,11 @@ import os +import sys import logging from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler from slack_sdk import WebClient from slack_sdk.models.metadata import EventAndEntityMetadata -from metadata import sample_entities +from metadata import sample_entities, sample_task_unfurl_url, sample_file_unfurl_url # Initializes your app with your bot token @@ -64,11 +65,13 @@ def work_object_edit_callback(view: dict, body: dict, client: WebClient, logger: entity_url = view["entity_url"] entity = sample_entities[entity_url] - attributes = entity.entity_payload.entity_attributes - attributes.title.text = view["state"]["values"]["title"]["title.input"]["value"] - entity.entity_payload.entity_attributes = attributes - - entity.entity_payload.fields.description.value = view["state"]["values"]["description"]["description.input"]["value"] + if "title" in view["state"]["values"]: + attributes = entity.entity_payload.entity_attributes + attributes.title.text = view["state"]["values"]["title"]["title.input"]["value"] + entity.entity_payload.entity_attributes = attributes + + if "description" in view["state"]["values"]: + entity.entity_payload.fields.description.value = view["state"]["values"]["description"]["description.input"]["value"] if entity is not None: client.entity_presentDetails( @@ -85,4 +88,10 @@ def work_object_edit_callback(view: dict, body: dict, client: WebClient, logger: # Start your app if __name__ == "__main__": - SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start() + try: + print("Try sharing this link: ", sample_task_unfurl_url) + print("Or this link: ", sample_file_unfurl_url) + SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start() + except Exception as error: + print(f"Failed to start app: {error}", file=sys.stderr) + sys.exit(1) diff --git a/messaging/work-objects/manifest.json b/messaging/work-objects/manifest.json index 838c0ed..1885efb 100644 --- a/messaging/work-objects/manifest.json +++ b/messaging/work-objects/manifest.json @@ -1,30 +1,22 @@ { "display_information": { - "name": "Work Objects for Bolt Python App" + "name": "work_objects_python", + "description": "A sample app demonstrating Slack Work Objects functionality" }, "features": { "rich_previews": { "is_active": true, - "entity_types": [ - "slack#/entities/task" - ] - }, - "unfurl_domains": [ - "tmpdomain.com" - ], - "app_home": { - "home_tab_enabled": false, - "messages_tab_enabled": true, - "messages_tab_read_only_enabled": false + "entity_types": ["slack#/entities/task", "slack#/entities/file"] }, + "unfurl_domains": ["work-objects-python.com"], "bot_user": { - "display_name": "WorkObjectsApp", + "display_name": "work_objects_python_bot", "always_online": true } }, "oauth_config": { "scopes": { - "bot": ["channels:history", "chat:write", "im:history", "links:read", "links:write"] + "bot": ["chat:write", "links:read", "links:write"] } }, "settings": { @@ -38,4 +30,4 @@ "socket_mode_enabled": true, "token_rotation_enabled": false } -} \ No newline at end of file +} diff --git a/messaging/work-objects/src/metadata.py b/messaging/work-objects/metadata.py similarity index 95% rename from messaging/work-objects/src/metadata.py rename to messaging/work-objects/metadata.py index 52ee72f..8d1a0ed 100644 --- a/messaging/work-objects/src/metadata.py +++ b/messaging/work-objects/metadata.py @@ -18,8 +18,8 @@ ) # Update the URL here to match your app's domain -sample_task_unfurl_url = "https://myappdomain.com/task" -sample_file_unfurl_url = "https://myappdomain.com/file" +sample_task_unfurl_url = "https://work-objects-python.com/task" +sample_file_unfurl_url = "https://work-objects-python.com/file" sample_entities = { sample_task_unfurl_url: EntityMetadata( diff --git a/messaging/work-objects/requirements.txt b/messaging/work-objects/requirements.txt index ba7dc97..4897476 100644 --- a/messaging/work-objects/requirements.txt +++ b/messaging/work-objects/requirements.txt @@ -1,5 +1,6 @@ mypy==1.18.2 pytest==9.0.1 ruff==0.14.5 -#slack_sdk==3.39.0 +slack_sdk>=3.39.0 +slack-bolt slack-cli-hooks<1.0.0 \ No newline at end of file diff --git a/messaging/work-objects/src/__init__.py b/messaging/work-objects/src/__init__.py deleted file mode 100644 index e69de29..0000000