Bug Bounties
{% if not bug_bounties %}
diff --git a/website/tests/test_webhook_comments.py b/website/tests/test_webhook_comments.py
new file mode 100644
index 0000000000..f7458b7f9d
--- /dev/null
+++ b/website/tests/test_webhook_comments.py
@@ -0,0 +1,110 @@
+import json
+
+from django.test import Client, TestCase
+from django.urls import reverse
+
+from website.models import GitHubComment, GitHubIssue, Repo, User, UserProfile
+
+
+class GitHubCommentWebhookTest(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.webhook_url = reverse("github-webhook")
+ self.repo = Repo.objects.create(repo_url="https://github.com/owner/repo")
+ self.user = User.objects.create(username="testuser")
+ self.user_profile, created = UserProfile.objects.get_or_create(
+ user=self.user, defaults={"github_url": "https://github.com/testuser"}
+ )
+ if not created:
+ self.user_profile.github_url = "https://github.com/testuser"
+ self.user_profile.save()
+ self.issue = GitHubIssue.objects.create(
+ issue_id=1,
+ repo=self.repo,
+ title="Test PR",
+ type="pull_request",
+ state="open",
+ created_at="2023-01-01T00:00:00Z",
+ updated_at="2023-01-01T00:00:00Z",
+ )
+
+ def test_issue_comment_on_pr(self):
+ payload = {
+ "action": "created",
+ "issue": {
+ "number": 1,
+ "pull_request": {}, # Indicates it is a PR
+ },
+ "comment": {
+ "id": 1001,
+ "body": "This is a comment",
+ "created_at": "2023-01-01T12:00:00Z",
+ "updated_at": "2023-01-01T12:00:00Z",
+ "html_url": "https://github.com/owner/repo/issues/1#issuecomment-1001",
+ },
+ "repository": {"html_url": "https://github.com/owner/repo", "full_name": "owner/repo"},
+ "sender": {"login": "testuser", "html_url": "https://github.com/testuser", "type": "User"},
+ }
+ response = self.client.post(
+ self.webhook_url,
+ data=json.dumps(payload),
+ content_type="application/json",
+ HTTP_X_GITHUB_EVENT="issue_comment",
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertTrue(GitHubComment.objects.filter(comment_id=1001).exists())
+ comment = GitHubComment.objects.get(comment_id=1001)
+ self.assertEqual(comment.github_issue, self.issue)
+ self.assertEqual(comment.user_profile, self.user_profile)
+
+ def test_issue_comment_not_on_pr(self):
+ payload = {
+ "action": "created",
+ "issue": {
+ "number": 1,
+ # No pull_request key
+ },
+ "comment": {
+ "id": 1002,
+ "body": "This is a comment on an issue",
+ "created_at": "2023-01-01T12:00:00Z",
+ "updated_at": "2023-01-01T12:00:00Z",
+ "html_url": "https://github.com/owner/repo/issues/1#issuecomment-1002",
+ },
+ "repository": {"html_url": "https://github.com/owner/repo", "full_name": "owner/repo"},
+ "sender": {"login": "testuser", "type": "User"},
+ }
+ response = self.client.post(
+ self.webhook_url,
+ data=json.dumps(payload),
+ content_type="application/json",
+ HTTP_X_GITHUB_EVENT="issue_comment",
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertFalse(GitHubComment.objects.filter(comment_id=1002).exists())
+
+ def test_bot_comment_ignored(self):
+ payload = {
+ "action": "created",
+ "issue": {
+ "number": 1,
+ "pull_request": {},
+ },
+ "comment": {
+ "id": 1003,
+ "body": "I am a bot",
+ "created_at": "2023-01-01T12:00:00Z",
+ "updated_at": "2023-01-01T12:00:00Z",
+ "html_url": "https://github.com/owner/repo/issues/1#issuecomment-1003",
+ },
+ "repository": {"html_url": "https://github.com/owner/repo", "full_name": "owner/repo"},
+ "sender": {"login": "copilot", "type": "Bot", "id": 12345},
+ }
+ response = self.client.post(
+ self.webhook_url,
+ data=json.dumps(payload),
+ content_type="application/json",
+ HTTP_X_GITHUB_EVENT="issue_comment",
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertFalse(GitHubComment.objects.filter(comment_id=1003).exists())
diff --git a/website/views/user.py b/website/views/user.py
index 41b8ca7e73..009a1a9dd5 100644
--- a/website/views/user.py
+++ b/website/views/user.py
@@ -17,7 +17,7 @@
from django.contrib.sites.shortcuts import get_current_site
from django.core.cache import cache
from django.core.mail import send_mail
-from django.db.models import Count, F, Q, Sum
+from django.db.models import Count, F, Max, Q, Sum
from django.db.models.functions import ExtractMonth
from django.dispatch import receiver
from django.http import Http404, HttpResponse, HttpResponseNotFound, JsonResponse
@@ -43,6 +43,7 @@
Contributor,
ContributorStats,
Domain,
+ GitHubComment,
GitHubIssue,
GitHubReview,
Hunt,
@@ -634,6 +635,34 @@ def get_context_data(self, *args, **kwargs):
)
context["code_review_leaderboard"] = reviewed_pr_leaderboard
+ # Comment Leaderboard - Use contributor data directly
+ # PR comments from last 6 months, ordered by recent activity
+ comment_leaderboard = (
+ GitHubComment.objects.filter(
+ github_issue__type="pull_request",
+ created_at__gte=since_date,
+ contributor__isnull=False, # Only comments with contributors
+ )
+ .filter(
+ Q(github_issue__repo__repo_url__startswith="https://github.com/OWASP-BLT/")
+ | Q(github_issue__repo__repo_url__startswith="https://github.com/owasp-blt/")
+ )
+ .exclude(contributor__name__icontains="copilot") # Exclude copilot contributors
+ .exclude(contributor__name__icontains="dependabot") # Exclude dependabot
+ .select_related("contributor")
+ .values(
+ "contributor__name",
+ "contributor__github_url",
+ "contributor__avatar_url",
+ )
+ .annotate(
+ total_comments=Count("id"),
+ latest_comment=Max("created_at"), # Get the most recent comment date
+ )
+ .order_by("-total_comments", "-latest_comment")[:10] # Order by comment count, then by recent activity
+ )
+ context["comment_leaderboard"] = comment_leaderboard
+
# Top visitors leaderboard
top_visitors = (
UserProfile.objects.select_related("user")
@@ -1295,6 +1324,8 @@ def github_webhook(request):
"push": handle_push_event,
"pull_request_review": handle_review_event,
"issues": handle_issue_event,
+ "issue_comment": handle_issue_comment_event,
+ "pull_request_review_comment": handle_review_comment_event,
"status": handle_status_event,
"fork": handle_fork_event,
"create": handle_create_event,
@@ -1571,6 +1602,116 @@ def handle_create_event(payload):
return JsonResponse({"status": "success"}, status=200)
+def handle_issue_comment_event(payload):
+ action = payload.get("action")
+ if action not in ["created", "edited"]:
+ return JsonResponse({"status": "ignored"}, status=200)
+
+ comment_data = payload.get("comment", {})
+ issue_data = payload.get("issue", {})
+ sender = payload.get("sender", {})
+ repo_data = payload.get("repository", {})
+
+ # Check if this comment is on a PR (GitHub API returns 'pull_request' key in issue object for PRs)
+ if "pull_request" not in issue_data:
+ return JsonResponse({"status": "ignored", "reason": "Not a PR comment"}, status=200)
+
+ # Bot filtering
+ login = sender.get("login", "")
+ is_bot = sender.get("type") == "Bot" or "bot" in login.lower()
+ bots = [
+ "copilot",
+ "coderabbitai",
+ "github-actions[bot]",
+ "dependabot",
+ "sonarcloud",
+ "vercel",
+ "netlify",
+ ]
+ if login.lower() in bots or (is_bot and login.lower() not in ["..."]): # Add exceptions if any bot is allowed
+ return JsonResponse({"status": "ignored", "reason": "Bot comment"}, status=200)
+
+ # Find Issue
+ issue_number = issue_data.get("number")
+ repo_html_url = repo_data.get("html_url")
+ try:
+ repo = Repo.objects.get(repo_url=repo_html_url)
+ github_issue = GitHubIssue.objects.get(issue_id=issue_number, repo=repo)
+ except (Repo.DoesNotExist, GitHubIssue.DoesNotExist):
+ return JsonResponse({"status": "ignored", "reason": "Repo or Issue not found"}, status=200)
+
+ # Find/Create User Profile
+ user_profile = UserProfile.objects.filter(github_url=sender.get("html_url")).first()
+
+ # Create Comment
+ GitHubComment.objects.update_or_create(
+ comment_id=comment_data.get("id"),
+ defaults={
+ "github_issue": github_issue,
+ "user_profile": user_profile,
+ "body": comment_data.get("body", ""),
+ "comment_type": "issue_comment",
+ "created_at": dateutil_parser.parse(comment_data.get("created_at")),
+ "updated_at": dateutil_parser.parse(comment_data.get("updated_at")),
+ "url": comment_data.get("html_url"),
+ },
+ )
+ return JsonResponse({"status": "success"}, status=200)
+
+
+def handle_review_comment_event(payload):
+ action = payload.get("action")
+ if action not in ["created", "edited"]:
+ return JsonResponse({"status": "ignored"}, status=200)
+
+ comment_data = payload.get("comment", {})
+ pr_data = payload.get("pull_request", {})
+ sender = payload.get("sender", {})
+ repo_data = payload.get("repository", {})
+
+ # Bot filtering
+ login = sender.get("login", "")
+ is_bot = sender.get("type") == "Bot" or "bot" in login.lower()
+ bots = [
+ "copilot",
+ "coderabbitai",
+ "github-actions[bot]",
+ "dependabot",
+ "sonarcloud",
+ "vercel",
+ "netlify",
+ ]
+ if login.lower() in bots or is_bot:
+ return JsonResponse({"status": "ignored", "reason": "Bot comment"}, status=200)
+
+ # Find Issue (PR is also a GitHubIssue)
+ pr_number = pr_data.get("number")
+ repo_html_url = repo_data.get("html_url")
+ try:
+ repo = Repo.objects.get(repo_url=repo_html_url)
+ github_issue = GitHubIssue.objects.get(issue_id=pr_number, repo=repo)
+ except (Repo.DoesNotExist, GitHubIssue.DoesNotExist):
+ return JsonResponse({"status": "ignored", "reason": "Repo or Issue not found"}, status=200)
+
+ # Find/Create User Profile
+ user_profile = UserProfile.objects.filter(github_url=sender.get("html_url")).first()
+
+ # Create Comment
+ GitHubComment.objects.update_or_create(
+ comment_id=comment_data.get("id"),
+ defaults={
+ "github_issue": github_issue,
+ "user_profile": user_profile,
+ "body": comment_data.get("body", ""),
+ "comment_type": "review_comment",
+ "created_at": dateutil_parser.parse(comment_data.get("created_at")),
+ "updated_at": dateutil_parser.parse(comment_data.get("updated_at")),
+ "url": comment_data.get("html_url"),
+ },
+ )
+ 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")