Repository: zulip/zulip

Issue:
integrations: Add Uptime Robot integration.
Add Uptime Robot Integration.

Uptime Robot webhook informs users if their site is up or down, so give appropriate message for these two events.

Fixes #13854.


Discussion:

Resolving patch:
diff --git a/zerver/lib/integrations.py b/zerver/lib/integrations.py
--- a/zerver/lib/integrations.py
+++ b/zerver/lib/integrations.py
@@ -445,6 +445,7 @@ def __init__(self, name: str, *args: Any, **kwargs: Any) -> None:
     WebhookIntegration("travis", ["continuous-integration"], display_name="Travis CI"),
     WebhookIntegration("trello", ["project-management"]),
     WebhookIntegration("updown", ["monitoring"]),
+    WebhookIntegration("uptimerobot", ["monitoring"], display_name="Uptime Robot"),
     WebhookIntegration(
         "yo",
         ["communication"],
@@ -781,6 +782,7 @@ def __init__(self, name: str, *args: Any, **kwargs: Any) -> None:
     "travis": [ScreenshotConfig("build.json", payload_as_query_param=True)],
     "trello": [ScreenshotConfig("adding_comment_to_card.json")],
     "updown": [ScreenshotConfig("check_multiple_events.json")],
+    "uptimerobot": [ScreenshotConfig("uptimerobot_monitor_up.json")],
     "wordpress": [ScreenshotConfig("publish_post.txt", "wordpress_post_created.png")],
     "yo": [
         ScreenshotConfig(
diff --git a/zerver/webhooks/uptimerobot/__init__.py b/zerver/webhooks/uptimerobot/__init__.py
new file mode 100644
diff --git a/zerver/webhooks/uptimerobot/view.py b/zerver/webhooks/uptimerobot/view.py
new file mode 100644
--- /dev/null
+++ b/zerver/webhooks/uptimerobot/view.py
@@ -0,0 +1,65 @@
+from typing import Any, Dict
+
+from django.http import HttpRequest, HttpResponse
+from django.utils.translation import ugettext as _
+
+from zerver.decorator import REQ, has_request_variables, webhook_view
+from zerver.lib.actions import send_rate_limited_pm_notification_to_bot_owner
+from zerver.lib.response import json_error, json_success
+from zerver.lib.send_email import FromAddress
+from zerver.lib.webhooks.common import check_send_webhook_message
+from zerver.models import UserProfile
+
+MISCONFIGURED_PAYLOAD_ERROR_MESSAGE = """
+Hi there! Your bot {bot_name} just received a Uptime Robot payload that is missing
+some data that Zulip requires. This usually indicates a configuration issue
+in your Uptime Robot webhook settings. Please make sure that you set the required parameters
+when configuring the Uptime Robot webhook. Contact {support_email} if you
+need further help!
+"""
+
+UPTIMEROBOT_TOPIC_TEMPLATE = "{monitor_friendly_name}"
+UPTIMEROBOT_MESSAGE_UP_TEMPLATE = """
+{monitor_friendly_name} ({monitor_url}) is back UP ({alert_details}).
+It was down for {alert_friendly_duration}.
+""".strip()
+UPTIMEROBOT_MESSAGE_DOWN_TEMPLATE = (
+    "{monitor_friendly_name} ({monitor_url}) is DOWN ({alert_details})."
+)
+
+
+@webhook_view("UptimeRobot")
+@has_request_variables
+def api_uptimerobot_webhook(
+    request: HttpRequest,
+    user_profile: UserProfile,
+    payload: Dict[str, Any] = REQ(argument_type="body"),
+) -> HttpResponse:
+
+    try:
+        body = get_body_for_http_request(payload)
+        subject = get_subject_for_http_request(payload)
+    except KeyError:
+        message = MISCONFIGURED_PAYLOAD_ERROR_MESSAGE.format(
+            bot_name=user_profile.full_name,
+            support_email=FromAddress.SUPPORT,
+        ).strip()
+        send_rate_limited_pm_notification_to_bot_owner(user_profile, user_profile.realm, message)
+
+        return json_error(_("Invalid payload"))
+
+    check_send_webhook_message(request, user_profile, subject, body)
+    return json_success()
+
+
+def get_subject_for_http_request(payload: Dict[str, Any]) -> str:
+    return UPTIMEROBOT_TOPIC_TEMPLATE.format(monitor_friendly_name=payload["monitor_friendly_name"])
+
+
+def get_body_for_http_request(payload: Dict[str, Any]) -> str:
+    if payload["alert_type_friendly_name"] == "Up":
+        body = UPTIMEROBOT_MESSAGE_UP_TEMPLATE.format(**payload)
+    elif payload["alert_type_friendly_name"] == "Down":
+        body = UPTIMEROBOT_MESSAGE_DOWN_TEMPLATE.format(**payload)
+
+    return body


Test patch:
diff --git a/zerver/webhooks/uptimerobot/tests.py b/zerver/webhooks/uptimerobot/tests.py
new file mode 100644
--- /dev/null
+++ b/zerver/webhooks/uptimerobot/tests.py
@@ -0,0 +1,47 @@
+from zerver.lib.send_email import FromAddress
+from zerver.lib.test_classes import WebhookTestCase
+from zerver.models import Recipient
+from zerver.webhooks.uptimerobot.view import MISCONFIGURED_PAYLOAD_ERROR_MESSAGE
+
+
+class UptimeRobotHookTests(WebhookTestCase):
+    STREAM_NAME = "uptimerobot"
+    URL_TEMPLATE = "/api/v1/external/uptimerobot?stream={stream}&api_key={api_key}"
+    FIXTURE_DIR_NAME = "uptimerobot"
+
+    def test_uptimerobot_monitor_down(self) -> None:
+        """
+        Tests if uptimerobot monitor down is handled correctly
+        """
+        expected_topic = "Web Server"
+        expected_message = "Web Server (server1.example.com) is DOWN (Host Is Unreachable)."
+        self.check_webhook("uptimerobot_monitor_down", expected_topic, expected_message)
+
+    def test_uptimerobot_monitor_up(self) -> None:
+        """
+        Tests if uptimerobot monitor up is handled correctly
+        """
+        expected_topic = "Mail Server"
+        expected_message = """
+Mail Server (server2.example.com) is back UP (Host Is Reachable).
+It was down for 44 minutes and 37 seconds.
+""".strip()
+        self.check_webhook("uptimerobot_monitor_up", expected_topic, expected_message)
+
+    def test_uptimerobot_invalid_payload_with_missing_data(self) -> None:
+        """
+        Tests if invalid uptime robot payloads are handled correctly
+        """
+        self.url = self.build_webhook_url()
+        payload = self.get_body("uptimerobot_invalid_payload_with_missing_data")
+        result = self.client_post(self.url, payload, content_type="application/json")
+        self.assert_json_error(result, "Invalid payload")
+
+        expected_message = MISCONFIGURED_PAYLOAD_ERROR_MESSAGE.format(
+            bot_name=self.test_user.full_name,
+            support_email=FromAddress.SUPPORT,
+        ).strip()
+
+        msg = self.get_last_message()
+        self.assertEqual(msg.content, expected_message)
+        self.assertEqual(msg.recipient.type, Recipient.PERSONAL)