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_incoming/__init__.py
Normal file
0
zerver/webhooks/slack_incoming/__init__.py
Normal file
37
zerver/webhooks/slack_incoming/doc.md
Normal file
37
zerver/webhooks/slack_incoming/doc.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Zulip's Slack-compatible incoming webhook
|
||||
|
||||
Zulip can process incoming webhook messages written to work with Slack's
|
||||
[incoming webhook API][1]. This makes it easy to quickly move your
|
||||
integrations when migrating your organization from Slack to Zulip.
|
||||
|
||||
!!! warn ""
|
||||
|
||||
**Note:** In the long term, the recommended approach is to use
|
||||
Zulip's native integrations, which take advantage of Zulip's topics.
|
||||
There may also be some quirks when Slack's formatting system is
|
||||
translated into Zulip's.
|
||||
|
||||
{start_tabs}
|
||||
|
||||
1. {!create-an-incoming-webhook.md!}
|
||||
|
||||
1. {!generate-webhook-url-basic.md!}
|
||||
|
||||
1. Use the generated URL anywhere you would use a Slack webhook.
|
||||
|
||||
{end_tabs}
|
||||
|
||||
### Related documentation
|
||||
|
||||
- [Slack's incoming webhook documentation][1]
|
||||
|
||||
- [Moving from Slack to Zulip](/help/moving-from-slack)
|
||||
|
||||
- [Forward Slack messages into Zulip](/integrations/slack)
|
||||
|
||||
- [Forward messages Slack <-> Zulip][2] (both directions)
|
||||
|
||||
{!webhooks-url-specification.md!}
|
||||
|
||||
[1]: https://api.slack.com/messaging/webhooks
|
||||
[2]: https://github.com/zulip/python-zulip-api/blob/main/zulip/integrations/bridge_with_slack/README.md
|
||||
48
zerver/webhooks/slack_incoming/fixtures/actions.json
Normal file
48
zerver/webhooks/slack_incoming/fixtures/actions.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"channel": "C1H9RESGL",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Danny Torrence left the following _review_ for your property:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"block_id": "section567",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "<https://google.com|Overlook Hotel> \n :star: \n Doors had too many axe holes, guest in room 237 was far too rowdy, whole place felt stuck in the 1920s."
|
||||
},
|
||||
"accessory": {
|
||||
"type": "image",
|
||||
"image_url": "https://is5-ssl.mzstatic.com/image/thumb/Purple3/v4/d3/72/5c/d3725c8f-c642-5d69-1904-aa36e4297885/source/256x256bb.jpg",
|
||||
"alt_text": "Haunted hotel image"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"block_id": "section789",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Average Rating*\n1.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Reply to review",
|
||||
"emoji": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
21
zerver/webhooks/slack_incoming/fixtures/attachment.json
Normal file
21
zerver/webhooks/slack_incoming/fixtures/attachment.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"channel": "#prometheus-alerts",
|
||||
"username": "Alertmanager",
|
||||
"blocks": null,
|
||||
"attachments": [
|
||||
{
|
||||
"title": "[FIRING:2] InstanceDown for api-server (env=\"prod\", severity=\"critical\")",
|
||||
"title_link": "https://alertmanager.local//#/alerts?receiver=default",
|
||||
"text": ":chart_with_upwards_trend: *<http://generator.local/1|Graph>* :notebook: *<https://runbook.local/1|Runbook>*\n\n*Alert details*:\n*Alert:* api-server down - `critical`\n*Description:* api-server at 1.2.3.4:8080 couldn't be scraped *Details:*\n • *alertname:* `InstanceDown`\n • *env:* `prod`\n • *instance:* `1.2.3.4:8080`\n • *job:* `api-server`\n • *severity:* `critical`\n\n*Alert:* api-server down - `critical`\n*Description:* api-server at 1.2.3.4:8081 couldn't be scraped *Details:*\n • *alertname:* `InstanceDown`\n • *env:* `prod`\n • *instance:* `1.2.3.4:8081`\n • *job:* `api-server`\n • *severity:* `critical`\n \n",
|
||||
"fallback": "[FIRING:2] InstanceDown api-server (prod critical) | https://alertmanager.local//#/alerts?receiver=default",
|
||||
"callback_id": "",
|
||||
"footer": "",
|
||||
"color": "danger",
|
||||
"mrkdwn_in": [
|
||||
"fallback",
|
||||
"pretext",
|
||||
"text"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "This is a section block with an accessory image."
|
||||
},
|
||||
"accessory": {
|
||||
"type": "image",
|
||||
"image_url": "https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg",
|
||||
"alt_text": "cute cat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "This is a section block with a button."
|
||||
},
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Click Me",
|
||||
"emoji": true
|
||||
},
|
||||
"value": "click_me_123",
|
||||
"action_id": "button-action"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "plain_text",
|
||||
"text": "one",
|
||||
"emoji": true
|
||||
},
|
||||
{
|
||||
"type": "plain_text",
|
||||
"text": "two",
|
||||
"emoji": true
|
||||
},
|
||||
{
|
||||
"type": "plain_text",
|
||||
"text": "three",
|
||||
"emoji": true
|
||||
},
|
||||
{
|
||||
"type": "plain_text",
|
||||
"text": "four",
|
||||
"emoji": true
|
||||
},
|
||||
{
|
||||
"type": "plain_text",
|
||||
"text": "five",
|
||||
"emoji": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"color": "good",
|
||||
"fields": [
|
||||
{
|
||||
"title": "Requested by",
|
||||
"value": "Some user",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": "Duration",
|
||||
"value": "00:02:03",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": "Build pipeline",
|
||||
"value": "ConsumerAddressModule",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": "Title with null value",
|
||||
"value": null,
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": "Title without value",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"value": "Value with null title",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"value": "Value without title",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": null,
|
||||
"value": null,
|
||||
"short": true
|
||||
}
|
||||
],
|
||||
"pretext": "Build bla bla succeeded",
|
||||
"mrkdwn_in": [
|
||||
"pretext"
|
||||
],
|
||||
"fallback": "Build ConsumerAddressModule_20150407.2 succeeded"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Test",
|
||||
"image_url": "https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg",
|
||||
"ts": 1655945306
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"title": null,
|
||||
"title_link": null,
|
||||
"image_url": null,
|
||||
"ts": null,
|
||||
"text": null,
|
||||
"pretext": null,
|
||||
"footer": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Sample title.",
|
||||
"title_link": "https://www.google.com",
|
||||
"image_url": "https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg",
|
||||
"ts": 1655945306,
|
||||
"text": "Sample text.",
|
||||
"pretext": "Sample pretext.",
|
||||
"footer": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Sample title.",
|
||||
"title_link": "https://www.google.com",
|
||||
"image_url": null,
|
||||
"ts": 1655945306,
|
||||
"text": "Sample text.",
|
||||
"pretext": "Sample pretext.",
|
||||
"footer": "Sample footer."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Sample title.",
|
||||
"title_link": "https://www.google.com",
|
||||
"image_url": "https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg",
|
||||
"ts": 1655945306,
|
||||
"text": "Sample text.",
|
||||
"pretext": null,
|
||||
"footer": "Sample footer."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Sample title.",
|
||||
"title_link": "https://www.google.com",
|
||||
"image_url": "https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg",
|
||||
"ts": 1655945306,
|
||||
"text": null,
|
||||
"pretext": "Sample pretext.",
|
||||
"footer": "Sample footer."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Sample title.",
|
||||
"title_link": null,
|
||||
"image_url": "https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg",
|
||||
"ts": 1655945306,
|
||||
"text": "Sample text.",
|
||||
"pretext": "Sample pretext.",
|
||||
"footer": "Sample footer."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"title": null,
|
||||
"title_link": "https://www.google.com",
|
||||
"image_url": "https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg",
|
||||
"ts": 1655945306,
|
||||
"text": "Sample text.",
|
||||
"pretext": "Sample pretext.",
|
||||
"footer": "Sample footer."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"title": "Sample title.",
|
||||
"title_link": "https://www.google.com",
|
||||
"image_url": "https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg",
|
||||
"ts": null,
|
||||
"text": "Sample text.",
|
||||
"pretext": "Sample pretext.",
|
||||
"footer": "Sample footer."
|
||||
}
|
||||
]
|
||||
}
|
||||
35
zerver/webhooks/slack_incoming/fixtures/blocks.json
Normal file
35
zerver/webhooks/slack_incoming/fixtures/blocks.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"text": "Danny Torrence left a 1 star review for your property.",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Danny Torrence left the following review for your property:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"block_id": "section567",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "<https://example.com|Overlook Hotel> \n :star: \n Doors had too many axe holes, guest in room 237 was far too rowdy, whole place felt stuck in the 1920s."
|
||||
},
|
||||
"accessory": {
|
||||
"type": "image",
|
||||
"image_url": "https://is5-ssl.mzstatic.com/image/thumb/Purple3/v4/d3/72/5c/d3725c8f-c642-5d69-1904-aa36e4297885/source/256x256bb.jpg",
|
||||
"alt_text": "Haunted hotel image"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"block_id": "section789",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Average Rating*\n1.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
16
zerver/webhooks/slack_incoming/fixtures/broken_image.json
Normal file
16
zerver/webhooks/slack_incoming/fixtures/broken_image.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Some image here"
|
||||
},
|
||||
"accessory": {
|
||||
"type": "image",
|
||||
"alt_text": "yay",
|
||||
"image_url": "null"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
91
zerver/webhooks/slack_incoming/fixtures/complicated.json
Normal file
91
zerver/webhooks/slack_incoming/fixtures/complicated.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "Hello from TaskBot",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Hey there 👋 I'm TaskBot. I'm here to help you create and manage tasks in Slack.\nThere are two ways to quickly create tasks:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*1️⃣ Use the `/task` command*. Type `/task` followed by a short description of your tasks and I'll ask for a due date (if applicable). Try it out by using the `/task` command in this channel."
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*2️⃣ Use the _Create a Task_ action.* If you want to create a task from a message, select `Create a Task` in a message's context menu. Try it out by selecting the _Create a Task_ action for this message (shown below)."
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"title": {
|
||||
"type": "plain_text",
|
||||
"text": "image1",
|
||||
"emoji": true
|
||||
},
|
||||
"image_url": "https://api.slack.com/img/blocks/bkb_template_images/onboardingComplex.jpg",
|
||||
"alt_text": "image1"
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "➕ To start tracking your team's tasks, *add me to a channel* and I'll introduce myself. I'm usually added to a team or project channel. Type `/invite @TaskBot` from the channel or pick a channel on the right."
|
||||
},
|
||||
"accessory": {
|
||||
"type": "conversations_select",
|
||||
"placeholder": {
|
||||
"type": "plain_text",
|
||||
"text": "Select a channel...",
|
||||
"emoji": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"element": {
|
||||
"type": "plain_text_input",
|
||||
"action_id": "plain_text_input-action"
|
||||
},
|
||||
"label": {
|
||||
"type": "plain_text",
|
||||
"text": "Try writing some task in here:",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "divider"
|
||||
},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{
|
||||
"type": "image",
|
||||
"image_url": "https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg",
|
||||
"alt_text": "cute cat"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "👀 View all tasks with `/task list`"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "❓Get help at any time with:\n- `/task help`, or\n- type *help* in a DM with me"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
3
zerver/webhooks/slack_incoming/fixtures/null_text.json
Normal file
3
zerver/webhooks/slack_incoming/fixtures/null_text.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"text": null
|
||||
}
|
||||
3
zerver/webhooks/slack_incoming/fixtures/text.json
Normal file
3
zerver/webhooks/slack_incoming/fixtures/text.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"text": "Hello, world."
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
payload=%7B%22username%22%3A%22DeployBot%22%2C%22icon_url%22%3A%22https%3A%2F%2Fraw.githubusercontent.com%2Fphallstrom%2Fslackistrano%2Fmaster%2Fimages%2Fslackistrano.png%22%2C%22icon_emoji%22%3A%22%3Azap%3A%22%2C%22text%22%3A%22chris+has+started+deploying+project+tag+v0.0.2rc10+to+staging%22%2C%22channel%22%3A%22%23devops%22%7D
|
||||
387
zerver/webhooks/slack_incoming/tests.py
Normal file
387
zerver/webhooks/slack_incoming/tests.py
Normal file
@@ -0,0 +1,387 @@
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.test_classes import WebhookTestCase
|
||||
|
||||
|
||||
class SlackIncomingHookTests(WebhookTestCase):
|
||||
def test_message(self) -> None:
|
||||
expected_topic_name = ""
|
||||
expected_message = """
|
||||
Hello, world.
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"text",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_message_formatting(self) -> None:
|
||||
tests = [
|
||||
("some *foo* word", "some **foo** word"),
|
||||
("*foo*", "**foo**"),
|
||||
("*foo* *bar*", "**foo** **bar**"),
|
||||
("*foo*a*bar*", "*foo*a*bar*"),
|
||||
("some _foo_ word", "some *foo* word"),
|
||||
]
|
||||
self.subscribe(self.test_user, self.channel_name)
|
||||
for input_value, output_value in tests:
|
||||
payload = {"text": input_value}
|
||||
msg = self.send_webhook_payload(
|
||||
self.test_user,
|
||||
self.url,
|
||||
payload,
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assert_channel_message(
|
||||
message=msg,
|
||||
channel_name=self.channel_name,
|
||||
topic_name="",
|
||||
content=output_value,
|
||||
)
|
||||
|
||||
def test_null_message(self) -> None:
|
||||
self.check_webhook(
|
||||
"null_text",
|
||||
expect_noop=True,
|
||||
)
|
||||
|
||||
def test_message_as_www_urlencoded(self) -> None:
|
||||
expected_topic_name = "devops"
|
||||
expected_message = """
|
||||
:zap: chris has started deploying project tag v0.0.2rc10 to staging
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"urlencoded_text",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
def test_message_without_payload(self) -> None:
|
||||
self.url = self.build_webhook_url()
|
||||
result = self.client_post(self.url)
|
||||
self.assertEqual(result.json()["error"], "Missing 'payload' argument")
|
||||
self.assertEqual(result.json()["ok"], False)
|
||||
|
||||
def test_message_with_actions(self) -> None:
|
||||
expected_topic_name = "C1H9RESGL"
|
||||
expected_message = """
|
||||
Danny Torrence left the following *review* for your property:
|
||||
|
||||
[Overlook Hotel](https://google.com) \n :star: \n Doors had too many axe holes, guest in room 237 was far too rowdy, whole place felt stuck in the 1920s.
|
||||
|
||||
[Haunted hotel image](https://is5-ssl.mzstatic.com/image/thumb/Purple3/v4/d3/72/5c/d3725c8f-c642-5d69-1904-aa36e4297885/source/256x256bb.jpg)
|
||||
|
||||
**Average Rating**
|
||||
1.0
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"actions",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_message_with_blocks(self) -> None:
|
||||
expected_topic_name = ""
|
||||
expected_message = """
|
||||
Danny Torrence left the following review for your property:
|
||||
|
||||
[Overlook Hotel](https://example.com) \n :star: \n Doors had too many axe holes, guest in room 237 was far too rowdy, whole place felt stuck in the 1920s.
|
||||
|
||||
[Haunted hotel image](https://is5-ssl.mzstatic.com/image/thumb/Purple3/v4/d3/72/5c/d3725c8f-c642-5d69-1904-aa36e4297885/source/256x256bb.jpg)
|
||||
|
||||
**Average Rating**
|
||||
1.0
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"blocks",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_message_with_attachment(self) -> None:
|
||||
expected_topic_name = "prometheus-alerts"
|
||||
expected_message = """
|
||||
## [[FIRING:2] InstanceDown for api-server (env="prod", severity="critical")](https://alertmanager.local//#/alerts?receiver=default)
|
||||
|
||||
:chart_with_upwards_trend: **[Graph](http://generator.local/1)** :notebook: **[Runbook](https://runbook.local/1)**
|
||||
|
||||
**Alert details**:
|
||||
**Alert:** api-server down - `critical`
|
||||
**Description:** api-server at 1.2.3.4:8080 couldn't be scraped **Details:**
|
||||
• **alertname:** `InstanceDown`
|
||||
• **env:** `prod`
|
||||
• **instance:** `1.2.3.4:8080`
|
||||
• **job:** `api-server`
|
||||
• **severity:** `critical`
|
||||
|
||||
**Alert:** api-server down - `critical`
|
||||
**Description:** api-server at 1.2.3.4:8081 couldn't be scraped **Details:**
|
||||
• **alertname:** `InstanceDown`
|
||||
• **env:** `prod`
|
||||
• **instance:** `1.2.3.4:8081`
|
||||
• **job:** `api-server`
|
||||
• **severity:** `critical`
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"attachment",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_complicated(self) -> None:
|
||||
# Paste the JSON into
|
||||
# https://api.slack.com/tools/block-kit-builder to see how it
|
||||
# is rendered in Slack
|
||||
expected_topic_name = ""
|
||||
expected_message = """
|
||||
## Hello from TaskBot
|
||||
|
||||
Hey there 👋 I'm TaskBot. I'm here to help you create and manage tasks in Slack.
|
||||
There are two ways to quickly create tasks:
|
||||
|
||||
**1️⃣ Use the `/task` command**. Type `/task` followed by a short description of your tasks and I'll ask for a due date (if applicable). Try it out by using the `/task` command in this channel.
|
||||
|
||||
**2️⃣ Use the *Create a Task* action.** If you want to create a task from a message, select `Create a Task` in a message's context menu. Try it out by selecting the *Create a Task* action for this message (shown below).
|
||||
|
||||
[image1](https://api.slack.com/img/blocks/bkb_template_images/onboardingComplex.jpg)
|
||||
|
||||
➕ To start tracking your team's tasks, **add me to a channel** and I'll introduce myself. I'm usually added to a team or project channel. Type `/invite @TaskBot` from the channel or pick a channel on the right.
|
||||
|
||||
----
|
||||
|
||||
[cute cat](https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg)
|
||||
|
||||
👀 View all tasks with `/task list`
|
||||
|
||||
❓Get help at any time with:
|
||||
- `/task help`, or
|
||||
- type **help** in a DM with me
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"complicated",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_attachment_blocks(self) -> None:
|
||||
# On https://api.slack.com/tools/block-kit-builder choose
|
||||
# "Attachment preview" and paste the JSON in.
|
||||
expected_topic_name = ""
|
||||
expected_message = """
|
||||
This is a section block with an accessory image.
|
||||
|
||||
[cute cat](https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg)
|
||||
|
||||
This is a section block with a button.
|
||||
|
||||
| | |
|
||||
|-|-|
|
||||
| one | two |
|
||||
| three | four |
|
||||
| five | |
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"attachment_blocks",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_attachment_fields(self) -> None:
|
||||
expected_topic_name = ""
|
||||
expected_message = """
|
||||
Build bla bla succeeded
|
||||
|
||||
**Requested by**: Some user
|
||||
**Duration**: 00:02:03
|
||||
**Build pipeline**: ConsumerAddressModule
|
||||
**Title with null value**
|
||||
**Title without value**
|
||||
Value with null title
|
||||
Value without title
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"attachment_fields",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_attachment_pieces(self) -> None:
|
||||
expected_topic_name = ""
|
||||
expected_message = """
|
||||
## Test
|
||||
|
||||
[](https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg)
|
||||
|
||||
<time:2022-06-23T00:48:26+00:00>
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"attachment_pieces",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
@override
|
||||
def get_body(self, fixture_name: str) -> str:
|
||||
if "urlencoded" in fixture_name:
|
||||
file_type = "txt"
|
||||
else:
|
||||
file_type = "json"
|
||||
return self.webhook_fixture_data("slack_incoming", fixture_name, file_type=file_type)
|
||||
|
||||
def test_attachment_pieces_title_null(self) -> None:
|
||||
expected_topic_name = ""
|
||||
expected_message = """
|
||||
Sample pretext.
|
||||
|
||||
Sample text.
|
||||
|
||||
[](https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg)
|
||||
|
||||
Sample footer.
|
||||
|
||||
<time:2022-06-23T00:48:26+00:00>
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"attachment_pieces_title_null",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_attachment_pieces_image_url_null(self) -> None:
|
||||
expected_topic_name = ""
|
||||
expected_message = """
|
||||
## [Sample title.](https://www.google.com)
|
||||
|
||||
Sample pretext.
|
||||
|
||||
Sample text.
|
||||
|
||||
Sample footer.
|
||||
|
||||
<time:2022-06-23T00:48:26+00:00>
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"attachment_pieces_image_url_null",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_attachment_pieces_ts_null(self) -> None:
|
||||
expected_topic_name = ""
|
||||
expected_message = """
|
||||
## [Sample title.](https://www.google.com)
|
||||
|
||||
Sample pretext.
|
||||
|
||||
Sample text.
|
||||
|
||||
[](https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg)
|
||||
|
||||
Sample footer.
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"attachment_pieces_ts_null",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_attachment_pieces_text_null(self) -> None:
|
||||
expected_topic_name = ""
|
||||
expected_message = """
|
||||
## [Sample title.](https://www.google.com)
|
||||
|
||||
Sample pretext.
|
||||
|
||||
[](https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg)
|
||||
|
||||
Sample footer.
|
||||
|
||||
<time:2022-06-23T00:48:26+00:00>
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"attachment_pieces_text_null",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_attachment_pieces_pretext_null(self) -> None:
|
||||
expected_topic_name = ""
|
||||
expected_message = """
|
||||
## [Sample title.](https://www.google.com)
|
||||
|
||||
Sample text.
|
||||
|
||||
[](https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg)
|
||||
|
||||
Sample footer.
|
||||
|
||||
<time:2022-06-23T00:48:26+00:00>
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"attachment_pieces_pretext_null",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_attachment_pieces_footer_null(self) -> None:
|
||||
expected_topic_name = ""
|
||||
expected_message = """
|
||||
## [Sample title.](https://www.google.com)
|
||||
|
||||
Sample pretext.
|
||||
|
||||
Sample text.
|
||||
|
||||
[](https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg)
|
||||
|
||||
<time:2022-06-23T00:48:26+00:00>
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"attachment_pieces_footer_null",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_attachment_pieces_title_link_null(self) -> None:
|
||||
expected_topic_name = ""
|
||||
expected_message = """
|
||||
## Sample title.
|
||||
|
||||
Sample pretext.
|
||||
|
||||
Sample text.
|
||||
|
||||
[](https://pbs.twimg.com/profile_images/625633822235693056/lNGUneLX_400x400.jpg)
|
||||
|
||||
Sample footer.
|
||||
|
||||
<time:2022-06-23T00:48:26+00:00>
|
||||
""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"attachment_pieces_title_link_null",
|
||||
expected_topic_name,
|
||||
expected_message,
|
||||
)
|
||||
|
||||
def test_attachment_pieces_all_null(self) -> None:
|
||||
self.check_webhook("attachment_pieces_all_null", expect_noop=True)
|
||||
|
||||
def test_broken_image(self) -> None:
|
||||
self.check_webhook("broken_image", "", "Some image here")
|
||||
107
zerver/webhooks/slack_incoming/view.py
Normal file
107
zerver/webhooks/slack_incoming/view.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# Webhooks for external integrations.
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import Concatenate
|
||||
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from zerver.data_import.slack_message_conversion import (
|
||||
LossyConversionError,
|
||||
convert_slack_formatting,
|
||||
render_attachment,
|
||||
render_block,
|
||||
replace_links,
|
||||
)
|
||||
from zerver.decorator import webhook_view
|
||||
from zerver.lib.exceptions import JsonableError
|
||||
from zerver.lib.request import RequestVariableMissingError
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.typed_endpoint import typed_endpoint
|
||||
from zerver.lib.validator import check_string, to_wild_value
|
||||
from zerver.lib.webhooks.common import OptionalUserSpecifiedTopicStr, check_send_webhook_message
|
||||
from zerver.models import UserProfile
|
||||
|
||||
ParamT = ParamSpec("ParamT")
|
||||
|
||||
|
||||
def slack_error_handler(
|
||||
view_func: Callable[Concatenate[HttpRequest, ParamT], HttpResponse],
|
||||
) -> Callable[Concatenate[HttpRequest, ParamT], HttpResponse]:
|
||||
"""
|
||||
A decorator that catches JsonableError exceptions and returns a
|
||||
Slack-compatible error response in the format:
|
||||
{ok: false, error: "error message"}.
|
||||
"""
|
||||
|
||||
@wraps(view_func)
|
||||
def wrapped_view(
|
||||
request: HttpRequest, /, *args: ParamT.args, **kwargs: ParamT.kwargs
|
||||
) -> HttpResponse:
|
||||
try:
|
||||
return view_func(request, *args, **kwargs)
|
||||
except JsonableError as error:
|
||||
return JsonResponse({"ok": False, "error": error.msg}, status=error.http_status_code)
|
||||
|
||||
return wrapped_view
|
||||
|
||||
|
||||
@webhook_view("SlackIncoming")
|
||||
@typed_endpoint
|
||||
@slack_error_handler
|
||||
def api_slack_incoming_webhook(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
user_specified_topic: OptionalUserSpecifiedTopicStr = None,
|
||||
) -> HttpResponse:
|
||||
# Slack accepts webhook payloads as payload="encoded json" as
|
||||
# application/x-www-form-urlencoded, as well as in the body as
|
||||
# application/json.
|
||||
if request.content_type == "application/json":
|
||||
try:
|
||||
val = request.body.decode(request.encoding or "utf-8")
|
||||
except UnicodeDecodeError: # nocoverage
|
||||
raise JsonableError(_("Malformed payload"))
|
||||
else:
|
||||
req_var = "payload"
|
||||
if req_var in request.POST:
|
||||
val = request.POST[req_var]
|
||||
elif req_var in request.GET: # nocoverage
|
||||
val = request.GET[req_var]
|
||||
else:
|
||||
raise RequestVariableMissingError(req_var)
|
||||
|
||||
payload = to_wild_value("payload", val)
|
||||
|
||||
if user_specified_topic is None and "channel" in payload:
|
||||
channel = payload["channel"].tame(check_string)
|
||||
user_specified_topic = re.sub(r"^[@#]", "", channel)
|
||||
|
||||
if user_specified_topic is None:
|
||||
user_specified_topic = "(no topic)"
|
||||
|
||||
pieces: list[str] = []
|
||||
if payload.get("blocks"):
|
||||
try:
|
||||
pieces += map(render_block, payload["blocks"])
|
||||
except LossyConversionError: # nocoverage
|
||||
pieces.append(payload.get("text", "").tame(check_string))
|
||||
|
||||
if payload.get("attachments"):
|
||||
pieces += map(render_attachment, payload["attachments"])
|
||||
|
||||
body = "\n\n".join(piece.strip() for piece in pieces if piece.strip() != "")
|
||||
|
||||
if body == "" and payload.get("text"):
|
||||
if payload.get("icon_emoji"):
|
||||
body = payload["icon_emoji"].tame(check_string) + " "
|
||||
body += payload["text"].tame(check_string)
|
||||
body = body.strip()
|
||||
|
||||
if body != "":
|
||||
body = convert_slack_formatting(replace_links(body).strip())
|
||||
check_send_webhook_message(request, user_profile, user_specified_topic, body)
|
||||
return json_success(request, data={"ok": True})
|
||||
Reference in New Issue
Block a user