1321 lines
51 KiB
Python
1321 lines
51 KiB
Python
import hashlib
|
|
import hmac
|
|
import json
|
|
import logging
|
|
from typing import Dict, Any, Optional
|
|
|
|
from fastapi import APIRouter, Request, HTTPException, Depends, Header, Path
|
|
import discord # For Color
|
|
|
|
# Import API server functions
|
|
try:
|
|
from .api_server import (
|
|
send_discord_message_via_api,
|
|
get_api_settings,
|
|
) # For settings
|
|
except ImportError:
|
|
# If api_server.py is in the same directory:
|
|
from api_service.api_server import send_discord_message_via_api, get_api_settings
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
api_settings = get_api_settings() # Get loaded API settings
|
|
|
|
|
|
async def get_monitored_repository_by_id_api(
|
|
request: Request, repo_db_id: int
|
|
) -> Dict | None:
|
|
"""Gets details of a monitored repository by its database ID using the API service's PostgreSQL pool.
|
|
This is an alternative to settings_manager.get_monitored_repository_by_id that doesn't rely on the bot instance.
|
|
"""
|
|
# Log the available attributes in app.state for debugging
|
|
log.info(f"Available attributes in app.state: {dir(request.app.state)}")
|
|
|
|
# Try to get the PostgreSQL pool from the FastAPI app state
|
|
pg_pool = getattr(request.app.state, "pg_pool", None)
|
|
if not pg_pool:
|
|
log.warning(
|
|
f"API service PostgreSQL pool not available for get_monitored_repository_by_id_api (ID {repo_db_id})."
|
|
)
|
|
|
|
# Instead of falling back to settings_manager, let's try to create a new connection
|
|
# This is a temporary solution to diagnose the issue
|
|
try:
|
|
import asyncpg
|
|
from api_service.api_server import get_api_settings
|
|
|
|
settings = get_api_settings()
|
|
log.info(
|
|
f"Attempting to create a new PostgreSQL connection for repo_db_id: {repo_db_id}"
|
|
)
|
|
|
|
# Create a new connection to the database
|
|
conn = await asyncpg.connect(
|
|
user=settings.POSTGRES_USER,
|
|
password=settings.POSTGRES_PASSWORD,
|
|
host=settings.POSTGRES_HOST,
|
|
database=settings.POSTGRES_SETTINGS_DB,
|
|
)
|
|
|
|
# Query the database
|
|
record = await conn.fetchrow(
|
|
"SELECT * FROM git_monitored_repositories WHERE id = $1", repo_db_id
|
|
)
|
|
|
|
# Close the connection
|
|
await conn.close()
|
|
|
|
log.info(
|
|
f"Successfully retrieved repository configuration for ID {repo_db_id} using a new connection"
|
|
)
|
|
return dict(record) if record else None
|
|
except Exception as e:
|
|
log.exception(f"Failed to create a new PostgreSQL connection: {e}")
|
|
# Don't fall back to settings_manager as it's already failing
|
|
return None
|
|
|
|
try:
|
|
async with pg_pool.acquire() as conn:
|
|
record = await conn.fetchrow(
|
|
"SELECT * FROM git_monitored_repositories WHERE id = $1", repo_db_id
|
|
)
|
|
log.info(
|
|
f"Retrieved repository configuration for ID {repo_db_id} using API service PostgreSQL pool"
|
|
)
|
|
return dict(record) if record else None
|
|
except Exception as e:
|
|
log.exception(
|
|
f"Database error getting monitored repository by ID {repo_db_id} using API service pool: {e}"
|
|
)
|
|
|
|
# Instead of falling back to settings_manager, try with a new connection
|
|
try:
|
|
import asyncpg
|
|
from api_service.api_server import get_api_settings
|
|
|
|
settings = get_api_settings()
|
|
log.info(
|
|
f"Attempting to create a new PostgreSQL connection after pool error for repo_db_id: {repo_db_id}"
|
|
)
|
|
|
|
# Create a new connection to the database
|
|
conn = await asyncpg.connect(
|
|
user=settings.POSTGRES_USER,
|
|
password=settings.POSTGRES_PASSWORD,
|
|
host=settings.POSTGRES_HOST,
|
|
database=settings.POSTGRES_SETTINGS_DB,
|
|
)
|
|
|
|
# Query the database
|
|
record = await conn.fetchrow(
|
|
"SELECT * FROM git_monitored_repositories WHERE id = $1", repo_db_id
|
|
)
|
|
|
|
# Close the connection
|
|
await conn.close()
|
|
|
|
log.info(
|
|
f"Successfully retrieved repository configuration for ID {repo_db_id} using a new connection after pool error"
|
|
)
|
|
return dict(record) if record else None
|
|
except Exception as e2:
|
|
log.exception(
|
|
f"Failed to create a new PostgreSQL connection after pool error: {e2}"
|
|
)
|
|
return None
|
|
|
|
|
|
async def get_allowed_events_for_repo(request: Request, repo_db_id: int) -> list[str]:
|
|
"""Helper to fetch allowed_webhook_events for a repo."""
|
|
repo_config = await get_monitored_repository_by_id_api(request, repo_db_id)
|
|
if repo_config and repo_config.get("allowed_webhook_events"):
|
|
return repo_config["allowed_webhook_events"]
|
|
return ["push"] # Default to 'push' if not set or not found, for safety
|
|
|
|
|
|
def verify_github_signature(
|
|
payload_body: bytes, secret_token: str, signature_header: str
|
|
) -> bool:
|
|
"""Verify that the payload was sent from GitHub by validating the signature."""
|
|
if not signature_header:
|
|
log.warning("No X-Hub-Signature-256 found on request.")
|
|
return False
|
|
if not secret_token:
|
|
log.error(
|
|
"Webhook secret is not configured for this repository. Cannot verify signature."
|
|
)
|
|
return False
|
|
|
|
hash_object = hmac.new(
|
|
secret_token.encode("utf-8"), msg=payload_body, digestmod=hashlib.sha256
|
|
)
|
|
expected_signature = "sha256=" + hash_object.hexdigest()
|
|
if not hmac.compare_digest(expected_signature, signature_header):
|
|
log.warning(
|
|
f"Request signature mismatch. Expected: {expected_signature}, Got: {signature_header}"
|
|
)
|
|
return False
|
|
return True
|
|
|
|
|
|
def verify_gitlab_token(secret_token: str, gitlab_token_header: str) -> bool:
|
|
"""Verify that the payload was sent from GitLab by validating the token."""
|
|
if not gitlab_token_header:
|
|
log.warning("No X-Gitlab-Token found on request.")
|
|
return False
|
|
if not secret_token:
|
|
log.error(
|
|
"Webhook secret is not configured for this repository. Cannot verify token."
|
|
)
|
|
return False
|
|
if not hmac.compare_digest(
|
|
secret_token, gitlab_token_header
|
|
): # Direct comparison for GitLab token
|
|
log.warning("Request token mismatch.")
|
|
return False
|
|
return True
|
|
|
|
|
|
# Placeholder for other GitHub event formatters
|
|
# def format_github_issue_embed(payload: Dict[str, Any], repo_url: str) -> discord.Embed: ...
|
|
# def format_github_pull_request_embed(payload: Dict[str, Any], repo_url: str) -> discord.Embed: ...
|
|
# def format_github_release_embed(payload: Dict[str, Any], repo_url: str) -> discord.Embed: ...
|
|
|
|
|
|
def format_github_push_embed(payload: Dict[str, Any], repo_url: str) -> discord.Embed:
|
|
"""Formats a GitHub push event payload into a Discord embed."""
|
|
try:
|
|
repo_name = payload.get("repository", {}).get("full_name", repo_url)
|
|
pusher = payload.get("pusher", {}).get("name", "Unknown Pusher")
|
|
compare_url = payload.get("compare", repo_url)
|
|
|
|
embed = discord.Embed(
|
|
title=f"New Push to {repo_name}",
|
|
url=compare_url,
|
|
color=discord.Color.blue(), # Or discord.Color.from_rgb(r, g, b)
|
|
)
|
|
embed.set_author(name=pusher)
|
|
|
|
for commit in payload.get("commits", []):
|
|
commit_id_short = commit.get("id", "N/A")[:7]
|
|
commit_msg = commit.get("message", "No commit message.")
|
|
commit_url = commit.get("url", "#")
|
|
author_name = commit.get("author", {}).get("name", "Unknown Author")
|
|
|
|
# Files changed, insertions/deletions
|
|
added = commit.get("added", [])
|
|
removed = commit.get("removed", [])
|
|
modified = commit.get("modified", [])
|
|
|
|
stats_lines = []
|
|
if added:
|
|
stats_lines.append(f"+{len(added)} added")
|
|
if removed:
|
|
stats_lines.append(f"-{len(removed)} removed")
|
|
if modified:
|
|
stats_lines.append(f"~{len(modified)} modified")
|
|
stats_str = ", ".join(stats_lines) if stats_lines else "No file changes."
|
|
|
|
# Verification status (GitHub specific)
|
|
verification = commit.get("verification", {})
|
|
verified_status = (
|
|
"Verified" if verification.get("verified") else "Unverified"
|
|
)
|
|
if verification.get("reason") and verification.get("reason") != "unsigned":
|
|
verified_status += f" ({verification.get('reason')})"
|
|
|
|
field_value = (
|
|
f"Author: {author_name}\n"
|
|
f"Message: {commit_msg.splitlines()[0]}\n" # First line of commit message.
|
|
f"Verification: {verified_status}\n"
|
|
f"Stats: {stats_str}\n"
|
|
f"[View Commit]({commit_url})"
|
|
)
|
|
embed.add_field(
|
|
name=f"Commit `{commit_id_short}`", value=field_value, inline=False
|
|
)
|
|
if len(embed.fields) >= 5: # Limit fields to avoid overly large embeds
|
|
embed.add_field(
|
|
name="...",
|
|
value=f"And {len(payload.get('commits')) - 5} more commits.",
|
|
inline=False,
|
|
)
|
|
break
|
|
|
|
if not payload.get("commits"):
|
|
embed.description = "Received push event with no commits (e.g., new branch created without commits)."
|
|
|
|
return embed
|
|
except Exception as e:
|
|
log.exception(f"Error formatting GitHub push embed: {e}")
|
|
embed = discord.Embed(
|
|
title="Error Processing GitHub Push Webhook",
|
|
description=f"Could not parse commit details. Raw payload might be available in logs.\nError: {e}",
|
|
color=discord.Color.red(),
|
|
)
|
|
return embed
|
|
|
|
|
|
# Placeholder for other GitLab event formatters
|
|
# def format_gitlab_issue_embed(payload: Dict[str, Any], repo_url: str) -> discord.Embed: ...
|
|
# def format_gitlab_merge_request_embed(payload: Dict[str, Any], repo_url: str) -> discord.Embed: ...
|
|
# def format_gitlab_tag_push_embed(payload: Dict[str, Any], repo_url: str) -> discord.Embed: ...
|
|
|
|
|
|
def format_gitlab_push_embed(payload: Dict[str, Any], repo_url: str) -> discord.Embed:
|
|
"""Formats a GitLab push event payload into a Discord embed."""
|
|
try:
|
|
project_name = payload.get("project", {}).get("path_with_namespace", repo_url)
|
|
user_name = payload.get("user_name", "Unknown Pusher")
|
|
|
|
# GitLab's compare URL is not directly in the main payload, but commits have URLs
|
|
# We can use the project's web_url as a base.
|
|
project_web_url = payload.get("project", {}).get("web_url", repo_url)
|
|
|
|
embed = discord.Embed(
|
|
title=f"New Push to {project_name}",
|
|
url=project_web_url, # Link to project
|
|
color=discord.Color.orange(), # Or discord.Color.from_rgb(r, g, b)
|
|
)
|
|
embed.set_author(name=user_name)
|
|
|
|
for commit in payload.get("commits", []):
|
|
commit_id_short = commit.get("id", "N/A")[:7]
|
|
commit_msg = commit.get("message", "No commit message.")
|
|
commit_url = commit.get("url", "#")
|
|
author_name = commit.get("author", {}).get("name", "Unknown Author")
|
|
|
|
# Files changed, insertions/deletions (GitLab provides total counts)
|
|
# GitLab commit objects don't directly list added/removed/modified files in the same way GitHub does per commit in a push.
|
|
# The overall push event has 'total_commits_count', but individual commit stats are usually fetched separately if needed.
|
|
# For simplicity, we'll list files if available, or just the message.
|
|
# GitLab's commit object in webhook doesn't typically include detailed file stats like GitHub's.
|
|
# It might have 'added', 'modified', 'removed' at the top level of the push event for the whole push, not per commit.
|
|
# We'll focus on commit message and author for now.
|
|
|
|
# GitLab commit verification is not as straightforward in the webhook payload as GitHub's.
|
|
# It's often handled via GPG keys and displayed in the UI. We'll omit for now.
|
|
|
|
field_value = (
|
|
f"Author: {author_name}\n"
|
|
f"Message: {commit_msg.splitlines()[0]}\n" # First line
|
|
f"[View Commit]({commit_url})"
|
|
)
|
|
embed.add_field(
|
|
name=f"Commit `{commit_id_short}`", value=field_value, inline=False
|
|
)
|
|
if len(embed.fields) >= 5:
|
|
embed.add_field(
|
|
name="...",
|
|
value=f"And {len(payload.get('commits')) - 5} more commits.",
|
|
inline=False,
|
|
)
|
|
break
|
|
|
|
if not payload.get("commits"):
|
|
embed.description = "Received push event with no commits (e.g., new branch created or tag pushed)."
|
|
|
|
return embed
|
|
except Exception as e:
|
|
log.exception(f"Error formatting GitLab push embed: {e}")
|
|
embed = discord.Embed(
|
|
title="Error Processing GitLab Push Webhook",
|
|
description=f"Could not parse commit details. Raw payload might be available in logs.\nError: {e}",
|
|
color=discord.Color.red(),
|
|
)
|
|
return embed
|
|
|
|
|
|
# --- GitHub - New Event Formatters ---
|
|
|
|
|
|
def format_github_issues_embed(payload: Dict[str, Any], repo_url: str) -> discord.Embed:
|
|
"""Formats a Discord embed for a GitHub issues event."""
|
|
try:
|
|
action = payload.get("action", "Unknown action")
|
|
issue_data = payload.get("issue", {})
|
|
repo_name = payload.get("repository", {}).get("full_name", repo_url)
|
|
sender = payload.get("sender", {})
|
|
|
|
title = issue_data.get("title", "Untitled Issue")
|
|
issue_number = issue_data.get("number")
|
|
issue_url = issue_data.get("html_url", repo_url)
|
|
user_login = sender.get("login", "Unknown User")
|
|
user_url = sender.get("html_url", "#")
|
|
user_avatar = sender.get("avatar_url")
|
|
|
|
color = (
|
|
discord.Color.green()
|
|
if action == "opened"
|
|
else (
|
|
discord.Color.red()
|
|
if action == "closed"
|
|
else (
|
|
discord.Color.gold()
|
|
if action == "reopened"
|
|
else discord.Color.light_grey()
|
|
)
|
|
)
|
|
)
|
|
|
|
embed = discord.Embed(
|
|
title=f"Issue {action.capitalize()}: #{issue_number} {title}",
|
|
url=issue_url,
|
|
description=f"Issue in `{repo_name}` was {action}.",
|
|
color=color,
|
|
)
|
|
embed.set_author(name=user_login, url=user_url, icon_url=user_avatar)
|
|
|
|
if issue_data.get("body") and action == "opened":
|
|
body = issue_data["body"]
|
|
embed.add_field(
|
|
name="Description",
|
|
value=body[:1020] + "..." if len(body) > 1024 else body,
|
|
inline=False,
|
|
)
|
|
|
|
if issue_data.get("labels"):
|
|
labels = ", ".join([f"`{label['name']}`" for label in issue_data["labels"]])
|
|
embed.add_field(
|
|
name="Labels", value=labels if labels else "None", inline=True
|
|
)
|
|
|
|
if issue_data.get("assignee"):
|
|
assignee = issue_data["assignee"]["login"]
|
|
embed.add_field(
|
|
name="Assignee",
|
|
value=f"[{assignee}]({issue_data['assignee']['html_url']})",
|
|
inline=True,
|
|
)
|
|
elif issue_data.get("assignees"):
|
|
assignees = ", ".join(
|
|
[f"[{a['login']}]({a['html_url']})" for a in issue_data["assignees"]]
|
|
)
|
|
embed.add_field(
|
|
name="Assignees", value=assignees if assignees else "None", inline=True
|
|
)
|
|
|
|
return embed
|
|
except Exception as e:
|
|
log.error(f"Error formatting GitHub issues embed: {e}\nPayload: {payload}")
|
|
return discord.Embed(
|
|
title="Error Processing GitHub Issue Event",
|
|
description=str(e),
|
|
color=discord.Color.red(),
|
|
)
|
|
|
|
|
|
def format_github_pull_request_embed(
|
|
payload: Dict[str, Any], repo_url: str
|
|
) -> discord.Embed:
|
|
"""Formats a Discord embed for a GitHub pull_request event."""
|
|
try:
|
|
action = payload.get("action", "Unknown action")
|
|
pr_data = payload.get("pull_request", {})
|
|
repo_name = payload.get("repository", {}).get("full_name", repo_url)
|
|
sender = payload.get("sender", {})
|
|
|
|
title = pr_data.get("title", "Untitled Pull Request")
|
|
pr_number = payload.get(
|
|
"number", pr_data.get("number")
|
|
) # 'number' is top-level for some PR actions
|
|
pr_url = pr_data.get("html_url", repo_url)
|
|
user_login = sender.get("login", "Unknown User")
|
|
user_url = sender.get("html_url", "#")
|
|
user_avatar = sender.get("avatar_url")
|
|
|
|
color = (
|
|
discord.Color.green()
|
|
if action == "opened"
|
|
else (
|
|
discord.Color.red()
|
|
if action == "closed" and pr_data.get("merged") is False
|
|
else (
|
|
discord.Color.purple()
|
|
if action == "closed" and pr_data.get("merged") is True
|
|
else (
|
|
discord.Color.gold()
|
|
if action == "reopened"
|
|
else (
|
|
discord.Color.blue()
|
|
if action in ["synchronize", "ready_for_review"]
|
|
else discord.Color.light_grey()
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
description = f"Pull Request #{pr_number} in `{repo_name}` was {action}."
|
|
if action == "closed" and pr_data.get("merged"):
|
|
description = f"Pull Request #{pr_number} in `{repo_name}` was merged."
|
|
|
|
embed = discord.Embed(
|
|
title=f"PR {action.capitalize()}: #{pr_number} {title}",
|
|
url=pr_url,
|
|
description=description,
|
|
color=color,
|
|
)
|
|
embed.set_author(name=user_login, url=user_url, icon_url=user_avatar)
|
|
|
|
if pr_data.get("body") and action == "opened":
|
|
body = pr_data["body"]
|
|
embed.add_field(
|
|
name="Description",
|
|
value=body[:1020] + "..." if len(body) > 1024 else body,
|
|
inline=False,
|
|
)
|
|
|
|
embed.add_field(
|
|
name="Base Branch",
|
|
value=f"`{pr_data.get('base', {}).get('ref', 'N/A')}`",
|
|
inline=True,
|
|
)
|
|
embed.add_field(
|
|
name="Head Branch",
|
|
value=f"`{pr_data.get('head', {}).get('ref', 'N/A')}`",
|
|
inline=True,
|
|
)
|
|
|
|
if action == "closed":
|
|
merged_by = pr_data.get("merged_by")
|
|
if merged_by:
|
|
embed.add_field(
|
|
name="Merged By",
|
|
value=f"[{merged_by['login']}]({merged_by['html_url']})",
|
|
inline=True,
|
|
)
|
|
else:
|
|
embed.add_field(
|
|
name="Status", value="Closed without merging", inline=True
|
|
)
|
|
|
|
return embed
|
|
except Exception as e:
|
|
log.error(f"Error formatting GitHub PR embed: {e}\nPayload: {payload}")
|
|
return discord.Embed(
|
|
title="Error Processing GitHub PR Event",
|
|
description=str(e),
|
|
color=discord.Color.red(),
|
|
)
|
|
|
|
|
|
def format_github_release_embed(
|
|
payload: Dict[str, Any], repo_url: str
|
|
) -> discord.Embed:
|
|
"""Formats a Discord embed for a GitHub release event."""
|
|
try:
|
|
action = payload.get(
|
|
"action", "Unknown action"
|
|
) # e.g., published, created, edited
|
|
release_data = payload.get("release", {})
|
|
repo_name = payload.get("repository", {}).get("full_name", repo_url)
|
|
sender = payload.get("sender", {})
|
|
|
|
tag_name = release_data.get("tag_name", "N/A")
|
|
release_name = release_data.get("name", tag_name)
|
|
release_url = release_data.get("html_url", repo_url)
|
|
user_login = sender.get("login", "Unknown User")
|
|
user_url = sender.get("html_url", "#")
|
|
user_avatar = sender.get("avatar_url")
|
|
|
|
color = (
|
|
discord.Color.teal() if action == "published" else discord.Color.blurple()
|
|
)
|
|
|
|
embed = discord.Embed(
|
|
title=f"Release {action.capitalize()}: {release_name}",
|
|
url=release_url,
|
|
description=f"A new release `{tag_name}` was {action} in `{repo_name}`.",
|
|
color=color,
|
|
)
|
|
embed.set_author(name=user_login, url=user_url, icon_url=user_avatar)
|
|
|
|
if release_data.get("body"):
|
|
body = release_data["body"]
|
|
embed.add_field(
|
|
name="Release Notes",
|
|
value=body[:1020] + "..." if len(body) > 1024 else body,
|
|
inline=False,
|
|
)
|
|
|
|
return embed
|
|
except Exception as e:
|
|
log.error(f"Error formatting GitHub release embed: {e}\nPayload: {payload}")
|
|
return discord.Embed(
|
|
title="Error Processing GitHub Release Event",
|
|
description=str(e),
|
|
color=discord.Color.red(),
|
|
)
|
|
|
|
|
|
def format_github_issue_comment_embed(
|
|
payload: Dict[str, Any], repo_url: str
|
|
) -> discord.Embed:
|
|
"""Formats a Discord embed for a GitHub issue_comment event."""
|
|
try:
|
|
action = payload.get("action", "Unknown action") # created, edited, deleted
|
|
comment_data = payload.get("comment", {})
|
|
issue_data = payload.get("issue", {})
|
|
repo_name = payload.get("repository", {}).get("full_name", repo_url)
|
|
sender = payload.get("sender", {})
|
|
|
|
comment_url = comment_data.get("html_url", repo_url)
|
|
user_login = sender.get("login", "Unknown User")
|
|
user_url = sender.get("html_url", "#")
|
|
user_avatar = sender.get("avatar_url")
|
|
|
|
issue_title = issue_data.get("title", "Untitled Issue")
|
|
issue_number = issue_data.get("number")
|
|
|
|
color = discord.Color.greyple()
|
|
|
|
embed = discord.Embed(
|
|
title=f"Comment {action} on Issue #{issue_number}: {issue_title}",
|
|
url=comment_url,
|
|
color=color,
|
|
)
|
|
embed.set_author(name=user_login, url=user_url, icon_url=user_avatar)
|
|
|
|
if comment_data.get("body"):
|
|
body = comment_data["body"]
|
|
embed.description = body[:2040] + "..." if len(body) > 2048 else body
|
|
|
|
return embed
|
|
except Exception as e:
|
|
log.error(
|
|
f"Error formatting GitHub issue_comment embed: {e}\nPayload: {payload}"
|
|
)
|
|
return discord.Embed(
|
|
title="Error Processing GitHub Issue Comment Event",
|
|
description=str(e),
|
|
color=discord.Color.red(),
|
|
)
|
|
|
|
|
|
# --- GitLab - New Event Formatters ---
|
|
|
|
|
|
def format_gitlab_issue_embed(payload: Dict[str, Any], repo_url: str) -> discord.Embed:
|
|
"""Formats a Discord embed for a GitLab issue event (object_kind: 'issue')."""
|
|
try:
|
|
attributes = payload.get("object_attributes", {})
|
|
user = payload.get("user", {})
|
|
project_data = payload.get("project", {})
|
|
repo_name = project_data.get("path_with_namespace", repo_url)
|
|
|
|
action = attributes.get("action", "unknown") # open, close, reopen, update
|
|
title = attributes.get("title", "Untitled Issue")
|
|
issue_iid = attributes.get("iid") # Internal ID for display
|
|
issue_url = attributes.get("url", repo_url)
|
|
user_name = user.get("name", "Unknown User")
|
|
user_avatar = user.get("avatar_url")
|
|
|
|
color = (
|
|
discord.Color.green()
|
|
if action == "open"
|
|
else (
|
|
discord.Color.red()
|
|
if action == "close"
|
|
else (
|
|
discord.Color.gold()
|
|
if action == "reopen"
|
|
else discord.Color.light_grey()
|
|
)
|
|
)
|
|
)
|
|
|
|
embed = discord.Embed(
|
|
title=f"Issue {action.capitalize()}: #{issue_iid} {title}",
|
|
url=issue_url,
|
|
description=f"Issue in `{repo_name}` was {action}.",
|
|
color=color,
|
|
)
|
|
embed.set_author(name=user_name, icon_url=user_avatar)
|
|
|
|
if attributes.get("description") and action == "open":
|
|
desc = attributes["description"]
|
|
embed.add_field(
|
|
name="Description",
|
|
value=desc[:1020] + "..." if len(desc) > 1024 else desc,
|
|
inline=False,
|
|
)
|
|
|
|
if attributes.get("labels"):
|
|
labels = ", ".join(
|
|
[f"`{label['title']}`" for label in attributes["labels"]]
|
|
)
|
|
embed.add_field(
|
|
name="Labels", value=labels if labels else "None", inline=True
|
|
)
|
|
|
|
assignees_data = payload.get("assignees", [])
|
|
if assignees_data:
|
|
assignees = ", ".join([f"{a['name']}" for a in assignees_data])
|
|
embed.add_field(name="Assignees", value=assignees, inline=True)
|
|
|
|
return embed
|
|
except Exception as e:
|
|
log.error(f"Error formatting GitLab issue embed: {e}\nPayload: {payload}")
|
|
return discord.Embed(
|
|
title="Error Processing GitLab Issue Event",
|
|
description=str(e),
|
|
color=discord.Color.red(),
|
|
)
|
|
|
|
|
|
def format_gitlab_merge_request_embed(
|
|
payload: Dict[str, Any], repo_url: str
|
|
) -> discord.Embed:
|
|
"""Formats a Discord embed for a GitLab merge_request event."""
|
|
try:
|
|
attributes = payload.get("object_attributes", {})
|
|
user = payload.get("user", {})
|
|
project_data = payload.get("project", {})
|
|
repo_name = project_data.get("path_with_namespace", repo_url)
|
|
|
|
action = attributes.get(
|
|
"action", "unknown"
|
|
) # open, close, reopen, update, merge
|
|
title = attributes.get("title", "Untitled Merge Request")
|
|
mr_iid = attributes.get("iid")
|
|
mr_url = attributes.get("url", repo_url)
|
|
user_name = user.get("name", "Unknown User")
|
|
user_avatar = user.get("avatar_url")
|
|
|
|
color = (
|
|
discord.Color.green()
|
|
if action == "open"
|
|
else (
|
|
discord.Color.red()
|
|
if action == "close"
|
|
else (
|
|
discord.Color.purple()
|
|
if action == "merge"
|
|
else (
|
|
discord.Color.gold()
|
|
if action == "reopen"
|
|
else (
|
|
discord.Color.blue()
|
|
if action == "update"
|
|
else discord.Color.light_grey()
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
description = f"Merge Request !{mr_iid} in `{repo_name}` was {action}."
|
|
if action == "merge":
|
|
description = f"Merge Request !{mr_iid} in `{repo_name}` was merged."
|
|
|
|
embed = discord.Embed(
|
|
title=f"MR {action.capitalize()}: !{mr_iid} {title}",
|
|
url=mr_url,
|
|
description=description,
|
|
color=color,
|
|
)
|
|
embed.set_author(name=user_name, icon_url=user_avatar)
|
|
|
|
if attributes.get("description") and action == "open":
|
|
desc = attributes["description"]
|
|
embed.add_field(
|
|
name="Description",
|
|
value=desc[:1020] + "..." if len(desc) > 1024 else desc,
|
|
inline=False,
|
|
)
|
|
|
|
embed.add_field(
|
|
name="Source Branch",
|
|
value=f"`{attributes.get('source_branch', 'N/A')}`",
|
|
inline=True,
|
|
)
|
|
embed.add_field(
|
|
name="Target Branch",
|
|
value=f"`{attributes.get('target_branch', 'N/A')}`",
|
|
inline=True,
|
|
)
|
|
|
|
if action == "merge" and attributes.get("merge_commit_sha"):
|
|
embed.add_field(
|
|
name="Merge Commit",
|
|
value=f"`{attributes['merge_commit_sha'][:8]}`",
|
|
inline=True,
|
|
)
|
|
|
|
return embed
|
|
except Exception as e:
|
|
log.error(f"Error formatting GitLab MR embed: {e}\nPayload: {payload}")
|
|
return discord.Embed(
|
|
title="Error Processing GitLab MR Event",
|
|
description=str(e),
|
|
color=discord.Color.red(),
|
|
)
|
|
|
|
|
|
def format_gitlab_release_embed(
|
|
payload: Dict[str, Any], repo_url: str
|
|
) -> discord.Embed:
|
|
"""Formats a Discord embed for a GitLab release event."""
|
|
try:
|
|
# GitLab release webhook payload structure is simpler
|
|
action = payload.get("action", "created") # create, update
|
|
tag_name = payload.get("tag", "N/A")
|
|
release_name = payload.get("name", tag_name)
|
|
release_url = payload.get("url", repo_url)
|
|
project_data = payload.get("project", {})
|
|
repo_name = project_data.get("path_with_namespace", repo_url)
|
|
# GitLab release hooks don't typically include a 'user' who performed the action directly in the root.
|
|
# It might be inferred or logged differently by GitLab. For now, we'll omit a specific author.
|
|
|
|
color = discord.Color.teal() if action == "create" else discord.Color.blurple()
|
|
|
|
embed = discord.Embed(
|
|
title=f"Release {action.capitalize()}: {release_name}",
|
|
url=release_url,
|
|
description=f"A release `{tag_name}` was {action} in `{repo_name}`.",
|
|
color=color,
|
|
)
|
|
# embed.set_author(name=project_data.get('namespace', 'GitLab')) # Or project name
|
|
|
|
if payload.get("description"):
|
|
desc = payload["description"]
|
|
embed.add_field(
|
|
name="Release Notes",
|
|
value=desc[:1020] + "..." if len(desc) > 1024 else desc,
|
|
inline=False,
|
|
)
|
|
|
|
return embed
|
|
except Exception as e:
|
|
log.error(f"Error formatting GitLab release embed: {e}\nPayload: {payload}")
|
|
return discord.Embed(
|
|
title="Error Processing GitLab Release Event",
|
|
description=str(e),
|
|
color=discord.Color.red(),
|
|
)
|
|
|
|
|
|
def format_gitlab_note_embed(payload: Dict[str, Any], repo_url: str) -> discord.Embed:
|
|
"""Formats a Discord embed for a GitLab note event (comments)."""
|
|
try:
|
|
attributes = payload.get("object_attributes", {})
|
|
user = payload.get("user", {})
|
|
project_data = payload.get("project", {})
|
|
repo_name = project_data.get("path_with_namespace", repo_url)
|
|
|
|
note_type = attributes.get(
|
|
"noteable_type", "Comment"
|
|
) # Issue, MergeRequest, Commit, Snippet
|
|
note_url = attributes.get("url", repo_url)
|
|
user_name = user.get("name", "Unknown User")
|
|
user_avatar = user.get("avatar_url")
|
|
|
|
title_prefix = "New Comment"
|
|
target_info = ""
|
|
|
|
if note_type == "Commit":
|
|
commit_data = payload.get("commit", {})
|
|
title_prefix = f"Comment on Commit `{commit_data.get('id', 'N/A')[:7]}`"
|
|
elif note_type == "Issue":
|
|
issue_data = payload.get("issue", {})
|
|
title_prefix = f"Comment on Issue #{issue_data.get('iid', 'N/A')}"
|
|
target_info = issue_data.get("title", "")
|
|
elif note_type == "MergeRequest":
|
|
mr_data = payload.get("merge_request", {})
|
|
title_prefix = f"Comment on MR !{mr_data.get('iid', 'N/A')}"
|
|
target_info = mr_data.get("title", "")
|
|
elif note_type == "Snippet":
|
|
snippet_data = payload.get("snippet", {})
|
|
title_prefix = f"Comment on Snippet #{snippet_data.get('id', 'N/A')}"
|
|
target_info = snippet_data.get("title", "")
|
|
|
|
embed = discord.Embed(
|
|
title=f"{title_prefix}: {target_info}".strip(),
|
|
url=note_url,
|
|
color=discord.Color.greyple(),
|
|
)
|
|
embed.set_author(name=user_name, icon_url=user_avatar)
|
|
|
|
if attributes.get("note"):
|
|
note_body = attributes["note"]
|
|
embed.description = (
|
|
note_body[:2040] + "..." if len(note_body) > 2048 else note_body
|
|
)
|
|
|
|
embed.set_footer(text=f"Comment in {repo_name}")
|
|
return embed
|
|
except Exception as e:
|
|
log.error(f"Error formatting GitLab note embed: {e}\nPayload: {payload}")
|
|
return discord.Embed(
|
|
title="Error Processing GitLab Note Event",
|
|
description=str(e),
|
|
color=discord.Color.red(),
|
|
)
|
|
|
|
|
|
@router.post("/github/{repo_db_id}")
|
|
async def webhook_github(
|
|
request: Request,
|
|
repo_db_id: int = Path(
|
|
..., description="The database ID of the monitored repository"
|
|
),
|
|
x_hub_signature_256: Optional[str] = Header(None),
|
|
):
|
|
log.info(f"Received GitHub webhook for repo_db_id: {repo_db_id}")
|
|
payload_bytes = await request.body()
|
|
|
|
# Use our new function that uses the API service's PostgreSQL pool
|
|
repo_config = await get_monitored_repository_by_id_api(request, repo_db_id)
|
|
if not repo_config:
|
|
log.error(f"No repository configuration found for repo_db_id: {repo_db_id}")
|
|
raise HTTPException(
|
|
status_code=404, detail="Repository configuration not found."
|
|
)
|
|
|
|
if (
|
|
repo_config["monitoring_method"] != "webhook"
|
|
or repo_config["platform"] != "github"
|
|
):
|
|
log.error(f"Repository {repo_db_id} is not configured for GitHub webhooks.")
|
|
raise HTTPException(
|
|
status_code=400, detail="Repository not configured for GitHub webhooks."
|
|
)
|
|
|
|
if not verify_github_signature(
|
|
payload_bytes, repo_config["webhook_secret"], x_hub_signature_256
|
|
):
|
|
log.warning(f"Invalid GitHub signature for repo_db_id: {repo_db_id}")
|
|
raise HTTPException(status_code=403, detail="Invalid signature.")
|
|
|
|
try:
|
|
payload = json.loads(payload_bytes.decode("utf-8"))
|
|
except json.JSONDecodeError:
|
|
log.error(
|
|
f"Invalid JSON payload received for GitHub webhook, repo_db_id: {repo_db_id}"
|
|
)
|
|
raise HTTPException(status_code=400, detail="Invalid JSON payload.")
|
|
|
|
log.debug(f"GitHub webhook payload for {repo_db_id}: {payload}")
|
|
|
|
event_type = request.headers.get("X-GitHub-Event")
|
|
allowed_events = repo_config.get(
|
|
"allowed_webhook_events", ["push"]
|
|
) # Default to 'push'
|
|
|
|
if event_type not in allowed_events:
|
|
log.info(
|
|
f"Ignoring GitHub event type '{event_type}' for repo_db_id: {repo_db_id} as it's not in allowed events: {allowed_events}"
|
|
)
|
|
return {
|
|
"status": "success",
|
|
"message": f"Event type '{event_type}' ignored per configuration.",
|
|
}
|
|
|
|
discord_embed = None
|
|
if event_type == "push":
|
|
if not payload.get("commits") and not payload.get(
|
|
"deleted", False
|
|
): # Also consider branch deletion as a push event
|
|
log.info(
|
|
f"GitHub push event for {repo_db_id} has no commits and is not a delete event. Ignoring."
|
|
)
|
|
return {
|
|
"status": "success",
|
|
"message": "Push event with no commits ignored.",
|
|
}
|
|
discord_embed = format_github_push_embed(payload, repo_config["repository_url"])
|
|
elif event_type == "issues":
|
|
discord_embed = format_github_issues_embed(
|
|
payload, repo_config["repository_url"]
|
|
)
|
|
elif event_type == "pull_request":
|
|
discord_embed = format_github_pull_request_embed(
|
|
payload, repo_config["repository_url"]
|
|
)
|
|
elif event_type == "release":
|
|
discord_embed = format_github_release_embed(
|
|
payload, repo_config["repository_url"]
|
|
)
|
|
elif event_type == "issue_comment":
|
|
discord_embed = format_github_issue_comment_embed(
|
|
payload, repo_config["repository_url"]
|
|
)
|
|
# Add other specific event types above this else block
|
|
else:
|
|
log.info(
|
|
f"GitHub event type '{event_type}' is allowed but not yet handled by a specific formatter for repo_db_id: {repo_db_id}. Sending generic message."
|
|
)
|
|
# For unhandled but allowed events, send a generic notification or log.
|
|
# For now, we'll just acknowledge. If you want to notify for all allowed events, create generic formatter.
|
|
# return {"status": "success", "message": f"Event type '{event_type}' received but no specific formatter yet."}
|
|
# Or, create a generic embed:
|
|
embed_title = f"GitHub Event: {event_type.replace('_', ' ').title()} in {repo_config.get('repository_url')}"
|
|
embed_description = f"Received a '{event_type}' event."
|
|
# Try to get a relevant URL
|
|
action_url = payload.get("repository", {}).get("html_url", "#")
|
|
if (
|
|
event_type == "issues"
|
|
and "issue" in payload
|
|
and "html_url" in payload["issue"]
|
|
):
|
|
action_url = payload["issue"]["html_url"]
|
|
elif (
|
|
event_type == "pull_request"
|
|
and "pull_request" in payload
|
|
and "html_url" in payload["pull_request"]
|
|
):
|
|
action_url = payload["pull_request"]["html_url"]
|
|
|
|
discord_embed = discord.Embed(
|
|
title=embed_title,
|
|
description=embed_description,
|
|
url=action_url,
|
|
color=discord.Color.light_grey(),
|
|
)
|
|
|
|
if not discord_embed:
|
|
log.warning(
|
|
f"No embed generated for allowed GitHub event '{event_type}' for repo {repo_db_id}. This shouldn't happen if event is handled."
|
|
)
|
|
return {
|
|
"status": "error",
|
|
"message": "Embed generation failed for an allowed event.",
|
|
}
|
|
|
|
notification_channel_id = repo_config["notification_channel_id"]
|
|
|
|
# Convert embed to dict for sending via API
|
|
send_payload_dict = {"embeds": [discord_embed.to_dict()]}
|
|
|
|
# Use the send_discord_message_via_api from api_server.py
|
|
# This requires DISCORD_BOT_TOKEN to be set in the environment for api_server
|
|
if not api_settings.DISCORD_BOT_TOKEN:
|
|
log.error(
|
|
"DISCORD_BOT_TOKEN not configured in API settings. Cannot send webhook notification."
|
|
)
|
|
# Still return 200 to GitHub to acknowledge receipt, but log error.
|
|
return {
|
|
"status": "error",
|
|
"message": "Notification sending failed (bot token not configured).",
|
|
}
|
|
|
|
log.info(
|
|
f"Sending GitHub notification to channel {notification_channel_id} for repo {repo_db_id}."
|
|
)
|
|
|
|
# Send the embed using the send_discord_message_via_api function
|
|
# The function can handle dict content with embeds
|
|
send_result = await send_discord_message_via_api(
|
|
channel_id=notification_channel_id,
|
|
content=send_payload_dict, # Pass the dict directly
|
|
)
|
|
|
|
if send_result.get("success"):
|
|
log.info(
|
|
f"Successfully sent GitHub webhook notification for repo {repo_db_id} to channel {notification_channel_id}."
|
|
)
|
|
return {
|
|
"status": "success",
|
|
"message": "Webhook received and notification sent.",
|
|
}
|
|
else:
|
|
log.error(
|
|
f"Failed to send GitHub webhook notification for repo {repo_db_id}. Error: {send_result.get('message')}"
|
|
)
|
|
# Still return 200 to GitHub to acknowledge receipt, but log the internal failure.
|
|
return {
|
|
"status": "error",
|
|
"message": f"Webhook received, but notification failed: {send_result.get('message')}",
|
|
}
|
|
|
|
|
|
@router.post("/gitlab/{repo_db_id}")
|
|
async def webhook_gitlab(
|
|
request: Request,
|
|
repo_db_id: int = Path(
|
|
..., description="The database ID of the monitored repository"
|
|
),
|
|
x_gitlab_token: Optional[str] = Header(None),
|
|
):
|
|
log.info(f"Received GitLab webhook for repo_db_id: {repo_db_id}")
|
|
payload_bytes = await request.body()
|
|
|
|
# Use our new function that uses the API service's PostgreSQL pool
|
|
repo_config = await get_monitored_repository_by_id_api(request, repo_db_id)
|
|
if not repo_config:
|
|
log.error(f"No repository configuration found for repo_db_id: {repo_db_id}")
|
|
raise HTTPException(
|
|
status_code=404, detail="Repository configuration not found."
|
|
)
|
|
|
|
if (
|
|
repo_config["monitoring_method"] != "webhook"
|
|
or repo_config["platform"] != "gitlab"
|
|
):
|
|
log.error(f"Repository {repo_db_id} is not configured for GitLab webhooks.")
|
|
raise HTTPException(
|
|
status_code=400, detail="Repository not configured for GitLab webhooks."
|
|
)
|
|
|
|
if not verify_gitlab_token(repo_config["webhook_secret"], x_gitlab_token):
|
|
log.warning(f"Invalid GitLab token for repo_db_id: {repo_db_id}")
|
|
raise HTTPException(status_code=403, detail="Invalid token.")
|
|
|
|
try:
|
|
payload = json.loads(payload_bytes.decode("utf-8"))
|
|
except json.JSONDecodeError:
|
|
log.error(
|
|
f"Invalid JSON payload received for GitLab webhook, repo_db_id: {repo_db_id}"
|
|
)
|
|
raise HTTPException(status_code=400, detail="Invalid JSON payload.")
|
|
|
|
log.debug(f"GitLab webhook payload for {repo_db_id}: {payload}")
|
|
|
|
# GitLab uses 'object_kind' for event type, or 'event_name' for system hooks
|
|
event_type = payload.get("object_kind", payload.get("event_name"))
|
|
allowed_events = repo_config.get(
|
|
"allowed_webhook_events", ["push"]
|
|
) # Default to 'push' (GitLab calls push hooks 'push events' or 'tag_push events')
|
|
|
|
# Normalize GitLab event types if needed, e.g. 'push' for 'push_hook' or 'tag_push_hook'
|
|
# For now, assume direct match or that 'push' covers both.
|
|
# GitLab event names for webhooks: push_events, tag_push_events, issues_events, merge_requests_events, etc.
|
|
# The payload's object_kind is often more specific: 'push', 'tag_push', 'issue', 'merge_request'.
|
|
# We should aim to match against object_kind primarily.
|
|
# Let's simplify: if 'push' is in allowed_events, we'll accept 'push' and 'tag_push' object_kinds.
|
|
|
|
effective_event_type = event_type
|
|
if (
|
|
event_type == "tag_push"
|
|
and "push" in allowed_events
|
|
and "tag_push" not in allowed_events
|
|
):
|
|
# If only "push" is allowed, but we receive "tag_push", treat it as a push for now.
|
|
# This logic might need refinement based on how granular the user wants control.
|
|
pass # It will be caught by the 'push' check if 'push' is allowed.
|
|
|
|
is_event_allowed = False
|
|
if event_type in allowed_events:
|
|
is_event_allowed = True
|
|
elif (
|
|
event_type == "tag_push" and "push" in allowed_events
|
|
): # Special handling if 'push' implies 'tag_push'
|
|
is_event_allowed = True
|
|
effective_event_type = (
|
|
"push" # Treat as push for formatter if only push is configured
|
|
)
|
|
|
|
if not is_event_allowed:
|
|
log.info(
|
|
f"Ignoring GitLab event type '{event_type}' (object_kind/event_name) for repo_db_id: {repo_db_id} as it's not in allowed events: {allowed_events}"
|
|
)
|
|
return {
|
|
"status": "success",
|
|
"message": f"Event type '{event_type}' ignored per configuration.",
|
|
}
|
|
|
|
discord_embed = None
|
|
# Use effective_event_type for choosing formatter
|
|
if (
|
|
effective_event_type == "push"
|
|
): # This will catch 'push' and 'tag_push' if 'push' is allowed
|
|
if not payload.get("commits") and payload.get("total_commits_count", 0) == 0:
|
|
log.info(f"GitLab push event for {repo_db_id} has no commits. Ignoring.")
|
|
return {
|
|
"status": "success",
|
|
"message": "Push event with no commits ignored.",
|
|
}
|
|
discord_embed = format_gitlab_push_embed(payload, repo_config["repository_url"])
|
|
elif effective_event_type == "issue": # Matches object_kind 'issue'
|
|
discord_embed = format_gitlab_issue_embed(
|
|
payload, repo_config["repository_url"]
|
|
)
|
|
elif effective_event_type == "merge_request":
|
|
discord_embed = format_gitlab_merge_request_embed(
|
|
payload, repo_config["repository_url"]
|
|
)
|
|
elif effective_event_type == "release":
|
|
discord_embed = format_gitlab_release_embed(
|
|
payload, repo_config["repository_url"]
|
|
)
|
|
elif effective_event_type == "note": # For comments
|
|
discord_embed = format_gitlab_note_embed(payload, repo_config["repository_url"])
|
|
# Add other specific event types above this else block
|
|
else:
|
|
log.info(
|
|
f"GitLab event type '{event_type}' (effective: {effective_event_type}) is allowed but not yet handled by a specific formatter for repo_db_id: {repo_db_id}. Sending generic message."
|
|
)
|
|
embed_title = f"GitLab Event: {event_type.replace('_', ' ').title()} in {repo_config.get('repository_url')}"
|
|
embed_description = f"Received a '{event_type}' event."
|
|
action_url = payload.get("project", {}).get("web_url", "#")
|
|
# Try to get more specific URLs for common GitLab events
|
|
if "object_attributes" in payload and "url" in payload["object_attributes"]:
|
|
action_url = payload["object_attributes"]["url"]
|
|
elif "project" in payload and "web_url" in payload["project"]:
|
|
action_url = payload["project"]["web_url"]
|
|
|
|
discord_embed = discord.Embed(
|
|
title=embed_title,
|
|
description=embed_description,
|
|
url=action_url,
|
|
color=discord.Color.dark_orange(),
|
|
)
|
|
|
|
if not discord_embed:
|
|
log.warning(
|
|
f"No embed generated for allowed GitLab event '{event_type}' for repo {repo_db_id}."
|
|
)
|
|
return {
|
|
"status": "error",
|
|
"message": "Embed generation failed for an allowed event.",
|
|
}
|
|
|
|
notification_channel_id = repo_config["notification_channel_id"]
|
|
|
|
# Use the send_discord_message_via_api from api_server.py
|
|
# This requires DISCORD_BOT_TOKEN to be set in the environment for api_server
|
|
if not api_settings.DISCORD_BOT_TOKEN:
|
|
log.error(
|
|
"DISCORD_BOT_TOKEN not configured in API settings. Cannot send webhook notification."
|
|
)
|
|
return {
|
|
"status": "error",
|
|
"message": "Notification sending failed (bot token not configured).",
|
|
}
|
|
|
|
# Convert embed to dict for sending via API
|
|
send_payload_dict = {"embeds": [discord_embed.to_dict()]}
|
|
|
|
log.info(
|
|
f"Sending GitLab notification to channel {notification_channel_id} for repo {repo_db_id}."
|
|
)
|
|
|
|
# Send the embed using the send_discord_message_via_api function
|
|
# The function can handle dict content with embeds
|
|
send_result = await send_discord_message_via_api(
|
|
channel_id=notification_channel_id,
|
|
content=send_payload_dict, # Pass the dict directly
|
|
)
|
|
|
|
if send_result.get("success"):
|
|
log.info(
|
|
f"Successfully sent GitLab webhook notification for repo {repo_db_id} to channel {notification_channel_id}."
|
|
)
|
|
return {
|
|
"status": "success",
|
|
"message": "Webhook received and notification sent.",
|
|
}
|
|
else:
|
|
log.error(
|
|
f"Failed to send GitLab webhook notification for repo {repo_db_id}. Error: {send_result.get('message')}"
|
|
)
|
|
return {
|
|
"status": "error",
|
|
"message": f"Webhook received, but notification failed: {send_result.get('message')}",
|
|
}
|
|
|
|
|
|
@router.get("/test")
|
|
async def test_webhook_router():
|
|
return {"message": "Webhook router is working. Or mounted, at least."}
|
|
|
|
|
|
@router.get("/test-repo/{repo_db_id}")
|
|
async def test_repo_retrieval(request: Request, repo_db_id: int):
|
|
"""Test endpoint to check if we can retrieve repository information."""
|
|
try:
|
|
# Try to get the repository using our new function
|
|
repo_config = await get_monitored_repository_by_id_api(request, repo_db_id)
|
|
|
|
if repo_config:
|
|
return {"message": "Repository found", "repo_config": repo_config}
|
|
else:
|
|
return {"message": "Repository not found", "repo_db_id": repo_db_id}
|
|
except Exception as e:
|
|
log.exception(f"Error retrieving repository {repo_db_id}: {e}")
|
|
return {
|
|
"message": "Error retrieving repository",
|
|
"repo_db_id": repo_db_id,
|
|
"error": str(e),
|
|
}
|
|
|
|
|
|
@router.get("/test-db")
|
|
async def test_db_connection(request: Request):
|
|
"""Test endpoint to check if the database connection is working."""
|
|
try:
|
|
# Log the available attributes in app.state for debugging
|
|
state_attrs = dir(request.app.state)
|
|
log.info(f"Available attributes in app.state: {state_attrs}")
|
|
|
|
# Try to get the PostgreSQL pool from the FastAPI app state
|
|
pg_pool = getattr(request.app.state, "pg_pool", None)
|
|
if not pg_pool:
|
|
log.warning(
|
|
"API service PostgreSQL pool not available for test-db endpoint."
|
|
)
|
|
|
|
# Try to create a new connection
|
|
try:
|
|
import asyncpg
|
|
|
|
settings = get_api_settings()
|
|
log.info(
|
|
"Attempting to create a new PostgreSQL connection for test-db endpoint"
|
|
)
|
|
|
|
# Create a new connection to the database
|
|
conn = await asyncpg.connect(
|
|
user=settings.POSTGRES_USER,
|
|
password=settings.POSTGRES_PASSWORD,
|
|
host=settings.POSTGRES_HOST,
|
|
database=settings.POSTGRES_SETTINGS_DB,
|
|
)
|
|
|
|
# Test query
|
|
version = await conn.fetchval("SELECT version()")
|
|
|
|
# Close the connection
|
|
await conn.close()
|
|
|
|
return {
|
|
"message": "Database connection successful using direct connection",
|
|
"app_state_attrs": state_attrs,
|
|
"pg_pool_available": False,
|
|
"version": version,
|
|
}
|
|
except Exception as e:
|
|
log.exception(
|
|
f"Failed to create a new PostgreSQL connection for test-db endpoint: {e}"
|
|
)
|
|
return {
|
|
"message": "Database connection failed using direct connection",
|
|
"app_state_attrs": state_attrs,
|
|
"pg_pool_available": False,
|
|
"error": str(e),
|
|
}
|
|
|
|
# Use the pool
|
|
try:
|
|
async with pg_pool.acquire() as conn:
|
|
version = await conn.fetchval("SELECT version()")
|
|
return {
|
|
"message": "Database connection successful using app.state.pg_pool",
|
|
"app_state_attrs": state_attrs,
|
|
"pg_pool_available": True,
|
|
"version": version,
|
|
}
|
|
except Exception as e:
|
|
log.exception(f"Database error using app.state.pg_pool: {e}")
|
|
return {
|
|
"message": "Database connection failed using app.state.pg_pool",
|
|
"app_state_attrs": state_attrs,
|
|
"pg_pool_available": True,
|
|
"error": str(e),
|
|
}
|
|
except Exception as e:
|
|
log.exception(f"Unexpected error in test-db endpoint: {e}")
|
|
return {"message": "Unexpected error in test-db endpoint", "error": str(e)}
|