slack-integration: Update Slack integration to handle Events API.

This updates the Slack webhook integration to handle the Slack Events
API[1], while maintaining backwards compatibility with Slack's legacy
Outgoing Webhook service.

The Events API introduces the "challenge" handshake[2] to verify and
add a new webhook URL for them to call. This commit adds a handler for
the challenge handshake.

Additionally, this commit reformats incoming payloads using the Slack
text reformatter from `slack_message_conversion.py`. There is some
duplicative code here because of the difference in Slack export data
and Slack's webhook payload.

Part of #30465

[1]: https://api.slack.com/apis/events-api#using-events-api
[2]: https://api.slack.com/apis/events-api#handshake

(cherry picked from commit f29312ce03)
This commit is contained in:
PieterCK
2024-08-14 11:50:59 +07:00
committed by Tim Abbott
parent 5e3030b072
commit 148f7cde6c
17 changed files with 1604 additions and 49 deletions

View File

@@ -806,7 +806,7 @@ DOC_SCREENSHOT_CONFIG: dict[str, list[BaseScreenshotConfig]] = {
ScreenshotConfig("event_for_exception_python.json"),
ScreenshotConfig("issue_assigned_to_team.json", "002.png"),
],
"slack": [ScreenshotConfig("message_info.txt")],
"slack": [ScreenshotConfig("message_with_normal_text.json")],
"sonarqube": [ScreenshotConfig("error.json")],
"sonarr": [ScreenshotConfig("sonarr_episode_grabbed.json")],
"splunk": [ScreenshotConfig("search_one_result.json")],

View File

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

View File

@@ -0,0 +1,64 @@
{
"token": "CqCsBJmXoNSHaNCt3wGWYSEe",
"team_id": "T06NRA6HM3P",
"context_team_id": "T06NRA6HM3P",
"context_enterprise_id": null,
"api_app_id": "A0757RM31HN",
"event": {
"user": "U074G5E1ANR",
"type": "message",
"ts": "1718856814.111759",
"bot_id": "B074K2M1NF4",
"app_id": "A0757RM31HN",
"text": "<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"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,14 @@ from typing_extensions import override
from zerver.lib.test_classes import WebhookTestCase
EXPECTED_TOPIC = "Message from Slack"
EXPECTED_MESSAGE = "**slack_user**: test"
MESSAGE_WITH_NORMAL_TEXT = "Hello, this is a normal text message"
USER = "U06NU4E26M9"
CHANNEL = "C06NRA6JLER"
EXPECTED_MESSAGE = "**{user}**: {message}"
TOPIC_WITH_CHANNEL = "channel: {channel}"
LEGACY_USER = "slack_user"
class SlackWebhookTests(WebhookTestCase):
@@ -12,50 +19,270 @@ class SlackWebhookTests(WebhookTestCase):
WEBHOOK_DIR_NAME = "slack"
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",
)
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",
)
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=CHANNEL)
self.check_webhook(
"message_with_normal_text",
expected_topic_name,
expected_message,
content_type="application/json",
)
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",
)
def test_slack_channels_map_to_topics_false(self) -> None:
self.CHANNEL_NAME = CHANNEL
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",
)
def test_slack_channels_map_to_topics_false_and_user_specified_topic(self) -> None:
self.CHANNEL_NAME = CHANNEL
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",
)
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")
def test_challenge_handshake_payload(self) -> None:
url = self.build_webhook_url(channels_map_to_topics="1")
payload = self.get_body("challenge_handshake_payload")
result = self.client_post(url, payload, content_type="application/json")
expected_challenge_response = {
"msg": "",
"result": "success",
"challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
}
self.assertJSONEqual(result.content.decode("utf-8"), expected_challenge_response)
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,
)
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",
)
def test_message_with_channel_and_user_mentions(self) -> None:
# TODO
pass
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",
)
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",
)
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",
)
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",
)
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",
)
def test_message_with_user_mentions(self) -> None:
# TODO
message_body = (
"<@U074VRHQ11T> <@U074G5E1ANR> <@U06NU4E26M9> 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",
)
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",
)
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",
)
def test_message_from_slack_integration_bot(self) -> None:
self.check_webhook(
"message_from_slack_integration_bot",
"",
"",
content_type="application/json",
expect_noop=True,
)
class SlackLegacyWebhookTests(WebhookTestCase):
CHANNEL_NAME = "slack"
URL_TEMPLATE = "/api/v1/external/slack?stream={stream}&api_key={api_key}"
WEBHOOK_DIR_NAME = "slack"
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,
EXPECTED_MESSAGE,
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,
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,
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,
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,
EXPECTED_MESSAGE,
expected_topic_name,
expected_message,
content_type="application/x-www-form-urlencoded",
)
@@ -63,10 +290,11 @@ class SlackWebhookTests(WebhookTestCase):
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,
expected_message,
content_type="application/x-www-form-urlencoded",
)
@@ -88,12 +316,6 @@ class SlackWebhookTests(WebhookTestCase):
result = self.client_post(url, payload, content_type="application/x-www-form-urlencoded")
self.assert_json_error(result, "Missing 'text' argument")
def test_invalid_channels_map_to_topics(self) -> None:
payload = self.get_body("message_info")
url = self.build_webhook_url(channels_map_to_topics="abc")
result = self.client_post(url, payload, content_type="application/x-www-form-urlencoded")
self.assert_json_error(result, "Error: channels_map_to_topics parameter other than 0 or 1")
@override
def get_body(self, fixture_name: str) -> str:
return self.webhook_fixture_data("slack", fixture_name, file_type="txt")

View File

@@ -1,17 +1,148 @@
import re
from typing import TypeAlias
from django.http import HttpRequest
from django.http.response import HttpResponse
from django.utils.translation import gettext as _
from zerver.data_import.slack_message_conversion import (
SLACK_BOLD_REGEX,
SLACK_ITALIC_REGEX,
SLACK_STRIKETHROUGH_REGEX,
SLACK_USERMENTION_REGEX,
convert_link_format,
convert_mailto_format,
convert_markdown_syntax,
)
from zerver.decorator import webhook_view
from zerver.lib.exceptions import JsonableError
from zerver.lib.exceptions import JsonableError, UnsupportedWebhookEventTypeError
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 WildValue, check_none_or, check_string, to_wild_value
from zerver.lib.webhooks.common import check_send_webhook_message
from zerver.models import UserProfile
ZULIP_MESSAGE_TEMPLATE = "**{message_sender}**: {text}"
FILE_LINK_TEMPLATE = "\n*[{file_name}]({file_link})*"
ZULIP_MESSAGE_TEMPLATE = "**{sender}**: {text}"
VALID_OPTIONS = {"SHOULD_NOT_BE_MAPPED": "0", "SHOULD_BE_MAPPED": "1"}
SlackFileListT: TypeAlias = list[dict[str, str]]
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 convert_slack_user_and_channel_mentions(text: 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:
# TODO: Callback to Slack API
pass
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_name = channel_info[1]
tokens[iterator] = f"**#{channel_name}**" if channel_name else "**#[private channel]**"
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) -> str:
text = convert_slack_formatting(text)
text = convert_slack_workspace_mentions(text)
text = convert_slack_user_and_channel_mentions(text)
return text
def convert_slack_formatting(text: str) -> str:
text = convert_markdown_syntax(text, SLACK_BOLD_REGEX, "**")
text = convert_markdown_syntax(text, SLACK_STRIKETHROUGH_REGEX, "~~")
text = convert_markdown_syntax(text, SLACK_ITALIC_REGEX, "*")
return text
def convert_slack_workspace_mentions(text: str) -> str:
# Map Slack's <!everyone>, <!channel> and <!here> mentions to @**all**.
# No regex for this as it can be present anywhere in the sentence.
text = text.replace("<!everyone>", "@**all**")
text = text.replace("<!channel>", "@**all**")
text = text.replace("<!here>", "@**all**")
return text
def replace_links(text: str) -> str:
text, _ = convert_link_format(text)
text, _ = convert_mailto_format(text)
return text
def convert_raw_file_data(file_dict: WildValue) -> SlackFileListT:
files = [
{
"file_link": file.get("permalink").tame(check_string),
"file_name": file.get("title").tame(check_string),
}
for file in file_dict
]
return files
def get_message_body(text: str, sender: str, files: SlackFileListT) -> str:
# TODO: Reformat mentions in messages, currently its <@USER_ID>.
body = ZULIP_MESSAGE_TEMPLATE.format(sender=sender, text=text)
for file in files:
body += FILE_LINK_TEMPLATE.format(**file)
return body
def is_challenge_handshake(payload: WildValue) -> bool:
return payload.get("type").tame(check_string) == "url_verification"
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"))
@webhook_view("Slack", notify_bot_owner_on_invalid_json=False)
@typed_endpoint
@@ -19,42 +150,77 @@ def api_slack_webhook(
request: HttpRequest,
user_profile: UserProfile,
*,
user_name: str,
text: str,
channel_name: str,
channels_map_to_topics: str | None = None,
) -> HttpResponse:
content = ZULIP_MESSAGE_TEMPLATE.format(message_sender=user_name, text=text)
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"]:
# If the webhook URL has a user_specified_topic,
# then this topic-channel mapping will not be used.
topic_name = f"channel: {channel_name}"
check_send_webhook_message(
request,
user_profile,
topic_name,
content,
)
elif channels_map_to_topics == VALID_OPTIONS["SHOULD_NOT_BE_MAPPED"]:
# This channel-channel mapping will be used even if
# there is a channel specified in the webhook URL.
check_send_webhook_message(
request,
user_profile,
topic_name,
content,
stream=channel_name,
)
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 JsonableError(_("Error: channels_map_to_topics parameter other than 0 or 1"))
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)
# Handle initial URL verification handshake for Slack Events API.
if is_challenge_handshake(payload):
challenge = payload.get("challenge").tame(check_string)
check_send_webhook_message(
request,
user_profile,
"Integration events",
"Successfully verified webhook URL with Slack!",
)
return json_success(request=request, data={"challenge": challenge})
# 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)
event_dict = payload.get("event", {})
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 = event_dict.get("text", "").tame(check_string)
text = convert_to_zulip_markdown(raw_text)
sender = event_dict.get("user").tame(check_none_or(check_string))
if sender is None:
# This is likely a Slack integration bot message. The sender of these
# messages doesn't have a user profile and would require additional
# formatting to handle. Refer to the Slack Incoming Webhook integration
# for how to add support for this type of payload.
raise UnsupportedWebhookEventTypeError(
"integration bot message"
if event_dict["subtype"] == "bot_message"
else "unknown Slack event"
)
content = get_message_body(text, sender, files)
channel = event_dict.get("channel").tame(check_string) if channels_map_to_topics else None
handle_slack_webhook_message(request, user_profile, content, channel, channels_map_to_topics)
return json_success(request)