From fb4535b2947964fc52f509c5e0040e7ae5c13a2a Mon Sep 17 00:00:00 2001 From: pablohashescobar <118773738+pablohashescobar@users.noreply.github.com> Date: Sat, 22 Apr 2023 18:15:52 +0530 Subject: [PATCH] feat: slack integration (#874) * feat: init slack integration * dev: create model and update existing view for slack * dev: update slack sync model and create view to install slack * dev: workspace integration query * dev: update the metadata validation for access_token and team_id and save config to database * dev: update validation for team_id * dev: update validation * dev: update validations * dev: remove bot access token field from sync * dev: handle integrity exception --- apiserver/plane/api/serializers/__init__.py | 1 + .../api/serializers/integration/__init__.py | 1 + .../api/serializers/integration/slack.py | 14 +++++ apiserver/plane/api/urls.py | 21 +++++++ apiserver/plane/api/views/__init__.py | 1 + .../plane/api/views/integration/__init__.py | 1 + apiserver/plane/api/views/integration/base.py | 27 ++++++--- .../plane/api/views/integration/slack.py | 59 +++++++++++++++++++ apiserver/plane/db/models/__init__.py | 1 + .../plane/db/models/integration/__init__.py | 1 + .../plane/db/models/integration/slack.py | 32 ++++++++++ 11 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 apiserver/plane/api/serializers/integration/slack.py create mode 100644 apiserver/plane/api/views/integration/slack.py create mode 100644 apiserver/plane/db/models/integration/slack.py diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 1cc866e8c..79014c53d 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -62,6 +62,7 @@ from .integration import ( GithubRepositorySerializer, GithubRepositorySyncSerializer, GithubCommentSyncSerializer, + SlackProjectSyncSerializer, ) from .importer import ImporterSerializer diff --git a/apiserver/plane/api/serializers/integration/__init__.py b/apiserver/plane/api/serializers/integration/__init__.py index 8aea68bd6..963fc295e 100644 --- a/apiserver/plane/api/serializers/integration/__init__.py +++ b/apiserver/plane/api/serializers/integration/__init__.py @@ -5,3 +5,4 @@ from .github import ( GithubIssueSyncSerializer, GithubCommentSyncSerializer, ) +from .slack import SlackProjectSyncSerializer \ No newline at end of file diff --git a/apiserver/plane/api/serializers/integration/slack.py b/apiserver/plane/api/serializers/integration/slack.py new file mode 100644 index 000000000..f535a64de --- /dev/null +++ b/apiserver/plane/api/serializers/integration/slack.py @@ -0,0 +1,14 @@ +# Module imports +from plane.api.serializers import BaseSerializer +from plane.db.models import SlackProjectSync + + +class SlackProjectSyncSerializer(BaseSerializer): + class Meta: + model = SlackProjectSync + fields = "__all__" + read_only_fields = [ + "project", + "workspace", + "workspace_integration", + ] diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 364b959e3..4d8fd7afe 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -132,6 +132,7 @@ from plane.api.views import ( GithubIssueSyncViewSet, GithubCommentSyncViewSet, BulkCreateGithubIssueSyncEndpoint, + SlackProjectSyncViewSet, ## End Integrations # Importer ServiceIssueImportSummaryEndpoint, @@ -1216,6 +1217,26 @@ urlpatterns = [ ), ), ## End Github Integrations + # Slack Integration + path( + "workspaces//projects//workspace-integrations//project-slack-sync/", + SlackProjectSyncViewSet.as_view( + { + "post": "create", + "get": "list", + } + ), + ), + path( + "workspaces//projects//workspace-integrations//project-slack-sync//", + SlackProjectSyncViewSet.as_view( + { + "delete": "destroy", + "get": "retrieve", + } + ), + ), + ## End Slack Integration ## End Integrations # Importer path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 83725e104..507d07816 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -106,6 +106,7 @@ from .integration import ( GithubCommentSyncViewSet, GithubRepositoriesEndpoint, BulkCreateGithubIssueSyncEndpoint, + SlackProjectSyncViewSet, ) from .importer import ( diff --git a/apiserver/plane/api/views/integration/__init__.py b/apiserver/plane/api/views/integration/__init__.py index 67dd370d9..ea20d96ea 100644 --- a/apiserver/plane/api/views/integration/__init__.py +++ b/apiserver/plane/api/views/integration/__init__.py @@ -6,3 +6,4 @@ from .github import ( GithubCommentSyncViewSet, GithubRepositoriesEndpoint, ) +from .slack import SlackProjectSyncViewSet diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py index 8312afa01..5213baf63 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/api/views/integration/base.py @@ -27,6 +27,7 @@ from plane.utils.integrations.github import ( ) from plane.api.permissions import WorkSpaceAdminPermission + class IntegrationViewSet(BaseViewSet): serializer_class = IntegrationSerializer model = Integration @@ -101,7 +102,6 @@ class WorkspaceIntegrationViewSet(BaseViewSet): WorkSpaceAdminPermission, ] - def get_queryset(self): return ( super() @@ -112,21 +112,30 @@ class WorkspaceIntegrationViewSet(BaseViewSet): def create(self, request, slug, provider): try: - installation_id = request.data.get("installation_id", None) - - if not installation_id: - return Response( - {"error": "Installation ID is required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - workspace = Workspace.objects.get(slug=slug) integration = Integration.objects.get(provider=provider) config = {} if provider == "github": + installation_id = request.data.get("installation_id", None) + if not installation_id: + return Response( + {"error": "Installation ID is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) metadata = get_github_metadata(installation_id) config = {"installation_id": installation_id} + if provider == "slack": + metadata = request.data.get("metadata", {}) + access_token = metadata.get("access_token", False) + team_id = metadata.get("team", {}).get("id", False) + if not metadata or not access_token or not team_id: + return Response( + {"error": "Access token and team id is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + config = {"team_id": team_id, "access_token": access_token} + # Create a bot user bot_user = User.objects.create( email=f"{uuid.uuid4().hex}@plane.so", diff --git a/apiserver/plane/api/views/integration/slack.py b/apiserver/plane/api/views/integration/slack.py new file mode 100644 index 000000000..06e2dfe39 --- /dev/null +++ b/apiserver/plane/api/views/integration/slack.py @@ -0,0 +1,59 @@ +# Django import +from django.db import IntegrityError + +# Third party imports +from rest_framework import status +from rest_framework.response import Response +from sentry_sdk import capture_exception + +# Module imports +from plane.api.views import BaseViewSet, BaseAPIView +from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember +from plane.api.serializers import SlackProjectSyncSerializer +from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission + + +class SlackProjectSyncViewSet(BaseViewSet): + permission_classes = [ + ProjectBasePermission, + ] + serializer_class = SlackProjectSyncSerializer + model = SlackProjectSync + + def create(self, request, slug, project_id, workspace_integration_id): + try: + serializer = SlackProjectSyncSerializer(data=request.data) + + workspace_integration = WorkspaceIntegration.objects.get( + workspace__slug=slug, pk=workspace_integration_id + ) + + if serializer.is_valid(): + serializer.save( + project_id=project_id, + workspace_integration_id=workspace_integration_id, + ) + + workspace_integration = WorkspaceIntegration.objects.get( + pk=workspace_integration_id, workspace__slug=slug + ) + + _ = ProjectMember.objects.get_or_create( + member=workspace_integration.actor, role=20, project_id=project_id + ) + + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except IntegrityError: + return Response({"error": "Slack is already enabled for the project"}, status=status.HTTP_400_BAD_REQUEST) + except WorkspaceIntegration.DoesNotExist: + return Response( + {"error": "Workspace Integration does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + print(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index b6ffe428c..e32d768e0 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -59,6 +59,7 @@ from .integration import ( GithubRepositorySync, GithubIssueSync, GithubCommentSync, + SlackProjectSync, ) from .importer import Importer diff --git a/apiserver/plane/db/models/integration/__init__.py b/apiserver/plane/db/models/integration/__init__.py index 4742a2529..3f2be93b8 100644 --- a/apiserver/plane/db/models/integration/__init__.py +++ b/apiserver/plane/db/models/integration/__init__.py @@ -1,2 +1,3 @@ from .base import Integration, WorkspaceIntegration from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync +from .slack import SlackProjectSync \ No newline at end of file diff --git a/apiserver/plane/db/models/integration/slack.py b/apiserver/plane/db/models/integration/slack.py new file mode 100644 index 000000000..6b29968f6 --- /dev/null +++ b/apiserver/plane/db/models/integration/slack.py @@ -0,0 +1,32 @@ +# Python imports +import uuid + +# Django imports +from django.db import models + +# Module imports +from plane.db.models import ProjectBaseModel + + +class SlackProjectSync(ProjectBaseModel): + access_token = models.CharField(max_length=300) + scopes = models.TextField() + bot_user_id = models.CharField(max_length=50) + webhook_url = models.URLField(max_length=1000) + data = models.JSONField(default=dict) + team_id = models.CharField(max_length=30) + team_name = models.CharField(max_length=300) + workspace_integration = models.ForeignKey( + "db.WorkspaceIntegration", related_name="slack_syncs", on_delete=models.CASCADE + ) + + def __str__(self): + """Return the repo name""" + return f"{self.project.name}" + + class Meta: + unique_together = ["team_id", "project"] + verbose_name = "Slack Project Sync" + verbose_name_plural = "Slack Project Syncs" + db_table = "slack_project_syncs" + ordering = ("-created_at",)