krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

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}