Skip to content

Commit

Permalink
Add webex as destination (#5574)
Browse files Browse the repository at this point in the history
* Add webex as destination

* import from destinations explicitly

* make format

* remove unattributed image

* make webex bot token required

* don't use magic string

* add metadata kwarg

* simplify link creation

* simplify alert description

* simplify alert subject

* split attachments template into method

* DRY message posting

* use api endpoint method

* add missing param to post_message

* static method

* static method attachments template

* log exception if send fails

* simplify destination handling

* transparent image / right size webex logo

* remove unused organization param

* rename api endpoint and make it a property

* add test

---------

Co-authored-by: Justin Clift <justin@postgresql.org>
Co-authored-by: Guido Petri <18634426+guidopetri@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 5, 2023
1 parent f1d5ac0 commit c2e7df0
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 0 deletions.
Binary file added client/app/assets/images/destinations/webex.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
138 changes: 138 additions & 0 deletions redash/destinations/webex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import logging
from copy import deepcopy

import requests

from redash.destinations import BaseDestination, register
from redash.models import Alert


class Webex(BaseDestination):
@classmethod
def configuration_schema(cls):
return {
"type": "object",
"properties": {
"webex_bot_token": {"type": "string", "title": "Webex Bot Token"},
"to_person_emails": {
"type": "string",
"title": "People (comma-separated)",
},
"to_room_ids": {
"type": "string",
"title": "Rooms (comma-separated)",
},
},
"secret": ["webex_bot_token"],
"required": ["webex_bot_token"],
}

@classmethod
def icon(cls):
return "fa-webex"

@property
def api_base_url(self):
return "https://webexapis.com/v1/messages"

@staticmethod
def formatted_attachments_template(subject, description, query_link, alert_link):
return [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": 4,
"items": [
{
"type": "TextBlock",
"text": {subject},
"weight": "bolder",
"size": "medium",
"wrap": True,
},
{
"type": "TextBlock",
"text": {description},
"isSubtle": True,
"wrap": True,
},
{
"type": "TextBlock",
"text": f"Click [here]({query_link}) to check your query!",
"wrap": True,
"isSubtle": True,
},
{
"type": "TextBlock",
"text": f"Click [here]({alert_link}) to check your alert!",
"wrap": True,
"isSubtle": True,
},
],
},
],
}
],
},
}
]

def notify(self, alert, query, user, new_state, app, host, metadata, options):
# Documentation: https://developer.webex.com/docs/api/guides/cards

query_link = f"{host}/queries/{query.id}"
alert_link = f"{host}/alerts/{alert.id}"

if new_state == Alert.TRIGGERED_STATE:
subject = alert.custom_subject or f"{alert.name} just triggered"
else:
subject = f"{alert.name} went back to normal"

attachments = self.formatted_attachments_template(
subject=subject, description=alert.custom_body, query_link=query_link, alert_link=alert_link
)

template_payload = {"markdown": subject + "\n" + alert.custom_body, "attachments": attachments}

headers = {"Authorization": f"Bearer {options['webex_bot_token']}"}

api_destinations = {
"toPersonEmail": options.get("to_person_emails"),
"roomId": options.get("to_room_ids"),
}

for payload_tag, destinations in api_destinations.items():
if destinations is None:
continue

# destinations is guaranteed to be a comma-separated string
for destination_id in destinations.split(","):
payload = deepcopy(template_payload)
payload[payload_tag] = destination_id
self.post_message(payload, headers)

def post_message(self, payload, headers):
try:
resp = requests.post(
self.api_base_url,
json=payload,
headers=headers,
timeout=5.0,
)
logging.warning(resp.text)
if resp.status_code != 200:
logging.error("Webex send ERROR. status_code => {status}".format(status=resp.status_code))
except Exception as e:
logging.exception(f"Webex send ERROR: {e}")


register(Webex)
1 change: 1 addition & 0 deletions redash/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ def email_server_is_configured():
"redash.destinations.hangoutschat",
"redash.destinations.microsoft_teams_webhook",
"redash.destinations.asana",
"redash.destinations.webex",
]

enabled_destinations = array_from_string(os.environ.get("REDASH_ENABLED_DESTINATIONS", ",".join(default_destinations)))
Expand Down
51 changes: 51 additions & 0 deletions tests/handlers/test_destinations.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from redash.destinations.asana import Asana
from redash.destinations.discord import Discord
from redash.destinations.webex import Webex
from redash.models import Alert, NotificationDestination
from tests import BaseTestCase

Expand Down Expand Up @@ -196,3 +197,53 @@ def test_asana_notify_calls_requests_post():
)

assert mock_response.status_code == 204


def test_webex_notify_calls_requests_post():
alert = mock.Mock(spec_set=["id", "name", "custom_subject", "custom_body", "render_template"])
alert.id = 1
alert.name = "Test Alert"
alert.custom_subject = "Test custom subject"
alert.custom_body = "Test custom body"

alert.render_template = mock.Mock(return_value={"Rendered": "template"})
query = mock.Mock()
query.id = 1

user = mock.Mock()
app = mock.Mock()
host = "https://localhost:5000"
options = {"webex_bot_token": "abcd", "to_room_ids": "1234"}
metadata = {"Scheduled": False}

new_state = Alert.TRIGGERED_STATE
destination = Webex(options)

with mock.patch("redash.destinations.webex.requests.post") as mock_post:
mock_response = mock.Mock()
mock_response.status_code = 204
mock_post.return_value = mock_response

destination.notify(alert, query, user, new_state, app, host, metadata, options)

query_link = f"{host}/queries/{query.id}"
alert_link = f"{host}/alerts/{alert.id}"

formatted_attachments = Webex.formatted_attachments_template(
alert.custom_subject, alert.custom_body, query_link, alert_link
)

expected_payload = {
"markdown": alert.custom_subject + "\n" + alert.custom_body,
"attachments": formatted_attachments,
"roomId": "1234",
}

mock_post.assert_called_once_with(
destination.api_base_url,
json=expected_payload,
headers={"Authorization": "Bearer abcd"},
timeout=5.0,
)

assert mock_response.status_code == 204

0 comments on commit c2e7df0

Please sign in to comment.