aocjs
Version:
Advent of Code API client.
244 lines (237 loc) • 7.95 kB
JavaScript
;
const defu = require('defu');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
const defu__default = /*#__PURE__*/_interopDefaultCompat(defu);
var __defProp$1 = Object.defineProperty;
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$1 = (obj, key, value) => {
__defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class ClientError extends Error {
constructor(message) {
super(message);
this.name = "AOCClientError";
}
}
class AuthenticationError extends ClientError {
constructor(message = "Authentication failed") {
super(message);
this.name = "AuthenticationError";
}
}
class NetworkError extends ClientError {
constructor(message, status, statusText) {
super(message);
__publicField$1(this, "status");
__publicField$1(this, "statusText");
this.name = "NetworkError";
this.status = status;
this.statusText = statusText;
}
}
class LeaderboardError extends ClientError {
constructor(message) {
super(message);
this.name = "LeaderboardError";
}
}
class SubmissionError extends ClientError {
constructor(message) {
super(message);
this.name = "SubmissionError";
}
}
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class Client {
constructor(options) {
__publicField(this, "session");
__publicField(this, "user-agent", "aocjs (https://npmjs.com/package/aocjs)");
__publicField(this, "logger");
this.session = options.session;
if (options["user-agent"])
this["user-agent"] = options["user-agent"];
this.logger = options.logger || console.error;
}
/**
* Internal fetcher with improved error handling
*/
async fetcher(path, _options) {
if (!this.session) {
throw new AuthenticationError("Session cookie is required");
}
const options = defu__default(_options, {
headers: {
Cookie: `session=${this.session}`,
"User-Agent": this["user-agent"]
}
});
try {
const request = await fetch(`https://adventofcode.com/${path}`, options);
if (!request.ok) {
const errorMessage = `Network request failed: ${request.status} ${request.statusText}`;
this.logger(errorMessage);
throw new NetworkError(errorMessage, request.status, request.statusText);
}
return request;
} catch (error) {
if (error instanceof NetworkError)
throw error;
this.logger(
`Fetch error: ${error instanceof Error ? error.message : String(error)}`
);
throw new NetworkError(
"Network request failed",
void 0,
error instanceof Error ? error.message : void 0
);
}
}
/**
* Get a puzzle's input.
*
* @param year Advent of Code year.
* @param day Advent of Code year's puzzle day.
*/
async getInput(year, day) {
try {
return await (await this.fetcher(`${year}/day/${day}/input`)).text();
} catch (error) {
this.logger(
`Failed to get input for year ${year}, day ${day}: ${error instanceof Error ? error.message : String(error)}`
);
throw error;
}
}
/**
* Gets your specified private leaderboard.
* @param year Advent of Code year.
* @param id Your leaderboard id.
* @param [sorted=false] If true, returns a sorted array of leaderboard members by stars.
*/
async getLeaderboard(year, id, sorted) {
try {
const request = await this.fetcher(
`${year}/leaderboard/private/view/${id}.json`
);
if (request.status === 302) {
throw new LeaderboardError(
`Cannot access leaderboard: Year ${year}, ID ${id}. Does the leaderboard exist or do you have access?`
);
}
const data = await request.json();
if (sorted) {
return Object.values(data.members).sort(
(a, b) => b.stars - a.stars || b.local_score - a.local_score || b.global_score - a.global_score
);
}
return data;
} catch (error) {
this.logger(
`Leaderboard retrieval failed: ${error instanceof Error ? error.message : String(error)}`
);
throw error;
}
}
/**
* Get a puzzle's problem.
*
* @param year Advent of Code year.
* @param day Advent of Code year's puzzle day.
* @param raw If true, returns the raw HTML of the problem.
*/
async getProblem(year, day, raw) {
try {
const request = await this.fetcher(`${year}/day/${day}`);
const html = await request.text();
if (raw) {
return html;
}
return this.getMainElementHtml(html);
} catch (error) {
this.logger(
`Failed to get problem for year ${year}, day ${day}: ${error instanceof Error ? error.message : String(error)}`
);
throw error;
}
}
/**
* Get the main element HTML from a response.
*
* @param html Response HTML.
*/
getMainElementHtml(html) {
const match = /<main\b[^>]*>(.*)<\/main>/s.exec(html);
if (!match) {
throw new ClientError("Could not find main element in response");
}
return match[1];
}
/**
* Submits a solution to the server.
*
* @param year Advent of Code year.
* @param day Advent of Code year's puzzle day.
* @param part Part of the puzzle to submit.
* @param solution Solution to the puzzle.
*/
async submit(year, day, part, solution) {
try {
const request = await this.fetcher(`${year}/day/${day}/answer`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
level: String(part),
answer: String(solution)
})
});
const response = this.getMainElementHtml(await request.text());
if (response.includes("That's the right answer!")) {
return true;
}
if (response.includes("That's not the right answer")) {
return false;
}
if (response.includes("You don't seem to be solving the right level.")) {
const problem = await this.getProblem(year, day);
const problemSplitByPart = problem.split("</article>");
let relevantProblemPart = problemSplitByPart[part];
if (!relevantProblemPart) {
throw new SubmissionError("Could not find correct answer in page");
}
relevantProblemPart = relevantProblemPart.split("<article")[0];
const match = /Your puzzle answer was <code>([^<]+)<\/code>/.exec(
relevantProblemPart
);
if (!match) {
throw new SubmissionError("Could not find correct answer in page");
}
const correctAnswer = match[1];
return String(correctAnswer) === solution;
}
if (response.includes("To play, please identify yourself")) {
throw new AuthenticationError("Session cookie is invalid or not set");
}
this.logger(`Unexpected submission response: ${JSON.stringify(response)}`);
throw new SubmissionError("Could not parse submission response");
} catch (error) {
this.logger(
`Submission failed for year ${year}, day ${day}, part ${part}: ${error instanceof Error ? error.message : String(error)}`
);
throw error;
}
}
}
exports.AuthenticationError = AuthenticationError;
exports.Client = Client;
exports.ClientError = ClientError;
exports.LeaderboardError = LeaderboardError;
exports.NetworkError = NetworkError;
exports.SubmissionError = SubmissionError;