refactor: provider-agnostic bot command system + Gitea commands

Refactored the monolithic command handler (707 lines) into a pluggable
provider-handler architecture:

- Abstract ProviderCommandHandler interface (base.py)
- Handler dispatch registry routes commands by provider type
- Extracted all Immich logic into ImmichCommandHandler
- New GiteaCommandHandler with /status, /repos, /issues, /prs, /commits
- Multi-provider routing: groups context by provider type, finds handler
- handler.py reduced to ~280 line thin orchestrator

Gitea commands:
- Extended GiteaClient with get_repo_issues, get_repo_pulls, get_repo_commits
- 30 Jinja2 command templates (15 EN + 15 RU)
- Gitea capabilities updated with 6 commands + 15 command_slots
- Default command config + command template config seeded on startup
- Rate limiting: Gitea API commands share "api" category (15s cooldown)

Also:
- Command configs API accepts "gitea" provider type
- System command configs (user_id=0) visible to all users
- Webhook URL shown on Gitea provider card and edit form
- Scan interval hidden for webhook-based providers
This commit is contained in:
2026-03-22 17:44:47 +03:00
parent 0562f78b35
commit 63437c1841
45 changed files with 1175 additions and 397 deletions
@@ -45,6 +45,7 @@
}: Props = $props();
let isScheduler = $derived(providerType === 'scheduler');
let isWebhook = $derived(providerType === 'gitea');
// Custom variable management for scheduler
function addVariable() {
@@ -162,10 +163,12 @@
</fieldset>
{:else}
<div class="grid grid-cols-2 gap-3">
{#if !isWebhook}
<div>
<label for="trk-interval" class="block text-sm font-medium mb-1">{t('notificationTracker.scanInterval')}<Hint text={t('hints.scanInterval')} /></label>
<input id="trk-interval" type="number" bind:value={form.scan_interval} min="10" max="3600" class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div>
{/if}
<div>
<label for="trk-batch" class="block text-sm font-medium mb-1">{t('notificationTracker.batchDuration')}<Hint text={t('hints.batchDuration')} /></label>
<input id="trk-batch" type="number" bind:value={form.batch_duration} min="0" max="3600" class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
@@ -166,6 +166,13 @@
<input id="prv-token" bind:value={form.api_token} type="password" class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
<p class="text-xs text-[var(--color-muted-foreground)] mt-1">{t('providers.apiTokenHint')}</p>
</div>
{#if editing}
<div class="bg-[var(--color-muted)] rounded-md p-3">
<label class="block text-sm font-medium mb-1">{t('providers.webhookUrl')}</label>
<code class="text-xs select-all break-all">/api/webhooks/gitea/{editing}</code>
<p class="text-xs text-[var(--color-muted-foreground)] mt-1">{t('providers.webhookUrlHint')}</p>
</div>
{/if}
{/if}
<button type="submit" disabled={submitting}
class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90 disabled:opacity-50">
@@ -196,6 +203,9 @@
{#if provider.config?.url}
<a href={provider.config.url} target="_blank" rel="noopener" class="text-xs text-[var(--color-muted-foreground)] font-mono hover:text-[var(--color-primary)] hover:underline">{provider.config.url}</a>
{/if}
{#if provider.type === 'gitea'}
<p class="text-xs text-[var(--color-muted-foreground)] font-mono mt-0.5">{t('providers.webhookUrl')}: <span class="select-all">/api/webhooks/gitea/{provider.id}</span></p>
{/if}
</div>
</div>
<div class="flex items-center gap-1">