299 lines · 10.5 KB
import { env } from "cloudflare:workers";
import { invalidateUnread } from "../user-inbox";
import { all, chunks, placeholders } from "../db";
import { accountDb } from "../scoped-db";
import { randomId } from "../crypto";
import { CAPS } from "../limits";
import { emailTemplates, queueEmail } from "../email";
import { publicUrl } from "../events/emit";
import type { DirectNotifyMessage, QueueMessage } from "../events/emit";
import { isNotificationCategory } from "~/lib/notification-categories";
import type { NotificationCategory } from "~/lib/notification-categories";
import { categoryPrefsFor, emailFooter, takeEmailBudget } from "./prefs";
import type { CategoryPref } from "./prefs";
import { ignoredWatchers, recipientHasAccess } from "./recipients";
import { isDevForge } from "./dev-mail";

const HOUR_MS = 60 * 60 * 1000;
const DAY_MS = 24 * HOUR_MS;
const MAX_DETAILS = 6;

/** Daily digest window; dev forges may shorten it with NOTIFY_DIGEST_DELAY_MS so tests can run end to end. */
function digestDelayMs(): number {
  const override = Number(env.NOTIFY_DIGEST_DELAY_MS);
  return isDevForge() && env.NOTIFY_DIGEST_DELAY_MS !== "" && Number.isFinite(override) && override >= 0
    ? override
    : DAY_MS;
}
const MAX_TITLE = 200;
const DIGEST_RETRY_MS = 5 * 60 * 1000;
/** Categories flushed per alarm; anything left re-arms the alarm. */
const DIGEST_CATEGORIES_PER_RUN = 20;
const DIGEST_ITEMS_PER_EMAIL = 50;
/** Categories where a repo's "ignore" watch level also mutes the notification; security and billing never mute. */
const MUTED_BY_IGNORE = new Set<NotificationCategory>(["ci", "ci_watching", "deploy", "mergefix"]);

export type NotifyInput = Omit<DirectNotifyMessage, "kind" | "recipients" | "id"> & {
  recipients: (string | null | undefined)[];
  /** Deterministic key for events that may be emitted more than once (e.g. a retried Workflow step). */
  dedupeKey?: string;
};