aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps/api/views.py
diff options
context:
space:
mode:
authorGravatar Johannes Christ <[email protected]>2023-12-10 15:39:11 +0100
committerGravatar Johannes Christ <[email protected]>2023-12-11 09:58:06 +0100
commit182c1356833d30f77bd1b83b138d7e5e57d63dd6 (patch)
treef8f90e99b8510cc1174dc00e1c3d6e73e92e7ca7 /pydis_site/apps/api/views.py
parentMerge pull request #1167 from python-discord/dependabot/pip/pre-commit-3.6.0 (diff)
Implement the github-filter worker in the API
The current github-filter worker, found at https://github.com/python-discord/workers/blob/main/github-filter/src/index.ts, fails to work at present because Discord's webhook endpoints block Cloudflare's IP ranges from accessing this endpoint. Whilst they use Cloudflare to guard themselves, it seems they do not wish others to use it. Implement it on the site to circumvent IP restrictions and allow to modify the code in Python.
Diffstat (limited to 'pydis_site/apps/api/views.py')
-rw-r--r--pydis_site/apps/api/views.py104
1 files changed, 104 insertions, 0 deletions
diff --git a/pydis_site/apps/api/views.py b/pydis_site/apps/api/views.py
index 829086e7..8a9eebd7 100644
--- a/pydis_site/apps/api/views.py
+++ b/pydis_site/apps/api/views.py
@@ -1,3 +1,8 @@
+import json
+import urllib.request
+from collections.abc import Mapping
+
+from rest_framework import status
from rest_framework.exceptions import ParseError
from rest_framework.request import Request
from rest_framework.response import Response
@@ -226,3 +231,102 @@ class GitHubArtifactsView(APIView):
"error": str(e),
"requested_resource": f"{owner}/{repo}/{sha}/{action_name}/{artifact_name}"
}, status=e.status)
+
+
+class GitHubWebhookFilterView(APIView):
+ """
+ Filters uninteresting events from webhooks sent by GitHub to Discord.
+
+ ## Routes
+ ### POST /github/webhook-filter/:webhook_id/:webhook_token
+ Takes the GitHub webhook payload as the request body, documented on here:
+ https://docs.github.com/en/webhooks/webhook-events-and-payloads. The endpoint
+ will then determine whether the sent webhook event is of interest,
+ and if so, will forward it to Discord. The response from Discord is
+ then returned back to the client of this website, including the original
+ status code and headers (excluding `Content-Type`).
+
+ ## Authentication
+ Does not require any authentication nor permissions on its own, however,
+ Discord will validate that the webhook originates from GitHub and respond
+ with a 403 forbidden error if not.
+ """
+
+ authentication_classes = ()
+ permission_classes = ()
+
+ def post(self, request: Request, *, webhook_id: str, webhook_token: str) -> Response:
+ """Filter a webhook POST from GitHub before sending it to Discord."""
+ sender = request.data.get('sender', {})
+ sender_name = sender.get('login', '')
+ event = request.headers.get('X-GitHub-Event')
+ repository = request.data.get('repository', {})
+
+ is_coveralls = 'coveralls' in sender_name
+ is_github_bot = sender.get('type') == 'bot'
+ is_sentry = 'sentry-io' in sender_name
+ is_dependabot_branch_deletion = (
+ 'dependabot' in request.data.get('ref', '')
+ and event == 'delete'
+ )
+ is_bot_pr_approval = (
+ '[bot]' in request.data.get('pull_request', {}).get('user', {}).get('login', '')
+ and event == 'pull_request_review'
+ )
+ is_empty_review = (
+ request.data.get('review', {}).get('state') == 'commented'
+ and event == 'pull_request_review'
+ and request.data.get('review', {}).get('body') is None
+ )
+ is_black_non_main_push = (
+ request.data.get('ref') != 'refs/heads/main'
+ and repository.get('name') == 'black'
+ and repository.get('owner', {}).get('login') == 'psf'
+ and event == 'push'
+ )
+
+ is_bot_payload = (
+ is_coveralls
+ or (is_github_bot and not is_sentry)
+ or is_dependabot_branch_deletion
+ or is_bot_pr_approval
+ )
+ is_noisy_user_action = is_empty_review
+ should_ignore = is_bot_payload or is_noisy_user_action or is_black_non_main_push
+
+ if should_ignore:
+ return Response(
+ {'message': "Ignored by github-filter endpoint"},
+ status=status.HTTP_203_NON_AUTHORITATIVE_INFORMATION,
+ )
+
+ (response_status, headers, body) = self.send_webhook(
+ webhook_id, webhook_token, request.data, dict(request.headers),
+ )
+ headers.pop('Connection', None)
+ headers.pop('Content-Length', None)
+ return Response(data=body, headers=headers, status=response_status)
+
+ def send_webhook(
+ self,
+ webhook_id: str,
+ webhook_token: str,
+ data: dict,
+ headers: Mapping[str, str],
+ ) -> tuple[int, dict[str, str], bytes]:
+ """Execute a webhook on Discord's GitHub webhook endpoint."""
+ payload = json.dumps(data).encode()
+ headers.pop('Content-Length', None)
+ headers.pop('Content-Type', None)
+ headers.pop('Host', None)
+ request = urllib.request.Request( # noqa: S310
+ f'https://discord.com/api/webhooks/{webhook_id}/{webhook_token}/github?wait=1',
+ data=payload,
+ headers={'Content-Type': 'application/json', **headers},
+ )
+
+ try:
+ with urllib.request.urlopen(request) as response: # noqa: S310
+ return (response.status, dict(response.getheaders()), response.read())
+ except urllib.error.HTTPError as err: # pragma: no cover
+ return (err.code, dict(err.headers), err.fp.read())