Pārlūkot izejas kodu

Add slash command restrictions

Markus-Rost 4 gadi atpakaļ
vecāks
revīzija
64981160a0

+ 3 - 20
bot.js

@@ -20,7 +20,7 @@ global.got = require('got').extend( {
 const Lang = require('./util/i18n.js');
 const Wiki = require('./util/wiki.js');
 const newMessage = require('./util/newMessage.js');
-const {allowDelete} = require('./util/functions.js');
+const {slashCommands, allowDelete} = require('./util/functions.js');
 global.patreons = {};
 global.voice = {};
 const db = require('./util/database.js');
@@ -63,7 +63,6 @@ const client = new Discord.Client( {
 
 client.api.applications(process.env.bot).commands.get().then( response => {
 	console.log( '- ' + shardId + ': Slash commands successfully loaded.' );
-	const slashCommands = require('./interactions/commands.json');
 	response.forEach( command => {
 		var slashCommand = slashCommands.find( slashCommand => slashCommand.name === command.name );
 		if ( slashCommand ) {
@@ -241,24 +240,8 @@ client.ws.on( 'INTERACTION_CREATE', interaction => {
 	}
 	interaction.user = interaction.member.user;
 	interaction.member.permissions = new Discord.Permissions(+interaction.member.permissions);
-	db.query( 'SELECT wiki, lang, role FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', [interaction.guild_id, interaction.channel_id, '#' + channel?.parentID] ).then( ({rows:[row]}) => {
-		var lang = new Lang(( row?.lang || channel?.guild?.preferredLocale ));
-		if ( row?.role && !interaction.member.roles.includes( row.role ) && !interaction.member.permissions.has('MANAGE_GUILD') && channel?.guild?.roles.cache.has(row.role) && ( !interaction.member.roles.length || !interaction.member.roles.some( role => channel.guild.roles.cache.get(role)?.comparePositionTo(row.role) >= 0 ) ) ) {
-			return client.api.interactions(interaction.id, interaction.token).callback.post( {
-				data: {
-					type: 4,
-					data: {
-						content: lang.get('interaction.missingrole', '<@&' + row.role + '>'),
-						allowed_mentions: {
-							parse: []
-						},
-						flags: 64
-					}
-				}
-			} ).catch(log_error);
-		}
-		var wiki = new Wiki(row?.wiki);
-		return slash[interaction.data.name](interaction, lang, wiki, channel);
+	db.query( 'SELECT wiki, lang FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', [interaction.guild_id, interaction.channel_id, '#' + channel?.parentID] ).then( ({rows:[row]}) => {
+		return slash[interaction.data.name](interaction, new Lang(( row?.lang || channel?.guild?.preferredLocale )), new Wiki(row?.wiki), channel);
 	}, dberror => {
 		console.log( '- Slash: Error while getting the wiki: ' + dberror );
 		return client.api.interactions(interaction.id, interaction.token).callback.post( {

+ 1 - 1
cmds/verification.js

@@ -1,7 +1,7 @@
 const help_setup = require('../functions/helpsetup.js');
 const {limit: {verification: verificationLimit}} = require('../util/default.json');
 var db = require('../util/database.js');
-const slashCommand = require('../interactions/commands.json').find( slashCommand => slashCommand.name === 'verify' );
+const slashCommand = require('../util/functions.js').slashCommands.find( slashCommand => slashCommand.name === 'verify' );
 
 /**
  * Processes the "verification" command.

+ 8 - 2
dashboard/guilds.js

@@ -7,7 +7,8 @@ const {settingsData, addWidgets, createNotice} = require('./util.js');
 const forms = {
 	settings: require('./settings.js').get,
 	verification: require('./verification.js').get,
-	rcscript: require('./rcscript.js').get
+	rcscript: require('./rcscript.js').get,
+	slash: require('./slash.js').get
 };
 
 const DiscordOauth2 = require('discord-oauth2');
@@ -30,7 +31,7 @@ const file = require('fs').readFileSync('./dashboard/index.html');
  * @param {String[]} [actionArgs] - The arguments for the action
  */
 function dashboard_guilds(res, dashboardLang, theme, state, reqURL, action, actionArgs) {
-	reqURL.pathname = reqURL.pathname.replace( /^(\/(?:guild\/\d+(?:\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?)?)?)(?:\/.*)?$/, '$1' );
+	reqURL.pathname = reqURL.pathname.replace( /^(\/(?:guild\/\d+(?:\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new))?)?)?)(?:\/.*)?$/, '$1' );
 	var args = reqURL.pathname.split('/');
 	args = reqURL.pathname.split('/');
 	var settings = settingsData.get(state);
@@ -50,6 +51,7 @@ function dashboard_guilds(res, dashboardLang, theme, state, reqURL, action, acti
 	$('.channel#settings div').text(dashboardLang.get('general.settings'));
 	$('.channel#verification div').text(dashboardLang.get('general.verification'));
 	$('.channel#rcscript div').text(dashboardLang.get('general.rcscript'));
+	$('.channel#slash div').text(dashboardLang.get('general.slash'));
 	$('.guild#invite a').attr('alt', dashboardLang.get('general.invite'));
 	$('.guild#refresh a').attr('alt', dashboardLang.get('general.refresh'));
 	$('.guild#theme-dark a').attr('alt', dashboardLang.get('general.theme-dark'));
@@ -112,9 +114,11 @@ function dashboard_guilds(res, dashboardLang, theme, state, reqURL, action, acti
 		$('.channel#settings').attr('href', `/guild/${guild.id}/settings`);
 		$('.channel#verification').attr('href', `/guild/${guild.id}/verification`);
 		$('.channel#rcscript').attr('href', `/guild/${guild.id}/rcscript`);
+		$('.channel#slash').attr('href', `/guild/${guild.id}/slash`);
 		if ( args[3] === 'settings' ) return forms.settings(res, $, guild, args, dashboardLang);
 		if ( args[3] === 'verification' ) return forms.verification(res, $, guild, args, dashboardLang);
 		if ( args[3] === 'rcscript' ) return forms.rcscript(res, $, guild, args, dashboardLang);
+		if ( args[3] === 'slash' ) return forms.slash(res, $, guild, args, dashboardLang);
 		return forms.settings(res, $, guild, args, dashboardLang);
 	}
 	else if ( settings.guilds.notMember.has(id) ) {
@@ -154,9 +158,11 @@ function dashboard_guilds(res, dashboardLang, theme, state, reqURL, action, acti
 		$('.channel#settings').attr('href', `/guild/${guild.id}/settings?owner=true`);
 		$('.channel#verification').attr('href', `/guild/${guild.id}/verification?owner=true`);
 		$('.channel#rcscript').attr('href', `/guild/${guild.id}/rcscript?owner=true`);
+		$('.channel#slash').attr('href', `/guild/${guild.id}/slash?owner=true`);
 		if ( args[3] === 'settings' ) return forms.settings(res, $, guild, args, dashboardLang);
 		if ( args[3] === 'verification' ) return forms.verification(res, $, guild, args, dashboardLang);
 		if ( args[3] === 'rcscript' ) return forms.rcscript(res, $, guild, args, dashboardLang);
+		if ( args[3] === 'slash' ) return forms.slash(res, $, guild, args, dashboardLang);
 		return forms.settings(res, $, guild, args, dashboardLang);
 	}
 	else {

+ 17 - 1
dashboard/i18n/en.json

@@ -21,6 +21,7 @@
         "save": "Save",
         "selector": "Server Selector",
         "settings": "Settings",
+        "slash": "Slash Commands",
         "support": "Support Server",
         "theme-dark": "Use dark theme",
         "theme-light": "Use light theme",
@@ -134,7 +135,7 @@
     },
     "rcscript": {
         "desc": "These are the recent changes webhooks for $1:",
-        "explanation": "<h2>Recent Changes Webhook</h2>\n<p>Wiki-Bot is able to run a recent changes webhook based on <a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a>. The recent changes can be displayed in compact text messages with inline links or embed messages with edit tags and category changes.</p>\n<p>Requirements to add a recent changes webhook:</p>\n<ul>\n<li>The wiki needs to run on <a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a> or higher.</li>\n<li>The system message <code>MediaWiki:Custom-RcGcDw</code> needs to be set to the Discord server id <code class=\"user-select\" id=\"server-id\"></code>.</li>\n</ul>",
+        "explanation": "<h2>Recent Changes Webhook</h2>\n<p>Wiki-Bot is able to run a recent changes webhook based on <a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a>. The recent changes can be displayed in compact text messages with inline links or embed messages with edit tags and category changes.</p>\n<p>Requirements to add a recent changes webhook:</p>\n<ul>\n<li>The wiki needs to run on <a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a> or higher.</li>\n<li>The system message <code class=\"user-select\">MediaWiki:Custom-RcGcDw</code> needs to be set to the Discord server id <code class=\"user-select\" id=\"server-id\"></code>.</li>\n</ul>",
         "form": {
             "channel": "Channel:",
             "confirm": "Do you really want to delete the recent changes webhook?",
@@ -183,6 +184,21 @@
         },
         "new": "New channel overwrite"
     },
+    "slash": {
+        "desc": "These are the slash commands for $1:",
+        "explanation": "<h2>Slash Commands</h2>\n<p>The usage of specific slash commands can be restricted per role or user. You can do that for the slash commands of Wiki-Bot here.</p>",
+        "form": {
+            "add": "Add",
+            "allow": "Allow",
+            "default_allow": "By default this command is allowed to be used by everyone.",
+            "default_deny": "By default this command is not allowed to be used by anyone.",
+            "deny": "Deny",
+            "entry": "Command $1",
+            "default": "Default",
+            "role": "Role:",
+            "select_role": "-- Select a Role --"
+        }
+    },
     "verification": {
         "desc": "These are the verifications for $1:",
         "explanation": "<h2>User Verification</h2>\n<p>Using the <code class=\"prefix\">verify &lt;wiki username&gt;</code> command, users are able to verify themselves as a specific wiki user by using the Discord field on their wiki profile. If the user matches and user verifications are set up on the server, Wiki-Bot will give them the roles for all verification entries they matched.</p>\n<p>Every verification entry allows for multiple restrictions on when a user should match the verification:</p>\n<ul>\n<li>Channel to use the <code class=\"prefix\">verify</code> command in.</li>\n<li>Role to get when matching the verification entry.</li>\n<li>Required edit count on the wiki to match the verification entry.</li>\n<li>Required user group to be a member of on the wiki to match the verification entry.</li>\n<li>Required account age in days to match the verification entry.</li>\n<li>Whether the Discord users nickname should be set to their wiki username when they match the verification entry.</li>\n</ul>",

+ 4 - 0
dashboard/index.html

@@ -33,6 +33,10 @@
 				<img src="/src/settings.svg" alt="Settings">
 				<div>Recent Changes</div>
 			</a>
+			<a class="channel channel-header" id="slash">
+				<img src="/src/settings.svg" alt="Settings">
+				<div>Slash Commands</div>
+			</a>
 		</div>
 		<div id="navbar">
 			<a id="selector" href="/" style="width: 230px;">

+ 8 - 7
dashboard/index.js

@@ -10,7 +10,8 @@ global.isDebug = ( process.argv[2] === 'debug' );
 const posts = {
 	settings: require('./settings.js').post,
 	verification: require('./verification.js').post,
-	rcscript: require('./rcscript.js').post
+	rcscript: require('./rcscript.js').post,
+	slash: require('./slash.js').post
 };
 
 const fs = require('fs');
@@ -57,7 +58,7 @@ const server = http.createServer( (req, res) => {
 			return cookie.split('=')[0] === 'wikibot' && /^"(\w*(?:-\d+)*)"$/.test(( cookie.split('=')[1] || '' ));
 		} )?.map( cookie => cookie.replace( /^wikibot="(\w*(?:-\d+)*)"$/, '$1' ) )?.join();
 
-		if ( args.length === 5 && ['settings', 'verification', 'rcscript'].includes( args[3] )
+		if ( args.length === 5 && ['settings', 'verification', 'rcscript', 'slash'].includes( args[3] )
 		&& /^(?:default|new|\d+)$/.test(args[4]) && settingsData.has(state)
 		&& settingsData.get(state).guilds.isMember.has(args[2]) ) {
 			if ( process.env.READONLY ) return save_response(`${req.url}?save=failed`);
@@ -154,8 +155,8 @@ const server = http.createServer( (req, res) => {
 	res.setHeader('Content-Language', [dashboardLang.lang]);
 
 	var lastGuild = req.headers?.cookie?.split('; ')?.filter( cookie => {
-		return cookie.split('=')[0] === 'guild' && /^"\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?"$/.test(( cookie.split('=')[1] || '' ));
-	} )?.map( cookie => cookie.replace( /^guild="(\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?)"$/, '$1' ) )?.join();
+		return cookie.split('=')[0] === 'guild' && /^"\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new))?"$/.test(( cookie.split('=')[1] || '' ));
+	} )?.map( cookie => cookie.replace( /^guild="(\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new))?)"$/, '$1' ) )?.join();
 	if ( lastGuild ) res.setHeader('Set-Cookie', ['guild=""; HttpOnly; Path=/; Max-Age=0']);
 
 	var state = req.headers.cookie?.split('; ')?.filter( cookie => {
@@ -180,7 +181,7 @@ const server = http.createServer( (req, res) => {
 	if ( !state ) {
 		if ( reqURL.pathname.startsWith( '/guild/' ) ) {
 			let pathGuild = reqURL.pathname.split('/').slice(2, 5).join('/');
-			if ( /^\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?$/.test(pathGuild) ) {
+			if ( /^\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new))?$/.test(pathGuild) ) {
 				res.setHeader('Set-Cookie', [`guild="${pathGuild}"; HttpOnly; Path=/`]);
 			}
 		}
@@ -194,7 +195,7 @@ const server = http.createServer( (req, res) => {
 	if ( !settingsData.has(state) ) {
 		if ( reqURL.pathname.startsWith( '/guild/' ) ) {
 			let pathGuild = reqURL.pathname.split('/').slice(2, 5).join('/');
-			if ( /^\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?$/.test(pathGuild) ) {
+			if ( /^\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new))?$/.test(pathGuild) ) {
 				res.setHeader('Set-Cookie', [`guild="${pathGuild}"; HttpOnly; Path=/`]);
 			}
 		}
@@ -203,7 +204,7 @@ const server = http.createServer( (req, res) => {
 
 	if ( reqURL.pathname === '/refresh' ) {
 		let returnLocation = reqURL.searchParams.get('return');
-		if ( !/^\/guild\/\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?$/.test(returnLocation) ) {
+		if ( !/^\/guild\/\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new))?$/.test(returnLocation) ) {
 			returnLocation = '/';
 		}
 		return pages.refresh(res, state, returnLocation);

+ 1 - 1
dashboard/oauth.js

@@ -155,7 +155,7 @@ function dashboard_oauth(res, state, searchParams, lastGuild) {
 					lastGuild = searchParams.get('guild_id') + '/settings';
 				}
 				res.writeHead(302, {
-					Location: ( lastGuild && /^\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?$/.test(lastGuild) ? `/guild/${lastGuild}` : '/' ),
+					Location: ( lastGuild && /^\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new))?$/.test(lastGuild) ? `/guild/${lastGuild}` : '/' ),
 					'Set-Cookie': [`wikibot="${settings.state}"; HttpOnly; Path=/`]
 				});
 				return res.end();

+ 2 - 1
dashboard/settings.js

@@ -126,7 +126,7 @@ function createForm($, header, dashboardLang, settings, guildRoles, guildChannel
 			...guildRoles.map( guildRole => {
 				return $(`<option id="wb-settings-role-${guildRole.id}">`).val(guildRole.id).text(`${guildRole.id} – @${guildRole.name}`)
 			} ),
-			$(`<option id="wb-settings-role-everyone">`).val('').text(`@everyone`)
+			$(`<option id="wb-settings-role-everyone">`).val('').text('@everyone')
 		);
 		if ( settings.role ) role.find(`#wb-settings-role-${settings.role}`).attr('selected', '');
 		else role.find(`#wb-settings-role-everyone`).attr('selected', '');
@@ -174,6 +174,7 @@ function dashboard_settings(res, $, guild, args, dashboardLang) {
 	db.query( 'SELECT channel, wiki, lang, role, inline, prefix, patreon FROM discord WHERE guild = $1 ORDER BY channel DESC NULLS LAST', [guild.id] ).then( ({rows}) => {
 		$('<p>').html(dashboardLang.get('settings.desc', true, $('<code>').text(guild.name))).appendTo('#text .description');
 		if ( !rows.length ) {
+			createNotice($, 'nosettings', dashboardLang);
 			$('.channel#settings').addClass('selected');
 			createForm($, dashboardLang.get('settings.form.default'), dashboardLang, {
 				prefix: process.env.prefix,

+ 272 - 0
dashboard/slash.js

@@ -0,0 +1,272 @@
+const Lang = require('../util/i18n.js');
+const {got, db, slashCommands, sendMsg, createNotice, escapeText, hasPerm} = require('./util.js');
+
+const fieldset = {
+	role: '<label for="wb-settings-addrole">Role:</label>'
+	+ '<select id="wb-settings-addrole" name="role"></select>'
+	+ '<button type="button" id="wb-settings-addrole-add">Add</button>'
+	+ '<hr>',
+	permission: '<span title="@UNKNOWN">@UNKNOWN:</span>'
+	+ '<div class="wb-settings-permission">'
+	+ '<input type="radio" id="wb-settings-permission-0" name="permission" value="0" required>'
+	+ '<label for="wb-settings-permission-0" class="wb-settings-permission-deny">Deny</label>'
+	+ '</div><div class="wb-settings-permission">'
+	+ '<input type="radio" id="wb-settings-permission-1" name="permission" value="1" required>'
+	+ '<label for="wb-settings-permission-1" class="wb-settings-permission-allow">Allow</label>'
+	+ '</div><div class="wb-settings-permission">'
+	+ '<input type="radio" id="wb-settings-permission-default" name="permission" value="" required>'
+	+ '<label for="wb-settings-permission-default" class="wb-settings-permission-default">Default</label>'
+	+ '</div>',
+	save: '<input type="submit" id="wb-settings-save" name="save_settings">'
+};
+
+/**
+ * Create a settings form
+ * @param {import('cheerio')} $ - The response body
+ * @param {slashCommands[0]} slashCommand - The slash command
+ * @param {import('./i18n.js')} dashboardLang - The user language
+ * @param {Object[]} permissions - The current permissions
+ * @param {String} permissions.id
+ * @param {Number} permissions.type
+ * @param {Boolean} permissions.permission
+ * @param {String} guildId - The guild id
+ * @param {import('./util.js').Role[]} guildRoles - The guild roles
+ */
+function createForm($, slashCommand, dashboardLang, permissions, guildId, guildRoles) {
+	var readonly = ( process.env.READONLY ? true : false );
+	var fields = [];
+	if ( !readonly ) {
+		$('<script>').text(`const i18nSlashPermission = ${JSON.stringify({
+			allow: dashboardLang.get('slash.form.allow'),
+			deny: dashboardLang.get('slash.form.deny'),
+			default: dashboardLang.get('slash.form.default')
+		})};`).insertBefore('script#indexjs');
+		let role = $('<div>').append(fieldset.role);
+		role.find('label').text(dashboardLang.get('slash.form.role'));
+		role.find('#wb-settings-addrole').append(
+			$(`<option id="wb-settings-channel-default" selected hidden>`).val('').text(dashboardLang.get('slash.form.select_role')),
+			...guildRoles.filter( guildRole => !permissions.some( perm => perm.id === guildRole.id ) ).map( guildRole => {
+				return $(`<option id="wb-settings-addrole-${guildRole.id}">`).val(guildRole.id).text(`${guildRole.id} – @${guildRole.name}`)
+			} ),
+			( permissions.some( perm => perm.id === guildId ) ? '' : $(`<option id="wb-settings-addrole-${guildId}">`).val(guildId).text(`@everyone`) )
+		);
+		role.find('#wb-settings-addrole-add').text(dashboardLang.get('slash.form.add'));
+		fields.push(role);
+	}
+	let perms = permissions.sort( (a, b) => {
+		if ( a.id === guildId ) return 1;
+		if ( b.id === guildId ) return -1;
+		return guildRoles.findIndex( guildRole => guildRole.id === a.id ) - guildRoles.findIndex( guildRole => guildRole.id === b.id );
+	} ).map( perm => {
+		let permission = $('<div>').append(fieldset.permission);
+		let span = permission.find('span').attr('title', perm.id);
+		if ( perm.id === guildId ) span.text('@everyone').attr('title', '@everyone');
+		else span.text(`@${( guildRoles.find( guildRole => guildRole.id === perm.id )?.name || 'UNKNOWN' )}`);
+		permission.find('input[name="permission"]').attr('name', `permission-${perm.id}`);
+		permission.find('input#wb-settings-permission-0').attr('id', `wb-settings-permission-${perm.id}-0`);
+		permission.find('label[for="wb-settings-permission-0"]').attr('for', `wb-settings-permission-${perm.id}-0`);
+		permission.find('input#wb-settings-permission-1').attr('id', `wb-settings-permission-${perm.id}-1`);
+		permission.find('label[for="wb-settings-permission-1"]').attr('for', `wb-settings-permission-${perm.id}-1`);
+		permission.find('input#wb-settings-permission-default').attr('id', `wb-settings-permission-${perm.id}-default`);
+		permission.find('label[for="wb-settings-permission-default"]').attr('for', `wb-settings-permission-${perm.id}-default`);
+		permission.find(`#wb-settings-permission-${perm.id}-${( perm.permission ? '1' : '0' )}`).attr('checked', '');
+		return permission;
+	} );
+	fields.push(...perms);
+	fields.push($(fieldset.save).val(dashboardLang.get('general.save')));
+	var form = $('<fieldset>').append(
+		$('<legend>').text(( slashCommand.default_permission ? dashboardLang.get('slash.form.default_allow') : dashboardLang.get('slash.form.default_deny') )),
+		...fields);
+	if ( readonly ) {
+		form.find('input').attr('readonly', '');
+		form.find('input[type="radio"]:not(:checked), option').attr('disabled', '');
+		form.find('input[type="submit"]').remove();
+	}
+	form.find('label.wb-settings-permission-deny').text(dashboardLang.get('slash.form.deny'));
+	form.find('label.wb-settings-permission-allow').text(dashboardLang.get('slash.form.allow'));
+	form.find('label.wb-settings-permission-default').text(dashboardLang.get('slash.form.default'));
+	return $('<form id="wb-settings" method="post" enctype="application/x-www-form-urlencoded">').append(
+		$('<h2>').html(dashboardLang.get('slash.form.entry', true, $('<code>').text('/' + slashCommand.name))),
+		form
+	);
+}
+
+/**
+ * Let a user change slashs
+ * @param {import('http').ServerResponse} res - The server response
+ * @param {import('cheerio')} $ - The response body
+ * @param {import('./util.js').Guild} guild - The current guild
+ * @param {String[]} args - The url parts
+ * @param {import('./i18n.js')} dashboardLang - The user language
+ */
+function dashboard_slash(res, $, guild, args, dashboardLang) {
+	let suffix = ( args[0] === 'owner' ? '?owner=true' : '' );
+	$('#channellist #slash').after(
+		...slashCommands.filter( slashCommand => slashCommand.id ).map( slashCommand => {
+			return $('<a class="channel">').attr('id', `channel-${slashCommand.id}`).append(
+				$('<img>').attr('src', '/src/channel.svg'),
+				$('<div>').text(slashCommand.name)
+			).attr('href', `/guild/${guild.id}/slash/${slashCommand.id}${suffix}`);
+		} )
+	);
+	if ( args[4] ) {
+		let slashCommand = slashCommands.find( slashCommand => args[4] === slashCommand.id );
+		if ( slashCommand ) return got.get( `https://discord.com/api/v8/applications/${process.env.bot}/guilds/${guild.id}/commands/${slashCommand.id}/permissions`, {
+			headers: {
+				Authorization: `Bot ${process.env.token}`
+			},
+			timeout: 10000
+		} ).then( response=> {
+			var permissions = [];
+			if ( response.statusCode !== 200 || !response.body ) {
+				if ( response.statusCode !== 404 || response.body?.message !== 'Unknown application command permissions' ) {
+					console.log( '- Dashboard: ' + response.statusCode + ': Error while getting the slash command permissions: ' + response.body?.message );
+					createNotice($, 'error', dashboardLang);
+					$('#text .description').html(dashboardLang.get('slash.explanation'));
+					$('.channel#slash').addClass('selected');
+					return;
+				}
+				else if ( slashCommand.name === 'verify' ) {
+					res.writeHead(302, {Location: `/guild/${guild.id}/verification/new${suffix}`});
+					res.end();
+					return true;
+				}
+			}
+			else permissions = response.body.permissions;
+			$('<p>').html(dashboardLang.get('slash.desc', true, $('<code>').text(guild.name))).appendTo('#text .description');
+			$(`.channel#channel-${slashCommand.id}`).addClass('selected');
+			createForm($, slashCommand, dashboardLang, permissions, guild.id, guild.roles).attr('action', `/guild/${guild.id}/slash/${slashCommand.id}`).appendTo('#text');
+		}, error => {
+			console.log( '- Dashboard: Error while getting the slash command permissions: ' + error );
+			createNotice($, 'error', dashboardLang);
+			$('#text .description').html(dashboardLang.get('slash.explanation'));
+			$('.channel#slash').addClass('selected');
+		} ).then( isRedirected => {
+			if ( isRedirected ) return;
+			let body = $.html();
+			res.writeHead(200, {'Content-Length': Buffer.byteLength(body)});
+			res.write( body );
+			return res.end();
+		} );
+	}
+	$('#text .description').html(dashboardLang.get('slash.explanation'));
+	$('.channel#slash').addClass('selected');
+	let body = $.html();
+	res.writeHead(200, {'Content-Length': Buffer.byteLength(body)});
+	res.write( body );
+	return res.end();
+}
+
+/**
+ * Change slashs
+ * @param {Function} res - The server response
+ * @param {import('./util.js').Settings} userSettings - The settings of the user
+ * @param {String} guild - The id of the guild
+ * @param {String|Number} type - The setting to change
+ * @param {Object} settings - The new settings
+ */
+function update_slash(res, userSettings, guild, type, settings) {
+	if ( !slashCommands.some( slashCommand => slashCommand.id === type ) ) {
+		return res(`/guild/${guild}/slash`, 'savefail');
+	}
+	if ( !settings.save_settings ) {
+		return res(`/guild/${guild}/slash/${type}`, 'savefail');
+	}
+	let roles = userSettings.guilds.isMember.get(guild).roles;
+	var permissions = Object.keys(settings).filter( perm => roles.some( role => ( 'permission-' + role.id === perm ) ) || perm === 'permission-' + guild ).map( perm => {
+		return {
+			id: perm.replace( 'permission-', '' ), type: 1,
+			permission: ( settings[perm] === '1' ? true : false )
+		};
+	} );
+	sendMsg( {
+		type: 'getMember',
+		member: userSettings.user.id,
+		guild: guild
+	} ).then( response => {
+		if ( !response ) {
+			userSettings.guilds.notMember.set(guild, userSettings.guilds.isMember.get(guild));
+			userSettings.guilds.isMember.delete(guild);
+			return res(`/guild/${guild}`, 'savefail');
+		}
+		if ( response === 'noMember' || !hasPerm(response.userPermissions, 'MANAGE_GUILD') ) {
+			userSettings.guilds.isMember.delete(guild);
+			return res('/', 'savefail');
+		}
+		var commandName = slashCommands.find( slashCommand => slashCommand.id === type )?.name;
+		return got.get( `https://discord.com/api/v8/applications/${process.env.bot}/guilds/${guild}/commands/${type}/permissions`, {
+			headers: {
+				Authorization: `Bot ${process.env.token}`
+			},
+			timeout: 10000
+		} ).then( response=> {
+			if ( response.statusCode !== 200 || !response.body ) {
+				if ( response.statusCode !== 404 || response.body?.message !== 'Unknown application command permissions' ) {
+					console.log( '- Dashboard: ' + response.statusCode + ': Error while getting the old slash command permissions: ' + response.body?.message );
+				}
+				else if ( commandName === 'verify' ) {
+					return Promise.reject();
+				}
+				return [];
+			}
+			return response.body.permissions;
+		}, error => {
+			console.log( '- Dashboard: Error while getting the old slash command permissions: ' + error );
+			return [];
+		} ).then( oldPermissions => {
+			return got.put( `https://discord.com/api/v8/applications/${process.env.bot}/guilds/${guild}/commands/${type}/permissions`, {
+				headers: {
+					Authorization: `Bot ${process.env.token}`
+				},
+				json: {permissions},
+				timeout: 10000
+			} ).then( response=> {
+				if ( response.statusCode !== 200 || !response.body ) {
+					console.log( '- Dashboard: ' + response.statusCode + ': Error while saving the slash command permissions: ' + response.body?.message );
+					return res(`/guild/${guild}/slash/${type}`, 'savefail');
+				}
+				res(`/guild/${guild}/slash/${type}`, 'save');
+				return db.query( 'SELECT lang FROM discord WHERE guild = $1 AND channel IS NULL', [guild] ).then( ({rows:[channel]}) => {
+					var lang = new Lang(channel?.lang);
+					var text = lang.get('interaction.dashboard.updated', `<@${userSettings.user.id}>`, '/' + commandName);
+					text += permissions.map( perm => {
+						var oldPerm = oldPermissions.find( oldPerm => oldPerm.id === perm.id );
+						if ( !oldPerm ) {
+							return '\n' + ( perm.id === guild ? '@everyone' : `<@&${perm.id}>` ) + ': ~~`' + lang.get('interaction.dashboard.perm_default') + '`~~ → `' + lang.get('interaction.dashboard.perm_' + ( perm.permission ? 'allow' : 'deny' )) + '`';
+						}
+						if ( perm.permission === oldPerm.permission ) return '';
+						return '\n' + ( perm.id === guild ? '@everyone' : `<@&${perm.id}>` ) + ': ~~`' + lang.get('interaction.dashboard.perm_' + ( oldPerm.permission ? 'allow' : 'deny' )) + '`~~ → `' + lang.get('interaction.dashboard.perm_' + ( perm.permission ? 'allow' : 'deny' )) + '`';
+					} ).join('');
+					text += oldPermissions.filter( oldPerm => !permissions.some( perm => perm.id === oldPerm.id ) ).map( oldPerm => {
+						return '\n' + ( oldPerm.id === guild ? '@everyone' : `<@&${oldPerm.id}>` ) + ': ~~`' + lang.get('interaction.dashboard.perm_' + ( oldPerm.permission ? 'allow' : 'deny' )) + '`~~ → `' + lang.get('interaction.dashboard.perm_default') + '`';
+					} ).join('');
+					text += `\n<${new URL(`/guild/${guild}/slash/${type}`, process.env.dashboard).href}>`;
+					sendMsg( {
+						type: 'notifyGuild', guild, text
+					} ).catch( error => {
+						console.log( '- Dashboard: Error while notifying the guild: ' + error );
+					} );
+				}, dberror => {
+					console.log( '- Dashboard: Error while notifying the guild: ' + dberror );
+				} );
+			}, error => {
+				console.log( '- Dashboard: Error while saving the slash command permissions: ' + error );
+				return res(`/guild/${guild}/slash/${type}`, 'savefail');
+			} );
+		}, error => {
+			if ( error ) {
+				console.log( '- Dashboard: Error while getting the old slash command permissions: ' + error );
+				return res(`/guild/${guild}/slash/${type}`, 'savefail');
+			}
+			return res(`/guild/${guild}/verification/new`, 'savefail');
+		} );
+	}, error => {
+		console.log( '- Dashboard: Error while getting the member: ' + error );
+		return res(`/guild/${guild}/slash/${type}`, 'savefail');
+	} );
+}
+
+module.exports = {
+	get: dashboard_slash,
+	post: update_slash
+};

+ 19 - 1
dashboard/src/index.css

@@ -482,6 +482,22 @@ code {
 	font-weight: bold;
 	font-size: 90%;
 }
+fieldset {
+	border-color: #dcddde;
+}
+.theme-light fieldset {
+	border-color: #2e3338;
+}
+legend {
+	background: #dcddde;
+	color: #36393f;
+	padding: 3px 6px;
+	font-weight: bold;
+}
+.theme-light legend {
+	background-color: #2e3338;
+	color: #ffffff;
+}
 fieldset > div {
 	margin: 10px 0;
 }
@@ -497,10 +513,12 @@ fieldset input[type="url"] {
 fieldset input:invalid {
 	background: #FFAAAA;
 }
-.wb-settings-display:first-of-type {
+.wb-settings-display:first-of-type,
+.wb-settings-permission:first-of-type {
 	display: inline-block;
 }
 .wb-settings-display:not(:first-of-type),
+.wb-settings-permission:not(:first-of-type),
 .wb-settings-additional-select,
 .wb-settings-postcount:not(:nth-of-type(2)),
 #wb-settings-wiki-check-notice,

+ 42 - 0
dashboard/src/index.js

@@ -349,6 +349,48 @@ if ( prefix ) prefix.addEventListener( 'input', function() {
 	else prefix.setCustomValidity('');
 } );
 
+/** @type {HTMLSelectElement} */
+const addRole = document.getElementById('wb-settings-addrole');
+const addRoleButton = document.getElementById('wb-settings-addrole-add');
+if ( addRole && addRoleButton ) addRoleButton.onclick = function() {
+	if ( addRole.value ) {
+		var selectedRole = addRole.children.item(addRole.selectedIndex);
+		var newPermission = document.createElement('div');
+		var selectedRoleInfo = selectedRole.textContent.split(' – ');
+		var newPermissionSpan = document.createElement('span');
+		newPermissionSpan.textContent = ( selectedRoleInfo[1] || selectedRoleInfo[0] );
+		newPermissionSpan.setAttribute('title', selectedRoleInfo[0]);
+		var newPermissionDiv0 = document.createElement('div');
+		newPermissionDiv0.classList.add('wb-settings-permission');
+		var newPermissionInput = document.createElement('input');
+		newPermissionInput.setAttribute('type', 'radio');
+		newPermissionInput.setAttribute('name', 'permission-' +  addRole.value);
+		newPermissionInput.setAttribute('required', '');
+		newPermissionDiv0.append(newPermissionInput, document.createElement('label'));
+		/** @type {HTMLDivElement} */
+		var newPermissionDiv1 = newPermissionDiv0.cloneNode(true);
+		/** @type {HTMLDivElement} */
+		var newPermissionDiv2 = newPermissionDiv0.cloneNode(true);
+		newPermissionDiv0.firstElementChild.id = 'wb-settings-permission-' + addRole.value + '-0';
+		newPermissionDiv1.firstElementChild.id = 'wb-settings-permission-' + addRole.value + '-1';
+		newPermissionDiv2.firstElementChild.id = 'wb-settings-permission-' + addRole.value + '-default';
+		newPermissionDiv0.firstElementChild.setAttribute('value', '0');
+		newPermissionDiv1.firstElementChild.setAttribute('value', '1');
+		newPermissionDiv2.firstElementChild.setAttribute('value', '');
+		newPermissionDiv0.lastElementChild.setAttribute('for', 'wb-settings-permission-' + addRole.value + '-0');
+		newPermissionDiv1.lastElementChild.setAttribute('for', 'wb-settings-permission-' + addRole.value + '-1');
+		newPermissionDiv2.lastElementChild.setAttribute('for', 'wb-settings-permission-' + addRole.value + '-default');
+		newPermissionDiv0.lastElementChild.textContent = i18nSlashPermission.deny;
+		newPermissionDiv1.lastElementChild.textContent = i18nSlashPermission.allow;
+		newPermissionDiv2.lastElementChild.textContent = i18nSlashPermission.default;
+		newPermissionDiv2.firstElementChild.setAttribute('checked', '');
+		newPermission.append(newPermissionSpan, newPermissionDiv0, newPermissionDiv1, newPermissionDiv2);
+		addRole.parentElement.after(newPermission);
+		selectedRole.remove();
+		addRole.firstElementChild.setAttribute('selected', '');
+	}
+};
+
 /*
 var collapsible = document.getElementsByClassName('collapsible');
 for ( var i = 0; i < collapsible.length; i++ ) {

+ 8 - 6
dashboard/util.js

@@ -12,9 +12,11 @@ db.on( 'error', dberror => {
 	console.log( '- Dashboard: Error while connecting to the database: ' + dberror );
 } );
 
-got.get( 'https://discord.com/api/v8/applications/' + process.env.bot + '/commands', {
-	headers:{
-		Authorization: 'Bot ' + process.env.token
+const slashCommands = require('../interactions/commands.json');
+
+got.get( `https://discord.com/api/v8/applications/${process.env.bot}/commands`, {
+	headers: {
+		Authorization: `Bot ${process.env.token}`
 	},
 	timeout: 10000
 } ).then( response=> {
@@ -23,7 +25,6 @@ got.get( 'https://discord.com/api/v8/applications/' + process.env.bot + '/comman
 		return;
 	}
 	console.log( '- Dashboard: Slash commands successfully loaded.' );
-	const slashCommands = require('../interactions/commands.json');
 	response.body.forEach( command => {
 		var slashCommand = slashCommands.find( slashCommand => slashCommand.name === command.name );
 		if ( slashCommand ) {
@@ -120,6 +121,7 @@ function sendMsg(message) {
 	} );
 	return promise;
 }
+
 var botLists = [];
 if ( process.env.botlist ) {
 	let supportedLists = {
@@ -210,7 +212,7 @@ function createNotice($, notice, dashboardLang, args = []) {
 			type = 'info';
 			title.text(dashboardLang.get('notice.nosettings.title'));
 			text.text(dashboardLang.get('notice.nosettings.text'));
-			note = $('<a>').text(dashboardLang.get('notice.nosettings.note')).attr('href', `/guild/${args[0]}/settings`);
+			if ( args[0] ) note = $('<a>').text(dashboardLang.get('notice.nosettings.note')).attr('href', `/guild/${args[0]}/settings`);
 			break;
 		case 'logout':
 			type = 'success';
@@ -345,4 +347,4 @@ function hasPerm(all = 0, ...permission) {
 	} ).every( perm => perm );
 }
 
-module.exports = {got, db, settingsData, sendMsg, addWidgets, createNotice, escapeText, hasPerm};
+module.exports = {got, db, slashCommands, settingsData, sendMsg, addWidgets, createNotice, escapeText, hasPerm};

+ 2 - 2
dashboard/verification.js

@@ -1,7 +1,7 @@
 const {limit: {verification: verificationLimit}, usergroups} = require('../util/default.json');
 const Lang = require('../util/i18n.js');
-const {got, db, sendMsg, createNotice, escapeText, hasPerm} = require('./util.js');
-const slashCommand = require('../interactions/commands.json').find( slashCommand => slashCommand.name === 'verify' );
+const {got, db, slashCommands, sendMsg, createNotice, escapeText, hasPerm} = require('./util.js');
+const slashCommand = slashCommands.find( slashCommand => slashCommand.name === 'verify' );
 
 const fieldset = {
 	channel: '<label for="wb-settings-channel">Channel:</label>'

+ 0 - 1
i18n/bn.json

@@ -394,7 +394,6 @@
     },
     "interaction": {
         "inline": "এই কমাণ্ডের ব্যবহার করার জন্যে অনুগ্রহ করে [[উইকিটেক্সট]] লিংক দিয়ে কোনো টেক্সট দেন।",
-        "missingrole": "এই কমাণ্ডের ব্যবহীর করার জন্যে আপনার কাছে $1 বা এর থেকে উঁচু কোনো রোল থাকতে হবে।",
         "nowiki": "ব্যবহৃত উইকিটি পাওয়া যাচ্ছে না!"
     },
     "invite": {

+ 0 - 1
i18n/de.json

@@ -394,7 +394,6 @@
     },
     "interaction": {
         "inline": "Bitte gebe einen Text mit [[Wikitext]]-Links an um diesen Befehl zu nutzen.",
-        "missingrole": "Du benötigst die $1-Rolle oder eine höhere Rolle um diesen Befehl zu nutzen.",
         "nowiki": "Das genutzte Wiki existiert nicht!",
         "verify": "Bitte gebe deinen Wiki-Benutzernamen an um diesen Befehl dazu zu nutzen, deinen Discord-Account mit deinem Wiki-Account zu verifizieren und Rollen passend zu deinem Wiki-Account zu erhalten."
     },

+ 6 - 1
i18n/en.json

@@ -393,8 +393,13 @@
         "pause": "**I'm currently paused on this server!**\nOnly these commands can be performed:"
     },
     "interaction": {
+        "dashboard": {
+            "perm_allow": "Allow",
+            "perm_deny": "Deny",
+            "perm_default": "Default",
+            "updated": "$1 updated the slash command permission overwrites for `$2`."
+        },
         "inline": "Please provide some text with [[wikitext]] links to use this command.",
-        "missingrole": "You need to have the $1 role or any higher one to use this command.",
         "nowiki": "The used wiki doesn't exist!",
         "verify": "Please provide your wiki username to use this command to verify your Discord account with your wiki account and get roles matching your wiki account."
     },

+ 0 - 1
i18n/es.json

@@ -394,7 +394,6 @@
     },
     "interaction": {
         "inline": "Por favor proporciona un texto con enlaces de [[wikitexto]] para usar este comando.",
-        "missingrole": "Debes tener el rol de $1 o uno superior para usar este comando.",
         "nowiki": "¡El wiki usado no existe!",
         "verify": "Por favor proporciona tu nombre de usuario wiki para usar este comando para verificar tu cuenta en Discord con tu cuenta wiki y obtener roles que coincidan con tu cuenta wiki."
     },

+ 0 - 1
i18n/fr.json

@@ -394,7 +394,6 @@
     },
     "interaction": {
         "inline": "Veuillez fournir un texte avec des liens [[wikitext]] pour utiliser cette commande.",
-        "missingrole": "Vous devez avoir le rôle $1 ou un rôle supérieur pour utiliser cette commande.",
         "nowiki": "Le wiki utilisé n'existe pas !",
         "verify": "Veuillez fournir votre nom d'utilisateur wiki pour utiliser cette commande afin de vérifier votre compte Discord avec votre compte wiki et obtenir les rôles correspondant à votre compte wiki."
     },

+ 0 - 1
i18n/hi.json

@@ -394,7 +394,6 @@
     },
     "interaction": {
         "inline": "इस कमांड का इस्तेमाल करने के लिए कृपया [[विकिटेक्स्ट]] लिंक के साथ कोई टेक्स्ट दें।",
-        "missingrole": "इस कमांड का इस्तेमाल करने के लिए आपके पास $1 या इससे ऊँचा कोई रोल होना होगा।",
         "nowiki": "यह विकि मौजूद नहीं है!",
         "verify": "इस कमांड का इस्तेमाल करके अपने डिस्कॉर्ड अकाउंट को विकि अकाउंट के साथ जोड़कर विकि अकाउंट से संबंधित रोल पाने के लिए कृपया अपने विकि अकाउंट का नाम दें।"
     },

+ 1 - 2
i18n/ko.json

@@ -392,8 +392,7 @@
         "pause": "**저는 이 서버에서 일시중지되어 있어요!**\n지금은 이 명령어만 사용할 수 있어요."
     },
     "interaction": {
-        "inline": "이 명령어를 사용하려면 [[위키텍스트]]와 다른 텍스트를 같이 제공해야 합니다.",
-        "missingrole": "이 명령어를 사용하려면 $1 또는 이 역할보다 위에 있는 역할을 가지고 있어야 합니다."
+        "inline": "이 명령어를 사용하려면 [[위키텍스트]]와 다른 텍스트를 같이 제공해야 합니다."
     },
     "invite": {
         "bot": "저를 다른 서버에 초대하려면 이 링크를 사용하세요."

+ 0 - 1
i18n/pl.json

@@ -394,7 +394,6 @@
     },
     "interaction": {
         "inline": "Wyślij wiadomość z linkami [[wikitekstu]] aby użyć tej komendy.",
-        "missingrole": "Aby użyć tej komendy wymagane jest posiadanie roli $1 lub wyższej.",
         "nowiki": "Używana wiki nie istnieje!",
         "verify": "Podaj swoją nazwę użytkownika na wiki aby użyć komendy do zweryfikowania swojego konta na Discordzie kontem na wiki oraz otrzymać role odpowiadające Twojemu koncie na wiki."
     },

+ 0 - 1
i18n/pt-br.json

@@ -394,7 +394,6 @@
     },
     "interaction": {
         "inline": "Forneça algum texto com links de [[wikitexto]] para usar este comando.",
-        "missingrole": "Você precisa ter o cargo $1 ou qualquer superior para usar este comando.",
         "nowiki": "A wiki utilizada não existe!",
         "verify": "Por favor, forneça o seu nome de usuário na wiki para usar este comando e verificar a sua conta do Discord com a sua conta da wiki e obter cargos correspondentes à sua conta da wiki."
     },

+ 0 - 1
i18n/ru.json

@@ -394,7 +394,6 @@
     },
     "interaction": {
         "inline": "Чтобы использовать эту команду, предоставьте текст со ссылками в [[викитекст-разметке]].",
-        "missingrole": "Чтобы использовать эту команду, вам нужна роль $1 или выше.",
         "verify": "Укажите ваше имя пользователя на вики через эту команду, чтобы подтвердить, что ваш аккаунт Discord и ваша учётная запись на вики принадлежат вам, и получить роли соответствующие вашей учётной записи на вики."
     },
     "invite": {

+ 1 - 2
i18n/tr.json

@@ -391,8 +391,7 @@
         "pause": "**Şu an bu sunucuda duraklatılmış durumdayım!**\nSadece bu komutlar gerçekleştirilebilir:"
     },
     "interaction": {
-        "inline": "Bu komutu kullanmak için [[wikitext]] ile bir metin girin.",
-        "missingrole": "Bu komutu kullanmak için $1 ya da daha yüksek bir role sahip olman lazım."
+        "inline": "Bu komutu kullanmak için [[wikitext]] ile bir metin girin."
     },
     "invite": {
         "bot": "Beni başka bir sunucuya davet etmek için bu bağlantıyı kullan:"

+ 1 - 2
i18n/uk.json

@@ -352,8 +352,7 @@
         "noadmin": "вам потрібен дозвіл `управління сервером` для цих команд!"
     },
     "interaction": {
-        "inline": "Будь ласка, надайте деякий текст з посиланнями [[wikitext]], щоб використовувати цю команду.",
-        "missingrole": "Щоб використовувати цю команду, вам потрібно мати роль $1 або будь-яку вищу."
+        "inline": "Будь ласка, надайте деякий текст з посиланнями [[wikitext]], щоб використовувати цю команду."
     },
     "invite": {
         "bot": "Використовуйте це посилання, щоб запросити мене на інший сервер:"

+ 0 - 1
i18n/zh-hans.json

@@ -394,7 +394,6 @@
     },
     "interaction": {
         "inline": "请提供一些有 [[wiki 文本]] 的文字来使用此命令。",
-        "missingrole": "你需要拥有 $1 身份组或更高的身份组以使用此命令。",
         "nowiki": "所使用的 wiki 不存在!",
         "verify": "为了验证您的 Discord 与您的 wiki 账户相匹配并授予与您 wiki 账户相符合的身份组,请提供您的 wiki 用户名。"
     },

+ 0 - 1
i18n/zh-hant.json

@@ -394,7 +394,6 @@
     },
     "interaction": {
         "inline": "請提供一些有 [[wikitext]] 的文字來使用此指令。",
-        "missingrole": "您需要擁有 $1 身分組或更高的身分組以使用此指令。",
         "nowiki": "所使用的wiki不存在!",
         "verify": "為了能透過您的wiki帳號驗證您的Discord帳號與並授予與您wiki帳號相符的身分組,請提供您的wiki使用者名稱以使用此指令。"
     },

+ 3 - 0
util/functions.js

@@ -8,6 +8,8 @@ const got = require('got').extend( {
 	responseType: 'json'
 } );
 
+const slashCommands = require('../interactions/commands.json');
+
 /**
  * Parse infobox content
  * @param {Object} infobox - The content of the infobox.
@@ -449,6 +451,7 @@ function sendMessage(interaction, message, channel) {
 
 module.exports = {
 	got,
+	slashCommands,
 	parse_infobox,
 	toFormatting,
 	toMarkdown,