'use strict';
import fetch from 'cross-fetch';
import querystring from 'querystring';
import { pagination, api, aggregate } from './util';
import wikiPage from './page';
import QueryChain from './chain';
/**
* @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<string>} 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
};
}