discussion.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. const htmlparser = require('htmlparser2');
  2. const {MessageEmbed} = require('discord.js');
  3. const {limit: {discussion: discussionLimit}} = require('../util/default.json');
  4. /**
  5. * Processes discussion commands.
  6. * @param {import('../util/i18n.js')} lang - The user language.
  7. * @param {import('discord.js').Message} msg - The Discord message.
  8. * @param {String} wiki - The wiki for the page.
  9. * @param {String} title - The title of the discussion post.
  10. * @param {Object} query - The siteinfo from the wiki.
  11. * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  12. * @param {String} spoiler - If the response is in a spoiler.
  13. */
  14. function fandom_discussion(lang, msg, wiki, title, query, reaction, spoiler) {
  15. var limit = discussionLimit[( msg?.guild?.id in patreons ? 'patreon' : 'default' )];
  16. if ( !title ) {
  17. var pagelink = wiki + 'f';
  18. var embed = new MessageEmbed().setAuthor( query.general.sitename ).setTitle( lang.get('discussion.main') ).setURL( pagelink );
  19. got.get( wiki + 'f' ).then( descresponse => {
  20. var descbody = descresponse.body;
  21. if ( descresponse.statusCode !== 200 || !descbody ) {
  22. console.log( '- ' + descresponse.statusCode + ': Error while getting the description.' );
  23. } else {
  24. var thumbnail = wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', query.general);
  25. var parser = new htmlparser.Parser( {
  26. onopentag: (tagname, attribs) => {
  27. if ( tagname === 'meta' && attribs.property === 'og:description' ) {
  28. var description = attribs.content.escapeFormatting();
  29. if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
  30. embed.setDescription( description );
  31. }
  32. if ( tagname === 'meta' && attribs.property === 'og:image' ) {
  33. thumbnail = attribs.content;
  34. }
  35. }
  36. }, {decodeEntities:true} );
  37. parser.write( descbody );
  38. parser.end();
  39. embed.setThumbnail( thumbnail );
  40. }
  41. }, error => {
  42. console.log( '- Error while getting the description: ' + error );
  43. } ).finally( () => {
  44. msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
  45. if ( reaction ) reaction.removeEmoji();
  46. } );
  47. }
  48. else if ( !query.wikidesc ) {
  49. return got.get( 'https://community.fandom.com/api/v1/Wikis/ByString?includeDomain=true&limit=10&string=' + query.general.servername + query.general.scriptpath + '&format=json', {
  50. responseType: 'json'
  51. } ).then( wvresponse => {
  52. var wvbody = wvresponse.body;
  53. if ( wvresponse.statusCode !== 200 || !wvbody || wvbody.exception || !wvbody.items || !wvbody.items.length ) {
  54. console.log( '- ' + wvresponse.statusCode + ': Error while getting the wiki id: ' + ( wvbody && wvbody.exception && wvbody.exception.details ) );
  55. msg.sendChannelError( spoiler + '<' + wiki + 'f' + '>' + spoiler );
  56. if ( reaction ) reaction.removeEmoji();
  57. }
  58. else if ( wvbody.items.some( site => site.domain === query.general.servername + query.general.scriptpath ) ) {
  59. query.wikidesc = {id: wvbody.items.find( site => site.domain === query.general.servername + query.general.scriptpath ).id};
  60. fandom_discussion(lang, msg, wiki, title, query, reaction, spoiler);
  61. }
  62. else {
  63. msg.sendChannelError( spoiler + '<' + wiki + 'f' + '>' + spoiler );
  64. if ( reaction ) reaction.removeEmoji();
  65. }
  66. }, error => {
  67. console.log( '- Error while getting the wiki id: ' + error );
  68. msg.sendChannelError( spoiler + '<' + wiki + 'f' + '>' + spoiler );
  69. if ( reaction ) reaction.removeEmoji();
  70. } );
  71. }
  72. else if ( title.split(' ')[0].toLowerCase() === 'post' || title.split(' ')[0].toLowerCase() === lang.get('discussion.post') ) {
  73. title = title.split(' ').slice(1).join(' ');
  74. got.get( 'https://services.fandom.com/discussion/' + query.wikidesc.id + '/posts?limit=' + limit + '&format=json', {
  75. headers: {
  76. Accept: 'application/hal+json'
  77. },
  78. responseType: 'json'
  79. } ).then( response => {
  80. var body = response.body;
  81. if ( response.statusCode !== 200 || !body || body.title || !body._embedded || !body._embedded['doc:posts'] ) {
  82. console.log( '- ' + response.statusCode + ': Error while getting the posts: ' + ( body && body.title ) );
  83. msg.sendChannelError( spoiler + '<' + wiki + 'f' + '>' + spoiler );
  84. if ( reaction ) reaction.removeEmoji();
  85. }
  86. else if ( body._embedded['doc:posts'].length ) {
  87. var posts = body._embedded['doc:posts'];
  88. var embed = new MessageEmbed().setAuthor( query.general.sitename );
  89. if ( posts.some( post => post.id === title ) ) {
  90. discussion_send(lang, msg, wiki, posts.find( post => post.id === title ), embed, spoiler);
  91. if ( reaction ) reaction.removeEmoji();
  92. }
  93. else if ( /^\d+$/.test(title) ) {
  94. got.get( 'https://services.fandom.com/discussion/' + query.wikidesc.id + '/posts/' + title + '?format=json', {
  95. headers: {
  96. Accept: 'application/hal+json'
  97. },
  98. responseType: 'json'
  99. } ).then( presponse => {
  100. var pbody = presponse.body;
  101. if ( presponse.statusCode !== 200 || !pbody || pbody.id !== title ) {
  102. if ( pbody && pbody.title === 'The requested resource was not found.' ) {
  103. if ( posts.some( post => post.rawContent.toLowerCase().includes( title.toLowerCase() ) ) ) {
  104. discussion_send(lang, msg, wiki, posts.find( post => post.rawContent.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler);
  105. }
  106. else msg.reactEmoji('🤷');
  107. }
  108. else {
  109. console.log( '- ' + presponse.statusCode + ': Error while getting the post: ' + ( pbody && pbody.title ) );
  110. msg.sendChannelError( spoiler + '<' + wiki + 'f' + '>' + spoiler );
  111. }
  112. if ( reaction ) reaction.removeEmoji();
  113. }
  114. else if ( pbody.title ) {
  115. discussion_send(lang, msg, wiki, pbody, embed, spoiler);
  116. if ( reaction ) reaction.removeEmoji();
  117. }
  118. else got.get( 'https://services.fandom.com/discussion/' + query.wikidesc.id + '/threads/' + pbody.threadId + '?format=json', {
  119. headers: {
  120. Accept: 'application/hal+json'
  121. },
  122. responseType: 'json'
  123. } ).then( thresponse => {
  124. var thbody = thresponse.body;
  125. if ( thresponse.statusCode !== 200 || !thbody || thbody.id !== pbody.threadId ) {
  126. console.log( '- ' + thresponse.statusCode + ': Error while getting the thread: ' + ( thbody && thbody.title ) );
  127. embed.setTitle( '~~' + pbody.threadId + '~~' );
  128. }
  129. else embed.setTitle( thbody.title.escapeFormatting() );
  130. }, error => {
  131. console.log( '- Error while getting the thread: ' + error );
  132. embed.setTitle( '~~' + pbody.threadId + '~~' );
  133. } ).finally( () => {
  134. discussion_send(lang, msg, wiki, pbody, embed, spoiler);
  135. if ( reaction ) reaction.removeEmoji();
  136. } );
  137. }, error => {
  138. console.log( '- Error while getting the post: ' + error );
  139. msg.sendChannelError( spoiler + '<' + wiki + 'f' + '>' + spoiler );
  140. if ( reaction ) reaction.removeEmoji();
  141. } );
  142. }
  143. else if ( posts.some( post => post.rawContent.toLowerCase().includes( title.toLowerCase() ) ) ) {
  144. discussion_send(lang, msg, wiki, posts.find( post => post.rawContent.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler);
  145. if ( reaction ) reaction.removeEmoji();
  146. }
  147. else {
  148. msg.reactEmoji('🤷');
  149. if ( reaction ) reaction.removeEmoji();
  150. }
  151. }
  152. else {
  153. msg.reactEmoji('🤷');
  154. if ( reaction ) reaction.removeEmoji();
  155. }
  156. }, error => {
  157. console.log( '- Error while getting the posts: ' + error );
  158. msg.sendChannelError( spoiler + '<' + wiki + 'f' + '>' + spoiler );
  159. if ( reaction ) reaction.removeEmoji();
  160. } );
  161. }
  162. else {
  163. got.get( 'https://services.fandom.com/discussion/' + query.wikidesc.id + '/threads?sortKey=trending&limit=' + limit + '&format=json', {
  164. headers: {
  165. Accept: 'application/hal+json'
  166. },
  167. responseType: 'json'
  168. } ).then( response => {
  169. var body = response.body;
  170. if ( response.statusCode !== 200 || !body || body.title || !body._embedded || !body._embedded.threads ) {
  171. console.log( '- ' + response.statusCode + ': Error while getting the threads: ' + ( body && body.title ) );
  172. msg.sendChannelError( spoiler + '<' + wiki + 'f' + '>' + spoiler );
  173. if ( reaction ) reaction.removeEmoji();
  174. }
  175. else if ( body._embedded.threads.length ) {
  176. var threads = body._embedded.threads;
  177. var embed = new MessageEmbed().setAuthor( query.general.sitename );
  178. if ( threads.some( thread => thread.id === title ) ) {
  179. discussion_send(lang, msg, wiki, threads.find( thread => thread.id === title ), embed, spoiler);
  180. if ( reaction ) reaction.removeEmoji();
  181. }
  182. else if ( threads.some( thread => thread.title === title ) ) {
  183. discussion_send(lang, msg, wiki, threads.find( thread => thread.title === title ), embed, spoiler);
  184. if ( reaction ) reaction.removeEmoji();
  185. }
  186. else if ( threads.some( thread => thread.title.toLowerCase() === title.toLowerCase() ) ) {
  187. discussion_send(lang, msg, wiki, threads.find( thread => thread.title.toLowerCase() === title.toLowerCase() ), embed, spoiler);
  188. if ( reaction ) reaction.removeEmoji();
  189. }
  190. else if ( threads.some( thread => thread.title.includes( title ) ) ) {
  191. discussion_send(lang, msg, wiki, threads.find( thread => thread.title.includes( title ) ), embed, spoiler);
  192. if ( reaction ) reaction.removeEmoji();
  193. }
  194. else if ( threads.some( thread => thread.title.toLowerCase().includes( title.toLowerCase() ) ) ) {
  195. discussion_send(lang, msg, wiki, threads.find( thread => thread.title.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler);
  196. if ( reaction ) reaction.removeEmoji();
  197. }
  198. else if ( /^\d+$/.test(title) ) {
  199. got.get( 'https://services.fandom.com/discussion/' + query.wikidesc.id + '/threads/' + title + '?format=json', {
  200. headers: {
  201. Accept: 'application/hal+json'
  202. },
  203. responseType: 'json'
  204. } ).then( thresponse => {
  205. var thbody = thresponse.body;
  206. if ( thresponse.statusCode !== 200 || !thbody || thbody.id !== title ) {
  207. if ( thbody && thbody.status === 404 ) {
  208. if (threads.some( thread => thread.rawContent.toLowerCase().includes( title.toLowerCase() ) ) ) {
  209. discussion_send(lang, msg, wiki, threads.find( thread => thread.rawContent.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler);
  210. }
  211. else msg.reactEmoji('🤷');
  212. }
  213. else {
  214. console.log( '- ' + thresponse.statusCode + ': Error while getting the thread: ' + ( thbody && thbody.title ) );
  215. msg.sendChannelError( spoiler + '<' + wiki + 'f/p/' + title + '>' + spoiler );
  216. }
  217. }
  218. else discussion_send(lang, msg, wiki, thbody, embed, spoiler);
  219. }, error => {
  220. console.log( '- Error while getting the thread: ' + error );
  221. msg.sendChannelError( spoiler + '<' + wiki + 'f/p/' + title + '>' + spoiler );
  222. } ).finally( () => {
  223. if ( reaction ) reaction.removeEmoji();
  224. } );
  225. }
  226. else if ( threads.some( thread => thread.rawContent.toLowerCase().includes( title.toLowerCase() ) ) ) {
  227. discussion_send(lang, msg, wiki, threads.find( thread => thread.rawContent.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler);
  228. if ( reaction ) reaction.removeEmoji();
  229. }
  230. else {
  231. msg.reactEmoji('🤷');
  232. if ( reaction ) reaction.removeEmoji();
  233. }
  234. }
  235. else {
  236. msg.reactEmoji('🤷');
  237. if ( reaction ) reaction.removeEmoji();
  238. }
  239. }, error => {
  240. console.log( '- Error while getting the threads: ' + error );
  241. msg.sendChannelError( spoiler + '<' + wiki + 'f' + '>' + spoiler );
  242. if ( reaction ) reaction.removeEmoji();
  243. } );
  244. }
  245. }
  246. /**
  247. * Send discussion posts.
  248. * @param {import('../util/i18n.js')} lang - The user language.
  249. * @param {import('discord.js').Message} msg - The Discord message.
  250. * @param {String} wiki - The wiki for the page.
  251. * @param {Object} discussion - The discussion post.
  252. * @param {import('discord.js').MessageEmbed} embed - The embed for the page.
  253. * @param {String} spoiler - If the response is in a spoiler.
  254. */
  255. function discussion_send(lang, msg, wiki, discussion, embed, spoiler) {
  256. if ( discussion.title ) {
  257. embed.setTitle( discussion.title.escapeFormatting() );
  258. var pagelink = wiki + 'f/p/' + ( discussion.threadId || discussion.id );
  259. }
  260. else {
  261. if ( discussion._embedded.thread ) embed.setTitle( discussion._embedded.thread[0].title.escapeFormatting() );
  262. var pagelink = wiki + 'f/p/' + discussion.threadId + '/r/' + discussion.id;
  263. }
  264. var text = '<' + pagelink + '>';
  265. embed.setURL( pagelink ).setFooter( discussion.createdBy.name, discussion.createdBy.avatarUrl ).setTimestamp( discussion.creationDate.epochSecond * 1000 );
  266. var description = '';
  267. switch ( discussion.funnel ) {
  268. case 'IMAGE':
  269. embed.setImage( discussion._embedded.contentImages[0].url );
  270. break;
  271. case 'POLL':
  272. discussion.poll.answers.forEach( answer => embed.addField( answer.text.escapeFormatting(), ( answer.image ? '[__' + lang.get('discussion.image').escapeFormatting() + '__](' + answer.image.url + ')\n' : '' ) + lang.get('discussion.votes', answer.votes), true ) );
  273. break;
  274. case 'QUIZ':
  275. description = discussion._embedded.quizzes[0].title.escapeFormatting();
  276. embed.setThumbnail( discussion._embedded.quizzes[0].image );
  277. break;
  278. default:
  279. if ( discussion.jsonModel ) {
  280. try {
  281. description = discussion_formatting(JSON.parse(discussion.jsonModel)).replace( /(?:\*\*\*\*|(?<!\\)\_\_)/g, '' ).replace( /{@wiki}/g, wiki );
  282. if ( discussion._embedded.contentImages.length ) {
  283. if ( description.trim().endsWith( '{@0}' ) ) {
  284. embed.setImage( discussion._embedded.contentImages[0].url );
  285. description = description.replace( '{@0}', '' ).trim();
  286. }
  287. else {
  288. description = description.replace( /\{\@(\d+)\}/g, (match, n) => {
  289. if ( n >= discussion._embedded.contentImages.length ) return '';
  290. else return '[__' + lang.get('discussion.image').escapeFormatting() + '__](' + discussion._embedded.contentImages[n].url + ')';
  291. } );
  292. embed.setThumbnail( discussion._embedded.contentImages[0].url );
  293. }
  294. }
  295. else embed.setThumbnail( wiki.toLink('Special:FilePath/Wiki-wordmark.png') );
  296. }
  297. catch ( jsonerror ) {
  298. console.log( '- Error while getting the formatting: ' + jsonerror );
  299. description = discussion.rawContent.escapeFormatting();
  300. if ( discussion._embedded.contentImages.length ) embed.setThumbnail( discussion._embedded.contentImages[0].url );
  301. }
  302. }
  303. else if ( discussion.renderedContent ) {
  304. var current_tag = '';
  305. var parser = new htmlparser.Parser( {
  306. onopentag: (tagname, attribs) => {
  307. if ( tagname === 'a' ) {
  308. current_tag = attribs.href;
  309. description += '[';
  310. }
  311. },
  312. ontext: (htmltext) => {
  313. description += htmltext.escapeFormatting();
  314. },
  315. onclosetag: (tagname) => {
  316. if ( tagname === 'a' ) {
  317. description += '](' + current_tag + ')';
  318. current_tag = '';
  319. }
  320. if ( tagname === 'p' ) description += '\n';
  321. }
  322. }, {decodeEntities:true} );
  323. parser.write( discussion.renderedContent );
  324. parser.end();
  325. if ( discussion._embedded.contentImages.length ) embed.setThumbnail( discussion._embedded.contentImages[0].url );
  326. }
  327. else {
  328. description = discussion.rawContent.escapeFormatting();
  329. if ( discussion._embedded.contentImages.length ) embed.setThumbnail( discussion._embedded.contentImages[0].url );
  330. }
  331. }
  332. if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
  333. embed.setDescription( description );
  334. msg.sendChannel( spoiler + text + spoiler, {embed} );
  335. }
  336. /**
  337. * Format discussion content
  338. * @param {Object} jsonModel - The content of the discussion post.
  339. * @returns {String}
  340. */
  341. function discussion_formatting(jsonModel) {
  342. var description = '';
  343. switch ( jsonModel.type ) {
  344. case 'doc':
  345. if ( jsonModel.content ) jsonModel.content.forEach( content => description += discussion_formatting(content) );
  346. break;
  347. case 'paragraph':
  348. if ( jsonModel.content ) jsonModel.content.forEach( content => description += discussion_formatting(content) );
  349. description += '\n';
  350. break;
  351. case 'openGraph':
  352. if ( !jsonModel.attrs.wasAddedWithInlineLink ) description += jsonModel.attrs.url + '\n';
  353. break;
  354. case 'text':
  355. var prepend = '';
  356. var append = '';
  357. if ( jsonModel.marks ) {
  358. jsonModel.marks.forEach( mark => {
  359. switch ( mark.type ) {
  360. case 'mention':
  361. prepend += '[';
  362. append = ']({@wiki}f/u/' + mark.attrs.userId + ')' + append;
  363. break;
  364. case 'link':
  365. prepend += '[';
  366. append = '](' + mark.attrs.href + ')' + append;
  367. break;
  368. case 'strong':
  369. prepend += '**';
  370. append = '**' + append;
  371. break;
  372. case 'em':
  373. prepend += '_';
  374. append = '_' + append;
  375. break;
  376. }
  377. } );
  378. }
  379. description += prepend + jsonModel.text.escapeFormatting() + append;
  380. break;
  381. case 'image':
  382. if ( jsonModel.attrs.id !== null ) description += '{@' + jsonModel.attrs.id + '}\n';
  383. break;
  384. case 'code_block':
  385. description += '```\n';
  386. if ( jsonModel.content ) jsonModel.content.forEach( content => description += discussion_formatting(content) );
  387. description += '\n```\n';
  388. break;
  389. case 'bulletList':
  390. jsonModel.content.forEach( listItem => {
  391. description += '\t• ';
  392. if ( listItem.content ) listItem.content.forEach( content => description += discussion_formatting(content) );
  393. } );
  394. break;
  395. case 'orderedList':
  396. var n = 1;
  397. jsonModel.content.forEach( listItem => {
  398. description += '\t' + n + '. ';
  399. n++;
  400. if ( listItem.content ) listItem.content.forEach( content => description += discussion_formatting(content) );
  401. } );
  402. break;
  403. }
  404. return description;
  405. }
  406. module.exports = fandom_discussion;