▍ humdrum codex / soft
2.7 KB raw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import type { SourceModule } from "../registry";
import {
  BriefsConfig,
  BriefsPayload,
  type BriefsConfig as Config,
  type BriefsPayload as Payload,
} from "@/lib/schemas/sources/briefs";

// Shape of a single category file on screamer.humdrum.one.
interface RawBrief {
  date?: string;
  category?: string;
  stories?: Array<{
    title?: string;
    summary?: string;
    articles?: Array<{ publication?: string; link?: string }>;
  }>;
}

// Newest-first list of dates that have at least one published brief.
async function fetchDates(baseUrl: string, signal?: AbortSignal): Promise<string[]> {
  try {
    const res = await fetch(`${baseUrl}/data/index.json`, { signal });
    if (!res.ok) return [];
    const j = (await res.json()) as { dates?: string[] };
    return Array.isArray(j.dates) ? j.dates : [];
  } catch {
    return [];
  }
}

// Fetch the newest available brief for one category, walking back through dates
// until one resolves (briefs publish at staggered times overnight, so today's
// may not exist yet at 6 AM). Returns null if none found in the window.
async function fetchCategory(
  baseUrl: string,
  key: string,
  dates: string[],
  signal?: AbortSignal,
): Promise<{ date: string; raw: RawBrief } | null> {
  for (const date of dates) {
    try {
      const res = await fetch(`${baseUrl}/data/${date}-${key}.json`, { signal });
      if (!res.ok) continue;
      const raw = (await res.json()) as RawBrief;
      if (raw.stories && raw.stories.length > 0) return { date, raw };
    } catch {
      // try the next date
    }
  }
  return null;
}

export const briefsModule: SourceModule<Config, Payload> = {
  kind: "briefs",
  label: "News Briefs",
  keyless: true,
  defaultRefreshSeconds: 3600,
  configSchema: BriefsConfig,
  payloadSchema: BriefsPayload,

  isConfigured: (config) => config.categories.length > 0,

  async fetch({ config, signal }) {
    const baseUrl = config.baseUrl.replace(/\/$/, "");
    const allDates = await fetchDates(baseUrl, signal);
    // index.json is newest-first; cap how far back we'll look per category.
    const dates = (allDates.length ? allDates : []).slice(0, config.lookbackDays);

    const categories = await Promise.all(
      config.categories.map(async ({ key, name }) => {
        const hit = await fetchCategory(baseUrl, key, dates, signal);
        const stories = (hit?.raw.stories ?? []).map((s) => ({
          title: s.title ?? "",
          summary: s.summary ?? "",
          sources: [
            ...new Set((s.articles ?? []).map((a) => a.publication).filter((p): p is string => !!p)),
          ],
          link: (s.articles ?? []).find((a) => a.link)?.link,
        }));
        return { key, name, date: hit?.date ?? null, stories };
      }),
    );

    return { categories };
  },
};