posts "question of the day" to a discord webhook

Updated global interfaces, added weighted random theme picker

+193 -3
+30 -3
global.d.ts
··· 1 - interface QuestionItem { 1 + interface SheetData { 2 + themes: ThemesData; 3 + questions: QuestionsData; 4 + } 5 + 6 + interface QuestionsData { 7 + [key: string]: Question[]; 8 + } 9 + 10 + interface Question { 2 11 question: string; 3 12 by: string; 4 13 } 5 14 6 - interface QuestionsData { 7 - [key: string]: QuestionItem[]; 15 + interface ThemesData { 16 + [key: string]: Theme; 17 + } 18 + 19 + interface Theme { 20 + primaryColor: string; 21 + secondaryColor: string; 22 + image: string; 23 + thumbnail: string; 24 + title: string; 25 + author: string; 26 + avatar: string; 27 + footer: string; 28 + isSeasonal: boolean; 29 + startDate: string; 30 + endDate: string; 31 + weight: number; 32 + kvKey: string; 33 + questionCount?: number; 34 + index?: number; 8 35 }
+163
utils/themeUtils.ts
··· 1 + import { load } from "@std/dotenv"; 2 + 3 + await load({ export: true }); 4 + 5 + const SHEET_URL = Deno.env.get("SHEET_URL"); 6 + if (!SHEET_URL) { 7 + throw new Error("SHEET_URL environment variable is not set"); 8 + } 9 + 10 + export async function fetchData(): Promise<SheetData> { 11 + const themesUrl = `${SHEET_URL}?endpoint=themes`; 12 + const questionsUrl = `${SHEET_URL}?endpoint=questions`; 13 + 14 + const fetchPromises = [ 15 + fetch(themesUrl), 16 + fetch(questionsUrl), 17 + ]; 18 + 19 + const results = await Promise.allSettled(fetchPromises); 20 + const finalData: SheetData = {} as SheetData; 21 + 22 + const parsingPromises = results.map(async (result, index) => { 23 + if (result.status === "fulfilled") { 24 + const response = result.value; 25 + const data = await response.json(); 26 + 27 + if (index === 0) { 28 + finalData.themes = data; 29 + } else if (index === 1) { 30 + finalData.questions = data; 31 + } 32 + } else { 33 + const endpoint = index === 0 ? "themes" : "questions"; 34 + console.error(`Error fetching ${endpoint}:`, result.reason); 35 + } 36 + }); 37 + 38 + await Promise.all(parsingPromises); 39 + 40 + return finalData; 41 + } 42 + 43 + function createDateFromString( 44 + isoString: string, 45 + year: number, 46 + ): Temporal.PlainDate { 47 + return Temporal.Instant.from(isoString) 48 + .toZonedDateTimeISO("UTC") 49 + .toPlainDate() 50 + .with({ year: year }); 51 + } 52 + 53 + function weightedRandom(themesData: ThemesData): string { 54 + const keys = Object.keys(themesData); 55 + const totalWeight = keys.reduce((sum, key) => { 56 + const theme = themesData[key]; 57 + const adjustedWeight = theme.isSeasonal && 58 + theme.index !== undefined && 59 + theme.questionCount !== undefined && 60 + theme.index >= theme.questionCount - 1 61 + ? 0 62 + : theme.weight; 63 + return sum + adjustedWeight; 64 + }, 0); 65 + 66 + if (totalWeight === 0) { 67 + throw new Error("All themes have zero weight"); 68 + } 69 + 70 + let random = Math.random() * totalWeight; 71 + 72 + for (const key of keys) { 73 + const theme = themesData[key]; 74 + const adjustedWeight = theme.isSeasonal && 75 + theme.index !== undefined && 76 + theme.questionCount !== undefined && 77 + theme.index >= theme.questionCount - 1 78 + ? 0 79 + : theme.weight; 80 + 81 + random -= adjustedWeight; 82 + if (random < 0) { 83 + return key; 84 + } 85 + } 86 + 87 + // Fallback 88 + return "default"; 89 + } 90 + 91 + export function getAvailableThemes( 92 + date = Temporal.Now.plainDateISO(), 93 + themes: ThemesData, 94 + ): ThemesData { 95 + const currentYear = date.year; 96 + const availableThemes = {} as ThemesData; 97 + 98 + // Check all seasonal themes 99 + for (const [themeName, theme] of Object.entries(themes)) { 100 + if (!theme.isSeasonal || themeName === "default") { 101 + availableThemes[themeName] = theme; 102 + } 103 + 104 + if (!theme.startDate || !theme.endDate) { 105 + continue; 106 + } 107 + 108 + // Convert string dates to Temporal.PlainDate 109 + const startDate = createDateFromString(theme.startDate, currentYear); 110 + const endDate = createDateFromString(theme.endDate, currentYear); 111 + 112 + const inRange = Temporal.PlainDate.compare(startDate, endDate) > 0 113 + ? Temporal.PlainDate.compare(date, startDate) >= 0 || 114 + Temporal.PlainDate.compare( 115 + date, 116 + createDateFromString(theme.endDate, currentYear + 1), 117 + ) <= 0 118 + : Temporal.PlainDate.compare(date, startDate) >= 0 && 119 + Temporal.PlainDate.compare(date, endDate) <= 0; 120 + 121 + if (inRange) { 122 + availableThemes[themeName] = theme; 123 + } 124 + } 125 + 126 + return availableThemes; 127 + } 128 + 129 + export async function drawNextQuestion( 130 + themes: ThemesData, 131 + questions: QuestionsData, 132 + ): Promise<{ 133 + themeName: string; 134 + theme: Theme; 135 + question: Question; 136 + }> { 137 + const kv = await Deno.openKv( 138 + // `https://api.deno.com/databases/${ 139 + // Deno.env.get("DENO_KV_DATABASE_ID") 140 + // }/connect`, 141 + ); 142 + 143 + // Extract all kvKeys from ThemesData 144 + const themeEntries = Object.entries(themes); 145 + const kvKeys = themeEntries.map(([_, theme]) => [theme.kvKey]); 146 + 147 + // Fetch all indices in a single call 148 + const results = await kv.getMany<number[]>(kvKeys); 149 + 150 + themeEntries.forEach(([themeName, theme], i) => { 151 + theme.index = results[i].value ?? 0; 152 + theme.questionCount = questions[themeName]?.length ?? 0; 153 + }); 154 + 155 + const randomThemeName = weightedRandom(themes); 156 + const randomTheme = themes[randomThemeName]; 157 + 158 + return { 159 + themeName: randomThemeName, 160 + theme: randomTheme, 161 + question: questions[randomThemeName][randomTheme.index!], 162 + }; 163 + }