mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
chore: apply Prettier formatting to entire codebase
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
+575
-75
@@ -2,79 +2,579 @@
|
||||
* Collection of common nouns for feed ID generation
|
||||
*/
|
||||
export const nouns = [
|
||||
'actor', 'almond', 'amber', 'anchor', 'angel', 'animal', 'answer', 'apple',
|
||||
'autumn', 'avenue', 'badge', 'bagel', 'baker', 'ballet', 'bamboo',
|
||||
'banana', 'basket', 'beach', 'beard', 'beauty', 'beetle', 'berry', 'bicycle',
|
||||
'bird', 'blanket', 'blossom', 'boat', 'bottle', 'bowl', 'breeze', 'bubble',
|
||||
'bucket', 'button', 'cabin', 'cactus', 'cafe', 'camera', 'candle', 'candy',
|
||||
'canvas', 'canyon', 'captain', 'carpet', 'carrot', 'castle', 'cave', 'cellar',
|
||||
'chair', 'chalk', 'cheese', 'cherry', 'chest', 'chicken', 'chimney',
|
||||
'circus', 'cliff', 'clock', 'cloud', 'clover', 'coast', 'cobalt', 'cocoa',
|
||||
'coffee', 'coin', 'comet', 'compass', 'cookie', 'copper', 'coral', 'corner',
|
||||
'cotton', 'cradle', 'craft', 'creek', 'cricket', 'crown', 'crystal', 'cube',
|
||||
'cupboard', 'curtain', 'cushion', 'daisy', 'dance', 'date', 'dawn', 'deer',
|
||||
'desert', 'dew', 'diamond', 'dinner', 'dish', 'doctor', 'dolphin',
|
||||
'donut', 'door', 'dream', 'dress', 'drink', 'drum', 'duck', 'dusk',
|
||||
'eagle', 'earth', 'echo', 'emerald', 'engine', 'evening', 'face', 'fairy',
|
||||
'fall', 'family', 'fan', 'farm', 'feather', 'fence', 'ferry', 'field',
|
||||
'finger', 'fire', 'fish', 'flag', 'flame', 'flash', 'flavor', 'flight',
|
||||
'floor', 'flour', 'flower', 'flute', 'fog', 'foil', 'forest', 'fork',
|
||||
'fox', 'frame', 'friend', 'frog', 'frost', 'fruit', 'garden', 'garlic',
|
||||
'gate', 'gem', 'gift', 'ginger', 'giraffe', 'glacier', 'glass',
|
||||
'glitter', 'glove', 'glow', 'goat', 'gold', 'grape', 'grass', 'gravel',
|
||||
'gravity', 'guitar', 'gum', 'hair', 'hammer', 'hand', 'harbor', 'harp',
|
||||
'hat', 'hawk', 'heart', 'heath', 'heaven', 'helmet', 'herb', 'hill',
|
||||
'hippo', 'honey', 'hood', 'horn', 'horse', 'hotel', 'hour', 'house',
|
||||
'hunter', 'ice', 'icicle', 'idea', 'ink', 'insect', 'iron', 'island',
|
||||
'ivy', 'jacket', 'jade', 'jam', 'jasmine', 'jelly', 'jewel',
|
||||
'joke', 'journal', 'journey', 'joy', 'judge', 'jungle', 'kettle', 'key',
|
||||
'kid', 'kingdom', 'kitchen', 'kite', 'kitten', 'knight',
|
||||
'lab', 'ladder', 'lake', 'lamb', 'lamp', 'land', 'lantern',
|
||||
'laptop', 'laugh', 'lava', 'lawn', 'leaf', 'legend', 'lemon', 'letter',
|
||||
'library', 'light', 'lily', 'lime', 'lion', 'lip', 'lobby', 'lock',
|
||||
'locket', 'lodge', 'lotus', 'love', 'lunch', 'lyric', 'magic', 'magnet',
|
||||
'mango', 'maple', 'marble', 'market', 'mask', 'meadow', 'melody', 'melon',
|
||||
'memory', 'metal', 'meteor', 'milk', 'mint', 'mirror', 'mist', 'mitten',
|
||||
'moon', 'morning', 'moth', 'motor', 'mountain', 'mouse',
|
||||
'movie', 'muffin', 'museum', 'music', 'myth', 'napkin', 'nectar', 'needle',
|
||||
'nest', 'net', 'nickel', 'night', 'nose', 'note', 'novel', 'number',
|
||||
'nurse', 'nutmeg', 'oasis', 'ocean', 'olive', 'onion', 'opera', 'orange',
|
||||
'orbit', 'orchard', 'orchid', 'ostrich', 'otter', 'oven', 'owl', 'oxygen',
|
||||
'oyster', 'page', 'paint', 'palace', 'palm', 'pan', 'pancake', 'panda',
|
||||
'paper', 'parade', 'parcel', 'park', 'parrot', 'party', 'pasta', 'patch',
|
||||
'path', 'peach', 'peanut', 'pear', 'pearl', 'pebble', 'pencil', 'penny',
|
||||
'people', 'pepper', 'petal', 'phone', 'photo', 'piano', 'pickle', 'picture',
|
||||
'pie', 'pillow', 'pine', 'pink', 'pirate', 'pizza', 'planet',
|
||||
'plant', 'plum', 'pocket', 'poem', 'poet', 'point', 'pony', 'pool',
|
||||
'popcorn', 'porch', 'port', 'potato', 'powder', 'prairie', 'pretzel', 'prism',
|
||||
'prose', 'puppet', 'puppy', 'puzzle', 'quail', 'quartz', 'queen', 'quilt',
|
||||
'rabbit', 'raccoon', 'radio', 'raft', 'rain', 'rainbow', 'raisin',
|
||||
'ranch', 'rapids', 'raven', 'ray', 'record', 'reef', 'ribbon', 'rice',
|
||||
'ring', 'river', 'road', 'robin', 'robot', 'rock', 'rocket', 'rodeo',
|
||||
'roof', 'room', 'root', 'rope', 'rose', 'ruby', 'rug', 'ruler', 'sage',
|
||||
'sail', 'salad', 'salmon', 'salt', 'sand', 'sandal', 'sauce', 'saucer',
|
||||
'scale', 'scarf', 'school', 'sea', 'seed', 'shadow', 'shell', 'ship',
|
||||
'shirt', 'shoe', 'shop', 'shower', 'shrimp', 'side', 'sign', 'silk',
|
||||
'silver', 'singer', 'sink', 'sky', 'sled', 'sleet', 'sleigh', 'slice',
|
||||
'slide', 'slipper', 'slope', 'smoke', 'snail', 'snake', 'snow', 'soap',
|
||||
'sock', 'soda', 'sofa', 'soil', 'song', 'soup', 'spade', 'spark', 'sparrow',
|
||||
'spice', 'spider', 'spoon', 'spot', 'spring', 'sprout', 'square', 'squirrel',
|
||||
'stable', 'stage', 'stair', 'stamp', 'star', 'station', 'steam', 'steel',
|
||||
'stem', 'stick', 'stone', 'stork', 'storm', 'story', 'stove', 'straw',
|
||||
'stream', 'street', 'string', 'studio', 'sugar', 'summer', 'sun', 'sunset',
|
||||
'swan', 'sweater', 'sweets', 'sword', 'table', 'tablet', 'tail', 'talent',
|
||||
'tangerine', 'tank', 'tea', 'team', 'teapot', 'tear', 'temple', 'tennis',
|
||||
'tent', 'theater', 'thistle', 'thought', 'thread', 'thunder',
|
||||
'ticket', 'tide', 'tiger', 'tile', 'time', 'toast', 'toffee', 'tomato',
|
||||
'tooth', 'top', 'torch', 'tower', 'town', 'toy', 'track',
|
||||
'train', 'tree', 'triangle', 'trick', 'truck', 'trumpet', 'tulip', 'tunnel',
|
||||
'turkey', 'turtle', 'twig', 'uncle', 'unicorn', 'universe', 'vacuum', 'valley',
|
||||
'vanilla', 'vase', 'velvet', 'vessel', 'village', 'vine', 'violin',
|
||||
'voice', 'volcano', 'voyage', 'wagon', 'walnut', 'waltz', 'water',
|
||||
'wave', 'wax', 'weather', 'web', 'wedding', 'whale', 'wheat',
|
||||
'wheel', 'whistle', 'whisper', 'willow', 'wind', 'window', 'wine', 'wing',
|
||||
'winter', 'wire', 'wish', 'wizard', 'wood', 'wool',
|
||||
'word', 'world', 'wreath', 'wrist', 'writer', 'xylophone', 'yacht',
|
||||
'yard', 'yarn', 'year', 'yolk', 'zebra', 'zephyr', 'zinc', 'zipper',
|
||||
'zone', 'zoo'
|
||||
"actor",
|
||||
"almond",
|
||||
"amber",
|
||||
"anchor",
|
||||
"angel",
|
||||
"animal",
|
||||
"answer",
|
||||
"apple",
|
||||
"autumn",
|
||||
"avenue",
|
||||
"badge",
|
||||
"bagel",
|
||||
"baker",
|
||||
"ballet",
|
||||
"bamboo",
|
||||
"banana",
|
||||
"basket",
|
||||
"beach",
|
||||
"beard",
|
||||
"beauty",
|
||||
"beetle",
|
||||
"berry",
|
||||
"bicycle",
|
||||
"bird",
|
||||
"blanket",
|
||||
"blossom",
|
||||
"boat",
|
||||
"bottle",
|
||||
"bowl",
|
||||
"breeze",
|
||||
"bubble",
|
||||
"bucket",
|
||||
"button",
|
||||
"cabin",
|
||||
"cactus",
|
||||
"cafe",
|
||||
"camera",
|
||||
"candle",
|
||||
"candy",
|
||||
"canvas",
|
||||
"canyon",
|
||||
"captain",
|
||||
"carpet",
|
||||
"carrot",
|
||||
"castle",
|
||||
"cave",
|
||||
"cellar",
|
||||
"chair",
|
||||
"chalk",
|
||||
"cheese",
|
||||
"cherry",
|
||||
"chest",
|
||||
"chicken",
|
||||
"chimney",
|
||||
"circus",
|
||||
"cliff",
|
||||
"clock",
|
||||
"cloud",
|
||||
"clover",
|
||||
"coast",
|
||||
"cobalt",
|
||||
"cocoa",
|
||||
"coffee",
|
||||
"coin",
|
||||
"comet",
|
||||
"compass",
|
||||
"cookie",
|
||||
"copper",
|
||||
"coral",
|
||||
"corner",
|
||||
"cotton",
|
||||
"cradle",
|
||||
"craft",
|
||||
"creek",
|
||||
"cricket",
|
||||
"crown",
|
||||
"crystal",
|
||||
"cube",
|
||||
"cupboard",
|
||||
"curtain",
|
||||
"cushion",
|
||||
"daisy",
|
||||
"dance",
|
||||
"date",
|
||||
"dawn",
|
||||
"deer",
|
||||
"desert",
|
||||
"dew",
|
||||
"diamond",
|
||||
"dinner",
|
||||
"dish",
|
||||
"doctor",
|
||||
"dolphin",
|
||||
"donut",
|
||||
"door",
|
||||
"dream",
|
||||
"dress",
|
||||
"drink",
|
||||
"drum",
|
||||
"duck",
|
||||
"dusk",
|
||||
"eagle",
|
||||
"earth",
|
||||
"echo",
|
||||
"emerald",
|
||||
"engine",
|
||||
"evening",
|
||||
"face",
|
||||
"fairy",
|
||||
"fall",
|
||||
"family",
|
||||
"fan",
|
||||
"farm",
|
||||
"feather",
|
||||
"fence",
|
||||
"ferry",
|
||||
"field",
|
||||
"finger",
|
||||
"fire",
|
||||
"fish",
|
||||
"flag",
|
||||
"flame",
|
||||
"flash",
|
||||
"flavor",
|
||||
"flight",
|
||||
"floor",
|
||||
"flour",
|
||||
"flower",
|
||||
"flute",
|
||||
"fog",
|
||||
"foil",
|
||||
"forest",
|
||||
"fork",
|
||||
"fox",
|
||||
"frame",
|
||||
"friend",
|
||||
"frog",
|
||||
"frost",
|
||||
"fruit",
|
||||
"garden",
|
||||
"garlic",
|
||||
"gate",
|
||||
"gem",
|
||||
"gift",
|
||||
"ginger",
|
||||
"giraffe",
|
||||
"glacier",
|
||||
"glass",
|
||||
"glitter",
|
||||
"glove",
|
||||
"glow",
|
||||
"goat",
|
||||
"gold",
|
||||
"grape",
|
||||
"grass",
|
||||
"gravel",
|
||||
"gravity",
|
||||
"guitar",
|
||||
"gum",
|
||||
"hair",
|
||||
"hammer",
|
||||
"hand",
|
||||
"harbor",
|
||||
"harp",
|
||||
"hat",
|
||||
"hawk",
|
||||
"heart",
|
||||
"heath",
|
||||
"heaven",
|
||||
"helmet",
|
||||
"herb",
|
||||
"hill",
|
||||
"hippo",
|
||||
"honey",
|
||||
"hood",
|
||||
"horn",
|
||||
"horse",
|
||||
"hotel",
|
||||
"hour",
|
||||
"house",
|
||||
"hunter",
|
||||
"ice",
|
||||
"icicle",
|
||||
"idea",
|
||||
"ink",
|
||||
"insect",
|
||||
"iron",
|
||||
"island",
|
||||
"ivy",
|
||||
"jacket",
|
||||
"jade",
|
||||
"jam",
|
||||
"jasmine",
|
||||
"jelly",
|
||||
"jewel",
|
||||
"joke",
|
||||
"journal",
|
||||
"journey",
|
||||
"joy",
|
||||
"judge",
|
||||
"jungle",
|
||||
"kettle",
|
||||
"key",
|
||||
"kid",
|
||||
"kingdom",
|
||||
"kitchen",
|
||||
"kite",
|
||||
"kitten",
|
||||
"knight",
|
||||
"lab",
|
||||
"ladder",
|
||||
"lake",
|
||||
"lamb",
|
||||
"lamp",
|
||||
"land",
|
||||
"lantern",
|
||||
"laptop",
|
||||
"laugh",
|
||||
"lava",
|
||||
"lawn",
|
||||
"leaf",
|
||||
"legend",
|
||||
"lemon",
|
||||
"letter",
|
||||
"library",
|
||||
"light",
|
||||
"lily",
|
||||
"lime",
|
||||
"lion",
|
||||
"lip",
|
||||
"lobby",
|
||||
"lock",
|
||||
"locket",
|
||||
"lodge",
|
||||
"lotus",
|
||||
"love",
|
||||
"lunch",
|
||||
"lyric",
|
||||
"magic",
|
||||
"magnet",
|
||||
"mango",
|
||||
"maple",
|
||||
"marble",
|
||||
"market",
|
||||
"mask",
|
||||
"meadow",
|
||||
"melody",
|
||||
"melon",
|
||||
"memory",
|
||||
"metal",
|
||||
"meteor",
|
||||
"milk",
|
||||
"mint",
|
||||
"mirror",
|
||||
"mist",
|
||||
"mitten",
|
||||
"moon",
|
||||
"morning",
|
||||
"moth",
|
||||
"motor",
|
||||
"mountain",
|
||||
"mouse",
|
||||
"movie",
|
||||
"muffin",
|
||||
"museum",
|
||||
"music",
|
||||
"myth",
|
||||
"napkin",
|
||||
"nectar",
|
||||
"needle",
|
||||
"nest",
|
||||
"net",
|
||||
"nickel",
|
||||
"night",
|
||||
"nose",
|
||||
"note",
|
||||
"novel",
|
||||
"number",
|
||||
"nurse",
|
||||
"nutmeg",
|
||||
"oasis",
|
||||
"ocean",
|
||||
"olive",
|
||||
"onion",
|
||||
"opera",
|
||||
"orange",
|
||||
"orbit",
|
||||
"orchard",
|
||||
"orchid",
|
||||
"ostrich",
|
||||
"otter",
|
||||
"oven",
|
||||
"owl",
|
||||
"oxygen",
|
||||
"oyster",
|
||||
"page",
|
||||
"paint",
|
||||
"palace",
|
||||
"palm",
|
||||
"pan",
|
||||
"pancake",
|
||||
"panda",
|
||||
"paper",
|
||||
"parade",
|
||||
"parcel",
|
||||
"park",
|
||||
"parrot",
|
||||
"party",
|
||||
"pasta",
|
||||
"patch",
|
||||
"path",
|
||||
"peach",
|
||||
"peanut",
|
||||
"pear",
|
||||
"pearl",
|
||||
"pebble",
|
||||
"pencil",
|
||||
"penny",
|
||||
"people",
|
||||
"pepper",
|
||||
"petal",
|
||||
"phone",
|
||||
"photo",
|
||||
"piano",
|
||||
"pickle",
|
||||
"picture",
|
||||
"pie",
|
||||
"pillow",
|
||||
"pine",
|
||||
"pink",
|
||||
"pirate",
|
||||
"pizza",
|
||||
"planet",
|
||||
"plant",
|
||||
"plum",
|
||||
"pocket",
|
||||
"poem",
|
||||
"poet",
|
||||
"point",
|
||||
"pony",
|
||||
"pool",
|
||||
"popcorn",
|
||||
"porch",
|
||||
"port",
|
||||
"potato",
|
||||
"powder",
|
||||
"prairie",
|
||||
"pretzel",
|
||||
"prism",
|
||||
"prose",
|
||||
"puppet",
|
||||
"puppy",
|
||||
"puzzle",
|
||||
"quail",
|
||||
"quartz",
|
||||
"queen",
|
||||
"quilt",
|
||||
"rabbit",
|
||||
"raccoon",
|
||||
"radio",
|
||||
"raft",
|
||||
"rain",
|
||||
"rainbow",
|
||||
"raisin",
|
||||
"ranch",
|
||||
"rapids",
|
||||
"raven",
|
||||
"ray",
|
||||
"record",
|
||||
"reef",
|
||||
"ribbon",
|
||||
"rice",
|
||||
"ring",
|
||||
"river",
|
||||
"road",
|
||||
"robin",
|
||||
"robot",
|
||||
"rock",
|
||||
"rocket",
|
||||
"rodeo",
|
||||
"roof",
|
||||
"room",
|
||||
"root",
|
||||
"rope",
|
||||
"rose",
|
||||
"ruby",
|
||||
"rug",
|
||||
"ruler",
|
||||
"sage",
|
||||
"sail",
|
||||
"salad",
|
||||
"salmon",
|
||||
"salt",
|
||||
"sand",
|
||||
"sandal",
|
||||
"sauce",
|
||||
"saucer",
|
||||
"scale",
|
||||
"scarf",
|
||||
"school",
|
||||
"sea",
|
||||
"seed",
|
||||
"shadow",
|
||||
"shell",
|
||||
"ship",
|
||||
"shirt",
|
||||
"shoe",
|
||||
"shop",
|
||||
"shower",
|
||||
"shrimp",
|
||||
"side",
|
||||
"sign",
|
||||
"silk",
|
||||
"silver",
|
||||
"singer",
|
||||
"sink",
|
||||
"sky",
|
||||
"sled",
|
||||
"sleet",
|
||||
"sleigh",
|
||||
"slice",
|
||||
"slide",
|
||||
"slipper",
|
||||
"slope",
|
||||
"smoke",
|
||||
"snail",
|
||||
"snake",
|
||||
"snow",
|
||||
"soap",
|
||||
"sock",
|
||||
"soda",
|
||||
"sofa",
|
||||
"soil",
|
||||
"song",
|
||||
"soup",
|
||||
"spade",
|
||||
"spark",
|
||||
"sparrow",
|
||||
"spice",
|
||||
"spider",
|
||||
"spoon",
|
||||
"spot",
|
||||
"spring",
|
||||
"sprout",
|
||||
"square",
|
||||
"squirrel",
|
||||
"stable",
|
||||
"stage",
|
||||
"stair",
|
||||
"stamp",
|
||||
"star",
|
||||
"station",
|
||||
"steam",
|
||||
"steel",
|
||||
"stem",
|
||||
"stick",
|
||||
"stone",
|
||||
"stork",
|
||||
"storm",
|
||||
"story",
|
||||
"stove",
|
||||
"straw",
|
||||
"stream",
|
||||
"street",
|
||||
"string",
|
||||
"studio",
|
||||
"sugar",
|
||||
"summer",
|
||||
"sun",
|
||||
"sunset",
|
||||
"swan",
|
||||
"sweater",
|
||||
"sweets",
|
||||
"sword",
|
||||
"table",
|
||||
"tablet",
|
||||
"tail",
|
||||
"talent",
|
||||
"tangerine",
|
||||
"tank",
|
||||
"tea",
|
||||
"team",
|
||||
"teapot",
|
||||
"tear",
|
||||
"temple",
|
||||
"tennis",
|
||||
"tent",
|
||||
"theater",
|
||||
"thistle",
|
||||
"thought",
|
||||
"thread",
|
||||
"thunder",
|
||||
"ticket",
|
||||
"tide",
|
||||
"tiger",
|
||||
"tile",
|
||||
"time",
|
||||
"toast",
|
||||
"toffee",
|
||||
"tomato",
|
||||
"tooth",
|
||||
"top",
|
||||
"torch",
|
||||
"tower",
|
||||
"town",
|
||||
"toy",
|
||||
"track",
|
||||
"train",
|
||||
"tree",
|
||||
"triangle",
|
||||
"trick",
|
||||
"truck",
|
||||
"trumpet",
|
||||
"tulip",
|
||||
"tunnel",
|
||||
"turkey",
|
||||
"turtle",
|
||||
"twig",
|
||||
"uncle",
|
||||
"unicorn",
|
||||
"universe",
|
||||
"vacuum",
|
||||
"valley",
|
||||
"vanilla",
|
||||
"vase",
|
||||
"velvet",
|
||||
"vessel",
|
||||
"village",
|
||||
"vine",
|
||||
"violin",
|
||||
"voice",
|
||||
"volcano",
|
||||
"voyage",
|
||||
"wagon",
|
||||
"walnut",
|
||||
"waltz",
|
||||
"water",
|
||||
"wave",
|
||||
"wax",
|
||||
"weather",
|
||||
"web",
|
||||
"wedding",
|
||||
"whale",
|
||||
"wheat",
|
||||
"wheel",
|
||||
"whistle",
|
||||
"whisper",
|
||||
"willow",
|
||||
"wind",
|
||||
"window",
|
||||
"wine",
|
||||
"wing",
|
||||
"winter",
|
||||
"wire",
|
||||
"wish",
|
||||
"wizard",
|
||||
"wood",
|
||||
"wool",
|
||||
"word",
|
||||
"world",
|
||||
"wreath",
|
||||
"wrist",
|
||||
"writer",
|
||||
"xylophone",
|
||||
"yacht",
|
||||
"yard",
|
||||
"yarn",
|
||||
"year",
|
||||
"yolk",
|
||||
"zebra",
|
||||
"zephyr",
|
||||
"zinc",
|
||||
"zipper",
|
||||
"zone",
|
||||
"zoo",
|
||||
];
|
||||
@@ -285,13 +285,16 @@ describe("Admin Routes", () => {
|
||||
)) as { feeds: Array<{ id: string; title: string }> } | null;
|
||||
const feedId = feedList?.feeds[0].id as string;
|
||||
|
||||
const deleteRes = await request(`/admin/feeds/${feedId}/delete?view=list`, {
|
||||
const deleteRes = await request(
|
||||
`/admin/feeds/${feedId}/delete?view=list`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: authCookie,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
expect(deleteRes.status).toBe(200);
|
||||
const payload = await deleteRes.json();
|
||||
@@ -334,8 +337,12 @@ describe("Admin Routes", () => {
|
||||
});
|
||||
|
||||
expect(bulkDeleteRes.status).toBe(302);
|
||||
expect(bulkDeleteRes.headers.get("Location")).toContain("/admin?view=list");
|
||||
expect(bulkDeleteRes.headers.get("Location")).toContain("message=bulkDeleted");
|
||||
expect(bulkDeleteRes.headers.get("Location")).toContain(
|
||||
"/admin?view=list",
|
||||
);
|
||||
expect(bulkDeleteRes.headers.get("Location")).toContain(
|
||||
"message=bulkDeleted",
|
||||
);
|
||||
|
||||
const feedListAfter = (await mockEnv.EMAIL_STORAGE.get(
|
||||
"feeds:list",
|
||||
@@ -386,7 +393,9 @@ describe("Admin Routes", () => {
|
||||
const feedMetadata = (await mockEnv.EMAIL_STORAGE.get(
|
||||
feedMetadataKey,
|
||||
"json",
|
||||
)) as { emails: Array<{ key: string; subject: string; receivedAt: number }> } | null;
|
||||
)) as {
|
||||
emails: Array<{ key: string; subject: string; receivedAt: number }>;
|
||||
} | null;
|
||||
const updatedMetadata = {
|
||||
emails: [
|
||||
...(feedMetadata?.emails || []),
|
||||
@@ -398,13 +407,16 @@ describe("Admin Routes", () => {
|
||||
JSON.stringify(updatedMetadata),
|
||||
);
|
||||
|
||||
const deleteRes = await request(`/admin/emails/${emailKey}/delete?feedId=${feedId}`, {
|
||||
const deleteRes = await request(
|
||||
`/admin/emails/${emailKey}/delete?feedId=${feedId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: authCookie,
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
expect(deleteRes.status).toBe(200);
|
||||
const payload = await deleteRes.json();
|
||||
@@ -417,7 +429,9 @@ describe("Admin Routes", () => {
|
||||
const metadataAfter = (await mockEnv.EMAIL_STORAGE.get(
|
||||
feedMetadataKey,
|
||||
"json",
|
||||
)) as { emails: Array<{ key: string; subject: string; receivedAt: number }> } | null;
|
||||
)) as {
|
||||
emails: Array<{ key: string; subject: string; receivedAt: number }>;
|
||||
} | null;
|
||||
expect(metadataAfter?.emails.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
+181
-50
@@ -285,7 +285,9 @@ app.get("/", async (c) => {
|
||||
layout(
|
||||
"Dashboard",
|
||||
html`
|
||||
<div class="container ${view === "table" ? "container-wide" : ""} fade-in">
|
||||
<div
|
||||
class="container ${view === "table" ? "container-wide" : ""} fade-in"
|
||||
>
|
||||
<div class="header-with-actions">
|
||||
<div class="header-title">
|
||||
<h1>Email to RSS Admin</h1>
|
||||
@@ -430,33 +432,101 @@ app.get("/", async (c) => {
|
||||
onchange="toggleAllFeeds(this.checked)"
|
||||
/>
|
||||
</th>
|
||||
<th class="th-resizable" data-sort-key="title" aria-sort="none">
|
||||
<button type="button" class="th-button" data-sort-key="title">
|
||||
Title <span class="sort-indicator" aria-hidden="true"></span>
|
||||
<th
|
||||
class="th-resizable"
|
||||
data-sort-key="title"
|
||||
aria-sort="none"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="th-button"
|
||||
data-sort-key="title"
|
||||
>
|
||||
Title
|
||||
<span
|
||||
class="sort-indicator"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
<div class="col-resizer" data-col="title" title="Resize"></div>
|
||||
<div
|
||||
class="col-resizer"
|
||||
data-col="title"
|
||||
title="Resize"
|
||||
></div>
|
||||
</th>
|
||||
<th class="th-resizable" data-sort-key="feedId" aria-sort="none">
|
||||
<button type="button" class="th-button" data-sort-key="feedId">
|
||||
Feed ID <span class="sort-indicator" aria-hidden="true"></span>
|
||||
<th
|
||||
class="th-resizable"
|
||||
data-sort-key="feedId"
|
||||
aria-sort="none"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="th-button"
|
||||
data-sort-key="feedId"
|
||||
>
|
||||
Feed ID
|
||||
<span
|
||||
class="sort-indicator"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
<div class="col-resizer" data-col="feedId" title="Resize"></div>
|
||||
<div
|
||||
class="col-resizer"
|
||||
data-col="feedId"
|
||||
title="Resize"
|
||||
></div>
|
||||
</th>
|
||||
<th class="th-resizable" data-sort-key="email" aria-sort="none">
|
||||
<button type="button" class="th-button" data-sort-key="email">
|
||||
Email <span class="sort-indicator" aria-hidden="true"></span>
|
||||
<th
|
||||
class="th-resizable"
|
||||
data-sort-key="email"
|
||||
aria-sort="none"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="th-button"
|
||||
data-sort-key="email"
|
||||
>
|
||||
Email
|
||||
<span
|
||||
class="sort-indicator"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
<div class="col-resizer" data-col="email" title="Resize"></div>
|
||||
<div
|
||||
class="col-resizer"
|
||||
data-col="email"
|
||||
title="Resize"
|
||||
></div>
|
||||
</th>
|
||||
<th class="th-resizable" data-sort-key="rss" aria-sort="none">
|
||||
<button type="button" class="th-button" data-sort-key="rss">
|
||||
RSS <span class="sort-indicator" aria-hidden="true"></span>
|
||||
<th
|
||||
class="th-resizable"
|
||||
data-sort-key="rss"
|
||||
aria-sort="none"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="th-button"
|
||||
data-sort-key="rss"
|
||||
>
|
||||
RSS
|
||||
<span
|
||||
class="sort-indicator"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
<div class="col-resizer" data-col="rss" title="Resize"></div>
|
||||
<div
|
||||
class="col-resizer"
|
||||
data-col="rss"
|
||||
title="Resize"
|
||||
></div>
|
||||
</th>
|
||||
<th class="th-resizable">
|
||||
<span>Actions</span>
|
||||
<div class="col-resizer" data-col="actions" title="Resize"></div>
|
||||
<div
|
||||
class="col-resizer"
|
||||
data-col="actions"
|
||||
title="Resize"
|
||||
></div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -470,8 +540,14 @@ app.get("/", async (c) => {
|
||||
const sortFeedId = feed.id.toLowerCase();
|
||||
const sortEmail = emailAddress.toLowerCase();
|
||||
const sortRss = rssUrl.toLowerCase();
|
||||
const descDisplay = clampText(feed.description || "", 220);
|
||||
const descHover = clampText(feed.description || "", 1000);
|
||||
const descDisplay = clampText(
|
||||
feed.description || "",
|
||||
220,
|
||||
);
|
||||
const descHover = clampText(
|
||||
feed.description || "",
|
||||
1000,
|
||||
);
|
||||
const searchHaystack =
|
||||
`${clampText(feed.title, 320)} ${feed.id} ${clampText(feed.description || "", 320)}`.toLowerCase();
|
||||
|
||||
@@ -495,7 +571,9 @@ app.get("/", async (c) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<strong class="truncate" title="${titleHover}"
|
||||
<strong
|
||||
class="truncate"
|
||||
title="${titleHover}"
|
||||
>${titleDisplay}</strong
|
||||
>
|
||||
${feed.description
|
||||
@@ -553,9 +631,7 @@ app.get("/", async (c) => {
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M20 6L9 17l-5-5"
|
||||
></path>
|
||||
<path d="M20 6L9 17l-5-5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -605,9 +681,7 @@ app.get("/", async (c) => {
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M20 6L9 17l-5-5"
|
||||
></path>
|
||||
<path d="M20 6L9 17l-5-5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -667,7 +741,10 @@ app.get("/", async (c) => {
|
||||
const rssUrl = `https://${env.DOMAIN}/rss/${feed.id}`;
|
||||
const titleDisplay = clampText(feed.title, 140);
|
||||
const titleHover = clampText(feed.title, 1000);
|
||||
const descDisplay = clampText(feed.description || "", 240);
|
||||
const descDisplay = clampText(
|
||||
feed.description || "",
|
||||
240,
|
||||
);
|
||||
const descHover = clampText(feed.description || "", 1000);
|
||||
const searchHaystack =
|
||||
`${clampText(feed.title, 320)} ${feed.id} ${clampText(feed.description || "", 320)}`.toLowerCase();
|
||||
@@ -684,7 +761,9 @@ app.get("/", async (c) => {
|
||||
</h3>
|
||||
${feed.description
|
||||
? html`<p class="feed-description">
|
||||
<span title="${descHover}">${descDisplay}</span>
|
||||
<span title="${descHover}"
|
||||
>${descDisplay}</span
|
||||
>
|
||||
</p>`
|
||||
: ""}
|
||||
</div>
|
||||
@@ -1490,7 +1569,8 @@ app.post("/feeds/create", async (c) => {
|
||||
const title = formData.get("title")?.toString() || "";
|
||||
const description = formData.get("description")?.toString();
|
||||
const language = formData.get("language")?.toString() || "en";
|
||||
const view = formData.get("view")?.toString() === "table" ? "table" : "list";
|
||||
const view =
|
||||
formData.get("view")?.toString() === "table" ? "table" : "list";
|
||||
const allowedSenders = parseAllowedSenders(
|
||||
formData.get("allowed_senders")?.toString() || "",
|
||||
);
|
||||
@@ -1776,15 +1856,16 @@ async function purgeFeedKeysStep(
|
||||
listComplete: boolean;
|
||||
}> {
|
||||
const prefix = `feed:${feedId}:`;
|
||||
const limit = Math.min(
|
||||
1000,
|
||||
Math.max(1, Math.floor(options.limit || 100)),
|
||||
);
|
||||
const limit = Math.min(1000, Math.max(1, Math.floor(options.limit || 100)));
|
||||
const cursor = options.cursor || undefined;
|
||||
|
||||
const listed = await emailStorage.list({ prefix, cursor, limit });
|
||||
const keys = (listed.keys || []).map((k) => k.name);
|
||||
const { ok, failed } = await deleteKeysWithConcurrency(emailStorage, keys, 35);
|
||||
const { ok, failed } = await deleteKeysWithConcurrency(
|
||||
emailStorage,
|
||||
keys,
|
||||
35,
|
||||
);
|
||||
|
||||
return {
|
||||
deletedKeys: ok,
|
||||
@@ -1816,7 +1897,10 @@ app.post("/feeds/:feedId/delete", async (c) => {
|
||||
} catch (error) {
|
||||
console.error("Error deleting feed:", error);
|
||||
if (wantsJson) {
|
||||
return c.json({ ok: false, error: "Error deleting feed. Please try again." }, 400);
|
||||
return c.json(
|
||||
{ ok: false, error: "Error deleting feed. Please try again." },
|
||||
400,
|
||||
);
|
||||
}
|
||||
return c.text("Error deleting feed. Please try again.", 400);
|
||||
}
|
||||
@@ -1953,7 +2037,8 @@ app.post("/feeds/bulk-delete", async (c) => {
|
||||
}
|
||||
|
||||
const formData = await c.req.formData();
|
||||
const view = formData.get("view")?.toString() === "table" ? "table" : "list";
|
||||
const view =
|
||||
formData.get("view")?.toString() === "table" ? "table" : "list";
|
||||
const redirectBase = `/admin?view=${view}`;
|
||||
const rawIds = formData.getAll("feedIds").map((value) => value.toString());
|
||||
const parsedFeedIds = Array.from(new Set(rawIds.filter(Boolean)));
|
||||
@@ -2136,7 +2221,9 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
||||
</div>
|
||||
|
||||
<h2>
|
||||
Emails (<span id="email-total-count">${feedMetadata.emails.length}</span>)
|
||||
Emails (<span id="email-total-count"
|
||||
>${feedMetadata.emails.length}</span
|
||||
>)
|
||||
</h2>
|
||||
|
||||
${message === "bulkDeleted"
|
||||
@@ -2211,21 +2298,57 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
||||
onchange="toggleAllEmails(this.checked)"
|
||||
/>
|
||||
</th>
|
||||
<th class="th-resizable" data-sort-key="subject" aria-sort="none">
|
||||
<button type="button" class="th-button" data-sort-key="subject">
|
||||
Subject <span class="sort-indicator" aria-hidden="true"></span>
|
||||
<th
|
||||
class="th-resizable"
|
||||
data-sort-key="subject"
|
||||
aria-sort="none"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="th-button"
|
||||
data-sort-key="subject"
|
||||
>
|
||||
Subject
|
||||
<span
|
||||
class="sort-indicator"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
<div class="col-resizer" data-col="subject" title="Resize"></div>
|
||||
<div
|
||||
class="col-resizer"
|
||||
data-col="subject"
|
||||
title="Resize"
|
||||
></div>
|
||||
</th>
|
||||
<th class="th-resizable" data-sort-key="receivedAt" aria-sort="none">
|
||||
<button type="button" class="th-button" data-sort-key="receivedAt">
|
||||
Received <span class="sort-indicator" aria-hidden="true"></span>
|
||||
<th
|
||||
class="th-resizable"
|
||||
data-sort-key="receivedAt"
|
||||
aria-sort="none"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="th-button"
|
||||
data-sort-key="receivedAt"
|
||||
>
|
||||
Received
|
||||
<span
|
||||
class="sort-indicator"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
<div class="col-resizer" data-col="receivedAt" title="Resize"></div>
|
||||
<div
|
||||
class="col-resizer"
|
||||
data-col="receivedAt"
|
||||
title="Resize"
|
||||
></div>
|
||||
</th>
|
||||
<th class="th-resizable">
|
||||
<span>Actions</span>
|
||||
<div class="col-resizer" data-col="actions" title="Resize"></div>
|
||||
<div
|
||||
class="col-resizer"
|
||||
data-col="actions"
|
||||
title="Resize"
|
||||
></div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -2235,7 +2358,10 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
||||
const subjectHover = clampText(email.subject, 1000);
|
||||
const sortSubject = subjectHover.toLowerCase();
|
||||
const sortReceivedAt = String(email.receivedAt);
|
||||
const searchHaystack = clampText(email.subject, 320).toLowerCase();
|
||||
const searchHaystack = clampText(
|
||||
email.subject,
|
||||
320,
|
||||
).toLowerCase();
|
||||
|
||||
return html`
|
||||
<tr
|
||||
@@ -3261,7 +3387,10 @@ app.post("/emails/:emailKey/delete", async (c) => {
|
||||
} catch (error) {
|
||||
console.error("Error deleting email:", error);
|
||||
if (wantsJson) {
|
||||
return c.json({ ok: false, error: "Error deleting email. Please try again." }, 400);
|
||||
return c.json(
|
||||
{ ok: false, error: "Error deleting email. Please try again." },
|
||||
400,
|
||||
);
|
||||
}
|
||||
return c.text("Error deleting email. Please try again.", 400);
|
||||
}
|
||||
@@ -3295,7 +3424,9 @@ app.post("/feeds/:feedId/emails/bulk-delete", async (c) => {
|
||||
emailKeys?: unknown;
|
||||
} | null;
|
||||
|
||||
const rawEmailKeys = Array.isArray(body?.emailKeys) ? body?.emailKeys : [];
|
||||
const rawEmailKeys = Array.isArray(body?.emailKeys)
|
||||
? body?.emailKeys
|
||||
: [];
|
||||
const emailKeys = Array.from(
|
||||
new Set(rawEmailKeys.map((value) => String(value)).filter(Boolean)),
|
||||
);
|
||||
|
||||
+26
-17
@@ -1,6 +1,6 @@
|
||||
import { Context } from 'hono';
|
||||
import { Env, FeedConfig, FeedMetadata, EmailData } from '../types';
|
||||
import { generateRssFeed } from '../utils/feed-generator';
|
||||
import { Context } from "hono";
|
||||
import { Env, FeedConfig, FeedMetadata, EmailData } from "../types";
|
||||
import { generateRssFeed } from "../utils/feed-generator";
|
||||
|
||||
/**
|
||||
* Generates an RSS feed for a specific feed ID
|
||||
@@ -11,10 +11,10 @@ export async function handle(c: Context): Promise<Response> {
|
||||
const env = c.env as unknown as Env;
|
||||
|
||||
// Extract the feed ID from the route params
|
||||
const feedId = c.req.param('feedId');
|
||||
const feedId = c.req.param("feedId");
|
||||
|
||||
if (!feedId) {
|
||||
return new Response('Feed ID is required', { status: 400 });
|
||||
return new Response("Feed ID is required", { status: 400 });
|
||||
}
|
||||
|
||||
// Get the KV namespace
|
||||
@@ -22,21 +22,27 @@ export async function handle(c: Context): Promise<Response> {
|
||||
|
||||
// Check if the feed exists
|
||||
const feedMetadataKey = `feed:${feedId}:metadata`;
|
||||
const feedMetadata = await emailStorage.get(feedMetadataKey, 'json') as FeedMetadata | null;
|
||||
const feedMetadata = (await emailStorage.get(
|
||||
feedMetadataKey,
|
||||
"json",
|
||||
)) as FeedMetadata | null;
|
||||
|
||||
if (!feedMetadata) {
|
||||
return new Response('Feed not found', { status: 404 });
|
||||
return new Response("Feed not found", { status: 404 });
|
||||
}
|
||||
|
||||
// Get feed configuration (title, description, etc.)
|
||||
const feedConfigKey = `feed:${feedId}:config`;
|
||||
const feedConfig = await emailStorage.get(feedConfigKey, 'json') as FeedConfig | null || {
|
||||
const feedConfig = ((await emailStorage.get(
|
||||
feedConfigKey,
|
||||
"json",
|
||||
)) as FeedConfig | null) || {
|
||||
title: `Newsletter Feed ${feedId}`,
|
||||
description: 'Converted email newsletter',
|
||||
description: "Converted email newsletter",
|
||||
site_url: `https://${env.DOMAIN}/rss/${feedId}`,
|
||||
feed_url: `https://${env.DOMAIN}/rss/${feedId}`,
|
||||
language: 'en',
|
||||
created_at: Date.now()
|
||||
language: "en",
|
||||
created_at: Date.now(),
|
||||
};
|
||||
|
||||
// Get the emails for this feed (up to the last 20)
|
||||
@@ -45,7 +51,10 @@ export async function handle(c: Context): Promise<Response> {
|
||||
|
||||
// Fetch all email content
|
||||
for (const email of emails) {
|
||||
const emailData = await emailStorage.get(email.key, 'json') as EmailData | null;
|
||||
const emailData = (await emailStorage.get(
|
||||
email.key,
|
||||
"json",
|
||||
)) as EmailData | null;
|
||||
if (emailData) {
|
||||
emailsData.push(emailData);
|
||||
}
|
||||
@@ -59,12 +68,12 @@ export async function handle(c: Context): Promise<Response> {
|
||||
return new Response(rssXml, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/rss+xml',
|
||||
'Cache-Control': 'max-age=1800' // 30 minutes cache
|
||||
}
|
||||
"Content-Type": "application/rss+xml",
|
||||
"Cache-Control": "max-age=1800", // 30 minutes cache
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating RSS feed:', error);
|
||||
return new Response('Internal Server Error', { status: 500 });
|
||||
console.error("Error generating RSS feed:", error);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file is kept for backwards compatibility
|
||||
// It re-exports the new modular design system
|
||||
|
||||
import { designSystem } from './index';
|
||||
import { interactiveScripts, authHelpers } from '../scripts/index';
|
||||
import { designSystem } from "./index";
|
||||
import { interactiveScripts, authHelpers } from "../scripts/index";
|
||||
|
||||
export { designSystem, interactiveScripts, authHelpers };
|
||||
+12
-5
@@ -1,10 +1,10 @@
|
||||
// Main style exports file
|
||||
// Combines all style components and re-exports them for easy imports
|
||||
|
||||
import { variables, lightModeTheme, fontImport } from './variables';
|
||||
import { layoutStyles } from './layout';
|
||||
import { componentStyles } from './components';
|
||||
import { utilityStyles } from './utilities';
|
||||
import { variables, lightModeTheme, fontImport } from "./variables";
|
||||
import { layoutStyles } from "./layout";
|
||||
import { componentStyles } from "./components";
|
||||
import { utilityStyles } from "./utilities";
|
||||
|
||||
// Combine all style components into a single CSS string
|
||||
export const designSystem = `
|
||||
@@ -17,4 +17,11 @@ export const designSystem = `
|
||||
`;
|
||||
|
||||
// Re-export everything for modular usage if needed
|
||||
export { variables, lightModeTheme, fontImport, layoutStyles, componentStyles, utilityStyles };
|
||||
export {
|
||||
variables,
|
||||
lightModeTheme,
|
||||
fontImport,
|
||||
layoutStyles,
|
||||
componentStyles,
|
||||
utilityStyles,
|
||||
};
|
||||
|
||||
Vendored
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { Env } from './index';
|
||||
import { Env } from "./index";
|
||||
|
||||
// Extend Hono's types to include our custom environment
|
||||
declare module 'hono' {
|
||||
declare module "hono" {
|
||||
interface ContextVariableMap {
|
||||
env: Env;
|
||||
}
|
||||
|
||||
Vendored
+6
-3
@@ -1,8 +1,11 @@
|
||||
// Extend mailparser types for Buffer in worker environment
|
||||
declare module 'buffer-polyfill' {
|
||||
declare module "buffer-polyfill" {
|
||||
global {
|
||||
var Buffer: {
|
||||
from(data: string, encoding?: string): {
|
||||
from(
|
||||
data: string,
|
||||
encoding?: string,
|
||||
): {
|
||||
toString(encoding?: string): string;
|
||||
};
|
||||
};
|
||||
@@ -10,7 +13,7 @@ declare module 'buffer-polyfill' {
|
||||
}
|
||||
|
||||
// Add missing atob declaration
|
||||
declare module 'atob-polyfill' {
|
||||
declare module "atob-polyfill" {
|
||||
global {
|
||||
function atob(data: string): string;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
declare module 'rss' {
|
||||
declare module "rss" {
|
||||
interface RSSOptions {
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
+25
-19
@@ -1,4 +1,4 @@
|
||||
import { EmailData } from '../types';
|
||||
import { EmailData } from "../types";
|
||||
|
||||
/**
|
||||
* Simple email parser specialized for ForwardEmail.net's webhook format
|
||||
@@ -21,25 +21,26 @@ export class EmailParser {
|
||||
*/
|
||||
static parseForwardEmailPayload(payload: any): EmailData {
|
||||
if (!payload) {
|
||||
throw new Error('Missing or invalid webhook payload');
|
||||
throw new Error("Missing or invalid webhook payload");
|
||||
}
|
||||
|
||||
// Extract the "to" address
|
||||
const toAddress = payload.recipients?.[0] || '';
|
||||
const toAddress = payload.recipients?.[0] || "";
|
||||
|
||||
// Extract the sender information using ForwardEmail's structure
|
||||
const fromAddress = payload.from?.text ||
|
||||
(payload.from?.value?.[0]?.address ?
|
||||
`${payload.from.value[0].name || ''} <${payload.from.value[0].address}>` :
|
||||
'Unknown Sender');
|
||||
const fromAddress =
|
||||
payload.from?.text ||
|
||||
(payload.from?.value?.[0]?.address
|
||||
? `${payload.from.value[0].name || ""} <${payload.from.value[0].address}>`
|
||||
: "Unknown Sender");
|
||||
|
||||
// Extract subject
|
||||
let subject = payload.subject || 'No Subject';
|
||||
let subject = payload.subject || "No Subject";
|
||||
// Decode any encoded words in the subject
|
||||
subject = this.decodeEncodedWords(subject);
|
||||
|
||||
// Get content, preferring HTML over plain text
|
||||
const content = payload.html || payload.text || '';
|
||||
const content = payload.html || payload.text || "";
|
||||
|
||||
// Create simple email data object
|
||||
return {
|
||||
@@ -47,7 +48,7 @@ export class EmailParser {
|
||||
from: fromAddress,
|
||||
content,
|
||||
receivedAt: payload.date ? new Date(payload.date).getTime() : Date.now(),
|
||||
headers: this.extractHeaders(payload)
|
||||
headers: this.extractHeaders(payload),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,14 +60,16 @@ export class EmailParser {
|
||||
|
||||
// Extract headers from headerLines if available
|
||||
if (payload.headerLines && Array.isArray(payload.headerLines)) {
|
||||
payload.headerLines.forEach((h: {key: string; line: string}) => {
|
||||
payload.headerLines.forEach((h: { key: string; line: string }) => {
|
||||
const key = h.key.toLowerCase();
|
||||
const value = h.line.replace(new RegExp(`^${h.key}:\\s*`, 'i'), '').trim();
|
||||
const value = h.line
|
||||
.replace(new RegExp(`^${h.key}:\\s*`, "i"), "")
|
||||
.trim();
|
||||
headers[key] = value;
|
||||
});
|
||||
}
|
||||
// Or from headers string if provided
|
||||
else if (typeof payload.headers === 'string') {
|
||||
else if (typeof payload.headers === "string") {
|
||||
payload.headers.split(/\r?\n/).forEach((line: string) => {
|
||||
const match = line.match(/^([^:]+):\s*(.*)$/);
|
||||
if (match) {
|
||||
@@ -83,11 +86,13 @@ export class EmailParser {
|
||||
* @param text Text that may contain encoded words like =?UTF-8?Q?Hello_World?=
|
||||
*/
|
||||
static decodeEncodedWords(text: string): string {
|
||||
if (!text) return '';
|
||||
if (!text) return "";
|
||||
|
||||
// Simple RFC 2047 encoded-word decoder
|
||||
return text.replace(/=\?([^?]+)\?([BQ])\?([^?]+)\?=/gi, (_, charset, encoding, text) => {
|
||||
if (encoding.toUpperCase() === 'B') {
|
||||
return text.replace(
|
||||
/=\?([^?]+)\?([BQ])\?([^?]+)\?=/gi,
|
||||
(_, charset, encoding, text) => {
|
||||
if (encoding.toUpperCase() === "B") {
|
||||
// Base64 encoding
|
||||
try {
|
||||
const decoded = atob(text);
|
||||
@@ -95,12 +100,13 @@ export class EmailParser {
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
} else if (encoding.toUpperCase() === 'Q') {
|
||||
} else if (encoding.toUpperCase() === "Q") {
|
||||
// Quoted-printable encoding
|
||||
return this.decodeQuotedPrintable(text.replace(/_/g, ' '));
|
||||
return this.decodeQuotedPrintable(text.replace(/_/g, " "));
|
||||
}
|
||||
return text;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+12
-10
@@ -1,5 +1,5 @@
|
||||
import { Feed } from 'feed';
|
||||
import { FeedConfig, EmailData } from '../types';
|
||||
import { Feed } from "feed";
|
||||
import { FeedConfig, EmailData } from "../types";
|
||||
|
||||
/**
|
||||
* Generate an RSS feed from a list of emails
|
||||
@@ -7,31 +7,33 @@ import { FeedConfig, EmailData } from '../types';
|
||||
export function generateRssFeed(
|
||||
feedConfig: FeedConfig,
|
||||
emails: EmailData[],
|
||||
baseUrl: string
|
||||
baseUrl: string,
|
||||
): string {
|
||||
// Create a new feed
|
||||
const feed = new Feed({
|
||||
title: feedConfig.title,
|
||||
description: feedConfig.description || '',
|
||||
description: feedConfig.description || "",
|
||||
id: feedConfig.feed_url,
|
||||
link: feedConfig.site_url,
|
||||
language: feedConfig.language,
|
||||
updated: new Date(),
|
||||
generator: 'Email-to-RSS',
|
||||
generator: "Email-to-RSS",
|
||||
copyright: `Copyright © ${new Date().getFullYear()} ${feedConfig.title}`,
|
||||
feedLinks: {
|
||||
rss: feedConfig.feed_url
|
||||
rss: feedConfig.feed_url,
|
||||
},
|
||||
author: feedConfig.author ? {
|
||||
author: feedConfig.author
|
||||
? {
|
||||
name: feedConfig.author,
|
||||
email: `noreply@${new URL(feedConfig.site_url).hostname}`
|
||||
} : undefined
|
||||
email: `noreply@${new URL(feedConfig.site_url).hostname}`,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// Add each email as a feed item
|
||||
for (const email of emails) {
|
||||
const date = new Date(email.receivedAt);
|
||||
const uniqueId = `${email.receivedAt}-${Buffer.from(email.subject).toString('base64').substring(0, 10)}`;
|
||||
const uniqueId = `${email.receivedAt}-${Buffer.from(email.subject).toString("base64").substring(0, 10)}`;
|
||||
|
||||
feed.addItem({
|
||||
title: email.subject,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { nouns } from '../data/nouns';
|
||||
import { nouns } from "../data/nouns";
|
||||
|
||||
/**
|
||||
* Generates a random feed ID in the format noun1.noun2.XY
|
||||
|
||||
+38
-21
@@ -1,4 +1,10 @@
|
||||
import { EmailData, FeedConfig, FeedMetadata, FeedList, EmailMetadata } from '../types';
|
||||
import {
|
||||
EmailData,
|
||||
FeedConfig,
|
||||
FeedMetadata,
|
||||
FeedList,
|
||||
EmailMetadata,
|
||||
} from "../types";
|
||||
|
||||
/**
|
||||
* Store email data in KV
|
||||
@@ -6,7 +12,7 @@ import { EmailData, FeedConfig, FeedMetadata, FeedList, EmailMetadata } from '..
|
||||
export async function storeEmail(
|
||||
kv: KVNamespace,
|
||||
feedId: string,
|
||||
emailData: EmailData
|
||||
emailData: EmailData,
|
||||
): Promise<string> {
|
||||
// Generate a unique key for this email
|
||||
const timestamp = Date.now();
|
||||
@@ -19,7 +25,7 @@ export async function storeEmail(
|
||||
await updateFeedMetadata(kv, feedId, {
|
||||
key,
|
||||
subject: emailData.subject,
|
||||
receivedAt: timestamp
|
||||
receivedAt: timestamp,
|
||||
});
|
||||
|
||||
return key;
|
||||
@@ -31,10 +37,12 @@ export async function storeEmail(
|
||||
async function updateFeedMetadata(
|
||||
kv: KVNamespace,
|
||||
feedId: string,
|
||||
emailMetadata: EmailMetadata
|
||||
emailMetadata: EmailMetadata,
|
||||
): Promise<void> {
|
||||
const feedMetadataKey = `feed:${feedId}:metadata`;
|
||||
const existingMetadata = await kv.get(feedMetadataKey, { type: 'json' }) as FeedMetadata | null;
|
||||
const existingMetadata = (await kv.get(feedMetadataKey, {
|
||||
type: "json",
|
||||
})) as FeedMetadata | null;
|
||||
|
||||
const metadata: FeedMetadata = existingMetadata || { emails: [] };
|
||||
|
||||
@@ -55,10 +63,12 @@ async function updateFeedMetadata(
|
||||
*/
|
||||
export async function getFeedMetadata(
|
||||
kv: KVNamespace,
|
||||
feedId: string
|
||||
feedId: string,
|
||||
): Promise<FeedMetadata | null> {
|
||||
const feedMetadataKey = `feed:${feedId}:metadata`;
|
||||
return await kv.get(feedMetadataKey, { type: 'json' }) as FeedMetadata | null;
|
||||
return (await kv.get(feedMetadataKey, {
|
||||
type: "json",
|
||||
})) as FeedMetadata | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,10 +76,10 @@ export async function getFeedMetadata(
|
||||
*/
|
||||
export async function getFeedConfig(
|
||||
kv: KVNamespace,
|
||||
feedId: string
|
||||
feedId: string,
|
||||
): Promise<FeedConfig | null> {
|
||||
const feedConfigKey = `feed:${feedId}:config`;
|
||||
return await kv.get(feedConfigKey, { type: 'json' }) as FeedConfig | null;
|
||||
return (await kv.get(feedConfigKey, { type: "json" })) as FeedConfig | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,9 +87,9 @@ export async function getFeedConfig(
|
||||
*/
|
||||
export async function getEmailData(
|
||||
kv: KVNamespace,
|
||||
key: string
|
||||
key: string,
|
||||
): Promise<EmailData | null> {
|
||||
return await kv.get(key, { type: 'json' }) as EmailData | null;
|
||||
return (await kv.get(key, { type: "json" })) as EmailData | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +98,7 @@ export async function getEmailData(
|
||||
export async function createFeed(
|
||||
kv: KVNamespace,
|
||||
feedId: string,
|
||||
feedConfig: FeedConfig
|
||||
feedConfig: FeedConfig,
|
||||
): Promise<void> {
|
||||
// Store feed configuration
|
||||
const feedConfigKey = `feed:${feedId}:config`;
|
||||
@@ -96,9 +106,12 @@ export async function createFeed(
|
||||
|
||||
// Create empty metadata for the feed
|
||||
const feedMetadataKey = `feed:${feedId}:metadata`;
|
||||
await kv.put(feedMetadataKey, JSON.stringify({
|
||||
emails: []
|
||||
}));
|
||||
await kv.put(
|
||||
feedMetadataKey,
|
||||
JSON.stringify({
|
||||
emails: [],
|
||||
}),
|
||||
);
|
||||
|
||||
// Add feed to the list of all feeds
|
||||
await addFeedToList(kv, feedId, feedConfig.title, feedConfig.description);
|
||||
@@ -111,17 +124,19 @@ export async function addFeedToList(
|
||||
kv: KVNamespace,
|
||||
feedId: string,
|
||||
title: string,
|
||||
description?: string
|
||||
description?: string,
|
||||
): Promise<void> {
|
||||
const feedListKey = 'feeds:list';
|
||||
const existingList = await kv.get(feedListKey, { type: 'json' }) as FeedList | null;
|
||||
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
|
||||
description,
|
||||
});
|
||||
|
||||
await kv.put(feedListKey, JSON.stringify(feedList));
|
||||
@@ -131,8 +146,10 @@ export async function addFeedToList(
|
||||
* 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;
|
||||
const feedListKey = "feeds:list";
|
||||
const feedList = (await kv.get(feedListKey, {
|
||||
type: "json",
|
||||
})) as FeedList | null;
|
||||
|
||||
return feedList || { feeds: [] };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user