Add admin-owned broadcast destinations ("community channels"): Discord or
generic webhooks fed straight from release_events by a per-channel watermark
sweep, announcing newly added movies/episodes as grouped digest posts plus
configurable media request lifecycle events (submitted/approved/declined/
fulfilled).
- Extend release_events with a kind discriminator and add a movie
availability spine (movie_availability + kind-keyed
notification_content_seed_state; first full scan seeds silently so
upgrades never flood the movie back catalog)
- Sweep worker reads events by (created_at, id) cursor with batch-window
grouping, per-channel backoff, and auto-disable; request events post
best-effort via new requests.LifecycleNotifier hooks
- Reuse the webhook stack throughout: URL encryption (new AAD namespace),
SSRF guard, embed limits, HMAC signing; shared type/name validation
extracted for both services
- Admin CRUD API under /admin/notifications/server-channels and a Server
Channels section in the notifications admin settings UI
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { api } from "@/api/client";
|
|
import type {
|
|
NotificationWebhookTestResult,
|
|
ServerNotificationChannel,
|
|
ServerNotificationChannelInput,
|
|
} from "@/api/types";
|
|
import { adminKeys } from "../keys";
|
|
import { toast } from "sonner";
|
|
|
|
export function useServerNotificationChannels() {
|
|
return useQuery({
|
|
queryKey: adminKeys.serverNotificationChannels(),
|
|
queryFn: () =>
|
|
api<{ channels: ServerNotificationChannel[] }>("/admin/notifications/server-channels").then(
|
|
(d) => d.channels ?? [],
|
|
),
|
|
});
|
|
}
|
|
|
|
export function useCreateServerNotificationChannel() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (input: ServerNotificationChannelInput) =>
|
|
api<ServerNotificationChannel>("/admin/notifications/server-channels", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
}),
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: adminKeys.serverNotificationChannels() });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateServerNotificationChannel() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, ...input }: ServerNotificationChannelInput & { id: string }) =>
|
|
api<ServerNotificationChannel>(`/admin/notifications/server-channels/${id}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(input),
|
|
}),
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: adminKeys.serverNotificationChannels() });
|
|
},
|
|
onError: (error) => {
|
|
toast.error(error instanceof Error ? error.message : "Failed to update channel");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteServerNotificationChannel() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (id: string) =>
|
|
api(`/admin/notifications/server-channels/${id}`, { method: "DELETE" }),
|
|
onSuccess: () => {
|
|
toast.success("Channel deleted");
|
|
void queryClient.invalidateQueries({ queryKey: adminKeys.serverNotificationChannels() });
|
|
},
|
|
onError: () => {
|
|
toast.error("Failed to delete channel");
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useTestServerNotificationChannel() {
|
|
return useMutation({
|
|
mutationFn: (id: string) =>
|
|
api<NotificationWebhookTestResult>(`/admin/notifications/server-channels/${id}/test`, {
|
|
method: "POST",
|
|
}),
|
|
});
|
|
}
|
|
|
|
export function useRotateServerNotificationChannelSecret() {
|
|
return useMutation({
|
|
mutationFn: (id: string) =>
|
|
api<{ signing_secret: string }>(`/admin/notifications/server-channels/${id}/rotate-secret`, {
|
|
method: "POST",
|
|
}),
|
|
onError: (error) => {
|
|
toast.error(error instanceof Error ? error.message : "Failed to rotate signing secret");
|
|
},
|
|
});
|
|
}
|