UNPKG

mediawiki-sdk

Version:

TypeScript SDK for generating and working with WikiText and MediaWiki API. Allows to get pages from wiki projects built on mediawiki.

1,257 lines (1,256 loc) 59.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MediaWiki = exports.MediaWikiApiError = exports.MediaWikiQueryUploadFileResponseClass = exports.MediaWikiQueryEditPageResponseClass = exports.MediaWikiQueryTokensResponseClass = exports.MediaWikiQueryUserInfoResponseClass = exports.MediaWikiQuerySummaryResponseClass = exports.MediaWikiQueryParseResponseClass = exports.MediaWikiQueryPageResponseClass = exports.CookieStore = void 0; class CookieStore { store = []; parseSetCookie(setCookieHeader, originHost) { const parts = setCookieHeader.split(";").map(p => p.trim()); const [name, value] = parts[0].split("="); const cookie = { name, value, domain: "", path: "/", creationTime: new Date(), }; for (const attr of parts.slice(1)) { const [k, v] = attr.includes("=") ? attr.split("=") : [attr, ""]; const key = k.toLowerCase(); switch (key) { case "expires": const date = new Date(v); if (!isNaN(date.getTime())) cookie.expires = date; break; case "path": cookie.path = v; break; case "domain": cookie.domain = v.startsWith(".") ? v.slice(1) : v; break; case "secure": cookie.secure = true; break; case "httponly": cookie.httpOnly = true; break; case "samesite": cookie.sameSite = v; break; } } if (!cookie.domain) { cookie.domain = originHost; cookie.hostOnly = true; } this.storeCookie(cookie); } storeCookie(cookie) { this.store = this.store.filter(c => !(c.name === cookie.name && c.domain === cookie.domain && c.path === cookie.path)); this.store.push(cookie); } getCookieHeader(url) { const u = new URL(url); const now = new Date(); const validCookies = this.store.filter(cookie => { if (cookie.expires && cookie.expires < now) return false; if (cookie.secure && u.protocol !== "https:") return false; const domainMatch = cookie.hostOnly ? u.hostname === cookie.domain : u.hostname === cookie.domain || u.hostname.endsWith("." + cookie.domain); const pathMatch = u.pathname.startsWith(cookie.path); return domainMatch && pathMatch; }); return validCookies; } } exports.CookieStore = CookieStore; class MediaWikiQueryPageResponseClass { batchcomplete; query; wiki; pageDetails; constructor(data, wikiInstance) { this.batchcomplete = data.batchcomplete; this.query = data.query; this.wiki = wikiInstance; if (data.query?.pages) { const pageIds = Object.keys(data.query.pages); if (pageIds.length > 0) { this.pageDetails = data.query.pages[pageIds[0]]; } } } /** * Returns the HTML content of the page. * @example "<p>Hello <strong>world</strong>!</p>" */ html() { return this.pageDetails?.extract ?? ""; } /** * Returns the title of the page. * @example "Main Page" */ title() { return this.pageDetails?.title ?? ""; } /** * Returns the page ID of the parsed page. * @example 123456 */ categories() { return this.pageDetails?.categories?.map((c) => c.title) ?? []; } /** * Edits this page. * @param options - Options for editing, like new text and summary. * @returns A promise resolving to the edit page response. */ async edit(options) { if (!this.pageDetails) { throw new Error("Page details not available for editing."); } if (!this.pageDetails.title && !this.pageDetails.pageid) { throw new Error("Page title or ID is required for editing."); } const editPayload = { ...options, }; if (this.pageDetails.pageid) { editPayload.pageid = this.pageDetails.pageid; } else { editPayload.title = this.pageDetails.title; } if (typeof options.text !== 'string') { throw new Error("Parameter 'text' is required for editing."); } return this.wiki.client.editPage(editPayload); } } exports.MediaWikiQueryPageResponseClass = MediaWikiQueryPageResponseClass; /** * A helper class to wrap the parse response and provide convenience methods. */ class MediaWikiQueryParseResponseClass { warnings; parse; constructor(data) { Object.assign(this, data); this.parse = data.parse; this.warnings = data.warnings; } /** * Returns the parsed text content of the page. * @example "Hello '''world'''!" */ text() { return this.parse?.text ?? ""; } /** * Returns the parsed HTML content of the page. * @example "<p>Hello <strong>world</strong>!</p>" */ html() { return this.parse?.text ?? ""; } /** * Returns the title of the page. * @example "Main Page" */ title() { return this.parse?.title ?? ""; } /** * Returns a list of categories for the parsed page. * @example ["Living people", "Software developers", "Ukrainians"] */ categories() { if (this.parse && Array.isArray(this.parse.categories)) { return this.parse.categories .map((cat) => cat && typeof cat === 'object' && cat['*']) .filter(Boolean); } return []; } } exports.MediaWikiQueryParseResponseClass = MediaWikiQueryParseResponseClass; /** * A helper class to wrap the summary response and provide convenience methods. */ class MediaWikiQuerySummaryResponseClass { batchcomplete; query; constructor(data) { Object.assign(this, data); this.query = data.query; this.batchcomplete = data.batchcomplete; } /** * Returns the text content of the page. * @example "JavaScript is a programming language..." */ text() { return this.query?.pages?.[0]?.extract ?? ""; } } exports.MediaWikiQuerySummaryResponseClass = MediaWikiQuerySummaryResponseClass; /** * A utility class for handling and accessing user information retrieved from the * MediaWiki API's `action=query&meta=userinfo` endpoint. * It provides convenient methods to extract key user details. */ class MediaWikiQueryUserInfoResponseClass { /** * Indicates whether the entire batch of requests completed successfully. * For single requests, this is typically `true` upon success. */ batchcomplete; /** * The main query object containing the specific data requested, * including detailed user information. */ query; /** * Optional warnings returned by the API during the request processing. * This field is present if warnings occurred. */ warnings; /** * Optional errors returned by the API during the request processing. * This field is present if errors occurred. */ errors; /** * Constructs an instance of `MediaWikiQueryUserInfoResponseClass`. * @param data The raw response object from the MediaWiki API's user info query. */ constructor(data) { this.batchcomplete = data.batchcomplete; this.query = data.query; if (data.warnings) this.warnings = data.warnings; if (data.errors) this.errors = data.errors; } /** * Checks if the current user is anonymous (not logged in). * An anonymous user typically has `anon` flag set to `true` or an `id` of `0`. * @returns `true` if the user is anonymous, `false` otherwise. */ isAnonymous() { if (this.query.userinfo.anon === true) { return true; } if (this.query.userinfo.id === 0) { return true; } return false; } /** * Retrieves the unique numerical ID of the current user. * For anonymous users, this will typically be `0`. * @returns The user's ID. */ getUserId() { return this.query.userinfo.id; } /** * Retrieves the username of the current user. * For anonymous users, this might be their IP address. * @returns The user's name. */ getUserName() { return this.query.userinfo.name; } /** * Retrieves the complete detailed information object for the current user. * @returns An object containing all available user details. */ getUserInfo() { return this.query.userinfo; } /** * Retrieves the user-specific preferences and settings. * These options often relate to UI appearance or editor behavior. * @returns An object containing the user's preferences. */ getUserOptions() { return this.query.userinfo.options; } } exports.MediaWikiQueryUserInfoResponseClass = MediaWikiQueryUserInfoResponseClass; /** * A utility class for handling and accessing security tokens retrieved from the * MediaWiki API's `action=query&meta=tokens` endpoint. * It provides direct access to the requested token details. */ class MediaWikiQueryTokensResponseClass { /** * Indicates whether the entire batch of requests completed successfully. * For single requests, this is typically `true` upon success. */ batchcomplete; /** * The main query object containing the specific data requested, * including detailed token information. */ query; /** * Optional warnings returned by the API during the request processing. * This field is present if warnings occurred. */ warnings; /** * Optional errors returned by the API during the request processing. * This field is present if errors occurred. */ errors; /** * Constructs an instance of `MediaWikiQueryTokensResponseClass`. * @param data The raw response object from the MediaWiki API's tokens query. */ constructor(data) { this.batchcomplete = data.batchcomplete; this.query = data.query; if (data.warnings) this.warnings = data.warnings; if (data.errors) this.errors = data.errors; } /** * Retrieves the Cross-Site Request Forgery (CSRF) token. * This token is essential for most write actions (e.g., editing, deleting, moving pages) * to prevent unauthorized requests. It is also aliased as `edittoken`. * @returns The CSRF token string. */ getCsrfToken() { return this.query.tokens.csrftoken; } /** * Retrieves the token required for watching or unwatching pages. * Use this token with the `action=watch` endpoint. * @returns The watch token string. */ getWatchToken() { return this.query.tokens.watchtoken; } /** * Retrieves the token used for patrolling recent changes. * This token is necessary for the `action=patrol` endpoint. * @returns The patrol token string. */ getPatrolToken() { return this.query.tokens.patroltoken; } /** * Retrieves the token used for rolling back edits on a page. * Use this token with the `action=rollback` endpoint. * @returns The rollback token string. */ getRollbackToken() { return this.query.tokens.rollbacktoken; } /** * Retrieves the token required for modifying user rights or group memberships. * This token is used with the `action=userrights` endpoint. * @returns The user rights token string. */ getUserRightsToken() { return this.query.tokens.userrightstoken; } /** * Retrieves the token specifically designed for login actions. * This token is used with the `action=login` endpoint. * @returns The login token string. */ getLoginToken() { return this.query.tokens.logintoken; } /** * Retrieves the token required for creating new user accounts. * This token is used with the `action=createaccount` endpoint. * @returns The create account token string. */ getCreateAccountToken() { return this.query.tokens.createaccounttoken; } /** * Retrieves the edit token. This is an alias for the CSRF token, * which is required for making edits to pages. * @returns The edit token (CSRF token) string. */ getEditToken() { return this.query.tokens.csrftoken; } } exports.MediaWikiQueryTokensResponseClass = MediaWikiQueryTokensResponseClass; /** * A utility class for handling and accessing the response from a MediaWiki page edit operation. * It provides convenient methods to extract key details about the edit result. */ class MediaWikiQueryEditPageResponseClass { /** * Indicates whether the entire batch of requests completed successfully. * For single requests, this is typically `true` upon success. */ batchcomplete; /** * The main query object containing the specific data requested, * including details about the edit. */ query; /** * Optional warnings returned by the API during the request processing. * This field is present if warnings occurred. */ warnings; /** * Optional errors returned by the API during the request processing. * This field is present if errors occurred. */ errors; /** * Constructs an instance of `MediaWikiQueryEditPageResponseClass`. * @param data The raw response object from the MediaWiki API's edit query. */ constructor(data) { const defaultEditDetails = { result: "Unknown", pageid: 0, title: "", contentmodel: "wikitext", oldrevid: 0, newrevid: 0, newtimestamp: "", watched: false }; if (data && typeof data === 'object') { this.batchcomplete = data.batchcomplete ?? false; this.query = data.query ?? { edit: defaultEditDetails }; if (!this.query.edit) this.query.edit = defaultEditDetails; if (data.warnings) this.warnings = data.warnings; if (data.errors) this.errors = data.errors; } else { this.batchcomplete = false; this.query = { edit: defaultEditDetails }; } } /** * Retrieves the result status of the edit operation. * For a successful edit, this will be "Success". * @returns The result string of the edit. */ getResult() { return this.query?.edit?.result ?? "Unknown"; } /** * Retrieves the unique identifier of the page that was edited. * @returns The page ID. */ getPageId() { return this.query.edit.pageid ?? 0; } /** * Retrieves the canonical title of the page that was edited. * @returns The page title. */ getTitle() { return this.query.edit.title ?? ""; } /** * Retrieves the content model of the edited page (e.g., 'wikitext'). * @returns The content model string. */ getContentModel() { return this.query.edit.contentmodel ?? ""; } /** * Retrieves the revision ID of the page *before* the current edit. * @returns The old revision ID. */ getOldRevisionId() { return this.query.edit.oldrevid ?? 0; } /** * Retrieves the revision ID of the *new* version of the page after the edit. * @returns The new revision ID. */ getNewRevisionId() { return this.query.edit.newrevid ?? 0; } /** * Retrieves the timestamp of the new revision in ISO 8601 format. * This indicates when the edit was officially recorded. * @returns The new timestamp string. */ getNewTimestamp() { return this.query.edit.newtimestamp ?? ""; } /** * Checks if the edited page is currently on the user's watchlist. * @returns `true` if the page is watched, `false` otherwise. */ isWatched() { return this.query.edit.watched ?? false; } /** * Checks if the API response contains any warnings. * @returns `true` if warnings are present, `false` otherwise. */ hasWarnings() { return this.warnings !== undefined; } /** * Retrieves any warnings returned by the API. * The structure of warnings can vary based on the API response. * @returns An object containing warnings, or `undefined` if none. */ getWarnings() { return this.warnings; } /** * Checks if the API response indicates any errors. * Note: This checks for API-level errors within the response payload, not HTTP errors. * @returns `true` if errors are present, `false` otherwise. */ hasErrors() { return this.errors !== undefined; } /** * Retrieves any errors returned by the API. * The structure of errors can vary based on the API response. * @returns An object containing errors, or `undefined` if none. */ getErrors() { return this.errors; } } exports.MediaWikiQueryEditPageResponseClass = MediaWikiQueryEditPageResponseClass; /** * A utility class for handling and accessing the response from a MediaWiki file upload operation. * Provides convenient methods to extract key details about the upload result. */ class MediaWikiQueryUploadFileResponseClass { batchcomplete; upload; error; constructor(data) { this.batchcomplete = data?.batchcomplete ?? false; this.upload = data?.upload; this.error = data?.error; } getResult() { return this.upload?.result ?? "Error"; } getFilename() { return this.upload?.filename ?? ""; } getCanonicalTitle() { return this.upload?.imageinfo.canonicaltitle ?? ""; } getFileUrl() { return this.upload?.imageinfo.url ?? ""; } getDescriptionUrl() { return this.upload?.imageinfo.descriptionurl ?? ""; } getSha1() { return this.upload?.imageinfo.sha1 ?? ""; } getMime() { return this.upload?.imageinfo.mime ?? ""; } getMediaType() { return this.upload?.imageinfo.mediatype ?? ""; } getError() { return this.error; } } exports.MediaWikiQueryUploadFileResponseClass = MediaWikiQueryUploadFileResponseClass; /** * Custom error class for MediaWiki API-specific errors. * This class extends the standard `Error` and provides additional properties * to better convey the nature of API failures, including HTTP status, * MediaWiki error codes, and detailed information. */ class MediaWikiApiError extends Error { /** * The HTTP status code of the response that caused the error. * E.g., 400, 403, 500. */ status; /** * The specific MediaWiki API error code (e.g., 'badtoken', 'permissiondenied'). * This is present if the API itself returned a structured error. */ code; /** * A more detailed human-readable description of the API error, * often accompanying the `code`. */ info; /** * The raw response text from the API, useful for debugging. */ responseText; /** * The parsed JSON response data from the API, if available and applicable. * This might contain the `error` object directly. */ responseData; /** * Constructs a new `MediaWikiApiError` instance. * @param message A general error message. * @param status The HTTP status code. * @param responseText The raw response text from the API. * @param responseData Optional: The parsed JSON response data, which might contain `error.code` and `error.info`. */ constructor(message, status, responseText, responseData) { super(message); this.name = "MediaWikiApiError"; this.status = status; this.responseText = responseText; this.responseData = responseData; if (responseData && responseData.error) { this.code = responseData.error.code; this.info = responseData.error.info; // Refine the error message if more specific API error details are available if (this.info && message.startsWith("Request failed")) { this.message = `Request failed with status ${status}: ${this.info} (Code: ${this.code || "N/A"})`; } else if (this.code && message.startsWith("Request failed")) { this.message = `Request failed with status ${status} (Code: ${this.code})`; } } } } exports.MediaWikiApiError = MediaWikiApiError; /** * A client for interacting with the MediaWiki API. * Provides methods for common API actions. */ class MediaWiki { baseURL; params; cookieStore; authorized; siteInfo; client; parent; /** * Creates an instance of the MediaWiki client. * @param options - The configuration options for the client. * @throws {Error} If baseURL is not provided in options. * * @example * const client = new MediaWiki({ baseURL: "https://en.wikipedia.org/w/api.php" }); */ constructor(options) { if (!options.baseURL) { throw new Error("baseURL is required"); } this.cookieStore = new CookieStore(); this.baseURL = options.baseURL.endsWith("/api.php") ? options.baseURL : `${options.baseURL}/api.php`; this.params = { servedby: options.servedby, curtimestamp: options.curtimestamp, responselanginfo: options.responselanginfo, requestid: options.requestid, format: options.format ?? "json", formatversion: options.formatversion ?? 2, ascii: options.ascii, utf8: options.utf8 }; this.authorized = false; this.siteInfo = null; if (options.format && options.format !== "json") { throw new Error(`Expected "json" format but got "${options.format}". The library only speaks JSON...`); } this.parent = this; this.client = { parent: this.parent, /** * Executes a full MediaWiki API query with various combinations of prop, meta, and list options. * * @param options - The detailed query options. * @returns A promise resolving to the full MediaWiki API response. * @throws {Error} If required options are missing or incompatible. * * @example * mediaWiki.client.query({ * titles: ["Main Page"], * prop: ["extracts", "categories"], * indexpageids: true * }).then(response => { * console.log(response); * }); */ async query(options) { if (!options) { throw new Error("Options are required for the query method."); } if (options.title && options.titles) { throw new Error(`Use either "title" or "titles", not both.`); } if (options.titles && options.pageids) { throw new Error(`Cannot use both "titles" and "pageids". Use only one identifier method.`); } if (options.titles && (!options.prop && !options.meta && options.list)) { throw new Error(`"titles" provided but no "prop", "meta", or "list" specified. Nothing to retrieve.`); } if (!options.titles && options.export) { throw new Error(`"export" requires "titles" to be set.`); } if (options.indexpageids && (!options.titles && !options.pageids)) { throw new Error(`"indexpageids" only works with "titles" or "pageids".`); } if (options.redirects && (!options.titles && !options.title && !options.pageids)) { throw new Error(`"redirects" has no effect without "titles", "pageids", or "title".`); } if (options.prop && (!options.titles && !options.pageids)) { throw new Error(`"prop" requires either "titles", "pageids".`); } const queryParams = { action: "query", prop: options.prop?.join("|"), list: options.list?.join("|"), meta: options.meta?.join("|"), indexpageids: options.indexpageids, export: options.export, titles: options.titles?.join("|"), pageids: options.pageids?.join("|"), srsearch: options.srsearch, srnamespace: options.srnamespace?.join("|"), srlimit: options.srlimit, srprop: options.srprop?.join("|"), srwhat: options.srwhat, srinfo: options.srinfo, rvlimit: options.rvlimit, exintro: options.exintro, explaintext: options.explaintext, uiprop: options.uiprop, type: options.type }; const filteredParams = this.parent.filterParams(queryParams); return this.parent.fetchData({ method: "GET", params: filteredParams, url: this.parent.baseURL }); }, /** * Retrieves basic content and metadata for one or more pages by title. * Uses 'query' with predefined props such as 'extracts', 'categories', 'revisions'. * * @param titles - List of page titles to retrieve. * @returns A promise resolving to the page response data. * @throws {Error} If titles is missing or empty. * * @example * mediaWiki.client.page(["Main Page"]) * .then(pageData => console.log(pageData)); */ async page(titles) { if (!titles || titles.length === 0) { throw new Error("Missing or empty 'titles' - must be a non-empty."); } const query = { prop: ["info", "extracts", "categories", "revisions"], titles: titles, indexpageids: true }; const res = await this.query(query); if (!res || !res.query) { return new MediaWikiQueryPageResponseClass(res, this.parent); } return new MediaWikiQueryPageResponseClass(res, this.parent); }, /** * Searches the wiki using the 'search' list API. * @param srsearch - The search query string; must be non-empty. * @param srnamespace - Optional array of namespaces to limit the search. * @param srlimit - Optional number to limit the number of results (default 10). * @returns Promise resolving to the search results response. */ async search(srsearch, srnamespace, srlimit) { if (!srsearch || srsearch.trim() === "") { throw new Error(`Missing "srsearch" - must be a non-empty string.`); } const query = { list: ["search"], srsearch, srnamespace: srnamespace ?? [], srlimit: srlimit ?? 10 }; const res = await this.parent.client.query(query); if (!res || !res.query) { return res; } return res; }, /** * Fetches general site metadata via 'meta=siteinfo'. * Includes site name, generator, case sensitivity, and namespaces. * @returns Promise resolving to site info data. * @example * async function getSiteDetails() { * const client = new MediaWiki({ baseURL: "https://en.wikipedia.org/w/api.php" }); * try { * const siteInfo = await client.client.siteInfo(); * console.log(siteInfo.query.general.sitename); * } catch (error) { * console.error("Failed to get site info:", error); * } * } * getSiteDetails(); * @see https://www.mediawiki.org/wiki/API:Siteinfo */ async siteInfo() { const query = { meta: ["siteinfo"] }; const res = await this.query(query); this.parent.siteInfo = res?.query ?? null; if (!this.parent.siteInfo || !this.parent.siteInfo.general) { return res; } return res; }, /** * Performs a legacy 'opensearch' API call for autocomplete-like suggestions. * @param options - Object containing 'search' string and optional 'limit', 'namespace', 'suggest'. * @returns Promise resolving to OpenSearch formatted results. * @throws If options or search term is missing. */ async opensearch(options) { if (!options) { throw new Error("Options are required for the opensearch method."); } if (!options.search) { throw new Error("A search is required for the opensearch method."); } const query = { action: "opensearch", ...options }; const filteredParams = this.parent.filterParams(query); const res = await this.parent.fetchData({ method: "GET", url: this.parent.baseURL, params: filteredParams }); if (!res || !res.query) { return res; } return res; }, /** * Parses a page or text content using the 'parse' API action. * Can return rendered HTML, sections, categories, etc. * @param options - Must include at least one of 'page', 'pageid', or 'text'. * @returns Promise resolving to the parsed content response class instance. * @throws If required parameters are missing. */ async parse(options) { if (!options) { throw new Error(`Options are required for the parse method.`); } if (!options.page && !options.pageid && !options.text) { throw new Error(`You must provide either "page", "pageid" or "text" for the parse method.`); } const query = { action: "parse", ...options }; const filteredParams = this.parent.filterParams(query); const res = await this.parent.fetchData({ method: "GET", url: this.parent.baseURL, params: filteredParams }); if (!res || !res.parse) { return new MediaWikiQueryParseResponseClass(res); } return new MediaWikiQueryParseResponseClass(res); }, /** * Retrieves categories of a wiki page by its title. * @param options - Must include a non-empty 'title' string. * @returns Promise resolving to categories response. * @throws If 'title' is missing or invalid. */ async categories(options) { if (!options || !options.title) { throw new Error(`Missing or invalid "title" - must be a non-empty string.`); } const resQuery = { titles: [options.title], prop: ["categories"] }; const res = await this.query(resQuery); if (!res || !res.query || typeof res.query.pages !== "object") { return { continue: res.continue ?? {}, query: { normalized: [], pages: [] } }; } const pagesArr = Object.values(res.query.pages ?? {}); const normalizedArr = Array.isArray(res.query.normalized) ? res.query.normalized : []; return { continue: res.continue, query: { normalized: normalizedArr, pages: pagesArr } }; }, /** * Retrieves revision history of a page by its title. * @param options - Must include a non-empty 'title' string; optionally 'rvlimit'. * @returns Promise resolving to revisions response. * @throws If 'title' is missing or invalid. */ async revisions(options) { if (!options || !options.title) { throw new Error(`Missing or invalid "title" - must be a non-empty string.`); } const query = { titles: [options.title], prop: ["revisions"], rvlimit: options.rvlimit }; const res = await this.parent.client.query(query); if (!res || !res.query || typeof res.query.pages !== "object") { return { batchcomplete: false, query: { normalized: [], pages: [] } }; } const pagesArr = Object.values(res.query.pages ?? {}); const normalizedArr = Array.isArray(res.query.normalized) ? res.query.normalized : []; return { batchcomplete: res.batchcomplete ?? false, query: { normalized: normalizedArr, pages: pagesArr } }; }, /** * Retrieves a summary extract (intro paragraph) of a page. * @param options - Must include a non-empty 'title' string. * @returns Promise resolving to a summary response class instance. * @throws If 'title' is missing or invalid. */ async summary(options) { if (!options || !options.title) { throw new Error(`Missing or invalid "title" - must be a non-empty string.`); } const query = { titles: [options.title], prop: ["extracts"], exintro: true, explaintext: true }; const res = await this.query(query); if (!res) { return new MediaWikiQuerySummaryResponseClass({ batchcomplete: false, query: { pages: [], normalized: [] } }); } if (!res || !res.query || !res.query.normalized || !res.query.pages || typeof res.query.pages !== "object") { return new MediaWikiQuerySummaryResponseClass({ batchcomplete: res.batchcomplete ?? false, query: { pages: (res.query?.pages && typeof res.query.pages === 'object' ? Object.values(res.query.pages) : []), normalized: res.query?.normalized ?? [] }, warnings: res.warnings, errors: res.errors }); } const pagesArr = Object.values(res.query.pages ?? {}); return new MediaWikiQuerySummaryResponseClass({ batchcomplete: res.batchcomplete, query: { normalized: res.query.normalized ?? [], pages: pagesArr } }); }, /** * Retrieves information about the current user. * @returns Promise resolving to user info response class instance. */ async userInfo() { const query = { meta: ["userinfo"], uiprop: "*" }; const res = await this.query(query); if (!res) { return new MediaWikiQueryUserInfoResponseClass({ batchcomplete: false, query: { userinfo: { id: -1, name: "", groups: [], groupmemberships: [], implicitgroups: [], rights: [], changeablegroups: { add: [], remove: [], "add-self": [], "remove-self": [] }, options: {}, editcount: -1, ratelimits: {} } } }); } if (!res || !res.query || !res.query.normalized || !res.query.pages || typeof res.query.pages !== "object") { return new MediaWikiQueryUserInfoResponseClass({ batchcomplete: res.batchcomplete ?? false, query: { userinfo: res.query?.userinfo, }, warnings: res.warnings, errors: res.errors }); } return new MediaWikiQueryUserInfoResponseClass(res); }, /** * Retrieves tokens for given types, such as CSRF tokens. * @param options - Object with 'type' specifying token types to fetch. * @returns Promise resolving to tokens response class instance. */ async getToken(options) { const query = { meta: ["tokens"], type: options.type.join("|") }; const res = await this.query(query); if (!res || !res.query || !res.query.tokens) { throw new Error(`Failed to retrieve tokens or unexpected response structure: ${JSON.stringify(res)}`); } return new MediaWikiQueryTokensResponseClass(res); }, /** * Edits a page by providing 'title' or 'pageid' and new 'text'. * Requires a CSRF token. * @param options - Must include either 'title' or 'pageid', and 'text'. * @returns Promise resolving to the edit page response class instance. * @throws If required parameters are missing. */ async editPage(options) { if (!options) { throw new Error("Options are required for the editpage method."); } if (!options.title && !options.pageid && !options.text) { throw new Error(`You must provide either "page", "pageid" or "text" for the editpage method.`); } const csrfResponse = await this.getToken({ type: ["csrf"] }); const query = { token: csrfResponse.query.tokens.csrftoken, action: "edit", ...options }; const filteredParams = this.parent.filterParams(query); const res = await this.parent.fetchData({ method: "POST", url: this.parent.baseURL, headers: { "Content-Type": "application/x-www-form-urlencoded" }, data: this.parent.formURLEncoder(filteredParams) }); if (!res || !res.query) { return new MediaWikiQueryEditPageResponseClass(res); } return new MediaWikiQueryEditPageResponseClass(res); }, async random() { const query = { action: "query", list: ["random"] }; const res = await this.query(query); if (!res || !res.query) { return res; } return res; }, /** * Uploads a file to MediaWiki. * @param options - Options for the file upload. * @returns Promise resolving to an instance of MediaWikiQueryUploadFileResponseClass. * @throws {Error} If required parameters are missing or an error occurs during upload. */ async uploadFile(options) { if (!options) { throw new Error("The 'options' parameter is required for uploadFile."); } if (!options.filekey && !options.filename) { throw new Error("The 'filename' parameter is required if 'filekey' is not provided."); } if (!options.file && !options.url && !options.filekey) { throw new Error("A file source must be provided: 'file', 'url', or 'filekey'."); } if (options.file && !options.mimeType && options.file instanceof Buffer) { console.warn("It's recommended to specify 'mimeType' when 'file' is a Buffer. Defaulting to 'application/octet-stream'."); } const tokenResponse = await this.getToken({ type: ["csrf"] }); const csrfToken = tokenResponse.getCsrfToken(); if (!csrfToken) { throw new Error("Failed to obtain CSRF token for file upload."); } const formData = new FormData(); formData.append("action", "upload"); formData.append("token", csrfToken); formData.append("format", this.parent.params.format ?? "json"); if (this.parent.params.formatversion) { formData.append("formatversion", String(this.parent.params.formatversion)); } if (options.filename) formData.append("filename", options.filename); if (options.comment) formData.append("comment", options.comment); if (options.text) formData.append("text", options.text); if (options.tags) formData.append("tags", options.tags); if (options.watchlist) formData.append("watchlist", options.watchlist); if (options.ignorewarnings !== undefined) formData.append("ignorewarnings", options.ignorewarnings ? "1" : "0"); if (options.async !== undefined) formData.append("async", options.async ? "1" : "0"); if (options.bot !== undefined) formData.append("bot", options.bot ? "1" : "0"); if (options.file) { let fileData; let partFilename = options.filename || "upload.dat"; if (options.file instanceof Buffer) { fileData = new Blob([options.file], { type: options.mimeType || 'application/octet-stream' }); } else if (typeof options.file.pipe === 'function' && typeof options.file.on === 'function') { throw new Error("Direct upload of ReadableStream is not supported. Convert to Buffer or Blob, or use 'url' or 'filekey'."); } else { fileData = options.file; if (options.file instanceof File && options.file.name) { partFilename = options.file.name; } } formData.append("file", fileData, partFilename); } else if (options.url) { formData.append("url", options.url); } else if (options.filekey) { formData.append("filekey", options.filekey); } const responseData = await this.parent.fetchData({ method: "POST", url: this.parent.baseURL, data: formData }); return new MediaWikiQueryUploadFileResponseClass(responseData); }, async searchTitles(options) { const { query, limit, namespace } = options; if (!query || query.trim() === "") { throw new Error("Search query cannot be empty."); } const queryParams = { action: "query", list: ["search"], srsearch: query, srlimit: limit, srprop: ["titlesnippet"] }; if (namespace !== undefined) { if (Array.isArray(namespace)) { queryParams.srnamespace = namespace.map(ns => String(ns)); } else { queryParams.srnamespace = [String(namespace)]; } } try { const response = await this.query(queryParams); if (response.query && response.query.search) { return response.query.search.map((item) => item.title); } return []; } catch (error) { console.error(`Error in searchTitles for query "${query}":`, error); throw error; } } }; Object.defineProperty(this.client, 'parent', { value: this.parent, writable: false, enumerable: false, configurable: false }); } /** * Fetches data from the MediaWiki API endpoint. * This is a private helper method. * @param options - Options for the fetch request, including method, URL, params, etc. * @returns A promise that resolves with the response data from the API. * @throws {Error} If the fetch operation fails. */ async fetchData(options) { try { const params = {}; for (const key in options.params) { if (options.params[key] !== undefined && options.params[key] !== null) { params[key] = options.params[key]; } } for (const key in this.params) { if (params[key] === undefined && this.params[key] !== undefined && this.params[key] !== null) { params[key] = this.params[key]; } } const urlObj = new URL(options.url); Object.entries(params).forEach(([key, value]) => { urlObj.searchParams.append(key, value); }); const cookieObjects = this.cookieStore.getCookieHeader(this.baseURL); const headers = { ...options.headers, }; if (cookieObjects && cookieObjects.length > 0) { const cookieString = cookieObjects.map(cookie => `${cookie.name}=${cookie.value}`).join("; "); headers["Cookie"] = cookieString; } let bodyToSend = undefined; if (options.method !== "GET" && options.data) { if (options.data instanceof FormData || typeof options.data === 'string' || options.data instanceof URLSearchParams || options.data instanceof ReadableStream) { bodyToSend = options.data; } else { bodyToSend = JSON.stringify(options.data); if (options.headers && !options.headers['Content-Type'] && !options.headers['content-type']) { options.headers['Content-Type'] = 'application/json'; } else if (!options.headers) { options.headers = { 'Content-Type': 'application/json' };