'use strict'; import fetch from 'cross-fetch'; import querystring from 'querystring'; import { pagination, api, aggregate } from './util.js'; import wikiPage from './page.js'; import QueryChain from './chain.js'; /** * @namespace * @constant * @property {string} apiUrl - URL of Wikipedia API * @property {string} headers - Headers to pass through to the API request * @property {string} origin - When accessing the API using a cross-domain AJAX * request (CORS), set this to the originating domain. This must be included in * any pre-flight request, and therefore must be part of the request URI (not * the POST body). This must match one of the origins in the Origin header * exactly, so it has to be set to something like https://en.wikipedia.org or * https://meta.wikimedia.org. If this parameter does not match the Origin * header, a 403 response will be returned. If this parameter matches the Origin * header and the origin is whitelisted, an Access-Control-Allow-Origin header * will be set. */ const defaultOptions = { apiUrl: 'http://en.wikipedia.org/w/api.php', origin: '*' }; /** * wiki * @example * wiki({ apiUrl: 'http://fr.wikipedia.org/w/api.php' }).search(...); * @namespace Wiki * @param {Object} options * @return {Object} - wiki (for chaining methods) */ export default function wiki(options = {}) { if (this instanceof wiki) { // eslint-disable-next-line console.log('Please do not use wikijs ^1.0.0 as a class. Please see the new README.'); } const apiOptions = Object.assign({}, defaultOptions, options); function handleRedirect(res) { if (res.query.redirects && res.query.redirects.length === 1) { return api(apiOptions, { prop: 'info|pageprops', inprop: 'url', ppprop: 'disambiguation', titles: res.query.redirects[0].to }); } return res; } /** * Search articles * @example * wiki.search('star wars').then(data => console.log(data.results.length)); * @example * wiki.search('star wars').then(data => { * data.next().then(...); * }); * @method Wiki#search * @param {string} query - keyword query * @param {Number} [limit] - limits the number of results * @param {Boolean} [all] - returns entire article objects instead of just titles * @return {Promise} - pagination promise with results and next page function */ function search(query, limit = 50, all = false) { return pagination(apiOptions, { list: 'search', srsearch: query, srlimit: limit }, res => res.query.search.map(article => { return all ? article : article.title; })).catch(err => { if (err.message === '"text" search is disabled.') { // Try backup search method return opensearch(query, limit); } throw err; }); } /** * Search articles using "fuzzy" prefixsearch * @example * wiki.prefixSearch('star wars').then(data => console.log(data.results.length)); * @example * wiki.prefixSearch('star wars').then(data => { * data.next().then(...); * }); * @method Wiki#prefixSearch * @param {string} query - keyword query * @param {Number} [limit] - limits the number of results * @return {Promise} - pagination promise with results and next page function */ function prefixSearch(query, limit = 50) { return pagination(apiOptions, { list: 'prefixsearch', pslimit: limit, psprofile: 'fuzzy', pssearch: query }, res => res.query.prefixsearch.map(article => article.title)); } /** * Opensearch (mainly used as a backup to normal text search) * @param {string} query - keyword query * @param {Number} limit - limits the number of results * @return {Array} List of page title results */ function opensearch(query, limit = 50) { return api(apiOptions, { search: query, limit, namespace: 0, action: 'opensearch', redirects: undefined }).then(res => res[1]); } /** * Random articles * @example * wiki.random(3).then(results => console.log(results[0])); * @method Wiki#random * @param {Number} [limit] - limits the number of random articles * @return {Promise} - List of page titles */ function random(limit = 1) { return api(apiOptions, { list: 'random', rnnamespace: 0, rnlimit: limit }).then(res => res.query.random.map(article => article.title)); } /** * Get Page * @example * wiki.page('Batman').then(page => console.log(page.pageid)); * @method Wiki#page * @param {string} title - title of article * @return {Promise} */ function page(title) { return api(apiOptions, { prop: 'info|pageprops', inprop: 'url', ppprop: 'disambiguation', titles: title }) .then(handleRedirect) .then(res => { const id = Object.keys(res.query.pages)[0]; if (!id || id === '-1') { throw new Error('No article found'); } return wikiPage(res.query.pages[id], apiOptions); }); } /** * Get Page by PageId * @example * wiki.findById(4335).then(page => console.log(page.title)); * @method Wiki#findById * @param {integer} pageid, id of the page * @return {Promise} */ function findById(pageid) { return api(apiOptions, { prop: 'info|pageprops', inprop: 'url', ppprop: 'disambiguation', pageids: pageid }) .then(handleRedirect) .then(res => { const id = Object.keys(res.query.pages)[0]; if (!id || id === '-1') { throw new Error('No article found'); } return wikiPage(res.query.pages[id], apiOptions); }); } /** * Find page by query and optional predicate * @example * wiki.find('luke skywalker').then(page => console.log(page.title)); * @method Wiki#find * @param {string} search query * @param {function} [predicate] - testing function for choosing which page result to fetch. Default is first result. * @return {Promise} */ function find(query, predicate = results => results[0]) { return search(query) .then(res => predicate(res.results)) .then(name => page(name)); } /** * Geographical Search * @example * wiki.geoSearch(32.329, -96.136).then(titles => console.log(titles.length)); * @method Wiki#geoSearch * @param {Number} lat - latitude * @param {Number} lon - longitude * @param {Number} [radius=1000] - search radius in meters (default: 1km) * @param {Number} [limit=10] - number of results (default: 10 results) * @return {Promise} - List of page titles */ function geoSearch(lat, lon, radius = 1000, limit = 10) { return api(apiOptions, { list: 'geosearch', gsradius: radius, gscoord: `${lat}|${lon}`, gslimit: limit }).then(res => res.query.geosearch.map(article => article.title)); } /** * @summary Find the most viewed pages with counts * @example * wiki.mostViewed().then(list => console.log(`${list[0].title}: ${list[0].count}`)) * @method Wiki#mostViewed * @returns {Promise} - Array of {title,count} */ function mostViewed() { return api(apiOptions, { list: 'mostviewed' }).then(res => { return res.query.mostviewed.map(({ title, count }) => ({ title, count })); }); } /** * Fetch all page titles in wiki * @method Wiki#allPages * @return {Array} Array of pages */ function allPages() { return aggregate(apiOptions, {}, 'allpages', 'title', 'ap'); } /** * Fetch all categories in wiki * @method Wiki#allCategories * @return {Array} Array of categories */ function allCategories() { return aggregate(apiOptions, {}, 'allcategories', '*', 'ac'); } /** * Fetch all pages in category * @method Wiki#pagesInCategory * @param {String} category Category to fetch from * @return {Array} Array of pages */ function pagesInCategory(category) { return aggregate(apiOptions, { cmtitle: category }, 'categorymembers', 'title', 'cm'); } /** * @summary Helper function to query API directly * @method Wiki#api * @param {Object} params [https://www.mediawiki.org/wiki/API:Query](https://www.mediawiki.org/wiki/API:Query) * @returns {Promise} Query Response * @example * wiki().api({ * action: 'parse', * page: 'Pet_door' * }).then(res => res.parse.title.should.equal('Pet door')); */ function rawApi(params) { return api(apiOptions, params); } /** * @summary Returns a QueryChain to efficiently query specific data * @method Wiki#chain * @returns {QueryChain} * @example * // Find summaries and images of places near a specific location * wiki() * .chain() * .geosearch(52.52437, 13.41053) * .summary() * .image() * .coordinates() * .request() */ function chain() { return new QueryChain(apiOptions); } /** * @summary Returns the Export XML for a page to be used for importing into another MediaWiki * @method Wiki#exportXml * @param {string} pageName * @returns {Promise} Export XML */ function exportXml(pageName) { const qs = { title: 'Special:Export', pages: pageName }; // The replace here is kinda hacky since // the export action does not use // the normal api endpoint. const url = `${apiOptions.apiUrl.replace('api', 'index')}?${querystring.stringify(qs)}`; const headers = Object.assign({ 'User-Agent': 'WikiJS Bot v1.0' }, apiOptions.headers); return fetch(url, { headers }).then(res => res.text()); } return { search, random, page, geoSearch, options, findById, find, allPages, allCategories, pagesInCategory, opensearch, prefixSearch, mostViewed, api: rawApi, chain, exportXml }; }