advent-of-code-client
Version:
A NodeJS client for fetching inputs, running puzzle challenges and submitting answers to Advent Of Code directly from your JavaScript code.
193 lines (177 loc) • 5.28 kB
text/typescript
import fetch, { RequestInit } from 'node-fetch';
import { Cache, Config, Result } from '../AocClient.types';
import logger from './logger';
const pkg = require('../../package.json');
const HOST_URI = 'https://adventofcode.com';
const USER_AGENT = `node/${process.version} ${pkg.name}/${pkg.version}`;
const TOO_EARLY_REQUEST_TEXT =
"please don't repeatedly request this endpoint before it unlocks";
const UNAUTHENTICATED_INPUT_TEXT = 'please log in to get your puzzle input';
const INTERNAL_SERVER_ERROR_TEXT = 'internal server error';
const HTML_RESPONSE_TEXT = '!DOCTYPE HTML';
const CORRECT_ANSWER_TEXT = "that's the right answer";
const INCORRECT_ANSWER_TEXT = "that's not the right answer";
const TOO_RECENT_ANSWER_TEXT =
'you gave an answer too recently; you have to wait after submitting an answer before trying again.';
const INCORRECT_LEVEL_TEXT = "you don't seem to be solving the right level";
const fetchFromCacheOrAoC = async (
cacheKey: string,
uri: string,
options: RequestInit,
config: Config,
cache: Cache
): Promise<string> => {
if (config.useCache) {
const cachedResponse = cache.get(cacheKey);
// use the cached response response if it exists
if (cachedResponse) {
logger.debug(
'Found a previously cached response, returning response from cache'
);
return Promise.resolve(cachedResponse);
}
}
// otherwise call AoC
logger.debug(
'No previously cached response found, fetching from Advent Of Code'
);
const response = await fetch(uri, options);
return response.text();
};
export async function getInput(config: Config, cache: Cache) {
const { year, day, token } = config;
const uri = `${HOST_URI}/${year}/day/${day}/input`;
const options = {
method: 'get',
headers: {
'Content-Type': 'text/plain',
Cookie: `session=${token}`,
'User-Agent': USER_AGENT
}
};
const cacheKey = JSON.stringify({
uri,
token
});
const textResponse = await fetchFromCacheOrAoC(
cacheKey,
uri,
options,
config,
cache
);
if (textResponse.toLowerCase().includes(UNAUTHENTICATED_INPUT_TEXT)) {
return Promise.reject(
new Error(
'You must log in to get your puzzle input, please provide a valid token'
)
);
}
if (textResponse.toLowerCase().includes(TOO_EARLY_REQUEST_TEXT)) {
return Promise.reject(
new Error(
'This puzzle has not opened yet, please wait until the puzzle unlocks!'
)
);
}
if (textResponse.toLowerCase().includes(INTERNAL_SERVER_ERROR_TEXT)) {
return Promise.reject(
new Error(
'An unexpected error occurred while fetching the input, internal server error.'
)
);
}
if (textResponse.includes(HTML_RESPONSE_TEXT)) {
return Promise.reject(
new Error(
'An error occurred while fetching the input. Are you authenticated correctly?'
)
);
}
if (config.useCache && !cache.get(cacheKey)) {
// update cache if it had not been set previously
cache.set(cacheKey, textResponse);
}
return textResponse;
}
type Params = {
part: Result;
answer: Result;
};
export const postAnswer = async (
{ part, answer }: Params,
config: Config,
cache: Cache
) => {
const { year, day, token } = config;
const uri = `${HOST_URI}/${year}/day/${day}/answer`;
const options = {
method: 'post',
headers: {
Cookie: `session=${token}`,
'User-Agent': USER_AGENT,
'cache-control': 'no-cache',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
level: `${part}`,
answer: `${answer}`
})
};
const cacheKey = JSON.stringify({
uri,
token,
part,
answer
});
const cachedResponse = cache.get(cacheKey);
const textResponse = await fetchFromCacheOrAoC(
cacheKey,
uri,
// @ts-expect-error for some reason TS thinks that the body type is incompatible
options,
config,
cache
);
const text = textResponse.toLowerCase();
if (text.includes(CORRECT_ANSWER_TEXT)) {
if (config.useCache && !cachedResponse) {
// Update cache if no previously cached response
cache.set(cacheKey, textResponse);
}
return Promise.resolve({ correct: true });
}
if (text.includes(INCORRECT_ANSWER_TEXT)) {
if (config.useCache && !cachedResponse) {
// Update cache if no previously cached response
cache.set(cacheKey, textResponse);
}
return Promise.resolve({ correct: false });
}
if (text.includes(TOO_RECENT_ANSWER_TEXT)) {
const leftToWaitText = text
.split(TOO_RECENT_ANSWER_TEXT)[1]
.split('.')[0]
.trim();
return Promise.reject(
new Error(`You gave an answer too recently, ${leftToWaitText}.`)
);
}
if (text.includes(TOO_EARLY_REQUEST_TEXT)) {
return Promise.reject(
new Error(
'This puzzle has not opened yet, please wait until the puzzle unlocks!'
)
);
}
if (text.includes(INCORRECT_LEVEL_TEXT)) {
return Promise.reject(
new Error(
"You don't seem to be solving the correct level. Did you already complete it?"
)
);
}
return Promise.reject(
new Error('Unknown response from AoC. Are you authenticated correctly?')
);
};