diff --git a/README.md b/README.md index e683a1a0d8..5b44b1df93 100644 --- a/README.md +++ b/README.md @@ -467,8 +467,6 @@ Ntfy.sh - - Pushover
@@ -481,6 +479,8 @@ Resend
+ + SendGrid
@@ -499,6 +499,18 @@ SMTP
+ + + Telegram
+ Telegram +
+ + + + Twilio
+ Twilio +
+ Teams
@@ -507,22 +519,16 @@
- Teams
+ Zoom
Zoom
- - Telegram
- Telegram -
- - - - Twilio
- Twilio +
+ Zoom Chat
+ Zoom Chat
diff --git a/docs/images/zoom_chat-provider1.png b/docs/images/zoom_chat-provider1.png new file mode 100644 index 0000000000..3fedb4f11e Binary files /dev/null and b/docs/images/zoom_chat-provider1.png differ diff --git a/docs/images/zoom_chat-provider10.png b/docs/images/zoom_chat-provider10.png new file mode 100644 index 0000000000..83a1af63e0 Binary files /dev/null and b/docs/images/zoom_chat-provider10.png differ diff --git a/docs/images/zoom_chat-provider2.png b/docs/images/zoom_chat-provider2.png new file mode 100644 index 0000000000..a315f5b573 Binary files /dev/null and b/docs/images/zoom_chat-provider2.png differ diff --git a/docs/images/zoom_chat-provider3.png b/docs/images/zoom_chat-provider3.png new file mode 100644 index 0000000000..44af6d78bb Binary files /dev/null and b/docs/images/zoom_chat-provider3.png differ diff --git a/docs/images/zoom_chat-provider4.png b/docs/images/zoom_chat-provider4.png new file mode 100644 index 0000000000..c073ae4f75 Binary files /dev/null and b/docs/images/zoom_chat-provider4.png differ diff --git a/docs/images/zoom_chat-provider5.png b/docs/images/zoom_chat-provider5.png new file mode 100644 index 0000000000..56ae5e5531 Binary files /dev/null and b/docs/images/zoom_chat-provider5.png differ diff --git a/docs/images/zoom_chat-provider6.png b/docs/images/zoom_chat-provider6.png new file mode 100644 index 0000000000..b217320a3c Binary files /dev/null and b/docs/images/zoom_chat-provider6.png differ diff --git a/docs/images/zoom_chat-provider7.png b/docs/images/zoom_chat-provider7.png new file mode 100644 index 0000000000..fb34a190b2 Binary files /dev/null and b/docs/images/zoom_chat-provider7.png differ diff --git a/docs/images/zoom_chat-provider8.png b/docs/images/zoom_chat-provider8.png new file mode 100644 index 0000000000..812fa24346 Binary files /dev/null and b/docs/images/zoom_chat-provider8.png differ diff --git a/docs/images/zoom_chat-provider9.png b/docs/images/zoom_chat-provider9.png new file mode 100644 index 0000000000..4c2aa7c5bf Binary files /dev/null and b/docs/images/zoom_chat-provider9.png differ diff --git a/docs/mint.json b/docs/mint.json index 07a6c3ce4c..625116c134 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -266,7 +266,8 @@ "providers/documentation/youtrack-provider", "providers/documentation/zabbix-provider", "providers/documentation/zenduty-provider", - "providers/documentation/zoom-provider" + "providers/documentation/zoom-provider", + "providers/documentation/zoom_chat-provider" ] }, "providers/adding-a-new-provider" diff --git a/docs/providers/documentation/zoom_chat-provider.mdx b/docs/providers/documentation/zoom_chat-provider.mdx new file mode 100644 index 0000000000..6240726beb --- /dev/null +++ b/docs/providers/documentation/zoom_chat-provider.mdx @@ -0,0 +1,82 @@ +--- +title: "Zoom Chat" +sidebarTitle: "Zoom Chat Provider" +description: "Zoom Chat provider allows you to send Zoom Chats using the Incoming Webhook Zoom application." +--- +import AutoGeneratedSnippet from '/snippets/providers/zoom_chat-snippet-autogenerated.mdx'; + + +For this integration, you will need to add and configure the Incoming Webhook application from the Zoom App Marketplace: https://marketplace.zoom.us/apps/eH_dLuquRd-VYcOsNGy-hQ + + + + +## Connecting with the Provider + +### Enable the Incoming Webhook Application + +The Incoming Webhook application is available in the Zoom App Marketplace. + + + + + + + + + +### Create Team Chat Channel: + +This channel will be the recipient of the Keep notifications. + + + + + + + + + +### Enable the Incoming Webhook Application + +Send `/inc connect ` to the channel to enable a webhook with authorization code. The app will respond with the webhook url and authorization code. + + +You should use the "Full Format" Incoming Webhook Url, which ends in `?format=full`. + + + + + + + + + + +## (Optional) Enabling User JID Lookup + +Messages can optionally include Zoom user JIDs, which are used to tag a particular Zoom user in a message. +This is useful, for example, if a team subscribes to a chat channel but members only wish to be notified when they are explicitly tagged. + +### Create a Zoom Application + +User lookup requires authorization. Create an internal only, Zoom Server to Server OAuth application. + + + + + + + + + +### Assign Required Scopes + + + + + + + + + diff --git a/docs/providers/overview.md b/docs/providers/overview.md index 3ef8a0c7c6..13cb496b62 100644 --- a/docs/providers/overview.md +++ b/docs/providers/overview.md @@ -129,3 +129,4 @@ By leveraging Keep Providers, users are able to deeply integrate Keep with the t - [Zabbix](/providers/documentation/zabbix-provider) - [Zenduty](/providers/documentation/zenduty-provider) - [Zoom](/providers/documentation/zoom-provider) +- [Zoom Chat](/providers/documentation/zoom_chat-provider) diff --git a/docs/providers/overview.mdx b/docs/providers/overview.mdx index dd6ebeef36..37d08694e6 100644 --- a/docs/providers/overview.mdx +++ b/docs/providers/overview.mdx @@ -976,3 +976,10 @@ By leveraging Keep Providers, users are able to deeply integrate Keep with the t icon={ } > + + } +> + diff --git a/docs/snippets/providers/zoom_chat-snippet-autogenerated.mdx b/docs/snippets/providers/zoom_chat-snippet-autogenerated.mdx new file mode 100644 index 0000000000..497a542be1 --- /dev/null +++ b/docs/snippets/providers/zoom_chat-snippet-autogenerated.mdx @@ -0,0 +1,42 @@ +{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py +Do not edit it manually, as it will be overwritten */} + +## Authentication +This provider requires authentication. +- **webhook_url**: Zoom Incoming Webhook Full Format Url (required: True, sensitive: True) +- **authorization_token**: Incoming Webhook Authorization Token (required: True, sensitive: True) +- **account_id**: Zoom Account ID (required: False, sensitive: True) +- **client_id**: Zoom Client ID (required: False, sensitive: True) +- **client_secret**: Zoom Client Secret (required: False, sensitive: True) + +Certain scopes may be required to perform specific actions or queries via the provider. Below is a summary of relevant scopes and their use cases: +- **user:read:user:admin**: View a Zoom user's details +- **user:read:list_users:admin**: List Zoom users + + + +## In workflows + +This provider can be used in workflows. + + + +As "action" to make changes or update data, example: +```yaml +actions: + - name: Query zoom_chat + provider: zoom_chat + config: "{{ provider.my_provider_name }}" + with: + severity: {value} # The severity of the alert. + title: {value} # The title to use for the message. (optional) + message: {value} # The text message to send. Supports Markdown formatting. + tagged_users: {value} # A list of Zoom user email addresses to tag. (optional) + details_url: {value} # A URL linking to more information. (optional) +``` + + + + +Check the following workflow example: +- [zoom_chat_example.yml](https://github.com/keephq/keep/blob/main/examples/workflows/zoom_chat_example.yml) diff --git a/examples/workflows/zoom_chat_example.yml b/examples/workflows/zoom_chat_example.yml new file mode 100644 index 0000000000..55bd11213e --- /dev/null +++ b/examples/workflows/zoom_chat_example.yml @@ -0,0 +1,17 @@ +workflow: + id: zoom_chat-message + name: Zoom Chat Message + description: Sends a notification to a Zoom Chat channel via the Incoming Webhook application. + triggers: + - type: manual + actions: + - name: zoom_chat-action + provider: + type: zoom_chat + config: "{{ providers.zoom_chat }}" + with: + message: test message from keep + severity: critical + title: critical test message + tagged_users: joesmith@mail.com + details_url: https://www.github.com/keep \ No newline at end of file diff --git a/keep-ui/public/icons/zoom_chat-icon.png b/keep-ui/public/icons/zoom_chat-icon.png new file mode 100644 index 0000000000..c5d6f985f3 Binary files /dev/null and b/keep-ui/public/icons/zoom_chat-icon.png differ diff --git a/keep/providers/zoom_chat_provider/__init__.py b/keep/providers/zoom_chat_provider/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keep/providers/zoom_chat_provider/zoom_chat_provider.py b/keep/providers/zoom_chat_provider/zoom_chat_provider.py new file mode 100644 index 0000000000..375672fbf2 --- /dev/null +++ b/keep/providers/zoom_chat_provider/zoom_chat_provider.py @@ -0,0 +1,368 @@ +""" +ZoomChatProvider is a class that provides a way to send Zoom Chats programmatically using the Incoming Webhook Zoom application. +""" + +import dataclasses +import http +import os +import time +from typing import Optional + +import pydantic +import requests +from requests.auth import HTTPBasicAuth + +from keep.contextmanager.contextmanager import ContextManager +from keep.exceptions.provider_exception import ProviderException +from keep.providers.base.base_provider import BaseProvider +from keep.providers.models.provider_config import ProviderConfig, ProviderScope +from keep.validation.fields import HttpsUrl + + +@pydantic.dataclasses.dataclass +class ZoomChatProviderAuthConfig: + """ + ZoomChatProviderAuthConfig holds the authentication information for the ZoomChatProvider. + """ + + webhook_url: HttpsUrl = dataclasses.field( + metadata={ + "name": "webhook_url", + "description": "Zoom Incoming Webhook Full Format Url", + "required": True, + "sensitive": True, + "validation": "https_url", + }, + ) + authorization_token: str = dataclasses.field( + metadata={ + "name": "authorization_token", + "description": "Incoming Webhook Authorization Token", + "required": True, + "sensitive": True, + }, + ) + account_id: Optional[str] = dataclasses.field( + default="zoom_account_id", + metadata={ + "required": False, + "description": "Zoom Account ID", + "sensitive": True, + } + ) + client_id: Optional[str] = dataclasses.field( + default="zoom_client_id", + metadata={ + "required": False, + "description": "Zoom Client ID", + "sensitive": True, + } + ) + client_secret: Optional[str] = dataclasses.field( + default="zoom_client_secret", + metadata={ + "required": False, + "description": "Zoom Client Secret", + "sensitive": True, + } + ) + + +class ZoomChatProvider(BaseProvider): + """Send alert message to Zoom Chat using the Incoming Webhook application.""" + + PROVIDER_DISPLAY_NAME = "Zoom Chat" + PROVIDER_TAGS = ["messaging"] + PROVIDER_CATEGORY = ["Communication"] + BASE_URL = "https://api.zoom.us/v2" + + PROVIDER_SCOPES = [ + ProviderScope( + name="user:read:user:admin", + description="View a Zoom user's details", + mandatory=False, + alias="View a Zoom user", + ), + ProviderScope( + name="user:read:list_users:admin", + description="List Zoom users", + mandatory=False, + alias="List Zoom users", + ), + ] + + def __init__( + self, + context_manager: ContextManager, + provider_id: str, + config: ProviderConfig, + ): + super().__init__(context_manager, provider_id, config) + self.access_token = None + + def validate_config(self): + """Validates required configuration for Zoom Chat provider.""" + self.authentication_config = ZoomChatProviderAuthConfig( + **self.config.authentication + ) + if ( + not self.authentication_config.webhook_url + and not self.authentication_config.authorization_token + ): + raise Exception( + "Zoom Incoming Webhook URL and authorization token are required." + ) + + def _get_access_token(self) -> str: + """ + Get OAuth access token from Zoom. + Returns: + str: Access token + """ + try: + token_url = "https://zoom.us/oauth/token" + auth = HTTPBasicAuth( + self.authentication_config.client_id, + self.authentication_config.client_secret, + ) + data = { + "grant_type": "account_credentials", + "account_id": self.authentication_config.account_id, + } + response = requests.post(token_url, auth=auth, data=data) + if response.status_code != 200: + raise ProviderException( + f"Failed to get access token: {response.json()}" + ) + return response.json()["access_token"] + except Exception as e: + raise ProviderException(f"Failed to get access token: {str(e)}") + + def _get_headers(self) -> dict: + """ + Get headers for API requests. + Returns: + dict: Headers including authorization + """ + if not self.access_token: + self.access_token = self._get_access_token() + return { + "Authorization": f"Bearer {self.access_token}", + "Content-Type": "application/json", + } + + def validate_scopes(self) -> dict[str, bool | str]: + """Validate scopes for the provider.""" + if not all( + [ + self.authentication_config.account_id, + self.authentication_config.client_id, + self.authentication_config.client_secret, + ] + ): + return { + "user:read:user:admin": "OAuth credentials not configured", + "user:read:list_users:admin": "OAuth credentials not configured", + } + try: + # Test API access by listing users + response = requests.get( + f"{self.BASE_URL}/users", headers=self._get_headers() + ) + if response.status_code != 200: + raise Exception(f"Failed to validate scopes: {response.json()}") + return { + "user:read:user:admin": True, + "user:read:list_users:admin": True, + } + except Exception as e: + self.logger.exception("Failed to validate scopes") + return { + "user:read:user:admin": str(e), + "user:read:list_users:admin": str(e), + } + + def dispose(self): + """Clean up resources.""" + self.access_token = None + pass + + def _get_zoom_userinfo(self, email: str) -> dict: + """Get a user's information from Zoom API using email address.""" + try: + response = requests.get( + f"{self.BASE_URL}/users/{email}", + headers=self._get_headers(), + ) + if response.status_code == 200: + self.logger.info("User details retrieved successfully") + return response.json() + else: + raise ProviderException( + f"Failed to retrieve user info for {email}: {response.status_code} - {response.text}" + ) + except requests.exceptions.RequestException as e: + raise ProviderException(f"Failed to retrieve user info: {str(e)}") + + def _notify( + self, + severity: str = "info", + title: Optional[str] = "", + message: str = "", + tagged_users: Optional[str] = "", + details_url: Optional[str] = "", + **kwargs: dict, + ) -> str: + """ + Send a message to Zoom Chat using a Incoming Webhook URL. + Args: + title (str): The title to use for the message. (optional) + message (str): The text message to send. Supports Markdown formatting. + tagged_users (list): A list of Zoom user email addresses to tag. (optional) + severity (str): The severity of the alert. + details_url (str): A URL linking to more information. (optional) + Raises: + ProviderException: If the message could not be sent successfully. + """ + self.logger.debug("Sending message to Zoom Chat Incoming Webhook") + webhook_url = self.authentication_config.webhook_url + authorization_token = self.authentication_config.authorization_token + if not message: + raise ProviderException("Message is required") + + def __send_message(url, body, headers, retries=3): + for attempt in range(retries): + try: + resp = requests.post(url, json=body, headers=headers) + if resp.status_code == http.HTTPStatus.OK: + return resp + self.logger.warning( + f"Attempt {attempt + 1} failed with status code {resp.status_code}" + ) + except requests.exceptions.RequestException as e: + self.logger.error(f"Attempt {attempt + 1} failed: {e}") + if attempt < retries - 1: + time.sleep(1) + raise requests.exceptions.RequestException( + f"Failed to notify message after {retries} attempts" + ) + + payload = { + "content": { + "settings": { + "default_sidebar_color": ( + "#EF4444" + if severity == "critical" + else ( + "#F97316" + if severity == "high" + else ( + "#EAB308" + if severity == "warning" + else "#10B981" if severity == "low" else "#3B82F6" + ) + ) + ) + }, + "body": [ + { + "type": "message", + "is_markdown_support": "true", + "text": message, + } + ], + } + } + + # Conditionally add a title entry + if title: + payload["content"]["head"] = { + "text": title, + "style": {"bold": "true"}, + } + + # Conditionally add the "View More Details" entry + if details_url: + payload["content"]["body"].append( + {"type": "message", "text": "View More Details", "link": details_url} + ) + + # Conditionally add tagged users + if tagged_users: + tagged_users_list = [user.strip() for user in tagged_users.split(",")] + tagged_user_jid_list = [] + + for user in tagged_users_list: + try: + user_data = self._get_zoom_userinfo(user) + jid = user_data.get("jid") + display_name = user_data.get("display_name") + if jid and display_name: + tagged_user_jid_list.append(f"") + except ProviderException as e: + self.logger.warning(f"Failed to get info for user {user}: {e}") + continue + + if tagged_user_jid_list: + tagged_user_string = " ".join(tagged_user_jid_list) + payload["content"]["body"].insert( + 0, + { + "type": "message", + "is_markdown_support": True, + "text": tagged_user_string, + }, + ) + + request_headers = { + "Authorization": authorization_token, + "Content-Type": "application/json", + } + response = __send_message(webhook_url, body=payload, headers=request_headers) + if response.status_code != http.HTTPStatus.OK: + raise ProviderException( + f"Failed to send message to Zoom Chat: {response.text}" + ) + self.logger.debug("Alert message sent to Zoom Chat successfully") + return "Alert message sent to Zoom Chat successfully" + + +if __name__ == "__main__": + import logging + + # Set up logging + logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler()]) + + # Get webhook details from environment + webhook_url = os.environ.get("ZOOM_WEBHOOK_URL") + webhook_auth_token = os.environ.get("ZOOM_WEBHOOK_AUTH_TOKEN") + + if not all([webhook_url, webhook_auth_token]): + raise Exception( + "ZOOM_WEBHOOK_URL and ZOOM_WEBHOOK_AUTH_TOKEN are required" + ) + + # Create context manager + context_manager = ContextManager( + tenant_id="singletenant", + workflow_id="test", + ) + + # Initialize the provider and provider config + config = ProviderConfig( + name="Zoom Chat", + description="Zoom Chat Output Provider", + authentication={ + "webhook_url": webhook_url, + "authorization_token": webhook_auth_token, + }, + ) + + # Initialize provider + provider = ZoomChatProvider( + context_manager=context_manager, + provider_id="zoom_chat_provider", + config=config, + ) + + provider.notify(message="Simple alert to Zoom chat.")