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
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:
0
zerver/webhooks/slack/__init__.py
Normal file
0
zerver/webhooks/slack/__init__.py
Normal file
139
zerver/webhooks/slack/doc.md
Normal file
139
zerver/webhooks/slack/doc.md
Normal 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!}
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
7
zerver/webhooks/slack/fixtures/app_rate_limited.json
Normal file
7
zerver/webhooks/slack/fixtures/app_rate_limited.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
|
||||
"type": "app_rate_limited",
|
||||
"team_id": "T123ABC456",
|
||||
"minute_rate_limited": 1518467820,
|
||||
"api_app_id": "A123ABC456"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
|
||||
"challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
|
||||
"type": "url_verification"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": "<Desdemona> 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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
1
zerver/webhooks/slack/fixtures/message_info.txt
Normal file
1
zerver/webhooks/slack/fixtures/message_info.txt
Normal file
@@ -0,0 +1 @@
|
||||
token=aotJImaVOVEVWssawxmegDWt&team_id=T3W8CTX0F&team_domain=gsoc2017&service_id=133126759712&channel_id=C3W8CTZFZ&channel_name=general×tamp=1485947263.000010&user_id=U3W8CTX2T&user_name=slack_user&text=test
|
||||
@@ -0,0 +1 @@
|
||||
token=aotJImaVOVEVWssawxmegDWt&team_id=T3W8CTX0F&team_domain=gsoc2017&service_id=133126759712&channel_id=C3W8CTZFZ×tamp=1485947263.000010&user_id=U3W8CTX2T&user_name=slack_user&text=test
|
||||
@@ -0,0 +1 @@
|
||||
token=aotJImaVOVEVWssawxmegDWt&team_id=T3W8CTX0F&team_domain=gsoc2017&service_id=133126759712&channel_id=C3W8CTZFZ&channel_name=general×tamp=1485947263.000010&user_name=slack_user&user_id=U3W8CTX2T
|
||||
@@ -0,0 +1 @@
|
||||
token=aotJImaVOVEVWssawxmegDWt&team_id=T3W8CTX0F&team_domain=gsoc2017&service_id=133126759712&channel_id=C3W8CTZFZ&channel_name=general×tamp=1485947263.000010&user_id=U3W8CTX2T&text=test
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
50
zerver/webhooks/slack/fixtures/message_with_code_block.json
Normal file
50
zerver/webhooks/slack/fixtures/message_with_code_block.json
Normal 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) -> 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
179
zerver/webhooks/slack/fixtures/message_with_image_files.json
Normal file
179
zerver/webhooks/slack/fixtures/message_with_image_files.json
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
52
zerver/webhooks/slack/fixtures/message_with_inline_code.json
Normal file
52
zerver/webhooks/slack/fixtures/message_with_inline_code.json
Normal 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"
|
||||
}
|
||||
49
zerver/webhooks/slack/fixtures/message_with_normal_text.json
Normal file
49
zerver/webhooks/slack/fixtures/message_with_normal_text.json
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
49
zerver/webhooks/slack/fixtures/message_with_quote_block.json
Normal file
49
zerver/webhooks/slack/fixtures/message_with_quote_block.json
Normal 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": "> 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
222
zerver/webhooks/slack/fixtures/message_with_variety_files.json
Normal file
222
zerver/webhooks/slack/fixtures/message_with_variety_files.json
Normal 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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
595
zerver/webhooks/slack/tests.py
Normal file
595
zerver/webhooks/slack/tests.py
Normal 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) -> 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 = "> 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")
|
||||
317
zerver/webhooks/slack/view.py
Normal file
317
zerver/webhooks/slack/view.py
Normal 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)
|
||||
Reference in New Issue
Block a user