util.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. import gotDefault from 'got';
  2. import pg from 'pg';
  3. import DiscordOauth2 from 'discord-oauth2';
  4. import {oauthSites} from '../util/wiki.js';
  5. import {createRequire} from 'module';
  6. const require = createRequire(import.meta.url);
  7. const slashCommands = require('../interactions/commands.json');
  8. globalThis.isDebug = ( process.argv[2] === 'debug' );
  9. const got = gotDefault.extend( {
  10. throwHttpErrors: false,
  11. timeout: {
  12. request: 5000
  13. },
  14. headers: {
  15. 'User-Agent': 'Wiki-Bot/' + ( isDebug ? 'testing' : process.env.npm_package_version ) + '/dashboard (Discord; ' + process.env.npm_package_name + ( process.env.invite ? '; ' + process.env.invite : '' ) + ')'
  16. },
  17. responseType: 'json'
  18. } );
  19. const db = new pg.Pool();
  20. db.on( 'error', dberror => {
  21. console.log( '- Dashboard: Error while connecting to the database: ' + dberror );
  22. } );
  23. const oauth = new DiscordOauth2( {
  24. clientId: process.env.bot,
  25. clientSecret: process.env.secret,
  26. redirectUri: process.env.dashboard
  27. } );
  28. const enabledOAuth2 = [
  29. ...oauthSites.filter( oauthSite => {
  30. let site = new URL(oauthSite);
  31. site = site.hostname + site.pathname.slice(0, -1);
  32. return ( process.env[`oauth_${site}`] && process.env[`oauth_${site}_secret`] );
  33. } ).map( oauthSite => {
  34. let site = new URL(oauthSite);
  35. return {
  36. id: site.hostname + site.pathname.slice(0, -1),
  37. name: oauthSite, url: oauthSite,
  38. };
  39. } )
  40. ];
  41. if ( process.env.oauth_miraheze && process.env.oauth_miraheze_secret ) {
  42. enabledOAuth2.unshift({
  43. id: 'miraheze',
  44. name: 'Miraheze',
  45. url: 'https://meta.miraheze.org/w/',
  46. });
  47. }
  48. if ( process.env.oauth_wikimedia && process.env.oauth_wikimedia_secret ) {
  49. enabledOAuth2.unshift({
  50. id: 'wikimedia',
  51. name: 'Wikimedia (Wikipedia)',
  52. url: 'https://meta.wikimedia.org/w/',
  53. });
  54. }
  55. got.get( `https://discord.com/api/v8/applications/${process.env.bot}/commands`, {
  56. headers: {
  57. Authorization: `Bot ${process.env.token}`
  58. },
  59. timeout: {
  60. request: 10000
  61. }
  62. } ).then( response=> {
  63. if ( response.statusCode !== 200 || !response.body ) {
  64. console.log( '- Dashboard: ' + response.statusCode + ': Error while getting the global slash commands: ' + response.body?.message );
  65. return;
  66. }
  67. console.log( '- Dashboard: Slash commands successfully loaded.' );
  68. response.body.forEach( command => {
  69. var slashCommand = slashCommands.find( slashCommand => slashCommand.name === command.name );
  70. if ( slashCommand ) {
  71. slashCommand.id = command.id;
  72. slashCommand.application_id = command.application_id;
  73. }
  74. else slashCommands.push(slashCommand);
  75. } );
  76. }, error => {
  77. console.log( '- Dashboard: Error while getting the global slash commands: ' + error );
  78. } );
  79. /**
  80. * @typedef UserSession
  81. * @property {String} state
  82. * @property {String} access_token
  83. * @property {String} user_id
  84. */
  85. /**
  86. * @typedef Settings
  87. * @property {User} user
  88. * @property {Object} guilds
  89. * @property {Number} guilds.count
  90. * @property {Map<String, Guild>} guilds.isMember
  91. * @property {Map<String, Guild>} guilds.notMember
  92. */
  93. /**
  94. * @typedef User
  95. * @property {String} id
  96. * @property {String} username
  97. * @property {String} discriminator
  98. * @property {String} avatar
  99. * @property {String} locale
  100. */
  101. /**
  102. * @typedef Guild
  103. * @property {String} id
  104. * @property {String} name
  105. * @property {String} acronym
  106. * @property {String} [icon]
  107. * @property {String} userPermissions
  108. * @property {Boolean} [patreon]
  109. * @property {Number} [memberCount]
  110. * @property {String} [botPermissions]
  111. * @property {Channel[]} [channels]
  112. * @property {Role[]} [roles]
  113. * @property {String} [locale]
  114. */
  115. /**
  116. * @typedef Channel
  117. * @property {String} id
  118. * @property {String} name
  119. * @property {Boolean} isCategory
  120. * @property {Number} userPermissions
  121. * @property {Number} botPermissions
  122. */
  123. /**
  124. * @typedef Role
  125. * @property {String} id
  126. * @property {String} name
  127. * @property {Boolean} lower
  128. */
  129. /**
  130. * @type {Map<String, UserSession>}
  131. */
  132. const sessionData = new Map();
  133. /**
  134. * @type {Map<String, Settings>}
  135. */
  136. const settingsData = new Map();
  137. /**
  138. * @type {Map<String, String>}
  139. */
  140. const oauthVerify = new Map();
  141. /**
  142. * @type {Map<Number, PromiseConstructor>}
  143. */
  144. const messages = new Map();
  145. var messageId = 1;
  146. process.on( 'message', message => {
  147. if ( message?.id === 'verifyUser' ) return oauthVerify.set(message.state, message.user);
  148. if ( message?.id ) {
  149. if ( message.data.error ) messages.get(message.id).reject(message.data.error);
  150. else messages.get(message.id).resolve(message.data.response);
  151. return messages.delete(message.id);
  152. }
  153. if ( message === 'toggleDebug' ) isDebug = !isDebug;
  154. console.log( '- [Dashboard]: Message received!', message );
  155. } );
  156. /**
  157. * Send messages to the manager.
  158. * @param {Object} [message] - The message.
  159. * @returns {Promise<Object>}
  160. */
  161. function sendMsg(message) {
  162. var id = messageId++;
  163. var promise = new Promise( (resolve, reject) => {
  164. messages.set(id, {resolve, reject});
  165. process.send( {id, data: message} );
  166. } );
  167. return promise;
  168. }
  169. var botLists = [];
  170. if ( process.env.botlist ) {
  171. let supportedLists = {
  172. 'blist.xyz': {
  173. link: 'https://blist.xyz/bot/' + process.env.bot,
  174. widget: 'https://blist.xyz/api/v2/bot/' + process.env.bot + '/widget'
  175. },
  176. 'botlists.com': {
  177. link: 'https://botlists.com/bot/' + process.env.bot,
  178. widget: 'https://botlists.com/bot/' + process.env.bot + '/widget'
  179. },
  180. 'bots.ondiscord.xyz': {
  181. link: 'https://bots.ondiscord.xyz/bots/' + process.env.bot,
  182. widget: 'https://bots.ondiscord.xyz/bots/' + process.env.bot + '/embed?theme=dark&showGuilds=true'
  183. },
  184. 'discord.boats': {
  185. link: 'https://discord.boats/bot/' + process.env.bot,
  186. widget: 'https://discord.boats/api/widget/' + process.env.bot
  187. },
  188. 'discords.com': {
  189. link: 'https://discords.com/bots/bot/' + process.env.bot,
  190. widget: 'https://discords.com/bots/api/bot/' + process.env.bot + '/widget?theme=dark'
  191. },
  192. 'infinitybotlist.com': {
  193. link: 'https://infinitybotlist.com/bots/' + process.env.bot,
  194. widget: 'https://infinitybotlist.com/bots/' + process.env.bot + '/widget?size=medium'
  195. },
  196. 'top.gg': {
  197. link: 'https://top.gg/bot/' + process.env.bot,
  198. widget: 'https://top.gg/api/widget/' + process.env.bot + '.svg'
  199. },
  200. 'voidbots.net': {
  201. link: 'https://voidbots.net/bot/' + process.env.bot,
  202. widget: 'https://voidbots.net/api/embed/' + process.env.bot + '?theme=dark'
  203. }
  204. };
  205. botLists = Object.keys(JSON.parse(process.env.botlist)).filter( botList => {
  206. return supportedLists.hasOwnProperty(botList);
  207. } ).map( botList => {
  208. return `<a href="${supportedLists[botList].link}" target="_blank">
  209. <img src="${supportedLists[botList].widget}" alt="${botList}" height="150px" loading="lazy" />
  210. </a>`;
  211. } );
  212. }
  213. /**
  214. * Add bot list widgets.
  215. * @param {import('cheerio').default} $ - The cheerio static
  216. * @param {import('./i18n.js').default} dashboardLang - The user language
  217. * @returns {import('cheerio').default}
  218. */
  219. function addWidgets($, dashboardLang) {
  220. if ( !botLists.length ) return;
  221. return $('<div class="widgets">').append(
  222. $('<h3 id="bot-lists">').text(dashboardLang.get('general.botlist.title')),
  223. $('<p>').text(dashboardLang.get('general.botlist.text')),
  224. ...botLists
  225. ).appendTo('#text');
  226. }
  227. /**
  228. * Create a red notice
  229. * @param {import('cheerio').default} $ - The cheerio static
  230. * @param {String} notice - The notice to create
  231. * @param {import('./i18n.js').default} dashboardLang - The user language
  232. * @param {String[]} [args] - The arguments for the notice
  233. * @returns {import('cheerio').default}
  234. */
  235. function createNotice($, notice, dashboardLang, args = []) {
  236. if ( !notice ) return;
  237. var type = 'info';
  238. var title = $('<b>');
  239. var text = $('<div>');
  240. var note;
  241. switch (notice) {
  242. case 'unauthorized':
  243. type = 'info';
  244. title.text(dashboardLang.get('notice.unauthorized.title'));
  245. text.text(dashboardLang.get('notice.unauthorized.text'));
  246. break;
  247. case 'save':
  248. type = 'success';
  249. title.text(dashboardLang.get('notice.save.title'));
  250. text.text(dashboardLang.get('notice.save.text'));
  251. break;
  252. case 'nosettings':
  253. type = 'info';
  254. title.text(dashboardLang.get('notice.nosettings.title'));
  255. text.text(dashboardLang.get('notice.nosettings.text'));
  256. if ( args[0] ) note = $('<a>').text(dashboardLang.get('notice.nosettings.note')).attr('href', `/guild/${args[0]}/settings`);
  257. break;
  258. case 'logout':
  259. type = 'success';
  260. title.text(dashboardLang.get('notice.logout.title'));
  261. text.text(dashboardLang.get('notice.logout.text'));
  262. break;
  263. case 'refresh':
  264. type = 'success';
  265. title.text(dashboardLang.get('notice.refresh.title'));
  266. text.text(dashboardLang.get('notice.refresh.text'));
  267. break;
  268. case 'missingperm':
  269. type = 'error';
  270. title.text(dashboardLang.get('notice.missingperm.title'));
  271. text.html(dashboardLang.get('notice.missingperm.text', true, $('<code>').text(args[0])));
  272. break;
  273. case 'loginfail':
  274. type = 'error';
  275. title.text(dashboardLang.get('notice.loginfail.title'));
  276. text.text(dashboardLang.get('notice.loginfail.text'));
  277. break;
  278. case 'sysmessage':
  279. type = 'info';
  280. title.text(dashboardLang.get('notice.sysmessage.title'));
  281. text.html(dashboardLang.get('notice.sysmessage.text', true, $('<a target="_blank">').append(
  282. $('<code>').text('MediaWiki:Custom-RcGcDw')
  283. ).attr('href', args[1]), $('<code class="user-select">').text(args[0])));
  284. note = $('<a target="_blank">').text(args[1]).attr('href', args[1]);
  285. break;
  286. case 'mwversion':
  287. type = 'error';
  288. title.text(dashboardLang.get('notice.mwversion.title'));
  289. text.text(dashboardLang.get('notice.mwversion.text', false, args[0], args[1]));
  290. note = $('<a target="_blank">').text('https://www.mediawiki.org/wiki/MediaWiki_1.30').attr('href', 'https://www.mediawiki.org/wiki/MediaWiki_1.30');
  291. break;
  292. case 'oauth':
  293. type = 'success';
  294. title.text(dashboardLang.get('notice.oauth.title'));
  295. text.text(dashboardLang.get('notice.oauth.text'));
  296. break;
  297. case 'oauthfail':
  298. type = 'error';
  299. title.text(dashboardLang.get('notice.oauthfail.title'));
  300. text.text(dashboardLang.get('notice.oauthfail.text'));
  301. break;
  302. case 'oauthverify':
  303. type = 'success';
  304. title.text(dashboardLang.get('notice.oauthverify.title'));
  305. text.text(dashboardLang.get('notice.oauthverify.text'));
  306. break;
  307. case 'oauthother':
  308. type = 'info';
  309. title.text(dashboardLang.get('notice.oauthother.title'));
  310. text.text(dashboardLang.get('notice.oauthother.text'));
  311. note = $('<a>').text(dashboardLang.get('notice.oauthother.note')).attr('href', args[0]);
  312. break;
  313. case 'oauthlogin':
  314. type = 'info';
  315. title.text(dashboardLang.get('notice.oauthlogin.title'));
  316. text.text(dashboardLang.get('notice.oauthlogin.text'));
  317. break;
  318. case 'nochange':
  319. type = 'info';
  320. title.text(dashboardLang.get('notice.nochange.title'));
  321. text.text(dashboardLang.get('notice.nochange.text'));
  322. break;
  323. case 'invalidusergroup':
  324. type = 'error';
  325. title.text(dashboardLang.get('notice.invalidusergroup.title'));
  326. text.text(dashboardLang.get('notice.invalidusergroup.text'));
  327. break;
  328. case 'noverify':
  329. type = 'info';
  330. title.text(dashboardLang.get('notice.noverify.title'));
  331. text.html(dashboardLang.get('notice.noverify.text', true, $('<code>').text('/verify')));
  332. break;
  333. case 'noslash':
  334. type = 'error';
  335. title.text(dashboardLang.get('notice.noslash.title'));
  336. text.text(dashboardLang.get('notice.noslash.text'));
  337. note = $('<a target="_blank">').text(dashboardLang.get('notice.noslash.note')).attr('href', `https://discord.com/api/oauth2/authorize?client_id=${process.env.bot}&scope=applications.commands&guild_id=${args[0]}&disable_guild_select=true`);
  338. break;
  339. case 'wikiblocked':
  340. type = 'error';
  341. title.text(dashboardLang.get('notice.wikiblocked.title'));
  342. text.text(dashboardLang.get('notice.wikiblocked.text', false, args[0]));
  343. if ( args[1] ) note = $('<div>').append(
  344. dashboardLang.get('notice.wikiblocked.note', true) + ' ',
  345. $('<code>').text(args[1])
  346. );
  347. break;
  348. case 'savefail':
  349. type = 'error';
  350. title.text(dashboardLang.get('notice.savefail.title'));
  351. text.text(dashboardLang.get('notice.savefail.text'));
  352. if ( typeof args[0] === 'string' ) {
  353. note = $('<div>').text(dashboardLang.get('notice.savefail.note_' + args[0]));
  354. }
  355. break;
  356. case 'webhookfail':
  357. type = 'info';
  358. title.text(dashboardLang.get('notice.webhookfail.title'));
  359. text.text(dashboardLang.get('notice.webhookfail.text'));
  360. note = $('<div>').text(dashboardLang.get('notice.webhookfail.note'));
  361. break;
  362. case 'refreshfail':
  363. type = 'error';
  364. title.text(dashboardLang.get('notice.refreshfail.title'));
  365. text.text(dashboardLang.get('notice.refreshfail.text'));
  366. break;
  367. case 'error':
  368. type = 'error';
  369. title.text(dashboardLang.get('notice.error.title'));
  370. text.text(dashboardLang.get('notice.error.text'));
  371. break;
  372. case 'readonly':
  373. type = 'info';
  374. title.text(dashboardLang.get('notice.readonly.title'));
  375. text.text(dashboardLang.get('notice.readonly.text'));
  376. break;
  377. default:
  378. return;
  379. }
  380. return $(`<div class="notice notice-${type}">`).append(
  381. title,
  382. text,
  383. note
  384. ).appendTo('#text #notices');
  385. }
  386. /**
  387. * HTML escape text
  388. * @param {String} text - The text to escape
  389. * @returns {String}
  390. */
  391. function escapeText(text) {
  392. return text.replace( /&/g, '&amp;' ).replace( /</g, '&lt;' ).replace( />/g, '&gt;' );
  393. }
  394. const permissions = {
  395. ADMINISTRATOR: 1n << 3n,
  396. MANAGE_CHANNELS: 1n << 4n,
  397. MANAGE_GUILD: 1n << 5n,
  398. ADD_REACTIONS: 1n << 6n,
  399. VIEW_CHANNEL: 1n << 10n,
  400. SEND_MESSAGES: 1n << 11n,
  401. MANAGE_MESSAGES: 1n << 13n,
  402. EMBED_LINKS: 1n << 14n,
  403. ATTACH_FILES: 1n << 15n,
  404. READ_MESSAGE_HISTORY: 1n << 16n,
  405. MENTION_EVERYONE: 1n << 17n,
  406. USE_EXTERNAL_EMOJIS: 1n << 18n,
  407. MANAGE_NICKNAMES: 1n << 27n,
  408. MANAGE_ROLES: 1n << 28n,
  409. MANAGE_WEBHOOKS: 1n << 29n,
  410. SEND_MESSAGES_IN_THREADS: 1n << 38n
  411. }
  412. /**
  413. * Check if a permission is included in the BitField
  414. * @param {String|Number|BigInt} all - BitField of multiple permissions
  415. * @param {String[]} permission - Name of the permission to check for
  416. * @returns {Boolean}
  417. */
  418. function hasPerm(all = 0n, ...permission) {
  419. all = BigInt(all);
  420. if ( (all & permissions.ADMINISTRATOR) === permissions.ADMINISTRATOR ) return true;
  421. return permission.every( perm => {
  422. let bit = permissions[perm];
  423. return ( (all & bit) === bit );
  424. } );
  425. }
  426. export {
  427. got,
  428. db,
  429. oauth,
  430. enabledOAuth2,
  431. slashCommands,
  432. sessionData,
  433. settingsData,
  434. oauthVerify,
  435. sendMsg,
  436. addWidgets,
  437. createNotice,
  438. escapeText,
  439. hasPerm
  440. };