aboutsummaryrefslogtreecommitdiffstats
path: root/backend
diff options
context:
space:
mode:
Diffstat (limited to 'backend')
-rw-r--r--backend/constants.py2
-rw-r--r--backend/models/form.py51
-rw-r--r--backend/routes/forms/submit.py66
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()