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..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): @@ -100,7 +104,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,) @@ -109,64 +113,207 @@ 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) - 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 = int(config.VIKUNJA_DEFAULT_PROJECT_ID) + vikunja_label_id = int(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_API_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 == 201 + ): + task_id = "unknown" + try: + task_id = task_response.json()["id"] + vikunja_task_url = f"{config.VIKUNJA_API_URL}/tasks/{task_id}" + label_body = { + "label_id": int(vikunja_label_id), + "created": "1970-01-01T00:00:00.000Z", + } + + label_response = requests.request( + "PUT", + f"{config.VIKUNJA_API_URL}/api/v1/tasks/{task_id}/labels", + json=label_body, + headers={ + "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 != 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: + 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 - 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, - ) + response = requests.request("POST", trello_url, params=querystring) + + 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: + 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 + 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 + ) - else: - return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) + if failed: + return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) + else: + 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..d0d450b2 100644 --- a/memberportal/membermatters/constance_config.py +++ b/memberportal/membermatters/constance_config.py @@ -120,15 +120,40 @@ 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.", + ), + # Discord config + "REPORT_ISSUE_ENABLE_DISCORD": ( + False, + "Enable the submit issue to Discord integration.", + ), # Vikunja config + "REPORT_ISSUE_ENABLE_VIKUNJA": ( + 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.", + ), + "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."), @@ -212,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.", @@ -416,6 +445,7 @@ "SMS_ENABLE", "TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", + "TWILIO_AUTH_TOKEN", "SMS_DEFAULT_COUNTRY_CODE", "SMS_SENDER_ID", "SMS_MESSAGES", @@ -432,11 +462,28 @@ "MEMBERBUCKS_CURRENCY", ), ), - ("Vikunja Integration", ("VIKUNJA_TEAMS",)), + ( + "Report Issue Services", + ( + "REPORT_ISSUE_ENABLE_EMAIL", + "REPORT_ISSUE_ENABLE_DISCORD", + "REPORT_ISSUE_ENABLE_VIKUNJA", + "REPORT_ISSUE_ENABLE_TRELLO", + ), + ), + ( + "Vikunja Integration", + ( + "VIKUNJA_TEAMS", + "VIKUNJA_API_URL", + "VIKUNJA_API_TOKEN", + "VIKUNJA_DEFAULT_PROJECT_ID", + "VIKUNJA_DEFAULT_LABEL_ID", + ), + ), ( "Trello Integration", ( - "ENABLE_TRELLO_INTEGRATION", "TRELLO_API_KEY", "TRELLO_API_TOKEN", "TRELLO_ID_LIST", @@ -508,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 524793a0..0791f8d2 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": { @@ -292,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