mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
eb12f21894
Resolve each feed's most recent sender domain and serve its favicon at GET /favicon/:feedId, falling back to the project icon. Icons are fetched in the background on ingestion (direct /favicon.ico then a DuckDuckGo fallback), cached base64 in KV keyed by domain with a 1-week TTL so the fetch only fires when absent. Exposed via RSS <image> / Atom <icon>/<logo> and rendered in the admin feed list, plus a landing-page feature card. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
170 lines
3.8 KiB
TypeScript
170 lines
3.8 KiB
TypeScript
import {
|
|
EmailData,
|
|
FeedConfig,
|
|
FeedMetadata,
|
|
FeedList,
|
|
EmailMetadata,
|
|
} from "../types";
|
|
import { MAX_METADATA_EMAILS } from "../config/constants";
|
|
|
|
/**
|
|
* KV key for a domain's cached favicon (shared across feeds from the same sender).
|
|
*/
|
|
export function iconKey(domain: string): string {
|
|
return `icon:${domain}`;
|
|
}
|
|
|
|
/**
|
|
* Store email data in KV
|
|
*/
|
|
export async function storeEmail(
|
|
kv: KVNamespace,
|
|
feedId: string,
|
|
emailData: EmailData,
|
|
): Promise<string> {
|
|
// Generate a unique key for this email
|
|
const timestamp = Date.now();
|
|
const key = `feed:${feedId}:email:${timestamp}`;
|
|
|
|
// Store the email content
|
|
await kv.put(key, JSON.stringify(emailData));
|
|
|
|
// Update the feed's metadata (list of emails)
|
|
await updateFeedMetadata(kv, feedId, {
|
|
key,
|
|
subject: emailData.subject,
|
|
receivedAt: timestamp,
|
|
});
|
|
|
|
return key;
|
|
}
|
|
|
|
/**
|
|
* Update feed metadata with a new email
|
|
*/
|
|
async function updateFeedMetadata(
|
|
kv: KVNamespace,
|
|
feedId: string,
|
|
emailMetadata: EmailMetadata,
|
|
): Promise<void> {
|
|
const feedMetadataKey = `feed:${feedId}:metadata`;
|
|
const existingMetadata = (await kv.get(feedMetadataKey, {
|
|
type: "json",
|
|
})) as FeedMetadata | null;
|
|
|
|
const metadata: FeedMetadata = existingMetadata || { emails: [] };
|
|
|
|
// Add new email to the beginning of the list
|
|
metadata.emails.unshift(emailMetadata);
|
|
|
|
// Keep only the last MAX_METADATA_EMAILS in the metadata; delete orphaned KV entries
|
|
const toDelete =
|
|
metadata.emails.length > MAX_METADATA_EMAILS
|
|
? metadata.emails.slice(MAX_METADATA_EMAILS)
|
|
: [];
|
|
if (toDelete.length > 0) {
|
|
metadata.emails = metadata.emails.slice(0, MAX_METADATA_EMAILS);
|
|
}
|
|
|
|
await Promise.all([
|
|
kv.put(feedMetadataKey, JSON.stringify(metadata)),
|
|
...toDelete.map((e) => kv.delete(e.key)),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get feed metadata
|
|
*/
|
|
export async function getFeedMetadata(
|
|
kv: KVNamespace,
|
|
feedId: string,
|
|
): Promise<FeedMetadata | null> {
|
|
const feedMetadataKey = `feed:${feedId}:metadata`;
|
|
return (await kv.get(feedMetadataKey, {
|
|
type: "json",
|
|
})) as FeedMetadata | null;
|
|
}
|
|
|
|
/**
|
|
* Get feed configuration
|
|
*/
|
|
export async function getFeedConfig(
|
|
kv: KVNamespace,
|
|
feedId: string,
|
|
): Promise<FeedConfig | null> {
|
|
const feedConfigKey = `feed:${feedId}:config`;
|
|
return (await kv.get(feedConfigKey, { type: "json" })) as FeedConfig | null;
|
|
}
|
|
|
|
/**
|
|
* Get email data
|
|
*/
|
|
export async function getEmailData(
|
|
kv: KVNamespace,
|
|
key: string,
|
|
): Promise<EmailData | null> {
|
|
return (await kv.get(key, { type: "json" })) as EmailData | null;
|
|
}
|
|
|
|
/**
|
|
* Create a new feed
|
|
*/
|
|
export async function createFeed(
|
|
kv: KVNamespace,
|
|
feedId: string,
|
|
feedConfig: FeedConfig,
|
|
): Promise<void> {
|
|
// Store feed configuration
|
|
const feedConfigKey = `feed:${feedId}:config`;
|
|
await kv.put(feedConfigKey, JSON.stringify(feedConfig));
|
|
|
|
// Create empty metadata for the feed
|
|
const feedMetadataKey = `feed:${feedId}:metadata`;
|
|
await kv.put(
|
|
feedMetadataKey,
|
|
JSON.stringify({
|
|
emails: [],
|
|
}),
|
|
);
|
|
|
|
// Add feed to the list of all feeds
|
|
await addFeedToList(kv, feedId, feedConfig.title, feedConfig.description);
|
|
}
|
|
|
|
/**
|
|
* Add a feed to the global list
|
|
*/
|
|
export async function addFeedToList(
|
|
kv: KVNamespace,
|
|
feedId: string,
|
|
title: string,
|
|
description?: string,
|
|
): Promise<void> {
|
|
const feedListKey = "feeds:list";
|
|
const existingList = (await kv.get(feedListKey, {
|
|
type: "json",
|
|
})) as FeedList | null;
|
|
|
|
const feedList: FeedList = existingList || { feeds: [] };
|
|
|
|
feedList.feeds.push({
|
|
id: feedId,
|
|
title,
|
|
description,
|
|
});
|
|
|
|
await kv.put(feedListKey, JSON.stringify(feedList));
|
|
}
|
|
|
|
/**
|
|
* Get all feeds
|
|
*/
|
|
export async function getAllFeeds(kv: KVNamespace): Promise<FeedList> {
|
|
const feedListKey = "feeds:list";
|
|
const feedList = (await kv.get(feedListKey, {
|
|
type: "json",
|
|
})) as FeedList | null;
|
|
|
|
return feedList || { feeds: [] };
|
|
}
|