From d22c551d935aa566fa89cd73de23eb649f300f35 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 5 Feb 2024 12:31:46 +0530 Subject: [PATCH 1/7] fix: mention rstrip error --- apiserver/plane/bgtasks/email_notification_task.py | 2 +- apiserver/plane/bgtasks/notification_task.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py index 713835033f6..3297ee18fc6 100644 --- a/apiserver/plane/bgtasks/email_notification_task.py +++ b/apiserver/plane/bgtasks/email_notification_task.py @@ -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 diff --git a/apiserver/plane/bgtasks/notification_task.py b/apiserver/plane/bgtasks/notification_task.py index 6cfbec72a96..0a843e4a63a 100644 --- a/apiserver/plane/bgtasks/notification_task.py +++ b/apiserver/plane/bgtasks/notification_task.py @@ -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"), }, }, ) From 73cfbdfe9643d21c58b701ba1b18a6da5d6fe629 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 5 Feb 2024 15:46:35 +0530 Subject: [PATCH 2/7] chore: webhook deactivation email --- apiserver/plane/app/views/base.py | 1 + .../plane/bgtasks/email_notification_task.py | 215 +++++------ apiserver/plane/bgtasks/webhook_task.py | 78 +++- apiserver/plane/settings/common.py | 2 - .../notifications/webhook-deactivate.html | 352 ++++++++++++++++++ 5 files changed, 535 insertions(+), 113 deletions(-) create mode 100644 apiserver/templates/emails/notifications/webhook-deactivate.html diff --git a/apiserver/plane/app/views/base.py b/apiserver/plane/app/views/base.py index e07cb811cc8..fa1e7559b06 100644 --- a/apiserver/plane/app/views/base.py +++ b/apiserver/plane/app/views/base.py @@ -64,6 +64,7 @@ def finalize_response(self, request, response, *args, **kwargs): action=self.request.method, slug=self.workspace_slug, bulk=self.bulk, + current_site=request.META.get("HTTP_ORIGIN"), ) return response diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py index 3297ee18fc6..9584d6f40f7 100644 --- a/apiserver/plane/bgtasks/email_notification_task.py +++ b/apiserver/plane/bgtasks/email_notification_task.py @@ -1,4 +1,3 @@ -import json from datetime import datetime # Third party imports @@ -9,7 +8,6 @@ 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 @@ -129,114 +127,117 @@ def create_payload(notification_data): 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, - }, - } + try: + 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), + } ) - 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, + 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)}", - }, - "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", + "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) + 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 diff --git a/apiserver/plane/bgtasks/webhook_task.py b/apiserver/plane/bgtasks/webhook_task.py index 34bba0cf87a..1e34b138893 100644 --- a/apiserver/plane/bgtasks/webhook_task.py +++ b/apiserver/plane/bgtasks/webhook_task.py @@ -7,6 +7,9 @@ # 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 @@ ModuleIssue, CycleIssue, IssueComment, + User, ) from plane.api.serializers import ( ProjectSerializer, - IssueSerializer, CycleSerializer, ModuleSerializer, CycleIssueSerializer, @@ -34,6 +37,9 @@ 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,55 @@ 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 = { + "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 diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 444248382ff..f032092504c 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -282,10 +282,8 @@ 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", diff --git a/apiserver/templates/emails/notifications/webhook-deactivate.html b/apiserver/templates/emails/notifications/webhook-deactivate.html new file mode 100644 index 00000000000..42b8efd093d --- /dev/null +++ b/apiserver/templates/emails/notifications/webhook-deactivate.html @@ -0,0 +1,352 @@ + + + + + + + + {{ message }} + + + + + + + + + + + + + + + + + + From 9762048ababfe752d47020716d54aa3b6906a20f Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Tue, 6 Feb 2024 15:58:49 +0530 Subject: [PATCH 3/7] chore: changed template --- apiserver/plane/bgtasks/webhook_task.py | 1 + .../notifications/webhook-deactivate.html | 1864 ++++++++++++++--- 2 files changed, 1529 insertions(+), 336 deletions(-) diff --git a/apiserver/plane/bgtasks/webhook_task.py b/apiserver/plane/bgtasks/webhook_task.py index 1e34b138893..605f48dd944 100644 --- a/apiserver/plane/bgtasks/webhook_task.py +++ b/apiserver/plane/bgtasks/webhook_task.py @@ -262,6 +262,7 @@ def send_webhook_deactivation_email(webhook_id, receiver_id, current_site, reaso # Send the mail context = { + "email": receiver.email, "message": message, "webhook_url":f"{current_site}/{str(webhook.workspace.slug)}/settings/webhooks/{str(webhook.id)}", } diff --git a/apiserver/templates/emails/notifications/webhook-deactivate.html b/apiserver/templates/emails/notifications/webhook-deactivate.html index 42b8efd093d..05439523514 100644 --- a/apiserver/templates/emails/notifications/webhook-deactivate.html +++ b/apiserver/templates/emails/notifications/webhook-deactivate.html @@ -1,352 +1,1544 @@ - - - - - - - {{ message }} - - - - - - - - - - - - - - + + + + + + From 159da8b46959b3bdccf4fc0781b5aa66e31ab74c Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Tue, 6 Feb 2024 17:34:40 +0530 Subject: [PATCH 4/7] chore: current site for external api's --- apiserver/plane/api/views/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py index b069ef78c1a..edb89f9b187 100644 --- a/apiserver/plane/api/views/base.py +++ b/apiserver/plane/api/views/base.py @@ -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 @@ def finalize_response(self, request, response, *args, **kwargs): 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 @@ def finalize_response(self, request, response, *args, **kwargs): action=self.request.method, slug=self.workspace_slug, bulk=self.bulk, + current_site=f"{scheme}://{netloc}", ) return response From 4ddfbdd57a3c5ab9648b867221782acdad103b75 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 7 Feb 2024 14:08:34 +0530 Subject: [PATCH 5/7] chore: mention in template displayed --- .../plane/bgtasks/email_notification_task.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py index 9584d6f40f7..ef85574c339 100644 --- a/apiserver/plane/bgtasks/email_notification_task.py +++ b/apiserver/plane/bgtasks/email_notification_task.py @@ -1,4 +1,6 @@ from datetime import datetime +from bs4 import BeautifulSoup + # Third party imports from celery import shared_task @@ -122,6 +124,25 @@ 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) + if soup.p: + soup.p['id'] = 'mention-box' + 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( @@ -152,6 +173,7 @@ def send_email_notification( 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( @@ -164,6 +186,19 @@ def send_email_notification( }, } ) + 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") From e35ca8b5682a2130a31e2f48cbddff26155e715b Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 7 Feb 2024 14:12:15 +0530 Subject: [PATCH 6/7] chore: mention tag fix --- apiserver/plane/bgtasks/email_notification_task.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py index ef85574c339..0e3e9330e6d 100644 --- a/apiserver/plane/bgtasks/email_notification_task.py +++ b/apiserver/plane/bgtasks/email_notification_task.py @@ -131,10 +131,8 @@ def process_mention(mention_component): user_id = mention['id'] user = User.objects.get(pk=user_id) user_name = user.display_name - highlighted_name = f'@{user_name}' + highlighted_name = f'@{user_name}' mention.replace_with(highlighted_name) - if soup.p: - soup.p['id'] = 'mention-box' return str(soup) def process_html_content(content): From a083cd07fe9c542420db283f49eea6a7f61cb002 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 7 Feb 2024 15:10:05 +0530 Subject: [PATCH 7/7] chore: comment user name displayed --- .../plane/bgtasks/email_notification_task.py | 2 +- .../emails/notifications/issue-updates.html | 46 +++++++++++-------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/apiserver/plane/bgtasks/email_notification_task.py b/apiserver/plane/bgtasks/email_notification_task.py index 0e3e9330e6d..9e9b348e197 100644 --- a/apiserver/plane/bgtasks/email_notification_task.py +++ b/apiserver/plane/bgtasks/email_notification_task.py @@ -131,7 +131,7 @@ def process_mention(mention_component): user_id = mention['id'] user = User.objects.get(pk=user_id) user_name = user.display_name - highlighted_name = f'@{user_name}' + highlighted_name = f"@{user_name}" mention.replace_with(highlighted_name) return str(soup) diff --git a/apiserver/templates/emails/notifications/issue-updates.html b/apiserver/templates/emails/notifications/issue-updates.html index fa50631c557..a7990562d47 100644 --- a/apiserver/templates/emails/notifications/issue-updates.html +++ b/apiserver/templates/emails/notifications/issue-updates.html @@ -108,25 +108,33 @@ margin-bottom: 15px; " /> - {% if actors_involved == 1 %} -

- {{summary}} - - {{ data.0.actor_detail.first_name}} - {{data.0.actor_detail.last_name}} - . -

- {% else %} -

- {{summary}} - - {{ data.0.actor_detail.first_name}} - {{data.0.actor_detail.last_name }} - and others. -

- {% endif %} - - + {% if actors_involved == 1 %} +

+ {{summary}} + + {% 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 %} + . +

+ {% else %} +

+ {{summary}} + + {% 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 %} + and others. +

+ {% endif %}