forked from github/plane
fix: email notification duplicates (#3719)
This commit is contained in:
parent
133c9b3ddb
commit
614096fd2f
@ -1,9 +1,9 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
|
from sentry_sdk import capture_exception
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -16,6 +16,17 @@ from plane.db.models import EmailNotificationLog, User, Issue
|
|||||||
from plane.license.utils.instance_value import get_email_configuration
|
from plane.license.utils.instance_value import get_email_configuration
|
||||||
from plane.settings.redis import redis_instance
|
from plane.settings.redis import redis_instance
|
||||||
|
|
||||||
|
# acquire and delete redis lock
|
||||||
|
def acquire_lock(lock_id, expire_time=300):
|
||||||
|
redis_client = redis_instance()
|
||||||
|
"""Attempt to acquire a lock with a specified expiration time."""
|
||||||
|
return redis_client.set(lock_id, 'true', nx=True, ex=expire_time)
|
||||||
|
|
||||||
|
def release_lock(lock_id):
|
||||||
|
"""Release a lock."""
|
||||||
|
redis_client = redis_instance()
|
||||||
|
redis_client.delete(lock_id)
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def stack_email_notification():
|
def stack_email_notification():
|
||||||
# get all email notifications
|
# get all email notifications
|
||||||
@ -142,135 +153,153 @@ def process_html_content(content):
|
|||||||
processed_content_list.append(processed_content)
|
processed_content_list.append(processed_content)
|
||||||
return processed_content_list
|
return processed_content_list
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def send_email_notification(
|
def send_email_notification(
|
||||||
issue_id, notification_data, receiver_id, email_notification_ids
|
issue_id, notification_data, receiver_id, email_notification_ids
|
||||||
):
|
):
|
||||||
|
# Convert UUIDs to a sorted, concatenated string
|
||||||
|
sorted_ids = sorted(email_notification_ids)
|
||||||
|
ids_str = "_".join(str(id) for id in sorted_ids)
|
||||||
|
lock_id = f"send_email_notif_{issue_id}_{receiver_id}_{ids_str}"
|
||||||
|
|
||||||
|
# acquire the lock for sending emails
|
||||||
try:
|
try:
|
||||||
ri = redis_instance()
|
if acquire_lock(lock_id=lock_id):
|
||||||
base_api = (ri.get(str(issue_id)).decode())
|
# get the redis instance
|
||||||
data = create_payload(notification_data=notification_data)
|
ri = redis_instance()
|
||||||
|
base_api = (ri.get(str(issue_id)).decode())
|
||||||
|
data = create_payload(notification_data=notification_data)
|
||||||
|
|
||||||
# Get email configurations
|
# Get email configurations
|
||||||
(
|
(
|
||||||
EMAIL_HOST,
|
EMAIL_HOST,
|
||||||
EMAIL_HOST_USER,
|
EMAIL_HOST_USER,
|
||||||
EMAIL_HOST_PASSWORD,
|
EMAIL_HOST_PASSWORD,
|
||||||
EMAIL_PORT,
|
EMAIL_PORT,
|
||||||
EMAIL_USE_TLS,
|
EMAIL_USE_TLS,
|
||||||
EMAIL_FROM,
|
EMAIL_FROM,
|
||||||
) = get_email_configuration()
|
) = get_email_configuration()
|
||||||
|
|
||||||
receiver = User.objects.get(pk=receiver_id)
|
receiver = User.objects.get(pk=receiver_id)
|
||||||
issue = Issue.objects.get(pk=issue_id)
|
issue = Issue.objects.get(pk=issue_id)
|
||||||
template_data = []
|
template_data = []
|
||||||
total_changes = 0
|
total_changes = 0
|
||||||
comments = []
|
comments = []
|
||||||
actors_involved = []
|
actors_involved = []
|
||||||
for actor_id, changes in data.items():
|
for actor_id, changes in data.items():
|
||||||
actor = User.objects.get(pk=actor_id)
|
actor = User.objects.get(pk=actor_id)
|
||||||
total_changes = total_changes + len(changes)
|
total_changes = total_changes + len(changes)
|
||||||
comment = changes.pop("comment", False)
|
comment = changes.pop("comment", False)
|
||||||
mention = changes.pop("mention", False)
|
mention = changes.pop("mention", False)
|
||||||
actors_involved.append(actor_id)
|
actors_involved.append(actor_id)
|
||||||
if comment:
|
if comment:
|
||||||
comments.append(
|
comments.append(
|
||||||
{
|
{
|
||||||
"actor_comments": comment,
|
"actor_comments": comment,
|
||||||
"actor_detail": {
|
"actor_detail": {
|
||||||
"avatar_url": actor.avatar,
|
"avatar_url": actor.avatar,
|
||||||
"first_name": actor.first_name,
|
"first_name": actor.first_name,
|
||||||
"last_name": actor.last_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),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
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:
|
summary = "Updates were made to the issue by"
|
||||||
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}"
|
||||||
# Send the mail
|
context = {
|
||||||
subject = f"{issue.project.identifier}-{issue.sequence_id} {issue.name}"
|
"data": template_data,
|
||||||
context = {
|
"summary": summary,
|
||||||
"data": template_data,
|
"actors_involved": len(set(actors_involved)),
|
||||||
"summary": summary,
|
"issue": {
|
||||||
"actors_involved": len(set(actors_involved)),
|
"issue_identifier": f"{str(issue.project.identifier)}-{str(issue.sequence_id)}",
|
||||||
"issue": {
|
"name": issue.name,
|
||||||
"issue_identifier": f"{str(issue.project.identifier)}-{str(issue.sequence_id)}",
|
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
|
||||||
"name": issue.name,
|
},
|
||||||
|
"receiver": {
|
||||||
|
"email": receiver.email,
|
||||||
|
},
|
||||||
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
|
"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/",
|
||||||
"receiver": {
|
"workspace":str(issue.project.workspace.slug),
|
||||||
"email": receiver.email,
|
"project": str(issue.project.name),
|
||||||
},
|
"user_preference": f"{base_api}/profile/preferences/email",
|
||||||
"issue_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/{str(issue.id)}",
|
"comments": comments,
|
||||||
"project_url": f"{base_api}/{str(issue.project.workspace.slug)}/projects/{str(issue.project.id)}/issues/",
|
}
|
||||||
"workspace":str(issue.project.workspace.slug),
|
html_content = render_to_string(
|
||||||
"project": str(issue.project.name),
|
"emails/notifications/issue-updates.html", context
|
||||||
"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",
|
|
||||||
)
|
)
|
||||||
|
text_content = strip_tags(html_content)
|
||||||
|
|
||||||
msg = EmailMultiAlternatives(
|
try:
|
||||||
subject=subject,
|
connection = get_connection(
|
||||||
body=text_content,
|
host=EMAIL_HOST,
|
||||||
from_email=EMAIL_FROM,
|
port=int(EMAIL_PORT),
|
||||||
to=[receiver.email],
|
username=EMAIL_HOST_USER,
|
||||||
connection=connection,
|
password=EMAIL_HOST_PASSWORD,
|
||||||
)
|
use_tls=EMAIL_USE_TLS == "1",
|
||||||
msg.attach_alternative(html_content, "text/html")
|
)
|
||||||
msg.send()
|
|
||||||
|
|
||||||
EmailNotificationLog.objects.filter(
|
msg = EmailMultiAlternatives(
|
||||||
pk__in=email_notification_ids
|
subject=subject,
|
||||||
).update(sent_at=timezone.now())
|
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())
|
||||||
|
|
||||||
|
# release the lock
|
||||||
|
release_lock(lock_id=lock_id)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
capture_exception(e)
|
||||||
|
# release the lock
|
||||||
|
release_lock(lock_id=lock_id)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print("Duplicate task recived. Skipping...")
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except (Issue.DoesNotExist, User.DoesNotExist) as e:
|
||||||
print(e)
|
release_lock(lock_id=lock_id)
|
||||||
return
|
|
||||||
except Issue.DoesNotExist:
|
|
||||||
return
|
return
|
||||||
|
Loading…
Reference in New Issue
Block a user