util.js 12 KB

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