瀏覽代碼

Finish Dashboard functions

Markus-Rost 4 年之前
父節點
當前提交
aadd10de0e
共有 8 個文件被更改,包括 1051 次插入133 次删除
  1. 11 7
      dashboard/guilds.js
  2. 5 5
      dashboard/index.js
  3. 495 33
      dashboard/rcscript.js
  4. 84 60
      dashboard/settings.js
  5. 4 2
      dashboard/util.js
  6. 355 19
      dashboard/verification.js
  7. 26 0
      i18n/en.json
  8. 71 7
      main.js

+ 11 - 7
dashboard/guilds.js

@@ -42,7 +42,7 @@ function dashboard_guilds(res, state, reqURL) {
 		}).prependTo('#text');
 		}).prependTo('#text');
 	}
 	}
 	if ( reqURL.searchParams.get('save') === 'success' ) {
 	if ( reqURL.searchParams.get('save') === 'success' ) {
-		$('<script>').text(`history.replaceState(null, null, '${reqURL.pathname}')`).insertBefore('script#indexjs');
+		$('<script>').text(`history.replaceState(null, null, '${reqURL.pathname}');`).insertBefore('script#indexjs');
 		createNotice($, {
 		createNotice($, {
 			type: 'success',
 			type: 'success',
 			title: 'Settings saved!',
 			title: 'Settings saved!',
@@ -50,15 +50,19 @@ function dashboard_guilds(res, state, reqURL) {
 		}).prependTo('#text');
 		}).prependTo('#text');
 	}
 	}
 	if ( reqURL.searchParams.get('save') === 'failed' ) {
 	if ( reqURL.searchParams.get('save') === 'failed' ) {
-		$('<script>').text(`history.replaceState(null, null, '${reqURL.pathname}')`).insertBefore('script#indexjs');
-		let reason = '';
-		if ( reqURL.searchParams.has('reason') ) {
-			reason += '\nReason: ' + reqURL.searchParams.get('reason');
-		}
+		$('<script>').text(`history.replaceState(null, null, '${reqURL.pathname}');`).insertBefore('script#indexjs');
 		createNotice($, {
 		createNotice($, {
 			type: 'error',
 			type: 'error',
 			title: 'Save failed!',
 			title: 'Save failed!',
-			text: 'The settings could not be saved, please try again.' + reason
+			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');
 		}).prependTo('#text');
 	}
 	}
 	if ( process.env.READONLY ) {
 	if ( process.env.READONLY ) {

+ 5 - 5
dashboard/index.js

@@ -3,7 +3,7 @@ const pages = require('./oauth.js');
 const dashboard = require('./guilds.js');
 const dashboard = require('./guilds.js');
 const {db, settingsData} = require('./util.js');
 const {db, settingsData} = require('./util.js');
 
 
-const isDebug = ( process.argv[2] === 'debug' );
+global.isDebug = ( process.argv[2] === 'debug' );
 
 
 const posts = {
 const posts = {
 	settings: require('./settings.js').post,
 	settings: require('./settings.js').post,
@@ -73,7 +73,7 @@ const server = http.createServer((req, res) => {
 						}
 						}
 					}
 					}
 				} );
 				} );
-				if ( isDebug ) console.log( settings );
+				if ( isDebug ) console.log( '- Dashboard:', req.url, settings, settingsData.get(state).user.id );
 				return posts[args[3]](save_response, settingsData.get(state), args[2], args[4], settings);
 				return posts[args[3]](save_response, settingsData.get(state), args[2], args[4], settings);
 			} );
 			} );
 
 
@@ -113,12 +113,12 @@ const server = http.createServer((req, res) => {
 	res.setHeader('Content-Language', ['en']);
 	res.setHeader('Content-Language', ['en']);
 
 
 	var lastGuild = req.headers?.cookie?.split('; ')?.filter( cookie => {
 	var lastGuild = req.headers?.cookie?.split('; ')?.filter( cookie => {
-		return cookie.split('=')[0] === 'guild' && /^\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?$/.test(( cookie.split('=')[1] || '' ));
+		return cookie.split('=')[0] === 'guild' && /^"\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?"$/.test(( cookie.split('=')[1] || '' ));
 	} )?.map( cookie => cookie.replace( /^guild="(\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?)"$/, '$1' ) )?.join();
 	} )?.map( cookie => cookie.replace( /^guild="(\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?)"$/, '$1' ) )?.join();
 	if ( lastGuild ) res.setHeader('Set-Cookie', ['guild=""; HttpOnly; Path=/; Max-Age=0']);
 	if ( lastGuild ) res.setHeader('Set-Cookie', ['guild=""; HttpOnly; Path=/; Max-Age=0']);
 
 
 	var state = req.headers.cookie?.split('; ')?.filter( cookie => {
 	var state = req.headers.cookie?.split('; ')?.filter( cookie => {
-		return cookie.split('=')[0] === 'wikibot';
+		return cookie.split('=')[0] === 'wikibot' && /^"(\w*(?:-\d+)?)"$/.test(( cookie.split('=')[1] || '' ));
 	} )?.map( cookie => cookie.replace( /^wikibot="(\w*(?:-\d+)?)"$/, '$1' ) )?.join();
 	} )?.map( cookie => cookie.replace( /^wikibot="(\w*(?:-\d+)?)"$/, '$1' ) )?.join();
 
 
 	if ( reqURL.pathname === '/login' ) {
 	if ( reqURL.pathname === '/login' ) {
@@ -160,7 +160,7 @@ const server = http.createServer((req, res) => {
 
 
 	if ( reqURL.pathname === '/refresh' ) {
 	if ( reqURL.pathname === '/refresh' ) {
 		let returnLocation = reqURL.searchParams.get('return');
 		let returnLocation = reqURL.searchParams.get('return');
-		if ( returnLocation && ( !returnLocation.startsWith('/') || returnLocation.startsWith('//') ) ) {
+		if ( !/^\/guild\/\d+\/(?:settings|verification|rcscript)(?:\/(?:\d+|new))?$/.test(returnLocation) ) {
 			returnLocation = '/';
 			returnLocation = '/';
 		}
 		}
 		return pages.refresh(res, state, returnLocation);
 		return pages.refresh(res, state, returnLocation);

+ 495 - 33
dashboard/rcscript.js

@@ -1,7 +1,17 @@
+const cheerio = require('cheerio');
 const {defaultSettings, limit: {rcgcdw: rcgcdwLimit}} = require('../util/default.json');
 const {defaultSettings, limit: {rcgcdw: rcgcdwLimit}} = require('../util/default.json');
-const {RcGcDw: allLangs} = require('../i18n/allLangs.json');
+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, hasPerm} = require('./util.js');
 
 
+const display_types = [
+	'compact',
+	'embed',
+	'image',
+	'diff'
+];
+
 const fieldset = {
 const fieldset = {
 	channel: '<label for="wb-settings-channel">Channel:</label>'
 	channel: '<label for="wb-settings-channel">Channel:</label>'
 	+ '<select id="wb-settings-channel" name="channel" required></select>',
 	+ '<select id="wb-settings-channel" name="channel" required></select>',
@@ -47,39 +57,44 @@ const fieldset = {
  * @param {String} header - The form header
  * @param {String} header - The form header
  * @param {Object} settings - The current settings
  * @param {Object} settings - The current settings
  * @param {Boolean} settings.patreon
  * @param {Boolean} settings.patreon
- * @param {String} settings.channel
+ * @param {String} [settings.channel]
  * @param {String} settings.wiki
  * @param {String} settings.wiki
  * @param {String} settings.lang
  * @param {String} settings.lang
  * @param {Number} settings.display
  * @param {Number} settings.display
- * @param {Number} settings.wikiid
- * @param {Number} settings.rcid
+ * @param {Number} [settings.wikiid]
+ * @param {Number} [settings.rcid]
  * @param {Object[]} guildChannels - The guild channels
  * @param {Object[]} guildChannels - The guild channels
  * @param {String} guildChannels.id
  * @param {String} guildChannels.id
  * @param {String} guildChannels.name
  * @param {String} guildChannels.name
- * @param {Number} guildChannels.permissions
+ * @param {Number} guildChannels.userPermissions
+ * @param {Number} guildChannels.botPermissions
  */
  */
 function createForm($, header, settings, guildChannels) {
 function createForm($, header, settings, guildChannels) {
 	var readonly = ( process.env.READONLY ? true : false );
 	var readonly = ( process.env.READONLY ? true : false );
-	readonly = true;
+	var curChannel = guildChannels.find( guildChannel => settings.channel === guildChannel.id );
 	var fields = [];
 	var fields = [];
 	let channel = $('<div>').append(fieldset.channel);
 	let channel = $('<div>').append(fieldset.channel);
-	channel.find('#wb-settings-channel').append(
-		...guildChannels.filter( guildChannel => {
-			return ( hasPerm(guildChannel.permissions, 'VIEW_CHANNEL', 'MANAGE_WEBHOOKS') || settings.channel === guildChannel.id );
-		} ).map( guildChannel => {
-			var optionChannel = $(`<option id="wb-settings-channel-${guildChannel.id}">`).val(guildChannel.id);
-			if ( settings.channel === guildChannel.id ) {
-				optionChannel.attr('selected', '');
-				if ( !hasPerm(guildChannel.permissions, 'VIEW_CHANNEL', 'MANAGE_WEBHOOKS') ) {
-					optionChannel.addClass('wb-settings-error');
-					readonly = true;
+	if ( !settings.channel || ( curChannel && hasPerm(curChannel.botPermissions, 'MANAGE_WEBHOOKS') && hasPerm(curChannel.userPermissions, 'VIEW_CHANNEL', 'MANAGE_WEBHOOKS') ) ) {
+		channel.find('#wb-settings-channel').append(
+			...guildChannels.filter( guildChannel => {
+				return ( hasPerm(guildChannel.userPermissions, 'VIEW_CHANNEL', 'MANAGE_WEBHOOKS') && hasPerm(guildChannel.botPermissions, 'MANAGE_WEBHOOKS') );
+			} ).map( guildChannel => {
+				var optionChannel = $(`<option id="wb-settings-channel-${guildChannel.id}">`).val(guildChannel.id);
+				if ( settings.channel === guildChannel.id ) {
+					optionChannel.attr('selected', '');
 				}
 				}
-			}
-			return optionChannel.text(`${guildChannel.id} – #${guildChannel.name}`);
-		} )
+				return optionChannel.text(`${guildChannel.id} – #${guildChannel.name}`);
+			} )
+		);
+		if ( !settings.channel ) channel.find('#wb-settings-channel').prepend(
+			$(`<option id="wb-settings-channel-default" selected>`).val('').text('-- Select a Channel --')
+		);
+	}
+	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}`)
 	);
 	);
-	if ( !settings.channel ) channel.find('#wb-settings-channel').prepend(
-		$(`<option id="wb-settings-channel-default" selected>`).val('').text('-- Select a Channel --')
+	else channel.find('#wb-settings-channel').append(
+		$(`<option id="wb-settings-channel-${settings.channel}">`).val(settings.channel).attr('selected', '').text(settings.channel)
 	);
 	);
 	fields.push(channel);
 	fields.push(channel);
 	let wiki = $('<div>').append(fieldset.wiki);
 	let wiki = $('<div>').append(fieldset.wiki);
@@ -91,7 +106,7 @@ function createForm($, header, settings, guildChannels) {
 	let display = $('<div>').append(fieldset.display);
 	let display = $('<div>').append(fieldset.display);
 	display.find(`#wb-settings-display-${settings.display}`).attr('checked', '');
 	display.find(`#wb-settings-display-${settings.display}`).attr('checked', '');
 	if ( !settings.patreon ) display.find('.wb-settings-display').filter( (i, radioDisplay) => {
 	if ( !settings.patreon ) display.find('.wb-settings-display').filter( (i, radioDisplay) => {
-		return ( i >= rcgcdwLimit.display && !$(radioDisplay).has('input:checked').length );
+		return ( i > rcgcdwLimit.display && !$(radioDisplay).has('input:checked').length );
 	} ).remove();
 	} ).remove();
 	fields.push(display);
 	fields.push(display);
 	let feeds = $('<div id="wb-settings-feeds-hide">').append(fieldset.feeds);
 	let feeds = $('<div id="wb-settings-feeds-hide">').append(fieldset.feeds);
@@ -108,7 +123,7 @@ function createForm($, header, settings, guildChannels) {
 	}
 	}
 	fields.push(feeds);
 	fields.push(feeds);
 	fields.push($(fieldset.save).val('Save'));
 	fields.push($(fieldset.save).val('Save'));
-	if ( settings.channel ) {
+	if ( settings.channel && curChannel && hasPerm(curChannel.userPermissions, 'MANAGE_WEBHOOKS') ) {
 		fields.push($(fieldset.delete).val('Delete').attr('onclick', `return confirm('Are you sure?');`));
 		fields.push($(fieldset.delete).val('Delete').attr('onclick', `return confirm('Are you sure?');`));
 	}
 	}
 	var form = $('<fieldset>').append(...fields);
 	var form = $('<fieldset>').append(...fields);
@@ -131,7 +146,7 @@ function createForm($, header, settings, guildChannels) {
  * @param {String[]} args - The url parts
  * @param {String[]} args - The url parts
  */
  */
 function dashboard_rcscript(res, $, guild, args) {
 function dashboard_rcscript(res, $, guild, args) {
-	db.all( 'SELECT webhook, configid, wiki, lang, display, wikiid, rcid FROM rcgcdw WHERE guild = ? ORDER BY configid ASC', [guild.id], function(dberror, rows) {
+	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) {
 		if ( dberror ) {
 		if ( dberror ) {
 			console.log( '- Dashboard: Error while getting the RcGcDw: ' + dberror );
 			console.log( '- Dashboard: Error while getting the RcGcDw: ' + dberror );
 			$('#text .description').text('Failed to load the recent changes webhooks!');
 			$('#text .description').text('Failed to load the recent changes webhooks!');
@@ -141,10 +156,25 @@ function dashboard_rcscript(res, $, guild, args) {
 			res.write( body );
 			res.write( body );
 			return res.end();
 			return res.end();
 		}
 		}
+		if ( rows.length === 0 ) {
+			$('#text .description').text(`You need to set up the server first: /guild/${guild.id}/settings`);
+			$('.channel#rcscript').addClass('selected');
+			let body = $.html();
+			res.writeHead(200, {'Content-Length': body.length});
+			res.write( body );
+			return res.end();
+		}
+		var wiki = rows[0].mainwiki;
+		var lang = rows[0].mainlang;
+		if ( rows.length === 1 && rows[0].configid === null ) rows.pop();
 		$('#text .description').text(`These are the recent changes webhooks for "${guild.name}":`);
 		$('#text .description').text(`These are the recent changes webhooks for "${guild.name}":`);
 		Promise.all(rows.map( row => {
 		Promise.all(rows.map( row => {
 			return got.get( 'https://discord.com/api/webhooks/' + row.webhook ).then( response => {
 			return got.get( 'https://discord.com/api/webhooks/' + row.webhook ).then( response => {
-				row.channel = response.body.channel_id;
+				if ( !response.body?.channel_id ) {
+					console.log( '- Dashboard: ' + response.statusCode + ': Error while getting the webhook: ' + response.body?.message );
+					row.channel = 'UNKNOWN';
+				}
+				else row.channel = response.body.channel_id;
 			}, error => {
 			}, error => {
 				console.log( '- Dashboard: Error while getting the webhook: ' + error );
 				console.log( '- Dashboard: Error while getting the webhook: ' + error );
 				row.channel = 'UNKNOWN';
 				row.channel = 'UNKNOWN';
@@ -168,12 +198,12 @@ function dashboard_rcscript(res, $, guild, args) {
 			if ( args[4] === 'new' ) {
 			if ( args[4] === 'new' ) {
 				$('.channel#channel-new').addClass('selected');
 				$('.channel#channel-new').addClass('selected');
 				createForm($, 'New Recent Changes Webhook', {
 				createForm($, 'New Recent Changes Webhook', {
-					wiki: defaultSettings.wiki, lang: defaultSettings.lang,
+					wiki, lang: ( lang in allLangs.names ? lang : defaultSettings.lang ),
 					display: 1, patreon: guild.patreon
 					display: 1, patreon: guild.patreon
 				}, guild.channels).attr('action', `/guild/${guild.id}/rcscript/new`).appendTo('#text');
 				}, guild.channels).attr('action', `/guild/${guild.id}/rcscript/new`).appendTo('#text');
 			}
 			}
-			else if ( rows.some( row => row.configid == args[4] ) ) {
-				let row = rows.find( row => row.configid == args[4] );
+			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');
 				$(`.channel#channel-${row.configid}`).addClass('selected');
 				createForm($, `Recent Changes Webhook #${row.configid}`, Object.assign({
 				createForm($, `Recent Changes Webhook #${row.configid}`, Object.assign({
 					patreon: guild.patreon
 					patreon: guild.patreon
@@ -196,21 +226,453 @@ function dashboard_rcscript(res, $, guild, args) {
  * @param {Function} res - The server response
  * @param {Function} res - The server response
  * @param {import('./util.js').Settings} userSettings - The settings of the user
  * @param {import('./util.js').Settings} userSettings - The settings of the user
  * @param {String} guild - The id of the guild
  * @param {String} guild - The id of the guild
- * @param {String} type - The setting to change
+ * @param {String|Number} type - The setting to change
  * @param {Object} settings - The new settings
  * @param {Object} settings - The new settings
  * @param {String} settings.channel
  * @param {String} settings.channel
  * @param {String} settings.wiki
  * @param {String} settings.wiki
  * @param {String} settings.lang
  * @param {String} settings.lang
- * @param {String} settings.display
+ * @param {Number} settings.display
  * @param {String} [settings.feeds]
  * @param {String} [settings.feeds]
  * @param {String} [settings.feeds_only]
  * @param {String} [settings.feeds_only]
  * @param {String} [settings.save_settings]
  * @param {String} [settings.save_settings]
  * @param {String} [settings.delete_settings]
  * @param {String} [settings.delete_settings]
  */
  */
 function update_rcscript(res, userSettings, guild, type, settings) {
 function update_rcscript(res, userSettings, guild, type, settings) {
-	
-	console.log( settings );
-	return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+	if ( type === 'default' ) {
+		return res(`/guild/${guild}/rcscript?save=failed`);
+	}
+	if ( !settings.save_settings === !settings.delete_settings ) {
+		return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+	}
+	if ( settings.save_settings ) {
+		if ( !settings.wiki || !( settings.lang in allLangs.names ) ) {
+			return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+		}
+		if ( !['0', '1', '2', '3'].includes( settings.display ) ) {
+			return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+		}
+		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`);
+	}
+	if ( settings.delete_settings && type === 'new' ) {
+		return res(`/guild/${guild}/rcscript/new?save=failed`);
+	}
+	if ( type === 'new' ) return sendMsg( {
+		type: 'getMember',
+		member: userSettings.user.id,
+		guild: guild,
+		channel: settings.channel
+	} ).then( response => {
+		if ( !response ) {
+			userSettings.guilds.notMember.set(guild, userSettings.guilds.isMember.get(guild));
+			userSettings.guilds.isMember.delete(guild);
+			return res(`/guild/${guild}?save=failed`);
+		}
+		if ( response === 'noMember' || !hasPerm(response.userPermissions, 'MANAGE_GUILD') ) {
+			userSettings.guilds.isMember.delete(guild);
+			return res('/?save=failed');
+		}
+		if ( response.message === 'noChannel' || !hasPerm(response.botPermissions, 'MANAGE_WEBHOOKS') || !hasPerm(response.userPermissions, 'VIEW_CHANNEL', 'MANAGE_WEBHOOKS') ) {
+			return res(`/guild/${guild}/rcscript/new?save=failed`);
+		}
+		if ( settings.display > rcgcdwLimit.display && !response.patreon ) {
+			settings.display = rcgcdwLimit.display;
+		}
+		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`);
+			}
+			if ( !row ) return res(`/guild/${guild}/rcscript?save=failed`);
+			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`);
+			}
+			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 => {
+				if ( fresponse.statusCode === 404 && typeof fresponse.body === 'string' ) {
+					let api = cheerio.load(fresponse.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|recentchanges&amenableparser=true&siprop=general' + ( wiki.isFandom() ? '|variables' : '' ) + '&titles=Special:RecentChanges&format=json' );
+					}
+				}
+				return fresponse;
+			} ).then( fresponse => {
+				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`);
+				}
+				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`);
+				}
+				if ( body.query.allmessages[0]['*'] !== guild ) {
+					return res(`/guild/${guild}/rcscript/new?save=failed`);
+				}
+				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`);
+					}
+					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(), {
+							headers: {
+								Accept: 'application/hal+json'
+							}
+						} ).then( dsresponse => {
+							var dsbody = dsresponse.body;
+							if ( dsresponse.statusCode !== 200 || !dsbody || dsbody.title ) {
+								if ( dsbody?.title !== 'site doesn\'t exists' ) console.log( '- Dashboard: ' + dsresponse.statusCode + ': Error while checking for discussions: ' + dsbody?.title );
+								return createWebhook();
+							}
+							return createWebhook(parseInt(wikiid, 10));
+						}, error => {
+							console.log( '- Dashboard: Error while checking for discussions: ' + error );
+							return createWebhook();
+						} );
+					}
+					return createWebhook();
+
+					/**
+					 * Creates the webhook.
+					 * @param {Number} wikiid - The ID of the wiki.
+					 */
+					function createWebhook(wikiid = null) {
+						var lang = new Lang(row.lang);
+						var webhook_lang = new Lang(settings.lang, 'rcscript.webhook');
+						sendMsg( {
+							type: 'createWebhook',
+							guild: guild,
+							channel: settings.channel,
+							name: ( body.query.allmessages[1]['*'] || 'Recent changes' ),
+							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`);
+							var configid = 1;
+							for ( let i of row.count ) {
+								if ( configid === i ) configid++;
+								else break;
+							}
+							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`);
+								}
+								console.log( `- Dashboard: RcGcDw successfully added: ${guild}#${configid}` );
+								res(`/guild/${guild}/rcscript/${configid}?save=success`);
+								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}>`;
+								text += `\n${lang.get('rcscript.lang')} \`${allLangs.names[settings.lang]}\``;
+								text += `\n${lang.get('rcscript.display')} \`${display_types[settings.display]}\``;
+								if ( wikiid && settings.feeds_only ) text += `\n${lang.get('rcscript.rc')} *\`${lang.get('rcscript.disabled')}\`*`;
+								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
+								} ).catch( error => {
+									console.log( '- Dashboard: Error while notifying the guild: ' + error );
+								} );
+							} );
+						}, error => {
+							console.log( '- Dashboard: Error while creating the webhook: ' + error );
+							return res(`/guild/${guild}/rcscript/new?save=failed`);
+						} );
+					}
+				} );
+			}, error => {
+				console.log( '- Dashboard: Error while testing the wiki: ' + error );
+				return res(`/guild/${guild}/rcscript/new?save=failed`);
+			} );
+		} );
+	}, error => {
+		console.log( '- Dashboard: Error while getting the member: ' + error );
+		return res(`/guild/${guild}/rcscript/new?save=failed`);
+	} );
+	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`);
+		}
+		if ( !row?.webhook ) return res(`/guild/${guild}/rcscript?save=failed`);
+		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`);
+			}
+			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`);
+				newChannel = true;
+			}
+			return sendMsg( {
+				type: 'getMember',
+				member: userSettings.user.id,
+				guild: guild,
+				channel: row.channel,
+				newchannel: ( newChannel ? settings.channel : undefined )
+			} ).then( response => {
+				if ( !response ) {
+					userSettings.guilds.notMember.set(guild, userSettings.guilds.isMember.get(guild));
+					userSettings.guilds.isMember.delete(guild);
+					return res(`/guild/${guild}?save=failed`);
+				}
+				if ( response === 'noMember' || !hasPerm(response.userPermissions, 'MANAGE_GUILD') ) {
+					userSettings.guilds.isMember.delete(guild);
+					return res('/?save=failed');
+				}
+				if ( response.message === 'noChannel' ) {
+					return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+				}
+				if ( settings.delete_settings ) {
+					if ( !hasPerm(response.userPermissions, 'VIEW_CHANNEL', 'MANAGE_WEBHOOKS') ) {
+						return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+					}
+					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`);
+						}
+						console.log( `- Dashboard: RcGcDw successfully removed: ${guild}#${type}` );
+						res(`/guild/${guild}/rcscript?save=success`);
+						var lang = new Lang(row.mainlang);
+						var webhook_lang = new Lang(row.lang, 'rcscript.webhook');
+						got.post( 'https://discord.com/api/webhooks/' + row.webhook, {
+							json: {
+								content: webhook_lang.get('deleted')
+							}
+						} ).then( delresponse => {
+							if ( delresponse.statusCode !== 204 ) {
+								console.log( '- Dashboard: ' + delresponse.statusCode + ': Error while sending to the webhook: ' + delresponse.body?.message );
+							}
+						}, error => {
+							console.log( '- Dashboard: Error while sending to the webhook: ' + error );
+						} ).finally( () => {
+							got.delete( 'https://discord.com/api/webhooks/' + row.webhook, {
+								headers: {
+									'X-Audit-Log-Reason': lang.get('rcscript.audit_reason_delete')
+								}
+							} ).then( delresponse => {
+								if ( delresponse.statusCode !== 204 ) {
+									console.log( '- Dashboard: ' + delresponse.statusCode + ': Error while removing the webhook: ' + delresponse.body?.message );
+								}
+								else console.log( `- Dashboard: Webhook successfully removed: ${guild}#${row.channel}` );
+							}, error => {
+								console.log( '- Dashboard: Error while removing the webhook: ' + error );
+							} )
+						} );
+						var text = lang.get('rcscript.dashboard.deleted', `<@${userSettings.user.id}>`, type);
+						text += `\n${lang.get('rcscript.channel')} <#${row.channel}>`;
+						text += `\n${lang.get('rcscript.wiki')} <${row.wiki}>`;
+						text += `\n${lang.get('rcscript.lang')} \`${allLangs.names[row.lang]}\``;
+						text += `\n${lang.get('rcscript.display')} \`${display_types[row.display]}\``;
+						if ( row.rcid === -1 ) {
+							text += `\n${lang.get('rcscript.rc')} *\`${lang.get('rcscript.disabled')}\`*`;
+						}
+						if ( new Wiki(row.wiki).isFandom(false) ) text += `\n${lang.get('rcscript.feeds')} *\`${lang.get('rcscript.' + ( row.wikiid ? 'enabled' : 'disabled' ))}\`*`;
+						text += `\n<${new URL(`/guild/${guild}/rcscript`, process.env.dashboard).href}>`;
+						sendMsg( {
+							type: 'notifyGuild', guild, text
+						} ).catch( error => {
+							console.log( '- Dashboard: Error while notifying the guild: ' + error );
+						} );
+					} );
+				}
+				if ( newChannel && ( !hasPerm(response.botPermissions, 'MANAGE_WEBHOOKS') 
+				|| !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`);
+				}
+				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 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' ) {
+						let api = cheerio.load(fresponse.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' + ( wiki.isFandom() ? '|variables' : '' ) + '&format=json' );
+						}
+					}
+					return fresponse;
+				} ).then( fresponse => {
+					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`);
+					}
+					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`);
+					}
+					if ( row.wiki !== wiki.href && body.query.allmessages[0]['*'] !== guild ) {
+						return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+					}
+					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`);
+						}
+						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(), {
+								headers: {
+									Accept: 'application/hal+json'
+								}
+							} ).then( dsresponse => {
+								var dsbody = dsresponse.body;
+								if ( dsresponse.statusCode !== 200 || !dsbody || dsbody.title ) {
+									if ( dsbody?.title !== 'site doesn\'t exists' ) console.log( '- Dashboard: ' + dsresponse.statusCode + ': Error while checking for discussions: ' + dsbody?.title );
+									return updateWebhook();
+								}
+								return updateWebhook(parseInt(wikiid, 10));
+							}, error => {
+								console.log( '- Dashboard: Error while checking for discussions: ' + error );
+								return updateWebhook();
+							} );
+						}
+						return updateWebhook();
+
+						/**
+						 * Creates the webhook.
+						 * @param {Number} wikiid - The ID of the wiki.
+						 */
+						function updateWebhook(wikiid = null) {
+							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`);
+								}
+								console.log( `- Dashboard: RcGcDw successfully updated: ${guild}#${type}` );
+								var lang = new Lang(row.mainlang);
+								var webhook_lang = new Lang(settings.lang, 'rcscript.webhook');
+								var diff = [];
+								var webhook_diff = [];
+								if ( newChannel ) {
+									diff.push(lang.get('rcscript.channel') + ` ~~<#${row.channel}>~~ → <#${settings.channel}>`);
+									webhook_diff.push(webhook_lang.get('dashboard.channel'));
+								}
+								if ( row.wiki !== wiki.href ) {
+									diff.push(lang.get('rcscript.wiki') + ` ~~<${row.wiki}>~~ → <${wiki.href}>`);
+									webhook_diff.push(webhook_lang.get('dashboard.wiki', `[${body.query.general.sitename}](<${wiki.href}>)`));
+								}
+								if ( row.lang !== settings.lang ) {
+									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]));
+								}
+								if ( row.display !== settings.display ) {
+									diff.push(lang.get('rcscript.display') + ` ~~\`${display_types[row.display]}\`~~ → \`${display_types[settings.display]}\``);
+									webhook_diff.push(webhook_lang.get('dashboard.display_' + display_types[settings.display]));
+								}
+								if ( ( row.rcid !== -1 ) !== !( wikiid && settings.feeds_only ) ) {
+									diff.push(lang.get('rcscript.rc') + ` ~~*\`${lang.get('rcscript.' + ( row.rcid === -1 ? 'disabled' : 'enabled' ))}\`*~~ → *\`${lang.get('rcscript.' + ( settings.feeds_only ? 'disabled' : 'enabled' ))}\`*`);
+									webhook_diff.push(webhook_lang.get('dashboard.' + ( settings.feeds_only ? 'disabled_rc' : 'enabled_rc' )));
+								}
+								if ( !row.wikiid !== !wikiid ) {
+									diff.push(lang.get('rcscript.feeds') + ` ~~*\`${lang.get('rcscript.' + ( row.wikiid ? 'enabled' : 'disabled' ))}\`*~~ → *\`${lang.get('rcscript.' + ( wikiid ? 'enabled' : 'disabled' ))}\`*`);
+									webhook_diff.push(webhook_lang.get('dashboard.' + ( wikiid ? 'disabled_feeds' : 'enabled_feeds' )));
+								}
+								if ( newChannel ) return sendMsg( {
+									type: 'moveWebhook',
+									guild: guild,
+									webhook: row.webhook,
+									channel: settings.channel,
+									reason: lang.get('rcscript.audit_reason_move'),
+									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`);
+									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}>`;
+									sendMsg( {
+										type: 'notifyGuild', guild, text
+									} ).catch( error => {
+										console.log( '- Dashboard: Error while notifying the guild: ' + error );
+									} );
+								}, error => {
+									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();
+									got.post( 'https://discord.com/api/webhooks/' + row.webhook, {
+										json: {
+											content: webhook_lang.get('dashboard.updated') + '\n' + webhook_diff.join('\n')
+										}
+									} ).then( delresponse => {
+										if ( delresponse.statusCode !== 204 ) {
+											console.log( '- Dashboard: ' + delresponse.statusCode + ': Error while sending to the webhook: ' + delresponse.body?.message );
+										}
+									}, error => {
+										console.log( '- Dashboard: Error while sending to the webhook: ' + error );
+									} )
+									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}>`;
+									sendMsg( {
+										type: 'notifyGuild', guild, text
+									} ).catch( error => {
+										console.log( '- Dashboard: Error while notifying the guild: ' + error );
+									} );
+								} );
+								res(`/guild/${guild}/rcscript/${type}?save=success`);
+								got.post( 'https://discord.com/api/webhooks/' + row.webhook, {
+									json: {
+										content: webhook_lang.get('dashboard.updated') + '\n' + webhook_diff.join('\n')
+									}
+								} ).then( delresponse => {
+									if ( delresponse.statusCode !== 204 ) {
+										console.log( '- Dashboard: ' + delresponse.statusCode + ': Error while sending to the webhook: ' + delresponse.body?.message );
+									}
+								}, error => {
+									console.log( '- Dashboard: Error while sending to the webhook: ' + error );
+								} )
+								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}>`;
+								sendMsg( {
+									type: 'notifyGuild', guild, text
+								} ).catch( error => {
+									console.log( '- Dashboard: Error while notifying the guild: ' + error );
+								} );
+							} );
+						}
+					} );
+				}, error => {
+					console.log( '- Dashboard: Error while testing the wiki: ' + error );
+					return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+				} );
+			}, error => {
+				console.log( '- Dashboard: Error while getting the member: ' + error );
+				return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+			} );
+		}, error => {
+			console.log( '- Dashboard: Error while getting the webhook: ' + error );
+			return res(`/guild/${guild}/rcscript/${type}?save=failed`);
+		} );
+	} );
 }
 }
 
 
 module.exports = {
 module.exports = {

+ 84 - 60
dashboard/settings.js

@@ -1,3 +1,4 @@
+const cheerio = require('cheerio');
 const {defaultSettings} = require('../util/default.json');
 const {defaultSettings} = require('../util/default.json');
 const Lang = require('../util/i18n.js');
 const Lang = require('../util/i18n.js');
 const allLangs = Lang.allLangs();
 const allLangs = Lang.allLangs();
@@ -44,11 +45,11 @@ const fieldset = {
  * @param {Object[]} guildChannels - The guild channels
  * @param {Object[]} guildChannels - The guild channels
  * @param {String} guildChannels.id
  * @param {String} guildChannels.id
  * @param {String} guildChannels.name
  * @param {String} guildChannels.name
- * @param {Number} guildChannels.permissions
+ * @param {Number} guildChannels.userPermissions
  */
  */
 function createForm($, header, settings, guildChannels) {
 function createForm($, header, settings, guildChannels) {
 	var readonly = ( process.env.READONLY ? true : false );
 	var readonly = ( process.env.READONLY ? true : false );
-	if ( settings.channel && guildChannels.permissions === 0 && guildChannels.name === 'UNKNOWN' ) {
+	if ( settings.channel && guildChannels.userPermissions === 0 && guildChannels.name === 'UNKNOWN' ) {
 		readonly = true;
 		readonly = true;
 	}
 	}
 	var fields = [];
 	var fields = [];
@@ -61,7 +62,7 @@ function createForm($, header, settings, guildChannels) {
 		);
 		);
 		if ( guildChannels.length === 1 ) {
 		if ( guildChannels.length === 1 ) {
 			channel.find(`#wb-settings-channel-${settings.channel}`).attr('selected', '');
 			channel.find(`#wb-settings-channel-${settings.channel}`).attr('selected', '');
-			if ( !hasPerm(guildChannels[0].permissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') ) {
+			if ( !hasPerm(guildChannels[0].userPermissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') ) {
 				readonly = true;
 				readonly = true;
 			}
 			}
 		}
 		}
@@ -137,7 +138,7 @@ function dashboard_settings(res, $, guild, args) {
 		let isPatreon = rows.some( row => row.patreon );
 		let isPatreon = rows.some( row => row.patreon );
 		let channellist = rows.filter( row => row.channel ).map( row => {
 		let channellist = rows.filter( row => row.channel ).map( row => {
 			let channel = guild.channels.find( channel => channel.id === row.channel );
 			let channel = guild.channels.find( channel => channel.id === row.channel );
-			return ( channel || {id: row.channel, name: 'UNKNOWN', permissions: 0} );
+			return ( channel || {id: row.channel, name: 'UNKNOWN', userPermissions: 0} );
 		} ).sort( (a, b) => {
 		} ).sort( (a, b) => {
 			return guild.channels.indexOf(a) - guild.channels.indexOf(b);
 			return guild.channels.indexOf(a) - guild.channels.indexOf(b);
 		} );
 		} );
@@ -149,7 +150,7 @@ function dashboard_settings(res, $, guild, args) {
 				).attr('href', `/guild/${guild.id}/settings/${channel.id}`).attr('title', channel.id);
 				).attr('href', `/guild/${guild.id}/settings/${channel.id}`).attr('title', channel.id);
 			} ),
 			} ),
 			( process.env.READONLY || !guild.channels.filter( channel => {
 			( process.env.READONLY || !guild.channels.filter( channel => {
-				return ( hasPerm(channel.permissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') && !rows.some( row => row.channel === channel.id ) );
+				return ( hasPerm(channel.userPermissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') && !rows.some( row => row.channel === channel.id ) );
 			} ).length ? '' :
 			} ).length ? '' :
 			$('<a class="channel" id="channel-new">').append(
 			$('<a class="channel" id="channel-new">').append(
 				$('<img>').attr('src', '/src/channel.svg'),
 				$('<img>').attr('src', '/src/channel.svg'),
@@ -162,7 +163,7 @@ function dashboard_settings(res, $, guild, args) {
 				patreon: isPatreon,
 				patreon: isPatreon,
 				channel: 'new'
 				channel: 'new'
 			}), guild.channels.filter( channel => {
 			}), guild.channels.filter( channel => {
-				return ( hasPerm(channel.permissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') && !rows.some( row => row.channel === channel.id ) );
+				return ( hasPerm(channel.userPermissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') && !rows.some( row => row.channel === channel.id ) );
 			} )).attr('action', `/guild/${guild.id}/settings/new`).appendTo('#text');
 			} )).attr('action', `/guild/${guild.id}/settings/new`).appendTo('#text');
 		}
 		}
 		else if ( channellist.some( channel => channel.id === args[4] ) ) {
 		else if ( channellist.some( channel => channel.id === args[4] ) ) {
@@ -202,55 +203,78 @@ function dashboard_settings(res, $, guild, args) {
  * @param {String} [settings.delete_settings]
  * @param {String} [settings.delete_settings]
  */
  */
 function update_settings(res, userSettings, guild, type, settings) {
 function update_settings(res, userSettings, guild, type, settings) {
+	if ( type !== 'default' && type !== 'new' && type !== settings.channel ) {
+		return res(`/guild/${guild}/settings?save=failed`);
+	}
+	if ( !settings.save_settings === !settings.delete_settings ) {
+		return res(`/guild/${guild}/settings/${type}?save=failed`);
+	}
+	if ( settings.save_settings ) {
+		if ( !settings.wiki || ( settings.lang && !( settings.lang in allLangs.names ) ) ) {
+			return res(`/guild/${guild}/settings/${type}?save=failed`);
+		}
+		if ( settings.channel && !userSettings.guilds.isMember.get(guild).channels.some( channel => {
+			return ( channel.id === settings.channel );
+		} ) ) return res(`/guild/${guild}/settings/${type}?save=failed`);
+	}
+	if ( settings.delete_settings && ( type === 'default' || type === 'new' ) ) {
+		return res(`/guild/${guild}/settings/${type}?save=failed`);
+	}
 	sendMsg( {
 	sendMsg( {
 		type: 'getMember',
 		type: 'getMember',
 		member: userSettings.user.id,
 		member: userSettings.user.id,
-		guild: guild
+		guild: guild,
+		channel: ( type === settings.channel ? type : undefined )
 	} ).then( response => {
 	} ).then( response => {
 		if ( !response ) {
 		if ( !response ) {
 			userSettings.guilds.notMember.set(guild, userSettings.guilds.isMember.get(guild));
 			userSettings.guilds.notMember.set(guild, userSettings.guilds.isMember.get(guild));
 			userSettings.guilds.isMember.delete(guild);
 			userSettings.guilds.isMember.delete(guild);
 			return res(`/guild/${guild}?save=failed`);
 			return res(`/guild/${guild}?save=failed`);
 		}
 		}
-		if ( response === 'noMember' || !hasPerm(response.permissions, 'MANAGE_GUILD') ) {
+		if ( response === 'noMember' || !hasPerm(response.userPermissions, 'MANAGE_GUILD') ) {
 			userSettings.guilds.isMember.delete(guild);
 			userSettings.guilds.isMember.delete(guild);
 			return res('/?save=failed');
 			return res('/?save=failed');
 		}
 		}
-		if ( type !== 'default' && type !== 'new' && type !== settings.channel ) {
+		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`);
+			}
+			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 ( type === settings.channel && !hasPerm(response.userPermissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') ) {
 			return res(`/guild/${guild}/settings/${type}?save=failed`);
 			return res(`/guild/${guild}/settings/${type}?save=failed`);
 		}
 		}
-		if ( settings.channel && !userSettings.guilds.isMember.get(guild).channels.some( channel => {
-			return ( channel.id === settings.channel );
-		} ) ) return res(`/guild/${guild}/settings/${type}?save=failed`);
-		if ( !settings.save_settings ) {
-			if ( settings.delete_settings && type !== 'default' && type !== 'new' ) return db.run( 'DELETE FROM discord WHERE guild = ? AND channel = ?', [guild, type], function (delerror) {
+		if ( settings.delete_settings ) return db.get( 'SELECT main.lang mainlang, main.patreon, main.lang mainwiki, main.lang maininline, old.wiki, old.lang, 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 ) {
 				if ( delerror ) {
 					console.log( '- Dashboard: Error while removing the settings: ' + delerror );
 					console.log( '- Dashboard: Error while removing the settings: ' + delerror );
 					return res(`/guild/${guild}/settings/${type}?save=failed`);
 					return res(`/guild/${guild}/settings/${type}?save=failed`);
 				}
 				}
 				console.log( `- Dashboard: Settings successfully removed: ${guild}#${type}` );
 				console.log( `- Dashboard: Settings successfully removed: ${guild}#${type}` );
 				res(`/guild/${guild}/settings?save=success`);
 				res(`/guild/${guild}/settings?save=success`);
-				db.get( 'SELECT lang FROM discord WHERE guild = ? AND channel IS NULL', [guild], function(dberror, row) {
-					if ( dberror ) {
-						console.log( '- Dashboard: Error while notifying the guild: ' + dberror );
-						return;
-					}
-					if ( !row ) return;
-					var lang = new Lang(row.lang);
-					var text = lang.get('settings.dashboard.removed', `<@${userSettings.user.id}>`, `<#${type}>`);
-					text += '\n' + new URL(`/guild/${guild}/settings`, process.env.dashboard).href;
-					sendMsg( {
-						type: 'notifyGuild', guild, text
-					} ).catch( error => {
-						console.log( '- Dashboard: Error while notifying the guild: ' + error );
-					} );
+				if ( dberror ) {
+					console.log( '- Dashboard: Error while notifying the guild: ' + dberror );
+					return;
+				}
+				if ( !row || row.wiki === null ) return;
+				var lang = new Lang(row.mainlang);
+				var text = lang.get('settings.dashboard.removed', `<@${userSettings.user.id}>`, `<#${type}>`);
+				if ( row.wiki !== row.mainwiki ) text += `\n${lang.get('settings.currentwiki')} <${row.wiki}>`;
+				if ( row.patreon ) {
+					if ( row.lang !== row.mainlang ) text += `\n${lang.get('settings.currentlang')} \`${allLangs.names[row.lang]}\``;
+					if ( row.inline !== row.maininline ) text += `\n${lang.get('settings.currentinline')} ${( row.inline ? '~~' : '' )}\`[[${inlinepage}]]\`${( row.inline ? '~~' : '' )}`;
+				}
+				text += `\n<${new URL(`/guild/${guild}/settings`, process.env.dashboard).href}>`;
+				sendMsg( {
+					type: 'notifyGuild', guild, text
+				} ).catch( error => {
+					console.log( '- Dashboard: Error while notifying the guild: ' + error );
 				} );
 				} );
 			} );
 			} );
-			return res(`/guild/${guild}/settings?save=failed`);
-		}
-		if ( !settings.wiki || ( settings.lang && !( settings.lang in allLangs.names ) ) ) {
-			return res(`/guild/${guild}/settings?save=failed`);
-		}
+		} );
 		var wiki = Wiki.fromInput(settings.wiki);
 		var wiki = Wiki.fromInput(settings.wiki);
 		return got.get( wiki + 'api.php?&action=query&meta=siteinfo&siprop=general|extensions&format=json' ).then( fresponse => {
 		return got.get( wiki + 'api.php?&action=query&meta=siteinfo&siprop=general|extensions&format=json' ).then( fresponse => {
 			if ( fresponse.statusCode === 404 && typeof fresponse.body === 'string' ) {
 			if ( fresponse.statusCode === 404 && typeof fresponse.body === 'string' ) {
@@ -263,7 +287,7 @@ function update_settings(res, userSettings, guild, type, settings) {
 			return fresponse;
 			return fresponse;
 		} ).then( fresponse => {
 		} ).then( fresponse => {
 			return new Promise( function (resolve, reject) {
 			return new Promise( function (resolve, reject) {
-				db.get( 'SELECT lang, wiki, prefix, inline, patreon FROM discord WHERE guild = ? AND channel IS NULL', [guild], function(error, row) {
+				db.get( 'SELECT lang, wiki, prefix, inline FROM discord WHERE guild = ? AND channel IS NULL', [guild], function(error, row) {
 					if ( error ) {
 					if ( error ) {
 						console.log( '- Dashboard: Error while getting the settings: ' + error );
 						console.log( '- Dashboard: Error while getting the settings: ' + error );
 						reject();
 						reject();
@@ -340,7 +364,7 @@ function update_settings(res, userSettings, guild, type, settings) {
 						text += '\n' + lang.get('settings.currentprefix') + ` \`${settings.prefix.replace( /\\/g, '\\$&' )}\``;
 						text += '\n' + lang.get('settings.currentprefix') + ` \`${settings.prefix.replace( /\\/g, '\\$&' )}\``;
 					}
 					}
 					text += '\n' + lang.get('settings.currentinline') + ` ${( settings.inline ? '' : '~~' )}\`[[${( lang.localNames.page || 'page' )}]]\`${( settings.inline ? '' : '~~' )}`;
 					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;
+					text += `\n<${new URL(`/guild/${guild}/settings`, process.env.dashboard).href}>`;
 					sendMsg( {
 					sendMsg( {
 						type: 'notifyGuild', guild, text, embed
 						type: 'notifyGuild', guild, text, embed
 					} ).catch( error => {
 					} ).catch( error => {
@@ -370,7 +394,7 @@ function update_settings(res, userSettings, guild, type, settings) {
 					res(`/guild/${guild}/settings?save=success`);
 					res(`/guild/${guild}/settings?save=success`);
 					var text = lang.get('settings.dashboard.updated', `<@${userSettings.user.id}>`);
 					var text = lang.get('settings.dashboard.updated', `<@${userSettings.user.id}>`);
 					text += '\n' + diff.join('\n');
 					text += '\n' + diff.join('\n');
-					text += '\n' + new URL(`/guild/${guild}/settings`, process.env.dashboard).href;
+					text += `\n<${new URL(`/guild/${guild}/settings`, process.env.dashboard).href}>`;
 					sendMsg( {
 					sendMsg( {
 						type: 'notifyGuild', guild, text, embed,
 						type: 'notifyGuild', guild, text, embed,
 						prefix: settings.prefix, voice: settings.lang
 						prefix: settings.prefix, voice: settings.lang
@@ -397,7 +421,7 @@ function update_settings(res, userSettings, guild, type, settings) {
 					console.log( `- Dashboard: Settings successfully removed: ${guild}#${type}` );
 					console.log( `- Dashboard: Settings successfully removed: ${guild}#${type}` );
 					res(`/guild/${guild}/settings?save=success`);
 					res(`/guild/${guild}/settings?save=success`);
 					var text = lang.get('settings.dashboard.removed', `<@${userSettings.user.id}>`, `<#${type}>`);
 					var text = lang.get('settings.dashboard.removed', `<@${userSettings.user.id}>`, `<#${type}>`);
-					text += '\n' + new URL(`/guild/${guild}/settings`, process.env.dashboard).href;
+					text += `\n<${new URL(`/guild/${guild}/settings`, process.env.dashboard).href}>`;
 					sendMsg( {
 					sendMsg( {
 						type: 'notifyGuild', guild, text
 						type: 'notifyGuild', guild, text
 					} ).catch( error => {
 					} ).catch( error => {
@@ -422,31 +446,31 @@ function update_settings(res, userSettings, guild, type, settings) {
 					let inlinepage = ( lang.localNames.page || 'page' );
 					let inlinepage = ( lang.localNames.page || 'page' );
 					diff.push(lang.get('settings.currentinline') + ` ${( channel.inline ? '~~' : '' )}\`[[${inlinepage}]]\`${( channel.inline ? '~~' : '' )} → ${( settings.inline ? '' : '~~' )}\`[[${inlinepage}]]\`${( settings.inline ? '' : '~~' )}`);
 					diff.push(lang.get('settings.currentinline') + ` ${( channel.inline ? '~~' : '' )}\`[[${inlinepage}]]\`${( channel.inline ? '~~' : '' )} → ${( settings.inline ? '' : '~~' )}\`[[${inlinepage}]]\`${( settings.inline ? '' : '~~' )}`);
 				}
 				}
-				if ( diff.length ) {
-					let sql = 'UPDATE discord SET wiki = ?, lang = ?, inline = ? WHERE guild = ? AND channel = ?';
-					let sqlargs = [wiki.href, ( settings.lang || channel.lang ), ( response.patreon ? ( settings.inline ? null : 1 ) : channel.inline ), guild, settings.channel];
-					if ( channel === row ) {
-						sql = 'INSERT INTO discord(wiki, lang, inline, guild, channel, prefix) VALUES(?, ?, ?, ?, ?, ?)';
-						sqlargs.push(row.prefix);
+				if ( !diff.length ) {
+					return res(`/guild/${guild}/settings/${settings.channel}?save=success`);
+				}
+				let sql = 'UPDATE discord SET wiki = ?, lang = ?, inline = ? WHERE guild = ? AND channel = ?';
+				let sqlargs = [wiki.href, ( settings.lang || channel.lang ), ( response.patreon ? ( settings.inline ? null : 1 ) : channel.inline ), guild, settings.channel];
+				if ( channel === row ) {
+					sql = 'INSERT INTO discord(wiki, lang, inline, guild, channel, prefix) VALUES(?, ?, ?, ?, ?, ?)';
+					sqlargs.push(row.prefix);
+				}
+				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 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`);
-						}
-						console.log( `- Dashboard: Settings successfully saved: ${guild}#${settings.channel}` );
-						res(`/guild/${guild}/settings/${settings.channel}?save=success`);
-						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;
-						sendMsg( {
-							type: 'notifyGuild', guild, text, embed
-						} ).catch( error => {
-							console.log( '- Dashboard: Error while notifying the guild: ' + error );
-						} );
+					console.log( `- Dashboard: Settings successfully saved: ${guild}#${settings.channel}` );
+					res(`/guild/${guild}/settings/${settings.channel}?save=success`);
+					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}>`;
+					sendMsg( {
+						type: 'notifyGuild', guild, text, embed
+					} ).catch( error => {
+						console.log( '- Dashboard: Error while notifying the guild: ' + error );
 					} );
 					} );
-				}
-				return res(`/guild/${guild}/settings/${settings.channel}?save=success`);
+				} );
 			} );
 			} );
 		}, () => {
 		}, () => {
 			return res(`/guild/${guild}/settings/${type}?save=failed`);
 			return res(`/guild/${guild}/settings/${type}?save=failed`);

+ 4 - 2
dashboard/util.js

@@ -1,4 +1,6 @@
 const got = require('got').extend( {
 const got = require('got').extend( {
+	throwHttpErrors: false,
+	timeout: 5000,
 	headers: {
 	headers: {
 		'User-Agent': 'Wiki-Bot/dashboard (Discord; ' + process.env.npm_package_name + ')'
 		'User-Agent': 'Wiki-Bot/dashboard (Discord; ' + process.env.npm_package_name + ')'
 	},
 	},
@@ -48,7 +50,7 @@ const db = new sqlite3.Database( './wikibot.db', mode, dberror => {
  * @property {String} userPermissions
  * @property {String} userPermissions
  * @property {Boolean} [patreon]
  * @property {Boolean} [patreon]
  * @property {String} [botPermissions]
  * @property {String} [botPermissions]
- * @property {{id: String, name: String, permissions: Number}[]} [channels]
+ * @property {{id: String, name: String, userPermissions: Number, botPermissions: Number}[]} [channels]
  * @property {{id: String, name: String, lower: Boolean}[]} [roles]
  * @property {{id: String, name: String, lower: Boolean}[]} [roles]
  */
  */
 
 
@@ -126,7 +128,7 @@ const permissions = {
  * @param {String[]} permission - Name of the permission to check for
  * @param {String[]} permission - Name of the permission to check for
  * @returns {Boolean}
  * @returns {Boolean}
  */
  */
-function hasPerm(all, ...permission) {
+function hasPerm(all = 0, ...permission) {
 	if ( (all & permissions.ADMINISTRATOR) === permissions.ADMINISTRATOR ) return true;
 	if ( (all & permissions.ADMINISTRATOR) === permissions.ADMINISTRATOR ) return true;
 	return permission.map( perm => {
 	return permission.map( perm => {
 		let bit = permissions[perm];
 		let bit = permissions[perm];

+ 355 - 19
dashboard/verification.js

@@ -1,5 +1,6 @@
 const {limit: {verification: verificationLimit}} = require('../util/default.json');
 const {limit: {verification: verificationLimit}} = require('../util/default.json');
-const {db, sendMsg, hasPerm} = require('./util.js');
+const Lang = require('../util/i18n.js');
+const {got, db, sendMsg, hasPerm} = require('./util.js');
 
 
 const fieldset = {
 const fieldset = {
 	channel: '<label for="wb-settings-channel">Channel:</label>'
 	channel: '<label for="wb-settings-channel">Channel:</label>'
@@ -9,7 +10,10 @@ const fieldset = {
 	+ '<select id="wb-settings-role" name="role" required></select>'
 	+ '<select id="wb-settings-role" name="role" required></select>'
 	+ '<button type="button" id="wb-settings-role-more" class="addmore">Add more</button>',
 	+ '<button type="button" id="wb-settings-role-more" class="addmore">Add more</button>',
 	usergroup: '<label for="wb-settings-usergroup">Wiki user group:</label>'
 	usergroup: '<label for="wb-settings-usergroup">Wiki user group:</label>'
-	+ '<input type="text" id="wb-settings-usergroup" name="usergroup" required>',
+	+ '<input type="text" id="wb-settings-usergroup" name="usergroup">'
+	+ '<br>'
+	+ '<label for="wb-settings-usergroup-and">Require all user groups:</label>'
+	+ '<input type="checkbox" id="wb-settings-usergroup-and" name="usergroup_and">',
 	editcount: '<label for="wb-settings-editcount">Minimal edit count:</label>'
 	editcount: '<label for="wb-settings-editcount">Minimal edit count:</label>'
 	+ '<input type="number" id="wb-settings-editcount" name="editcount" min="0" required>',
 	+ '<input type="number" id="wb-settings-editcount" name="editcount" min="0" required>',
 	accountage: '<label for="wb-settings-accountage">Account age (in days):</label>'
 	accountage: '<label for="wb-settings-accountage">Account age (in days):</label>'
@@ -34,7 +38,7 @@ const fieldset = {
  * @param {Object[]} guildChannels - The guild channels
  * @param {Object[]} guildChannels - The guild channels
  * @param {String} guildChannels.id
  * @param {String} guildChannels.id
  * @param {String} guildChannels.name
  * @param {String} guildChannels.name
- * @param {Number} guildChannels.permissions
+ * @param {Number} guildChannels.userPermissions
  * @param {Object[]} guildRoles - The guild roles
  * @param {Object[]} guildRoles - The guild roles
  * @param {String} guildRoles.id
  * @param {String} guildRoles.id
  * @param {String} guildRoles.name
  * @param {String} guildRoles.name
@@ -42,16 +46,15 @@ const fieldset = {
  */
  */
 function createForm($, header, settings, guildChannels, guildRoles) {
 function createForm($, header, settings, guildChannels, guildRoles) {
 	var readonly = ( process.env.READONLY ? true : false );
 	var readonly = ( process.env.READONLY ? true : false );
-	readonly = true;
 	var fields = [];
 	var fields = [];
 	let channel = $('<div>').append(fieldset.channel);
 	let channel = $('<div>').append(fieldset.channel);
 	channel.find('#wb-settings-channel').append(
 	channel.find('#wb-settings-channel').append(
 		$('<option class="wb-settings-channel-default defaultSelect" hidden>').val('').text('-- Select a Channel --'),
 		$('<option class="wb-settings-channel-default defaultSelect" hidden>').val('').text('-- Select a Channel --'),
 		...guildChannels.filter( guildChannel => {
 		...guildChannels.filter( guildChannel => {
-			return ( hasPerm(guildChannel.permissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') || settings.channel.includes( '|' + guildChannel.id + '|' ) );
+			return ( hasPerm(guildChannel.userPermissions, 'VIEW_CHANNEL') || settings.channel.includes( '|' + guildChannel.id + '|' ) );
 		} ).map( guildChannel => {
 		} ).map( guildChannel => {
 			var optionChannel = $(`<option class="wb-settings-channel-${guildChannel.id}">`).val(guildChannel.id);
 			var optionChannel = $(`<option class="wb-settings-channel-${guildChannel.id}">`).val(guildChannel.id);
-			if ( !hasPerm(guildChannel.permissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') ) {
+			if ( !hasPerm(guildChannel.userPermissions, 'VIEW_CHANNEL') ) {
 				optionChannel.addClass('wb-settings-error');
 				optionChannel.addClass('wb-settings-error');
 			}
 			}
 			return optionChannel.text(`${guildChannel.id} – #${guildChannel.name}`);
 			return optionChannel.text(`${guildChannel.id} – #${guildChannel.name}`);
@@ -119,6 +122,10 @@ function createForm($, header, settings, guildChannels, guildRoles) {
 	}
 	}
 	fields.push(role);
 	fields.push(role);
 	let usergroup = $('<div>').append(fieldset.usergroup);
 	let usergroup = $('<div>').append(fieldset.usergroup);
+	if ( settings.usergroup.startsWith( 'AND|' ) ) {
+		settings.usergroup = settings.usergroup.substring(4);
+		usergroup.find('#wb-settings-usergroup-and').attr('checked', '');
+	}
 	usergroup.find('#wb-settings-usergroup').val(settings.usergroup.split('|').join(', '));
 	usergroup.find('#wb-settings-usergroup').val(settings.usergroup.split('|').join(', '));
 	fields.push(usergroup);
 	fields.push(usergroup);
 	let editcount = $('<div>').append(fieldset.editcount);
 	let editcount = $('<div>').append(fieldset.editcount);
@@ -154,7 +161,15 @@ function createForm($, header, settings, guildChannels, guildRoles) {
  * @param {String[]} args - The url parts
  * @param {String[]} args - The url parts
  */
  */
 function dashboard_verification(res, $, guild, args) {
 function dashboard_verification(res, $, guild, args) {
-	db.all( 'SELECT configid, channel, role, editcount, usergroup, accountage, rename FROM verification WHERE guild = ? ORDER BY configid ASC', [guild.id], function(dberror, rows) {
+	if ( !hasPerm(guild.botPermissions, 'MANAGE_ROLES') ) {
+		$('#text .description').text('Wiki-Bot is missing the "MANAGE_ROLES" permission!\n\n*Insert explanation about verification here*');
+		$('.channel#verification').addClass('selected');
+		let body = $.html();
+		res.writeHead(200, {'Content-Length': body.length});
+		res.write( body );
+		return res.end();
+	}
+	db.all( 'SELECT wiki, configid, verification.channel, 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 ) {
 		if ( dberror ) {
 			console.log( '- Dashboard: Error while getting the verifications: ' + dberror );
 			console.log( '- Dashboard: Error while getting the verifications: ' + dberror );
 			$('#text .description').text('Failed to load the verifications!');
 			$('#text .description').text('Failed to load the verifications!');
@@ -164,6 +179,16 @@ function dashboard_verification(res, $, guild, args) {
 			res.write( body );
 			res.write( body );
 			return res.end();
 			return res.end();
 		}
 		}
+		if ( rows.length === 0 ) {
+			$('#text .description').text(`You need to set up the server first: /guild/${guild.id}/settings`);
+			$('.channel#verification').addClass('selected');
+			let body = $.html();
+			res.writeHead(200, {'Content-Length': body.length});
+			res.write( body );
+			return res.end();
+		}
+		var wiki = rows[0].wiki;
+		if ( rows.length === 1 && rows[0].configid === null ) rows.pop();
 		$('#text .description').text(`These are the verifications for "${guild.name}":`);
 		$('#text .description').text(`These are the verifications for "${guild.name}":`);
 		$('#channellist #verification').after(
 		$('#channellist #verification').after(
 			...rows.map( row => {
 			...rows.map( row => {
@@ -189,14 +214,14 @@ function dashboard_verification(res, $, guild, args) {
 				editcount: 0, accountage: 0, rename: false
 				editcount: 0, accountage: 0, rename: false
 			}, guild.channels, guild.roles).attr('action', `/guild/${guild.id}/verification/new`).appendTo('#text');
 			}, guild.channels, guild.roles).attr('action', `/guild/${guild.id}/verification/new`).appendTo('#text');
 		}
 		}
-		else if ( rows.some( row => row.configid == args[4] ) ) {
-			let row = rows.find( row => row.configid == args[4] );
+		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');
 			$(`.channel#channel-${row.configid}`).addClass('selected');
 			createForm($, `Verification #${row.configid}`, row, guild.channels, guild.roles).attr('action', `/guild/${guild.id}/verification/${row.configid}`).appendTo('#text');
 			createForm($, `Verification #${row.configid}`, row, guild.channels, guild.roles).attr('action', `/guild/${guild.id}/verification/${row.configid}`).appendTo('#text');
 		}
 		}
 		else {
 		else {
 			$('.channel#verification').addClass('selected');
 			$('.channel#verification').addClass('selected');
-			$('#text .description').text(`*Insert explanation about verification here*`);
+			$('#text .description').text('*Insert explanation about verification here*');
 		}
 		}
 		let body = $.html();
 		let body = $.html();
 		res.writeHead(200, {'Content-Length': body.length});
 		res.writeHead(200, {'Content-Length': body.length});
@@ -210,21 +235,332 @@ function dashboard_verification(res, $, guild, args) {
  * @param {Function} res - The server response
  * @param {Function} res - The server response
  * @param {import('./util.js').Settings} userSettings - The settings of the user
  * @param {import('./util.js').Settings} userSettings - The settings of the user
  * @param {String} guild - The id of the guild
  * @param {String} guild - The id of the guild
- * @param {String} type - The setting to change
+ * @param {String|Number} type - The setting to change
  * @param {Object} settings - The new settings
  * @param {Object} settings - The new settings
- * @param {String|String[]} settings.channel
- * @param {String|String[]} settings.role
- * @param {String|String[]} settings.usergroup
- * @param {String} settings.editcount
- * @param {String} settings.accountage
+ * @param {String[]} settings.channel
+ * @param {String[]} settings.role
+ * @param {String[]} [settings.usergroup]
+ * @param {String} [settings.usergroup_and]
+ * @param {Number} settings.editcount
+ * @param {Number} settings.accountage
  * @param {String} [settings.rename]
  * @param {String} [settings.rename]
  * @param {String} [settings.save_settings]
  * @param {String} [settings.save_settings]
  * @param {String} [settings.delete_settings]
  * @param {String} [settings.delete_settings]
  */
  */
 function update_verification(res, userSettings, guild, type, settings) {
 function update_verification(res, userSettings, guild, type, settings) {
-	
-	console.log( settings );
-	return res(`/guild/${guild}/verification/${type}?save=failed`);
+	if ( type === 'default' ) {
+		return res(`/guild/${guild}/verification?save=failed`);
+	}
+	if ( !settings.save_settings === !settings.delete_settings ) {
+		return res(`/guild/${guild}/verification/${type}?save=failed`);
+	}
+	if ( settings.save_settings ) {
+		if ( !/^[\d|]+ [\d|]+$/.test(`${settings.channel} ${settings.role}`) ) {
+			return res(`/guild/${guild}/verification/${type}?save=failed`);
+		}
+		if ( !/^\d+ \d+$/.test(`${settings.editcount} ${settings.accountage}`) ) {
+			return res(`/guild/${guild}/verification/${type}?save=failed`);
+		}
+		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`);
+		}
+		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`);
+		}
+		if ( !settings.usergroup ) settings.usergroup = 'user';
+		settings.usergroup = settings.usergroup.replace( /_/g, ' ' ).trim().toLowerCase();
+		settings.usergroup = settings.usergroup.split(/\s*[,|]\s*/).map( usergroup => {
+			if ( usergroup === '*' ) return 'user';
+			return usergroup.replace( / /g, '_' );
+		} ).filter( (usergroup, i, self) => {
+			return ( usergroup.length && self.indexOf(usergroup) === i );
+		} );
+		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`);
+		settings.editcount = parseInt(settings.editcount, 10);
+		settings.accountage = parseInt(settings.accountage, 10);
+		if ( type === 'new' ) {
+			let curGuild = userSettings.guilds.isMember.get(guild);
+			if ( settings.channel.some( channel => {
+				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`);
+		}
+	}
+	if ( settings.delete_settings && type === 'new' ) {
+		return res(`/guild/${guild}/verification/new?save=failed`);
+	}
+	if ( type !== 'new' ) type = parseInt(type, 10);
+	sendMsg( {
+		type: 'getMember',
+		member: userSettings.user.id,
+		guild: guild
+	} ).then( response => {
+		if ( !response ) {
+			userSettings.guilds.notMember.set(guild, userSettings.guilds.isMember.get(guild));
+			userSettings.guilds.isMember.delete(guild);
+			return res(`/guild/${guild}?save=failed`);
+		}
+		if ( response === 'noMember' || !hasPerm(response.userPermissions, 'MANAGE_GUILD') ) {
+			userSettings.guilds.isMember.delete(guild);
+			return res('/?save=failed');
+		}
+		if ( settings.delete_settings ) return db.get( 'SELECT lang, verification.channel, 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`);
+			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`);
+				}
+				console.log( `- Dashboard: Verification successfully removed: ${guild}#${type}` );
+				res(`/guild/${guild}/verification?save=success`);
+				if ( dberror ) {
+					console.log( '- Dashboard: Error while notifying the guild: ' + dberror );
+					return;
+				}
+				var lang = new Lang(row.lang);
+				var text = lang.get('verification.dashboard.removed', `<@${userSettings.user.id}>`, type);
+				if ( row ) {
+					text += '\n' + lang.get('verification.channel') + ' <#' + row.channel.split('|').filter( channel => channel.length ).join('>, <#') + '>';
+					text += '\n' + lang.get('verification.role') + ' <@&' + row.role.split('|').join('>, <@&') + '>';
+					text += '\n' + lang.get('verification.editcount') + ' `' + row.editcount + '`';
+					text += '\n' + lang.get('verification.usergroup') + ' `' + ( row.usergroup.startsWith( 'AND|' ) ? row.usergroup.split('|').slice(1).join('` ' + lang.get('verification.and') + ' `') : row.usergroup.split('|').join('` ' + lang.get('verification.or') + ' `') ) + '`';
+					text += '\n' + lang.get('verification.accountage') + ' `' + row.accountage + '` ' + lang.get('verification.indays');
+					text += '\n' + lang.get('verification.rename') + ' *`' + lang.get('verification.' + ( row.rename ? 'enabled' : 'disabled')) + '`*';
+				}
+				text += `\n<${new URL(`/guild/${guild}/verification`, process.env.dashboard).href}>`;
+				sendMsg( {
+					type: 'notifyGuild', guild, text
+				} ).catch( error => {
+					console.log( '- Dashboard: Error while notifying the guild: ' + error );
+				} );
+			} );
+		} );
+		if ( !hasPerm(response.botPermissions, 'MANAGE_ROLES') ) {
+			return res(`/guild/${guild}/verification?save=failed`);
+		}
+		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`);
+			}
+			if ( !row ) return res(`/guild/${guild}/verification?save=failed`);
+			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 got.get( row.wiki + 'api.php?action=query&meta=allmessages&amprefix=group-&amincludelocal=true&amenableparser=true&format=json' ).then( gresponse => {
+				var body = gresponse.body;
+				if ( gresponse.statusCode !== 200 || !body || !body.query || !body.query.allmessages ) {
+					console.log( '- Dashboard: ' + gresponse.statusCode + ': Error while getting the usergroups: ' + body?.error?.info );
+					return;
+				}
+				var groups = body.query.allmessages.filter( group => {
+					if ( group.name === 'group-all' ) return false;
+					if ( group.name === 'group-membership-link-with-expiry' ) return false;
+					if ( group.name.endsWith( '.css' ) || group.name.endsWith( '.js' ) ) return false;
+					return true;
+				} ).map( group => {
+					return {
+						name: group.name.replace( /^group-/, '' ).replace( /-member$/, '' ),
+						content: group['*'].replace( / /g, '_' ).toLowerCase()
+					};
+				} );
+				settings.usergroup = settings.usergroup.map( usergroup => {
+					if ( groups.some( group => group.name === usergroup ) ) return usergroup;
+					if ( groups.some( group => group.content === usergroup ) ) {
+						return groups.find( group => group.content === usergroup ).name;
+					}
+					if ( /^admins?$/.test(usergroup) ) return 'sysop';
+					if ( usergroup === '*' ) return 'user';
+					return usergroup;
+				} );
+			}, error => {
+				console.log( '- Dashboard: Error while getting the usergroups: ' + error );
+			} ).finally( () => {
+				if ( settings.usergroup_and ) settings.usergroup.unshift('AND');
+				var configid = 1;
+				for ( let i of row.count ) {
+					if ( configid === i ) configid++;
+					else break;
+				}
+				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`);
+					}
+					console.log( `- Dashboard: Verification successfully added: ${guild}#${configid}` );
+					res(`/guild/${guild}/verification/${configid}?save=success`);
+					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('>, <#') + '>';
+					text += '\n' + lang.get('verification.role') + ' <@&' + settings.role.join('>, <@&') + '>';
+					text += '\n' + lang.get('verification.editcount') + ' `' + settings.editcount + '`';
+					text += '\n' + lang.get('verification.usergroup') + ' `' + ( settings.usergroup_and ? settings.usergroup.slice(1).join('` ' + lang.get('verification.and') + ' `') : settings.usergroup.join('` ' + lang.get('verification.or') + ' `') ) + '`';
+					text += '\n' + lang.get('verification.accountage') + ' `' + settings.accountage + '` ' + lang.get('verification.indays');
+					text += '\n' + lang.get('verification.rename') + ' *`' + lang.get('verification.' + ( settings.rename ? 'enabled' : 'disabled')) + '`*';
+					text += `\n<${new URL(`/guild/${guild}/verification/${configid}`, process.env.dashboard).href}>`;
+					if ( settings.rename && !hasPerm(response.botPermissions, 'MANAGE_NICKNAMES') ) {
+						text += '\n\n' + lang.get('verification.rename_no_permission', `<@${process.env.bot}>`);
+					}
+					if ( settings.role.some( role => {
+						return !userSettings.guilds.isMember.get(guild).roles.some( guildRole => {
+							return ( guildRole.id === role && guildRole.lower );
+						} );
+					} ) ) {
+						text += '\n';
+						settings.role.forEach( role => {
+							if ( !userSettings.guilds.isMember.get(guild).roles.some( guildRole => {
+								return ( guildRole.id === role );
+							} ) ) {
+								text += '\n' + lang.get('verification.role_deleted', `<@&${role}>`);
+							}
+							else if ( userSettings.guilds.isMember.get(guild).roles.some( guildRole => {
+								return ( guildRole.id === role && !guildRole.lower );
+							} ) ) {
+								text += '\n' + lang.get('verification.role_too_high', `<@&${role}>`, `<@${process.env.bot}>`);
+							}
+						} );
+					}
+					sendMsg( {
+						type: 'notifyGuild', guild, text
+					} ).catch( error => {
+						console.log( '- Dashboard: Error while notifying the guild: ' + error );
+					} );
+				} );
+			} );
+		} );
+		return db.get( 'SELECT wiki, lang, verification.channel, 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`);
+			}
+			if ( !row?.channel ) return res(`/guild/${guild}/verification?save=failed`);
+			row.channel = row.channel.split('|').filter( channel => channel.length );
+			var newChannel = settings.channel.filter( channel => !row.channel.includes( channel ) );
+			row.role = row.role.split('|');
+			var newRole = settings.role.filter( role => !row.role.includes( role ) );
+			row.usergroup = row.usergroup.split('|');
+			var newUsergroup = settings.usergroup.filter( group => !row.usergroup.includes( group ) );
+			if ( newChannel.length || newRole.length ) {
+				let curGuild = userSettings.guilds.isMember.get(guild);
+				if ( newChannel.some( channel => {
+					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`);
+			}
+			( 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;
+				if ( gresponse.statusCode !== 200 || !body || !body.query || !body.query.allmessages ) {
+					console.log( '- Dashboard: ' + gresponse.statusCode + ': Error while getting the usergroups: ' + body?.error?.info );
+					return;
+				}
+				var groups = body.query.allmessages.filter( group => {
+					if ( group.name === 'group-all' ) return false;
+					if ( group.name === 'group-membership-link-with-expiry' ) return false;
+					if ( group.name.endsWith( '.css' ) || group.name.endsWith( '.js' ) ) return false;
+					return true;
+				} ).map( group => {
+					return {
+						name: group.name.replace( /^group-/, '' ).replace( /-member$/, '' ),
+						content: group['*'].replace( / /g, '_' ).toLowerCase()
+					};
+				} );
+				settings.usergroup = settings.usergroup.map( usergroup => {
+					if ( groups.some( group => group.name === usergroup ) ) return usergroup;
+					if ( groups.some( group => group.content === usergroup ) ) {
+						return groups.find( group => group.content === usergroup ).name;
+					}
+					if ( /^admins?$/.test(usergroup) ) return 'sysop';
+					if ( usergroup === '*' ) return 'user';
+					return usergroup;
+				} );
+			}, error => {
+				console.log( '- Dashboard: Error while getting the usergroups: ' + error );
+			} ) : Promise.resolve() ).finally( () => {
+				if ( settings.usergroup_and ) settings.usergroup.unshift('AND');
+				var lang = new Lang(row.lang);
+				var diff = [];
+				if ( newChannel.length || row.channel.some( channel => {
+					return !settings.channel.includes( channel );
+				} ) ) {
+					diff.push(lang.get('verification.channel') + ` ~~<#${row.channel.join('>, <#')}>~~ → <#${settings.channel.join('>, <#')}>`);
+				}
+				if ( newRole.length || row.role.some( role => {
+					return !settings.role.includes( role );
+				} ) ) {
+					diff.push(lang.get('verification.role') + ` ~~<@&${row.role.join('>, <@&')}>~~ → <@&${settings.role.join('>, <@&')}>`);
+				}
+				if ( row.editcount !== settings.editcount ) {
+					diff.push(lang.get('verification.editcount') + ` ~~\`${row.editcount}\`~~ → \`${settings.editcount}\``);
+				}
+				if ( newUsergroup.length || row.usergroup.some( usergroup => {
+					return !settings.usergroup.includes( usergroup );
+				} ) ) {
+					diff.push(lang.get('verification.usergroup') + ' ~~`' + ( row.usergroup[0] === 'AND' ? row.usergroup.slice(1).join('` ' + lang.get('verification.and') + ' `') : row.usergroup.join('` ' + lang.get('verification.or') + ' `') ) + '`~~ → `' + ( settings.usergroup_and ? settings.usergroup.slice(1).join('` ' + lang.get('verification.and') + ' `') : settings.usergroup.join('` ' + lang.get('verification.or') + ' `') ) + '`');
+				}
+				if ( row.accountage !== settings.accountage ) {
+					diff.push(lang.get('verification.accountage') + ` ~~\`${row.accountage}\`~~ → \`${settings.accountage}\``);
+				}
+				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`);
+				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`);
+					}
+					console.log( `- Dashboard: Verification successfully unpdated: ${guild}#${type}` );
+					res(`/guild/${guild}/verification/${type}?save=success`);
+					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}>`;
+					if ( settings.rename && !hasPerm(response.botPermissions, 'MANAGE_NICKNAMES') ) {
+						text += '\n\n' + lang.get('verification.rename_no_permission', `<@${process.env.bot}>`);
+					}
+					if ( settings.role.some( role => {
+						return !userSettings.guilds.isMember.get(guild).roles.some( guildRole => {
+							return ( guildRole.id === role && guildRole.lower );
+						} );
+					} ) ) {
+						text += '\n';
+						settings.role.forEach( role => {
+							if ( !userSettings.guilds.isMember.get(guild).roles.some( guildRole => {
+								return ( guildRole.id === role );
+							} ) ) {
+								text += '\n' + lang.get('verification.role_deleted', `<@&${role}>`);
+							}
+							else if ( userSettings.guilds.isMember.get(guild).roles.some( guildRole => {
+								return ( guildRole.id === role && !guildRole.lower );
+							} ) ) {
+								text += '\n' + lang.get('verification.role_too_high', `<@&${role}>`, `<@${process.env.bot}>`);
+							}
+						} );
+					}
+					sendMsg( {
+						type: 'notifyGuild', guild, text
+					} ).catch( error => {
+						console.log( '- Dashboard: Error while notifying the guild: ' + error );
+					} );
+				} );
+			} );
+		} );
+	}, error => {
+		console.log( '- Dashboard: Error while getting the member: ' + error );
+		return res(`/guild/${guild}/verification/${type}?save=failed`);
+	} );
 }
 }
 
 
 module.exports = {
 module.exports = {

+ 26 - 0
i18n/en.json

@@ -437,6 +437,7 @@
         "all_inactive": "you can't have wiki changes and feeds based changes disabled at the same time.",
         "all_inactive": "you can't have wiki changes and feeds based changes disabled at the same time.",
         "audit_reason": "Recent changes webhook for \"$1\"",
         "audit_reason": "Recent changes webhook for \"$1\"",
         "audit_reason_delete": "Removed recent changes webhook",
         "audit_reason_delete": "Removed recent changes webhook",
+        "audit_reason_move": "Moved recent changes webhook",
         "blocked": "this wiki has been blocked from being added as a recent changes webhook!",
         "blocked": "this wiki has been blocked from being added as a recent changes webhook!",
         "blocked_reason": "this wiki has been blocked from being added as a recent changes webhook for `$1`!",
         "blocked_reason": "this wiki has been blocked from being added as a recent changes webhook for `$1`!",
         "channel": "Channel:",
         "channel": "Channel:",
@@ -445,6 +446,11 @@
         "current_lang": "the language for this webhook is:",
         "current_lang": "the language for this webhook is:",
         "current_selected": "this is the recent changes webhook for this server:",
         "current_selected": "this is the recent changes webhook for this server:",
         "current_wiki": "the wiki for this webhook is:",
         "current_wiki": "the wiki for this webhook is:",
+        "dashboard": {
+            "added": "$1 added the recent changes webhook with id `$2`.",
+            "removed": "$1 removed the recent changes webhook with id `$2`.",
+            "updated": "$1 updated the recent changes webhook with id `$2`."
+        },
         "delete": "Delete this recent changes webhook:",
         "delete": "Delete this recent changes webhook:",
         "deleted": "the recent changes webhook has been deleted.",
         "deleted": "the recent changes webhook has been deleted.",
         "disabled": "disabled",
         "disabled": "disabled",
@@ -480,6 +486,20 @@
             "blocked": "This recent changes webhook will be deleted because the wiki has been blocked!",
             "blocked": "This recent changes webhook will be deleted because the wiki has been blocked!",
             "blocked_reason": "This recent changes webhook will be deleted because the wiki has been blocked for `$1`!",
             "blocked_reason": "This recent changes webhook will be deleted because the wiki has been blocked for `$1`!",
             "created": "A recent changes webhook for $1 has been added to this channel.",
             "created": "A recent changes webhook for $1 has been added to this channel.",
+            "dashboard": {
+                "channel": "• The webhook has been moved to this channel.",
+                "disabled_feeds": "• The feeds based changes, like discussions, message walls and article comments, have been disabled.",
+                "disabled_rc": "• The wiki changes have been disabled.",
+                "display_compact": "• The display mode has been changed to compact text messages with inline links.",
+                "display_diff": "• The display mode has been changed to embed messages with image previews and edit differences.",
+                "display_embed": "• The display mode has been changed to embed messages with edit tags and category changes.",
+                "display_image": "• The display mode has been changed to embed messages with image previews.",
+                "enabled_feeds": "• The feeds based changes, like discussions, message walls and article comments, have been enabled.",
+                "enabled_rc": "• The wiki changes have been enabled.",
+                "lang": "• The language has been changed to $1.",
+                "updated": "This recent changes webhook has been updated:",
+                "wiki": "• The wiki has been changed to $1."
+            },
             "deleted": "This recent changes webhook will be deleted.",
             "deleted": "This recent changes webhook will be deleted.",
             "disabled_feeds": "The feeds based changes, like discussions, message walls and article comments, have been disabled for this recent changes webhook.",
             "disabled_feeds": "The feeds based changes, like discussions, message walls and article comments, have been disabled for this recent changes webhook.",
             "disabled_rc": "The wiki changes have been disabled for this recent changes webhook.",
             "disabled_rc": "The wiki changes have been disabled for this recent changes webhook.",
@@ -669,6 +689,11 @@
         "channel_missing": "the provided channel does not exist.",
         "channel_missing": "the provided channel does not exist.",
         "current": "these are the current verifications for this server:",
         "current": "these are the current verifications for this server:",
         "current_selected": "this is the verification `$1` for this server:",
         "current_selected": "this is the verification `$1` for this server:",
+        "dashboard": {
+            "added": "$1 added the verification with id `$2`.",
+            "removed": "$1 removed the verification with id `$2`.",
+            "updated": "$1 updated the verification with id `$2`."
+        },
         "delete_current": "Delete this verification:",
         "delete_current": "Delete this verification:",
         "deleted": "the verification has been deleted.",
         "deleted": "the verification has been deleted.",
         "disabled": "disabled",
         "disabled": "disabled",
@@ -691,6 +716,7 @@
         "role_max": "you provided too many roles.",
         "role_max": "you provided too many roles.",
         "role_missing": "the provided role does not exist.",
         "role_missing": "the provided role does not exist.",
         "role_too_high": "**The role $1 is too high for $2 to assign!**",
         "role_too_high": "**The role $1 is too high for $2 to assign!**",
+        "role_deleted": "**The role $1 doesn't seem to exist anymore!**",
         "save_failed": "sadly the verification couldn't be saved, please try again later.",
         "save_failed": "sadly the verification couldn't be saved, please try again later.",
         "toggle": "(toggle)",
         "toggle": "(toggle)",
         "updated": "the verification has been updated:",
         "updated": "the verification has been updated:",

+ 71 - 7
main.js

@@ -101,18 +101,19 @@ if ( process.env.dashboard ) {
 											return {
 											return {
 												id: channel.id,
 												id: channel.id,
 												name: channel.name,
 												name: channel.name,
-												permissions: member.permissionsIn(channel).bitfield
+												userPermissions: member.permissionsIn(channel).bitfield,
+												botPermissions: guild.me.permissionsIn(channel).bitfield
 											};
 											};
 										} ),
 										} ),
 										roles: guild.roles.cache.filter( role => {
 										roles: guild.roles.cache.filter( role => {
-											return ( role.id !== guild.id );
+											return ( role.id !== guild.id && !role.managed );
 										} ).sort( (a, b) => {
 										} ).sort( (a, b) => {
 											return b.rawPosition - a.rawPosition;
 											return b.rawPosition - a.rawPosition;
 										} ).map( role => {
 										} ).map( role => {
 											return {
 											return {
 												id: role.id,
 												id: role.id,
 												name: role.name,
 												name: role.name,
-												lower: ( guild.me.roles.highest.comparePositionTo(role) > 0 && !role.managed )
+												lower: ( guild.me.roles.highest.comparePositionTo(role) > 0 )
 											};
 											};
 										} )
 										} )
 									};
 									};
@@ -135,10 +136,28 @@ if ( process.env.dashboard ) {
 					return manager.broadcastEval(`if ( this.guilds.cache.has('${message.data.guild}') ) {
 					return manager.broadcastEval(`if ( this.guilds.cache.has('${message.data.guild}') ) {
 						let guild = this.guilds.cache.get('${message.data.guild}');
 						let guild = this.guilds.cache.get('${message.data.guild}');
 						guild.members.fetch('${message.data.member}').then( member => {
 						guild.members.fetch('${message.data.member}').then( member => {
-							return {
+							var response = {
 								patreon: guild.id in global.patreons,
 								patreon: guild.id in global.patreons,
-								permissions: member.permissions.bitfield
+								userPermissions: member.permissions.bitfield,
+								botPermissions: guild.me.permissions.bitfield
 							};
 							};
+							if ( '${( message.data.channel || '' )}' ) {
+								let channel = guild.channels.cache.get('${message.data.channel}');
+								if ( channel?.isText() ) {
+									response.userPermissions = channel.permissionsFor(member).bitfield;
+									response.botPermissions = channel.permissionsFor(guild.me).bitfield;
+								}
+								else response.message = 'noChannel';
+							}
+							if ( '${( message.data.newchannel || '' )}' ) {
+								let newchannel = guild.channels.cache.get('${message.data.newchannel}');
+								if ( newchannel?.isText() ) {
+									response.userPermissionsNew = newchannel.permissionsFor(member).bitfield;
+									response.botPermissionsNew = newchannel.permissionsFor(guild.me).bitfield;
+								}
+								else response.message = 'noChannel';
+							}
+							return response;
 						}, error => {
 						}, error => {
 							return 'noMember';
 							return 'noMember';
 						} );
 						} );
@@ -160,14 +179,59 @@ if ( process.env.dashboard ) {
 					if ( this.guilds.cache.has('${message.data.guild}') ) {
 					if ( this.guilds.cache.has('${message.data.guild}') ) {
 						let channel = this.guilds.cache.get('${message.data.guild}').publicUpdatesChannel;
 						let channel = this.guilds.cache.get('${message.data.guild}').publicUpdatesChannel;
 						if ( channel ) channel.send( \`${message.data.text.replace( /`/g, '\\`' )}\`, {
 						if ( channel ) channel.send( \`${message.data.text.replace( /`/g, '\\`' )}\`, {
-							embed: ${JSON.stringify(message.data.embed)}
-						} ).catch( error => console.log( '- Dashboard: ' + error.name + ': ' + error.message ) );
+							embed: ${JSON.stringify(message.data.embed)},
+							allowedMentions: {parse: []}, split: true
+						} ).catch( error => {} );
 					}`).catch( error => {
 					}`).catch( error => {
 						data.error = error.toString();
 						data.error = error.toString();
 					} ).finally( () => {
 					} ).finally( () => {
 						return dashboard.send( {id: message.id, data} );
 						return dashboard.send( {id: message.id, data} );
 					} );
 					} );
 					break;
 					break;
+				case 'createWebhook':
+					return manager.broadcastEval(`if ( this.guilds.cache.has('${message.data.guild}') ) {
+						let channel = this.guilds.cache.get('${message.data.guild}').channels.cache.get('${message.data.channel}');
+						if ( channel ) channel.createWebhook( \`${message.data.name.replace( /`/g, '\\`' )}\`, {
+							avatar: this.user.displayAvatarURL({format:'png',size:4096}),
+							reason: \`${message.data.reason.replace( /`/g, '\\`' )}\`
+						} ).then( webhook => {
+							console.log( '- Dashboard: Webhook successfully created: ${message.data.guild}#${message.data.channel}' );
+							webhook.send( \`${message.data.text.replace( /`/g, '\\`' )}\` ).catch(log_error);
+							return webhook.id + '/' + webhook.token;
+						}, error => {
+							console.log( '- Dashboard: Error while creating the webhook: ' + error );
+						} );
+					}`).then( results => {
+						data.response = results.find( result => result );
+					}, error => {
+						data.error = error.toString();
+					} ).finally( () => {
+						return dashboard.send( {id: message.id, data} );
+					} );
+					break;
+				case 'moveWebhook':
+					return manager.broadcastEval(`if ( this.guilds.cache.has('${message.data.guild}') ) {
+						this.fetchWebhook(...'${message.data.webhook}'.split('/')).then( webhook => {
+							return webhook.edit( {
+								channel: '${message.data.channel}'
+							}, \`${message.data.reason.replace( /`/g, '\\`' )}\` ).then( newwebhook => {
+								console.log( '- Dashboard: Webhook successfully moved: ${message.data.guild}#${message.data.channel}' );
+								webhook.send( \`${message.data.text.replace( /`/g, '\\`' )}\` ).catch(log_error);
+								return true;
+							}, error => {
+								console.log( '- Dashboard: Error while moving the webhook: ' + error );
+							} );
+						}, error => {
+							console.log( '- Dashboard: Error while moving the webhook: ' + error );
+						} );
+					}`).then( results => {
+						data.response = results.find( result => result );
+					}, error => {
+						data.error = error.toString();
+					} ).finally( () => {
+						return dashboard.send( {id: message.id, data} );
+					} );
+					break;
 				default:
 				default:
 					console.log( '- [Dashboard]: Unknown message received!', message.data );
 					console.log( '- [Dashboard]: Unknown message received!', message.data );
 					data.error = 'Unknown message type: ' + message.data.type;
 					data.error = 'Unknown message type: ' + message.data.type;