special_page.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import { MessageEmbed, Util } from 'discord.js';
  2. import logging from '../../util/logging.js';
  3. import { got, toMarkdown, escapeFormatting } from '../../util/functions.js';
  4. import { createRequire } from 'module';
  5. const require = createRequire(import.meta.url);
  6. const {timeoptions} = require('../../util/default.json');
  7. const overwrites = {
  8. randompage: (fn, lang, msg, wiki, querystring, fragment, reaction, spoiler, noEmbed, args, embed, query) => {
  9. let namespaces = Object.values(query.namespaces);
  10. let contentNamespaces = namespaces.filter( ns => ns.content !== undefined );
  11. let namespaceData = [contentNamespaces.map( ns => ns.id ).join('|'), contentNamespaces.map( ns => ( ns['*'] || '*' ) ).join(', ')];
  12. if ( args[0] ) {
  13. args[0] = args[0].replace( /_/g, ' ' ).toLowerCase().trim();
  14. let namespaceMap = {};
  15. namespaces.forEach( namespace => {
  16. if ( namespace.id < 0 ) return;
  17. if ( namespace.canonical ) namespaceMap[namespace.canonical.toLowerCase()] = namespace.id;
  18. namespaceMap[namespace['*'].toLowerCase()] = namespace.id;
  19. } );
  20. query.namespacealiases.forEach( namespace => {
  21. if ( namespace.id < 0 ) return;
  22. namespaceMap[namespace['*'].toLowerCase()] = namespace.id;
  23. } );
  24. if ( namespaceMap.hasOwnProperty(args[0]) ) {
  25. namespaceData = [namespaceMap[args[0]].toString(), ( namespaces.find( namespace => namespace.id === namespaceMap[args[0]] )?.['*'] || '*' )];
  26. }
  27. else if ( args[0] === '*' ) namespaceData = ['*', '*'];
  28. }
  29. fn.random(lang, msg, wiki, reaction, spoiler, noEmbed, namespaceData, querystring, fragment, embed);
  30. },
  31. statistics: (fn, lang, msg, wiki, querystring, fragment, reaction, spoiler, noEmbed) => {
  32. fn.overview(lang, msg, wiki, reaction, spoiler, noEmbed, querystring, fragment);
  33. },
  34. diff: (fn, lang, msg, wiki, querystring, fragment, reaction, spoiler, noEmbed, args, embed) => {
  35. fn.diff(lang, msg, args, wiki, reaction, spoiler, noEmbed, embed);
  36. }
  37. }
  38. const queryfunctions = {
  39. title: (query, wiki) => query.querypage.results.map( result => {
  40. return '[' + escapeFormatting(result.title) + '](' + wiki.toLink(result.title, '', '', true) + ')';
  41. } ).join('\n'),
  42. times: (query, wiki, lang) => query.querypage.results.map( result => {
  43. return parseInt(result.value, 10).toLocaleString(lang.get('dateformat')) + '× [' + escapeFormatting(result.title) + '](' + wiki.toLink(result.title, '', '', true) + ')';
  44. } ).join('\n'),
  45. size: (query, wiki, lang) => query.querypage.results.map( result => {
  46. return lang.get('diff.info.bytes', parseInt(result.value, 10).toLocaleString(lang.get('dateformat')), result.value) + ': [' + escapeFormatting(result.title) + '](' + wiki.toLink(result.title, '', '', true) + ')';
  47. } ).join('\n'),
  48. redirect: (query, wiki) => query.querypage.results.map( result => {
  49. return '[' + escapeFormatting(result.title) + '](' + wiki.toLink(result.title, 'redirect=no', '', true) + ')' + ( result.databaseResult && result.databaseResult.rd_title ? ' → ' + escapeFormatting(result.databaseResult.rd_title) : '' );
  50. } ).join('\n'),
  51. doubleredirect: (query, wiki) => query.querypage.results.map( result => {
  52. return '[' + escapeFormatting(result.title) + '](' + wiki.toLink(result.title, 'redirect=no', '', true) + ')' + ( result.databaseResult && result.databaseResult.b_title && result.databaseResult.c_title ? ' → ' + escapeFormatting(result.databaseResult.b_title) + ' → ' + escapeFormatting(result.databaseResult.c_title) : '' );
  53. } ).join('\n'),
  54. timestamp: (query, wiki, lang) => query.querypage.results.map( result => {
  55. try {
  56. var dateformat = new Intl.DateTimeFormat(lang.get('dateformat'), Object.assign({
  57. timeZone: query.general.timezone
  58. }, timeoptions));
  59. }
  60. catch ( error ) {
  61. var dateformat = new Intl.DateTimeFormat(lang.get('dateformat'), Object.assign({
  62. timeZone: 'UTC'
  63. }, timeoptions));
  64. }
  65. let lastEditDate = new Date(result.timestamp);
  66. return dateformat.format(lastEditDate) + ' <t:' + Math.trunc(lastEditDate.getTime() / 1000) + ':R>: [' + escapeFormatting(result.title) + '](' + wiki.toLink(result.title, '', '', true) + ')';
  67. } ).join('\n'),
  68. media: (query, wiki, lang) => query.querypage.results.map( result => {
  69. var ms = result.title.split(';');
  70. return '**' + ms[1] + '**: ' + lang.get('search.category.files', parseInt(ms[2], 10).toLocaleString(lang.get('dateformat')), parseInt(ms[2], 10)) + ' (' + lang.get('diff.info.bytes', parseInt(ms[3], 10).toLocaleString(lang.get('dateformat')), parseInt(ms[3], 10)) + ')';
  71. } ).join('\n'),
  72. category: (query, wiki, lang) => query.querypage.results.map( result => {
  73. return parseInt(result.value, 10).toLocaleString(lang.get('dateformat')) + '× [' + escapeFormatting(result.title) + '](' + wiki.toLink('Category:' + result.title, '', '', true) + ')';
  74. } ).join('\n'),
  75. gadget: (query, wiki, lang) => query.querypage.results.map( result => {
  76. result.title = result.title.replace( /^(?:.*:)?gadget-/, '' );
  77. return '**' + escapeFormatting(result.title) + '**: ' + parseInt(result.value, 10).toLocaleString(lang.get('dateformat')) + ' users (' + result.ns.toLocaleString(lang.get('dateformat')) + ' active)';
  78. } ).join('\n'),
  79. recentchanges: (query, wiki) => query.recentchanges.map( result => {
  80. return '[' + escapeFormatting(result.title) + '](' + wiki.toLink(result.title, ( result.type === 'edit' ? {diff:result.revid,oldid:result.old_revid} : '' ), '', true) + ')';
  81. } ).join('\n')
  82. }
  83. const querypages = {
  84. ancientpages: ['&list=querypage&qplimit=10&qppage=Ancientpages', queryfunctions.timestamp],
  85. brokenredirects: ['&list=querypage&qplimit=10&qppage=BrokenRedirects', queryfunctions.redirect],
  86. deadendpages: ['&list=querypage&qplimit=10&qppage=Deadendpages', queryfunctions.title],
  87. doubleredirects: ['&list=querypage&qplimit=10&qppage=DoubleRedirects', queryfunctions.doubleredirect],
  88. fewestrevisions: ['&list=querypage&qplimit=10&qppage=Fewestrevisions', queryfunctions.times],
  89. listduplicatedfiles: ['&list=querypage&qplimit=10&qppage=ListDuplicatedFiles', queryfunctions.times],
  90. listredirects: ['&list=querypage&qplimit=10&qppage=Listredirects', queryfunctions.redirect],
  91. lonelypages: ['&list=querypage&qplimit=10&qppage=Lonelypages', queryfunctions.title],
  92. longpages: ['&list=querypage&qplimit=10&qppage=Longpages', queryfunctions.size],
  93. mediastatistics: ['&list=querypage&qplimit=10&qppage=MediaStatistics', queryfunctions.media],
  94. mostcategories: ['&list=querypage&qplimit=10&qppage=Mostcategories', queryfunctions.times],
  95. mostimages: ['&list=querypage&qplimit=10&qppage=Mostimages', queryfunctions.times],
  96. mostinterwikis: ['&list=querypage&qplimit=10&qppage=Mostinterwikis', queryfunctions.times],
  97. mostlinked: ['&list=querypage&qplimit=10&qppage=Mostlinked', queryfunctions.times],
  98. mostlinkedcategories: ['&list=querypage&qplimit=10&qppage=Mostlinkedcategories', queryfunctions.times],
  99. mostlinkedtemplates: ['&list=querypage&qplimit=10&qppage=Mostlinkedtemplates', queryfunctions.times],
  100. mostrevisions: ['&list=querypage&qplimit=10&qppage=Mostrevisions', queryfunctions.times],
  101. shortpages: ['&list=querypage&qplimit=10&qppage=Shortpages', queryfunctions.size],
  102. uncategorizedcategories: ['&list=querypage&qplimit=10&qppage=Uncategorizedcategories', queryfunctions.title],
  103. uncategorizedpages: ['&list=querypage&qplimit=10&qppage=Uncategorizedpages', queryfunctions.title],
  104. uncategorizedimages: ['&list=querypage&qplimit=10&qppage=Uncategorizedimages', queryfunctions.title],
  105. uncategorizedtemplates: ['&list=querypage&qplimit=10&qppage=Uncategorizedtemplates', queryfunctions.title],
  106. unusedcategories: ['&list=querypage&qplimit=10&qppage=Unusedcategories', queryfunctions.title],
  107. unusedimages: ['&list=querypage&qplimit=10&qppage=Unusedimages', queryfunctions.title],
  108. unusedtemplates: ['&list=querypage&qplimit=10&qppage=Unusedtemplates', queryfunctions.title],
  109. unwatchedpages: ['&list=querypage&qplimit=10&qppage=Unwatchedpages', queryfunctions.title],
  110. wantedcategories: ['&list=querypage&qplimit=10&qppage=Wantedcategories', queryfunctions.times],
  111. wantedfiles: ['&list=querypage&qplimit=10&qppage=Wantedfiles', queryfunctions.times],
  112. wantedpages: ['&list=querypage&qplimit=10&qppage=Wantedpages', queryfunctions.times],
  113. wantedtemplates: ['&list=querypage&qplimit=10&qppage=Wantedtemplates', queryfunctions.times],
  114. withoutinterwiki: ['&list=querypage&qplimit=10&qppage=Withoutinterwiki', queryfunctions.title],
  115. gadgetusage: ['&list=querypage&qplimit=10&qppage=GadgetUsage', queryfunctions.gadget],
  116. recentchanges: ['&list=recentchanges&rctype=edit|new|log&rclimit=10', queryfunctions.recentchanges],
  117. disambiguations: ['&list=querypage&qplimit=10&qppage=Disambiguations', queryfunctions.title],
  118. mostpopularcategories: ['&list=querypage&qplimit=10&qppage=Mostpopularcategories', queryfunctions.category],
  119. mostlinkedfilesincontent: ['&list=querypage&qplimit=10&qppage=MostLinkedFilesInContent', queryfunctions.times],
  120. unusedvideos: ['&list=querypage&qplimit=10&qppage=UnusedVideos', queryfunctions.title],
  121. withoutimages: ['&list=querypage&qplimit=10&qppage=Withoutimages', queryfunctions.title],
  122. nonportableinfoboxes: ['&list=querypage&qplimit=10&qppage=Nonportableinfoboxes', queryfunctions.title],
  123. popularpages: ['&list=querypage&qplimit=10&qppage=Popularpages', queryfunctions.title],
  124. pageswithoutinfobox: ['&list=querypage&qplimit=10&qppage=Pageswithoutinfobox', queryfunctions.title],
  125. templateswithouttype: ['&list=querypage&qplimit=10&qppage=Templateswithouttype', queryfunctions.title],
  126. allinfoboxes: ['&list=querypage&qplimit=10&qppage=AllInfoboxes', queryfunctions.title]
  127. }
  128. const descriptions = {
  129. block: 'blockiptext&amargs=16|19',
  130. checkuser: 'checkuser-summary&amargs=16|19',
  131. resettokens: 'resettokens-text',
  132. allmessages: 'allmessagestext',
  133. expandtemplates: 'expand_templates_intro',
  134. apisandbox: 'apisandbox-intro',
  135. abusefilter: 'abusefilter-intro',
  136. gadgets: 'gadgets-pagetext',
  137. categorytree: 'categorytree-header',
  138. drafts: 'drafts-view-summary&amargs=30',
  139. analytics: 'analytics_confidential',
  140. mostlinkedfilesincontent: 'mostimagesincontent-summary',
  141. popularpages: 'insights-list-description-popularpages'
  142. }
  143. /**
  144. * Processes special pages.
  145. * @param {import('../../util/i18n.js').default} lang - The user language.
  146. * @param {import('discord.js').Message} msg - The Discord message.
  147. * @param {Object} querypage - The details of the special page.
  148. * @param {String} querypage.title - The title of the special page.
  149. * @param {String} querypage.uselang - The language of the special page.
  150. * @param {String} specialpage - The canonical name of the special page.
  151. * @param {Object} query - The siteinfo from the wiki.
  152. * @param {import('../../util/wiki.js').default} wiki - The wiki for the page.
  153. * @param {URLSearchParams} querystring - The querystring for the link.
  154. * @param {String} fragment - The section for the link.
  155. * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  156. * @param {String} spoiler - If the response is in a spoiler.
  157. * @param {Boolean} noEmbed - If the response should be without an embed.
  158. */
  159. export default function special_page(lang, msg, {title, uselang = lang.lang}, specialpage, query, wiki, querystring, fragment, reaction, spoiler, noEmbed) {
  160. var pagelink = wiki.toLink(title, querystring, fragment);
  161. var embed = new MessageEmbed().setAuthor( {name: query.general.sitename} ).setTitle( escapeFormatting(title) ).setURL( pagelink ).setThumbnail( new URL(query.general.logo, wiki).href );
  162. if ( overwrites.hasOwnProperty(specialpage) ) {
  163. var args = title.split('/').slice(1,3);
  164. overwrites[specialpage](this, lang, msg, wiki, querystring, fragment, reaction, spoiler, noEmbed, args, embed, query);
  165. return;
  166. }
  167. logging(wiki, msg.guildId, 'general', 'special');
  168. if ( !msg.showEmbed() || noEmbed ) {
  169. msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler );
  170. if ( reaction ) reaction.removeEmoji();
  171. return;
  172. }
  173. if ( specialpage === 'recentchanges' && msg.isAdmin() ) {
  174. embed.addField( lang.get('rcscript.title'), lang.get('rcscript.ad', ( patreonGuildsPrefix.get(msg.guildId) ?? process.env.prefix ), '[RcGcDw](https://gitlab.com/piotrex43/RcGcDw)') );
  175. }
  176. got.get( wiki + 'api.php?uselang=' + uselang + '&action=query&meta=allmessages|siteinfo&siprop=general&amenableparser=true&amtitle=' + encodeURIComponent( title ) + '&ammessages=' + encodeURIComponent( specialpage ) + '|' + ( descriptions.hasOwnProperty(specialpage) ? descriptions[specialpage] : encodeURIComponent( specialpage ) + '-summary' ) + ( querypages.hasOwnProperty(specialpage) ? querypages[specialpage][0] : '' ) + '&converttitles=true&titles=%1F' + encodeURIComponent( title ) + '&format=json' ).then( response => {
  177. var body = response.body;
  178. if ( body && body.warnings ) log_warning(body.warnings);
  179. if ( response.statusCode !== 200 || body?.batchcomplete === undefined ) {
  180. console.log( '- ' + response.statusCode + ': Error while getting the special page: ' + ( body && body.error && body.error.info ) );
  181. return;
  182. }
  183. if ( body.query.pages?.['-1']?.title ) {
  184. title = body.query.pages['-1'].title;
  185. pagelink = wiki.toLink(title, querystring, fragment);
  186. embed.setTitle( escapeFormatting(title) );
  187. }
  188. if ( body.query.allmessages?.[0]?.['*']?.trim?.() ) {
  189. let displaytitle = escapeFormatting(body.query.allmessages[0]['*'].trim());
  190. if ( displaytitle.length > 250 ) displaytitle = displaytitle.substring(0, 250) + '\u2026';
  191. embed.setTitle( displaytitle );
  192. }
  193. if ( body.query.allmessages?.[1]?.['*']?.trim?.() ) {
  194. var description = toMarkdown(body.query.allmessages[1]['*'], wiki, title, true);
  195. if ( description.length > 1000 ) description = description.substring(0, 1000) + '\u2026';
  196. embed.setDescription( description );
  197. }
  198. if ( msg.inGuild() && patreonGuildsPrefix.has(msg.guildId) && querypages.hasOwnProperty(specialpage) ) {
  199. var text = Util.splitMessage( querypages[specialpage][1](body.query, wiki, lang), {maxLength:1000} )[0];
  200. embed.addField( lang.get('search.special'), ( text || lang.get('search.empty') ) );
  201. if ( body.query.querypage?.cached !== undefined ) {
  202. embed.setFooter( {text: lang.get('search.cached')} ).setTimestamp(new Date(body.query.querypage.cachedtimestamp));
  203. }
  204. }
  205. }, error => {
  206. console.log( '- Error while getting the special page: ' + error );
  207. } ).finally( () => {
  208. msg.sendChannel( {content: spoiler + '<' + pagelink + '>' + spoiler, embeds: [embed]} );
  209. if ( reaction ) reaction.removeEmoji();
  210. } );
  211. }