Browse Source

Add read-only verifications and rcscript

Markus-Rost 4 years ago
parent
commit
49284f13ae

+ 7 - 5
dashboard/guilds.js

@@ -76,7 +76,7 @@ function dashboard_guilds(res, state, reqURL) {
 		settings.guilds.isMember.forEach( guild => {
 		settings.guilds.isMember.forEach( guild => {
 			$('<div class="guild">').attr('id', guild.id).append(
 			$('<div class="guild">').attr('id', guild.id).append(
 				$('<div class="bar">'),
 				$('<div class="bar">'),
-				$('<a>').attr('href', `/guild/${guild.id}`).attr('alt', guild.name).append(
+				$('<a>').attr('href', `/guild/${guild.id}/settings`).attr('alt', guild.name).append(
 					( guild.icon ? 
 					( guild.icon ? 
 						$('<img class="avatar">').attr('src', `${guild.icon}?size=64`).attr('alt', guild.name)
 						$('<img class="avatar">').attr('src', `${guild.icon}?size=64`).attr('alt', guild.name)
 					 : $('<div class="avatar noicon">').text(guild.acronym) )
 					 : $('<div class="avatar noicon">').text(guild.acronym) )
@@ -106,11 +106,13 @@ function dashboard_guilds(res, state, reqURL) {
 		if ( settings.guilds.isMember.has(id) ) {
 		if ( settings.guilds.isMember.has(id) ) {
 			let guild = settings.guilds.isMember.get(id);
 			let guild = settings.guilds.isMember.get(id);
 			$('head title').text(`${guild.name} – ` + $('head title').text());
 			$('head title').text(`${guild.name} – ` + $('head title').text());
-			$('.channel#settings').attr('href', `/guild/${guild.id}`);
+			$('<script>').text(`const isPatreon = ${guild.patreon};`).insertBefore('script#indexjs');
+			$('.channel#settings').attr('href', `/guild/${guild.id}/settings`);
 			$('.channel#verification').attr('href', `/guild/${guild.id}/verification`);
 			$('.channel#verification').attr('href', `/guild/${guild.id}/verification`);
-			$('.channel#rcgcdb').attr('href', `/guild/${guild.id}/rcscript`);
-			if ( args[3] === 'rcscript' ) return forms.rcscript(res, $, guild, args);
+			$('.channel#rcscript').attr('href', `/guild/${guild.id}/rcscript`);
+			if ( args[3] === 'settings' ) return forms.settings(res, $, guild, args);
 			if ( args[3] === 'verification' ) return forms.verification(res, $, guild, args);
 			if ( args[3] === 'verification' ) return forms.verification(res, $, guild, args);
+			if ( args[3] === 'rcscript' ) return forms.rcscript(res, $, guild, args);
 			return forms.settings(res, $, guild, args);
 			return forms.settings(res, $, guild, args);
 		}
 		}
 		else if ( settings.guilds.notMember.has(id) ) {
 		else if ( settings.guilds.notMember.has(id) ) {
@@ -141,7 +143,7 @@ function dashboard_guilds(res, state, reqURL) {
 			).appendTo('#channellist');
 			).appendTo('#channellist');
 			$('<div class="server-selector" id="isMember">').appendTo('#text');
 			$('<div class="server-selector" id="isMember">').appendTo('#text');
 			settings.guilds.isMember.forEach( guild => {
 			settings.guilds.isMember.forEach( guild => {
-				$('<a class="server">').attr('href', `/guild/${guild.id}`).append(
+				$('<a class="server">').attr('href', `/guild/${guild.id}/settings`).append(
 					( guild.icon ? 
 					( guild.icon ? 
 						$('<img class="avatar">').attr('src', `${guild.icon}?size=256`).attr('alt', guild.name)
 						$('<img class="avatar">').attr('src', `${guild.icon}?size=256`).attr('alt', guild.name)
 					 : $('<div class="avatar noicon">').text(guild.acronym) ),
 					 : $('<div class="avatar noicon">').text(guild.acronym) ),

+ 2 - 2
dashboard/index.html

@@ -11,7 +11,7 @@
 	<meta property="og:site_name" content="Wiki-Bot Settings">
 	<meta property="og:site_name" content="Wiki-Bot Settings">
 	<meta itemprop="author" content="MarkusRost">
 	<meta itemprop="author" content="MarkusRost">
 	<link rel="stylesheet" type="text/css" href="/src/index.css">
 	<link rel="stylesheet" type="text/css" href="/src/index.css">
-	<!--<script src="/src/index.js"></script>-->
+	<script id="indexjs" src="/src/index.js" defer></script>
 </head>
 </head>
 <body>
 <body>
 	<div id="text">
 	<div id="text">
@@ -27,7 +27,7 @@
 				<img src="/src/settings.svg" alt="Settings">
 				<img src="/src/settings.svg" alt="Settings">
 				<div>Verifications</div>
 				<div>Verifications</div>
 			</a>
 			</a>
-			<a class="channel channel-header" id="rcgcdb">
+			<a class="channel channel-header" id="rcscript">
 				<img src="/src/settings.svg" alt="Settings">
 				<img src="/src/settings.svg" alt="Settings">
 				<div>Recent Changes</div>
 				<div>Recent Changes</div>
 			</a>
 			</a>

+ 1 - 1
dashboard/index.js

@@ -49,7 +49,7 @@ const server = http.createServer((req, res) => {
 			return cookie.split('=')[0] === 'wikibot';
 			return cookie.split('=')[0] === 'wikibot';
 		} )?.map( cookie => cookie.replace( /^wikibot="(\w*(?:-\d+)?)"$/, '$1' ) )?.join();
 		} )?.map( cookie => cookie.replace( /^wikibot="(\w*(?:-\d+)?)"$/, '$1' ) )?.join();
 
 
-		if ( args.length <= 4 && ['settings', 'verification', 'rcscript'].includes( args[3] ) 
+		if ( args.length === 5 && ['settings', 'verification', 'rcscript'].includes( args[3] ) 
 		&& settingsData.has(state) && settingsData.get(state).guilds.isMember.has(args[2]) ) {
 		&& settingsData.has(state) && settingsData.get(state).guilds.isMember.has(args[2]) ) {
 			let body = '';
 			let body = '';
 			req.on( 'data', chunk => {
 			req.on( 'data', chunk => {

+ 1 - 1
dashboard/oauth.js

@@ -155,7 +155,7 @@ function dashboard_oauth(res, state, searchParams, lastGuild) {
 				} );
 				} );
 				settingsData.set(settings.state, settings);
 				settingsData.set(settings.state, settings);
 				res.writeHead(302, {
 				res.writeHead(302, {
-					Location: ( lastGuild ? '/guild/' + lastGuild : '/' ),
+					Location: ( lastGuild ? '/guild/' + lastGuild : '/settings' ),
 					'Set-Cookie': [`wikibot="${settings.state}"; HttpOnly; Path=/`]
 					'Set-Cookie': [`wikibot="${settings.state}"; HttpOnly; Path=/`]
 				});
 				});
 				return res.end();
 				return res.end();

+ 178 - 8
dashboard/rcscript.js

@@ -1,28 +1,198 @@
+const got = require('got').extend( {
+	headers: {
+		'User-Agent': 'Wiki-Bot/dashboard (Discord; ' + process.env.npm_package_name + ')'
+	},
+	responseType: 'json'
+} );
+const {defaultSettings, limit: {rcgcdw: rcgcdwLimit}} = require('../util/default.json');
+const {RcGcDw: {names: allLangs}} = require('../i18n/allLangs.json');
 const {db, settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
 const {db, settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
 
 
+const fieldset = {
+	channel: '<label for="wb-settings-channel">Channel:</label>'
+	+ '<select id="wb-settings-channel" name="channel" required></select>',
+	wiki: '<label for="wb-settings-wiki">Wiki:</label>'
+	+ '<input type="url" id="wb-settings-wiki" name="wiki" required>',
+	//+ '<button type="button" id="wb-settings-wiki-search" class="collapsible">Search wiki</button>'
+	//+ '<fieldset style="display: none;">'
+	//+ '<legend>Wiki search</legend>'
+	//+ '</fieldset>',
+	lang: '<label for="wb-settings-lang">Language:</label>'
+	+ '<select id="wb-settings-lang" name="lang" required>'
+	+ Object.keys(allLangs).map( lang => {
+		return `<option id="wb-settings-lang-${lang}" value="${lang}">${allLangs[lang]}</option>`
+	} ).join('\n')
+	+ '</select>',
+	display: '<span>Display mode:</span>'
+	+ '<div class="wb-settings-display">'
+	+ '<input type="radio" id="wb-settings-display-0" name="display" value="0" required>'
+	+ '<label for="wb-settings-display-0">Compact text messages with inline links.</label>'
+	+ '</div><div class="wb-settings-display">'
+	+ '<input type="radio" id="wb-settings-display-1" name="display" value="1" required>'
+	+ '<label for="wb-settings-display-1">Embed messages with edit tags and category changes.</label>'
+	+ '</div><div class="wb-settings-display">'
+	+ '<input type="radio" id="wb-settings-display-2" name="display" value="2" required>'
+	+ '<label for="wb-settings-display-2">Embed messages with image previews.</label>'
+	+ '</div><div class="wb-settings-display">'
+	+ '<input type="radio" id="wb-settings-display-3" name="display" value="3" required>'
+	+ '<label for="wb-settings-display-3">Embed messages with image previews and edit differences.</label>'
+	+ '</div>',
+	feeds: '<label for="wb-settings-feeds">Feeds based changes:</label>'
+	+ '<input type="checkbox" id="wb-settings-feeds" name="feeds">'
+	+ '<div id="wb-settings-feeds-only-hide">'
+	+ '<label for="wb-settings-feeds-only">Only feeds based changes:</label>'
+	+ '<input type="checkbox" id="wb-settings-feeds-only" name="feeds-only">'
+	+ '</div>',
+	save: '<input type="submit" id="wb-settings-save" name="save-settings">',
+	delete: '<input type="submit" id="wb-settings-delete" name="delete-settings">'
+};
+
+/**
+ * Create a settings form
+ * @param {import('cheerio')} $ - The response body
+ * @param {String} header - The form header
+ * @param {Object} settings - The current settings
+ * @param {Boolean} settings.patreon
+ * @param {String} settings.channel
+ * @param {String} settings.wiki
+ * @param {String} settings.lang
+ * @param {Number} settings.display
+ * @param {Number} settings.wikiid
+ * @param {Number} settings.rcid
+ * @param {Object[]} guildChannels - The guild channels
+ * @param {String} guildChannels.id
+ * @param {String} guildChannels.name
+ * @param {Number} guildChannels.permissions
+ */
+function createForm($, header, settings, guildChannels) {
+	var readonly = ( process.env.READONLY ? true : false );
+	var fields = [];
+	let channel = $('<div>').append(fieldset.channel);
+	channel.find('#wb-settings-channel').append(
+		...guildChannels.filter( guildChannel => {
+			return ( hasPerm(guildChannel.permissions, 'VIEW_CHANNEL', 'MANAGE_WEBHOOKS') || settings.channel === guildChannel.id );
+		} ).map( guildChannel => {
+			var optionChannel = $(`<option id="wb-settings-channel-${guildChannel.id}">`).val(guildChannel.id);
+			if ( settings.channel === guildChannel.id ) {
+				optionChannel.attr('selected', '');
+				if ( !hasPerm(guildChannel.permissions, 'VIEW_CHANNEL', 'MANAGE_WEBHOOKS') ) {
+					optionChannel.addClass('wb-settings-error');
+					readonly = true;
+				}
+			}
+			return optionChannel.text(`${guildChannel.id} – #${guildChannel.name}`);
+		} )
+	);
+	if ( !settings.channel ) channel.find('#wb-settings-channel').prepend(
+		$(`<option id="wb-settings-channel-default" selected>`).val('').text('-- Select a Channel --')
+	);
+	fields.push(channel);
+	let wiki = $('<div>').append(fieldset.wiki);
+	wiki.find('#wb-settings-wiki').val(settings.wiki);
+	fields.push(wiki);
+	let lang = $('<div>').append(fieldset.lang);
+	lang.find(`#wb-settings-lang-${settings.lang}`).attr('selected', '');
+	fields.push(lang);
+	let display = $('<div>').append(fieldset.display);
+	display.find(`#wb-settings-display-${settings.display}`).attr('checked', '');
+	if ( !settings.patreon ) display.find('.wb-settings-display').filter( (i, radioDisplay) => {
+		return ( i >= rcgcdwLimit.display && !$(radioDisplay).has('input:checked').length );
+	} ).remove();
+	fields.push(display);
+	let feeds = $('<div id="wb-settings-feeds-hide">').append(fieldset.feeds);
+	if ( /\.(?:fandom\.com|wikia\.org)$/.test(new URL(settings.wiki).hostname) ) {
+		if ( settings.wikiid ) {
+			feeds.find('#wb-settings-feeds').attr('checked', '');
+			if ( settings.rcid === -1 ) feeds.find('#wb-settings-feeds-only').attr('checked', '');
+		}
+		else feeds.find('#wb-settings-feeds-only-hide').attr('style', 'visibility: hidden;');
+	}
+	else {
+		feeds.attr('style', 'display: none;');
+		feeds.find('#wb-settings-feeds-only-hide').attr('style', 'visibility: hidden;');
+	}
+	fields.push(feeds);
+	fields.push($(fieldset.save).val('Save'));
+	if ( settings.channel ) {
+		fields.push($(fieldset.delete).val('Delete').attr('onclick', `return confirm('Are you sure?');`));
+	}
+	var form = $('<fieldset>').append(...fields);
+	if ( readonly ) {
+		form.find('input').attr('readonly', '');
+		form.find('input[type="checkbox"], input[type="radio"]:not(:checked), option').attr('disabled', '');
+		form.find('input[type="submit"], button.addmore').remove();
+	}
+	return $('<form id="wb-settings" method="post" enctype="application/x-www-form-urlencoded">').append(
+		$('<h2>').text(header),
+		form
+	);
+}
+
 /**
 /**
  * Let a user change recent changes scripts
  * Let a user change recent changes scripts
  * @param {import('http').ServerResponse} res - The server response
  * @param {import('http').ServerResponse} res - The server response
- * @param {CheerioStatic} $ - The response body
+ * @param {import('cheerio')} $ - The response body
  * @param {import('./util.js').Guild} guild - The current guild
  * @param {import('./util.js').Guild} guild - The current guild
  * @param {String[]} args - The url parts
  * @param {String[]} args - The url parts
  */
  */
 function dashboard_rcscript(res, $, guild, args) {
 function dashboard_rcscript(res, $, guild, args) {
-	$('.channel#rcgcdb').addClass('selected');
-	db.all( 'SELECT configid, wiki, lang, display, wikiid, rcid FROM rcgcdw WHERE guild = ? ORDER BY configid ASC', [guild.id], function(dberror, rows) {
+	db.all( 'SELECT webhook, configid, wiki, lang, display, wikiid, rcid FROM rcgcdw WHERE guild = ? ORDER BY configid ASC', [guild.id], function(dberror, rows) {
 		if ( dberror ) {
 		if ( dberror ) {
 			console.log( '- Dashboard: Error while getting the RcGcDw: ' + dberror );
 			console.log( '- Dashboard: Error while getting the RcGcDw: ' + dberror );
 			$('#text .description').text('Failed to load the recent changes webhooks!');
 			$('#text .description').text('Failed to load the recent changes webhooks!');
+			$('.channel#rcscript').addClass('selected');
 			let body = $.html();
 			let body = $.html();
 			res.writeHead(200, {'Content-Length': body.length});
 			res.writeHead(200, {'Content-Length': body.length});
 			res.write( body );
 			res.write( body );
 			return res.end();
 			return res.end();
 		}
 		}
-		$('<pre>').text(JSON.stringify(rows, null, '\t')).appendTo('#text .description');
-		let body = $.html();
-		res.writeHead(200, {'Content-Length': body.length});
-		res.write( body );
-		return res.end();
+		$('#text .description').text(`These are the recent changes webhooks for "${guild.name}":`);
+		Promise.all(rows.map( row => {
+			return got.get( 'https://discord.com/api/webhooks/' + row.webhook ).then( response => {
+				row.channel = response.body.channel_id;
+			}, error => {
+				console.log( '- Dashboard: Error while getting the webhook: ' + error );
+				row.channel = 'UNKNOWN';
+			} );
+		} )).finally( () => {
+			$('#channellist #rcscript').after(
+				...rows.map( row => {
+					return $('<a class="channel">').attr('id', `channel-${row.configid}`).append(
+						$('<img>').attr('src', '/src/channel.svg'),
+						$('<div>').text(`${row.configid} - ${( guild.channels.find( channel => {
+							return channel.id === row.channel;
+						} )?.name || row.channel )}`)
+					).attr('href', `/guild/${guild.id}/rcscript/${row.configid}`);
+				} ),
+				( process.env.READONLY || rows.length >= rcgcdwLimit[( guild.patreon ? 'patreon' : 'default' )] ? '' :
+				$('<a class="channel" id="channel-new">').append(
+					$('<img>').attr('src', '/src/channel.svg'),
+					$('<div>').text('New webhook')
+				).attr('href', `/guild/${guild.id}/rcscript/new`) )
+			);
+			if ( args[4] === 'new' ) {
+				$('.channel#channel-new').addClass('selected');
+				createForm($, 'New Recent Changes Webhook', {
+					wiki: defaultSettings.wiki, lang: defaultSettings.lang,
+					display: 1, patreon: guild.patreon
+				}, guild.channels).attr('action', `/guild/${guild.id}/rcscript/new`).appendTo('#text');
+			}
+			else if ( rows.some( row => row.configid == args[4] ) ) {
+				let row = rows.find( row => row.configid == args[4] );
+				$(`.channel#channel-${row.configid}`).addClass('selected');
+				createForm($, `Recent Changes Webhook #${row.configid}`, Object.assign({
+					patreon: guild.patreon
+				}, row), guild.channels).attr('action', `/guild/${guild.id}/rcscript/${row.configid}`).appendTo('#text');
+			}
+			else {
+				$('.channel#rcscript').addClass('selected');
+				$('#text .description').text(`*Insert explanation about recent changes webhooks here*`);
+			}
+			let body = $.html();
+			res.writeHead(200, {'Content-Length': body.length});
+			res.write( body );
+			return res.end();
+		} );
 	} );
 	} );
 }
 }
 
 

+ 46 - 30
dashboard/settings.js

@@ -18,19 +18,34 @@ const fieldset = {
 	} ).join('\n')
 	} ).join('\n')
 	+ '</select>',
 	+ '</select>',
 	prefix: '<label for="wb-settings-prefix">Prefix:</label>'
 	prefix: '<label for="wb-settings-prefix">Prefix:</label>'
-	+ '<input type="text" id="wb-settings-prefix" name="prefix" pattern="^[^ \`]+$" required>'
+	+ '<input type="text" id="wb-settings-prefix" name="prefix" pattern="^\\s*[^\\s`]+\\s*$" required>'
 	+ '<br>'
 	+ '<br>'
 	+ '<label for="wb-settings-prefix-space">Prefix ends with space:</label>'
 	+ '<label for="wb-settings-prefix-space">Prefix ends with space:</label>'
 	+ '<input type="checkbox" id="wb-settings-prefix-space" name="prefix-space">',
 	+ '<input type="checkbox" id="wb-settings-prefix-space" name="prefix-space">',
 	inline: '<label for="wb-settings-inline">Inline commands:</label>'
 	inline: '<label for="wb-settings-inline">Inline commands:</label>'
 	+ '<input type="checkbox" id="wb-settings-inline" name="inline">',
 	+ '<input type="checkbox" id="wb-settings-inline" name="inline">',
 	voice: '<label for="wb-settings-voice">Voice channels:</label>'
 	voice: '<label for="wb-settings-voice">Voice channels:</label>'
-	+ '<input type="checkbox" id="wb-settings-voice" name="voice">'
+	+ '<input type="checkbox" id="wb-settings-voice" name="voice">',
+	save: '<input type="submit" id="wb-settings-save" name="save-settings">',
+	delete: '<input type="submit" id="wb-settings-delete" name="delete-settings">'
 };
 };
 
 
 /**
 /**
- * Let a user change settings
- * @param {CheerioStatic} $ - The response body
+ * Create a settings form
+ * @param {import('cheerio')} $ - The response body
+ * @param {String} header - The form header
+ * @param {Object} settings - The current settings
+ * @param {Boolean} settings.patreon
+ * @param {String} settings.channel
+ * @param {String} settings.wiki
+ * @param {String} settings.lang
+ * @param {Boolean} settings.inline
+ * @param {String} settings.prefix
+ * @param {Boolean} settings.voice
+ * @param {Object[]} guildChannels - The guild channels
+ * @param {String} guildChannels.id
+ * @param {String} guildChannels.name
+ * @param {Number} guildChannels.permissions
  */
  */
 function createForm($, header, settings, guildChannels) {
 function createForm($, header, settings, guildChannels) {
 	var readonly = ( process.env.READONLY ? true : false );
 	var readonly = ( process.env.READONLY ? true : false );
@@ -77,10 +92,15 @@ function createForm($, header, settings, guildChannels) {
 		if ( settings.voice ) voice.find('#wb-settings-voice').attr('checked', '');
 		if ( settings.voice ) voice.find('#wb-settings-voice').attr('checked', '');
 		fields.push(voice);
 		fields.push(voice);
 	}
 	}
-	var form = $('<fieldset>').append(...fields, '<input type="submit">');
+	fields.push($(fieldset.save).val('Save'));
+	if ( settings.channel && settings.channel !== 'new' ) {
+		fields.push($(fieldset.delete).val('Delete').attr('onclick', `return confirm('Are you sure?');`));
+	}
+	var form = $('<fieldset>').append(...fields);
 	if ( readonly ) {
 	if ( readonly ) {
 		form.find('input').attr('readonly', '');
 		form.find('input').attr('readonly', '');
-		form.find('input[type="submit"], input[type="checkbox"], option').attr('disabled', '');
+		form.find('input[type="checkbox"], option').attr('disabled', '');
+		form.find('input[type="submit"]').remove();
 	}
 	}
 	return $('<form id="wb-settings" method="post" enctype="application/x-www-form-urlencoded">').append(
 	return $('<form id="wb-settings" method="post" enctype="application/x-www-form-urlencoded">').append(
 		$('<h2>').text(header),
 		$('<h2>').text(header),
@@ -91,7 +111,7 @@ function createForm($, header, settings, guildChannels) {
 /**
 /**
  * Let a user change settings
  * Let a user change settings
  * @param {import('http').ServerResponse} res - The server response
  * @param {import('http').ServerResponse} res - The server response
- * @param {CheerioStatic} $ - The response body
+ * @param {import('cheerio')} $ - The response body
  * @param {import('./util.js').Guild} guild - The current guild
  * @param {import('./util.js').Guild} guild - The current guild
  * @param {String[]} args - The url parts
  * @param {String[]} args - The url parts
  */
  */
@@ -111,7 +131,7 @@ function dashboard_settings(res, $, guild, args) {
 			$('.channel#settings').addClass('selected');
 			$('.channel#settings').addClass('selected');
 			createForm($, 'Server-wide Settings', Object.assign({
 			createForm($, 'Server-wide Settings', Object.assign({
 				prefix: process.env.prefix
 				prefix: process.env.prefix
-			}, defaultSettings)).attr('action', `/guild/${guild.id}`).appendTo('#text');
+			}, defaultSettings)).attr('action', `/guild/${guild.id}/settings/default`).appendTo('#text');
 			let body = $.html();
 			let body = $.html();
 			res.writeHead(200, {'Content-Length': body.length});
 			res.writeHead(200, {'Content-Length': body.length});
 			res.write( body );
 			res.write( body );
@@ -126,45 +146,41 @@ function dashboard_settings(res, $, guild, args) {
 		} );
 		} );
 		$('#channellist #settings').after(
 		$('#channellist #settings').after(
 			...channellist.map( channel => {
 			...channellist.map( channel => {
-				return $('<a class="channel">').attr('href', `/guild/${guild.id}/${channel.id}`).append(
+				return $('<a class="channel">').attr('id', `channel-${channel.id}`).append(
 					$('<img>').attr('src', '/src/channel.svg'),
 					$('<img>').attr('src', '/src/channel.svg'),
 					$('<div>').text(channel.name)
 					$('<div>').text(channel.name)
-				).attr('id', `channel-${channel.id}`).attr('title', channel.id);
+				).attr('href', `/guild/${guild.id}/settings/${channel.id}`).attr('title', channel.id);
 			} ),
 			} ),
-			( process.env.READONLY ? '' :
-			$('<a class="channel" id="channel-new">').attr('href', `/guild/${guild.id}/new`).append(
+			( process.env.READONLY || !guild.channels.filter( channel => {
+				return ( hasPerm(channel.permissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') && !rows.some( row => row.channel === channel.id ) );
+			} ).length ? '' :
+			$('<a class="channel" id="channel-new">').append(
 				$('<img>').attr('src', '/src/channel.svg'),
 				$('<img>').attr('src', '/src/channel.svg'),
 				$('<div>').text('New channel overwrite')
 				$('<div>').text('New channel overwrite')
-			) )
+			).attr('href', `/guild/${guild.id}/settings/new`) )
 		);
 		);
-		if ( args[3] === 'new' ) {
+		if ( args[4] === 'new' ) {
 			$('.channel#channel-new').addClass('selected');
 			$('.channel#channel-new').addClass('selected');
-			createForm($, 'New channel overwrite', Object.assign({}, rows.find( row => !row.channel ), {
+			createForm($, 'New Channel Overwrite', Object.assign({}, rows.find( row => !row.channel ), {
 				patreon: isPatreon,
 				patreon: isPatreon,
 				channel: 'new'
 				channel: 'new'
 			}), guild.channels.filter( channel => {
 			}), guild.channels.filter( channel => {
-				return hasPerm(channel.permissions, 'VIEW_CHANNEL', 'SEND_MESSAGES');
-			} )).attr('action', `/guild/${guild.id}`).appendTo('#text');
-			let body = $.html();
-			res.writeHead(200, {'Content-Length': body.length});
-			res.write( body );
-			return res.end();
+				return ( hasPerm(channel.permissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') && !rows.some( row => row.channel === channel.id ) );
+			} )).attr('action', `/guild/${guild.id}/settings/new`).appendTo('#text');
 		}
 		}
-		if ( channellist.some( channel => channel.id === args[3] ) ) {
-			let channel = channellist.find( channel => channel.id === args[3] );
+		else if ( channellist.some( channel => channel.id === args[4] ) ) {
+			let channel = channellist.find( channel => channel.id === args[4] );
 			$(`.channel#channel-${channel.id}`).addClass('selected');
 			$(`.channel#channel-${channel.id}`).addClass('selected');
 			createForm($, `#${channel.name} Settings`, Object.assign({}, rows.find( row => {
 			createForm($, `#${channel.name} Settings`, Object.assign({}, rows.find( row => {
 				return row.channel === channel.id;
 				return row.channel === channel.id;
 			} ), {
 			} ), {
 				patreon: isPatreon
 				patreon: isPatreon
-			}), [channel]).attr('action', `/guild/${guild.id}`).appendTo('#text');
-			let body = $.html();
-			res.writeHead(200, {'Content-Length': body.length});
-			res.write( body );
-			return res.end();
+			}), [channel]).attr('action', `/guild/${guild.id}/settings/${channel.id}`).appendTo('#text');
+		}
+		else {
+			$('.channel#settings').addClass('selected');
+			createForm($, 'Server-wide Settings', rows.find( row => !row.channel )).attr('action', `/guild/${guild.id}/settings/default`).appendTo('#text');
 		}
 		}
-		$('.channel#settings').addClass('selected');
-		createForm($, 'Server-wide Settings', rows.find( row => !row.channel )).attr('action', `/guild/${guild.id}`).appendTo('#text');
 		let body = $.html();
 		let body = $.html();
 		res.writeHead(200, {'Content-Length': body.length});
 		res.writeHead(200, {'Content-Length': body.length});
 		res.write( body );
 		res.write( body );

+ 23 - 2
dashboard/src/index.css

@@ -286,10 +286,11 @@ a:hover .description {
 	font-weight: bold;
 	font-weight: bold;
 	text-shadow: none;
 	text-shadow: none;
 }
 }
-fieldset div {
+fieldset > div {
 	margin: 10px 0;
 	margin: 10px 0;
 }
 }
-fieldset label {
+fieldset label,
+fieldset span {
 	display: inline-block;
 	display: inline-block;
 	min-width: 20%;
 	min-width: 20%;
 }
 }
@@ -297,6 +298,26 @@ fieldset input[type="url"] {
 	min-width: 30%;
 	min-width: 30%;
 	margin-right: 5px;
 	margin-right: 5px;
 }
 }
+.wb-settings-display:first-of-type {
+	display: inline-block;
+}
+.wb-settings-display:not(:first-of-type),
+.wb-settings-additional-select,
+button.addmore:not([hidden]) {
+	display: block;
+	margin-left: 20%;
+}
+.wb-settings-error {
+	color: red;
+}
+.wb-settings-error:disabled {
+	color: darkred;
+}
+#wb-settings-delete {
+	float: right;
+	color: red;
+	font-weight: bold;
+}
 #login-button {
 #login-button {
 	display: flex;
 	display: flex;
 	margin: 20px auto;
 	margin: 20px auto;

+ 95 - 3
dashboard/src/index.js

@@ -1,4 +1,4 @@
-const wiki = document.getElementById('wb-settings-wiki');
+/*const wiki = document.getElementById('wb-settings-wiki');
 if ( wiki ) wiki.addEventListener( 'input', function (event) {
 if ( wiki ) wiki.addEventListener( 'input', function (event) {
 	if ( wiki.validity.valid ) {
 	if ( wiki.validity.valid ) {
 		wiki.setCustomValidity('I am expecting an e-mail address!');
 		wiki.setCustomValidity('I am expecting an e-mail address!');
@@ -26,7 +26,7 @@ if ( form ) form.addEventListener( 'submit', function (event) {
 	}
 	}
 	else if ( wiki && wiki.validity.valid ) {
 	else if ( wiki && wiki.validity.valid ) {
 		wiki.value
 		wiki.value
-		fetch()/*
+		fetch()
 		got.get( wikinew + 'api.php?&action=query&meta=siteinfo&siprop=general&format=json' ).then( response => {
 		got.get( wikinew + 'api.php?&action=query&meta=siteinfo&siprop=general&format=json' ).then( response => {
 			if ( !isForced && response.statusCode === 404 && typeof response.body === 'string' ) {
 			if ( !isForced && response.statusCode === 404 && typeof response.body === 'string' ) {
 				let api = cheerio.load(response.body)('head link[rel="EditURI"]').prop('href');
 				let api = cheerio.load(response.body)('head link[rel="EditURI"]').prop('href');
@@ -65,7 +65,7 @@ if ( form ) form.addEventListener( 'submit', function (event) {
 			if ( reaction ) reaction.removeEmoji();
 			if ( reaction ) reaction.removeEmoji();
 			msg.reactEmoji('nowiki', true);
 			msg.reactEmoji('nowiki', true);
 			return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {}, true );
 			return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {}, true );
-		} );*/
+		} );
 	}
 	}
 	else form.dispatchEvent(new Event('submit'));
 	else form.dispatchEvent(new Event('submit'));
 } );
 } );
@@ -85,4 +85,96 @@ for ( var i = 0; i < collapsible.length; i++ ) {
 			content.style.display = 'block';
 			content.style.display = 'block';
 		}
 		}
 	}
 	}
+}
+*/
+
+var baseSelect = document.getElementsByTagName('select');
+for ( var b = 0; b < baseSelect.length; b++ ) {
+	if ( baseSelect[b].parentNode.querySelector('button.addmore') ) {
+		baseSelect[b].addEventListener( 'input', toggleOption );
+		toggleOption.call(baseSelect[b]);
+	}
+}
+
+var addmore = document.getElementsByClassName('addmore');
+for ( var j = 0; j < addmore.length; j++ ) {
+	addmore[j].onclick = function() {
+		var clone = this.previousElementSibling.cloneNode(true);
+		clone.classList.add('wb-settings-additional-select');
+		clone.removeAttribute('id');
+		clone.removeAttribute('required');
+		clone.childNodes.forEach( function(child) {
+			child.removeAttribute('hidden');
+			child.removeAttribute('selected');
+		} );
+		clone.querySelector('option.defaultSelect').setAttribute('selected', '');
+		clone.addEventListener( 'input', toggleOption );
+		this.before(clone);
+		toggleOption.call(clone);
+	}
+}
+
+/**
+ * @this HTMLSelectElement
+ */
+function toggleOption() {
+	var options = [];
+	var selected = [];
+	var allSelect = this.parentNode.querySelectorAll('select');
+	allSelect.forEach( function(select) {
+		options.push(...select.options);
+		selected.push(...select.selectedOptions);
+	} );
+	var button = this.parentNode.querySelector('button.addmore');
+	if ( selected.some( function(option) {
+		if ( option && option.value ) return false;
+		else return true;
+	} ) || allSelect.length >= 10 || allSelect.length >= this.options.length-1 ) {
+		button.setAttribute('hidden', '');
+	}
+	else button.removeAttribute('hidden');
+	selected = selected.filter( function(option) {
+		if ( option && option.value ) return true;
+		else return false;
+	} ).map( function(option) {
+		return option.value;
+	} );
+	options.forEach( function(option) {
+		if ( selected.includes(option.value) && !option.selected ) {
+			option.setAttribute('disabled', '');
+		}
+		else if ( option.disabled ) option.removeAttribute('disabled');
+	} );
+}
+
+const wiki = document.getElementById('wb-settings-wiki');
+if ( wiki ) {
+	const feeds = document.getElementById('wb-settings-feeds');
+	if ( feeds ) {
+		const hidefeeds = document.getElementById('wb-settings-feeds-hide');
+		const feedsonly = document.getElementById('wb-settings-feeds-only');
+		const hidefeedsonly = document.getElementById('wb-settings-feeds-only-hide');
+		feeds.addEventListener( 'change', function() {
+			if ( this.checked ) {
+				hidefeedsonly.removeAttribute('style');
+				if ( !hidefeeds.hasAttribute('style') ) feedsonly.removeAttribute('disabled');
+			}
+			else {
+				hidefeedsonly.setAttribute('style', 'visibility: hidden;');
+				feedsonly.setAttribute('disabled', '');
+			}
+		} );
+		wiki.addEventListener( 'input', function() {
+			if ( this.validity.valid && /\.(?:fandom\.com|wikia\.org)$/.test(new URL(this.value).hostname) ) {
+				hidefeeds.removeAttribute('style');
+				feeds.removeAttribute('disabled');
+				if ( !hidefeedsonly.hasAttribute('style') ) feedsonly.removeAttribute('disabled');
+			}
+			else {
+				hidefeeds.setAttribute('style', 'visibility: hidden;');
+				feeds.setAttribute('disabled', '');
+				feedsonly.setAttribute('disabled', '');
+			}
+		} );
+	}
 }
 }

+ 181 - 3
dashboard/verification.js

@@ -1,24 +1,202 @@
+const {limit: {verification: verificationLimit}} = require('../util/default.json');
 const {db, settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
 const {db, settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
 
 
+const fieldset = {
+	channel: '<label for="wb-settings-channel">Channel:</label>'
+	+ '<select id="wb-settings-channel" name="channel" required></select>'
+	+ '<button type="button" id="wb-settings-channel-more" class="addmore">Add more</button>',
+	role: '<label for="wb-settings-role">Role:</label>'
+	+ '<select id="wb-settings-role" name="role" required></select>'
+	+ '<button type="button" id="wb-settings-role-more" class="addmore">Add more</button>',
+	usergroup: '<label for="wb-settings-usergroup">Wiki user group:</label>'
+	+ '<input type="text" id="wb-settings-usergroup" name="usergroup" required>',
+	editcount: '<label for="wb-settings-editcount">Minimal edit count:</label>'
+	+ '<input type="number" id="wb-settings-editcount" name="editcount" min="0" required>',
+	accountage: '<label for="wb-settings-accountage">Account age (in days):</label>'
+	+ '<input type="number" id="wb-settings-accountage" name="accountage" min="0" required>',
+	rename: '<label for="wb-settings-rename">Rename users:</label>'
+	+ '<input type="checkbox" id="wb-settings-rename" name="rename">',
+	save: '<input type="submit" id="wb-settings-save" name="save-settings">',
+	delete: '<input type="submit" id="wb-settings-delete" name="delete-settings">'
+};
+
+/**
+ * Create a settings form
+ * @param {import('cheerio')} $ - The response body
+ * @param {String} header - The form header
+ * @param {Object} settings - The current settings
+ * @param {String} settings.channel
+ * @param {String} settings.role
+ * @param {String} settings.usergroup
+ * @param {Number} settings.editcount
+ * @param {Number} settings.accountage
+ * @param {Boolean} settings.rename
+ * @param {Object[]} guildChannels - The guild channels
+ * @param {String} guildChannels.id
+ * @param {String} guildChannels.name
+ * @param {Number} guildChannels.permissions
+ * @param {Object[]} guildRoles - The guild roles
+ * @param {String} guildRoles.id
+ * @param {String} guildRoles.name
+ * @param {Boolean} guildRoles.lower
+ */
+function createForm($, header, settings, guildChannels, guildRoles) {
+	var readonly = ( process.env.READONLY ? true : false );
+	var fields = [];
+	let channel = $('<div>').append(fieldset.channel);
+	channel.find('#wb-settings-channel').append(
+		$('<option class="wb-settings-channel-default defaultSelect" hidden>').val('').text('-- Select a Channel --'),
+		...guildChannels.filter( guildChannel => {
+			return ( hasPerm(guildChannel.permissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') || settings.channel.includes( '|' + guildChannel.id + '|' ) );
+		} ).map( guildChannel => {
+			var optionChannel = $(`<option class="wb-settings-channel-${guildChannel.id}">`).val(guildChannel.id);
+			if ( !hasPerm(guildChannel.permissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') ) {
+				optionChannel.addClass('wb-settings-error');
+			}
+			return optionChannel.text(`${guildChannel.id} – #${guildChannel.name}`);
+		} )
+	);
+	if ( settings.channel ) {
+		let settingsChannels = settings.channel.split('|').filter( guildChannel => guildChannel.length );
+		channel.find('#wb-settings-channel').append(
+			...settingsChannels.filter( guildChannel => {
+				return !channel.find(`.wb-settings-channel-${guildChannel}`).length;
+			} ).map( guildChannel => {
+				return $(`<option class="wb-settings-channel-${guildChannel}">`).val(guildChannel).text(`${guildChannel} – #UNKNOWN`).addClass('wb-settings-error');
+			} )
+		);
+		if ( settingsChannels.length > 1 ) channel.find('#wb-settings-channel').after(
+			...settingsChannels.slice(1).map( guildChannel => {
+				var additionalChannel = channel.find('#wb-settings-channel').clone();
+				additionalChannel.addClass('wb-settings-additional-select');
+				additionalChannel.find(`.wb-settings-channel-default`).removeAttr('hidden');
+				additionalChannel.find(`.wb-settings-channel-${guildChannel}`).attr('selected', '');
+				return additionalChannel.removeAttr('id').removeAttr('required');
+			} )
+		);
+		channel.find(`#wb-settings-channel .wb-settings-channel-${settingsChannels[0]}`).attr('selected', '');
+	}
+	else {
+		channel.find('.wb-settings-channel-default').attr('selected', '');
+		channel.find('button.addmore').attr('hidden', '');
+	}
+	fields.push(channel);
+	let role = $('<div>').append(fieldset.role);
+	role.find('#wb-settings-role').append(
+		$('<option class="wb-settings-role-default defaultSelect" hidden>').val('').text('-- Select a Role --'),
+		...guildRoles.filter( guildRole => {
+			return guildRole.lower || settings.role.split('|').includes( guildRole.id );
+		} ).map( guildRole => {
+			var optionRole = $(`<option class="wb-settings-role-${guildRole.id}">`).val(guildRole.id);
+			if ( !guildRole.lower ) optionRole.addClass('wb-settings-error');
+			return optionRole.text(`${guildRole.id} – @${guildRole.name}`);
+		} )
+	);
+	if ( settings.role ) {
+		let settingsRoles = settings.role.split('|');
+		role.find('#wb-settings-role').append(
+			...settingsRoles.filter( guildRole => {
+				return !role.find(`.wb-settings-role-${guildRole}`).length;
+			} ).map( guildRole => {
+				return $(`<option class="wb-settings-role-${guildRole}">`).val(guildRole).text(`${guildRole} – @UNKNOWN`).addClass('wb-settings-error');
+			} )
+		);
+		if ( settingsRoles.length > 1 ) role.find('#wb-settings-role').after(
+			...settingsRoles.slice(1).map( guildRole => {
+				var additionalRole = role.find('#wb-settings-role').clone();
+				additionalRole.addClass('wb-settings-additional-select');
+				additionalRole.find(`.wb-settings-role-default`).removeAttr('hidden');
+				additionalRole.find(`.wb-settings-role-${guildRole}`).attr('selected', '');
+				return additionalRole.removeAttr('id').removeAttr('required');
+			} )
+		);
+		role.find(`#wb-settings-role .wb-settings-role-${settingsRoles[0]}`).attr('selected', '');
+	}
+	else {
+		role.find('.wb-settings-role-default').attr('selected', '');
+		role.find('button.addmore').attr('hidden', '');
+	}
+	fields.push(role);
+	let usergroup = $('<div>').append(fieldset.usergroup);
+	usergroup.find('#wb-settings-usergroup').val(settings.usergroup.split('|').join(', '));
+	fields.push(usergroup);
+	let editcount = $('<div>').append(fieldset.editcount);
+	editcount.find('#wb-settings-editcount').val(settings.editcount);
+	fields.push(editcount);
+	let accountage = $('<div>').append(fieldset.accountage);
+	accountage.find('#wb-settings-accountage').val(settings.accountage);
+	fields.push(accountage);
+	let rename = $('<div>').append(fieldset.rename);
+	if ( settings.rename ) rename.find('#wb-settings-rename').attr('checked', '');
+	fields.push(rename);
+	fields.push($(fieldset.save).val('Save'));
+	if ( settings.channel ) {
+		fields.push($(fieldset.delete).val('Delete').attr('onclick', `return confirm('Are you sure?');`));
+	}
+	var form = $('<fieldset>').append(...fields);
+	if ( readonly ) {
+		form.find('input').attr('readonly', '');
+		form.find('input[type="checkbox"], option').attr('disabled', '');
+		form.find('input[type="submit"], button.addmore').remove();
+	}
+	return $('<form id="wb-settings" method="post" enctype="application/x-www-form-urlencoded">').append(
+		$('<h2>').text(header),
+		form
+	);
+}
+
 /**
 /**
  * Let a user change verifications
  * Let a user change verifications
  * @param {import('http').ServerResponse} res - The server response
  * @param {import('http').ServerResponse} res - The server response
- * @param {CheerioStatic} $ - The response body
+ * @param {import('cheerio')} $ - The response body
  * @param {import('./util.js').Guild} guild - The current guild
  * @param {import('./util.js').Guild} guild - The current guild
  * @param {String[]} args - The url parts
  * @param {String[]} args - The url parts
  */
  */
 function dashboard_verification(res, $, guild, args) {
 function dashboard_verification(res, $, guild, args) {
-	$('.channel#verification').addClass('selected');
 	db.all( 'SELECT configid, channel, role, editcount, usergroup, accountage, rename FROM verification WHERE guild = ? ORDER BY configid ASC', [guild.id], function(dberror, rows) {
 	db.all( 'SELECT configid, channel, role, editcount, usergroup, accountage, rename FROM verification WHERE guild = ? ORDER BY configid ASC', [guild.id], function(dberror, rows) {
 		if ( dberror ) {
 		if ( dberror ) {
 			console.log( '- Dashboard: Error while getting the verifications: ' + dberror );
 			console.log( '- Dashboard: Error while getting the verifications: ' + dberror );
 			$('#text .description').text('Failed to load the verifications!');
 			$('#text .description').text('Failed to load the verifications!');
+			$('.channel#verification').addClass('selected');
 			let body = $.html();
 			let body = $.html();
 			res.writeHead(200, {'Content-Length': body.length});
 			res.writeHead(200, {'Content-Length': body.length});
 			res.write( body );
 			res.write( body );
 			return res.end();
 			return res.end();
 		}
 		}
-		$('<pre>').text(JSON.stringify(rows, null, '\t')).appendTo('#text .description');
+		$('#text .description').text(`These are the verifications for "${guild.name}":`);
+		$('#channellist #verification').after(
+			...rows.map( row => {
+				return $('<a class="channel">').attr('id', `channel-${row.configid}`).append(
+					$('<img>').attr('src', '/src/channel.svg'),
+					$('<div>').text(`${row.configid} - ${( guild.roles.find( role => {
+						return role.id === row.role.split('|')[0];
+					} )?.name || guild.channels.find( channel => {
+						return channel.id === row.channel.split('|')[1];
+					} )?.name || row.usergroup.split('|')[0] )}`)
+				).attr('href', `/guild/${guild.id}/verification/${row.configid}`);
+			} ),
+			( process.env.READONLY || rows.length >= verificationLimit[( guild.patreon ? 'patreon' : 'default' )] ? '' :
+			$('<a class="channel" id="channel-new">').append(
+				$('<img>').attr('src', '/src/channel.svg'),
+				$('<div>').text('New verification')
+			).attr('href', `/guild/${guild.id}/verification/new`) )
+		);
+		if ( args[4] === 'new' ) {
+			$('.channel#channel-new').addClass('selected');
+			createForm($, 'New Verification', {
+				channel: '', role: '', usergroup: 'user',
+				editcount: 0, accountage: 0, rename: false
+			}, guild.channels, guild.roles).attr('action', `/guild/${guild.id}/verification/new`).appendTo('#text');
+		}
+		else if ( rows.some( row => row.configid == args[4] ) ) {
+			let row = rows.find( row => row.configid == args[4] );
+			$(`.channel#channel-${row.configid}`).addClass('selected');
+			createForm($, `Verification #${row.configid}`, row, guild.channels, guild.roles).attr('action', `/guild/${guild.id}/verification/${row.configid}`).appendTo('#text');
+		}
+		else {
+			$('.channel#verification').addClass('selected');
+			$('#text .description').text(`*Insert explanation about verification here*`);
+		}
 		let body = $.html();
 		let body = $.html();
 		res.writeHead(200, {'Content-Length': body.length});
 		res.writeHead(200, {'Content-Length': body.length});
 		res.write( body );
 		res.write( body );