dev: update webhook logic for issues

This commit is contained in:
pablohashescobar 2024-05-01 14:04:36 +05:30
parent aa09ec7cd4
commit fe68e14d8b
5 changed files with 196 additions and 12 deletions

View File

@ -52,8 +52,7 @@ from plane.db.models import (
from .base import BaseAPIView, WebhookMixin from .base import BaseAPIView, WebhookMixin
class WorkspaceIssueAPIEndpoint(BaseAPIView):
class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
""" """
This viewset provides `retrieveByIssueId` on workspace level This viewset provides `retrieveByIssueId` on workspace level
@ -61,12 +60,9 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
model = Issue model = Issue
webhook_event = "issue" webhook_event = "issue"
permission_classes = [ permission_classes = [ProjectEntityPermission]
ProjectEntityPermission
]
serializer_class = IssueSerializer serializer_class = IssueSerializer
@property @property
def project__identifier(self): def project__identifier(self):
return self.kwargs.get("project__identifier", None) return self.kwargs.get("project__identifier", None)
@ -92,7 +88,9 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
.order_by(self.kwargs.get("order_by", "-created_at")) .order_by(self.kwargs.get("order_by", "-created_at"))
).distinct() ).distinct()
def get(self, request, slug, project__identifier=None, issue__identifier=None): def get(
self, request, slug, project__identifier=None, issue__identifier=None
):
if issue__identifier and project__identifier: if issue__identifier and project__identifier:
issue = Issue.issue_objects.annotate( issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter( sub_issues_count=Issue.issue_objects.filter(
@ -101,7 +99,11 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
).get(workspace__slug=slug, project__identifier=project__identifier, sequence_id=issue__identifier) ).get(
workspace__slug=slug,
project__identifier=project__identifier,
sequence_id=issue__identifier,
)
return Response( return Response(
IssueSerializer( IssueSerializer(
issue, issue,
@ -111,7 +113,8 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
class IssueAPIEndpoint(BaseAPIView):
""" """
This viewset automatically provides `list`, `create`, `retrieve`, This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to issue. `update` and `destroy` actions related to issue.

View File

@ -249,6 +249,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
update_cycle_issue_activity = [] update_cycle_issue_activity = []
# Iterate over each cycle_issue in cycle_issues # Iterate over each cycle_issue in cycle_issues
for cycle_issue in cycle_issues: for cycle_issue in cycle_issues:
old_cycle_id = cycle_issue.cycle_id
# Update the cycle_issue's cycle_id # Update the cycle_issue's cycle_id
cycle_issue.cycle_id = cycle_id cycle_issue.cycle_id = cycle_id
# Add the modified cycle_issue to the records_to_update list # Add the modified cycle_issue to the records_to_update list
@ -256,7 +257,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
# Record the update activity # Record the update activity
update_cycle_issue_activity.append( update_cycle_issue_activity.append(
{ {
"old_cycle_id": str(cycle_issue.cycle_id), "old_cycle_id": str(old_cycle_id),
"new_cycle_id": str(cycle_id), "new_cycle_id": str(cycle_id),
"issue_id": str(cycle_issue.issue_id), "issue_id": str(cycle_issue.issue_id),
} }

View File

@ -52,7 +52,7 @@ from plane.db.models import (
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
# Module imports # Module imports
from .. import BaseAPIView, BaseViewSet, WebhookMixin from .. import BaseAPIView, BaseViewSet
class IssueListEndpoint(BaseAPIView): class IssueListEndpoint(BaseAPIView):
@ -244,7 +244,7 @@ class IssueListEndpoint(BaseAPIView):
return Response(issues, status=status.HTTP_200_OK) return Response(issues, status=status.HTTP_200_OK)
class IssueViewSet(WebhookMixin, BaseViewSet): class IssueViewSet(BaseViewSet):
def get_serializer_class(self): def get_serializer_class(self):
return ( return (
IssueCreateSerializer IssueCreateSerializer

View File

@ -31,6 +31,7 @@ from plane.db.models import (
) )
from plane.settings.redis import redis_instance from plane.settings.redis import redis_instance
from plane.utils.exception_logger import log_exception from plane.utils.exception_logger import log_exception
from plane.bgtasks.webhook_task import webhook_activity
# Track Changes in name # Track Changes in name
@ -1692,6 +1693,19 @@ def issue_activity(
except Exception as e: except Exception as e:
log_exception(e) log_exception(e)
for activity in issue_activities_created:
webhook_activity.delay(
event="issue",
event_id=activity.issue_id,
verb=activity.verb,
field=activity.field,
old_value=activity.old_value,
new_value=activity.new_value,
actor_id=activity.actor_id,
current_site=origin,
slug=activity.workspace.slug,
)
if notification: if notification:
notifications.delay( notifications.delay(
type=type, type=type,

View File

@ -294,3 +294,169 @@ def send_webhook_deactivation_email(
except Exception as e: except Exception as e:
log_exception(e) log_exception(e)
return return
@shared_task(
bind=True,
autoretry_for=(requests.RequestException,),
retry_backoff=600,
max_retries=5,
retry_jitter=True,
)
def webhook_send_task(
self,
webhook,
slug,
event,
event_data,
action,
current_site,
activity,
):
try:
webhook = Webhook.objects.get(id=webhook, workspace__slug=slug)
headers = {
"Content-Type": "application/json",
"User-Agent": "Autopilot",
"X-Plane-Delivery": str(uuid.uuid4()),
"X-Plane-Event": event,
}
# # Your secret key
event_data = (
json.loads(json.dumps(event_data, cls=DjangoJSONEncoder))
if event_data is not None
else None
)
action = {
"POST": "create",
"PATCH": "update",
"PUT": "update",
"DELETE": "delete",
}.get(action, action)
payload = {
"event": event,
"action": action,
"webhook_id": str(webhook.id),
"workspace_id": str(webhook.workspace_id),
"data": event_data,
"activity": activity,
}
# Use HMAC for generating signature
if webhook.secret_key:
hmac_signature = hmac.new(
webhook.secret_key.encode("utf-8"),
json.dumps(payload).encode("utf-8"),
hashlib.sha256,
)
signature = hmac_signature.hexdigest()
headers["X-Plane-Signature"] = signature
# Send the webhook event
response = requests.post(
webhook.url,
headers=headers,
json=payload,
timeout=30,
)
# Log the webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
request_body=str(payload),
response_status=str(response.status_code),
response_headers=str(response.headers),
response_body=str(response.text),
retry_count=str(self.request.retries),
)
except requests.RequestException as e:
# Log the failed webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook_id=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
request_body=str(payload),
response_status=500,
response_headers="",
response_body=str(e),
retry_count=str(self.request.retries),
)
# Retry logic
if self.request.retries >= self.max_retries:
Webhook.objects.filter(pk=webhook.id).update(is_active=False)
if webhook:
# send email for the deactivation of the webhook
send_webhook_deactivation_email(
webhook_id=webhook.id,
receiver_id=webhook.created_by_id,
reason=str(e),
current_site=current_site,
)
return
raise requests.RequestException()
except Exception as e:
if settings.DEBUG:
print(e)
log_exception(e)
return
@shared_task
def webhook_activity(
event,
verb,
field,
old_value,
new_value,
actor_id,
slug,
current_site,
event_id,
):
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
if event == "project":
webhooks = webhooks.filter(project=True)
if event == "issue":
webhooks = webhooks.filter(issue=True)
if event == "module" or event == "module_issue":
webhooks = webhooks.filter(module=True)
if event == "cycle" or event == "cycle_issue":
webhooks = webhooks.filter(cycle=True)
if event == "issue_comment":
webhooks = webhooks.filter(issue_comment=True)
for webhook in webhooks:
webhook_send_task.delay(
webhook=webhook.id,
slug=slug,
event=event,
event_data=get_model_data(
event=event,
event_id=event_id,
),
action=verb,
current_site=current_site,
activity={
"field": field,
"new_value": new_value,
"old_value": old_value,
"actor_id": actor_id,
},
)