wiki.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. const util = require('util');
  2. const {defaultSettings, wikiProjects} = require('./default.json');
  3. const wikimediaSites = [
  4. 'wikipedia.org',
  5. 'mediawiki.org',
  6. 'wikimedia.org',
  7. 'wiktionary.org',
  8. 'wikibooks.org',
  9. 'wikisource.org',
  10. 'wikidata.org',
  11. 'wikiversity.org',
  12. 'wikiquote.org',
  13. 'wikinews.org',
  14. 'wikivoyage.org'
  15. ];
  16. const oauthSites = [];
  17. /**
  18. * A wiki.
  19. * @class Wiki
  20. */
  21. class Wiki extends URL {
  22. /**
  23. * Creates a new wiki.
  24. * @param {String|URL|Wiki} [wiki] - The wiki script path.
  25. * @param {String|URL|Wiki} [base] - The base for the wiki.
  26. * @constructs Wiki
  27. */
  28. constructor(wiki = defaultSettings.wiki, base = defaultSettings.wiki) {
  29. super(wiki, base);
  30. this.protocol = 'https';
  31. let articlepath = '/index.php?title=$1';
  32. if ( this.isFandom() ) articlepath = this.pathname + 'wiki/$1';
  33. this.gamepedia = this.hostname.endsWith( '.gamepedia.com' );
  34. if ( this.isGamepedia() ) articlepath = '/$1';
  35. let project = wikiProjects.find( project => this.hostname.endsWith( project.name ) );
  36. if ( project ) {
  37. let regex = ( this.host + this.pathname ).match( new RegExp( '^' + project.regex + project.scriptPath + '$' ) );
  38. if ( regex ) articlepath = 'https://' + regex[1] + project.articlePath + '$1';
  39. }
  40. this.articlepath = articlepath;
  41. this.mainpage = '';
  42. this.miraheze = this.hostname.endsWith( '.miraheze.org' );
  43. this.wikimedia = wikimediaSites.includes( this.hostname.split('.').slice(-2).join('.') );
  44. this.centralauth = ( ( this.isWikimedia() || this.isMiraheze() ) ? 'CentralAuth' : 'local' );
  45. this.oauth2 = oauthSites.includes( this.href );
  46. }
  47. /**
  48. * @type {String}
  49. */
  50. get articlepath() {
  51. return this.articleURL.pathname + this.articleURL.search;
  52. }
  53. set articlepath(path) {
  54. this.articleURL = new articleURL(path, this);
  55. }
  56. /**
  57. * @type {String}
  58. */
  59. get mainpage() {
  60. return this.articleURL.mainpage;
  61. }
  62. set mainpage(title) {
  63. this.articleURL.mainpage = title;
  64. }
  65. /**
  66. * Updates the wiki url.
  67. * @param {Object} siteinfo - Siteinfo from the wiki API.
  68. * @param {String} siteinfo.servername - Hostname of the wiki.
  69. * @param {String} siteinfo.scriptpath - Scriptpath of the wiki.
  70. * @param {String} siteinfo.articlepath - Articlepath of the wiki.
  71. * @param {String} siteinfo.mainpage - Main page of the wiki.
  72. * @param {String} siteinfo.centralidlookupprovider - Central auth of the wiki.
  73. * @param {String} siteinfo.logo - Logo of the wiki.
  74. * @param {String} [siteinfo.gamepedia] - If the wiki is a Gamepedia wiki.
  75. * @returns {Wiki}
  76. */
  77. updateWiki({servername, scriptpath, articlepath, mainpage, centralidlookupprovider, logo, gamepedia = 'false'}) {
  78. this.hostname = servername;
  79. this.pathname = scriptpath + '/';
  80. this.articlepath = articlepath;
  81. this.mainpage = mainpage;
  82. this.centralauth = centralidlookupprovider;
  83. this.miraheze = /^(?:https?:)?\/\/static\.miraheze\.org\//.test(logo);
  84. this.gamepedia = ( gamepedia === 'true' ? true : this.hostname.endsWith( '.gamepedia.com' ) );
  85. this.wikimedia = wikimediaSites.includes( this.hostname.split('.').slice(-2).join('.') );
  86. this.oauth2 = oauthSites.includes( this.href );
  87. return this;
  88. }
  89. /**
  90. * Check for a Fandom wiki.
  91. * @param {Boolean} [includeGP] - If Gamepedia wikis are included.
  92. * @returns {Boolean}
  93. */
  94. isFandom(includeGP = true) {
  95. return ( this.hostname.endsWith( '.fandom.com' ) || this.hostname.endsWith( '.wikia.org' )
  96. || ( includeGP && this.isGamepedia() ) );
  97. }
  98. /**
  99. * Check for a Gamepedia wiki.
  100. * @returns {Boolean}
  101. */
  102. isGamepedia() {
  103. return this.gamepedia;
  104. }
  105. /**
  106. * Check for a Miraheze wiki.
  107. * @returns {Boolean}
  108. */
  109. isMiraheze() {
  110. return this.miraheze;
  111. }
  112. /**
  113. * Check for a WikiMedia wiki.
  114. * @returns {Boolean}
  115. */
  116. isWikimedia() {
  117. return this.wikimedia;
  118. }
  119. /**
  120. * Check for CentralAuth.
  121. * @returns {Boolean}
  122. */
  123. hasCentralAuth() {
  124. return this.centralauth === 'CentralAuth';
  125. }
  126. /**
  127. * Check for OAuth2.
  128. * @returns {Boolean}
  129. */
  130. hasOAuth2() {
  131. return ( this.isWikimedia() || this.isMiraheze() || this.oauth2 );
  132. }
  133. /**
  134. * Check if a wiki is missing.
  135. * @param {String} [message] - Error message or response url.
  136. * @param {Number} [statusCode] - Status code of the response.
  137. * @returns {Boolean}
  138. */
  139. noWiki(message = '', statusCode = 0) {
  140. if ( statusCode === 410 || statusCode === 404 ) return true;
  141. if ( !this.isFandom() ) return false;
  142. if ( this.hostname.startsWith( 'www.' ) || message.startsWith( 'https://www.' ) ) return true;
  143. return [
  144. 'https://community.fandom.com/wiki/Community_Central:Not_a_valid_community?from=' + this.hostname,
  145. this + 'language-wikis'
  146. ].includes( message.replace( /Unexpected token < in JSON at position 0 in "([^ ]+)"/, '$1' ) );
  147. }
  148. /**
  149. * Get a page link.
  150. * @param {String} [title] - Name of the page.
  151. * @param {URLSearchParams} [querystring] - Query arguments of the page.
  152. * @param {String} [fragment] - Fragment of the page.
  153. * @param {Boolean} [isMarkdown] - Use the link in markdown.
  154. * @returns {String}
  155. */
  156. toLink(title = '', querystring = '', fragment = '', isMarkdown = false) {
  157. querystring = new URLSearchParams(querystring);
  158. if ( !querystring.toString().length ) title = ( title || this.mainpage );
  159. title = title.replace( / /g, '_' ).replace( /%/g, '%2525' );
  160. let link = new URL(this.articleURL);
  161. link.username = '';
  162. link.password = '';
  163. link.pathname = link.pathname.replace( '$1', title.replace( /\\/g, '%5C' ) );
  164. link.searchParams.forEach( (value, name, searchParams) => {
  165. if ( value.includes( '$1' ) ) {
  166. if ( !title ) searchParams.delete(name);
  167. else searchParams.set(name, value.replace( '$1', title ));
  168. }
  169. } );
  170. querystring.forEach( (value, name) => {
  171. link.searchParams.append(name, value);
  172. } );
  173. let output = decodeURI( link ).replace( /\\/g, '%5C' ).replace( /@(here|everyone)/g, '%40$1' ) + Wiki.toSection(fragment);
  174. if ( isMarkdown ) return output.replace( /\(/g, '%28' ).replace( /\)/g, '%29' );
  175. else return output;
  176. }
  177. /**
  178. * Encode a page title.
  179. * @param {String} [title] - Title of the page.
  180. * @returns {String}
  181. * @static
  182. */
  183. static toTitle(title = '') {
  184. return title.replace( / /g, '_' ).replace( /[?&%\\]/g, (match) => {
  185. return '%' + match.charCodeAt().toString(16).toUpperCase();
  186. } ).replace( /@(here|everyone)/g, '%40$1' ).replace( /[()]/g, '\\$&' );
  187. };
  188. /**
  189. * Encode a link section.
  190. * @param {String} [fragment] - Fragment of the page.
  191. * @param {Boolean} [simpleEncoding] - Don't fully encode the anchor.
  192. * @returns {String}
  193. * @static
  194. */
  195. static toSection(fragment = '', simpleEncoding = true) {
  196. if ( !fragment ) return '';
  197. fragment = fragment.replace( / /g, '_' );
  198. if ( simpleEncoding && !/['"`^{}<>|\\]|@(everyone|here)/.test(fragment) ) return '#' + fragment;
  199. return '#' + encodeURIComponent( fragment ).replace( /[!'()*~]/g, (match) => {
  200. return '%' + match.charCodeAt().toString(16).toUpperCase();
  201. } ).replace( /%3A/g, ':' ).replace( /%/g, '.' );
  202. }
  203. /**
  204. * Turn user input into a wiki.
  205. * @param {String} input - The user input referring to a wiki.
  206. * @returns {Wiki}
  207. * @static
  208. */
  209. static fromInput(input = '') {
  210. if ( input instanceof URL ) return new this(input);
  211. input = input.replace( /^(?:https?:)?\/\//, 'https://' );
  212. var regex = input.match( /^(?:https:\/\/)?([a-z\d-]{1,50}\.(?:gamepedia\.com|(?:fandom\.com|wikia\.org)(?:(?!\/(?:wiki|api)\/)\/[a-z-]{2,12})?))(?:\/|$)/ );
  213. if ( regex ) return new this('https://' + regex[1] + '/');
  214. if ( input.startsWith( 'https://' ) ) {
  215. let project = wikiProjects.find( project => input.split('/')[2].endsWith( project.name ) );
  216. if ( project ) {
  217. regex = input.match( new RegExp( project.regex + `(?:${project.articlePath}|${project.scriptPath}|/?$)` ) );
  218. if ( regex ) return new this('https://' + regex[1] + project.scriptPath);
  219. }
  220. let wiki = input.replace( /\/(?:api|load|index)\.php(?:|\?.*)$/, '/' );
  221. if ( !wiki.endsWith( '/' ) ) wiki += '/';
  222. return new this(wiki);
  223. }
  224. let project = wikiProjects.find( project => input.split('/')[0].endsWith( project.name ) );
  225. if ( project ) {
  226. regex = input.match( new RegExp( project.regex + `(?:${project.articlePath}|${project.scriptPath}|/?$)` ) );
  227. if ( regex ) return new this('https://' + regex[1] + project.scriptPath);
  228. }
  229. if ( /^(?:[a-z-]{2,12}\.)?[a-z\d-]{1,50}$/.test(input) ) {
  230. if ( !input.includes( '.' ) ) return new this('https://' + input + '.fandom.com/');
  231. else return new this('https://' + input.split('.')[1] + '.fandom.com/' + input.split('.')[0] + '/');
  232. }
  233. return null;
  234. }
  235. [util.inspect.custom](depth, opts) {
  236. if ( typeof depth === 'number' && depth < 0 ) return this;
  237. const wiki = {
  238. href: this.href,
  239. origin: this.origin,
  240. protocol: this.protocol,
  241. username: this.username,
  242. password: this.password,
  243. host: this.host,
  244. hostname: this.hostname,
  245. port: this.port,
  246. pathname: this.pathname,
  247. search: this.search,
  248. searchParams: this.searchParams,
  249. hash: this.hash,
  250. articlepath: this.articlepath,
  251. articleURL: this.articleURL,
  252. mainpage: this.mainpage
  253. }
  254. return 'Wiki ' + util.inspect(wiki, opts);
  255. }
  256. }
  257. /**
  258. * An article URL.
  259. * @class articleURL
  260. */
  261. class articleURL extends URL {
  262. /**
  263. * Creates a new article URL.
  264. * @param {String|URL|Wiki} [articlepath] - The article path.
  265. * @param {String|URL|Wiki} [wiki] - The wiki.
  266. * @constructs articleURL
  267. */
  268. constructor(articlepath = '/index.php?title=$1', wiki) {
  269. super(articlepath, wiki);
  270. this.protocol = 'https';
  271. this.mainpage = '';
  272. }
  273. [util.inspect.custom](depth, opts) {
  274. if ( typeof depth === 'number' && depth < 0 ) return this;
  275. if ( typeof depth === 'number' && depth < 2 ) {
  276. var link = this.href;
  277. var mainpage = link.replace( '$1', ( this.mainpage || 'Main Page' ).replace( / /g, '_' ) );
  278. return 'articleURL { ' + util.inspect(link, opts) + ' => ' + util.inspect(mainpage, opts) + ' }';
  279. }
  280. return super[util.inspect.custom](depth, opts);
  281. }
  282. }
  283. module.exports = Wiki;