diff --git a/templates/zerver/api/outgoing-webhooks.md b/templates/zerver/api/outgoing-webhooks.md index 87e0e0d8ed..a35ed40600 100644 --- a/templates/zerver/api/outgoing-webhooks.md +++ b/templates/zerver/api/outgoing-webhooks.md @@ -116,27 +116,31 @@ Here's how we fill in the fields that a Slack-format webhook expects: team_id - String ID of the Zulip organization + ID of the Zulip organization prefixed by "T". team_domain - Domain of the Zulip organization + Hostname of the Zulip organization channel_id - Stream ID + Stream ID prefixed by "C" channel_name Stream name + + thread_ts + Timestamp for when message was sent + timestamp Timestamp for when message was sent user_id - ID of the user who sent the message + ID of the user who sent the message prefixed by "U" user_name @@ -161,13 +165,14 @@ The above data is posted as list of tuples (not JSON), here's an example: ``` [('token', 'v9fpCdldZIej2bco3uoUvGp06PowKFOf'), - ('team_id', 'zulip'), - ('team_domain', 'zulip.com'), - ('channel_id', '123'), + ('team_id', 'T1512'), + ('team_domain', 'zulip.example.com'), + ('channel_id', 'C123'), ('channel_name', 'integrations'), + ('thread_ts', 1532078950), ('timestamp', 1532078950), - ('user_id', 21), - ('user_name', 'Sample User'), + ('user_id', 'U21'), + ('user_name', 'Full Name'), ('text', '@**test**'), ('trigger_word', 'mention'), ('service_id', 27)] diff --git a/zerver/lib/outgoing_webhook.py b/zerver/lib/outgoing_webhook.py index a1ea236989..c553f367cd 100644 --- a/zerver/lib/outgoing_webhook.py +++ b/zerver/lib/outgoing_webhook.py @@ -20,9 +20,9 @@ from zerver.lib.url_encoding import near_message_url from zerver.models import ( GENERIC_INTERFACE, SLACK_INTERFACE, + Realm, Service, UserProfile, - email_to_domain, get_client, get_user_profile_by_id, ) @@ -40,7 +40,9 @@ class OutgoingWebhookServiceInterface(metaclass=abc.ABCMeta): ) @abc.abstractmethod - def make_request(self, base_url: str, event: Dict[str, Any]) -> Optional[Response]: + def make_request( + self, base_url: str, event: Dict[str, Any], realm: Realm + ) -> Optional[Response]: raise NotImplementedError @abc.abstractmethod @@ -49,7 +51,9 @@ class OutgoingWebhookServiceInterface(metaclass=abc.ABCMeta): class GenericOutgoingWebhookService(OutgoingWebhookServiceInterface): - def make_request(self, base_url: str, event: Dict[str, Any]) -> Optional[Response]: + def make_request( + self, base_url: str, event: Dict[str, Any], realm: Realm + ) -> Optional[Response]: """ We send a simple version of the message to outgoing webhooks, since most of them really only need @@ -98,20 +102,38 @@ class GenericOutgoingWebhookService(OutgoingWebhookServiceInterface): class SlackOutgoingWebhookService(OutgoingWebhookServiceInterface): - def make_request(self, base_url: str, event: Dict[str, Any]) -> Optional[Response]: + def make_request( + self, base_url: str, event: Dict[str, Any], realm: Realm + ) -> Optional[Response]: if event["message"]["type"] == "private": failure_message = "Slack outgoing webhooks don't support private messages." fail_with_message(event, failure_message) return None + # https://api.slack.com/legacy/custom-integrations/outgoing-webhooks#legacy-info__post-data + # documents the Slack outgoing webhook format: + # + # token=XXXXXXXXXXXXXXXXXX + # team_id=T0001 + # team_domain=example + # channel_id=C2147483705 + # channel_name=test + # thread_ts=1504640714.003543 + # timestamp=1504640775.000005 + # user_id=U2147483697 + # user_name=Steve + # text=googlebot: What is the air-speed velocity of an unladen swallow? + # trigger_word=googlebot: + request_data = [ ("token", self.token), - ("team_id", event["message"]["sender_realm_str"]), - ("team_domain", email_to_domain(event["message"]["sender_email"])), - ("channel_id", event["message"]["stream_id"]), + ("team_id", f"T{realm.id}"), + ("team_domain", realm.host), + ("channel_id", f"C{event['message']['stream_id']}"), ("channel_name", event["message"]["display_recipient"]), + ("thread_ts", event["message"]["timestamp"]), ("timestamp", event["message"]["timestamp"]), - ("user_id", event["message"]["sender_id"]), + ("user_id", f"U{event['message']['sender_id']}"), ("user_name", event["message"]["sender_full_name"]), ("text", event["command"]), ("trigger_word", event["trigger"]), @@ -326,11 +348,12 @@ def do_rest_call( """Returns response of call if no exception occurs.""" try: start_time = perf_counter() + bot_profile = service_handler.user_profile response = service_handler.make_request( base_url, event, + bot_profile.realm, ) - bot_profile = service_handler.user_profile logging.info( "Outgoing webhook request from %s@%s took %f seconds", bot_profile.id, diff --git a/zerver/tests/test_outgoing_webhook_interfaces.py b/zerver/tests/test_outgoing_webhook_interfaces.py index e0ef231af6..64e371971b 100644 --- a/zerver/tests/test_outgoing_webhook_interfaces.py +++ b/zerver/tests/test_outgoing_webhook_interfaces.py @@ -107,6 +107,7 @@ class TestGenericOutgoingWebhookService(ZulipTestCase): self.handler.make_request( test_url, event, + othello.realm, ) session.post.assert_called_once() self.assertEqual(session.post.call_args[0], (test_url,)) @@ -150,6 +151,7 @@ class TestGenericOutgoingWebhookService(ZulipTestCase): class TestSlackOutgoingWebhookService(ZulipTestCase): def setUp(self) -> None: super().setUp() + self.bot_user = get_user("outgoing-webhook@zulip.com", get_realm("zulip")) self.stream_message_event = { "command": "@**test**", "user_profile_id": 12, @@ -187,7 +189,9 @@ class TestSlackOutgoingWebhookService(ZulipTestCase): } service_class = get_service_interface_class(SLACK_INTERFACE) - self.handler = service_class(token="abcdef", user_profile=None, service_name="test-service") + self.handler = service_class( + token="abcdef", user_profile=self.bot_user, service_name="test-service" + ) def test_make_request_stream_message(self) -> None: test_url = "https://example.com/example" @@ -195,22 +199,24 @@ class TestSlackOutgoingWebhookService(ZulipTestCase): self.handler.make_request( test_url, self.stream_message_event, + self.bot_user.realm, ) session.post.assert_called_once() self.assertEqual(session.post.call_args[0], (test_url,)) request_data = session.post.call_args[1]["data"] self.assertEqual(request_data[0][1], "abcdef") # token - self.assertEqual(request_data[1][1], "zulip") # team_id - self.assertEqual(request_data[2][1], "zulip.com") # team_domain - self.assertEqual(request_data[3][1], "123") # channel_id + self.assertEqual(request_data[1][1], "T2") # team_id + self.assertEqual(request_data[2][1], "zulip.testserver") # team_domain + self.assertEqual(request_data[3][1], "C123") # channel_id self.assertEqual(request_data[4][1], "integrations") # channel_name - self.assertEqual(request_data[5][1], 123456) # timestamp - self.assertEqual(request_data[6][1], 21) # user_id - self.assertEqual(request_data[7][1], "Sample User") # user_name - self.assertEqual(request_data[8][1], "@**test**") # text - self.assertEqual(request_data[9][1], "mention") # trigger_word - self.assertEqual(request_data[10][1], 12) # user_profile_id + self.assertEqual(request_data[5][1], 123456) # thread_id + self.assertEqual(request_data[6][1], 123456) # timestamp + self.assertEqual(request_data[7][1], "U21") # user_id + self.assertEqual(request_data[8][1], "Sample User") # user_name + self.assertEqual(request_data[9][1], "@**test**") # text + self.assertEqual(request_data[10][1], "mention") # trigger_word + self.assertEqual(request_data[11][1], 12) # user_profile_id @mock.patch("zerver.lib.outgoing_webhook.fail_with_message") def test_make_request_private_message(self, mock_fail_with_message: mock.Mock) -> None: @@ -219,6 +225,7 @@ class TestSlackOutgoingWebhookService(ZulipTestCase): response = self.handler.make_request( test_url, self.private_message_event, + self.bot_user.realm, ) session.post.assert_not_called() self.assertIsNone(response)