util.js 11 KB

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