aboutsummaryrefslogtreecommitdiffstats
path: root/server/daily.ts
blob: 35350f7dbc12575ad5e53ccead7e09b487cdb22f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import { Router } from "express";
import rateLimit from "express-rate-limit";
import {
  buildSessionToken,
  getDailySong,
  getUtcDate,
  GuessEntry,
  normalizeSignedState,
  obfuscateSong,
  SignedState,
  signState,
  verifySessionToken,
  verifyStateSignature,
} from "./shared";

export const dailyRouter = Router();

const guessLimiter = rateLimit({
  windowMs: 60 * 1000,
  limit: 30,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: "Too many requests. Please slow down." },
});

dailyRouter.get("/today", (_req, res) => {
  const date = getUtcDate();
  const song = getDailySong(date);

  const initialState: SignedState = {
    date,
    currentTry: 0,
    didGuess: false,
    guesses: [],
  };

  res.json({
    date,
    data: obfuscateSong(song, date),
    sessionToken: buildSessionToken(date),
    initialSig: signState(initialState),
  });
});

dailyRouter.post("/guess", 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 = getDailySong(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,
  });
});
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage