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>
65 lines
3.2 KiB
Go
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
|
|
}
|