From bbbfa152ba14f37d0cd5686ed4cdcfc7cd83514e Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Thu, 17 Dec 2020 12:48:04 +0300 Subject: Adds Webhook Sending Functionality Builds and sends a discord webhook on form submission. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- backend/routes/forms/submit.py | 60 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) (limited to 'backend/routes/forms/submit.py') diff --git a/backend/routes/forms/submit.py b/backend/routes/forms/submit.py index 48ae4f6..7cd7576 100644 --- a/backend/routes/forms/submit.py +++ b/backend/routes/forms/submit.py @@ -9,11 +9,11 @@ import uuid import httpx import pydnsbl from pydantic import ValidationError +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 @@ -108,11 +108,63 @@ 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" }) + + @staticmethod + 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_", f"<@{user.id}>") + + # Post hook + httpx.post(form.meta.webhook.url, json=hook).raise_for_status() -- cgit v1.2.3 From cbd38cebcc32940de14eaa4af3927430026e9cea Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Fri, 18 Dec 2020 00:22:54 +0300 Subject: Sends Embed Asynchronously Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- backend/models/form.py | 2 +- backend/routes/forms/submit.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) (limited to 'backend/routes/forms/submit.py') diff --git a/backend/models/form.py b/backend/models/form.py index 6c435e7..9a3a8a2 100644 --- a/backend/models/form.py +++ b/backend/models/form.py @@ -66,7 +66,7 @@ class Form(BaseModel): questions: list[Question] name: str description: str - meta: _FormMeta + 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 c476468..538a5b9 100644 --- a/backend/routes/forms/submit.py +++ b/backend/routes/forms/submit.py @@ -152,7 +152,7 @@ class SubmitForm(Route): }) @staticmethod - def send_submission_webhook(form: Form, response: FormResponse) -> None: + 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: @@ -189,7 +189,9 @@ class SubmitForm(Route): # Set hook message message = form.meta.webhook.message if message: - hook["content"] = message.replace("_USER_MENTION_", f"<@{user.id}>") + hook["content"] = message.replace("_USER_MENTION_", user_mention) # Post hook - httpx.post(form.meta.webhook.url, json=hook).raise_for_status() + async with httpx.AsyncClient() as client: + r = await client.post(form.meta.webhook.url, json=hook) + r.raise_for_status() -- cgit v1.2.3 From df0a8a15c93dc238bfd55ee010a321b15f1e4179 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Fri, 18 Dec 2020 04:51:44 +0300 Subject: Change Embed Description Co-authored-by: Joe Banks --- backend/routes/forms/submit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'backend/routes/forms/submit.py') diff --git a/backend/routes/forms/submit.py b/backend/routes/forms/submit.py index d8b178f..c50e4dd 100644 --- a/backend/routes/forms/submit.py +++ b/backend/routes/forms/submit.py @@ -165,7 +165,7 @@ class SubmitForm(Route): # Build Embed embed = { "title": "New Form Response", - "description": f"{user_mention} submitted a response.", + "description": f"{user_mention} submitted a response to `{form.name}`.", "url": f"{FRONTEND_URL}/path_to_view_form/{response.id}", # noqa # TODO: Enter Form View URL "timestamp": response.timestamp, "color": 7506394, -- cgit v1.2.3 From 34c94baba9758ae300fe8641252c621768543abc Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Fri, 18 Dec 2020 11:25:00 +0300 Subject: Uses Builtin User Class Uses builtin user formatting for username and mentions. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- backend/authentication/user.py | 4 ++++ backend/routes/forms/submit.py | 26 +++++++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) (limited to 'backend/routes/forms/submit.py') diff --git a/backend/authentication/user.py b/backend/authentication/user.py index 722c348..f40c68c 100644 --- a/backend/authentication/user.py +++ b/backend/authentication/user.py @@ -19,3 +19,7 @@ class User(BaseUser): def display_name(self) -> str: """Return username and discriminator as display name.""" return f"{self.payload['username']}#{self.payload['discriminator']}" + + @property + def discord_mention(self) -> str: + return f"<@{self.payload['id']}>" diff --git a/backend/routes/forms/submit.py b/backend/routes/forms/submit.py index c50e4dd..3b22155 100644 --- a/backend/routes/forms/submit.py +++ b/backend/routes/forms/submit.py @@ -138,7 +138,8 @@ class SubmitForm(Route): send_webhook = BackgroundTask( self.send_submission_webhook, form=form, - response=response_obj + response=response_obj, + request_user=request.user ) return JSONResponse({ @@ -152,30 +153,37 @@ class SubmitForm(Route): }, status_code=404) @staticmethod - async def send_submission_webhook(form: Form, response: FormResponse) -> None: + async def send_submission_webhook( + form: Form, + response: FormResponse, + request_user: Request.user + ) -> 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.") + try: + mention = request_user.discord_mention + except AttributeError: + mention = "User" + 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 to `{form.name}`.", + "description": f"{mention} submitted a response to `{form.name}`.", "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 request_user.is_authenticated: + embed["author"] = {"name": request_user.display_name} - if user.avatar is not None: + if user and user.avatar: url = f"https://cdn.discordapp.com/avatars/{user.id}/{user.avatar}.png" embed["author"]["icon_url"] = url @@ -189,7 +197,7 @@ class SubmitForm(Route): # Set hook message message = form.meta.webhook.message if message: - hook["content"] = message.replace("_USER_MENTION_", user_mention) + hook["content"] = message.replace("_USER_MENTION_", mention) # Post hook async with httpx.AsyncClient() as client: -- cgit v1.2.3 From 9d8ca77b1f18891cef88e9b8235b41172067f9b0 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Mon, 21 Dec 2020 04:41:27 +0300 Subject: Adds and Documents Webhook Message Variables Adds better parsing and formatting for webhook message variables, and documents them in SCHEMA.md. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- SCHEMA.md | 18 +++++++++++++++--- backend/routes/forms/submit.py | 12 ++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) (limited to 'backend/routes/forms/submit.py') diff --git a/SCHEMA.md b/SCHEMA.md index b55635f..fa5f247 100644 --- a/SCHEMA.md +++ b/SCHEMA.md @@ -33,9 +33,21 @@ In this document: | `WEBHOOK_ENABLED` | The form should notify the webhook. Has no effect if no webhook is set. | ### Meta options -| Field | Description | Example | -| --------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | -| `webhook` | Mapping of webhook url and message. Message can use `_USER_MENTION_` to mention the submitting user. | `"webhook": {"url": "https://discord.com/api/webhooks/id/key", "message": "Hello World! _USER_MENTION_"}` | +| Field | Description | Example | +| --------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `webhook` | Mapping of webhook url and message. Message can use certain [context variables](#webhook-variables). | `"webhook": {"url": "https://discord.com/api/webhooks/id/key", "message": "{user} submitted a form."}` | + + +#### Webhook Variables +The following variables can be used in a webhook's message. The variables must be wrapped by braces (`{}`). + +| Name | Description | +| ------------- | ---------------------------------------------------------------------------- | +| `user` | A discord mention of the user submitting the form, or "User" if unavailable. | +| `response_id` | ID of the submitted response. | +| `form` | Name of the submitted form. | +| `form_id` | ID of the submitted form. | +| `time` | ISO submission timestamp. | ### Form question diff --git a/backend/routes/forms/submit.py b/backend/routes/forms/submit.py index 3b22155..5c0cfdd 100644 --- a/backend/routes/forms/submit.py +++ b/backend/routes/forms/submit.py @@ -197,6 +197,18 @@ class SubmitForm(Route): # Set hook message message = form.meta.webhook.message if message: + # Available variables, see SCHEMA.md + ctx = { + "user": mention, + "response_id": response.id, + "form": form.name, + "form_id": form.id, + "time": response.timestamp, + } + + for key in ctx: + message = message.replace(f"{{{key}}}", str(ctx[key])) + hook["content"] = message.replace("_USER_MENTION_", mention) # Post hook -- cgit v1.2.3