95 lines · 4.1 KB
import { listAddressesNeedingExpiryWarning, markExpiryWarned } from "./db.ts";
import type { Dispatcher } from "./dispatch.ts";
import type { SqlExecutor } from "./storage.ts";
import type { AddressRow, OwnerRef } from "./types.ts";

// The reminder window, driven by the cleanup cron running once a day.
//
// Warning anything inside [24h, 48h] means every address gets seen by a run
// while it still has at least a day left. Using [0h, 24h] instead would let
// an address expiring 25 hours from now slip past today's run and only get
// warned at tomorrow's, an hour before it goes.
//
// The tradeoff: an address whose whole life is under ~48h may never be
// warned, because no run ever catches it inside the window. That's
// deliberate. "Expires in 24 hours" seconds after someone made a 24-hour
// address is noise, not a service.
const WARN_AFTER_SECONDS = 24 * 60 * 60;
const WARN_BEFORE_SECONDS = 48 * 60 * 60;

function groupByOwner(rows: AddressRow[]): Map<string, { owner: OwnerRef; rows: AddressRow[] }> {
  const groups = new Map<string, { owner: OwnerRef; rows: AddressRow[] }>();
  for (const row of rows) {
    const key = `${row.owner_type}:${row.owner_id}`;
    const existing = groups.get(key);
    if (existing) {
      existing.rows.push(row);
    } else {
      groups.set(key, { owner: { type: row.owner_type, id: row.owner_id }, rows: [row] });
    }
  }
  return groups;
}

// <t:...:R> renders as "in 20 hours" in the reader's own locale and timezone,
// so the message never has to guess either.
function buildMessage(rows: AddressRow[]): string {
  const lines = rows.map((row) => {
    const label = row.note ? ` (${row.note.replaceAll("`", "")})` : "";
    return `\`${row.address}\`${label}, expires <t:${row.expires_at}:R>`;
  });