server/src/news.ts
53 lines · 2040 bytes
7 symbols in this file
1// Spaceflight News API (The Space Devs). Free, no key. Used only for the daily news digest push.
2
3export interface Article {
4 id: number;
5 title: string;
6 news_site: string;
7 published_at: string;
8}
9
10export const DIGEST_HOUR = 9;
11
12/** Whether the device's local time is in the first 10 minutes of the digest hour (the 10-minute cron fires once in it). */
13export function digestDue(now: Date, utcOffsetMinutes: number): boolean {
14 const minutes = (((now.getUTCHours() * 60 + now.getUTCMinutes() + utcOffsetMinutes) % 1440) + 1440) % 1440;
15 return minutes >= DIGEST_HOUR * 60 && minutes < DIGEST_HOUR * 60 + 10;
16}
17
18/** The device's local date, so each device gets one digest per local day. */
19export function localDate(now: Date, utcOffsetMinutes: number): string {
20 return new Date(now.getTime() + utcOffsetMinutes * 60_000).toISOString().slice(0, 10);
21}
22
23/** Up to three of the newest headlines from the last 24 hours, one per site, without APOD. */
24export function pickHeadlines(articles: Article[], now: Date): Article[] {
25 const since = now.getTime() - 86_400_000;
26 const sites = new Set<string>();
27 const out: Article[] = [];
28 for (const a of [...articles].sort((x, y) => y.published_at.localeCompare(x.published_at))) {
29 if (Date.parse(a.published_at) < since || a.title.startsWith("APOD:") || sites.has(a.news_site)) continue;
30 sites.add(a.news_site);
31 out.push(a);
32 if (out.length === 3) break;
33 }
34 return out;
35}
36
37export function digestPayload(headlines: Article[]) {
38 return {
39 aps: {
40 alert: { title: "Space news", body: headlines.map((a) => `${a.title} (${a.news_site})`).join("\n") },
41 sound: "default",
42 },
43 tab: "news",
44 };
45}
46
47export async function fetchLatest(): Promise<Article[]> {
48 const res = await fetch("https://api.spaceflightnewsapi.net/v4/articles/?limit=50", {
49 headers: { "user-agent": "moonrise-server (sh.krz.moonrise)" },
50 });
51 if (res.status !== 200) throw new Error(`SNAPI ${res.status}`);
52 return ((await res.json()) as { results: Article[] }).results;
53}