From bf95e9480b965fedef69c6219331395ffe38f338 Mon Sep 17 00:00:00 2001 From: Jaimyn Mayer Date: Wed, 7 Aug 2024 15:57:19 +1000 Subject: [PATCH 1/6] added report issue vikunja integration --- memberportal/api_general/views.py | 1 - memberportal/api_member_tools/views.py | 208 ++++++++++++++---- .../membermatters/constance_config.py | 40 +++- 3 files changed, 196 insertions(+), 53 deletions(-) diff --git a/memberportal/api_general/views.py b/memberportal/api_general/views.py index e004f813..a14ff517 100644 --- a/memberportal/api_general/views.py +++ b/memberportal/api_general/views.py @@ -37,7 +37,6 @@ def get(self, request): "memberbucks_topup_options": json.loads( config.STRIPE_MEMBERBUCKS_TOPUP_OPTIONS ), - "trelloIntegration": config.ENABLE_TRELLO_INTEGRATION, "enableProxyVoting": config.ENABLE_PROXY_VOTING, "enableStripe": config.ENABLE_STRIPE and len(config.STRIPE_PUBLISHABLE_KEY) > 0 diff --git a/memberportal/api_member_tools/views.py b/memberportal/api_member_tools/views.py index 4dfb962a..ef0cb15e 100644 --- a/memberportal/api_member_tools/views.py +++ b/memberportal/api_member_tools/views.py @@ -100,7 +100,7 @@ def get(self, request): class IssueDetail(APIView): """ - post: Creates a new issue by creating a trello card or emailing the management committee + post: Creates a new issue by creating a task card or emailing the management committee """ permission_classes = (permissions.IsAuthenticated,) @@ -113,60 +113,172 @@ def post(self, request): if not (title and description): return Response(status=status.HTTP_400_BAD_REQUEST) - use_trello = config.ENABLE_TRELLO_INTEGRATION - trello_key = config.TRELLO_API_KEY - trello_token = config.TRELLO_API_TOKEN - trello_id_list = config.TRELLO_ID_LIST - - if use_trello: - url = "https://api.trello.com/1/cards" - - querystring = { - "name": title, - "desc": description, - "pos": "top", - "idList": trello_id_list, - "keepFromSource": "all", - "key": trello_key, - "token": trello_token, - } + failed = False - response = requests.request("POST", url, params=querystring) + request.user.log_event( + "Submitted issue: " + title + " Content: " + description, + "generic", + ) - if response.status_code == 200: - request.user.log_event( - "Submitted issue: " + title + " Content: " + description, - "generic", - ) + if config.REPORT_ISSUE_ENABLE_VIKUNJA and bool( + config.VIKUNJA_DEFAULT_PROJECT_ID + ): + vikunja_project_id = config.VIKUNJA_DEFAULT_PROJECT_ID + vikunja_label_id = config.VIKUNJA_DEFAULT_LABEL_ID + + try: + task_body = { + "max_right": None, + "id": 0, + "title": title, + "description": description, + "done": False, + "done_at": None, + "priority": 0, + "labels": [], + "assignees": [], + "due_date": None, + "start_date": None, + "end_date": None, + "repeat_after": 0, + "repeat_from_current_date": False, + "repeat_mode": 0, + "reminders": [], + "parent_task_id": 0, + "hex_color": "", + "percent_done": 0, + "related_tasks": {}, + "attachments": [], + "cover_image_attachment_id": None, + "identifier": "", + "index": 0, + "is_favorite": False, + "subscription": None, + "position": 64, + "reactions": {}, + "created_by": { + "max_right": None, + "id": 0, + "email": "", + "username": "", + "name": "", + "exp": 0, + "type": 0, + "created": None, + "updated": None, + "settings": { + "max_right": None, + "name": "", + "email_reminders_enabled": False, + "discoverable_by_name": False, + "discoverable_by_email": True, + "overdue_tasks_reminders_enabled": False, + "week_start": 0, + "timezone": "", + "language": "en", + "frontend_settings": { + "play_sound_when_done": False, + "quick_add_magic_mode": "vikunja", + "color_schema": "auto", + "default_view": "first", + }, + }, + }, + "created": "1970-01-01T00:00:00.000Z", + "updated": "1970-01-01T00:00:00.000Z", + "project_id": vikunja_project_id, + "bucket_id": 0, + "reminder_dates": None, + } - return Response( - {"success": True, "url": response.json()["shortUrl"]}, - status=status.HTTP_201_CREATED, + task_response = requests.request( + "PUT", + f"{config.VIKUNJA_URL}/api/v1/projects/{vikunja_project_id}/tasks", + json=task_body, + headers={"Authorization": "Bearer " + config.VIKUNJA_API_TOKEN}, ) - else: - return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) + if (vikunja_label_id is not None) and ( + task_response.status_code == 200 + ): + try: + task_id = task_response.json["id"] + label_body = { + "label_id": vikunja_label_id, + "created": "1970-01-01T00:00:00.000Z", + } + + label_response = requests.request( + "PUT", + f"{config.VIKUNJA_URL}/api/v1/tasks/{task_id}/labels", + json=label_body, + headers={ + "Authorization": "Bearer " + config.VIKUNJA_API_TOKEN + }, + ) + except Exception: + pass + + if task_response.status_code != 200: + failed = True + + except Exception: + # uh oh, but don't stop processing other ones + failed = True + + if config.REPORT_ISSUE_ENABLE_TRELLO: + try: + trello_key = config.TRELLO_API_KEY + trello_token = config.TRELLO_API_TOKEN + trello_id_list = config.TRELLO_ID_LIST + trello_url = "https://api.trello.com/1/cards" + + querystring = { + "name": title, + "desc": description, + "pos": "top", + "idList": trello_id_list, + "keepFromSource": "all", + "key": trello_key, + "token": trello_token, + } - # if Trello isn't configured, use email instead + response = requests.request("POST", trello_url, params=querystring) + + if response.status_code != 200: + failed = True + + except Exception: + # uh oh, but don't stop processing other ones + failed = True + + # email report + if config.REPORT_ISSUE_ENABLE_EMAIL: + try: + subject = f"{request.user.profile.get_full_name()}: {title}" + + if not send_email_to_admin( + subject=subject, + template_vars={ + "title": subject, + "message": description, + }, + user=request.user, + reply_to=request.user.email, + ): + failed = True + + except Exception: + # uh oh, but don't stop processing other ones + failed = True + + if failed: + return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) else: - subject = f"{request.user.profile.get_full_name()} submitted an issue about {title}" - - if send_email_to_admin( - subject=subject, - template_vars={ - "title": subject, - "message": description, - }, - user=request.user, - reply_to=request.user.email, - ): - return Response( - {"success": True}, - status=status.HTTP_201_CREATED, - ) - - else: - return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response( + {"success": True}, + status=status.HTTP_201_CREATED, + ) class MeetingList(APIView): diff --git a/memberportal/membermatters/constance_config.py b/memberportal/membermatters/constance_config.py index eb32922c..8be57585 100644 --- a/memberportal/membermatters/constance_config.py +++ b/memberportal/membermatters/constance_config.py @@ -120,15 +120,33 @@ False, "Enable integration with stripe for membership payments.", ), + # ==== Report Issue Services ==== + # Email config + "REPORT_ISSUE_ENABLE_EMAIL": ( + True, + "Enable the submit issue to email integration.", + ), # Vikunja config + "REPORT_ISSUE_ENABLE_VIKUNJA": ( + False, + "Enable the submit issue to Vikunja integration.", + ), + "VIKUNJA_DEFAULT_PROJECT_ID": ( + "", + "Set this to the ID of your default project to create issues in.", + ), + "VIKUNJA_DEFAULT_LABEL_ID": ( + "", + "[optional] Set this to the ID of your default label if you want new issues to be tagged.", + ), "VIKUNJA_TEAMS": ( '[{"name": "Members", "oidcID": "members", "description": "The default team for all members.", "isPublic": false}]', "A JSON array of Vikunja teams to add users to when they login via SSO. Returned as an OIDC claim with the 'vikunja_teams' scope. Check Vikunja docs for syntax.", ), # Trello config - "ENABLE_TRELLO_INTEGRATION": ( + "REPORT_ISSUE_ENABLE_TRELLO": ( False, - "Enable the submit issue to trello integration. If disabled we'll send an email to EMAIL_ADMIN instead.", + "Enable the submit issue to trello integration.", ), "TRELLO_API_KEY": ("", "Set this to your Trello API key."), "TRELLO_API_TOKEN": ("", "Set this to your Trello API token."), @@ -432,11 +450,25 @@ "MEMBERBUCKS_CURRENCY", ), ), - ("Vikunja Integration", ("VIKUNJA_TEAMS",)), + ( + "Report Issue Services", + ( + "REPORT_ISSUE_ENABLE_EMAIL", + "REPORT_ISSUE_ENABLE_VIKUNJA", + "REPORT_ISSUE_ENABLE_TRELLO", + ), + ), + ( + "Vikunja Integration", + ( + "VIKUNJA_TEAMS", + "VIKUNJA_DEFAULT_PROJECT_ID", + "VIKUNJA_DEFAULT_LABEL_ID", + ), + ), ( "Trello Integration", ( - "ENABLE_TRELLO_INTEGRATION", "TRELLO_API_KEY", "TRELLO_API_TOKEN", "TRELLO_ID_LIST", From c458c0951cf5462694ae4764dd2f91002e40fa0d Mon Sep 17 00:00:00 2001 From: Jaimyn Mayer Date: Wed, 7 Aug 2024 16:28:11 +1000 Subject: [PATCH 2/6] added vikunja api token/url to config --- memberportal/api_member_tools/views.py | 4 ++-- memberportal/membermatters/constance_config.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/memberportal/api_member_tools/views.py b/memberportal/api_member_tools/views.py index ef0cb15e..89339553 100644 --- a/memberportal/api_member_tools/views.py +++ b/memberportal/api_member_tools/views.py @@ -193,7 +193,7 @@ def post(self, request): task_response = requests.request( "PUT", - f"{config.VIKUNJA_URL}/api/v1/projects/{vikunja_project_id}/tasks", + f"{config.VIKUNJA_API_URL}/api/v1/projects/{vikunja_project_id}/tasks", json=task_body, headers={"Authorization": "Bearer " + config.VIKUNJA_API_TOKEN}, ) @@ -210,7 +210,7 @@ def post(self, request): label_response = requests.request( "PUT", - f"{config.VIKUNJA_URL}/api/v1/tasks/{task_id}/labels", + f"{config.VIKUNJA_API_URL}/api/v1/tasks/{task_id}/labels", json=label_body, headers={ "Authorization": "Bearer " + config.VIKUNJA_API_TOKEN diff --git a/memberportal/membermatters/constance_config.py b/memberportal/membermatters/constance_config.py index 8be57585..0040c024 100644 --- a/memberportal/membermatters/constance_config.py +++ b/memberportal/membermatters/constance_config.py @@ -131,6 +131,8 @@ False, "Enable the submit issue to Vikunja integration.", ), + "VIKUNJA_API_URL": ("", "Set this to your Vikunja instance public URL."), + "VIKUNJA_API_TOKEN": ("", "Set this to your Vikunja API token."), "VIKUNJA_DEFAULT_PROJECT_ID": ( "", "Set this to the ID of your default project to create issues in.", @@ -462,6 +464,8 @@ "Vikunja Integration", ( "VIKUNJA_TEAMS", + "VIKUNJA_URL", + "VIKUNJA_API_TOKEN", "VIKUNJA_DEFAULT_PROJECT_ID", "VIKUNJA_DEFAULT_LABEL_ID", ), From 939e3e3b43bda5ffeff5e1d849f661e84cdab6d1 Mon Sep 17 00:00:00 2001 From: Jaimyn Mayer Date: Wed, 7 Aug 2024 16:34:23 +1000 Subject: [PATCH 3/6] fixed typo in constance --- memberportal/membermatters/constance_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/memberportal/membermatters/constance_config.py b/memberportal/membermatters/constance_config.py index 0040c024..86c86f5d 100644 --- a/memberportal/membermatters/constance_config.py +++ b/memberportal/membermatters/constance_config.py @@ -464,7 +464,7 @@ "Vikunja Integration", ( "VIKUNJA_TEAMS", - "VIKUNJA_URL", + "VIKUNJA_API_URL", "VIKUNJA_API_TOKEN", "VIKUNJA_DEFAULT_PROJECT_ID", "VIKUNJA_DEFAULT_LABEL_ID", From 7df3c67eacb68ea96896204aabfec0aab4e0a17f Mon Sep 17 00:00:00 2001 From: Jaimyn Mayer Date: Sun, 11 Aug 2024 12:08:29 +1000 Subject: [PATCH 4/6] cast vikunja label and project ID to int --- memberportal/api_member_tools/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/memberportal/api_member_tools/views.py b/memberportal/api_member_tools/views.py index 89339553..9889894a 100644 --- a/memberportal/api_member_tools/views.py +++ b/memberportal/api_member_tools/views.py @@ -123,8 +123,8 @@ def post(self, request): if config.REPORT_ISSUE_ENABLE_VIKUNJA and bool( config.VIKUNJA_DEFAULT_PROJECT_ID ): - vikunja_project_id = config.VIKUNJA_DEFAULT_PROJECT_ID - vikunja_label_id = config.VIKUNJA_DEFAULT_LABEL_ID + vikunja_project_id = int(config.VIKUNJA_DEFAULT_PROJECT_ID) + vikunja_label_id = int(config.VIKUNJA_DEFAULT_LABEL_ID) try: task_body = { From 84db9bfdf1dd7ff4b8cec966036f004a84b98a71 Mon Sep 17 00:00:00 2001 From: Jaimyn Mayer Date: Sun, 11 Aug 2024 13:50:39 +1000 Subject: [PATCH 5/6] removed deprecated MMDB_SECRET env var --- memberportal/membermatters/settings.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/memberportal/membermatters/settings.py b/memberportal/membermatters/settings.py index 524793a0..f7660f99 100644 --- a/memberportal/membermatters/settings.py +++ b/memberportal/membermatters/settings.py @@ -153,20 +153,6 @@ "PORT": os.environ.get("POSTGRES_PORT", "5432"), } } -elif "MMDB_SECRET" in os.environ: - # This is a JSON blob containing the database connection details, generated by "copilot" in an AWS deployment - # Fields in this JSON blob are: {username, host, dbname, password, port} - database_config = json.loads(os.environ["MMDB_SECRET"]) - DATABASES = { - "default": { - "ENGINE": "django_prometheus.db.backends.mysql", - "NAME": database_config.get("dbname"), - "USER": database_config.get("username"), - "PASSWORD": database_config.get("password"), - "HOST": database_config.get("host"), - "PORT": database_config.get("port"), - } - } else: DATABASES = { "default": { From e41cc6a80f2bc5c1b62bb1d66e5ae0c172ef5074 Mon Sep 17 00:00:00 2001 From: Jaimyn Mayer Date: Sun, 11 Aug 2024 13:50:56 +1000 Subject: [PATCH 6/6] added report issue to discord feature --- memberportal/api_member_tools/views.py | 45 ++++++++++++++++--- .../membermatters/constance_config.py | 12 +++++ memberportal/membermatters/settings.py | 5 +++ memberportal/services/discord.py | 36 +++++++++++++++ 4 files changed, 93 insertions(+), 5 deletions(-) diff --git a/memberportal/api_member_tools/views.py b/memberportal/api_member_tools/views.py index 9889894a..fe78b792 100644 --- a/memberportal/api_member_tools/views.py +++ b/memberportal/api_member_tools/views.py @@ -3,13 +3,17 @@ from api_meeting.models import Meeting from constance import config from services.emails import send_email_to_admin +from services import discord + from random import shuffle import requests from django.utils import timezone - from rest_framework import status, permissions from rest_framework.response import Response from rest_framework.views import APIView +import logging + +logger = logging.getLogger("api_member_tools") class SwipesList(APIView): @@ -109,6 +113,8 @@ def post(self, request): body = request.data title = body["title"] description = request.user.profile.get_full_name() + ": " + body["description"] + vikunja_task_url = None + trello_card_url = None if not (title and description): return Response(status=status.HTTP_400_BAD_REQUEST) @@ -199,12 +205,14 @@ def post(self, request): ) if (vikunja_label_id is not None) and ( - task_response.status_code == 200 + task_response.status_code == 201 ): + task_id = "unknown" try: - task_id = task_response.json["id"] + task_id = task_response.json()["id"] + vikunja_task_url = f"{config.VIKUNJA_API_URL}/tasks/{task_id}" label_body = { - "label_id": vikunja_label_id, + "label_id": int(vikunja_label_id), "created": "1970-01-01T00:00:00.000Z", } @@ -216,15 +224,29 @@ def post(self, request): "Authorization": "Bearer " + config.VIKUNJA_API_TOKEN }, ) + + if label_response.status_code != 201: + logger.warning( + f"Failed to add label to Vikunja task {task_id}: %s", + label_response.json(), + ) + except Exception: + logger.exception( + f"Failed to add label to Vikunja task {task_id}." + ) pass - if task_response.status_code != 200: + if task_response.status_code != 201: + logger.error( + "Failed to create Vikunja task: %s", task_response.json() + ) failed = True except Exception: # uh oh, but don't stop processing other ones failed = True + logger.exception("Failed to create reported issue Vikunja task.") if config.REPORT_ISSUE_ENABLE_TRELLO: try: @@ -248,9 +270,12 @@ def post(self, request): if response.status_code != 200: failed = True + trello_card_url = response.json()["shortUrl"] + except Exception: # uh oh, but don't stop processing other ones failed = True + logger.exception("Failed to create reported issue Trello card.") # email report if config.REPORT_ISSUE_ENABLE_EMAIL: @@ -271,6 +296,16 @@ def post(self, request): except Exception: # uh oh, but don't stop processing other ones failed = True + logger.exception("Failed to send reported issue email.") + + # discord report + if config.REPORT_ISSUE_ENABLE_DISCORD: + username = request.user.profile.get_full_name() + description = body["description"] + + discord.post_reported_issue_to_discord( + username, title, description, vikunja_task_url, trello_card_url + ) if failed: return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/memberportal/membermatters/constance_config.py b/memberportal/membermatters/constance_config.py index 86c86f5d..d0d450b2 100644 --- a/memberportal/membermatters/constance_config.py +++ b/memberportal/membermatters/constance_config.py @@ -126,6 +126,11 @@ True, "Enable the submit issue to email integration.", ), + # Discord config + "REPORT_ISSUE_ENABLE_DISCORD": ( + False, + "Enable the submit issue to Discord integration.", + ), # Vikunja config "REPORT_ISSUE_ENABLE_VIKUNJA": ( False, @@ -232,6 +237,10 @@ "https://discordapp.com/api/webhooks/", "Discord URL to send webhook notifications to for vending/memberbucks purchases.", ), + "DISCORD_REPORT_ISSUE_WEBHOOK": ( + "https://discordapp.com/api/webhooks/", + "Discord URL to send webhook notifications to when reporting issues.", + ), "ENABLE_DISCOURSE_SSO_PROTOCOL": ( False, "Enable support for the discourse SSO protocol.", @@ -436,6 +445,7 @@ "SMS_ENABLE", "TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", + "TWILIO_AUTH_TOKEN", "SMS_DEFAULT_COUNTRY_CODE", "SMS_SENDER_ID", "SMS_MESSAGES", @@ -456,6 +466,7 @@ "Report Issue Services", ( "REPORT_ISSUE_ENABLE_EMAIL", + "REPORT_ISSUE_ENABLE_DISCORD", "REPORT_ISSUE_ENABLE_VIKUNJA", "REPORT_ISSUE_ENABLE_TRELLO", ), @@ -544,6 +555,7 @@ "DISCORD_DOOR_WEBHOOK", "DISCORD_INTERLOCK_WEBHOOK", "DISCORD_MEMBERBUCKS_PURCHASE_WEBHOOK", + "DISCORD_REPORT_ISSUE_WEBHOOK", ), ), ] diff --git a/memberportal/membermatters/settings.py b/memberportal/membermatters/settings.py index f7660f99..0791f8d2 100644 --- a/memberportal/membermatters/settings.py +++ b/memberportal/membermatters/settings.py @@ -278,6 +278,11 @@ "level": os.environ.get("MM_LOG_LEVEL_SPACEDIRECTORY", "INFO"), "propagate": False, }, + "api_member_tools": { + "handlers": ["console", "file"], + "level": os.environ.get("MM_LOG_LEVEL_MEMBER_TOOLS", "INFO"), + "propagate": False, + }, "metrics": { "handlers": ["console", "file"], "level": os.environ.get("MM_LOG_LEVEL_METRICS", "INFO"), diff --git a/memberportal/services/discord.py b/memberportal/services/discord.py index ce878dc6..ebb5b1f3 100644 --- a/memberportal/services/discord.py +++ b/memberportal/services/discord.py @@ -178,3 +178,39 @@ def post_purchase_to_discord(description): return True return True + + +def post_reported_issue_to_discord( + fullname, title, description, vikunja_task_url=None, trello_card_url=None +): + if config.ENABLE_DISCORD_INTEGRATION and config.DISCORD_REPORT_ISSUE_WEBHOOK: + logger.debug("Posting reported issue to Discord!") + + url = config.DISCORD_REPORT_ISSUE_WEBHOOK + + if vikunja_task_url or trello_card_url: + description += ( + f"\n\n[View in Vikunja]({vikunja_task_url})" if vikunja_task_url else "" + ) + description += ( + f"\n\n[View in Trello]({trello_card_url})" if trello_card_url else "" + ) + + json_message = { + "content": f"{fullname} just reported a new issue!", + "embeds": [], + } + + json_message["embeds"].append( + { + "title": title, + "description": description, + "color": 5025616, + } + ) + try: + requests.post(url, json=json_message, timeout=settings.REQUEST_TIMEOUT) + except requests.exceptions.ReadTimeout: + return True + + return True