Repository: keephq/keep
Pull request: feat: add squadcast provider.


<!-- 
Thanks for creating this pull request 🤗

Please make sure that the pull request is limited to one type (docs, feature, etc.) and keep it as small as possible. You can open multiple prs instead of opening a huge one.
-->

<!-- If this pull request closes an issue, please mention the issue number below -->
Closes #867 
/claim #867 

## 📑 Description
<!-- Add a brief description of the pr -->
- Added squadcast API as a provider
- Updated docs.

<!-- You can also choose to add a list of changes and if they have been completed or not by using the markdown to-do list syntax
- [ ] Not Completed
- [x] Completed
-->

## ✅ Checks
<!-- Make sure your pr passes the CI checks and do check the following fields as needed - -->
- [x] My pull request adheres to the code style of this project
- [x] My code requires changes to the documentation
- [x] I have updated the documentation as required
- [ ] All the tests have passed

## ℹ Additional Information
<!-- Any additional information like breaking changes, dependencies added, screenshots, comparisons between new and old behavior, etc. -->


Problem statement:
New Provider: Squadcast
**Describe the provider you want to add**
Squadcast is a incident management tool similar to Grafana OnCall or PagerDuty.  

**Describe your use case**
We route all our alerts through Squadcast for escalation, schedules and notification routing. With the discovery of keep we would like to have keep in the middle for using keeps workflows and mappings.

**Are you already using Keep?**
Yes, we plan to implement it.

Hints from issue discussion:
Hey @mfroembgen, thanks for opening this ticket :) I'll start digging around Squadcast, hopefully adding it in the next couple of days.

Can you please share what exactly integration you want? for example, for Grafana OnCall and Pager Duty, we have to create ticket, comment on ticket, etc. What will you need - functionality wise?
Hey @shahargl, thank you for getting back that quickly :) What we need function wise it could be actually quite simple. Even if squadcast offers more possibilities it would be enough for us if keep would just send the payload to an endpoint.

Maybe it would be enough to have a general webhook provider where you could just define the endpoint for sending the payload. (https://support.squadcast.com/integrations/incident-webhook-incident-webhook-api) That could be helpful for some other not supported or custom applications in the future.

Please let me know if you need some more information or details :) 


@mfroembgen no problem! my first priority is to help our users :)

First, we already have that :) You can use our generic HTTP provider where you can trigger any HTTP request you want, based on alerts - https://docs.keephq.dev/providers/documentation/http-provider - if you have any questions about it feel free to join our Slack at https://slack.keephq.dev and I'll answer pretty fast :)

But of course, we want to make our user's life better than that, so I'll start working on it. I'll start with creating incident and we will go from there.

@shahargl Thank you so much! I seem to have missed that somehow. 
/bounty 50
I would like to attempt it.
/attempt #867

<div id="algora" />

| [Algora profile](https://console.algora.io/@/35C4n0r) | Completed bounties | Tech | Active attempts | Options |
| --- | --- | --- | --- | --- |
| @35C4n0r | 2 keephq bounties | <div align="center">Python, JavaScript, <br />HTML & more</div> | <div align="center"></div> | [Cancel attempt](https://console.algora.io/api/bounties/clt7a473v0002jv0fi7digmts/cancel-attempt) |
@35C4n0r assigned
/attempt #867

On Thu, 29 Feb 2024 at 7:16 PM, algora-pbc[bot] ***@***.***>
wrote:

> 💎 *$50* bounty created by *keephq*
> 🙋 If you start working on this, comment /attempt #867 along with your
> implementation plan
> 👉 To claim this bounty, submit a pull request that includes the text /claim
> #867 somewhere in its body
> 📝 Before proceeding, please make sure you can *receive payouts*
> <https://docs.algora.io/bounties/payments#country-support> in your country
> 💵 Payment arrives in your account 2-5 days after the bounty is rewarded
> 💯 You keep 100% of the bounty award
> 🙏 Thank you for contributing to keephq/keep!
>
> 👉 *Add a bounty
> <https://console.algora.io/org/keephq/bounties/community?fund=keephq%2Fkeep%23867>*
> • *Share on socials
> <https://twitter.com/intent/tweet?text=%2450+bounty%21+%F0%9F%92%8E+https%3A%2F%2Fgithub.com%2Fkeephq%2Fkeep%2Fissues%2F867&related=algoraio>*
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/keephq/keep/issues/867#issuecomment-1971181163>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ALHRTKIM7FM7T3VC2ZODXRTYV4YMJAVCNFSM6AAAAABD6GFP6KVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSNZRGE4DCMJWGM>
> .
> You are receiving this because you are subscribed to this thread.Message
> ID: ***@***.***>
>

Commit message: feat: add squadcast provider.

- Added squadcast API as a provider
- Updated docs.

Signed-off-by: Jay <jaykumar20march@gmail.com>

Patch to review:
diff --git a/README.md b/README.md
index 10ed6f962e..a75147036b 100644
--- a/README.md
+++ b/README.md
@@ -164,6 +164,8 @@ Workflow triggers can either be executed manually when an alert is activated or
     <img width=32 height=32 src="https://github.com/keephq/keep/blob/main/keep-ui/public/icons/opsgenie-icon.png?raw=true"/>
     &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
     <img width=32 height=32 src="https://github.com/keephq/keep/blob/main/keep-ui/public/icons/zenduty-icon.png?raw=true"/>
+    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+    <img width=32 height=32 src="https://github.com/keephq/keep/blob/main/keep-ui/public/icons/squadcast-icon.png?raw=true"/>
 </p>
 <h3 align="center">Ticketing tools</h2>
 <p align="center">
diff --git a/docs/providers/documentation/squadcast-provider.mdx b/docs/providers/documentation/squadcast-provider.mdx
new file mode 100644
index 0000000000..17a1867123
--- /dev/null
+++ b/docs/providers/documentation/squadcast-provider.mdx
@@ -0,0 +1,43 @@
+---
+title: "Squadcast Provider"
+sidebarTitle: "Squadcast Provider"
+description: "Squadcast provider is a provider used for creating issues in Squadcast"
+---
+
+## Inputs
+
+The `notify` function take following parameters as inputs:
+
+- `notif_type` (required): Takes either of `incident` or `notes` depending on weather you want to create an incident or a note.
+1. ##### parametres for `incident`
+   - `message` (required): This will be the incident message.
+   - `description` (required): This will be the incident description.
+   - `priority` (optional): Priority of the incident.
+   - `status` (optional): Status of the event.
+   - `event_id` (optional): event_id is used to resolve an incident
+2. ##### parametres for `notes`
+   - `message` (required): The message of the note.
+   - `incident_id` (required): Id of the incident where the Note has to be created.
+   - `attachments` (optional): List of attachments for the notes.
+
+See [documentation](https://support.squadcast.com/integrations/incident-webhook-incident-webhook-api) for more
+
+## Authentication Parameters
+The Squadcast provider requires at least one of the following authentication parameter:
+
+- `refresh_token` (optional): Your Squadcast refresh_token.
+- `webhook_url` (optional): URL of your `incidents_webhook`.
+
+See [Squadcast Refresh Tokens](https://support.squadcast.com/terraform-and-api-documentation/public-api-refresh-token#from-your-profile-page) for more.
+
+## Connecting with the Provider
+
+1. Go to [Refresh Tokens](https://support.squadcast.com/terraform-and-api-documentation/public-api-refresh-token#from-your-profile-page) to see how to create a `refresh_token`.
+2. Visit [Documentations](https://support.squadcast.com/integrations/incident-webhook-incident-webhook-api) to learn how to setup `incident_webhooks` & get the `webhook_url`
+
+
+## Useful Links
+
+- [Squadcast Incident API](https://support.squadcast.com/integrations/incident-webhook-incident-webhook-api)
+- [Squadcast Refresh Tokens](https://support.squadcast.com/terraform-and-api-documentation/public-api-refresh-token#from-your-profile-page)
+- [Incident Notes](https://support.squadcast.com/incidents-page/incident-notes)
diff --git a/keep-ui/public/icons/squadcast-icon.png b/keep-ui/public/icons/squadcast-icon.png
new file mode 100644
index 0000000000..d78601466c
Binary files /dev/null and b/keep-ui/public/icons/squadcast-icon.png differ
diff --git a/keep/providers/squadcast_provider/__init__.py b/keep/providers/squadcast_provider/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/keep/providers/squadcast_provider/squadcast_provider.py b/keep/providers/squadcast_provider/squadcast_provider.py
new file mode 100644
index 0000000000..72db8bcc04
--- /dev/null
+++ b/keep/providers/squadcast_provider/squadcast_provider.py
@@ -0,0 +1,194 @@
+"""
+SquadcastProvider is a class that implements the Squadcast API and allows creating incidents and notes.
+"""
+import dataclasses
+import json
+
+import pydantic
+import requests
+from requests import HTTPError
+
+from keep.contextmanager.contextmanager import ContextManager
+from keep.exceptions.provider_config_exception import ProviderConfigException
+from keep.providers.base.base_provider import BaseProvider
+from keep.providers.models.provider_config import ProviderConfig, ProviderScope
+
+
+@pydantic.dataclasses.dataclass
+class SquadcastProviderAuthConfig:
+    service_region: str = dataclasses.field(
+        metadata={
+            "required": True,
+            "description": "Service region: EU/US",
+            "hint": "https://apidocs.squadcast.com/#intro",
+            "sensitive": False
+        }
+    )
+    refresh_token: str | None = dataclasses.field(
+        metadata={
+            "required": False,
+            "description": "Squadcast Refresh Token",
+            "hint": "https://support.squadcast.com/docs/squadcast-public-api",
+            "sensitive": True,
+        },
+        default=None
+    )
+    webhook_url: str | None = dataclasses.field(
+        metadata={
+            "required": False,
+            "description": "Incident webhook url",
+            "hint": "https://support.squadcast.com/integrations/incident-webhook-incident-webhook-api",
+            "sensitive": True,
+        },
+        default=None
+    )
+
+
+class SquadcastProvider(BaseProvider):
+    """Send email using the Squadcast API."""
+
+    PROVIDER_TAGS = ["alert"]
+
+    PROVIDER_SCOPES = [
+        ProviderScope(
+            name="authenticated",
+            description="The user can connect to the client",
+            mandatory=False,
+            alias="Connect to the client",
+        ),
+    ]
+
+    def __init__(
+            self, context_manager: ContextManager, provider_id: str, config: ProviderConfig
+    ):
+        super().__init__(context_manager, provider_id, config)
+
+    def validate_scopes(self):
+        """
+        Validates that the user has the required scopes to use the provider.
+        """
+        refresh_headers = {
+            "content-type": "application/json",
+            "X-Refresh-Token": f"{self.authentication_config.refresh_token}"
+        }
+        resp = requests.get(f"{self.__get_endpoint('auth')}/oauth/access-token", headers=refresh_headers)
+        try:
+            resp.raise_for_status()
+            scopes = {
+                "authenticated": True,
+            }
+        except Exception as e:
+            self.logger.exception("Error validating scopes")
+            scopes = {
+                "authenticated": str(e),
+            }
+        return scopes
+
+    def __get_endpoint(self, endpoint: str):
+        if endpoint == 'auth':
+            return ('https://auth.eu.squadcast.com', 'https://auth.squadcast.com')[
+                self.authentication_config.service_region == 'US']
+        elif endpoint == 'api':
+            return ('https://api.eu.squadcast.com', 'https://api.squadcast.com')[
+                self.authentication_config.service_region == 'US']
+
+    def validate_config(self):
+        self.authentication_config = SquadcastProviderAuthConfig(
+            **self.config.authentication
+        )
+        if (
+                not self.authentication_config.refresh_token
+                and not self.authentication_config.webhook_url
+        ):
+            raise ProviderConfigException(
+                "SquadcastProvider requires either refresh_token or webhook_url",
+                provider_id=self.provider_id,
+            )
+
+    def _create_incidents(self, headers: dict, message: str, description: str, priority: str = "",
+                          status: str = "",
+                          event_id: str = ""):
+
+        body = json.dumps({
+            "message": message,
+            "description": description,
+            "priority": priority,
+            "status": status,
+            "event_id": event_id
+        })
+
+        return requests.post(self.authentication_config.webhook_url, data=body, headers=headers)
+
+    def _crete_notes(self, headers: dict, message: str, incident_id: str, attachments: list = []):
+        body = json.dumps({
+            "message": message,
+            "attachments": attachments
+        })
+        return requests.post(f"{self.__get_endpoint('api')}/v3/incidents/{incident_id}/warroom", data=body,
+                             headers=headers)
+
+    def _notify(self, notif_type: str, message: str = "", description: str = "", incident_id: str = "",
+                priority: str = "",
+                status: str = "",
+                event_id: str = "", attachments: list = [], **kwargs) -> dict:
+        """
+        Creat an incident or notes using the Squadcast API.
+        """
+        self.logger.info(
+            "Sending email using Squadcast API",
+            extra={
+                notif_type: notif_type
+            })
+        refresh_headers = {
+            "content-type": "application/json",
+            "X-Refresh-Token": f"{self.authentication_config.refresh_token}"
+        }
+        api_key_resp = requests.get(f"{self.__get_endpoint('auth')}/oauth/access-token", headers=refresh_headers)
+        headers = {
+            "content-type": "application/json",
+            "Authorization": f"Bearer {api_key_resp.json()['data']['access_token']}",
+        }
+        if notif_type == 'incident':
+            if message == "" or description == "":
+                raise Exception(f"message: \"{message}\" and description: \"{description}\" cannot be empty")
+            resp = self._create_incidents(headers=headers, message=message, description=description, priority=priority,
+                                          status=status, event_id=event_id)
+        elif notif_type == 'notes':
+            if message == "" or incident_id == "":
+                raise Exception(f"message: \"{message}\" and incident_id: \"{incident_id}\" cannot be empty")
+            resp = self._crete_notes(headers=headers, message=message, incident_id=incident_id, attachments=attachments)
+        else:
+            raise Exception("notif_type is a mandatory field, expected: incident | notes")
+        try:
+            resp.raise_for_status()
+            return resp.json()
+        except HTTPError as e:
+            raise Exception(f"Failed to create issue: {str(e)}")
+
+    def dispose(self):
+        """
+        No need to dispose of anything, so just do nothing.
+        """
+        pass
+
+
+if __name__ == "__main__":
+    import os
+
+    squadcast_api_key = os.environ.get("MAILCHIMP_API_KEY")
+    context_manager = ContextManager(
+        tenant_id="singletenant",
+        workflow_id="test",
+    )
+    # Initalize the provider and provider config
+    config = ProviderConfig(
+        authentication={"api_key": squadcast_api_key},
+    )
+    provider = SquadcastProvider(context_manager, provider_id="squadcast-test", config=config)
+    response = provider.notify(
+        "onboarding@squadcast.dev",
+        "youremail@gmail.com",
+        "Hello World from Keep!",
+        "<strong>Test</strong> with HTML",
+    )
+    print(response)


Code review comments:
diff_hunk: @@ -0,0 +1,194 @@
+"""
+SquadcastProvider is a class that implements the Squadcast API and allows creating incidents and notes.
+"""
+import dataclasses
+import json
+
+import pydantic
+import requests
+from requests import HTTPError
+
+from keep.contextmanager.contextmanager import ContextManager
+from keep.exceptions.provider_config_exception import ProviderConfigException
+from keep.providers.base.base_provider import BaseProvider
+from keep.providers.models.provider_config import ProviderConfig, ProviderScope
+
+
+@pydantic.dataclasses.dataclass
+class SquadcastProviderAuthConfig:
+    service_region: str = dataclasses.field(
+        metadata={
+            "required": True,
+            "description": "Service region: EU/US",
+            "hint": "https://apidocs.squadcast.com/#intro",
+            "sensitive": False
+        }
+    )
+    refresh_token: str | None = dataclasses.field(
+        metadata={
+            "required": False,
+            "description": "Squadcast Refresh Token",
+            "hint": "https://support.squadcast.com/docs/squadcast-public-api",
+            "sensitive": True,
+        },
+        default=None
+    )
+    webhook_url: str | None = dataclasses.field(
+        metadata={
+            "required": False,
+            "description": "Incident webhook url",
+            "hint": "https://support.squadcast.com/integrations/incident-webhook-incident-webhook-api",
+            "sensitive": True,
+        },
+        default=None
+    )
+
+
+class SquadcastProvider(BaseProvider):
+    """Send email using the Squadcast API."""
path: keep/providers/squadcast_provider/squadcast_provider.py
text: @user1:
Email?

diff_hunk: @@ -0,0 +1,43 @@
+---
+title: "Squadcast Provider"
+sidebarTitle: "Squadcast Provider"
+description: "Squadcast provider is a provider used for creating issues in Squadcast"
+---
+
+## Inputs
+
+The `notify` function take following parameters as inputs:
+
+- `notif_type` (required): Takes either of `incident` or `notes` depending on weather you want to create an incident or a note.
path: docs/providers/documentation/squadcast-provider.mdx
text: @user1:
```suggestion
- `notify_type` (required): Takes either of `incident` or `notes` depending on weather you want to create an incident or a note.
```