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:
2026-03-22 12:58:35 +03:00
parent 1167d138a3
commit 6d28cfb8d8
39 changed files with 1588 additions and 25 deletions
@@ -32,6 +32,7 @@ from .api.command_configs import router as command_configs_router
from .api.command_trackers import router as command_trackers_router
from .api.command_template_configs import router as command_template_configs_router
from .commands.webhook import router as webhook_router, set_webhook_secret
from .api.webhooks import router as webhooks_router
@asynccontextmanager
@@ -88,6 +89,7 @@ app.include_router(command_configs_router)
app.include_router(command_trackers_router)
app.include_router(command_template_configs_router)
app.include_router(webhook_router)
app.include_router(webhooks_router)
@app.get("/api/health")
@@ -120,7 +122,7 @@ async def _seed_default_templates():
}
for locale in ("en", "ru"):
slots = load_default_templates(locale)
slots = load_default_templates(locale, provider_type="immich")
if not slots:
continue
@@ -193,6 +195,86 @@ async def _seed_default_templates():
template=template_text,
))
# --- Seed Gitea default templates ---
gitea_result = await session.exec(
select(TemplateConfig).where(
TemplateConfig.user_id == 0,
TemplateConfig.provider_type == "gitea",
)
)
gitea_configs = gitea_result.all()
gitea_existing_locales = {
(c.locale if c.locale else "en"): c for c in gitea_configs
}
for locale in ("en", "ru"):
gitea_slots = load_default_templates(locale, provider_type="gitea")
if not gitea_slots:
continue
if locale not in gitea_existing_locales:
from datetime import datetime as _dt, timezone as _tz
now = _dt.now(_tz.utc).isoformat()
name = f"Default Gitea ({locale.upper()})"
desc = f"Default Gitea templates ({locale.upper()})"
col_info = (await session.execute(
text("PRAGMA table_info(template_config)")
)).fetchall()
col_names = [c[1] for c in col_info if c[1] != "id"]
values = {}
for col in col_names:
if col == "user_id":
values[col] = 0
elif col == "provider_type":
values[col] = "gitea"
elif col == "name":
values[col] = name
elif col == "description":
values[col] = desc
elif col == "created_at":
values[col] = now
elif col == "date_format":
values[col] = "%d.%m.%Y, %H:%M UTC"
elif col == "date_only_format":
values[col] = "%d.%m.%Y"
elif col == "locale":
values[col] = locale
else:
values[col] = ""
cols_str = ", ".join(values.keys())
placeholders = ", ".join(f":{k}" for k in values.keys())
await session.execute(
text(f"INSERT INTO template_config ({cols_str}) VALUES ({placeholders})"),
values,
)
row = (await session.execute(text("SELECT last_insert_rowid()"))).scalar()
gitea_config_id = row
for slot_name, template_text in gitea_slots.items():
session.add(TemplateSlot(
config_id=gitea_config_id,
slot_name=slot_name,
template=template_text,
))
else:
config = gitea_existing_locales[locale]
for slot_name, template_text in gitea_slots.items():
slot_result = await session.exec(
select(TemplateSlot).where(
TemplateSlot.config_id == config.id,
TemplateSlot.slot_name == slot_name,
)
)
existing = slot_result.first()
if existing:
existing.template = template_text
session.add(existing)
else:
session.add(TemplateSlot(
config_id=config.id,
slot_name=slot_name,
template=template_text,
))
await session.commit()