forked from github/plane
chore: mention notification and webhook faliure (#3573)
* fix: mention rstrip error * chore: webhook deactivation email * chore: changed template * chore: current site for external api's * chore: mention in template displayed * chore: mention tag fix * chore: comment user name displayed
This commit is contained in:
parent
4563b50fad
commit
76db394ab1
@ -1,6 +1,8 @@
|
||||
# Python imports
|
||||
import zoneinfo
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
@ -51,6 +53,11 @@ class WebhookMixin:
|
||||
and self.request.method in ["POST", "PATCH", "DELETE"]
|
||||
and response.status_code in [200, 201, 204]
|
||||
):
|
||||
url = request.build_absolute_uri()
|
||||
parsed_url = urlparse(url)
|
||||
# Extract the scheme and netloc
|
||||
scheme = parsed_url.scheme
|
||||
netloc = parsed_url.netloc
|
||||
# Push the object to delay
|
||||
send_webhook.delay(
|
||||
event=self.webhook_event,
|
||||
@ -59,6 +66,7 @@ class WebhookMixin:
|
||||
action=self.request.method,
|
||||
slug=self.workspace_slug,
|
||||
bulk=self.bulk,
|
||||
current_site=f"{scheme}://{netloc}",
|
||||
)
|
||||
|
||||
return response
|
||||
|
@ -64,6 +64,7 @@ class WebhookMixin:
|
||||
action=self.request.method,
|
||||
slug=self.workspace_slug,
|
||||
bulk=self.bulk,
|
||||
current_site=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return response
|
||||
|
@ -1,5 +1,6 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
@ -9,7 +10,6 @@ from django.utils import timezone
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import EmailNotificationLog, User, Issue
|
||||
@ -40,7 +40,7 @@ def stack_email_notification():
|
||||
processed_notifications = []
|
||||
# Loop through all the issues to create the emails
|
||||
for receiver_id in receivers:
|
||||
# Notifcation triggered for the receiver
|
||||
# Notification triggered for the receiver
|
||||
receiver_notifications = [
|
||||
notification
|
||||
for notification in email_notifications
|
||||
@ -124,119 +124,153 @@ def create_payload(notification_data):
|
||||
|
||||
return data
|
||||
|
||||
def process_mention(mention_component):
|
||||
soup = BeautifulSoup(mention_component, 'html.parser')
|
||||
mentions = soup.find_all('mention-component')
|
||||
for mention in mentions:
|
||||
user_id = mention['id']
|
||||
user = User.objects.get(pk=user_id)
|
||||
user_name = user.display_name
|
||||
highlighted_name = f"@{user_name}"
|
||||
mention.replace_with(highlighted_name)
|
||||
return str(soup)
|
||||
|
||||
def process_html_content(content):
|
||||
processed_content_list = []
|
||||
for html_content in content:
|
||||
processed_content = process_mention(html_content)
|
||||
processed_content_list.append(processed_content)
|
||||
return processed_content_list
|
||||
|
||||
@shared_task
|
||||
def send_email_notification(
|
||||
issue_id, notification_data, receiver_id, email_notification_ids
|
||||
):
|
||||
ri = redis_instance()
|
||||
base_api = (ri.get(str(issue_id)).decode())
|
||||
data = create_payload(notification_data=notification_data)
|
||||
|
||||
# Get email configurations
|
||||
(
|
||||
EMAIL_HOST,
|
||||
EMAIL_HOST_USER,
|
||||
EMAIL_HOST_PASSWORD,
|
||||
EMAIL_PORT,
|
||||
EMAIL_USE_TLS,
|
||||
EMAIL_FROM,
|
||||
) = get_email_configuration()
|
||||
|
||||
receiver = User.objects.get(pk=receiver_id)
|
||||
issue = Issue.objects.get(pk=issue_id)
|
||||
template_data = []
|
||||
total_changes = 0
|
||||
comments = []
|
||||
actors_involved = []
|
||||
for actor_id, changes in data.items():
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
total_changes = total_changes + len(changes)
|
||||
comment = changes.pop("comment", False)
|
||||
actors_involved.append(actor_id)
|
||||
if comment:
|
||||
comments.append(
|
||||
{
|
||||
"actor_comments": comment,
|
||||
"actor_detail": {
|
||||
"avatar_url": actor.avatar,
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
}
|
||||
)
|
||||
activity_time = changes.pop("activity_time")
|
||||
# Parse the input string into a datetime object
|
||||
formatted_time = datetime.strptime(activity_time, "%Y-%m-%d %H:%M:%S").strftime("%H:%M %p")
|
||||
|
||||
if changes:
|
||||
template_data.append(
|
||||
{
|
||||
"actor_detail": {
|
||||
"avatar_url": actor.avatar,
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
"changes": changes,
|
||||
"issue_details": {
|
||||
"name": issue.name,
|
||||
"identifier": f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
},
|
||||
"activity_time": str(formatted_time),
|
||||
}
|
||||
)
|
||||
|
||||
summary = "Updates were made to the issue by"
|
||||
|
||||
# Send the mail
|
||||
subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
|
||||
context = {
|
||||
"data": template_data,
|
||||
"summary": summary,
|
||||
"actors_involved": len(set(actors_involved)),
|
||||
"issue": {
|
||||
"issue_identifier": f"{str(issue.project.identifier)}-{str(issue.sequence_id)}",
|
||||
"name": issue.name,
|
||||
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
|
||||
},
|
||||
"receiver": {
|
||||
"email": receiver.email,
|
||||
},
|
||||
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
|
||||
"project_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/",
|
||||
"workspace":str(issue.project.workspace.slug),
|
||||
"project": str(issue.project.name),
|
||||
"user_preference": f"{base_api}/profile/preferences/email",
|
||||
"comments": comments,
|
||||
}
|
||||
html_content = render_to_string(
|
||||
"emails/notifications/issue-updates.html", context
|
||||
)
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
try:
|
||||
connection = get_connection(
|
||||
host=EMAIL_HOST,
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
ri = redis_instance()
|
||||
base_api = (ri.get(str(issue_id)).decode())
|
||||
data = create_payload(notification_data=notification_data)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=EMAIL_FROM,
|
||||
to=[receiver.email],
|
||||
connection=connection,
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
# Get email configurations
|
||||
(
|
||||
EMAIL_HOST,
|
||||
EMAIL_HOST_USER,
|
||||
EMAIL_HOST_PASSWORD,
|
||||
EMAIL_PORT,
|
||||
EMAIL_USE_TLS,
|
||||
EMAIL_FROM,
|
||||
) = get_email_configuration()
|
||||
|
||||
EmailNotificationLog.objects.filter(
|
||||
pk__in=email_notification_ids
|
||||
).update(sent_at=timezone.now())
|
||||
return
|
||||
except Exception as e:
|
||||
print(e)
|
||||
receiver = User.objects.get(pk=receiver_id)
|
||||
issue = Issue.objects.get(pk=issue_id)
|
||||
template_data = []
|
||||
total_changes = 0
|
||||
comments = []
|
||||
actors_involved = []
|
||||
for actor_id, changes in data.items():
|
||||
actor = User.objects.get(pk=actor_id)
|
||||
total_changes = total_changes + len(changes)
|
||||
comment = changes.pop("comment", False)
|
||||
mention = changes.pop("mention", False)
|
||||
actors_involved.append(actor_id)
|
||||
if comment:
|
||||
comments.append(
|
||||
{
|
||||
"actor_comments": comment,
|
||||
"actor_detail": {
|
||||
"avatar_url": actor.avatar,
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
}
|
||||
)
|
||||
if mention:
|
||||
mention["new_value"] = process_html_content(mention.get("new_value"))
|
||||
mention["old_value"] = process_html_content(mention.get("old_value"))
|
||||
comments.append(
|
||||
{
|
||||
"actor_comments": mention,
|
||||
"actor_detail": {
|
||||
"avatar_url": actor.avatar,
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
}
|
||||
)
|
||||
activity_time = changes.pop("activity_time")
|
||||
# Parse the input string into a datetime object
|
||||
formatted_time = datetime.strptime(activity_time, "%Y-%m-%d %H:%M:%S").strftime("%H:%M %p")
|
||||
|
||||
if changes:
|
||||
template_data.append(
|
||||
{
|
||||
"actor_detail": {
|
||||
"avatar_url": actor.avatar,
|
||||
"first_name": actor.first_name,
|
||||
"last_name": actor.last_name,
|
||||
},
|
||||
"changes": changes,
|
||||
"issue_details": {
|
||||
"name": issue.name,
|
||||
"identifier": f"{issue.project.identifier}-{issue.sequence_id}",
|
||||
},
|
||||
"activity_time": str(formatted_time),
|
||||
}
|
||||
)
|
||||
|
||||
summary = "Updates were made to the issue by"
|
||||
|
||||
# Send the mail
|
||||
subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
|
||||
context = {
|
||||
"data": template_data,
|
||||
"summary": summary,
|
||||
"actors_involved": len(set(actors_involved)),
|
||||
"issue": {
|
||||
"issue_identifier": f"{str(issue.project.identifier)}-{str(issue.sequence_id)}",
|
||||
"name": issue.name,
|
||||
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
|
||||
},
|
||||
"receiver": {
|
||||
"email": receiver.email,
|
||||
},
|
||||
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
|
||||
"project_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/",
|
||||
"workspace":str(issue.project.workspace.slug),
|
||||
"project": str(issue.project.name),
|
||||
"user_preference": f"{base_api}/profile/preferences/email",
|
||||
"comments": comments,
|
||||
}
|
||||
html_content = render_to_string(
|
||||
"emails/notifications/issue-updates.html", context
|
||||
)
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
try:
|
||||
connection = get_connection(
|
||||
host=EMAIL_HOST,
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=EMAIL_FROM,
|
||||
to=[receiver.email],
|
||||
connection=connection,
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
|
||||
EmailNotificationLog.objects.filter(
|
||||
pk__in=email_notification_ids
|
||||
).update(sent_at=timezone.now())
|
||||
return
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return
|
||||
except Issue.DoesNotExist:
|
||||
return
|
||||
|
@ -515,7 +515,7 @@ def notifications(
|
||||
bulk_email_logs.append(
|
||||
EmailNotificationLog(
|
||||
triggered_by_id=actor_id,
|
||||
receiver_id=subscriber,
|
||||
receiver_id=mention_id,
|
||||
entity_identifier=issue_id,
|
||||
entity_name="issue",
|
||||
data={
|
||||
@ -552,6 +552,7 @@ def notifications(
|
||||
"old_value": str(
|
||||
issue_activity.get("old_value")
|
||||
),
|
||||
"activity_time": issue_activity.get("created_at"),
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -639,6 +640,7 @@ def notifications(
|
||||
"old_value": str(
|
||||
last_activity.old_value
|
||||
),
|
||||
"activity_time": issue_activity.get("created_at"),
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -695,6 +697,7 @@ def notifications(
|
||||
"old_value"
|
||||
)
|
||||
),
|
||||
"activity_time": issue_activity.get("created_at"),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -7,6 +7,9 @@ import hmac
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
@ -22,10 +25,10 @@ from plane.db.models import (
|
||||
ModuleIssue,
|
||||
CycleIssue,
|
||||
IssueComment,
|
||||
User,
|
||||
)
|
||||
from plane.api.serializers import (
|
||||
ProjectSerializer,
|
||||
IssueSerializer,
|
||||
CycleSerializer,
|
||||
ModuleSerializer,
|
||||
CycleIssueSerializer,
|
||||
@ -34,6 +37,9 @@ from plane.api.serializers import (
|
||||
IssueExpandSerializer,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
|
||||
SERIALIZER_MAPPER = {
|
||||
"project": ProjectSerializer,
|
||||
"issue": IssueExpandSerializer,
|
||||
@ -72,7 +78,7 @@ def get_model_data(event, event_id, many=False):
|
||||
max_retries=5,
|
||||
retry_jitter=True,
|
||||
)
|
||||
def webhook_task(self, webhook, slug, event, event_data, action):
|
||||
def webhook_task(self, webhook, slug, event, event_data, action, current_site):
|
||||
try:
|
||||
webhook = Webhook.objects.get(id=webhook, workspace__slug=slug)
|
||||
|
||||
@ -151,7 +157,18 @@ def webhook_task(self, webhook, slug, event, event_data, action):
|
||||
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:
|
||||
@ -162,7 +179,7 @@ def webhook_task(self, webhook, slug, event, event_data, action):
|
||||
|
||||
|
||||
@shared_task()
|
||||
def send_webhook(event, payload, kw, action, slug, bulk):
|
||||
def send_webhook(event, payload, kw, action, slug, bulk, current_site):
|
||||
try:
|
||||
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
|
||||
|
||||
@ -216,6 +233,7 @@ def send_webhook(event, payload, kw, action, slug, bulk):
|
||||
event=event,
|
||||
event_data=data,
|
||||
action=action,
|
||||
current_site=current_site,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@ -223,3 +241,56 @@ def send_webhook(event, payload, kw, action, slug, bulk):
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return
|
||||
|
||||
|
||||
@shared_task
|
||||
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_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)
|
||||
|
||||
try:
|
||||
connection = get_connection(
|
||||
host=EMAIL_HOST,
|
||||
port=int(EMAIL_PORT),
|
||||
username=EMAIL_HOST_USER,
|
||||
password=EMAIL_HOST_PASSWORD,
|
||||
use_tls=EMAIL_USE_TLS == "1",
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=EMAIL_FROM,
|
||||
to=[receiver.email],
|
||||
connection=connection,
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
|
||||
return
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return
|
||||
|
@ -282,10 +282,8 @@ if REDIS_SSL:
|
||||
redis_url = os.environ.get("REDIS_URL")
|
||||
broker_url = f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
|
||||
CELERY_BROKER_URL = broker_url
|
||||
CELERY_RESULT_BACKEND = broker_url
|
||||
else:
|
||||
CELERY_BROKER_URL = REDIS_URL
|
||||
CELERY_RESULT_BACKEND = REDIS_URL
|
||||
|
||||
CELERY_IMPORTS = (
|
||||
"plane.bgtasks.issue_automation_task",
|
||||
|
@ -108,25 +108,33 @@
|
||||
margin-bottom: 15px;
|
||||
"
|
||||
/>
|
||||
{% if actors_involved == 1 %}
|
||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
||||
{{summary}}
|
||||
<span style="font-size: 1rem; font-weight: 700; line-height: 28px">
|
||||
{{ data.0.actor_detail.first_name}}
|
||||
{{data.0.actor_detail.last_name}}
|
||||
</span>.
|
||||
</p>
|
||||
{% else %}
|
||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
||||
{{summary}}
|
||||
<span style="font-size: 1rem; font-weight: 700; line-height: 28px">
|
||||
{{ data.0.actor_detail.first_name}}
|
||||
{{data.0.actor_detail.last_name }}
|
||||
</span>and others.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if actors_involved == 1 %}
|
||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
||||
{{summary}}
|
||||
<span style="font-size: 1rem; font-weight: 700; line-height: 28px">
|
||||
{% if data|length > 0 %}
|
||||
{{ data.0.actor_detail.first_name}}
|
||||
{{data.0.actor_detail.last_name}}
|
||||
{% else %}
|
||||
{{ comments.0.actor_detail.first_name}}
|
||||
{{comments.0.actor_detail.last_name}}
|
||||
{% endif %}
|
||||
</span>.
|
||||
</p>
|
||||
{% else %}
|
||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
||||
{{summary}}
|
||||
<span style="font-size: 1rem; font-weight: 700; line-height: 28px">
|
||||
{% if data|length > 0 %}
|
||||
{{ data.0.actor_detail.first_name}}
|
||||
{{data.0.actor_detail.last_name}}
|
||||
{% else %}
|
||||
{{ comments.0.actor_detail.first_name}}
|
||||
{{comments.0.actor_detail.last_name}}
|
||||
{% endif %}
|
||||
</span>and others.
|
||||
</p>
|
||||
{% endif %}
|
||||
<!-- {% if actors_involved == 1 %}
|
||||
{% if data|length > 0 and comments|length == 0 %}
|
||||
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px">
|
||||
|
1544
apiserver/templates/emails/notifications/webhook-deactivate.html
Normal file
1544
apiserver/templates/emails/notifications/webhook-deactivate.html
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user