util.js 12 KB

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