mirror of https://github.com/zulip/zulip.git
outgoing webhooks: Fix inconsistencies with Slack's API.
Apparently, our slack compatible outgoing webhook format didn't exactly match Slack, especially in the types used for values. Fix this by using a much more consistent format, where we preserve their pattern of prefixing IDs with letters. This fixes a bug where Zulip's team_id could be the empty string, which tripped up using GitLab's slash commands with Zulip. Fixes #19588.
This commit is contained in:
parent
c233ee9935
commit
417c32629d
|
@ -116,27 +116,31 @@ Here's how we fill in the fields that a Slack-format webhook expects:
|
|||
</tr>
|
||||
<tr>
|
||||
<td><code>team_id</code></td>
|
||||
<td>String ID of the Zulip organization</td>
|
||||
<td>ID of the Zulip organization prefixed by "T".</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>team_domain</code></td>
|
||||
<td>Domain of the Zulip organization</td>
|
||||
<td>Hostname of the Zulip organization</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>channel_id</code></td>
|
||||
<td>Stream ID</td>
|
||||
<td>Stream ID prefixed by "C"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>channel_name</code></td>
|
||||
<td>Stream name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>thread_ts</code></td>
|
||||
<td>Timestamp for when message was sent</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>timestamp</code></td>
|
||||
<td>Timestamp for when message was sent</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>user_id</code></td>
|
||||
<td>ID of the user who sent the message</td>
|
||||
<td>ID of the user who sent the message prefixed by "U"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>user_name</code></td>
|
||||
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue