Selaa lähdekoodia

Merge remote-tracking branch 'origin/master'

Owen Diffey 4 vuotta sitten
vanhempi
sitoutus
d1e22eddce
68 muutettua tiedostoa jossa 1255 lisäystä ja 654 poistoa
  1. 2 1
      bot.js
  2. 31 18
      cmds/verification.js
  3. 1 1
      cmds/verify.js
  4. 1 1
      cmds/wiki/general.js
  5. 1 1
      cmds/wiki/search.js
  6. 0 5
      dashboard/i18n/bn.json
  7. 22 13
      dashboard/i18n/de.json
  8. 14 5
      dashboard/i18n/en.json
  9. 0 5
      dashboard/i18n/es.json
  10. 0 5
      dashboard/i18n/fr.json
  11. 6 7
      dashboard/i18n/hi.json
  12. 35 26
      dashboard/i18n/ja.json
  13. 0 5
      dashboard/i18n/ko.json
  14. 0 5
      dashboard/i18n/pl.json
  15. 4 5
      dashboard/i18n/pt-br.json
  16. 16 5
      dashboard/i18n/ru.json
  17. 0 5
      dashboard/i18n/tr.json
  18. 14 5
      dashboard/i18n/zh-hans.json
  19. 14 5
      dashboard/i18n/zh-hant.json
  20. 7 2
      dashboard/index.js
  21. 95 17
      dashboard/rcscript.js
  22. 3 3
      dashboard/slash.js
  23. 46 4
      dashboard/src/index.css
  24. 99 14
      dashboard/src/index.js
  25. 6 1
      dashboard/src/lang.js
  26. 4 4
      dashboard/util.js
  27. 123 70
      dashboard/verification.js
  28. 155 65
      functions/verify.js
  29. 0 4
      i18n/bn.json
  30. 65 57
      i18n/de.json
  31. 12 4
      i18n/en.json
  32. 0 4
      i18n/es.json
  33. 0 4
      i18n/fr.json
  34. 14 4
      i18n/hi.json
  35. 133 9
      i18n/ja.json
  36. 0 4
      i18n/ko.json
  37. 0 1
      i18n/nl.json
  38. 20 5
      i18n/pl.json
  39. 9 4
      i18n/pt-br.json
  40. 14 4
      i18n/ru.json
  41. 15 4
      i18n/sv.json
  42. 0 4
      i18n/tr.json
  43. BIN
      i18n/widgets/bn.png
  44. BIN
      i18n/widgets/de.png
  45. BIN
      i18n/widgets/en.png
  46. BIN
      i18n/widgets/es.png
  47. BIN
      i18n/widgets/fr.png
  48. BIN
      i18n/widgets/hi.png
  49. BIN
      i18n/widgets/it.png
  50. BIN
      i18n/widgets/ja.png
  51. BIN
      i18n/widgets/ko.png
  52. BIN
      i18n/widgets/nl.png
  53. BIN
      i18n/widgets/pl.png
  54. BIN
      i18n/widgets/pt-br.png
  55. BIN
      i18n/widgets/ru.png
  56. BIN
      i18n/widgets/sv.png
  57. BIN
      i18n/widgets/th.png
  58. BIN
      i18n/widgets/tr.png
  59. BIN
      i18n/widgets/uk.png
  60. BIN
      i18n/widgets/vi.png
  61. BIN
      i18n/widgets/zh-hans.png
  62. BIN
      i18n/widgets/zh-hant.png
  63. 12 4
      i18n/zh-hans.json
  64. 12 4
      i18n/zh-hant.json
  65. 35 33
      main.js
  66. 201 186
      package-lock.json
  67. 5 5
      package.json
  68. 9 7
      util/edit_diff.js

+ 2 - 1
bot.js

@@ -231,11 +231,12 @@ fs.readdir( './interactions', (error, files) => {
 client.ws.on( 'INTERACTION_CREATE', interaction => {
 	if ( interaction.version !== 1 ) return;
 	interaction.client = client;
+	var channel = client.channels.cache.get(interaction.channel_id);
 	if ( interaction.guild_id ) {
 		interaction.user = interaction.member.user;
 		interaction.member.permissions = new Discord.Permissions(+interaction.member.permissions);
+		channel?.guild?.members.add(interaction.member);
 	}
-	var channel = client.channels.cache.get(interaction.channel_id);
 	if ( interaction.type === 2 ) return slash_command(interaction, channel);
 	if ( interaction.type === 3 ) return message_button(interaction, channel);
 } );

+ 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 => {

+ 1 - 1
cmds/wiki/general.js

@@ -196,7 +196,7 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 				if ( reaction ) reaction.removeEmoji();
 				return;
 			}
-			if ( ( querypage.missing !== undefined && querypage.known === undefined && !( noRedirect || querypage.categoryinfo ) ) || querypage.invalid !== undefined ) return got.get( wiki + 'api.php?uselang=' + uselang + '&action=query&prop=categoryinfo|info|pageprops|pageimages|extracts&piprop=original|name&ppprop=description|displaytitle|page_image_free|disambiguation|infoboxes&explaintext=true&exsectionformat=raw&exlimit=1&generator=search&gsrnamespace=4|12|14|' + ( querypage.ns >= 0 ? querypage.ns + '|' : '' ) + Object.values(body.query.namespaces).filter( ns => ns.content !== undefined ).map( ns => ns.id ).join('|') + '&gsrlimit=1&gsrsearch=' + encodeURIComponent( title ) + '&format=json' ).then( srresponse => {
+			if ( ( querypage.missing !== undefined && querypage.known === undefined && !( noRedirect || querypage.categoryinfo ) ) || querypage.invalid !== undefined ) return got.get( wiki + 'api.php?uselang=' + uselang + '&action=query&prop=categoryinfo|info|pageprops|pageimages|extracts&piprop=original|name&ppprop=description|displaytitle|page_image_free|disambiguation|infoboxes&explaintext=true&exsectionformat=raw&exlimit=1&generator=search&gsrwhat=text&gsrnamespace=4|12|14|' + ( querypage.ns >= 0 ? querypage.ns + '|' : '' ) + Object.values(body.query.namespaces).filter( ns => ns.content !== undefined ).map( ns => ns.id ).join('|') + '&gsrlimit=1&gsrsearch=' + encodeURIComponent( title ) + '&format=json' ).then( srresponse => {
 				logging(wiki, msg.guild?.id, 'general', 'search');
 				var srbody = srresponse.body;
 				if ( srbody && srbody.warnings ) log_warn(srbody.warnings);

+ 1 - 1
cmds/wiki/search.js

@@ -25,7 +25,7 @@ function gamepedia_search(lang, msg, searchterm, wiki, query, reaction, spoiler)
 	var querypage = ( Object.values(( query.pages || {} ))?.[0] || {title:'',ns:0,invalid:''} );
 	var description = [];
 	var limit = searchLimit[( patreons[msg.guild?.id] ? 'patreon' : 'default' )];
-	got.get( wiki + 'api.php?action=query&titles=Special:Search&list=search&srinfo=totalhits&srprop=redirecttitle|sectiontitle&srnamespace=4|12|14|' + ( querypage.ns >= 0 ? querypage.ns + '|' : '' ) + Object.values(query.namespaces).filter( ns => ns.content !== undefined ).map( ns => ns.id ).join('|') + '&srlimit=' + limit + '&srsearch=' + encodeURIComponent( ( searchterm || ' ' ) ) + '&format=json' ).then( response => {
+	got.get( wiki + 'api.php?action=query&titles=Special:Search&list=search&srwhat=text&srinfo=totalhits&srprop=redirecttitle|sectiontitle&srnamespace=4|12|14|' + ( querypage.ns >= 0 ? querypage.ns + '|' : '' ) + Object.values(query.namespaces).filter( ns => ns.content !== undefined ).map( ns => ns.id ).join('|') + '&srlimit=' + limit + '&srsearch=' + encodeURIComponent( ( searchterm || ' ' ) ) + '&format=json' ).then( response => {
 		var body = response.body;
 		if ( body && body.warnings ) log_warn(body.warnings);
 		if ( response.statusCode !== 200 || !body || !body.query || !body.query.search || body.batchcomplete === undefined ) {

+ 0 - 5
dashboard/i18n/bn.json

@@ -76,11 +76,6 @@
             "text": "হয় আপনি বা উইকি-বট এই ফাংশনের জন্য $1 এর অনুপস্থিতি হারিয়েছেন।",
             "title": "অনুমতি অনুপস্থিত!"
         },
-        "movefail": {
-            "note": "ওয়েবহুক চ্যানেল পরিবর্তন করা যায়নি!",
-            "text": "সেটিংসটি কেবলমাত্র আংশিকভাবে আপডেট হয়েছে।",
-            "title": "সেটিংস আংশিকভাবে সংরক্ষণ করা হয়েছে!"
-        },
         "mwversion": {
             "text": "কমপক্ষে MediaWiki 1.30 প্রয়োজন, $1 $2--এ পাওয়া যায়।",
             "title": "পুরানো মিডিয়াউইকি সংস্করণ!"

+ 22 - 13
dashboard/i18n/de.json

@@ -30,6 +30,10 @@
         "welcome": "<h2>Willkommen auf dem Wiki-Bot Dashboard.</h2>\n<p>Wiki-Bot ist ein Discord-Bot der dazu dient Discord-Server und MediaWiki-Wikis zu verbinden. Er hilft durch das Verlinken von Wiki-Artikeln, Verifizieren von Wiki-Benutzern, informiert über die neusten Änderungen auf dem Wiki und mehr. <a href=\"https://wiki.wikibot.de/wiki/Wiki-Bot_Wiki\" target=\"_blank\">[Mehr Information]</a></p>\n<p>Hier kannst du die Bot-Einstellungen für Server ändern auf denen du die Server verwalten Berechtigung besitzt. Dafür musst du dich mit deinem Discord-Account anmelden, was du über diesen Knopf tuen kannst:</p>"
     },
     "indexjs": {
+        "avatar": {
+            "content_type": "Der Inhalt des angegebenen Links hat den Typ $1, aber es sind nur die folgenden Inhaltstypen erlaubt:",
+            "invalid_url": "Die URL konnte nicht zu einer gültigen Bilddatei aufgelöst werden."
+        },
         "invalid": {
             "note_http": "Die angegebene Website besitzt kein gültiges TLS/SSL-Zertifikat! Aus Sicherheitsgründen werden nur Wikis die HTTPS nutzen unterstützt.",
             "note_private": "Das angegebene Wiki ist privat!",
@@ -38,7 +42,7 @@
             "title": "Ungültiges Wiki!"
         },
         "outdated": {
-            "text": "Der Letzte Änderungen-Webhook benötigt mindestens MediaWiki 1.30!",
+            "text": "Der Letzte Änderungen-WebHook benötigt mindestens MediaWiki 1.30!",
             "title": "Veraltete MediaWiki-Version!"
         },
         "prefix": {
@@ -76,11 +80,6 @@
             "text": "Entweder dir oder Wiki-Bot fehlt die $1-Berechtigung für diese Funktion.",
             "title": "Fehlende Berechtigung!"
         },
-        "movefail": {
-            "note": "Der Webhook-Kanal konnte nicht geändert werden!",
-            "text": "Die Einstellungen konnten nur teilweise aktualisiert werden.",
-            "title": "Einstellungen teilweise gespeichert!"
-        },
         "mwversion": {
             "text": "Es wird mindestens MediaWiki 1.30 benötigt, auf $2 wurde $1 gefunden.",
             "title": "Veraltete MediaWiki version!"
@@ -134,33 +133,41 @@
             "text": "Bitte melde dich an, bevor du Einstellungen ändern kannst.",
             "title": "Nicht angemeldet!"
         },
+        "webhookfail": {
+            "note": "Der Discord-WebHook konnte nicht geändert werden!",
+            "text": "Die Einstellungen wurden nur teilweise gespeichert.",
+            "title": "Einstellungen teilweise gespeichert!"
+        },
         "wikiblocked": {
             "note": "Grund:",
-            "text": "$1 wurde gesperrt und kann nicht als Letzte Änderungen-Webhook hinzugefügt werden.",
+            "text": "$1 wurde gesperrt und kann nicht als Letzte Änderungen-WebHook hinzugefügt werden.",
             "title": "Wiki ist gesperrt!"
         }
     },
     "rcscript": {
-        "desc": "Dies sind die Letzte Änderungen-Webhooks für $1:",
-        "explanation": "<h2>Letzte Änderungen-Webhook</h2>\n<p>Wiki-Bot ist in der Lage einen Letzte Änderungen-Webhook basierend auf <a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a> auszuführen. Die letzten Änderungen können als kompakte Textnachrichten mit Inline-Links oder als Einbettungen mit Bearbeitungsmarkierungen und Kategorieänderungen angezeigt werden.</p>\n<p>Voraussetzungen um einen Letzte Änderungen-Webhook hinzuzufügen:</p>\n<ul>\n<li>Das Wiki benötigt mindestens <a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a>.</li>\n<li>Die Systemnachricht <code class=\"user-select\">MediaWiki:Custom-RcGcDw</code> muss mit der Discord-Server-ID <code class=\"user-select\" id=\"server-id\"></code> übereinstimmen.</li>\n</ul>",
+        "desc": "Dies sind die Letzte Änderungen-WebHooks für $1:",
+        "explanation": "<h2>Letzte Änderungen-WebHook</h2>\n<p>Wiki-Bot ist in der Lage einen Letzte Änderungen-WebHook basierend auf <a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a> auszuführen. Die letzten Änderungen können als kompakte Textnachrichten mit Inline-Links oder als Einbettungen mit Bearbeitungsmarkierungen und Kategorieänderungen angezeigt werden.</p>\n<p>Voraussetzungen um einen Letzte Änderungen-WebHook hinzuzufügen:</p>\n<ul>\n<li>Das Wiki benötigt mindestens <a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a>.</li>\n<li>Die Systemnachricht <code class=\"user-select\">MediaWiki:Custom-RcGcDw</code> muss mit der Discord-Server-ID <code class=\"user-select\" id=\"server-id\"></code> übereinstimmen.</li>\n</ul>",
         "form": {
+            "avatar": "WebHook-Avatar:",
+            "avatar_preview": "Vorschau",
             "channel": "Kanal:",
-            "confirm": "Möchtest du wirklich den Letzte Änderungen-Webhook löschen?",
+            "confirm": "Möchtest du wirklich den Letzte Änderungen-WebHook löschen?",
             "display": "Anzeigemodus:",
             "display_compact": "Kompakte Textnachrichten mit Inline-Links.",
             "display_diff": "Einbettungen mit Bildvorschau und Bearbeitungsunterschieden.",
             "display_embed": "Einbettungen mit Bearbeitungsmarkierungen und Kategorieänderungen.",
             "display_image": "Einbettungen mit Bildvorschau.",
-            "entry": "$1. Letzte Änderungen-Webhook",
+            "entry": "$1. Letzte Änderungen-WebHook",
             "feeds": "Feeds-basierte Änderungen:",
             "feeds_only": "Nur Feeds-basierte Änderungen:",
             "lang": "Sprache:",
-            "new": "Neuer Letzte Änderungen-Webhook",
+            "name": "WebHook-Name:",
+            "new": "Neuer Letzte Änderungen-WebHook",
             "select_channel": "-- Wähle einen Kanal --",
             "wiki": "Wiki:",
             "wiki_check": "Wiki testen"
         },
-        "new": "Neuer Webhook"
+        "new": "Neuer WebHook"
     },
     "selector": {
         "desc": "Dies ist eine Liste aller Server dessen Einstellungen du ändern kannst, da du die [Server verwalten]($1)-Berechtigung hast. Bitte wähle einen Server:",
@@ -230,6 +237,8 @@
             "postcount_or": "Benötige entweder das Bearbeitungs- oder das Diskussionsbeitragslimit.",
             "rename": "Benutzer umbenennen:",
             "role": "Rolle:",
+            "role_add": "Vergeben",
+            "role_remove": "Entfernen",
             "select_channel": "-- Wähle einen Kanal --",
             "select_role": "-- Wähle eine Rolle --",
             "success": "Hinweis bei Erfolg:",

+ 14 - 5
dashboard/i18n/en.json

@@ -30,6 +30,10 @@
         "welcome": "<h2>Welcome to the Wiki-Bot Dashboard.</h2>\n<p>Wiki-Bot is a Discord bot made to bring Discord servers and MediaWiki wikis together. It helps with linking wiki pages, verifying wiki users, informing about latest changes on the wiki and more. <a href=\"https://wiki.wikibot.de/wiki/Wiki-Bot_Wiki\" target=\"_blank\">[More information]</a></p>\n<p>Here you can change different bot settings for servers you have Manage Server permission on. To begin, you will have to authenticate your Discord account which you can do with this button:</p>"
     },
     "indexjs": {
+        "avatar": {
+            "content_type": "The provided link has the content type $1, but only the following content types are allowed:",
+            "invalid_url": "The URL couldn't be resolved to a valid image file."
+        },
         "invalid": {
             "note_http": "The provided website doesn't have a valid TLS/SSL certificate! For security reasons only wikis using HTTPS are supported.",
             "note_private": "The provided wiki is private!",
@@ -76,11 +80,6 @@
             "text": "Either you or Wiki-Bot are missing the $1 permission for this function.",
             "title": "Missing permission!"
         },
-        "movefail": {
-            "note": "The webhook channel could not be changed!",
-            "text": "The settings have only been partially updated.",
-            "title": "Settings partially saved!"
-        },
         "mwversion": {
             "text": "Requires at least MediaWiki 1.30, found $1 on $2.",
             "title": "Outdated MediaWiki version!"
@@ -134,6 +133,11 @@
             "text": "Please login before you can change any settings.",
             "title": "Not logged in!"
         },
+        "webhookfail": {
+            "note": "The Discord webhook could not be changed!",
+            "text": "The settings have only been partially updated.",
+            "title": "Settings partially saved!"
+        },
         "wikiblocked": {
             "note": "Reason:",
             "text": "$1 has been blocked from being added as a recent changes webhook.",
@@ -144,6 +148,8 @@
         "desc": "These are the recent changes webhooks for $1:",
         "explanation": "<h2>Recent Changes Webhook</h2>\n<p>Wiki-Bot is able to run a recent changes webhook based on <a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a>. The recent changes can be displayed in compact text messages with inline links or embed messages with edit tags and category changes.</p>\n<p>Requirements to add a recent changes webhook:</p>\n<ul>\n<li>The wiki needs to run on <a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a> or higher.</li>\n<li>The system message <code class=\"user-select\">MediaWiki:Custom-RcGcDw</code> needs to be set to the Discord server id <code class=\"user-select\" id=\"server-id\"></code>.</li>\n</ul>",
         "form": {
+            "avatar": "Webhook avatar:",
+            "avatar_preview": "Preview",
             "channel": "Channel:",
             "confirm": "Do you really want to delete the recent changes webhook?",
             "display": "Display mode:",
@@ -155,6 +161,7 @@
             "feeds": "Feeds based changes:",
             "feeds_only": "Only feeds based changes:",
             "lang": "Language:",
+            "name": "Webhook name:",
             "new": "New Recent Changes Webhook",
             "select_channel": "-- Select a Channel --",
             "wiki": "Wiki:",
@@ -230,6 +237,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:",

+ 0 - 5
dashboard/i18n/es.json

@@ -76,11 +76,6 @@
             "text": "A ti o a Wiki-Bot les falta el permiso de $1 para realizar esta función.",
             "title": "¡Falta el permiso!"
         },
-        "movefail": {
-            "note": "¡No se pudo cambiar el canal del webhook!",
-            "text": "La configuración se ha actualizado parcialmente.",
-            "title": "¡Configuración parcialmente guardada!"
-        },
         "mwversion": {
             "text": "Requiere al menos MediaWiki 1.30, se encontró $1 en $2.",
             "title": "¡Versión de MediaWiki desactualizada!"

+ 0 - 5
dashboard/i18n/fr.json

@@ -75,11 +75,6 @@
             "text": "Soit vous, soit le bot manque la permission $1 pour effectuer cette action.",
             "title": "Permission manquante !"
         },
-        "movefail": {
-            "note": "Le salon de l'intégration n'a pas pu être changé !",
-            "text": "Les paramètres n'ont été que partiellement mis à jour.",
-            "title": "Paramètres partiellement sauvegardés !"
-        },
         "mwversion": {
             "text": "Requiert au moins la version 1.30 de MediaWiki, trouvé $1 dans $2.",
             "title": "Version obsolète de MediaWiki !"

+ 6 - 7
dashboard/i18n/hi.json

@@ -31,7 +31,7 @@
     },
     "indexjs": {
         "invalid": {
-            "note_http": "यह वेबसाइट HTTPS का इस्तेमाल नहीं करता!",
+            "note_http": "इस वेबसाइट के पास स्वीकृत TLS/SSL प्रमाणपत्र नहीं है! सुरक्षा के कारण सिर्फ HTTPS का इस्तेमाल करने वाले विकियाँ ही समर्थित हैं।",
             "note_private": "यह विकि व्यक्तिगत है!",
             "note_timeout": "इस लिंक को जवाब देने में कुछ ज़्यादा ही वक्त लगा!",
             "text": "URL किसी स्वीकृत मीडियाविकि साइट का नहीं है!",
@@ -76,11 +76,6 @@
             "text": "या तो आप या विकी-बॉट के पास इस फंक्शन के लिए $1 अनुमति नहीं है।",
             "title": "अनुमति नहीं है!"
         },
-        "movefail": {
-            "note": "वेबहुक चैनल को बदला न जा सका!",
-            "text": "सेटिंग्स को अधूरे तरीके से अपडेट किया गया है।",
-            "title": "सेटिंग्स को अधूरे तरीके से सेव किया गया!"
-        },
         "mwversion": {
             "text": "कम से कम मीडियाविकि १.३० चाहिए, $2 पर $1 मिला।",
             "title": "आउटडेटिड मीडियाविकि संस्करण!"
@@ -120,7 +115,7 @@
             "title": "सेटिंग्स को सेव कर दिया गया है!"
         },
         "savefail": {
-            "note_http": "यह वेबसाइट HTTPS का इस्तेमाल नहीं करता!",
+            "note_http": "इस वेबसाइट के पास स्वीकृत TLS/SSL प्रमाणपत्र नहीं है! सुरक्षा के कारण सिर्फ HTTPS का इस्तेमाल करने वाले विकियाँ ही समर्थित हैं।",
             "note_private": "यह विकि व्यक्तिगत है!",
             "note_timeout": "इस लिंक को जवाब देने में कुछ ज़्यादा ही वक्त लगा!",
             "text": "सेटिंग्स को सेव न किया जा सका, कृपया दोबारा कोशिश करें।",
@@ -215,6 +210,8 @@
             "confirm": "क्या आप सच में वेरिफिकेशन को डिलीट करना चाहते हैं?",
             "editcount": "न्यूनतम सम्पादना की मात्रा:",
             "entry": "वेरिफिकेशन #$1",
+            "flag_logall": "असफल वेरिफिकेशन लॉग करें:",
+            "flag_private": "व्यक्तिगत कमांड के जवाब:",
             "logging": "लॉग करने के लिए चैनल:",
             "match": "आवश्यकता में न आने के लिए सूचना:",
             "match_placeholder": "डिस्कॉर्ड टैग के मिलते हुए भी आवश्यकता में न आने के लिए मार्कडाउन टेक्स्ट।",
@@ -228,6 +225,8 @@
             "postcount_or": "या तो सम्पादना या फिर पोस्ट की मात्रा का इस्तेमाल करें।",
             "rename": "सदस्यों को रीनेम करना है:",
             "role": "रोल:",
+            "role_add": "जोड़ें",
+            "role_remove": "हटाएँ",
             "select_channel": "-- एक चैनल चुनिए --",
             "select_role": "-- एक रोल चुनिए --",
             "success": "सफलता की सूचना:",

+ 35 - 26
dashboard/i18n/ja.json

@@ -8,7 +8,7 @@
     ],
     "general": {
         "botlist": {
-            "text": "Botリストに投票することにより、他のユーザーがWiki-Botを見つけやすくなります:",
+            "text": "Botリストに投票することにより、他の利用者がWiki-Botを見つけやすくなります:",
             "title": "Botリスト"
         },
         "delete": "削除",
@@ -16,7 +16,7 @@
         "language": "言語の変更",
         "login": "ログイン",
         "logout": "ログアウト",
-        "rcscript": "最近の変更点",
+        "rcscript": "最近の更新",
         "refresh": "サーバーリストの更新",
         "save": "保存",
         "selector": "サーバーを選択",
@@ -27,9 +27,13 @@
         "theme-light": "ライトテーマを使う",
         "title": "Wiki-Botの設定",
         "verification": "認証",
-        "welcome": "<h2>Wiki-Bot ダッシュボードへようこそ。</h2>\n<p>Wiki-Botは、DiscordサーバーとMediaWikiで作られたWikiを連携させるために作られたDiscord Botです。Wikiページへのリンク、Wikiユーザーの確認、Wikiの最新の変更についての情報提供などを行います。<a href=\"https://wiki.wikibot.de/wiki/Wiki-Bot_Wiki\" target=\"_blank\">[詳しく見る]</a></p>\n<p>ここでは、あなたがDiscordで「サーバー管理」権限を持っているサーバーの様々なBotの設定を変更することができます。まず、Discordアカウントを認証する必要がありますが、このボタンで認証できます。</p>"
+        "welcome": "<h2>Wiki-Bot ダッシュボードへようこそ。</h2>\n<p>Wiki-Botは、DiscordサーバーとMediaWikiで作られたWikiを連携させるために作られたDiscord Botです。Wikiページへのリンク、Wiki利用者の確認、Wikiの最新の変更についての情報提供などを行います。<a href=\"https://wiki.wikibot.de/wiki/Wiki-Bot_Wiki\" target=\"_blank\">[詳しく見る]</a></p>\n<p>ここでは、あなたがDiscordで「サーバー管理」権限を持っているサーバーの様々なBotの設定を変更することができます。まず、Discordアカウントを認証する必要がありますが、このボタンで認証できます。</p>"
     },
     "indexjs": {
+        "avatar": {
+            "content_type": "提供されたリンクのコンテンツタイプは $1 ですが、以下のコンテンツタイプのみ許可されています:",
+            "invalid_url": "URLが有効な画像ファイルにアクセスできませんでした。"
+        },
         "invalid": {
             "note_http": "提供されたWebサイトに有効なTLS/SSL証明書がありません。セキュリティ上の理由から、HTTPSを使用したWikiのみがサポートされています。",
             "note_private": "提供されたWikiはプライベートなものです!",
@@ -38,7 +42,7 @@
             "title": "無効なWikiです!"
         },
         "outdated": {
-            "text": "最近の更のウェブフックには、MediaWiki 1.30以降が必要です!",
+            "text": "最近の更のウェブフックには、MediaWiki 1.30以降が必要です!",
             "title": "古いMediaWikiのバージョンです!"
         },
         "prefix": {
@@ -61,8 +65,8 @@
             "title": "不明なエラーが発生!"
         },
         "invalidusergroup": {
-            "text": "ユーザーグループ名が長すぎるか、指定した数が多すぎました。",
-            "title": "無効なユーザーグループです!"
+            "text": "利用者グループ名が長すぎるか、指定した数が多すぎました。",
+            "title": "無効な利用者グループです!"
         },
         "loginfail": {
             "text": "ログイン中にエラーが発生しました、もう一度お試しください。",
@@ -76,11 +80,6 @@
             "text": "あなたかWiki-Botのどちらかが、この $1 の権限を許可されていません。",
             "title": "許可がありません!"
         },
-        "movefail": {
-            "note": "ウェブフックチャンネルを変更できませんでした!",
-            "text": "一部の設定は更新されました。",
-            "title": "設定が一部保存されました!"
-        },
         "mwversion": {
             "text": "少なくともMediaWiki 1.30が必要で、 $2 で $1 が見つかりました。",
             "title": "古いMediaWikiのバージョンです!"
@@ -134,28 +133,36 @@
             "text": "設定を変更するにはログインが必要です。",
             "title": "ログインしていません!"
         },
+        "webhookfail": {
+            "note": "DiscordのWebhookを変更できませんでした!",
+            "text": "設定は一部しか更新されていません。",
+            "title": "設定が一部保存されました!"
+        },
         "wikiblocked": {
             "note": "理由:",
-            "text": "$1 が最近の変更のウェブフックとして追加されるのを阻止しました。",
+            "text": "$1 が最近の更のウェブフックとして追加されるのを阻止しました。",
             "title": "Wikiがブロックされています!"
         }
     },
     "rcscript": {
-        "desc": "$1 の最近の変更点のウェブフックです。",
-        "explanation": "<h2>最近の変更点のウェブフック</h2>\n<p>Wiki-Botは、<a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a>をベースにした最近の変更のウェブフックを実行することができます。最近の変更点は、直リンク付きの小さなテキストメッセージや、編集されたタグやカテゴリーの変更ができる埋め込みメッセージを表示することができます。</p>\n<p>最近の変更のウェブフックを追加するための要件:</p>\n<ul>\n<li>Wikiは<a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a>以上で動作している必要があります。</li>\n<li>システムメッセージの<code class=\"user-select\">MediaWiki:Custom-RcGcDw</code> にDiscordのサーバーID <code class=\"user-select\" id=\"server-id\"></code> を設定する必要があります。</li>\n</ul>",
+        "desc": "$1 の最近の更新のウェブフックです。",
+        "explanation": "<h2>最近の変更点のウェブフック</h2>\n<p>Wiki-Botは、<a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a>をベースにした最近の変更のウェブフックを実行することができます。最近の変更、直リンク付きの小さなテキストメッセージや、編集されたタグやカテゴリーの変更ができる埋め込みメッセージを表示することができます。</p>\n<p>最近の変更のウェブフックを追加するための要件:</p>\n<ul>\n<li>Wikiは<a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a>以上で動作している必要があります。</li>\n<li>システムメッセージの<code class=\"user-select\">MediaWiki:Custom-RcGcDw</code> にDiscordのサーバーID <code class=\"user-select\" id=\"server-id\"></code> を設定する必要があります。</li>\n</ul>",
         "form": {
+            "avatar": "Webhookのアバター:",
+            "avatar_preview": "プレビュー",
             "channel": "チャンネル:",
-            "confirm": "本当に最近の変更のウェブフックを削除しますか?",
+            "confirm": "本当に最近の更のウェブフックを削除しますか?",
             "display": "表示形態:",
             "display_compact": "直リンク付きの小型のテキストメッセージ。",
             "display_diff": "画像プレビューと編集の違いを表示した埋め込みメッセージ。",
             "display_embed": "編集されたタグやカテゴリーの変更ができる埋め込みメッセージ。",
             "display_image": "画像プレビューを表示した埋め込みメッセージ。",
-            "entry": "最近の変更点のウェブフック #$1",
+            "entry": "最近の更新のウェブフック #$1",
             "feeds": "フィードベースの変更:",
             "feeds_only": "フィードベースの変更のみ:",
             "lang": "言語:",
-            "new": "新しい最近の変更点のウェブフック",
+            "name": "Webhook名:",
+            "new": "新しい最近の変更のウェブフック",
             "select_channel": "-- チャンネルを選択 --",
             "wiki": "Wiki:",
             "wiki_check": "Wikiを確認する"
@@ -180,16 +187,16 @@
             "default": "サーバー全体の設定",
             "inline": "インラインコマンド:",
             "lang": "言語:",
-            "new": "新しいチャンネルの上書き",
+            "new": "別のWikiの通知を追加",
             "overwrite": "$1 設定",
             "prefix": "プレフィックス:",
             "prefix_space": "プレフィックスはスペースで終わります:",
             "role": "最小のロール:",
             "select_channel": "-- チャンネルを選択 --",
-            "wiki": "既定のWiki:",
+            "wiki": "通知するWiki:",
             "wiki_check": "Wikiを確認"
         },
-        "new": "新しいチャンネルの上書き"
+        "new": "別のWikiの通知を追加"
     },
     "slash": {
         "desc": "これらは、 $1 のスラッシュコマンドです:",
@@ -208,9 +215,9 @@
     },
     "verification": {
         "desc": "$1 の認証結果:",
-        "explanation": "<h2>ユーザー認証</h2>\n<p><code class=\"prefix\">verify &lt;wiki username&gt;</code>コマンドを使用すると、ユーザーは自分のWikiプロフィールのDiscord欄を使用し、自分が特定のWikiユーザーであることを認証することができます。ユーザーが一致し、サーバー上でユーザー認証が設定されている場合、Wiki-Botは一致したすべての認証エントリの役割を与えます。</p>\n<p>各認証項目では、ユーザーがどのような場合に認証に一致するかについて、複数の制限を設けることができます。:</p>\n<ul>\n<li><code class=\"prefix\">verify</code>コマンドを使用するチャンネル。</li>\n<li>認証項目の照合時に取得する役割</li>\n<li>認証項目と一致するために必要なWikiの編集回数。</li>\n<li>認証項目と一致するために、Wiki上でメンバーとなる必要のあるユーザーグループ。</li>\n<li>認証項目と一致するために必要なアカウントを作成してからの日数。</li>\n<li>Discordユーザーのニックネームを、認証項目に一致したときのWikiユーザー名に設定するかどうか。</li>\n</ul>",
+        "explanation": "<h2>利用者認証</h2>\n<p><code class=\"prefix\">verify &lt;wiki username&gt;</code>コマンドを使用すると、利用者は自分のWikiプロフィールのDiscord欄を使用し、自分が特定のWiki利用者であることを認証することができます。利用者が一致し、サーバー上で利用者認証が設定されている場合、Wiki-Botは一致したすべての認証エントリの役割を与えます。</p>\n<p>各認証項目では、利用者がどのような場合に認証に一致するかについて、複数の制限を設けることができます。:</p>\n<ul>\n<li><code class=\"prefix\">verify</code>コマンドを使用するチャンネル。</li>\n<li>認証項目の照合時に取得する役割</li>\n<li>認証項目と一致するために必要なWikiの編集回数。</li>\n<li>認証項目と一致するために、Wiki上でメンバーとなる必要のある利用者グループ。</li>\n<li>認証項目と一致するために必要なアカウントを作成してからの日数。</li>\n<li>Discord利用者のニックネームを、認証項目に一致したときのWiki利用者名に設定するかどうか。</li>\n</ul>",
         "form": {
-            "accountage": "アカウントを作成してからの日数:",
+            "accountage": "会員登録後経過日数:",
             "channel": "チャンネル:",
             "confirm": "本当にこの認証を削除しますか?",
             "editcount": "最低編集回数:",
@@ -228,16 +235,18 @@
             "postcount_both": "編集回数と投稿回数を合計した回数を必要要件にする。",
             "postcount_fandom": "Fandom Wikiのみ:",
             "postcount_or": "編集回数と投稿回数のどちらかを必要要件にする。",
-            "rename": "ユーザー名の変更:",
+            "rename": "利用者名の変更:",
             "role": "ロール:",
+            "role_add": "追加",
+            "role_remove": "削除",
             "select_channel": "-- チャンネルを選択 --",
             "select_role": "-- ロールを選択 --",
             "success": "成功の通知:",
             "success_placeholder": "認証に成功した場合のMarkdownテキスト。",
-            "usergroup": "Wikiのユーザーグループ:",
-            "usergroup_and": "全てのユーザーグループを要求します:"
+            "usergroup": "Wikiの利用者グループ:",
+            "usergroup_and": "全ての利用者グループを要求します:"
         },
-        "help_notice": "<p>カスタム通知は、いくつかの簡単な関数や変数をサポートしています。</p>\n<ul>\n<li><code class=\"form-button user-select\">$editcount</code> – そのユーザーの現在の編集回数です。</li>\n<li><code class=\"form-button user-select\">$accountage</code> – そのユーザーの現在のアカウントを作成してからの日数です。</li>\n<li><code class=\"form-button user-select\">$postcount</code> – そのユーザーの現在の議論投稿数です (Fandom Wikiのみ)。</li>\n<li><code class=\"form-button user-select\" data-after=\" }}\" data-before=\"{{#expr: \">{{#expr: 1+1}}</code> – 式の結果を返します。\n<ul>\n<li>加算の <code class=\"form-button user-select\">+</code> と減算の <code class=\"form-button user-select\">-</code>のみ対応しています。</li>\n</ul></li>\n<li><code class=\"form-button user-select\" data-after=\" |  |  }}\" data-before=\"{{#ifexpr: \">{{#ifexpr: 1 &gt; 1 | <i>if true</i> | <i>if false</i> }}</code> – 式の結果に応じたテキストを返します。\n<ul>\n<li><code class=\"form-button user-select\">&lt;</code>、 <code class=\"form-button user-select\">&gt;</code>、 <code class=\"form-button user-select\">=</code>、 <code class=\"form-button user-select\">&lt;=</code>、 <code class=\"form-button user-select\">&gt;=</code>、 <code class=\"form-button user-select\">!=</code>、 <code class=\"form-button user-select\">&lt;&gt;</code> のほか、 <code class=\"form-button user-select\">and</code>、 <code class=\"form-button user-select\">or</code>に対応しています。</li>\n</ul></li>\n</ul>",
+        "help_notice": "<p>カスタム通知は、いくつかの簡単な関数や変数をサポートしています。</p>\n<ul>\n<li><code class=\"form-button user-select\">$editcount</code> – その利用者の現在の編集回数です。</li>\n<li><code class=\"form-button user-select\">$accountage</code> – その利用者の現在のアカウントを作成してからの日数です。</li>\n<li><code class=\"form-button user-select\">$postcount</code> – その利用者の現在のディスカッション投稿数です (Fandom Wikiのみ)。</li>\n<li><code class=\"form-button user-select\" data-after=\" }}\" data-before=\"{{#expr: \">{{#expr: 1+1}}</code> – 式の結果を返します。\n<ul>\n<li>加算の <code class=\"form-button user-select\">+</code> と減算の <code class=\"form-button user-select\">-</code>のみ対応しています。</li>\n</ul></li>\n<li><code class=\"form-button user-select\" data-after=\" |  |  }}\" data-before=\"{{#ifexpr: \">{{#ifexpr: 1 &gt; 1 | <i>if true</i> | <i>if false</i> }}</code> – 式の結果に応じたテキストを返します。\n<ul>\n<li><code class=\"form-button user-select\">&lt;</code>、 <code class=\"form-button user-select\">&gt;</code>、 <code class=\"form-button user-select\">=</code>、 <code class=\"form-button user-select\">&lt;=</code>、 <code class=\"form-button user-select\">&gt;=</code>、 <code class=\"form-button user-select\">!=</code>、 <code class=\"form-button user-select\">&lt;&gt;</code> のほか、 <code class=\"form-button user-select\">and</code>、 <code class=\"form-button user-select\">or</code>に対応しています。</li>\n</ul></li>\n</ul>",
         "new": "新しい認証",
         "notice": "認証の通知"
     }

+ 0 - 5
dashboard/i18n/ko.json

@@ -69,11 +69,6 @@
             "text": "사용자 또는 Wiki-Bot에 $1 권한이 없어서 이 기능을 설정할 수 없습니다.",
             "title": "권한 부족!"
         },
-        "movefail": {
-            "note": "웹훅 채널을 생성할 수 없었습니다!",
-            "text": "설정이 일부만 적용되었습니다.",
-            "title": "설정 일부 적용!"
-        },
         "mwversion": {
             "text": "미디어위키 1.30 이상이 필요하지만, $2 위키에서 $1 버전을 발견했습니다.",
             "title": "오래된 미디어위키 버전!"

+ 0 - 5
dashboard/i18n/pl.json

@@ -76,11 +76,6 @@
             "text": "Ty lub Wiki-Bot nie posiada zezwolenia $1 dla tej funkcji.",
             "title": "Niewystarczające zezwolenia!"
         },
-        "movefail": {
-            "note": "Kanał webhooków nie mógł zostać zmieniony!",
-            "text": "Ustawienia zostały jedynie częściowo zapisane.",
-            "title": "Ustawienia częściowo zapisane!"
-        },
         "mwversion": {
             "text": "Wymaga przynajmniej MediaWiki w wersji 1.30, znaleziono $1 na $2.",
             "title": "Przestarzała wersja MediaWiki!"

+ 4 - 5
dashboard/i18n/pt-br.json

@@ -76,11 +76,6 @@
             "text": "Ou você ou o Wiki-Bot não têm a permissão $1 para esta função.",
             "title": "Faltando permissão!"
         },
-        "movefail": {
-            "note": "O canal do webhook não pode ser alterado!",
-            "text": "As configurações foram apenas parcialmente atualizadas.",
-            "title": "Configurações parcialmente salvas!"
-        },
         "mwversion": {
             "text": "Requer pelo menos MediaWiki 1,30, encontrado $1 em $2.",
             "title": "Versão desatualizada do MediaWiki!"
@@ -215,6 +210,8 @@
             "confirm": "Tem certeza de que deseja excluir a verificação?",
             "editcount": "Contagem mínima de edições:",
             "entry": "Verificação #$1",
+            "flag_logall": "Registrar verificações malsucedidas:",
+            "flag_private": "Respostas de comando privadas:",
             "logging": "Canal de registro:",
             "match": "Aviso de requisitos ausentes:",
             "match_placeholder": "Texto de marcação nas etiquetas do Discord correspondentes, mas não cumprindo os requisitos para os cargos.",
@@ -228,6 +225,8 @@
             "postcount_or": "Requer edição ou contagem de postagens.",
             "rename": "Renomear usuários:",
             "role": "Cargo:",
+            "role_add": "Adicionar",
+            "role_remove": "Remover",
             "select_channel": "-- Selecionar canal --",
             "select_role": "-- Selecionar cargo --",
             "success": "Aviso de sucesso:",

+ 16 - 5
dashboard/i18n/ru.json

@@ -30,6 +30,10 @@
         "welcome": "<h2>Добро пожаловать на панель управления Вики-Ботом.</h2>\n<p>Вики-Бот - это бот, созданный для связывания серверов Discord и вики-сайтов MediaWiki. Он может предоставлять ссылки на страницы, верифицировать пользователей, информировать о свежих правках и многое другое. <a href=\"https://wiki.wikibot.de/wiki/Wiki-Bot_Wiki\" target=\"_blank\">[Узнать больше]</a></p>\n<p>На этой панели управления можно изменять настройки для серверов на которых у вас есть разрешение Управлять Сервером. Чтобы начать, вам нужно авторизовать ваш аккаунт Discord, что вы можете сделать, нажав на эту кнопку:</p>"
     },
     "indexjs": {
+        "avatar": {
+            "content_type": "Файл, находящийся по данной ссылке, имеет тип содержимого $1, но поддерживаются только следующие типы содержимого:",
+            "invalid_url": "Этот URL не указывает на изображение."
+        },
         "invalid": {
             "note_http": "Этот сайт не имеет действительного сертификата TLS/SSL! В целях безопасности поддерживаются только вики, использующие HTTPS.",
             "note_private": "Эта вики приватная!",
@@ -76,11 +80,6 @@
             "text": "Либо у вас, либо у Вики-Бота отсутствует разрешение $1 для выполнения этой функции.",
             "title": "Отсутствует разрешение!"
         },
-        "movefail": {
-            "note": "Канал вебхука не мог быть изменён!",
-            "text": "Настройки были изменены лишь частично.",
-            "title": "Настройки частично сохранены!"
-        },
         "mwversion": {
             "text": "Необходима как минимум MediaWiki 1.30, но на $2 версия $1.",
             "title": "Устаревшая версия MediaWiki!"
@@ -134,6 +133,11 @@
             "text": "Перед тем, как бы сможете изменить какие-либо настройки, вам нужно войти.",
             "title": "Вы не вошли!"
         },
+        "webhookfail": {
+            "note": "Этот вебхук не удалось изменить!",
+            "text": "Настройки были применены лишь частично.",
+            "title": "Настройки частично сохранены!"
+        },
         "wikiblocked": {
             "note": "Причина:",
             "text": "$1 была блокирована от добавления к вебхуку свежих правок.",
@@ -144,6 +148,8 @@
         "desc": "Вебхуки свежих правок на $1:",
         "explanation": "<h2>Вебхук Свежих Правок</h2>\n<p>Вики-Бот может создавать вебхук свежих правок, основанный на <a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a>. Свежие правки могут отображаться как компактные текстовые сообщения со встроенными ссылками или встроенные сообщения с метками правок и изменениями категорий.</p>\n<p>Требования для создания нового вебхука свежих правок:</p>\n<ul>\n<li>Вики должна работать на <a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a> или выше.</li>\n<li>Системное сообщение <code class=\"user-select\">MediaWiki:Custom-RcGcDw</code> должно содержать ID сервера: <code class=\"user-select\" id=\"server-id\"></code>.</li>\n</ul>",
         "form": {
+            "avatar": "Аватар вебхука:",
+            "avatar_preview": "Предпросмотр",
             "channel": "Канал:",
             "confirm": "Вы действительно хотите удалить вебхук свежих правок?",
             "display": "Режим просмотра:",
@@ -155,6 +161,7 @@
             "feeds": "Изменения в лентах:",
             "feeds_only": "Только изменения в лентах:",
             "lang": "Язык:",
+            "name": "Имя вебхука:",
             "new": "Новый Вебхук Свежих Правок",
             "select_channel": "-- Выберите Канал --",
             "wiki": "Вики:",
@@ -215,6 +222,8 @@
             "confirm": "Вы действительно хотите удалить верификацию?",
             "editcount": "Минимальное количество правок:",
             "entry": "Верификация №$1",
+            "flag_logall": "Записывать неуспешные попытки верификации в журнал:",
+            "flag_private": "Приватные ответы:",
             "logging": "Канал для журнала:",
             "match": "Уведомление о несоответствии требованиям:",
             "match_placeholder": "Текст (с разметкой) при верном тэге Discord, но несоответствии требованиям.",
@@ -228,6 +237,8 @@
             "postcount_or": "Проверять либо количество постов, либо количество правок.",
             "rename": "Изменять никнейм участников:",
             "role": "Роль:",
+            "role_add": "Добавить",
+            "role_remove": "Убрать",
             "select_channel": "-- Выберите Канал --",
             "select_role": "-- Выберите Роль --",
             "success": "Уведомление об успехе:",

+ 0 - 5
dashboard/i18n/tr.json

@@ -69,11 +69,6 @@
             "text": "Ya sen ya da Wiki-Bot bu işlev için gereken $1 iznine sahip değil.",
             "title": "Eksik izin!"
         },
-        "movefail": {
-            "note": "Bu webhook kanalı değiştirilemedi!",
-            "text": "Ayarlar sadece kısmen güncellendi.",
-            "title": "Ayarlar kısmen kaydedildi!"
-        },
         "mwversion": {
             "text": "En az MediaWiki 1.30 gerekli. $2, $1 sürümüne sahip.",
             "title": "Eski MediaWiki sürümü!"

+ 14 - 5
dashboard/i18n/zh-hans.json

@@ -30,6 +30,10 @@
         "welcome": "<h2>欢迎来到 Wiki-Bot 控制面板。</h2>\n<p>Wiki-Bot 是一个 Discord 机器人,旨在将 MediaWiki 驱动的 wiki 站点与 Discord 服务器整合在一起。它可以帮忙链接到 wiki 页面,验证 wiki 用户,通知 wiki 上最近的更改,等等。 <a href=\"https://wiki.wikibot.de/wiki/Wiki-Bot_Wiki\" target=\"_blank\">[更多信息]</a></p>\n<p>在此,你可以为每个你拥有“管理服务器”权限的服务器管理并使用不同的机器人设置。但首先,你需要通过这个按钮验证你的 Discord 账号:</p>"
     },
     "indexjs": {
+        "avatar": {
+            "content_type": "所提供的链接的内容类型(Content-Type)为$1,但仅允许以下内容类型:",
+            "invalid_url": "此 URL 无法解析到有效的图像文件。"
+        },
         "invalid": {
             "note_http": "所提供的网站没有合法的TLS/SSL证书!由于安全原因,仅支持使用HTTPS的wiki。",
             "note_private": "所提供的 wiki 是非公开的!",
@@ -76,11 +80,6 @@
             "text": "您或 Wiki-Bot 缺少此功能所需的 $1 权限。",
             "title": "缺少权限!"
         },
-        "movefail": {
-            "note": "无法更改 webhook 频道!",
-            "text": "仅成功更新部分设置。",
-            "title": "已保存部分设置!"
-        },
         "mwversion": {
             "text": "至少需要 MediaWiki 1.30,但 $2 为 $1。",
             "title": "过时的 MediaWiki 版本!"
@@ -134,6 +133,11 @@
             "text": "在更改设置前,请先登录。",
             "title": "未登录!"
         },
+        "webhookfail": {
+            "note": "无法更改Discord Webhook!",
+            "text": "仅更新了一部分设置。",
+            "title": "已保存部分设置!"
+        },
         "wikiblocked": {
             "note": "原因:",
             "text": "$1 已被阻止添加为最近更改 webhook。",
@@ -144,6 +148,8 @@
         "desc": "这些是 $1 的最近更改 webhook:",
         "explanation": "<h2>最近更改 webhook</h2>\n<p>Wiki-Bot可以运行基于<a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a>的最近更改 webhook。最近更改可以以多种显示模式显示:有行内链接的紧凑型文字信息或有编辑标签和分类更改的嵌入式消息。</p>\n<p>添加最近更改 webhook 的基本要求:</p>\n<ul>\n<li>需要以<a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a>或更高版本运行的 wiki。</li>\n<li>系统消息<code class=\"user-select\">MediaWiki:Custom-RcGcDw</code>需要设置为 Discord 服务器的服务器 ID <code class=\"user-select\" id=\"server-id\"></code>。</li>\n</ul>",
         "form": {
+            "avatar": "Webhook 头像:",
+            "avatar_preview": "预览",
             "channel": "频道:",
             "confirm": "你真的想删除最近更改 webhook 吗?",
             "display": "显示模式:",
@@ -155,6 +161,7 @@
             "feeds": "基于推送页的更改:",
             "feeds_only": "仅启用基于推送页的更改:",
             "lang": "语言:",
+            "name": "Webhook 名称:",
             "new": "新的最近更改 webhook",
             "select_channel": "-- 选择频道 --",
             "wiki": "wiki:",
@@ -230,6 +237,8 @@
             "postcount_or": "要求编辑数或帖子数。",
             "rename": "重命名用户:",
             "role": "身份组:",
+            "role_add": "添加",
+            "role_remove": "移除",
             "select_channel": "-- 选择频道 --",
             "select_role": "-- 选择身份组 --",
             "success": "成功提示:",

+ 14 - 5
dashboard/i18n/zh-hant.json

@@ -30,6 +30,10 @@
         "welcome": "<h2>歡迎使用Wiki-Bot控制台。</h2>\n<p>Wiki-Bot是一個Discord機器人,旨在將使用MediaWiki的wiki與Discord伺服器整合在一起。它可以幫忙連結到wiki頁面,驗證wiki使用者,傳送近期變更等。<a href=\"https://wiki.wikibot.de/wiki/Wiki-Bot_Wiki\" target=\"_blank\">[更多資訊]</a></p>\n<p>您可以在此為您擁有「管理伺服器」權限的伺服器變更機器人設定。在開始之前,您需要透過這個按鈕驗證您的Discord帳號:</p>"
     },
     "indexjs": {
+        "avatar": {
+            "content_type": "所提供的連結的內容類型(Content-Type)為$1,但僅允許以下內容類型:",
+            "invalid_url": "此URL無法解析至有效的圖檔。"
+        },
         "invalid": {
             "note_http": "您提供的網站沒有有效的 TLS/SSL 憑證!由於安全原因,僅支援使用 HTTPS 的 wiki。",
             "note_private": "所提供的 wiki 是私人 wiki!",
@@ -76,11 +80,6 @@
             "text": "您或Wiki-Bot缺少此功能需要的 $1 權限。",
             "title": "缺少權限!"
         },
-        "movefail": {
-            "note": "無法變更webhook頻道!",
-            "text": "僅成功更新部分設定。",
-            "title": "已儲存部分設定!"
-        },
         "mwversion": {
             "text": "至少需要MediaWiki 1.30,但 $2 為 $1。",
             "title": "過時的MediaWiki版本!"
@@ -134,6 +133,11 @@
             "text": "請登入以變更設定。",
             "title": "未登入!"
         },
+        "webhookfail": {
+            "note": "無法變更Discord Webhook!",
+            "text": "僅更新了一部分設定。",
+            "title": "已儲存部分設定!"
+        },
         "wikiblocked": {
             "note": "原因:",
             "text": "$1 已被阻止加入近期變更webhook。",
@@ -144,6 +148,8 @@
         "desc": "以下為 $1 的近期變更webhook:",
         "explanation": "<h2>近期變更webhook</h2>\n<p>Wiki-Bot能運行基於<a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a>的近期變更webhook。近期變更可以以多種顯示模式顯示:包含行內連結的精簡文字訊息或包含圖片預覽及編輯差異的嵌入式訊息。</p>\n<p>加入近期變更webhook的基本需求:</p>\n<ul>\n<li>需要以<a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a>或更高版本運行的wiki。</li>\n<li>系統訊息<code class=\"user-select\">MediaWiki:Custom-RcGcDw</code>需要設為Discord伺服器的伺服器id <code class=\"user-select\" id=\"server-id\"></code>。</li>\n</ul>",
         "form": {
+            "avatar": "Webhook 頭像:",
+            "avatar_preview": "預覽",
             "channel": "頻道:",
             "confirm": "您確定要刪除近期變更webhook嗎?",
             "display": "顯示模式:",
@@ -155,6 +161,7 @@
             "feeds": "基於推送頁的變更:",
             "feeds_only": "僅啟用基於推送頁的變更:",
             "lang": "語言:",
+            "name": "Webhook 名稱:",
             "new": "新的近期變更webhook",
             "select_channel": "-- 選擇頻道 --",
             "wiki": "wiki:",
@@ -230,6 +237,8 @@
             "postcount_or": "需要編輯次數或貼文數。",
             "rename": "重新命名使用者:",
             "role": "身分組:",
+            "role_add": "新增",
+            "role_remove": "移除",
             "select_channel": "-- 選擇頻道 --",
             "select_role": "-- 選擇身分組 --",
             "success": "成功提示:",

+ 7 - 2
dashboard/index.js

@@ -115,6 +115,13 @@ const server = http.createServer( (req, res) => {
 		}
 	}
 
+	var reqURL = new URL(req.url, process.env.dashboard);
+
+	if ( req.method === 'HEAD' && files.has(reqURL.pathname) ) {
+		let file = files.get(reqURL.pathname);
+		res.writeHead(200, {'Content-Type': file.contentType});
+		return res.end();
+	}
 	if ( req.method !== 'GET' ) {
 		let body = '<img width="400" src="https://http.cat/418"><br><strong>' + http.STATUS_CODES[418] + '</strong>';
 		res.writeHead(418, {
@@ -125,8 +132,6 @@ const server = http.createServer( (req, res) => {
 		return res.end();
 	}
 
-	var reqURL = new URL(req.url, process.env.dashboard);
-
 	if ( reqURL.pathname === '/oauth/mw' ) {
 		return pages.verify(res, reqURL.searchParams);
 	}

+ 95 - 17
dashboard/rcscript.js

@@ -11,10 +11,17 @@ const display_types = [
 	'image',
 	'diff'
 ];
+const avatar_content_types = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
 
 const fieldset = {
 	channel: '<label for="wb-settings-channel">Channel:</label>'
 	+ '<select id="wb-settings-channel" name="channel" required></select>',
+	name: '<label for="wb-settings-name">Webhook name:</label>'
+	+ '<input type="text" id="wb-settings-name" name="name" minlength="2" maxlength="32" autocomplete="on">',
+	avatar: '<label for="wb-settings-avatar">Webhook avatar:</label>'
+	+ '<input type="url" id="wb-settings-avatar" name="avatar" list="wb-settings-avatar-list" autocomplete="url">'
+	+ '<datalist id="wb-settings-avatar-list"></datalist>'
+	+ '<button type="button" id="wb-settings-avatar-preview">Preview</button>',
 	wiki: '<label for="wb-settings-wiki">Wiki:</label>'
 	+ '<input type="url" id="wb-settings-wiki" name="wiki" list="wb-settings-wiki-list" required autocomplete="url">'
 	+ '<datalist id="wb-settings-wiki-list"></datalist>'
@@ -34,16 +41,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">'
@@ -63,6 +70,8 @@ const fieldset = {
  * @param {Object} settings - The current settings
  * @param {Boolean} settings.patreon
  * @param {String} [settings.channel]
+ * @param {String} [settings.name]
+ * @param {String} [settings.avatar]
  * @param {String} settings.wiki
  * @param {String} settings.lang
  * @param {Number} settings.display
@@ -115,6 +124,20 @@ function createForm($, header, dashboardLang, settings, guildChannels, allWikis)
 		$(`<option id="wb-settings-channel-${settings.channel}">`).val(settings.channel).attr('selected', '').text(settings.channel)
 	);
 	fields.push(channel);
+	let webhook_name = $('<div>').append(fieldset.name);
+	webhook_name.find('label').text(dashboardLang.get('rcscript.form.name'));
+	webhook_name.find('#wb-settings-name').val(settings.name);
+	fields.push(webhook_name);
+	let avatar = $('<div>').append(fieldset.avatar);
+	avatar.find('label').text(dashboardLang.get('rcscript.form.avatar'));
+	avatar.find('#wb-settings-avatar-preview').text(dashboardLang.get('rcscript.form.avatar_preview'));
+	avatar.find('#wb-settings-avatar').val(( settings.avatar || '' ));
+	if ( settings.avatar ) avatar.find('#wb-settings-avatar').attr('size', settings.avatar.length + 10);
+	avatar.find('#wb-settings-avatar-list').append(
+		$(`<option>`).val(new URL('/src/icon.png', process.env.dashboard).href),
+		( settings.avatar ? $(`<option>`).val(settings.avatar) : null )
+	);
+	fields.push(avatar);
 	let wiki = $('<div>').append(fieldset.wiki);
 	wiki.find('label').text(dashboardLang.get('rcscript.form.wiki'));
 	wiki.find('#wb-settings-wiki-check').text(dashboardLang.get('rcscript.form.wiki_check'));
@@ -161,7 +184,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),
@@ -199,11 +222,19 @@ function dashboard_rcscript(res, $, guild, args, dashboardLang) {
 				if ( !response.body?.channel_id ) {
 					console.log( '- Dashboard: ' + response.statusCode + ': Error while getting the webhook: ' + response.body?.message );
 					row.channel = 'UNKNOWN';
+					row.name = 'UNKNOWN';
+					row.avatar = '';
+				}
+				else {
+					row.channel = response.body.channel_id;
+					row.name = response.body.name;
+					row.avatar = ( response.body.avatar ? `https://cdn.discordapp.com/avatars/${response.body.id}/${response.body.avatar}` : '' );
 				}
-				else row.channel = response.body.channel_id;
 			}, error => {
 				console.log( '- Dashboard: Error while getting the webhook: ' + error );
 				row.channel = 'UNKNOWN';
+				row.name = 'UNKNOWN';
+				row.avatar = '';
 			} );
 		} )).finally( () => {
 			let suffix = ( args[0] === 'owner' ? '?owner=true' : '' );
@@ -268,6 +299,8 @@ function dashboard_rcscript(res, $, guild, args, dashboardLang) {
  * @param {String|Number} type - The setting to change
  * @param {Object} settings - The new settings
  * @param {String} settings.channel
+ * @param {String} [settings.name]
+ * @param {String} [settings.avatar]
  * @param {String} settings.wiki
  * @param {String} settings.lang
  * @param {Number} settings.display
@@ -294,6 +327,9 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 		if ( type === 'new' && !userSettings.guilds.isMember.get(guild).channels.some( channel => {
 			return ( channel.id === settings.channel && !channel.isCategory );
 		} ) ) return res(`/guild/${guild}/rcscript/new`, 'savefail');
+		settings.name = ( settings.name || '' ).trim();
+		if ( settings.name.length < 2 ) settings.name = '';
+		if ( !settings.avatar || !/^https?:\/\//.test(settings.avatar) ) settings.avatar = '';
 	}
 	if ( settings.delete_settings && type === 'new' ) {
 		return res(`/guild/${guild}/rcscript/new`, 'savefail');
@@ -357,7 +393,16 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 				if ( body.query.allmessages[0]['*'] !== guild ) {
 					return res(`/guild/${guild}/rcscript/new`, 'sysmessage', guild, wiki.toLink('MediaWiki:Custom-RcGcDw', 'action=edit'));
 				}
-				return db.query( 'SELECT reason FROM blocklist WHERE wiki = $1', [wiki.href] ).then( ({rows:[block]}) => {
+				return Promise.all([
+					db.query( 'SELECT reason FROM blocklist WHERE wiki = $1', [wiki.href] ),
+					( settings.avatar ? got.head( settings.avatar ).then( headresponse => {
+						if ( avatar_content_types.includes( headresponse.headers?.['content-type'] ) ) return;
+						settings.avatar = '';
+					}, error => {
+						console.log( '- Dashboard: Error while checking for the HEAD: ' + error );
+						settings.avatar = '';
+					} ) : null )
+				]).then( ([{rows:[block]}]) => {
 					if ( block ) {
 						console.log( `- Dashboard: ${wiki.href} is blocked: ${block.reason}` );
 						return res(`/guild/${guild}/rcscript/new`, 'wikiblocked', body.query.general.sitename, block.reason);
@@ -390,7 +435,8 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 							type: 'createWebhook',
 							guild: guild,
 							channel: settings.channel,
-							name: ( body.query.allmessages[1]['*'] || 'Recent changes' ),
+							name: ( settings.name || body.query.allmessages[1]['*'] || 'Recent changes' ),
+							avatar: settings.avatar,
 							reason: lang.get('rcscript.audit_reason', wiki.href),
 							text: webhook_lang.get('created', body.query.general.sitename) + ( enableFeeds && settings.feeds_only ? '' : `\n<${wiki.toLink(body.query.pages['-1'].title)}>` ) + ( enableFeeds ? `\n<${wiki.href}f>` : '' )
 						} ).then( webhook => {
@@ -405,6 +451,8 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 								res(`/guild/${guild}/rcscript/${configid}`, 'save');
 								var text = lang.get('rcscript.dashboard.added', `<@${userSettings.user.id}>`, configid);
 								text += `\n${lang.get('rcscript.channel')} <#${settings.channel}>`;
+								text += `\n${lang.get('rcscript.name')} \`${( settings.name || body.query.allmessages[1]['*'] || 'Recent changes' )}\``;
+								if ( settings.avatar ) text += `\n${lang.get('rcscript.avatar')} <${settings.avatar}>`;
 								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]}\``;
@@ -458,6 +506,8 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 				return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 			}
 			row.channel = wresponse.body.channel_id;
+			row.name = wresponse.body.name;
+			row.avatar = ( wresponse.body.avatar ? `https://cdn.discordapp.com/avatars/${wresponse.body.id}/${wresponse.body.avatar}` : '' );
 			var newChannel = false;
 			if ( settings.save_settings && row.channel !== settings.channel ) {
 				if ( !userSettings.guilds.isMember.get(guild).channels.some( channel => {
@@ -519,6 +569,7 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 						} );
 						var text = lang.get('rcscript.dashboard.removed', `<@${userSettings.user.id}>`, type);
 						text += `\n${lang.get('rcscript.channel')} <#${row.channel}>`;
+						text += `\n${lang.get('rcscript.name')} \`${row.name}\``;
 						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]}\``;
@@ -545,6 +596,8 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 				}
 				var hasDiff = false;
 				if ( newChannel ) hasDiff = true;
+				if ( settings.name && row.name !== settings.name ) hasDiff = true;
+				if ( settings.avatar && row.avatar !== settings.avatar ) hasDiff = true;
 				if ( row.wiki !== settings.wiki ) hasDiff = true;
 				if ( row.lang !== settings.lang ) hasDiff = true;
 				if ( row.display !== settings.display ) hasDiff = true;
@@ -584,7 +637,16 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 					if ( row.wiki !== wiki.href && body.query.allmessages[0]['*'] !== guild ) {
 						return res(`/guild/${guild}/rcscript/${type}`, 'sysmessage', guild, wiki.toLink('MediaWiki:Custom-RcGcDw', 'action=edit'));
 					}
-					return db.query( 'SELECT reason FROM blocklist WHERE wiki = $1', [wiki.href] ).then( ({rows:[block]}) => {
+					return Promise.all([
+						db.query( 'SELECT reason FROM blocklist WHERE wiki = $1', [wiki.href] ),
+						( settings.avatar && row.avatar !== settings.avatar ? got.head( settings.avatar ).then( headresponse => {
+							if ( avatar_content_types.includes( headresponse.headers?.['content-type'] ) ) return;
+							settings.avatar = '';
+						}, error => {
+							console.log( '- Dashboard: Error while checking for the HEAD: ' + error );
+							settings.avatar = '';
+						} ) : null )
+					]).then( ([{rows:[block]}]) => {
 						if ( block ) {
 							console.log( `- Dashboard: ${wiki.href} is blocked: ${block.reason}` );
 							return res(`/guild/${guild}/rcscript/${type}`, 'wikiblocked', body.query.general.sitename, block.reason);
@@ -637,6 +699,7 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 							}
 							db.query( sql + ' WHERE webhook = $1', sqlargs ).then( () => {
 								console.log( `- Dashboard: RcGcDw successfully updated: ${guild}#${type}` );
+								var webhook_changes = {};
 								var lang = new Lang(row.mainlang);
 								var webhook_lang = new Lang(settings.lang, 'rcscript.webhook');
 								var diff = [];
@@ -645,6 +708,17 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 								if ( newChannel ) {
 									diff.push(lang.get('rcscript.channel') + ` ~~<#${row.channel}>~~ → <#${settings.channel}>`);
 									webhook_diff.push(webhook_lang.get('dashboard.channel'));
+									webhook_changes.channel = settings.channel;
+								}
+								if ( settings.name && row.name !== settings.name ) {
+									diff.push(lang.get('rcscript.name') + ` ~~\`${row.name}\`~~ → \`${settings.name}\``);
+									webhook_diff.push(webhook_lang.get('dashboard.name', settings.name));
+									webhook_changes.name = settings.name;
+								}
+								if ( settings.avatar && row.avatar !== settings.avatar ) {
+									diff.push(lang.get('rcscript.avatar') + ` <${settings.avatar}>`);
+									webhook_diff.push(webhook_lang.get('dashboard.avatar'));
+									webhook_changes.avatar = settings.avatar;
 								}
 								if ( row.wiki !== wiki.href ) {
 									diff.push(lang.get('rcscript.wiki') + ` ~~<${row.wiki}>~~ → <${wiki.href}>`);
@@ -667,12 +741,14 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 									diff.push(lang.get('rcscript.feeds') + ` ~~*\`${lang.get('rcscript.' + ( row.postid === '-1' ? 'disabled' : 'enabled' ))}\`*~~ → *\`${lang.get('rcscript.' + ( enableFeeds ? 'enabled' : 'disabled' ))}\`*`);
 									webhook_diff.push(webhook_lang.get('dashboard.' + ( enableFeeds ? 'enabled_feeds' : 'disabled_feeds' )));
 								}
-								if ( newChannel ) return sendMsg( {
-									type: 'moveWebhook',
+								if ( Object.keys(webhook_changes).length ) return sendMsg( {
+									type: 'editWebhook',
 									guild: guild,
 									webhook: row.webhook,
-									channel: settings.channel,
-									reason: lang.get('rcscript.audit_reason_move'),
+									channel: webhook_changes.channel,
+									name: webhook_changes.name,
+									avatar: webhook_changes.avatar,
+									reason: lang.get('rcscript.audit_reason_edit'),
 									text: webhook_lang.get('dashboard.updated') + '\n' + webhook_diff.join('\n')
 								} ).then( webhook => {
 									if ( !webhook ) return Promise.reject();
@@ -686,15 +762,17 @@ function update_rcscript(res, userSettings, guild, type, settings) {
 										console.log( '- Dashboard: Error while notifying the guild: ' + error );
 									} );
 								}, error => {
-									console.log( '- Dashboard: Error while moving the webhook: ' + error );
+									console.log( '- Dashboard: Error while editing the webhook: ' + error );
 									return Promise.reject();
 								} ).catch( () => {
-									diff.shift();
-									webhook_diff.shift();
+									Object.keys(webhook_changes).forEach( () => {
+										diff.shift();
+										webhook_diff.shift();
+									} );
 									if ( !diff.length ) {
 										return res(`/guild/${guild}/rcscript/${type}`, 'savefail');
 									}
-									res(`/guild/${guild}/rcscript/${type}`, 'movefail');
+									res(`/guild/${guild}/rcscript/${type}`, 'webhookfail');
 									diff.shift();
 									webhook_diff.shift();
 									got.post( 'https://discord.com/api/webhooks/' + row.webhook, {

+ 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">'
 };

+ 46 - 4
dashboard/src/index.css

@@ -89,10 +89,42 @@ a[alt]:hover:after {
 }
 .scrollbar {
 	-ms-overflow-style: none;
-	scrollbar-width: none;
+	scrollbar-width: thin;
+	scrollbar-color: transparent transparent;
+}
+.scrollbar:hover {
+	scrollbar-color: #202225 transparent;
+}
+.theme-light .scrollbar:hover {
+	scrollbar-color: rgba(79,84,92,0.3) transparent;
 }
 .scrollbar::-webkit-scrollbar {
-	display: none;
+	width: 8px;
+	height: 8px;
+}
+.scrollbar::-webkit-scrollbar-corner {
+	background-color: transparent;
+}
+.scrollbar::-webkit-scrollbar-thumb, .scrollbar::-webkit-scrollbar-track {
+	visibility: hidden;
+}
+.scrollbar:hover::-webkit-scrollbar-thumb, .scrollbar:hover::-webkit-scrollbar-track {
+	visibility: visible;
+}
+.scrollbar::-webkit-scrollbar-thumb {
+	background-clip: padding-box;
+	border: 2px solid transparent;
+	border-radius: 4px;
+	background-color: #202225;
+	min-height: 40px;
+}
+.theme-light .scrollbar::-webkit-scrollbar-thumb {
+	background-color: rgba(79,84,92,0.3);
+}
+.scrollbar::-webkit-scrollbar-track {
+	border-color: transparent;
+	background-color: transparent;
+	border: 2px solid transparent;
 }
 #sidebar {
 	position: fixed;
@@ -129,6 +161,7 @@ a[alt]:hover:after {
 	border-radius: 50%;
 	width: 48px;
 	height: 48px;
+	object-fit: cover;
 	display: flex;
 	align-items: center;
 	justify-content: center;
@@ -223,7 +256,7 @@ a[alt]:hover:after {
 }
 .channel {
 	padding: 3px 8px;
-	margin: 0 8px 2px 12px;
+	margin: 0 0 2px 12px;
 	min-height: 26px;
 	border-radius: 4px;
 	display: flex;
@@ -520,11 +553,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;
@@ -548,10 +584,16 @@ fieldset textarea {
 	color: #2e3338;
 	background-color: #ebedef;
 }
+#wb-settings-avatar-preview-img {
+	width: 128px;
+	height: 128px;
+	background: #32353b;
+}
 .wb-settings-display:first-of-type,
 .wb-settings-permission:first-of-type {
 	display: inline-block;
 }
+#wb-settings-avatar-preview-img,
 .wb-settings-display:not(:first-of-type),
 .wb-settings-permission:not(:first-of-type),
 .wb-settings-additional-select,

+ 99 - 14
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;
@@ -98,15 +124,17 @@ function toggleOption() {
 	} );
 }
 
+var divTemp = document.createElement('div');
+divTemp.innerHTML = '<input type="url" value="invalid">';
+const validationMessageInvalidURL = divTemp.firstChild.validationMessage;
+
 /** @type {HTMLInputElement} */
 const wiki = document.getElementById('wb-settings-wiki');
 if ( wiki ) {
 	wiki.addEventListener( 'input', function() {
 		if ( !/^(?:https?:)?\/\//.test(this.value) ) {
 			if ( this.validity.valid ) {
-				var divTemp = document.createElement('div');
-				divTemp.innerHTML = '<input type="url" value="invalid">';
-				this.setCustomValidity(divTemp.firstChild.validationMessage);
+				this.setCustomValidity(validationMessageInvalidURL);
 			}
 		}
 		else this.setCustomValidity('');
@@ -288,6 +316,58 @@ if ( wiki ) {
 	}
 }
 
+/** @type {HTMLInputElement} */
+const avatar = document.getElementById('wb-settings-avatar');
+if ( avatar ) {
+	avatar.addEventListener( 'input', function() {
+		if ( !/^(?:https?:)?\/\//.test(this.value) ) {
+			if ( this.validity.valid ) {
+				this.setCustomValidity(validationMessageInvalidURL);
+			}
+		}
+		else this.setCustomValidity('');
+	} );
+	/** @type {HTMLButtonElement} */
+	const avatarbutton = document.getElementById('wb-settings-avatar-preview');
+	if ( avatarbutton ) {
+		const avatarpreview = document.createElement('img');
+		avatarpreview.id = 'wb-settings-avatar-preview-img';
+		avatarpreview.classList.add('avatar');
+		const validContentTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
+		avatarbutton.onclick = function() {
+			if ( !avatar.value ) return;
+			if ( !avatar.validity.valid ) return avatar.reportValidity();
+			if ( avatar.value === avatar.defaultValue ) {
+				avatarpreview.src = avatar.value;
+				avatarbutton.after(avatarpreview);
+				return;
+			}
+			fetch( avatar.value, {
+				method: 'HEAD',
+				referrer: ''
+			} ).then( function(response) {
+				if ( !validContentTypes.includes( response.headers.get('content-type') ) ) {
+					var invalidContentType = lang('avatar.content_type').replace( /\$1/g, response.headers.get('content-type') );
+					avatar.setCustomValidity(invalidContentType + '\n' + validContentTypes.join(', ') );
+					avatar.reportValidity();
+					return console.log( 'Invalid content type:', response.headers.get('content-type') );
+				}
+				avatarpreview.src = avatar.value;
+				avatarbutton.after(avatarpreview);
+				
+			}, function(error) {
+				console.log(error);
+				avatar.setCustomValidity(lang('avatar.invalid_url'));
+				avatar.reportValidity();
+			} );
+		};
+		if ( avatar.value ) {
+			avatarpreview.src = avatar.value;
+			avatarbutton.after(avatarpreview);
+		}
+	}
+}
+
 /** @type {HTMLInputElement} */
 const logall = document.getElementById('wb-settings-flag_logall');
 if ( logall ) {
@@ -410,6 +490,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 +537,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 +562,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 +570,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;
 	}
 }
 

+ 6 - 1
dashboard/src/lang.js

@@ -48,4 +48,9 @@ var langOptions = Object.keys(allLangs).map( function(lang) {
 langDropdown.append(...langOptions);
 langSelector.append(langDropdown);
 channellist.after(langSelector);
-channellist.setAttribute('style', 'bottom: 32px;');
+channellist.setAttribute('style', 'bottom: 32px;');
+var selectedChannel = channellist.querySelector('.channel.selected');
+if ( selectedChannel ) {
+	var selectedChannelOffset = channellist.offsetHeight - selectedChannel.offsetTop;
+	if ( selectedChannelOffset < 64 ) channellist.scrollBy(0, 64 - selectedChannelOffset);
+}

+ 4 - 4
dashboard/util.js

@@ -309,11 +309,11 @@ function createNotice($, notice, dashboardLang, args = []) {
 				note = $('<div>').text(dashboardLang.get('notice.savefail.note_' + args[0]));
 			}
 			break;
-		case 'movefail':
+		case 'webhookfail':
 			type = 'info';
-			title.text(dashboardLang.get('notice.movefail.title'));
-			text.text(dashboardLang.get('notice.movefail.text'));
-			note = $('<div>').text(dashboardLang.get('notice.movefail.note'));
+			title.text(dashboardLang.get('notice.webhookfail.title'));
+			text.text(dashboardLang.get('notice.webhookfail.text'));
+			note = $('<div>').text(dashboardLang.get('notice.webhookfail.note'));
 			break;
 		case 'refreshfail':
 			type = 'error';

+ 123 - 70
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);
@@ -206,7 +231,7 @@ function createForm($, header, dashboardLang, settings, guildChannels, guildRole
 	var form = $('<fieldset>').append(...fields);
 	if ( readonly ) {
 		form.find('input').attr('readonly', '');
-		form.find('input[type="checkbox"], option, optgroup').attr('disabled', '');
+		form.find('input[type="checkbox"], input[type="radio"]:not(:checked), option, optgroup').attr('disabled', '');
 		form.find('input[type="submit"], button.addmore').remove();
 	}
 	form.find('button.addmore').text(dashboardLang.get('verification.form.more'));
@@ -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 => {

+ 0 - 4
i18n/bn.json

@@ -494,7 +494,6 @@
         "all_inactive": "আপনি উইকি পরিবর্তন অর ফিড-আধারিত পরিবর্তন এক বাড়ে অক্ষম রাখতে পারবেন না।",
         "audit_reason": "\"$1\"এর জন্যে রিসেন্ট চেঞ্জেস ওয়েবহুক",
         "audit_reason_delete": "রিসেন্ট চেঞ্জেস ওয়েবহুক সরিয়ে দেওয়া হয়েছে",
-        "audit_reason_move": "রিসেন্ট চেঞ্জেস ওয়েবহুকটিকে সরানো হয়ে গেছে",
         "blocked": "এই উইকিকে রিসেন্ট চেঞ্জেস ওয়েবহুকের মধ্যে হওয়ার থেকে ব্ল'ক করা হয়েছে!",
         "blocked_reason": "এই উইকিকে `$1`কারণের জন্যে রিসেন্ট চেঞ্জেস ওয়েবহুকের মধ্যে হওয়ার থেকে ব্ল'ক করা হয়েছে!",
         "channel": "চ্যানেল:",
@@ -763,7 +762,6 @@
         "posteditcount": "সম্পাদনা আর পোস্টের মাত্রা মিলিয়ে:",
         "rename": "নিকনেম বদলান:",
         "rename_no_permission": "**$1এর কাছে উইকি সদস্যনামে জোর করার জন্যে `Manage Nicknames` অনুমতি নেই!**",
-        "role": "রোল:",
         "role_deleted": "**মনে হয় রোল $1 আর উপস্থিত নয়!**",
         "role_managed": "এই রোল দেওয়া যাবে না।",
         "role_max": "আপনি কিছু বেশিই রোল দিয়ে দিয়েছেন।",
@@ -793,8 +791,6 @@
         "help_subpage": "অনুগ্রহ করে নিজের ডিসকর্ড ট্যাগ ($1) নিজের Discord উপপৃষ্ঠে লাগান:",
         "missing": "এই চ্যানেলের জন্যে কোনও ভেরিফিকেশন নেই।",
         "notice": "বিজ্ঞপ্তি:",
-        "qualified": "এগুলি রোল পাবে:",
-        "qualified_error": "এগুলি পাওয়া যেতো, কিন্তু লাগানো যায়নি:",
         "user_blocked": "**উইকি ব্যাবহারকারি $1 ব্লক্ড!**",
         "user_blocked_reply": "আপনি লিংক করা উইকি ব্যাবহারকারি **\"$1\" ব্লক্ড!**",
         "user_disabled": "**উইকি ব্যাবহারকারি $1 অক্ষম!**",

+ 65 - 57
i18n/de.json

@@ -265,31 +265,31 @@
             "rcscript": {
                 "add": {
                     "cmd": "rcscript add [<Wiki>]",
-                    "desc": "Ich füge einen neuen Letzte Änderungen-Webhook hinzu."
+                    "desc": "Ich füge einen neuen Letzte Änderungen-WebHook hinzu."
                 },
                 "default": {
                     "cmd": "rcscript",
-                    "desc": "Ich ändere den Letzte Änderungen-Webhook."
+                    "desc": "Ich ändere den Letzte Änderungen-WebHook."
                 },
                 "delete": {
                     "cmd": "rcscript delete",
-                    "desc": "Ich lösche den Letzte Änderungen-Webhook."
+                    "desc": "Ich lösche den Letzte Änderungen-WebHook."
                 },
                 "display": {
                     "cmd": "rcscript display <neuer Anzeigemodus>",
-                    "desc": "Ich ändere den Anzeigemodus für den Letzte Änderungen-Webhook."
+                    "desc": "Ich ändere den Anzeigemodus für den Letzte Änderungen-WebHook."
                 },
                 "feeds": {
                     "cmd": "rcscript feeds",
-                    "desc": "Ich ändere ob Feeds-basierte Änderungen von einen Fandom-Wiki angezeigt werden sollen für den Letzte Änderungen-Webhook."
+                    "desc": "Ich ändere ob Feeds-basierte Änderungen von einen Fandom-Wiki angezeigt werden sollen für den Letzte Änderungen-WebHook."
                 },
                 "lang": {
                     "cmd": "rcscript lang <neue Sprache>",
-                    "desc": "Ich ändere die Sprache für den Letzte Änderungen-Webhook."
+                    "desc": "Ich ändere die Sprache für den Letzte Änderungen-WebHook."
                 },
                 "wiki": {
                     "cmd": "rcscript wiki <neues Wiki>",
-                    "desc": "Ich ändere das Wiki für den Letzte Änderungen-Webhook."
+                    "desc": "Ich ändere das Wiki für den Letzte Änderungen-WebHook."
                 }
             },
             "search": {
@@ -488,35 +488,36 @@
         "on": "ich bin auf diesem Server nun pausiert und werde die meisten Befehle ignorieren!"
     },
     "rcscript": {
-        "ad": "Du möchtest letzte Änderungen direkt in Discord? Nutze `$1rcscript` um einen Letzte Änderungen-Webhook basierend auf **$2** zu deinen Diesocrd server hinzuzufügen!",
-        "add_more": "Füge mehr Letzte Änderungen-Webhooks hinzu:",
-        "added": "ein Letzte Änderungen-Webhook wurde hinzugefügt für:",
+        "ad": "Du möchtest letzte Änderungen direkt in Discord? Nutze `$1rcscript` um einen Letzte Änderungen-WebHook basierend auf **$2** zu deinen Diesocrd server hinzuzufügen!",
+        "add_more": "Füge mehr Letzte Änderungen-WebHooks hinzu:",
+        "added": "ein Letzte Änderungen-WebHook wurde hinzugefügt für:",
         "all_inactive": "du kannst Wiki-Änderungen und Feeds-basierte Änderungen nicht gleichzeitig deaktiviert haben.",
-        "audit_reason": "Letzte Änderungen-Webhook für \"$1\"",
-        "audit_reason_delete": "Letzte Änderungen-Webhook entfernt",
-        "audit_reason_move": "Letzte Änderungen-Webhook verschoben",
-        "blocked": "diese Wiki wurde davon gesperrt als Letzte Änderungen-Webhook hinzugefügt zu werden!",
-        "blocked_reason": "diese Wiki wurde wegen `$1` davon gesperrt als Letzte Änderungen-Webhook hinzugefügt zu werden!",
+        "audit_reason": "Letzte Änderungen-WebHook für \"$1\"",
+        "audit_reason_delete": "Letzte Änderungen-WebHook entfernt",
+        "audit_reason_edit": "Letzte Änderungen-WebHook aktualisiert",
+        "avatar": "WebHook-Avatar:",
+        "blocked": "diese Wiki wurde davon gesperrt als Letzte Änderungen-WebHook hinzugefügt zu werden!",
+        "blocked_reason": "diese Wiki wurde wegen `$1` davon gesperrt als Letzte Änderungen-WebHook hinzugefügt zu werden!",
         "channel": "Kanal:",
-        "current": "dies sind die aktuellen Letzte Änderungen-Webhooks für diesen Server:",
-        "current_display": "der Anzeigemodus für diesen Webhook ist:",
-        "current_lang": "die Sprache für diesen Webhook ist:",
-        "current_selected": "dies ist der Letzte Änderungen-Webhook `$1` für diesen Server:",
-        "current_wiki": "das Wiki für diesen Webhook ist:",
+        "current": "dies sind die aktuellen Letzte Änderungen-WebHooks für diesen Server:",
+        "current_display": "der Anzeigemodus für diesen WebHook ist:",
+        "current_lang": "die Sprache für diesen WebHook ist:",
+        "current_selected": "dies ist der Letzte Änderungen-WebHook `$1` für diesen Server:",
+        "current_wiki": "das Wiki für diesen WebHook ist:",
         "dashboard": {
-            "added": "$1 hat den Letzte Änderungen-Webhook mit der ID `$2` hinzugefügt.",
-            "removed": "$1 hat den Letzte Änderungen-Webhook mit der ID `$2` gelöscht.",
-            "updated": "$1 hat den Letzte Änderungen-Webhook mit der ID `$2` bearbeitet."
+            "added": "$1 hat den Letzte Änderungen-WebHook mit der ID `$2` hinzugefügt.",
+            "removed": "$1 hat den Letzte Änderungen-WebHook mit der ID `$2` gelöscht.",
+            "updated": "$1 hat den Letzte Änderungen-WebHook mit der ID `$2` bearbeitet."
         },
-        "delete": "Lösche diesen Letzte Änderungen-Webhook:",
-        "deleted": "der Letzte Änderungen-Webhook wurde gelöscht.",
+        "delete": "Lösche diesen Letzte Änderungen-WebHook:",
+        "deleted": "der Letzte Änderungen-WebHook wurde gelöscht.",
         "disabled": "deaktiviert",
-        "disabled_feeds": "die Feeds-basierten Änderungen, wie Diskussionen, Nachrichtenseiten und Artikelkommentare, für diesen Webhook wurden deaktiviert.",
-        "disabled_rc": "die Wiki-Änderungen für diesen Webhook wurden deaktiviert.",
+        "disabled_feeds": "die Feeds-basierten Änderungen, wie Diskussionen, Nachrichtenseiten und Artikelkommentare, für diesen WebHook wurden deaktiviert.",
+        "disabled_rc": "die Wiki-Änderungen für diesen WebHook wurden deaktiviert.",
         "display": "Anzeigemodus:",
         "enabled": "aktiviert",
-        "enabled_feeds": "die Feeds-basierten Änderungen, wie Diskussionen, Nachrichtenseiten und Artikelkommentare, für diesen Webhook wurden aktiviert.",
-        "enabled_rc": "die Wiki-Änderungen für diesen Webhook wurden aktiviert.",
+        "enabled_feeds": "die Feeds-basierten Änderungen, wie Diskussionen, Nachrichtenseiten und Artikelkommentare, für diesen WebHook wurden aktiviert.",
+        "enabled_rc": "die Wiki-Änderungen für diesen WebHook wurden aktiviert.",
         "feeds": "Feeds-basierte Änderungen:",
         "help_display_compact": "Kompakte Textnachrichten mit Inline-Links.",
         "help_display_diff": "Einbettungen mit Bildvorschau und Bearbeitungsunterschieden.",
@@ -526,26 +527,28 @@
         "help_lang": "Bisher mögliche Sprachen sind:",
         "help_wiki": "Link zu einer MediaWiki Website, wie `https://<Wiki>.fandom.com/`",
         "lang": "Sprache:",
-        "max_entries": "du hast bereits die maximale Anzahl an Letzte Änderungen-Webhooks erreicht.",
-        "missing": "es ist noch kein Letzte Änderungen-Webhook für diesen Server vorhanden.",
+        "max_entries": "du hast bereits die maximale Anzahl an Letzte Änderungen-WebHooks erreicht.",
+        "missing": "es ist noch kein Letzte Änderungen-WebHook für diesen Server vorhanden.",
+        "name": "WebHook-Name:",
         "new_lang": "<neue Sprache>",
         "new_wiki": "<Link zum Wiki>",
-        "no_feeds": "das Wiki für diesen Webhook hat keine Feeds-basierten Funktionen, wie Diskussionen, Nachrichtenseiten oder Artikelkommentare, aktiviert.",
+        "no_feeds": "das Wiki für diesen WebHook hat keine Feeds-basierten Funktionen, wie Diskussionen, Nachrichtenseiten oder Artikelkommentare, aktiviert.",
         "noadmin": "du benötigst die `WebHooks verwalten`-Berechtigung für diesen Befehl!",
         "rc": "Wiki-Änderungen:",
-        "sysmessage": "die Systemnachricht `$1` muss die Server-ID `$2` sein um einen Letzte Änderungen-Webhook hinzuzufügen.",
-        "title": "Letzte Änderungen-Webhook",
+        "sysmessage": "die Systemnachricht `$1` muss die Server-ID `$2` sein um einen Letzte Änderungen-WebHook hinzuzufügen.",
+        "title": "Letzte Änderungen-WebHook",
         "toggle": "(umschalten)",
-        "updated_display": "der Anzeigemodus für diesen Webhook wurde geändert zu:",
-        "updated_lang": "die Sprache für diesen Webhook wurde geändert zu:",
-        "updated_wiki": "das Wiki für diesen Webhook wurde geändert zu:",
+        "updated_display": "der Anzeigemodus für diesen WebHook wurde geändert zu:",
+        "updated_lang": "die Sprache für diesen WebHook wurde geändert zu:",
+        "updated_wiki": "das Wiki für diesen WebHook wurde geändert zu:",
         "webhook": {
-            "blocked": "Dieser Letzte Änderungen-Webhook wird gelöscht, da das Wiki gesperrt wurde!",
+            "blocked": "Dieser Letzte Änderungen-WebHook wird gelöscht, da das Wiki gesperrt wurde!",
             "blocked_help": "Du kannst auf dem [Support-Server]($1) nach mehr Details fragen.",
-            "blocked_reason": "Dieser Letzte Änderungen-Webhook wird gelöscht, da das Wiki wegen `$1` gesperrt wurde!",
-            "created": "Ein Letzte Änderungen-Webhook für $1 wurde zu diesem Kanal hinzugefügt.",
+            "blocked_reason": "Dieser Letzte Änderungen-WebHook wird gelöscht, da das Wiki wegen `$1` gesperrt wurde!",
+            "created": "Ein Letzte Änderungen-WebHook für $1 wurde zu diesem Kanal hinzugefügt.",
             "dashboard": {
-                "channel": "• Der Webhook wurde in diesen Kanal verschoben.",
+                "avatar": "• Der WebHook-Avatar wurde geändert.",
+                "channel": "• Der WebHook wurde in diesen Kanal verschoben.",
                 "disabled_feeds": "• Die Feeds-basierten Änderungen, wie Diskussionen, Nachrichtenseiten und Artikelkommentare, wurden deaktiviert.",
                 "disabled_rc": "• Die Wiki-Änderungen wurden deaktiviert.",
                 "display_compact": "• Der Anzeigemodus wurde zu kompakten Textnachrichten mit Inline-Links geändert.",
@@ -555,22 +558,23 @@
                 "enabled_feeds": "• Die Feeds-basierten Änderungen, wie Diskussionen, Nachrichtenseiten und Artikelkommentare, wurden aktiviert.",
                 "enabled_rc": "• Die Wiki-Änderungen wurden aktiviert.",
                 "lang": "• Die Sprache wurde zu $1 geändert.",
-                "updated": "Dieser Letzte Änderungen-Webhook wurde bearbeitet:",
+                "name": "• Der WebHook-Name wurde zu „$1“ geändert.",
+                "updated": "Dieser Letzte Änderungen-WebHook wurde bearbeitet:",
                 "wiki": "• Das Wiki wurde zu $1 geändert."
             },
-            "deleted": "Dieser Letzte Änderungen-Webhook wird gelöscht.",
-            "disabled_feeds": "Die Feeds-basierten Änderungen, wie Diskussionen, Nachrichtenseiten und Artikelkommentare, wurden für diesen Letzte Änderungen-Webhook deaktiviert.",
-            "disabled_rc": "Die Wiki-Änderungen wurden für diesen Letzte Änderungen-Webhook deaktiviert.",
-            "enabled_feeds": "Die Feeds-basierten Änderungen, wie Diskussionen, Nachrichtenseiten und Artikelkommentare, wurden für diesen Letzte Änderungen-Webhook aktiviert.",
-            "enabled_rc": "Die Wiki-Änderungen wurden für diesen Letzte Änderungen-Webhook aktiviert.",
-            "updated_display_compact": "Der Anzeigemodus für diesen Letzte Änderungen-Webhook wurde zu kompakten Textnachrichten mit Inline-Links geändert.",
-            "updated_display_diff": "Der Anzeigemodus für diesen Letzte Änderungen-Webhook wurde zu Einbettungen mit Bildvorschau und Bearbeitungsunterschieden geändert.",
-            "updated_display_embed": "Der Anzeigemodus für diesen Letzte Änderungen-Webhook wurde zu Einbettungen mit Bearbeitungsmarkierungen und Kategorieänderungen geändert.",
-            "updated_display_image": "Der Anzeigemodus für diesen Letzte Änderungen-Webhook wurde zu Einbettungen mit Bildvorschau geändert.",
-            "updated_lang": "Die Sprache für diesen Letzte Änderungen-Webhook wurde zu `$1` geändert.",
-            "updated_wiki": "Das Wiki für diesen Letzte Änderungen-Webhook wurde zu $1 geändert."
+            "deleted": "Dieser Letzte Änderungen-WebHook wird gelöscht.",
+            "disabled_feeds": "Die Feeds-basierten Änderungen, wie Diskussionen, Nachrichtenseiten und Artikelkommentare, wurden für diesen Letzte Änderungen-WebHook deaktiviert.",
+            "disabled_rc": "Die Wiki-Änderungen wurden für diesen Letzte Änderungen-WebHook deaktiviert.",
+            "enabled_feeds": "Die Feeds-basierten Änderungen, wie Diskussionen, Nachrichtenseiten und Artikelkommentare, wurden für diesen Letzte Änderungen-WebHook aktiviert.",
+            "enabled_rc": "Die Wiki-Änderungen wurden für diesen Letzte Änderungen-WebHook aktiviert.",
+            "updated_display_compact": "Der Anzeigemodus für diesen Letzte Änderungen-WebHook wurde zu kompakten Textnachrichten mit Inline-Links geändert.",
+            "updated_display_diff": "Der Anzeigemodus für diesen Letzte Änderungen-WebHook wurde zu Einbettungen mit Bildvorschau und Bearbeitungsunterschieden geändert.",
+            "updated_display_embed": "Der Anzeigemodus für diesen Letzte Änderungen-WebHook wurde zu Einbettungen mit Bearbeitungsmarkierungen und Kategorieänderungen geändert.",
+            "updated_display_image": "Der Anzeigemodus für diesen Letzte Änderungen-WebHook wurde zu Einbettungen mit Bildvorschau geändert.",
+            "updated_lang": "Die Sprache für diesen Letzte Änderungen-WebHook wurde zu `$1` geändert.",
+            "updated_wiki": "Das Wiki für diesen Letzte Änderungen-WebHook wurde zu $1 geändert."
         },
-        "webhook_failed": "der Webhook konnten leider nicht erstellt werden, bitte versuche es später erneut.",
+        "webhook_failed": "der WebHook konnten leider nicht erstellt werden, bitte versuche es später erneut.",
         "wiki": "Wiki:"
     },
     "search": {
@@ -771,11 +775,13 @@
         "posteditcount": "Bearbeitungs- und Diskussionsbeitragslimit zusammen:",
         "rename": "Ändere Nickname:",
         "rename_no_permission": "**$1 fehlt die `Nicknames verwalten` Berechtigung um Wiki-Benutzernamen zu erzwingen!**",
-        "role": "Rolle:",
+        "role_add": "Rolle zum Vergeben:",
         "role_deleted": "**Die Rolle $1 scheint nicht mehr zu existieren!**",
         "role_managed": "die angegebe Rolle kann nicht vergeben werden.",
         "role_max": "du hast zu viele Rollen angegeben.",
         "role_missing": "die angegebe Rolle existiert nicht.",
+        "role_none": "keine",
+        "role_remove": "Rolle zum Entfernen:",
         "role_too_high": "**Die Rolle $1 ist zu hoch für $2 um sie zu vergeben!**",
         "save_failed": "die Verifizierungen konnten leider nicht gespeichert werden, bitte versuche es später erneut.",
         "success": "Hinweis bei Erfolg:",
@@ -810,8 +816,10 @@
         "oauth_message_dm": "Bitte nutze diesen Link um deinen Wiki-Account für $1 zu authentifizieren.",
         "oauth_private": "dieses Wiki nutzt OAuth2 zur Verifizierung. Bitte erlaube Direktnachrichten von diesem Server oder nutze den `/verify`-Befehl damit ich dir privat einen Authentifizierungslink senden kann.",
         "oauth_used": "*Verifiziert über OAuth2*",
-        "qualified": "Qualifiziert für:",
-        "qualified_error": "Qualifiziert für, aber kann nicht hinzugefügt werden:",
+        "qualified_add": "Hinzugefügt zu:",
+        "qualified_add_error": "Kann nicht hinzugefügt werden zu:",
+        "qualified_remove": "Entfernt von:",
+        "qualified_remove_error": "Kann nicht entfernt werden von:",
         "user_blocked": "**{{GENDER:$2|Der Wiki-Benutzer|Die Wiki-Benutzerin}} $1 ist gesperrt!**",
         "user_blocked_reply": "{{GENDER:$2|der|die}} von dir verlinkte {{GENDER:$2|Wiki-Benutzer|Wiki-Benutzerin}} **„$1“ ist gesperrt!**",
         "user_disabled": "**Das Benutzerkonto $1 ist deaktiviert!**",

+ 12 - 4
i18n/en.json

@@ -494,7 +494,8 @@
         "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_delete": "Removed recent changes webhook",
-        "audit_reason_move": "Moved recent changes webhook",
+        "audit_reason_edit": "Updated recent changes webhook",
+        "avatar": "Webhook avatar:",
         "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`!",
         "channel": "Channel:",
@@ -528,6 +529,7 @@
         "lang": "Language:",
         "max_entries": "you already reached the maximal amount of recent changes webhooks.",
         "missing": "there are no recent changes webhooks for this server yet.",
+        "name": "Webhook name:",
         "new_lang": "<new language>",
         "new_wiki": "<link to wiki>",
         "no_feeds": "the wiki for this webhook has no feeds based features, like discussions, message walls or article comments, enabled.",
@@ -545,6 +547,7 @@
             "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.",
             "dashboard": {
+                "avatar": "• The webhook avatar has been changed.",
                 "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.",
@@ -555,6 +558,7 @@
                 "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.",
+                "name": "• The webhook name has been changed to \"$1\".",
                 "updated": "This recent changes webhook has been updated:",
                 "wiki": "• The wiki has been changed to $1."
             },
@@ -771,11 +775,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 +816,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!**",

+ 0 - 4
i18n/es.json

@@ -494,7 +494,6 @@
         "all_inactive": "no puedes deshabilitar los cambios al wiki y los cambios basados en feeds al mismo tiempo.",
         "audit_reason": "Webhook de cambios recientes para \"$1\"",
         "audit_reason_delete": "Webhook de cambios recientes eliminado",
-        "audit_reason_move": "Webhook de cambios recientes movido",
         "blocked": "¡este wiki ha sido bloqueado para que no se agregue como un webhook de cambios recientes!",
         "blocked_reason": "¡este wiki ha sido bloqueado para que no se agregue como un webhook de cambios recientes por `$1`!",
         "channel": "Canal:",
@@ -768,7 +767,6 @@
         "posteditcount": "Número de ediciones y publicaciones combinadas:",
         "rename": "Cambiar alias:",
         "rename_no_permission": "**¡A $1 le falta el permiso `Manage Nicknames` para imponer nombres de usuario wiki!**",
-        "role": "Rol:",
         "role_deleted": "**¡El rol $1 parece que ya no existe!**",
         "role_managed": "no se puede asignar el rol proporcionado.",
         "role_max": "proporcionaste demasiados roles.",
@@ -800,8 +798,6 @@
         "help_subpage": "Por favor agrega tu etiqueta de Discord ($1) a tu subpágina de Discord en el wiki:",
         "missing": "no hay verificaciones configuradas en este canal.",
         "notice": "Aviso:",
-        "qualified": "Calificado para:",
-        "qualified_error": "Calificado para, pero no puede agregar:",
         "user_blocked": "**El usuario wiki $1 está bloqueado!**",
         "user_blocked_reply": "¡tu usuario wiki vinculado **\"$1\" está bloqueado!**",
         "user_disabled": "**¡La cuenta wiki $1 está deshabilitada!**",

+ 0 - 4
i18n/fr.json

@@ -488,7 +488,6 @@
         "all_inactive": "vous ne pouvez pas avoir les modifications de fils de discussion et de modifications de pages désactivés en même temps.",
         "audit_reason": "Intégration des modifications récentes pour \"$1\"",
         "audit_reason_delete": "Intégration des modifications récentes retirée",
-        "audit_reason_move": "Intégration des modifications récentes déplacée",
         "blocked": "ce wiki a été bloqué de l'utilisation de l'intégration des modifications récentes !",
         "blocked_reason": "ce wiki a été bloqué de l'utilisation de l'intégration des modifications récentes pour `$1` !",
         "channel": "Salon :",
@@ -757,7 +756,6 @@
         "posteditcount": "Nombre de modification et post combinés :",
         "rename": "Changer le surnom :",
         "rename_no_permission": "**$1 a besoin de la permission `Gérer les pseudos` pour mettre les pseudos wiki !**",
-        "role": "Rôle :",
         "role_deleted": "**Le rôle $1 ne semble plus exister !**",
         "role_managed": "le rôle fourni ne peut pas être attribué.",
         "role_max": "vous avez fourni trop de rôles.",
@@ -787,8 +785,6 @@
         "help_subpage": "Veuillez rajouter votre tag Discord ($1) à votre sous-page Discord sur le wiki :",
         "missing": "il n'y a aucune vérification mise en place pour ce salon.",
         "notice": "Note :",
-        "qualified": "Qualifié pour :",
-        "qualified_error": "Qualifié pour, mais ne peut pas rajouter :",
         "user_blocked": "**L'utilisateur wiki $1 est bloqué !**",
         "user_blocked_reply": "votre utilisateur wiki lié **\"$1\" est bloqué !**",
         "user_disabled": "**Le compte wiki $1 est désactivé !**",

+ 14 - 4
i18n/hi.json

@@ -494,7 +494,8 @@
         "all_inactive": "आप विकी-बदलाव और फीड-आधारित बदलावों को एक साथ सक्षम नहीं रख सकतें।",
         "audit_reason": "\"$1\" के लिए रीसेंट चेंजेस वेबहुक",
         "audit_reason_delete": "रीसेंट चेंजेस वेबहुक हटा दिया गया",
-        "audit_reason_move": "रीसेंट चेंजेस वेबहुक मूव कर दिया गया है",
+        "audit_reason_edit": "रीसेंट चेंजेस वेबहुक को अपडेट किया गया",
+        "avatar": "वेबहुक का अवतार:",
         "blocked": "इस विकी को रीसेंट चेंजेस वेबहुक बनाने से ब्लॉक कर दिया गया है!",
         "blocked_reason": "इस विकी को `$1` के लिए रीसेंट चेंजेस वेबहुक बनाने से ब्लॉक कर दिया गया है!",
         "channel": "चैनल:",
@@ -528,6 +529,7 @@
         "lang": "भाषा:",
         "max_entries": "आप रीसेंट चेंजेस वेबहुक के अधिकतम संख्या तक पहुँच चुके हैं।",
         "missing": "इस सर्वर के लिए अब तक कोई रीसेंट चेंजेस वेबहुक नहीं बना है।",
+        "name": "वेबहुक का नाम:",
         "new_lang": "<नई भाषा>",
         "new_wiki": "<विकी का लिंक>",
         "no_feeds": "इस वेबहुक के विकी पर कोई फीड-आधारित बदलाव, जैसे डिसकशंस, मैसेज वॉल और आर्टिकल कमेंट, सक्षम नहीं है।",
@@ -545,6 +547,7 @@
             "blocked_reason": "इस रीसेंट चेंजेस वेबहुक को डिलीट कर दिया जाएगा क्योंकि विकी को `$1` के लिए ब्लॉक कर दिया गया है!",
             "created": "$1 के लिए एक रीसेंट चेंजेस वेबहुक को इस चैनल पर जोड़ दिया गया है।",
             "dashboard": {
+                "avatar": "• वेबहुक के अवतार को बदल दिया गया है।",
                 "channel": "• वेबहुक को इस चैनल पर लाया गया है।",
                 "disabled_feeds": "• फीड-आधारित बदलावों, जैसे डिसकशंस, मैसेज वॉल, और आर्टिकल कमेंट, को अक्षम कर दिया गया है।",
                 "disabled_rc": "• विकी बदलावों को अक्षम कर दिया गया है।",
@@ -555,6 +558,7 @@
                 "enabled_feeds": "• फीड-आधारित बदलावों, जैसे डिसकशंस, मैसेज वॉल, और आर्टिकल कमेंट, को सक्षम कर दिया गया है।",
                 "enabled_rc": "• विकी बदलावों को सक्षम कर दिया गया है।",
                 "lang": "• भाषा को $1 में बदल दिया गया है।",
+                "name": "• वेबहुक के नाम को \"$1\" में बदल दिया गया है।",
                 "updated": "रीसेंट चेंजेस वेबहुक को अपडेट किया गया है:",
                 "wiki": "• विकी को $1 में बदल दिया गया है।"
             },
@@ -750,6 +754,8 @@
         "disabled": "अक्षम",
         "editcount": "सम्पादना की मात्रा:",
         "enabled": "सक्षम",
+        "flag_logall": "असफल वेरिफिकेशनों को लॉग करना:",
+        "flag_private": "वेरिफिकेशन कमांड के जवाब को व्यक्तिगत रूप में भेजना:",
         "indays": "(दिनों में)",
         "logging": "लॉग करने के लिए चैनल:",
         "match": "आवश्यकता में न आने के लिए सूचना:",
@@ -769,11 +775,13 @@
         "posteditcount": "सम्पादना और पोस्ट की मात्रा को मिलाकर:",
         "rename": "निकनेम बदलें:",
         "rename_no_permission": "**$1 के पास विकी यूज़रनेम पर मजबूर करने के लिए `Manage Nicknames` अनुमति नहीं है!**",
-        "role": "रोल:",
+        "role_add": "जोड़ने के लिए रोल:",
         "role_deleted": "**शायद रोल $1 अब मौजूद नहीं है!**",
         "role_managed": "इस रोल को नहीं दिया जा सकता।",
         "role_max": "आपने कुछ ज़्यादा ही रोल दे दिए हैं।",
         "role_missing": "यह रोल मौजूद नहीं है।",
+        "role_none": "कुछ नहीं",
+        "role_remove": "हटाने के लिए रोल:",
         "role_too_high": "**रोल $1 $2 को देने के लिए कुछ ज़्यादा ही ऊँचा है!**",
         "save_failed": "बदकिस्मती से वेरिफिकेशन सेव नहीं हो पाया। कृपया थोड़ी देर बाद कोशिश करें।",
         "success": "सफलता की सूचना:",
@@ -808,8 +816,10 @@
         "oauth_message_dm": "कृपया $1 के लिए अपने विकि अकाउंट को ऑथेंटिकेट करने के लिए इस लिंक का इस्तेमाल करें।",
         "oauth_private": "यह विकि वेरिफिकेशन के लिए OAuth2 का इस्तेमाल करता है। कृपया सर्वर से डायरेक्ट मैसेज सक्षम करें या `/verify` कमांड का इस्तेमाल करें ताकि मैं आपको व्यक्तिगत रूप से एक ऑथेंटिकेशन लिंक भेज सकूँ।",
         "oauth_used": "*OAuth2 द्वारा वेरिफाई किया गया*",
-        "qualified": "इनके लिए अर्हता प्राप्त किया:",
-        "qualified_error": "इनके लिए अर्हता प्राप्त किया पर जोड़े न जा सकें:",
+        "qualified_add": "इसमें जोड़ा गया है:",
+        "qualified_add_error": "इसमें जोड़ा नहीं गया है:",
+        "qualified_remove": "इससे हटाया गया है:",
+        "qualified_remove_error": "इससे हटाया नहीं गया है:",
         "user_blocked": "**विकी सदस्य $1 ब्लॉक्ड है!**",
         "user_blocked_reply": "आप द्वारा लिंक किया गया विकी सदस्य **\"$1\" ब्लॉक्ड है!**",
         "user_disabled": "**विकी अकाउंट $1 अक्षम है!**",

+ 133 - 9
i18n/ja.json

@@ -97,7 +97,7 @@
             " "
         ],
         "user": [
-            "ユーザー",
+            "利用者",
             "利用者",
             " ",
             " ",
@@ -120,7 +120,7 @@
             "bytes": "$1 {{PLURAL:$2|バイト|バイト}} $3",
             "comment": "コメント:",
             "editor": "編集者:",
-            "minor": "(m)",
+            "minor": "()",
             "more": "その他にも",
             "removed": "削除:",
             "size": "差分:",
@@ -132,7 +132,7 @@
     },
     "discussion": {
         "image": "画像を見る",
-        "main": "議論",
+        "main": "ディスカッション",
         "post": "ポスト",
         "tags": "タグ:",
         "votes": "$1 {{PLURAL:$2|票|票}} ($3%)"
@@ -178,11 +178,25 @@
             },
             "discussion": {
                 "post": {
-                    "cmd": "discussion post <キーワード>"
+                    "cmd": "discussion post <キーワード>",
+                    "desc": "Fandom Wikiの一致する投稿されたディスカッションへのリンクで答えます。"
+                },
+                "thread": {
+                    "cmd": "discussion <キーワード>",
+                    "desc": "Fandom Wikiの一致するディスカッションスレッドへのリンクでお答えします。"
                 }
             },
+            "fandom": {
+                "cmd": "?<wiki> <キーワード>",
+                "desc": "名前のついたFandom wikiの一致する記事へのリンクでお答えします:`https://<wiki>.fandom.com/`"
+            },
+            "gamepedia": {
+                "cmd": "!<wiki> <キーワード>",
+                "desc": "名前のついたGamepedia wikiの一致する記事へのリンクでお答えします: `https://<wiki>.gamepedia.com/`"
+            },
             "help": {
                 "admin": {
+                    "cmd": "help admin",
                     "desc": "Wiki-Botが全ての管理者コマンドを一覧表示します。"
                 },
                 "command": {
@@ -190,11 +204,17 @@
                     "desc": "コマンドの動作を知りたいですか?私がお答えしましょう!"
                 },
                 "default": {
+                    "cmd": "help",
                     "desc": "Wiki-Botが実行できる全てのコマンドの一覧を表示します。"
                 }
             },
+            "info": {
+                "cmd": "info",
+                "desc": "自己紹介をします。"
+            },
             "inline": {
                 "link": {
+                    "cmd": "[[<ページ名>]]",
                     "desc": "Wiki-BotがWikiの記事への直リンクを返します。"
                 },
                 "template": {
@@ -204,6 +224,7 @@
             },
             "minecraft": {
                 "bug": {
+                    "cmd": "bug <Minecraft バグ>",
                     "desc": "Wiki-BotがMinecraftバグトラッカーへのリンクを返します。"
                 },
                 "command": {
@@ -217,7 +238,7 @@
             },
             "mwprojects": {
                 "cmd": "!!<wiki> <キーワード>",
-                "desc": "Wiki-Botが該当した記事のMediaWikiプロジェクトのページへのリンクを返します。例:`$1!!en.wikipedia.org Cookie`"
+                "desc": "Wiki-Botが該当した記事のMediaWikiプロジェクトのページへのリンクを返します。例:`$1!!ja.wikipedia.org クッキー`"
             },
             "overview": {
                 "cmd": "overview",
@@ -244,11 +265,11 @@
             "rcscript": {
                 "add": {
                     "cmd": "rcscript add [<wiki>]",
-                    "desc": "新しい最近の更のウェブフックを追加します。"
+                    "desc": "新しい最近の更のウェブフックを追加します。"
                 },
                 "default": {
                     "cmd": "rcscript",
-                    "desc": "最近の更のウェブフックを変更します。"
+                    "desc": "最近の更のウェブフックを変更します。"
                 },
                 "delete": {
                     "cmd": "rcscript delete",
@@ -256,12 +277,115 @@
                 },
                 "display": {
                     "cmd": "rcscript display <新規 表示モード>",
-                    "desc": "Wiki-BOTの最近の更新Webhookの表示モードを変更します。"
+                    "desc": "Wiki-Botの最近の更新Webhookの表示モードを変更します。"
                 },
                 "feeds": {
                     "cmd": "rcscript feeds",
-                    "desc": "Fandom Wikiの更新された議論のWebhookを最近の更新Webhookへ切り替えます。"
+                    "desc": "Fandom Wikiの更新されたディスカッションのウェブフックを最近の更新のウェブフックへ切り替えます。"
+                },
+                "lang": {
+                    "cmd": "rcscript lang <新規言語>",
+                    "desc": "Wiki-Botの最近の更新のウェブフックの言語を変更します。"
+                },
+                "wiki": {
+                    "cmd": "rcscript wiki <新規wiki>",
+                    "desc": "Web-Botの最近の更新ウェブフックのWikiを変更します。"
+                }
+            },
+            "search": {
+                "cmd": "search <検索キーワード>",
+                "desc": "Wiki-BotがWikiの検索ワードに該当した記事のリンクを返します。"
+            },
+            "settings": {
+                "channel": {
+                    "cmd": "settings channel",
+                    "desc": "Wiki-Botの現在のチャンネルのオーバーライドを変更します。"
+                },
+                "default": {
+                    "cmd": "settings",
+                    "desc": "このサーバーのWiki-Botの設定を変更します。"
+                },
+                "inline": {
+                    "cmd": "settings inline toggle",
+                    "desc": "このサーバーのWiki-Botのインラインコマンドを切り替えます。"
+                },
+                "lang": {
+                    "cmd": "settings lang <言語>",
+                    "desc": "このサーバーのWiki-Botの言語を変更します。"
+                },
+                "prefix": {
+                    "cmd": "settings prefix <プレフィックス>",
+                    "desc": "このサーバーのプレフィックスを変更します。"
+                },
+                "role": {
+                    "cmd": "settings role <権限>",
+                    "desc": "このサーバーのWiki-Botがコマンドを使用するための最低限の権限を設定します。"
+                },
+                "wiki": {
+                    "cmd": "settings wiki <wiki>",
+                    "desc": "このサーバーのWiki-BotのデフォルトのWikiを設定します。"
+                }
+            },
+            "test": {
+                "cmd": "test",
+                "desc": "私が活動していれば答えます!そうでなければしません。"
+            },
+            "user": {
+                "cmd": "ユーザー:<利用者名>",
+                "desc": "利用者に関する情報を表示します。"
+            },
+            "verification": {
+                "accountage": {
+                    "cmd": "verification <id> accountage <新規アカウントの登録日からの日数>",
+                    "desc": "アカウント作成から認証までの最短期間(日数)を変更します。"
+                },
+                "add": {
+                    "cmd": "verification add <ロール>",
+                    "desc": "新しい認証を追加します。`|`で区切られたリストを受け取ります。"
+                },
+                "channel": {
+                    "cmd": "verification <id> channel <新しいチャンネル>",
+                    "desc": "認証チャンネルを変更します。`|`で区切られたリストを受け取ります。"
+                },
+                "default": {
+                    "cmd": "verification",
+                    "desc": "`$1verify`コマンドで使用する認証方法を変更します。"
+                },
+                "delete": {
+                    "cmd": "verification <id> delete",
+                    "desc": "認証を削除します。"
+                },
+                "editcount": {
+                    "cmd": "verification <id> editcount <編集回数>",
+                    "desc": "認証のための最低編集回数を変更します。"
+                },
+                "postcount": {
+                    "cmd": "verification <id> postcount <新規投稿数>",
+                    "desc": "認証のためのディスカッションの最低投稿回数を変更します。\n\t• 投稿数と編集数のどちらかにチェックが入るように、マイナスの数値を入力してください。\n\t• 投稿数と編集数の両方をまとめてチェックする場合は、`null`を入力してください。"
+                },
+                "rename": {
+                    "cmd": "verification <id> rename",
+                    "desc": "Discordのユーザー名を認証時のWiki上のメンバー名に変更するかどうかを変更します。"
+                },
+                "role": {
+                    "cmd": "verification <id> role <新規ロール>",
+                    "desc": "認証のためのロールを変更します。`|`で区切られたリストを受け取ります。"
+                },
+                "usergroup": {
+                    "cmd": "verification <id> usergroup <新規ユーザーグループ>",
+                    "desc": "認証のためのユーザーグループを変更します。`|`で区切られたリストを指定します。\n\t•リストの最初の要素に `AND` を入れると、リスト内のすべてのユーザーグループが認証の対象となります。"
                 }
+            },
+            "verify": {
+                "cmd": "verify <Wikiユーザー名>",
+                "desc": "このコマンドを使用して、DiscordアカウントとWikiアカウントを照合し、Wikiアカウントに一致するロールを取得します。"
+            },
+            "voice": {
+                "cmd": "voice",
+                "desc": "このボイスチャンネルの全員に特定のロールを与えるようにします。"
+            },
+            "wikia": {
+                "cmd": "??<wiki> <検索キーワード>"
             }
         }
     },

+ 0 - 4
i18n/ko.json

@@ -485,7 +485,6 @@
         "all_inactive": "위키 최근 바뀜과 피드 기반 바뀜이 동시에 비활성화되면 안 됩니다.",
         "audit_reason": "\"$1\" 위키의 최근 바뀜 웹훅",
         "audit_reason_delete": "삭제된 최근 바뀜 웹훅",
-        "audit_reason_move": "이동된 최근 바뀜 웹훅",
         "blocked": "이 위키는 최근 바뀜 웹훅으로 추가되지 못하도록 차단되어 있습니다!",
         "blocked_reason": "이 위키는 `$1` 때문에 최근 바뀜 웹훅으로 추가되지 못하도록 차단되어 있습니다!",
         "channel": "채널:",
@@ -740,7 +739,6 @@
         "posteditcount": "편집 횟수와 게시글 횟수 통합:",
         "rename": "닉네임 변경하기:",
         "rename_no_permission": "**$1 봇은 위키 계정 이름을 강제하기 위해 `닉네임 관리하기` 권한이 필요해요!**",
-        "role": "역할:",
         "role_deleted": "**$1 역할이 더이상 존재하지 않는 것 같아요!**",
         "role_managed": "제공한 역할을 부여할 수 없었어요.",
         "role_max": "역할을 너무 많이 지정했어요.",
@@ -770,8 +768,6 @@
         "help_subpage": "디스코드 태그($1)를 사용자 문서의 Discord 하위문서에 생성해 주세요:",
         "missing": "이 채널에 인증이 설정되어 있지 않아요.",
         "notice": "공지:",
-        "qualified": "적합:",
-        "qualified_error": "적합하지만 추가할 수 없음:",
         "user_blocked": "**$1 사용자는 차단되어 있습니다!**",
         "user_blocked_reply": "연결된 위키 계정 **\"$1\" 이(가) 차단되어 있습니다!**",
         "user_disabled": "**$1 사용자는 비활성화되어 있습니다!**",

+ 0 - 1
i18n/nl.json

@@ -443,7 +443,6 @@
         "new_role": "<nieuwe rol>",
         "new_usergroup": "<nieuwe gebruikersgroep>",
         "or": "of",
-        "role": "Rol:",
         "role_managed": "de opgegeven rol kan niet toegekend worden.",
         "role_max": "je hebt te veel rollen opgegeven.",
         "role_missing": "de opgegeven rol bestaat niet.",

+ 20 - 5
i18n/pl.json

@@ -494,7 +494,8 @@
         "all_inactive": "śledzenie ostatnich zmian oraz dyskusji nie może być wyłączone jednocześnie.",
         "audit_reason": "Webhook ostatnich zmian dla „$1”",
         "audit_reason_delete": "Usunięto webhook ostatnich zmian",
-        "audit_reason_move": "Przeniesiono webhook ostatnich zmian",
+        "audit_reason_edit": "Zaktualizowano webhook ostatnich zmian",
+        "avatar": "Awatar webhooka:",
         "blocked": "ta wiki nie może zostać dodana do webhooka ostatnich zmian gdyż została zablokowana!",
         "blocked_reason": "zablokowano możliwość dodawania tej wiki do webhooka ostatnich zmian z powodu `$1`!",
         "channel": "Kanał:",
@@ -528,6 +529,7 @@
         "lang": "Język:",
         "max_entries": "osiągnięto maksymalną ilość webhooków ostatnich zmian.",
         "missing": "obecnie nie istnieją żadne webhooki ostatnich zmian na tym serwerze.",
+        "name": "Nazwa webhooka:",
         "new_lang": "<nowy język>",
         "new_wiki": "<link do wiki>",
         "no_feeds": "wiki dla tego webhooka nie ma włączonej usługi Dyskusji.",
@@ -545,6 +547,7 @@
             "blocked_reason": "Ten webhook ostatnich zmian zostanie usunięty gdyż wiki została zablokowana za `$1`!",
             "created": "Webhook ostatnich zmian dla $1 został dodany do tego kanału.",
             "dashboard": {
+                "avatar": "• Awatar webhooka został zmieniony.",
                 "channel": "• Webhook został przeniesiony do tego kanału.",
                 "disabled_feeds": "• Zmiany bazowane na technologii Feedów takie jak dyskusje, tablice wiadomości oraz komentarze do artykułów zostały wyłączone.",
                 "disabled_rc": "• Ostatnie zmiany na wiki zostały wyłączone.",
@@ -555,6 +558,7 @@
                 "enabled_feeds": "• Zmiany bazowane na technologii Feedów takie jak dyskusje, tablice wiadomości oraz komentarze do artykułów zostały włączone.",
                 "enabled_rc": "• Ostatnie zmiany na wiki zostały włączone.",
                 "lang": "• Język został zmieniony na $1.",
+                "name": "• Nazwa webhooka została zmieniona na „$1”.",
                 "updated": "Webhook ostatnich zmian został zaktualizowany:",
                 "wiki": "• Wiki została zmieniona na $1."
             },
@@ -590,6 +594,7 @@
         "special": "Zawartość tej strony specjalnej:"
     },
     "settings": {
+        "button": "Otwórz panel sterowania",
         "channel current": "obecne ustawienia dla tego kanału:",
         "channel lang": "językiem tego kanału jest:",
         "channel langchanged": "zmieniono język kanału na:",
@@ -739,17 +744,21 @@
         "current_selected": "to weryfikacja `$1` dla tego serwera:",
         "dashboard": {
             "added": "$1 dodał weryfikację z id `$2`.",
+            "added_notice": "$1 dodał(a) dodatkowe powiadomienia weryfikacji.",
             "removed": "$1 usunął weryfikację z id `$2`.",
-            "updated": "$1 zaktualizował weryfikację z id `$2`."
+            "updated": "$1 zaktualizował weryfikację z id `$2`.",
+            "updated_notice": "$1 zaktualizował(a) dodatkowe powiadomienia weryfikacji."
         },
         "delete_current": "Usuń tę weryfikację:",
         "deleted": "weryfikacja została usunięta.",
         "disabled": "wyłączone",
         "editcount": "Ilość edycji:",
         "enabled": "włączone",
+        "flag_logall": "Logowanie nieudanych weryfikacji:",
         "flag_private": "Wysyłanie odpowiedzi do komendy weryfikacyjnej prywatnie:",
         "indays": "(w dniach)",
         "logging": "Kanał logujący:",
+        "match": "Powiadomienie o niespełnionych wymaganiach:",
         "max_entries": "osiągnięto maksymalną ilość weryfikacji.",
         "missing": "na tym serwerze nie istnieją żadne weryfikacje na tę chwilę.",
         "new_accountage": "<nowy wiek konta>",
@@ -765,11 +774,13 @@
         "posteditcount": "Łączona ilość edycji oraz postów w dyskusjach:",
         "rename": "Zmień nazwę użytkownika:",
         "rename_no_permission": "**$1 wymaga uprawnienia `Zarządzanie pseudonimami` do zmiany nazw użytkowników na wiki!**",
-        "role": "Rola:",
+        "role_add": "Role do dodania:",
         "role_deleted": "**Nie wygląda na to aby rola $1 nadal istniała!**",
         "role_managed": "podana rola nie może zostać ustawiona.",
         "role_max": "podano zbyt dużo ról.",
         "role_missing": "podana rola nie istnieje.",
+        "role_none": "brak",
+        "role_remove": "Role do usunięcia:",
         "role_too_high": "**Rola $1 jest wyżej niż najwyższa rola $2, dlatego nie będzie możliwe jej ustawienie!**",
         "save_failed": "niestety, weryfikacja nie mogła zostać zapisana, spróbuj ponownie później.",
         "toggle": "(przełącz)",
@@ -781,6 +792,8 @@
     },
     "verify": {
         "audit_reason": "Zweryfikowano jako „$1”",
+        "button_again": "Sprawdź ponownie",
+        "button_wrong_user": "Nie możesz sprawdzić tej weryfikacji ponownie, to weryfikacja $1!",
         "discord": "{{GENDER:$1|Użytkownik Discord|Użytkowniczka Discord|Użytkownik/Użytkowniczka Discord}}:",
         "empty": "*puste*",
         "error": "Weryfikacja nie powiodła się z powodu błędu.",
@@ -801,8 +814,10 @@
         "oauth_message_dm": "Użyj tego linku aby uwierzytelnić swoje konto wiki dla $1.",
         "oauth_private": "wiki używa OAuth2 do weryfikacji. Aby się uwierzytelnić, zezwól na komunikację przez prywatne wiadomości dla tego serwera lub użyj komendy `/verify` aby zweryfikować się za pomocą linku wysłanego prywatnie.",
         "oauth_used": "*Zweryfikowano z użyciem OAuth2*",
-        "qualified": "Pozwala na otrzymanie ról:",
-        "qualified_error": "Pozwala na otrzymanie ról, których nie udało się dodać:",
+        "qualified_add": "Dodano do:",
+        "qualified_add_error": "Nie udało się dodać:",
+        "qualified_remove": "Usunięto z:",
+        "qualified_remove_error": "Nie można usunąć z:",
         "user_blocked": "**Konto {{GENDER:$2|użytkownika|użytkowniczki|użytkownika/użytkowniczki}} $1 jest zablokowane!**",
         "user_blocked_reply": "konto zalinkowanego {{GENDER:$2|użytkownika|użytkowniczki|użytkownika/użytkowniczki}} wiki **„$1” jest zablokowane!**",
         "user_disabled": "**Konto wiki $1 jest wyłączone!**",

+ 9 - 4
i18n/pt-br.json

@@ -494,7 +494,6 @@
         "all_inactive": "você não pode ter mudanças da wiki e alterações baseadas em feeds desativadas ao mesmo tempo.",
         "audit_reason": "Mudanças recentes no webhook para \"$1\"",
         "audit_reason_delete": "Removido o webhook das mudanças recentes",
-        "audit_reason_move": "Movido o webhook das mudanças recentes",
         "blocked": "essa wiki foi impedido de ser adicionado como um webhook de mudanças recentes!",
         "blocked_reason": "essa wiki foi bloqueada de ser adicionado como um webhook de mudanças recentes para `$1`!",
         "channel": "Canal:",
@@ -750,6 +749,8 @@
         "disabled": "desativado",
         "editcount": "Contagem de edições:",
         "enabled": "ativado",
+        "flag_logall": "Registro de verificações malsucedidas:",
+        "flag_private": "Envio de respostas de comando de verificação em particular:",
         "indays": "(em dias)",
         "logging": "Canal de registro:",
         "match": "Aviso de requisitos ausentes:",
@@ -769,11 +770,13 @@
         "posteditcount": "Edição e contagem de postagens combinadas:",
         "rename": "Mudar apelido:",
         "rename_no_permission": "**$1 não tem a permissão de `Gerenciar apelidos` para substituir o apelido pelos nomes de usuário da wiki!**",
-        "role": "Cargo:",
+        "role_add": "Cargo para adicionar:",
         "role_deleted": "**O cargo $1 parece não existir mais! **",
         "role_managed": "o cargo fornecido não pode ser atribuído.",
         "role_max": "muitos cargos fornecidos.",
         "role_missing": "o cargo fornecido não existe.",
+        "role_none": "nenhum",
+        "role_remove": "Cargo para remover:",
         "role_too_high": "**O cargo $1 é muito alto para o $2 atribuí-lo!**",
         "save_failed": "infelizmente a verificação não pôde ser salva, tente novamente mais tarde.",
         "success": "Aviso de sucesso:",
@@ -808,8 +811,10 @@
         "oauth_message_dm": "Por favor, use este link para autenticar sua conta de wiki para $1.",
         "oauth_private": "a wiki usa OAuth2 para verificação. Ative as mensagens diretas deste servidor ou use o comando `/verify` para que eu possa enviar um link de autenticação de forma privada.",
         "oauth_used": "*Verificado usando OAuth2 *",
-        "qualified": "Qualificado para o cargo:",
-        "qualified_error": "Não foi possível adicionar, mas está qualificado para o cargo:",
+        "qualified_add": "Adicionar para:",
+        "qualified_add_error": "Não pode ser adicionado a:",
+        "qualified_remove": "Removido de:",
+        "qualified_remove_error": "Não pode ser removido de:",
         "user_blocked": "**{{GENDER:$2|O usuário|A usuária}} $1 está {{GENDER:$2|bloqueado|bloqueada}} na wiki!**",
         "user_blocked_reply": "Seu usuário vinculado da wiki **\"$1\" está bloqueado!**",
         "user_disabled": "**A conta $1 está desabilitado na wiki!**",

+ 14 - 4
i18n/ru.json

@@ -494,7 +494,8 @@
         "all_inactive": "нельзя одновременно отключить отслеживание и правок на вики, и изменений в лентах.",
         "audit_reason": "Вебхук свежих правок для \"$1\"",
         "audit_reason_delete": "Вебхук свежих правок удалён",
-        "audit_reason_move": "Вебхук свежих правок перемещён",
+        "audit_reason_edit": "Вебхук свежих правок обновлён",
+        "avatar": "Аватар вебхука:",
         "blocked": "эта вики была блокирована от добавления к вебхуку свежих правок!",
         "blocked_reason": "эта вики была блокирована от добавления к вебхуку свежих правок по причине: `$1`!",
         "channel": "Канал:",
@@ -528,6 +529,7 @@
         "lang": "Язык:",
         "max_entries": "вы уже достигли максимального количества вебхуков свежих правок.",
         "missing": "на этом сервере ещё нет вебхуков свежих правок.",
+        "name": "Имя вебхука:",
         "new_lang": "<новый язык>",
         "new_wiki": "<ссылка на вики>",
         "no_feeds": "на вики для этого вебхука не поддерживаются или не включены функции на основе лент, такие как обсуждения, стены обсуждения и комментарии к статьям.",
@@ -545,6 +547,7 @@
             "blocked_reason": "Этот вебхук свежих правок будет удалён, потому что эта вики была блокирована от добавления к вебхуку свежих правок по причине: `$1`!",
             "created": "Вебхук свежих правок для $1 был создан в этом канале.",
             "dashboard": {
+                "avatar": "• Аватар вебхука был изменён.",
                 "channel": "• Вебхук был перемещён в этот канал.",
                 "disabled_feeds": "• Отслеживание изменений в лентах, таких как обсуждения, стены обсуждения и комментарии к статьям, было отключено.",
                 "disabled_rc": "• Отслеживание правок на вики было отключено.",
@@ -555,6 +558,7 @@
                 "enabled_feeds": "• Отслеживание изменений в лентах, таких как обсуждения, стены обсуждения и комментарии к статьям, было включено.",
                 "enabled_rc": "• Отслеживание правок на вики было включено.",
                 "lang": "• Язык был изменён на $1.",
+                "name": "• Имя вебхука было изменено на \"$1\".",
                 "updated": "Этот вебхук свежих правок был изменён:",
                 "wiki": "• Вики была изменена на $1."
             },
@@ -750,6 +754,8 @@
         "disabled": "отключено",
         "editcount": "Количество правок:",
         "enabled": "включено",
+        "flag_logall": "Запись неуспешных попыток верификации в журнал:",
+        "flag_private": "Отвечать на запросы верификации приватно:",
         "indays": "(в днях)",
         "logging": "Канал для журнала:",
         "match": "Уведомление о несоответствии требованиям:",
@@ -769,11 +775,13 @@
         "posteditcount": "Количество правок и количество постов, вместе взятые:",
         "rename": "Изменить псевдоним:",
         "rename_no_permission": "**$1 отсутствует разрешение `Управлять никнеймами` для принудительного использования имён участников вики!**",
-        "role": "Роль:",
+        "role_add": "Добавить роль:",
         "role_deleted": "**Роль $1 больше не существует!**",
         "role_managed": "данная роль не может быть назначена.",
         "role_max": "Вы достигли максимального кол-ва ролей.",
         "role_missing": "такой роли не существует.",
+        "role_none": "нет",
+        "role_remove": "Убрать роль:",
         "role_too_high": "**Роль $1 слишком высока $2 для назначения!**",
         "save_failed": "к сожалению, проверка не сохранена, повторите попытку позже.",
         "success": "Уведомление об успехе:",
@@ -808,8 +816,10 @@
         "oauth_message_dm": "Пройдите по этой ссылке, чтобы авторизовать свой вики-аккаунт для сервера $1.",
         "oauth_private": "данная вики использует OAuth2 для верификации. Разрешите личные сообщения от участников сервера или используйте команду `/verify`, чтобы я смог приватно отправить вам ссылку для верификации.",
         "oauth_used": "*Верифицирован с помощью OAuth2*",
-        "qualified": "Оценен для:",
-        "qualified_error": "Квалифицирован на, но не может быть добавлен:",
+        "qualified_add": "Добавлено:",
+        "qualified_add_error": "Не может быть добавлено:",
+        "qualified_remove": "Убрано:",
+        "qualified_remove_error": "Не может быть убрано:",
         "user_blocked": "**Wiki-пользователь $1 заблокирован.**",
         "user_blocked_reply": "Ваш связанный wiki-пользователь **\"$1\" заблокирован!**",
         "user_disabled": "**Wiki-пользователь $1 отключен!**",

+ 15 - 4
i18n/sv.json

@@ -487,7 +487,6 @@
         "added": "En \"senaste ändringarna\"-webhook har nu blivit tillagt för:",
         "audit_reason": "\"Senaste ändringarna\"-webhook för \"$1\"",
         "audit_reason_delete": "Tog bort \"senaste ändringarna\"-webhooken",
-        "audit_reason_move": "Flyttade \"senaste ändringarna\"-webhooken",
         "blocked": "denna wiki har blockerats från att lägga till en \"senaste ändringarna\"-webhook!",
         "blocked_reason": "denna wiki har blockerats från att lägga till en \"senaste ändringarna\"-webhook på grund av`$1`!",
         "channel": "Kanal:",
@@ -739,8 +738,11 @@
         "disabled": "inaktiverad",
         "editcount": "Antal redigeringar:",
         "enabled": "aktiverad",
+        "flag_logall": "Loggning av misslyckade verifieringar:",
+        "flag_private": "Privat svar vid användning av verifieringskommandot:",
         "indays": "(i dagar)",
         "logging": "Logg kanal:",
+        "match": "Meddelande om krav som saknas:",
         "max_entries": "du har redan nått den högsta gränsen för antal verifikationer.",
         "missing": "det finns ännu inte några verifikationer för den här servern.",
         "new_accountage": "",
@@ -757,11 +759,13 @@
         "posteditcount": "Antal ändringar och inlägg tillsammans:",
         "rename": "Ändra smeknamn:",
         "rename_no_permission": "**$1 saker behörigheten `Ändra Smeknamn`, detta för att kunna tvinga wiki användarnamn!**",
-        "role": "Roll:",
+        "role_add": "Roll att lägga till:",
         "role_deleted": "**Rollen $1 ser inte ut att existera längre!**",
         "role_managed": "den angivna rollen kan inte tilldelas.",
         "role_max": "du angav för många roller.",
         "role_missing": "den angivna rollen existerar inte.",
+        "role_none": "ingen",
+        "role_remove": "Roll att ta bort:",
         "role_too_high": "**Rolle $1 är för hög för att $2 ska kunna tilldela den!**",
         "save_failed": "verifikationen kunde tyvärr inte sparas, vänligen försök igen senare.",
         "updated": "verifikationen har uppdaterats:",
@@ -789,8 +793,15 @@
         "help_subpage": "Lägg till din Discord-tagg ($1) på din Discord-undersida på wikin:",
         "missing": "det finns inte någon verifikation uppsatt för den här kanalen.",
         "notice": "Notera:",
-        "qualified": "Kvalifierad för:",
-        "qualified_error": "Kvalificerad för, men kan inte lägga till:",
+        "oauth_button": "Autentisera",
+        "oauth_message": "Använd [denna länk]($1) för att autentisera ditt wiki-konto.",
+        "oauth_message_dm": "Använd den här länken för att autentisera ditt wiki-konto för $1.",
+        "oauth_private": "wikinuses OAuth2 för verifiering. Aktivera direktmeddelanden från den här servern eller använd `/verify`-kommandot så att jag kan skicka en autentiseringslänk privat.",
+        "oauth_used": "* Verifierad med OAuth2 *",
+        "qualified_add": "Tillagd i:",
+        "qualified_add_error": "Kan inte läggas till i:",
+        "qualified_remove": "Borttagen från:",
+        "qualified_remove_error": "Kan inte tas bort från:",
         "user_blocked": "**Wiki-användaren $1 är blockerad!**",
         "user_blocked_reply": "din länkade Wiki-användare **\"$1\" är blockerad!**",
         "user_disabled": "**Wiki-kontot $1 är inaktiverat!**",

+ 0 - 4
i18n/tr.json

@@ -484,7 +484,6 @@
         "all_inactive": "viki değişiklikleri ve yayın bazlı değişiklikleri aynı anda devre dışı bırakamazsın.",
         "audit_reason": "\"$1\" için son değişiklikler webhook'u",
         "audit_reason_delete": "Son değişiklikler webhook'u kaldırıldı",
-        "audit_reason_move": "Son değişiklikler webhook'u taşındı",
         "blocked": "bu vikinin son değişiklikler webhook'u olarak eklemesi yasaklanmış!",
         "blocked_reason": "bu vikinin son değişiklikler webhook'u olarak eklemesi `$1` yüzünden yasaklanmış!",
         "channel": "Kanal:",
@@ -739,7 +738,6 @@
         "posteditcount": "Düzenleme ve paylaşım sayısı toplamı:",
         "rename": "Kullanıcı adı değiştir:",
         "rename_no_permission": "**$1, viki kullanıcı isimlerini zorlamak için `Kullanıcı Adlarını Yönet`iznine sahip olmalı!**",
-        "role": "Rol:",
         "role_deleted": "**$1 rolü artık yok gibi duruyor!**",
         "role_managed": "belirtilen rol atanamıyor.",
         "role_max": "çok fazla rol belirttin.",
@@ -769,8 +767,6 @@
         "help_subpage": "Lütfen vikideki Discord alt sayfana Discord etiketini ($1) ekle:",
         "missing": "kanalda halihazırda bir doğrulama yok.",
         "notice": "Uyarı:",
-        "qualified": "Nitelikli:",
-        "qualified_error": "Nitelikli ancak eklenemedi:",
         "user_blocked": "**Viki kullanıcısı $1 engellendi!**",
         "user_blocked_reply": "bağlı olduğun viki kullanıcısı **\"$1\" engellendi!**",
         "user_disabled": "**Viki hesabı $1 devre dışı!**",

BIN
i18n/widgets/bn.png


BIN
i18n/widgets/de.png


BIN
i18n/widgets/en.png


BIN
i18n/widgets/es.png


BIN
i18n/widgets/fr.png


BIN
i18n/widgets/hi.png


BIN
i18n/widgets/it.png


BIN
i18n/widgets/ja.png


BIN
i18n/widgets/ko.png


BIN
i18n/widgets/nl.png


BIN
i18n/widgets/pl.png


BIN
i18n/widgets/pt-br.png


BIN
i18n/widgets/ru.png


BIN
i18n/widgets/sv.png


BIN
i18n/widgets/th.png


BIN
i18n/widgets/tr.png


BIN
i18n/widgets/uk.png


BIN
i18n/widgets/vi.png


BIN
i18n/widgets/zh-hans.png


BIN
i18n/widgets/zh-hant.png


+ 12 - 4
i18n/zh-hans.json

@@ -494,7 +494,8 @@
         "all_inactive": "Wiki 式更改和推送式更改无法同时禁用。",
         "audit_reason": "“$1”的最近更改 webhook",
         "audit_reason_delete": "最近更改 webhook 已删除",
-        "audit_reason_move": "已移动最近更改 webhook",
+        "audit_reason_edit": "最近更改 webhook 已更新",
+        "avatar": "Webhook 头像:",
         "blocked": "此 wiki 已经被禁止添加到最近更改 webhook !",
         "blocked_reason": "此 wiki 由于 `$1` 已經被禁止添加到最近更改 webhook!",
         "channel": "频道:",
@@ -528,6 +529,7 @@
         "lang": "语言:",
         "max_entries": "已到达最近更改 webhook 的最大数量。",
         "missing": "本服务器内暂无最近更改 webhook。",
+        "name": "Webhook 名称:",
         "new_lang": "<新语言>",
         "new_wiki": "<至 wiki 的链接>",
         "no_feeds": "此 webhook 的 wiki 没有如讨论版、留言墙和条目评论等推送式的更改,已启用。",
@@ -545,6 +547,7 @@
             "blocked_reason": "由于此 wiki 已因 `$1` 被封禁,此最近更改 webhook 将被删除!",
             "created": "$1 的最近更改 webhook 已被添加到此频道。",
             "dashboard": {
+                "avatar": "• Webhook 的头像已被更改。",
                 "channel": "• Webhook 已被移动到此频道。",
                 "disabled_feeds": "• 已禁用讨论版、留言墙和条目评论等推送式的更改推送。",
                 "disabled_rc": "• 已禁用wiki式更改。",
@@ -555,6 +558,7 @@
                 "enabled_feeds": "• 已启用讨论版、留言墙和条目评论等推送式的更改推送。",
                 "enabled_rc": "• 已启用wiki式更改。",
                 "lang": "• 语言已被更改为 $1。",
+                "name": "• Webhook 的名称已被更改为“$1”。",
                 "updated": "此最近更改 webhook 已被更新:",
                 "wiki": "• wiki 已被更改为 $1。"
             },
@@ -771,11 +775,13 @@
         "posteditcount": "编辑数和帖子数的总和:",
         "rename": "修改昵称:",
         "rename_no_permission": "**$1 缺少 `管理昵称` 权限来强制将暱称改为 wiki 用户名!**",
-        "role": "身份组:",
+        "role_add": "添加的身份组:",
         "role_deleted": "**身份组 $1 似乎不再存在!**",
         "role_managed": "提供的身份组权重过高,无法添加。",
         "role_max": "提供的身份组过多。",
         "role_missing": "提供的身份组不存在。",
+        "role_none": "无",
+        "role_remove": "移除的身份组:",
         "role_too_high": "**$1 身份组的权重过高,$2 无法添加!**",
         "save_failed": "抱歉,无法保存验证方式,请稍后再试。",
         "success": "成功提示:",
@@ -810,8 +816,10 @@
         "oauth_message_dm": "请使用此链接为$1验证您的wiki账号。",
         "oauth_private": "此wiki使用OAuth2进行验证。请开启来自此服务器的私信或使用`/verify`命令以允许我向您私信验证链接。",
         "oauth_used": "*已通过OAuth2验证*",
-        "qualified": "符合:",
-        "qualified_error": "符合,但无法添加:",
+        "qualified_add": "添加:",
+        "qualified_add_error": "无法添加:",
+        "qualified_remove": "移除:",
+        "qualified_remove_error": "无法移除:",
         "user_blocked": "**Wiki 用户 $1 已被封禁!**",
         "user_blocked_reply": "您连接的 wiki 用户**“$1”已被封禁!**",
         "user_disabled": "**Wiki 用户 $1 已被禁用!**",

+ 12 - 4
i18n/zh-hant.json

@@ -494,7 +494,8 @@
         "all_inactive": "wiki變更和討論版變更無法同時停用。",
         "audit_reason": "“$1”的近期變更webhook",
         "audit_reason_delete": "已移除近期變更webhook",
-        "audit_reason_move": "已移動近期變更webhook",
+        "audit_reason_edit": "近期變更 webhook 已更新",
+        "avatar": "Webhook 頭像:",
         "blocked": "此 wiki 已經被禁止加入到近期變更webhook !",
         "blocked_reason": "此 wiki 已經被禁止加入到 `$1` 近期變更 webhook !",
         "channel": "頻道:",
@@ -528,6 +529,7 @@
         "lang": "語言:",
         "max_entries": "已到達近期變更webhook的最大數量。",
         "missing": "本伺服器內暫無近期變更webhook。",
+        "name": "Webhook 名稱:",
         "new_lang": "<新語言>",
         "new_wiki": "<連結>",
         "no_feeds": "此webhook的wiki沒有如討論版、留言牆和條目評論等基於推送頁的變更,已啟用。",
@@ -545,6 +547,7 @@
             "blocked_reason": "由於此wiki已因 `$1` 被封鎖,此近期變更webhook將被刪除!",
             "created": "$1 的近期變更webhook已新增至此頻道。",
             "dashboard": {
+                "avatar": "• 已變更webhook的頭像。",
                 "channel": "• 已將webhook至此頻道。",
                 "disabled_feeds": "• 已停用討論版、留言牆和條目評論等基於推送頁的變更推送。",
                 "disabled_rc": "• 已停用wiki變更。",
@@ -555,6 +558,7 @@
                 "enabled_feeds": "• 已啟用討論版、留言牆和條目評論等基於推送頁的變更推送。",
                 "enabled_rc": "• 已啟用wiki變更。",
                 "lang": "• 已將語言變更為 $1。",
+                "name": "• 已將webhook的名稱變更為“$1”。",
                 "updated": "此近期變更webhook已被變更:",
                 "wiki": "• wiki已被變更為 $1。"
             },
@@ -771,11 +775,13 @@
         "posteditcount": "編輯次數與貼文數的總和:",
         "rename": "修改暱稱:",
         "rename_no_permission": "**$1 缺少`管理暱稱`權限以強制將暱稱變更為wiki使用者名稱!**",
-        "role": "身分組:",
+        "role_add": "新增的身分組:",
         "role_deleted": "**身分組 $1 似乎不再存在!**",
         "role_managed": "提供的身分組權重過高,無法添加。",
         "role_max": "提供的身分組過多。",
         "role_missing": "提供的身分組不存在。",
+        "role_none": "無",
+        "role_remove": "移除的身分組:",
         "role_too_high": "**$1 身分組的權重過高,$2 無法添加!**",
         "save_failed": "抱歉,無法保存驗證方式,請稍後再試。",
         "success": "成功提示:",
@@ -810,8 +816,10 @@
         "oauth_message_dm": "請使用此連結為$1驗證您的帳號。",
         "oauth_private": "此wiki使用OAuth2進行驗證。請開啟來自此伺服器的私訊或是使用`/verify`指令以允許我向您傳送驗證連結。",
         "oauth_used": "*已透過OAuth2驗證*",
-        "qualified": "符合:",
-        "qualified_error": "符合,但無法添加:",
+        "qualified_add": "新增:",
+        "qualified_add_error": "無法新增:",
+        "qualified_remove": "移除:",
+        "qualified_remove_error": "無法移除:",
         "user_blocked": "**Wiki 使用者 $1 已被封鎖!**",
         "user_blocked_reply": "您連接的 wiki 使用者**“$1”已被封鎖!**",
         "user_disabled": "**Wiki 使用者 $1 已被停用!**",

+ 35 - 33
main.js

@@ -102,7 +102,7 @@ if ( process.env.dashboard ) {
 						${JSON.stringify(message.data.guilds)}.map( id => {
 							if ( this.guilds.cache.has(id) ) {
 								let guild = this.guilds.cache.get(id);
-								return guild.members.fetch('${message.data.member}').then( member => {
+								return guild.members.fetch(${JSON.stringify(message.data.member)}).then( member => {
 									return {
 										patreon: global.patreons.hasOwnProperty(guild.id),
 										memberCount: guild.memberCount,
@@ -157,17 +157,17 @@ if ( process.env.dashboard ) {
 					} );
 					break;
 				case 'getMember':
-					return manager.broadcastEval(`if ( this.guilds.cache.has('${message.data.guild}') ) {
-						let guild = this.guilds.cache.get('${message.data.guild}');
-						guild.members.fetch('${message.data.member}').then( member => {
+					return manager.broadcastEval(`if ( this.guilds.cache.has(${JSON.stringify(message.data.guild)}) ) {
+						let guild = this.guilds.cache.get(${JSON.stringify(message.data.guild)});
+						guild.members.fetch(${JSON.stringify(message.data.member)}).then( member => {
 							var response = {
 								patreon: global.patreons.hasOwnProperty(guild.id),
 								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.patreon && ${message.data.allowCategory} && channel?.type === 'category' ) ) {
+							if ( ${JSON.stringify(message.data.channel)} ) {
+								let channel = guild.channels.cache.get(${JSON.stringify(message.data.channel)});
+								if ( channel?.isText() || ( response.patreon && ${JSON.stringify(message.data.allowCategory)} && channel?.type === 'category' ) ) {
 									response.userPermissions = channel.permissionsFor(member).bitfield;
 									response.botPermissions = channel.permissionsFor(guild.me).bitfield;
 									response.isCategory = ( channel.type === 'category' );
@@ -175,8 +175,8 @@ if ( process.env.dashboard ) {
 								}
 								else response.message = 'noChannel';
 							}
-							if ( '${( message.data.newchannel || '' )}' ) {
-								let newchannel = guild.channels.cache.get('${message.data.newchannel}');
+							if ( ${JSON.stringify(message.data.newchannel)} ) {
+								let newchannel = guild.channels.cache.get(${JSON.stringify(message.data.newchannel)});
 								if ( newchannel?.isText() ) {
 									response.userPermissionsNew = newchannel.permissionsFor(member).bitfield;
 									response.botPermissionsNew = newchannel.permissionsFor(guild.me).bitfield;
@@ -196,15 +196,15 @@ if ( process.env.dashboard ) {
 					} );
 					break;
 				case 'notifyGuild':
-					return manager.broadcastEval(`if ( '${( message.data.prefix || '' )}' ) {
-						global.patreons['${message.data.guild}'] = '${message.data.prefix}';
+					return manager.broadcastEval(`if ( ${JSON.stringify(message.data.prefix)} ) {
+						global.patreons[${JSON.stringify(message.data.guild)}] = ${JSON.stringify(message.data.prefix)};
 					}
-					if ( '${( message.data.voice || '' )}' && global.voice.hasOwnProperty('${message.data.guild}') ) {
-						global.voice['${message.data.guild}'] = '${message.data.voice}';
+					if ( ${JSON.stringify(message.data.voice)} && global.voice.hasOwnProperty(${JSON.stringify(message.data.guild)}) ) {
+						global.voice[${JSON.stringify(message.data.guild)}] = ${JSON.stringify(message.data.voice)};
 					}
-					if ( this.guilds.cache.has('${message.data.guild}') ) {
-						let channel = this.guilds.cache.get('${message.data.guild}').publicUpdatesChannel;
-						if ( channel ) channel.send( \`${message.data.text.replace( /`/g, '\\`' )}\`, {
+					if ( this.guilds.cache.has(${JSON.stringify(message.data.guild)}) ) {
+						let channel = this.guilds.cache.get(${JSON.stringify(message.data.guild)}).publicUpdatesChannel;
+						if ( channel ) channel.send( ${JSON.stringify(message.data.text)}, {
 							embed: ${JSON.stringify(message.data.embed)},
 							files: ${JSON.stringify(message.data.file)},
 							allowedMentions: {parse: []}, split: true
@@ -216,14 +216,14 @@ if ( process.env.dashboard ) {
 					} );
 					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, '\\`' )}\`
+					return manager.broadcastEval(`if ( this.guilds.cache.has(${JSON.stringify(message.data.guild)}) ) {
+						let channel = this.guilds.cache.get(${JSON.stringify(message.data.guild)}).channels.cache.get(${JSON.stringify(message.data.channel)});
+						if ( channel ) channel.createWebhook( ${JSON.stringify(message.data.name)}, {
+							avatar: ( ${JSON.stringify(message.data.avatar)} || this.user.displayAvatarURL({format:'png',size:4096}) ),
+							reason: ${JSON.stringify(message.data.reason)}
 						} ).then( webhook => {
 							console.log( '- Dashboard: Webhook successfully created: ${message.data.guild}#${message.data.channel}' );
-							webhook.send( \`${message.data.text.replace( /`/g, '\\`' )}\` ).catch(log_error);
+							webhook.send( ${JSON.stringify(message.data.text)} ).catch(log_error);
 							return webhook.id + '/' + webhook.token;
 						}, error => {
 							console.log( '- Dashboard: Error while creating the webhook: ' + error );
@@ -236,20 +236,22 @@ if ( process.env.dashboard ) {
 						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);
+				case 'editWebhook':
+					return manager.broadcastEval(`if ( this.guilds.cache.has(${JSON.stringify(message.data.guild)}) ) {
+						this.fetchWebhook(...${JSON.stringify(message.data.webhook.split('/'))}).then( webhook => {
+							var changes = {};
+							if ( ${JSON.stringify(message.data.channel)} ) changes.channel = ${JSON.stringify(message.data.channel)};
+							if ( ${JSON.stringify(message.data.name)} ) changes.name = ${JSON.stringify(message.data.name)};
+							if ( ${JSON.stringify(message.data.avatar)} ) changes.avatar = ${JSON.stringify(message.data.avatar)};
+							return webhook.edit( changes, ${JSON.stringify(message.data.reason)} ).then( newwebhook => {
+								console.log( '- Dashboard: Webhook successfully edited: ${message.data.guild}#' + ( ${JSON.stringify(message.data.channel)} || webhook.channelID ) );
+								webhook.send( ${JSON.stringify(message.data.text)} ).catch(log_error);
 								return true;
 							}, error => {
-								console.log( '- Dashboard: Error while moving the webhook: ' + error );
+								console.log( '- Dashboard: Error while editing the webhook: ' + error );
 							} );
 						}, error => {
-							console.log( '- Dashboard: Error while moving the webhook: ' + error );
+							console.log( '- Dashboard: Error while editing the webhook: ' + error );
 						} );
 					}`, shardIDForGuildID(message.data.guild, manager.totalShards)).then( result => {
 						data.response = result;
@@ -260,7 +262,7 @@ if ( process.env.dashboard ) {
 					} );
 					break;
 				case 'verifyUser':
-					return manager.broadcastEval(`global.verifyOauthUser('${message.data.state}', '${message.data.access_token}')`, message.data.state.split(' ')[1][0]).catch( error => {
+					return manager.broadcastEval(`global.verifyOauthUser(${JSON.stringify(message.data.state)}, ${JSON.stringify(message.data.access_token)})`, message.data.state.split(' ')[1][0]).catch( error => {
 						data.error = error.toString();
 					} ).finally( () => {
 						return dashboard.send( {id: message.id, data} );

+ 201 - 186
package-lock.json

@@ -8,15 +8,15 @@
       "version": "4.0.0",
       "license": "ISC",
       "dependencies": {
-        "cheerio": "^1.0.0-rc.6",
+        "cheerio": "^1.0.0-rc.10",
         "datetime-difference": "^1.0.2",
         "discord-oauth2": "^2.6.0",
-        "discord.js": "^12.5.1",
+        "discord.js": "^12.5.3",
         "dotenv": "^10.0.0",
-        "full-icu": "^1.3.1",
-        "got": "^11.8.1",
+        "full-icu": "^1.3.4",
+        "got": "^11.8.2",
         "htmlparser2": "^6.1.0",
-        "npm": "^7.11.2",
+        "npm": "^7.17.0",
         "pg": "^8.6.0"
       },
       "engines": {
@@ -88,9 +88,9 @@
       }
     },
     "node_modules/@types/node": {
-      "version": "15.6.1",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-15.6.1.tgz",
-      "integrity": "sha512-7EIraBEyRHEe7CH+Fm1XvgqU6uwZN8Q7jppJGcqjROMT29qhAuuOxYB1uEY5UMYQKEmA5D+5tBnhdaPXSsLONA=="
+      "version": "15.12.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
+      "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww=="
     },
     "node_modules/@types/responselike": {
       "version": "1.0.0",
@@ -138,16 +138,16 @@
       }
     },
     "node_modules/cacheable-request": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz",
-      "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==",
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
+      "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
       "dependencies": {
         "clone-response": "^1.0.2",
         "get-stream": "^5.1.0",
         "http-cache-semantics": "^4.0.0",
         "keyv": "^4.0.0",
         "lowercase-keys": "^2.0.0",
-        "normalize-url": "^4.1.0",
+        "normalize-url": "^6.0.1",
         "responselike": "^2.0.0"
       },
       "engines": {
@@ -155,12 +155,12 @@
       }
     },
     "node_modules/cheerio": {
-      "version": "1.0.0-rc.9",
-      "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.9.tgz",
-      "integrity": "sha512-QF6XVdrLONO6DXRF5iaolY+odmhj2CLj+xzNod7INPWMi/x9X4SOylH0S/vaPpX+AUU6t04s34SQNh7DbkuCng==",
+      "version": "1.0.0-rc.10",
+      "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz",
+      "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==",
       "dependencies": {
-        "cheerio-select": "^1.4.0",
-        "dom-serializer": "^1.3.1",
+        "cheerio-select": "^1.5.0",
+        "dom-serializer": "^1.3.2",
         "domhandler": "^4.2.0",
         "htmlparser2": "^6.1.0",
         "parse5": "^6.0.1",
@@ -175,15 +175,15 @@
       }
     },
     "node_modules/cheerio-select": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.4.0.tgz",
-      "integrity": "sha512-sobR3Yqz27L553Qa7cK6rtJlMDbiKPdNywtR95Sj/YgfpLfy0u6CGJuaBKe5YE/vTc23SCRKxWSdlon/w6I/Ew==",
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz",
+      "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==",
       "dependencies": {
-        "css-select": "^4.1.2",
-        "css-what": "^5.0.0",
+        "css-select": "^4.1.3",
+        "css-what": "^5.0.1",
         "domelementtype": "^2.2.0",
         "domhandler": "^4.2.0",
-        "domutils": "^2.6.0"
+        "domutils": "^2.7.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/fb55"
@@ -209,9 +209,9 @@
       }
     },
     "node_modules/css-select": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.2.tgz",
-      "integrity": "sha512-nu5ye2Hg/4ISq4XqdLY2bEatAcLIdt3OYGFc9Tm9n7VSlFBcfRv0gBNksHRgSdUDQGtN3XrZ94ztW+NfzkFSUw==",
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz",
+      "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==",
       "dependencies": {
         "boolbase": "^1.0.0",
         "css-what": "^5.0.0",
@@ -342,9 +342,9 @@
       }
     },
     "node_modules/domutils": {
-      "version": "2.6.0",
-      "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.6.0.tgz",
-      "integrity": "sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA==",
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz",
+      "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==",
       "dependencies": {
         "dom-serializer": "^1.0.1",
         "domelementtype": "^2.2.0",
@@ -495,19 +495,19 @@
       }
     },
     "node_modules/mime-db": {
-      "version": "1.47.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz",
-      "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==",
+      "version": "1.48.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz",
+      "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==",
       "engines": {
         "node": ">= 0.6"
       }
     },
     "node_modules/mime-types": {
-      "version": "2.1.30",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz",
-      "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==",
+      "version": "2.1.31",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz",
+      "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==",
       "dependencies": {
-        "mime-db": "1.47.0"
+        "mime-db": "1.48.0"
       },
       "engines": {
         "node": ">= 0.6"
@@ -530,17 +530,20 @@
       }
     },
     "node_modules/normalize-url": {
-      "version": "4.5.1",
-      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
-      "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.0.1.tgz",
+      "integrity": "sha512-VU4pzAuh7Kip71XEmO9aNREYAdMHFGTVj/i+CaTImS8x0i1d3jUZkXhqluy/PRgjPLMgsLQulYY3PJ/aSbSjpQ==",
       "engines": {
-        "node": ">=8"
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/npm": {
-      "version": "7.15.0",
-      "resolved": "https://registry.npmjs.org/npm/-/npm-7.15.0.tgz",
-      "integrity": "sha512-GIXNqy3obii54oPF0gbcBNq4aYuB/Ovuu/uYp1eS4nij2PEDMnoOh6RoSv2MDvAaB4a+JbpX/tjDxLO7JAADgQ==",
+      "version": "7.17.0",
+      "resolved": "https://registry.npmjs.org/npm/-/npm-7.17.0.tgz",
+      "integrity": "sha512-yNzj4vQellvUGiBM/SzhfT89EV0vb7iILjTehSydTY/IgK2Vjk7/7J8WNJ2ysqcgfLY21ptI/j7uknt15IbbKQ==",
       "bundleDependencies": [
         "@npmcli/arborist",
         "@npmcli/ci-detect",
@@ -611,7 +614,7 @@
         "write-file-atomic"
       ],
       "dependencies": {
-        "@npmcli/arborist": "^2.6.0",
+        "@npmcli/arborist": "^2.6.1",
         "@npmcli/ci-detect": "^1.2.0",
         "@npmcli/config": "^2.2.0",
         "@npmcli/run-script": "^1.8.5",
@@ -636,7 +639,7 @@
         "leven": "^3.1.0",
         "libnpmaccess": "^4.0.2",
         "libnpmdiff": "^2.0.4",
-        "libnpmexec": "^1.1.1",
+        "libnpmexec": "^1.2.0",
         "libnpmfund": "^1.1.0",
         "libnpmhook": "^6.0.2",
         "libnpmorg": "^2.0.2",
@@ -645,7 +648,7 @@
         "libnpmsearch": "^3.1.1",
         "libnpmteam": "^2.0.3",
         "libnpmversion": "^1.2.0",
-        "make-fetch-happen": "^8.0.14",
+        "make-fetch-happen": "^9.0.1",
         "minipass": "^3.1.3",
         "minipass-pipeline": "^1.2.4",
         "mkdirp": "^1.0.4",
@@ -654,10 +657,10 @@
         "node-gyp": "^7.1.2",
         "nopt": "^5.0.0",
         "npm-audit-report": "^2.1.5",
-        "npm-package-arg": "^8.1.2",
+        "npm-package-arg": "^8.1.4",
         "npm-pick-manifest": "^6.1.1",
         "npm-profile": "^5.0.3",
-        "npm-registry-fetch": "^10.1.2",
+        "npm-registry-fetch": "^11.0.0",
         "npm-user-validate": "^1.0.1",
         "npmlog": "~4.1.2",
         "opener": "^1.5.2",
@@ -688,7 +691,7 @@
       }
     },
     "node_modules/npm/node_modules/@npmcli/arborist": {
-      "version": "2.6.0",
+      "version": "2.6.2",
       "inBundle": true,
       "license": "ISC",
       "dependencies": {
@@ -708,7 +711,7 @@
         "npm-install-checks": "^4.0.0",
         "npm-package-arg": "^8.1.0",
         "npm-pick-manifest": "^6.1.0",
-        "npm-registry-fetch": "^10.0.0",
+        "npm-registry-fetch": "^11.0.0",
         "pacote": "^11.2.6",
         "parse-conflict-json": "^1.1.1",
         "promise-all-reject-late": "^1.0.0",
@@ -1453,19 +1456,6 @@
         "node": "*"
       }
     },
-    "node_modules/npm/node_modules/form-data": {
-      "version": "2.3.3",
-      "inBundle": true,
-      "license": "MIT",
-      "dependencies": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.6",
-        "mime-types": "^2.1.12"
-      },
-      "engines": {
-        "node": ">= 0.12"
-      }
-    },
     "node_modules/npm/node_modules/fs-minipass": {
       "version": "2.1.0",
       "inBundle": true,
@@ -1671,7 +1661,7 @@
       }
     },
     "node_modules/npm/node_modules/iconv-lite": {
-      "version": "0.6.2",
+      "version": "0.6.3",
       "inBundle": true,
       "license": "MIT",
       "optional": true,
@@ -1892,14 +1882,14 @@
       }
     },
     "node_modules/npm/node_modules/libnpmaccess": {
-      "version": "4.0.2",
+      "version": "4.0.3",
       "inBundle": true,
       "license": "ISC",
       "dependencies": {
         "aproba": "^2.0.0",
         "minipass": "^3.1.1",
         "npm-package-arg": "^8.1.2",
-        "npm-registry-fetch": "^10.0.0"
+        "npm-registry-fetch": "^11.0.0"
       },
       "engines": {
         "node": ">=10"
@@ -1924,7 +1914,7 @@
       }
     },
     "node_modules/npm/node_modules/libnpmexec": {
-      "version": "1.1.1",
+      "version": "1.2.0",
       "inBundle": true,
       "license": "ISC",
       "dependencies": {
@@ -1953,24 +1943,24 @@
       }
     },
     "node_modules/npm/node_modules/libnpmhook": {
-      "version": "6.0.2",
+      "version": "6.0.3",
       "inBundle": true,
       "license": "ISC",
       "dependencies": {
         "aproba": "^2.0.0",
-        "npm-registry-fetch": "^10.0.0"
+        "npm-registry-fetch": "^11.0.0"
       },
       "engines": {
         "node": ">=10"
       }
     },
     "node_modules/npm/node_modules/libnpmorg": {
-      "version": "2.0.2",
+      "version": "2.0.3",
       "inBundle": true,
       "license": "ISC",
       "dependencies": {
         "aproba": "^2.0.0",
-        "npm-registry-fetch": "^10.0.0"
+        "npm-registry-fetch": "^11.0.0"
       },
       "engines": {
         "node": ">=10"
@@ -1990,13 +1980,13 @@
       }
     },
     "node_modules/npm/node_modules/libnpmpublish": {
-      "version": "4.0.1",
+      "version": "4.0.2",
       "inBundle": true,
       "license": "ISC",
       "dependencies": {
         "normalize-package-data": "^3.0.2",
         "npm-package-arg": "^8.1.2",
-        "npm-registry-fetch": "^10.0.0",
+        "npm-registry-fetch": "^11.0.0",
         "semver": "^7.1.3",
         "ssri": "^8.0.1"
       },
@@ -2005,23 +1995,23 @@
       }
     },
     "node_modules/npm/node_modules/libnpmsearch": {
-      "version": "3.1.1",
+      "version": "3.1.2",
       "inBundle": true,
       "license": "ISC",
       "dependencies": {
-        "npm-registry-fetch": "^10.0.0"
+        "npm-registry-fetch": "^11.0.0"
       },
       "engines": {
         "node": ">=10"
       }
     },
     "node_modules/npm/node_modules/libnpmteam": {
-      "version": "2.0.3",
+      "version": "2.0.4",
       "inBundle": true,
       "license": "ISC",
       "dependencies": {
         "aproba": "^2.0.0",
-        "npm-registry-fetch": "^10.0.0"
+        "npm-registry-fetch": "^11.0.0"
       },
       "engines": {
         "node": ">=10"
@@ -2051,12 +2041,12 @@
       }
     },
     "node_modules/npm/node_modules/make-fetch-happen": {
-      "version": "8.0.14",
+      "version": "9.0.2",
       "inBundle": true,
       "license": "ISC",
       "dependencies": {
         "agentkeepalive": "^4.1.3",
-        "cacache": "^15.0.5",
+        "cacache": "^15.2.0",
         "http-cache-semantics": "^4.1.0",
         "http-proxy-agent": "^4.0.1",
         "https-proxy-agent": "^5.0.0",
@@ -2067,6 +2057,7 @@
         "minipass-fetch": "^1.3.2",
         "minipass-flush": "^1.0.5",
         "minipass-pipeline": "^1.2.4",
+        "negotiator": "^0.6.2",
         "promise-retry": "^2.0.1",
         "socks-proxy-agent": "^5.0.0",
         "ssri": "^8.0.0"
@@ -2076,7 +2067,7 @@
       }
     },
     "node_modules/npm/node_modules/mime-db": {
-      "version": "1.47.0",
+      "version": "1.48.0",
       "inBundle": true,
       "license": "MIT",
       "engines": {
@@ -2084,11 +2075,11 @@
       }
     },
     "node_modules/npm/node_modules/mime-types": {
-      "version": "2.1.30",
+      "version": "2.1.31",
       "inBundle": true,
       "license": "MIT",
       "dependencies": {
-        "mime-db": "1.47.0"
+        "mime-db": "1.48.0"
       },
       "engines": {
         "node": ">= 0.6"
@@ -2231,6 +2222,14 @@
       "inBundle": true,
       "license": "ISC"
     },
+    "node_modules/npm/node_modules/negotiator": {
+      "version": "0.6.2",
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/npm/node_modules/node-gyp": {
       "version": "7.1.2",
       "inBundle": true,
@@ -2318,7 +2317,7 @@
       "license": "ISC"
     },
     "node_modules/npm/node_modules/npm-package-arg": {
-      "version": "8.1.2",
+      "version": "8.1.4",
       "inBundle": true,
       "license": "ISC",
       "dependencies": {
@@ -2359,23 +2358,22 @@
       }
     },
     "node_modules/npm/node_modules/npm-profile": {
-      "version": "5.0.3",
+      "version": "5.0.4",
       "inBundle": true,
       "license": "ISC",
       "dependencies": {
-        "npm-registry-fetch": "^10.0.0"
+        "npm-registry-fetch": "^11.0.0"
       },
       "engines": {
         "node": ">=10"
       }
     },
     "node_modules/npm/node_modules/npm-registry-fetch": {
-      "version": "10.1.2",
+      "version": "11.0.0",
       "inBundle": true,
       "license": "ISC",
       "dependencies": {
-        "lru-cache": "^6.0.0",
-        "make-fetch-happen": "^8.0.9",
+        "make-fetch-happen": "^9.0.1",
         "minipass": "^3.1.3",
         "minipass-fetch": "^1.3.0",
         "minipass-json-stream": "^1.0.1",
@@ -2457,7 +2455,7 @@
       }
     },
     "node_modules/npm/node_modules/pacote": {
-      "version": "11.3.3",
+      "version": "11.3.4",
       "inBundle": true,
       "license": "ISC",
       "dependencies": {
@@ -2474,7 +2472,7 @@
         "npm-package-arg": "^8.0.1",
         "npm-packlist": "^2.1.4",
         "npm-pick-manifest": "^6.0.0",
-        "npm-registry-fetch": "^10.0.0",
+        "npm-registry-fetch": "^11.0.0",
         "promise-retry": "^2.0.1",
         "read-package-json-fast": "^2.0.1",
         "rimraf": "^3.0.2",
@@ -2507,7 +2505,7 @@
       }
     },
     "node_modules/npm/node_modules/path-parse": {
-      "version": "1.0.6",
+      "version": "1.0.7",
       "inBundle": true,
       "license": "MIT"
     },
@@ -2692,6 +2690,19 @@
         "node": ">= 6"
       }
     },
+    "node_modules/npm/node_modules/request/node_modules/form-data": {
+      "version": "2.3.3",
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.6",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 0.12"
+      }
+    },
     "node_modules/npm/node_modules/request/node_modules/tough-cookie": {
       "version": "2.5.0",
       "inBundle": true,
@@ -2831,7 +2842,7 @@
       }
     },
     "node_modules/npm/node_modules/spdx-license-ids": {
-      "version": "3.0.7",
+      "version": "3.0.9",
       "inBundle": true,
       "license": "CC0-1.0"
     },
@@ -3264,9 +3275,9 @@
       }
     },
     "node_modules/prism-media": {
-      "version": "1.2.9",
-      "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.2.9.tgz",
-      "integrity": "sha512-UHCYuqHipbTR1ZsXr5eg4JUmHER8Ss4YEb9Azn+9zzJ7/jlTtD1h0lc4g6tNx3eMlB8Mp6bfll0LPMAV4R6r3Q==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.1.tgz",
+      "integrity": "sha512-nyYAa3KB4qteJIqdguKmwxTJgy55xxUtkJ3uRnOvO5jO+frci+9zpRXw6QZVcfDeva3S654fU9+26P2OSTzjHw==",
       "peerDependencies": {
         "@discordjs/opus": "^0.5.0",
         "ffmpeg-static": "^4.2.7 || ^3.0.0 || ^2.4.0",
@@ -3375,9 +3386,9 @@
       }
     },
     "node_modules/tslib": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
-      "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
     },
     "node_modules/tweetnacl": {
       "version": "1.0.3",
@@ -3477,9 +3488,9 @@
       }
     },
     "@types/node": {
-      "version": "15.6.1",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-15.6.1.tgz",
-      "integrity": "sha512-7EIraBEyRHEe7CH+Fm1XvgqU6uwZN8Q7jppJGcqjROMT29qhAuuOxYB1uEY5UMYQKEmA5D+5tBnhdaPXSsLONA=="
+      "version": "15.12.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
+      "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww=="
     },
     "@types/responselike": {
       "version": "1.0.0",
@@ -3518,26 +3529,26 @@
       "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="
     },
     "cacheable-request": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz",
-      "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==",
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
+      "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
       "requires": {
         "clone-response": "^1.0.2",
         "get-stream": "^5.1.0",
         "http-cache-semantics": "^4.0.0",
         "keyv": "^4.0.0",
         "lowercase-keys": "^2.0.0",
-        "normalize-url": "^4.1.0",
+        "normalize-url": "^6.0.1",
         "responselike": "^2.0.0"
       }
     },
     "cheerio": {
-      "version": "1.0.0-rc.9",
-      "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.9.tgz",
-      "integrity": "sha512-QF6XVdrLONO6DXRF5iaolY+odmhj2CLj+xzNod7INPWMi/x9X4SOylH0S/vaPpX+AUU6t04s34SQNh7DbkuCng==",
+      "version": "1.0.0-rc.10",
+      "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz",
+      "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==",
       "requires": {
-        "cheerio-select": "^1.4.0",
-        "dom-serializer": "^1.3.1",
+        "cheerio-select": "^1.5.0",
+        "dom-serializer": "^1.3.2",
         "domhandler": "^4.2.0",
         "htmlparser2": "^6.1.0",
         "parse5": "^6.0.1",
@@ -3546,15 +3557,15 @@
       }
     },
     "cheerio-select": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.4.0.tgz",
-      "integrity": "sha512-sobR3Yqz27L553Qa7cK6rtJlMDbiKPdNywtR95Sj/YgfpLfy0u6CGJuaBKe5YE/vTc23SCRKxWSdlon/w6I/Ew==",
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz",
+      "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==",
       "requires": {
-        "css-select": "^4.1.2",
-        "css-what": "^5.0.0",
+        "css-select": "^4.1.3",
+        "css-what": "^5.0.1",
         "domelementtype": "^2.2.0",
         "domhandler": "^4.2.0",
-        "domutils": "^2.6.0"
+        "domutils": "^2.7.0"
       }
     },
     "clone-response": {
@@ -3574,9 +3585,9 @@
       }
     },
     "css-select": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.2.tgz",
-      "integrity": "sha512-nu5ye2Hg/4ISq4XqdLY2bEatAcLIdt3OYGFc9Tm9n7VSlFBcfRv0gBNksHRgSdUDQGtN3XrZ94ztW+NfzkFSUw==",
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz",
+      "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==",
       "requires": {
         "boolbase": "^1.0.0",
         "css-what": "^5.0.0",
@@ -3664,9 +3675,9 @@
       }
     },
     "domutils": {
-      "version": "2.6.0",
-      "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.6.0.tgz",
-      "integrity": "sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA==",
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz",
+      "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==",
       "requires": {
         "dom-serializer": "^1.0.1",
         "domelementtype": "^2.2.0",
@@ -3776,16 +3787,16 @@
       "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
     },
     "mime-db": {
-      "version": "1.47.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz",
-      "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw=="
+      "version": "1.48.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz",
+      "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ=="
     },
     "mime-types": {
-      "version": "2.1.30",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz",
-      "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==",
+      "version": "2.1.31",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz",
+      "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==",
       "requires": {
-        "mime-db": "1.47.0"
+        "mime-db": "1.48.0"
       }
     },
     "mimic-response": {
@@ -3799,16 +3810,16 @@
       "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
     },
     "normalize-url": {
-      "version": "4.5.1",
-      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
-      "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA=="
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.0.1.tgz",
+      "integrity": "sha512-VU4pzAuh7Kip71XEmO9aNREYAdMHFGTVj/i+CaTImS8x0i1d3jUZkXhqluy/PRgjPLMgsLQulYY3PJ/aSbSjpQ=="
     },
     "npm": {
-      "version": "7.15.0",
-      "resolved": "https://registry.npmjs.org/npm/-/npm-7.15.0.tgz",
-      "integrity": "sha512-GIXNqy3obii54oPF0gbcBNq4aYuB/Ovuu/uYp1eS4nij2PEDMnoOh6RoSv2MDvAaB4a+JbpX/tjDxLO7JAADgQ==",
+      "version": "7.17.0",
+      "resolved": "https://registry.npmjs.org/npm/-/npm-7.17.0.tgz",
+      "integrity": "sha512-yNzj4vQellvUGiBM/SzhfT89EV0vb7iILjTehSydTY/IgK2Vjk7/7J8WNJ2ysqcgfLY21ptI/j7uknt15IbbKQ==",
       "requires": {
-        "@npmcli/arborist": "^2.6.0",
+        "@npmcli/arborist": "^2.6.1",
         "@npmcli/ci-detect": "^1.2.0",
         "@npmcli/config": "^2.2.0",
         "@npmcli/run-script": "^1.8.5",
@@ -3833,7 +3844,7 @@
         "leven": "^3.1.0",
         "libnpmaccess": "^4.0.2",
         "libnpmdiff": "^2.0.4",
-        "libnpmexec": "^1.1.1",
+        "libnpmexec": "^1.2.0",
         "libnpmfund": "^1.1.0",
         "libnpmhook": "^6.0.2",
         "libnpmorg": "^2.0.2",
@@ -3842,7 +3853,7 @@
         "libnpmsearch": "^3.1.1",
         "libnpmteam": "^2.0.3",
         "libnpmversion": "^1.2.0",
-        "make-fetch-happen": "^8.0.14",
+        "make-fetch-happen": "^9.0.1",
         "minipass": "^3.1.3",
         "minipass-pipeline": "^1.2.4",
         "mkdirp": "^1.0.4",
@@ -3851,10 +3862,10 @@
         "node-gyp": "^7.1.2",
         "nopt": "^5.0.0",
         "npm-audit-report": "^2.1.5",
-        "npm-package-arg": "^8.1.2",
+        "npm-package-arg": "^8.1.4",
         "npm-pick-manifest": "^6.1.1",
         "npm-profile": "^5.0.3",
-        "npm-registry-fetch": "^10.1.2",
+        "npm-registry-fetch": "^11.0.0",
         "npm-user-validate": "^1.0.1",
         "npmlog": "~4.1.2",
         "opener": "^1.5.2",
@@ -3878,7 +3889,7 @@
       },
       "dependencies": {
         "@npmcli/arborist": {
-          "version": "2.6.0",
+          "version": "2.6.2",
           "bundled": true,
           "requires": {
             "@npmcli/installed-package-contents": "^1.0.7",
@@ -3897,7 +3908,7 @@
             "npm-install-checks": "^4.0.0",
             "npm-package-arg": "^8.1.0",
             "npm-pick-manifest": "^6.1.0",
-            "npm-registry-fetch": "^10.0.0",
+            "npm-registry-fetch": "^11.0.0",
             "pacote": "^11.2.6",
             "parse-conflict-json": "^1.1.1",
             "promise-all-reject-late": "^1.0.0",
@@ -4412,15 +4423,6 @@
           "version": "0.6.1",
           "bundled": true
         },
-        "form-data": {
-          "version": "2.3.3",
-          "bundled": true,
-          "requires": {
-            "asynckit": "^0.4.0",
-            "combined-stream": "^1.0.6",
-            "mime-types": "^2.1.12"
-          }
-        },
         "fs-minipass": {
           "version": "2.1.0",
           "bundled": true,
@@ -4567,7 +4569,7 @@
           }
         },
         "iconv-lite": {
-          "version": "0.6.2",
+          "version": "0.6.3",
           "bundled": true,
           "optional": true,
           "requires": {
@@ -4720,13 +4722,13 @@
           "bundled": true
         },
         "libnpmaccess": {
-          "version": "4.0.2",
+          "version": "4.0.3",
           "bundled": true,
           "requires": {
             "aproba": "^2.0.0",
             "minipass": "^3.1.1",
             "npm-package-arg": "^8.1.2",
-            "npm-registry-fetch": "^10.0.0"
+            "npm-registry-fetch": "^11.0.0"
           }
         },
         "libnpmdiff": {
@@ -4744,7 +4746,7 @@
           }
         },
         "libnpmexec": {
-          "version": "1.1.1",
+          "version": "1.2.0",
           "bundled": true,
           "requires": {
             "@npmcli/arborist": "^2.3.0",
@@ -4768,19 +4770,19 @@
           }
         },
         "libnpmhook": {
-          "version": "6.0.2",
+          "version": "6.0.3",
           "bundled": true,
           "requires": {
             "aproba": "^2.0.0",
-            "npm-registry-fetch": "^10.0.0"
+            "npm-registry-fetch": "^11.0.0"
           }
         },
         "libnpmorg": {
-          "version": "2.0.2",
+          "version": "2.0.3",
           "bundled": true,
           "requires": {
             "aproba": "^2.0.0",
-            "npm-registry-fetch": "^10.0.0"
+            "npm-registry-fetch": "^11.0.0"
           }
         },
         "libnpmpack": {
@@ -4793,29 +4795,29 @@
           }
         },
         "libnpmpublish": {
-          "version": "4.0.1",
+          "version": "4.0.2",
           "bundled": true,
           "requires": {
             "normalize-package-data": "^3.0.2",
             "npm-package-arg": "^8.1.2",
-            "npm-registry-fetch": "^10.0.0",
+            "npm-registry-fetch": "^11.0.0",
             "semver": "^7.1.3",
             "ssri": "^8.0.1"
           }
         },
         "libnpmsearch": {
-          "version": "3.1.1",
+          "version": "3.1.2",
           "bundled": true,
           "requires": {
-            "npm-registry-fetch": "^10.0.0"
+            "npm-registry-fetch": "^11.0.0"
           }
         },
         "libnpmteam": {
-          "version": "2.0.3",
+          "version": "2.0.4",
           "bundled": true,
           "requires": {
             "aproba": "^2.0.0",
-            "npm-registry-fetch": "^10.0.0"
+            "npm-registry-fetch": "^11.0.0"
           }
         },
         "libnpmversion": {
@@ -4837,11 +4839,11 @@
           }
         },
         "make-fetch-happen": {
-          "version": "8.0.14",
+          "version": "9.0.2",
           "bundled": true,
           "requires": {
             "agentkeepalive": "^4.1.3",
-            "cacache": "^15.0.5",
+            "cacache": "^15.2.0",
             "http-cache-semantics": "^4.1.0",
             "http-proxy-agent": "^4.0.1",
             "https-proxy-agent": "^5.0.0",
@@ -4852,20 +4854,21 @@
             "minipass-fetch": "^1.3.2",
             "minipass-flush": "^1.0.5",
             "minipass-pipeline": "^1.2.4",
+            "negotiator": "^0.6.2",
             "promise-retry": "^2.0.1",
             "socks-proxy-agent": "^5.0.0",
             "ssri": "^8.0.0"
           }
         },
         "mime-db": {
-          "version": "1.47.0",
+          "version": "1.48.0",
           "bundled": true
         },
         "mime-types": {
-          "version": "2.1.30",
+          "version": "2.1.31",
           "bundled": true,
           "requires": {
-            "mime-db": "1.47.0"
+            "mime-db": "1.48.0"
           }
         },
         "minimatch": {
@@ -4957,6 +4960,10 @@
           "version": "0.0.8",
           "bundled": true
         },
+        "negotiator": {
+          "version": "0.6.2",
+          "bundled": true
+        },
         "node-gyp": {
           "version": "7.1.2",
           "bundled": true,
@@ -5016,7 +5023,7 @@
           "bundled": true
         },
         "npm-package-arg": {
-          "version": "8.1.2",
+          "version": "8.1.4",
           "bundled": true,
           "requires": {
             "hosted-git-info": "^4.0.1",
@@ -5045,18 +5052,17 @@
           }
         },
         "npm-profile": {
-          "version": "5.0.3",
+          "version": "5.0.4",
           "bundled": true,
           "requires": {
-            "npm-registry-fetch": "^10.0.0"
+            "npm-registry-fetch": "^11.0.0"
           }
         },
         "npm-registry-fetch": {
-          "version": "10.1.2",
+          "version": "11.0.0",
           "bundled": true,
           "requires": {
-            "lru-cache": "^6.0.0",
-            "make-fetch-happen": "^8.0.9",
+            "make-fetch-happen": "^9.0.1",
             "minipass": "^3.1.3",
             "minipass-fetch": "^1.3.0",
             "minipass-json-stream": "^1.0.1",
@@ -5109,7 +5115,7 @@
           }
         },
         "pacote": {
-          "version": "11.3.3",
+          "version": "11.3.4",
           "bundled": true,
           "requires": {
             "@npmcli/git": "^2.0.1",
@@ -5125,7 +5131,7 @@
             "npm-package-arg": "^8.0.1",
             "npm-packlist": "^2.1.4",
             "npm-pick-manifest": "^6.0.0",
-            "npm-registry-fetch": "^10.0.0",
+            "npm-registry-fetch": "^11.0.0",
             "promise-retry": "^2.0.1",
             "read-package-json-fast": "^2.0.1",
             "rimraf": "^3.0.2",
@@ -5147,7 +5153,7 @@
           "bundled": true
         },
         "path-parse": {
-          "version": "1.0.6",
+          "version": "1.0.7",
           "bundled": true
         },
         "performance-now": {
@@ -5283,6 +5289,15 @@
             "uuid": "^3.3.2"
           },
           "dependencies": {
+            "form-data": {
+              "version": "2.3.3",
+              "bundled": true,
+              "requires": {
+                "asynckit": "^0.4.0",
+                "combined-stream": "^1.0.6",
+                "mime-types": "^2.1.12"
+              }
+            },
             "tough-cookie": {
               "version": "2.5.0",
               "bundled": true,
@@ -5377,7 +5392,7 @@
           }
         },
         "spdx-license-ids": {
-          "version": "3.0.7",
+          "version": "3.0.9",
           "bundled": true
         },
         "sshpk": {
@@ -5706,9 +5721,9 @@
       }
     },
     "prism-media": {
-      "version": "1.2.9",
-      "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.2.9.tgz",
-      "integrity": "sha512-UHCYuqHipbTR1ZsXr5eg4JUmHER8Ss4YEb9Azn+9zzJ7/jlTtD1h0lc4g6tNx3eMlB8Mp6bfll0LPMAV4R6r3Q==",
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.1.tgz",
+      "integrity": "sha512-nyYAa3KB4qteJIqdguKmwxTJgy55xxUtkJ3uRnOvO5jO+frci+9zpRXw6QZVcfDeva3S654fU9+26P2OSTzjHw==",
       "requires": {}
     },
     "pump": {
@@ -5775,9 +5790,9 @@
       }
     },
     "tslib": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
-      "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
     },
     "tweetnacl": {
       "version": "1.0.3",

+ 5 - 5
package.json

@@ -15,15 +15,15 @@
     "node": ">=14.0.0"
   },
   "dependencies": {
-    "cheerio": "^1.0.0-rc.6",
+    "cheerio": "^1.0.0-rc.10",
     "datetime-difference": "^1.0.2",
     "discord-oauth2": "^2.6.0",
-    "discord.js": "^12.5.1",
+    "discord.js": "^12.5.3",
     "dotenv": "^10.0.0",
-    "full-icu": "^1.3.1",
-    "got": "^11.8.1",
+    "full-icu": "^1.3.4",
+    "got": "^11.8.2",
     "htmlparser2": "^6.1.0",
-    "npm": "^7.11.2",
+    "npm": "^7.17.0",
     "pg": "^8.6.0"
   },
   "repository": {

+ 9 - 7
util/edit_diff.js

@@ -52,13 +52,14 @@ function diffParser(html, more, whitespace) {
 			}
 		},
 		onclosetag: (tagname) => {
-			current_tag = '';
 			if ( tagname === 'ins' ) current_tag = 'tda';
 			if ( tagname === 'del' ) current_tag = 'tdd';
+			if ( tagname === 'td' ) current_tag = '';
 			if ( tagname === 'tr' ) {
 				if ( last_ins !== null ) {
 					ins_length++;
-					if ( empty && last_ins.trim().length && !last_ins.includes( '**' ) ) {
+					if ( empty && last_ins.trim().length ) {
+						if ( last_ins.includes( '**' ) ) last_ins = last_ins.replace( /\*\*/g, '__' );
 						ins_length += 4;
 						last_ins = '**' + last_ins + '**';
 					}
@@ -68,7 +69,8 @@ function diffParser(html, more, whitespace) {
 				}
 				if ( last_del !== null ) {
 					del_length++;
-					if ( empty && last_del.trim().length && !last_del.includes( '~~' ) ) {
+					if ( empty && last_del.trim().length ) {
+						if ( last_del.includes( '~~' ) ) last_del = last_del.replace( /\~\~/g, '__' );
 						del_length += 4;
 						last_del = '~~' + last_del + '~~';
 					}
@@ -84,13 +86,13 @@ function diffParser(html, more, whitespace) {
 	parser.end();
 	var compare = ['', ''];
 	if ( small_prev_del.length ) {
-		if ( small_prev_del.replace( /\~\~/g, '' ).trim().length ) {
-			compare[0] = small_prev_del.replace( /\~\~\~\~/g, '' );
+		if ( small_prev_del.replace( /\~\~|__/g, '' ).trim().length ) {
+			compare[0] = small_prev_del.replace( /\~\~\~\~|____/g, '' );
 		} else compare[0] = whitespace;
 	}
 	if ( small_prev_ins.length ) {
-		if ( small_prev_ins.replace( /\*\*/g, '' ).trim().length ) {
-			compare[1] = small_prev_ins.replace( /\*\*\*\*/g, '' );
+		if ( small_prev_ins.replace( /\*\*|__/g, '' ).trim().length ) {
+			compare[1] = small_prev_ins.replace( /\*\*\*\*|____/g, '' );
 		} else compare[1] = whitespace;
 	}
 	return compare;