UNPKG

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
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?') ); };