// Extremely basic API client for MediaWiki import makeFetchCookie from 'fetch-cookie'; function formDataBody (entries) { let data = new FormData(); for (const [key, value] of Object.entries(entries)) { if (value != null && value != false) { data.set(key, value); } } return data; } export class MediaWikiClient { /** * Creates a new client. Remember to also call `.login()`. * @param {string} wikiURL Target wiki's MediaWiki path (i.e. the path that * contains `index.php` and `api.php`) without a trailing slash. For example * for English Wikipedia this would be `'https://en.wikipedia.org/w'`. */ constructor (wikiURL) { this.wikiURL = wikiURL; this.fetch = makeFetchCookie(fetch); } /** * Makes a GET request against `index.php`. * @param {Record} params Query string parameters * @param {RequestInit} [options] Additional fetch options * @returns {Promise} */ fetchIndexGet (params, options = {}) { return this.fetch(`${this.wikiURL}/index.php?${new URLSearchParams(params)}`, { ...options, method: 'GET', }); } /** * Makes a JSON GET request against `api.php`. * @param {Record} params Query string parameters * @param {RequestInit} [options] Additional fetch options * @returns {Promise} */ async fetchApiGet (params, options = {}) { const response = await this.fetch(`${this.wikiURL}/api.php?${new URLSearchParams({ ...params, format: 'json', })}`, { ...options, method: 'GET', }); const body = await response.json(); if (body.error) { throw new Error(`[${body.error.code}] ${body.error.info}`); } return body; } /** * Makes a JSON POST request against `api.php`. * @param {Record} params Form data body parameters * @param {RequestInit} [options] Additional fetch options * @returns {Promise} */ async fetchApiPost (params, options = {}) { const response = await this.fetch(`${this.wikiURL}/api.php`, { ...options, method: 'POST', body: formDataBody({ ...params, format: 'json', }), }); const body = await response.json(); if (body.error) { throw new Error(`[${body.error.code}] ${body.error.info}`); } return body; } /** * Obtains a login token for authenticating. * @returns {Promise} */ async getLoginToken () { const body = await this.fetchApiGet({ action: 'query', meta: 'tokens', type: 'login', }); return body.query.tokens.logintoken; } /** * Obtains a CSRF token for making edits. * @returns {Promise} */ async getCSRFToken () { const body = await this.fetchApiGet({ action: 'query', meta: 'tokens', }); return body.query.tokens.csrftoken; } /** * Logs in with the given bot credentials. * @param {string} username * @param {string} password * @returns {Promise} */ async login (username, password) { const loginToken = await this.getLoginToken(); const body = await this.fetchApiPost({ action: 'login', lgname: username, lgpassword: password, lgtoken: loginToken, }); if (body.login.result === 'Failed') { throw new Error(body.login.reason); } } /** * Gets the current contents of the named item's wiki page and returns the * contents with the infobox updated to use the given EDB item ID. * @param {string} name * @returns {Promise} */ async readPage (title) { const response = await this.fetchIndexGet({ action: 'raw', title, }); return response.text(); } /** * Updates the named page to the given text. * @param {string} title * @param {string} text * @param {string} summary Edit summary * @param {boolean} [minor] If true, this is a minor edit * @returns {Promise} */ async editPage (title, text, summary, minor = false) { const csrfToken = await this.getCSRFToken(); const body = await this.fetchApiPost({ action: 'edit', title, text, summary, minor, bot: true, watchlist: 'nochange', token: csrfToken, format: 'json', }); return body; } /** * Gets the list of wiki pages that belong to the given category. * @param {string} name Category name including the `Category:` namespace. * @param {number[] | '*'} namespaces Integer namespace ID(s) or the string * `'*'`. If namespace IDs are provided, only pages in those namespaces will * be returned. * @param {string} limit Maximum number of items to return. Must be 500 or * less. I'm lazy and not supporting API paging so deal with it. * @returns {Promise<{pageid: number; title: string}[]>} */ async listCategoryPages (name, namespaces = '*', limit = 50) { if (Array.isArray(namespaces)) { namespaces = namespaces.join('|'); } const body = await this.fetchApiGet({ action: 'query', list: 'categorymembers', cmtitle: name, cmlimit: limit, cmnamespace: namespaces, }); return body.query.categorymembers; } }