snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:05:30 +05:00
commit 64898b2c79
8999 changed files with 1757358 additions and 0 deletions

View File

View File

@@ -0,0 +1,139 @@
# Forward Slack messages into Zulip
Forward messages sent to your Slack workspace's public channels into Zulip!
This integration lets you choose how to organize your Slack messages in Zulip.
You can:
- Send messages from each Slack channel into a **matching Zulip channel**.
- Send messages from each Slack channel into a **matching Zulip topic**.
- Send all Slack messages into a **single Zulip topic**.
If you'd like to forward messages in both directions (Slack to Zulip and Zulip
to Slack), please see [separate instructions][6] for how to set this up.
If you are looking to quickly move your Slack integrations to Zulip, check out
[Zulip's Slack-compatible incoming webhook][1].
!!! warn ""
Using [Slack's legacy Outgoing Webhook service][5] is no longer
recommended. Follow these instructions to switch to the new
[Slack Events API][3].
{start_tabs}
1. {!create-an-incoming-webhook.md!}
1. {!generate-webhook-url-basic.md!}
To send messages from each Slack channel into a **matching Zulip channel**,
open the **Where to send notifications** dropdown and select **Matching
Zulip channel**.
To send messages from each Slack channel into a **matching Zulip topic**,
open the **Topics to use** dropdown, and select **Topics named after Slack
channels**.
To send all Slack messages into a **single Zulip topic**, open the **Topics
to use** dropdown, and select **Send all notifications to a single topic**.
1. *(optional)* If you're setting up a [Slack bridge][6] to forward Zulip messages
into your Slack workspace, replace the value of the `?api_key=` parameter in
the **integration URL** you generated with the API key of the **Generic bot**
you're using for the Slack bridge.
1. Create a new [Slack app][4], and open it. Navigate to the **OAuth
& Permissions** menu, and scroll down to the **Scopes** section.
1. Make sure **Bot Token Scopes** includes `channels:history`, `channels:read`,
and `users:read`. If you're setting up a [bidirectional bridge][6], make sure
to also include the `chat:write` scope.
!!! tip ""
See the [required bot token scopes](#required-bot-token-scopes)
section for details about these scopes.
1. Scroll to the **OAuth Tokens** section in the same menu, and click **Install
to Workspace**. Grant the app permission to access your workspace
by clicking **Allow** when prompted.
1. You will immediately see a **Bot User OAuth Token**. Copy it and add it
to the end of your **integration URL** as `&slack_app_token=BOT_OAUTH_TOKEN`.
1. Go to the **Event Subscriptions** menu, toggle **Enable Events**, and enter
your updated **integration URL** in the **Request URL** field.
1. In the same menu, scroll down to the **Subscribe to bot events**
section, and click on **Add Bot User Event**. Select the
`message.channels` event.
1. Add the bot as an app to the Slack channels you'd like to receive
notifications from.
{end_tabs}
{!congrats.md!}
![](/static/images/integrations/slack/001.png)
### Required bot token scopes
- `channels:history` is required by Slack's Event API's
[message.channels](https://api.slack.com/events/message.channels) event. This
is used to send new messages from Slack to Zulip.
- `channels:read` is required for Slack's
[conversations.info](https://api.slack.com/methods/conversations.info)
endpoint. This is used to get the name of the Slack channel a message came
from.
- For a [bidirectional bridge][6] setup, the `chat:write` is also required for
Slack's
[chat.postMessage](https://docs.slack.dev/reference/methods/chat.postMessage/)
method. This is used to send new messages from Zulip to Slack.
- `users:read` is required to call
Slack's [users.info](https://api.slack.com/methods/users.info) endpoint. This
is used to get the name of the Slack message's sender.
### Caveats
- Mentions in messages are not converted to Zulip mentions.
- Notifications will be delayed by a few seconds.
- Some emojis exported from Slack will display as `:emojiname:`, if
the emoji name is not present in the emoji database used by Zulip.
- Threads will appear in the main topic instead of in separate topics.
The following data is not included:
- Custom emoji
- Message attachments
- Message edit history
- Message reactions
### Related documentation
- [Forward messages Slack <-> Zulip][6] (both directions)
- [Slack Events API documentation][3]
- [Slack Apps][4]
- [Zulip's Slack-compatible incoming webhook][1]
{!webhooks-url-specification.md!}
[1]: /integrations/slack_incoming
[2]: /help/create-a-channel
[3]: https://api.slack.com/apis/events-api
[4]: https://api.slack.com/apps
[5]: https://api.slack.com/legacy/custom-integrations/outgoing-webhooks
[6]: https://github.com/zulip/python-zulip-api/blob/main/zulip/integrations/bridge_with_slack/README.md

View File

@@ -0,0 +1,7 @@
{
"token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
"type": "app_rate_limited",
"team_id": "T123ABC456",
"minute_rate_limited": 1518467820,
"api_app_id": "A123ABC456"
}

View File

@@ -0,0 +1,5 @@
{
"token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
"challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
"type": "url_verification"
}

View File

@@ -0,0 +1,23 @@
{
"createdAt": 1780754580717,
"id": "REDACTED",
"payload": {
"groupId": "REDACTED",
"level": "critical",
"project": {
"id": "REDACTED"
},
"projectId": "REDACTED",
"projectSlug": "REDACTED",
"startedAt": 1780754580717,
"team": {
"id": "REDACTED"
},
"teamId": "REDACTED",
"teamSlug": "REDACTED",
"user": {
"id": "REDACTED"
}
},
"type": "observability.usage-anomaly"
}

View File

@@ -0,0 +1,64 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U074G5E1ANR",
"type": "message",
"ts": "1718856814.111759",
"bot_id": "B074K2M1NF4",
"app_id": "A0757RM31HN",
"text": "&lt;Desdemona&gt; koo",
"team": "T06NRA6HM3P",
"bot_profile": {
"id": "B074K2M1NF4",
"deleted": false,
"name": "export-bot",
"updated": 1716360210,
"app_id": "A0757RM31HN",
"icons": {
"image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",
"image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",
"image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png"
},
"team_id": "T06NRA6HM3P"
},
"blocks": [
{
"type": "rich_text",
"block_id": "a6Fvn",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "<Desdemona> koo"
}
]
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1718856814.111759",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev078H33MYDV",
"event_time": 1718856814,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,114 @@
{
"token": "x8GlZSSaBheFZWjZPbxPasEY",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A07A20JQSAG",
"event": {
"subtype": "bot_message",
"text": "*Task status changed* from `complete` to `to do`",
"username": "ClickUp",
"attachments": [
{
"id": 1,
"blocks": [
{
"type": "section",
"block_id": "bZ4XB",
"text": {
"type": "mrkdwn",
"text": "*<https://app.clickup.com/t/25567147/86cw30wf2|asd>*",
"verbatim": false
}
},
{
"type": "context",
"block_id": "TEfLM",
"elements": [
{
"type": "image",
"image_url": "https://search.clickup-au.com/media/app-icons/clickup/logo_alpha.png",
"alt_text": "Cookie / The Goodiest of Cstdddddsdd / dds"
},
{
"type": "mrkdwn",
"text": "in dds",
"verbatim": false
},
{
"type": "image",
"image_url": "https://search.clickup-au.com/media/app-icons/clickup/status.png",
"alt_text": "Status"
},
{
"type": "mrkdwn",
"text": "To Do",
"verbatim": false
}
]
},
{
"type": "actions",
"block_id": "cg8fg",
"elements": [
{
"type": "button",
"action_id": "NOOP",
"text": {
"type": "plain_text",
"text": "View task",
"emoji": true
},
"url": "https://app.clickup.com/t/25567147/86cw30wf2"
},
{
"type": "button",
"action_id": "EDIT_TASK_ACTION",
"text": {
"type": "plain_text",
"text": "Edit task",
"emoji": true
},
"value": "{\"taskID\":\"86cw30wf2\",\"teamID\":\"25567147\",\"taskUrl\":\"https://app.clickup.com/t/25567147/86cw30wf2\"}"
}
]
}
],
"color": "#656f7d",
"fallback": "[no preview available]"
}
],
"type": "message",
"ts": "1723609070.703489",
"bot_id": "B06NWMNUQ3W",
"app_id": "A3G4A68V9",
"blocks": [
{
"type": "section",
"block_id": "X0xqC",
"text": {
"type": "mrkdwn",
"text": "*Task status changed* from `complete` to `to do`",
"verbatim": false
}
}
],
"channel": "C06P6T3QGD7",
"event_ts": "1723609070.703489",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev07GMTJQFEJ",
"event_time": 1723609070,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U07A1PEN4JW",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwN0EyMEpRU0FHIiwiY2lkIjoiQzA2UDZUM1FHRDcifQ"
}

View File

@@ -0,0 +1 @@
token=aotJImaVOVEVWssawxmegDWt&team_id=T3W8CTX0F&team_domain=gsoc2017&service_id=133126759712&channel_id=C3W8CTZFZ&channel_name=general&timestamp=1485947263.000010&user_id=U3W8CTX2T&user_name=slack_user&text=test

View File

@@ -0,0 +1 @@
token=aotJImaVOVEVWssawxmegDWt&team_id=T3W8CTX0F&team_domain=gsoc2017&service_id=133126759712&channel_id=C3W8CTZFZ&timestamp=1485947263.000010&user_id=U3W8CTX2T&user_name=slack_user&text=test

View File

@@ -0,0 +1 @@
token=aotJImaVOVEVWssawxmegDWt&team_id=T3W8CTX0F&team_domain=gsoc2017&service_id=133126759712&channel_id=C3W8CTZFZ&channel_name=general&timestamp=1485947263.000010&user_name=slack_user&user_id=U3W8CTX2T

View File

@@ -0,0 +1 @@
token=aotJImaVOVEVWssawxmegDWt&team_id=T3W8CTX0F&team_domain=gsoc2017&service_id=133126759712&channel_id=C3W8CTZFZ&channel_name=general&timestamp=1485947263.000010&user_id=U3W8CTX2T&text=test

View File

@@ -0,0 +1,66 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719211714.999529",
"client_msg_id": "8908e456-1821-4316-875c-810019adf321",
"text": "\u2022 list three\n\u2022 list two",
"team": "T06NRA6HM3P",
"blocks": [
{
"type": "rich_text",
"block_id": "Cua/K",
"elements": [
{
"type": "rich_text_list",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "list three"
}
]
},
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "list two"
}
]
}
],
"style": "bullet",
"indent": 0,
"border": 0
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1719211714.999529",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev07A31E3A2C",
"event_time": 1719211714,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,61 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719210664.234089",
"client_msg_id": "3e66cc71-119b-4576-b9b5-ec6000cd7dc8",
"text": "<@U074VRHQ11T> <#C06P6T3QGD7|general> message with both channel and user mentions",
"team": "T06NRA6HM3P",
"blocks": [
{
"type": "rich_text",
"block_id": "jkPDh",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "user",
"user_id": "U074VRHQ11T"
},
{
"type": "text",
"text": " "
},
{
"type": "channel",
"channel_id": "C06P6T3QGD7"
},
{
"type": "text",
"text": " message with both channel and user mentions"
}
]
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1719210664.234089",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev079B8H59UM",
"event_time": 1719210664,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,61 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719210488.508969",
"client_msg_id": "f90f528c-a4de-4ddf-8a6c-5315cc17c2d3",
"text": "<#C06NRA6JLER|zulip-mirror> <#C06P6T3QGD7|general> message with channel mentions",
"team": "T06NRA6HM3P",
"blocks": [
{
"type": "rich_text",
"block_id": "bVBDn",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "channel",
"channel_id": "C06NRA6JLER"
},
{
"type": "text",
"text": " "
},
{
"type": "channel",
"channel_id": "C06P6T3QGD7"
},
{
"type": "text",
"text": " message with channel mentions"
}
]
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1719210488.508969",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev079E1Z0DS7",
"event_time": 1719210488,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,50 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719211888.680799",
"client_msg_id": "452a95ae-9e35-4d84-a4c3-7012c6908f09",
"text": "```def is_bot_message(payload: WildValue) -&gt; bool:\n app_api_id = payload.get(\"api_app_id\").tame(check_none_or(check_string))\n bot_app_id = (\n payload.get(\"event\", {})\n .get(\"bot_profile\", {})\n .get(\"app_id\")\n .tame(check_none_or(check_string))\n )\n return bot_app_id is not None and app_api_id == bot_app_id```",
"team": "T06NRA6HM3P",
"blocks": [
{
"type": "rich_text",
"block_id": "293Eu",
"elements": [
{
"type": "rich_text_preformatted",
"elements": [
{
"type": "text",
"text": "def is_bot_message(payload: WildValue) -> bool:\n app_api_id = payload.get(\"api_app_id\").tame(check_none_or(check_string))\n bot_app_id = (\n payload.get(\"event\", {})\n .get(\"bot_profile\", {})\n .get(\"app_id\")\n .tame(check_none_or(check_string))\n )\n return bot_app_id is not None and app_api_id == bot_app_id"
}
],
"border": 0
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1719211888.680799",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev07A31RB0EL",
"event_time": 1719211888,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,99 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719211546.331399",
"client_msg_id": "b763525d-9211-4333-936c-b1c02ff0105e",
"text": "~_*<@U074VRHQ11T>*_~ ~_*<#C06P6T3QGD7|general>*_~ ~_*<!channel>*_~ ",
"team": "T06NRA6HM3P",
"blocks": [
{
"type": "rich_text",
"block_id": "yorkC",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "user",
"user_id": "U074VRHQ11T",
"style": {
"bold": true,
"italic": true,
"strike": true
}
},
{
"type": "text",
"text": " ",
"style": {
"bold": true,
"italic": true,
"strike": true
}
},
{
"type": "channel",
"channel_id": "C06P6T3QGD7",
"style": {
"bold": true,
"italic": true,
"strike": true
}
},
{
"type": "text",
"text": " ",
"style": {
"bold": true,
"italic": true,
"strike": true
}
},
{
"type": "broadcast",
"range": "channel",
"style": {
"bold": true,
"italic": true,
"strike": true
}
},
{
"type": "text",
"text": " ",
"style": {
"bold": true,
"italic": true,
"strike": true
}
}
]
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1719211546.331399",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev079E3ZLGNP",
"event_time": 1719211546,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,94 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719211406.531029",
"client_msg_id": "f9b21f6a-690b-4acb-b04d-d264cd7512a2",
"text": "this is text messages with overlapping formatting\n_*bold with italic*_\n~*bold with strike through*~\n~_italic with strike through_~\n~_*all three*_~",
"team": "T06NRA6HM3P",
"blocks": [
{
"type": "rich_text",
"block_id": "Gx+b7",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "this is text messages with overlapping formatting\n"
},
{
"type": "text",
"text": "bold with italic",
"style": {
"bold": true,
"italic": true
}
},
{
"type": "text",
"text": "\n"
},
{
"type": "text",
"text": "bold with strike through",
"style": {
"bold": true,
"strike": true
}
},
{
"type": "text",
"text": "\n"
},
{
"type": "text",
"text": "italic with strike through",
"style": {
"italic": true,
"strike": true
}
},
{
"type": "text",
"text": "\n"
},
{
"type": "text",
"text": "all three",
"style": {
"bold": true,
"italic": true,
"strike": true
}
}
]
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1719211406.531029",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev07A30U5YEL",
"event_time": 1719211406,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,66 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719210450.946059",
"client_msg_id": "266a366d-5e6d-4293-8e9d-1d3ada3a3c77",
"text": "*Bold text* _italic text_ ~strikethrough~",
"team": "T06NRA6HM3P",
"blocks": [
{
"type": "rich_text",
"block_id": "pQxnY",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "Bold text ",
"style": {
"bold": true
}
},
{
"type": "text",
"text": "italic text ",
"style": {
"italic": true
}
},
{
"type": "text",
"text": "strikethrough",
"style": {
"strike": true
}
}
]
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1719210450.946059",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev0797G216NS",
"event_time": 1719210450,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,179 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"text": "",
"files": [
{
"id": "F079E4173BL",
"created": 1719209777,
"timestamp": 1719209777,
"name": "5e44bcbc-e43c-4a2e-85de-4be126f392f4.jpg",
"title": "5e44bcbc-e43c-4a2e-85de-4be126f392f4.jpg",
"mimetype": "image/jpeg",
"filetype": "jpg",
"pretty_type": "JPEG",
"user": "U06NU4E26M9",
"user_team": "T06NRA6HM3P",
"editable": false,
"size": 3036645,
"mode": "hosted",
"is_external": false,
"external_type": "",
"is_public": true,
"public_url_shared": false,
"display_as_bot": false,
"username": "",
"url_private": "https://files.slack.com/files-pri/T06NRA6HM3P-F079E4173BL/5e44bcbc-e43c-4a2e-85de-4be126f392f4.jpg",
"url_private_download": "https://files.slack.com/files-pri/T06NRA6HM3P-F079E4173BL/download/5e44bcbc-e43c-4a2e-85de-4be126f392f4.jpg",
"media_display_type": "unknown",
"thumb_64": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079E4173BL-6e091e0261/5e44bcbc-e43c-4a2e-85de-4be126f392f4_64.jpg",
"thumb_80": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079E4173BL-6e091e0261/5e44bcbc-e43c-4a2e-85de-4be126f392f4_80.jpg",
"thumb_360": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079E4173BL-6e091e0261/5e44bcbc-e43c-4a2e-85de-4be126f392f4_360.jpg",
"thumb_360_w": 360,
"thumb_360_h": 360,
"thumb_480": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079E4173BL-6e091e0261/5e44bcbc-e43c-4a2e-85de-4be126f392f4_480.jpg",
"thumb_480_w": 480,
"thumb_480_h": 480,
"thumb_160": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079E4173BL-6e091e0261/5e44bcbc-e43c-4a2e-85de-4be126f392f4_160.jpg",
"thumb_720": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079E4173BL-6e091e0261/5e44bcbc-e43c-4a2e-85de-4be126f392f4_720.jpg",
"thumb_720_w": 720,
"thumb_720_h": 720,
"thumb_800": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079E4173BL-6e091e0261/5e44bcbc-e43c-4a2e-85de-4be126f392f4_800.jpg",
"thumb_800_w": 800,
"thumb_800_h": 800,
"thumb_960": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079E4173BL-6e091e0261/5e44bcbc-e43c-4a2e-85de-4be126f392f4_960.jpg",
"thumb_960_w": 960,
"thumb_960_h": 960,
"thumb_1024": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079E4173BL-6e091e0261/5e44bcbc-e43c-4a2e-85de-4be126f392f4_1024.jpg",
"thumb_1024_w": 1024,
"thumb_1024_h": 1024,
"original_w": 3468,
"original_h": 3468,
"thumb_tiny": "AwAwADCzSHgZxmnUhqRkYcYyccnHFOIqIMMkqMYqUHIoAYRTTTzTTQBPUcg6MM5HpT80132igBgyfYd+MUwEqcADj0pcqM54JHIppfI9e3WmBLkdqaaSMnacjFBpAS5ozTc0ZoARk3HO7FN2gDHUfyp2aQmgAz2700mgmmk0Af/Z",
"permalink": "https://ds-py62195.slack.com/files/U06NU4E26M9/F079E4173BL/5e44bcbc-e43c-4a2e-85de-4be126f392f4.jpg",
"permalink_public": "https://slack-files.com/T06NRA6HM3P-F079E4173BL-b1d2ae27be",
"has_rich_preview": false,
"file_access": "visible"
},
{
"id": "F079GJ49X4L",
"created": 1719209792,
"timestamp": 1719209792,
"name": "notif_bot.png",
"title": "notif_bot.png",
"mimetype": "image/png",
"filetype": "png",
"pretty_type": "PNG",
"user": "U06NU4E26M9",
"user_team": "T06NRA6HM3P",
"editable": false,
"size": 45054,
"mode": "hosted",
"is_external": false,
"external_type": "",
"is_public": true,
"public_url_shared": false,
"display_as_bot": false,
"username": "",
"url_private": "https://files.slack.com/files-pri/T06NRA6HM3P-F079GJ49X4L/notif_bot.png",
"url_private_download": "https://files.slack.com/files-pri/T06NRA6HM3P-F079GJ49X4L/download/notif_bot.png",
"media_display_type": "unknown",
"thumb_64": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079GJ49X4L-5f45c24098/notif_bot_64.png",
"thumb_80": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079GJ49X4L-5f45c24098/notif_bot_80.png",
"thumb_360": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079GJ49X4L-5f45c24098/notif_bot_360.png",
"thumb_360_w": 360,
"thumb_360_h": 360,
"thumb_480": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079GJ49X4L-5f45c24098/notif_bot_480.png",
"thumb_480_w": 480,
"thumb_480_h": 480,
"thumb_160": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079GJ49X4L-5f45c24098/notif_bot_160.png",
"original_w": 500,
"original_h": 500,
"thumb_tiny": "AwAwADBbKzia3V5F3M3PParH2O3/AOeS0WX/AB5xfSnC4Q3DQYO4DPtSEN+x2/8AzyWj7Hb/APPJanqOd/KhdxjIHGfWgBn2O3/55LVe9s4lt2eNdrLzx3q3AZDEpmAD9wKZe/8AHnL9KAEtGCWMbMcALkmmSs8wDRQSb1OVcgD/ACKZ/wAwlT6AH9auTb/JcxffxxQMi3XLdIUX/ef/AApssVxIo8ySJFBB4BP86fbb8vnzNnG3zOvvS3ELSMjKqPtyNr9Oe9ADJBPChlMokC8lSgGRReHNlIR0K091EVkUkbgIQSfpUU2f7MO7g+WM/pQIW1UPYIjdGXBpwW5UBRLGQOASpzVeyvIlt1SRtrLxz3qx9st/+eq0AMk85MbrhyWOAsaDJpERXfa73IYjIDnGfyoe6gNxCwkXC7sn8KJLqA3MTCVcBWyfyoGSrawqwbZkjuxJpL3/AI85fpR9st/+eq1XvbyJrdkjbczccdqBH//Z",
"permalink": "https://ds-py62195.slack.com/files/U06NU4E26M9/F079GJ49X4L/notif_bot.png",
"permalink_public": "https://slack-files.com/T06NRA6HM3P-F079GJ49X4L-b7e63652ec",
"has_rich_preview": false,
"file_access": "visible"
},
{
"id": "F07A2TA6PPS",
"created": 1719209799,
"timestamp": 1719209799,
"name": "books.jpg",
"title": "books.jpg",
"mimetype": "image/jpeg",
"filetype": "jpg",
"pretty_type": "JPEG",
"user": "U06NU4E26M9",
"user_team": "T06NRA6HM3P",
"editable": false,
"size": 1406170,
"mode": "hosted",
"is_external": false,
"external_type": "",
"is_public": true,
"public_url_shared": false,
"display_as_bot": false,
"username": "",
"url_private": "https://files.slack.com/files-pri/T06NRA6HM3P-F07A2TA6PPS/books.jpg",
"url_private_download": "https://files.slack.com/files-pri/T06NRA6HM3P-F07A2TA6PPS/download/books.jpg",
"media_display_type": "unknown",
"thumb_64": "https://files.slack.com/files-tmb/T06NRA6HM3P-F07A2TA6PPS-b8f86f8918/books_64.jpg",
"thumb_80": "https://files.slack.com/files-tmb/T06NRA6HM3P-F07A2TA6PPS-b8f86f8918/books_80.jpg",
"thumb_360": "https://files.slack.com/files-tmb/T06NRA6HM3P-F07A2TA6PPS-b8f86f8918/books_360.jpg",
"thumb_360_w": 360,
"thumb_360_h": 162,
"thumb_480": "https://files.slack.com/files-tmb/T06NRA6HM3P-F07A2TA6PPS-b8f86f8918/books_480.jpg",
"thumb_480_w": 480,
"thumb_480_h": 216,
"thumb_160": "https://files.slack.com/files-tmb/T06NRA6HM3P-F07A2TA6PPS-b8f86f8918/books_160.jpg",
"thumb_720": "https://files.slack.com/files-tmb/T06NRA6HM3P-F07A2TA6PPS-b8f86f8918/books_720.jpg",
"thumb_720_w": 720,
"thumb_720_h": 324,
"thumb_800": "https://files.slack.com/files-tmb/T06NRA6HM3P-F07A2TA6PPS-b8f86f8918/books_800.jpg",
"thumb_800_w": 800,
"thumb_800_h": 360,
"thumb_960": "https://files.slack.com/files-tmb/T06NRA6HM3P-F07A2TA6PPS-b8f86f8918/books_960.jpg",
"thumb_960_w": 960,
"thumb_960_h": 432,
"thumb_1024": "https://files.slack.com/files-tmb/T06NRA6HM3P-F07A2TA6PPS-b8f86f8918/books_1024.jpg",
"thumb_1024_w": 1024,
"thumb_1024_h": 461,
"original_w": 4624,
"original_h": 2080,
"thumb_tiny": "AwAVADCikrg8HP1rVWBD16/WsuAFpkBHGa0w5z96kA2aCMnByMdCDzVaW2JJkeQuT0yKtSZZs55FROCsBBOeaBlRwwByRj2qvVmQnb9fSoIxk80IGhYpDE+4DNTi8P8Ac/WqtA60xFhrpy2RgUxriRlIJGD7VGetJQAu4+tKn3qbTk+9QB//2Q==",
"permalink": "https://ds-py62195.slack.com/files/U06NU4E26M9/F07A2TA6PPS/books.jpg",
"permalink_public": "https://slack-files.com/T06NRA6HM3P-F07A2TA6PPS-44b4516dd5",
"has_rich_preview": false,
"file_access": "visible"
}
],
"upload": false,
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719209815.105159",
"client_msg_id": "15117486-b4e1-42a9-a553-ca2886d94222",
"channel": "C06NRA6JLER",
"subtype": "file_share",
"event_ts": "1719209815.105159",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev079GJ5KG5A",
"event_time": 1719209815,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,43 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"text": "Message with a file shared from a Slack Connect channel",
"files": [
{
"id": "F12345678",
"mode": "file_access",
"file_access": "check_file_info",
"created": 0,
"timestamp": 0,
"user": ""
}
],
"upload": false,
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719209815.105159",
"client_msg_id": "15117486-b4e1-42a9-a553-ca2886d94222",
"channel": "C06NRA6JLER",
"subtype": "file_share",
"event_ts": "1719209815.105159",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev079GJ5KG5A",
"event_time": 1719209815,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": true,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,52 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719211825.087659",
"client_msg_id": "ad34b81a-017a-4468-9dfe-6ec77aeb936b",
"text": "`asdasda this is a code block`",
"team": "T06NRA6HM3P",
"blocks": [
{
"type": "rich_text",
"block_id": "DkA3r",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "asdasda this is a code block",
"style": {
"code": true
}
}
]
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1719211825.087659",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev078ZLS9GNT",
"event_time": 1719211825,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,49 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719209506.817929",
"client_msg_id": "7cc3cc17-2fed-4356-b3c2-48106ab3a209",
"text": "Hello, this is a normal text message",
"team": "T06NRA6HM3P",
"blocks": [
{
"type": "rich_text",
"block_id": "Xh7lJ",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "Hello, this is a normal text message"
}
]
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1719209506.817929",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev079SP4TP1P",
"event_time": 1719209506,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,93 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719211791.356739",
"client_msg_id": "a0d2e7ba-c057-45df-b59a-d4e38f4ac055",
"text": "1. point one\n2. point two\n3. mix both\n4. pour water\n5. etc",
"team": "T06NRA6HM3P",
"blocks": [
{
"type": "rich_text",
"block_id": "G+/T6",
"elements": [
{
"type": "rich_text_list",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "point one"
}
]
},
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "point two"
}
]
},
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "mix both"
}
]
},
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "pour water"
}
]
},
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "etc"
}
]
}
],
"style": "ordered",
"indent": 0,
"border": 0
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1719211791.356739",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev079STB112M",
"event_time": 1719211791,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,49 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719211959.210699",
"client_msg_id": "116749a6-f2e2-4f99-96cb-fcef3300d1a1",
"text": "&gt; This is a quote",
"team": "T06NRA6HM3P",
"blocks": [
{
"type": "rich_text",
"block_id": "k3QzB",
"elements": [
{
"type": "rich_text_quote",
"elements": [
{
"type": "text",
"text": "This is a quote"
}
]
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1719211959.210699",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev079GN4DWNQ",
"event_time": 1719211959,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,69 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719209649.631399",
"client_msg_id": "ec2b7c8f-8530-43cc-9287-9f7617d5d335",
"text": "<@U074VRHQ11T> <@U074G5E1ANR> <@U06NU4E26M9> hello, this is a message with mentions",
"team": "T06NRA6HM3P",
"blocks": [
{
"type": "rich_text",
"block_id": "UST8x",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "user",
"user_id": "U074VRHQ11T"
},
{
"type": "text",
"text": " "
},
{
"type": "user",
"user_id": "U074G5E1ANR"
},
{
"type": "text",
"text": " "
},
{
"type": "user",
"user_id": "U06NU4E26M9"
},
{
"type": "text",
"text": " hello, this is a message with mentions"
}
]
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1719209649.631399",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev079B6N3H53",
"event_time": 1719209649,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,222 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"text": "Message with an assortment of file types",
"files": [
{
"id": "F079E4CMY5Q",
"created": 1719209974,
"timestamp": 1719209974,
"name": "postman-agent-0.4.25-linux-x64.tar.gz",
"title": "postman-agent-0.4.25-linux-x64.tar.gz",
"mimetype": "application/x-gzip",
"filetype": "gzip",
"pretty_type": "Gzip",
"user": "U06NU4E26M9",
"user_team": "T06NRA6HM3P",
"editable": false,
"size": 107287179,
"mode": "hosted",
"is_external": false,
"external_type": "",
"is_public": true,
"public_url_shared": false,
"display_as_bot": false,
"username": "",
"url_private": "https://files.slack.com/files-pri/T06NRA6HM3P-F079E4CMY5Q/postman-agent-0.4.25-linux-x64.tar.gz",
"url_private_download": "https://files.slack.com/files-pri/T06NRA6HM3P-F079E4CMY5Q/download/postman-agent-0.4.25-linux-x64.tar.gz",
"media_display_type": "unknown",
"permalink": "https://ds-py62195.slack.com/files/U06NU4E26M9/F079E4CMY5Q/postman-agent-0.4.25-linux-x64.tar.gz",
"permalink_public": "https://slack-files.com/T06NRA6HM3P-F079E4CMY5Q-22ef8b1a63",
"has_rich_preview": false,
"file_access": "visible"
},
{
"id": "F079SQ33CBT",
"created": 1719210050,
"timestamp": 1719210050,
"name": "discord-0.0.55.deb",
"title": "discord-0.0.55.deb",
"mimetype": "application/octet-stream",
"filetype": "binary",
"pretty_type": "Binary",
"user": "U06NU4E26M9",
"user_team": "T06NRA6HM3P",
"editable": false,
"size": 100957736,
"mode": "hosted",
"is_external": false,
"external_type": "",
"is_public": true,
"public_url_shared": false,
"display_as_bot": false,
"username": "",
"url_private": "https://files.slack.com/files-pri/T06NRA6HM3P-F079SQ33CBT/discord-0.0.55.deb",
"url_private_download": "https://files.slack.com/files-pri/T06NRA6HM3P-F079SQ33CBT/download/discord-0.0.55.deb",
"media_display_type": "unknown",
"permalink": "https://ds-py62195.slack.com/files/U06NU4E26M9/F079SQ33CBT/discord-0.0.55.deb",
"permalink_public": "https://slack-files.com/T06NRA6HM3P-F079SQ33CBT-2438647ea1",
"has_rich_preview": false,
"file_access": "visible"
},
{
"id": "F079SQ721A5",
"created": 1719210117,
"timestamp": 1719210117,
"name": "Slack-bot-scopes-List.xlsx",
"title": "Slack-bot-scopes-List.xlsx",
"mimetype": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"filetype": "xlsx",
"pretty_type": "Excel Spreadsheet",
"user": "U06NU4E26M9",
"user_team": "T06NRA6HM3P",
"editable": false,
"size": 75813,
"mode": "hosted",
"is_external": false,
"external_type": "",
"is_public": true,
"public_url_shared": false,
"display_as_bot": false,
"username": "",
"url_private": "https://files.slack.com/files-pri/T06NRA6HM3P-F079SQ721A5/slack-bot-scopes-list.xlsx",
"url_private_download": "https://files.slack.com/files-pri/T06NRA6HM3P-F079SQ721A5/download/slack-bot-scopes-list.xlsx",
"media_display_type": "unknown",
"converted_pdf": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079SQ721A5-315d1a255c/slack-bot-scopes-list_converted.pdf",
"thumb_pdf": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079SQ721A5-315d1a255c/slack-bot-scopes-list_thumb_pdf.png",
"thumb_pdf_w": 1210,
"thumb_pdf_h": 935,
"permalink": "https://ds-py62195.slack.com/files/U06NU4E26M9/F079SQ721A5/slack-bot-scopes-list.xlsx",
"permalink_public": "https://slack-files.com/T06NRA6HM3P-F079SQ721A5-77cdc07a4d",
"has_rich_preview": false,
"file_access": "visible"
},
{
"id": "F079B7G7NUD",
"created": 1719210121,
"timestamp": 1719210121,
"name": "wallpaper.jpg",
"title": "wallpaper.jpg",
"mimetype": "image/jpeg",
"filetype": "jpg",
"pretty_type": "JPEG",
"user": "U06NU4E26M9",
"user_team": "T06NRA6HM3P",
"editable": false,
"size": 1572520,
"mode": "hosted",
"is_external": false,
"external_type": "",
"is_public": true,
"public_url_shared": false,
"display_as_bot": false,
"username": "",
"url_private": "https://files.slack.com/files-pri/T06NRA6HM3P-F079B7G7NUD/wallpaper.jpg",
"url_private_download": "https://files.slack.com/files-pri/T06NRA6HM3P-F079B7G7NUD/download/wallpaper.jpg",
"media_display_type": "unknown",
"thumb_64": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079B7G7NUD-3b2ba7aee1/wallpaper_64.jpg",
"thumb_80": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079B7G7NUD-3b2ba7aee1/wallpaper_80.jpg",
"thumb_360": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079B7G7NUD-3b2ba7aee1/wallpaper_360.jpg",
"thumb_360_w": 360,
"thumb_360_h": 203,
"thumb_480": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079B7G7NUD-3b2ba7aee1/wallpaper_480.jpg",
"thumb_480_w": 480,
"thumb_480_h": 270,
"thumb_160": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079B7G7NUD-3b2ba7aee1/wallpaper_160.jpg",
"thumb_720": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079B7G7NUD-3b2ba7aee1/wallpaper_720.jpg",
"thumb_720_w": 720,
"thumb_720_h": 405,
"thumb_800": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079B7G7NUD-3b2ba7aee1/wallpaper_800.jpg",
"thumb_800_w": 800,
"thumb_800_h": 450,
"thumb_960": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079B7G7NUD-3b2ba7aee1/wallpaper_960.jpg",
"thumb_960_w": 960,
"thumb_960_h": 540,
"thumb_1024": "https://files.slack.com/files-tmb/T06NRA6HM3P-F079B7G7NUD-3b2ba7aee1/wallpaper_1024.jpg",
"thumb_1024_w": 1024,
"thumb_1024_h": 576,
"original_w": 3840,
"original_h": 2160,
"thumb_tiny": "AwAbADCUU8U0UyZyAFQZPemIl3qKFO7J7VVA7u3PoKl2sMKG4PpTESGmmiMNtbJzg8Gg0hirVadH356irIpJPu5oQFWD5nIP3hyPp6VZkYqq46ngVXT/AFwqyedmfU0xEka7U/CmSAA8U9+MVEaQz//Z",
"permalink": "https://ds-py62195.slack.com/files/U06NU4E26M9/F079B7G7NUD/wallpaper.jpg",
"permalink_public": "https://slack-files.com/T06NRA6HM3P-F079B7G7NUD-c461327fc3",
"has_rich_preview": false,
"file_access": "visible"
},
{
"id": "F07A2TVKNQ0",
"created": 1719210129,
"timestamp": 1719210129,
"name": "TestPDFfile.pdf",
"title": "TestPDFfile.pdf",
"mimetype": "application/pdf",
"filetype": "pdf",
"pretty_type": "PDF",
"user": "U06NU4E26M9",
"user_team": "T06NRA6HM3P",
"editable": false,
"size": 83186,
"mode": "hosted",
"is_external": false,
"external_type": "",
"is_public": true,
"public_url_shared": false,
"display_as_bot": false,
"username": "",
"url_private": "https://files.slack.com/files-pri/T06NRA6HM3P-F07A2TVKNQ0/testpdffile.pdf",
"url_private_download": "https://files.slack.com/files-pri/T06NRA6HM3P-F07A2TVKNQ0/download/testpdffile.pdf",
"media_display_type": "unknown",
"thumb_pdf": "https://files.slack.com/files-tmb/T06NRA6HM3P-F07A2TVKNQ0-67f8edb672/testpdffile_thumb_pdf.png",
"thumb_pdf_w": 935,
"thumb_pdf_h": 1210,
"permalink": "https://ds-py62195.slack.com/files/U06NU4E26M9/F07A2TVKNQ0/testpdffile.pdf",
"permalink_public": "https://slack-files.com/T06NRA6HM3P-F07A2TVKNQ0-4507eb87a4",
"has_rich_preview": false,
"file_access": "visible"
},
{
"id": "F07A2TVQ7C0",
"created": 1719210132,
"timestamp": 1719210132,
"name": "channels.json",
"title": "channels.json",
"mimetype": "text/plain",
"filetype": "json",
"pretty_type": "JSON",
"user": "U06NU4E26M9",
"user_team": "T06NRA6HM3P",
"editable": true,
"size": 1563,
"mode": "snippet",
"is_external": false,
"external_type": "",
"is_public": false,
"public_url_shared": false,
"display_as_bot": false,
"username": "",
"url_private": "https://files.slack.com/files-pri/T06NRA6HM3P-F07A2TVQ7C0/channels.json",
"url_private_download": "https://files.slack.com/files-pri/T06NRA6HM3P-F07A2TVQ7C0/download/channels.json",
"permalink": "https://ds-py62195.slack.com/files/U06NU4E26M9/F07A2TVQ7C0/channels.json",
"permalink_public": "https://slack-files.com/T06NRA6HM3P-F07A2TVQ7C0-5b1d277896",
"edit_link": "https://ds-py62195.slack.com/files/U06NU4E26M9/F07A2TVQ7C0/channels.json/edit",
"preview": "[\n{\n \"id\": \"C06NRA6JLER\",\n \"name\": \"random\",\n \"created\": 1710128735,",
"preview_highlight": "<div class="
}
],
"upload": false,
"type": "message",
"user": "U06NU4E26M9",
"ts": "1719209815.105159",
"client_msg_id": "15117486-b4e1-42a9-a553-ca2886d94222",
"channel": "C06NRA6JLER",
"subtype": "file_share",
"event_ts": "1719209815.105159",
"channel_type": "channel"
},
"type": "event_callback"
}

View File

@@ -0,0 +1,66 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U06NU4E26M9",
"type": "message",
"ts": "1719211249.499799",
"client_msg_id": "49d50b19-e55e-452b-a6c2-8c5e1d4b3ef9",
"text": "<!channel> <!here> Sorry for mentioning. This is for the test fixtures for the Slack integration update PR I'm working on and can't be done in a private channel. :bow:",
"team": "T06NRA6HM3P",
"blocks": [
{
"type": "rich_text",
"block_id": "298sd",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "broadcast",
"range": "channel"
},
{
"type": "text",
"text": " "
},
{
"type": "broadcast",
"range": "here"
},
{
"type": "text",
"text": " Sorry for mentioning. This is for the test fixtures for the Slack integration update PR I'm working on and can't be done in a private channel. "
},
{
"type": "emoji",
"name": "bow",
"unicode": "1f647"
}
]
}
]
}
],
"channel": "C06NRA6JLER",
"event_ts": "1719211249.499799",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "Ev079GLT13NY",
"event_time": 1719211249,
"authorizations": [
{
"enterprise_id": null,
"team_id": "T06NRA6HM3P",
"user_id": "U074G5E1ANR",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZOUkE2SE0zUCIsImFpZCI6IkEwNzU3Uk0zMUhOIiwiY2lkIjoiQzA2TlJBNkpMRVIifQ"
}

View File

@@ -0,0 +1,61 @@
{
"ok": true,
"channel": {
"id": "C012AB3CD",
"name": "Slack general",
"is_channel": true,
"is_group": false,
"is_im": false,
"is_mpim": false,
"is_private": false,
"created": 1654868334,
"is_archived": false,
"is_general": true,
"unlinked": 0,
"name_normalized": "Slack general",
"is_shared": false,
"is_frozen": false,
"is_org_shared": false,
"is_pending_ext_shared": false,
"pending_shared": [],
"context_team_id": "T123ABC456",
"updated": 1723130875818,
"parent_conversation": null,
"creator": "U123ABC456",
"is_ext_shared": false,
"shared_team_ids": [
"T123ABC456"
],
"pending_connected_team_ids": [],
"topic": {
"value": "For public discussion of generalities",
"creator": "W012A3BCD",
"last_set": 1449709364
},
"purpose": {
"value": "This part of the workspace is for fun. Make fun here.",
"creator": "W012A3BCD",
"last_set": 1449709364
},
"properties": {
"tabs": [
{
"id": "workflows",
"label": "",
"type": "workflows"
},
{
"id": "files",
"label": "",
"type": "files"
},
{
"id": "bookmarks",
"label": "",
"type": "bookmarks"
}
]
},
"previous_names": []
}
}

View File

@@ -0,0 +1,41 @@
{
"ok": true,
"user": {
"id": "W012A3CDE",
"team_id": "T012AB3C4",
"name": "supersecretemail",
"deleted": false,
"color": "9f69e7",
"real_name": "John Doe",
"tz": "America/Los_Angeles",
"tz_label": "Pacific Daylight Time",
"tz_offset": -25200,
"profile": {
"avatar_hash": "ge3b51ca72de",
"status_text": "Print is dead",
"status_emoji": ":books:",
"real_name": "John Doe",
"display_name": "john",
"real_name_normalized": "John Doe",
"display_name_normalized": "john",
"email": "supersecretemail@ghostbusters.example.com",
"image_original": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
"image_24": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
"image_32": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
"image_48": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
"image_72": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
"image_192": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
"image_512": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
"team": "T012AB3C4"
},
"is_admin": true,
"is_owner": false,
"is_primary_owner": false,
"is_restricted": false,
"is_ultra_restricted": false,
"is_bot": false,
"updated": 1502138686,
"is_app_user": false,
"has_2fa": false
}
}

View File

@@ -0,0 +1,595 @@
import json
from collections.abc import Callable
from functools import wraps
from typing import Concatenate
from unittest.mock import patch
import responses
from typing_extensions import ParamSpec, override
from zerver.lib.test_classes import WebhookTestCase
from zerver.webhooks.slack.view import INVALID_SLACK_TOKEN_MESSAGE
EXPECTED_TOPIC = "Message from Slack"
MESSAGE_WITH_NORMAL_TEXT = "Hello, this is a normal text message"
USER = "John Doe"
CHANNEL = "general"
API_CHANNEL_NAME = "Slack general"
EXPECTED_MESSAGE = "**{user}**: {message}"
TOPIC_WITH_CHANNEL = "channel: {channel}"
DEFAULT_TOPIC_NAME = "Message from Slack"
LEGACY_USER = "slack_user"
ParamT = ParamSpec("ParamT")
def mock_slack_api_calls(
test_func: Callable[Concatenate["SlackWebhookTests", ParamT], None],
) -> Callable[Concatenate["SlackWebhookTests", ParamT], None]:
@wraps(test_func)
@responses.activate
def _wrapped(self: "SlackWebhookTests", /, *args: ParamT.args, **kwargs: ParamT.kwargs) -> None:
self.make_stream(API_CHANNEL_NAME)
responses.add(
responses.GET,
"https://slack.com/api/users.info",
self.webhook_fixture_data("slack", "slack_users_info_api_response"),
)
responses.add(
responses.GET,
"https://slack.com/api/conversations.info",
self.webhook_fixture_data("slack", "slack_conversations_info_api_response"),
)
test_func(self, *args, **kwargs)
return _wrapped
class SlackWebhookTests(WebhookTestCase):
URL_TEMPLATE = "/api/v1/external/slack?stream={stream}&api_key={api_key}&slack_app_token=xoxp-XXXXXXXXXXXXXXXXXXXXX"
@mock_slack_api_calls
def test_slack_only_stream_parameter(self) -> None:
expected_message = EXPECTED_MESSAGE.format(user=USER, message=MESSAGE_WITH_NORMAL_TEXT)
self.check_webhook(
"message_with_normal_text",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_slack_with_user_specified_topic(self) -> None:
expected_topic_name = "test"
self.url = self.build_webhook_url(topic=expected_topic_name)
expected_message = EXPECTED_MESSAGE.format(user=USER, message=MESSAGE_WITH_NORMAL_TEXT)
self.check_webhook(
"message_with_normal_text",
expected_topic_name,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_slack_channels_map_to_topics_true(self) -> None:
self.url = self.build_webhook_url(channels_map_to_topics="1")
expected_message = EXPECTED_MESSAGE.format(user=USER, message=MESSAGE_WITH_NORMAL_TEXT)
expected_topic_name = TOPIC_WITH_CHANNEL.format(channel=API_CHANNEL_NAME)
self.check_webhook(
"message_with_normal_text",
expected_topic_name,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_slack_channels_map_to_topics_true_and_user_specified_topic(self) -> None:
expected_topic_name = "test"
self.url = self.build_webhook_url(topic=expected_topic_name, channels_map_to_topics="1")
expected_message = EXPECTED_MESSAGE.format(user=USER, message=MESSAGE_WITH_NORMAL_TEXT)
self.check_webhook(
"message_with_normal_text",
expected_topic_name,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_slack_channels_map_to_topics_false(self) -> None:
self.channel_name = API_CHANNEL_NAME
self.url = self.build_webhook_url(channels_map_to_topics="0")
expected_message = EXPECTED_MESSAGE.format(user=USER, message=MESSAGE_WITH_NORMAL_TEXT)
self.check_webhook(
"message_with_normal_text",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_url_options_channels_mapping_true(self) -> None:
self.channel_name = API_CHANNEL_NAME
self.url = self.build_webhook_url(mapping="channels")
expected_message = EXPECTED_MESSAGE.format(user=USER, message=MESSAGE_WITH_NORMAL_TEXT)
expected_topic_name = DEFAULT_TOPIC_NAME
self.check_webhook(
"message_with_normal_text",
expected_topic_name,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_slack_channels_map_to_topics_false_and_user_specified_topic(self) -> None:
self.channel_name = API_CHANNEL_NAME
expected_topic_name = "test"
self.url = self.build_webhook_url(topic=expected_topic_name, channels_map_to_topics="0")
expected_message = EXPECTED_MESSAGE.format(user=USER, message=MESSAGE_WITH_NORMAL_TEXT)
self.check_webhook(
"message_with_normal_text",
expected_topic_name,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_url_options_map_to_channels_and_user_specified_topic(self) -> None:
self.channel_name = API_CHANNEL_NAME
expected_topic_name = DEFAULT_TOPIC_NAME
self.url = self.build_webhook_url(topic=expected_topic_name, mapping="channels")
expected_message = EXPECTED_MESSAGE.format(user=USER, message=MESSAGE_WITH_NORMAL_TEXT)
self.check_webhook(
"message_with_normal_text",
expected_topic_name,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_invalid_channels_map_to_topics(self) -> None:
payload = self.get_body("message_with_normal_text")
url = self.build_webhook_url(channels_map_to_topics="abc")
result = self.client_post(url, payload, content_type="application/json")
self.assert_json_error(result, "Error: channels_map_to_topics parameter other than 0 or 1")
@mock_slack_api_calls
def test_challenge_handshake_payload(self) -> None:
url = self.build_webhook_url(channels_map_to_topics="1")
payload = self.get_body("challenge_handshake_payload")
with self.assertLogs(level="INFO") as info_logs:
result = self.client_post(url, payload, content_type="application/json")
expected_challenge_response = {
"msg": "",
"result": "success",
"challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
}
self.assertTrue(
"INFO:root:This is a Slack user token, which grants all rights the user has!"
in info_logs.output[0]
)
self.assertJSONEqual(result.content.decode("utf-8"), expected_challenge_response)
def test_app_rate_limited(self) -> None:
payload = self.get_body("app_rate_limited")
result = self.client_post(self.url, payload, content_type="application/json")
self.assert_json_success(result)
self.assert_in_response(
"The 'app_rate_limited' event isn't currently supported by the Slack webhook; ignoring",
result,
)
def test_anomalous_webhook_payload_error(self) -> None:
payload = self.get_body("example_anomalous_payload")
result = self.client_post(self.url, payload, content_type="application/json")
self.assert_json_error(
result, "Unable to parse request: Did Slack generate this event?", 400
)
@mock_slack_api_calls
def test_block_message_from_slack_bridge_bot(self) -> None:
self.check_webhook(
"message_from_slack_bridge_bot",
"",
"",
content_type="application/json",
expect_noop=True,
)
@mock_slack_api_calls
def test_message_with_bullet_points(self) -> None:
message_body = "• list three\n• list two"
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_bullet_points",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_with_channel_and_user_mentions(self) -> None:
message_body = "@**John Doe** **#general** message with both channel and user mentions"
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_channel_and_user_mentions",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_with_channel_mentions(self) -> None:
message_body = "**#zulip-mirror** **#general** message with channel mentions"
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_channel_mentions",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_with_formatted_texts(self) -> None:
message_body = "**Bold text** *italic text* ~~strikethrough~~"
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_formatted_texts",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_with_image_files(self) -> None:
message_body = """
*[5e44bcbc-e43c-4a2e-85de-4be126f392f4.jpg](https://ds-py62195.slack.com/files/U06NU4E26M9/F079E4173BL/5e44bcbc-e43c-4a2e-85de-4be126f392f4.jpg)*
*[notif_bot.png](https://ds-py62195.slack.com/files/U06NU4E26M9/F079GJ49X4L/notif_bot.png)*
*[books.jpg](https://ds-py62195.slack.com/files/U06NU4E26M9/F07A2TA6PPS/books.jpg)*"""
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_image_files",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_with_inaccessible_file(self) -> None:
message_body = """Message with a file shared from a Slack Connect channel
*Slack file F12345678*"""
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook("message_with_inaccessible_file", EXPECTED_TOPIC, expected_message)
@mock_slack_api_calls
def test_message_with_inline_code(self) -> None:
message_body = "`asdasda this is a code block`"
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_inline_code",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_with_ordered_list(self) -> None:
message_body = "1. point one\n2. point two\n3. mix both\n4. pour water\n5. etc"
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_ordered_list",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_with_user_mentions(self) -> None:
message_body = (
"@**John Doe** @**John Doe** @**John Doe** hello, this is a message with mentions"
)
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_user_mentions",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_with_variety_files(self) -> None:
message_body = """Message with an assortment of file types
*[postman-agent-0.4.25-linux-x64.tar.gz](https://ds-py62195.slack.com/files/U06NU4E26M9/F079E4CMY5Q/postman-agent-0.4.25-linux-x64.tar.gz)*
*[discord-0.0.55.deb](https://ds-py62195.slack.com/files/U06NU4E26M9/F079SQ33CBT/discord-0.0.55.deb)*
*[Slack-bot-scopes-List.xlsx](https://ds-py62195.slack.com/files/U06NU4E26M9/F079SQ721A5/slack-bot-scopes-list.xlsx)*
*[wallpaper.jpg](https://ds-py62195.slack.com/files/U06NU4E26M9/F079B7G7NUD/wallpaper.jpg)*
*[TestPDFfile.pdf](https://ds-py62195.slack.com/files/U06NU4E26M9/F07A2TVKNQ0/testpdffile.pdf)*
*[channels.json](https://ds-py62195.slack.com/files/U06NU4E26M9/F07A2TVQ7C0/channels.json)*"""
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_variety_files",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_with_workspace_mentions(self) -> None:
message_body = "@**all** @**all** Sorry for mentioning. This is for the test fixtures for the Slack integration update PR I'm working on and can't be done in a private channel. :bow:"
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_workspace_mentions",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_from_slack_integration_bot(self) -> None:
message_body = """
**Task status changed** from `complete` to `to do`
**<https://app.clickup.com/t/25567147/86cw30wf2|asd>**
[Cookie / The Goodiest of Cstdddddsdd / dds](https://search.clickup-au.com/media/app-icons/clickup/logo_alpha.png)
in dds
[Status](https://search.clickup-au.com/media/app-icons/clickup/status.png)
To Do""".strip()
# The integration message is from Slack's ClickUp webhook integration.
expected_message = EXPECTED_MESSAGE.format(user="ClickUp", message=message_body)
self.check_webhook(
"message_from_slack_integration_bot",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_with_code_block(self) -> None:
message_body = """```def is_bot_message(payload: WildValue) -&gt; bool:\n app_api_id = payload.get(\"api_app_id\").tame(check_none_or(check_string))\n bot_app_id = (\n payload.get(\"event\", {})\n .get(\"bot_profile\", {})\n .get(\"app_id\")\n .tame(check_none_or(check_string))\n )\n return bot_app_id is not None and app_api_id == bot_app_id```"""
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_code_block",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_with_complex_formatted_texts(self) -> None:
message_body = "this is text messages with overlapping formatting\n***bold with italic***\n~~**bold with strike through**~~\n~~*italic with strike through*~~\n~~***all three***~~"
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_complex_formatted_texts",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_with_complex_formatted_mentions(self) -> None:
message_body = "@**John Doe** **#general** ~~***@**all*****~~"
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_complex_formatted_mentions",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_message_with_quote_block(self) -> None:
message_body = "&gt; This is a quote"
expected_message = EXPECTED_MESSAGE.format(user=USER, message=message_body)
self.check_webhook(
"message_with_quote_block",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@mock_slack_api_calls
def test_block_slack_retries(self) -> None:
payload = self.get_body("message_with_normal_text")
with patch("zerver.webhooks.slack.view.check_send_webhook_message") as m:
result = self.client_post(
self.url,
payload,
headers={"X-Slack-Retry-Num": 1},
content_type="application/json",
)
self.assertFalse(m.called)
self.assert_json_success(result)
@mock_slack_api_calls
def test_missing_api_token_scope(self) -> None:
error_message = "Slack token is missing the following required scopes: ['users:read', 'users:read.email']"
user_facing_error_message = INVALID_SLACK_TOKEN_MESSAGE.format(error_message=error_message)
# We tested how `check_slack_token_access` may raise these errors in
# `test_slack_importer.py`. So, for simplicitys sake the function
# is directly mocked here to raise the ValueError we expect.
with (
patch("zerver.webhooks.slack.view.check_slack_token_access") as e,
patch("zerver.webhooks.slack.view.send_rate_limited_pm_notification_to_bot_owner") as s,
):
e.side_effect = ValueError(error_message)
self.check_webhook(
"challenge_handshake_payload",
expect_noop=True,
content_type="application/json",
)
s.assert_called_once()
_, _, actual_error_message = s.call_args[0]
self.assertEqual(actual_error_message, user_facing_error_message)
@mock_slack_api_calls
def test_missing_slack_api_token(self) -> None:
error_message = "slack_app_token is missing."
self.url = self.build_webhook_url(slack_app_token="")
user_facing_error_message = INVALID_SLACK_TOKEN_MESSAGE.format(error_message=error_message)
with (
patch("zerver.webhooks.slack.view.send_rate_limited_pm_notification_to_bot_owner") as s,
):
self.check_webhook(
"challenge_handshake_payload",
expect_noop=True,
content_type="application/json",
)
s.assert_called_once()
_, _, actual_error_message = s.call_args[0]
self.assertEqual(actual_error_message, user_facing_error_message)
@responses.activate
def test_user_info_api_response_no_real_name(self) -> None:
responses.add(
responses.GET,
"https://slack.com/api/users.info",
# In actuality, at least the "image_*" fields in the "profile" object
# will always be included. But, we only care about the missing
# "real_name" field in this case.
json.dumps({"ok": True, "user": {"profile": {}}}),
)
responses.add(
responses.GET,
"https://slack.com/api/conversations.info",
self.webhook_fixture_data("slack", "slack_conversations_info_api_response"),
)
user_id = "U06NU4E26M9"
expected_message = EXPECTED_MESSAGE.format(
user=f"Slack user {user_id}", message=MESSAGE_WITH_NORMAL_TEXT
)
self.check_webhook(
"message_with_normal_text",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
@responses.activate
def test_user_info_api_response_empty_real_name(self) -> None:
responses.add(
responses.GET,
"https://slack.com/api/users.info",
json.dumps(
{
"ok": True,
"user": {
"real_name": "",
"profile": {"real_name": ""},
},
}
),
)
responses.add(
responses.GET,
"https://slack.com/api/conversations.info",
self.webhook_fixture_data("slack", "slack_conversations_info_api_response"),
)
user_id = "U06NU4E26M9"
expected_message = EXPECTED_MESSAGE.format(
user=f"Slack user {user_id}", message=MESSAGE_WITH_NORMAL_TEXT
)
self.check_webhook(
"message_with_normal_text",
EXPECTED_TOPIC,
expected_message,
content_type="application/json",
)
class SlackLegacyWebhookTests(WebhookTestCase):
def test_slack_only_stream_parameter(self) -> None:
expected_topic_name = "Message from Slack"
expected_message = EXPECTED_MESSAGE.format(user=LEGACY_USER, message="test")
self.check_webhook(
"message_info",
expected_topic_name,
expected_message,
content_type="application/x-www-form-urlencoded",
)
def test_slack_with_user_specified_topic(self) -> None:
self.url = self.build_webhook_url(topic="test")
expected_topic_name = "test"
expected_message = EXPECTED_MESSAGE.format(user=LEGACY_USER, message="test")
self.check_webhook(
"message_info",
expected_topic_name,
expected_message,
content_type="application/x-www-form-urlencoded",
)
def test_slack_channels_map_to_topics_true(self) -> None:
self.url = self.build_webhook_url(channels_map_to_topics="1")
expected_topic_name = "channel: general"
expected_message = EXPECTED_MESSAGE.format(user=LEGACY_USER, message="test")
self.check_webhook(
"message_info",
expected_topic_name,
expected_message,
content_type="application/x-www-form-urlencoded",
)
def test_slack_channels_map_to_topics_true_and_user_specified_topic(self) -> None:
self.url = self.build_webhook_url(topic="test", channels_map_to_topics="1")
expected_topic_name = "test"
expected_message = EXPECTED_MESSAGE.format(user=LEGACY_USER, message="test")
self.check_webhook(
"message_info",
expected_topic_name,
expected_message,
content_type="application/x-www-form-urlencoded",
)
def test_slack_channels_map_to_topics_false(self) -> None:
self.channel_name = "general"
self.url = self.build_webhook_url(channels_map_to_topics="0")
expected_topic_name = "Message from Slack"
expected_message = EXPECTED_MESSAGE.format(user=LEGACY_USER, message="test")
self.check_webhook(
"message_info",
expected_topic_name,
expected_message,
content_type="application/x-www-form-urlencoded",
)
def test_slack_channels_map_to_topics_false_and_user_specified_topic(self) -> None:
self.channel_name = "general"
self.url = self.build_webhook_url(topic="test", channels_map_to_topics="0")
expected_topic_name = "test"
expected_message = EXPECTED_MESSAGE.format(user=LEGACY_USER, message="test")
self.check_webhook(
"message_info",
expected_topic_name,
expected_message,
content_type="application/x-www-form-urlencoded",
)
def test_missing_data_user_name(self) -> None:
payload = self.get_body("message_info_missing_user_name")
url = self.build_webhook_url()
result = self.client_post(url, payload, content_type="application/x-www-form-urlencoded")
self.assert_json_error(result, "Missing 'user_name' argument")
def test_missing_data_channel_name(self) -> None:
payload = self.get_body("message_info_missing_channel_name")
url = self.build_webhook_url()
result = self.client_post(url, payload, content_type="application/x-www-form-urlencoded")
self.assert_json_error(result, "Missing 'channel_name' argument")
def test_missing_data_text(self) -> None:
payload = self.get_body("message_info_missing_text")
url = self.build_webhook_url()
result = self.client_post(url, payload, content_type="application/x-www-form-urlencoded")
self.assert_json_error(result, "Missing 'text' argument")
@override
def get_body(self, fixture_name: str) -> str:
return self.webhook_fixture_data("slack", fixture_name, file_type="txt")

View File

@@ -0,0 +1,317 @@
import re
from typing import Annotated, Any, TypeAlias
from django.http import HttpRequest
from django.http.response import HttpResponse
from django.utils.translation import gettext as _
from zerver.actions.message_send import send_rate_limited_pm_notification_to_bot_owner
from zerver.data_import.slack import check_slack_token_access, get_slack_api_data
from zerver.data_import.slack_message_conversion import (
FALLBACK_SLACK_USER_FULL_NAME,
SLACK_USERMENTION_REGEX,
convert_slack_formatting,
convert_slack_workspace_mentions,
process_slack_block_and_attachment,
replace_links,
)
from zerver.decorator import webhook_view
from zerver.lib.exceptions import (
AnomalousWebhookPayloadError,
JsonableError,
UnsupportedWebhookEventTypeError,
)
from zerver.lib.request import RequestVariableMissingError
from zerver.lib.response import json_success
from zerver.lib.typed_endpoint import ApiParamConfig, typed_endpoint
from zerver.lib.validator import WildValue, check_none_or, check_string, to_wild_value
from zerver.lib.webhooks.common import check_send_webhook_message, get_setup_webhook_message
from zerver.models import UserProfile
FILE_LINK_TEMPLATE = "\n*[{file_name}]({file_link})*"
FILE_ID_TEMPLATE = "\n*Slack file {file_id}*"
ZULIP_MESSAGE_TEMPLATE = "**{sender}**: {text}"
VALID_OPTIONS = {"SHOULD_NOT_BE_MAPPED": "0", "SHOULD_BE_MAPPED": "1"}
SlackFileListT: TypeAlias = list[dict[str, str]]
SlackAPIResponseT: TypeAlias = dict[str, Any]
SLACK_CHANNELMENTION_REGEX = r"(?<=<#)(.*)(?=>)"
def is_zulip_slack_bridge_bot_message(payload: WildValue) -> bool:
app_api_id = payload.get("api_app_id").tame(check_none_or(check_string))
bot_app_id = (
payload.get("event", {})
.get("bot_profile", {})
.get("app_id")
.tame(check_none_or(check_string))
)
return bot_app_id is not None and app_api_id == bot_app_id
def get_slack_channel_name(channel_id: str, token: str) -> str:
slack_channel_data = get_slack_api_data(
"https://slack.com/api/conversations.info",
get_param="channel",
# Sleeping is not permitted from webhook code.
raise_if_rate_limited=True,
token=token,
channel=channel_id,
)
return slack_channel_data["name"]
def get_slack_sender_name(user_id: str, token: str) -> str:
slack_user_data = get_slack_api_data(
"https://slack.com/api/users.info",
get_param="user",
# Sleeping is not permitted from webhook code.
raise_if_rate_limited=True,
token=token,
user=user_id,
)
# The "real_name" field is not guaranteed to be included.
# If it is included -- although unlikely -- its type could
# be null, an empty string, or None.
user_name = slack_user_data.get("real_name")
if isinstance(user_name, str) and user_name.strip():
return user_name
return FALLBACK_SLACK_USER_FULL_NAME.format(id=user_id)
def convert_slack_user_and_channel_mentions(text: str, app_token: str) -> str:
tokens = text.split(" ")
for iterator in range(len(tokens)):
slack_usermention_match = re.search(SLACK_USERMENTION_REGEX, tokens[iterator], re.VERBOSE)
slack_channelmention_match = re.search(
SLACK_CHANNELMENTION_REGEX, tokens[iterator], re.MULTILINE
)
if slack_usermention_match:
# Convert Slack user mentions to a mention-like syntax since there
# is no way to map Slack and Zulip users.
slack_id = slack_usermention_match.group(2)
user_name = get_slack_sender_name(user_id=slack_id, token=app_token)
tokens[iterator] = "@**" + user_name + "**"
elif slack_channelmention_match:
# Convert Slack channel mentions to a mention-like syntax so that
# a mention isn't triggered for a Zulip channel with the same name.
channel_info: list[str] = slack_channelmention_match.group(0).split("|")
channel_id = channel_info[0]
channel_name = channel_info[1] if len(channel_info) > 1 else None
if channel_name:
tokens[iterator] = f"**#{channel_name}**"
else:
tokens[iterator] = f"**#[Slack channel {channel_id}]**" # nocoverage
text = " ".join(tokens)
return text
# This is a modified version of `convert_to_zulip_markdown` in
# `slack_message_conversion.py`, which cannot be used directly
# due to differences in the Slack import data and Slack webhook
# payloads.
def convert_to_zulip_markdown(text: str, slack_app_token: str) -> str:
text = convert_slack_formatting(text)
text = convert_slack_workspace_mentions(text)
text = convert_slack_user_and_channel_mentions(text, slack_app_token)
return text
def convert_raw_file_data(file_dict: WildValue) -> SlackFileListT:
# Files uploaded to Slack Connect channels arrive without their
# metadata. See https://docs.slack.dev/reference/objects/file-object/#slack_connect_files.
files = [
{
"file_link": file.get("permalink").tame(check_none_or(check_string)) or "",
"file_name": file.get("title").tame(check_none_or(check_string)) or "",
"file_id": file["id"].tame(check_string),
}
for file in file_dict
]
return files
def get_message_body(text: str, sender: str, files: SlackFileListT) -> str:
body = ZULIP_MESSAGE_TEMPLATE.format(sender=sender, text=text)
for file in files:
file_template = FILE_LINK_TEMPLATE if file["file_link"] else FILE_ID_TEMPLATE
body += file_template.format(**file)
return body
def handle_slack_webhook_message(
request: HttpRequest,
user_profile: UserProfile,
content: str,
channel: str | None,
channels_map_to_topics: str | None,
) -> None:
topic_name = "Message from Slack"
if channels_map_to_topics is None:
check_send_webhook_message(request, user_profile, topic_name, content)
elif channels_map_to_topics == VALID_OPTIONS["SHOULD_BE_MAPPED"]:
topic_name = f"channel: {channel}"
check_send_webhook_message(request, user_profile, topic_name, content)
elif channels_map_to_topics == VALID_OPTIONS["SHOULD_NOT_BE_MAPPED"]:
check_send_webhook_message(
request,
user_profile,
topic_name,
content,
stream=channel,
)
else:
raise JsonableError(_("Error: channels_map_to_topics parameter other than 0 or 1"))
def is_retry_call_from_slack(request: HttpRequest) -> bool:
return "X-Slack-Retry-Num" in request.headers
SLACK_INTEGRATION_TOKEN_SCOPES = {
# For Slack's users.info endpoint: https://api.slack.com/methods/users.info
"users:read",
# For Slack's conversations.info endpoint: https://api.slack.com/methods/conversations.info
"channels:read",
# For Slack's Event's API: https://api.slack.com/events/message.channels
"channels:history",
}
INVALID_SLACK_TOKEN_MESSAGE = """
Hi there! It looks like you're trying to set up a Slack webhook
integration. There seems to be an issue with the Slack app token
you've included in the URL (if any). Please check the error message
below to see if you're missing anything:
Error: {error_message}
Feel free to reach out to the [Zulip development community](https://chat.zulip.org/#narrow/channel/127-integrations)
if you need further help!
"""
@webhook_view("Slack", notify_bot_owner_on_invalid_json=False)
@typed_endpoint
def api_slack_webhook(
request: HttpRequest,
user_profile: UserProfile,
*,
slack_app_token: str = "",
channels_map_to_topics: str | None = None,
map_to_channels: Annotated[str | None, ApiParamConfig("mapping")] = None,
) -> HttpResponse:
if request.content_type != "application/json":
# Handle Slack's legacy Outgoing Webhook Service payload.
expected_legacy_variable = ["user_name", "text", "channel_name"]
legacy_payload = {}
for variable in expected_legacy_variable:
if variable in request.POST:
legacy_payload[variable] = request.POST[variable]
elif variable in request.GET: # nocoverage
legacy_payload[variable] = request.GET[variable]
else:
raise RequestVariableMissingError(variable)
text = convert_slack_formatting(legacy_payload["text"])
text = replace_links(text)
text = get_message_body(text, legacy_payload["user_name"], [])
handle_slack_webhook_message(
request,
user_profile,
text,
legacy_payload["channel_name"],
channels_map_to_topics,
)
return json_success(request)
try:
val = request.body.decode(request.encoding or "utf-8")
except UnicodeDecodeError: # nocoverage
raise JsonableError(_("Malformed payload"))
payload = to_wild_value("payload", val)
# The Slack Events API documents three outer event types:
# "url_verification", "app_rate_limited" and "event_callback".
payload_type = payload.get("type").tame(check_string)
# Handle initial URL verification handshake for Slack Events API.
if payload_type == "url_verification":
challenge = payload.get("challenge").tame(check_string)
try:
if slack_app_token == "":
raise ValueError("slack_app_token is missing.")
check_slack_token_access(slack_app_token, SLACK_INTEGRATION_TOKEN_SCOPES)
except (ValueError, Exception) as e:
send_rate_limited_pm_notification_to_bot_owner(
user_profile,
user_profile.realm,
INVALID_SLACK_TOKEN_MESSAGE.format(error_message=e),
)
# Return json success here as to not trigger retry calls
# from Slack.
return json_success(request)
check_send_webhook_message(
request,
user_profile,
"Integration events",
get_setup_webhook_message("Slack"),
)
return json_success(request=request, data={"challenge": challenge})
# A Slack fail condition occurs when we don't respond with HTTP 200
# within 3 seconds after Slack calls our endpoint. If this happens,
# Slack will retry sending the same payload. This is often triggered
# because of we have to do two callbacks for each call. To avoid
# sending the same message multiple times, we block subsequent retry
# calls from Slack.
if is_retry_call_from_slack(request):
return json_success(request)
# Prevent any Zulip messages sent through the Slack Bridge from looping
# back here.
if is_zulip_slack_bridge_bot_message(payload):
return json_success(request)
# Dispatched when a user's Slack app is rate limited:
# https://docs.slack.dev/reference/events/app_rate_limited.
# TODO: We could potentially handle this event by letting the bot owner
# know that their Slack app has been rate limited on Slack's end.
if payload_type == "app_rate_limited":
raise UnsupportedWebhookEventTypeError("app_rate_limited")
# Slack Event API payloads with an outer event type of "event_callback"
# should have an inner event object.
event_dict = payload.get("event", {})
if not event_dict:
raise AnomalousWebhookPayloadError
event_type = event_dict.get("type").tame(check_string)
if event_type != "message":
raise UnsupportedWebhookEventTypeError(event_type)
raw_files = event_dict.get("files")
files = convert_raw_file_data(raw_files) if raw_files else []
raw_text = process_slack_block_and_attachment(event_dict)
text = convert_to_zulip_markdown(raw_text, slack_app_token)
user_id = event_dict.get("user").tame(check_none_or(check_string))
if user_id is None:
# This is likely a Slack integration bot message. The sender of these
# messages doesn't have a user profile.
sender = event_dict["username"].tame(check_string)
else:
sender = get_slack_sender_name(user_id, slack_app_token)
content = get_message_body(text, sender, files)
# channels_map_to_topics=0 is ported to use PresetUrlOption.CHANNEL_MAPPING
# (map_to_channels).
if map_to_channels == "channels" and channels_map_to_topics is None:
channels_map_to_topics = VALID_OPTIONS["SHOULD_NOT_BE_MAPPED"]
channel_id = event_dict.get("channel").tame(check_string)
channel = (
get_slack_channel_name(channel_id, slack_app_token) if channels_map_to_topics else None
)
handle_slack_webhook_message(request, user_profile, content, channel, channels_map_to_topics)
return json_success(request)