Pārlūkot izejas kodu

Finish Dashboard

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

+ 59 - 37
dashboard/guilds.js

@@ -1,6 +1,6 @@
 const cheerio = require('cheerio');
 const {defaultPermissions} = require('../util/default.json');
-const {settingsData, createNotice} = require('./util.js');
+const {settingsData, createNotice, escapeText} = require('./util.js');
 
 const forms = {
 	settings: require('./settings.js').get,
@@ -74,41 +74,53 @@ function dashboard_guilds(res, state, reqURL, action, actionArgs) {
 		} );
 	}
 
-	if ( args[1] === 'guild' ) {
-		let id = args[2];
-		$(`.guild#${id}`).addClass('selected');
-		if ( settings.guilds.isMember.has(id) ) {
-			let guild = settings.guilds.isMember.get(id);
-			$('head title').text(`${guild.name} – ` + $('head title').text());
-			$('<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#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] === 'rcscript' ) return forms.rcscript(res, $, guild, args);
-			return forms.settings(res, $, guild, args);
-		}
-		else if ( settings.guilds.notMember.has(id) ) {
-			let guild = settings.guilds.notMember.get(id);
-			$('head title').text(`${guild.name} – ` + $('head title').text());
-			res.setHeader('Set-Cookie', [`guild="${guild.id}/settings"; HttpOnly; Path=/`]);
-			let url = oauth.generateAuthUrl( {
-				scope: ['identify', 'guilds', 'bot'],
-				permissions: defaultPermissions,
-				guildId: guild.id, state
-			} );
-			$('<a>').attr('href', url).text(guild.name).appendTo('#text .description');
-		}
-		else {
-			$('head title').text('Unknown Server – ' + $('head title').text());
-			$('#text .description').text('You are missing the <code>MANAGE_GUILD</code> permission.');
-		}
+	let id = args[2];
+	$(`.guild#${id}`).addClass('selected');
+	if ( settings.guilds.isMember.has(id) ) {
+		let guild = settings.guilds.isMember.get(id);
+		$('head title').text(`${guild.name} – ` + $('head title').text());
+		$('<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#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] === 'rcscript' ) return forms.rcscript(res, $, guild, args);
+		return forms.settings(res, $, guild, args);
+	}
+	else if ( settings.guilds.notMember.has(id) ) {
+		let guild = settings.guilds.notMember.get(id);
+		$('head title').text(`${guild.name} – ` + $('head title').text());
+		res.setHeader('Set-Cookie', [`guild="${guild.id}/settings"; HttpOnly; Path=/`]);
+		let url = oauth.generateAuthUrl( {
+			scope: ['identify', 'guilds', 'bot'],
+			permissions: defaultPermissions,
+			guildId: guild.id, state
+		} );
+		$('#channellist').empty();
+		$('<a class="channel channel-header">').attr('href', url).append(
+			$('<img>').attr('src', '/src/settings.svg'),
+			$('<div>').text('Invite Wiki-Bot')
+		).appendTo('#channellist');
+		$('#text .description').append(
+			$('<p>').append(
+				escapeText(`Wiki-Bot is not a member of "${guild.name}" yet, but you can `),
+				$('<a>').attr('href', url).text('invite Wiki-Bot'),
+				escapeText('.')
+			),
+			$('<a id="login-button">').attr('href', url).text('Invite Wiki-Bot').prepend(
+				$('<img alt="Discord">').attr('src', 'https://discord.com/assets/f8389ca1a741a115313bede9ac02e2c0.svg')
+			)
+		);
 	}
 	else {
 		$('head title').text('Server Selector – ' + $('head title').text());
 		$('#channellist').empty();
-		$('#text .description').text('This is a list of all servers you can change settings on. Please select a server:');
+		$('<p>').append(
+			escapeText('This is a list of all servers you can change settings on because you have the '),
+			$('<code>').text('Manage Server'),
+			escapeText(' permission. Please select a server:')
+		).appendTo('#text .description');
 		if ( settings.guilds.isMember.size ) {
 			$('<h2 id="with-wikibot">').text('Server with Wiki-Bot').appendTo('#text');
 			$('<a class="channel">').attr('href', '#with-wikibot').append(
@@ -142,14 +154,24 @@ function dashboard_guilds(res, state, reqURL, action, actionArgs) {
 			} );
 		}
 		if ( !settings.guilds.count ) {
-			$('#text .description').text('You currently don\'t have the MANAGE_GUILD permission on any servers, are you logged into the correct account?');
-			$('<a class="channel">').attr('href', oauth.generateAuthUrl( {
+			let url = oauth.generateAuthUrl( {
 				scope: ['identify', 'guilds'],
 				prompt: 'consent', state
-			} )).append(
-				$('<img>').attr('src', '/src/channel.svg'),
-				$('<div>').text('Switch accounts')
+			} );
+			$('<a class="channel channel-header">').attr('href', url).append(
+				$('<img>').attr('src', '/src/settings.svg'),
+				$('<div>').text('Switch Accounts')
 			).appendTo('#channellist');
+			$('#text .description').append(
+				$('<p>').append(
+					escapeText('You currently don\'t have the '),
+					$('<code>').text('Manage Server'),
+					escapeText(' permission on any servers, are you logged into the correct account?')
+				),
+				$('<a id="login-button">').attr('href', url).text('Switch Accounts').prepend(
+					$('<img alt="Discord">').attr('src', 'https://discord.com/assets/f8389ca1a741a115313bede9ac02e2c0.svg')
+				)
+			);
 		}
 	}
 	let body = $.html();

+ 17 - 5
dashboard/index.js

@@ -13,7 +13,17 @@ const posts = {
 
 const fs = require('fs');
 const path = require('path');
-const files = new Map(fs.readdirSync( './dashboard/src' ).map( file => {
+const files = new Map([
+	...fs.readdirSync( './dashboard/src' ).map( file => {
+		return [`/src/${file}`, `./dashboard/src/${file}`];
+	} ),
+	...fs.readdirSync( './i18n/widgets' ).map( file => {
+		return [`/src/widgets/${file}`, `./i18n/widgets/${file}`];
+	} ),
+	...fs.readdirSync( './RcGcDb/locale/widgets' ).map( file => {
+		return [`/src/widgets/RcGcDb/${file}`, `./RcGcDb/locale/widgets/${file}`];
+	} )
+].map( ([file, filepath]) => {
 	let contentType = 'text/html';
 	switch ( path.extname(file) ) {
 		case '.css':
@@ -35,10 +45,7 @@ const files = new Map(fs.readdirSync( './dashboard/src' ).map( file => {
 			contentType = 'image/jpg';
 			break;
 	}
-	return [`/src/${file}`, {
-		name: file, contentType,
-		path: `./dashboard/src/${file}`
-	}];
+	return [file, {path: filepath, contentType}];
 } ));
 
 const server = http.createServer((req, res) => {
@@ -170,6 +177,11 @@ const server = http.createServer((req, res) => {
 		return pages.refresh(res, state, returnLocation);
 	}
 
+	if ( reqURL.pathname === '/api' ) {
+		let wiki = reqURL.searchParams.get('wiki');
+		if ( wiki ) return pages.api(res, wiki);
+	}
+
 	let action = '';
 	if ( reqURL.searchParams.get('refresh') === 'success' ) action = 'refresh';
 	if ( reqURL.searchParams.get('refresh') === 'failed' ) action = 'refreshfail';

+ 66 - 2
dashboard/oauth.js

@@ -1,7 +1,8 @@
 const crypto = require('crypto');
 const cheerio = require('cheerio');
 const {defaultPermissions} = require('../util/default.json');
-const {db, settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
+const Wiki = require('../util/wiki.js');
+const {got, settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
 
 const DiscordOauth2 = require('discord-oauth2');
 const oauth = new DiscordOauth2( {
@@ -204,8 +205,71 @@ function dashboard_refresh(res, state, returnLocation = '/') {
 	} );
 }
 
+/**
+ * Check if a wiki is availabe
+ * @param {import('http').ServerResponse} res - The server response
+ * @param {String} input - The wiki to check
+ */
+function dashboard_api(res, input) {
+	var wiki = Wiki.fromInput('https://' + input + '/');
+	var result = {
+		api: true,
+		error: false,
+		wiki: wiki.href,
+		MediaWiki: false,
+		TextExtracts: false,
+		PageImages: false,
+		RcGcDw: '',
+		customRcGcDw: wiki.toLink('MediaWiki:Custom-RcGcDw', 'action=edit')
+	};
+	return got.get( wiki + 'api.php?&action=query&meta=allmessages|siteinfo&ammessages=custom-RcGcDw&amenableparser=true&siprop=general|extensions' + ( wiki.isFandom() ? '|variables' : '' ) + '&format=json' ).then( response => {
+		if ( response.statusCode === 404 && typeof response.body === 'string' ) {
+			let api = cheerio.load(response.body)('head link[rel="EditURI"]').prop('href');
+			if ( api ) {
+				wiki = new Wiki(api.split('api.php?')[0], wiki);
+				return got.get( wiki + 'api.php?action=query&meta=allmessages|siteinfo&ammessages=custom-RcGcDw&amenableparser=true&siprop=general|extensions' + ( wiki.isFandom() ? '|variables' : '' ) + '&format=json' );
+			}
+		}
+		return response;
+	} ).then( response => {
+		var body = response.body;
+		if ( response.statusCode !== 200 || !body?.query?.allmessages || !body?.query?.general || !body?.query?.extensions ) {
+			console.log( '- Dashboard: ' + response.statusCode + ': Error while checking the wiki: ' + body?.error?.info );
+			result.error = true;
+			return;
+		}
+		wiki.updateWiki(body.query.general);
+		result.wiki = wiki.href;
+		if ( body.query.general.generator.replace( /^MediaWiki 1\.(\d\d).*$/, '$1' ) >= 30 ) {
+			result.MediaWiki = true;
+		}
+		if ( body.query.extensions.some( extension => extension.name === 'TextExtracts' ) ) {
+			result.TextExtracts = true;
+		}
+		if ( body.query.extensions.some( extension => extension.name === 'PageImages' ) ) {
+			result.PageImages = true;
+		}
+		if ( body.query.allmessages[0]['*'] ) {
+			result.RcGcDw = body.query.allmessages[0]['*'];
+		}
+		result.customRcGcDw = wiki.toLink('MediaWiki:Custom-RcGcDw', 'action=edit');
+	}, error => {
+		console.log( '- Dashboard: Error while checking the wiki: ' + error );
+		result.error = true;
+	} ).finally( () => {
+		let body = JSON.stringify(result);
+		res.writeHead(200, {
+			'Content-Length': body.length,
+			'Content-Type': 'application/json'
+		});
+		res.write( body );
+		return res.end();
+	} );
+}
+
 module.exports = {
 	login: dashboard_login,
 	oauth: dashboard_oauth,
-	refresh: dashboard_refresh
+	refresh: dashboard_refresh,
+	api: dashboard_api
 };

+ 51 - 19
dashboard/rcscript.js

@@ -16,7 +16,10 @@ 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 autocomplete="url">',
+	+ '<input type="url" id="wb-settings-wiki" name="wiki" list="wb-settings-wiki-list" required autocomplete="url">'
+	+ '<datalist id="wb-settings-wiki-list"></datalist>'
+	+ '<button type="button" id="wb-settings-wiki-check">Check wiki</button>'
+	+ '<div id="wb-settings-wiki-check-notice"></div>',
 	//+ '<button type="button" id="wb-settings-wiki-search" class="collapsible">Search wiki</button>'
 	//+ '<fieldset style="display: none;">'
 	//+ '<legend>Wiki search</legend>'
@@ -25,8 +28,9 @@ const fieldset = {
 	+ '<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')
-	+ '</select>',
+	} ).join('')
+	+ '</select>'
+	+ '<img id="wb-settings-lang-widget">',
 	display: '<span>Display mode:</span>'
 	+ '<div class="wb-settings-display">'
 	+ '<input type="radio" id="wb-settings-display-0" name="display" value="0" required>'
@@ -48,7 +52,7 @@ const fieldset = {
 	+ '<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">'
+	delete: '<input type="submit" id="wb-settings-delete" name="delete_settings" formnovalidate>'
 };
 
 /**
@@ -68,8 +72,9 @@ const fieldset = {
  * @param {String} guildChannels.name
  * @param {Number} guildChannels.userPermissions
  * @param {Number} guildChannels.botPermissions
+ * @param {String[]} allWikis - The guild wikis
  */
-function createForm($, header, settings, guildChannels) {
+function createForm($, header, settings, guildChannels, allWikis) {
 	var readonly = ( process.env.READONLY ? true : false );
 	var curChannel = guildChannels.find( guildChannel => settings.channel === guildChannel.id );
 	var fields = [];
@@ -86,9 +91,14 @@ function createForm($, header, settings, guildChannels) {
 				return optionChannel.text(`${guildChannel.id} – #${guildChannel.name}`);
 			} )
 		);
-		if ( !settings.channel ) channel.find('#wb-settings-channel').prepend(
-			$(`<option id="wb-settings-channel-default" selected hidden>`).val('').text('-- Select a Channel --')
-		);
+		if ( !settings.channel ) {
+			if ( !channel.find('#wb-settings-channel').children('option').length ) {
+				createNotice($, 'missingperm', ['Manage Webhooks']);
+			}
+			channel.find('#wb-settings-channel').prepend(
+				$(`<option id="wb-settings-channel-default" selected hidden>`).val('').text('-- Select a Channel --')
+			);
+		}
 	}
 	else if ( curChannel ) channel.find('#wb-settings-channel').append(
 		$(`<option id="wb-settings-channel-${curChannel.id}">`).val(curChannel.id).attr('selected', '').text(`${curChannel.id} – #${curChannel.name}`)
@@ -99,6 +109,9 @@ function createForm($, header, settings, guildChannels) {
 	fields.push(channel);
 	let wiki = $('<div>').append(fieldset.wiki);
 	wiki.find('#wb-settings-wiki').val(settings.wiki);
+	wiki.find('#wb-settings-wiki-list').append(
+		...allWikis.map( listWiki => $(`<option>`).val(listWiki) )
+	);
 	fields.push(wiki);
 	let lang = $('<div>').append(fieldset.lang);
 	lang.find(`#wb-settings-lang-${settings.lang}`).attr('selected', '');
@@ -138,6 +151,16 @@ function createForm($, header, settings, guildChannels) {
 	);
 }
 
+const explanation = `
+<h2>Recent Changes Webhook</h2>
+<p>Wiki-Bot is able to run a recent changes webhook based on <a href="https://gitlab.com/piotrex43/RcGcDw" target="_blank">RcGcDw</a>. The recent changes can be displayed in compact text messages with inline links or embed messages with edit tags and category changes.</p>
+<p>Requirements to add a recent changes webhook:</p>
+<ul>
+	<li>The wiki needs to run on <a href="https://www.mediawiki.org/wiki/MediaWiki_1.30" target="_blank">MediaWiki 1.30</a> or higher.</li>
+	<li>The system message <code>MediaWiki:Custom-RcGcDw</code> needs to be set to the Discord server id <code id="server-id" class="user-select"></code>.</li>
+</ul>
+`;
+
 /**
  * Let a user change recent changes scripts
  * @param {import('http').ServerResponse} res - The server response
@@ -146,10 +169,12 @@ function createForm($, header, settings, guildChannels) {
  * @param {String[]} args - The url parts
  */
 function dashboard_rcscript(res, $, guild, args) {
-	db.all( 'SELECT discord.wiki mainwiki, discord.lang mainlang, webhook, configid, rcgcdw.wiki, rcgcdw.lang, display, wikiid, rcid FROM discord LEFT JOIN rcgcdw ON discord.guild = rcgcdw.guild WHERE discord.guild = ? AND discord.channel IS NULL ORDER BY configid ASC', [guild.id], function(dberror, rows) {
+	db.all( 'SELECT discord.wiki mainwiki, discord.lang mainlang, (SELECT GROUP_CONCAT(DISTINCT wiki) FROM discord WHERE guild = ?) allwikis, webhook, configid, rcgcdw.wiki, rcgcdw.lang, display, wikiid, rcid FROM discord LEFT JOIN rcgcdw ON discord.guild = rcgcdw.guild WHERE discord.guild = ? AND discord.channel IS NULL ORDER BY configid ASC', [guild.id, guild.id], function(dberror, rows) {
 		if ( dberror ) {
 			console.log( '- Dashboard: Error while getting the RcGcDw: ' + dberror );
-			$('#text .description').text('Failed to load the recent changes webhooks!');
+			createNotice($, 'error');
+			$('#text .description').html(explanation);
+			$('#text code#server-id').text(guild.id);
 			$('.channel#rcscript').addClass('selected');
 			let body = $.html();
 			res.writeHead(200, {'Content-Length': body.length});
@@ -157,7 +182,9 @@ function dashboard_rcscript(res, $, guild, args) {
 			return res.end();
 		}
 		if ( rows.length === 0 ) {
-			$('#text .description').text(`You need to set up the server first: /guild/${guild.id}/settings`);
+			createNotice($, 'nosettings', [guild.id]);
+			$('#text .description').html(explanation);
+			$('#text code#server-id').text(guild.id);
 			$('.channel#rcscript').addClass('selected');
 			let body = $.html();
 			res.writeHead(200, {'Content-Length': body.length});
@@ -166,8 +193,9 @@ function dashboard_rcscript(res, $, guild, args) {
 		}
 		var wiki = rows[0].mainwiki;
 		var lang = rows[0].mainlang;
+		var allwikis = rows[0].allwikis.split(',').sort();
 		if ( rows.length === 1 && rows[0].configid === null ) rows.pop();
-		$('#text .description').text(`These are the recent changes webhooks for "${guild.name}":`);
+		$('<p>').text(`These are the recent changes webhooks for "${guild.name}":`).appendTo('#text .description');
 		Promise.all(rows.map( row => {
 			return got.get( 'https://discord.com/api/webhooks/' + row.webhook ).then( response => {
 				if ( !response.body?.channel_id ) {
@@ -200,18 +228,19 @@ function dashboard_rcscript(res, $, guild, args) {
 				createForm($, 'New Recent Changes Webhook', {
 					wiki, lang: ( lang in allLangs.names ? lang : defaultSettings.lang ),
 					display: 1, patreon: guild.patreon
-				}, guild.channels).attr('action', `/guild/${guild.id}/rcscript/new`).appendTo('#text');
+				}, guild.channels, allwikis).attr('action', `/guild/${guild.id}/rcscript/new`).appendTo('#text');
 			}
 			else if ( rows.some( row => row.configid.toString() === args[4] ) ) {
 				let row = rows.find( row => row.configid.toString() === 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');
+				}, row), guild.channels, allwikis).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*`);
+				$('#text .description').html(explanation);
+				$('#text code#server-id').text(guild.id);
 			}
 			let body = $.html();
 			res.writeHead(200, {'Content-Length': body.length});
@@ -380,7 +409,8 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 								if ( wiki.isFandom(false) ) text += `\n${lang.get('rcscript.feeds')} *\`${lang.get('rcscript.' + ( wikiid ? 'enabled' : 'disabled' ))}\`*`;
 								text += `\n<${new URL(`/guild/${guild}/rcscript/${configid}`, process.env.dashboard).href}>`;
 								sendMsg( {
-									type: 'notifyGuild', guild, text
+									type: 'notifyGuild', guild, text,
+									file: [`./RcGcDb/locale/widgets/${settings.lang}.png`]
 								} ).catch( error => {
 									console.log( '- Dashboard: Error while notifying the guild: ' + error );
 								} );
@@ -573,6 +603,7 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 								var lang = new Lang(row.mainlang);
 								var webhook_lang = new Lang(settings.lang, 'rcscript.webhook');
 								var diff = [];
+								var file = [];
 								var webhook_diff = [];
 								if ( newChannel ) {
 									diff.push(lang.get('rcscript.channel') + ` ~~<#${row.channel}>~~ → <#${settings.channel}>`);
@@ -583,6 +614,7 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 									webhook_diff.push(webhook_lang.get('dashboard.wiki', `[${body.query.general.sitename}](<${wiki.href}>)`));
 								}
 								if ( row.lang !== settings.lang ) {
+									file.push(`./RcGcDb/locale/widgets/${settings.lang}.png`);
 									diff.push(lang.get('rcscript.lang') + ` ~~\`${allLangs.names[row.lang]}\`~~ → \`${allLangs.names[settings.lang]}\``);
 									webhook_diff.push(webhook_lang.get('dashboard.lang', allLangs.names[settings.lang]));
 								}
@@ -612,7 +644,7 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 									text += '\n' + diff.join('\n');
 									text += `\n<${new URL(`/guild/${guild}/rcscript/${type}`, process.env.dashboard).href}>`;
 									sendMsg( {
-										type: 'notifyGuild', guild, text
+										type: 'notifyGuild', guild, text, file
 									} ).catch( error => {
 										console.log( '- Dashboard: Error while notifying the guild: ' + error );
 									} );
@@ -643,7 +675,7 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 									text += '\n' + diff.join('\n');
 									text += `\n<${new URL(`/guild/${guild}/rcscript/${type}`, process.env.dashboard).href}>`;
 									sendMsg( {
-										type: 'notifyGuild', guild, text
+										type: 'notifyGuild', guild, text, file
 									} ).catch( error => {
 										console.log( '- Dashboard: Error while notifying the guild: ' + error );
 									} );
@@ -664,7 +696,7 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 								text += '\n' + diff.join('\n');
 								text += `\n<${new URL(`/guild/${guild}/rcscript/${type}`, process.env.dashboard).href}>`;
 								sendMsg( {
-									type: 'notifyGuild', guild, text
+									type: 'notifyGuild', guild, text, file
 								} ).catch( error => {
 									console.log( '- Dashboard: Error while notifying the guild: ' + error );
 								} );

+ 22 - 9
dashboard/settings.js

@@ -9,7 +9,9 @@ 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 autocomplete="url">',
+	+ '<input type="url" id="wb-settings-wiki" name="wiki" required autocomplete="url">'
+	+ '<button type="button" id="wb-settings-wiki-check">Check wiki</button>'
+	+ '<div id="wb-settings-wiki-check-notice"></div>',
 	//+ '<button type="button" id="wb-settings-wiki-search" class="collapsible">Search wiki</button>'
 	//+ '<fieldset style="display: none;">'
 	//+ '<legend>Wiki search</legend>'
@@ -18,8 +20,9 @@ const fieldset = {
 	+ '<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')
-	+ '</select>',
+	} ).join('')
+	+ '</select>'
+	+ '<img id="wb-settings-lang-widget">',
 	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>'
@@ -30,7 +33,7 @@ const fieldset = {
 	inline: '<label for="wb-settings-inline">Inline commands:</label>'
 	+ '<input type="checkbox" id="wb-settings-inline" name="inline">',
 	save: '<input type="submit" id="wb-settings-save" name="save_settings">',
-	delete: '<input type="submit" id="wb-settings-delete" name="delete_settings">'
+	delete: '<input type="submit" id="wb-settings-delete" name="delete_settings" formnovalidate>'
 };
 
 /**
@@ -134,14 +137,15 @@ function dashboard_settings(res, $, guild, args) {
 	db.all( 'SELECT channel, wiki, lang, role, inline, prefix, patreon FROM discord WHERE guild = ? ORDER BY channel ASC', [guild.id], function(dberror, rows) {
 		if ( dberror ) {
 			console.log( '- Dashboard: Error while getting the settings: ' + dberror );
-			$('#text .description').text('Failed to load the settings!');
+			createNotice($, 'error');
+			$('<p>').text('Failed to load the settings!').appendTo('#text .description');
 			$('.channel#settings').addClass('selected');
 			let body = $.html();
 			res.writeHead(200, {'Content-Length': body.length});
 			res.write( body );
 			return res.end();
 		}
-		$('#text .description').text(`These are the settings for "${guild.name}":`);
+		$('<p>').text(`These are the settings for "${guild.name}":`).appendTo('#text .description');
 		if ( !rows.length ) {
 			$('.channel#settings').addClass('selected');
 			createForm($, 'Server-wide Settings', Object.assign({
@@ -389,12 +393,14 @@ function update_settings(res, userSettings, guild, type, settings) {
 					text += '\n' + lang.get('settings.currentinline') + ` ${( settings.inline ? '' : '~~' )}\`[[${( lang.localNames.page || 'page' )}]]\`${( settings.inline ? '' : '~~' )}`;
 					text += `\n<${new URL(`/guild/${guild}/settings`, process.env.dashboard).href}>`;
 					sendMsg( {
-						type: 'notifyGuild', guild, text, embed
+						type: 'notifyGuild', guild, text, embed,
+						file: [`./i18n/widgets/${settings.lang}.png`]
 					} ).catch( error => {
 						console.log( '- Dashboard: Error while notifying the guild: ' + error );
 					} );
 				} );
 				var diff = [];
+				var file = [];
 				var updateGuild = false;
 				var updateChannel = false;
 				if ( row.wiki !== wiki.href ) {
@@ -403,6 +409,7 @@ function update_settings(res, userSettings, guild, type, settings) {
 				}
 				if ( row.lang !== settings.lang ) {
 					updateChannel = true;
+					file.push(`./i18n/widgets/${settings.lang}.png`);
 					diff.push(lang.get('settings.currentlang') + ` ~~\`${allLangs.names[row.lang]}\`~~ → \`${allLangs.names[settings.lang]}\``);
 				}
 				if ( response.patreon && row.prefix !== settings.prefix ) {
@@ -456,7 +463,8 @@ function update_settings(res, userSettings, guild, type, settings) {
 						text += '\n' + diff.join('\n');
 						text += `\n<${new URL(`/guild/${guild}/settings`, process.env.dashboard).href}>`;
 						sendMsg( {
-							type: 'notifyGuild', guild, text, embed,
+							type: 'notifyGuild', guild, text, file,
+							embed: ( updateGuild ? embed : undefined ),
 							prefix: settings.prefix, voice: settings.lang
 						} ).catch( error => {
 							console.log( '- Dashboard: Error while notifying the guild: ' + error );
@@ -500,10 +508,14 @@ function update_settings(res, userSettings, guild, type, settings) {
 				}
 				if ( !channel ) channel = row;
 				var diff = [];
+				var file = [];
+				var useEmbed = false;
 				if ( channel.wiki !== wiki.href ) {
+					useEmbed = true;
 					diff.push(lang.get('settings.currentwiki') + ` ~~<${channel.wiki}>~~ → <${wiki.href}>`);
 				}
 				if ( response.patreon && channel.lang !== settings.lang ) {
+					file.push(`./i18n/widgets/${settings.lang}.png`);
 					diff.push(lang.get('settings.currentlang') + ` ~~\`${allLangs.names[channel.lang]}\`~~ → \`${allLangs.names[settings.lang]}\``);
 				}
 				if ( response.patreon && channel.role !== ( settings.role || null ) ) {
@@ -533,7 +545,8 @@ function update_settings(res, userSettings, guild, type, settings) {
 					text += '\n' + diff.join('\n');
 					text += `\n<${new URL(`/guild/${guild}/settings/${settings.channel}`, process.env.dashboard).href}>`;
 					sendMsg( {
-						type: 'notifyGuild', guild, text, embed
+						type: 'notifyGuild', guild, text, file,
+						embed: ( useEmbed ? embed : undefined )
 					} ).catch( error => {
 						console.log( '- Dashboard: Error while notifying the guild: ' + error );
 					} );

+ 25 - 6
dashboard/src/index.css

@@ -35,6 +35,15 @@ a:hover .description,
 .notice a:hover {
 	text-decoration: underline;
 }
+code {
+	background: #2f3136;
+	padding: 1px 3px;
+	margin: -2px 0;
+	border: 1px solid #202225;
+	border-radius: 3px;
+	white-space: pre-wrap;
+	font-size: 120%;
+}
 #text {
 	position: relative;
 	padding: 8px;
@@ -42,6 +51,12 @@ a:hover .description,
 	top: 48px;
 	left: 312px;
 }
+.user-select {
+	-webkit-user-select: all;
+	-moz-user-select: all;
+	-ms-user-select: all;
+	user-select: all;
+}
 .notice {
 	padding: 5px 10px;
 	line-height: 1.6;
@@ -50,17 +65,17 @@ a:hover .description,
 	width: fit-content;
 	border: 2px solid;
 }
-.notice-error {
-	background-color: #200;
-	border-color: #500;
+.notice-success {
+	background-color: #020;
+	border-color: #050;
 }
 .notice-info {
 	background-color: #220;
 	border-color: #550;
 }
-.notice-success {
-	background-color: #020;
-	border-color: #050;
+.notice-error {
+	background-color: #200;
+	border-color: #500;
 }
 .server-selector {
 	display: flex;
@@ -308,6 +323,7 @@ fieldset input:invalid {
 }
 .wb-settings-display:not(:first-of-type),
 .wb-settings-additional-select,
+#wb-settings-wiki-check-notice,
 button.addmore:not([hidden]) {
 	display: block;
 	margin-left: 20%;
@@ -323,6 +339,9 @@ button.addmore:not([hidden]) {
 	color: red;
 	font-weight: bold;
 }
+#wb-settings-lang-widget {
+	vertical-align: top;
+}
 #login-button {
 	display: flex;
 	margin: 20px auto;

+ 214 - 93
dashboard/src/index.js

@@ -1,85 +1,18 @@
-/*const wiki = document.getElementById('wb-settings-wiki');
-if ( wiki ) wiki.addEventListener( 'input', function (event) {
-	if ( wiki.validity.valid ) {
-		wiki.setCustomValidity('I am expecting an e-mail address!');
-	}
-	else {
-		wiki.setCustomValidity();
-	}
-} );
-
-const form = document.getElementById('wb-settings');
-if ( form ) form.addEventListener( 'submit', function (event) {
-	if ( prefix && prefix.validity.patternMismatch ) {
-		prefix.setCustomValidity('The prefix may not include spaces or code markdown!');
-		event.preventDefault();
-	}
-	else if ( wiki && wiki.validity.valid ) {
-		wiki.value
-		fetch()
-		got.get( wikinew + 'api.php?&action=query&meta=siteinfo&siprop=general&format=json' ).then( response => {
-			if ( !isForced && response.statusCode === 404 && typeof response.body === 'string' ) {
-				let api = cheerio.load(response.body)('head link[rel="EditURI"]').prop('href');
-				if ( api ) {
-					wikinew = new Wiki(api.split('api.php?')[0], wikinew);
-					return got.get( wikinew + 'api.php?action=query&meta=siteinfo&siprop=generals&format=json' );
-				}
-			}
-			return response;
-		} ).then( response => {
-			var body = response.body;
-			if ( response.statusCode !== 200 || !body?.query?.allmessages || !body?.query?.general || !body?.query?.extensions ) {
-				console.log( '- ' + response.statusCode + ': Error while testing the wiki: ' + body?.error?.info );
-				if ( reaction ) reaction.removeEmoji();
-				msg.reactEmoji('nowiki', true);
-				return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {}, true );
-			}
-			if ( !isForced ) wikinew.updateWiki(body.query.general);
-			if ( wikinew.isGamepedia() && !isForced ) {
-				let site = allSites.find( site => site.wiki_domain === wikinew.hostname );
-				if ( site ) wikinew = new Wiki('https://' + ( site.wiki_crossover || site.wiki_domain ) + '/');
-			}
-			else if ( wikinew.isFandom() && !isForced ) {
-				let crossover = '';
-				if ( body.query.allmessages[0]['*'] ) {
-					crossover = 'https://' + body.query.allmessages[0]['*'] + '.gamepedia.com/';
-				}
-				else if ( body.query.allmessages[1]['*'] ) {
-					let merge = body.query.allmessages[1]['*'].split('/');
-					crossover = 'https://' + merge[0] + '.fandom.com/' + ( merge[1] ? merge[1] + '/' : '' );
-				}
-				if ( crossover ) wikinew = new Wiki(crossover);
+var baseSelect = document.getElementsByTagName('select');
+for ( var b = 0; b < baseSelect.length; b++ ) {
+	if ( baseSelect[b].id === 'wb-settings-lang' ) {
+		const langWidget = document.getElementById('wb-settings-lang-widget');
+		if ( langWidget ) {
+			var widgetPath = 'widgets';
+			if ( document.location.pathname.split('/')[3] === 'rcscript' ) {
+				widgetPath = 'widgets/RcGcDb';
 			}
-		}, ferror => {
-			console.log( '- Error while testing the wiki: ' + ferror );
-			if ( reaction ) reaction.removeEmoji();
-			msg.reactEmoji('nowiki', true);
-			return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {}, true );
-		} );
-	}
-	else form.dispatchEvent(new Event('submit'));
-} );
-
-var collapsible = document.getElementsByClassName('collapsible');
-for ( var i = 0; i < collapsible.length; i++ ) {
-	collapsible[i].onclick = function() {
-		this.classList.toggle('active');
-		if ( this.id === 'wb-settings-wiki-search' ) {
-			wiki.toggleAttribute('readonly');
-		}
-		var content = this.nextElementSibling;
-		if ( content.style.display === 'block' ) {
-			content.style.display = 'none';
-		}
-		else {
-			content.style.display = 'block';
+			langWidget.setAttribute('src', `/src/${widgetPath}/${baseSelect[b].value}.png`);
+			baseSelect[b].addEventListener( 'input', function() {
+				langWidget.setAttribute('src', `/src/${widgetPath}/${this.value}.png`);
+			} );
 		}
 	}
-}
-*/
-
-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]);
@@ -139,6 +72,157 @@ function toggleOption() {
 
 const wiki = document.getElementById('wb-settings-wiki');
 if ( wiki ) {
+	wiki.addEventListener( 'input', function() {
+		if ( !/^(?:https?:)?\/\//.test(this.value) ) {
+			if ( this.validity.valid ) {
+				var divTemp = document.createElement('div');
+				divTemp.innerHTML = '<input type="url" value="invalid">';
+				this.setCustomValidity(divTemp.firstChild.validationMessage);
+			}
+		}
+		else this.setCustomValidity('');
+	} );
+	const wikicheck = document.getElementById('wb-settings-wiki-check');
+	const wikichecknotice = document.getElementById('wb-settings-wiki-check-notice');
+	if ( wikicheck && wikichecknotice ) {
+		wikicheck.onclick = function() {
+			var wikinew = wiki.value.replace( /^(?:https?:)?\/\//, '' );
+			var regex = wikinew.match( /^([a-z\d-]{1,50}\.(?:gamepedia\.com|(?:fandom\.com|wikia\.org)(?:(?!\/(?:wiki|api)\/)\/[a-z-]{2,12})?))(?:\/|$)/ );
+			if ( regex ) wikinew = regex[1];
+			else if ( !wiki.validity.valid ) return wiki.reportValidity();
+			else {
+				wikinew = wikinew.replace( /\/(?:api|load|index)\.php(?:|\?.*)$/, '' ).replace( /\/$/, '' );
+			}
+			wiki.setAttribute('readonly', '');
+			wikicheck.setAttribute('disabled', '');
+			fetch( '/api?wiki=' + encodeURIComponent( wikinew ), {
+				method: 'GET',
+				cache: 'no-cache',
+				mode: 'same-origin',
+				headers: {
+					Accept: 'application/json'
+				}
+			} ).then( function(response) {
+				if ( response.ok && response.status === 200 ) return response.json();
+				else return Promise.reject('Error: The server did not respond correctly.');
+			} ).then( function(response) {
+				if ( !response.api ) {
+					console.log('Error: The server did not respond correctly.');
+					return;
+				}
+				wikichecknotice.className = 'notice';
+				wikichecknotice.innerHTML = '';
+				if ( response.error ) {
+					wiki.setCustomValidity('Invalid wiki!');
+					wikichecknotice.classList.add('notice-error');
+					var noticeTitle = document.createElement('b');
+					noticeTitle.textContent = 'Invalid wiki!';
+					var noticeText = document.createElement('div');
+					noticeText.textContent = 'The URL could not be resolved to a valid MediaWiki site!';
+					wikichecknotice.append(noticeTitle, noticeText);
+					return;
+				}
+				wiki.value = response.wiki;
+				if ( document.location.pathname.split('/')[3] === 'rcscript' ) {
+					if ( !response.MediaWiki ) {
+						wiki.setCustomValidity('Outdated MediaWiki version!');
+						wikichecknotice.classList.add('notice-error');
+						var noticeTitle = document.createElement('b');
+						noticeTitle.textContent = 'Outdated MediaWiki version!';
+						var noticeText = document.createElement('div');
+						noticeText.textContent = 'The recent changes webhook requires at least MediaWiki 1.30!';
+						var noticeLink = document.createElement('a');
+						noticeLink.setAttribute('target', '_blank');
+						noticeLink.setAttribute('href', 'https://www.mediawiki.org/wiki/MediaWiki_1.30');
+						noticeLink.textContent = 'https://www.mediawiki.org/wiki/MediaWiki_1.30';
+						wikichecknotice.append(noticeTitle, noticeText, noticeLink);
+						return;
+					}
+					if ( response.RcGcDw !== document.location.pathname.split('/')[2] ) {
+						wikichecknotice.classList.add('notice-info');
+						var noticeTitle = document.createElement('b');
+						noticeTitle.textContent = 'System message does not match!';
+						var sysmessageLink = document.createElement('a');
+						sysmessageLink.setAttribute('target', '_blank');
+						sysmessageLink.setAttribute('href', response.customRcGcDw);
+						var sysmessageCode = document.createElement('code');
+						sysmessageCode.textContent = 'MediaWiki:Custom-RcGcDw';
+						sysmessageLink.append(sysmessageCode);
+						var guildCode = document.createElement('code');
+						guildCode.className = 'user-select';
+						guildCode.textContent = document.location.pathname.split('/')[2];
+						var noticeText = document.createElement('div');
+						noticeText.append(
+							document.createTextNode('The page '),
+							sysmessageLink,
+							document.createTextNode(' needs to be the server id '),
+							guildCode,
+							document.createTextNode('.')
+						);
+						var noticeLink = sysmessageLink.cloneNode();
+						noticeLink.textContent = response.customRcGcDw;
+						wikichecknotice.append(noticeTitle, noticeText, noticeLink);
+						return;
+					}
+					wikichecknotice.classList.add('notice-success');
+					var noticeTitle = document.createElement('b');
+					noticeTitle.textContent = 'The wiki is valid and can be used!';
+					wikichecknotice.append(noticeTitle);
+					return;
+				}
+				wikichecknotice.classList.add('notice-success');
+				var noticeTitle = document.createElement('b');
+				noticeTitle.textContent = 'The wiki is valid and can be used!';
+				wikichecknotice.append(noticeTitle);
+				if ( !/\.(?:gamepedia\.com|fandom\.com|wikia\.org)$/.test(wiki.value.split('/')[2]) ) {
+					if ( !response.MediaWiki ) {
+						var noticeLink = document.createElement('a');
+						noticeLink.setAttribute('target', '_blank');
+						noticeLink.setAttribute('href', 'https://www.mediawiki.org/wiki/MediaWiki_1.30');
+						noticeLink.textContent = 'MediaWiki 1.30';
+						var noticeText = document.createElement('div');
+						noticeText.append(
+							document.createTextNode('Warning: Requires at least '),
+							noticeLink,
+							document.createTextNode(' for full functionality.')
+						);
+						wikichecknotice.append(noticeText);
+					}
+					if ( !response.TextExtracts ) {
+						var noticeLink = document.createElement('a');
+						noticeLink.setAttribute('target', '_blank');
+						noticeLink.setAttribute('href', 'https://www.mediawiki.org/wiki/Extension:TextExtracts');
+						noticeLink.textContent = 'TextExtracts';
+						var noticeText = document.createElement('div');
+						noticeText.append(
+							document.createTextNode('Warning: Requires the extension '),
+							noticeLink,
+							document.createTextNode(' for page descriptions.')
+						);
+						wikichecknotice.append(noticeText);
+					}
+					if ( !response.PageImages ) {
+						var noticeLink = document.createElement('a');
+						noticeLink.setAttribute('target', '_blank');
+						noticeLink.setAttribute('href', 'https://www.mediawiki.org/wiki/Extension:PageImages');
+						noticeLink.textContent = 'PageImages';
+						var noticeText = document.createElement('div');
+						noticeText.append(
+							document.createTextNode('Warning: Requires the extension '),
+							noticeLink,
+							document.createTextNode(' for page thumbnails.')
+						);
+						wikichecknotice.append(noticeText);
+					}
+				}
+			}, function(error) {
+				console.log(error)
+			} ).finally( function() {
+				wiki.removeAttribute('readonly');
+				wikicheck.removeAttribute('disabled');
+			} );
+		}
+	}
 	const feeds = document.getElementById('wb-settings-feeds');
 	if ( feeds ) {
 		const hidefeeds = document.getElementById('wb-settings-feeds-hide');
@@ -155,7 +239,7 @@ if ( wiki ) {
 			}
 		} );
 		wiki.addEventListener( 'input', function() {
-			if ( this.validity.valid && /\.(?:fandom\.com|wikia\.org)$/.test(new URL(this.value).hostname) ) {
+			if ( this.validity.valid && /\.(?:fandom\.com|wikia\.org)$/.test(this.value.split('/')[2]) ) {
 				hidefeeds.removeAttribute('style');
 				feeds.removeAttribute('disabled');
 				if ( !hidefeedsonly.hasAttribute('style') ) feedsonly.removeAttribute('disabled');
@@ -170,20 +254,38 @@ 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', '');
-	}
-} );
+if ( usergroup ) {
+	const multigroup = document.getElementById('wb-settings-usergroup-multiple');
+	const usergrouplist = document.getElementById('wb-settings-usergroup-list');
+	usergroup.addEventListener( 'input', function() {
+		if ( /\s*[,|]\s*$/.test(usergroup.value) ) {
+			var usedGroups = usergroup.value.trim().split(/\s*[,|]\s*/);
+			var lastChar = usergroup.value.substring(usergroup.value.length - 1);
+			usergrouplist.childNodes.forEach( function(listedGroup) {
+				if ( !listedGroup.value ) return;
+				var lastIndex = listedGroup.value.lastIndexOf(lastChar);
+				var originalGroup = listedGroup.value.substring(lastIndex + 1).trim();
+				if ( usedGroups.includes( originalGroup ) ) return;
+				listedGroup.value = `${usergroup.value.trim()} ${originalGroup}`;
+			} );
+		}
+		var newWidth = usergroup.value.trim().length * 7;
+		if ( newWidth < usergroup.parentElement.clientWidth * 0.75 ) {
+			usergroup.setAttribute('style', `min-width: ${newWidth}px;`);
+		}
+		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 ) prefix.addEventListener( 'input', function() {
 	if ( prefix.validity.patternMismatch ) {
 		if ( prefix.value.trim().includes( ' ' ) ) {
 			prefix.setCustomValidity('The prefix may not include spaces!');
@@ -197,4 +299,23 @@ if ( prefix ) prefix.addEventListener( 'input', function () {
 		else prefix.setCustomValidity('');
 	}
 	else prefix.setCustomValidity('');
-} );
+} );
+
+/*
+var collapsible = document.getElementsByClassName('collapsible');
+for ( var i = 0; i < collapsible.length; i++ ) {
+	collapsible[i].onclick = function() {
+		this.classList.toggle('active');
+		if ( this.id === 'wb-settings-wiki-search' ) {
+			wiki.toggleAttribute('readonly');
+		}
+		var content = this.nextElementSibling;
+		if ( content.style.display === 'block' ) {
+			content.style.display = 'none';
+		}
+		else {
+			content.style.display = 'block';
+		}
+	}
+}
+*/

+ 40 - 3
dashboard/util.js

@@ -95,7 +95,7 @@ function sendMsg(message) {
  * @param {String[]} [args] - The arguments for the notice
  * @returns {import('cheerio')}
  */
-function createNotice($, notice, args) {
+function createNotice($, notice, args = []) {
 	if ( !notice ) return;
 	var type = 'info';
 	var title = $('<b>');
@@ -112,6 +112,12 @@ function createNotice($, notice, args) {
 			title.text('Settings saved!');
 			text.text('The settings have been updated successfully.');
 			break;
+		case 'nosettings':
+			type = 'info';
+			title.text('Server not set up yet!');
+			text.text('Please define settings for the server first.');
+			note = $('<a>').text('Change settings.').attr('href', `/guild/${args[0]}/settings`);
+			break;
 		case 'logout':
 			type = 'success';
 			title.text('Successfully logged out!');
@@ -122,6 +128,15 @@ function createNotice($, notice, args) {
 			title.text('Refresh successful!');
 			text.text('Your server list has been successfully refeshed.');
 			break;
+		case 'missingperm':
+			type = 'error';
+			title.text('Missing permission!');
+			text.append(
+				escapeText('Either you or Wiki-Bot are missing the '),
+				$('<code>').text(args[0]),
+				escapeText(' permission for this function.')
+			);
+			break;
 		case 'loginfail':
 			type = 'error';
 			title.text('Login failed!');
@@ -130,7 +145,15 @@ function createNotice($, notice, args) {
 		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]}".`);
+			text.append(
+				escapeText('The page '),
+				$('<a target="_blank">').append(
+					$('<code>').text('MediaWiki:Custom-RcGcDw')
+				).attr('href', args[1]),
+				escapeText(' needs to be the server id '),
+				$('<code class="user-select">').text(args[0]),
+				escapeText('.')
+			);
 			note = $('<a target="_blank">').text(args[1]).attr('href', args[1]);
 			break;
 		case 'mwversion':
@@ -171,6 +194,11 @@ function createNotice($, notice, args) {
 			title.text('Refresh failed!');
 			text.text('You server list could not be refreshed, please try again.');
 			break;
+		case 'error':
+			type = 'error';
+			title.text('Unknown error!');
+			text.text('An unknown error occured, please try again.');
+			break;
 		case 'readonly':
 			type = 'info';
 			title.text('Read-only database!');
@@ -186,6 +214,15 @@ function createNotice($, notice, args) {
 	).appendTo('#text #notices');
 }
 
+/**
+ * HTML escape text
+ * @param {String} text - The text to escape
+ * @returns {String}
+ */
+function escapeText(text) {
+	return text.replace( /&/g, '&amp;' ).replace( /</g, '&lt;' ).replace( />/g, '&gt;' );
+}
+
 const permissions = {
 	ADMINISTRATOR: 1 << 3,
 	MANAGE_CHANNELS: 1 << 4,
@@ -217,4 +254,4 @@ function hasPerm(all = 0, ...permission) {
 	} ).every( perm => perm );
 }
 
-module.exports = {got, db, settingsData, sendMsg, createNotice, hasPerm};
+module.exports = {got, db, settingsData, sendMsg, createNotice, escapeText, hasPerm};

+ 41 - 11
dashboard/verification.js

@@ -1,4 +1,4 @@
-const {limit: {verification: verificationLimit}} = require('../util/default.json');
+const {limit: {verification: verificationLimit}, usergroups} = require('../util/default.json');
 const Lang = require('../util/i18n.js');
 const {got, db, sendMsg, createNotice, hasPerm} = require('./util.js');
 
@@ -10,7 +10,15 @@ 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" autocomplete="on">'
+	+ '<input type="text" id="wb-settings-usergroup" name="usergroup" list="wb-settings-usergroup-list" autocomplete="on">'
+	+ '<datalist id="wb-settings-usergroup-list">'
+	+ usergroups.sorted.filter( group => group !== '__CUSTOM__' ).map( group => {
+		return `<option value="${group}"></option>`
+	} ).join('')
+	+ usergroups.global.filter( group => group !== '__CUSTOM__' ).map( group => {
+		return `<option value="${group}"></option>`
+	} ).join('')
+	+ '</datalist>'
 	+ '<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">'
@@ -22,7 +30,7 @@ const fieldset = {
 	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">'
+	delete: '<input type="submit" id="wb-settings-delete" name="delete_settings" formnovalidate>'
 };
 
 /**
@@ -41,6 +49,7 @@ const fieldset = {
  * @param {String} guildChannels.id
  * @param {String} guildChannels.name
  * @param {Number} guildChannels.userPermissions
+ * @param {Number} guildChannels.botPermissions
  * @param {Object[]} guildRoles - The guild roles
  * @param {String} guildRoles.id
  * @param {String} guildRoles.name
@@ -142,9 +151,13 @@ function createForm($, header, settings, guildChannels, guildRoles) {
 	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);
+	if ( settings.rename || guildChannels.some( guildChannel => {
+		return hasPerm(guildChannel.botPermissions, 'MANAGE_NICKNAMES');
+	} ) ) {
+		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?');`));
@@ -161,6 +174,20 @@ function createForm($, header, settings, guildChannels, guildRoles) {
 	);
 }
 
+const explanation = `
+<h2>User Verification</h2>
+<p>Using the <code>!wiki verify &lt;wiki username&gt;</code> command, users are able to verify themselves as a specific wiki user by using the Discord field on their wiki profile. If the user matches and user verifications are set up on the server, Wiki-Bot will give them the roles for all verification entries they matched.</p>
+<p>Every verification entry allows for multiple restrictions on when a user should match the verification:</p>
+<ul>
+	<li>Channel to use the <code>!wiki verify</code> command in.</li>
+	<li>Role to get when matching the verification entry.</li>
+	<li>Required edit count on the wiki to match the verification entry.</li>
+	<li>Required user group to be a member of on the wiki to match the verification entry.</li>
+	<li>Required account age in days to match the verification entry.</li>
+	<li>Whether the Discord users nickname should be set to their wiki username when they match the verification entry.</li>
+</ul>
+`;
+
 /**
  * Let a user change verifications
  * @param {import('http').ServerResponse} res - The server response
@@ -170,7 +197,8 @@ function createForm($, header, settings, guildChannels, guildRoles) {
  */
 function dashboard_verification(res, $, guild, args) {
 	if ( !hasPerm(guild.botPermissions, 'MANAGE_ROLES') ) {
-		$('#text .description').text('Wiki-Bot is missing the "MANAGE_ROLES" permission!\n\n*Insert explanation about verification here*');
+		createNotice($, 'missingperm', ['Manage Roles']);
+		$('#text .description').html(explanation);
 		$('.channel#verification').addClass('selected');
 		let body = $.html();
 		res.writeHead(200, {'Content-Length': body.length});
@@ -180,7 +208,8 @@ function dashboard_verification(res, $, guild, args) {
 	db.all( 'SELECT wiki, discord.role defaultrole, configid, verification.channel, verification.role, editcount, usergroup, accountage, rename FROM discord LEFT JOIN verification ON discord.guild = verification.guild WHERE discord.guild = ? AND discord.channel IS NULL ORDER BY configid ASC', [guild.id], function(dberror, rows) {
 		if ( dberror ) {
 			console.log( '- Dashboard: Error while getting the verifications: ' + dberror );
-			$('#text .description').text('Failed to load the verifications!');
+			createNotice($, 'error');
+			$('#text .description').html(explanation);
 			$('.channel#verification').addClass('selected');
 			let body = $.html();
 			res.writeHead(200, {'Content-Length': body.length});
@@ -188,7 +217,8 @@ function dashboard_verification(res, $, guild, args) {
 			return res.end();
 		}
 		if ( rows.length === 0 ) {
-			$('#text .description').text(`You need to set up the server first: /guild/${guild.id}/settings`);
+			createNotice($, 'nosettings', [guild.id]);
+			$('#text .description').html(explanation);
 			$('.channel#verification').addClass('selected');
 			let body = $.html();
 			res.writeHead(200, {'Content-Length': body.length});
@@ -198,7 +228,7 @@ function dashboard_verification(res, $, guild, args) {
 		var wiki = rows[0].wiki;
 		var defaultrole = rows[0].defaultrole;
 		if ( rows.length === 1 && rows[0].configid === null ) rows.pop();
-		$('#text .description').text(`These are the verifications for "${guild.name}":`);
+		$('<p>').text(`These are the verifications for "${guild.name}":`).appendTo('#text .description');
 		$('#channellist #verification').after(
 			...rows.map( row => {
 				return $('<a class="channel">').attr('id', `channel-${row.configid}`).append(
@@ -230,7 +260,7 @@ function dashboard_verification(res, $, guild, args) {
 		}
 		else {
 			$('.channel#verification').addClass('selected');
-			$('#text .description').text('*Insert explanation about verification here*');
+			$('#text .description').html(explanation);
 		}
 		let body = $.html();
 		res.writeHead(200, {'Content-Length': body.length});

+ 1 - 0
main.js

@@ -180,6 +180,7 @@ if ( process.env.dashboard ) {
 						let channel = this.guilds.cache.get('${message.data.guild}').publicUpdatesChannel;
 						if ( channel ) channel.send( \`${message.data.text.replace( /`/g, '\\`' )}\`, {
 							embed: ${JSON.stringify(message.data.embed)},
+							files: ${JSON.stringify(message.data.file)},
 							allowedMentions: {parse: []}, split: true
 						} ).catch( error => {} );
 					}`).catch( error => {