Browse Source

Add option of removing roles during verification

Markus-Rost 4 years ago
parent
commit
00b3b135e9

+ 31 - 18
cmds/verification.js

@@ -53,15 +53,19 @@ function cmd_verification(lang, msg, args, line, wiki) {
 			if ( !roles.length ) return msg.replyMsg( lang.get('verification.no_role') + '\n`' + prefix + 'verification add ' + lang.get('verification.new_role') + '`', {components}, true );
 			if ( roles.length > 10 ) return msg.replyMsg( lang.get('verification.role_max'), {components}, true );
 			roles = roles.map( role => {
-				var new_role = '';
-				if ( /^\d+$/.test(role) ) new_role = msg.guild.roles.cache.get(role);
-				if ( !new_role ) new_role = msg.guild.roles.cache.find( gc => gc.name === role.replace( /^@/, '' ) );
-				if ( !new_role ) new_role = msg.guild.roles.cache.find( gc => gc.name.toLowerCase() === role.toLowerCase().replace( /^@/, '' ) );
+				var new_role = ['', null];
+				if ( role.startsWith( '-' ) ) {
+					role = role.replace( '-', '' );
+					new_role[0] = '-';
+				}
+				if ( /^\d+$/.test(role) ) new_role[1] = msg.guild.roles.cache.get(role);
+				if ( !new_role[1] ) new_role[1] = msg.guild.roles.cache.find( gc => gc.name === role.replace( /^@/, '' ) );
+				if ( !new_role[1] ) new_role[1] = msg.guild.roles.cache.find( gc => gc.name.toLowerCase() === role.toLowerCase().replace( /^@/, '' ) );
 				return new_role;
 			} );
-			if ( roles.some( role => !role ) ) return msg.replyMsg( lang.get('verification.role_missing'), {components}, true );
-			if ( roles.some( role => role.managed ) ) return msg.replyMsg( lang.get('verification.role_managed'), {components}, true );
-			roles = roles.map( role => role.id ).join('|');
+			if ( roles.some( role => !role[1] ) ) return msg.replyMsg( lang.get('verification.role_missing'), {components}, true );
+			if ( roles.some( role => role[1].managed || role[1].id === msg.guild.id ) ) return msg.replyMsg( lang.get('verification.role_managed'), {components}, true );
+			roles = roles.map( role => role[0] + role[1].id ).join('|');
 			var new_configid = 1;
 			for ( let i of rows.map( row => row.configid ) ) {
 				if ( new_configid === i ) new_configid++;
@@ -174,15 +178,19 @@ function cmd_verification(lang, msg, args, line, wiki) {
 				var roles = args[2].replace( /\s*>?\s*[,|]\s*<?\s*/g, '|' ).split('|').filter( role => role.length );
 				if ( roles.length > 10 ) return msg.replyMsg( lang.get('verification.role_max'), {components}, true );
 				roles = roles.map( role => {
-					var new_role = null;
-					if ( /^\d+$/.test(role) ) new_role = msg.guild.roles.cache.get(role);
-					if ( !new_role ) new_role = msg.guild.roles.cache.find( gc => gc.name === role.replace( /^@/, '' ) );
-					if ( !new_role ) new_role = msg.guild.roles.cache.find( gc => gc.name.toLowerCase() === role.toLowerCase().replace( /^@/, '' ) );
+					var new_role = ['', null];
+					if ( role.startsWith( '-' ) ) {
+						role = role.replace( '-', '' );
+						new_role[0] = '-';
+					}
+					if ( /^\d+$/.test(role) ) new_role[1] = msg.guild.roles.cache.get(role);
+					if ( !new_role[1] ) new_role[1] = msg.guild.roles.cache.find( gc => gc.name === role.replace( /^@/, '' ) );
+					if ( !new_role[1] ) new_role[1] = msg.guild.roles.cache.find( gc => gc.name.toLowerCase() === role.toLowerCase().replace( /^@/, '' ) );
 					return new_role;
 				} );
-				if ( roles.some( role => !role ) ) return msg.replyMsg( lang.get('verification.role_missing'), {components}, true );
-				if ( roles.some( role => role.managed || role.id === msg.guild.id ) ) return msg.replyMsg( lang.get('verification.role_managed'), {components}, true );
-				roles = roles.map( role => role.id ).join('|');
+				if ( roles.some( role => !role[1] ) ) return msg.replyMsg( lang.get('verification.role_missing'), {components}, true );
+				if ( roles.some( role => role[1].managed || role[1].id === msg.guild.id ) ) return msg.replyMsg( lang.get('verification.role_managed'), {components}, true );
+				roles = roles.map( role => role[0] + role[1].id ).join('|');
 				if ( roles.length ) return db.query( 'UPDATE verification SET role = $1 WHERE guild = $2 AND configid = $3', [roles, msg.guild.id, row.configid] ).then( () => {
 					console.log( '- Verification successfully updated.' );
 					row.role = roles;
@@ -268,17 +276,22 @@ function cmd_verification(lang, msg, args, line, wiki) {
 		function formatVerification(showCommands, hideNotice, {
 			configid,
 			channel = '|' + msg.channel.id + '|',
-			role,
+			role = '',
 			editcount = 0,
 			postcount = 0,
 			usergroup = 'user',
 			accountage = 0,
 			rename = 0
 		} = row) {
+			var roles = [
+				role.split('|').filter( roleid => !roleid.startsWith( '-' ) ),
+				role.split('|').filter( roleid => roleid.startsWith( '-' ) ).map( roleid => roleid.replace( '-', '' ) )
+			];
 			var verification_text = '\n\n`' + prefix + 'verification ' + configid + '`';
 			verification_text += '\n' + lang.get('verification.channel') + ' <#' + channel.split('|').filter( channel => channel.length ).join('>, <#') + '>';
 			if ( showCommands ) verification_text += '\n`' + prefix + 'verification ' + row.configid + ' channel ' + lang.get('verification.new_channel') + '`\n';
-			verification_text += '\n' + lang.get('verification.role') + ' <@&' + role.split('|').join('>, <@&') + '>';
+			if ( roles[0].length ) verification_text += '\n' + lang.get('verification.role_add') + ' <@&' + roles[0].join('>, <@&') + '>';
+			if ( roles[1].length ) verification_text += '\n' + lang.get('verification.role_remove') + ' <@&' + roles[1].join('>, <@&') + '>';
 			if ( showCommands ) verification_text += '\n`' + prefix + 'verification ' + row.configid + ' role ' + lang.get('verification.new_role') + '`\n';
 			if ( postcount === null ) verification_text += '\n' + lang.get('verification.posteditcount') + ' `' + editcount + '`';
 			else verification_text += '\n' + lang.get('verification.editcount') + ' `' + editcount + '`';
@@ -297,11 +310,11 @@ function cmd_verification(lang, msg, args, line, wiki) {
 			if ( !hideNotice && rename && !msg.guild.me.permissions.has('MANAGE_NICKNAMES') ) {
 				verification_text += '\n\n' + lang.get('verification.rename_no_permission', msg.guild.me.toString());
 			}
-			if ( !hideNotice && role.split('|').some( role => {
+			if ( !hideNotice && role.replace( /-/g, '' ).split('|').some( role => {
 				return ( !msg.guild.roles.cache.has(role) || msg.guild.me.roles.highest.comparePositionTo(role) <= 0 );
 			} ) ) {
 				verification_text += '\n';
-				role.split('|').forEach( role => {
+				role.replace( /-/g, '' ).split('|').forEach( role => {
 					if ( !msg.guild.roles.cache.has(role) ) {
 						verification_text += '\n' + lang.get('verification.role_deleted', '<@&' + role + '>');
 					}

+ 1 - 1
cmds/verify.js

@@ -155,7 +155,7 @@ function cmd_verify(lang, msg, args, line, wiki) {
 						dmEmbed.fields.forEach( field => {
 							field.value = field.value.replace( /<@&(\d+)>/g, (mention, id) => {
 								if ( !msg.guild.roles.cache.has(id) ) return mention;
-								return '@' + msg.guild.roles.cache.get(id)?.name;
+								return escapeFormatting('@' + msg.guild.roles.cache.get(id)?.name);
 							} );
 						} );
 						msg.member.send( msg.channel.toString() + '; ' + result.content, {embed: dmEmbed, components: []} ).then( message => {

+ 2 - 0
dashboard/i18n/en.json

@@ -230,6 +230,8 @@
             "postcount_or": "Require either edit or post count.",
             "rename": "Rename users:",
             "role": "Role:",
+            "role_add": "Add",
+            "role_remove": "Remove",
             "select_channel": "-- Select a Channel --",
             "select_role": "-- Select a Role --",
             "success": "Success notice:",

+ 5 - 5
dashboard/rcscript.js

@@ -34,16 +34,16 @@ const fieldset = {
 	display: '<span>Display mode:</span>'
 	+ '<div class="wb-settings-display">'
 	+ '<input type="radio" id="wb-settings-display-0" name="display" value="0" required>'
-	+ '<label for="wb-settings-display-0">Compact text messages with inline links.</label>'
+	+ '<label for="wb-settings-display-0" class="radio-label">Compact text messages with inline links.</label>'
 	+ '</div><div class="wb-settings-display">'
 	+ '<input type="radio" id="wb-settings-display-1" name="display" value="1" required>'
-	+ '<label for="wb-settings-display-1">Embed messages with edit tags and category changes.</label>'
+	+ '<label for="wb-settings-display-1" class="radio-label">Embed messages with edit tags and category changes.</label>'
 	+ '</div><div class="wb-settings-display">'
 	+ '<input type="radio" id="wb-settings-display-2" name="display" value="2" required>'
-	+ '<label for="wb-settings-display-2">Embed messages with image previews.</label>'
+	+ '<label for="wb-settings-display-2" class="radio-label">Embed messages with image previews.</label>'
 	+ '</div><div class="wb-settings-display">'
 	+ '<input type="radio" id="wb-settings-display-3" name="display" value="3" required>'
-	+ '<label for="wb-settings-display-3">Embed messages with image previews and edit differences.</label>'
+	+ '<label for="wb-settings-display-3" class="radio-label">Embed messages with image previews and edit differences.</label>'
 	+ '</div>',
 	feeds: '<label for="wb-settings-feeds">Feeds based changes:</label>'
 	+ '<input type="checkbox" id="wb-settings-feeds" name="feeds">'
@@ -161,7 +161,7 @@ function createForm($, header, dashboardLang, settings, guildChannels, allWikis)
 	if ( readonly ) {
 		form.find('input').attr('readonly', '');
 		form.find('input[type="checkbox"], input[type="radio"]:not(:checked), option, optgroup').attr('disabled', '');
-		form.find('input[type="submit"], button.addmore').remove();
+		form.find('input[type="submit"]').remove();
 	}
 	return $('<form id="wb-settings" method="post" enctype="application/x-www-form-urlencoded">').append(
 		$('<h2>').text(header),

+ 3 - 3
dashboard/slash.js

@@ -9,13 +9,13 @@ const fieldset = {
 	permission: '<span title="@UNKNOWN">@UNKNOWN:</span>'
 	+ '<div class="wb-settings-permission">'
 	+ '<input type="radio" id="wb-settings-permission-0" name="permission" value="0" required>'
-	+ '<label for="wb-settings-permission-0" class="wb-settings-permission-deny">Deny</label>'
+	+ '<label for="wb-settings-permission-0" class="wb-settings-permission-deny radio-label">Deny</label>'
 	+ '</div><div class="wb-settings-permission">'
 	+ '<input type="radio" id="wb-settings-permission-1" name="permission" value="1" required>'
-	+ '<label for="wb-settings-permission-1" class="wb-settings-permission-allow">Allow</label>'
+	+ '<label for="wb-settings-permission-1" class="wb-settings-permission-allow radio-label">Allow</label>'
 	+ '</div><div class="wb-settings-permission">'
 	+ '<input type="radio" id="wb-settings-permission-default" name="permission" value="" required>'
-	+ '<label for="wb-settings-permission-default" class="wb-settings-permission-default">Default</label>'
+	+ '<label for="wb-settings-permission-default" class="wb-settings-permission-default radio-label">Default</label>'
 	+ '</div>',
 	save: '<input type="submit" id="wb-settings-save" name="save_settings">'
 };

+ 4 - 1
dashboard/src/index.css

@@ -520,11 +520,14 @@ legend {
 fieldset > div {
 	margin: 10px 0;
 }
-fieldset label,
+fieldset label:not(.radio-label),
 fieldset span {
 	display: inline-block;
 	min-width: 20%;
 }
+fieldset label.radio-label {
+	padding-right: 5px;
+}
 fieldset label div {
 	padding-top: 30px;
 	padding-right: 10px;

+ 42 - 11
dashboard/src/index.js

@@ -39,7 +39,7 @@ for ( var b = 0; b < baseSelect.length; b++ ) {
 			} );
 		}
 	}
-	if ( baseSelect[b].parentNode.querySelector('button.addmore') ) {
+	if ( baseSelect[b].parentElement.parentElement.querySelector('button.addmore') ) {
 		baseSelect[b].addEventListener( 'input', toggleOption );
 		toggleOption.call(baseSelect[b]);
 	}
@@ -48,20 +48,45 @@ for ( var b = 0; b < baseSelect.length; b++ ) {
 /** @type {HTMLCollectionOf<HTMLButtonElement>} */
 var addmore = document.getElementsByClassName('addmore');
 for ( var j = 0; j < addmore.length; j++ ) {
+	/** @this HTMLButtonElement */
 	addmore[j].onclick = function() {
-		/** @type {HTMLSelectElement} */
+		/** @type {HTMLDivElement} */
 		var clone = this.previousElementSibling.cloneNode(true);
 		clone.classList.add('wb-settings-additional-select');
-		clone.removeAttribute('id');
-		clone.required = false;
-		clone.childNodes.forEach( function(child) {
+		if ( clone.firstElementChild.tagName === 'LABEL' ) clone.removeChild(clone.firstElementChild);
+		/** @type {HTMLSelectElement} */
+		var cloneSelect = clone.firstElementChild;
+		var newName = cloneSelect.name.replace( /^([a-z]+-)(\d)$/, function(fullname, base, id) {
+			return base + (+id + 1);
+		} );
+		cloneSelect.name = newName;
+		cloneSelect.removeAttribute('id');
+		cloneSelect.required = false;
+		cloneSelect.childNodes.forEach( function(child) {
 			child.hidden = false;
 			child.selected = false;
+			child.defaultSelected = false;
 		} );
-		clone.querySelector('option.defaultSelect').selected = true;
-		clone.addEventListener( 'input', toggleOption );
+		cloneSelect.querySelector('option.defaultSelect').defaultSelected = true;
+		cloneSelect.querySelector('option.defaultSelect').selected = true;
+		cloneSelect.addEventListener( 'input', toggleOption );
+		cloneSelect.name
+		cloneSelect.htmlFor
+		cloneSelect.id
+		if ( clone.children.length === 5 ) {
+			clone.children.item(1).name = newName + '-change';
+			clone.children.item(1).id = 'wb-settings-' + newName + '-add';
+			clone.children.item(1).checked = false;
+			clone.children.item(2).htmlFor = 'wb-settings-' + newName + '-add';
+			clone.children.item(3).name = newName + '-change';
+			clone.children.item(3).id = 'wb-settings-' + newName + '-remove';
+			clone.children.item(3).checked = false;
+			clone.children.item(4).htmlFor = 'wb-settings-' + newName + '-remove';
+			clone.children.item(1).defaultChecked = true;
+			clone.children.item(1).checked = true;
+		}
 		this.before(clone);
-		toggleOption.call(clone);
+		toggleOption.call(cloneSelect);
 	};
 }
 
@@ -71,12 +96,13 @@ function toggleOption() {
 	var options = [];
 	/** @type {HTMLOptionElement[]} */
 	var selected = [];
-	var allSelect = this.parentNode.querySelectorAll('select');
+	var allSelect = this.parentElement.parentElement.querySelectorAll('select');
 	allSelect.forEach( function(select) {
 		options.push(...select.options);
 		selected.push(...select.selectedOptions);
 	} );
-	var button = this.parentNode.querySelector('button.addmore');
+	/** @type {HTMLButtonElement} */
+	var button = this.parentElement.parentElement.querySelector('button.addmore');
 	if ( selected.some( function(option) {
 		if ( option && option.value ) return false;
 		else return true;
@@ -410,6 +436,9 @@ if ( addRole && addRoleButton ) addRoleButton.onclick = function() {
 		newPermissionDiv0.lastElementChild.htmlFor = 'wb-settings-permission-' + addRole.value + '-0';
 		newPermissionDiv1.lastElementChild.htmlFor = 'wb-settings-permission-' + addRole.value + '-1';
 		newPermissionDiv2.lastElementChild.htmlFor = 'wb-settings-permission-' + addRole.value + '-default';
+		newPermissionDiv0.lastElementChild.classList.add('wb-settings-permission-deny', 'radio-label');
+		newPermissionDiv1.lastElementChild.classList.add('wb-settings-permission-allow', 'radio-label');
+		newPermissionDiv2.lastElementChild.classList.add('wb-settings-permission-default', 'radio-label');
 		newPermissionDiv0.lastElementChild.textContent = i18nSlashPermission.deny;
 		newPermissionDiv1.lastElementChild.textContent = i18nSlashPermission.allow;
 		newPermissionDiv2.lastElementChild.textContent = i18nSlashPermission.default;
@@ -454,6 +483,7 @@ if ( textAreas.length ) {
 		var end = textArea.selectionEnd;
 		var valueBefore = ( this.dataset?.before || this.innerText );
 		var valueAfter = ( this.dataset?.after || '' );
+		if ( (textArea.textLength - (end - start)) + (valueBefore.length + valueAfter.length) > textArea.maxLength ) return document.getSelection().selectAllChildren(this);
 		if ( valueAfter ) {
 			textArea.value = textArea.value.substring(0, start) + valueBefore + textArea.value.substring(start, end) + valueAfter + textArea.value.substring(end);
 			textArea.selectionStart = start + valueBefore.length;
@@ -478,6 +508,7 @@ if ( textAreas.length ) {
 		var end = this.selectionEnd;
 		if ( this.value.substring(0, start).includes( '```' ) && this.value.substring(end).includes( '```' ) ) {
 			e.preventDefault();
+			if ( this.textLength > this.maxLength ) return;
 			this.value = this.value.substring(0, start) + '\t' + this.value.substring(end);
 			this.selectionStart = this.selectionEnd = start + 1;
 		}
@@ -485,7 +516,7 @@ if ( textAreas.length ) {
 
 	/** @this HTMLTextAreaElement */
 	function updateTextLength() {
-		this.labels.item(0).children.item(0).textContent = this.value.length + ' / ' + this.maxLength;
+		this.labels.item(0).children.item(0).textContent = this.textLength + ' / ' + this.maxLength;
 	}
 }
 

+ 122 - 69
dashboard/verification.js

@@ -4,11 +4,19 @@ const {got, db, slashCommands, sendMsg, createNotice, escapeText, hasPerm} = req
 const slashCommand = slashCommands.find( slashCommand => slashCommand.name === 'verify' );
 
 const fieldset = {
-	channel: '<label for="wb-settings-channel">Channel:</label>'
-	+ '<select id="wb-settings-channel" name="channel" required></select>'
+	channel: '<div>'
+	+ '<label for="wb-settings-channel">Channel:</label>'
+	+ '<select id="wb-settings-channel" name="channel-0" required></select>'
+	+ '</div>'
 	+ '<button type="button" id="wb-settings-channel-more" class="addmore">Add more</button>',
-	role: '<label for="wb-settings-role">Role:</label>'
-	+ '<select id="wb-settings-role" name="role" required></select>'
+	role: '<div>'
+	+ '<label for="wb-settings-role">Role:</label>'
+	+ '<select id="wb-settings-role" name="role-0" required></select>'
+	+ '<input type="radio" id="wb-settings-role-0-add" name="role-0-change" value="+">'
+	+ '<label for="wb-settings-role-0-add" class="radio-label">Add</label>'
+	+ '<input type="radio" id="wb-settings-role-0-remove" name="role-0-change" value="-">'
+	+ '<label for="wb-settings-role-0-remove" class="radio-label">Remove</label>'
+	+ '</div>'
 	+ '<button type="button" id="wb-settings-role-more" class="addmore">Add more</button>',
 	usergroup: '<label for="wb-settings-usergroup">Wiki user group:</label>'
 	+ '<input type="text" id="wb-settings-usergroup" name="usergroup" list="wb-settings-usergroup-list" autocomplete="on">'
@@ -32,13 +40,13 @@ const fieldset = {
 	+ '</div><div class="wb-settings-postcount">'
 	+ '<span>Only Fandom wikis:</span>'
 	+ '<input type="radio" id="wb-settings-postcount-and" name="posteditcount" value="and" required>'
-	+ '<label for="wb-settings-postcount-and">Require both edit and post count.</label>'
+	+ '<label for="wb-settings-postcount-and" class="radio-label">Require both edit and post count.</label>'
 	+ '</div><div class="wb-settings-postcount">'
 	+ '<input type="radio" id="wb-settings-postcount-or" name="posteditcount" value="or" required>'
-	+ '<label for="wb-settings-postcount-or">Require either edit or post count.</label>'
+	+ '<label for="wb-settings-postcount-or" class="radio-label">Require either edit or post count.</label>'
 	+ '</div><div class="wb-settings-postcount">'
 	+ '<input type="radio" id="wb-settings-postcount-both" name="posteditcount" value="both" required>'
-	+ '<label for="wb-settings-postcount-both">Require combined edit and post count.</label>'
+	+ '<label for="wb-settings-postcount-both" class="radio-label">Require combined edit and post count.</label>'
 	+ '</div>',
 	accountage: '<label for="wb-settings-accountage">Account age (in days):</label>'
 	+ '<input type="number" id="wb-settings-accountage" name="accountage" min="0" max="1000000" required>',
@@ -101,13 +109,14 @@ function createForm($, header, dashboardLang, settings, guildChannels, guildRole
 				return $(`<option class="wb-settings-channel-${guildChannel}">`).val(guildChannel).text(`${guildChannel} – #UNKNOWN`).addClass('wb-settings-error');
 			} )
 		);
-		if ( settingsChannels.length > 1 ) channel.find('#wb-settings-channel').after(
-			...settingsChannels.slice(1).map( guildChannel => {
+		if ( settingsChannels.length > 1 ) channel.find('div').after(
+			...settingsChannels.slice(1).map( (guildChannel, i) => {
 				var additionalChannel = channel.find('#wb-settings-channel').clone();
-				additionalChannel.addClass('wb-settings-additional-select');
 				additionalChannel.find(`.wb-settings-channel-default`).removeAttr('hidden');
 				additionalChannel.find(`.wb-settings-channel-${guildChannel}`).attr('selected', '');
-				return additionalChannel.removeAttr('id').removeAttr('required');
+				additionalChannel.removeAttr('id').removeAttr('required');
+				additionalChannel.attr('name', 'channel-' + (i + 1));
+				return $('<div>').addClass('wb-settings-additional-select').append(additionalChannel);
 			} )
 		);
 		channel.find(`#wb-settings-channel .wb-settings-channel-${settingsChannels[0]}`).attr('selected', '');
@@ -118,11 +127,13 @@ function createForm($, header, dashboardLang, settings, guildChannels, guildRole
 	}
 	fields.push(channel);
 	let role = $('<div>').append(fieldset.role);
-	role.find('label').text(dashboardLang.get('verification.form.role'));
+	role.find('label').eq(0).text(dashboardLang.get('verification.form.role'));
+	role.find('label').eq(1).text(dashboardLang.get('verification.form.role_add'));
+	role.find('label').eq(2).text(dashboardLang.get('verification.form.role_remove'));
 	role.find('#wb-settings-role').append(
 		$('<option class="wb-settings-role-default defaultSelect" hidden>').val('').text(dashboardLang.get('verification.form.select_role')),
 		...guildRoles.filter( guildRole => {
-			return guildRole.lower || settings.role.split('|').includes( guildRole.id );
+			return guildRole.lower || settings.role.replace( /-/g, '' ).split('|').includes( guildRole.id );
 		} ).map( guildRole => {
 			var optionRole = $(`<option class="wb-settings-role-${guildRole.id}">`).val(guildRole.id);
 			if ( !guildRole.lower ) optionRole.addClass('wb-settings-error');
@@ -130,30 +141,44 @@ function createForm($, header, dashboardLang, settings, guildChannels, guildRole
 		} )
 	);
 	if ( settings.role ) {
-		let settingsRoles = settings.role.split('|');
+		let settingsRoles = settings.role.split('|').map( guildRole => {
+			if ( !guildRole.startsWith( '-' ) ) return {id: guildRole, suffix: 'add'};
+			return {id: guildRole.replace( '-', '' ), suffix: 'remove'};
+		} );
 		role.find('#wb-settings-role').append(
 			...settingsRoles.filter( guildRole => {
-				return !role.find(`.wb-settings-role-${guildRole}`).length;
+				return !role.find(`.wb-settings-role-${guildRole.id}`).length;
 			} ).map( guildRole => {
-				return $(`<option class="wb-settings-role-${guildRole}">`).val(guildRole).text(`${guildRole} – @UNKNOWN`).addClass('wb-settings-error');
+				return $(`<option class="wb-settings-role-${guildRole.id}">`).val(guildRole.id).text(`${guildRole.id} – @UNKNOWN`).addClass('wb-settings-error');
 			} )
 		);
-		if ( settingsRoles.length > 1 ) role.find('#wb-settings-role').after(
-			...settingsRoles.slice(1).map( guildRole => {
-				var additionalRole = role.find('#wb-settings-role').clone();
-				additionalRole.addClass('wb-settings-additional-select');
+		if ( settingsRoles.length > 1 ) role.find('div').after(
+			...settingsRoles.slice(1).map( (guildRole, i) => {
+				var id = i + 1;
+				var additionalDiv = role.find('div').clone();
+				additionalDiv.find('label').eq(0).remove();
+				var additionalRole = additionalDiv.find('#wb-settings-role');
 				additionalRole.find(`.wb-settings-role-default`).removeAttr('hidden');
-				additionalRole.find(`.wb-settings-role-${guildRole}`).attr('selected', '');
-				return additionalRole.removeAttr('id').removeAttr('required');
+				additionalRole.find(`.wb-settings-role-${guildRole.id}`).attr('selected', '');
+				additionalRole.removeAttr('id').removeAttr('required').attr('name', 'role-' + id);
+				additionalDiv.find('input').attr('name', 'role-' + id + '-change');
+				additionalDiv.find('input').eq(0).attr('id', 'wb-settings-role-' + id + '-add');
+				additionalDiv.find('label').eq(0).attr('for', 'wb-settings-role-' + id + '-add');
+				additionalDiv.find('input').eq(1).attr('id', 'wb-settings-role-' + id + '-remove');
+				additionalDiv.find('label').eq(1).attr('for', 'wb-settings-role-' + id + '-remove');
+				additionalDiv.find(`#wb-settings-role-${id}-${guildRole.suffix}`).attr('checked', '');
+				return additionalDiv.addClass('wb-settings-additional-select');
 			} )
 		);
-		role.find(`#wb-settings-role .wb-settings-role-${settingsRoles[0]}`).attr('selected', '');
+		role.find(`#wb-settings-role .wb-settings-role-${settingsRoles[0].id}`).attr('selected', '');
+		role.find(`#wb-settings-role-0-${settingsRoles[0].suffix}`).attr('checked', '');
 	}
 	else {
 		if ( role.find(`.wb-settings-role-${settings.defaultrole}`).length ) {
 			role.find(`.wb-settings-role-${settings.defaultrole}`).attr('selected', '');
 		}
 		else role.find('.wb-settings-role-default').attr('selected', '');
+		role.find('#wb-settings-role-0-add').attr('checked', '');
 		role.find('button.addmore').attr('hidden', '');
 	}
 	fields.push(role);
@@ -255,7 +280,7 @@ function dashboard_verification(res, $, guild, args, dashboardLang) {
 		$('#channellist #verification').after(
 			...rows.map( row => {
 				let text = `${row.configid} - ${( guild.roles.find( role => {
-					return role.id === row.role.split('|')[0];
+					return role.id === row.role.replace( /-/g, '' ).split('|')[0];
 				} )?.name || guild.channels.find( channel => {
 					return channel.id === row.channel.split('|')[1];
 				} )?.name || row.usergroup.split('|')[( row.usergroup.startsWith('AND|') ? 1 : 0 )] )}`;
@@ -391,8 +416,6 @@ function dashboard_verification(res, $, guild, args, dashboardLang) {
  * @param {String} guild - The id of the guild
  * @param {String|Number} type - The setting to change
  * @param {Object} settings - The new settings
- * @param {String[]} settings.channel
- * @param {String[]} settings.role
  * @param {String[]} [settings.usergroup]
  * @param {String} [settings.usergroup_and]
  * @param {Number} settings.editcount
@@ -411,8 +434,20 @@ function update_verification(res, userSettings, guild, type, settings) {
 	if ( !settings.save_settings === !settings.delete_settings ) {
 		return res(`/guild/${guild}/verification/${type}`, 'savefail');
 	}
+	/** @type {String[]} */
+	var channels = [];
+	/** @type {{id: String, prefix: String}[]} */
+	var roles = [];
 	if ( settings.save_settings ) {
-		if ( !/^[\d|]+ [\d|]+$/.test(`${settings.channel} ${settings.role}`) ) {
+		channels = Object.keys(settings).filter( channel => {
+			return /^channel-\d$/.test(channel) && /^\d+$/.test(settings[channel]);
+		} ).map( channel => settings[channel] );
+		roles = Object.keys(settings).filter( role => {
+			return /^role-\d$/.test(role) && /^\d+$/.test(settings[role]);
+		} ).map( role => {
+			return {id: settings[role], prefix: ( settings[role + '-change'] === '-' ? '-' : '' )};
+		} );
+		if ( !channels.length || !roles.length ) {
 			return res(`/guild/${guild}/verification/${type}`, 'savefail');
 		}
 		if ( !/^\d+ \d+$/.test(`${settings.editcount} ${settings.accountage}`) ) {
@@ -421,18 +456,9 @@ function update_verification(res, userSettings, guild, type, settings) {
 		if ( !( ['and','or','both'].includes( settings.posteditcount ) && ( /^\d+$/.test(settings.postcount) || settings.posteditcount === 'both' ) ) ) {
 			return res(`/guild/${guild}/verification/${type}`, 'savefail');
 		}
-		settings.channel = settings.channel.split('|').filter( (channel, i, self) => {
-			return ( channel.length && self.indexOf(channel) === i );
-		} );
-		if ( !settings.channel.length || settings.channel.length > 10 ) {
-			return res(`/guild/${guild}/verification/${type}`, 'savefail');
-		}
-		settings.role = settings.role.split('|').filter( (role, i, self) => {
-			return ( role.length && self.indexOf(role) === i );
+		channels = channels.filter( (channel, i, self) => {
+			return self.indexOf(channel) === i;
 		} );
-		if ( !settings.role.length || settings.role.length > 10 ) {
-			return res(`/guild/${guild}/verification/${type}`, 'savefail');
-		}
 		if ( !settings.usergroup ) settings.usergroup = 'user';
 		settings.usergroup = settings.usergroup.replace( /_/g, ' ' ).trim().toLowerCase();
 		settings.usergroup = settings.usergroup.split(/\s*[,|]\s*/).map( usergroup => {
@@ -458,13 +484,13 @@ function update_verification(res, userSettings, guild, type, settings) {
 		}
 		if ( type === 'new' ) {
 			let curGuild = userSettings.guilds.isMember.get(guild);
-			if ( settings.channel.some( channel => {
+			if ( channels.some( channel => {
 				return !curGuild.channels.some( guildChannel => {
 					return ( guildChannel.id === channel && !guildChannel.isCategory );
 				} );
-			} ) || settings.role.some( role => {
+			} ) || roles.some( role => {
 				return !curGuild.roles.some( guildRole => {
-					return ( guildRole.id === role && guildRole.lower );
+					return ( guildRole.id === role.id && guildRole.lower );
 				} );
 			} ) ) return res(`/guild/${guild}/verification/new`, 'savefail');
 		}
@@ -517,7 +543,12 @@ function update_verification(res, userSettings, guild, type, settings) {
 				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('>, <@&') + '>';
+					let rolesRow = [
+						row.role.split('|').filter( role => !role.startsWith( '-' ) ),
+						row.role.split('|').filter( role => role.startsWith( '-' ) ).map( role => role.replace( '-', '' ) )
+					];
+					if ( rolesRow[0].length ) text += '\n' + lang.get('verification.role_add') + ' <@&' + rolesRow[0].join('>, <@&') + '>';
+					if ( rolesRow[1].length ) text += '\n' + lang.get('verification.role_remove') + ' <@&' + rolesRow[1].join('>, <@&') + '>';
 					if ( row.postcount === null ) {
 						text += '\n' + lang.get('verification.posteditcount') + ' `' + row.editcount + '`';
 					}
@@ -586,7 +617,7 @@ function update_verification(res, userSettings, guild, type, settings) {
 					if ( configid === i ) configid++;
 					else break;
 				}
-				db.query( 'INSERT INTO verification(guild, configid, channel, role, editcount, postcount, usergroup, accountage, rename) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9)', [guild, configid, '|' + settings.channel.join('|') + '|', settings.role.join('|'), settings.editcount, settings.postcount, settings.usergroup.join('|'), settings.accountage, ( settings.rename ? 1 : 0 )] ).then( () => {
+				db.query( 'INSERT INTO verification(guild, configid, channel, role, editcount, postcount, usergroup, accountage, rename) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9)', [guild, configid, '|' + channels.join('|') + '|', roles.map( role => role.prefix + role.id ).join('|'), settings.editcount, settings.postcount, settings.usergroup.join('|'), settings.accountage, ( settings.rename ? 1 : 0 )] ).then( () => {
 					console.log( `- Dashboard: Verification successfully added: ${guild}#${configid}` );
 					res(`/guild/${guild}/verification/${configid}`, 'save');
 					if ( !row.count.length && slashCommand?.id ) got.put( 'https://discord.com/api/v8/applications/' + process.env.bot + '/guilds/' + guild + '/commands/' + slashCommand.id + '/permissions', {
@@ -614,8 +645,13 @@ function update_verification(res, userSettings, guild, type, settings) {
 					} );
 					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.channel') + ' <#' + channels.join('>, <#') + '>';
+					let rolesRow = [
+						roles.filter( role => !role.prefix ).map( role => '<@&' + role.id + '>' ),
+						roles.filter( role => role.prefix ).map( role => '<@&' + role.id + '>' )
+					];
+					if ( rolesRow[0].length ) text += '\n' + lang.get('verification.role_add') + ' ' + rolesRow[0].join(', ');
+					if ( rolesRow[1].length ) text += '\n' + lang.get('verification.role_remove') + ' ' + rolesRow[1].join(', ');
 					if ( settings.postcount === null ) {
 						text += '\n' + lang.get('verification.posteditcount') + ' `' + settings.editcount + '`';
 					}
@@ -631,22 +667,22 @@ function update_verification(res, userSettings, guild, type, settings) {
 					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 => {
+					if ( roles.some( role => {
 						return !userSettings.guilds.isMember.get(guild).roles.some( guildRole => {
-							return ( guildRole.id === role && guildRole.lower );
+							return ( guildRole.id === role.id && guildRole.lower );
 						} );
 					} ) ) {
 						text += '\n';
-						settings.role.forEach( role => {
+						roles.forEach( role => {
 							if ( !userSettings.guilds.isMember.get(guild).roles.some( guildRole => {
-								return ( guildRole.id === role );
+								return ( guildRole.id === role.id );
 							} ) ) {
-								text += '\n' + lang.get('verification.role_deleted', `<@&${role}>`);
+								text += '\n' + lang.get('verification.role_deleted', `<@&${role.id}>`);
 							}
 							else if ( userSettings.guilds.isMember.get(guild).roles.some( guildRole => {
-								return ( guildRole.id === role && !guildRole.lower );
+								return ( guildRole.id === role.id && !guildRole.lower );
 							} ) ) {
-								text += '\n' + lang.get('verification.role_too_high', `<@&${role}>`, `<@${process.env.bot}>`);
+								text += '\n' + lang.get('verification.role_too_high', `<@&${role.id}>`, `<@${process.env.bot}>`);
 							}
 						} );
 					}
@@ -667,9 +703,15 @@ function update_verification(res, userSettings, guild, type, settings) {
 		return db.query( 'SELECT wiki, lang, verification.channel, verification.role, editcount, postcount, usergroup, accountage, rename FROM discord LEFT JOIN verification ON discord.guild = verification.guild AND verification.configid = $1 WHERE discord.guild = $2 AND discord.channel IS NULL', [type, guild] ).then( ({rows:[row]}) => {
 			if ( !row?.channel ) return res(`/guild/${guild}/verification`, 'savefail');
 			row.channel = row.channel.split('|').filter( channel => channel.length );
-			var newChannel = settings.channel.filter( channel => !row.channel.includes( channel ) );
-			row.role = row.role.split('|');
-			var newRole = settings.role.filter( role => !row.role.includes( role ) );
+			var newChannel = channels.filter( channel => !row.channel.includes( channel ) );
+			/** @type {String[][]} */
+			var rolesRow = [
+				row.role.split('|').filter( role => !role.startsWith( '-' ) ),
+				row.role.split('|').filter( role => role.startsWith( '-' ) ).map( role => role.replace( '-', '' ) )
+			];
+			var newRole = roles.filter( role => {
+				return !rolesRow[0].includes( role.id ) && !rolesRow[1].includes( role.id );
+			} );
 			row.usergroup = row.usergroup.split('|');
 			var newUsergroup = settings.usergroup.filter( group => !row.usergroup.includes( group ) );
 			if ( newChannel.length || newRole.length ) {
@@ -680,7 +722,7 @@ function update_verification(res, userSettings, guild, type, settings) {
 					} );
 				} ) || newRole.some( role => {
 					return !curGuild.roles.some( guildRole => {
-						return ( guildRole.id === role && guildRole.lower );
+						return ( guildRole.id === role.id && guildRole.lower );
 					} );
 				} ) ) return res(`/guild/${guild}/verification/${type}`, 'savefail');
 			}
@@ -717,14 +759,25 @@ function update_verification(res, userSettings, guild, type, settings) {
 				var lang = new Lang(row.lang);
 				var diff = [];
 				if ( newChannel.length || row.channel.some( channel => {
-					return !settings.channel.includes( channel );
+					return !channels.includes( channel );
+				} ) ) {
+					diff.push(lang.get('verification.channel') + ` ~~<#${row.channel.join('>, <#')}>~~ → <#${channels.join('>, <#')}>`);
+				}
+				if ( roles.some( role => {
+					if ( role.prefix ) return false;
+					return !rolesRow[0].includes( role.id );
+				} ) || rolesRow[0].some( roleid => {
+					return !roles.some( role => !role.prefix && role.id === roleid );
 				} ) ) {
-					diff.push(lang.get('verification.channel') + ` ~~<#${row.channel.join('>, <#')}>~~ → <#${settings.channel.join('>, <#')}>`);
+					diff.push(lang.get('verification.role_add') + ' ~~' + ( rolesRow[0].length ? '<@&' + rolesRow[0].join('>, <@&') + '>' : '*`' + lang.get('verification.role_none') + '`*' ) + '~~ → ' + ( roles.some( role => !role.prefix ) ? roles.filter( role => !role.prefix ).map( role => '<@&' + role.id + '>' ).join(', ') : '*`' + lang.get('verification.role_none') + '`*' ));
 				}
-				if ( newRole.length || row.role.some( role => {
-					return !settings.role.includes( role );
+				if ( roles.some( role => {
+					if ( !role.prefix ) return false;
+					return !rolesRow[1].includes( role.id );
+				} ) || rolesRow[1].some( roleid => {
+					return !roles.some( role => role.prefix && role.id === roleid );
 				} ) ) {
-					diff.push(lang.get('verification.role') + ` ~~<@&${row.role.join('>, <@&')}>~~ → <@&${settings.role.join('>, <@&')}>`);
+					diff.push(lang.get('verification.role_remove') + ' ~~' + ( rolesRow[1].length ? '<@&' + rolesRow[1].join('>, <@&') + '>' : '*`' + lang.get('verification.role_none') + '`*' ) + '~~ → ' + ( roles.some( role => role.prefix ) ? roles.filter( role => role.prefix ).map( role => '<@&' + role.id + '>' ).join(', ') : '*`' + lang.get('verification.role_none') + '`*' ));
 				}
 				if ( row.postcount !== settings.postcount && ( row.postcount === null || settings.postcount === null ) ) {
 					if ( row.postcount === null ) {
@@ -762,7 +815,7 @@ function update_verification(res, userSettings, guild, type, settings) {
 					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');
-				db.query( 'UPDATE verification SET channel = $1, role = $2, editcount = $3, postcount = $4, usergroup = $5, accountage = $6, rename = $7 WHERE guild = $8 AND configid = $9', ['|' + settings.channel.join('|') + '|', settings.role.join('|'), settings.editcount, settings.postcount, settings.usergroup.join('|'), settings.accountage, ( settings.rename ? 1 : 0 ), guild, type] ).then( () => {
+				db.query( 'UPDATE verification SET channel = $1, role = $2, editcount = $3, postcount = $4, usergroup = $5, accountage = $6, rename = $7 WHERE guild = $8 AND configid = $9', ['|' + channels.join('|') + '|', roles.map( role => role.prefix + role.id ).join('|'), settings.editcount, settings.postcount, settings.usergroup.join('|'), settings.accountage, ( settings.rename ? 1 : 0 ), guild, type] ).then( () => {
 					console.log( `- Dashboard: Verification successfully updated: ${guild}#${type}` );
 					res(`/guild/${guild}/verification/${type}`, 'save');
 					var text = lang.get('verification.dashboard.updated', `<@${userSettings.user.id}>`, type);
@@ -771,22 +824,22 @@ function update_verification(res, userSettings, guild, type, settings) {
 					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 => {
+					if ( roles.some( role => {
 						return !userSettings.guilds.isMember.get(guild).roles.some( guildRole => {
-							return ( guildRole.id === role && guildRole.lower );
+							return ( guildRole.id === role.id && guildRole.lower );
 						} );
 					} ) ) {
 						text += '\n';
-						settings.role.forEach( role => {
+						roles.forEach( role => {
 							if ( !userSettings.guilds.isMember.get(guild).roles.some( guildRole => {
-								return ( guildRole.id === role );
+								return ( guildRole.id === role.id );
 							} ) ) {
-								text += '\n' + lang.get('verification.role_deleted', `<@&${role}>`);
+								text += '\n' + lang.get('verification.role_deleted', `<@&${role.id}>`);
 							}
 							else if ( userSettings.guilds.isMember.get(guild).roles.some( guildRole => {
-								return ( guildRole.id === role && !guildRole.lower );
+								return ( guildRole.id === role.id && !guildRole.lower );
 							} ) ) {
-								text += '\n' + lang.get('verification.role_too_high', `<@&${role}>`, `<@${process.env.bot}>`);
+								text += '\n' + lang.get('verification.role_too_high', `<@&${role.id}>`, `<@${process.env.bot}>`);
 							}
 						} );
 					}

+ 155 - 65
functions/verify.js

@@ -200,8 +200,8 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 					queryuser.editcount = body.query.usercontribs.length;
 					if ( body.continue?.uccontinue ) queryuser.editcount++;
 				}
-				var roles = [];
-				var missing = [];
+				var addRoles = [new Set(), new Set()];
+				var removeRoles = [new Set(), new Set()];
 				var verified = false;
 				var rename = false;
 				var accountage = ( Date.now() - new Date(queryuser.registration) ) / 86400000;
@@ -215,13 +215,18 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 					if ( row.postcount === null ) matchEditcount = ( ( queryuser.editcount + queryuser.postcount ) >= row.editcount );
 					else if ( row.postcount < 0 ) matchEditcount = ( queryuser.editcount >= row.editcount || queryuser.postcount >= Math.abs(row.postcount) );
 					else matchEditcount = ( queryuser.editcount >= row.editcount && queryuser.postcount >= row.postcount );
-					if ( matchEditcount && row.usergroup.split('|')[and_or]( usergroup => queryuser.groups.includes( usergroup ) ) && accountage >= row.accountage && row.role.split('|').some( role => !roles.includes( role ) ) ) {
+					if ( matchEditcount && row.usergroup.split('|')[and_or]( usergroup => queryuser.groups.includes( usergroup ) ) && accountage >= row.accountage ) {
 						verified = true;
 						if ( row.rename ) rename = true;
 						row.role.split('|').forEach( role => {
-							if ( !roles.includes( role ) ) {
-								if ( channel.guild.roles.cache.has(role) && channel.guild.me.roles.highest.comparePositionTo(role) > 0 ) roles.push(role);
-								else if ( !missing.includes( role ) ) missing.push(role);
+							var modifyRoles = addRoles;
+							if ( role.startsWith( '-' ) ) {
+								role = role.replace( '-', '' );
+								modifyRoles = removeRoles;
+							}
+							if ( !modifyRoles[0].has(role) ) {
+								if ( channel.guild.roles.cache.has(role) && channel.guild.me.roles.highest.comparePositionTo(role) > 0 ) modifyRoles[0].add(role);
+								else if ( !modifyRoles[1].has(role) ) modifyRoles[1].add(role);
 							}
 						} );
 					}
@@ -229,10 +234,22 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 				if ( verified ) {
 					embed.setColor('#00FF00').setDescription( lang.get('verify.user_verified', member.toString(), '[' + escapeFormatting(username) + '](' + pagelink + ')', queryuser.gender) + ( rename ? '\n' + lang.get('verify.user_renamed', queryuser.gender) : '' ) );
 					var text = lang.get('verify.user_verified_reply', escapeFormatting(username), queryuser.gender);
+					removeRoles[0].forEach( role => addRoles[0].delete(role) );
+					removeRoles[1].forEach( role => addRoles[1].delete(role) );
+					var changeRoles = [];
+					if ( addRoles[0].size + removeRoles[0].size === 1 ) {
+						if ( addRoles[0].size === 1 ) changeRoles.push('add', [...addRoles[0]][0]);
+						else changeRoles.push('remove', [...removeRoles[0]][0]);
+					}
+					else {
+						let roles = new Set([...member.roles.cache.filter( role => {
+							return !removeRoles[0].has(role.id);
+						} ).keys(), ...addRoles[0]]);
+						changeRoles.push('set', [...roles]);
+					}
 					var verify_promise = [
-						member.roles.add( roles, lang.get('verify.audit_reason', username) ).catch( error => {
+						member.roles[changeRoles[0]]( changeRoles[1], lang.get('verify.audit_reason', username) ).catch( error => {
 							log_error(error);
-							embed.setColor('#008800');
 							comment.push(lang.get('verify.failed_roles'));
 						} )
 					];
@@ -240,24 +257,30 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 						if ( channel.guild.me.roles.highest.comparePositionTo(member.roles.highest) > 0 ) {
 							verify_promise.push(member.setNickname( username.substring(0, 32), lang.get('verify.audit_reason', username) ).catch( error => {
 								log_error(error);
-								embed.setColor('#008800');
 								comment.push(lang.get('verify.failed_rename', queryuser.gender));
 							} ));
 						}
-						else {
-							embed.setColor('#008800');
-							comment.push(lang.get('verify.failed_rename', queryuser.gender));
-						}
+						else comment.push(lang.get('verify.failed_rename', queryuser.gender));
 					}
 					return Promise.all(verify_promise).then( () => {
+						var addRolesMentions = [
+							[...addRoles[0]].map( role => '<@&' + role + '>' ),
+							[...addRoles[1]].map( role => '<@&' + role + '>' )
+						];
+						var removeRolesMentions = [
+							[...removeRoles[0]].map( role => '<@&' + role + '>' ),
+							[...removeRoles[1]].map( role => '<@&' + role + '>' )
+						];
 						var useLogging = false;
 						if ( verifynotice.logchannel ) {
 							useLogging = true;
 							result.logging.channel = verifynotice.logchannel.id;
 							if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
 								let logembed = new MessageEmbed(embed);
-								if ( roles.length ) logembed.addField( lang.get('verify.qualified'), roles.map( role => '<@&' + role + '>' ).join('\n') );
-								if ( missing.length ) logembed.setColor('#008800').addField( lang.get('verify.qualified_error'), missing.map( role => '<@&' + role + '>' ).join('\n') );
+								if ( addRolesMentions[0].length ) logembed.addField( lang.get('verify.qualified_add'), addRolesMentions[0].join('\n') );
+								if ( addRolesMentions[1].length ) logembed.setColor('#008800').addField( lang.get('verify.qualified_add_error'), addRolesMentions[1].join('\n') );
+								if ( removeRolesMentions[0].length ) logembed.addField( lang.get('verify.qualified_remove'), removeRolesMentions[0].join('\n') );
+								if ( removeRolesMentions[1].length ) logembed.setColor('#008800').addField( lang.get('verify.qualified_remove_error'), removeRolesMentions[1].join('\n') );
 								if ( comment.length ) logembed.setColor('#008800').addField( lang.get('verify.notice'), comment.join('\n') );
 								result.logging.embed = logembed;
 							}
@@ -265,8 +288,10 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 								let logtext = '🔸 ' + lang.get('verify.user_verified', member.toString(), escapeFormatting(username), queryuser.gender);
 								if ( rename ) logtext += '\n' + lang.get('verify.user_renamed', queryuser.gender);
 								logtext += '\n<' + pagelink + '>';
-								if ( roles.length ) logtext += '\n**' + lang.get('verify.qualified') + '** ' + roles.map( role => '<@&' + role + '>' ).join(', ');
-								if ( missing.length ) logtext += '\n**' + lang.get('verify.qualified_error') + '** ' + missing.map( role => '<@&' + role + '>' ).join(', ');
+								if ( addRolesMentions[0].length ) logtext += '\n**' + lang.get('verify.qualified_add') + '** ' + addRolesMentions[0].join(', ');
+								if ( addRolesMentions[1].length ) logtext += '\n**' + lang.get('verify.qualified_add_error') + '** ' + addRolesMentions[1].join(', ');
+								if ( removeRolesMentions[0].length ) logtext += '\n**' + lang.get('verify.qualified_remove') + '** ' + removeRolesMentions[0].join(', ');
+								if ( removeRolesMentions[1].length ) logtext += '\n**' + lang.get('verify.qualified_remove_error') + '** ' + removeRolesMentions[1].join(', ');
 								if ( comment.length ) logtext += '\n**' + lang.get('verify.notice') + '** ' + comment.join('\n**' + lang.get('verify.notice') + '** ');
 								result.logging.content = logtext;
 							}
@@ -278,14 +303,19 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 							dateformat: lang.get('dateformat')
 						}).trim() : '' );
 						if ( channel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
-							if ( roles.length ) embed.addField( lang.get('verify.qualified'), roles.map( role => '<@&' + role + '>' ).join('\n') );
-							if ( missing.length && !useLogging ) embed.setColor('#008800').addField( lang.get('verify.qualified_error'), missing.map( role => '<@&' + role + '>' ).join('\n') );
+							if ( addRolesMentions[0].length ) embed.addField( lang.get('verify.qualified_add'), addRolesMentions[0].join('\n') );
+							if ( addRolesMentions[1].length && !useLogging ) embed.setColor('#008800').addField( lang.get('verify.qualified_add_error'), addRolesMentions[1].join('\n') );
+							if ( removeRolesMentions[0].length ) embed.addField( lang.get('verify.qualified_remove'), removeRolesMentions[0].join('\n') );
+							if ( removeRolesMentions[1].length && !useLogging ) embed.setColor('#008800').addField( lang.get('verify.qualified_remove_error'), removeRolesMentions[1].join('\n') );
 							if ( comment.length && !useLogging ) embed.setColor('#008800').addField( lang.get('verify.notice'), comment.join('\n') );
 							if ( onsuccess ) embed.addField( lang.get('verify.notice'), onsuccess );
 						}
 						else {
-							if ( roles.length ) text += '\n\n' + lang.get('verify.qualified') + ' ' + roles.map( role => '<@&' + role + '>' ).join(', ');
-							if ( missing.length && !useLogging ) text += '\n\n' + lang.get('verify.qualified_error') + ' ' + missing.map( role => '<@&' + role + '>' ).join(', ');
+							text += '\n';
+							if ( addRolesMentions[0].length ) text += '\n**' + lang.get('verify.qualified_add') + '** ' + addRolesMentions[0].join(', ');
+							if ( addRolesMentions[1].length && !useLogging ) text += '\n**' + lang.get('verify.qualified_add_error') + '** ' + addRolesMentions[1].join(', ');
+							if ( removeRolesMentions[0].length ) text += '\n**' + lang.get('verify.qualified_remove') + '** ' + removeRolesMentions[0].join(', ');
+							if ( removeRolesMentions[1].length && !useLogging ) text += '\n**' + lang.get('verify.qualified_remove_error') + '** ' + removeRolesMentions[1].join(', ');
 							if ( comment.length && !useLogging ) text += '\n\n' + comment.join('\n');
 							if ( onsuccess ) text += '\n\n**' + lang.get('verify.notice') + '** ' + onsuccess;
 						}
@@ -402,8 +432,8 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 				return;
 			}
 			
-			var roles = [];
-			var missing = [];
+			var addRoles = [new Set(), new Set()];
+			var removeRoles = [new Set(), new Set()];
 			var verified = false;
 			var rename = false;
 			var accountage = ( Date.now() - new Date(queryuser.registration) ) / 86400000;
@@ -413,13 +443,18 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 					row.usergroup = row.usergroup.replace( 'AND|', '' );
 					and_or = 'every';
 				}
-				if ( queryuser.editcount >= row.editcount && row.usergroup.split('|')[and_or]( usergroup => queryuser.groups.includes( usergroup ) ) && accountage >= row.accountage && row.role.split('|').some( role => !roles.includes( role ) ) ) {
+				if ( queryuser.editcount >= row.editcount && row.usergroup.split('|')[and_or]( usergroup => queryuser.groups.includes( usergroup ) ) && accountage >= row.accountage ) {
 					verified = true;
 					if ( row.rename ) rename = true;
 					row.role.split('|').forEach( role => {
-						if ( !roles.includes( role ) ) {
-							if ( channel.guild.roles.cache.has(role) && channel.guild.me.roles.highest.comparePositionTo(role) > 0 ) roles.push(role);
-							else if ( !missing.includes( role ) ) missing.push(role);
+						var modifyRoles = addRoles;
+						if ( role.startsWith( '-' ) ) {
+							role = role.replace( '-', '' );
+							modifyRoles = removeRoles;
+						}
+						if ( !modifyRoles[0].has(role) ) {
+							if ( channel.guild.roles.cache.has(role) && channel.guild.me.roles.highest.comparePositionTo(role) > 0 ) modifyRoles[0].add(role);
+							else if ( !modifyRoles[1].has(role) ) modifyRoles[1].add(role);
 						}
 					} );
 				}
@@ -427,10 +462,22 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 			if ( verified ) {
 				embed.setColor('#00FF00').setDescription( lang.get('verify.user_verified', member.toString(), '[' + escapeFormatting(username) + '](' + pagelink + ')', queryuser.gender) + ( rename ? '\n' + lang.get('verify.user_renamed', queryuser.gender) : '' ) );
 				var text = lang.get('verify.user_verified_reply', escapeFormatting(username), queryuser.gender);
+				removeRoles[0].forEach( role => addRoles[0].delete(role) );
+				removeRoles[1].forEach( role => addRoles[1].delete(role) );
+				var changeRoles = [];
+				if ( addRoles[0].size + removeRoles[0].size === 1 ) {
+					if ( addRoles[0].size === 1 ) changeRoles.push('add', [...addRoles[0]][0]);
+					else changeRoles.push('remove', [...removeRoles[0]][0]);
+				}
+				else {
+					let roles = new Set([...member.roles.cache.filter( role => {
+						return !removeRoles[0].has(role.id);
+					} ).keys(), ...addRoles[0]]);
+					changeRoles.push('set', [...roles]);
+				}
 				var verify_promise = [
-					member.roles.add( roles, lang.get('verify.audit_reason', username) ).catch( error => {
+					member.roles[changeRoles[0]]( changeRoles[1], lang.get('verify.audit_reason', username) ).catch( error => {
 						log_error(error);
-						embed.setColor('#008800');
 						comment.push(lang.get('verify.failed_roles'));
 					} )
 				];
@@ -438,24 +485,30 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 					if ( channel.guild.me.roles.highest.comparePositionTo(member.roles.highest) > 0 ) {
 						verify_promise.push(member.setNickname( username.substring(0, 32), lang.get('verify.audit_reason', username) ).catch( error => {
 							log_error(error);
-							embed.setColor('#008800');
 							comment.push(lang.get('verify.failed_rename', queryuser.gender));
 						} ));
 					}
-					else {
-						embed.setColor('#008800');
-						comment.push(lang.get('verify.failed_rename', queryuser.gender));
-					}
+					else comment.push(lang.get('verify.failed_rename', queryuser.gender));
 				}
 				return Promise.all(verify_promise).then( () => {
+					var addRolesMentions = [
+						[...addRoles[0]].map( role => '<@&' + role + '>' ),
+						[...addRoles[1]].map( role => '<@&' + role + '>' )
+					];
+					var removeRolesMentions = [
+						[...removeRoles[0]].map( role => '<@&' + role + '>' ),
+						[...removeRoles[1]].map( role => '<@&' + role + '>' )
+					];
 					var useLogging = false;
 					if ( verifynotice.logchannel ) {
 						useLogging = true;
 						result.logging.channel = verifynotice.logchannel.id;
 						if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
 							var logembed = new MessageEmbed(embed);
-							if ( roles.length ) logembed.addField( lang.get('verify.qualified'), roles.map( role => '<@&' + role + '>' ).join('\n') );
-							if ( missing.length ) logembed.setColor('#008800').addField( lang.get('verify.qualified_error'), missing.map( role => '<@&' + role + '>' ).join('\n') );
+							if ( addRolesMentions[0].length ) logembed.addField( lang.get('verify.qualified_add'), addRolesMentions[0].join('\n') );
+							if ( addRolesMentions[1].length ) logembed.setColor('#008800').addField( lang.get('verify.qualified_add_error'), addRolesMentions[1].join('\n') );
+							if ( removeRolesMentions[0].length ) logembed.addField( lang.get('verify.qualified_remove'), removeRolesMentions[0].join('\n') );
+							if ( removeRolesMentions[1].length ) logembed.setColor('#008800').addField( lang.get('verify.qualified_remove_error'), removeRolesMentions[1].join('\n') );
 							if ( comment.length ) logembed.setColor('#008800').addField( lang.get('verify.notice'), comment.join('\n') );
 							result.logging.embed = logembed;
 						}
@@ -463,8 +516,10 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 							var logtext = '🔸 ' + lang.get('verify.user_verified', member.toString(), escapeFormatting(username), queryuser.gender);
 							if ( rename ) logtext += '\n' + lang.get('verify.user_renamed', queryuser.gender);
 							logtext += '\n<' + pagelink + '>';
-							if ( roles.length ) logtext += '\n**' + lang.get('verify.qualified') + '** ' + roles.map( role => '<@&' + role + '>' ).join(', ');
-							if ( missing.length ) logtext += '\n**' + lang.get('verify.qualified_error') + '** ' + missing.map( role => '<@&' + role + '>' ).join(', ');
+							if ( addRolesMentions[0].length ) logtext += '\n**' + lang.get('verify.qualified_add') + '** ' + addRolesMentions[0].join(', ');
+							if ( addRolesMentions[1].length ) logtext += '\n**' + lang.get('verify.qualified_add_error') + '** ' + addRolesMentions[1].join(', ');
+							if ( removeRolesMentions[0].length ) logtext += '\n**' + lang.get('verify.qualified_remove') + '** ' + removeRolesMentions[0].join(', ');
+							if ( removeRolesMentions[1].length ) logtext += '\n**' + lang.get('verify.qualified_remove_error') + '** ' + removeRolesMentions[1].join(', ');
 							if ( comment.length ) logtext += '\n**' + lang.get('verify.notice') + '** ' + comment.join('\n**' + lang.get('verify.notice') + '** ');
 							result.logging.content = logtext;
 						}
@@ -475,14 +530,19 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 						dateformat: lang.get('dateformat')
 					}).trim() : '' );
 					if ( channel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
-						if ( roles.length ) embed.addField( lang.get('verify.qualified'), roles.map( role => '<@&' + role + '>' ).join('\n') );
-						if ( missing.length && !useLogging ) embed.setColor('#008800').addField( lang.get('verify.qualified_error'), missing.map( role => '<@&' + role + '>' ).join('\n') );
+						if ( addRolesMentions[0].length ) embed.addField( lang.get('verify.qualified_add'), addRolesMentions[0].join('\n') );
+						if ( addRolesMentions[1].length && !useLogging ) embed.setColor('#008800').addField( lang.get('verify.qualified_add_error'), addRolesMentions[1].join('\n') );
+						if ( removeRolesMentions[0].length ) embed.addField( lang.get('verify.qualified_remove'), removeRolesMentions[0].join('\n') );
+						if ( removeRolesMentions[1].length && !useLogging ) embed.setColor('#008800').addField( lang.get('verify.qualified_remove_error'), removeRolesMentions[1].join('\n') );
 						if ( comment.length && !useLogging ) embed.setColor('#008800').addField( lang.get('verify.notice'), comment.join('\n') );
 						if ( onsuccess ) embed.addField( lang.get('verify.notice'), onsuccess );
 					}
 					else {
-						if ( roles.length ) text += '\n\n' + lang.get('verify.qualified') + ' ' + roles.map( role => '<@&' + role + '>' ).join(', ');
-						if ( missing.length && !useLogging ) text += '\n\n' + lang.get('verify.qualified_error') + ' ' + missing.map( role => '<@&' + role + '>' ).join(', ');
+						text += '\n';
+						if ( addRolesMentions[0].length ) text += '\n**' + lang.get('verify.qualified_add') + '** ' + addRolesMentions[0].join(', ');
+						if ( addRolesMentions[1].length && !useLogging ) text += '\n**' + lang.get('verify.qualified_add_error') + '** ' + addRolesMentions[1].join(', ');
+						if ( removeRolesMentions[0].length ) text += '\n**' + lang.get('verify.qualified_remove') + '** ' + removeRolesMentions[0].join(', ');
+						if ( removeRolesMentions[1].length && !useLogging ) text += '\n**' + lang.get('verify.qualified_remove_error') + '** ' + removeRolesMentions[1].join(', ');
 						if ( comment.length && !useLogging ) text += '\n\n' + comment.join('\n');
 						if ( onsuccess ) text += '\n\n**' + lang.get('verify.notice') + '** ' + onsuccess;
 					}
@@ -643,8 +703,8 @@ global.verifyOauthUser = function(state, access_token, settings) {
 			}
 			queryuser.groups.push(...body.query.globaluserinfo.groups);
 
-			var roles = [];
-			var missing = [];
+			var addRoles = [new Set(), new Set()];
+			var removeRoles = [new Set(), new Set()];
 			var verified = false;
 			var rename = false;
 			var accountage = ( Date.now() - new Date(queryuser.registration) ) / 86400000;
@@ -654,13 +714,18 @@ global.verifyOauthUser = function(state, access_token, settings) {
 					row.usergroup = row.usergroup.replace( 'AND|', '' );
 					and_or = 'every';
 				}
-				if ( queryuser.editcount >= row.editcount && row.usergroup.split('|')[and_or]( usergroup => queryuser.groups.includes( usergroup ) ) && accountage >= row.accountage && row.role.split('|').some( role => !roles.includes( role ) ) ) {
+				if ( queryuser.editcount >= row.editcount && row.usergroup.split('|')[and_or]( usergroup => queryuser.groups.includes( usergroup ) ) && accountage >= row.accountage ) {
 					verified = true;
 					if ( row.rename ) rename = true;
 					row.role.split('|').forEach( role => {
-						if ( !roles.includes( role ) ) {
-							if ( channel.guild.roles.cache.has(role) && channel.guild.me.roles.highest.comparePositionTo(role) > 0 ) roles.push(role);
-							else if ( !missing.includes( role ) ) missing.push(role);
+						var modifyRoles = addRoles;
+						if ( role.startsWith( '-' ) ) {
+							role = role.replace( '-', '' );
+							modifyRoles = removeRoles;
+						}
+						if ( !modifyRoles[0].has(role) ) {
+							if ( channel.guild.roles.cache.has(role) && channel.guild.me.roles.highest.comparePositionTo(role) > 0 ) modifyRoles[0].add(role);
+							else if ( !modifyRoles[1].has(role) ) modifyRoles[1].add(role);
 						}
 					} );
 				}
@@ -669,10 +734,22 @@ global.verifyOauthUser = function(state, access_token, settings) {
 				embed.setColor('#00FF00').setDescription( lang.get('verify.user_verified', member.toString(), '[' + escapeFormatting(username) + '](' + pagelink + ')', queryuser.gender) + ( rename ? '\n' + lang.get('verify.user_renamed', queryuser.gender) : '' ) );
 				var text = lang.get('verify.user_verified_reply', escapeFormatting(username), queryuser.gender);
 				var comment = [];
+				removeRoles[0].forEach( role => addRoles[0].delete(role) );
+				removeRoles[1].forEach( role => addRoles[1].delete(role) );
+				var changeRoles = [];
+				if ( addRoles[0].size + removeRoles[0].size === 1 ) {
+					if ( addRoles[0].size === 1 ) changeRoles.push('add', [...addRoles[0]][0]);
+					else changeRoles.push('remove', [...removeRoles[0]][0]);
+				}
+				else {
+					let roles = new Set([...member.roles.cache.filter( role => {
+						return !removeRoles[0].has(role.id);
+					} ).keys(), ...addRoles[0]]);
+					changeRoles.push('set', [...roles]);
+				}
 				var verify_promise = [
-					member.roles.add( roles, lang.get('verify.audit_reason', username) ).catch( error => {
+					member.roles[changeRoles[0]]( changeRoles[1], lang.get('verify.audit_reason', username) ).catch( error => {
 						log_error(error);
-						embed.setColor('#008800');
 						comment.push(lang.get('verify.failed_roles'));
 					} )
 				];
@@ -680,16 +757,20 @@ global.verifyOauthUser = function(state, access_token, settings) {
 					if ( channel.guild.me.roles.highest.comparePositionTo(member.roles.highest) > 0 ) {
 						verify_promise.push(member.setNickname( username.substring(0, 32), lang.get('verify.audit_reason', username) ).catch( error => {
 							log_error(error);
-							embed.setColor('#008800');
 							comment.push(lang.get('verify.failed_rename', queryuser.gender));
 						} ));
 					}
-					else {
-						embed.setColor('#008800');
-						comment.push(lang.get('verify.failed_rename', queryuser.gender));
-					}
+					else comment.push(lang.get('verify.failed_rename', queryuser.gender));
 				}
 				return Promise.all(verify_promise).then( () => {
+					var addRolesMentions = [
+						[...addRoles[0]].map( role => '<@&' + role + '>' ),
+						[...addRoles[1]].map( role => '<@&' + role + '>' )
+					];
+					var removeRolesMentions = [
+						[...removeRoles[0]].map( role => '<@&' + role + '>' ),
+						[...removeRoles[1]].map( role => '<@&' + role + '>' )
+					];
 					var useLogging = false;
 					var logembed;
 					var logtext = '';
@@ -697,16 +778,20 @@ global.verifyOauthUser = function(state, access_token, settings) {
 						useLogging = true;
 						if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
 							logembed = new MessageEmbed(embed);
-							if ( roles.length ) logembed.addField( lang.get('verify.qualified'), roles.map( role => '<@&' + role + '>' ).join('\n') );
-							if ( missing.length ) logembed.setColor('#008800').addField( lang.get('verify.qualified_error'), missing.map( role => '<@&' + role + '>' ).join('\n') );
+							if ( addRolesMentions[0].length ) logembed.addField( lang.get('verify.qualified_add'), addRolesMentions[0].join('\n') );
+							if ( addRolesMentions[1].length ) logembed.setColor('#008800').addField( lang.get('verify.qualified_add_error'), addRolesMentions[1].join('\n') );
+							if ( removeRolesMentions[0].length ) logembed.addField( lang.get('verify.qualified_remove'), removeRolesMentions[0].join('\n') );
+							if ( removeRolesMentions[1].length ) logembed.setColor('#008800').addField( lang.get('verify.qualified_remove_error'), removeRolesMentions[1].join('\n') );
 							if ( comment.length ) logembed.setColor('#008800').addField( lang.get('verify.notice'), comment.join('\n') );
 						}
 						else {
 							logtext = '🔸 ' + lang.get('verify.user_verified', member.toString(), escapeFormatting(username), queryuser.gender);
 							if ( rename ) logtext += '\n' + lang.get('verify.user_renamed', queryuser.gender);
 							logtext += '\n<' + pagelink + '>';
-							if ( roles.length ) logtext += '\n**' + lang.get('verify.qualified') + '** ' + roles.map( role => '<@&' + role + '>' ).join(', ');
-							if ( missing.length ) logtext += '\n**' + lang.get('verify.qualified_error') + '** ' + missing.map( role => '<@&' + role + '>' ).join(', ');
+							if ( addRolesMentions[0].length ) logtext += '\n**' + lang.get('verify.qualified_add') + '** ' + addRolesMentions[0].join(', ');
+							if ( addRolesMentions[1].length ) logtext += '\n**' + lang.get('verify.qualified_add_error') + '** ' + addRolesMentions[1].join(', ');
+							if ( removeRolesMentions[0].length ) logtext += '\n**' + lang.get('verify.qualified_remove') + '** ' + removeRolesMentions[0].join(', ');
+							if ( removeRolesMentions[1].length ) logtext += '\n**' + lang.get('verify.qualified_remove_error') + '** ' + removeRolesMentions[1].join(', ');
 							if ( comment.length ) logtext += '\n**' + lang.get('verify.notice') + '** ' + comment.join('\n**' + lang.get('verify.notice') + '** ');
 						}
 					}
@@ -717,14 +802,19 @@ global.verifyOauthUser = function(state, access_token, settings) {
 						dateformat: lang.get('dateformat')
 					}).trim() : '' );
 					if ( channel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
-						if ( roles.length ) embed.addField( lang.get('verify.qualified'), roles.map( role => '<@&' + role + '>' ).join('\n') );
-						if ( missing.length && !useLogging ) embed.setColor('#008800').addField( lang.get('verify.qualified_error'), missing.map( role => '<@&' + role + '>' ).join('\n') );
+						if ( addRolesMentions[0].length ) embed.addField( lang.get('verify.qualified_add'), addRolesMentions[0].join('\n') );
+						if ( addRolesMentions[1].length && !useLogging ) embed.setColor('#008800').addField( lang.get('verify.qualified_add_error'), addRolesMentions[1].join('\n') );
+						if ( removeRolesMentions[0].length ) embed.addField( lang.get('verify.qualified_remove'), removeRolesMentions[0].join('\n') );
+						if ( removeRolesMentions[1].length && !useLogging ) embed.setColor('#008800').addField( lang.get('verify.qualified_remove_error'), removeRolesMentions[1].join('\n') );
 						if ( comment.length && !useLogging ) embed.setColor('#008800').addField( lang.get('verify.notice'), comment.join('\n') );
 						if ( onsuccess ) embed.addField( lang.get('verify.notice'), onsuccess );
 					}
 					else {
-						if ( roles.length ) text += '\n\n' + lang.get('verify.qualified') + ' ' + roles.map( role => '<@&' + role + '>' ).join(', ');
-						if ( missing.length && !useLogging ) text += '\n\n' + lang.get('verify.qualified_error') + ' ' + missing.map( role => '<@&' + role + '>' ).join(', ');
+						text += '\n';
+						if ( addRolesMentions[0].length ) text += '\n**' + lang.get('verify.qualified_add') + '** ' + addRolesMentions[0].join(', ');
+						if ( addRolesMentions[1].length && !useLogging ) text += '\n**' + lang.get('verify.qualified_add_error') + '** ' + addRolesMentions[1].join(', ');
+						if ( removeRolesMentions[0].length ) text += '\n**' + lang.get('verify.qualified_remove') + '** ' + removeRolesMentions[0].join(', ');
+						if ( removeRolesMentions[1].length && !useLogging ) text += '\n**' + lang.get('verify.qualified_remove_error') + '** ' + removeRolesMentions[1].join(', ');
 						if ( comment.length && !useLogging ) text += '\n\n' + comment.join('\n');
 						if ( onsuccess ) text += '\n\n**' + lang.get('verify.notice') + '** ' + onsuccess;
 					}
@@ -818,7 +908,7 @@ global.verifyOauthUser = function(state, access_token, settings) {
 							dmEmbed.fields.forEach( field => {
 								field.value = field.value.replace( /<@&(\d+)>/g, (mention, id) => {
 									if ( !channel.guild.roles.cache.has(id) ) return mention;
-									return '@' + channel.guild.roles.cache.get(id)?.name;
+									return escapeFormatting('@' + channel.guild.roles.cache.get(id)?.name);
 								} );
 							} );
 							member.send(channel.toString() + '; ' + content, Object.assign({}, options, {embed: dmEmbed})).then( message => {
@@ -838,7 +928,7 @@ global.verifyOauthUser = function(state, access_token, settings) {
 					dmEmbed.fields.forEach( field => {
 						field.value = field.value.replace( /<@&(\d+)>/g, (mention, id) => {
 							if ( !channel.guild.roles.cache.has(id) ) return mention;
-							return '@' + channel.guild.roles.cache.get(id)?.name;
+							return escapeFormatting('@' + channel.guild.roles.cache.get(id)?.name);
 						} );
 					} );
 					member.send(channel.toString() + '; ' + content, Object.assign({}, options, {embed: dmEmbed})).then( message => {

+ 7 - 3
i18n/en.json

@@ -771,11 +771,13 @@
         "posteditcount": "Edit and post count combined:",
         "rename": "Change nickname:",
         "rename_no_permission": "**$1 is missing the `Manage Nicknames` permission to force wiki usernames!**",
-        "role": "Role:",
+        "role_add": "Role to add:",
         "role_deleted": "**The role $1 doesn't seem to exist anymore!**",
         "role_managed": "the provided role can't be assigned.",
         "role_max": "you provided too many roles.",
         "role_missing": "the provided role does not exist.",
+        "role_none": "none",
+        "role_remove": "Role to remove:",
         "role_too_high": "**The role $1 is too high for $2 to assign!**",
         "save_failed": "sadly the verification couldn't be saved, please try again later.",
         "success": "Success notice:",
@@ -810,8 +812,10 @@
         "oauth_message_dm": "Please use this link to authenticate your wiki account for $1.",
         "oauth_private": "the wiki uses OAuth2 for verification. Please enable direct messages from this server or use the `/verify` command so I can send you an authentication link privately.",
         "oauth_used": "*Verified using OAuth2*",
-        "qualified": "Qualified for:",
-        "qualified_error": "Qualified for, but can't add:",
+        "qualified_add": "Added to:",
+        "qualified_add_error": "Can't be added to:",
+        "qualified_remove": "Removed from:",
+        "qualified_remove_error": "Can't be removed from:",
         "user_blocked": "**The wiki user $1 is blocked!**",
         "user_blocked_reply": "your linked wiki user **\"$1\" is blocked!**",
         "user_disabled": "**The wiki account $1 is disabled!**",