feat(notifications): Discord bot DM channel with account linking
Adds Discord direct messages as a notification channel. Users link their Discord account via OAuth2 (identify scope only, one-time server-side state rows); a bot delivers their inbox notifications as DMs. - Extract the email channel's watermark sweep into a generic account-channel engine; email and Discord are now thin adapters, so the SKIP LOCKED claim / watermark-after-send durability logic exists once. - New internal/discord REST client (token exchange, identity, open DM, send message) — no Gateway connection, no new dependencies. - Opt-in master switch (notifications.discord_enabled, default off) gates delivery, linking, capability, and the admin settings reveal. - Admin UI: credentials (secret + bot token encrypted at rest), dev portal setup checklist, bot invite link buttons, and a test button that bypasses the settings read cache and is disabled while credential edits are unsaved. - DM failures from missing shared guild (Discord 50007) surface as link health in user settings and self-heal via capped backoff. - New combined mode (per_episode_and_digest) for email and Discord: instant sends all day plus a daily digest recapping the whole window since the previous digest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+29
-2
@@ -2327,22 +2327,49 @@ export interface NotificationWebhookTestResult {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/** An account-level digest channel (email, Discord DMs). */
|
||||
export interface NotificationAccountChannelCapability {
|
||||
available: boolean;
|
||||
modes: string[];
|
||||
digest_hour: number;
|
||||
}
|
||||
|
||||
export interface NotificationCapability {
|
||||
in_app: { enabled: boolean };
|
||||
apple_push: { available: boolean; provider: string; supported_modes: string[] };
|
||||
android_push: { available: boolean; provider: string; supported_modes: string[] };
|
||||
web_push: { available: boolean; public_key?: string };
|
||||
webhooks: { available: boolean; max_per_profile: number; supported_types: string[] };
|
||||
email: { available: boolean; modes: string[]; digest_hour: number };
|
||||
email: NotificationAccountChannelCapability;
|
||||
discord: NotificationAccountChannelCapability;
|
||||
}
|
||||
|
||||
export type NotificationEmailMode = "off" | "per_episode" | "daily_digest";
|
||||
export type NotificationChannelMode =
|
||||
| "off"
|
||||
| "per_episode"
|
||||
| "daily_digest"
|
||||
| "per_episode_and_digest";
|
||||
export type NotificationEmailMode = NotificationChannelMode;
|
||||
export type NotificationDiscordMode = NotificationChannelMode;
|
||||
|
||||
/** Account-level (not per-profile): one mode covers all profiles. */
|
||||
export interface NotificationEmailPreferences {
|
||||
mode: NotificationEmailMode;
|
||||
}
|
||||
|
||||
/** Account-level Discord DM channel: link state, mode, and delivery health. */
|
||||
export interface NotificationDiscordPreferences {
|
||||
linked: boolean;
|
||||
discord_username?: string;
|
||||
mode: NotificationDiscordMode;
|
||||
/** Last DM delivery failure, surfaced as link health. Empty when healthy. */
|
||||
link_failure?: string;
|
||||
}
|
||||
|
||||
export interface NotificationDiscordLinkInit {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface WebPushSubscriptionView {
|
||||
id: string;
|
||||
endpoint: string;
|
||||
|
||||
Reference in New Issue
Block a user