feat: add Gitea as webhook-based service provider
First webhook-based provider integration (Immich uses polling).
Gitea pushes events via POST /api/webhooks/gitea/{provider_id} with
HMAC-SHA256 signature validation.
- 9 event types: push, issue opened/closed/commented, PR opened/closed/merged/commented, release published
- Generic filters system on NotificationTracker (collections, senders, exclude_senders)
- Provider capabilities include supported_filters and webhook_based flag
- Gitea API client for connection testing and repository listing
- 18 default Jinja2 notification templates (EN + RU)
- Frontend: conditional provider forms, Gitea event toggles in tracking config
- Auto-migration for filters column and Gitea tracking flags
This commit is contained in:
@@ -13,7 +13,7 @@ import aiohttp
|
||||
from ..auth.dependencies import get_current_user
|
||||
from ..database.engine import get_session
|
||||
from ..database.models import ServiceProvider, User
|
||||
from ..services import make_immich_provider
|
||||
from ..services import make_immich_provider, make_gitea_provider
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -81,6 +81,22 @@ async def create_provider(
|
||||
if test_result.get("external_domain"):
|
||||
config["external_domain"] = test_result["external_domain"]
|
||||
|
||||
elif body.type == "gitea":
|
||||
config = body.config
|
||||
# api_token is optional (webhook_secret is required, but token only for repo listing)
|
||||
if config.get("api_token"):
|
||||
async with aiohttp.ClientSession() as http_session:
|
||||
from notify_bridge_core.providers.gitea import GiteaServiceProvider
|
||||
gitea = GiteaServiceProvider(
|
||||
http_session, config.get("url", ""), config.get("api_token", ""), body.name,
|
||||
)
|
||||
test_result = await gitea.test_connection()
|
||||
if not test_result.get("ok"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=test_result.get("message", "Cannot connect to Gitea"),
|
||||
)
|
||||
|
||||
provider = ServiceProvider(
|
||||
user_id=user.id,
|
||||
type=body.type,
|
||||
@@ -107,6 +123,8 @@ async def list_provider_capabilities():
|
||||
"command_slots": caps.command_slots,
|
||||
"events": caps.events,
|
||||
"commands": caps.commands,
|
||||
"supported_filters": caps.supported_filters,
|
||||
"webhook_based": caps.webhook_based,
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -125,6 +143,8 @@ async def get_provider_capabilities(provider_type: str):
|
||||
"command_slots": caps.command_slots,
|
||||
"events": caps.events,
|
||||
"commands": caps.commands,
|
||||
"supported_filters": caps.supported_filters,
|
||||
"webhook_based": caps.webhook_based,
|
||||
}
|
||||
|
||||
|
||||
@@ -175,6 +195,22 @@ async def update_provider(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Connection error: {err}",
|
||||
)
|
||||
elif config_changed and provider.type == "gitea":
|
||||
if provider.config.get("api_token"):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as http_session:
|
||||
gitea = make_gitea_provider(http_session, provider)
|
||||
test_result = await gitea.test_connection()
|
||||
if not test_result.get("ok"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=test_result.get("message", "Cannot connect to Gitea"),
|
||||
)
|
||||
except aiohttp.ClientError as err:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Connection error: {err}",
|
||||
)
|
||||
|
||||
session.add(provider)
|
||||
await session.commit()
|
||||
@@ -210,6 +246,13 @@ async def test_provider(
|
||||
immich = make_immich_provider(http_session, provider)
|
||||
return await immich.test_connection()
|
||||
|
||||
if provider.type == "gitea":
|
||||
if not provider.config.get("api_token"):
|
||||
return {"ok": True, "message": "Gitea webhook-only mode (no API token for testing)"}
|
||||
async with aiohttp.ClientSession() as http_session:
|
||||
gitea = make_gitea_provider(http_session, provider)
|
||||
return await gitea.test_connection()
|
||||
|
||||
return {"ok": False, "message": f"Unknown provider type: {provider.type}"}
|
||||
|
||||
|
||||
@@ -227,6 +270,13 @@ async def list_collections(
|
||||
immich = make_immich_provider(http_session, provider)
|
||||
return await immich.list_collections()
|
||||
|
||||
if provider.type == "gitea":
|
||||
if not provider.config.get("api_token"):
|
||||
return []
|
||||
async with aiohttp.ClientSession() as http_session:
|
||||
gitea = make_gitea_provider(http_session, provider)
|
||||
return await gitea.list_collections()
|
||||
|
||||
return []
|
||||
|
||||
|
||||
@@ -285,9 +335,10 @@ def _provider_response(p: ServiceProvider) -> dict:
|
||||
"""Build a safe response dict for a provider."""
|
||||
config = dict(p.config)
|
||||
# Mask sensitive fields
|
||||
if "api_key" in config:
|
||||
key = config["api_key"]
|
||||
config["api_key"] = f"{key[:8]}...{key[-4:]}" if len(key) > 12 else "***"
|
||||
for secret_field in ("api_key", "api_token", "webhook_secret"):
|
||||
if secret_field in config:
|
||||
key = config[secret_field]
|
||||
config[secret_field] = f"{key[:8]}...{key[-4:]}" if len(key) > 12 else "***"
|
||||
return {
|
||||
"id": p.id,
|
||||
"type": p.type,
|
||||
|
||||
Reference in New Issue
Block a user