eval.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import { inspect } from 'util';
  2. import { load as cheerioLoad } from 'cheerio';
  3. import Discord from 'discord.js';
  4. import { got } from '../util/functions.js';
  5. import newMessage from '../util/newMessage.js';
  6. import Wiki from '../util/wiki.js';
  7. import db from '../util/database.js';
  8. import { createRequire } from 'module';
  9. const require = createRequire(import.meta.url);
  10. const {limit: {verification: verificationLimit, rcgcdw: rcgcdwLimit}} = require('../util/default.json');
  11. inspect.defaultOptions = {compact: false, breakLength: Infinity};
  12. /**
  13. * Processes the "eval" command.
  14. * @param {import('../util/i18n.js').default} lang - The user language.
  15. * @param {Discord.Message} msg - The Discord message.
  16. * @param {String[]} args - The command arguments.
  17. * @param {String} line - The command as plain text.
  18. * @param {Wiki} wiki - The wiki for the message.
  19. * @async
  20. */
  21. async function cmd_eval(lang, msg, args, line, wiki) {
  22. try {
  23. var text = inspect( await eval( args.join(' ') ) );
  24. } catch ( error ) {
  25. var text = error.toString();
  26. }
  27. if ( isDebug ) console.log( '--- EVAL START ---\n' + text + '\n--- EVAL END ---' );
  28. if ( text.length > 1990 ) msg.reactEmoji('✅', true);
  29. else msg.sendChannel( '```js\n' + text + '\n```', true );
  30. /**
  31. * Runs a command with admin permissions.
  32. * @param {String} cmdline - The message text.
  33. */
  34. function backdoor(cmdline) {
  35. msg.evalUsed = true;
  36. msg.onlyVerifyCommand = false;
  37. newMessage(msg, lang, wiki, patreonGuildsPrefix.get(msg.guildId), msg.noInline, cmdline);
  38. return cmdline;
  39. }
  40. }
  41. /**
  42. * Runs database queries.
  43. * @param {String} sql - The SQL command.
  44. * @param {String[]} [sqlargs] - The command arguments.
  45. */
  46. function database(sql, sqlargs = []) {
  47. return db.query( sql, sqlargs ).then( ({rows}) => {
  48. return rows;
  49. } );
  50. }
  51. /**
  52. * Checks a wiki and it's recent changes webhooks.
  53. * @param {Wiki} wiki - The wiki to check.
  54. */
  55. function checkWiki(wiki) {
  56. wiki = Wiki.fromInput(wiki);
  57. if ( !wiki ) return `Couldn't resolve "${wiki}" into a valid url.`;
  58. return got.get( wiki + 'api.php?&action=query&meta=siteinfo&siprop=general&list=recentchanges&rcshow=!bot&rctype=edit|new|log|categorize&rcprop=ids|timestamp&rclimit=100&format=json' ).then( response => {
  59. if ( response.statusCode === 404 && typeof response.body === 'string' ) {
  60. let api = cheerioLoad(response.body)('head link[rel="EditURI"]').prop('href');
  61. if ( api ) {
  62. wiki = new Wiki(api.split('api.php?')[0], wiki);
  63. return got.get( wiki + 'api.php?action=query&meta=siteinfo&siprop=general&list=recentchanges&rcshow=!bot&rctype=edit|new|log|categorize&rcprop=ids|timestamp&rclimit=100&format=json' );
  64. }
  65. }
  66. return response;
  67. } ).then( response => {
  68. var body = response.body;
  69. if ( response.statusCode !== 200 || body?.batchcomplete === undefined || !body?.query?.recentchanges ) {
  70. return response.statusCode + ': Error while checking the wiki: ' + body?.error?.info;
  71. }
  72. wiki.updateWiki(body.query.general);
  73. var result = {
  74. wiki: wiki.href,
  75. activity: [],
  76. rcid: 0,
  77. postid: '-1'
  78. }
  79. var rc = body.query.recentchanges;
  80. if ( rc.length ) {
  81. result.rcid = rc[0].rcid;
  82. let text = '';
  83. let len = ( Date.parse(rc[0].timestamp) - Date.parse(rc[rc.length - 1].timestamp) ) / 60_000;
  84. len = Math.round(len);
  85. let rdays = ( len / 1440 );
  86. let days = Math.floor(rdays);
  87. if ( days > 0 ) {
  88. if ( days === 1 ) text += ` ${days} day`;
  89. else text += ` ${days} days`;
  90. }
  91. let rhours = ( rdays - days ) * 24;
  92. let hours = Math.floor(rhours);
  93. if ( hours > 0 ) {
  94. if ( text.length ) text += ' and';
  95. if ( hours === 1 ) text += ` ${hours} hour`;
  96. else text += ` ${hours} hours`;
  97. }
  98. let rminutes = ( rhours - hours ) * 60;
  99. let minutes = Math.round(rminutes);
  100. if ( minutes > 0 ) {
  101. if ( text.length ) text += ' and';
  102. if ( minutes === 1 ) text += ` ${minutes} minute`;
  103. else text += ` ${minutes} minutes`;
  104. }
  105. result.activity.push(`${rc.length} edits in${text}`);
  106. }
  107. return Promise.all([
  108. db.query( 'SELECT guild, lang, display, rcid, postid FROM rcgcdw WHERE wiki = $1', [result.wiki] ).then( ({rows}) => {
  109. result.rcgcdb = rows;
  110. }, dberror => {
  111. result.rcgcdb = dberror.toString();
  112. } ),
  113. ( wiki.isFandom() ? got.get( wiki + 'wikia.php?controller=DiscussionPost&method=getPosts&includeCounters=false&sortDirection=descending&sortKey=creation_date&limit=100&format=json&cache=' + Date.now(), {
  114. headers: {
  115. Accept: 'application/hal+json'
  116. }
  117. } ).then( dsresponse => {
  118. var dsbody = dsresponse.body;
  119. if ( dsresponse.statusCode !== 200 || !dsbody || dsbody.status === 404 ) {
  120. if ( dsbody?.status !== 404 ) result.postid = dsresponse.statusCode + ': Error while checking discussions: ' + dsbody?.title;
  121. return;
  122. }
  123. var posts = dsbody._embedded?.['doc:posts'];
  124. result.postid = ( posts[0]?.id || '0' );
  125. if ( posts?.length ) {
  126. let text = '';
  127. let len = ( posts[0].creationDate.epochSecond - posts[posts.length - 1].creationDate.epochSecond ) / 60;
  128. len = Math.round(len);
  129. let rdays = ( len / 1440 );
  130. let days = Math.floor(rdays);
  131. if ( days > 0 ) {
  132. if ( days === 1 ) text += ` ${days} day`;
  133. else text += ` ${days} days`;
  134. }
  135. let rhours = ( rdays - days ) * 24;
  136. let hours = Math.floor(rhours);
  137. if ( hours > 0 ) {
  138. if ( text.length ) text += ' and';
  139. if ( hours === 1 ) text += ` ${hours} hour`;
  140. else text += ` ${hours} hours`;
  141. }
  142. let rminutes = ( rhours - hours ) * 60;
  143. let minutes = Math.round(rminutes);
  144. if ( minutes > 0 ) {
  145. if ( text.length ) text += ' and';
  146. if ( minutes === 1 ) text += ` ${minutes} minute`;
  147. else text += ` ${minutes} minutes`;
  148. }
  149. result.activity.push(`${posts.length} posts in${text}`);
  150. }
  151. }, error => {
  152. result.postid = 'Error while checking discussions: ' + error;
  153. } ) : null )
  154. ]).then( () => {
  155. return result;
  156. } );
  157. }, error => {
  158. return 'Error while checking the wiki: ' + error;
  159. } );
  160. }
  161. /**
  162. * Removes the patreon features for a guild.
  163. * @param {String} guild - The guild ID.
  164. * @param {Discord.Message} msg - The Discord message.
  165. */
  166. function removePatreons(guild, msg) {
  167. if ( !( typeof guild === 'string' || msg instanceof Discord.Message ) ) {
  168. return 'removePatreons(guild, msg) – No guild or message provided!';
  169. }
  170. return db.connect().then( client => {
  171. var messages = [];
  172. return client.query( 'SELECT lang, role, inline FROM discord WHERE guild = $1 AND channel IS NULL', [guild] ).then( ({rows:[row]}) => {
  173. if ( !row ) {
  174. messages.push('The guild doesn\'t exist!');
  175. return Promise.reject();
  176. }
  177. return client.query( 'UPDATE discord SET lang = $1, role = $2, inline = $3, prefix = $4, patreon = NULL WHERE guild = $5', [row.lang, row.role, row.inline, process.env.prefix, guild] ).then( ({rowCount}) => {
  178. if ( rowCount ) {
  179. console.log( '- Guild successfully updated.' );
  180. messages.push('Guild successfully updated.');
  181. }
  182. msg.client.shard.broadcastEval( (discordClient, evalData) => {
  183. patreonGuildsPrefix.delete(evalData);
  184. }, {context: guild} );
  185. }, dberror => {
  186. console.log( '- Error while updating the guild: ' + dberror );
  187. messages.push('Error while updating the guild: ' + dberror);
  188. return Promise.reject();
  189. } ).then( () => {
  190. return client.query( 'DELETE FROM discord WHERE guild = $1 AND channel LIKE $2 RETURNING channel, wiki', [guild, '#%'] ).then( ({rows}) => {
  191. if ( rows.length ) {
  192. console.log( '- Channel categories successfully deleted.' );
  193. messages.push('Channel categories successfully deleted.');
  194. return msg.client.shard.broadcastEval( (discordClient, evalData) => {
  195. if ( discordClient.guilds.cache.has(evalData.guild) ) {
  196. let rows = evalData.rows;
  197. return discordClient.guilds.cache.get(evalData.guild).channels.cache.filter( channel => {
  198. return ( ( channel.isText() && !channel.isThread() ) && rows.some( row => {
  199. return ( row.channel === '#' + channel.parentId );
  200. } ) );
  201. } ).map( channel => {
  202. return {
  203. id: channel.id,
  204. wiki: rows.find( row => {
  205. return ( row.channel === '#' + channel.parentId );
  206. } ).wiki
  207. };
  208. } );
  209. }
  210. }, {
  211. context: {guild, rows},
  212. shard: Discord.ShardClientUtil.shardIdForGuildId(guild, msg.client.shard.count)
  213. } ).then( channels => {
  214. if ( channels.length ) return Promise.all(channels.map( channel => {
  215. return client.query( 'INSERT INTO discord(wiki, guild, channel, lang, role, inline, prefix) VALUES($1, $2, $3, $4, $5, $6, $7)', [channel.wiki, guild, channel.id, row.lang, row.role, row.inline, process.env.prefix] ).catch( dberror => {
  216. if ( dberror.message !== 'duplicate key value violates unique constraint "discord_guild_channel_key"' ) {
  217. console.log( '- Error while adding category settings to channels: ' + dberror );
  218. }
  219. } );
  220. } ));
  221. }, error => {
  222. console.log( '- Error while getting the channels in categories: ' + error );
  223. messages.push('Error while getting the channels in categories: ' + error);
  224. } );
  225. }
  226. }, dberror => {
  227. console.log( '- Error while deleting the channel categories: ' + dberror );
  228. messages.push('Error while deleting the channel categories: ' + dberror);
  229. return Promise.reject();
  230. } );
  231. } );
  232. }, dberror => {
  233. console.log( '- Error while getting the guild: ' + dberror );
  234. messages.push('Error while getting the guild: ' + dberror);
  235. return Promise.reject();
  236. } ).then( () => {
  237. return client.query( 'SELECT configid FROM verification WHERE guild = $1 ORDER BY configid ASC OFFSET $2', [guild, verificationLimit.default] ).then( ({rows}) => {
  238. if ( rows.length ) {
  239. return client.query( 'DELETE FROM verification WHERE guild = $1 AND configid IN (' + rows.map( (row, i) => '$' + ( i + 2 ) ).join(', ') + ')', [guild, ...rows.map( row => row.configid )] ).then( () => {
  240. console.log( '- Verifications successfully deleted.' );
  241. messages.push('Verifications successfully deleted.');
  242. }, dberror => {
  243. console.log( '- Error while deleting the verifications: ' + dberror );
  244. messages.push('Error while deleting the verifications: ' + dberror);
  245. } );
  246. }
  247. }, dberror => {
  248. console.log( '- Error while getting the verifications: ' + dberror );
  249. messages.push('Error while getting the verifications: ' + dberror);
  250. } );
  251. } ).then( () => {
  252. return client.query( 'SELECT webhook FROM rcgcdw WHERE guild = $1 ORDER BY configid ASC OFFSET $2', [guild, rcgcdwLimit.default] ).then( ({rows}) => {
  253. if ( rows.length ) {
  254. return client.query( 'DELETE FROM rcgcdw WHERE webhook IN (' + rows.map( (row, i) => '$' + ( i + 1 ) ).join(', ') + ')', rows.map( row => row.webhook ) ).then( () => {
  255. console.log( '- RcGcDw successfully deleted.' );
  256. messages.push('RcGcDw successfully deleted.');
  257. rows.forEach( row => msg.client.fetchWebhook(...row.webhook.split('/')).then( webhook => {
  258. webhook.delete('Removed extra recent changes webhook').catch(log_error);
  259. }, log_error ) );
  260. }, dberror => {
  261. console.log( '- Error while deleting the RcGcDw: ' + dberror );
  262. messages.push('Error while deleting the RcGcDw: ' + dberror);
  263. } );
  264. }
  265. }, dberror => {
  266. console.log( '- Error while getting the RcGcDw: ' + dberror );
  267. messages.push('Error while getting the RcGcDw: ' + dberror);
  268. } );
  269. } ).then( () => {
  270. return client.query( 'UPDATE rcgcdw SET display = $1 WHERE guild = $2 AND display > $1', [rcgcdwLimit.display, guild] ).then( () => {
  271. console.log( '- RcGcDw successfully updated.' );
  272. messages.push('RcGcDw successfully updated.');
  273. }, dberror => {
  274. console.log( '- Error while updating the RcGcDw: ' + dberror );
  275. messages.push('Error while updating the RcGcDw: ' + dberror);
  276. } );
  277. } ).then( () => {
  278. if ( !messages.length ) messages.push('No settings found that had to be removed.');
  279. return messages;
  280. }, error => {
  281. if ( error ) {
  282. console.log( '- Error while removing the patreon features: ' + error );
  283. messages.push('Error while removing the patreon features: ' + error);
  284. }
  285. if ( !messages.length ) messages.push('No settings found that had to be removed.');
  286. return messages;
  287. } ).finally( () => {
  288. client.release();
  289. } );
  290. }, dberror => {
  291. console.log( '- Error while connecting to the database client: ' + dberror );
  292. return 'Error while connecting to the database client: ' + dberror;
  293. } );
  294. }
  295. /**
  296. * Removes the settings for deleted guilds and channels.
  297. * @param {Discord.Message} msg - The Discord message.
  298. */
  299. function removeSettings(msg) {
  300. if ( !( msg instanceof Discord.Message ) ) return 'removeSettings(msg) – No message provided!';
  301. return db.connect().then( client => {
  302. var messages = [];
  303. return msg.client.shard.broadcastEval( discordClient => {
  304. return [
  305. [...discordClient.guilds.cache.keys()],
  306. discordClient.channels.cache.filter( channel => {
  307. return ( ( channel.isText() && channel.guildId ) || ( channel.type === 'GUILD_CATEGORY' && patreonGuildsPrefix.has(channel.guildId) ) );
  308. } ).map( channel => ( channel.type === 'GUILD_CATEGORY' ? '#' : '' ) + channel.id )
  309. ];
  310. } ).then( results => {
  311. var all_guilds = results.map( result => result[0] ).reduce( (acc, val) => acc.concat(val), [] );
  312. var all_channels = results.map( result => result[1] ).reduce( (acc, val) => acc.concat(val), [] );
  313. var guilds = [];
  314. var channels = [];
  315. return client.query( 'SELECT guild, channel FROM discord' ).then( ({rows}) => {
  316. return rows.forEach( row => {
  317. if ( !all_guilds.includes(row.guild) ) {
  318. if ( !row.channel ) {
  319. if ( patreonGuildsPrefix.has(row.guild) || voiceGuildsLang.has(row.guild) ) {
  320. msg.client.shard.broadcastEval( (discordClient, evalData) => {
  321. patreonGuildsPrefix.delete(evalData);
  322. voiceGuildsLang.delete(evalData);
  323. }, {context: row.guild} );
  324. }
  325. return guilds.push(row.guild);
  326. }
  327. }
  328. else if ( row.channel && !all_channels.includes(row.channel) ) {
  329. return channels.push(row.channel);
  330. }
  331. } );
  332. }, dberror => {
  333. console.log( '- Error while getting the settings: ' + dberror );
  334. messages.push('Error while getting the settings: ' + dberror);
  335. } ).then( () => {
  336. if ( guilds.length ) {
  337. return client.query( 'DELETE FROM discord WHERE main IN (' + guilds.map( (guild, i) => '$' + ( i + 1 ) ).join(', ') + ')', guilds ).then( ({rowCount}) => {
  338. console.log( '- Guilds successfully removed: ' + rowCount );
  339. messages.push('Guilds successfully removed: ' + rowCount);
  340. }, dberror => {
  341. console.log( '- Error while removing the guilds: ' + dberror );
  342. messages.push('Error while removing the guilds: ' + dberror);
  343. } );
  344. }
  345. } ).then( () => {
  346. if ( channels.length ) {
  347. return client.query( 'DELETE FROM discord WHERE channel IN (' + channels.map( (channel, i) => '$' + ( i + 1 ) ).join(', ') + ')', channels ).then( ({rowCount}) => {
  348. console.log( '- Channels successfully removed: ' + rowCount );
  349. messages.push('Channels successfully removed: ' + rowCount);
  350. }, dberror => {
  351. console.log( '- Error while removing the channels: ' + dberror );
  352. messages.push('Error while removing the channels: ' + dberror);
  353. } );
  354. }
  355. } );
  356. } ).then( () => {
  357. if ( !messages.length ) messages.push('No settings found that had to be removed.');
  358. return messages;
  359. }, error => {
  360. if ( error ) {
  361. console.log( '- Error while removing the settings: ' + error );
  362. messages.push('Error while removing the settings: ' + error);
  363. }
  364. if ( !messages.length ) messages.push('No settings found that had to be removed.');
  365. return messages;
  366. } ).finally( () => {
  367. client.release();
  368. } );
  369. }, dberror => {
  370. console.log( '- Error while connecting to the database client: ' + dberror );
  371. return 'Error while connecting to the database client: ' + dberror;
  372. } );
  373. }
  374. export default {
  375. name: 'eval',
  376. everyone: false,
  377. pause: false,
  378. owner: true,
  379. run: cmd_eval
  380. };