feat: webhook activity payload reverted (#3094)

* chore: revert webhook activity task

* fix: channel id
This commit is contained in:
Henit Chobisa
2025-04-30 22:33:17 +05:30
committed by GitHub
parent 1844581db9
commit 44ca3f02bc
3 changed files with 228 additions and 436 deletions

View File

@@ -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(

View File

@@ -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=(

View File

@@ -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,
});