@cedx/akismet
Version:
Prevent comment spam using the Akismet service.
155 lines (132 loc) • 5.04 kB
text/typescript
import type {Blog} from "./blog.js";
import {CheckResult} from "./check_result.js";
import type {Comment} from "./comment.js";
/**
* Submits comments to the [Akismet](https://akismet.com) service.
*/
export class Client {
/**
* The response returned by the `submit-ham` and `submit-spam` endpoints when the outcome is a success.
*/
static readonly #success = "Thanks for making the web a better place.";
/**
* The package version.
*/
static readonly #version = "16.2.1";
/**
* The Akismet API key.
*/
apiKey: string;
/**
* The base URL of the remote API endpoint.
*/
baseUrl: URL;
/**
* The front page or home URL of the instance making requests.
*/
blog: Blog;
/**
* Value indicating whether the client operates in test mode.
*/
isTest: boolean;
/**
* The user agent string to use when making requests.
*/
userAgent: string;
/**
* Creates a new client.
* @param apiKey The Akismet API key.
* @param blog The front page or home URL of the instance making requests.
* @param options An object providing values to initialize this instance.
*/
constructor(apiKey: string, blog: Blog, options: ClientOptions = {}) {
const {baseUrl = "https://rest.akismet.com"} = options;
const url = baseUrl instanceof URL ? baseUrl.href : baseUrl;
this.apiKey = apiKey;
this.baseUrl = new URL(url.endsWith("/") ? url : `${url}/`);
this.blog = blog;
this.isTest = options.isTest ?? false;
this.userAgent = options.userAgent ?? `${navigator.userAgent} | Akismet/${Client.#version}`;
}
/**
* Checks the specified comment against the service database, and returns a value indicating whether it is spam.
* @param comment The comment to be checked.
* @returns A value indicating whether the specified comment is spam.
*/
async checkComment(comment: Comment): Promise<CheckResult> {
const response = await this.#fetch("1.1/comment-check", comment.toJSON());
return await response.text() == "false"
? CheckResult.ham
: response.headers.get("x-akismet-pro-tip") == "discard" ? CheckResult.pervasiveSpam : CheckResult.spam;
}
/**
* Submits the specified comment that was incorrectly marked as spam but should not have been.
* @param comment The comment to be submitted.
* @returns Resolves once the comment has been submitted.
*/
async submitHam(comment: Comment): Promise<void> {
const response = await this.#fetch("1.1/submit-ham", comment.toJSON());
if (await response.text() != Client.#success) throw Error("Invalid server response.");
}
/**
* Submits the specified comment that was not marked as spam but should have been.
* @param comment The comment to be submitted.
* @returns Resolves once the comment has been submitted.
*/
async submitSpam(comment: Comment): Promise<void> {
const response = await this.#fetch("1.1/submit-spam", comment.toJSON());
if (await response.text() != Client.#success) throw Error("Invalid server response.");
}
/**
* Checks the API key against the service database, and returns a value indicating whether it is valid.
* @returns `true` if the specified API key is valid, otherwise `false`.
*/
async verifyKey(): Promise<boolean> {
try {
const response = await this.#fetch("1.1/verify-key");
return await response.text() == "valid";
}
catch {
return false;
}
}
/**
* Queries the service by posting the specified fields to a given end point, and returns the response.
* @param endpoint The URL of the end point to query.
* @param fields The fields describing the query body.
* @returns The server response.
*/
async #fetch(endpoint: string, fields: Record<string, any> = {}): Promise<Response> {
const body = new URLSearchParams({...this.blog.toJSON(), api_key: this.apiKey});
if (this.isTest) body.set("is_test", "1");
for (const [key, value] of Object.entries(fields))
if (!Array.isArray(value)) body.set(key, String(value));
else {
let index = 0;
for (const item of value) body.set(`${key}[${index++}]`, String(item));
}
const response = await fetch(new URL(endpoint, this.baseUrl), {method: "POST", headers: {"user-agent": this.userAgent}, body});
if (!response.ok) throw Error(`${response.status} ${response.statusText}`);
const {headers} = response;
if (headers.has("x-akismet-alert-code")) throw Error(`${headers.get("x-akismet-alert-code")} ${headers.get("x-akismet-alert-msg")}`);
if (headers.has("x-akismet-debug-help")) throw Error(headers.get("x-akismet-debug-help")!);
return response;
}
}
/**
* Defines the options of a {@link Client} instance.
*/
export type ClientOptions = Partial<{
/**
* The base URL of the remote API endpoint.
*/
baseUrl: URL|string;
/**
* Value indicating whether the client operates in test mode.
*/
isTest: boolean;
/**
* The user agent string to use when making requests.
*/
userAgent: string;
}>;