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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
import { Song } from "../types/song";
import { GuessState, GuessType } from "../types/guess";
const SALT = import.meta.env.VITE_HEARDLE_SALT ?? "changeme";
const API_URL = import.meta.env.VITE_HEARDLE_API_URL ?? "http://localhost:3001";
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
}
return bytes;
}
function xor(data: Uint8Array, key: Uint8Array): Uint8Array {
const output = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
output[i] = data[i] ^ key[i % key.length];
}
return output;
}
function getObfuscationKey(date = new Date().toISOString().split("T")[0]): Uint8Array {
return new TextEncoder().encode(SALT + date);
}
function decryptResponse(data: string, date?: string): Song {
const obfuscationKey = getObfuscationKey(date);
const obfuscatedBytes = hexToBytes(data);
const decrypted = xor(obfuscatedBytes, obfuscationKey);
return JSON.parse(new TextDecoder().decode(decrypted)) as Song;
}
export interface DailySolution {
date: string;
song: Song;
sessionToken: string;
initialSig: string;
}
export interface DailyGameState {
date: string;
currentTry: number;
didGuess: boolean;
guesses: GuessType[];
}
interface SongGuessPayload {
artist: string;
name: string;
}
interface SubmitDailyGuessRequest {
sessionToken: string;
state: DailyGameState;
sig: string;
guess?: SongGuessPayload | null;
}
interface SubmitDailyGuessResponse {
state: DailyGameState;
sig: string;
guessState: GuessState;
}
export async function getDailySolution(): Promise<DailySolution> {
const solutionData = await fetch(`${API_URL}/today`);
if (!solutionData.ok) {
throw new Error(`Failed to fetch solution: ${solutionData.statusText}`);
}
const { data, date, sessionToken, initialSig } = await solutionData.json();
return {
date,
sessionToken,
initialSig,
song: decryptResponse(data, date),
};
}
export async function getDailyMVSolution(): Promise<DailySolution> {
const solutionData = await fetch(`${API_URL}/todayMV`);
if (!solutionData.ok) {
throw new Error(`Failed to fetch MV solution: ${solutionData.statusText}`);
}
const { data, date, sessionToken, initialSig } = await solutionData.json();
return {
date,
sessionToken,
initialSig,
song: decryptResponse(data, date),
};
}
export async function submitDailyGuess(
payload: SubmitDailyGuessRequest
): Promise<SubmitDailyGuessResponse> {
const response = await fetch(`${API_URL}/guess`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Failed to submit guess: ${response.statusText}`);
}
return (await response.json()) as SubmitDailyGuessResponse;
}
export async function submitDailyMVGuess(
payload: SubmitDailyGuessRequest
): Promise<SubmitDailyGuessResponse> {
const response = await fetch(`${API_URL}/guessMV`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Failed to submit MV guess: ${response.statusText}`);
}
return (await response.json()) as SubmitDailyGuessResponse;
}
export async function getSelectSolution(): Promise<Song> {
const solutionData = await fetch(`${API_URL}/select`);
if (!solutionData.ok) {
throw new Error(`Failed to fetch solution: ${solutionData.statusText}`);
}
const { data } = await solutionData.json();
return decryptResponse(data);
}
|