From 3842c9aa34c1300f652db469b3cb66e16f321df9 Mon Sep 17 00:00:00 2001 From: Krrish Sehgal <133865424+krrish-sehgal@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:19:31 +0530 Subject: [PATCH] Automated GitHub badges through webhook (#3045) * initial * pre-commit fix * auto * user auth * webhooks added * fix * auth added * env ex * print remoe * rev changes --- .env.example | 4 +- blt/urls.py | 2 + website/views/user.py | 125 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index ca69c212f..18896652e 100644 --- a/.env.example +++ b/.env.example @@ -28,4 +28,6 @@ DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${POSTGR SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 SLACK_CLIENT_ID= -SLACK_CLIENT_SECRET= \ No newline at end of file +SLACK_CLIENT_SECRET= + +GITHUB_ACCESS_TOKEN="abc123" \ No newline at end of file diff --git a/blt/urls.py b/blt/urls.py index ab314b3c6..4281927b9 100644 --- a/blt/urls.py +++ b/blt/urls.py @@ -164,6 +164,7 @@ deletions, follow_user, get_score, + github_webhook, invite_friend, profile, profile_edit, @@ -638,6 +639,7 @@ ), path("delete_time_entry/", delete_time_entry, name="delete_time_entry"), path("assign-badge//", assign_badge, name="assign_badge"), + path("github-webhook/", github_webhook, name="github-webhook"), ] if settings.DEBUG: diff --git a/website/views/user.py b/website/views/user.py index 672d4acdc..2570d69e8 100644 --- a/website/views/user.py +++ b/website/views/user.py @@ -1,3 +1,5 @@ +import hashlib +import hmac import json import os from datetime import datetime, timezone @@ -28,6 +30,7 @@ from django.template.loader import render_to_string from django.urls import reverse from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt from django.views.generic import DetailView, ListView, TemplateView, View from rest_framework.authtoken.models import Token from rest_framework.authtoken.views import ObtainAuthToken @@ -874,3 +877,125 @@ def assign_badge(request, username): UserBadge.objects.create(user=user, badge=badge, awarded_by=request.user, reason=reason) messages.success(request, f"{badge.title} badge assigned to {user.username}.") return redirect("profile", slug=username) + + +@csrf_exempt +def github_webhook(request): + if request.method == "POST": + # Validate GitHub signature + signature = request.headers.get("X-Hub-Signature-256") + if not validate_signature(request.body, signature): + return JsonResponse({"status": "error", "message": "Unauthorized request"}, status=403) + + payload = json.loads(request.body) + event_type = request.headers.get("X-GitHub-Event", "") + + event_handlers = { + "pull_request": handle_pull_request_event, + "push": handle_push_event, + "pull_request_review": handle_review_event, + "issues": handle_issue_event, + "status": handle_status_event, + "fork": handle_fork_event, + "create": handle_create_event, + } + + handler = event_handlers.get(event_type) + if handler: + return handler(payload) + else: + return JsonResponse({"status": "error", "message": "Unhandled event type"}, status=400) + else: + return JsonResponse({"status": "error", "message": "Invalid method"}, status=400) + + +def handle_pull_request_event(payload): + if payload["action"] == "closed" and payload["pull_request"]["merged"]: + pr_user_profile = UserProfile.objects.filter( + github_url=payload["pull_request"]["user"]["html_url"] + ).first() + if pr_user_profile: + pr_user_instance = pr_user_profile.user + assign_github_badge(pr_user_instance, "First PR Merged") + return JsonResponse({"status": "success"}, status=200) + + +def handle_push_event(payload): + pusher_profile = UserProfile.objects.filter(github_url=payload["sender"]["html_url"]).first() + if pusher_profile: + pusher_user = pusher_profile.user + if payload.get("commits"): + assign_github_badge(pusher_user, "First Commit") + return JsonResponse({"status": "success"}, status=200) + + +def handle_review_event(payload): + reviewer_profile = UserProfile.objects.filter(github_url=payload["sender"]["html_url"]).first() + if reviewer_profile: + reviewer_user = reviewer_profile.user + assign_github_badge(reviewer_user, "First Code Review") + return JsonResponse({"status": "success"}, status=200) + + +def handle_issue_event(payload): + print("issue closed") + if payload["action"] == "closed": + closer_profile = UserProfile.objects.filter( + github_url=payload["sender"]["html_url"] + ).first() + if closer_profile: + closer_user = closer_profile.user + assign_github_badge(closer_user, "First Issue Closed") + return JsonResponse({"status": "success"}, status=200) + + +def handle_status_event(payload): + user_profile = UserProfile.objects.filter(github_url=payload["sender"]["html_url"]).first() + if user_profile: + user = user_profile.user + build_status = payload["state"] + if build_status == "success": + assign_github_badge(user, "First CI Build Passed") + elif build_status == "failure": + assign_github_badge(user, "First CI Build Failed") + return JsonResponse({"status": "success"}, status=200) + + +def handle_fork_event(payload): + user_profile = UserProfile.objects.filter(github_url=payload["sender"]["html_url"]).first() + if user_profile: + user = user_profile.user + assign_github_badge(user, "First Fork Created") + return JsonResponse({"status": "success"}, status=200) + + +def handle_create_event(payload): + if payload["ref_type"] == "branch": + user_profile = UserProfile.objects.filter(github_url=payload["sender"]["html_url"]).first() + if user_profile: + user = user_profile.user + assign_github_badge(user, "First Branch Created") + return JsonResponse({"status": "success"}, status=200) + + +def assign_github_badge(user, action_title): + try: + badge, created = Badge.objects.get_or_create(title=action_title, type="automatic") + if not UserBadge.objects.filter(user=user, badge=badge).exists(): + UserBadge.objects.create(user=user, badge=badge) + print(f"Assigned '{action_title}' badge to {user.username}") + else: + print(f"{user.username} already has the '{action_title}' badge.") + except Badge.DoesNotExist: + print(f"Badge '{action_title}' does not exist.") + + +def validate_signature(payload, signature): + if not signature: + return False + + secret = bytes(os.environ.get("GITHUB_ACCESS_TOKEN", ""), "utf-8") + computed_hmac = hmac.new(secret, payload, hashlib.sha256) + computed_signature = f"sha256={computed_hmac.hexdigest()}" + + return hmac.compare_digest(computed_signature, signature)