UNPKG

wiki-saikou

Version:

The library provides the out of box accessing to MediaWiki API in both browsers & Node.js, and the syntax is very similar to vanilla `new mw.Api()`. TypeScript definition included~

530 lines (521 loc) 16.2 kB
'use strict'; const fexios = require('fexios'); exports.MwParamNormalizer = void 0; ((MwParamNormalizer2) => { function normalizeParamValue(item) { if (Array.isArray(item)) { return item.join("|"); } else if (typeof item === "boolean" || item === null) { return item ? "1" : void 0; } else if (typeof item === "number") { return "" + item; } else { return item; } } MwParamNormalizer2.normalizeParamValue = normalizeParamValue; function normalizeBody(body) { const isFormLike = (body2) => body2 && (body2 instanceof URLSearchParams || body2 instanceof FormData); if (body === void 0 || body === null) { return void 0; } const formData = new FormData(); if (isFormLike(body)) { body.forEach((value, key) => { const data = normalizeParamValue(value); if (data !== null && data !== void 0) { formData.append(key, data); } }); return formData; } if (fexios.isPlainObject(body)) { Object.entries(body).forEach(([key, value]) => { const data = normalizeParamValue(value); if (data !== null && data !== void 0) { formData.append(key, data); } }); return formData; } return void 0; } MwParamNormalizer2.normalizeBody = normalizeBody; })(exports.MwParamNormalizer || (exports.MwParamNormalizer = {})); var WikiSaikouErrorCode = /* @__PURE__ */ ((WikiSaikouErrorCode2) => { WikiSaikouErrorCode2["HTTP_ERROR"] = "HTTP_ERROR"; WikiSaikouErrorCode2["LOGIN_FAILED"] = "LOGIN_FAILED"; WikiSaikouErrorCode2["LOGIN_RETRY_LIMIT_EXCEEDED"] = "LOGIN_RETRY_LIMIT_EXCEEDED"; WikiSaikouErrorCode2["TOKEN_RETRY_LIMIT_EXCEEDED"] = "TOKEN_RETRY_LIMIT_EXCEEDED"; return WikiSaikouErrorCode2; })(WikiSaikouErrorCode || {}); class WikiSaikouError extends Error { constructor(code, message = "", cause) { super(); this.code = code; this.message = message; this.cause = cause; } name = "WikiSaikouError"; static is(data, code) { return data instanceof this && (code === void 0 || data.code === code); } } class MediaWikiApiError extends Error { constructor(errors, cause) { super(); this.errors = errors; this.cause = cause; this.errors = MediaWikiApiError.normalizeErrors(errors); this.message = errors.map((error) => error.text).filter(Boolean).join("\n"); this.code = this.isBadTokenError() ? "badtoken" : this.errors[0]?.code || "Unknown Error"; } name = "MediaWikiApiError"; code; get firstError() { return this.errors[0]; } isBadTokenError() { return this.errors.some((error) => error.code === "badtoken") || ["NeedToken", "WrongToken"].includes(this.cause?.data?.login?.result); } toString() { return `[${this.name} ${this.code}]`; } static is(data) { return data instanceof this; } static normalizeErrors(errors) { if (Array.isArray(errors) === false) { return []; } return errors.filter((e) => typeof e === "object" && !!e?.code).map((error) => { if (!error.text) { if (error.info) { return { ...error, text: error.info }; } else if (error["*"]) { return { ...error, text: error["*"] }; } else { return { ...error, text: "" }; } } return error; }); } } ((WikiSaikouError2) => { function extractResponseDataFromAny(data) { if (data == null) return void 0; if (data?.response?.data !== void 0) return data.response.data; if (data?.data !== void 0) return data.data; const cause = data instanceof Error ? data.cause : void 0; if (cause?.response?.data !== void 0) return cause.response.data; if (cause?.data !== void 0) return cause.data; return data || void 0; } function includesMediaWikiApiError(data) { return extractMediaWikiApiErrors(data).length > 0; } WikiSaikouError2.includesMediaWikiApiError = includesMediaWikiApiError; WikiSaikouError2.normalizeMwApiErrors = MediaWikiApiError.normalizeErrors; function extractMediaWikiApiErrors(data) { let r = extractResponseDataFromAny(data); if (typeof r !== "object" || r === null) { return []; } const error = r?.error; const errors = r?.errors; const result = []; if (error) { result.push(error); } if (Array.isArray(errors)) { result.push(...errors); } return (0, WikiSaikouError2.normalizeMwApiErrors)(result); } WikiSaikouError2.extractMediaWikiApiErrors = extractMediaWikiApiErrors; function isBadTokenError(data) { if (MediaWikiApiError.is(data)) { return data.isBadTokenError(); } else { const errors = extractMediaWikiApiErrors(data); return new MediaWikiApiError(errors).isBadTokenError(); } } WikiSaikouError2.isBadTokenError = isBadTokenError; })(WikiSaikouError || (WikiSaikouError = {})); const FEXIOS_SAIKOU_MARKER = Symbol.for("__FEXIOS_SAIKOU__"); /** * FexiosSaikou * * A pre-configured Fexios instance with MediaWiki-friendly defaults: * - MediaWiki-specific request/response params normalization * - built-in token management * - error handling * * @author dragon-fish <dragon-fish@qq.com> * @license MIT * * @param {string|URL} payload create a new FexiosSaikou instance with the given baseURL * @param {Fexios} payload or make the given Fexios instance a FexiosSaikou instance */ function createFexiosSaikou(payload) { const instance = payload instanceof fexios.Fexios ? payload : new fexios.Fexios({ baseURL: payload instanceof URL ? payload.toString() : String(payload), responseType: "json" }); if (instance[FEXIOS_SAIKOU_MARKER]) { return instance; } else { Reflect.defineProperty(instance, FEXIOS_SAIKOU_MARKER, { get: () => true, enumerable: false, configurable: false }); } instance.on("beforeInit", (ctx) => { if (ctx.request.method?.toLowerCase() !== "post") { return ctx; } if (ctx.request.body === void 0 || ctx.request.body === null) { ctx.request.body = void 0; return ctx; } const body = ctx.request.body = exports.MwParamNormalizer.normalizeBody( ctx.request.body ); const query = new URLSearchParams(ctx.request.query); if (body.has("format")) { query.delete("format"); } if (body.has("formatversion")) { query.delete("formatversion"); } if (body.has("action")) { query.delete("action"); } if (body.has("origin")) { query.set("origin", "" + body.get("origin")); body.delete("origin"); } ctx.request.query = Object.fromEntries(query.entries()); return ctx; }); instance.on("beforeInit", (ctx) => { ctx.request.query = fexios.FexiosQueryBuilder.mergeQueries( {}, exports.MwParamNormalizer.normalizeBody(ctx.request.query) ); return ctx; }); instance.on("beforeRequest", (ctx) => { const url = new URL(ctx.request.url); const query = fexios.FexiosQueryBuilder.mergeQueries(url.searchParams, ctx.query); if (globalThis.location) { if (!query.origin && location.origin !== url.origin) { query.origin = location.origin; instance.baseConfigs.credentials = "include"; instance.baseConfigs.mode = "cors"; } else if (location.origin === url.origin) { delete query.origin; instance.baseConfigs.credentials = void 0; instance.baseConfigs.mode = void 0; } } if (query.origin) { const origin = encodeURIComponent(query.origin).replace(/\./g, "%2E"); delete query.origin; ctx.request.query = query; ctx.request.url = `${url.origin}${url.pathname}?origin=${origin}`; } else { ctx.request.query = query; } return ctx; }); instance._tokens = /* @__PURE__ */ new Map(); instance.on("afterResponse", (ctx) => { const { data } = ctx; const tokenName = ctx.runtime.customEnv?.mwTokenName; if (tokenName && WikiSaikouError.isBadTokenError(data)) { instance._tokens.delete(tokenName); } const tokens = data?.query?.tokens; if (tokens && typeof tokens === "object") { Object.entries(tokens).forEach(([type, token]) => { typeof token === "string" && instance._tokens.set(type.replace(/token$/i, "").toLowerCase(), token); }); } const loginToken = data?.login?.token; if (typeof loginToken === "string") { instance._tokens.set("login", loginToken); } return ctx; }); return instance; } const resolveLegacyCtor = (configOrBaseURL, defaultOptions, defaultParams) => { let config = { ...WikiSaikouCore.DEFAULT_CONFIGS }; if (typeof configOrBaseURL === "string") { config = fexios.deepMerge(config, { baseURL: configOrBaseURL, fexiosConfigs: defaultOptions || {}, defaultParams: defaultParams || {}, throwOnApiError: false // Set a default value for throwOnApiError }); } else if (typeof configOrBaseURL === "object" && configOrBaseURL !== null) { config = fexios.deepMerge(config, configOrBaseURL); } if (!config.baseURL && typeof window === "object" && window.mediaWiki) { const { wgServer, wgScriptPath } = window.mediaWiki?.config?.get(["wgServer", "wgScriptPath"]) || {}; if (typeof wgServer === "string" && typeof wgScriptPath === "string") { config.baseURL = `${wgServer}${wgScriptPath}/api.php`; } } if (typeof config.baseURL !== "string") { throw new Error("baseURL is required"); } return config; }; const noop = () => { }; const useRetry = async (fn, options) => { let retryCount = 0; const { retry = 3, onRetry = noop, shouldRetry = () => true } = options; let lastError; do { try { return await fn(); } catch (error) { lastError = error; if (shouldRetry(error, retryCount)) { onRetry(error, retryCount); retryCount++; } else { throw error; } } } while (retryCount < retry); throw lastError || new Error("Retry failed"); }; /** * WikiSaikou Core class * @internal You **SHOULD NOT** use this class directly, instead, use the `MediaWikiApi` class. * @author dragon-fish <dragon-fish@qq.com> * @license MIT */ class WikiSaikouCore { config; version = "7.6.0"; request; static INIT_DEFAULT_PARAMS = { action: "query", errorformat: "plaintext", format: "json", formatversion: 2 }; static DEFAULT_CONFIGS = { baseURL: void 0, fexiosConfigs: { responseType: "json" }, defaultParams: this.INIT_DEFAULT_PARAMS, throwOnApiError: false }; constructor(configOrBaseURL, defaultOptions, defaultParams) { const config = this.config = resolveLegacyCtor( configOrBaseURL, defaultOptions, defaultParams ); this.request = createFexiosSaikou(config.baseURL); } setBaseURL(baseURL) { this.config.baseURL = baseURL; this.request.baseConfigs.baseURL = baseURL; return this; } /** Base methods encapsulation */ async get(query, options) { const payload = fexios.deepMerge( {}, this.config.fexiosConfigs, { query: fexios.FexiosQueryBuilder.mergeQueries( this.config.defaultParams, query ) }, options ); return this.runRequestWithApiErrorMapping( () => this.request.get("", payload) ); } async post(data, options) { return this.runRequestWithApiErrorMapping( () => this.request.post( "", data, fexios.deepMerge( {}, this.config.fexiosConfigs, { query: this.config.defaultParams }, options ) ) ); } /** * Wrap a request to map non-2xx responses containing MediaWiki API error bodies * into MediaWikiApiError when throwOnApiError=true, and then pass 2xx responses * through handleApiResponse for unified processing. */ async runRequestWithApiErrorMapping(doRequest) { try { const res = await doRequest(); return this.handleApiResponse(res); } catch (err) { if (this.config.throwOnApiError && WikiSaikouError.includesMediaWikiApiError(err)) { throw new MediaWikiApiError( WikiSaikouError.extractMediaWikiApiErrors(err), err ); } throw err; } } throwIfApiError(data) { const errors = WikiSaikouError.extractMediaWikiApiErrors(data); if (errors.length > 0) { throw new MediaWikiApiError(errors, data); } } handleApiResponse(res) { if (this.config.throwOnApiError) { this.throwIfApiError(res.data); } return res; } /** Token Handler */ get tokens() { return this.request._tokens; } async fetchTokens(types = ["csrf"]) { this.config.fexiosConfigs.credentials = "include"; await this.get({ action: "query", meta: "tokens", type: types }); return this.tokens; } async getToken(type = "csrf", noCache = false) { if (!this.tokens.get(type) || noCache) { this.tokens.delete(type); await this.fetchTokens([type]); } return this.tokens.get(type); } badToken(type) { this.tokens.delete(type); return this.tokens; } async postWithToken(tokenType, body, options) { const { tokenName = "token", retry = 3, noCache = false, fexiosOptions } = options || {}; if (retry < 1) { throw new WikiSaikouError( WikiSaikouErrorCode.TOKEN_RETRY_LIMIT_EXCEEDED, "The limit of the number of times to automatically re-acquire the token has been exceeded" ); } let attemptIndex = 0; return useRetry( async () => { const token = await this.getToken( tokenType, noCache || attemptIndex > 0 ); try { const ctx = await this.post( { [tokenName]: token, ...body }, fexios.deepMerge(fexiosOptions || {}, { customEnv: { mwTokenName: tokenType } }) ); if (WikiSaikouError.isBadTokenError(ctx.data)) { throw ctx; } return ctx; } catch (err) { if (WikiSaikouError.isBadTokenError(err) || err?.ok === false) { throw err; } else if (MediaWikiApiError.is(err)) { throw err; } else { throw new WikiSaikouError( WikiSaikouErrorCode.HTTP_ERROR, "Network/transport or SDK-internal error (not a MediaWiki API error)", err ); } } }, { retry, onRetry: (_error, count) => { attemptIndex = count + 1; }, shouldRetry: (error) => WikiSaikouError.isBadTokenError(error) || error?.ok === false } ).catch((err) => { if (WikiSaikouError.isBadTokenError(err) || err?.ok === false) { throw new WikiSaikouError( WikiSaikouErrorCode.TOKEN_RETRY_LIMIT_EXCEEDED, "Retry attempts for acquiring/using token exhausted", err ); } throw err; }); } postWithEditToken(body) { return this.postWithToken("csrf", body); } // for backward compatibility /** @deprecated Use `this.config.baseURL` instead */ get baseURL() { return this.config.baseURL; } /** @deprecated Use `this.config.defaultParams` instead */ get defaultParams() { return this.config.defaultParams; } /** @deprecated Use `this.config.fexiosConfigs` instead */ get defaultOptions() { return this.config.fexiosConfigs; } /** @deprecated Use `createFexiosSaikou` instead */ static createRequestHandler = createFexiosSaikou; /** @deprecated Use `this.getToken` instead */ token = this.getToken; } exports.MediaWikiApiError = MediaWikiApiError; exports.WikiSaikouCore = WikiSaikouCore; exports.WikiSaikouError = WikiSaikouError; exports.WikiSaikouErrorCode = WikiSaikouErrorCode; exports.createFexiosSaikou = createFexiosSaikou; exports.resolveLegacyCtor = resolveLegacyCtor; //# sourceMappingURL=wiki-saikou.1DPaxo4l.cjs.map