Files
silo-server/internal/requests/store.go
QuickandClaude Fable 5 d9e27da59e feat(notifications): request-fulfilled notifications across all channels
Notify the requesting profile once its media request is actually present
in the catalog (roadmap 06, item 2). Completion transitions stay
notification-agnostic; a presence-gated pass at the end of each
reconcile run fires the notice, so it means "watchable in Silo", not
"download finished".

- New System.DispatchOperational: delivery insert + webhook/web-push
  outbox enqueue in one transaction, post-commit multi-dispatch. The
  webhook auto-disable notice now rides the same path (replacing its
  hand-rolled hub publish and the now-removed InsertOperational), which
  also delivers auto-disable notices over web push.
- At-most-once delivery: partial unique index on
  (profile_id, reason_flags->>'request_id') plus a fulfilled_notified_at
  marker on media_requests, backfilled for pre-existing completed
  requests so deploys never flood.
- Per-webhook notify_requests toggle (default on) through repo, service,
  API, and settings UI; gated independently of the episode reason flags.
- request.fulfilled rendering in web inbox, realtime toast, web push
  payload, and Discord/generic webhook payloads, deep-linking to the
  matched catalog item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:04:22 -04:00

65 lines
3.2 KiB
Go

package requests
import (
"context"
"time"
)
type Store interface {
GetSettings(ctx context.Context) (Settings, error)
UpdateSettings(ctx context.Context, settings Settings) (Settings, error)
GetUserLimit(ctx context.Context, userID int) (*UserLimit, error)
UpsertUserLimit(ctx context.Context, limit UserLimit) (*UserLimit, error)
CountUserRequestsSince(ctx context.Context, userID int, since time.Time) (int, error)
ListActiveByTMDB(ctx context.Context, mediaType MediaType, tmdbIDs []int) (map[int]*Request, error)
// DeleteFailedByTMDB removes prior failed requests for a given media so a
// re-request does not leave behind stale rows in user/admin lists.
DeleteFailedByTMDB(ctx context.Context, mediaType MediaType, tmdbID int) (int, error)
CreateRequest(ctx context.Context, input CreateRequestRecord) (*Request, error)
GetRequest(ctx context.Context, id string) (*Request, error)
ListReconciliationCandidates(ctx context.Context, limit int) ([]*Request, error)
// ListFulfilledUnnotified returns completed requests whose fulfillment
// notification has not fired yet (presence-gated notify pass).
ListFulfilledUnnotified(ctx context.Context, limit int) ([]*Request, error)
// MarkFulfilledNotified stamps a request's fulfillment-notification
// marker so the notify pass stops considering it. Idempotent.
MarkFulfilledNotified(ctx context.Context, id string) error
ListMine(ctx context.Context, userID int, filter ListFilter) ([]*Request, error)
ListAdmin(ctx context.Context, filter ListFilter) ([]*Request, error)
SetStatus(ctx context.Context, id string, status Status, actor Viewer) (*Request, error)
SetOutcome(ctx context.Context, id string, outcome Outcome, actor Viewer, message string) (*Request, error)
ListTargets(ctx context.Context, requestID string) ([]Target, error)
CreateTarget(ctx context.Context, target Target) (Target, error)
DeleteTarget(ctx context.Context, id int64) error
UpdateTargetStatus(ctx context.Context, targetID int64, status Status, externalID, externalStatus, lastErr string, actor Viewer) (*Request, error)
ListIntegrations(ctx context.Context) ([]Integration, error)
GetIntegration(ctx context.Context, id string) (*Integration, error)
CreateIntegration(ctx context.Context, integration Integration) (*Integration, error)
UpdateIntegration(ctx context.Context, integration Integration) (*Integration, error)
// SaveIntegrationWithDefaults clears the conflicting kind default(s) and
// creates (isCreate) or updates the instance atomically in one transaction.
SaveIntegrationWithDefaults(ctx context.Context, integration Integration, isCreate bool) (*Integration, error)
DeleteIntegration(ctx context.Context, id string) error
}
type CreateRequestRecord struct {
ID string
Input CreateRequestInput
Status Status
Outcome Outcome
IsAnime bool
Requester Viewer
Now time.Time
// Quota, when non-nil, instructs the store to atomically verify the
// requester is below their per-user limit before inserting. The check
// runs inside the same transaction as the insert with a per-user
// advisory lock so concurrent submissions cannot both exceed the limit.
Quota *QuotaCheck
}
type QuotaCheck struct {
UserID int
WindowStart time.Time
MaxRequests int
}