inline.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. const logging = require('../util/logging.js');
  2. const Wiki = require('../util/wiki.js');
  3. const {limitLength, partialURIdecode, allowDelete} = require('../util/functions.js');
  4. /**
  5. * Post a message with inline wiki links.
  6. * @param {Object} interaction - The interaction.
  7. * @param {import('discord.js').Client} interaction.client - The client of the interaction.
  8. * @param {import('../util/i18n.js')} lang - The user language.
  9. * @param {import('../util/wiki.js')} wiki - The wiki for the interaction.
  10. * @param {import('discord.js').TextChannel} [channel] - The channel for the interaction.
  11. */
  12. function slash_inline(interaction, lang, wiki, channel) {
  13. var text = ( interaction.data.options?.[0]?.value || '' ).replace( /\]\(/g, ']\\(' );
  14. text = text.replace( /\x1F/g, '' ).replace( /(?<!@)\u200b/g, '' ).trim();
  15. if ( !text.includes( '{{' ) && !( text.includes( '[[' ) && text.includes( ']]' ) ) && !text.includes( 'PMID' ) && !text.includes( 'RFC' ) && !text.includes( 'ISBN' ) ) {
  16. return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
  17. data: {
  18. type: 4,
  19. data: {
  20. content: lang.get('interaction.inline'),
  21. allowed_mentions: {
  22. parse: []
  23. },
  24. flags: 64
  25. }
  26. }
  27. } ).catch(log_error);
  28. }
  29. var allowed_mentions = {
  30. parse: ['users']
  31. };
  32. if ( interaction.guild_id ) {
  33. if ( ( (interaction.member.permissions & 1 << 3) === 1 << 3 ) // ADMINISTRATOR
  34. || ( (interaction.member.permissions & 1 << 17) === 1 << 17 ) ) { // MENTION_EVERYONE
  35. allowed_mentions.parse = ['users', 'roles', 'everyone'];
  36. }
  37. else if ( channel?.guild ) {
  38. allowed_mentions.roles = channel.guild.roles.cache.filter( role => role.mentionable ).map( role => role.id ).slice(0, 100);
  39. }
  40. if ( channel?.guild && ( (interaction.member.permissions & 1 << 3) !== 1 << 3 ) // ADMINISTRATOR
  41. && ( (interaction.member.permissions & 1 << 18) !== 1 << 18 ) ) { // USE_EXTERNAL_EMOJIS
  42. text = text.replace( /(?<!\\)<a?(:\w+:)\d+>/g, (replacement, emoji, id) => {
  43. if ( channel.guild.emojis.cache.has(id) ) {
  44. return replacement;
  45. }
  46. return emoji;
  47. } );
  48. }
  49. }
  50. if ( text.length > 1800 ) text = text.substring(0, 1800) + '\u2026';
  51. var message = {
  52. content: text.replace( /(?<!\\)<a?(:\w+:)\d+>/g, (replacement, emoji, id) => {
  53. if ( channel?.guild?.emojis.cache.has(id) ) {
  54. return replacement;
  55. }
  56. return emoji;
  57. } ),
  58. allowed_mentions
  59. };
  60. return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
  61. data: {
  62. type: 5,
  63. data: {
  64. flags: 0
  65. }
  66. }
  67. } ).then( () => {
  68. var textReplacement = [];
  69. var magiclinks = [];
  70. var replacedText = text.replace( /(?<!\\)(?:<a?(:\w+:)\d+>|<#(\d+)>|<@!?(\d+)>|<@&(\d+)>|```.+?```|``.+?``|`.+?`)/gs, (replacement, emoji, textchannel, user, role) => {
  71. textReplacement.push(replacement);
  72. var arg = '';
  73. if ( emoji ) arg = emoji;
  74. if ( textchannel ) {
  75. let tempchannel = interaction.client.channels.cache.get(textchannel);
  76. if ( tempchannel ) arg = '#' + tempchannel.name;
  77. }
  78. if ( user ) {
  79. let tempuser = channel?.guild?.members.cache.get(user);
  80. if ( tempuser ) arg = '@' + tempuser.displayName;
  81. else {
  82. tempuser = interaction.client.users.cache.get(user);
  83. if ( tempuser ) arg = '@' + tempuser.username;
  84. }
  85. }
  86. if ( role ) {
  87. let temprole = channel?.guild?.roles.cache.get(role);
  88. if ( temprole ) arg = '@' + temprole.name;
  89. }
  90. return '\x1F<replacement\x1F' + textReplacement.length + ( arg ? '\x1F' + arg : '' ) + '>\x1F';
  91. } ).replace( /\b(PMID|RFC) +([0-9]+)\b/g, (replacement, type, id) => {
  92. magiclinks.push({type, id, replacementId: textReplacement.length});
  93. textReplacement.push(replacement);
  94. return '\x1F<replacement\x1F' + textReplacement.length + '\x1F' + replacement + '>\x1F';
  95. } ).replace( /\bISBN +((?:97[89][- ]?)?(?:[0-9][- ]?){9}[0-9Xx])\b/g, (replacement, id) => {
  96. let isbn = id.replace( /[- ]/g, '' ).replace( /x/g, 'X' );
  97. magiclinks.push({type: 'ISBN', id, isbn, replacementId: textReplacement.length});
  98. textReplacement.push(replacement);
  99. return '\x1F<replacement\x1F' + textReplacement.length + '\x1F' + replacement + '>\x1F';
  100. } );
  101. var templates = [];
  102. var links = [];
  103. var breakInline = false;
  104. replacedText.replace( /\x1F<replacement\x1F\d+\x1F(.+?)>\x1F/g, '$1' ).replace( /(?:%[\dA-F]{2})+/g, partialURIdecode ).split('\n').forEach( line => {
  105. if ( line.startsWith( '>>> ' ) ) breakInline = true;
  106. if ( line.startsWith( '> ' ) || breakInline ) return;
  107. var inlineLink = null;
  108. var regex = /(?<!\\|\{)\{\{(?:\s*(?:subst|safesubst|raw|msg|msgnw):)?([^<>\[\]\|\{\}\x01-\x1F\x7F#]+)(?<!\\)(?:\||\}\})/g;
  109. while ( ( inlineLink = regex.exec(line) ) !== null ) {
  110. let title = inlineLink[1].trim();
  111. if ( !title.replace( /:/g, '' ).trim().length || title.startsWith( '/' ) ) continue;
  112. if ( title.startsWith( 'int:' ) ) templates.push({
  113. raw: title,
  114. title: title.replace( /^int:/, 'MediaWiki:' ),
  115. template: title.replace( /^int:/, 'MediaWiki:' )
  116. });
  117. else templates.push({raw: title, title, template: 'Template:' + title});
  118. }
  119. inlineLink = null;
  120. regex = /(?<!\\)\[\[([^<>\[\]\|\{\}\x01-\x1F\x7F]+)(?:\|(?:(?!\[\[|\]\\\]).)*?)?(?<!\\)\]\]/g;
  121. while ( ( inlineLink = regex.exec(line) ) !== null ) {
  122. inlineLink[1] = inlineLink[1].trim();
  123. let title = inlineLink[1].split('#')[0].trim();
  124. let section = inlineLink[1].split('#').slice(1).join('#');
  125. if ( !title.replace( /:/g, '' ).trim().length || title.startsWith( '/' ) ) continue;
  126. links.push({raw: title, title, section});
  127. }
  128. } );
  129. if ( !templates.length && !links.length && !magiclinks.length ) {
  130. return sendMessage(interaction, message, channel);
  131. }
  132. return got.get( wiki + 'api.php?action=query&meta=siteinfo' + ( magiclinks.length ? '|allmessages&ammessages=pubmedurl|rfcurl&amenableparser=true' : '' ) + '&siprop=general&iwurl=true&titles=' + encodeURIComponent( [
  133. ...templates.map( link => link.title + '|' + link.template ),
  134. ...links.map( link => link.title ),
  135. ...( magiclinks.length ? ['Special:BookSources'] : [] )
  136. ].join('|') ) + '&format=json' ).then( response => {
  137. var body = response.body;
  138. if ( response.statusCode !== 200 || body?.batchcomplete === undefined || !body?.query ) {
  139. if ( wiki.noWiki(response.url, response.statusCode) ) {
  140. console.log( '- This wiki doesn\'t exist!' );
  141. }
  142. else {
  143. console.log( '- ' + response.statusCode + ': Error while following the links: ' + body?.error?.info );
  144. }
  145. return sendMessage(interaction, message, channel);
  146. }
  147. logging(wiki, interaction.guild_id, 'slash', 'inline');
  148. wiki.updateWiki(body.query.general);
  149. if ( body.query.normalized ) {
  150. body.query.normalized.forEach( title => {
  151. templates.filter( link => link.title === title.from ).forEach( link => link.title = title.to );
  152. templates.filter( link => link.template === title.from ).forEach( link => link.template = title.to );
  153. links.filter( link => link.title === title.from ).forEach( link => link.title = title.to );
  154. } );
  155. }
  156. if ( body.query.interwiki ) {
  157. body.query.interwiki.forEach( interwiki => {
  158. templates.filter( link => link.title === interwiki.title ).forEach( link => {
  159. link.url = decodeURI(interwiki.url)
  160. } );
  161. links.filter( link => link.title === interwiki.title ).forEach( link => {
  162. link.url = ( link.section ? decodeURI(interwiki.url.split('#')[0]) + Wiki.toSection(link.section) : decodeURI(interwiki.url) );
  163. } );
  164. } );
  165. }
  166. if ( body.query.pages ) {
  167. Object.values(body.query.pages).forEach( page => {
  168. templates.filter( link => link.title === page.title ).forEach( link => {
  169. if ( page.invalid !== undefined || ( page.missing !== undefined && page.known === undefined ) ) {
  170. link.title = '';
  171. }
  172. else if ( page.ns === 0 && !link.raw.startsWith( ':' ) ) {
  173. link.title = '';
  174. }
  175. } );
  176. templates.filter( link => link.template === page.title ).forEach( link => {
  177. if ( page.invalid !== undefined || ( page.missing !== undefined && page.known === undefined ) ) {
  178. link.template = '';
  179. }
  180. } );
  181. links.filter( link => link.title === page.title ).forEach( link => {
  182. link.ns = page.ns;
  183. if ( page.invalid !== undefined ) return links.splice(links.indexOf(link), 1);
  184. if ( page.missing !== undefined && page.known === undefined ) {
  185. if ( ( page.ns === 2 || page.ns === 202 ) && !page.title.includes( '/' ) ) {
  186. return;
  187. }
  188. if ( wiki.isMiraheze() && page.ns === 0 && /^Mh:[a-z\d]+:/.test(page.title) ) {
  189. var iw_parts = page.title.split(':');
  190. var iw = new Wiki('https://' + iw_parts[1] + '.miraheze.org/w/');
  191. link.url = iw.toLink(iw_parts.slice(2).join(':'), '', link.section, true);
  192. return;
  193. }
  194. return links.splice(links.indexOf(link), 1);
  195. }
  196. } );
  197. } );
  198. }
  199. if ( magiclinks.length && body.query?.allmessages?.length === 2 ) {
  200. magiclinks = magiclinks.filter( link => body.query.general.magiclinks.hasOwnProperty(link.type) );
  201. if ( magiclinks.length ) magiclinks.forEach( link => {
  202. if ( link.type === 'PMID' && body.query.allmessages[0]?.['*']?.includes( '$1' ) ) {
  203. link.url = new URL(body.query.allmessages[0]['*'].replace( /\$1/g, link.id ), wiki).href;
  204. }
  205. if ( link.type === 'RFC' && body.query.allmessages[1]?.['*']?.includes( '$1' ) ) {
  206. link.url = new URL(body.query.allmessages[1]['*'].replace( /\$1/g, link.id ), wiki).href;
  207. }
  208. if ( link.type === 'ISBN' ) {
  209. let title = 'Special:BookSources';
  210. title = ( body.query.normalized?.find( title => title.from === title )?.to || title );
  211. link.url = wiki.toLink(title + '/' + link.isbn, '', '', true);
  212. }
  213. if ( link.url ) {
  214. console.log( ( interaction.guild_id || '@' + interaction.user.id ) + ': Slash: ' + link.type + ' ' + link.id );
  215. textReplacement[link.replacementId] = '[' + link.type + ' ' + link.id + '](<' + link.url + '>)';
  216. }
  217. } );
  218. }
  219. templates = templates.filter( link => link.title || link.template );
  220. if ( templates.length || links.length || magiclinks.length ) {
  221. breakInline = false;
  222. if ( templates.length || links.length ) replacedText = replacedText.split('\n').map( line => {
  223. if ( line.startsWith( '>>> ' ) ) breakInline = true;
  224. if ( line.startsWith( '> ' ) || breakInline ) return line;
  225. let regex = null;
  226. if ( line.includes( '{{' ) ) {
  227. regex = /(?<!\\|\{)(\{\{(?:\s*(?:subst|safesubst|raw|msg|msgnw):)?\s*)((?:[^<>\[\]\|\{\}\x01-\x1F\x7F#]|\x1F<replacement\x1F\d+\x1F.+?>\x1F)+?)(\s*(?<!\\)\||\}\})/g;
  228. line = line.replace( regex, (fullLink, linkprefix, title, linktrail) => {
  229. title = title.replace( /(?:%[\dA-F]{2})+/g, partialURIdecode ).replace( /\x1F<replacement\x1F\d+\x1F(.+?)>\x1F/g, '$1' ).trim();
  230. let link = templates.find( link => link.raw === title );
  231. if ( !link ) return fullLink;
  232. console.log( ( interaction.guild_id || '@' + interaction.user.id ) + ': Slash: ' + fullLink );
  233. if ( title.startsWith( 'int:' ) ) {
  234. title = title.replace( /^int:\s*/, replacement => {
  235. linkprefix += replacement;
  236. return '';
  237. } );
  238. }
  239. return linkprefix + '[' + title + '](<' + ( link.url || wiki.toLink(link.title || link.template, '', '', true) ) + '>)' + linktrail;
  240. } );
  241. }
  242. if ( line.includes( '[[' ) && line.includes( ']]' ) ) {
  243. regex = new RegExp( '([' + body.query.general.linkprefixcharset.replace( /\\x([a-fA-f0-9]{4,6}|\{[a-fA-f0-9]{4,6}\})/g, '\\u$1' ) + ']+)?' + '(?<!\\\\)\\[\\[' + '((?:[^' + "<>\\[\\]\\|\{\}\\x01-\\x1F\\x7F" + ']|' + '\\x1F<replacement\\x1F\\d+\\x1F.+?>\\x1F' + ')+)' + '(?:\\|((?:(?!\\[\\[|\\]\\(|\\]\\\\\\]).)*?))?' + '(?<!\\\\)\\]\\]' + body.query.general.linktrail.replace( /\\x([a-fA-f0-9]{4,6}|\{[a-fA-f0-9]{4,6}\})/g, '\\u$1' ).replace( /^\/\^(\(\[.+?\]\+\))\(\.\*\)\$\/sDu?$/, '$1?' ), 'gu' );
  244. line = line.replace( regex, (fullLink, linkprefix = '', title, display, linktrail = '') => {
  245. title = title.replace( /(?:%[\dA-F]{2})+/g, partialURIdecode ).replace( /\x1F<replacement\x1F\d+\x1F(.+?)>\x1F/g, '$1' ).split('#')[0].trim();
  246. let link = links.find( link => link.raw === title );
  247. if ( !link ) return fullLink;
  248. console.log( ( interaction.guild_id || '@' + interaction.user.id ) + ': Slash: ' + fullLink );
  249. if ( display === undefined ) display = title.replace( /^\s*:?/, '' );
  250. if ( !display.trim() ) {
  251. display = title.replace( /^\s*:/, '' );
  252. if ( display.includes( ',' ) && !/ ([^\(\)]+)$/.test(display) ) {
  253. display = display.replace( /^([^,]+), .*$/, '$1' );
  254. }
  255. display = display.replace( / \([^\(\)]+\)$/, '' );
  256. if ( link.url || link.ns !== 0 ) {
  257. display = display.split(':').slice(1).join(':');
  258. }
  259. }
  260. return '[' + ( linkprefix + display + linktrail ).replace( /\x1F<replacement\x1F\d+\x1F((?:PMID|RFC|ISBN) .+?)>\x1F/g, '$1' ).replace( /[\[\]\(\)]/g, '\\$&' ) + '](<' + ( link.url || wiki.toLink(link.title, '', link.section, true) ) + '>)';
  261. } );
  262. }
  263. return line;
  264. } ).join('\n');
  265. text = replacedText.replace( /\x1F<replacement\x1F(\d+)(?:\x1F.+?)?>\x1F/g, (replacement, id) => {
  266. return textReplacement[id - 1];
  267. } );
  268. if ( text.length > 1900 ) text = limitLength(text, 1900, 100);
  269. message.content = text;
  270. return sendMessage(interaction, message, channel);
  271. }
  272. else return sendMessage(interaction, message, channel);
  273. }, error => {
  274. if ( wiki.noWiki(error.message) ) {
  275. console.log( '- This wiki doesn\'t exist!' );
  276. }
  277. else {
  278. console.log( '- Error while following the links: ' + error );
  279. }
  280. return sendMessage(interaction, message, channel);
  281. } );
  282. }, log_error );
  283. }
  284. /**
  285. * Sends an interaction response.
  286. * @param {Object} interaction - The interaction.
  287. * @param {Object} message - The message.
  288. * @param {String} message.content - The message content.
  289. * @param {{parse: String[], roles?: String[]}} message.allowed_mentions - The allowed mentions.
  290. * @param {import('discord.js').TextChannel} [channel] - The channel for the interaction.
  291. */
  292. function sendMessage(interaction, message, channel) {
  293. return interaction.client.api.webhooks(interaction.application_id, interaction.token).messages('@original').patch( {
  294. data: message
  295. } ).then( msg => {
  296. if ( channel ) allowDelete(channel.messages.add(msg), ( interaction.member?.user.id || interaction.user.id ));
  297. }, log_error );
  298. }
  299. module.exports = {
  300. name: 'inline',
  301. run: slash_inline
  302. };