diff --git a/api/chalicelib/core/collaboration_base.py b/api/chalicelib/core/collaboration_base.py new file mode 100644 index 000000000..c04490508 --- /dev/null +++ b/api/chalicelib/core/collaboration_base.py @@ -0,0 +1,49 @@ +from abc import ABC, abstractmethod +import requests +from decouple import config +from datetime import datetime + +import schemas +from chalicelib.core import webhook + + +class BaseCollaboration(ABC): + @classmethod + @abstractmethod + def add(cls, tenant_id, data: schemas.AddCollaborationSchema): + pass + + @classmethod + @abstractmethod + def say_hello(cls, url): + pass + + @classmethod + @abstractmethod + def send_raw(cls, tenant_id, webhook_id, body): + pass + + @classmethod + @abstractmethod + def send_batch(cls, tenant_id, webhook_id, attachments): + pass + + @classmethod + @abstractmethod + def __share(cls, tenant_id, integration_id, fallback, pretext, title, title_link, text): + pass + + @classmethod + @abstractmethod + def share_session(cls, tenant_id, project_id, session_id, user, comment, integration_id=None): + pass + + @classmethod + @abstractmethod + def share_error(cls, tenant_id, project_id, error_id, user, comment, integration_id=None): + pass + + @classmethod + @abstractmethod + def __get(cls, tenant_id, integration_id=None): + pass diff --git a/api/chalicelib/core/collaboration_msteams.py b/api/chalicelib/core/collaboration_msteams.py new file mode 100644 index 000000000..43cc9f0ec --- /dev/null +++ b/api/chalicelib/core/collaboration_msteams.py @@ -0,0 +1,121 @@ +import requests +from decouple import config +from datetime import datetime + +import schemas +from chalicelib.core import webhook +from chalicelib.core.collaboration_base import BaseCollaboration + + +class MSTeams(BaseCollaboration): + @classmethod + def add(cls, tenant_id, data: schemas.AddCollaborationSchema): + if cls.say_hello(data.url): + return webhook.add(tenant_id=tenant_id, + endpoint=data.url, + webhook_type="msteams", + name=data.name) + return None + # https://messagecardplayground.azurewebsites.net + @classmethod + def say_hello(cls, url): + r = requests.post( + url=url, + json={ + "@type": "MessageCard", + "@context": "https://schema.org/extensions", + "summary": "Hello message", + "title": "Welcome to OpenReplay" + }) + if r.status_code != 200: + print("MSTeams integration failed") + print(r.text) + return False + return True + + @classmethod + def send_raw(cls, tenant_id, webhook_id, body): + integration = cls.__get(tenant_id=tenant_id, integration_id=webhook_id) + if integration is None: + return {"errors": ["slack integration not found"]} + try: + r = requests.post( + url=integration["endpoint"], + json=body, + timeout=5) + if r.status_code != 200: + print(f"!! issue sending slack raw; webhookId:{webhook_id} code:{r.status_code}") + print(r.text) + return None + except requests.exceptions.Timeout: + print(f"!! Timeout sending slack raw webhookId:{webhook_id}") + return None + except Exception as e: + print(f"!! Issue sending slack raw webhookId:{webhook_id}") + print(str(e)) + return None + return {"data": r.text} + + @classmethod + def send_batch(cls, tenant_id, webhook_id, attachments): + integration = cls.__get(tenant_id=tenant_id, integration_id=webhook_id) + if integration is None: + return {"errors": ["slack integration not found"]} + print(f"====> sending slack batch notification: {len(attachments)}") + for i in range(0, len(attachments), 100): + r = requests.post( + url=integration["endpoint"], + json={"attachments": attachments[i:i + 100]}) + if r.status_code != 200: + print("!!!! something went wrong") + print(r) + print(r.text) + + @classmethod + def __share(cls, tenant_id, integration_id, fallback, pretext, title, title_link, text): + integration = cls.__get(tenant_id=tenant_id, integration_id=integration_id) + if integration is None: + return {"errors": ["slack integration not found"]} + r = requests.post( + url=integration["endpoint"], + json={ + "attachments": [ + { + "fallback": fallback, + "pretext": pretext, + "title": title, + "title_link": title_link, + "text": text, + "ts": datetime.now().timestamp() + } + ] + }) + return r.text + + @classmethod + def share_session(cls, tenant_id, project_id, session_id, user, comment, integration_id=None): + args = {"fallback": f"{user} has shared the below session!", + "pretext": f"{user} has shared the below session!", + "title": f"{config('SITE_URL')}/{project_id}/session/{session_id}", + "title_link": f"{config('SITE_URL')}/{project_id}/session/{session_id}", + "text": comment} + return {"data": cls.__share(tenant_id, integration_id, **args)} + + @classmethod + def share_error(cls, tenant_id, project_id, error_id, user, comment, integration_id=None): + args = {"fallback": f"{user} has shared the below error!", + "pretext": f"{user} has shared the below error!", + "title": f"{config('SITE_URL')}/{project_id}/errors/{error_id}", + "title_link": f"{config('SITE_URL')}/{project_id}/errors/{error_id}", + "text": comment} + return {"data": cls.__share(tenant_id, integration_id, **args)} + + @classmethod + def __get(cls, tenant_id, integration_id=None): + if integration_id is not None: + return webhook.get(tenant_id=tenant_id, webhook_id=integration_id) + + integrations = webhook.get_by_type(tenant_id=tenant_id, webhook_type="slack") + if integrations is None or len(integrations) == 0: + return None + return integrations[0] diff --git a/api/chalicelib/core/collaboration_slack.py b/api/chalicelib/core/collaboration_slack.py index 15f090f5d..b4557cfd3 100644 --- a/api/chalicelib/core/collaboration_slack.py +++ b/api/chalicelib/core/collaboration_slack.py @@ -1,19 +1,20 @@ import requests from decouple import config from datetime import datetime + +import schemas from chalicelib.core import webhook +from chalicelib.core.collaboration_base import BaseCollaboration -class Slack: +class Slack(BaseCollaboration): @classmethod - def add_channel(cls, tenant_id, **args): - url = args["url"] - name = args["name"] - if cls.say_hello(url): + def add(cls, tenant_id, data: schemas.AddCollaborationSchema): + if cls.say_hello(data.url): return webhook.add(tenant_id=tenant_id, - endpoint=url, + endpoint=data.url, webhook_type="slack", - name=name) + name=data.name) return None @classmethod @@ -34,37 +35,6 @@ class Slack: return False return True - @classmethod - def send_text_attachments(cls, tenant_id, webhook_id, text, **args): - integration = cls.__get(tenant_id=tenant_id, integration_id=webhook_id) - if integration is None: - return {"errors": ["slack integration not found"]} - try: - r = requests.post( - url=integration["endpoint"], - json={ - "attachments": [ - { - "text": text, - "ts": datetime.now().timestamp(), - **args - } - ] - }, - timeout=5) - if r.status_code != 200: - print(f"!! issue sending slack text attachments; webhookId:{webhook_id} code:{r.status_code}") - print(r.text) - return None - except requests.exceptions.Timeout: - print(f"!! Timeout sending slack text attachments webhookId:{webhook_id}") - return None - except Exception as e: - print(f"!! Issue sending slack text attachments webhookId:{webhook_id}") - print(str(e)) - return None - return {"data": r.text} - @classmethod def send_raw(cls, tenant_id, webhook_id, body): integration = cls.__get(tenant_id=tenant_id, integration_id=webhook_id) @@ -104,7 +74,7 @@ class Slack: print(r.text) @classmethod - def __share_to_slack(cls, tenant_id, integration_id, fallback, pretext, title, title_link, text): + def __share(cls, tenant_id, integration_id, fallback, pretext, title, title_link, text): integration = cls.__get(tenant_id=tenant_id, integration_id=integration_id) if integration is None: return {"errors": ["slack integration not found"]} @@ -131,7 +101,7 @@ class Slack: "title": f"{config('SITE_URL')}/{project_id}/session/{session_id}", "title_link": f"{config('SITE_URL')}/{project_id}/session/{session_id}", "text": comment} - return {"data": cls.__share_to_slack(tenant_id, integration_id, **args)} + return {"data": cls.__share(tenant_id, integration_id, **args)} @classmethod def share_error(cls, tenant_id, project_id, error_id, user, comment, integration_id=None): @@ -140,12 +110,7 @@ class Slack: "title": f"{config('SITE_URL')}/{project_id}/errors/{error_id}", "title_link": f"{config('SITE_URL')}/{project_id}/errors/{error_id}", "text": comment} - return {"data": cls.__share_to_slack(tenant_id, integration_id, **args)} - - @classmethod - def has_slack(cls, tenant_id): - integration = cls.__get(tenant_id=tenant_id) - return not (integration is None or len(integration) == 0) + return {"data": cls.__share(tenant_id, integration_id, **args)} @classmethod def __get(cls, tenant_id, integration_id=None): diff --git a/api/routers/core.py b/api/routers/core.py index 935eac873..a1e431991 100644 --- a/api/routers/core.py +++ b/api/routers/core.py @@ -12,6 +12,7 @@ from chalicelib.core import log_tool_rollbar, sourcemaps, events, sessions_assig log_tool_newrelic, announcements, log_tool_bugsnag, weekly_report, integration_jira_cloud, integration_github, \ assist, mobile, signup, tenants, boarding, notifications, webhook, users, \ custom_metrics, saved_search, integrations_global +from chalicelib.core.collaboration_msteams import MSTeams from chalicelib.core.collaboration_slack import Slack from chalicelib.utils import helper, captcha from or_dependencies import OR_context @@ -948,6 +949,32 @@ def get_limits(context: schemas.CurrentContext = Depends(OR_context)): } +@app.post('/integrations/msteams', tags=['integrations']) +def add_msteams_integration(data: schemas.AddCollaborationSchema, + context: schemas.CurrentContext = Depends(OR_context)): + n = MSTeams.add(tenant_id=context.tenant_id, data=data) + if n is None: + return { + "errors": ["We couldn't send you a test message on your Microsoft Teams channel. Please verify your webhook url."] + } + return {"data": n} + + +@app.post('/integrations/msteams/{integrationId}', tags=['integrations']) +def edit_msteams_integration(integrationId: int, data: schemas.EditCollaborationSchema = Body(...), + context: schemas.CurrentContext = Depends(OR_context)): + if len(data.url) > 0: + old = webhook.get(tenant_id=context.tenant_id, webhook_id=integrationId) + if old["endpoint"] != data.url: + if not Slack.say_hello(data.url): + return { + "errors": [ + "We couldn't send you a test message on your Slack channel. Please verify your webhook url."] + } + return {"data": webhook.update(tenant_id=context.tenant_id, webhook_id=integrationId, + changes={"name": data.name, "endpoint": data.url})} + + @public_app.get('/', tags=["health"]) @public_app.post('/', tags=["health"]) @public_app.put('/', tags=["health"]) diff --git a/api/routers/core_dynamic.py b/api/routers/core_dynamic.py index 58d29793a..bfa7a9b83 100644 --- a/api/routers/core_dynamic.py +++ b/api/routers/core_dynamic.py @@ -71,8 +71,8 @@ def get_project(projectId: int, context: schemas.CurrentContext = Depends(OR_con @app.post('/integrations/slack', tags=['integrations']) @app.put('/integrations/slack', tags=['integrations']) -def add_slack_client(data: schemas.AddSlackSchema, context: schemas.CurrentContext = Depends(OR_context)): - n = Slack.add_channel(tenant_id=context.tenant_id, url=data.url, name=data.name) +def add_slack_integration(data: schemas.AddCollaborationSchema, context: schemas.CurrentContext = Depends(OR_context)): + n = Slack.add(tenant_id=context.tenant_id, data=data) if n is None: return { "errors": ["We couldn't send you a test message on your Slack channel. Please verify your webhook url."] @@ -81,7 +81,7 @@ def add_slack_client(data: schemas.AddSlackSchema, context: schemas.CurrentConte @app.post('/integrations/slack/{integrationId}', tags=['integrations']) -def edit_slack_integration(integrationId: int, data: schemas.EditSlackSchema = Body(...), +def edit_slack_integration(integrationId: int, data: schemas.EditCollaborationSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): if len(data.url) > 0: old = webhook.get(tenant_id=context.tenant_id, webhook_id=integrationId) diff --git a/api/schemas.py b/api/schemas.py index f1f3d9cb7..ea6240a54 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -78,14 +78,13 @@ class CurrentContext(CurrentAPIContext): _transform_email = validator('email', pre=True, allow_reuse=True)(transform_email) -class AddSlackSchema(BaseModel): +class AddCollaborationSchema(BaseModel): name: str = Field(...) url: HttpUrl = Field(...) -class EditSlackSchema(BaseModel): +class EditCollaborationSchema(AddCollaborationSchema): name: Optional[str] = Field(None) - url: HttpUrl = Field(...) class CreateNotificationSchema(BaseModel): diff --git a/ee/api/routers/core_dynamic.py b/ee/api/routers/core_dynamic.py index 047ecaca4..82306bfd3 100644 --- a/ee/api/routers/core_dynamic.py +++ b/ee/api/routers/core_dynamic.py @@ -75,8 +75,8 @@ def get_project(projectId: int, context: schemas.CurrentContext = Depends(OR_con @app.post('/integrations/slack', tags=['integrations']) @app.put('/integrations/slack', tags=['integrations']) -def add_slack_client(data: schemas.AddSlackSchema, context: schemas.CurrentContext = Depends(OR_context)): - n = Slack.add_channel(tenant_id=context.tenant_id, url=data.url, name=data.name) +def add_slack_client(data: schemas.AddCollaborationSchema, context: schemas.CurrentContext = Depends(OR_context)): + n = Slack.add(tenant_id=context.tenant_id, data=data) if n is None: return { "errors": ["We couldn't send you a test message on your Slack channel. Please verify your webhook url."] @@ -85,7 +85,7 @@ def add_slack_client(data: schemas.AddSlackSchema, context: schemas.CurrentConte @app.post('/integrations/slack/{integrationId}', tags=['integrations']) -def edit_slack_integration(integrationId: int, data: schemas.EditSlackSchema = Body(...), +def edit_slack_integration(integrationId: int, data: schemas.EditCollaborationSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): if len(data.url) > 0: old = webhook.get(tenant_id=context.tenant_id, webhook_id=integrationId)