diff options
Diffstat (limited to 'server')
| -rw-r--r-- | server/daily.ts | 100 | ||||
| -rw-r--r-- | server/data/mvs.ts | 122 | ||||
| -rw-r--r-- | server/shared.ts | 14 |
3 files changed, 236 insertions, 0 deletions
diff --git a/server/daily.ts b/server/daily.ts index cfc9d6a..f3eb9d8 100644 --- a/server/daily.ts +++ b/server/daily.ts @@ -11,6 +11,7 @@ import { verifySessionToken, verifyStateSignature, getDailySong, + getDailyMusicVideo, } from "./shared"; export const dailyRouter = Router(); @@ -42,6 +43,25 @@ dailyRouter.get("/today", (_req, res) => { }); }); +dailyRouter.get("/todayMV", (_req, res) => { + const date = getUtcDate(); + const mv = getDailyMusicVideo(date); + + const initialState: SignedState = { + date, + currentTry: 0, + didGuess: false, + guesses: [], + }; + + res.json({ + date, + data: obfuscateSong(mv, date), + sessionToken: buildSessionToken(date), + initialSig: signState(initialState), + }); +}); + dailyRouter.post("/guess", guessLimiter, (req, res) => { const today = getUtcDate(); const body = req.body as { @@ -121,3 +141,83 @@ dailyRouter.post("/guess", guessLimiter, (req, res) => { guessState: nextGuess.state, }); }); + +dailyRouter.post("/guessMV", guessLimiter, (req, res) => { + const today = getUtcDate(); + const body = req.body as { + sessionToken?: unknown; + state?: unknown; + sig?: unknown; + guess?: unknown; + }; + + if (!verifySessionToken(body.sessionToken, today)) { + res.status(401).json({ error: "Invalid session token." }); + return; + } + + const state = normalizeSignedState(body.state); + if (!state) { + res.status(400).json({ error: "Invalid state payload." }); + return; + } + + if (state.date !== today) { + res.status(409).json({ error: "State date is not valid for today." }); + return; + } + + if (!verifyStateSignature(state, body.sig)) { + res.status(403).json({ error: "State signature mismatch." }); + return; + } + + if (state.didGuess || state.currentTry >= 6) { + res.status(409).json({ error: "Round already finished." }); + return; + } + + let nextGuess: GuessEntry; + const guess = body.guess as { artist?: unknown; name?: unknown } | null | undefined; + + if (guess == null) { + nextGuess = { + state: "Skipped", + }; + } else if (typeof guess.artist === "string" && typeof guess.name === "string") { + const solution = getDailyMusicVideo(today); + + if (guess.artist === solution.artist && guess.name === solution.name) { + nextGuess = { + song: { artist: guess.artist, name: guess.name }, + state: "Correct", + }; + } else if (guess.artist === solution.artist) { + nextGuess = { + song: { artist: guess.artist, name: guess.name }, + state: "PartiallyCorrect", + }; + } else { + nextGuess = { + song: { artist: guess.artist, name: guess.name }, + state: "Incorrect", + }; + } + } else { + res.status(400).json({ error: "Invalid guess payload." }); + return; + } + + const nextState: SignedState = { + date: state.date, + currentTry: Math.min(6, state.currentTry + 1), + didGuess: state.didGuess || nextGuess.state === "Correct", + guesses: [...state.guesses, nextGuess], + }; + + res.json({ + state: nextState, + sig: signState(nextState), + guessState: nextGuess.state, + }); +}); diff --git a/server/data/mvs.ts b/server/data/mvs.ts new file mode 100644 index 0000000..74a1d00 --- /dev/null +++ b/server/data/mvs.ts @@ -0,0 +1,122 @@ +export const musicVideos = [ + { artist: "aespa", name: "Supernova", youtubeId: "phuiiNCxRMg" }, + { artist: "aespa", name: "Spicy", youtubeId: "Os_heh8vPfs" }, + { artist: "ILLIT", name: "Magnetic", youtubeId: "Vk5-c_v4gMU" }, + { artist: "aespa", name: "Whiplash", youtubeId: "jWQx2f-CErU" }, + { artist: "i-dle", name: "Senorita", youtubeId: "2cevbhEqQF4" }, + { artist: "LE SSERAFIM", name: "CRAZY", youtubeId: "n6B5gQXlB-0" }, + { artist: "KATSEYE", name: "Gameboy", youtubeId: "-bC4iak3kxg" }, + { artist: "i-dle", name: "I Want That", youtubeId: "Iewisp9KYRw" }, + { artist: "i-dle", name: "Nxde", youtubeId: "fCO7f0SmrDc" }, + { artist: "TWICE", name: "What is Love?", youtubeId: "i0p1bmr0EmE" }, + { artist: "NewJeans", name: "OMG", youtubeId: "sVTy_wmn5SU" }, + { artist: "ITZY", name: "WANNABE", youtubeId: "fE2h3lGlOsk" }, + { artist: "i-dle", name: "LATATA", youtubeId: "p4RIhcY7V3c" }, + { artist: "STAYC", name: "GPT", youtubeId: "DskqpUrvlmw" }, + { artist: "XG", name: "LEFT RIGHT", youtubeId: "T6YVgEpRU6Q" }, + { artist: "i-dle", name: "Good Thing", youtubeId: "hAONx6nuEgI" }, + { artist: "LE SSERAFIM", name: "HOT", youtubeId: "r9AEGPB6qIU" }, + { artist: "XG", name: "GALA", youtubeId: "8cEdndsuzSE" }, + { artist: "Hearts2Hearts", name: "FOCUS", youtubeId: "Ur7aK4FvK-U" }, + { artist: "LE SSERAFIM", name: "DIFFERENT", youtubeId: "HFZUAXhdnHk" }, + { artist: "MINNIE", name: "HER", youtubeId: "UTHqh7Vpa64" }, + { artist: "YEJI (ITZY)", name: "Air", youtubeId: "TamDOXponnM" }, + { artist: "i-dle", name: "DUMDi DUMDi", youtubeId: "HPQ5mqovXHo" }, + { artist: "aespa", name: "Drama", youtubeId: "D8VEhcPeSlc" }, + { artist: "i-dle", name: "Klaxon", youtubeId: "rTKqSmX9XhQ" }, + { artist: "IVE", name: "I AM", youtubeId: "6ZUIwj3FgUY" }, + { artist: "IVE", name: "BANG BANG", youtubeId: "9qkpcLK422o" }, + { artist: "aespa", name: "Next Level", youtubeId: "4TWR90KJl84" }, + { artist: "i-dle", name: "TOMBOY", youtubeId: "Jh4QFaPmdss" }, + { artist: "NMIXX", name: "Love Me Like This", youtubeId: "EDnwWcFpObo" }, + { artist: "LE SSERAFIM", name: "ANTIFRAGILE", youtubeId: "pyf8cbqyfPs" }, + { artist: "ITZY", name: "Motto", youtubeId: "1jKnI_ib8c8" }, + { artist: "i-dle", name: "Mono", youtubeId: "DYgE3SGPEqk" }, + { artist: "NewJeans", name: "How Sweet", youtubeId: "Q3K0TOvTOno" }, + { artist: "aespa", name: "Rich Man", youtubeId: "5oQVTnq-UKk" }, + { artist: "ILLIT", name: "Do the Dance", youtubeId: "negtrQu5mTA" }, + { artist: "MEOVV", name: "BURNING UP", youtubeId: "hlULtigNNao" }, + { artist: "ITZY", name: "Girls Will Be Girls", youtubeId: "1KhOhW_O8-k" }, + { artist: "ILLIT", name: "Lucky Girl Syndrome", youtubeId: "UCmgGZbfjmk" }, + { artist: "TWICE", name: "THIS IS FOR", youtubeId: "eHHQaoEW30Q" }, + { artist: "BLACKPINK", name: "GO", youtubeId: "2GJfWMYCWY0" }, + { artist: "MIYEON", name: "Drive", youtubeId: "35lirBqwgTs" }, + { artist: "KATSEYE", name: "Debut", youtubeId: "bYg6aMDQ_TA" }, + { artist: "FIFTY FIFTY", name: "Cupid", youtubeId: "Qc7_zRjH808" }, + { artist: "NMIXX", name: "Young, Dumb, Stupid", youtubeId: "5eh6Vj_vVg4" }, + { artist: "NMIXX", name: "DICE", youtubeId: "p1bjnyDqI9k" }, + { artist: "ITZY", name: "Cheshire", youtubeId: "zugAhfd2r0g" }, + { artist: "ITZY", name: "DALLA DALLA", youtubeId: "pNfTK39k55U" }, + { artist: "ITZY", name: "LOCO", youtubeId: "MjCZfZfucEc" }, + { artist: "XG", name: "NEW DANCE", youtubeId: "LAZlfCjXPb0" }, + { artist: "BLACKPINK", name: "DDU-DU DDU-DU", youtubeId: "IHNzOHi8sJs" }, + { artist: "BLACKPINK", name: "AS IF IT'S YOUR LAST", youtubeId: "Amq-qlqbjYA" }, + { artist: "BLACKPINK", name: "STAY", youtubeId: "FzVR_fymZw4" }, + { artist: "NMIXX", name: "Party O’Clock", youtubeId: "Rd2wppggYxo" }, + { artist: "IVE", name: "XOXZ", youtubeId: "B1ShLiq3EVc" }, + { artist: "IVE", name: "BLACKHOLE", youtubeId: "1Lmy7qwmSMc" }, + { artist: "IVE", name: "After LIKE", youtubeId: "F0B7HDiY-10" }, + { artist: "IVE", name: "REBEL HEART", youtubeId: "g36q0ZLvygQ" }, + { artist: "IVE", name: "Kitsch", youtubeId: "pG6iaOMV46I" }, + { artist: "IVE", name: "Supernova Love", youtubeId: "fyk6vjwI3wc" }, + { artist: "aespa", name: "Armageddon", youtubeId: "nFYwcndNuOY" }, + { artist: "tripleS", name: "Girls Never Die", youtubeId: "2tda_TCjz8w" }, + { artist: "tripleS", name: "Baby Flower", youtubeId: "6q8UMVXhXsE" }, + { artist: "tripleS", name: "Rising", youtubeId: "3TQd2ahq6oU" }, + { artist: "tripleS", name: "Are You Alive", youtubeId: "pTycfmVzdl8" }, + { artist: "tripleS", name: "Cameo Love", youtubeId: "With0YDpuWQ" }, + { artist: "STAYC", name: "2:LOVE", youtubeId: "HgewvBTOJwU" }, + { artist: "STAYC", name: "MWUAH", youtubeId: "udOf9JGMA7U" }, + { artist: "STAYC", name: "BEBE", youtubeId: "z-xfGoabprU" }, + { artist: "STAYC", name: "I WANT IT", youtubeId: "W8ml4ryPwic" }, + { artist: "Hearts2Hearts", name: "Lemon Tang", youtubeId: "1VqxWNwgf5Q" }, + { artist: "Hearts2Hearts", name: "The Chase", youtubeId: "kxUA2wwYiME" }, + { artist: "Hearts2Hearts", name: "RUDE!", youtubeId: "F7sGJVUrkjQ" }, + { artist: "Hearts2Hearts", name: "STYLE", youtubeId: "n7kFRxFIPrI" }, + { artist: "Hearts2Hearts", name: "Butterflies", youtubeId: "hJ9Wp3PO3c8" }, + { artist: "TWICE", name: "LIKEY", youtubeId: "V2hlQkVJZhE" }, + { artist: "TWICE", name: "Talk that Talk", youtubeId: "k6jqx9kZgPM" }, + { artist: "TWICE", name: "FANCY", youtubeId: "kOHB85vDuow" }, + { artist: "Red Velvet", name: "Cosmic", youtubeId: "FyG21rXCxlY" }, + { artist: "Red Velvet", name: "Chill Kill", youtubeId: "xlyrt5eAtKI" }, + { artist: "Red Velvet", name: "Psycho", youtubeId: "uR8Mrt1IpXg" }, + { artist: "Red Velvet", name: "Peek-A-Boo", youtubeId: "6uJf2IT2Zh8" }, + { artist: "Red Velvet", name: "Russian Roulette", youtubeId: "QslJYDX3o8s" }, + { artist: "Red Velvet", name: "Feel My Rhythm", youtubeId: "R9At2ICm4LQ" }, + { artist: "BABYMONSTER", name: "WE GO UP", youtubeId: "wlHwjkYpSr0" }, + { artist: "BABYMONSTER", name: "PSYCHO", youtubeId: "yd_uG3TtREs" }, + { artist: "BABYMONSTER", name: "SUGAR HONEY ICE TEA", youtubeId: "naoGk-Zjc1s" }, + { artist: "BABYMONSTER", name: "DRIP", youtubeId: "Zp-Jhuhq0bQ" }, + { artist: "BABYMONSTER", name: "Really Like You", youtubeId: "XShaIZs7J7M" }, + { artist: "BABYMONSTER", name: "SHEESH", youtubeId: "2wA_b6YHjqQ" }, + { artist: "BABYMONSTER", name: "SUPA DUPA LUV", youtubeId: "SbdOIdg2McI" }, + { artist: "fromis_9", name: "Supersonic", youtubeId: "0LiQp7y8Wwc" }, + { artist: "fromis_9", name: "DM", youtubeId: "4gXmClk8rKI" }, + { artist: "fromis_9", name: "LIKE YOU BETTER", youtubeId: "r1QapH1SIAI" }, + { artist: "fromis_9", name: "#menow", youtubeId: "QrYIOjs7K8E" }, + { artist: "fromis_9", name: "LOVE BOMB", youtubeId: "-SK6cvkK4c0" }, + { artist: "fromis_9", name: "FUN!", youtubeId: "zsRyyLtcXho" }, + { artist: "ifeye", name: "NERDY", youtubeId: "I_msu2-4UAU" }, + { artist: "ifeye", name: "r u ok?", youtubeId: "tGMy0z1akLs" }, + { artist: "ifeye", name: "Hazy (Daisy)", youtubeId: "e-bwzm8-xnU" }, + { artist: "RESCENE", name: "Deja Vu", youtubeId: "ZbO9PBdFRdc" }, + { artist: "RESCENE", name: "LOVE ATTACK", youtubeId: "9XttLI0oH0I" }, + { artist: "RESCENE", name: "Bloom", youtubeId: "MC6-82GRK5I" }, + { artist: "RESCENE", name: "Runaway", youtubeId: "rsZwrTNklos" }, + { artist: "RESCENE", name: "Glow Up", youtubeId: "h0xUtrb_JBc" }, + { artist: "RESCENE", name: "Pinball", youtubeId: "B8JJ8RNM-60" }, + { artist: "ILLIT", name: "It’s Me", youtubeId: "bMhDJ0S0OBA" }, + { artist: "ILLIT", name: "jellyous", youtubeId: "GkG60kISnfc" }, + { artist: "ILLIT", name: "NOT CUTE ANYMORE", youtubeId: "x_RYZsOfpKY" }, + { artist: "ILLIT", name: "ICONIC BY MISTAKE", youtubeId: "27C4pfRsf9g" }, + { artist: "ILLIT", name: "Cherish (My Love)", youtubeId: "tbDGl7jEazA" }, + { artist: "ablume", name: "Echo", youtubeId: "EtecXqZnSDA" }, + { artist: "izna", name: "BEEP", youtubeId: "wLKpQpC3-Sw" }, + { artist: "izna", name: "IZNA", youtubeId: "d3mqW9wqqx0" }, + { artist: "izna", name: "SIGN", youtubeId: "88GkYKvnvvI" }, + { artist: "NiziU", name: "Too Bad", youtubeId: "b_GqrwmptWo" }, + { artist: "NiziU", name: "Make you happy", youtubeId: "QW28YKqdxe0" }, + { artist: "NiziU", name: "HEARTIS", youtubeId: "gRfDY9WrGwk" }, + { artist: "NiziU", name: "♡Emotion", youtubeId: "1brvTNG4Caw" }, + { artist: "NiziU", name: "Chopstick", youtubeId: "nCjmXHsRJNY" }, +] diff --git a/server/shared.ts b/server/shared.ts index 3b5344c..9744de9 100644 --- a/server/shared.ts +++ b/server/shared.ts @@ -1,5 +1,7 @@ import { createHmac, timingSafeEqual } from "crypto"; import { songs } from "./data/songs"; +import { musicVideos } from "./data/mvs"; + function getSalt(): string { return process.env.VITE_HEARDLE_SALT ?? "changeme"; @@ -58,6 +60,7 @@ export function pickSong(date: string): Song { return songs[index]; } + function getLastNDates(n: number): string[] { const dates: string[] = []; for (let i = 1; i <= n; i++) { @@ -83,6 +86,17 @@ export function getDailySong(today: string): Song { return candidate; } +export function pickMusicVideo(date: string): Song { + const seed = hashString(date); + const index = seed % musicVideos.length; + return musicVideos[index]; +} + +export function getDailyMusicVideo(today: string): Song { + // potentially use deterministic no-repeat here too, for now this is fine + return pickMusicVideo(today); +} + function signValue(prefix: string, value: string): string { return createHmac("sha256", getSigningSecret()) .update(`${prefix}:${value}`) |
