Lacie/events/git_webhook.py
2026-02-18 03:09:43 +01:00

235 lines
9.5 KiB
Python

import discord
from discord.ext import commands
from aiohttp import web
import hmac
import hashlib
import os
from dotenv import load_dotenv
from utils.logger import get_logger
load_dotenv()
logger = get_logger(__name__)
COMMIT_CHANNEL_IDS = [876777562599194644, 1437941632849940563, 1470441786810826884]
WEBHOOK_SECRET = os.getenv("GITHUB_WEBHOOK_SECRET", "")
# Updated port to be 8000 to prevent conflicts (should work)
# I think the port itself wants me dead
WEBHOOK_PORT = int(os.getenv("WEBHOOK_PORT", 8000))
class GitWebhook(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.app = web.Application()
self.app.router.add_post('/webhook', self.handle_webhook)
self.app.router.add_get('/health', self.health_check)
self.runner = None
self.site = None
async def cog_load(self):
"""Start the webhook server when cog loads."""
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.site = web.TCPSite(self.runner, '0.0.0.0', WEBHOOK_PORT)
await self.site.start()
logger.info(f"Git webhook server started on port {WEBHOOK_PORT}")
logger.info(f"Sending notifications to {len(COMMIT_CHANNEL_IDS)} channel(s)")
async def cog_unload(self):
"""Stop the webhook server when cog unloads."""
if self.site:
await self.site.stop()
if self.runner:
await self.runner.cleanup()
logger.info("Git webhook server stopped")
def verify_signature(self, payload_body, signature_header):
"""Verify GitHub webhook signature for security."""
if not WEBHOOK_SECRET:
return True
hash_object = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
msg=payload_body,
digestmod=hashlib.sha256
)
expected_signature = "sha256=" + hash_object.hexdigest()
return hmac.compare_digest(expected_signature, signature_header)
async def health_check(self, request):
"""Health check endpoint."""
return web.json_response({"status": "healthy"})
async def handle_webhook(self, request):
"""Handle incoming Git webhook from GitHub/GitLab."""
try:
# Verify signature if secret is configured
if WEBHOOK_SECRET:
signature = request.headers.get('X-Hub-Signature-256', '')
body = await request.read()
if not self.verify_signature(body, signature):
return web.json_response({"error": "Invalid signature"}, status=403)
data = await request.json()
else:
data = await request.json()
# Handle GitHub ping event (test from GitHub)
if 'zen' in data and 'hook_id' in data:
logger.info("Received GitHub ping event - webhook is configured correctly!")
return web.json_response({"status": "pong"}, status=200)
# Get all Discord channels
channels = []
for channel_id in COMMIT_CHANNEL_IDS:
channel = self.bot.get_channel(channel_id)
if channel:
channels.append(channel)
else:
logger.warning(f"Channel {channel_id} not found!")
if not channels:
logger.error("No valid channels found!")
return web.json_response({"error": "No channels found"}, status=500)
# Handle GitHub push events
if 'commits' in data and 'repository' in data:
await self.handle_github_push(data, channels)
return web.json_response({"status": "success"}, status=200)
# Handle GitLab push events
elif 'project' in data and 'commits' in data:
await self.handle_gitlab_push(data, channels)
return web.json_response({"status": "success"}, status=200)
logger.warning(f"Unknown webhook format. Keys in data: {list(data.keys())}")
return web.json_response({"error": "Unknown webhook format"}, status=400)
except Exception as e:
logger.error(f"Webhook error: {e}", exc_info=True)
return web.json_response({"error": str(e)}, status=500)
async def handle_github_push(self, data, channels):
"""Handle GitHub push webhook."""
repo_name = data['repository']['full_name']
repo_url = data['repository']['html_url']
branch = data['ref'].split('/')[-1]
pusher = data['pusher']['name']
commits = data['commits']
compare_url = data.get('compare', '')
if not commits:
return
# For single commit, use fields for better formatting
if len(commits) == 1:
commit = commits[0]
short_sha = commit['id'][:7]
message = commit['message']
author = commit['author']['name']
url = commit['url']
embed = discord.Embed(
title=f"📝 [{repo_name}:{branch}] New commit",
url=compare_url if compare_url else repo_url,
color=discord.Color.blue()
)
embed.add_field(name="Commit", value=f"[`{short_sha}`]({url})", inline=True)
embed.add_field(name="Author", value=author, inline=True)
embed.add_field(name="Message", value=message, inline=False)
embed.set_author(name=pusher, icon_url="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png")
embed.set_footer(text="GitHub")
else:
# For multiple commits, use description with truncated messages
commit_lines = []
for commit in commits[:10]:
short_sha = commit['id'][:7]
message = commit['message'].split('\n')[0] # First line only
if len(message) > 72:
message = message[:69] + "..."
author = commit['author']['name']
url = commit['url']
commit_lines.append(f"[`{short_sha}`]({url}) {message} - {author}")
embed = discord.Embed(
title=f"📝 [{repo_name}:{branch}] {len(commits)} new commits",
url=compare_url if compare_url else repo_url,
description="\n".join(commit_lines),
color=discord.Color.blue()
)
if len(commits) > 10:
embed.description += f"\n\n*...and {len(commits) - 10} more commit(s)*"
embed.set_author(name=pusher, icon_url="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png")
embed.set_footer(text="GitHub")
# Send to all Discord channels
for channel in channels:
try:
await channel.send(embed=embed)
except Exception as e:
logger.error(f"Failed to send to channel {channel.id}: {e}")
async def handle_gitlab_push(self, data, channels):
"""Handle GitLab push webhook."""
repo_name = data['project']['path_with_namespace']
repo_url = data['project']['web_url']
branch = data['ref'].split('/')[-1]
pusher = data['user_name']
commits = data['commits']
if not commits:
return
# For single commit, use fields for better formatting
if len(commits) == 1:
commit = commits[0]
short_sha = commit['id'][:7]
message = commit['message']
author = commit['author']['name']
url = commit['url']
embed = discord.Embed(
title=f"📝 [{repo_name}:{branch}] New commit",
url=repo_url,
color=0xFC6D26 # GitLab orange
)
embed.add_field(name="Commit", value=f"[`{short_sha}`]({url})", inline=True)
embed.add_field(name="Author", value=author, inline=True)
embed.add_field(name="Message", value=message, inline=False)
embed.set_author(name=pusher)
embed.set_footer(text="GitLab")
else:
# For multiple commits, use description with truncated messages
commit_lines = []
for commit in commits[:10]:
short_sha = commit['id'][:7]
message = commit['message'].split('\n')[0]
if len(message) > 72:
message = message[:69] + "..."
author = commit['author']['name']
url = commit['url']
commit_lines.append(f"[`{short_sha}`]({url}) {message} - {author}")
embed = discord.Embed(
title=f"📝 [{repo_name}:{branch}] {len(commits)} new commits",
url=repo_url,
description="\n".join(commit_lines),
color=0xFC6D26 # GitLab orange
)
if len(commits) > 10:
embed.description += f"\n\n*...and {len(commits) - 10} more commit(s)*"
embed.set_author(name=pusher)
embed.set_footer(text="GitLab")
# Send to all Discord channels
for channel in channels:
try:
await channel.send(embed=embed)
except Exception as e:
logger.error(f"Failed to send to channel {channel.id}: {e}")
async def setup(bot):
await bot.add_cog(GitWebhook(bot))