UNPKG

4chan-ts

Version:

A typescript client wrapper for 4chan api

588 lines (586 loc) 19.5 kB
// node_modules/@better-fetch/fetch/dist/index.js var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => (key in obj) ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var BetterFetchError = class extends Error { constructor(status, statusText, error) { super(statusText || status.toString(), { cause: error }); this.status = status; this.statusText = statusText; this.error = error; } }; var initializePlugins = async (url, options) => { var _a, _b, _c, _d, _e, _f; let opts = options || {}; const hooks = { onRequest: [options == null ? undefined : options.onRequest], onResponse: [options == null ? undefined : options.onResponse], onSuccess: [options == null ? undefined : options.onSuccess], onError: [options == null ? undefined : options.onError], onRetry: [options == null ? undefined : options.onRetry] }; if (!options || !(options == null ? undefined : options.plugins)) { return { url, options: opts, hooks }; } for (const plugin of (options == null ? undefined : options.plugins) || []) { if (plugin.init) { const pluginRes = await ((_a = plugin.init) == null ? undefined : _a.call(plugin, url.toString(), options)); opts = pluginRes.options || opts; url = pluginRes.url; } hooks.onRequest.push((_b = plugin.hooks) == null ? undefined : _b.onRequest); hooks.onResponse.push((_c = plugin.hooks) == null ? undefined : _c.onResponse); hooks.onSuccess.push((_d = plugin.hooks) == null ? undefined : _d.onSuccess); hooks.onError.push((_e = plugin.hooks) == null ? undefined : _e.onError); hooks.onRetry.push((_f = plugin.hooks) == null ? undefined : _f.onRetry); } return { url, options: opts, hooks }; }; var LinearRetryStrategy = class { constructor(options) { this.options = options; } shouldAttemptRetry(attempt, response) { if (this.options.shouldRetry) { return Promise.resolve(attempt < this.options.attempts && this.options.shouldRetry(response)); } return Promise.resolve(attempt < this.options.attempts); } getDelay() { return this.options.delay; } }; var ExponentialRetryStrategy = class { constructor(options) { this.options = options; } shouldAttemptRetry(attempt, response) { if (this.options.shouldRetry) { return Promise.resolve(attempt < this.options.attempts && this.options.shouldRetry(response)); } return Promise.resolve(attempt < this.options.attempts); } getDelay(attempt) { const delay = Math.min(this.options.maxDelay, this.options.baseDelay * 2 ** attempt); return delay; } }; function createRetryStrategy(options) { if (typeof options === "number") { return new LinearRetryStrategy({ type: "linear", attempts: options, delay: 1000 }); } switch (options.type) { case "linear": return new LinearRetryStrategy(options); case "exponential": return new ExponentialRetryStrategy(options); default: throw new Error("Invalid retry strategy"); } } var getAuthHeader = async (options) => { const headers = {}; const getValue = async (value) => typeof value === "function" ? await value() : value; if (options == null ? undefined : options.auth) { if (options.auth.type === "Bearer") { const token = await getValue(options.auth.token); if (!token) { return headers; } headers["authorization"] = `Bearer ${token}`; } else if (options.auth.type === "Basic") { const username = getValue(options.auth.username); const password = getValue(options.auth.password); if (!username || !password) { return headers; } headers["authorization"] = `Basic ${btoa(`${username}:${password}`)}`; } else if (options.auth.type === "Custom") { const value = getValue(options.auth.value); if (!value) { return headers; } headers["authorization"] = `${getValue(options.auth.prefix)} ${value}`; } } return headers; }; var JSON_RE = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(;.+)?$/i; function detectResponseType(request) { const _contentType = request.headers.get("content-type"); const textTypes = /* @__PURE__ */ new Set([ "image/svg", "application/xml", "application/xhtml", "application/html" ]); if (!_contentType) { return "json"; } const contentType = _contentType.split(";").shift() || ""; if (JSON_RE.test(contentType)) { return "json"; } if (textTypes.has(contentType) || contentType.startsWith("text/")) { return "text"; } return "blob"; } function isJSONParsable(value) { try { JSON.parse(value); return true; } catch (error) { return false; } } function isJSONSerializable(value) { if (value === undefined) { return false; } const t = typeof value; if (t === "string" || t === "number" || t === "boolean" || t === null) { return true; } if (t !== "object") { return false; } if (Array.isArray(value)) { return true; } if (value.buffer) { return false; } return value.constructor && value.constructor.name === "Object" || typeof value.toJSON === "function"; } function jsonParse(text) { try { return JSON.parse(text); } catch (error) { return text; } } function isFunction(value) { return typeof value === "function"; } function getFetch(options) { if (options == null ? undefined : options.customFetchImpl) { return options.customFetchImpl; } if (typeof globalThis !== "undefined" && isFunction(globalThis.fetch)) { return globalThis.fetch; } if (typeof window !== "undefined" && isFunction(window.fetch)) { return window.fetch; } throw new Error("No fetch implementation found"); } async function getHeaders(opts) { const headers = new Headers(opts == null ? undefined : opts.headers); const authHeader = await getAuthHeader(opts); for (const [key, value] of Object.entries(authHeader || {})) { headers.set(key, value); } if (!headers.has("content-type")) { const t = detectContentType(opts == null ? undefined : opts.body); if (t) { headers.set("content-type", t); } } return headers; } function detectContentType(body) { if (isJSONSerializable(body)) { return "application/json"; } return null; } function getBody(options) { if (!(options == null ? undefined : options.body)) { return null; } const headers = new Headers(options == null ? undefined : options.headers); if (isJSONSerializable(options.body) && !headers.has("content-type")) { for (const [key, value] of Object.entries(options == null ? undefined : options.body)) { if (value instanceof Date) { options.body[key] = value.toISOString(); } } return JSON.stringify(options.body); } return options.body; } function getMethod(url, options) { var _a; if (options == null ? undefined : options.method) { return options.method.toUpperCase(); } if (url.startsWith("@")) { const pMethod = (_a = url.split("@")[1]) == null ? undefined : _a.split("/")[0]; if (!methods.includes(pMethod)) { return (options == null ? undefined : options.body) ? "POST" : "GET"; } return pMethod.toUpperCase(); } return (options == null ? undefined : options.body) ? "POST" : "GET"; } function getTimeout(options, controller) { let abortTimeout; if (!(options == null ? undefined : options.signal) && (options == null ? undefined : options.timeout)) { abortTimeout = setTimeout(() => controller == null ? undefined : controller.abort(), options == null ? undefined : options.timeout); } return { abortTimeout, clearTimeout: () => { if (abortTimeout) { clearTimeout(abortTimeout); } } }; } var ValidationError = class _ValidationError extends Error { constructor(issues, message) { super(message || JSON.stringify(issues, null, 2)); this.issues = issues; Object.setPrototypeOf(this, _ValidationError.prototype); } }; async function parseStandardSchema(schema, input) { let result = await schema["~standard"].validate(input); if (result.issues) { throw new ValidationError(result.issues); } return result.value; } var methods = ["get", "post", "put", "patch", "delete"]; var applySchemaPlugin = (config) => ({ id: "apply-schema", name: "Apply Schema", version: "1.0.0", async init(url, options) { var _a, _b, _c, _d; const schema = ((_b = (_a = config.plugins) == null ? undefined : _a.find((plugin) => { var _a2; return ((_a2 = plugin.schema) == null ? undefined : _a2.config) ? url.startsWith(plugin.schema.config.baseURL || "") || url.startsWith(plugin.schema.config.prefix || "") : false; })) == null ? undefined : _b.schema) || config.schema; if (schema) { let urlKey = url; if ((_c = schema.config) == null ? undefined : _c.prefix) { if (urlKey.startsWith(schema.config.prefix)) { urlKey = urlKey.replace(schema.config.prefix, ""); if (schema.config.baseURL) { url = url.replace(schema.config.prefix, schema.config.baseURL); } } } if ((_d = schema.config) == null ? undefined : _d.baseURL) { if (urlKey.startsWith(schema.config.baseURL)) { urlKey = urlKey.replace(schema.config.baseURL, ""); } } const keySchema = schema.schema[urlKey]; if (keySchema) { let opts = __spreadProps(__spreadValues({}, options), { method: keySchema.method, output: keySchema.output }); if (!(options == null ? undefined : options.disableValidation)) { opts = __spreadProps(__spreadValues({}, opts), { body: keySchema.input ? await parseStandardSchema(keySchema.input, options == null ? undefined : options.body) : options == null ? undefined : options.body, params: keySchema.params ? await parseStandardSchema(keySchema.params, options == null ? undefined : options.params) : options == null ? undefined : options.params, query: keySchema.query ? await parseStandardSchema(keySchema.query, options == null ? undefined : options.query) : options == null ? undefined : options.query }); } return { url, options: opts }; } } return { url, options }; } }); var createFetch = (config) => { async function $fetch(url, options) { const opts = __spreadProps(__spreadValues(__spreadValues({}, config), options), { plugins: [...(config == null ? undefined : config.plugins) || [], applySchemaPlugin(config || {})] }); if (config == null ? undefined : config.catchAllError) { try { return await betterFetch(url, opts); } catch (error) { return { data: null, error: { status: 500, statusText: "Fetch Error", message: "Fetch related error. Captured by catchAllError option. See error property for more details.", error } }; } } return await betterFetch(url, opts); } return $fetch; }; function getURL2(url, option) { let { baseURL, params, query } = option || { query: {}, params: {}, baseURL: "" }; let basePath = url.startsWith("http") ? url.split("/").slice(0, 3).join("/") : baseURL || ""; if (url.startsWith("@")) { const m = url.toString().split("@")[1].split("/")[0]; if (methods.includes(m)) { url = url.replace(`@${m}/`, "/"); } } if (!basePath.endsWith("/")) basePath += "/"; let [path, urlQuery] = url.replace(basePath, "").split("?"); const queryParams = new URLSearchParams(urlQuery); for (const [key, value] of Object.entries(query || {})) { if (value == null) continue; queryParams.set(key, String(value)); } if (params) { if (Array.isArray(params)) { const paramPaths = path.split("/").filter((p) => p.startsWith(":")); for (const [index, key] of paramPaths.entries()) { const value = params[index]; path = path.replace(key, value); } } else { for (const [key, value] of Object.entries(params)) { path = path.replace(`:${key}`, String(value)); } } } path = path.split("/").map(encodeURIComponent).join("/"); if (path.startsWith("/")) path = path.slice(1); let queryParamString = queryParams.toString(); queryParamString = queryParamString.length > 0 ? `?${queryParamString}`.replace(/\+/g, "%20") : ""; if (!basePath.startsWith("http")) { return `${basePath}${path}${queryParamString}`; } const _url = new URL(`${path}${queryParamString}`, basePath); return _url; } var betterFetch = async (url, options) => { var _a, _b, _c, _d, _e, _f, _g, _h; const { hooks, url: __url, options: opts } = await initializePlugins(url, options); const fetch = getFetch(opts); const controller = new AbortController; const signal = (_a = opts.signal) != null ? _a : controller.signal; const _url = getURL2(__url, opts); const body = getBody(opts); const headers = await getHeaders(opts); const method = getMethod(__url, opts); let context = __spreadProps(__spreadValues({}, opts), { url: _url, headers, body, method, signal }); for (const onRequest of hooks.onRequest) { if (onRequest) { const res = await onRequest(context); if (res instanceof Object) { context = res; } } } if ("pipeTo" in context && typeof context.pipeTo === "function" || typeof ((_b = options == null ? undefined : options.body) == null ? undefined : _b.pipe) === "function") { if (!("duplex" in context)) { context.duplex = "half"; } } const { clearTimeout: clearTimeout2 } = getTimeout(opts, controller); let response = await fetch(context.url, context); clearTimeout2(); const responseContext = { response, request: context }; for (const onResponse of hooks.onResponse) { if (onResponse) { const r = await onResponse(__spreadProps(__spreadValues({}, responseContext), { response: ((_c = options == null ? undefined : options.hookOptions) == null ? undefined : _c.cloneResponse) ? response.clone() : response })); if (r instanceof Response) { response = r; } else if (r instanceof Object) { response = r.response; } } } if (response.ok) { const hasBody = context.method !== "HEAD"; if (!hasBody) { return { data: "", error: null }; } const responseType = detectResponseType(response); const successContext = { data: "", response, request: context }; if (responseType === "json" || responseType === "text") { const text = await response.text(); const parser2 = (_d = context.jsonParser) != null ? _d : jsonParse; const data = await parser2(text); successContext.data = data; } else { successContext.data = await response[responseType](); } if (context == null ? undefined : context.output) { if (context.output && !context.disableValidation) { successContext.data = await parseStandardSchema(context.output, successContext.data); } } for (const onSuccess of hooks.onSuccess) { if (onSuccess) { await onSuccess(__spreadProps(__spreadValues({}, successContext), { response: ((_e = options == null ? undefined : options.hookOptions) == null ? undefined : _e.cloneResponse) ? response.clone() : response })); } } if (options == null ? undefined : options.throw) { return successContext.data; } return { data: successContext.data, error: null }; } const parser = (_f = options == null ? undefined : options.jsonParser) != null ? _f : jsonParse; const responseText = await response.text(); const isJSONResponse = isJSONParsable(responseText); const errorObject = isJSONResponse ? await parser(responseText) : null; const errorContext = { response, responseText, request: context, error: __spreadProps(__spreadValues({}, errorObject), { status: response.status, statusText: response.statusText }) }; for (const onError of hooks.onError) { if (onError) { await onError(__spreadProps(__spreadValues({}, errorContext), { response: ((_g = options == null ? undefined : options.hookOptions) == null ? undefined : _g.cloneResponse) ? response.clone() : response })); } } if (options == null ? undefined : options.retry) { const retryStrategy = createRetryStrategy(options.retry); const _retryAttempt = (_h = options.retryAttempt) != null ? _h : 0; if (await retryStrategy.shouldAttemptRetry(_retryAttempt, response)) { for (const onRetry of hooks.onRetry) { if (onRetry) { await onRetry(responseContext); } } const delay = retryStrategy.getDelay(_retryAttempt); await new Promise((resolve) => setTimeout(resolve, delay)); return await betterFetch(url, __spreadProps(__spreadValues({}, options), { retryAttempt: _retryAttempt + 1 })); } } if (options == null ? undefined : options.throw) { throw new BetterFetchError(response.status, response.statusText, isJSONResponse ? errorObject : responseText); } return { data: null, error: __spreadProps(__spreadValues({}, errorObject), { status: response.status, statusText: response.statusText }) }; }; // src/client.ts class FourChanClient { baseUrl = "https://a.4cdn.org"; imageBaseUrl = "https://i.4cdn.org"; thumbnailBaseUrl = "https://t.4cdn.org"; $fetch = createFetch({ baseURL: this.baseUrl, headers: { Accept: "application/json" }, retry: { type: "linear", attempts: 3, delay: 1200 } }); async getBoards() { return this.$fetch("/boards.json"); } async getCatalog(board) { if (!board) return { data: null, error: { message: "Board name is required", status: 400, statusText: "Bad Request" } }; return this.$fetch(`/${board}/catalog.json`); } async getIndexes(board, page) { if (!board || !page) return { data: null, error: { message: "Board name or page number is missing", status: 400, statusText: "Bad Request" } }; return this.$fetch(`/${board}/${page}.json`); } async getThread(board, id) { if (!board || !id) return { data: null, error: { message: "Board name or thread ID is missing", status: 400, statusText: "Bad Request" } }; return this.$fetch(`/${board}/thread/${id}.json`); } } export { FourChanClient };