Browse Source

add more useful error messages

Markus-Rost 4 years ago
parent
commit
9830c48b2f

+ 10 - 46
dashboard/guilds.js

@@ -22,56 +22,20 @@ const file = require('fs').readFileSync('./dashboard/index.html');
  * @param {import('http').ServerResponse} res - The server response
  * @param {String} state - The user state
  * @param {URL} reqURL - The used url
+ * @param {String} [action] - The action the user made
+ * @param {String[]} [actionArgs] - The arguments for the action
  */
-function dashboard_guilds(res, state, reqURL) {
+function dashboard_guilds(res, state, reqURL, action, actionArgs) {
+	reqURL.pathname = reqURL.pathname.replace( /^(\/(?:guild\/\d+(?:\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?)?)?)(?:\/.*)?$/, '$1' );
 	var args = reqURL.pathname.split('/');
+	args = reqURL.pathname.split('/');
 	var settings = settingsData.get(state);
 	var $ = cheerio.load(file);
-	if ( reqURL.searchParams.get('refresh') === 'success' ) {
-		createNotice($, {
-			type: 'success',
-			title: 'Refresh successful!',
-			text: 'Your server list has been successfully refeshed.'
-		}).prependTo('#text');
-	}
-	if ( reqURL.searchParams.get('refresh') === 'failed' ) {
-		createNotice($, {
-			type: 'error',
-			title: 'Refresh failed!',
-			text: 'You server list could not be refreshed, please try again.'
-		}).prependTo('#text');
-	}
-	if ( reqURL.searchParams.get('save') === 'success' ) {
-		$('<script>').text(`history.replaceState(null, null, '${reqURL.pathname}');`).insertBefore('script#indexjs');
-		createNotice($, {
-			type: 'success',
-			title: 'Settings saved!',
-			text: 'The settings have been updated successfully.'
-		}).prependTo('#text');
-	}
-	if ( reqURL.searchParams.get('save') === 'failed' ) {
-		$('<script>').text(`history.replaceState(null, null, '${reqURL.pathname}');`).insertBefore('script#indexjs');
-		createNotice($, {
-			type: 'error',
-			title: 'Save failed!',
-			text: 'The settings could not be saved, please try again.'
-		}).prependTo('#text');
-	}
-	if ( reqURL.searchParams.get('save') === 'partial' ) {
-		$('<script>').text(`history.replaceState(null, null, '${reqURL.pathname}');`).insertBefore('script#indexjs');
-		createNotice($, {
-			type: 'info',
-			title: 'Settings partially saved!',
-			text: 'The settings have only been partially updated.'
-		}).prependTo('#text');
-	}
-	if ( process.env.READONLY ) {
-		createNotice($, {
-			type: 'info',
-			title: 'Read-only database!',
-			text: 'You can currently only view your settings but not change them.'
-		}).prependTo('#text');
-	}
+	if ( process.env.READONLY ) createNotice($, 'readonly');
+	if ( action ) createNotice($, action, actionArgs);
+	$('head').append(
+		$('<script>').text(`history.replaceState(null, null, '${reqURL.pathname}');`)
+	);
 	$('#logout img').attr('src', settings.user.avatar);
 	$('#logout span').text(`${settings.user.username} #${settings.user.discriminator}`);
 	$('.guild#invite a').attr('href', oauth.generateAuthUrl( {

+ 1 - 0
dashboard/index.html

@@ -15,6 +15,7 @@
 </head>
 <body>
 	<div id="text">
+		<div id="notices"></div>
 		<div class="description"></div>
 	</div>
 	<div class="scrollbar" id="sidebar">

+ 11 - 8
dashboard/index.js

@@ -79,9 +79,11 @@ const server = http.createServer((req, res) => {
 
 			/**
 			 * @param {String} [resURL]
+			 * @param {String} [action]
+			 * @param {String[]} [actionArgs]
 			 */
-			function save_response(resURL = '/') {
-				return dashboard(res, state, new URL(resURL, process.env.dashboard));
+			function save_response(resURL = '/', action, ...actionArgs) {
+				return dashboard(res, state, new URL(resURL, process.env.dashboard), action, actionArgs);
 			}
 		}
 	}
@@ -122,7 +124,9 @@ const server = http.createServer((req, res) => {
 	} )?.map( cookie => cookie.replace( /^wikibot="(\w*(?:-\d+)?)"$/, '$1' ) )?.join();
 
 	if ( reqURL.pathname === '/login' ) {
-		return pages.login(res, state, reqURL.searchParams.get('action'));
+		let action = '';
+		if ( reqURL.searchParams.get('action') === 'failed' ) action = 'loginfail';
+		return pages.login(res, state, action);
 	}
 
 	if ( reqURL.pathname === '/logout' ) {
@@ -166,11 +170,10 @@ const server = http.createServer((req, res) => {
 		return pages.refresh(res, state, returnLocation);
 	}
 
-	if ( reqURL.pathname === '/' || reqURL.pathname.startsWith( '/guild/' ) ) {
-		return dashboard(res, state, reqURL);
-	}
-
-	return dashboard(res, state, new URL('/', process.env.dashboard));
+	let action = '';
+	if ( reqURL.searchParams.get('refresh') === 'success' ) action = 'refresh';
+	if ( reqURL.searchParams.get('refresh') === 'failed' ) action = 'refreshfail';
+	return dashboard(res, state, reqURL, action);
 });
 
 server.listen(8080, 'localhost', () => {

+ 1 - 0
dashboard/login.html

@@ -14,6 +14,7 @@
 </head>
 <body>
 	<div id="text">
+		<div id="notices"></div>
 		<div class="description">
 			<h2>Welcome on Wiki-Bot Dashboard.</h2>
 			<p>Wiki-Bot is a Discord bot made to bring Discord servers and MediaWiki wikis together. It helps with linking wiki pages, verifying wiki users, informing about latest changes on the wiki and more.</p>

+ 7 - 30
dashboard/oauth.js

@@ -29,36 +29,13 @@ function dashboard_login(res, state, action) {
 	var $ = cheerio.load(file);
 	let responseCode = 200;
 	let prompt = 'none';
-	if ( action === 'unauthorized' ) {
-		createNotice($, {
-			type: 'info',
-			title: 'Not logged in!',
-			text: 'Please login before you can change any settings.'
-		}).prependTo('#text');
-	}
-	if ( action === 'failed' ) {
-		responseCode = 400;
-		createNotice($, {
-			type: 'error',
-			title: 'Login failed!',
-			text: 'An error occurred while logging you in, please try again.'
-		}).prependTo('#text');
-	}
-	if ( action === 'logout' ) {
-		prompt = 'consent';
-		createNotice($, {
-			type: 'success',
-			title: 'Successfully logged out!',
-			text: 'You have been successfully logged out. To change any settings you need to login again.'
-		}).prependTo('#text');
-	}
-	if ( process.env.READONLY ) {
-		createNotice($, {
-			type: 'info',
-			title: 'Read-only database!',
-			text: 'You can currently only view your settings but not change them.'
-		}).prependTo('#text');
-	}
+	if ( process.env.READONLY ) createNotice($, 'readonly');
+	if ( action ) createNotice($, action);
+	if ( action === 'unauthorized' ) $('head').append(
+		$('<script>').text('history.replaceState(null, null, "/login");')
+	);
+	if ( action === 'logout' ) prompt = 'consent';
+	if ( action === 'loginfail' ) responseCode = 400;
 	state = crypto.randomBytes(16).toString("hex");
 	while ( settingsData.has(state) ) {
 		state = crypto.randomBytes(16).toString("hex");

+ 70 - 59
dashboard/rcscript.js

@@ -3,7 +3,7 @@ const {defaultSettings, limit: {rcgcdw: rcgcdwLimit}} = require('../util/default
 const Lang = require('../util/i18n.js');
 const allLangs = Lang.allLangs(true);
 const Wiki = require('../util/wiki.js');
-const {got, db, sendMsg, hasPerm} = require('./util.js');
+const {got, db, sendMsg, createNotice, hasPerm} = require('./util.js');
 
 const display_types = [
 	'compact',
@@ -16,13 +16,13 @@ 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>',
+	+ '<input type="url" id="wb-settings-wiki" name="wiki" required autocomplete="url">',
 	//+ '<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>'
+	+ '<select id="wb-settings-lang" name="lang" required autocomplete="language">'
 	+ Object.keys(allLangs.names).map( lang => {
 		return `<option id="wb-settings-lang-${lang}" value="${lang}">${allLangs.names[lang]}</option>`
 	} ).join('\n')
@@ -87,7 +87,7 @@ function createForm($, header, settings, guildChannels) {
 			} )
 		);
 		if ( !settings.channel ) channel.find('#wb-settings-channel').prepend(
-			$(`<option id="wb-settings-channel-default" selected>`).val('').text('-- Select a Channel --')
+			$(`<option id="wb-settings-channel-default" selected hidden>`).val('').text('-- Select a Channel --')
 		);
 	}
 	else if ( curChannel ) channel.find('#wb-settings-channel').append(
@@ -195,7 +195,7 @@ function dashboard_rcscript(res, $, guild, args) {
 					$('<div>').text('New webhook')
 				).attr('href', `/guild/${guild.id}/rcscript/new`) )
 			);
-			if ( args[4] === 'new' ) {
+			if ( args[4] === 'new' && !( process.env.READONLY || rows.length >= rcgcdwLimit[( guild.patreon ? 'patreon' : 'default' )] ) ) {
 				$('.channel#channel-new').addClass('selected');
 				createForm($, 'New Recent Changes Webhook', {
 					wiki, lang: ( lang in allLangs.names ? lang : defaultSettings.lang ),
@@ -239,25 +239,25 @@ function dashboard_rcscript(res, $, guild, args) {
  */
 function update_rcscript(res, userSettings, guild, type, settings) {
 	if ( type === 'default' ) {
-		return res(`/guild/${guild}/rcscript?save=failed`);
+		return res(`/guild/${guild}/rcscript`, 'savefail');
 	}
 	if ( !settings.save_settings === !settings.delete_settings ) {
-		return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+		return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 	}
 	if ( settings.save_settings ) {
 		if ( !settings.wiki || !( settings.lang in allLangs.names ) ) {
-			return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+			return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 		}
 		if ( !['0', '1', '2', '3'].includes( settings.display ) ) {
-			return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+			return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 		}
 		settings.display = parseInt(settings.display, 10);
 		if ( type === 'new' && !userSettings.guilds.isMember.get(guild).channels.some( channel => {
 			return ( channel.id === settings.channel );
-		} ) ) return res(`/guild/${guild}/rcscript/new?save=failed`);
+		} ) ) return res(`/guild/${guild}/rcscript/new`, 'savefail');
 	}
 	if ( settings.delete_settings && type === 'new' ) {
-		return res(`/guild/${guild}/rcscript/new?save=failed`);
+		return res(`/guild/${guild}/rcscript/new`, 'savefail');
 	}
 	if ( type === 'new' ) return sendMsg( {
 		type: 'getMember',
@@ -268,14 +268,14 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 		if ( !response ) {
 			userSettings.guilds.notMember.set(guild, userSettings.guilds.isMember.get(guild));
 			userSettings.guilds.isMember.delete(guild);
-			return res(`/guild/${guild}?save=failed`);
+			return res(`/guild/${guild}`, 'savefail');
 		}
 		if ( response === 'noMember' || !hasPerm(response.userPermissions, 'MANAGE_GUILD') ) {
 			userSettings.guilds.isMember.delete(guild);
-			return res('/?save=failed');
+			return res('/', 'savefail');
 		}
 		if ( response.message === 'noChannel' || !hasPerm(response.botPermissions, 'MANAGE_WEBHOOKS') || !hasPerm(response.userPermissions, 'VIEW_CHANNEL', 'MANAGE_WEBHOOKS') ) {
-			return res(`/guild/${guild}/rcscript/new?save=failed`);
+			return res(`/guild/${guild}/rcscript/new`, 'savefail');
 		}
 		if ( settings.display > rcgcdwLimit.display && !response.patreon ) {
 			settings.display = rcgcdwLimit.display;
@@ -283,13 +283,13 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 		return db.get( 'SELECT discord.lang, GROUP_CONCAT(configid) count FROM discord LEFT JOIN rcgcdw ON discord.guild = rcgcdw.guild WHERE discord.guild = ? AND discord.channel IS NULL', [guild], function(curerror, row) {
 			if ( curerror ) {
 				console.log( '- Dashboard: Error while checking for RcGcDw: ' + curerror );
-				return res(`/guild/${guild}/rcscript/new?save=failed`);
+				return res(`/guild/${guild}/rcscript/new`, 'savefail');
 			}
-			if ( !row ) return res(`/guild/${guild}/rcscript?save=failed`);
+			if ( !row ) return res(`/guild/${guild}/rcscript`, 'savefail');
 			if ( row.count === null ) row.count = [];
 			else row.count = row.count.split(',').map( configid => parseInt(configid, 10) );
 			if ( row.count.length >= rcgcdwLimit[( response.patreon ? 'patreon' : 'default' )] ) {
-				return res(`/guild/${guild}/rcscript?save=failed`);
+				return res(`/guild/${guild}/rcscript`, 'savefail');
 			}
 			var wiki = Wiki.fromInput(settings.wiki);
 			return got.get( wiki + 'api.php?&action=query&meta=allmessages|siteinfo&ammessages=custom-RcGcDw|recentchanges&amenableparser=true&siprop=general' + ( wiki.isFandom() ? '|variables' : '' ) + '&titles=Special:RecentChanges&format=json' ).then( fresponse => {
@@ -305,21 +305,24 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 				var body = fresponse.body;
 				if ( fresponse.statusCode !== 200 || !body?.query?.allmessages || !body?.query?.general || !body?.query?.pages?.['-1'] ) {
 					console.log( '- Dashboard: ' + fresponse.statusCode + ': Error while testing the wiki: ' + body?.error?.info );
-					return res(`/guild/${guild}/rcscript/new?save=failed`);
+					return res(`/guild/${guild}/rcscript/new`, 'savefail');
 				}
 				wiki.updateWiki(body.query.general);
 				if ( body.query.general.generator.replace( /^MediaWiki 1\.(\d\d).*$/, '$1' ) < 30 ) {
-					return res(`/guild/${guild}/rcscript/new?save=failed`);
+					return res(`/guild/${guild}/rcscript/new`, 'mwversion', body.query.general.generator, body.query.general.sitename);
 				}
 				if ( body.query.allmessages[0]['*'] !== guild ) {
-					return res(`/guild/${guild}/rcscript/new?save=failed`);
+					return res(`/guild/${guild}/rcscript/new`, 'sysmessage', guild, wiki.toLink('MediaWiki:Custom-RcGcDw', 'action=edit'));
 				}
 				return db.get( 'SELECT reason FROM blocklist WHERE wiki = ?', [wiki.href], (blerror, block) => {
 					if ( blerror ) {
 						console.log( '- Dashboard: Error while getting the blocklist: ' + blerror );
-						return res(`/guild/${guild}/rcscript/new?save=failed`);
+						return res(`/guild/${guild}/rcscript/new`, 'savefail');
+					}
+					if ( block ) {
+						console.log( `- Dashboard: ${wiki.href} is blocked: ${block.reason}` );
+						return res(`/guild/${guild}/rcscript/new`, 'wikiblocked', body.query.general.sitename, block.reason);
 					}
-					if ( block ) return res(`/guild/${guild}/rcscript/new?save=failed`);
 					if ( settings.feeds ) {
 						let wikiid = body.query.variables?.find?.( variable => variable?.id === 'wgCityId' )?.['*'];
 						if ( wiki.isFandom(false) && wikiid ) return got.get( 'https://services.fandom.com/discussion/' + wikiid + '/posts?limit=1&format=json&cache=' + Date.now(), {
@@ -355,7 +358,7 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 							reason: lang.get('rcscript.audit_reason', wiki.href),
 							text: webhook_lang.get('created', body.query.general.sitename) + ( wikiid && settings.feeds_only ? '' : `\n<${wiki.toLink(body.query.pages['-1'].title)}>` ) + ( wikiid ? `\n<${wiki.href}f>` : '' )
 						} ).then( webhook => {
-							if ( !webhook ) return res(`/guild/${guild}/rcscript/new?save=failed`);
+							if ( !webhook ) return res(`/guild/${guild}/rcscript/new`, 'savefail');
 							var configid = 1;
 							for ( let i of row.count ) {
 								if ( configid === i ) configid++;
@@ -364,10 +367,10 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 							db.run( 'INSERT INTO rcgcdw(guild, configid, webhook, wiki, lang, display, wikiid, rcid) VALUES(?, ?, ?, ?, ?, ?, ?, ?)', [guild, configid, webhook, wiki.href, settings.lang, settings.display, wikiid, ( wikiid && settings.feeds_only ? -1 : null )], function (dberror) {
 								if ( dberror ) {
 									console.log( '- Dashboard: Error while adding the RcGcDw: ' + dberror );
-									return res(`/guild/${guild}/rcscript/new?save=failed`);
+									return res(`/guild/${guild}/rcscript/new`, 'savefail');
 								}
 								console.log( `- Dashboard: RcGcDw successfully added: ${guild}#${configid}` );
-								res(`/guild/${guild}/rcscript/${configid}?save=success`);
+								res(`/guild/${guild}/rcscript/${configid}`, 'save');
 								var text = lang.get('rcscript.dashboard.added', `<@${userSettings.user.id}>`, configid);
 								text += `\n${lang.get('rcscript.channel')} <#${settings.channel}>`;
 								text += `\n${lang.get('rcscript.wiki')} <${wiki.href}>`;
@@ -384,37 +387,37 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 							} );
 						}, error => {
 							console.log( '- Dashboard: Error while creating the webhook: ' + error );
-							return res(`/guild/${guild}/rcscript/new?save=failed`);
+							return res(`/guild/${guild}/rcscript/new`, 'savefail');
 						} );
 					}
 				} );
 			}, error => {
 				console.log( '- Dashboard: Error while testing the wiki: ' + error );
-				return res(`/guild/${guild}/rcscript/new?save=failed`);
+				return res(`/guild/${guild}/rcscript/new`, 'savefail');
 			} );
 		} );
 	}, error => {
 		console.log( '- Dashboard: Error while getting the member: ' + error );
-		return res(`/guild/${guild}/rcscript/new?save=failed`);
+		return res(`/guild/${guild}/rcscript/new`, 'savefail');
 	} );
 	type = parseInt(type, 10);
 	return db.get( 'SELECT discord.lang mainlang, webhook, rcgcdw.wiki, rcgcdw.lang, display, wikiid, rcid FROM discord LEFT JOIN rcgcdw ON discord.guild = rcgcdw.guild AND configid = ? WHERE discord.guild = ? AND discord.channel IS NULL', [type, guild], function(curerror, row) {
 		if ( curerror ) {
 			console.log( '- Dashboard: Error while checking for RcGcDw: ' + curerror );
-			return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+			return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 		}
-		if ( !row?.webhook ) return res(`/guild/${guild}/rcscript?save=failed`);
+		if ( !row?.webhook ) return res(`/guild/${guild}/rcscript`, 'savefail');
 		return got.get( 'https://discord.com/api/webhooks/' + row.webhook ).then( wresponse => {
 			if ( !wresponse.body?.channel_id ) {
 				console.log( '- Dashboard: ' + wresponse.statusCode + ': Error while getting the webhook: ' + wresponse.body?.message );
-				return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+				return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 			}
 			row.channel = wresponse.body.channel_id;
 			var newChannel = false;
 			if ( settings.save_settings && row.channel !== settings.channel ) {
 				if ( !userSettings.guilds.isMember.get(guild).channels.some( channel => {
 					return ( channel.id === settings.channel );
-				} ) ) return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+				} ) ) return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 				newChannel = true;
 			}
 			return sendMsg( {
@@ -427,26 +430,26 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 				if ( !response ) {
 					userSettings.guilds.notMember.set(guild, userSettings.guilds.isMember.get(guild));
 					userSettings.guilds.isMember.delete(guild);
-					return res(`/guild/${guild}?save=failed`);
+					return res(`/guild/${guild}`, 'savefail');
 				}
 				if ( response === 'noMember' || !hasPerm(response.userPermissions, 'MANAGE_GUILD') ) {
 					userSettings.guilds.isMember.delete(guild);
-					return res('/?save=failed');
+					return res('/', 'savefail');
 				}
 				if ( response.message === 'noChannel' ) {
-					return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+					return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 				}
 				if ( settings.delete_settings ) {
 					if ( !hasPerm(response.userPermissions, 'VIEW_CHANNEL', 'MANAGE_WEBHOOKS') ) {
-						return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+						return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 					}
 					return db.run( 'DELETE FROM rcgcdw WHERE webhook = ?', [row.webhook], function (delerror) {
 						if ( delerror ) {
 							console.log( '- Dashboard: Error while removing the RcGcDw: ' + delerror );
-							return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+							return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 						}
 						console.log( `- Dashboard: RcGcDw successfully removed: ${guild}#${type}` );
-						res(`/guild/${guild}/rcscript?save=success`);
+						res(`/guild/${guild}/rcscript`, 'save');
 						var lang = new Lang(row.mainlang);
 						var webhook_lang = new Lang(row.lang, 'rcscript.webhook');
 						got.post( 'https://discord.com/api/webhooks/' + row.webhook, {
@@ -494,16 +497,16 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 				|| !hasPerm(response.userPermissions, 'VIEW_CHANNEL', 'MANAGE_WEBHOOKS') 
 				|| !hasPerm(response.userPermissionsNew, 'VIEW_CHANNEL', 'MANAGE_WEBHOOKS') 
 				|| !hasPerm(response.botPermissionsNew, 'MANAGE_WEBHOOKS') ) ) {
-					return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+					return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 				}
-				var diff = false;
-				if ( newChannel ) diff = true;
-				if ( row.wiki !== settings.wiki ) diff = true;
-				if ( row.lang !== settings.lang ) diff = true;
-				if ( row.display !== settings.display ) diff = true;
-				if ( ( row.rcid !== -1 ) !== !( settings.feeds && settings.feeds_only ) ) diff = true;
-				if ( !row.wikiid !== !settings.feeds ) diff = true;
-				if ( !diff ) return res(`/guild/${guild}/rcscript/${type}?save=success`);
+				var hasDiff = false;
+				if ( newChannel ) hasDiff = true;
+				if ( row.wiki !== settings.wiki ) hasDiff = true;
+				if ( row.lang !== settings.lang ) hasDiff = true;
+				if ( row.display !== settings.display ) hasDiff = true;
+				if ( ( row.rcid !== -1 ) !== !( settings.feeds && settings.feeds_only ) ) hasDiff = true;
+				if ( !row.wikiid !== !settings.feeds ) hasDiff = true;
+				if ( !hasDiff ) return res(`/guild/${guild}/rcscript/${type}`, 'save');
 				var wiki = Wiki.fromInput(settings.wiki);
 				return got.get( wiki + 'api.php?&action=query&meta=allmessages|siteinfo&ammessages=custom-RcGcDw&amenableparser=true&siprop=general' + ( wiki.isFandom() ? '|variables' : '' ) + '&format=json' ).then( fresponse => {
 					if ( fresponse.statusCode === 404 && typeof fresponse.body === 'string' ) {
@@ -518,21 +521,24 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 					var body = fresponse.body;
 					if ( fresponse.statusCode !== 200 || !body?.query?.allmessages || !body?.query?.general ) {
 						console.log( '- Dashboard: ' + fresponse.statusCode + ': Error while testing the wiki: ' + body?.error?.info );
-						return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+						return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 					}
 					wiki.updateWiki(body.query.general);
 					if ( body.query.general.generator.replace( /^MediaWiki 1\.(\d\d).*$/, '$1' ) < 30 ) {
-						return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+						return res(`/guild/${guild}/rcscript/${type}`, 'mwversion', body.query.general.generator, body.query.general.sitename);
 					}
 					if ( row.wiki !== wiki.href && body.query.allmessages[0]['*'] !== guild ) {
-						return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+						return res(`/guild/${guild}/rcscript/${type}`, 'sysmessage', guild, wiki.toLink('MediaWiki:Custom-RcGcDw', 'action=edit'));
 					}
 					return db.get( 'SELECT reason FROM blocklist WHERE wiki = ?', [wiki.href], (blerror, block) => {
 						if ( blerror ) {
 							console.log( '- Dashboard: Error while getting the blocklist: ' + blerror );
-							return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+							return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
+						}
+						if ( block ) {
+							console.log( `- Dashboard: ${wiki.href} is blocked: ${block.reason}` );
+							return res(`/guild/${guild}/rcscript/${type}`, 'wikiblocked', body.query.general.sitename, block.reason);
 						}
-						if ( block ) return res(`/guild/${guild}/rcscript/${type}?save=failed`);
 						if ( settings.feeds ) {
 							let wikiid = body.query.variables?.find?.( variable => variable?.id === 'wgCityId' )?.['*'];
 							if ( wiki.isFandom(false) && wikiid ) return got.get( 'https://services.fandom.com/discussion/' + wikiid + '/posts?limit=1&format=json&cache=' + Date.now(), {
@@ -561,7 +567,7 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 							db.run( 'UPDATE rcgcdw SET wiki = ?, lang = ?, display = ?, wikiid = ?, rcid = ? WHERE webhook = ?', [wiki.href, settings.lang, settings.display, wikiid, ( wikiid && settings.feeds_only ? -1 : null ), row.webhook], function (dberror) {
 								if ( dberror ) {
 									console.log( '- Dashboard: Error while updating the RcGcDw: ' + dberror );
-									return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+									return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 								}
 								console.log( `- Dashboard: RcGcDw successfully updated: ${guild}#${type}` );
 								var lang = new Lang(row.mainlang);
@@ -601,7 +607,7 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 									text: webhook_lang.get('dashboard.updated') + '\n' + webhook_diff.join('\n')
 								} ).then( webhook => {
 									if ( !webhook ) return Promise.reject();
-									res(`/guild/${guild}/rcscript/${type}?save=success`);
+									res(`/guild/${guild}/rcscript/${type}`, 'save');
 									var text = lang.get('rcscript.dashboard.updated', `<@${userSettings.user.id}>`, type);
 									text += '\n' + diff.join('\n');
 									text += `\n<${new URL(`/guild/${guild}/rcscript/${type}`, process.env.dashboard).href}>`;
@@ -614,7 +620,12 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 									console.log( '- Dashboard: Error while moving the webhook: ' + error );
 									return Promise.reject();
 								} ).catch( () => {
-									res(`/guild/${guild}/rcscript/${type}?save=partial`);
+									diff.shift();
+									webhook_diff.shift();
+									if ( !diff.length ) {
+										return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
+									}
+									res(`/guild/${guild}/rcscript/${type}`, 'movefail');
 									diff.shift();
 									webhook_diff.shift();
 									got.post( 'https://discord.com/api/webhooks/' + row.webhook, {
@@ -637,7 +648,7 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 										console.log( '- Dashboard: Error while notifying the guild: ' + error );
 									} );
 								} );
-								res(`/guild/${guild}/rcscript/${type}?save=success`);
+								res(`/guild/${guild}/rcscript/${type}`, 'save');
 								got.post( 'https://discord.com/api/webhooks/' + row.webhook, {
 									json: {
 										content: webhook_lang.get('dashboard.updated') + '\n' + webhook_diff.join('\n')
@@ -662,15 +673,15 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 					} );
 				}, error => {
 					console.log( '- Dashboard: Error while testing the wiki: ' + error );
-					return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+					return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 				} );
 			}, error => {
 				console.log( '- Dashboard: Error while getting the member: ' + error );
-				return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+				return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 			} );
 		}, error => {
 			console.log( '- Dashboard: Error while getting the webhook: ' + error );
-			return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+			return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 		} );
 	} );
 }

+ 39 - 38
dashboard/settings.js

@@ -9,13 +9,13 @@ 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">Default Wiki:</label>'
-	+ '<input type="url" id="wb-settings-wiki" name="wiki" required>',
+	+ '<input type="url" id="wb-settings-wiki" name="wiki" required autocomplete="url">',
 	//+ '<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>'
+	+ '<select id="wb-settings-lang" name="lang" required autocomplete="language">'
 	+ Object.keys(allLangs.names).map( lang => {
 		return `<option id="wb-settings-lang-${lang}" value="${lang}">${allLangs.names[lang]}</option>`
 	} ).join('\n')
@@ -23,7 +23,7 @@ const fieldset = {
 	role: '<label for="wb-settings-role">Minimal Role:</label>'
 	+ '<select id="wb-settings-role" name="role"></select>',
 	prefix: '<label for="wb-settings-prefix">Prefix:</label>'
-	+ '<input type="text" id="wb-settings-prefix" name="prefix" pattern="^\\s*[^\\s`\\\\]{1,100}\\s*$" required>'
+	+ '<input type="text" id="wb-settings-prefix" name="prefix" pattern="^\\s*[^\\s`\\\\]{1,100}\\s*$" minlength="1" maxlength="100" required autocomplete="on">'
 	+ '<br>'
 	+ '<label for="wb-settings-prefix-space">Prefix ends with space:</label>'
 	+ '<input type="checkbox" id="wb-settings-prefix-space" name="prefix_space">',
@@ -73,7 +73,7 @@ function createForm($, header, settings, guildRoles, guildChannels = []) {
 			}
 		}
 		else channel.find('#wb-settings-channel').prepend(
-			$(`<option id="wb-settings-channel-default" selected>`).val('').text('-- Select a Channel --')
+			$(`<option id="wb-settings-channel-default" selected hidden>`).val('').text('-- Select a Channel --')
 		);
 		fields.push(channel);
 	}
@@ -86,10 +86,11 @@ function createForm($, header, settings, guildRoles, guildChannels = []) {
 		fields.push(lang);
 		let role = $('<div>').append(fieldset.role);
 		role.find('#wb-settings-role').append(
+			$(`<option id="wb-settings-role-default">`).val('').text(`@everyone`),
 			...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', '');
@@ -173,7 +174,7 @@ function dashboard_settings(res, $, guild, args) {
 				$('<div>').text('New channel overwrite')
 			).attr('href', `/guild/${guild.id}/settings/new`) )
 		);
-		if ( args[4] === 'new' ) {
+		if ( args[4] === 'new' && !process.env.READONLY ) {
 			$('.channel#channel-new').addClass('selected');
 			createForm($, 'New Channel Overwrite', Object.assign({}, rows.find( row => !row.channel ), {
 				patreon: isPatreon,
@@ -221,24 +222,24 @@ function dashboard_settings(res, $, guild, args) {
  */
 function update_settings(res, userSettings, guild, type, settings) {
 	if ( type !== 'default' && type !== 'new' && type !== settings.channel ) {
-		return res(`/guild/${guild}/settings?save=failed`);
+		return res(`/guild/${guild}/settings`, 'savefail');
 	}
 	if ( !settings.save_settings === !settings.delete_settings ) {
-		return res(`/guild/${guild}/settings/${type}?save=failed`);
+		return res(`/guild/${guild}/settings/${type}`, 'savefail');
 	}
 	if ( settings.save_settings ) {
 		if ( !settings.wiki || ( settings.lang && !( settings.lang in allLangs.names ) ) ) {
-			return res(`/guild/${guild}/settings/${type}?save=failed`);
+			return res(`/guild/${guild}/settings/${type}`, 'savefail');
 		}
 		if ( settings.channel && !userSettings.guilds.isMember.get(guild).channels.some( channel => {
 			return ( channel.id === settings.channel );
-		} ) ) return res(`/guild/${guild}/settings/${type}?save=failed`);
+		} ) ) return res(`/guild/${guild}/settings/${type}`, 'savefail');
 		if ( settings.role && !userSettings.guilds.isMember.get(guild).roles.some( role => {
 			return ( role.id === settings.role );
-		} ) ) return res(`/guild/${guild}/settings/${type}?save=failed`);
+		} ) ) return res(`/guild/${guild}/settings/${type}`, 'savefail');
 	}
 	if ( settings.delete_settings && ( type === 'default' || type === 'new' ) ) {
-		return res(`/guild/${guild}/settings/${type}?save=failed`);
+		return res(`/guild/${guild}/settings/${type}`, 'savefail');
 	}
 	sendMsg( {
 		type: 'getMember',
@@ -249,32 +250,32 @@ function update_settings(res, userSettings, guild, type, settings) {
 		if ( !response ) {
 			userSettings.guilds.notMember.set(guild, userSettings.guilds.isMember.get(guild));
 			userSettings.guilds.isMember.delete(guild);
-			return res(`/guild/${guild}?save=failed`);
+			return res(`/guild/${guild}`, 'savefail');
 		}
 		if ( response === 'noMember' || !hasPerm(response.userPermissions, 'MANAGE_GUILD') ) {
 			userSettings.guilds.isMember.delete(guild);
-			return res('/?save=failed');
+			return res('/', 'savefail');
 		}
 		if ( response.message === 'noChannel' ) return db.run( 'DELETE FROM discord WHERE guild = ? AND channel = ?', [guild, type], function (delerror) {
 			if ( delerror ) {
 				console.log( '- Dashboard: Error while removing the settings: ' + delerror );
-				return res(`/guild/${guild}/settings?save=failed`);
+				return res(`/guild/${guild}/settings`, 'savefail');
 			}
 			console.log( `- Dashboard: Settings successfully removed: ${guild}#${type}` );
-			if ( settings.delete_settings ) return res(`/guild/${guild}/settings?save=success`);
-			else return res(`/guild/${guild}/settings?save=failed`);
+			if ( settings.delete_settings ) return res(`/guild/${guild}/settings`, 'save');
+			else return res(`/guild/${guild}/settings`, 'savefail');
 		} );
 		if ( type === settings.channel && !hasPerm(response.userPermissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') ) {
-			return res(`/guild/${guild}/settings/${type}?save=failed`);
+			return res(`/guild/${guild}/settings/${type}`, 'savefail');
 		}
 		if ( settings.delete_settings ) return db.get( 'SELECT main.lang mainlang, main.patreon, main.lang mainwiki, main.role mainrole, main.inline maininline, old.wiki, old.lang, old.role, old.inline FROM discord main LEFT JOIN discord old ON main.guild = old.guild AND old.channel = ? WHERE main.guild = ? AND main.channel IS NULL', [type, guild], function(dberror, row) {
 			db.run( 'DELETE FROM discord WHERE guild = ? AND channel = ?', [guild, type], function (delerror) {
 				if ( delerror ) {
 					console.log( '- Dashboard: Error while removing the settings: ' + delerror );
-					return res(`/guild/${guild}/settings/${type}?save=failed`);
+					return res(`/guild/${guild}/settings/${type}`, 'savefail');
 				}
 				console.log( `- Dashboard: Settings successfully removed: ${guild}#${type}` );
-				res(`/guild/${guild}/settings?save=success`);
+				res(`/guild/${guild}/settings`, 'save');
 				if ( dberror ) {
 					console.log( '- Dashboard: Error while notifying the guild: ' + dberror );
 					return;
@@ -361,12 +362,12 @@ function update_settings(res, userSettings, guild, type, settings) {
 				}
 			}
 			if ( type === 'default' ) {
-				if ( settings.channel || !settings.lang || ( !response.patreon && settings.prefix ) ) {
-					return res(`/guild/${guild}/settings?save=failed`);
+				if ( settings.channel || !settings.lang || ( !response.patreon !== !settings.prefix ) ) {
+					return res(`/guild/${guild}/settings`, 'savefail');
 				}
 				if ( settings.prefix ) {
 					if ( !/^\s*[^\s`\\]{1,100}\s*$/.test(settings.prefix) ) {
-						return res(`/guild/${guild}/settings?save=failed`);
+						return res(`/guild/${guild}/settings`, 'savefail');
 					}
 					settings.prefix = settings.prefix.trim().toLowerCase();
 					if ( settings.prefix_space ) settings.prefix += ' ';
@@ -374,10 +375,10 @@ function update_settings(res, userSettings, guild, type, settings) {
 				if ( !row ) return db.run( 'INSERT INTO discord(wiki, lang, role, inline, prefix, guild, main) VALUES(?, ?, ?, ?, ?, ?)', [wiki.href, settings.lang, ( settings.role || null ), ( settings.inline ? null : 1 ), ( settings.prefix || process.env.prefix ), guild, guild], function(dberror) {
 					if ( dberror ) {
 						console.log( '- Dashboard: Error while saving the settings: ' + dberror );
-						return res(`/guild/${guild}/settings?save=failed`);
+						return res(`/guild/${guild}/settings`, 'savefail');
 					}
 					console.log( '- Dashboard: Settings successfully saved: ' + guild );
-					res(`/guild/${guild}/settings?save=success`);
+					res(`/guild/${guild}/settings`, 'save');
 					var text = lang.get('settings.dashboard.updated', `<@${userSettings.user.id}>`);
 					text += '\n' + lang.get('settings.currentwiki') + ` <${wiki.href}>`;
 					text += '\n' + lang.get('settings.currentlang') + ` \`${allLangs.names[settings.lang]}\``;
@@ -413,10 +414,10 @@ function update_settings(res, userSettings, guild, type, settings) {
 				if ( diff.length ) return db.run( 'UPDATE discord SET wiki = ?, lang = ?, role = ?, inline = ?, prefix = ? WHERE guild = ? AND channel IS NULL', [wiki.href, settings.lang, ( settings.role || null ), ( settings.inline ? null : 1 ), ( settings.prefix || process.env.prefix ), guild], function(dberror) {
 					if ( dberror ) {
 						console.log( '- Dashboard: Error while saving the settings: ' + dberror );
-						return res(`/guild/${guild}/settings?save=failed`);
+						return res(`/guild/${guild}/settings`, 'savefail');
 					}
 					console.log( '- Dashboard: Settings successfully saved: ' + guild );
-					res(`/guild/${guild}/settings?save=success`);
+					res(`/guild/${guild}/settings`, 'save');
 					var text = lang.get('settings.dashboard.updated', `<@${userSettings.user.id}>`);
 					text += '\n' + diff.join('\n');
 					text += `\n<${new URL(`/guild/${guild}/settings`, process.env.dashboard).href}>`;
@@ -427,24 +428,24 @@ function update_settings(res, userSettings, guild, type, settings) {
 						console.log( '- Dashboard: Error while notifying the guild: ' + error );
 					} );
 				} );
-				return res(`/guild/${guild}/settings?save=success`);
+				return res(`/guild/${guild}/settings`, 'save');
 			}
 			if ( !row || !settings.channel || settings.prefix || 
 			( !response.patreon && ( settings.lang || settings.role || settings.inline ) ) ) {
-				return res(`/guild/${guild}/settings?save=failed`);
+				return res(`/guild/${guild}/settings`, 'savefail');
 			}
 			if ( row.wiki === wiki.href && ( !response.patreon || 
 			( row.lang === settings.lang && row.inline === ( settings.inline ? null : 1 ) && row.role === ( settings.role || null ) ) ) ) {
 				if ( type === 'new' ) {
-					return res(`/guild/${guild}/settings/${type}?save=failed`);
+					return res(`/guild/${guild}/settings/${type}`, 'nochange');
 				}
 				return db.run( 'DELETE FROM discord WHERE guild = ? AND channel = ?', [guild, type], function (delerror) {
 					if ( delerror ) {
 						console.log( '- Dashboard: Error while removing the settings: ' + delerror );
-						return res(`/guild/${guild}/settings/${type}?save=failed`);
+						return res(`/guild/${guild}/settings/${type}`, 'savefail');
 					}
 					console.log( `- Dashboard: Settings successfully removed: ${guild}#${type}` );
-					res(`/guild/${guild}/settings?save=success`);
+					res(`/guild/${guild}/settings`, 'save');
 					var text = lang.get('settings.dashboard.removed', `<@${userSettings.user.id}>`, `<#${type}>`);
 					text += `\n<${new URL(`/guild/${guild}/settings`, process.env.dashboard).href}>`;
 					sendMsg( {
@@ -457,7 +458,7 @@ function update_settings(res, userSettings, guild, type, settings) {
 			return db.get( 'SELECT lang, wiki, role, inline FROM discord WHERE guild = ? AND channel = ?', [guild, settings.channel], function(curerror, channel) {
 				if ( curerror ) {
 					console.log( '- Dashboard: Error while getting the channel settings: ' + curerror );
-					return res(`/guild/${guild}/settings/${type}?save=failed`);
+					return res(`/guild/${guild}/settings/${type}`, 'savefail');
 				}
 				if ( !channel ) channel = row;
 				var diff = [];
@@ -475,7 +476,7 @@ function update_settings(res, userSettings, guild, type, settings) {
 					diff.push(lang.get('settings.currentinline') + ` ${( channel.inline ? '~~' : '' )}\`[[${inlinepage}]]\`${( channel.inline ? '~~' : '' )} → ${( settings.inline ? '' : '~~' )}\`[[${inlinepage}]]\`${( settings.inline ? '' : '~~' )}`);
 				}
 				if ( !diff.length ) {
-					return res(`/guild/${guild}/settings/${settings.channel}?save=success`);
+					return res(`/guild/${guild}/settings/${settings.channel}`, 'save');
 				}
 				let sql = 'UPDATE discord SET wiki = ?, lang = ?, role = ?, inline = ? WHERE guild = ? AND channel = ?';
 				let sqlargs = [wiki.href, ( settings.lang || channel.lang ), ( response.patreon ? ( settings.role || null ) : channel.role ), ( response.patreon ? ( settings.inline ? null : 1 ) : channel.inline ), guild, settings.channel];
@@ -486,10 +487,10 @@ function update_settings(res, userSettings, guild, type, settings) {
 				return db.run( sql, sqlargs, function(dberror) {
 					if ( dberror ) {
 						console.log( '- Dashboard: Error while saving the settings: ' + dberror );
-						return res(`/guild/${guild}/settings/${type}?save=failed`);
+						return res(`/guild/${guild}/settings/${type}`, 'savefail');
 					}
 					console.log( `- Dashboard: Settings successfully saved: ${guild}#${settings.channel}` );
-					res(`/guild/${guild}/settings/${settings.channel}?save=success`);
+					res(`/guild/${guild}/settings/${settings.channel}`, 'save');
 					var text = lang.get('settings.dashboard.channel', `<@${userSettings.user.id}>`, `<#${settings.channel}>`);
 					text += '\n' + diff.join('\n');
 					text += `\n<${new URL(`/guild/${guild}/settings/${settings.channel}`, process.env.dashboard).href}>`;
@@ -501,11 +502,11 @@ function update_settings(res, userSettings, guild, type, settings) {
 				} );
 			} );
 		}, () => {
-			return res(`/guild/${guild}/settings/${type}?save=failed`);
+			return res(`/guild/${guild}/settings/${type}`, 'savefail');
 		} );
 	}, error => {
 		console.log( '- Dashboard: Error while getting the member: ' + error );
-		return res(`/guild/${guild}/settings/${type}?save=failed`);
+		return res(`/guild/${guild}/settings/${type}`, 'savefail');
 	} );
 }
 

+ 7 - 2
dashboard/src/index.css

@@ -26,11 +26,13 @@ a[alt]:hover:after {
 	padding: 8px;
 }
 .description a,
-a .description {
+a .description,
+.notice a {
 	color: #00b0f4;
 }
 .description a:hover,
-a:hover .description {
+a:hover .description,
+.notice a:hover {
 	text-decoration: underline;
 }
 #text {
@@ -298,6 +300,9 @@ fieldset input[type="url"] {
 	min-width: 30%;
 	margin-right: 5px;
 }
+fieldset input:invalid {
+	background: #FFAAAA;
+}
 .wb-settings-display:first-of-type {
 	display: inline-block;
 }

+ 32 - 12
dashboard/src/index.js

@@ -8,16 +8,6 @@ if ( wiki ) wiki.addEventListener( 'input', function (event) {
 	}
 } );
 
-const prefix = document.getElementById('wb-settings-prefix');
-if ( prefix ) prefix.addEventListener( 'input', function (event) {
-	if ( prefix.validity.patternMismatch ) {
-		prefix.setCustomValidity('The prefix may not include spaces or code markdown!');
-	}
-	else {
-		prefix.setCustomValidity();
-	}
-} );
-
 const form = document.getElementById('wb-settings');
 if ( form ) form.addEventListener( 'submit', function (event) {
 	if ( prefix && prefix.validity.patternMismatch ) {
@@ -140,7 +130,7 @@ function toggleOption() {
 		return option.value;
 	} );
 	options.forEach( function(option) {
-		if ( selected.includes(option.value) && !option.selected ) {
+		if ( selected.includes( option.value ) && !option.selected ) {
 			option.setAttribute('disabled', '');
 		}
 		else if ( option.disabled ) option.removeAttribute('disabled');
@@ -177,4 +167,34 @@ if ( wiki ) {
 			}
 		} );
 	}
-}
+}
+
+const usergroup = document.getElementById('wb-settings-usergroup');
+const multigroup = document.getElementById('wb-settings-usergroup-multiple');
+if ( usergroup && multigroup ) usergroup.addEventListener( 'input', function () {
+	if ( usergroup.value.includes( ',' ) || usergroup.value.includes( '|' ) ) {
+		multigroup.removeAttribute('style');
+		multigroup.removeAttribute('disabled');
+	}
+	else if ( !multigroup.hasAttribute('style') ) {
+		multigroup.setAttribute('style', 'visibility: hidden;');
+		multigroup.setAttribute('disabled', '');
+	}
+} );
+
+const prefix = document.getElementById('wb-settings-prefix');
+if ( prefix ) prefix.addEventListener( 'input', function () {
+	if ( prefix.validity.patternMismatch ) {
+		if ( prefix.value.trim().includes( ' ' ) ) {
+			prefix.setCustomValidity('The prefix may not include spaces!');
+		}
+		else if ( prefix.value.includes( '`' ) ) {
+			prefix.setCustomValidity('The prefix may not include code markdown!');
+		}
+		else if ( prefix.value.includes( '\\' ) ) {
+			prefix.setCustomValidity('The prefix may not include backslashes!');
+		}
+		else prefix.setCustomValidity('');
+	}
+	else prefix.setCustomValidity('');
+} );

+ 93 - 12
dashboard/util.js

@@ -90,19 +90,100 @@ function sendMsg(message) {
 
 /**
  * Create a red notice
- * @param {CheerioStatic} $ - The cheerio static
- * @param {Object} notice - The notices to create
- * @param {String} notice.title - The title of the notice
- * @param {String} notice.text - The text of the notice
- * @param {String} [notice.type] - The type of the notice
- * @returns {Cheerio}
+ * @param {import('cheerio')} $ - The cheerio static
+ * @param {String} notice - The notice to create
+ * @param {String[]} [args] - The arguments for the notice
+ * @returns {import('cheerio')}
  */
-function createNotice($, notice) {
-	var type = ( notice.type ? `notice-${notice.type}` : '' );
-	return $('<div class="notice">').append(
-		$('<b>').text(notice.title),
-		$('<div>').text(notice.text)
-	).addClass(type);
+function createNotice($, notice, args) {
+	if ( !notice ) return;
+	var type = 'info';
+	var title = $('<b>');
+	var text = $('<div>');
+	var note;
+	switch (notice) {
+		case 'unauthorized':
+			type = 'info';
+			title.text('Not logged in!');
+			text.text('Please login before you can change any settings.');
+			break;
+		case 'save':
+			type = 'success';
+			title.text('Settings saved!');
+			text.text('The settings have been updated successfully.');
+			break;
+		case 'logout':
+			type = 'success';
+			title.text('Successfully logged out!');
+			text.text('You have been successfully logged out. To change any settings you need to login again.');
+			break;
+		case 'refresh':
+			type = 'success';
+			title.text('Refresh successful!');
+			text.text('Your server list has been successfully refeshed.');
+			break;
+		case 'loginfail':
+			type = 'error';
+			title.text('Login failed!');
+			text.text('An error occurred while logging you in, please try again.');
+			break;
+		case 'sysmessage':
+			type = 'info';
+			title.text('System message does not match!');
+			text.text(`The page "MediaWiki:Custom-RcGcDw" need to be the server id "${args[0]}".`);
+			note = $('<a target="_blank">').text(args[1]).attr('href', args[1]);
+			break;
+		case 'mwversion':
+			type = 'error';
+			title.text('Outdated MediaWiki version!');
+			text.text(`Requires at least MediaWiki 1.30, found ${args[0]} on ${args[1]}.`);
+			note = $('<a target="_blank">').text('https://www.mediawiki.org/wiki/MediaWiki_1.30').attr('href', 'https://www.mediawiki.org/wiki/MediaWiki_1.30');
+			break;
+		case 'nochange':
+			type = 'info';
+			title.text('Save failed!');
+			text.text('The settings matched the current default settings.');
+			break;
+		case 'invalidusergroup':
+			type = 'error';
+			title.text('Invalid user group!');
+			text.text('The user group name was too long or you provided too many.');
+			break;
+		case 'wikiblocked':
+			type = 'error';
+			title.text('Wiki is blocked!');
+			text.text(`${args[0]} has been blocked from being added as a recent changes webhook.`);
+			if ( args[1] ) note = $('<div>').text(`Reason: ${args[1]}`);
+			break;
+		case 'savefail':
+			type = 'error';
+			title.text('Save failed!');
+			text.text('The settings could not be saved, please try again.');
+			break;
+		case 'movefail':
+			type = 'info';
+			title.text('Settings partially saved!');
+			text.text('The settings have only been partially updated.');
+			note = $('<div>').text('The webhook channel could not be changed!');
+			break;
+		case 'refreshfail':
+			type = 'error';
+			title.text('Refresh failed!');
+			text.text('You server list could not be refreshed, please try again.');
+			break;
+		case 'readonly':
+			type = 'info';
+			title.text('Read-only database!');
+			text.text('You can currently only view your settings, but not change them.');
+			break;
+		default:
+			return;
+	}
+	return $(`<div class="notice notice-${type}">`).append(
+		title,
+		text,
+		note
+	).appendTo('#text #notices');
 }
 
 const permissions = {

+ 37 - 33
dashboard/verification.js

@@ -1,6 +1,6 @@
 const {limit: {verification: verificationLimit}} = require('../util/default.json');
 const Lang = require('../util/i18n.js');
-const {got, db, sendMsg, hasPerm} = require('./util.js');
+const {got, db, sendMsg, createNotice, hasPerm} = require('./util.js');
 
 const fieldset = {
 	channel: '<label for="wb-settings-channel">Channel:</label>'
@@ -10,10 +10,11 @@ const fieldset = {
 	+ '<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">'
-	+ '<br>'
+	+ '<input type="text" id="wb-settings-usergroup" name="usergroup" autocomplete="on">'
+	+ '<div id="wb-settings-usergroup-multiple">'
 	+ '<label for="wb-settings-usergroup-and">Require all user groups:</label>'
-	+ '<input type="checkbox" id="wb-settings-usergroup-and" name="usergroup_and">',
+	+ '<input type="checkbox" id="wb-settings-usergroup-and" name="usergroup_and">'
+	+ '</div>',
 	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>'
@@ -131,6 +132,9 @@ function createForm($, header, settings, guildChannels, guildRoles) {
 		usergroup.find('#wb-settings-usergroup-and').attr('checked', '');
 	}
 	usergroup.find('#wb-settings-usergroup').val(settings.usergroup.split('|').join(', '));
+	if ( !settings.usergroup.includes( '|' ) ) {
+		usergroup.find('#wb-settings-usergroup-multiple').attr('style', 'display: none;');
+	}
 	fields.push(usergroup);
 	let editcount = $('<div>').append(fieldset.editcount);
 	editcount.find('#wb-settings-editcount').val(settings.editcount);
@@ -212,7 +216,7 @@ function dashboard_verification(res, $, guild, args) {
 				$('<div>').text('New verification')
 			).attr('href', `/guild/${guild.id}/verification/new`) )
 		);
-		if ( args[4] === 'new' ) {
+		if ( args[4] === 'new' && !( process.env.READONLY || rows.length >= verificationLimit[( guild.patreon ? 'patreon' : 'default' )] ) ) {
 			$('.channel#channel-new').addClass('selected');
 			createForm($, 'New Verification', {
 				channel: '', role: '', usergroup: 'user',
@@ -254,29 +258,29 @@ function dashboard_verification(res, $, guild, args) {
  */
 function update_verification(res, userSettings, guild, type, settings) {
 	if ( type === 'default' ) {
-		return res(`/guild/${guild}/verification?save=failed`);
+		return res(`/guild/${guild}/verification`, 'savefail');
 	}
 	if ( !settings.save_settings === !settings.delete_settings ) {
-		return res(`/guild/${guild}/verification/${type}?save=failed`);
+		return res(`/guild/${guild}/verification/${type}`, 'savefail');
 	}
 	if ( settings.save_settings ) {
 		if ( !/^[\d|]+ [\d|]+$/.test(`${settings.channel} ${settings.role}`) ) {
-			return res(`/guild/${guild}/verification/${type}?save=failed`);
+			return res(`/guild/${guild}/verification/${type}`, 'savefail');
 		}
 		if ( !/^\d+ \d+$/.test(`${settings.editcount} ${settings.accountage}`) ) {
-			return res(`/guild/${guild}/verification/${type}?save=failed`);
+			return res(`/guild/${guild}/verification/${type}`, 'savefail');
 		}
 		settings.channel = settings.channel.split('|').filter( (channel, i, self) => {
 			return ( channel.length && self.indexOf(channel) === i );
 		} );
 		if ( !settings.channel.length || settings.channel.length > 10 ) {
-			return res(`/guild/${guild}/verification/${type}?save=failed`);
+			return res(`/guild/${guild}/verification/${type}`, 'savefail');
 		}
 		settings.role = settings.role.split('|').filter( (role, i, self) => {
 			return ( role.length && self.indexOf(role) === i );
 		} );
 		if ( !settings.role.length || settings.role.length > 10 ) {
-			return res(`/guild/${guild}/verification/${type}?save=failed`);
+			return res(`/guild/${guild}/verification/${type}`, 'savefail');
 		}
 		if ( !settings.usergroup ) settings.usergroup = 'user';
 		settings.usergroup = settings.usergroup.replace( /_/g, ' ' ).trim().toLowerCase();
@@ -289,7 +293,7 @@ function update_verification(res, userSettings, guild, type, settings) {
 		if ( !settings.usergroup.length ) settings.usergroup.push('user');
 		if ( settings.usergroup.length > 10 || settings.usergroup.some( usergroup => {
 			return ( usergroup.length > 100 );
-		} ) ) return res(`/guild/${guild}/verification/${type}?save=failed`);
+		} ) ) return res(`/guild/${guild}/verification/${type}`, 'invalidusergroup');
 		settings.editcount = parseInt(settings.editcount, 10);
 		settings.accountage = parseInt(settings.accountage, 10);
 		if ( type === 'new' ) {
@@ -298,11 +302,11 @@ function update_verification(res, userSettings, guild, type, settings) {
 				return !curGuild.channels.some( guildChannel => guildChannel.id === channel );
 			} ) || settings.role.some( role => {
 				return !curGuild.roles.some( guildRole => guildRole.id === role && guildRole.lower );
-			} ) ) return res(`/guild/${guild}/verification/new?save=failed`);
+			} ) ) return res(`/guild/${guild}/verification/new`, 'savefail');
 		}
 	}
 	if ( settings.delete_settings && type === 'new' ) {
-		return res(`/guild/${guild}/verification/new?save=failed`);
+		return res(`/guild/${guild}/verification/new`, 'savefail');
 	}
 	if ( type !== 'new' ) type = parseInt(type, 10);
 	sendMsg( {
@@ -313,21 +317,21 @@ function update_verification(res, userSettings, guild, type, settings) {
 		if ( !response ) {
 			userSettings.guilds.notMember.set(guild, userSettings.guilds.isMember.get(guild));
 			userSettings.guilds.isMember.delete(guild);
-			return res(`/guild/${guild}?save=failed`);
+			return res(`/guild/${guild}`, 'savefail');
 		}
 		if ( response === 'noMember' || !hasPerm(response.userPermissions, 'MANAGE_GUILD') ) {
 			userSettings.guilds.isMember.delete(guild);
-			return res('/?save=failed');
+			return res('/', 'savefail');
 		}
 		if ( settings.delete_settings ) return db.get( 'SELECT lang, verification.channel, verification.role, editcount, usergroup, accountage, rename FROM discord LEFT JOIN verification ON discord.guild = verification.guild AND configid = ? WHERE discord.guild = ? AND discord.channel IS NULL', [type, guild], function(dberror, row) {
-			if ( !dberror && !row?.channel ) return res(`/guild/${guild}/verification?save=success`);
+			if ( !dberror && !row?.channel ) return res(`/guild/${guild}/verification`, 'save');
 			db.run( 'DELETE FROM verification WHERE guild = ? AND configid = ?', [guild, type], function (delerror) {
 				if ( delerror ) {
 					console.log( '- Dashboard: Error while removing the verification: ' + delerror );
-					return res(`/guild/${guild}/verification/${type}?save=failed`);
+					return res(`/guild/${guild}/verification/${type}`, 'savefail');
 				}
 				console.log( `- Dashboard: Verification successfully removed: ${guild}#${type}` );
-				res(`/guild/${guild}/verification?save=success`);
+				res(`/guild/${guild}/verification`, 'save');
 				if ( dberror ) {
 					console.log( '- Dashboard: Error while notifying the guild: ' + dberror );
 					return;
@@ -351,18 +355,18 @@ function update_verification(res, userSettings, guild, type, settings) {
 			} );
 		} );
 		if ( !hasPerm(response.botPermissions, 'MANAGE_ROLES') ) {
-			return res(`/guild/${guild}/verification?save=failed`);
+			return res(`/guild/${guild}/verification`, 'savefail');
 		}
 		if ( type === 'new' ) return db.get( 'SELECT wiki, lang, GROUP_CONCAT(configid) count FROM discord LEFT JOIN verification ON discord.guild = verification.guild WHERE discord.guild = ? AND discord.channel IS NULL', [guild], function(curerror, row) {
 			if ( curerror ) {
 				console.log( '- Dashboard: Error while checking for verifications: ' + curerror );
-				return res(`/guild/${guild}/verification/new?save=failed`);
+				return res(`/guild/${guild}/verification/new`, 'savefail');
 			}
-			if ( !row ) return res(`/guild/${guild}/verification?save=failed`);
+			if ( !row ) return res(`/guild/${guild}/verification`, 'savefail');
 			if ( row.count === null ) row.count = [];
 			else row.count = row.count.split(',').map( configid => parseInt(configid, 10) );
 			if ( row.count.length >= verificationLimit[( response.patreon ? 'patreon' : 'default' )] ) {
-				return res(`/guild/${guild}/verification?save=failed`);
+				return res(`/guild/${guild}/verification`, 'savefail');
 			}
 			return got.get( row.wiki + 'api.php?action=query&meta=allmessages&amprefix=group-&amincludelocal=true&amenableparser=true&format=json' ).then( gresponse => {
 				var body = gresponse.body;
@@ -402,10 +406,10 @@ function update_verification(res, userSettings, guild, type, settings) {
 				db.run( 'INSERT INTO verification(guild, configid, channel, role, editcount, usergroup, accountage, rename) VALUES(?, ?, ?, ?, ?, ?, ?, ?)', [guild, configid, '|' + settings.channel.join('|') + '|', settings.role.join('|'), settings.editcount, settings.usergroup.join('|'), settings.accountage, ( settings.rename ? 1 : 0 )], function (dberror) {
 					if ( dberror ) {
 						console.log( '- Dashboard: Error while adding the verification: ' + dberror );
-						return res(`/guild/${guild}/verification/new?save=failed`);
+						return res(`/guild/${guild}/verification/new`, 'savefail');
 					}
 					console.log( `- Dashboard: Verification successfully added: ${guild}#${configid}` );
-					res(`/guild/${guild}/verification/${configid}?save=success`);
+					res(`/guild/${guild}/verification/${configid}`, 'save');
 					var lang = new Lang(row.lang);
 					var text = lang.get('verification.dashboard.added', `<@${userSettings.user.id}>`, configid);
 					text += '\n' + lang.get('verification.channel') + ' <#' + settings.channel.join('>, <#') + '>';
@@ -448,9 +452,9 @@ function update_verification(res, userSettings, guild, type, settings) {
 		return db.get( 'SELECT wiki, lang, verification.channel, verification.role, editcount, usergroup, accountage, rename FROM discord LEFT JOIN verification ON discord.guild = verification.guild AND verification.configid = ? WHERE discord.guild = ? AND discord.channel IS NULL', [type, guild], function(curerror, row) {
 			if ( curerror ) {
 				console.log( '- Dashboard: Error while checking for verifications: ' + curerror );
-				return res(`/guild/${guild}/verification/${type}?save=failed`);
+				return res(`/guild/${guild}/verification/${type}`, 'savefail');
 			}
-			if ( !row?.channel ) return res(`/guild/${guild}/verification?save=failed`);
+			if ( !row?.channel ) return res(`/guild/${guild}/verification`, 'savefail');
 			row.channel = row.channel.split('|').filter( channel => channel.length );
 			var newChannel = settings.channel.filter( channel => !row.channel.includes( channel ) );
 			row.role = row.role.split('|');
@@ -463,7 +467,7 @@ function update_verification(res, userSettings, guild, type, settings) {
 					return !curGuild.channels.some( guildChannel => guildChannel.id === channel );
 				} ) || newRole.some( role => {
 					return !curGuild.roles.some( guildRole => guildRole.id === role && guildRole.lower );
-				} ) ) return res(`/guild/${guild}/verification/${type}?save=failed`);
+				} ) ) return res(`/guild/${guild}/verification/${type}`, 'savefail');
 			}
 			( newUsergroup.length ? got.get( row.wiki + 'api.php?action=query&meta=allmessages&amprefix=group-&amincludelocal=true&amenableparser=true&format=json' ).then( gresponse => {
 				var body = gresponse.body;
@@ -521,14 +525,14 @@ function update_verification(res, userSettings, guild, type, settings) {
 				if ( row.rename !== ( settings.rename ? 1 : 0 ) ) {
 					diff.push(lang.get('verification.rename') + ` ~~*\`${lang.get('verification.' + ( row.rename ? 'enabled' : 'disabled'))}\`*~~ → *\`${lang.get('verification.' + ( settings.rename ? 'enabled' : 'disabled'))}\`*`);
 				}
-				if ( !diff.length ) return res(`/guild/${guild}/verification/${type}?save=success`);
+				if ( !diff.length ) return res(`/guild/${guild}/verification/${type}`, 'save');
 				db.run( 'UPDATE verification SET channel = ?, role = ?, editcount = ?, usergroup = ?, accountage = ?, rename = ? WHERE guild = ? AND configid = ?', ['|' + settings.channel.join('|') + '|', settings.role.join('|'), settings.editcount, settings.usergroup.join('|'), settings.accountage, ( settings.rename ? 1 : 0 ), guild, type], function (dberror) {
 					if ( dberror ) {
 						console.log( '- Dashboard: Error while updating the verification: ' + dberror );
-						return res(`/guild/${guild}/verification/${type}?save=failed`);
+						return res(`/guild/${guild}/verification/${type}`, 'savefail');
 					}
-					console.log( `- Dashboard: Verification successfully unpdated: ${guild}#${type}` );
-					res(`/guild/${guild}/verification/${type}?save=success`);
+					console.log( `- Dashboard: Verification successfully updated: ${guild}#${type}` );
+					res(`/guild/${guild}/verification/${type}`, 'save');
 					var text = lang.get('verification.dashboard.updated', `<@${userSettings.user.id}>`, type);
 					text += '\n' + diff.join('\n');
 					text += `\n<${new URL(`/guild/${guild}/verification/${type}`, process.env.dashboard).href}>`;
@@ -564,7 +568,7 @@ function update_verification(res, userSettings, guild, type, settings) {
 		} );
 	}, error => {
 		console.log( '- Dashboard: Error while getting the member: ' + error );
-		return res(`/guild/${guild}/verification/${type}?save=failed`);
+		return res(`/guild/${guild}/verification/${type}`, 'savefail');
 	} );
 }