diff options
Diffstat (limited to 'backend')
-rw-r--r-- | backend/constants.py | 2 | ||||
-rw-r--r-- | backend/models/form.py | 51 | ||||
-rw-r--r-- | backend/routes/forms/submit.py | 66 |
3 files changed, 113 insertions, 6 deletions
diff --git a/backend/constants.py b/backend/constants.py index 4218bff..f5e4304 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -6,6 +6,7 @@ import binascii # noqa from enum import Enum # noqa +FRONTEND_URL = os.getenv("FRONTEND_URL", "https://forms.pythondiscord.com") DATABASE_URL = os.getenv("DATABASE_URL") MONGO_DATABASE = os.getenv("MONGO_DATABASE", "pydis_forms") @@ -60,3 +61,4 @@ class FormFeatures(Enum): OPEN = "OPEN" COLLECT_EMAIL = "COLLECT_EMAIL" DISABLE_ANTISPAM = "DISABLE_ANTISPAM" + WEBHOOK_ENABLED = "WEBHOOK_ENABLED" diff --git a/backend/models/form.py b/backend/models/form.py index 9d8ffaa..9a3a8a2 100644 --- a/backend/models/form.py +++ b/backend/models/form.py @@ -1,5 +1,6 @@ import typing as t +import httpx from pydantic import BaseModel, Field, validator from backend.constants import FormFeatures @@ -8,6 +9,55 @@ from .question import Question PUBLIC_FIELDS = ["id", "features", "questions", "name", "description"] +class _WebHook(BaseModel): + """Schema model of discord webhooks.""" + url: str + message: t.Optional[str] + + @validator("url") + def validate_url(cls, url: str) -> str: + """Checks if discord webhook urls are valid.""" + if not isinstance(url, str): + raise ValueError("Webhook URL must be a string.") + + if "discord.com/api/webhooks/" not in url: + raise ValueError("URL must be a discord webhook.") + + # Attempt to connect to URL + try: + httpx.get(url).raise_for_status() + + except httpx.RequestError as e: + # Catch exceptions in request format + raise ValueError( + f"Encountered error while trying to connect to url: `{e}`" + ) + + except httpx.HTTPStatusError as e: + # Catch exceptions in response + status = e.response.status_code + + if status == 401: + raise ValueError( + "Could not authenticate with target. Please check the webhook url." + ) + elif status == 404: + raise ValueError( + "Target could not find webhook url. Please check the webhook url." + ) + else: + raise ValueError( + f"Unknown error ({status}) while connecting to target: {e}" + ) + + return url + + +class _FormMeta(BaseModel): + """Schema model for form meta data.""" + webhook: _WebHook = None + + class Form(BaseModel): """Schema model for form.""" @@ -16,6 +66,7 @@ class Form(BaseModel): questions: list[Question] name: str description: str + meta: _FormMeta = _FormMeta() class Config: allow_population_by_field_name = True diff --git a/backend/routes/forms/submit.py b/backend/routes/forms/submit.py index 42fcf79..d8b178f 100644 --- a/backend/routes/forms/submit.py +++ b/backend/routes/forms/submit.py @@ -4,19 +4,19 @@ Submit a form. import binascii import hashlib -from typing import Any, Optional import uuid +from typing import Any, Optional import httpx -from pydantic.main import BaseModel import pydnsbl from pydantic import ValidationError +from pydantic.main import BaseModel from spectree import Response +from starlette.background import BackgroundTask from starlette.requests import Request - from starlette.responses import JSONResponse -from backend.constants import HCAPTCHA_API_SECRET, FormFeatures +from backend.constants import FRONTEND_URL, FormFeatures, HCAPTCHA_API_SECRET from backend.models import Form, FormResponse from backend.route import Route from backend.validation import AuthorizationHeaders, ErrorMessage, api @@ -133,11 +133,65 @@ class SubmitForm(Route): response_obj.dict(by_alias=True) ) + send_webhook = None + if FormFeatures.WEBHOOK_ENABLED.value in form.features: + send_webhook = BackgroundTask( + self.send_submission_webhook, + form=form, + response=response_obj + ) + return JSONResponse({ - "form": form.dict(), + "form": form.dict(admin=False), "response": response_obj.dict() - }) + }, background=send_webhook) + else: return JSONResponse({ "error": "Open form not found" }, status_code=404) + + @staticmethod + async def send_submission_webhook(form: Form, response: FormResponse) -> None: + """Helper to send a submission message to a discord webhook.""" + # Stop if webhook is not available + if form.meta.webhook is None: + raise ValueError("Got empty webhook.") + + user = response.user + username = f"{user.username}#{user.discriminator}" if user else None + user_mention = f"<@{user.id}>" if user else f"{username or 'User'}" + + # Build Embed + embed = { + "title": "New Form Response", + "description": f"{user_mention} submitted a response.", + "url": f"{FRONTEND_URL}/path_to_view_form/{response.id}", # noqa # TODO: Enter Form View URL + "timestamp": response.timestamp, + "color": 7506394, + } + + # Add author to embed + if user is not None: + embed["author"] = {"name": username} + + if user.avatar is not None: + url = f"https://cdn.discordapp.com/avatars/{user.id}/{user.avatar}.png" + embed["author"]["icon_url"] = url + + # Build Hook + hook = { + "embeds": [embed], + "allowed_mentions": {"parse": ["users", "roles"]}, + "username": form.name or "Python Discord Forms" + } + + # Set hook message + message = form.meta.webhook.message + if message: + hook["content"] = message.replace("_USER_MENTION_", user_mention) + + # Post hook + async with httpx.AsyncClient() as client: + r = await client.post(form.meta.webhook.url, json=hook) + r.raise_for_status() |