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..84e785d --- /dev/null +++ b/messaging/work-objects/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +.mypy_cache +.pytest_cache +.ruff_cache +.venv +.slack diff --git a/messaging/work-objects/README.md b/messaging/work-objects/README.md new file mode 100644 index 0000000..636dfe3 --- /dev/null +++ b/messaging/work-objects/README.md @@ -0,0 +1,40 @@ +# 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! + +## Setup + +### Option 1: Create a new app with the Slack CLI + +```bash +slack init && slack run +``` + +### Option 2: Create a Slack App in the UI and copy over your tokens + +Create an app on [https://api.slack.com/apps](https://api.slack.com/apps) from the app manifest in this repo (`manifest.json`). + +Configure environment variables: +```bash +# OAuth & Permissions → Bot User OAuth Token +export SLACK_BOT_TOKEN= +# Basic Information → App-Level Tokens (create one with the `connections:write` scope) +export SLACK_APP_TOKEN= +``` + +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 + +# Start the server +python3 app.py +``` diff --git a/messaging/work-objects/app.py b/messaging/work-objects/app.py new file mode 100644 index 0000000..9916483 --- /dev/null +++ b/messaging/work-objects/app.py @@ -0,0 +1,97 @@ +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, sample_task_unfurl_url, sample_file_unfurl_url + + +# 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] + + 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( + 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__": + 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 new file mode 100644 index 0000000..1885efb --- /dev/null +++ b/messaging/work-objects/manifest.json @@ -0,0 +1,33 @@ +{ + "display_information": { + "name": "work_objects_python", + "description": "A sample app demonstrating Slack Work Objects functionality" + }, + "features": { + "rich_previews": { + "is_active": true, + "entity_types": ["slack#/entities/task", "slack#/entities/file"] + }, + "unfurl_domains": ["work-objects-python.com"], + "bot_user": { + "display_name": "work_objects_python_bot", + "always_online": true + } + }, + "oauth_config": { + "scopes": { + "bot": ["chat:write", "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 + } +} diff --git a/messaging/work-objects/metadata.py b/messaging/work-objects/metadata.py new file mode 100644 index 0000000..8d1a0ed --- /dev/null +++ b/messaging/work-objects/metadata.py @@ -0,0 +1,82 @@ +from slack_sdk.models.metadata import ( + EntityMetadata, + EntityType, + ExternalRef, + EntityPayload, + EntityAttributes, + EntityTitle, + TaskEntityFields, + EntityStringField, + EntityTitle, + EntityAttributes, + TaskEntityFields, + EntityStringField, + EntityEditSupport, + EntityCustomField, + ExternalRef, + EntityFullSizePreview +) + +# Update the URL here to match your app's domain +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( + 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 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), + ) + ), + 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/requirements.txt b/messaging/work-objects/requirements.txt new file mode 100644 index 0000000..4897476 --- /dev/null +++ b/messaging/work-objects/requirements.txt @@ -0,0 +1,6 @@ +mypy==1.18.2 +pytest==9.0.1 +ruff==0.14.5 +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/tests/__init__.py b/messaging/work-objects/tests/__init__.py new file mode 100644 index 0000000..e69de29