mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 22:09:12 +02:00
feat: webhook activity payload reverted (#3094)
* chore: revert webhook activity task * fix: channel id
This commit is contained in:
@@ -8,12 +8,11 @@ from celery import shared_task
|
||||
# Django imports
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
# Module imports
|
||||
from plane.app.serializers import IssueActivitySerializer
|
||||
from plane.bgtasks.notification_task import notifications
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import (
|
||||
CommentReaction,
|
||||
Cycle,
|
||||
@@ -28,14 +27,11 @@ from plane.db.models import (
|
||||
State,
|
||||
User,
|
||||
EstimatePoint,
|
||||
IssueType,
|
||||
)
|
||||
from plane.db.models.workspace import Workspace
|
||||
from plane.settings.redis import redis_instance
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.bgtasks.webhook_task import webhook_activity
|
||||
from plane.utils.issue_relation_mapper import get_inverse_relation
|
||||
from plane.utils.uuid import is_valid_uuid
|
||||
|
||||
|
||||
# Track Changes in name
|
||||
@@ -92,27 +88,6 @@ def track_description(
|
||||
):
|
||||
last_activity.created_at = timezone.now()
|
||||
last_activity.save(update_fields=["created_at"])
|
||||
|
||||
# Get the current origin from redis
|
||||
ri = redis_instance()
|
||||
origin = ri.get(str(issue_id))
|
||||
|
||||
workspace = Workspace.objects.get(pk=workspace_id)
|
||||
|
||||
if workspace:
|
||||
webhook_activity.delay(
|
||||
event="issue",
|
||||
event_id=issue_id,
|
||||
verb="updated",
|
||||
field="description",
|
||||
old_value=None,
|
||||
new_value=None,
|
||||
current_site=origin,
|
||||
actor_id=actor_id,
|
||||
slug=workspace.slug,
|
||||
old_identifier=last_activity.old_identifier,
|
||||
new_identifier=last_activity.new_identifier,
|
||||
)
|
||||
else:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -218,16 +193,9 @@ def track_state(
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
current_state_id = current_instance.get("state_id") or current_instance.get("state")
|
||||
requested_state_id = requested_data.get("state_id") or requested_data.get("state")
|
||||
|
||||
if current_state_id != requested_state_id:
|
||||
# check if the current state
|
||||
if not is_valid_uuid(requested_state_id):
|
||||
return
|
||||
|
||||
new_state = State.objects.get(pk=requested_state_id)
|
||||
old_state = State.objects.get(pk=current_state_id)
|
||||
if current_instance.get("state_id") != requested_data.get("state_id"):
|
||||
new_state = State.objects.get(pk=requested_data.get("state_id", None))
|
||||
old_state = State.objects.get(pk=current_instance.get("state_id", None))
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -338,10 +306,6 @@ def track_labels(
|
||||
|
||||
# Set of newly added labels
|
||||
for added_label in added_labels:
|
||||
# validate uuids
|
||||
if not is_valid_uuid(added_label):
|
||||
continue
|
||||
|
||||
label = Label.objects.get(pk=added_label)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -362,10 +326,6 @@ def track_labels(
|
||||
|
||||
# Set of dropped labels
|
||||
for dropped_label in dropped_labels:
|
||||
# validate uuids
|
||||
if not is_valid_uuid(dropped_label):
|
||||
continue
|
||||
|
||||
label = Label.objects.get(pk=dropped_label)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -412,10 +372,6 @@ def track_assignees(
|
||||
|
||||
bulk_subscribers = []
|
||||
for added_asignee in added_assignees:
|
||||
# validate uuids
|
||||
if not is_valid_uuid(added_asignee):
|
||||
continue
|
||||
|
||||
assignee = User.objects.get(pk=added_asignee)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -449,10 +405,6 @@ def track_assignees(
|
||||
)
|
||||
|
||||
for dropped_assignee in dropped_assginees:
|
||||
# validate uuids
|
||||
if not is_valid_uuid(dropped_assignee):
|
||||
continue
|
||||
|
||||
assignee = User.objects.get(pk=dropped_assignee)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
@@ -496,12 +448,6 @@ def track_estimate_points(
|
||||
if requested_data.get("estimate_point") is not None
|
||||
else None
|
||||
)
|
||||
|
||||
if new_estimate is None:
|
||||
field = "estimate_" + old_estimate.estimate.type
|
||||
else:
|
||||
field = "estimate_" + new_estimate.estimate.type
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
@@ -519,7 +465,7 @@ def track_estimate_points(
|
||||
),
|
||||
old_value=old_estimate.value if old_estimate else None,
|
||||
new_value=new_estimate.value if new_estimate else None,
|
||||
field=field,
|
||||
field="estimate_point",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment="updated the estimate point to ",
|
||||
@@ -609,7 +555,7 @@ def track_closed_to(
|
||||
)
|
||||
|
||||
|
||||
def track_type(
|
||||
def create_issue_activity(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
@@ -619,60 +565,31 @@ def track_type(
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
new_type_id = requested_data.get("type_id")
|
||||
old_type_id = current_instance.get("type_id")
|
||||
|
||||
if new_type_id != old_type_id:
|
||||
verb = "updated" if new_type_id else "deleted"
|
||||
old_type_id = (
|
||||
IssueType.objects.filter(pk=old_type_id).first() if old_type_id else None
|
||||
)
|
||||
new_type_id = (
|
||||
IssueType.objects.filter(pk=new_type_id).first() if new_type_id else None
|
||||
)
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor_id=actor_id,
|
||||
verb=verb,
|
||||
old_value=old_type_id.name if old_type_id else None,
|
||||
new_value=new_type_id.name if new_type_id else None,
|
||||
field="type",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment="",
|
||||
old_identifier=old_type_id.id if old_type_id else None,
|
||||
new_identifier=new_type_id.id if new_type_id else None,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_epic_activity(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
actor_id,
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
epic = Issue.objects.get(pk=issue_id)
|
||||
issue = Issue.objects.get(pk=issue_id)
|
||||
issue_activity = IssueActivity.objects.create(
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment="created the epic",
|
||||
comment="created the issue",
|
||||
verb="created",
|
||||
field="epic",
|
||||
actor_id=actor_id,
|
||||
epoch=epoch,
|
||||
)
|
||||
issue_activity.created_at = epic.created_at
|
||||
issue_activity.actor_id = epic.created_by_id
|
||||
issue_activity.created_at = issue.created_at
|
||||
issue_activity.actor_id = issue.created_by_id
|
||||
issue_activity.save(update_fields=["created_at", "actor_id"])
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
if requested_data.get("assignee_ids") is not None:
|
||||
track_assignees(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
actor_id,
|
||||
issue_activities,
|
||||
epoch,
|
||||
)
|
||||
|
||||
|
||||
def update_issue_activity(
|
||||
@@ -690,7 +607,6 @@ def update_issue_activity(
|
||||
"parent_id": track_parent,
|
||||
"priority": track_priority,
|
||||
"state_id": track_state,
|
||||
"state": track_state,
|
||||
"description_html": track_description,
|
||||
"target_date": track_target_date,
|
||||
"start_date": track_start_date,
|
||||
@@ -699,7 +615,6 @@ def update_issue_activity(
|
||||
"estimate_point": track_estimate_points,
|
||||
"archived_at": track_archive_at,
|
||||
"closed_to": track_closed_to,
|
||||
"type_id": track_type,
|
||||
}
|
||||
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
@@ -875,15 +790,14 @@ def create_cycle_issue_activity(
|
||||
issue_id=updated_record.get("issue_id"),
|
||||
actor_id=actor_id,
|
||||
verb="updated",
|
||||
old_value=old_cycle.name if old_cycle else "",
|
||||
new_value=new_cycle.name if new_cycle else "",
|
||||
old_value=old_cycle.name,
|
||||
new_value=new_cycle.name,
|
||||
field="cycles",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"""updated cycle from {old_cycle.name if old_cycle else ""}
|
||||
to {new_cycle.name if new_cycle else ""}""",
|
||||
old_identifier=old_cycle.id if old_cycle else None,
|
||||
new_identifier=new_cycle.id if new_cycle else None,
|
||||
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
|
||||
old_identifier=old_cycle.id,
|
||||
new_identifier=new_cycle.id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
@@ -937,7 +851,7 @@ def delete_cycle_issue_activity(
|
||||
issues = requested_data.get("issues")
|
||||
for issue in issues:
|
||||
current_issue = Issue.objects.filter(pk=issue).first()
|
||||
if current_issue:
|
||||
if issue:
|
||||
current_issue.updated_at = timezone.now()
|
||||
current_issue.save(update_fields=["updated_at"])
|
||||
issue_activities.append(
|
||||
@@ -979,11 +893,11 @@ def create_module_issue_activity(
|
||||
actor_id=actor_id,
|
||||
verb="created",
|
||||
old_value="",
|
||||
new_value=module.name if module else "",
|
||||
new_value=module.name,
|
||||
field="modules",
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"added module {module.name if module else ''}",
|
||||
comment=f"added module {module.name}",
|
||||
new_identifier=requested_data.get("module_id"),
|
||||
epoch=epoch,
|
||||
)
|
||||
@@ -1499,7 +1413,7 @@ def delete_issue_relation_activity(
|
||||
),
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment=f"deleted {requested_data.get('relation_type')} relation",
|
||||
comment=f'deleted {requested_data.get("relation_type")} relation',
|
||||
old_identifier=requested_data.get("related_issue"),
|
||||
epoch=epoch,
|
||||
)
|
||||
@@ -1635,160 +1549,6 @@ def create_intake_activity(
|
||||
)
|
||||
|
||||
|
||||
def create_issue_activity(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
actor_id,
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
issue = Issue.objects.get(pk=issue_id)
|
||||
issue_activity = IssueActivity.objects.create(
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment="created the issue",
|
||||
verb="created",
|
||||
actor_id=actor_id,
|
||||
epoch=epoch,
|
||||
)
|
||||
|
||||
issue_activity.created_at = issue.created_at
|
||||
issue_activity.actor_id = issue.created_by_id
|
||||
issue_activity.save(update_fields=["created_at", "actor_id"])
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
if requested_data.get("assignee_ids") is not None:
|
||||
track_assignees(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
actor_id,
|
||||
issue_activities,
|
||||
epoch,
|
||||
)
|
||||
|
||||
|
||||
def create_customer_activity(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
actor_id,
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
|
||||
if requested_data.get("customer_request_id") is not None:
|
||||
field = "customer_request"
|
||||
elif requested_data.get("customer_request_id") is None:
|
||||
field = "customer"
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
actor_id=actor_id,
|
||||
verb="created",
|
||||
comment="linked to the customer",
|
||||
new_value=requested_data.get("name"),
|
||||
old_value=None,
|
||||
field=field,
|
||||
new_identifier=requested_data.get("customer_id"),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_customer_activity(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
actor_id,
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
requested_data = json.loads(requested_data) if requested_data is not None else None
|
||||
|
||||
if requested_data.get("customer_request_id") is not None:
|
||||
field = "customer_request"
|
||||
elif requested_data.get("customer_request_id") is None:
|
||||
field = "customer"
|
||||
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
actor_id=actor_id,
|
||||
verb="deleted",
|
||||
comment="removed from customer",
|
||||
new_value=None,
|
||||
old_value=requested_data.get("name"),
|
||||
field=field,
|
||||
old_identifier=requested_data.get("customer_id"),
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def convert_epic_to_work_item_activity(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
actor_id,
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment="converted the epic to work item",
|
||||
field="epic",
|
||||
verb="converted",
|
||||
actor_id=actor_id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def convert_work_item_to_epic_activity(
|
||||
requested_data,
|
||||
current_instance,
|
||||
issue_id,
|
||||
project_id,
|
||||
workspace_id,
|
||||
actor_id,
|
||||
issue_activities,
|
||||
epoch,
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment="converted the work item to epic",
|
||||
field="work_item",
|
||||
verb="converted",
|
||||
actor_id=actor_id,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Receive message from room group
|
||||
@shared_task
|
||||
def issue_activity(
|
||||
@@ -1807,29 +1567,22 @@ def issue_activity(
|
||||
try:
|
||||
issue_activities = []
|
||||
|
||||
# check if project_id is valid
|
||||
if not is_valid_uuid(str(project_id)):
|
||||
return
|
||||
|
||||
project = Project.objects.get(pk=project_id)
|
||||
workspace_id = project.workspace_id
|
||||
|
||||
if issue_id is not None:
|
||||
issue = (
|
||||
Issue.objects.filter(pk=issue_id)
|
||||
.filter(Q(type__isnull=True) | Q(type__is_epic=False))
|
||||
.first()
|
||||
)
|
||||
if origin and issue:
|
||||
if origin:
|
||||
ri = redis_instance()
|
||||
# set the request origin in redis
|
||||
ri.set(str(issue_id), origin, ex=600)
|
||||
issue = Issue.objects.filter(pk=issue_id).first()
|
||||
if issue:
|
||||
try:
|
||||
issue.updated_at = timezone.now()
|
||||
issue.save(update_fields=["updated_at"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ACTIVITY_MAPPER = {
|
||||
"issue.activity.created": create_issue_activity,
|
||||
"issue.activity.updated": update_issue_activity,
|
||||
@@ -1858,11 +1611,6 @@ def issue_activity(
|
||||
"issue_draft.activity.updated": update_draft_issue_activity,
|
||||
"issue_draft.activity.deleted": delete_draft_issue_activity,
|
||||
"intake.activity.created": create_intake_activity,
|
||||
"epic.activity.created": create_epic_activity,
|
||||
"work_item.activity.converted": convert_epic_to_work_item_activity,
|
||||
"epic.activity.converted": convert_work_item_to_epic_activity,
|
||||
"customer.activity.created": create_customer_activity,
|
||||
"customer.activity.deleted": delete_customer_activity,
|
||||
}
|
||||
|
||||
func = ACTIVITY_MAPPER.get(type)
|
||||
@@ -1880,6 +1628,40 @@ def issue_activity(
|
||||
|
||||
# Save all the values to database
|
||||
issue_activities_created = IssueActivity.objects.bulk_create(issue_activities)
|
||||
# Post the updates to segway for integrations and webhooks
|
||||
if len(issue_activities_created):
|
||||
for activity in issue_activities_created:
|
||||
webhook_activity.delay(
|
||||
event=(
|
||||
"issue_comment"
|
||||
if activity.field == "comment"
|
||||
else "intake_issue"
|
||||
if intake
|
||||
else "issue"
|
||||
),
|
||||
event_id=(
|
||||
activity.issue_comment_id
|
||||
if activity.field == "comment"
|
||||
else intake
|
||||
if intake
|
||||
else activity.issue_id
|
||||
),
|
||||
verb=activity.verb,
|
||||
field=(
|
||||
"description" if activity.field == "comment" else activity.field
|
||||
),
|
||||
old_value=(
|
||||
activity.old_value if activity.old_value != "" else None
|
||||
),
|
||||
new_value=(
|
||||
activity.new_value if activity.new_value != "" else None
|
||||
),
|
||||
actor_id=activity.actor_id,
|
||||
current_site=origin,
|
||||
slug=activity.workspace.slug,
|
||||
old_identifier=activity.old_identifier,
|
||||
new_identifier=activity.new_identifier,
|
||||
)
|
||||
|
||||
if notification:
|
||||
notifications.delay(
|
||||
|
||||
@@ -5,7 +5,6 @@ import logging
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
@@ -71,89 +70,150 @@ MODEL_MAPPER = {
|
||||
}
|
||||
|
||||
|
||||
logger = logging.getLogger("plane.worker")
|
||||
|
||||
|
||||
def get_model_data(
|
||||
event: str, event_id: Union[str, List[str]], many: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Retrieve and serialize model data based on the event type.
|
||||
|
||||
Args:
|
||||
event (str): The type of event/model to retrieve data for
|
||||
event_id (Union[str, List[str]]): The ID or list of IDs of the model instance(s)
|
||||
many (bool): Whether to retrieve multiple instances
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Serialized model data
|
||||
|
||||
Raises:
|
||||
ValueError: If serializer is not found for the event
|
||||
ObjectDoesNotExist: If model instance is not found
|
||||
"""
|
||||
def get_model_data(event, event_id, many=False):
|
||||
model = MODEL_MAPPER.get(event)
|
||||
if model is None:
|
||||
raise ValueError(f"Model not found for event: {event}")
|
||||
if many:
|
||||
queryset = model.objects.filter(pk__in=event_id)
|
||||
else:
|
||||
queryset = model.objects.get(pk=event_id)
|
||||
serializer = SERIALIZER_MAPPER.get(event)
|
||||
return serializer(queryset, many=many).data
|
||||
|
||||
|
||||
@shared_task(
|
||||
bind=True,
|
||||
autoretry_for=(requests.RequestException,),
|
||||
retry_backoff=600,
|
||||
max_retries=5,
|
||||
retry_jitter=True,
|
||||
)
|
||||
def webhook_task(self, webhook, slug, event, event_data, action, current_site):
|
||||
try:
|
||||
if many:
|
||||
queryset = model.objects.filter(pk__in=event_id)
|
||||
else:
|
||||
queryset = model.objects.get(pk=event_id)
|
||||
webhook = Webhook.objects.get(id=webhook, workspace__slug=slug)
|
||||
|
||||
serializer = SERIALIZER_MAPPER.get(event)
|
||||
if serializer is None:
|
||||
raise ValueError(f"Serializer not found for event: {event}")
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Autopilot",
|
||||
"X-Plane-Delivery": str(uuid.uuid4()),
|
||||
"X-Plane-Event": event,
|
||||
}
|
||||
|
||||
return serializer(queryset, many=many).data
|
||||
except ObjectDoesNotExist:
|
||||
raise ObjectDoesNotExist(f"No {event} found with id: {event_id}")
|
||||
# # 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,
|
||||
}
|
||||
|
||||
# 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=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 Webhook.DoesNotExist:
|
||||
return
|
||||
except requests.RequestException as e:
|
||||
# Log the failed webhook request
|
||||
WebhookLog.objects.create(
|
||||
workspace_id=str(webhook.workspace_id),
|
||||
webhook=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 send_webhook_deactivation_email(
|
||||
webhook_id: str, receiver_id: str, current_site: str, reason: str
|
||||
) -> None:
|
||||
"""
|
||||
Send an email notification when a webhook is deactivated.
|
||||
def send_webhook_deactivation_email(webhook_id, receiver_id, current_site, reason):
|
||||
# Get email configurations
|
||||
(
|
||||
EMAIL_HOST,
|
||||
EMAIL_HOST_USER,
|
||||
EMAIL_HOST_PASSWORD,
|
||||
EMAIL_PORT,
|
||||
EMAIL_USE_TLS,
|
||||
EMAIL_USE_SSL,
|
||||
EMAIL_FROM,
|
||||
) = get_email_configuration()
|
||||
|
||||
receiver = User.objects.get(pk=receiver_id)
|
||||
webhook = Webhook.objects.get(pk=webhook_id)
|
||||
subject = "Webhook Deactivated"
|
||||
message = f"Webhook {webhook.url} has been deactivated due to failed requests."
|
||||
|
||||
# Send the mail
|
||||
context = {
|
||||
"email": receiver.email,
|
||||
"message": message,
|
||||
"webhook_url": f"{current_site}/{str(webhook.workspace.slug)}/settings/webhooks/{str(webhook.id)}",
|
||||
}
|
||||
html_content = render_to_string(
|
||||
"emails/notifications/webhook-deactivate.html", context
|
||||
)
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
Args:
|
||||
webhook_id (str): ID of the deactivated webhook
|
||||
receiver_id (str): ID of the user to receive the notification
|
||||
current_site (str): Current site URL
|
||||
reason (str): Reason for webhook deactivation
|
||||
"""
|
||||
try:
|
||||
(
|
||||
EMAIL_HOST,
|
||||
EMAIL_HOST_USER,
|
||||
EMAIL_HOST_PASSWORD,
|
||||
EMAIL_PORT,
|
||||
EMAIL_USE_TLS,
|
||||
EMAIL_USE_SSL,
|
||||
EMAIL_FROM,
|
||||
) = get_email_configuration()
|
||||
|
||||
receiver = User.objects.get(pk=receiver_id)
|
||||
webhook = Webhook.objects.get(pk=webhook_id)
|
||||
|
||||
# Get the webhook payload
|
||||
subject = "Webhook Deactivated"
|
||||
message = f"Webhook {webhook.url} has been deactivated due to failed requests."
|
||||
|
||||
# Send the mail
|
||||
context = {
|
||||
"email": receiver.email,
|
||||
"message": message,
|
||||
"webhook_url": f"{current_site}/{str(webhook.workspace.slug)}/settings/webhooks/{str(webhook.id)}",
|
||||
}
|
||||
html_content = render_to_string(
|
||||
"emails/notifications/webhook-deactivate.html", context
|
||||
)
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
# Set the email connection
|
||||
connection = get_connection(
|
||||
host=EMAIL_HOST,
|
||||
port=int(EMAIL_PORT),
|
||||
@@ -163,7 +223,6 @@ def send_webhook_deactivation_email(
|
||||
use_ssl=EMAIL_USE_SSL == "1",
|
||||
)
|
||||
|
||||
# Create the email message
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
@@ -173,10 +232,11 @@ def send_webhook_deactivation_email(
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
logger.info("Email sent successfully.")
|
||||
logging.getLogger("plane").info("Email sent successfully.")
|
||||
return
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
logger.error(f"Failed to send email: {e}")
|
||||
return
|
||||
|
||||
|
||||
@shared_task(
|
||||
@@ -187,29 +247,10 @@ def send_webhook_deactivation_email(
|
||||
retry_jitter=True,
|
||||
)
|
||||
def webhook_send_task(
|
||||
self,
|
||||
webhook_id: str,
|
||||
slug: str,
|
||||
event: str,
|
||||
event_data: Optional[Dict[str, Any]],
|
||||
action: str,
|
||||
current_site: str,
|
||||
activity: Optional[Dict[str, Any]],
|
||||
) -> None:
|
||||
"""
|
||||
Send webhook notifications to configured endpoints.
|
||||
|
||||
Args:
|
||||
webhook (str): Webhook ID
|
||||
slug (str): Workspace slug
|
||||
event (str): Event type
|
||||
event_data (Optional[Dict[str, Any]]): Event data to be sent
|
||||
action (str): HTTP method/action
|
||||
current_site (str): Current site URL
|
||||
activity (Optional[Dict[str, Any]]): Activity data
|
||||
"""
|
||||
self, webhook, slug, event, event_data, action, current_site, activity
|
||||
):
|
||||
try:
|
||||
webhook = Webhook.objects.get(id=webhook_id, workspace__slug=slug)
|
||||
webhook = Webhook.objects.get(id=webhook, workspace__slug=slug)
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -256,12 +297,7 @@ def webhook_send_task(
|
||||
)
|
||||
signature = hmac_signature.hexdigest()
|
||||
headers["X-Plane-Signature"] = signature
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
logger.error(f"Failed to send webhook: {e}")
|
||||
return
|
||||
|
||||
try:
|
||||
# Send the webhook event
|
||||
response = requests.post(webhook.url, headers=headers, json=payload, timeout=30)
|
||||
|
||||
@@ -278,7 +314,7 @@ def webhook_send_task(
|
||||
response_body=str(response.text),
|
||||
retry_count=str(self.request.retries),
|
||||
)
|
||||
logger.info(f"Webhook {webhook.id} sent successfully")
|
||||
|
||||
except requests.RequestException as e:
|
||||
# Log the failed webhook request
|
||||
WebhookLog.objects.create(
|
||||
@@ -293,13 +329,12 @@ def webhook_send_task(
|
||||
response_body=str(e),
|
||||
retry_count=str(self.request.retries),
|
||||
)
|
||||
logger.error(f"Webhook {webhook.id} failed with error: {e}")
|
||||
# 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.delay(
|
||||
send_webhook_deactivation_email(
|
||||
webhook_id=webhook.id,
|
||||
receiver_id=webhook.created_by_id,
|
||||
reason=str(e),
|
||||
@@ -309,50 +344,26 @@ def webhook_send_task(
|
||||
raise requests.RequestException()
|
||||
|
||||
except Exception as e:
|
||||
if settings.DEBUG:
|
||||
print(e)
|
||||
log_exception(e)
|
||||
return
|
||||
|
||||
|
||||
@shared_task
|
||||
def webhook_activity(
|
||||
event: str,
|
||||
verb: str,
|
||||
field: Optional[str],
|
||||
old_value: Any,
|
||||
new_value: Any,
|
||||
actor_id: str | uuid.UUID,
|
||||
slug: str,
|
||||
current_site: str,
|
||||
event_id: str | uuid.UUID,
|
||||
old_identifier: Optional[str],
|
||||
new_identifier: Optional[str],
|
||||
) -> None:
|
||||
"""
|
||||
Process and send webhook notifications for various activities in the system.
|
||||
|
||||
This task filters relevant webhooks based on the event type and sends notifications
|
||||
to all active webhooks for the workspace.
|
||||
|
||||
Args:
|
||||
event (str): Type of event (project, issue, module, cycle, issue_comment)
|
||||
verb (str): Action performed (created, updated, deleted)
|
||||
field (Optional[str]): Name of the field that was changed
|
||||
old_value (Any): Previous value of the field
|
||||
new_value (Any): New value of the field
|
||||
actor_id (str | uuid.UUID): ID of the user who performed the action
|
||||
slug (str): Workspace slug
|
||||
current_site (str): Current site URL
|
||||
event_id (str | uuid.UUID): ID of the event object
|
||||
old_identifier (Optional[str]): Previous identifier if any
|
||||
new_identifier (Optional[str]): New identifier if any
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Note:
|
||||
The function silently returns on ObjectDoesNotExist exceptions to handle
|
||||
race conditions where objects might have been deleted.
|
||||
"""
|
||||
event,
|
||||
verb,
|
||||
field,
|
||||
old_value,
|
||||
new_value,
|
||||
actor_id,
|
||||
slug,
|
||||
current_site,
|
||||
event_id,
|
||||
old_identifier,
|
||||
new_identifier,
|
||||
):
|
||||
try:
|
||||
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
|
||||
|
||||
@@ -373,7 +384,7 @@ def webhook_activity(
|
||||
|
||||
for webhook in webhooks:
|
||||
webhook_send_task.delay(
|
||||
webhook_id=webhook.id,
|
||||
webhook=webhook.id,
|
||||
slug=slug,
|
||||
event=event,
|
||||
event_data=(
|
||||
|
||||
@@ -7,7 +7,6 @@ const ignoredFieldUpdates = ["description", "attachment"];
|
||||
|
||||
export const handleIssueWebhook = async (payload: PlaneWebhookPayload) => {
|
||||
const activities = await getActivities(payload);
|
||||
|
||||
// Ideally we won't hit this case, but just in case
|
||||
if (activities.length === 0) {
|
||||
return;
|
||||
@@ -24,14 +23,14 @@ export const handleIssueWebhook = async (payload: PlaneWebhookPayload) => {
|
||||
const message = createSlackBlocksFromActivity(activities);
|
||||
const entityData = entityConnection.entity_data as any;
|
||||
|
||||
const channel = entityData.channel.id;
|
||||
const channel = entityData.channel || entityData.channel.id;
|
||||
const messageTs = entityData.message.ts;
|
||||
|
||||
if (message.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await slackService.sendThreadMessage(channel, messageTs, {
|
||||
const response = await slackService.sendThreadMessage(channel, messageTs, {
|
||||
text: "Work Item Updated",
|
||||
blocks: message,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user