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