Browse Source

Merge remote-tracking branch 'origin/master'

Owen Diffey 3 years ago
parent
commit
0f68a74320
100 changed files with 3663 additions and 2577 deletions
  1. 0 4
      .github/labeler.yml
  2. 0 72
      .github/workflows/codeql-analysis.yml
  3. 0 12
      .github/workflows/label.yml
  4. 0 38
      .github/workflows/nodejs.yml
  5. 0 39
      .github/workflows/translations.yml
  6. 3 2
      PRIVACY.md
  7. 5 6
      README.md
  8. 151 202
      bot.js
  9. 39 27
      cmds/eval.js
  10. 75 44
      cmds/get.js
  11. 12 11
      cmds/help.js
  12. 7 3
      cmds/link.js
  13. 38 31
      cmds/minecraft/bug.js
  14. 5 4
      cmds/minecraft/command.js
  15. 6 3
      cmds/minecraft/syntax.js
  16. 117 85
      cmds/patreon.js
  17. 6 6
      cmds/pause.js
  18. 108 119
      cmds/rcscript.js
  19. 5 3
      cmds/say.js
  20. 80 89
      cmds/settings.js
  21. 4 4
      cmds/stop.js
  22. 13 15
      cmds/test.js
  23. 76 92
      cmds/verification.js
  24. 185 118
      cmds/verify.js
  25. 13 13
      cmds/voice.js
  26. 26 23
      cmds/wiki/diff.js
  27. 107 64
      cmds/wiki/general.js
  28. 35 27
      cmds/wiki/overview.js
  29. 66 13
      cmds/wiki/random.js
  30. 107 61
      cmds/wiki/search.js
  31. 34 32
      cmds/wiki/user.js
  32. 45 7
      dashboard/guilds.js
  33. 37 0
      dashboard/i18n/en.json
  34. 18 2
      dashboard/i18n/es.json
  35. 12 0
      dashboard/i18n/hi.json
  36. 59 4
      dashboard/i18n/ko.json
  37. 12 0
      dashboard/i18n/pt-br.json
  38. 61 6
      dashboard/i18n/tr.json
  39. 11 1
      dashboard/i18n/vi.json
  40. 6 6
      dashboard/i18n/zh-hant.json
  41. 1 1
      dashboard/index.html
  42. 51 15
      dashboard/index.js
  43. 2 2
      dashboard/login.html
  44. 53 15
      dashboard/oauth.js
  45. 2 2
      dashboard/settings.js
  46. 27 1
      dashboard/src/index.css
  47. 10 1
      dashboard/src/index.js
  48. 129 0
      dashboard/user.js
  49. 66 10
      dashboard/util.js
  50. 37 2
      database.js
  51. 28 20
      functions/discussion.js
  52. 9 9
      functions/global_block.js
  53. 1 1
      functions/helpsetup.js
  54. 8 6
      functions/parse_page.js
  55. 15 11
      functions/phabricator.js
  56. 71 26
      functions/special_page.js
  57. 216 197
      functions/verify.js
  58. 2 2
      i18n/bn.json
  59. 3 2
      i18n/de.json
  60. 90 89
      i18n/en.json
  61. 24 2
      i18n/es.json
  62. 2 2
      i18n/fr.json
  63. 2 2
      i18n/hi.json
  64. 102 11
      i18n/ja.json
  65. 33 4
      i18n/ko.json
  66. 3 2
      i18n/pl.json
  67. 7 2
      i18n/pt-br.json
  68. 3 2
      i18n/ru.json
  69. 2 2
      i18n/sv.json
  70. 2 2
      i18n/th.json
  71. 61 8
      i18n/tr.json
  72. 13 5
      i18n/uk.json
  73. 122 2
      i18n/vi.json
  74. BIN
      i18n/widgets/bn.png
  75. BIN
      i18n/widgets/de.png
  76. BIN
      i18n/widgets/en.png
  77. BIN
      i18n/widgets/es.png
  78. BIN
      i18n/widgets/fr.png
  79. BIN
      i18n/widgets/hi.png
  80. BIN
      i18n/widgets/it.png
  81. BIN
      i18n/widgets/ja.png
  82. BIN
      i18n/widgets/ko.png
  83. BIN
      i18n/widgets/nl.png
  84. BIN
      i18n/widgets/pl.png
  85. BIN
      i18n/widgets/pt-br.png
  86. BIN
      i18n/widgets/ru.png
  87. BIN
      i18n/widgets/sv.png
  88. BIN
      i18n/widgets/th.png
  89. BIN
      i18n/widgets/tr.png
  90. BIN
      i18n/widgets/uk.png
  91. BIN
      i18n/widgets/vi.png
  92. BIN
      i18n/widgets/zh-hans.png
  93. BIN
      i18n/widgets/zh-hant.png
  94. 3 2
      i18n/zh-hans.json
  95. 4 3
      i18n/zh-hant.json
  96. 31 51
      interactions/inline.js
  97. 340 414
      interactions/verify.js
  98. 169 152
      main.js
  99. 329 203
      package-lock.json
  100. 6 6
      package.json

+ 0 - 4
.github/labeler.yml

@@ -1,4 +0,0 @@
-# Add 'translation' label to any change to i18n files
-translation:
-  - 'i18n/*.json'
-  - 'dashboard/i18n/*.json'

+ 0 - 72
.github/workflows/codeql-analysis.yml

@@ -1,72 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL"
-
-on:
-  push:
-    branches: [ master ]
-  pull_request:
-    # The branches below must be a subset of the branches above
-    branches: [ master ]
-  schedule:
-    - cron: '39 12 * * 4'
-
-jobs:
-  analyze:
-    name: Analyze
-    runs-on: ubuntu-latest
-    if: ${{ github.head_ref != 'translations' }}
-    permissions:
-      actions: read
-      contents: read
-      security-events: write
-
-    strategy:
-      fail-fast: false
-      matrix:
-        language: [ 'javascript' ]
-        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
-        # Learn more:
-        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
-
-    steps:
-    - name: Checkout repository
-      uses: actions/checkout@v2
-
-    # Initializes the CodeQL tools for scanning.
-    - name: Initialize CodeQL
-      uses: github/codeql-action/init@v1
-      with:
-        languages: ${{ matrix.language }}
-        # If you wish to specify custom queries, you can do so here or in a config file.
-        # By default, queries listed here will override any specified in a config file.
-        # Prefix the list here with "+" to use these queries and those in the config file.
-        # queries: ./path/to/local/query, your-org/your-repo/queries@main
-
-    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
-    # If this step fails, then you should remove it and run the build manually (see below)
-    - name: Autobuild
-      uses: github/codeql-action/autobuild@v1
-
-    # ℹ️ Command-line programs to run using the OS shell.
-    # 📚 https://git.io/JvXDl
-
-    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
-    #    and modify them (or add more) to build your code if your project
-    #    uses a compiled language
-
-    #- run: |
-    #   make bootstrap
-    #   make release
-
-    - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v1

+ 0 - 12
.github/workflows/label.yml

@@ -1,12 +0,0 @@
-name: "Label pull requests"
-on:
-- pull_request_target
-
-jobs:
-  triage:
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/labeler@v3
-      with:
-        repo-token: "${{ secrets.WIKIBOT_TOKEN }}"
-        sync-labels: true

+ 0 - 38
.github/workflows/nodejs.yml

@@ -1,38 +0,0 @@
-name: Node.js CI
-on:
-  push:
-    branches: [ master ]
-  pull_request:
-    branches: [ master ]
-jobs:
-  runner-job:
-    runs-on: ubuntu-latest
-    if: ${{ github.head_ref != 'translations' }}
-    services:
-      postgres:
-        image: postgres
-        env:
-          POSTGRES_PASSWORD: postgres
-        options: >-
-          --health-cmd pg_isready
-          --health-interval 10s
-          --health-timeout 5s
-          --health-retries 5
-        ports:
-          - 5432:5432
-    strategy:	
-      matrix:	
-        node-version: [14.x]
-    steps:
-    - uses: actions/checkout@v2
-    - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v1
-      with:
-        node-version: ${{ matrix.node-version }}
-    - run: npm ci
-    - run: npm run build --if-present
-    - run: sed -i -e 's/"<Discord bot token>"/"${{secrets.DISCORD_TOKEN}}"/g' .env
-    - run: sed -i -e 's/"<Discord client secret>"/"${{secrets.DISCORD_SECRET}}"/g' .env
-    - run: sed -i -e 's/"!wiki "/"!test "/g' .env
-    - run: npm test -- --timeout:60
-      timeout-minutes: 5

+ 0 - 39
.github/workflows/translations.yml

@@ -1,39 +0,0 @@
-name: Update translation widgets
-on:
-  workflow_dispatch:
-    inputs:
-      languages:
-        description: 'Languages to update translation widgets for.'
-        required: true
-        default: 'i18n/*.json'
-jobs:
-  translations:
-    runs-on: ubuntu-latest
-    steps:
-    - name: Load Repository
-      uses: actions/checkout@v2
-      with:
-        token: ${{ secrets.WIKIBOT_TOKEN }}
-    - name: Update Widgets
-      run: |
-        sudo apt update
-        sudo apt-get install inkscape
-        for language in ${{ github.event.inputs.languages }}
-        do
-          language=$(basename $language .json)
-          if [[ "$language" =~ [^[:lower:]-] ]]
-          then
-            echo "$language" is not a translation
-          else
-            wget https://translate.wikibot.de/widgets/wiki-bot/$language/discord/svg-badge.svg
-            convert -background none svg-badge.svg i18n/widgets/$language.png
-            rm svg-badge.svg
-          fi
-        done
-    - name: Commit changes
-      uses: EndBug/add-and-commit@v4
-      with:
-        author_name: WikiBot-bot
-        author_email: 69196528+WikiBot-bot@users.noreply.github.com
-        message: "Update translation widgets"
-        add: "i18n/widgets/*.png"

+ 3 - 2
PRIVACY.md

@@ -6,9 +6,10 @@ The bot does not collect, save or share any private data.
 
 ## Other data
 The bot needs to collect and save some data in order to function properly:
-* **Settings**: Modified guild settings are saved as long as the bot is a member of that guild and deleted once the bot leaves the guild.
+* **Wiki Accounts**: Wiki accounts connected using [OAuth2](https://www.mediawiki.org/wiki/Extension:OAuth) are saved for easier verification. Using the [Dashboard](https://settings.wikibot.de/user), users can disconnect their accounts again or disable saving the connection completely.
+* **Settings**: Modified guild settings are saved as long as the bot is a member of that guild and deleted shortly after the bot leaves the guild.
 * **Supporters**: The bot maintains a list of translators and [Patreon supporters](https://www.patreon.com/WikiBot) to provide some extra functionality for them.
-* **Commands**: Commands are logged together with the guild id or user id for up to 24 hours for debugging purposes.
+* **Commands**: Commands are logged together with the guild id or user id for up to one week for debugging purposes.
 
 The bot does not share any data with third parties.
 

+ 5 - 6
README.md

@@ -1,11 +1,11 @@
 # Wiki-Bot[<img src="https://translate.wikibot.de/widgets/wiki-bot/-/svg-badge.svg" alt="Translation status" align="right" />](#translations)[<img src="https://github.com/Markus-Rost/discord-wiki-bot/workflows/Node.js CI/badge.svg" alt="Node.js CI" align="right" />](https://github.com/Markus-Rost/discord-wiki-bot/actions)
-[<img src="/dashboard/src/icon.png" alt="Wiki-Bot" align="right" />](https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939904064&scope=bot%20applications.commands)
+[<img src="/dashboard/src/icon.png" alt="Wiki-Bot" align="right" />](https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939904064&scope=bot+applications.commands)
 
 **Wiki-Bot** is a bot for [Discord](https://discord.com/) with the purpose to easily link and search [MediaWiki](https://www.mediawiki.org/wiki/MediaWiki) sites like [Wikipedia](https://www.wikipedia.org/) and [Fandom](https://www.fandom.com/) wikis. **Wiki-Bot** shows short descriptions and additional info about pages and is able to resolve redirects and follow interwiki links.
 
-**Wiki-Bot** has translations for Bengali, German, English, Spanish, French, Hindi, Korean, Dutch, Polish, Brazilian Portuguese, Russian, Turkish, Simplified Chinese and Traditional Chinese.
+**Wiki-Bot** has translations for Bengali, German, English, Spanish, French, Hindi, Korean, Polish, Brazilian Portuguese, Russian, Swedish, Turkish, Simplified Chinese and Traditional Chinese.
 
-[Use this link to invite **Wiki-Bot** to your Discord server.](https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939904064&scope=bot%20applications.commands)
+[Use this link to invite **Wiki-Bot** to your Discord server.](https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939904064&scope=bot+applications.commands)
 
 [Change the server settings for **Wiki-Bot** using the dashboard.](https://settings.wikibot.de/)
 
@@ -20,7 +20,7 @@ Support server: [https://discord.gg/v77RTk5](https://discord.gg/v77RTk5)
 * [Voice Channel](#voice-channel)
 
 ## Setup
-After [inviting](https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939904064&scope=bot%20applications.commands) **Wiki-Bot** to your server you need to set the wiki you want to search by default. You do this with the `!wiki settings` command.
+After [inviting](https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939904064&scope=bot+applications.commands) **Wiki-Bot** to your server you need to set the wiki you want to search by default. You do this with the `!wiki settings` command or by using the [dashboard](https://settings.wikibot.de/).
 * Change the wiki with `!wiki settings wiki <url>`
   * Example: `!wiki settings wiki https://minecraft.fandom.com/wiki/Minecraft_Wiki`
 * Change the language with `!wiki settings lang <language>`
@@ -97,10 +97,9 @@ Use `!wiki voice` to get the format for the role name.
 Help other users find Wiki-Bot by voting on bot lists:
 
 [<img src="https://blist.xyz/api/v2/bot/461189216198590464/widget" alt="blist.xyz" height="150px" />](https://blist.xyz/bot/461189216198590464)
-[<img src="https://botlists.com/bot/461189216198590464/widget" alt="botlists.com" height="150px" />](https://botlists.com/bot/461189216198590464)
 [<img src="https://bots.ondiscord.xyz/bots/461189216198590464/embed?theme=dark&showGuilds=true" alt="bots.ondiscord.xyz" height="150px" />](https://bots.ondiscord.xyz/bots/461189216198590464)
-[<img src="https://botsfordiscord.com/api/bot/461189216198590464/widget?theme=dark" alt="botsfordiscord.com" height="150px" />](https://botsfordiscord.com/bots/461189216198590464)
 [<img src="https://discord.boats/api/widget/461189216198590464" alt="discord.boats" height="150px" />](https://discord.boats/bot/461189216198590464)
+[<img src="https://discords.com/bots/api/bot/461189216198590464/widget?theme=dark" alt="discords.com" height="150px" />](https://discords.com/bots/bot/461189216198590464)
 [<img src="https://infinitybotlist.com/bots/461189216198590464/widget?size=medium" alt="infinitybotlist.com" height="150px" />](https://infinitybotlist.com/bots/461189216198590464)
 [<img src="https://top.gg/api/widget/461189216198590464.svg" alt="top.gg" height="150px" />](https://top.gg/bot/461189216198590464)
 [<img src="https://voidbots.net/api/embed/461189216198590464?theme=dark" alt="voidbots.net" height="150px" />](https://voidbots.net/bot/461189216198590464)

+ 151 - 202
bot.js

@@ -2,85 +2,69 @@ const util = require('util');
 util.inspect.defaultOptions = {compact:false,breakLength:Infinity};
 
 global.isDebug = ( process.argv[2] === 'debug' );
-global.shardId = null;
-process.on( 'message', message => {
-	if ( !message.shard ) return;
-	shardId = message.shard.id;
-} );
-
-global.got = require('got').extend( {
-	throwHttpErrors: false,
-	timeout: 5000,
-	headers: {
-		'User-Agent': 'Wiki-Bot/' + ( isDebug ? 'testing' : process.env.npm_package_version ) + ' (Discord; ' + process.env.npm_package_name + ')'
-	},
-	responseType: 'json'
-} );
 
 const Lang = require('./util/i18n.js');
 const Wiki = require('./util/wiki.js');
 const newMessage = require('./util/newMessage.js');
-const {slashCommands, allowDelete} = require('./util/functions.js');
+const {allowDelete} = require('./util/functions.js');
 global.patreons = {};
 global.voice = {};
 const db = require('./util/database.js');
 
 const Discord = require('discord.js');
 const client = new Discord.Client( {
-	messageEditHistoryMaxSize: 1,
-	messageCacheLifetime: 300,
-	messageSweepInterval: 300,
+	makeCache: Discord.Options.cacheWithLimits( {
+		MessageManager: {
+			maxSize: 100,
+			sweepInterval: 300,
+			sweepFilter: Discord.LimitedCollection.filterByLifetime( {
+				lifetime: 300,
+			} )
+		},
+		PresenceManager: 0
+	} ),
 	allowedMentions: {
-		parse: []
+		parse: [],
+		repliedUser: true
 	},
+	failIfNotExists: false,
 	presence: ( process.env.READONLY ? {
 		status: 'dnd',
-		activity: {
+		activities: [{
 			type: 'PLAYING',
-			name: 'READONLY: ' + process.env.prefix + 'test'
-		}
+			name: 'READONLY: ' + process.env.prefix + 'test' + ( process.env.SHARD_COUNT > 1 ? ' • Shard: ' + process.env.SHARDS : '' ),
+		}],
+		shardId: process.env.SHARDS
 	} : {
 		status: 'online',
-		activity: {
+		activities: [{
 			type: 'PLAYING',
 			name: process.env.prefix + 'help'
-		}
+		}],
+		shardId: process.env.SHARDS
 	} ),
-	ws: {
-		large_threshold: 1000,
-		intents: [
-			'GUILDS',
-			'GUILD_MESSAGES',
-			'GUILD_MESSAGE_REACTIONS',
-			'GUILD_VOICE_STATES',
-			'GUILD_INTEGRATIONS',
-			'DIRECT_MESSAGES',
-			'DIRECT_MESSAGE_REACTIONS'
-		]
-	}
-} );
-
-client.api.applications(process.env.bot).commands.get().then( response => {
-	console.log( '- ' + shardId + ': Slash commands successfully loaded.' );
-	response.forEach( command => {
-		var slashCommand = slashCommands.find( slashCommand => slashCommand.name === command.name );
-		if ( slashCommand ) {
-			slashCommand.id = command.id;
-			slashCommand.application_id = command.application_id;
-		}
-		else slashCommands.push(slashCommand);
-	} );
-}, error => {
-	console.log( '- ' + shardId + ': Error while getting the global slash commands: ' + error );
+	intents: [
+		Discord.Intents.FLAGS.GUILDS,
+		Discord.Intents.FLAGS.GUILD_MESSAGES,
+		Discord.Intents.FLAGS.GUILD_MESSAGE_REACTIONS,
+		Discord.Intents.FLAGS.GUILD_VOICE_STATES,
+		Discord.Intents.FLAGS.GUILD_INTEGRATIONS,
+		Discord.Intents.FLAGS.DIRECT_MESSAGES,
+		Discord.Intents.FLAGS.DIRECT_MESSAGE_REACTIONS
+	],
+	partials: [
+		'CHANNEL'
+	]
 } );
 
 global.pause = {};
 var isStop = false;
 client.on( 'ready', () => {
-	console.log( '\n- ' + shardId + ': Successfully logged in as ' + client.user.username + '!\n' );
+	console.log( '\n- ' + process.env.SHARDS + ': Successfully logged in as ' + client.user.username + '!\n' );
 	Object.keys(voice).forEach( guild => {
 		if ( !client.guilds.cache.has(guild) ) delete voice[guild];
 	} );
+	client.application.commands.fetch();
 } );
 
 
@@ -89,12 +73,12 @@ String.prototype.isMention = function(guild) {
 	return text === '@' + client.user.username || text.replace( /^<@!?(\d+)>$/, '$1' ) === client.user.id || ( guild && text === '@' + guild.me.displayName );
 };
 
-Discord.Channel.prototype.isGuild = function() {
-	return ['text', 'news'].includes( this.type );
+Discord.Channel.prototype.isGuild = function(includeThreads = true) {
+	return this.isText() && this.type.startsWith( 'GUILD_' ) && ( includeThreads || !this.isThread() );
 }
 
 Discord.Message.prototype.isAdmin = function() {
-	return this.channel.isGuild() && this.member && ( this.member.permissions.has('MANAGE_GUILD') || ( this.isOwner() && this.evalUsed ) );
+	return this.channel.isGuild() && this.member && ( this.member.permissions.has(Discord.Permissions.FLAGS.MANAGE_GUILD) || ( this.isOwner() && this.evalUsed ) );
 };
 
 Discord.Message.prototype.isOwner = function() {
@@ -102,33 +86,26 @@ Discord.Message.prototype.isOwner = function() {
 };
 
 Discord.Message.prototype.showEmbed = function() {
-	return !this.channel.isGuild() || this.channel.permissionsFor(client.user).has('EMBED_LINKS');
+	return !this.channel.isGuild() || this.channel.permissionsFor(client.user).has(Discord.Permissions.FLAGS.EMBED_LINKS);
 };
 
 Discord.Message.prototype.uploadFiles = function() {
-	return !this.channel.isGuild() || this.channel.permissionsFor(client.user).has('ATTACH_FILES');
+	return !this.channel.isGuild() || this.channel.permissionsFor(client.user).has(Discord.Permissions.FLAGS.ATTACH_FILES);
 };
 
 String.prototype.replaceSave = function(pattern, replacement) {
 	return this.replace( pattern, ( typeof replacement === 'string' ? replacement.replace( /\$/g, '$$$$' ) : replacement ) );
 };
 
-Discord.APIMessage.prototype._resolveDataOld = Discord.APIMessage.prototype.resolveData;
-Discord.APIMessage.prototype.resolveData = function() {
-	this._resolveDataOld();
-	if ( this.options.components ) this.data.components = this.options.components;
-	return this;
-};
-
 Discord.Message.prototype.reactEmoji = function(name, ignorePause = false) {
-	if ( !this.channel.isGuild() || !pause[this.guild.id] || ( ignorePause && ( this.isAdmin() || this.isOwner() ) ) ) {
-		var emoji = ':error:440871715938238494';
+	if ( !this.channel.isGuild() || !pause[this.guildId] || ( ignorePause && ( this.isAdmin() || this.isOwner() ) ) ) {
+		var emoji = '<:error:440871715938238494>';
 		switch ( name ) {
 			case 'nowiki':
-				emoji = ':unknown_wiki:505884572001763348';
+				emoji = '<:unknown_wiki:505884572001763348>';
 				break;
 			case 'error':
-				emoji = ':error:440871715938238494';
+				emoji = '<:error:440871715938238494>';
 				break;
 			default:
 				emoji = name;
@@ -144,12 +121,11 @@ Discord.MessageReaction.prototype.removeEmoji = function() {
 	return this.users.remove().catch(log_error);
 };
 
-Discord.Message.prototype.sendChannel = function(content, options = {}, ignorePause = false) {
-	if ( !this.channel.isGuild() || !pause[this.guild.id] || ( ignorePause && ( this.isAdmin() || this.isOwner() ) ) ) {
-		if ( !options.allowedMentions ) options.allowedMentions = {users:[this.author.id]};
-		return this.channel.send(content, options).then( msg => {
-			if ( msg.length ) msg.forEach( message => allowDelete(message, this.author.id) );
-			else allowDelete(msg, this.author.id);
+Discord.Message.prototype.sendChannel = function(message, ignorePause = false) {
+	if ( !this.channel.isGuild() || !pause[this.guildId] || ( ignorePause && ( this.isAdmin() || this.isOwner() ) ) ) {
+		if ( message?.embeds?.length && !message.embeds[0] ) message.embeds = [];
+		return this.channel.send( message ).then( msg => {
+			allowDelete(msg, this.author.id);
 			return msg;
 		}, error => {
 			log_error(error);
@@ -161,17 +137,11 @@ Discord.Message.prototype.sendChannel = function(content, options = {}, ignorePa
 	}
 };
 
-Discord.Message.prototype.sendChannelError = function(content, options = {}) {
-	if ( !options.allowedMentions ) options.allowedMentions = {users:[this.author.id]};
-	return this.channel.send(content, options).then( msg => {
-		if ( msg.length ) msg.forEach( message => {
-			message.reactEmoji('error');
-			allowDelete(message, this.author.id);
-		} );
-		else {
-			msg.reactEmoji('error');
-			allowDelete(msg, this.author.id);
-		}
+Discord.Message.prototype.sendChannelError = function(message) {
+	if ( message?.embeds?.length && !message.embeds[0] ) message.embeds = [];
+	return this.channel.send( message ).then( msg => {
+		msg.reactEmoji('error');
+		allowDelete(msg, this.author.id);
 		return msg;
 	}, error => {
 		log_error(error);
@@ -179,14 +149,11 @@ Discord.Message.prototype.sendChannelError = function(content, options = {}) {
 	} );
 };
 
-Discord.Message.prototype.replyMsg = function(content, options = {}, ignorePause = false, letDelete = true) {
-	if ( !this.channel.isGuild() || !pause[this.guild.id] || ( ignorePause && ( this.isAdmin() || this.isOwner() ) ) ) {
-		if ( !options.allowedMentions ) options.allowedMentions = {users:[this.author.id]};
-		return this.reply(content, options).then( msg => {
-			if ( letDelete ) {
-				if ( msg.length ) msg.forEach( message => allowDelete(message, this.author.id) );
-				else allowDelete(msg, this.author.id);
-			}
+Discord.Message.prototype.replyMsg = function(message, ignorePause = false, letDelete = true) {
+	if ( !this.channel.isGuild() || !pause[this.guildId] || ( ignorePause && ( this.isAdmin() || this.isOwner() ) ) ) {
+		if ( message?.embeds?.length && !message.embeds[0] ) message.embeds = [];
+		return this.reply( message ).then( msg => {
+			if ( letDelete ) allowDelete(msg, this.author.id);
 			return msg;
 		}, error => {
 			log_error(error);
@@ -228,118 +195,88 @@ 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);
+client.on( 'interactionCreate', interaction => {
+	if ( interaction.inGuild() && typeof interaction.member.permissions === 'string' ) {
+		interaction.member.permissions = new Discord.Permissions(interaction.member.permissions);
 	}
-	if ( interaction.type === 2 ) return slash_command(interaction, channel);
-	if ( interaction.type === 3 ) return message_button(interaction, channel);
+	if ( interaction.channel.partial ) return interaction.channel.fetch().then( () => {
+		if ( interaction.isCommand() ) return slash_command(interaction);
+		if ( interaction.isButton() ) return message_button(interaction);
+	}, log_error );
+	if ( interaction.isCommand() ) return slash_command(interaction);
+	if ( interaction.isButton() ) return message_button(interaction);
 } );
 
 /**
  * Handle slash commands.
- * @param {Object} interaction - The interaction.
- * @param {Discord.Client} interaction.client - The client of the interaction.
- * @param {Discord.TextChannel} [channel] - The channel for the interaction.
+ * @param {Discord.CommandInteraction} interaction - The interaction.
  */
-function slash_command(interaction, channel) {
-	if ( interaction.data.name !== 'inline' ) {
-		console.log( ( interaction.guild_id || '@' + interaction.user.id ) + ': Slash: /' + interaction.data.name + ' ' + ( interaction.data.options?.map( option => {
-			return option.name + ':' + option.value;
-		} ).join(' ') || '' ) );
+function slash_command(interaction) {
+	if ( interaction.commandName === 'inline' ) console.log( ( interaction.guildId || '@' + interaction.user.id ) + ': Slash: /' + interaction.commandName );
+	else console.log( ( interaction.guildId || '@' + interaction.user.id ) + ': Slash: /' + interaction.commandName + ' ' + interaction.options.data.map( option => {
+		return option.name + ':' + option.value;
+	} ).join(' ') );
+	if ( !slash.hasOwnProperty(interaction.commandName) ) return;
+	if ( !interaction.inGuild() ) {
+		return slash[interaction.commandName](interaction, new Lang(), new Wiki());
 	}
-	else console.log( ( interaction.guild_id || '@' + interaction.user.id ) + ': Slash: /' + interaction.data.name );
-	if ( !slash.hasOwnProperty(interaction.data.name) ) {
-		console.log( '- Slash: Unknown command: ' + ( isDebug ? JSON.stringify(interaction, null, '\t') : interaction.data.name ) );
-		return client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {
-				type: 4,
-				data: {
-					content: '<:error:440871715938238494> [Unknown Command!](<' + process.env.invite + '>) <:error:440871715938238494>',
-					allowed_mentions: {
-						parse: []
-					},
-					flags: 64
-				}
-			}
-		} ).catch(log_error);
-	}
-	if ( !interaction.guild_id ) {
-		return slash[interaction.data.name](interaction, new Lang(), new Wiki(), channel);
-	}
-	db.query( 'SELECT wiki, lang FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', [interaction.guild_id, interaction.channel_id, '#' + channel?.parentID] ).then( ({rows:[row]}) => {
-		return slash[interaction.data.name](interaction, new Lang(( row?.lang || channel?.guild?.preferredLocale )), new Wiki(row?.wiki), channel);
+	let sqlargs = [interaction.guildId];
+	if ( interaction.channel?.isThread() ) sqlargs.push(interaction.channel.parentId, '#' + interaction.channel.parent?.parentId);
+	else sqlargs.push(interaction.channelId, '#' + interaction.channel?.parentId);
+	db.query( 'SELECT wiki, lang FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', sqlargs ).then( ({rows:[row]}) => {
+		return slash[interaction.commandName](interaction, new Lang(( row?.lang || interaction.guild?.preferredLocale )), new Wiki(row?.wiki));
 	}, dberror => {
 		console.log( '- Slash: Error while getting the wiki: ' + dberror );
-		return client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {
-				type: 4,
-				data: {
-					content: '<:error:440871715938238494> [Error!](<' + process.env.invite + '>) <:error:440871715938238494>',
-					allowed_mentions: {
-						parse: []
-					},
-					flags: 64
-				}
-			}
-		} ).catch(log_error);
+		return interaction.reply( {content: new Lang(interaction.guild?.preferredLocale, 'general').get('database') + '\n' + process.env.invite, ephemeral: true} ).catch(log_error);
 	} );
 }
 
 /**
- * Handle message components.
- * @param {Object} interaction - The interaction.
- * @param {Discord.Client} interaction.client - The client of the interaction.
- * @param {Discord.TextChannel} [channel] - The channel for the interaction.
+ * Handle message buttons.
+ * @param {Discord.ButtonInteraction} interaction - The interaction.
  */
-function message_button(interaction, channel) {
-	if ( interaction.data.component_type !== 2 ) return;
-	var cmd = ( buttonsMap.hasOwnProperty(interaction.data.custom_id) ? buttonsMap[interaction.data.custom_id] : interaction.data.custom_id );
-	if ( !buttons.hasOwnProperty(cmd) ) {
-		console.log( '- Button: Unknown command: ' + ( isDebug ? JSON.stringify(interaction, null, '\t') : interaction.data.custom_id ) );
-		return client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {type: 6}
-		} ).then( () => {
-			client.api.webhooks(interaction.application_id, interaction.token).post( {
-				data: {
-					content: '<:error:440871715938238494> [Unknown Button!](<' + process.env.invite + '>) <:error:440871715938238494>',
-					allowed_mentions: {
-						parse: []
-					},
-					flags: 64
-				}
-			} ).catch(log_error);
-		}, log_error);
-	}
-	if ( !interaction.guild_id ) {
-		return buttons[cmd](interaction, new Lang(), new Wiki(), channel);
+function message_button(interaction) {
+	var cmd = ( buttonsMap.hasOwnProperty(interaction.customId) ? buttonsMap[interaction.customId] : interaction.customId );
+	if ( !buttons.hasOwnProperty(cmd) ) return;
+	if ( !interaction.inGuild() ) {
+		return buttons[cmd](interaction, new Lang(), new Wiki());
 	}
-	db.query( 'SELECT wiki, lang FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', [interaction.guild_id, interaction.channel_id, '#' + channel?.parentID] ).then( ({rows:[row]}) => {
-		return buttons[cmd](interaction, new Lang(( row?.lang || channel?.guild?.preferredLocale )), new Wiki(row?.wiki), channel);
+	let sqlargs = [interaction.guildId];
+	if ( interaction.channel?.isThread() ) sqlargs.push(interaction.channel.parentId, '#' + interaction.channel.parent?.parentId);
+	else sqlargs.push(interaction.channelId, '#' + interaction.channel?.parentId);
+	db.query( 'SELECT wiki, lang FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', sqlargs ).then( ({rows:[row]}) => {
+		return buttons[cmd](interaction, new Lang(( row?.lang || interaction.guild?.preferredLocale )), new Wiki(row?.wiki));
 	}, dberror => {
 		console.log( '- Button: Error while getting the wiki: ' + dberror );
-		return client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {type: 6}
-		} ).catch(log_error);
+		return interaction.reply( {content: new Lang(interaction.guild?.preferredLocale, 'general').get('database') + '\n' + process.env.invite, ephemeral: true} ).catch(log_error);
 	} );
 }
 
-client.on( 'message', msg => {
-	if ( isStop || msg.type !== 'DEFAULT' || msg.system || msg.webhookID || msg.author.bot || msg.author.id === msg.client.user.id ) return;
-	if ( !msg.content.hasPrefix(( msg.channel.isGuild() && patreons[msg.guild.id] || process.env.prefix ), 'm') ) {
+client.on( 'messageCreate', msg => {
+	if ( msg.channel.partial ) return msg.channel.fetch().then( () => {
+		return messageCreate(msg);
+	}, log_error );
+	return messageCreate(msg);
+} );
+
+/**
+ * Handle new messages.
+ * @param {Discord.Message} msg - The message.
+ */
+function messageCreate(msg) {
+	if ( isStop || !msg.channel.isText() || msg.system || msg.webhookId || msg.author.bot || msg.author.id === msg.client.user.id ) return;
+	if ( !msg.content.hasPrefix(( msg.channel.isGuild() && patreons[msg.guildId] || process.env.prefix ), 'm') ) {
 		if ( msg.content === process.env.prefix + 'help' && ( msg.isAdmin() || msg.isOwner() ) ) {
-			if ( msg.channel.permissionsFor(msg.client.user).has('SEND_MESSAGES') ) {
-				console.log( msg.guild.name + ': ' + msg.content );
-				db.query( 'SELECT lang FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', [msg.guild.id, msg.channel.id, '#' + msg.channel.parentID] ).then( ({rows:[row]}) => {
-					msg.replyMsg( new Lang(( row?.lang || msg.guild.preferredLocale ), 'general').get('prefix', patreons[msg.guild.id]), {}, true );
+			if ( msg.channel.permissionsFor(msg.client.user).has(Discord.Permissions.FLAGS.SEND_MESSAGES) ) {
+				console.log( msg.guildId + ': ' + msg.content );
+				let sqlargs = [msg.guildId];
+				if ( msg.channel?.isThread() ) sqlargs.push(msg.channel.parentId, '#' + msg.channel.parent?.parentId);
+				else sqlargs.push(msg.channelId, '#' + msg.channel.parentId);
+				db.query( 'SELECT lang FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', sqlargs ).then( ({rows:[row]}) => {
+					msg.replyMsg( new Lang(( row?.lang || msg.guild.preferredLocale ), 'general').get('prefix', patreons[msg.guildId]), true );
 				}, dberror => {
 					console.log( '- Error while getting the lang: ' + dberror );
-					msg.replyMsg( new Lang(msg.guild.preferredLocale, 'general').get('prefix', patreons[msg.guild.id]), {}, true );
+					msg.replyMsg( new Lang(msg.guild.preferredLocale, 'general').get('prefix', patreons[msg.guildId]), true );
 				} );
 			}
 			return;
@@ -347,28 +284,36 @@ client.on( 'message', msg => {
 		if ( !( msg.content.includes( '[[' ) && msg.content.includes( ']]' ) ) && !( msg.content.includes( '{{' ) && msg.content.includes( '}}' ) ) ) return;
 	}
 	if ( msg.channel.isGuild() ) {
+		let sqlargs = [msg.guildId];
+		if ( msg.channel.isThread() ) sqlargs.push(msg.channel.parentId, '#' + msg.channel.parent?.parentId);
+		else sqlargs.push(msg.channelId, '#' + msg.channel.parentId);
 		var permissions = msg.channel.permissionsFor(msg.client.user);
-		var missing = permissions.missing(['SEND_MESSAGES','ADD_REACTIONS','USE_EXTERNAL_EMOJIS','READ_MESSAGE_HISTORY']);
+		var missing = permissions.missing([
+			Discord.Permissions.FLAGS.SEND_MESSAGES,
+			Discord.Permissions.FLAGS.ADD_REACTIONS,
+			Discord.Permissions.FLAGS.USE_EXTERNAL_EMOJIS,
+			Discord.Permissions.FLAGS.READ_MESSAGE_HISTORY
+		]);
 		if ( missing.length ) {
-			if ( ( msg.isAdmin() || msg.isOwner() ) && msg.content.hasPrefix(( patreons[msg.guild.id] || process.env.prefix ), 'm') ) {
-				console.log( msg.guild.id + ': Missing permissions - ' + missing.join(', ') );
+			if ( ( msg.isAdmin() || msg.isOwner() ) && msg.content.hasPrefix(( patreons[msg.guildId] || process.env.prefix ), 'm') ) {
+				console.log( msg.guildId + ': Missing permissions - ' + missing.join(', ') );
 				if ( !missing.includes( 'SEND_MESSAGES' ) ) {
-					db.query( 'SELECT lang FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', [msg.guild.id, msg.channel.id, '#' + msg.channel.parentID] ).then( ({rows:[row]}) => {
-						msg.replyMsg( new Lang(( row?.lang || msg.guild.preferredLocale ), 'general').get('missingperm') + ' `' + missing.join('`, `') + '`', {}, true );
+					db.query( 'SELECT lang FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', sqlargs ).then( ({rows:[row]}) => {
+						msg.replyMsg( new Lang(( row?.lang || msg.guild.preferredLocale ), 'general').get('missingperm') + ' `' + missing.join('`, `') + '`', true );
 					}, dberror => {
 						console.log( '- Error while getting the lang: ' + dberror );
-						msg.replyMsg( new Lang(msg.guild.preferredLocale, 'general').get('missingperm') + ' `' + missing.join('`, `') + '`', {}, true );
+						msg.replyMsg( new Lang(msg.guild.preferredLocale, 'general').get('missingperm') + ' `' + missing.join('`, `') + '`', true );
 					} );
 				}
 			}
 			return;
 		}
-		db.query( 'SELECT wiki, lang, role, inline FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', [msg.guild.id, msg.channel.id, '#' + msg.channel.parentID] ).then( ({rows:[row]}) => {
+		db.query( 'SELECT wiki, lang, role, inline FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', sqlargs ).then( ({rows:[row]}) => {
 			if ( row ) {
 				if ( msg.guild.roles.cache.has(row.role) && msg.guild.roles.cache.get(row.role).comparePositionTo(msg.member.roles.highest) > 0 && !msg.isAdmin() ) {
 					msg.onlyVerifyCommand = true;
 				}
-				newMessage(msg, new Lang(row.lang), row.wiki, patreons[msg.guild.id], row.inline);
+				newMessage(msg, new Lang(row.lang), row.wiki, patreons[msg.guildId], row.inline);
 			}
 			else {
 				msg.defaultSettings = true;
@@ -376,27 +321,27 @@ client.on( 'message', msg => {
 			}
 		}, dberror => {
 			console.log( '- Error while getting the wiki: ' + dberror );
-			msg.sendChannel( new Lang(msg.guild.preferredLocale, 'general').get('database') + '\n' + process.env.invite, {}, true );
+			msg.sendChannel( new Lang(msg.guild.preferredLocale, 'general').get('database') + '\n' + process.env.invite, true );
 		} );
 	}
 	else newMessage(msg, new Lang());
-} );
+};
 
 
 client.on( 'voiceStateUpdate', (olds, news) => {
-	if ( isStop || !( voice.hasOwnProperty(olds.guild.id) ) || !olds.guild.me.permissions.has('MANAGE_ROLES') || olds.channelID === news.channelID ) return;
+	if ( isStop || !( voice.hasOwnProperty(olds.guild.id) ) || !olds.guild.me.permissions.has('MANAGE_ROLES') || olds.channelId === news.channelId ) return;
 	var lang = new Lang(voice[olds.guild.id], 'voice');
 	if ( olds.member && olds.channel ) {
 		var oldrole = olds.member.roles.cache.find( role => role.name === lang.get('channel') + ' – ' + olds.channel.name );
 		if ( oldrole && oldrole.comparePositionTo(olds.guild.me.roles.highest) < 0 ) {
-			console.log( olds.guild.id + ': ' + olds.member.id + ' left the voice channel "' + olds.channel.id + '".' );
+			console.log( olds.guild.id + ': ' + olds.member.id + ' left the voice channel "' + olds.channelId + '".' );
 			olds.member.roles.remove( oldrole, lang.get('left', olds.member.displayName, olds.channel.name) ).catch(log_error);
 		}
 	}
 	if ( news.member && news.channel ) {
 		var newrole = news.guild.roles.cache.find( role => role.name === lang.get('channel') + ' – ' + news.channel.name );
 		if ( newrole && newrole.comparePositionTo(news.guild.me.roles.highest) < 0 ) {
-			console.log( news.guild.id + ': ' + news.member.id + ' joined the voice channel "' + news.channel.id + '".' );
+			console.log( news.guild.id + ': ' + news.member.id + ' joined the voice channel "' + news.channelId + '".' );
 			news.member.roles.add( newrole, lang.get('join', news.member.displayName, news.channel.name) ).catch(log_error);
 		}
 	}
@@ -419,14 +364,16 @@ client.on( 'guildDelete', guild => {
 		return;
 	}
 	console.log( '- ' + guild.id + ': I\'ve been removed from a server.' );
-	leftGuilds.set(guild.id, client.setTimeout(removeSettings, 300000, guild.id));
+	leftGuilds.set(guild.id, setTimeout(removeSettings, 300000, guild.id).unref());
 } );
 
 function removeSettings(guild) {
 	leftGuilds.delete(guild);
 	if ( client.guilds.cache.has(guild) ) return;
 	db.query( 'DELETE FROM discord WHERE main = $1', [guild] ).then( ({rowCount}) => {
-		if ( patreons.hasOwnProperty(guild) ) client.shard.broadcastEval( `delete global.patreons['${guild}']` );
+		if ( patreons.hasOwnProperty(guild) ) client.shard.broadcastEval( (discordClient, evalData) => {
+			delete global.patreons[evalData];
+		}, {context: guild} );
 		if ( voice.hasOwnProperty(guild) ) delete voice[guild];
 		if ( rowCount ) console.log( '- ' + guild + ': Settings successfully removed.' );
 	}, dberror => {
@@ -450,7 +397,7 @@ client.login(process.env.token).catch( error => {
 } );
 
 if ( isDebug ) client.on( 'debug', debug => {
-	if ( isDebug ) console.log( '- ' + shardId + ': Debug: ' + debug );
+	if ( isDebug ) console.log( '- ' + process.env.SHARDS + ': Debug: ' + debug );
 } );
 
 
@@ -474,7 +421,9 @@ const common_warnings = {
 		'Unrecognized values for parameter "prop": pageimages, extracts.',
 		'Unrecognized values for parameter "prop": pageimages, extracts',
 		'Unrecognized value for parameter "prop": extracts.',
-		'Unrecognized value for parameter "prop": pageimages.'
+		'Unrecognized value for parameter "prop": extracts',
+		'Unrecognized value for parameter "prop": pageimages.',
+		'Unrecognized value for parameter "prop": pageimages'
 	]
 }
 
@@ -497,15 +446,15 @@ global.log_warn = function(warning, api = true) {
  */
 function graceful(signal) {
 	isStop = true;
-	console.log( '- ' + shardId + ': ' + signal + ': Preparing to close...' );
+	console.log( '- ' + process.env.SHARDS + ': ' + signal + ': Preparing to close...' );
 	setTimeout( () => {
-		console.log( '- ' + shardId + ': ' + signal + ': Destroying client...' );
+		console.log( '- ' + process.env.SHARDS + ': ' + signal + ': Destroying client...' );
 		client.destroy();
 		db.end().then( () => {
-			console.log( '- ' + shardId + ': ' + signal + ': Closed the database connection.' );
+			console.log( '- ' + process.env.SHARDS + ': ' + signal + ': Closed the database connection.' );
 			process.exit(0);
 		}, dberror => {
-			console.log( '- ' + shardId + ': ' + signal + ': Error while closing the database connection: ' + dberror );
+			console.log( '- ' + process.env.SHARDS + ': ' + signal + ': Error while closing the database connection: ' + dberror );
 		} );
 	}, 1000 ).unref();
 }

+ 39 - 27
cmds/eval.js

@@ -4,6 +4,7 @@ util.inspect.defaultOptions = {compact:false,breakLength:Infinity};
 const cheerio = require('cheerio');
 const Discord = require('discord.js');
 const {limit: {verification: verificationLimit, rcgcdw: rcgcdwLimit}} = require('../util/default.json');
+const {got} = require('../util/functions.js');
 const newMessage = require('../util/newMessage.js');
 const Wiki = require('../util/wiki.js');
 var db = require('../util/database.js');
@@ -24,8 +25,8 @@ async function cmd_eval(lang, msg, args, line, wiki) {
 		var text = error.toString();
 	}
 	if ( isDebug ) console.log( '--- EVAL START ---\n' + text + '\n--- EVAL END ---' );
-	if ( text.length > 2000 ) msg.reactEmoji('✅', true);
-	else msg.sendChannel( '```js\n' + text + '\n```', {split:{prepend:'```js\n',append:'\n```'},allowedMentions:{}}, true );
+	if ( text.length > 1990 ) msg.reactEmoji('✅', true);
+	else msg.sendChannel( '```js\n' + text + '\n```', true );
 
 	/**
 	 * Runs a command with admin permissions.
@@ -34,7 +35,7 @@ async function cmd_eval(lang, msg, args, line, wiki) {
 	function backdoor(cmdline) {
 		msg.evalUsed = true;
 		msg.onlyVerifyCommand = false;
-		newMessage(msg, lang, wiki, patreons[msg.guild.id], msg.noInline, cmdline);
+		newMessage(msg, lang, wiki, patreons[msg.guildId], msg.noInline, cmdline);
 		return cmdline;
 	}
 }
@@ -181,7 +182,9 @@ function removePatreons(guild, msg) {
 					console.log( '- Guild successfully updated.' );
 					messages.push('Guild successfully updated.');
 				}
-				msg.client.shard.broadcastEval( `delete global.patreons['${guild}']` );
+				msg.client.shard.broadcastEval( (discordClient, evalData) => {
+					delete global.patreons[evalData];
+				}, {context: guild} );
 			}, dberror => {
 				console.log( '- Error while updating the guild: ' + dberror );
 				messages.push('Error while updating the guild: ' + dberror);
@@ -191,21 +194,26 @@ function removePatreons(guild, msg) {
 					if ( rows.length ) {
 						console.log( '- Channel categories successfully deleted.' );
 						messages.push('Channel categories successfully deleted.');
-						return msg.client.shard.broadcastEval( `if ( this.guilds.cache.has('${guild}') ) {
-							let rows = ${JSON.stringify(rows)};
-							this.guilds.cache.get('${guild}').channels.cache.filter( channel => {
-								return ( channel.isGuild() && rows.some( row => {
-									return ( row.channel === '#' + channel.parentID );
-								} ) );
-							} ).map( channel => {
-								return {
-									id: channel.id,
-									wiki: rows.find( row => {
-										return ( row.channel === '#' + channel.parentID );
-									} ).wiki
-								};
-							} )
-						}`, Discord.ShardClientUtil.shardIDForGuildID(guild, msg.client.shard.count) ).then( channels => {
+						return msg.client.shard.broadcastEval( (discordClient, evalData) => {
+							if ( discordClient.guilds.cache.has(evalData.guild) ) {
+								let rows = evalData.rows;
+								return discordClient.guilds.cache.get(evalData.guild).channels.cache.filter( channel => {
+									return ( channel.isGuild(false) && rows.some( row => {
+										return ( row.channel === '#' + channel.parentId );
+									} ) );
+								} ).map( channel => {
+									return {
+										id: channel.id,
+										wiki: rows.find( row => {
+											return ( row.channel === '#' + channel.parentId );
+										} ).wiki
+									};
+								} );
+							}
+						}, {
+							context: {guild, rows},
+							shard: Discord.ShardClientUtil.shardIdForGuildId(guild, msg.client.shard.count)
+						} ).then( channels => {
 							if ( channels.length ) return Promise.all(channels.map( channel => {
 								return client.query( 'INSERT INTO discord(wiki, guild, channel, lang, role, inline, prefix) VALUES($1, $2, $3, $4, $5, $6, $7)', [channel.wiki, guild, channel.id, row.lang, row.role, row.inline, process.env.prefix] ).catch( dberror => {
 									if ( dberror.message !== 'duplicate key value violates unique constraint "discord_guild_channel_key"' ) {
@@ -296,12 +304,14 @@ function removeSettings(msg) {
 	if ( !( msg instanceof Discord.Message ) ) return 'removeSettings(msg) – No message provided!';
 	return db.connect().then( client => {
 		var messages = [];
-		return msg.client.shard.broadcastEval( `[
-			[...this.guilds.cache.keys()],
-			this.channels.cache.filter( channel => {
-				return ( channel.isGuild() || ( channel.type === 'category' && global.patreons.hasOwnProperty(channel.guild.id) ) );
-			} ).map( channel => ( channel.type === 'category' ? '#' : '' ) + channel.id )
-		]` ).then( results => {
+		return msg.client.shard.broadcastEval( discordClient => {
+			return [
+				[...discordClient.guilds.cache.keys()],
+				discordClient.channels.cache.filter( channel => {
+					return ( channel.isGuild() || ( channel.type === 'GUILD_CATEGORY' && global.patreons.hasOwnProperty(channel.guildId) ) );
+				} ).map( channel => ( channel.type === 'GUILD_CATEGORY' ? '#' : '' ) + channel.id )
+			];
+		} ).then( results => {
 			var all_guilds = results.map( result => result[0] ).reduce( (acc, val) => acc.concat(val), [] );
 			var all_channels = results.map( result => result[1] ).reduce( (acc, val) => acc.concat(val), [] );
 			var guilds = [];
@@ -311,8 +321,10 @@ function removeSettings(msg) {
 					if ( !all_guilds.includes(row.guild) ) {
 						if ( !row.channel ) {
 							if ( patreons.hasOwnProperty(row.guild) || voice.hasOwnProperty(row.guild) ) {
-								msg.client.shard.broadcastEval( `delete global.patreons['${row.guild}'];
-								delete global.voice['${row.guild}'];` );
+								msg.client.shard.broadcastEval( (discordClient, evalData) => {
+									delete global.patreons[evalData];
+									delete global.voice[evalData];
+								}, {context: row.guild} );
 							}
 							return guilds.push(row.guild);
 						}

+ 75 - 44
cmds/get.js

@@ -1,4 +1,4 @@
-const {MessageEmbed, Util, ShardClientUtil: {shardIDForGuildID}} = require('discord.js');
+const {MessageEmbed, Util, ShardClientUtil: {shardIdForGuildId}, Permissions: {FLAGS}} = require('discord.js');
 const {defaultSettings, defaultPermissions} = require('../util/default.json');
 const {escapeFormatting} = require('../util/functions.js');
 var db = require('../util/database.js');
@@ -14,21 +14,30 @@ var db = require('../util/database.js');
  */
 async function cmd_get(lang, msg, args, line, wiki) {
 	var id = args.join().replace( /^\\?<(?:@!?|#)(\d+)>$/, '$1' );
-	if ( /^\d+$/.test(id) ) {
-		var guild = await msg.client.shard.broadcastEval( `if ( this.guilds.cache.has('${id}') ) {
-			var guild = this.guilds.cache.get('${id}');
-			( {
-				name: guild.name, id: guild.id, memberCount: guild.memberCount,
-				ownerID: guild.ownerID, owner: guild.owner?.user?.tag,
-				channel: guild.publicUpdatesChannelID, icon: guild.iconURL({dynamic:true}),
-				permissions: guild.me.permissions.missing(${defaultPermissions}),
-				pause: global.pause.hasOwnProperty(guild.id), voice: global.voice.hasOwnProperty(guild.id),
-				shardId: global.shardId
-			} )
-		}`, shardIDForGuildID(id, msg.client.shard.count) );
+	if ( !/^\d+$/.test(id) ) {
+		if ( !msg.channel.isGuild() || !pause[msg.guildId] ) this.LINK(lang, msg, line, wiki);
+		return;
+	}
+	try {
+		var guild = await msg.client.shard.broadcastEval( (discordClient, evalData) => {
+			if ( discordClient.guilds.cache.has(evalData.id) ) {
+				var guild = discordClient.guilds.cache.get(evalData.id);
+				return {
+					name: guild.name, id: guild.id, memberCount: guild.memberCount,
+					ownerId: guild.ownerId, owner: discordClient.users.cache.get(guild.ownerId)?.tag,
+					channel: guild.publicUpdatesChannelId, icon: guild.iconURL({dynamic:true}),
+					permissions: guild.me.permissions.missing(evalData.defaultPermissions),
+					pause: global.pause.hasOwnProperty(guild.id), voice: global.voice.hasOwnProperty(guild.id),
+					shardId: process.env.SHARDS
+				};
+			}
+		}, {
+			context: {id, defaultPermissions},
+			shard: shardIdForGuildId(id, msg.client.shard.count)
+		} );
 		if ( guild ) {
 			var guildname = ['Guild:', escapeFormatting(guild.name) + ' `' + guild.id + '`' + ( guild.pause ? '\\*' : '' )];
-			var guildowner = ['Owner:', ( guild.owner ? escapeFormatting(guild.owner) + ' ' : '' ) + '`' + guild.ownerID + '` <@' + guild.ownerID + '>'];
+			var guildowner = ['Owner:', ( guild.owner ? escapeFormatting(guild.owner) + ' ' : '' ) + '`' + guild.ownerId + '` <@' + guild.ownerId + '>'];
 			var guildsize = ['Size:', guild.memberCount + ' members'];
 			var guildshard = ['Shard:', guild.shardId];
 			var guildpermissions = ['Missing permissions:', ( guild.permissions.length ? '`' + guild.permissions.join('`, `') + '`' : '*none*' )];
@@ -51,41 +60,58 @@ async function cmd_get(lang, msg, args, line, wiki) {
 					if ( guild.channel ) embed.addField( guildchannel[0], guildchannel[1] );
 					var split = Util.splitMessage( guildsettings[1], {char:',\n',maxLength:1000,prepend:'```json\n',append:',\n```'} );
 					if ( split.length > 5 ) {
-						msg.sendChannel( '', {embed}, true );
-						msg.sendChannel( guildsettings.join(' '), {split:{char:',\n',prepend:'```json\n',append:',\n```'}}, true );
+						msg.sendChannel( {embeds: [embed]}, true );
+						Util.splitMessage( guildsettings.join(' '), {
+							char: ',\n',
+							maxLength: 2000,
+							prepend: '```json\n',
+							append: ',\n```'
+						} ).forEach( textpart => msg.sendChannel( textpart, true ) );
 					}
 					else {
-						split.forEach( guildsettingspart => embed.addField( guildsettings[0], guildsettingspart ) );
-						msg.sendChannel( '', {embed}, true );
+						split.forEach( textpart => embed.addField( guildsettings[0], textpart ) );
+						msg.sendChannel( {embeds: [embed]}, true );
 					}
 				}
 				else {
 					var text = guildname.join(' ') + '\n' + guildowner.join(' ') + '\n' + guildsize.join(' ') + '\n' + guildshard.join(' ') + '\n' + guildpermissions.join(' ') + ( guild.channel ? '\n' + guildchannel.join(' ') : '' ) + '\n' + guildsettings.join(' ');
-					msg.sendChannel( text, {split:{char:',\n',prepend:'```json\n',append:',\n```'}}, true );
+					Util.splitMessage( text, {
+						char: ',\n',
+						maxLength: 2000,
+						prepend: '```json\n',
+						append: ',\n```'
+					} ).forEach( textpart => msg.sendChannel( textpart, true ) );
 				}
 			} );
 		}
 		
-		var channel = await msg.client.shard.broadcastEval( `if ( this.channels.cache.filter( channel => channel.isGuild() || channel.type === 'category' ).has('${id}') ) {
-			var {name, id, type, parentID, guild: {name: guild, id: guildID, me}} = this.channels.cache.get('${id}');
-			( {
-				name, id, type, parentID, guild, guildID,
-				permissions: me.permissionsIn(id).missing(${defaultPermissions}),
-				pause: global.pause.hasOwnProperty(guildID),
-				shardId: global.shardId
-			} )
-		}` ).then( results => results.find( result => result ) );
+		var channel = await msg.client.shard.broadcastEval( (discordClient, evalData) => {
+			if ( discordClient.channels.cache.filter( channel => channel.isGuild() || channel.type === 'GUILD_CATEGORY' ).has(evalData.id) ) {
+				var channel = discordClient.channels.cache.get(evalData.id);
+				return {
+					name: channel.name, id: channel.id, type: channel.type, parentId: channel.parentId,
+					isThread: channel.isThread(), threadParentId: channel.parent?.parentId,
+					guild: channel.guild.name, guildId: channel.guildId,
+					permissions: channel.guild.me.permissionsIn(channel.id).missing(evalData.defaultPermissions),
+					pause: global.pause.hasOwnProperty(channel.guildId),
+					shardId: process.env.SHARDS
+				};
+			}
+		}, {context: {id, defaultPermissions}} ).then( results => results.find( result => result ) );
 		if ( channel ) {
-			var channelguild = ['Guild:', escapeFormatting(channel.guild) + ' `' + channel.guildID + '`' + ( channel.pause ? '\\*' : '' )];
+			var channelguild = ['Guild:', escapeFormatting(channel.guild) + ' `' + channel.guildId + '`' + ( channel.pause ? '\\*' : '' )];
 			var channelname = ['Channel:', '#' + escapeFormatting(channel.name) + ' `' + channel.id + '` <#' + channel.id + '>'];
-			var channeldetails = ['Details:', '`' + channel.type + '`' + ( channel.parentID ? ' – `' + channel.parentID + '` <#' + channel.parentID + '>' : '' )];
+			var channeldetails = ['Details:', '`' + channel.type + '`' + ( channel.parentId ? ' – `' + channel.parentId + '` <#' + channel.parentId + '>' + ( channel.isThread ? ' – `' + channel.threadParentId + '` <#' + channel.threadParentId + '>' : '' ) : '' )];
 			var channelpermissions = ['Missing permissions:', ( channel.permissions.length ? '`' + channel.permissions.join('`, `') + '`' : '*none*' )];
 			var channellang = ['Language:', '*unknown*'];
 			var channelwiki = ['Default Wiki:', '*unknown*'];
 			var channelrole = ['Minimal Role:', '*unknown*'];
 			var channelinline = ['Inline commands:', '*unknown*'];
 			
-			return db.query( 'SELECT wiki, lang, role, inline FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', [channel.guildID, channel.id, '#' + ( channel.type === 'category' ? channel.id : channel.parentID )] ).then( ({rows:[row]}) => {
+			let sqlargs = [channel.guildId];
+			if ( channel.isThread ) sqlargs.push(channel.parentId, '#' + channel.threadParentId);
+			else sqlargs.push(channel.id, '#' + ( channel.type === 'GUILD_CATEGORY' ? channel.id : channel.parentId ));
+			return db.query( 'SELECT wiki, lang, role, inline FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', sqlargs ).then( ({rows:[row]}) => {
 				if ( row ) {
 					channellang[1] = row.lang;
 					channelwiki[1] = row.wiki;
@@ -109,7 +135,7 @@ async function cmd_get(lang, msg, args, line, wiki) {
 				else {
 					text += channelguild.join(' ') + '\n' + channelname.join(' ') + '\n' + channeldetails.join(' ') + '\n' + channelpermissions.join(' ') + '\n' + channellang.join(' ') + '\n' + channelwiki[0] + ' <' + channelwiki[1] + '>\n' + channelrole.join(' ') + '\n' + channelinline.join(' ');
 				}
-				msg.sendChannel( text, {embed}, true );
+				msg.sendChannel( {content: text, embeds: [embed]}, true );
 			} );
 		}
 		
@@ -117,15 +143,17 @@ async function cmd_get(lang, msg, args, line, wiki) {
 		if ( user ) {
 			var username = ['User:', escapeFormatting(user.tag) + ' `' + user.id + '` <@' + user.id + '>'];
 			var guildlist = ['Guilds:', '*none*'];
-			var guilds = await msg.client.shard.broadcastEval( `this.guilds.cache.filter( guild => guild.members.cache.has('${user.id}') ).map( guild => {
-				var member = guild.members.cache.get('${user.id}');
-				return {
-					name: guild.name,
-					id: guild.id,
-					isAdmin: member.permissions.has('MANAGE_GUILD'),
-					shardId: global.shardId
-				}
-			} )` ).then( results => {
+			var guilds = await msg.client.shard.broadcastEval( (discordClient, evalData) => {
+				return discordClient.guilds.cache.filter( guild => guild.members.cache.has(evalData.user) ).map( guild => {
+					var member = guild.members.cache.get(evalData.user);
+					return {
+						name: guild.name,
+						id: guild.id,
+						isAdmin: member.permissions.has(evalData.MANAGE_GUILD),
+						shardId: process.env.SHARDS
+					}
+				} );
+			}, {context: {user: user.id, MANAGE_GUILD: FLAGS.MANAGE_GUILD.toString()}} ).then( results => {
 				return results.reduce( (acc, val) => acc.concat(val), [] ).map( user_guild => {
 					return escapeFormatting(user_guild.name) + ' `' + user_guild.id + '`' + ( user_guild.isAdmin ? '\\*' : '' );
 				} );
@@ -136,11 +164,14 @@ async function cmd_get(lang, msg, args, line, wiki) {
 			var embed = null;
 			if ( msg.showEmbed() ) embed = new MessageEmbed().setThumbnail( user.displayAvatarURL({dynamic:true}) ).addField( username[0], username[1] ).addField( guildlist[0], guildlist[1] );
 			else text += username.join(' ') + '\n' + guildlist.join('\n');
-			return msg.sendChannel( text, {embed}, true );
+			return msg.sendChannel( {content: text, embeds: [embed]}, true );
 		}
 		
-		msg.replyMsg( 'I couldn\'t find a result for `' + id + '`', {}, true );
-	} else if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
+		msg.replyMsg( 'I couldn\'t find a result for `' + id + '`', true );
+	} catch ( error ) {
+		log_error(error);
+		msg.reactEmoji('error');
+	}
 }
 
 module.exports = {

+ 12 - 11
cmds/help.js

@@ -1,3 +1,4 @@
+const {Util} = require('discord.js');
 const help_server = require('../functions/helpserver.js');
 const {wikis: mcw} = require('./minecraft/commands.json');
 
@@ -79,7 +80,7 @@ const restrictions = {
  * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  */
 function cmd_help(lang, msg, args, line, wiki) {
-	if ( msg.channel.isGuild() && pause[msg.guild.id] && ( args.join('') || !msg.isAdmin() ) ) return;
+	if ( msg.channel.isGuild() && pause[msg.guildId] && ( args.join('') || !msg.isAdmin() ) ) return;
 	if ( msg.isAdmin() && msg.defaultSettings ) help_server(lang, msg);
 	var isMinecraft = mcw.hasOwnProperty(wiki.href);
 	var maxLength = ( ['hi', 'bn'].includes( lang.lang ) ? 480 : 2000 );
@@ -96,17 +97,17 @@ function cmd_help(lang, msg, args, line, wiki) {
 				if ( process.env.READONLY ) cmdlist = msg.author.toString() + ', ' + lang.get('general.readonly') + '\n' + process.env.invite + '\n\n' + cmdlist;
 				cmdlist += formathelp(helplist.admin, msg, lang);
 				cmdlist += '\n\n🔸 ' + lang.get('help.adminfooter');
-				if ( process.env.dashboard ) cmdlist += '\n\t\t' + new URL(( msg.channel.isGuild() ? `/guild/${msg.guild.id}/settings` : '/' ), process.env.dashboard).href;
-				msg.sendChannel( cmdlist, {split:{char:'\n🔹',prepend:'🔹',maxLength}} );
+				if ( process.env.dashboard ) cmdlist += '\n\t\t' + new URL(( msg.channel.isGuild() ? `/guild/${msg.guildId}/settings` : '/' ), process.env.dashboard).href;
+				Util.splitMessage( cmdlist, {char: '\n🔹', maxLength, prepend: '🔹'} ).forEach( textpart => msg.sendChannel( textpart ) );
 			}
 			else {
-				msg.replyMsg( lang.get('help.noadmin') );
+				msg.replyMsg( {content: lang.get('help.noadmin'), allowedMentions: {repliedUser: false}} );
 			}
 		}
 		else if ( cmd === 'minecraft' ) {
 			var cmdlist = '<' + ( isMinecraft ? wiki : 'https://minecraft.fandom.com/' ) + '>\n';
 			cmdlist += formathelp(helplist.minecraft, msg, lang);
-			msg.sendChannel( cmdlist, {split:{char:'\n🔹',prepend:'🔹',maxLength}} );
+			Util.splitMessage( cmdlist, {char: '\n🔹', maxLength, prepend: '🔹'} ).forEach( textpart => msg.sendChannel( textpart ) );
 		}
 		else if ( helpmap.hasOwnProperty(cmd) && 
 		( !restrictions.fandom.includes( cmd ) || wiki.isFandom(false) ) && 
@@ -114,14 +115,14 @@ function cmd_help(lang, msg, args, line, wiki) {
 		( !restrictions.admin.includes( cmd ) || msg.isAdmin() ) ) {
 			var cmdlist = formathelp(helpmap[cmd], msg, lang);
 			if ( !cmdlist.length ) msg.reactEmoji('❓');
-			else msg.sendChannel( cmdlist, {split:{char:'\n🔹',prepend:'🔹',maxLength}} );
+			else Util.splitMessage( cmdlist, {char: '\n🔹', maxLength, prepend: '🔹'} ).forEach( textpart => msg.sendChannel( textpart ) );
 		}
 		else msg.reactEmoji('❓');
 	}
-	else if ( msg.isAdmin() && pause[msg.guild.id] ) {
+	else if ( msg.isAdmin() && pause[msg.guildId] ) {
 		var cmdlist = lang.get('help.pause') + '\n';
 		cmdlist += formathelp(helplist.pause, msg, lang);
-		msg.sendChannel( cmdlist, {split:{char:'\n🔹',prepend:'🔹',maxLength}}, true );
+		Util.splitMessage( cmdlist, {char: '\n🔹', maxLength, prepend: '🔹'} ).forEach( textpart => msg.sendChannel( textpart ) );
 	}
 	else {
 		var cmdlist = lang.get('help.all') + '\n';
@@ -132,7 +133,7 @@ function cmd_help(lang, msg, args, line, wiki) {
 			}
 		} );
 		cmdlist += '\n🔸 ' + lang.get('help.footer');
-		msg.sendChannel( cmdlist, {split:{char:'\n🔹',prepend:'🔹',maxLength}} );
+		Util.splitMessage( cmdlist, {char: '\n🔹', maxLength, prepend: '🔹'} ).forEach( textpart => msg.sendChannel( textpart ) );
 	}
 }
 
@@ -143,12 +144,12 @@ function cmd_help(lang, msg, args, line, wiki) {
  * @param {import('../util/i18n.js')} lang - The user language.
  */
 function formathelp(messages, msg, lang) {
-	var prefix = ( msg.channel.isGuild() && patreons[msg.guild.id] || process.env.prefix );
+	var prefix = ( msg.channel.isGuild() && patreons[msg.guildId] || process.env.prefix );
 	var mention = '@' + ( msg.channel.isGuild() ? msg.guild.me.displayName : msg.client.user.username );
 	return messages.filter( message => {
 		if ( restrictions.inline.includes( message ) && msg.noInline ) return false;
 		if ( !restrictions.patreon.includes( message ) ) return true;
-		return ( msg.channel.isGuild() && patreons[msg.guild.id] );
+		return ( msg.channel.isGuild() && patreons[msg.guildId] );
 	} ).map( message => {
 		var cmd = message.split('.')[0];
 		var intro = ( restrictions.inline.includes( message ) ? '' : prefix );

+ 7 - 3
cmds/link.js

@@ -16,14 +16,18 @@ const phabricator = require('../functions/phabricator.js');
 function cmd_link(lang, msg, title, wiki, cmd = '') {
 	if ( msg.isAdmin() && msg.defaultSettings ) help_setup(lang, msg);
 	if ( /^\|\|(?:(?!\|\|).)+\|\|$/.test(title) ) {
-		title = title.substring( 2, title.length - 2);
+		title = title.substring(2, title.length - 2);
 		var spoiler = '||';
 	}
+	if ( /^<[^<>]+>$/.test(title) ) {
+		title = title.substring(1, title.length - 1);
+		var noEmbed = true;
+	}
 	msg.reactEmoji('⏳').then( reaction => {
 		if ( /^phabricator\.(wikimedia|miraheze)\.org$/.test(wiki.hostname) ) {
-			return phabricator(lang, msg, wiki, new URL('/' + title, wiki), reaction, spoiler);
+			return phabricator(lang, msg, wiki, new URL('/' + title, wiki), reaction, spoiler, noEmbed);
 		}
-		else check_wiki.general(lang, msg, title, wiki, cmd, reaction, spoiler);
+		else check_wiki.general(lang, msg, title, wiki, cmd, reaction, spoiler, noEmbed);
 	} );
 }
 

+ 38 - 31
cmds/minecraft/bug.js

@@ -1,5 +1,5 @@
 const {MessageEmbed} = require('discord.js');
-const {escapeFormatting, limitLength} = require('../../util/functions.js');
+const {got, escapeFormatting, limitLength} = require('../../util/functions.js');
 
 /**
  * Sends a Minecraft issue.
@@ -11,8 +11,9 @@ const {escapeFormatting, limitLength} = require('../../util/functions.js');
  * @param {String} cmd - The command at this point.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  * @param {String} spoiler - If the response is in a spoiler.
+ * @param {Boolean} noEmbed - If the response should be without an embed.
  */
-function minecraft_bug(lang, msg, wiki, args, title, cmd, reaction, spoiler) {
+function minecraft_bug(lang, msg, wiki, args, title, cmd, reaction, spoiler, noEmbed) {
 	var invoke = args[0];
 	args = args.slice(1);
 	if ( invoke && /\d+$/.test(invoke) && !args.length ) {
@@ -48,28 +49,31 @@ function minecraft_bug(lang, msg, wiki, args, title, cmd, reaction, spoiler) {
 					var summary = escapeFormatting(body.fields.summary);
 					if ( summary.length > 250 ) summary = summary.substring(0, 250) + '\u2026';
 					var description = parse_links( ( body.fields.description || '' ).replace( /\{code\}/g, '```' ) );
-					var embed = new MessageEmbed().setAuthor( 'Mojira' ).setTitle( summary ).setURL( baseBrowseUrl + body.key ).setDescription( limitLength(description, 2000, 20) );
-					var links = body.fields.issuelinks.filter( link => link.outwardIssue || ( link.inwardIssue && link.type.name !== 'Duplicate' ) );
-					if ( links.length ) {
-						var linkList = lang.get('minecraft.issue_link');
-						var extralinks = [];
-						links.forEach( link => {
-							var ward = ( link.outwardIssue ? 'outward' : 'inward' );
-							var issue = link[ward + 'Issue']; // looks for property (in|out)wardIssue
-							var name = ( linkList?.[link.type.name]?.[ward]?.replaceSave( /\$1/g, issue.key ) || link.type[ward] + ' ' + issue.key );
-							var status = issue.fields.status.name;
-							var value = ( statusList?.[status] || status ) + ': [' + escapeFormatting(issue.fields.summary) + '](' + baseBrowseUrl + issue.key + ')';
-							if ( embed.fields.length < 25 && ( embed.length + name.length + value.length ) < 6000 ) embed.addField( name, value );
-							else extralinks.push({name,value,inline:false});
-						} );
-						if ( extralinks.length ) embed.setFooter( lang.get('minecraft.more', extralinks.length.toLocaleString(lang.get('dateformat')), extralinks.length) );
+					var embed = null;
+					if ( msg.showEmbed() && !noEmbed ) {
+						embed = new MessageEmbed().setAuthor( 'Mojira' ).setTitle( summary ).setURL( baseBrowseUrl + body.key ).setDescription( limitLength(description, 2000, 20) );
+						var links = body.fields.issuelinks.filter( link => link.outwardIssue || ( link.inwardIssue && link.type.name !== 'Duplicate' ) );
+						if ( links.length ) {
+							var linkList = lang.get('minecraft.issue_link');
+							var extralinks = [];
+							links.forEach( link => {
+								var ward = ( link.outwardIssue ? 'outward' : 'inward' );
+								var issue = link[ward + 'Issue']; // looks for property (in|out)wardIssue
+								var name = ( linkList?.[link.type.name]?.[ward]?.replaceSave( /\$1/g, issue.key ) || link.type[ward] + ' ' + issue.key );
+								var status = issue.fields.status.name;
+								var value = ( statusList?.[status] || status ) + ': [' + escapeFormatting(issue.fields.summary) + '](' + baseBrowseUrl + issue.key + ')';
+								if ( embed.fields.length < 25 && ( embed.length + name.length + value.length ) < 6000 ) embed.addField( name, value );
+								else extralinks.push({name,value,inline:false});
+							} );
+							if ( extralinks.length ) embed.setFooter( lang.get('minecraft.more', extralinks.length.toLocaleString(lang.get('dateformat')), extralinks.length) );
+						}
 					}
 					var status = ( body.fields.resolution ? body.fields.resolution.name : body.fields.status.name );
 					var fixed = '';
 					if ( body.fields.resolution && body.fields.fixVersions && body.fields.fixVersions.length ) {
 						fixed = '\n' + lang.get('minecraft.fixed', body.fields.fixVersions.length) + ' ' + body.fields.fixVersions.map( v => v.name ).join(', ');
 					}
-					msg.sendChannel( spoiler + '**' + ( statusList?.[status] || status ) + '**: ' + escapeFormatting(body.fields.summary) + '\n<' + baseBrowseUrl + body.key + '>' + fixed + spoiler, {embed} );
+					msg.sendChannel( {content: spoiler + '**' + ( statusList?.[status] || status ) + '**: ' + escapeFormatting(body.fields.summary) + '\n<' + baseBrowseUrl + body.key + '>' + fixed + spoiler, embeds: [embed]} );
 				}
 			}
 		}, error => {
@@ -107,21 +111,24 @@ function minecraft_bug(lang, msg, wiki, args, title, cmd, reaction, spoiler) {
 					msg.reactEmoji('error');
 				}
 				else {
-					var embed = new MessageEmbed().setAuthor( 'Mojira' ).setTitle( args.join(' ') ).setURL( uri );
-					if ( body.total > 0 ) {
-						var statusList = lang.get('minecraft.status');
-						body.issues.forEach( bug => {
-							var status = ( bug.fields.resolution ? bug.fields.resolution.name : bug.fields.status.name );
-							var value = ( statusList?.[status] || status ) + ': [' + escapeFormatting(bug.fields.summary) + '](https://bugs.mojang.com/browse/' + bug.key + ')';
-							embed.addField( bug.key, value );
-						} );
-						if ( body.total > 25 ) {
-							var extrabugs = body.total - 25;
-							embed.setFooter( lang.get('minecraft.more', extrabugs.toLocaleString(lang.get('dateformat')), extrabugs) );
+					var embed = null;
+					if ( msg.showEmbed() && !noEmbed ) {
+						embed = new MessageEmbed().setAuthor( 'Mojira' ).setTitle( args.join(' ') ).setURL( uri );
+						if ( body.total > 0 ) {
+							var statusList = lang.get('minecraft.status');
+							body.issues.forEach( bug => {
+								var status = ( bug.fields.resolution ? bug.fields.resolution.name : bug.fields.status.name );
+								var value = ( statusList?.[status] || status ) + ': [' + escapeFormatting(bug.fields.summary) + '](https://bugs.mojang.com/browse/' + bug.key + ')';
+								embed.addField( bug.key, value );
+							} );
+							if ( body.total > 25 ) {
+								var extrabugs = body.total - 25;
+								embed.setFooter( lang.get('minecraft.more', extrabugs.toLocaleString(lang.get('dateformat')), extrabugs) );
+							}
 						}
 					}
 					var total = '**' + args.join(' ') + ':** ' + lang.get('minecraft.total', body.total.toLocaleString(lang.get('dateformat')), body.total);
-					msg.sendChannel( spoiler + total + '\n<' + uri + '>' + spoiler, {embed} );
+					msg.sendChannel( {content: spoiler + total + '\n<' + uri + '>' + spoiler, embeds: [embed]} );
 				}
 			}
 		}, error => {
@@ -133,7 +140,7 @@ function minecraft_bug(lang, msg, wiki, args, title, cmd, reaction, spoiler) {
 	}
 	else {
 		msg.notMinecraft = true;
-		this.WIKI.general(lang, msg, title, wiki, cmd, reaction, spoiler);
+		this.WIKI.general(lang, msg, title, wiki, cmd, reaction, spoiler, noEmbed);
 	}
 }
 

+ 5 - 4
cmds/minecraft/command.js

@@ -8,15 +8,16 @@
  * @param {String} cmd - The command at this point.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  * @param {String} spoiler - If the response is in a spoiler.
+ * @param {Boolean} noEmbed - If the response should be without an embed.
  */
-function minecraft_command(lang, msg, wiki, args, title, cmd, reaction, spoiler) {
+function minecraft_command(lang, msg, wiki, args, title, cmd, reaction, spoiler, noEmbed) {
 	if ( args.join('') ) {
-		if ( args[0].startsWith( '/' ) ) this.SYNTAX(lang, msg, wiki, args[0].substring(1), args.slice(1), title, cmd, reaction, spoiler);
-		else this.SYNTAX(lang, msg, wiki, args[0], args.slice(1), title, cmd, reaction, spoiler);
+		if ( args[0].startsWith( '/' ) ) this.SYNTAX(lang, msg, wiki, args[0].substring(1), args.slice(1), title, cmd, reaction, spoiler, noEmbed);
+		else this.SYNTAX(lang, msg, wiki, args[0], args.slice(1), title, cmd, reaction, spoiler, noEmbed);
 	}
 	else {
 		msg.notMinecraft = true;
-		this.WIKI.general(lang, msg, title, wiki, cmd, reaction, spoiler);
+		this.WIKI.general(lang, msg, title, wiki, cmd, reaction, spoiler, noEmbed);
 	}
 }
 

+ 6 - 3
cmds/minecraft/syntax.js

@@ -1,3 +1,5 @@
+const {Util} = require('discord.js');
+const {got} = require('../../util/functions.js');
 const Wiki = require('../../util/wiki.js');
 const commands = require('./commands.json');
 
@@ -12,8 +14,9 @@ const commands = require('./commands.json');
  * @param {String} cmd - The command at this point.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  * @param {String} spoiler - If the response is in a spoiler.
+ * @param {Boolean} noEmbed - If the response should be without an embed.
  */
-function minecraft_syntax(lang, msg, wiki, mccmd, args, title, cmd, reaction, spoiler) {
+function minecraft_syntax(lang, msg, wiki, mccmd, args, title, cmd, reaction, spoiler, noEmbed) {
 	mccmd = mccmd.toLowerCase();
 	var aliasCmd = ( commands.aliases[mccmd] || mccmd );
 	var cmdpage = commands.wikis[wiki.href];
@@ -65,14 +68,14 @@ function minecraft_syntax(lang, msg, wiki, mccmd, args, title, cmd, reaction, sp
 		}, error => {
 			console.log( '- Error while getting the command page: ' + error );
 		} ).finally( () => {
-			msg.sendChannel( spoiler + '```md\n' + cmdSyntax + '```<' + wiki.toLink(( cmdpage.endsWith( '/' ) ? cmdpage + aliasCmd : cmdpage ), '', ( cmdpage.endsWith( '/' ) ? '' : aliasCmd )) + '>' + spoiler, {split:{maxLength:2000,prepend:spoiler + '```md\n',append:'```' + spoiler}} );
+			Util.splitMessage( spoiler + '```md\n' + cmdSyntax + '```<' + wiki.toLink(( cmdpage.endsWith( '/' ) ? cmdpage + aliasCmd : cmdpage ), '', ( cmdpage.endsWith( '/' ) ? '' : aliasCmd )) + '>' + spoiler, {maxLength: 2000, prepend: spoiler + '```md\n', append: '```' + spoiler} ).forEach( textpart => msg.sendChannel( textpart ) );
 
 			if ( reaction ) reaction.removeEmoji();
 		} );
 	}
 	else {
 		msg.notMinecraft = true;
-		this.WIKI.general(lang, msg, title, wiki, cmd, reaction, spoiler);
+		this.WIKI.general(lang, msg, title, wiki, cmd, reaction, spoiler, noEmbed);
 	}
 }
 

+ 117 - 85
cmds/patreon.js

@@ -1,4 +1,4 @@
-const {ShardClientUtil: {shardIDForGuildID}} = require('discord.js');
+const {ShardClientUtil: {shardIdForGuildId}} = require('discord.js');
 const {defaultPermissions, limit: {verification: verificationLimit, rcgcdw: rcgcdwLimit}} = require('../util/default.json');
 var db = require('../util/database.js');
 
@@ -11,85 +11,108 @@ var db = require('../util/database.js');
  * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  */
 function cmd_patreon(lang, msg, args, line, wiki) {
-	if ( !( process.env.channel.split('|').includes( msg.channel.id ) && args.join('') ) ) {
-		if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
+	if ( !( process.env.channel.split('|').includes( msg.channelId ) && args.join('') ) ) {
+		if ( !msg.channel.isGuild() || !pause[msg.guildId] ) this.LINK(lang, msg, line, wiki);
 		return;
 	}
 	
-	if ( args[0] === 'enable' && /^\d+$/.test(args.slice(1).join(' ')) ) return msg.client.shard.broadcastEval( `this.guilds.cache.get('${args[1]}')?.name`, shardIDForGuildID(args[1], msg.client.shard.count) ).then( guild => {
-		if ( !guild ) return msg.client.generateInvite({
-			permissions: defaultPermissions,
-			guild: args[1]
-		}).then( invite => {
-			msg.replyMsg( 'I\'m not on a server with the id `' + args[1] + '`.\n<' + invite + '%20applications.commands' + '>', {}, true )
-		}, log_error );
-		if ( patreons[args[1]] ) return msg.replyMsg( '"' + guild + '" has the patreon features already enabled.', {}, true );
+	if ( args[0] === 'enable' && /^\d+$/.test(args.slice(1).join(' ')) ) return msg.client.shard.broadcastEval( (discordClient, evalData) => {
+		return discordClient.guilds.cache.get(evalData)?.name;
+	}, {
+		context: args[1],
+		shard: shardIdForGuildId(args[1], msg.client.shard.count)
+	} ).then( guild => {
+		if ( !guild ) {
+			let invite = msg.client.generateInvite({
+				scopes: ['bot', 'applications.commands'],
+				permissions: defaultPermissions,
+				guild: args[1],
+				disableGuildSelect: true
+			});
+			return msg.replyMsg( 'I\'m not on a server with the id `' + args[1] + '`.\n<' + invite + '>', true );
+		}
+		if ( patreons[args[1]] ) return msg.replyMsg( '"' + guild + '" has the patreon features already enabled.', true );
 		db.query( 'SELECT count, COUNT(guild) guilds FROM patreons LEFT JOIN discord ON discord.patreon = patreons.patreon WHERE patreons.patreon = $1 GROUP BY patreons.patreon', [msg.author.id] ).then( ({rows:[row]}) => {
-			if ( !row ) return msg.replyMsg( 'you can\'t have any servers.', {}, true );
-			if ( row.count <= row.guilds ) return msg.replyMsg( 'you already reached your maximal server count.', {}, true );
-			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+			if ( !row ) return msg.replyMsg( 'You can\'t have any servers.', true );
+			if ( row.count <= row.guilds ) return msg.replyMsg( 'You already reached your maximal server count.', true );
+			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 			db.query( 'UPDATE discord SET patreon = $1 WHERE guild = $2 AND channel IS NULL', [msg.author.id, args[1]] ).then( ({rowCount}) => {
 				if ( !rowCount ) return db.query( 'INSERT INTO discord(main, guild, patreon) VALUES($1, $1, $2)', [args[1], msg.author.id] ).then( () => {
 					console.log( '- Guild successfully added.' );
-					msg.client.shard.broadcastEval( `global.patreons['${args[1]}'] = '${process.env.prefix}'` );
-					msg.replyMsg( 'the patreon features are now enabled on "' + guild + '".', {}, true );
+					msg.client.shard.broadcastEval( (discordClient, evalData) => {
+						global.patreons[evalData.guild] = evalData.prefix;
+					}, {context: {guild: args[1], prefix: process.env.prefix}} );
+					msg.replyMsg( 'The patreon features are now enabled on "' + guild + '".', true );
 				}, dberror => {
 					console.log( '- Error while adding the guild: ' + dberror );
-					msg.replyMsg( 'I got an error while updating the server, please try again later.', {}, true );
+					msg.replyMsg( 'I got an error while updating the server, please try again later.', true );
 				} );
 				console.log( '- Guild successfully updated.' );
-				msg.client.shard.broadcastEval( `global.patreons['${args[1]}'] = '${process.env.prefix}'` );
-				msg.replyMsg( 'the patreon features are now enabled on "' + guild + '".', {}, true );
+				msg.client.shard.broadcastEval( (discordClient, evalData) => {
+					global.patreons[evalData.guild] = evalData.prefix;
+				}, {context: {guild: args[1], prefix: process.env.prefix}} );
+				msg.replyMsg( 'The patreon features are now enabled on "' + guild + '".', true );
 			}, dberror => {
 				console.log( '- Error while updating the guild: ' + dberror );
-				msg.replyMsg( 'I got an error while updating the server, please try again later.', {}, true );
+				msg.replyMsg( 'I got an error while updating the server, please try again later.', true );
 			} );
 		}, dberror => {
 			console.log( '- Error while getting the patreon: ' + dberror );
-			msg.replyMsg( 'I got an error while searching for you, please try again later.', {}, true );
+			msg.replyMsg( 'I got an error while searching for you, please try again later.', true );
 		} );
 	} );
 	
-	if ( args[0] === 'disable' && /^\d+$/.test(args.slice(1).join(' ')) ) return msg.client.shard.broadcastEval( `this.guilds.cache.get('${args[1]}')?.name`, shardIDForGuildID(args[1], msg.client.shard.count) ).then( guild => {
-		if ( !guild ) return msg.replyMsg( 'I\'m not on a server with the id `' + args[1] + '`.', {}, true );
-		if ( !patreons[args[1]] ) return msg.replyMsg( '"' + guild + '" doesn\'t have the patreon features enabled.', {}, true );
+	if ( args[0] === 'disable' && /^\d+$/.test(args.slice(1).join(' ')) ) return msg.client.shard.broadcastEval( (discordClient, evalData) => {
+		return discordClient.guilds.cache.get(evalData)?.name;
+	}, {
+		context: args[1],
+		shard: shardIdForGuildId(args[1], msg.client.shard.count)
+	} ).then( guild => {
+		if ( !guild ) return msg.replyMsg( 'I\'m not on a server with the id `' + args[1] + '`.', true );
+		if ( !patreons[args[1]] ) return msg.replyMsg( '"' + guild + '" doesn\'t have the patreon features enabled.', true );
 		return db.connect().then( client => {
 			return client.query( 'SELECT lang, role, inline FROM discord WHERE guild = $1 AND patreon = $2', [args[1], msg.author.id] ).then( ({rows:[row]}) => {
 				if ( !row ) {
-					msg.replyMsg( 'you didn\'t enable the patreon features for "' + guild + '"!', {}, true );
+					msg.replyMsg( 'You didn\'t enable the patreon features for "' + guild + '"!', true );
 					return Promise.reject();
 				}
 				if ( process.env.READONLY ) {
-					msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+					msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 					return Promise.reject();
 				}
 				return client.query( 'UPDATE discord SET lang = $1, role = $2, inline = $3, prefix = $4, patreon = NULL WHERE guild = $5', [row.lang, row.role, row.inline, process.env.prefix, args[1]] ).then( () => {
 					console.log( '- Guild successfully updated.' );
-					msg.client.shard.broadcastEval( `delete global.patreons['${args[1]}']` );
-					msg.replyMsg( 'the patreon features are now disabled on "' + guild + '".', {}, true );
+					msg.client.shard.broadcastEval( (discordClient, evalData) => {
+						delete global.patreons[evalData];
+					}, {context: args[1]} );
+					msg.replyMsg( 'The patreon features are now disabled on "' + guild + '".', true );
 				}, dberror => {
 					console.log( '- Error while updating the guild: ' + dberror );
-					msg.replyMsg( 'I got an error while searching for the server, please try again later.', {}, true );
+					msg.replyMsg( 'I got an error while searching for the server, please try again later.', true );
 					return Promise.reject();
 				} ).then( () => {
 					return client.query( 'DELETE FROM discord WHERE guild = $1 AND channel LIKE $2 RETURNING channel, wiki', [args[1], '#%'] ).then( ({rows}) => {
 						if ( rows.length ) {
 							console.log( '- Channel categories successfully deleted.' );
-							return msg.client.shard.broadcastEval( `if ( this.guilds.cache.has('${args[1]}') ) {
-								let rows = ${JSON.stringify(rows)};
-								this.guilds.cache.get('${args[1]}').channels.cache.filter( channel => {
-									return ( channel.isGuild() && rows.some( row => {
-										return ( row.channel === '#' + channel.parentID );
-									} ) );
-								} ).map( channel => {
-									return {
-										id: channel.id,
-										wiki: rows.find( row => {
-											return ( row.channel === '#' + channel.parentID );
-										} ).wiki
-									};
-								} )
-							}`, shardIDForGuildID(args[1], msg.client.shard.count) ).then( channels => {
+							return msg.client.shard.broadcastEval( (discordClient, evalData) => {
+								if ( discordClient.guilds.cache.has(evalData.guild) ) {
+									return discordClient.guilds.cache.get(evalData.guild).channels.cache.filter( channel => {
+										return ( channel.isGuild(false) && evalData.rows.some( row => {
+											return ( row.channel === '#' + channel.parentId );
+										} ) );
+									} ).map( channel => {
+										return {
+											id: channel.id,
+											wiki: evalData.rows.find( row => {
+												return ( row.channel === '#' + channel.parentId );
+											} ).wiki
+										};
+									} );
+								}
+							}, {
+								context: {guild: args[1], rows},
+								shard: shardIdForGuildId(args[1], msg.client.shard.count)
+							} ).then( channels => {
 								if ( channels.length ) return Promise.all(channels.map( channel => {
 									return client.query( 'INSERT INTO discord(wiki, guild, channel, lang, role, inline, prefix) VALUES($1, $2, $3, $4, $5, $6, $7)', [channel.wiki, args[1], channel.id, row.lang, row.role, row.inline, process.env.prefix] ).catch( dberror => {
 										if ( dberror.message !== 'duplicate key value violates unique constraint "discord_guild_channel_key"' ) {
@@ -107,7 +130,7 @@ function cmd_patreon(lang, msg, args, line, wiki) {
 				} );
 			}, dberror => {
 				console.log( '- Error while getting the guild: ' + dberror );
-				msg.replyMsg( 'I got an error while searching for the server, please try again later.', {}, true );
+				msg.replyMsg( 'I got an error while searching for the server, please try again later.', true );
 				return Promise.reject();
 			} ).then( () => {
 				return client.query( 'SELECT configid FROM verification WHERE guild = $1 ORDER BY configid ASC OFFSET $2', [args[1], verificationLimit.default] ).then( ({rows}) => {
@@ -149,7 +172,7 @@ function cmd_patreon(lang, msg, args, line, wiki) {
 			} );
 		}, dberror => {
 			console.log( '- Error while connecting to the database client: ' + dberror );
-			msg.replyMsg( 'I got an error while searching for the server, please try again later.', {}, true );
+			msg.replyMsg( 'I got an error while searching for the server, please try again later.', true );
 		} );
 	} );
 	
@@ -157,42 +180,46 @@ function cmd_patreon(lang, msg, args, line, wiki) {
 	
 	if ( args[0] === 'check' ) {
 		if ( !args.slice(1).join('') ) return db.query( 'SELECT count, ARRAY_REMOVE(ARRAY_AGG(guild), NULL) guilds FROM patreons LEFT JOIN discord ON discord.patreon = patreons.patreon WHERE patreons.patreon = $1 GROUP BY patreons.patreon', [msg.author.id] ).then( ({rows:[row]}) => {
-			if ( !row ) return msg.replyMsg( 'you can\'t have any servers.', {}, true );
-			var text = 'you can have up to ' + row.count + ' servers.\n\n';
+			if ( !row ) return msg.replyMsg( 'You can\'t have any servers.', true );
+			var text = 'You can have up to ' + row.count + ' servers.\n\n';
 			if ( row.guilds.length ) {
-				msg.client.shard.broadcastEval( `${JSON.stringify(row.guilds)}.map( guild => this.guilds.cache.get(guild)?.name )` ).then( results => {
+				msg.client.shard.broadcastEval( (discordClient, evalData) => {
+					return evalData.map( guild => discordClient.guilds.cache.get(guild)?.name );
+				}, {context: row.guilds} ).then( results => {
 					var guilds = row.guilds.map( (guild, i) => '`' + guild + '` ' + ( results.find( result => result[i] !== null )?.[i] || '' ) );
 					text += 'Currently you have ' + guilds.length + ' servers:\n' + guilds.join('\n');
 					if ( row.count < guilds.length ) text += '\n\n**You are above your server limit!**';
-					msg.replyMsg( text, {}, true );
+					msg.replyMsg( text, true );
 				} );
 			}
 			else {
 				text += '*You don\'t have any servers yet.*';
-				msg.replyMsg( text, {}, true );
+				msg.replyMsg( text, true );
 			}
 		}, dberror => {
 			console.log( '- Error while getting the patreon: ' + dberror );
-			msg.replyMsg( 'I got an error while searching for you, please try again later.', {}, true );
+			msg.replyMsg( 'I got an error while searching for you, please try again later.', true );
 		} );
 		if ( msg.isOwner() && /^\d+$/.test(args.slice(1).join(' ')) ) return db.query( 'SELECT count, ARRAY_REMOVE(ARRAY_AGG(guild), NULL) guilds FROM patreons LEFT JOIN discord ON discord.patreon = patreons.patreon WHERE patreons.patreon = $1 GROUP BY patreons.patreon', [args[1]] ).then( ({rows:[row]}) => {
-			if ( !row ) return msg.replyMsg( '<@' + args[1] + '> can\'t have any servers.', {}, true );
+			if ( !row ) return msg.replyMsg( '<@' + args[1] + '> can\'t have any servers.', true );
 			var text = '<@' + args[1] + '> can have up to ' + row.count + ' servers.\n\n';
 			if ( row.guilds.length ) {
-				msg.client.shard.broadcastEval( `${JSON.stringify(row.guilds)}.map( guild => this.guilds.cache.get(guild)?.name )` ).then( results => {
+				msg.client.shard.broadcastEval( (discordClient, evalData) => {
+					return evalData.map( guild => discordClient.guilds.cache.get(guild)?.name );
+				}, {context: row.guilds} ).then( results => {
 					var guilds = row.guilds.map( (guild, i) => '`' + guild + '` ' + ( results.find( result => result[i] !== null )?.[i] || '' ) );
 					text += 'Currently they have ' + guilds.length + ' servers:\n' + guilds.join('\n');
 					if ( row.count < guilds.length ) text += '\n\n**They are above their server limit!**';
-					msg.replyMsg( text, {}, true );
+					msg.replyMsg( text, true );
 				} );
 			}
 			else {
 				text += '*They don\'t have any servers yet.*';
-				msg.replyMsg( text, {}, true );
+				msg.replyMsg( text, true );
 			}
 		}, dberror => {
 			console.log( '- Error while getting the patreon: ' + dberror );
-			msg.replyMsg( 'I got an error while searching for <@' + args[1] + '>, please try again later.', {}, true );
+			msg.replyMsg( 'I got an error while searching for <@' + args[1] + '>, please try again later.', true );
 		} );
 	}
 	
@@ -202,16 +229,20 @@ function cmd_patreon(lang, msg, args, line, wiki) {
 		var guilds = ( row ? row.guilds : [] );
 		if ( args[2].startsWith( '+' ) || args[2].startsWith( '-' ) ) count += value;
 		else count = value;
-		if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+		if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 		if ( count <= 0 ) return db.connect().then( client => {
 			return client.query( 'DELETE FROM patreons WHERE patreon = $1', [args[1]] ).then( () => {
 				console.log( '- Patreon successfully deleted.' );
-				msg.replyMsg( '<@' + args[1] + '> is no longer a patreon.', {}, true );
+				msg.replyMsg( '<@' + args[1] + '> is no longer a patreon.', true );
 				if ( !guilds.length ) return Promise.reject();
-				msg.client.shard.broadcastEval( `${JSON.stringify(row.guilds)}.forEach( guild => delete global.patreons[guild] )` );
+				msg.client.shard.broadcastEval( (discordClient, evalData) => {
+					return evalData.map( guild => {
+						delete global.patreons[guild];
+					} );
+				}, {context: row.guilds} );
 			}, dberror => {
 				console.log( '- Error while deleting the patreon: ' + dberror );
-				msg.replyMsg( 'I got an error while deleting <@' + args[1] + '>, please try again later.', {}, true );
+				msg.replyMsg( 'I got an error while deleting <@' + args[1] + '>, please try again later.', true );
 				return Promise.reject();
 			} ).then( () => {
 				return client.query( 'SELECT guild, lang, role, inline FROM discord WHERE guild IN (' + guilds.map( (guild, i) => '$' + ( i + 1 ) ).join(', ') + ') AND channel IS NULL', guilds ).then( ({rows}) => {
@@ -229,24 +260,25 @@ function cmd_patreon(lang, msg, args, line, wiki) {
 				return client.query( 'DELETE FROM discord WHERE guild IN (' + guilds.map( (guild, i) => '$' + ( i + 2 ) ).join(', ') + ') AND channel LIKE $1 RETURNING wiki, guild, channel, lang, role, inline', ['#%', ...guilds] ).then( ({rows}) => {
 					if ( rows.length ) {
 						console.log( '- Channel categories successfully deleted.' );
-						return msg.client.shard.broadcastEval( `let rows = ${JSON.stringify(rows)};
-						[].concat(...${JSON.stringify(guilds)}.filter( guild => {
-							return this.guilds.cache.has(guild);
-						} ).map( guild => {
-							return this.guilds.cache.get(guild).channels.cache.filter( channel => {
-								return ( channel.isGuild() && rows.some( row => {
-									return ( row.channel === '#' + channel.parentID );
-								} ) );
-							} ).map( channel => {
-								let row = rows.find( row => {
-									return ( row.channel === '#' + channel.parentID );
+						return msg.client.shard.broadcastEval( (discordClient, evalData) => {
+							return [].concat(...evalData.guilds.filter( guild => {
+								return discordClient.guilds.cache.has(guild);
+							} ).map( guild => {
+								return discordClient.guilds.cache.get(guild).channels.cache.filter( channel => {
+									return ( channel.isGuild(false) && evalData.rows.some( row => {
+										return ( row.channel === '#' + channel.parentId );
+									} ) );
+								} ).map( channel => {
+									let row = evalData.rows.find( row => {
+										return ( row.channel === '#' + channel.parentId );
+									} );
+									return {
+										id: channel.id, guild: row.guild, wiki: row.wiki,
+										lang: row.lang, role: row.role, inline: row.inline
+									};
 								} );
-								return {
-									id: channel.id, guild: row.guild, wiki: row.wiki,
-									lang: row.lang, role: row.role, inline: row.inline
-								};
-							} );
-						} ))` ).then( response => {
+							} ));
+						}, {context: {rows, guilds}} ).then( response => {
 							var channels = [].concat(...response);
 							if ( channels.length ) return Promise.all(channels.map( channel => {
 								return client.query( 'INSERT INTO discord(wiki, guild, channel, lang, role, inline, prefix) VALUES($1, $2, $3, $4, $5, $6, $7)', [channel.wiki, channel.guild, channel.id, channel.lang, channel.role, channel.inline, process.env.prefix] ).catch( dberror => {
@@ -306,30 +338,30 @@ function cmd_patreon(lang, msg, args, line, wiki) {
 			} );
 		}, dberror => {
 			console.log( '- Error while connecting to the database client: ' + dberror );
-			msg.replyMsg( 'I got an error while updating <@' + args[1] + '>, please try again later.', {}, true );
+			msg.replyMsg( 'I got an error while updating <@' + args[1] + '>, please try again later.', true );
 		} );
 		if ( !row ) return db.query( 'INSERT INTO patreons(patreon, count) VALUES($1, $2)', [args[1], count] ).then( () => {
 			console.log( '- Patreon successfully added.' );
-			msg.replyMsg( '<@' + args[1] + '> can now have up to ' + count + ' servers.', {}, true );
+			msg.replyMsg( '<@' + args[1] + '> can now have up to ' + count + ' servers.', true );
 		}, dberror => {
 			console.log( '- Error while adding the patreon: ' + dberror );
-			msg.replyMsg( 'I got an error while adding <@' + args[1] + '>, please try again later.', {}, true );
+			msg.replyMsg( 'I got an error while adding <@' + args[1] + '>, please try again later.', true );
 		} );
 		db.query( 'UPDATE patreons SET count = $1 WHERE patreon = $2', [count, args[1]] ).then( () => {
 			console.log( '- Patreon successfully updated.' );
 			var text = '<@' + args[1] + '> can now have up to ' + count + ' servers.';
 			if ( count < guilds.length ) text += '\n\n**They are now above their server limit!**';
-			msg.replyMsg( text, {}, true );
+			msg.replyMsg( text, true );
 		}, dberror => {
 			console.log( '- Error while updating the patreon: ' + dberror );
-			msg.replyMsg( 'I got an error while updating <@' + args[1] + '>, please try again later.', {}, true );
+			msg.replyMsg( 'I got an error while updating <@' + args[1] + '>, please try again later.', true );
 		} );
 	}, dberror => {
 		console.log( '- Error while getting the patreon: ' + dberror );
-		msg.replyMsg( 'I got an error while searching for <@' + args[1] + '>, please try again later.', {}, true );
+		msg.replyMsg( 'I got an error while searching for <@' + args[1] + '>, please try again later.', true );
 	} );
 	
-	if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
+	if ( !msg.channel.isGuild() || !pause[msg.guildId] ) this.LINK(lang, msg, line, wiki);
 }
 
 module.exports = {

+ 6 - 6
cmds/pause.js

@@ -8,16 +8,16 @@
  */
 function cmd_pause(lang, msg, args, line, wiki) {
 	if ( msg.channel.isGuild() && args.join(' ').split('\n')[0].isMention(msg.guild) && ( msg.isAdmin() || msg.isOwner() ) ) {
-		if ( pause[msg.guild.id] ) {
-			delete pause[msg.guild.id];
+		if ( pause[msg.guildId] ) {
+			delete pause[msg.guildId];
 			console.log( '- Pause ended.' );
-			msg.replyMsg( lang.get('pause.off'), {}, true );
+			msg.replyMsg( lang.get('pause.off'), true );
 		} else {
-			msg.replyMsg( lang.get('pause.on'), {}, true );
+			msg.replyMsg( lang.get('pause.on'), true );
 			console.log( '- Pause started.' );
-			pause[msg.guild.id] = true;
+			pause[msg.guildId] = true;
 		}
-	} else if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) {
+	} else if ( !msg.channel.isGuild() || !pause[msg.guildId] ) {
 		this.LINK(lang, msg, line, wiki);
 	}
 }

+ 108 - 119
cmds/rcscript.js

@@ -1,6 +1,8 @@
 const cheerio = require('cheerio');
+const {Util, MessageActionRow, MessageButton, Permissions: {FLAGS}} = require('discord.js');
 const help_setup = require('../functions/helpsetup.js');
 const {limit: {rcgcdw: rcgcdwLimit}} = require('../util/default.json');
+const {got} = require('../util/functions.js');
 const Lang = require('../util/i18n.js');
 const allLangs = Lang.allLangs(true);
 const Wiki = require('../util/wiki.js');
@@ -29,53 +31,40 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 	if ( !msg.isAdmin() ) return msg.reactEmoji('❌');
 	if ( msg.defaultSettings ) return help_setup(lang, msg);
 	
-	db.query( 'SELECT configid, webhook, wiki, lang, display, rcid, postid FROM rcgcdw WHERE guild = $1 ORDER BY configid ASC', [msg.guild.id] ).then( ({rows}) => {
+	db.query( 'SELECT configid, webhook, wiki, lang, display, rcid, postid FROM rcgcdw WHERE guild = $1 ORDER BY configid ASC', [msg.guildId] ).then( ({rows}) => {
 		var prefix = process.env.prefix;
 		var limit = rcgcdwLimit.default;
 		var display = display_types.slice(0, rcgcdwLimit.display + 1);
-		if ( patreons[msg.guild.id] ) {
-			prefix = patreons[msg.guild.id];
+		if ( patreons[msg.guildId] ) {
+			prefix = patreons[msg.guildId];
 			limit = rcgcdwLimit.patreon;
 			display = display_types.slice();
 		}
-		var button = {
-			type: 2,
-			style: 5,
-			label: lang.get('settings.button'),
-			emoji: {
-				id: '588723255972593672',
-				name: 'wikibot',
-				animated: false
-			},
-			url: new URL(`/guild/${msg.guild.id}/rcscript`, process.env.dashboard).href,
-			disabled: false
-		};
+		var button = null;
 		var components = [];
-		if ( process.env.dashboard ) components.push({
-			type: 1,
-			components: [
-				button
-			]
-		});
+		if ( process.env.dashboard ) {
+			button = new MessageButton().setLabel(lang.get('settings.button')).setEmoji('<:wikibot:588723255972593672>').setStyle('LINK').setURL(new URL(`/guild/${msg.guildId}/rcscript`, process.env.dashboard).href);
+			components.push(new MessageActionRow().addComponents(button));
+		}
 
 		if ( args[0] === 'add' ) {
-			if ( !msg.channel.permissionsFor(msg.client.user).has('MANAGE_WEBHOOKS') ) {
-				console.log( msg.guild.id + ': Missing permissions - MANAGE_WEBHOOKS' );
+			if ( !msg.channel.permissionsFor(msg.client.user).has(FLAGS.MANAGE_WEBHOOKS) ) {
+				console.log( msg.guildId + ': Missing permissions - MANAGE_WEBHOOKS' );
 				return msg.replyMsg( lang.get('general.missingperm') + ' `MANAGE_WEBHOOKS`' );
 			}
-			if ( !( msg.channel.permissionsFor(msg.member).has('MANAGE_WEBHOOKS') || ( msg.isOwner() && msg.evalUsed ) ) ) {
+			if ( !( msg.channel.permissionsFor(msg.member).has(FLAGS.MANAGE_WEBHOOKS) || ( msg.isOwner() && msg.evalUsed ) ) ) {
 				return msg.replyMsg( lang.get('rcscript.noadmin') );
 			}
-			if ( rows.length >= limit ) return msg.replyMsg( lang.get('rcscript.max_entries'), {}, true );
-			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+			if ( rows.length >= limit ) return msg.replyMsg( lang.get('rcscript.max_entries'), true );
+			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 
-			button.url = new URL(`/guild/${msg.guild.id}/rcscript/new`, process.env.dashboard).href;
+			button?.setURL(new URL(`/guild/${msg.guildId}/rcscript/new`, button.url).href);
 			var wikihelp = '\n`' + prefix + 'rcscript add ' + lang.get('rcscript.new_wiki') + '`\n' + lang.get('rcscript.help_wiki');
 			var input = args.slice(1).join(' ').toLowerCase().trim().replace( /^<\s*(.*?)\s*>$/, '$1' );
 			var wikinew = new Wiki(wiki);
 			if ( input ) {
 				wikinew = Wiki.fromInput(input);
-				if ( !wikinew ) return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {components}, true );
+				if ( !wikinew ) return msg.replyMsg( {content: lang.get('settings.wikiinvalid') + wikihelp, components}, true );
 			}
 			return msg.reactEmoji('⏳', true).then( reaction => got.get( wikinew + 'api.php?&action=query&meta=allmessages|siteinfo&ammessages=custom-RcGcDw|recentchanges&amenableparser=true&siprop=general&titles=Special:RecentChanges&format=json', {
 				responseType: 'text'
@@ -99,25 +88,25 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 					console.log( '- ' + response.statusCode + ': Error while testing the wiki: ' + body?.error?.info );
 					if ( reaction ) reaction.removeEmoji();
 					if ( body?.error?.info === 'You need read permission to use this module.' ) {
-						return msg.replyMsg( lang.get('settings.wikiinvalid_private') + wikihelp, {components}, true );
+						return msg.replyMsg( {content: lang.get('settings.wikiinvalid_private') + wikihelp, components}, true );
 					}
 					msg.reactEmoji('nowiki', true);
-					return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {components}, true );
+					return msg.replyMsg( {content: lang.get('settings.wikiinvalid') + wikihelp, components}, true );
 				}
 				wikinew.updateWiki(body.query.general);
 				if ( body.query.general.generator.replace( /^MediaWiki 1\.(\d\d).*$/, '$1' ) < 30 ) {
 					if ( reaction ) reaction.removeEmoji();
-					return msg.replyMsg( lang.get('test.MediaWiki', 'MediaWiki 1.30', body.query.general.generator) + '\nhttps://www.mediawiki.org/wiki/MediaWiki_1.30', {components}, true );
+					return msg.replyMsg( {content: lang.get('test.MediaWiki', 'MediaWiki 1.30', body.query.general.generator) + '\nhttps://www.mediawiki.org/wiki/MediaWiki_1.30', components}, true );
 				}
-				if ( body.query.allmessages[0]['*'] !== msg.guild.id ) {
+				if ( body.query.allmessages[0]['*'] !== msg.guildId ) {
 					if ( reaction ) reaction.removeEmoji();
-					return msg.replyMsg( lang.get('rcscript.sysmessage', 'MediaWiki:Custom-RcGcDw', msg.guild.id) + '\n<' + wikinew.toLink('MediaWiki:Custom-RcGcDw', 'action=edit') + '>', {components}, true );
+					return msg.replyMsg( {content: lang.get('rcscript.sysmessage', 'MediaWiki:Custom-RcGcDw', msg.guildId) + '\n<' + wikinew.toLink('MediaWiki:Custom-RcGcDw', 'action=edit') + '>', components}, true );
 				}
 				return db.query( 'SELECT reason FROM blocklist WHERE wiki = $1', [wikinew.href] ).then( ({rows:[block]}) => {
 					if ( block ) {
 						console.log( '- This wiki is blocked: ' + block.reason );
 						if ( reaction ) reaction.removeEmoji();
-						return msg.replyMsg( ( block.reason ? lang.get('rcscript.blocked_reason', block.reason) : lang.get('rcscript.blocked') ), {components}, true );
+						return msg.replyMsg( {content: ( block.reason ? lang.get('rcscript.blocked_reason', block.reason) : lang.get('rcscript.blocked') ), components}, true );
 					}
 					if ( wikinew.isFandom(false) ) return got.get( wikinew + 'wikia.php?controller=DiscussionPost&method=getPosts&includeCounters=false&limit=1&format=json&cache=' + Date.now(), {
 						headers: {
@@ -153,19 +142,19 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 								if ( new_configid === i ) new_configid++;
 								else break;
 							}
-							db.query( 'INSERT INTO rcgcdw(guild, configid, webhook, wiki, lang, display, postid) VALUES($1, $2, $3, $4, $5, $6, $7)', [msg.guild.id, new_configid, webhook.id + '/' + webhook.token, wikinew.href, webhook_lang.lang, ( msg.showEmbed() ? 1 : 0 ), ( enableFeeds ? null : '-1' )] ).then( () => {
+							db.query( 'INSERT INTO rcgcdw(guild, configid, webhook, wiki, lang, display, postid) VALUES($1, $2, $3, $4, $5, $6, $7)', [msg.guildId, new_configid, webhook.id + '/' + webhook.token, wikinew.href, webhook_lang.lang, ( msg.showEmbed() ? 1 : 0 ), ( enableFeeds ? null : '-1' )] ).then( () => {
 								console.log( '- RcGcDw successfully added.' );
 								if ( reaction ) reaction.removeEmoji();
-								msg.replyMsg( lang.get('rcscript.added') + ' <' + wikinew + '>\n`' + prefix + 'rcscript' + ( rows.length ? ' ' + new_configid : '' ) + '`', {components}, true );
+								msg.replyMsg( {content: lang.get('rcscript.added') + ' <' + wikinew + '>\n`' + prefix + 'rcscript' + ( rows.length ? ' ' + new_configid : '' ) + '`', components}, true );
 							}, dberror => {
 								console.log( '- Error while adding the RcGcDw: ' + dberror );
 								if ( reaction ) reaction.removeEmoji();
-								msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+								msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 							} );
 						}, error => {
 							console.log( '- Error while creating the webhook: ' + error );
 							if ( reaction ) reaction.removeEmoji();
-							msg.replyMsg( lang.get('rcscript.webhook_failed'), {components}, true );
+							msg.replyMsg( {content: lang.get('rcscript.webhook_failed'), components}, true );
 						} );
 					}
 				}, dberror => {
@@ -177,14 +166,14 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 				if ( reaction ) reaction.removeEmoji();
 				if ( error.message?.startsWith( 'connect ECONNREFUSED ' ) || error.message?.startsWith( 'Hostname/IP does not match certificate\'s altnames: ' ) || error.message === 'certificate has expired' || error.message === 'self signed certificate' ) {
 					console.log( '- Error while testing the wiki: No HTTPS' );
-					return msg.replyMsg( lang.get('settings.wikiinvalid_http') + wikihelp, {components}, true );
+					return msg.replyMsg( {content: lang.get('settings.wikiinvalid_http') + wikihelp, components}, true );
 				}
 				console.log( '- Error while testing the wiki: ' + error );
 				if ( error.message === `Timeout awaiting 'request' for ${got.defaults.options.timeout.request}ms` ) {
-					return msg.replyMsg( lang.get('settings.wikiinvalid_timeout') + wikihelp, {components}, true );
+					return msg.replyMsg( {content: lang.get('settings.wikiinvalid_timeout') + wikihelp, components}, true );
 				}
 				msg.reactEmoji('nowiki', true);
-				return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {components}, true );
+				return msg.replyMsg( {content: lang.get('settings.wikiinvalid') + wikihelp, components}, true );
 			} ) );
 		}
 
@@ -204,10 +193,10 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 			let cmd = prefix + 'rcscript' + ( rows.length === 1 ? '' : ' ' + selected_row.configid );
 
 			if ( args[0] === 'delete' && !args[1] ) {
-				if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+				if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 				return msg.client.fetchWebhook(...selected_row.webhook.split('/')).then( webhook => {
-					var channel = msg.guild.channels.cache.get(webhook.channelID);
-					if ( !channel || !channel.permissionsFor(msg.member).has('MANAGE_WEBHOOKS') ) {
+					var channel = msg.guild.channels.cache.get(webhook.channelId);
+					if ( !channel || !channel.permissionsFor(msg.member).has(FLAGS.MANAGE_WEBHOOKS) ) {
 						return msg.replyMsg( lang.get('rcscript.noadmin') );
 					}
 					db.query( 'DELETE FROM rcgcdw WHERE webhook = $1', [selected_row.webhook] ).then( () => {
@@ -215,38 +204,38 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 						webhook.send( webhook_lang.get('deleted') ).catch(log_error).finally( () => {
 							webhook.delete(lang.get('rcscript.audit_reason_delete')).catch(log_error);
 						} );
-						msg.replyMsg( lang.get('rcscript.deleted'), {components}, true );
+						msg.replyMsg( {content: lang.get('rcscript.deleted'), components}, true );
 					}, dberror => {
 						console.log( '- Error while removing the RcGcDw: ' + dberror );
-						button.url = new URL(`/guild/${msg.guild.id}/rcscript/${selected_row.configid}`, process.env.dashboard).href;
-						msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+						button?.setURL(new URL(`/guild/${msg.guildId}/rcscript/${selected_row.configid}`, button.url).href);
+						msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 					} );
 				}, error => {
 					log_error(error);
 					if ( error.name === 'DiscordAPIError' && ['Unknown Webhook', 'Invalid Webhook Token'].includes( error.message ) ) {
-						button.url = new URL(`/guild/${msg.guild.id}/rcscript/${selected_row.configid}`, process.env.dashboard).href;
-						return msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+						button?.setURL(new URL(`/guild/${msg.guildId}/rcscript/${selected_row.configid}`, button.url).href);
+						return msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 					}
 					db.query( 'DELETE FROM rcgcdw WHERE webhook = $1', [selected_row.webhook] ).then( () => {
 						console.log( '- RcGcDw successfully removed.' );
-						msg.replyMsg( lang.get('rcscript.deleted'), {components}, true );
+						msg.replyMsg( {content: lang.get('rcscript.deleted'), components}, true );
 					}, dberror => {
 						console.log( '- Error while removing the RcGcDw: ' + dberror );
-						button.url = new URL(`/guild/${msg.guild.id}/rcscript/${selected_row.configid}`, process.env.dashboard).href;
-						msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+						button?.setURL(new URL(`/guild/${msg.guildId}/rcscript/${selected_row.configid}`, button.url).href);
+						msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 					} );
 				} );
 			}
-			button.url = new URL(`/guild/${msg.guild.id}/rcscript/${selected_row.configid}`, process.env.dashboard).href;
+			button?.setURL(new URL(`/guild/${msg.guildId}/rcscript/${selected_row.configid}`, button.url).href);
 			if ( args[0] === 'wiki' ) {
 				if ( !args[1] ) {
-					return msg.replyMsg( lang.get('rcscript.current_wiki') + ' <' + selected_row.wiki + '>\n`' + cmd + ' wiki ' + lang.get('rcscript.new_wiki') + '`\n' + lang.get('rcscript.help_wiki'), {components}, true );
+					return msg.replyMsg( {content: lang.get('rcscript.current_wiki') + ' <' + selected_row.wiki + '>\n`' + cmd + ' wiki ' + lang.get('rcscript.new_wiki') + '`\n' + lang.get('rcscript.help_wiki'), components}, true );
 				}
-				if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+				if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 
 				var wikihelp = '\n`' + cmd + ' wiki ' + lang.get('rcscript.new_wiki') + '`\n' + lang.get('rcscript.help_wiki');
 				var wikinew = Wiki.fromInput(args[1]);
-				if ( !wikinew ) return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {components}, true );
+				if ( !wikinew ) return msg.replyMsg( {content: lang.get('settings.wikiinvalid') + wikihelp, components}, true );
 				return msg.reactEmoji('⏳', true).then( reaction => got.get( wikinew + 'api.php?&action=query&meta=allmessages|siteinfo&ammessages=custom-RcGcDw&amenableparser=true&siprop=general&titles=Special:RecentChanges&format=json', {
 					responseType: 'text'
 				} ).then( response => {
@@ -269,26 +258,26 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 						console.log( '- ' + response.statusCode + ': Error while testing the wiki: ' + body?.error?.info );
 						if ( reaction ) reaction.removeEmoji();
 						if ( body?.error?.info === 'You need read permission to use this module.' ) {
-							return msg.replyMsg( lang.get('settings.wikiinvalid_private') + wikihelp, {components}, true );
+							return msg.replyMsg( {content: lang.get('settings.wikiinvalid_private') + wikihelp, components}, true );
 						}
 						msg.reactEmoji('nowiki', true);
-						return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {components}, true );
+						return msg.replyMsg( {content: lang.get('settings.wikiinvalid') + wikihelp, components}, true );
 					}
 					wikinew.updateWiki(body.query.general);
 					if ( body.query.general.generator.replace( /^MediaWiki 1\.(\d\d).*$/, '$1' ) <= 30 ) {
 						console.log( '- This wiki is using ' + body.query.general.generator + '.' );
 						if ( reaction ) reaction.removeEmoji();
-						return msg.replyMsg( lang.get('test.MediaWiki', 'MediaWiki 1.30', body.query.general.generator) + '\nhttps://www.mediawiki.org/wiki/MediaWiki_1.30', {components}, true );
+						return msg.replyMsg( {content: lang.get('test.MediaWiki', 'MediaWiki 1.30', body.query.general.generator) + '\nhttps://www.mediawiki.org/wiki/MediaWiki_1.30', components}, true );
 					}
-					if ( body.query.allmessages[0]['*'] !== msg.guild.id ) {
+					if ( body.query.allmessages[0]['*'] !== msg.guildId ) {
 						if ( reaction ) reaction.removeEmoji();
-						return msg.replyMsg( lang.get('rcscript.sysmessage', 'MediaWiki:Custom-RcGcDw', msg.guild.id) + '\n<' + wikinew.toLink('MediaWiki:Custom-RcGcDw', 'action=edit') + '>', {components}, true );
+						return msg.replyMsg( {content: lang.get('rcscript.sysmessage', 'MediaWiki:Custom-RcGcDw', msg.guildId) + '\n<' + wikinew.toLink('MediaWiki:Custom-RcGcDw', 'action=edit') + '>', components}, true );
 					}
 					return db.query( 'SELECT reason FROM blocklist WHERE wiki = $1', [wikinew.href] ).then( ({rows:[block]}) => {
 						if ( block ) {
 							console.log( '- This wiki is blocked: ' + block.reason );
 							if ( reaction ) reaction.removeEmoji();
-							return msg.replyMsg( ( block.reason ? lang.get('rcscript.blocked_reason', block.reason) : lang.get('rcscript.blocked') ), {components}, true );
+							return msg.replyMsg( {content: ( block.reason ? lang.get('rcscript.blocked_reason', block.reason) : lang.get('rcscript.blocked') ), components}, true );
 						}
 						if ( wikinew.isFandom(false) ) return got.get( wikinew + 'wikia.php?controller=DiscussionPost&method=getPosts&includeCounters=false&limit=1&format=json&cache=' + Date.now(), {
 							headers: {
@@ -318,11 +307,11 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 							db.query( 'UPDATE rcgcdw SET wiki = $1, rcid = $2, postid = $3 WHERE webhook = $4', [wikinew.href, null, ( enableFeeds ? null : '-1' ), selected_row.webhook] ).then( () => {
 								console.log( '- RcGcDw successfully updated.' );
 								if ( reaction ) reaction.removeEmoji();
-								msg.replyMsg( lang.get('rcscript.updated_wiki') + ' <' + wikinew + '>\n`' + cmd + '`', {components}, true );
+								msg.replyMsg( {content: lang.get('rcscript.updated_wiki') + ' <' + wikinew + '>\n`' + cmd + '`', components}, true );
 							}, dberror => {
 								console.log( '- Error while updating the RcGcDw: ' + dberror );
 								if ( reaction ) reaction.removeEmoji();
-								msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+								msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 							} );
 						}
 					}, dberror => {
@@ -334,43 +323,43 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 					if ( reaction ) reaction.removeEmoji();
 					if ( error.message?.startsWith( 'connect ECONNREFUSED ' ) || error.message?.startsWith( 'Hostname/IP does not match certificate\'s altnames: ' ) || error.message === 'certificate has expired' || error.message === 'self signed certificate' ) {
 						console.log( '- Error while testing the wiki: No HTTPS' );
-						return msg.replyMsg( lang.get('settings.wikiinvalid_http') + wikihelp, {components}, true );
+						return msg.replyMsg( {content: lang.get('settings.wikiinvalid_http') + wikihelp, components}, true );
 					}
 					console.log( '- Error while testing the wiki: ' + error );
 					if ( error.message === `Timeout awaiting 'request' for ${got.defaults.options.timeout.request}ms` ) {
-						return msg.replyMsg( lang.get('settings.wikiinvalid_timeout') + wikihelp, {components}, true );
+						return msg.replyMsg( {content: lang.get('settings.wikiinvalid_timeout') + wikihelp, components}, true );
 					}
 					msg.reactEmoji('nowiki', true);
-					return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {components}, true );
+					return msg.replyMsg( {content: lang.get('settings.wikiinvalid') + wikihelp, components}, true );
 				} ) );
 			}
 			if ( args[0] === 'lang' || args[0] === 'language' ) {
 				if ( !args[1] ) {
-					return msg.replyMsg( lang.get('rcscript.current_lang') + ' `' + allLangs.names[selected_row.lang] + '`\n`' + cmd + ' lang ' + lang.get('rcscript.new_lang') + '`\n' + lang.get('rcscript.help_lang') + ' `' + Object.values(allLangs.names).join('`, `') + '`', {files:( msg.uploadFiles() ? [`./RcGcDb/locale/widgets/${selected_row.lang}.png`] : [] ),components}, true );
+					return msg.replyMsg( {content: lang.get('rcscript.current_lang') + ' `' + allLangs.names[selected_row.lang] + '`\n`' + cmd + ' lang ' + lang.get('rcscript.new_lang') + '`\n' + lang.get('rcscript.help_lang') + ' `' + Object.values(allLangs.names).join('`, `') + '`', files: ( msg.uploadFiles() ? [`./RcGcDb/locale/widgets/${selected_row.lang}.png`] : [] ), components}, true );
 				}
-				if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+				if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 				if ( !allLangs.map.hasOwnProperty(args[1]) ) {
-					return msg.replyMsg( lang.get('settings.langinvalid') + '\n`' + cmd + ' lang ' + lang.get('rcscript.new_lang') + '`\n' + lang.get('rcscript.help_lang') + ' `' + Object.values(allLangs.names).join('`, `') + '`', {components}, true );
+					return msg.replyMsg( {content: lang.get('settings.langinvalid') + '\n`' + cmd + ' lang ' + lang.get('rcscript.new_lang') + '`\n' + lang.get('rcscript.help_lang') + ' `' + Object.values(allLangs.names).join('`, `') + '`', components}, true );
 				}
 
 				msg.client.fetchWebhook(...selected_row.webhook.split('/')).then( webhook => {
-					webhook.send( new Lang(allLangs.map[args[1]], 'rcscript.webhook').get('updated_lang', allLangs.names[allLangs.map[args[1]]]), {files:[`./RcGcDb/locale/widgets/${allLangs.map[args[1]]}.png`]} ).catch(log_error);
+					webhook.send( {content: new Lang(allLangs.map[args[1]], 'rcscript.webhook').get('updated_lang', allLangs.names[allLangs.map[args[1]]]), files: [`./RcGcDb/locale/widgets/${allLangs.map[args[1]]}.png`]} ).catch(log_error);
 				}, log_error );
 				return db.query( 'UPDATE rcgcdw SET lang = $1 WHERE webhook = $2', [allLangs.map[args[1]], selected_row.webhook] ).then( () => {
 					console.log( '- RcGcDw successfully updated.' );
-					msg.replyMsg( lang.get('rcscript.updated_lang') + ' `' + allLangs.names[allLangs.map[args[1]]] + '`\n`' + cmd + '`', {files:( msg.uploadFiles() ? [`./RcGcDb/locale/widgets/${allLangs.map[args[1]]}.png`] : [] ),components}, true );
+					msg.replyMsg( {content: lang.get('rcscript.updated_lang') + ' `' + allLangs.names[allLangs.map[args[1]]] + '`\n`' + cmd + '`', files: ( msg.uploadFiles() ? [`./RcGcDb/locale/widgets/${allLangs.map[args[1]]}.png`] : [] ), components}, true );
 				}, dberror => {
 					console.log( '- Error while updating the RcGcDw: ' + dberror );
-					msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+					msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 				} );
 			}
 			if ( args[0] === 'display' ) {
 				if ( !args[1] || !display_types.includes( args[1] ) ) {
-					return msg.replyMsg( lang.get('rcscript.current_display') + ' `' + display_types[selected_row.display] + '`\n`' + cmd + ' display (' + display.join('|') + ')`\n' + display.map( display_type => '`' + display_type + '`: ' + lang.get('rcscript.help_display_' + display_type) ).join('\n'), {components}, true );
+					return msg.replyMsg( {content: lang.get('rcscript.current_display') + ' `' + display_types[selected_row.display] + '`\n`' + cmd + ' display (' + display.join('|') + ')`\n' + display.map( display_type => '`' + display_type + '`: ' + lang.get('rcscript.help_display_' + display_type) ).join('\n'), components}, true );
 				}
-				if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+				if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 				if ( !display.includes( args[1] ) ) {
-					return msg.replyMsg( lang.get('general.patreon') + '\n<' + process.env.patreon + '>', {}, true );
+					return msg.replyMsg( lang.get('general.patreon') + '\n<' + process.env.patreon + '>', true );
 				}
 
 				msg.client.fetchWebhook(...selected_row.webhook.split('/')).then( webhook => {
@@ -378,14 +367,14 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 				}, log_error );
 				return db.query( 'UPDATE rcgcdw SET display = $1 WHERE webhook = $2', [display_types.indexOf(args[1]), selected_row.webhook] ).then( () => {
 					console.log( '- RcGcDw successfully updated.' );
-					msg.replyMsg( lang.get('rcscript.updated_display') + ' `' + args[1] + '`\n`' + cmd + '`', {components}, true );
+					msg.replyMsg( {content: lang.get('rcscript.updated_display') + ' `' + args[1] + '`\n`' + cmd + '`', components}, true );
 				}, dberror => {
 					console.log( '- Error while updating the RcGcDw: ' + dberror );
-					msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+					msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 				} );
 			}
 			if ( new Wiki(selected_row.wiki).isFandom(false) && args[0] === 'feeds' ) {
-				if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+				if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 				if ( args[1] === 'only' ) {
 					if ( selected_row.rcid === -1 ) {
 						msg.client.fetchWebhook(...selected_row.webhook.split('/')).then( webhook => {
@@ -393,41 +382,41 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 						}, log_error );
 						return db.query( 'UPDATE rcgcdw SET rcid = $1 WHERE webhook = $2', [null, selected_row.webhook] ).then( () => {
 							console.log( '- RcGcDw successfully updated.' );
-							msg.replyMsg( lang.get('rcscript.enabled_rc') + '\n`' + cmd + '`', {components}, true );
+							msg.replyMsg( {content: lang.get('rcscript.enabled_rc') + '\n`' + cmd + '`', components}, true );
 						}, dberror => {
 							console.log( '- Error while updating the RcGcDw: ' + dberror );
-							msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+							msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 						} );
 					}
 
 					if ( selected_row.postid === '-1' ) {
-						return msg.replyMsg( lang.get('rcscript.all_inactive') + '\n\n' + lang.get('rcscript.delete') + '\n`' + cmd + ' delete`', {components}, true );
+						return msg.replyMsg( {content: lang.get('rcscript.all_inactive') + '\n\n' + lang.get('rcscript.delete') + '\n`' + cmd + ' delete`', components}, true );
 					}
 					msg.client.fetchWebhook(...selected_row.webhook.split('/')).then( webhook => {
 						webhook.send( webhook_lang.get('disabled_rc') ).catch(log_error);
 					}, log_error );
 					return db.query( 'UPDATE rcgcdw SET rcid = $1 WHERE webhook = $2', [-1, selected_row.webhook] ).then( () => {
 						console.log( '- RcGcDw successfully updated.' );
-						msg.replyMsg( lang.get('rcscript.disabled_rc') + '\n`' + cmd + '`', {components}, true );
+						msg.replyMsg( {content: lang.get('rcscript.disabled_rc') + '\n`' + cmd + '`', components}, true );
 					}, dberror => {
 						console.log( '- Error while updating the RcGcDw: ' + dberror );
-						msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+						msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 					} );
 				}
 
 				if ( selected_row.postid !== '-1' ) {
 					if ( selected_row.rcid === -1 ) {
-						return msg.replyMsg( lang.get('rcscript.all_inactive') + '\n\n' + lang.get('rcscript.delete') + '\n`' + cmd + ' delete`', {components}, true );
+						return msg.replyMsg( {content: lang.get('rcscript.all_inactive') + '\n\n' + lang.get('rcscript.delete') + '\n`' + cmd + ' delete`', components}, true );
 					}
 					msg.client.fetchWebhook(...selected_row.webhook.split('/')).then( webhook => {
 						webhook.send( webhook_lang.get('disabled_feeds') ).catch(log_error);
 					}, log_error );
 					return db.query( 'UPDATE rcgcdw SET postid = $1 WHERE webhook = $2', ['-1', selected_row.webhook] ).then( () => {
 						console.log( '- RcGcDw successfully updated.' );
-						msg.replyMsg( lang.get('rcscript.disabled_feeds') + '\n`' + cmd + '`', {components}, true );
+						msg.replyMsg( {content: lang.get('rcscript.disabled_feeds') + '\n`' + cmd + '`', components}, true );
 					}, dberror => {
 						console.log( '- Error while updating the RcGcDw: ' + dberror );
-						msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+						msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 					} );
 				}
 
@@ -440,7 +429,7 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 					if ( dsresponse.statusCode !== 200 || !dsbody || dsbody.status === 404 ) {
 						if ( dsbody?.status !== 404 ) console.log( '- ' + dsresponse.statusCode + ': Error while checking for discussions: ' + dsbody?.title );
 						if ( reaction ) reaction.removeEmoji();
-						return msg.replyMsg( lang.get('rcscript.no_feeds'), {components}, true );
+						return msg.replyMsg( {content: lang.get('rcscript.no_feeds'), components}, true );
 					}
 					msg.client.fetchWebhook(...selected_row.webhook.split('/')).then( webhook => {
 						webhook.send( webhook_lang.get('enabled_feeds') + '\n<' + selected_row.wiki + 'f>' ).catch(log_error);
@@ -448,21 +437,21 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 					db.query( 'UPDATE rcgcdw SET postid = $1 WHERE webhook = $2', [null, selected_row.webhook] ).then( () => {
 						console.log( '- RcGcDw successfully updated.' );
 						if ( reaction ) reaction.removeEmoji();
-						msg.replyMsg( lang.get('rcscript.enabled_feeds') + '\n`' + cmd + '`', {components}, true );
+						msg.replyMsg( {content: lang.get('rcscript.enabled_feeds') + '\n`' + cmd + '`', components}, true );
 					}, dberror => {
 						console.log( '- Error while updating the RcGcDw: ' + dberror );
 						if ( reaction ) reaction.removeEmoji();
-						msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+						msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 					} );
 				}, error => {
 					console.log( '- Error while checking for discussions: ' + error );
 					if ( reaction ) reaction.removeEmoji();
-					return msg.replyMsg( lang.get('rcscript.no_feeds'), {components}, true );
+					return msg.replyMsg( {content: lang.get('rcscript.no_feeds'), components}, true );
 				} ) );
 			}
 
 			if ( rows.length > 1 ) return msg.client.fetchWebhook(...selected_row.webhook.split('/')).then( webhook => {
-				return webhook.channelID;
+				return webhook.channelId;
 			}, error => {
 				log_error(error);
 				if ( error.name === 'DiscordAPIError' && ['Unknown Webhook', 'Invalid Webhook Token'].includes( error.message ) ) {
@@ -476,7 +465,7 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 				return;
 			} ).then( channel => {
 				var text = lang.get('rcscript.current_selected', selected_row.configid);
-				if ( process.env.dashboard ) text += `\n<${button.url}>\n`;
+				if ( button ) text += `\n<${button.url}>\n`;
 				text += '\n' + lang.get('rcscript.channel') + ' <#' + channel + '>\n';
 				text += '\n' + lang.get('rcscript.wiki') + ' <' + selected_row.wiki + '>';
 				text += '\n`' + cmd + ' wiki ' + lang.get('rcscript.new_wiki') + '`\n';
@@ -493,12 +482,12 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 					text += '\n' + lang.get('rcscript.help_feeds') + '\n`' + cmd + ' feeds` ' + lang.get('rcscript.toggle') + '\n';
 				}
 				text += '\n' + lang.get('rcscript.delete') + '\n`' + cmd + ' delete`\n';
-				msg.replyMsg( text, {components}, true );
-			}, () => msg.replyMsg( lang.get('rcscript.deleted'), {components}, true ) );
+				msg.replyMsg( {content: text, components}, true );
+			}, () => msg.replyMsg( {content: lang.get('rcscript.deleted'), components}, true ) );
 		}
 
 		Promise.all(rows.map( row => msg.client.fetchWebhook(...row.webhook.split('/')).then( webhook => {
-			return webhook.channelID;
+			return webhook.channelId;
 		}, error => {
 			log_error(error);
 			if ( error.name === 'DiscordAPIError' && ['Unknown Webhook', 'Invalid Webhook Token'].includes( error.message ) ) {
@@ -519,7 +508,7 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 			var text = '';
 			if ( rows.length ) {
 				text += lang.get('rcscript.current');
-				if ( process.env.dashboard ) text += `\n<${button.url}>`;
+				if ( button ) text += `\n<${button.url}>`;
 				text += rows.map( row => {
 					var cmd = prefix + 'rcscript' + ( only ? '' : ' ' + row.configid );
 					var row_text = '\n';
@@ -546,10 +535,10 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 			}
 			else {
 				text += lang.get('rcscript.missing');
-				if ( process.env.dashboard ) text += `\n<${button.url}>`;
+				if ( button ) text += `\n<${button.url}>`;
 			}
 			if ( rows.length < limit ) text += '\n\n' + lang.get('rcscript.add_more') + '\n`' + prefix + 'rcscript add ' + lang.get('rcscript.new_wiki') + '`';
-			msg.replyMsg( text, {split:true,components}, true );
+			Util.splitMessage( text ).forEach( textpart => msg.replyMsg( {content: textpart, components}, true ) );
 		} );
 	}, dberror => {
 		console.log( '- Error while getting the RcGcDw: ' + dberror );
@@ -563,19 +552,19 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
  * @param {String[]} args - The command arguments.
  */
 function blocklist(msg, args) {
-	var prefix = ( patreons[msg?.guild?.id] || process.env.prefix );
+	var prefix = ( patreons[msg?.guildId] || process.env.prefix );
 	if ( args[0] === 'add' ) {
-		if ( !args[1] ) return msg.replyMsg( '`' + prefix + 'rcscript block add <wiki> [<reason>]`', {}, true );
-		if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+		if ( !args[1] ) return msg.replyMsg( '`' + prefix + 'rcscript block add <wiki> [<reason>]`', true );
+		if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 		let input = args[1].toLowerCase().replace( /^<(.*?)>$/, '$1' );
 		let wiki = Wiki.fromInput(input);
-		if ( !wiki ) return msg.replyMsg( '`' + prefix + 'rcscript block add <wiki> [<reason>]`', {}, true );
+		if ( !wiki ) return msg.replyMsg( '`' + prefix + 'rcscript block add <wiki> [<reason>]`', true );
 		let reason = ( args.slice(2).join(' ').trim() || null );
 		return db.query( 'INSERT INTO blocklist(wiki, reason) VALUES($1, $2)', [wiki.href, reason] ).then( () => {
 			console.log( '- Successfully added to the blocklist.' );
 			db.query( 'DELETE FROM rcgcdw WHERE wiki = $1 RETURNING webhook, lang', [wiki.href] ).then( ({rows}) => {
 				console.log( '- Successfully removed ' + rows.length + ' webhooks.' );
-				msg.replyMsg( 'I added `' + wiki + '` to the blocklist for `' + reason + '` and removed ' + rows.length + ' webhooks.', {}, true );
+				msg.replyMsg( 'I added `' + wiki + '` to the blocklist for `' + reason + '` and removed ' + rows.length + ' webhooks.', true );
 				if ( rows.length ) rows.forEach( row => {
 					msg.client.fetchWebhook(...row.webhook.split('/')).then( webhook => {
 						var lang = new Lang(row.lang, 'rcscript.webhook');
@@ -586,50 +575,50 @@ function blocklist(msg, args) {
 				} );
 			}, dberror => {
 				console.log( '- Error while removing the webhooks: ' + dberror );
-				msg.replyMsg( 'I added `' + wiki + '` to the blocklist for `' + reason + '` but got an error while removing the webhooks: ' + dberror, {}, true );
+				msg.replyMsg( 'I added `' + wiki + '` to the blocklist for `' + reason + '` but got an error while removing the webhooks: ' + dberror, true );
 			} );
 		}, dberror => {
 			if ( dberror.message === 'duplicate key value violates unique constraint "blocklist_wiki_key"' ) {
-				return msg.replyMsg( '`' + wiki + '` is already on the blocklist.\n`' + prefix + 'rcscript block <' + wiki + '>`', {}, true );
+				return msg.replyMsg( '`' + wiki + '` is already on the blocklist.\n`' + prefix + 'rcscript block <' + wiki + '>`', true );
 			}
 			console.log( '- Error while adding to the blocklist: ' + dberror );
-			msg.replyMsg( 'I got an error while adding to the blocklist: ' + dberror, {}, true );
+			msg.replyMsg( 'I got an error while adding to the blocklist: ' + dberror, true );
 		} );
 	}
 	if ( args[0] === 'remove' ) {
 		let input = args.slice(1).join(' ').toLowerCase().trim().replace( /^<\s*(.*?)\s*>$/, '$1' );
 		let wiki = Wiki.fromInput(input);
-		if ( !wiki ) return msg.replyMsg( '`' + prefix + 'rcscript block remove <wiki>`', {}, true );
-		if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+		if ( !wiki ) return msg.replyMsg( '`' + prefix + 'rcscript block remove <wiki>`', true );
+		if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 		return db.query( 'DELETE FROM blocklist WHERE wiki = $1', [wiki.href] ).then( ({rowCount}) => {
 			if ( rowCount ) {
 				console.log( '- Successfully removed from the blocklist.' );
-				msg.replyMsg( 'I removed `' + wiki + '` from the blocklist.', {}, true );
+				msg.replyMsg( 'I removed `' + wiki + '` from the blocklist.', true );
 			}
-			else msg.replyMsg( '`' + wiki + '` was not on the blocklist.', {}, true );
+			else msg.replyMsg( '`' + wiki + '` was not on the blocklist.', true );
 		}, dberror => {
 			console.log( '- Error while removing from the blocklist: ' + dberror );
-			msg.replyMsg( 'I got an error while removing from the blocklist: ' + dberror, {}, true );
+			msg.replyMsg( 'I got an error while removing from the blocklist: ' + dberror, true );
 		} );
 	}
 	if ( args.length ) {
 		let input = args.join(' ').toLowerCase().trim().replace( /^<\s*(.*?)\s*>$/, '$1' );
 		let wiki = Wiki.fromInput(input);
-		if ( !wiki ) return msg.replyMsg( '`' + prefix + 'rcscript block <wiki>`\n`' + prefix + 'rcscript block add <wiki> [<reason>]`\n`' + prefix + 'rcscript block remove <wiki>`', {}, true );
+		if ( !wiki ) return msg.replyMsg( '`' + prefix + 'rcscript block <wiki>`\n`' + prefix + 'rcscript block add <wiki> [<reason>]`\n`' + prefix + 'rcscript block remove <wiki>`', true );
 		return db.query( 'SELECT reason FROM blocklist WHERE wiki = $1', [wiki.href] ).then( ({rows:[row]}) => {
-			if ( !row ) return msg.replyMsg( '`' + wiki + '` is currently not on the blocklist.\n`' + prefix + 'rcscript block add <' + wiki + '> [<reason>]`', {}, true );
-			msg.replyMsg( '`' + wiki + '` is currently on the blocklist ' + ( row.reason ? 'for `' + row.reason + '`' : 'with no reason provided' ) + '.\n`' + prefix + 'rcscript block remove <' + wiki + '>`', {}, true );
+			if ( !row ) return msg.replyMsg( '`' + wiki + '` is currently not on the blocklist.\n`' + prefix + 'rcscript block add <' + wiki + '> [<reason>]`', true );
+			msg.replyMsg( '`' + wiki + '` is currently on the blocklist ' + ( row.reason ? 'for `' + row.reason + '`' : 'with no reason provided' ) + '.\n`' + prefix + 'rcscript block remove <' + wiki + '>`', true );
 		}, dberror => {
 			console.log( '- Error while checking the blocklist: ' + dberror );
-			msg.replyMsg( 'I got an error while checking the blocklist: ' + dberror, {}, true );
+			msg.replyMsg( 'I got an error while checking the blocklist: ' + dberror, true );
 		} );
 	}
 	db.query( 'SELECT wiki, reason FROM blocklist' ).then( ({rows}) => {
-		if ( !rows.length ) return msg.replyMsg( 'there are currently no wikis on the blocklist.\n`' + prefix + 'rcscript block add <wiki> [<reason>]`', {}, true );
-		msg.replyMsg( 'there are currently ' + row.length + ' wikis the blocklist:\n' + rows.map( row => '`' + row.wiki + '` – ' + ( row.reason ? '`' + row.reason + '`' : 'No reason provided.' ) ).join('\n') + '\n`' + prefix + 'rcscript block remove <wiki>`', {split:true}, true );
+		if ( !rows.length ) return msg.replyMsg( 'There are currently no wikis on the blocklist.\n`' + prefix + 'rcscript block add <wiki> [<reason>]`', true );
+		Util.splitMessage( 'There are currently ' + row.length + ' wikis the blocklist:\n' + rows.map( row => '`' + row.wiki + '` – ' + ( row.reason ? '`' + row.reason + '`' : 'No reason provided.' ) ).join('\n') + '\n`' + prefix + 'rcscript block remove <wiki>`' ).forEach( textpart => msg.replyMsg( textpart, true ) );
 	}, dberror => {
 		console.log( '- Error while checking the blocklist: ' + dberror );
-		msg.replyMsg( 'I got an error while checking the blocklist: ' + dberror, {}, true );
+		msg.replyMsg( 'I got an error while checking the blocklist: ' + dberror, true );
 	} );
 }
 

+ 5 - 3
cmds/say.js

@@ -1,3 +1,5 @@
+const {Permissions: {FLAGS}} = require('discord.js');
+
 /**
  * Processes the "say" command.
  * @param {import('../util/i18n.js')} lang - The user language.
@@ -21,13 +23,13 @@ function cmd_say(lang, msg, args, line, wiki) {
 	}
 	if ( text.trim() || imgs.length ) {
 		var allowedMentions = {parse:['users']};
-		if ( msg.member.hasPermission(['MENTION_EVERYONE']) ) allowedMentions.parse = ['users','roles','everyone'];
+		if ( msg.member.permissions.has(FLAGS.MENTION_EVERYONE) ) allowedMentions.parse = ['users','roles','everyone'];
 		else allowedMentions.roles = msg.guild.roles.cache.filter( role => role.mentionable ).map( role => role.id ).slice(0,100)
-		msg.channel.send( text, {allowedMentions,files:imgs} ).then( () => msg.delete().catch(log_error), error => {
+		msg.channel.send( {content: text, allowedMentions, files: imgs} ).then( () => msg.delete().catch(log_error), error => {
 			log_error(error);
 			msg.reactEmoji('error', true);
 		} );
-	} else if ( !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
+	} else if ( !pause[msg.guildId] ) this.LINK(lang, msg, line, wiki);
 }
 
 module.exports = {

+ 80 - 89
cmds/settings.js

@@ -1,6 +1,7 @@
 const cheerio = require('cheerio');
-const {MessageEmbed} = require('discord.js');
+const {MessageEmbed, Util, MessageActionRow, MessageButton} = require('discord.js');
 const {defaultSettings} = require('../util/default.json');
+const {got} = require('../util/functions.js');
 const Lang = require('../util/i18n.js');
 const allLangs = Lang.allLangs();
 const Wiki = require('../util/wiki.js');
@@ -17,7 +18,7 @@ var db = require('../util/database.js');
 function cmd_settings(lang, msg, args, line, wiki) {
 	if ( !msg.isAdmin() ) return msg.reactEmoji('❌');
 	
-	db.query( 'SELECT channel, wiki, lang, role, inline, prefix FROM discord WHERE guild = $1 ORDER BY channel DESC NULLS LAST', [msg.guild.id] ).then( ({rows}) => {
+	db.query( 'SELECT channel, wiki, lang, role, inline, prefix FROM discord WHERE guild = $1 ORDER BY channel DESC NULLS LAST', [msg.guildId] ).then( ({rows}) => {
 		var guild = rows.find( row => !row.channel );
 		if ( !guild ) guild = Object.assign({
 			role: null, inline: null,
@@ -25,64 +26,52 @@ function cmd_settings(lang, msg, args, line, wiki) {
 		}, defaultSettings);
 		var prefix = guild.prefix;
 		var inlinepage = ( lang.localNames.page || 'page' );
-		var button = {
-			type: 2,
-			style: 5,
-			label: lang.get('settings.button'),
-			emoji: {
-				id: '588723255972593672',
-				name: 'wikibot',
-				animated: false
-			},
-			url: new URL(`/guild/${msg.guild.id}/settings`, process.env.dashboard).href,
-			disabled: false
-		};
+		var button = null;
 		var components = [];
-		if ( process.env.dashboard ) components.push({
-			type: 1,
-			components: [
-				button
-			]
-		});
+		if ( process.env.dashboard ) {
+			button = new MessageButton().setLabel(lang.get('settings.button')).setEmoji('<:wikibot:588723255972593672>').setStyle('LINK').setURL(new URL(`/guild/${msg.guildId}/settings`, process.env.dashboard).href);
+			components.push(new MessageActionRow().addComponents(button));
+		}
 		var text = lang.get('settings.missing', '`' + prefix + 'settings lang`', '`' + prefix + 'settings wiki`');
 		if ( rows.length ) {
 			text = lang.get('settings.current');
-			if ( process.env.dashboard ) text += `\n<${button.url}>`;
+			if ( button ) text += `\n<${button.url}>`;
 			text += '\n' + lang.get('settings.currentlang') + ' `' + allLangs.names[guild.lang] + '` - `' + prefix + 'settings lang`';
-			if ( patreons[msg.guild.id] ) text += '\n' + lang.get('settings.currentprefix') + ' `' + prefix + '` - `' + prefix + 'settings prefix`';
+			if ( patreons[msg.guildId] ) text += '\n' + lang.get('settings.currentprefix') + ' `' + prefix + '` - `' + prefix + 'settings prefix`';
 			text += '\n' + lang.get('settings.currentrole') + ' ' + ( guild.role ? `<@&${guild.role}>` : '@everyone' ) + ' - `' + prefix + 'settings role`';
 			text += '\n' + lang.get('settings.currentinline') + ' ' + ( guild.inline ? '~~' : '' ) + '`[[' + inlinepage + ']]`' + ( guild.inline ? '~~' : '' ) + ' - `' + prefix + 'settings inline`';
 			text += '\n' + lang.get('settings.currentwiki') + ' ' + guild.wiki + ' - `' + prefix + 'settings wiki`';
 			text += '\n' + lang.get('settings.currentchannel') + ' `' + prefix + 'settings channel`\n';
 			if ( rows.length === 1 ) text += lang.get('settings.nochannels');
-			else text += rows.filter( row => row !== guild ).map( row => '<#' + row.channel.replace( /^#/, '' ) + '>: ' + ( patreons[msg.guild.id] ? '`' + allLangs.names[row.lang] + '` - ' : '' ) + '<' + row.wiki + '>' + ( patreons[msg.guild.id] ? ' - ' + ( row.role ? `<@&${row.role}>` : '@everyone' ) + ' - ' + ( row.inline ? '~~' : '' ) + '`[[' + inlinepage + ']]`' + ( row.inline ? '~~' : '' ) : '' ) ).join('\n');
+			else text += rows.filter( row => row !== guild ).map( row => '<#' + row.channel.replace( /^#/, '' ) + '>: ' + ( patreons[msg.guildId] ? '`' + allLangs.names[row.lang] + '` - ' : '' ) + '<' + row.wiki + '>' + ( patreons[msg.guildId] ? ' - ' + ( row.role ? `<@&${row.role}>` : '@everyone' ) + ' - ' + ( row.inline ? '~~' : '' ) + '`[[' + inlinepage + ']]`' + ( row.inline ? '~~' : '' ) : '' ) ).join('\n');
 		}
 		
 		if ( !args.length ) {
-			return msg.replyMsg( text, {split:true,components}, true );
+			return Util.splitMessage( text ).forEach( textpart => msg.replyMsg( {content: textpart, components}, true ) );
 		}
+		var channelId = ( msg.channel.isThread() ? msg.channel.parentId : msg.channelId );
 		
 		var prelang = '';
 		args[0] = args[0].toLowerCase();
 		if ( args[0] === 'channel' ) {
 			prelang = 'channel ';
-			if ( !rows.length ) return msg.replyMsg( text, {split:true,components}, true );
+			if ( !rows.length ) return Util.splitMessage( text ).forEach( textpart => msg.replyMsg( {content: textpart, components}, true ) );
 			
-			var channel = rows.find( row => row.channel === msg.channel.id );
+			var channel = rows.find( row => row.channel === channelId );
 			if ( !channel ) channel = Object.assign({}, rows.find( row => {
-				return ( row.channel === '#' + msg.channel.parentID );
-			} ) || guild, {channel: msg.channel.id});
-			text = lang.get('settings.' + prelang + 'current');
-			button.url = new URL(`/guild/${msg.guild.id}/settings/${msg.channel.id}`, process.env.dashboard).href;
-			if ( process.env.dashboard ) text += `\n<${button.url}>`;
-			if ( patreons[msg.guild.id] ) {
+				return ( row.channel === '#' + msg.channel.parentId );
+			} ) || guild, {channel: channelId});
+			text = lang.get('settings.channel current');
+			button?.setURL(new URL(`/guild/${msg.guildId}/settings/${channelId}`, button.url).href);
+			if ( button ) text += `\n<${button.url}>`;
+			if ( patreons[msg.guildId] ) {
 				text += '\n' + lang.get('settings.currentlang') + ' `' + allLangs.names[channel.lang] + '` - `' + prefix + 'settings channel lang`';
 				text += '\n' + lang.get('settings.currentrole') + ' ' + ( channel.role ? `<@&${channel.role}>` : '@everyone' ) + ' - `' + prefix + 'settings channel role`';
 				text += '\n' + lang.get('settings.currentinline') + ' ' + ( channel.inline ? '~~' : '' ) + '`[[' + inlinepage + ']]`' + ( channel.inline ? '~~' : '' ) + ' - `' + prefix + 'settings channel inline`';
 			}
 			text += '\n' + lang.get('settings.currentwiki') + ' ' + channel.wiki + ' - `' + prefix + 'settings channel wiki`';
 			
-			if ( !args[1] ) return msg.replyMsg( text, {components}, true );
+			if ( !args[1] ) return msg.replyMsg( {content: text, components}, true );
 			
 			args[0] = args[1].toLowerCase();
 			args[1] = args.slice(2).join(' ').toLowerCase().trim().replace( /^<\s*(.*)\s*>$/, '$1' );
@@ -93,15 +82,15 @@ function cmd_settings(lang, msg, args, line, wiki) {
 			prelang += 'wiki';
 			var wikihelp = '\n' + lang.get('settings.wikihelp', prefix + 'settings ' + prelang);
 			if ( !args[1] ) {
-				if ( !rows.length ) return msg.replyMsg( lang.get('settings.wikimissing') + wikihelp, {components}, true );
-				else return msg.replyMsg( lang.get('settings.' + prelang) + ' ' + ( channel || guild ).wiki + wikihelp, {components}, true );
+				if ( !rows.length ) return msg.replyMsg( {content: lang.get('settings.wikimissing') + wikihelp, components}, true );
+				else return msg.replyMsg( {content: lang.get('settings.' + prelang) + ' ' + ( channel || guild ).wiki + wikihelp, components}, true );
 			}
-			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 			var wikinew = Wiki.fromInput(args[1]);
 			if ( !wikinew ) {
-				var text = lang.get('settings.wikiinvalid') + wikihelp;
-				//text += '\n\n' + lang.get('settings.foundwikis') + '\n' + sites.map( site => site.wiki_display_name + ': `' + site.wiki_domain + '`' ).join('\n');
-				return msg.replyMsg( text, {split:true,components}, true );
+				let wikisuggest = lang.get('settings.wikiinvalid') + wikihelp;
+				//wikisuggest += '\n\n' + lang.get('settings.foundwikis') + '\n' + sites.map( site => site.wiki_display_name + ': `' + site.wiki_domain + '`' ).join('\n');
+				return Util.splitMessage( wikisuggest ).forEach( textpart => msg.replyMsg( {content: textpart, components}, true ) );
 			}
 			return msg.reactEmoji('⏳', true).then( reaction => {
 				got.get( wikinew + 'api.php?&action=query&meta=siteinfo&siprop=general&format=json', {
@@ -126,10 +115,10 @@ function cmd_settings(lang, msg, args, line, wiki) {
 						console.log( '- ' + response.statusCode + ': Error while testing the wiki: ' + body?.error?.info );
 						if ( reaction ) reaction.removeEmoji();
 						if ( body?.error?.info === 'You need read permission to use this module.' ) {
-							return msg.replyMsg( lang.get('settings.wikiinvalid_private') + wikihelp, {components}, true );
+							return msg.replyMsg( {content: lang.get('settings.wikiinvalid_private') + wikihelp, components}, true );
 						}
 						msg.reactEmoji('nowiki', true);
-						return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {components}, true );
+						return msg.replyMsg( {content: lang.get('settings.wikiinvalid') + wikihelp, components}, true );
 					}
 					wikinew.updateWiki(body.query.general);
 					var embed;
@@ -147,18 +136,18 @@ function cmd_settings(lang, msg, args, line, wiki) {
 						}
 					}
 					var sql = 'UPDATE discord SET wiki = $1 WHERE guild = $2 AND wiki = $3';
-					var sqlargs = [wikinew.href, msg.guild.id, guild.wiki];
+					var sqlargs = [wikinew.href, msg.guildId, guild.wiki];
 					if ( !rows.length ) {
 						sql = 'INSERT INTO discord(wiki, guild, main, lang) VALUES($1, $2, $2, $3)';
 						sqlargs[2] = lang.lang;
 					}
 					else if ( channel ) {
 						sql = 'UPDATE discord SET wiki = $1 WHERE guild = $2 AND channel = $3';
-						sqlargs[2] = msg.channel.id;
+						sqlargs[2] = channelId;
 						if ( !rows.includes( channel ) ) {
 							if ( channel.wiki === wikinew.href ) {
 								if ( reaction ) reaction.removeEmoji();
-								return msg.replyMsg( lang.get('settings.' + prelang + 'changed') + ' ' + channel.wiki + wikihelp, {embed,components}, true );
+								return msg.replyMsg( {content: lang.get('settings.' + prelang + 'changed') + ' ' + channel.wiki + wikihelp, embeds: [embed], components}, true );
 							}
 							sql = 'INSERT INTO discord(wiki, guild, channel, lang, role, inline, prefix) VALUES($1, $2, $3, $4, $5, $6, $7)';
 							sqlargs.push(guild.lang, guild.role, guild.inline, guild.prefix);
@@ -173,9 +162,9 @@ function cmd_settings(lang, msg, args, line, wiki) {
 							} );
 							guild.wiki = wikinew.href;
 						}
-						if ( channel || !rows.some( row => row.channel === msg.channel.id ) ) wiki = new Wiki(wikinew);
+						if ( channel || !rows.some( row => row.channel === channelId ) ) wiki = new Wiki(wikinew);
 						if ( reaction ) reaction.removeEmoji();
-						msg.replyMsg( lang.get('settings.' + prelang + 'changed') + ' ' + wikinew + wikihelp, {embed,components}, true );
+						msg.replyMsg( {content: lang.get('settings.' + prelang + 'changed') + ' ' + wikinew + wikihelp, embeds: [embed], components}, true );
 						var channels = rows.filter( row => row.channel && row.lang === guild.lang && row.wiki === guild.wiki && row.prefix === guild.prefix && row.role === guild.role && row.inline === guild.inline ).map( row => row.channel );
 						if ( channels.length ) db.query( 'DELETE FROM discord WHERE channel IN (' + channels.map( (row, i) => '$' + ( i + 1 ) ).join(', ') + ')', channels ).then( () => {
 							console.log( '- Settings successfully removed.' );
@@ -184,48 +173,48 @@ function cmd_settings(lang, msg, args, line, wiki) {
 						} );
 					}, dberror => {
 						console.log( '- Error while editing the settings: ' + dberror );
-						msg.replyMsg( lang.get('settings.save_failed'), {embed,components}, true );
+						msg.replyMsg( {content: lang.get('settings.save_failed'), embeds: [embed], components}, true );
 						if ( reaction ) reaction.removeEmoji();
 					} );
 				}, ferror => {
 					if ( reaction ) reaction.removeEmoji();
 					if ( ferror.message?.startsWith( 'connect ECONNREFUSED ' ) || ferror.message?.startsWith( 'Hostname/IP does not match certificate\'s altnames: ' ) || ferror.message === 'certificate has expired' || ferror.message === 'self signed certificate' ) {
 						console.log( '- Error while testing the wiki: No HTTPS' );
-						return msg.replyMsg( lang.get('settings.wikiinvalid_http') + wikihelp, {components}, true );
+						return msg.replyMsg( {content: lang.get('settings.wikiinvalid_http') + wikihelp, components}, true );
 					}
 					console.log( '- Error while testing the wiki: ' + ferror );
 					if ( ferror.message === `Timeout awaiting 'request' for ${got.defaults.options.timeout.request}ms` ) {
-						return msg.replyMsg( lang.get('settings.wikiinvalid_timeout') + wikihelp, {components}, true );
+						return msg.replyMsg( {content: lang.get('settings.wikiinvalid_timeout') + wikihelp, components}, true );
 					}
 					msg.reactEmoji('nowiki', true);
-					return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {components}, true );
+					return msg.replyMsg( {content: lang.get('settings.wikiinvalid') + wikihelp, components}, true );
 				} );
 			} );
 		}
 		
 		if ( args[0] === 'lang' || args[0] === 'language' ) {
-			if ( channel && !patreons[msg.guild.id] ) return msg.replyMsg( lang.get('general.patreon') + '\n<' + process.env.patreon + '>', {}, true );
+			if ( channel && !patreons[msg.guildId] ) return msg.replyMsg( lang.get('general.patreon') + '\n<' + process.env.patreon + '>', true );
 			prelang += 'lang';
 			var langhelp = '\n' + lang.get('settings.langhelp', prefix + 'settings ' + prelang) + ' `' + Object.values(allLangs.names).join('`, `') + '`';
 			if ( !args[1] ) {
-				return msg.replyMsg( lang.get('settings.' + prelang) + ' `' + allLangs.names[( channel || guild ).lang] + '`' + langhelp, {files:( msg.uploadFiles() ? [`./i18n/widgets/${( channel || guild ).lang}.png`] : [] ),components}, true );
+				return msg.replyMsg( {content: lang.get('settings.' + prelang) + ' `' + allLangs.names[( channel || guild ).lang] + '`' + langhelp, files: ( msg.uploadFiles() ? [`./i18n/widgets/${( channel || guild ).lang}.png`] : [] ), components}, true );
 			}
-			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 			if ( !allLangs.map.hasOwnProperty(args[1]) ) {
-				return msg.replyMsg( lang.get('settings.langinvalid') + langhelp, {components}, true );
+				return msg.replyMsg( {content: lang.get('settings.langinvalid') + langhelp, components}, true );
 			}
 			var sql = 'UPDATE discord SET lang = $1 WHERE guild = $2 AND lang = $3';
-			var sqlargs = [allLangs.map[args[1]], msg.guild.id, guild.lang];
+			var sqlargs = [allLangs.map[args[1]], msg.guildId, guild.lang];
 			if ( !rows.length ) {
 				sql = 'INSERT INTO discord(lang, guild, main) VALUES($1, $2, $2)';
 				sqlargs.pop();
 			}
 			else if ( channel ) {
 				sql = 'UPDATE discord SET lang = $1 WHERE guild = $2 AND channel = $3';
-				sqlargs[2] = msg.channel.id;
+				sqlargs[2] = channelId;
 				if ( !rows.includes( channel ) ) {
 					if ( channel.lang === allLangs.map[args[1]] ) {
-						return msg.replyMsg( lang.get('settings.' + prelang + 'changed') + ' `' + allLangs.names[channel.lang] + '`' + langhelp, {files:( msg.uploadFiles() ? [`./i18n/widgets/${channel.lang}.png`] : [] ),components}, true );
+						return msg.replyMsg( {content: lang.get('settings.' + prelang + 'changed') + ' `' + allLangs.names[channel.lang] + '`' + langhelp, files: ( msg.uploadFiles() ? [`./i18n/widgets/${channel.lang}.png`] : [] ), components}, true );
 					}
 					sql = 'INSERT INTO discord(lang, guild, channel, wiki, role, inline, prefix) VALUES($1, $2, $3, $4, $5, $6, $7)';
 					sqlargs.push(guild.wiki, guild.role, guild.inline, guild.prefix);
@@ -239,10 +228,10 @@ function cmd_settings(lang, msg, args, line, wiki) {
 						if ( row.channel && row.lang === guild.lang ) row.lang = allLangs.map[args[1]];
 					} );
 					guild.lang = allLangs.map[args[1]];
-					if ( voice[msg.guild.id] ) voice[msg.guild.id] = guild.lang;
+					if ( voice[msg.guildId] ) voice[msg.guildId] = guild.lang;
 				}
-				if ( channel || !patreons[msg.guild.id] || !rows.some( row => row.channel === msg.channel.id ) ) lang = new Lang(allLangs.map[args[1]]);
-				msg.replyMsg( lang.get('settings.' + prelang + 'changed') + ' `' + allLangs.names[allLangs.map[args[1]]] + '`\n' + lang.get('settings.langhelp', prefix + 'settings ' + prelang) + ' `' + Object.values(allLangs.names).join('`, `') + '`', {files:( msg.uploadFiles() ? [`./i18n/widgets/${allLangs.map[args[1]]}.png`] : [] ),components}, true );
+				if ( channel || !patreons[msg.guildId] || !rows.some( row => row.channel === channelId ) ) lang = new Lang(allLangs.map[args[1]]);
+				msg.replyMsg( {content: lang.get('settings.' + prelang + 'changed') + ' `' + allLangs.names[allLangs.map[args[1]]] + '`\n' + lang.get('settings.langhelp', prefix + 'settings ' + prelang) + ' `' + Object.values(allLangs.names).join('`, `') + '`', files: ( msg.uploadFiles() ? [`./i18n/widgets/${allLangs.map[args[1]]}.png`] : [] ), components}, true );
 				var channels = rows.filter( row => row.channel && row.lang === guild.lang && row.wiki === guild.wiki && row.prefix === guild.prefix && row.role === guild.role && row.inline === guild.inline ).map( row => row.channel );
 				if ( channels.length ) db.query( 'DELETE FROM discord WHERE channel IN (' + channels.map( (row, i) => '$' + ( i + 1 ) ).join(', ') + ')', channels ).then( () => {
 					console.log( '- Settings successfully removed.' );
@@ -251,40 +240,40 @@ function cmd_settings(lang, msg, args, line, wiki) {
 				} );
 			}, dberror => {
 				console.log( '- Error while editing the settings: ' + dberror );
-				msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+				msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 			} );
 		}
 		
 		if ( args[0] === 'role' ) {
-			if ( channel && !patreons[msg.guild.id] ) return msg.replyMsg( lang.get('general.patreon') + '\n<' + process.env.patreon + '>', {}, true );
+			if ( channel && !patreons[msg.guildId] ) return msg.replyMsg( lang.get('general.patreon') + '\n<' + process.env.patreon + '>', true );
 			prelang += 'role';
 			var rolehelp = '\n' + lang.get('settings.rolehelp', prefix + 'settings ' + prelang);
 			if ( !args[1] ) {
-				return msg.replyMsg( lang.get('settings.' + prelang) + ' ' + ( ( channel || guild ).role ? `<@&${( channel || guild ).role}>` : '@everyone' ) + rolehelp, {components}, true );
+				return msg.replyMsg( {content: lang.get('settings.' + prelang) + ' ' + ( ( channel || guild ).role ? `<@&${( channel || guild ).role}>` : '@everyone' ) + rolehelp, components}, true );
 			}
-			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 			var role = null;
 			if ( /^\d+$/.test(args[1]) ) role = msg.guild.roles.cache.get(args[1]);
 			if ( !role ) role = msg.guild.roles.cache.find( gc => gc.name.toLowerCase() === args[1].replace( /^@/, '' ) );
 			if ( !role && ['everyone', 'here', 'none', 'all'].includes( args[1].replace( /^@/, '' ) ) ) {
-				role = msg.guild.roles.cache.get(msg.guild.id);
+				role = msg.guild.roles.cache.get(msg.guildId);
 			}
 			if ( !role ) {
-				return msg.replyMsg( lang.get('settings.roleinvalid') + rolehelp, {components}, true );
+				return msg.replyMsg( {content: lang.get('settings.roleinvalid') + rolehelp, components}, true );
 			}
-			role = ( role.id === msg.guild.id ? null : role.id );
+			role = ( role.id === msg.guildId ? null : role.id );
 			var sql = 'UPDATE discord SET role = $1 WHERE guild = $2';
-			var sqlargs = [role, msg.guild.id];
+			var sqlargs = [role, msg.guildId];
 			if ( !rows.length ) {
 				sql = 'INSERT INTO discord(role, guild, main, lang) VALUES($1, $2, $2, $3)';
 				sqlargs.push(lang.lang);
 			}
 			else if ( channel ) {
 				sql = 'UPDATE discord SET role = $1 WHERE guild = $2 AND channel = $3';
-				sqlargs.push(msg.channel.id);
+				sqlargs.push(channelId);
 				if ( !rows.includes( channel ) ) {
 					if ( channel.role === role ) {
-						return msg.replyMsg( lang.get('settings.' + prelang + 'changed') + ' ' + ( channel.role ? `<@&${channel.role}>` : '@everyone' ) + rolehelp, {components}, true );
+						return msg.replyMsg( {content: lang.get('settings.' + prelang + 'changed') + ' ' + ( channel.role ? `<@&${channel.role}>` : '@everyone' ) + rolehelp, components}, true );
 					}
 					sql = 'INSERT INTO discord(role, guild, channel, wiki, lang, inline, prefix) VALUES($1, $2, $3, $4, $5, $6, $7)';
 					sqlargs.push(guild.wiki, guild.lang, guild.inline, guild.prefix);
@@ -304,7 +293,7 @@ function cmd_settings(lang, msg, args, line, wiki) {
 					} );
 					guild.role = role;
 				}
-				msg.replyMsg( lang.get('settings.' + prelang + 'changed') + ' ' + ( role ? `<@&${role}>` : '@everyone' ) + rolehelp, {components}, true );
+				msg.replyMsg( {content: lang.get('settings.' + prelang + 'changed') + ' ' + ( role ? `<@&${role}>` : '@everyone' ) + rolehelp, components}, true );
 				var channels = rows.filter( row => row.channel && row.lang === guild.lang && row.wiki === guild.wiki && row.prefix === guild.prefix && row.role === guild.role && row.inline === guild.inline ).map( row => row.channel );
 				if ( channels.length ) db.query( 'DELETE FROM discord WHERE channel IN (' + channels.map( (row, i) => '$' + ( i + 1 ) ).join(', ') + ')', channels ).then( () => {
 					console.log( '- Settings successfully removed.' );
@@ -313,26 +302,26 @@ function cmd_settings(lang, msg, args, line, wiki) {
 				} );
 			}, dberror => {
 				console.log( '- Error while editing the settings: ' + dberror );
-				msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+				msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 			} );
 		}
 		
 		if ( args[0] === 'prefix' && !channel ) {
-			if ( !patreons[msg.guild.id] ) {
-				return msg.replyMsg( lang.get('general.patreon') + '\n<' + process.env.patreon + '>', {}, true );
+			if ( !patreons[msg.guildId] ) {
+				return msg.replyMsg( lang.get('general.patreon') + '\n<' + process.env.patreon + '>', true );
 			}
 			var prefixhelp = '\n' + lang.get('settings.prefixhelp', prefix + 'settings prefix');
 			args[1] = args[1].replace( /(?<!\\)_$/, ' ' ).replace( /\\([_\W])/g, '$1' );
 			if ( !args[1].trim() ) {
-				return msg.replyMsg( lang.get('settings.prefix') + ' `' + prefix + '`' + prefixhelp, {components}, true );
+				return msg.replyMsg( {content: lang.get('settings.prefix') + ' `' + prefix + '`' + prefixhelp, components}, true );
 			}
-			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 			if ( args[1].includes( '`' ) || args[1].includes( '\\' ) || args[1].length > 100 ) {
-				return msg.replyMsg( lang.get('settings.prefixinvalid') + prefixhelp, {components}, true );
+				return msg.replyMsg( {content: lang.get('settings.prefixinvalid') + prefixhelp, components}, true );
 			}
 			if ( args[1] === 'reset' || args[1] === 'default' ) args[1] = process.env.prefix;
 			var sql = 'UPDATE discord SET prefix = $1 WHERE guild = $2';
-			var sqlargs = [args[1], msg.guild.id];
+			var sqlargs = [args[1], msg.guildId];
 			if ( !rows.length ) {
 				sql = 'INSERT INTO discord(prefix, guild, main, lang) VALUES($1, $2, $2, $3)';
 				sqlargs.push(lang.lang);
@@ -340,33 +329,35 @@ function cmd_settings(lang, msg, args, line, wiki) {
 			return db.query( sql, sqlargs ).then( () => {
 				console.log( '- Settings successfully updated.' );
 				guild.prefix = args[1];
-				msg.client.shard.broadcastEval( `global.patreons['${msg.guild.id}'] = '${args[1]}'` );
-				msg.replyMsg( lang.get('settings.prefixchanged') + ' `' + args[1] + '`\n' + lang.get('settings.prefixhelp', args[1] + 'settings prefix'), {components}, true );
+				msg.client.shard.broadcastEval( (discordClient, evalData) => {
+					global.patreons[evalData.guild] = evalData.prefix;
+				}, {context: {guild: msg.guildId, prefix: args[1]}} );
+				msg.replyMsg( {content: lang.get('settings.prefixchanged') + ' `' + args[1] + '`\n' + lang.get('settings.prefixhelp', args[1] + 'settings prefix'), components}, true );
 			}, dberror => {
 				console.log( '- Error while editing the settings: ' + dberror );
-				msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+				msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 			} );
 		}
 		
 		if ( args[0] === 'inline' ) {
-			if ( channel && !patreons[msg.guild.id] ) return msg.replyMsg( lang.get('general.patreon') + '\n<' + process.env.patreon + '>', {}, true );
+			if ( channel && !patreons[msg.guildId] ) return msg.replyMsg( lang.get('general.patreon') + '\n<' + process.env.patreon + '>', true );
 			prelang += 'inline';
 			var toggle = 'inline ' + ( ( channel || guild ).inline ? 'disabled' : 'enabled' );
 			var inlinehelp = '\n' + lang.get('settings.' + toggle + '.help', prefix + 'settings ' + prelang + ' toggle', inlinepage);
 			if ( args[1] !== 'toggle' ) {
-				return msg.replyMsg( lang.get('settings.' + toggle + '.' + prelang) + inlinehelp, {components}, true );
+				return msg.replyMsg( {content: lang.get('settings.' + toggle + '.' + prelang) + inlinehelp, components}, true );
 			}
-			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 			var value = ( ( channel || guild ).inline ? null : 1 );
 			var sql = 'UPDATE discord SET inline = $1 WHERE guild = $2';
-			var sqlargs = [value, msg.guild.id];
+			var sqlargs = [value, msg.guildId];
 			if ( !rows.length ) {
 				sql = 'INSERT INTO discord(inline, guild, main, lang) VALUES($1, $2, $2, $3)';
 				sqlargs.push(lang.lang);
 			}
 			else if ( channel ) {
 				sql = 'UPDATE discord SET inline = $1 WHERE guild = $2 AND channel = $3';
-				sqlargs.push(msg.channel.id);
+				sqlargs.push(channelId);
 				if ( !rows.includes( channel ) ) {
 					sql = 'INSERT INTO discord(inline, guild, channel, wiki, lang, role, prefix) VALUES($1, $2, $3, $4, $5, $6, $7)';
 					sqlargs.push(guild.wiki, guild.lang, guild.role, guild.prefix);
@@ -382,7 +373,7 @@ function cmd_settings(lang, msg, args, line, wiki) {
 					guild.inline = value;
 				}
 				toggle = 'inline ' + ( ( channel || guild ).inline ? 'disabled' : 'enabled' );
-				msg.replyMsg( lang.get('settings.' + toggle + '.' + prelang + 'changed') + '\n' + lang.get('settings.' + toggle + '.help', prefix + 'settings ' + prelang + ' toggle', inlinepage), {components}, true );
+				msg.replyMsg( {content: lang.get('settings.' + toggle + '.' + prelang + 'changed') + '\n' + lang.get('settings.' + toggle + '.help', prefix + 'settings ' + prelang + ' toggle', inlinepage), components}, true );
 				var channels = rows.filter( row => row.channel && row.lang === guild.lang && row.wiki === guild.wiki && row.prefix === guild.prefix && row.role === guild.role && row.inline === guild.inline ).map( row => row.channel );
 				if ( channels.length ) db.query( 'DELETE FROM discord WHERE channel IN (' + channels.map( (row, i) => '$' + ( i + 1 ) ).join(', ') + ')', channels ).then( () => {
 					console.log( '- Settings successfully removed.' );
@@ -391,11 +382,11 @@ function cmd_settings(lang, msg, args, line, wiki) {
 				} );
 			}, dberror => {
 				console.log( '- Error while editing the settings: ' + dberror );
-				msg.replyMsg( lang.get('settings.save_failed'), {components}, true );
+				msg.replyMsg( {content: lang.get('settings.save_failed'), components}, true );
 			} );
 		}
 		
-		return msg.replyMsg( text, {split:true,components}, true );
+		return Util.splitMessage( text ).forEach( textpart => msg.replyMsg( {content: textpart, components}, true ) );
 	}, dberror => {
 		console.log( '- Error while getting the settings: ' + dberror );
 		msg.reactEmoji('error', true);

+ 4 - 4
cmds/stop.js

@@ -9,13 +9,13 @@
  */
 async function cmd_stop(lang, msg, args, line, wiki) {
 	if ( args[0] === 'force' && args.slice(1).join(' ').split('\n')[0].isMention(msg.guild) ) {
-		await msg.replyMsg( 'I\'ll destroy myself now!', {}, true );
+		await msg.replyMsg( 'I\'ll destroy myself now!', true );
 		await msg.client.shard.send('SIGKILL');
 	} else if ( args.join(' ').split('\n')[0].isMention(msg.guild) ) {
-		await msg.replyMsg( 'I\'ll restart myself now!', {}, true );
+		await msg.replyMsg( 'I\'ll restart myself now!', true );
 		console.log( '\n- Restarting all shards!\n\n' );
-		await msg.client.shard.respawnAll();
-	} else if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) {
+		await msg.client.shard.respawnAll({timeout: -1});
+	} else if ( !msg.channel.isGuild() || !pause[msg.guildId] ) {
 		this.LINK(lang, msg, line, wiki);
 	}
 }

+ 13 - 15
cmds/test.js

@@ -1,5 +1,6 @@
 const {MessageEmbed} = require('discord.js');
 const help_setup = require('../functions/helpsetup.js');
+const {got} = require('../util/functions.js');
 const logging = require('../util/logging.js');
 
 const wsStatus = [
@@ -24,19 +25,19 @@ const wsStatus = [
  */
 function cmd_test(lang, msg, args, line, wiki) {
 	if ( args.join('') ) {
-		if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
+		if ( !msg.channel.isGuild() || !pause[msg.guildId] ) this.LINK(lang, msg, line, wiki);
 	}
-	else if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) {
+	else if ( !msg.channel.isGuild() || !pause[msg.guildId] ) {
 		if ( msg.isAdmin() && msg.defaultSettings ) help_setup(lang, msg);
 		let textList = lang.get('test.text').filter( text => text.trim() );
 		var text = ( textList[Math.floor(Math.random() * ( textList.length * 5 ))] || lang.get('test.text.0') );
 		if ( process.env.READONLY ) text = lang.get('general.readonly') + '\n' + process.env.invite;
-		console.log( '- Test[' + global.shardId + ']: Fully functioning!' );
+		console.log( '- Test[' + process.env.SHARDS + ']: Fully functioning!' );
 		var now = Date.now();
 		msg.replyMsg( text ).then( message => {
 			if ( !message ) return;
 			var then = Date.now();
-			var embed = new MessageEmbed().setTitle( lang.get('test.time') ).setFooter( 'Shard: ' + global.shardId ).addField( 'Discord', ( then - now ).toLocaleString(lang.get('dateformat')) + 'ms' );
+			var embed = new MessageEmbed().setTitle( lang.get('test.time') ).setFooter( 'Shard: ' + process.env.SHARDS ).addField( 'Discord', ( then - now ).toLocaleString(lang.get('dateformat')) + 'ms' );
 			now = Date.now();
 			got.get( wiki + 'api.php?action=query&meta=siteinfo&siprop=general&format=json', {
 				timeout: 10000
@@ -45,11 +46,8 @@ function cmd_test(lang, msg, args, line, wiki) {
 				var body = response.body;
 				if ( body && body.warnings ) log_warn(body.warnings);
 				var ping = ( then - now ).toLocaleString(lang.get('dateformat')) + 'ms';
-				if ( body?.query?.general ) {
-					wiki.updateWiki(body.query.general);
-					embed.addField( wiki.toLink(), ping );
-				}
-				else embed.addField( wiki, ping );
+				if ( body?.query?.general ) wiki.updateWiki(body.query.general);
+				embed.addField( wiki.toLink(), ping );
 				var notice = [];
 				if ( response.statusCode !== 200 || !body?.query?.general ) {
 					if ( wiki.noWiki(response.url, response.statusCode) ) {
@@ -62,13 +60,13 @@ function cmd_test(lang, msg, args, line, wiki) {
 					}
 				}
 				else if ( ( msg.isAdmin() || msg.isOwner() ) && !wiki.isFandom() ) {
-					logging(wiki, msg.guild?.id, 'test');
+					logging(wiki, msg.guildId, 'test');
 					if ( body.query.general.generator.replace( /^MediaWiki 1\.(\d\d).*$/, '$1' ) < 30 ) {
 						console.log( '- This wiki is using ' + body.query.general.generator + '.' );
 						notice.push(lang.get('test.MediaWiki', '[MediaWiki 1.30](https://www.mediawiki.org/wiki/MediaWiki_1.30)', body.query.general.generator));
 					}
 				}
-				else logging(wiki, msg.guild?.id, 'test');
+				else logging(wiki, msg.guildId, 'test');
 				if ( notice.length ) embed.addField( lang.get('test.notice'), notice.join('\n') );
 			}, error => {
 				then = Date.now();
@@ -81,7 +79,7 @@ function cmd_test(lang, msg, args, line, wiki) {
 					console.log( '- Error while reaching the wiki: ' + error );
 					ping += ' <:error:505887261200613376>';
 				}
-				embed.addField( wiki, ping );
+				embed.addField( wiki.toLink(), ping );
 			} ).finally( () => {
 				if ( msg.isOwner() ) return msg.client.shard.fetchClientValues('ws.status').then( values => {
 					return '```less\n' + values.map( (status, id) => '[' + id + ']: ' + ( wsStatus[status] || status ) ).join('\n') + '\n```';
@@ -89,15 +87,15 @@ function cmd_test(lang, msg, args, line, wiki) {
 					return '```js\n' + error + '\n```';
 				} ).then( shards => {
 					embed.addField( 'Shards', shards );
-					message.edit( message.content, {embed,allowedMentions:{users:[msg.author.id]}} ).catch(log_error);
+					message.edit( {content: message.content, embeds: [embed]} ).catch(log_error);
 				} );
-				message.edit( message.content, {embed,allowedMentions:{users:[msg.author.id]}} ).catch(log_error);
+				message.edit( {content: message.content, embeds: [embed]} ).catch(log_error);
 			} );
 		} );
 	}
 	else {
 		console.log( '- Test: Paused!' );
-		msg.replyMsg( lang.get('test.pause'), {}, true );
+		msg.replyMsg( lang.get('test.pause'), true );
 	}
 }
 

+ 76 - 92
cmds/verification.js

@@ -1,7 +1,8 @@
+const {Util, MessageActionRow, MessageButton, Permissions: {FLAGS}} = require('discord.js');
 const help_setup = require('../functions/helpsetup.js');
 const {limit: {verification: verificationLimit}} = require('../util/default.json');
 var db = require('../util/database.js');
-const slashCommand = require('../util/functions.js').slashCommands.find( slashCommand => slashCommand.name === 'verify' );
+const {got} = require('../util/functions.js');
 
 /**
  * Processes the "verification" command.
@@ -13,45 +14,32 @@ const slashCommand = require('../util/functions.js').slashCommands.find( slashCo
  */
 function cmd_verification(lang, msg, args, line, wiki) {
 	if ( !msg.isAdmin() ) {
-		if ( msg.channel.isGuild() && !pause[msg.guild.id] ) this.verify(lang, msg, args, line, wiki);
+		if ( msg.channel.isGuild() && !pause[msg.guildId] ) this.verify(lang, msg, args, line, wiki);
 		else msg.reactEmoji('❌');
 		return;
 	}
 	if ( msg.defaultSettings ) return help_setup(lang, msg);
-	if ( !msg.guild.me.permissions.has('MANAGE_ROLES') ) {
-		console.log( msg.guild.id + ': Missing permissions - MANAGE_ROLES' );
+	if ( !msg.guild.me.permissions.has(FLAGS.MANAGE_ROLES) ) {
+		console.log( msg.guildId + ': Missing permissions - MANAGE_ROLES' );
 		return msg.replyMsg( lang.get('general.missingperm') + ' `MANAGE_ROLES`' );
 	}
 	
-	db.query( 'SELECT configid, channel, role, editcount, postcount, usergroup, accountage, rename FROM verification WHERE guild = $1 ORDER BY configid ASC', [msg.guild.id] ).then( ({rows}) => {
-		var prefix = ( patreons[msg.guild.id] || process.env.prefix );
-		var button = {
-			type: 2,
-			style: 5,
-			label: lang.get('settings.button'),
-			emoji: {
-				id: '588723255972593672',
-				name: 'wikibot',
-				animated: false
-			},
-			url: new URL(`/guild/${msg.guild.id}/verification`, process.env.dashboard).href,
-			disabled: false
-		};
+	db.query( 'SELECT configid, channel, role, editcount, postcount, usergroup, accountage, rename FROM verification WHERE guild = $1 ORDER BY configid ASC', [msg.guildId] ).then( ({rows}) => {
+		var prefix = ( patreons[msg.guildId] || process.env.prefix );
+		var button = null;
 		var components = [];
-		if ( process.env.dashboard ) components.push({
-			type: 1,
-			components: [
-				button
-			]
-		});
+		if ( process.env.dashboard ) {
+			button = new MessageButton().setLabel(lang.get('settings.button')).setEmoji('<:wikibot:588723255972593672>').setStyle('LINK').setURL(new URL(`/guild/${msg.guildId}/verification`, process.env.dashboard).href);
+			components.push(new MessageActionRow().addComponents(button));
+		}
 		if ( args[0] && args[0].toLowerCase() === 'add' ) {
-			var limit = verificationLimit[( patreons[msg.guild.id] ? 'patreon' : 'default' )];
-			if ( rows.length >= limit ) return msg.replyMsg( lang.get('verification.max_entries'), {}, true );
-			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
-			button.url = new URL(`/guild/${msg.guild.id}/verification/new`, process.env.dashboard).href;
+			var limit = verificationLimit[( patreons[msg.guildId] ? 'patreon' : 'default' )];
+			if ( rows.length >= limit ) return msg.replyMsg( lang.get('verification.max_entries'), true );
+			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
+			button?.setURL(new URL(`/guild/${msg.guildId}/verification/new`, button.url).href);
 			var roles = args.slice(1).join(' ').split('|').map( role => role.replace( /^\s*<?\s*(.*?)\s*>?\s*$/, '$1' ) ).filter( role => role.length );
-			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 );
+			if ( !roles.length ) return msg.replyMsg( {content: lang.get('verification.no_role') + '\n`' + prefix + 'verification add ' + lang.get('verification.new_role') + '`', components}, true );
+			if ( roles.length > 10 ) return msg.replyMsg( {content: lang.get('verification.role_max'), components}, true );
 			roles = roles.map( role => {
 				var new_role = ['', null];
 				if ( role.startsWith( '-' ) ) {
@@ -63,120 +51,116 @@ function cmd_verification(lang, msg, args, line, wiki) {
 				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[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 );
+			if ( roles.some( role => !role[1] ) ) return msg.replyMsg( {content: lang.get('verification.role_missing'), components}, true );
+			if ( roles.some( role => role[1].managed || role[1].id === msg.guildId ) ) return msg.replyMsg( {content: 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++;
 				else break;
 			}
-			return db.query( 'INSERT INTO verification(guild, configid, channel, role) VALUES($1, $2, $3, $4)', [msg.guild.id, new_configid, '|' + msg.channel.id + '|', roles] ).then( () => {
+			return db.query( 'INSERT INTO verification(guild, configid, channel, role) VALUES($1, $2, $3, $4)', [msg.guildId, new_configid, '|' + ( msg.channel.isThread() ? msg.channel.parentId : msg.channelId ) + '|', roles] ).then( () => {
 				console.log( '- Verification successfully added.' );
-				if ( !rows.length && slashCommand?.id ) msg.client.api.applications(msg.client.user.id).guilds(msg.guild.id).commands(slashCommand.id).permissions.put( {
-					data: {
-						permissions: [
-							{
-								id: msg.guild.id,
-								type: 1,
-								permission: true
-							}
-						]
-					}
+				if ( !rows.length ) msg.client.application.commands.cache.find( slashCommand => slashCommand.name === 'verify' )?.permissions.set( {
+					guild: msg.guildId,
+					permissions: [{
+						id: msg.guildId,
+						type: 'ROLE',
+						permission: true
+					}]
 				} ).then( () => {
 					console.log( '- Slash command successfully enabled.' );
 				}, error => {
 					console.log( '- Error while enabling the slash command: ' + error );
 				} );
-				msg.replyMsg( lang.get('verification.added') + formatVerification(false, false, {configid: new_configid, role: roles}), {components}, true );
+				msg.replyMsg( {content: lang.get('verification.added') + formatVerification(false, false, {configid: new_configid, role: roles}), components}, true );
 			}, dberror => {
 				console.log( '- Error while adding the verification: ' + dberror );
-				msg.replyMsg( lang.get('verification.save_failed'), {components}, true );
+				msg.replyMsg( {content: lang.get('verification.save_failed'), components}, true );
 			} );
 		}
 		if ( !rows.some( row => row.configid.toString() === args[0] ) ) {
 			if ( args.length ) {
-				if ( !pause[msg.guild.id] ) this.verify(lang, msg, args, line, wiki);
+				if ( !pause[msg.guildId] ) this.verify(lang, msg, args, line, wiki);
 				return;
 			}
 			var text = '';
 			if ( rows.length ) {
 				text += lang.get('verification.current');
-				if ( process.env.dashboard ) text += `\n<${button.url}>`;
+				if ( button ) text += `\n<${button.url}>`;
 				text += rows.map( row => formatVerification(false, true, row) ).join('');
 			}
 			else {
 				text += lang.get('verification.missing');
-				if ( process.env.dashboard ) text += `\n<${button.url}>`;
+				if ( button ) text += `\n<${button.url}>`;
 			}
 			text += '\n\n' + lang.get('verification.add_more') + '\n`' + prefix + 'verification add ' + lang.get('verification.new_role') + '`';
-			return msg.sendChannel( '<@' + msg.author.id + '>, ' + text, {split:true,components}, true );
+			return Util.splitMessage( text ).forEach( textpart => msg.replyMsg( {content: textpart, components}, true ) );
 		}
 		var row = rows.find( row => row.configid.toString() === args[0] );
 		if ( args[1] ) args[1] = args[1].toLowerCase();
 		if ( args[1] === 'delete' && !args.slice(2).join('') ) {
-			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
-			return db.query( 'DELETE FROM verification WHERE guild = $1 AND configid = $2', [msg.guild.id, row.configid] ).then( () => {
+			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
+			return db.query( 'DELETE FROM verification WHERE guild = $1 AND configid = $2', [msg.guildId, row.configid] ).then( () => {
 				console.log( '- Verification successfully removed.' );
-				if ( rows.length === 1 && slashCommand?.id ) msg.client.api.applications(msg.client.user.id).guilds(msg.guild.id).commands(slashCommand.id).permissions.put( {
-					data: {
-						permissions: []
-					}
+				if ( rows.length === 1 ) msg.client.application.commands.cache.find( slashCommand => slashCommand.name === 'verify' )?.permissions.set( {
+					guild: msg.guildId,
+					permissions: []
 				} ).then( () => {
 					console.log( '- Slash command successfully disabled.' );
 				}, error => {
 					console.log( '- Error while disabling the slash command: ' + error );
 				} );
-				msg.replyMsg( lang.get('verification.deleted'), {components}, true );
+				msg.replyMsg( {content: lang.get('verification.deleted'), components}, true );
 			}, dberror => {
 				console.log( '- Error while removing the verification: ' + dberror );
-				button.url = new URL(`/guild/${msg.guild.id}/verification/${row.configid}`, process.env.dashboard).href;
-				msg.replyMsg( lang.get('verification.save_failed'), {components}, true );
+				button?.setURL(new URL(`/guild/${msg.guildId}/verification/${row.configid}`, button.url).href);
+				msg.replyMsg( {content: lang.get('verification.save_failed'), components}, true );
 			} );
 		}
-		button.url = new URL(`/guild/${msg.guild.id}/verification/${row.configid}`, process.env.dashboard).href;
+		button?.setURL(new URL(`/guild/${msg.guildId}/verification/${row.configid}`, button.url).href);
 		if ( args[1] === 'rename' && !args.slice(2).join('') ) {
-			if ( !row.rename && !msg.guild.me.permissions.has('MANAGE_NICKNAMES') ) {
-				console.log( msg.guild.id + ': Missing permissions - MANAGE_NICKNAMES' );
+			if ( !row.rename && !msg.guild.me.permissions.has(FLAGS.MANAGE_NICKNAMES) ) {
+				console.log( msg.guildId + ': Missing permissions - MANAGE_NICKNAMES' );
 				return msg.replyMsg( lang.get('general.missingperm') + ' `MANAGE_NICKNAMES`' );
 			}
-			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
-			return db.query( 'UPDATE verification SET rename = $1 WHERE guild = $2 AND configid = $3', [( row.rename ? 0 : 1 ), msg.guild.id, row.configid] ).then( () => {
+			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
+			return db.query( 'UPDATE verification SET rename = $1 WHERE guild = $2 AND configid = $3', [( row.rename ? 0 : 1 ), msg.guildId, row.configid] ).then( () => {
 				console.log( '- Verification successfully updated.' );
 				row.rename = ( row.rename ? 0 : 1 );
-				msg.sendChannel( '<@' + msg.author.id + '>, ' + lang.get('verification.updated') + formatVerification(), {split:true,components}, true );
+				Util.splitMessage( lang.get('verification.updated') + formatVerification() ).forEach( textpart => msg.replyMsg( {content: textpart, components}, true ) );
 			}, dberror => {
 				console.log( '- Error while updating the verification: ' + dberror );
-				msg.replyMsg( lang.get('verification.save_failed'), {components}, true );
+				msg.replyMsg( {content: lang.get('verification.save_failed'), components}, true );
 			} );
 		}
 		if ( args[2] ) {
-			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
+			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
 			args[2] = args.slice(2).join(' ').replace( /^\s*<?\s*(.*?)\s*>?\s*$/, '$1' );
 			if ( args[1] === 'channel' ) {
 				var channels = args[2].replace( /\s*>?\s*[,|]\s*<?\s*/g, '|' ).split('|').filter( channel => channel.length );
-				if ( channels.length > 10 ) return msg.replyMsg( lang.get('verification.channel_max'), {components}, true );
+				if ( channels.length > 10 ) return msg.replyMsg( {content: lang.get('verification.channel_max'), components}, true );
 				channels = channels.map( channel => {
 					var new_channel = '';
-					if ( /^\d+$/.test(channel) ) new_channel = msg.guild.channels.cache.filter( tc => tc.isGuild() ).get(channel);
-					if ( !new_channel ) new_channel = msg.guild.channels.cache.filter( gc => gc.isGuild() ).find( gc => gc.name === channel.replace( /^#/, '' ) );
-					if ( !new_channel ) new_channel = msg.guild.channels.cache.filter( gc => gc.isGuild() ).find( gc => gc.name.toLowerCase() === channel.toLowerCase().replace( /^#/, '' ) );
+					if ( /^\d+$/.test(channel) ) new_channel = msg.guild.channels.cache.filter( tc => tc.isGuild(false) ).get(channel);
+					if ( !new_channel ) new_channel = msg.guild.channels.cache.filter( gc => gc.isGuild(false) ).find( gc => gc.name === channel.replace( /^#/, '' ) );
+					if ( !new_channel ) new_channel = msg.guild.channels.cache.filter( gc => gc.isGuild(false) ).find( gc => gc.name.toLowerCase() === channel.toLowerCase().replace( /^#/, '' ) );
 					return new_channel;
 				} );
-				if ( channels.some( channel => !channel ) ) return msg.replyMsg( lang.get('verification.channel_missing'), {components}, true );
+				if ( channels.some( channel => !channel ) ) return msg.replyMsg( {content: lang.get('verification.channel_missing'), components}, true );
 				channels = channels.map( channel => channel.id ).join('|');
-				if ( channels.length ) return db.query( 'UPDATE verification SET channel = $1 WHERE guild = $2 AND configid = $3', ['|' + channels + '|', msg.guild.id, row.configid] ).then( () => {
+				if ( channels.length ) return db.query( 'UPDATE verification SET channel = $1 WHERE guild = $2 AND configid = $3', ['|' + channels + '|', msg.guildId, row.configid] ).then( () => {
 					console.log( '- Verification successfully updated.' );
 					row.channel = '|' + channels + '|';
-					msg.sendChannel( '<@' + msg.author.id + '>, ' + lang.get('verification.updated') + formatVerification(), {split:true,components}, true );
+					Util.splitMessage( lang.get('verification.updated') + formatVerification() ).forEach( textpart => msg.replyMsg( {content: textpart, components}, true ) );
 				}, dberror => {
 					console.log( '- Error while updating the verification: ' + dberror );
-					msg.replyMsg( lang.get('verification.save_failed'), {components}, true );
+					msg.replyMsg( {content: lang.get('verification.save_failed'), components}, true );
 				} );
 			}
 			if ( args[1] === 'role' ) {
 				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 );
+				if ( roles.length > 10 ) return msg.replyMsg( {content: lang.get('verification.role_max'), components}, true );
 				roles = roles.map( role => {
 					var new_role = ['', null];
 					if ( role.startsWith( '-' ) ) {
@@ -188,31 +172,31 @@ function cmd_verification(lang, msg, args, line, wiki) {
 					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[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 );
+				if ( roles.some( role => !role[1] ) ) return msg.replyMsg( {content: lang.get('verification.role_missing'), components}, true );
+				if ( roles.some( role => role[1].managed || role[1].id === msg.guildId ) ) return msg.replyMsg( {content: 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( () => {
+				if ( roles.length ) return db.query( 'UPDATE verification SET role = $1 WHERE guild = $2 AND configid = $3', [roles, msg.guildId, row.configid] ).then( () => {
 					console.log( '- Verification successfully updated.' );
 					row.role = roles;
-					msg.sendChannel( '<@' + msg.author.id + '>, ' + lang.get('verification.updated') + formatVerification(), {split:true,components}, true );
+					Util.splitMessage( lang.get('verification.updated') + formatVerification() ).forEach( textpart => msg.replyMsg( {content: textpart, components}, true ) );
 				}, dberror => {
 					console.log( '- Error while updating the verification: ' + dberror );
-					msg.replyMsg( lang.get('verification.save_failed'), {components}, true );
+					msg.replyMsg( {content: lang.get('verification.save_failed'), components}, true );
 				} );
 			}
 			if ( ( ( args[1] === 'editcount' || args[1] === 'accountage' ) && /^\d+$/.test(args[2]) ) || ( args[1] === 'postcount' && /^(?:-?\d+|null)$/.test(args[2]) ) ) {
 				args[2] = parseInt(args[2], 10);
 				if ( isNaN(args[2]) ) args[2] = null;
 				if ( args[2] > 1000000 || args[2] < -1000000 ) {
-					return msg.replyMsg( lang.get('verification.value_too_high'), {components}, true );
+					return msg.replyMsg( {content: lang.get('verification.value_too_high'), components}, true );
 				}
-				return db.query( 'UPDATE verification SET ' + args[1] + ' = $1 WHERE guild = $2 AND configid = $3', [args[2], msg.guild.id, row.configid] ).then( () => {
+				return db.query( 'UPDATE verification SET ' + args[1] + ' = $1 WHERE guild = $2 AND configid = $3', [args[2], msg.guildId, row.configid] ).then( () => {
 					console.log( '- Verification successfully updated.' );
 					row[args[1]] = args[2];
-					msg.sendChannel( '<@' + msg.author.id + '>, ' + lang.get('verification.updated') + formatVerification(), {split:true,components}, true );
+					Util.splitMessage( lang.get('verification.updated') + formatVerification() ).forEach( textpart => msg.replyMsg( {content: textpart, components}, true ) );
 				}, dberror => {
 					console.log( '- Error while updating the verification: ' + dberror );
-					msg.replyMsg( lang.get('verification.save_failed'), {components}, true );
+					msg.replyMsg( {content: lang.get('verification.save_failed'), components}, true );
 				} );
 			}
 			if ( args[1] === 'usergroup' ) {
@@ -222,8 +206,8 @@ function cmd_verification(lang, msg, args, line, wiki) {
 					usergroups = usergroups.slice(1);
 					and_or = 'AND|';
 				}
-				if ( usergroups.length > 10 ) return msg.replyMsg( lang.get('verification.usergroup_max'), {components}, true );
-				if ( usergroups.some( usergroup => usergroup.length > 100 ) ) return msg.replyMsg( lang.get('verification.usergroup_too_long'), {components}, true );
+				if ( usergroups.length > 10 ) return msg.replyMsg( {content: lang.get('verification.usergroup_max'), components}, true );
+				if ( usergroups.some( usergroup => usergroup.length > 100 ) ) return msg.replyMsg( {content: lang.get('verification.usergroup_too_long'), components}, true );
 				if ( usergroups.length ) return msg.reactEmoji('⏳').then( reaction => got.get( wiki + 'api.php?action=query&meta=allmessages&amprefix=group-&amincludelocal=true&amenableparser=true&format=json' ).then( response => {
 					var body = response.body;
 					if ( body && body.warnings ) log_warn(body.warnings);
@@ -256,26 +240,26 @@ function cmd_verification(lang, msg, args, line, wiki) {
 					console.log( '- Error while getting the usergroups: ' + error );
 				} ).finally( () => {
 					usergroups = usergroups.join('|');
-					db.query( 'UPDATE verification SET usergroup = $1 WHERE guild = $2 AND configid = $3', [and_or + usergroups, msg.guild.id, row.configid] ).then( () => {
+					db.query( 'UPDATE verification SET usergroup = $1 WHERE guild = $2 AND configid = $3', [and_or + usergroups, msg.guildId, row.configid] ).then( () => {
 						console.log( '- Verification successfully updated.' );
 						row.usergroup = and_or + usergroups;
-						msg.sendChannel( '<@' + msg.author.id + '>, ' + lang.get('verification.updated') + formatVerification(), {split:true,components}, true );
+						Util.splitMessage( lang.get('verification.updated') + formatVerification() ).forEach( textpart => msg.replyMsg( {content: textpart, components}, true ) );
 						
 						if ( reaction ) reaction.removeEmoji();
 					}, dberror => {
 						console.log( '- Error while updating the verification: ' + dberror );
-						msg.replyMsg( lang.get('verification.save_failed'), {components}, true );
+						msg.replyMsg( {content: lang.get('verification.save_failed'), components}, true );
 						
 						if ( reaction ) reaction.removeEmoji();
 					} );
 				} ) );
 			}
 		}
-		return msg.sendChannel( '<@' + msg.author.id + '>, ' + lang.get('verification.current_selected', row.configid) + ( process.env.dashboard ? `\n<${button.url}>` : '' ) + formatVerification(true) +'\n\n' + lang.get('verification.delete_current') + '\n`' + prefix + 'verification ' + row.configid + ' delete`', {split:true,components}, true );
+		return Util.splitMessage( lang.get('verification.current_selected', row.configid) + ( button ? `\n<${button.url}>` : '' ) + formatVerification(true) +'\n\n' + lang.get('verification.delete_current') + '\n`' + prefix + 'verification ' + row.configid + ' delete`' ).forEach( textpart => msg.replyMsg( {content: textpart, components}, true ) );
 		
 		function formatVerification(showCommands, hideNotice, {
 			configid,
-			channel = '|' + msg.channel.id + '|',
+			channel = '|' + ( msg.channel.isThread() ? msg.channel.parentId : msg.channelId ) + '|',
 			role = '',
 			editcount = 0,
 			postcount = 0,
@@ -307,7 +291,7 @@ function cmd_verification(lang, msg, args, line, wiki) {
 			if ( showCommands ) verification_text += '\n`' + prefix + 'verification ' + row.configid + ' accountage ' + lang.get('verification.new_accountage') + '`\n';
 			verification_text += '\n' + lang.get('verification.rename') + ' *`' + lang.get('verification.' + ( rename ? 'enabled' : 'disabled')) + '`*';
 			if ( showCommands ) verification_text += ' ' + lang.get('verification.toggle') + '\n`' + prefix + 'verification ' + row.configid + ' rename`\n';
-			if ( !hideNotice && rename && !msg.guild.me.permissions.has('MANAGE_NICKNAMES') ) {
+			if ( !hideNotice && rename && !msg.guild.me.permissions.has(FLAGS.MANAGE_NICKNAMES) ) {
 				verification_text += '\n\n' + lang.get('verification.rename_no_permission', msg.guild.me.toString());
 			}
 			if ( !hideNotice && role.replace( /-/g, '' ).split('|').some( role => {

+ 185 - 118
cmds/verify.js

@@ -1,8 +1,8 @@
 const {randomBytes} = require('crypto');
-const {MessageEmbed} = require('discord.js');
+const {MessageEmbed, MessageActionRow, MessageButton, Permissions: {FLAGS}} = require('discord.js');
 var db = require('../util/database.js');
 var verify = require('../functions/verify.js');
-const {oauthVerify, allowDelete, escapeFormatting} = require('../util/functions.js');
+const {got, oauthVerify, allowDelete, escapeFormatting} = require('../util/functions.js');
 
 /**
  * Processes the "verify" command.
@@ -14,19 +14,19 @@ const {oauthVerify, allowDelete, escapeFormatting} = require('../util/functions.
  */
 function cmd_verify(lang, msg, args, line, wiki) {
 	if ( !msg.channel.isGuild() || msg.defaultSettings ) return this.LINK(lang, msg, line, wiki);
-	if ( !msg.guild.me.permissions.has('MANAGE_ROLES') ) {
+	if ( !msg.guild.me.permissions.has(FLAGS.MANAGE_ROLES) ) {
 		if ( msg.isAdmin() ) {
-			console.log( msg.guild.id + ': Missing permissions - MANAGE_ROLES' );
+			console.log( msg.guildId + ': Missing permissions - MANAGE_ROLES' );
 			msg.replyMsg( lang.get('general.missingperm') + ' `MANAGE_ROLES`' );
 		}
 		else if ( !msg.onlyVerifyCommand ) this.LINK(lang, msg, line, wiki);
 		return;
 	}
 	
-	db.query( 'SELECT logchannel, flags, onsuccess, onmatch, role, editcount, postcount, usergroup, accountage, rename FROM verification LEFT JOIN verifynotice ON verification.guild = verifynotice.guild WHERE verification.guild = $1 AND channel LIKE $2 ORDER BY configid ASC', [msg.guild.id, '%|' + msg.channel.id + '|%'] ).then( ({rows}) => {
+	db.query( 'SELECT logchannel, flags, onsuccess, onmatch, role, editcount, postcount, usergroup, accountage, rename FROM verification LEFT JOIN verifynotice ON verification.guild = verifynotice.guild WHERE verification.guild = $1 AND channel LIKE $2 ORDER BY configid ASC', [msg.guildId, '%|' + ( msg.channel.isThread() ? msg.channel.parentId : msg.channelId ) + '|%'] ).then( ({rows}) => {
 		if ( !rows.length ) {
 			if ( msg.onlyVerifyCommand ) return;
-			return msg.replyMsg( lang.get('verify.missing') + ( msg.isAdmin() ? '\n`' + ( patreons[msg.guild.id] || process.env.prefix ) + 'verification`' : '' ) );
+			return msg.replyMsg( lang.get('verify.missing') + ( msg.isAdmin() ? '\n`' + ( patreons[msg.guildId] || process.env.prefix ) + 'verification`' : '' ) );
 		}
 		
 		if ( wiki.hasOAuth2() && process.env.dashboard ) {
@@ -34,46 +34,79 @@ function cmd_verify(lang, msg, args, line, wiki) {
 			if ( wiki.isWikimedia() ) oauth.push('wikimedia');
 			if ( wiki.isMiraheze() ) oauth.push('miraheze');
 			if ( process.env['oauth_' + ( oauth[1] || oauth[0] )] && process.env['oauth_' + ( oauth[1] || oauth[0] ) + '_secret'] ) {
-				let state = `${oauth[0]} ${global.shardId}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
-				while ( oauthVerify.has(state) ) {
-					state = `${oauth[0]} ${global.shardId}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
-				}
-				oauthVerify.set(state, {
-					state, wiki: wiki.href,
-					channel: msg.channel,
-					user: msg.author.id
-				});
-				msg.client.shard.send({id: 'verifyUser', state});
-				let oauthURL = wiki + 'rest.php/oauth2/authorize?' + new URLSearchParams({
-					response_type: 'code', redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
-					client_id: process.env['oauth_' + ( oauth[1] || oauth[0] )], state
-				}).toString();
-				return msg.member.send( lang.get('verify.oauth_message_dm', escapeFormatting(msg.guild.name)) + '\n<' + oauthURL + '>', {
-					components: [
-						{
-							type: 1,
-							components: [
-								{
-									type: 2,
-									style: 5,
-									label: lang.get('verify.oauth_button'),
-									emoji: {id: null, name: '🔗'},
-									url: oauthURL,
-									disabled: false
-								}
-							]
+				return db.query( 'SELECT token FROM oauthusers WHERE userid = $1 AND site = $2', [msg.author.id, ( oauth[1] || oauth[0] )] ).then( ({rows: [row]}) => {
+					if ( row?.token ) return got.post( wiki + 'rest.php/oauth2/access_token', {
+						form: {
+							grant_type: 'refresh_token', refresh_token: row.token,
+							redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
+							client_id: process.env['oauth_' + ( oauth[1] || oauth[0] )],
+							client_secret: process.env['oauth_' + ( oauth[1] || oauth[0] ) + '_secret']
+						}
+					} ).then( response => {
+						var body = response.body;
+						if ( response.statusCode !== 200 || !body?.access_token ) {
+							console.log( '- ' + response.statusCode + ': Error while refreshing the mediawiki token: ' + ( body?.message || body?.error ) );
+							return Promise.reject(row);
 						}
-					]
-				} ).then( message => {
-					msg.reactEmoji('📩');
-					allowDelete(message, msg.author.id);
-					msg.delete({timeout: 60000, reason: lang.get('verify.footer')}).catch(log_error);
-				}, error => {
-					if ( error?.code === 50007 ) { // CANNOT_MESSAGE_USER
-						return msg.replyMsg( lang.get('verify.oauth_private') );
+						if ( body?.refresh_token ) db.query( 'UPDATE oauthusers SET token = $1 WHERE userid = $2 AND site = $3', [body.refresh_token, msg.author.id, ( oauth[1] || oauth[0] )] ).then( () => {
+							console.log( '- Dashboard: OAuth2 token for ' + msg.author.id + ' successfully updated.' );
+						}, dberror => {
+							console.log( '- Dashboard: Error while updating the OAuth2 token for ' + msg.author.id + ': ' + dberror );
+						} );
+						return global.verifyOauthUser('', body.access_token, {
+							wiki: wiki.href, channel: msg.channel,
+							user: msg.author.id, sourceMessage: msg,
+							fail: () => msg.replyMsg( lang.get('verify.error_reply'), false, false ).then( message => {
+								if ( message ) message.reactEmoji('error');
+							} )
+						});
+					}, error => {
+						console.log( '- Error while refreshing the mediawiki token: ' + error );
+						return Promise.reject(row);
+					} );
+					return Promise.reject(row);
+				}, dberror => {
+					console.log( '- Error while getting the OAuth2 token: ' + dberror );
+					return Promise.reject();
+				} ).catch( row => {
+					if ( row ) {
+						if ( !row?.hasOwnProperty?.('token') ) console.log( '- Error while checking the OAuth2 refresh token: ' + row );
+						else if ( row.token ) db.query( 'DELETE FROM oauthusers WHERE userid = $1 AND site = $2', [msg.author.id, ( oauth[1] || oauth[0] )] ).then( () => {
+							console.log( '- Dashboard: OAuth2 token for ' + msg.author.id + ' successfully deleted.' );
+						}, dberror => {
+							console.log( '- Dashboard: Error while deleting the OAuth2 token for ' + msg.author.id + ': ' + dberror );
+						} );
+					}
+					let state = `${oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
+					while ( oauthVerify.has(state) ) {
+						state = `${oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
 					}
-					log_error(error);
-					msg.reactEmoji('error');
+					oauthVerify.set(state, {
+						state, wiki: wiki.href,
+						channel: msg.channel,
+						user: msg.author.id
+					});
+					msg.client.shard.send({id: 'verifyUser', state, user: ( row?.token === null ? '' : msg.author.id )});
+					let oauthURL = wiki + 'rest.php/oauth2/authorize?' + new URLSearchParams({
+						response_type: 'code', redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
+						client_id: process.env['oauth_' + ( oauth[1] || oauth[0] )], state
+					}).toString();
+					return msg.member.send( {
+						content: lang.get('verify.oauth_message_dm', escapeFormatting(msg.guild.name)) + '\n<' + oauthURL + '>',
+						components: [new MessageActionRow().addComponents(
+							new MessageButton().setLabel(lang.get('verify.oauth_button')).setEmoji('🔗').setStyle('LINK').setURL(oauthURL)
+						)]
+					} ).then( message => {
+						msg.reactEmoji('📩');
+						allowDelete(message, msg.author.id);
+						setTimeout( () => msg.delete().catch(log_error), 60000 ).unref();
+					}, error => {
+						if ( error?.code === 50007 ) { // CANNOT_MESSAGE_USER
+							return msg.replyMsg( lang.get('verify.oauth_private') );
+						}
+						log_error(error);
+						msg.reactEmoji('error');
+					} );
 				} );
 			}
 		}
@@ -92,113 +125,147 @@ function cmd_verify(lang, msg, args, line, wiki) {
 		msg.reactEmoji('⏳').then( reaction => {
 			verify(lang, msg.channel, msg.member, username, wiki, rows).then( result => {
 				if ( result.oauth.length ) {
-					let state = `${result.oauth[0]} ${global.shardId}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
-					while ( oauthVerify.has(state) ) {
-						state = `${result.oauth[0]} ${global.shardId}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
-					}
-					oauthVerify.set(state, {
-						state, wiki: wiki.href,
-						channel: msg.channel,
-						user: msg.author.id
-					});
-					msg.client.shard.send({id: 'verifyUser', state});
-					let oauthURL = wiki + 'rest.php/oauth2/authorize?' + new URLSearchParams({
-						response_type: 'code', redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
-						client_id: process.env['oauth_' + ( result.oauth[1] || result.oauth[0] )], state
-					}).toString();
-					msg.member.send( lang.get('verify.oauth_message_dm', escapeFormatting(msg.guild.name)) + '\n<' + oauthURL + '>', {
-						components: [
-							{
-								type: 1,
-								components: [
-									{
-										type: 2,
-										style: 5,
-										label: lang.get('verify.oauth_button'),
-										emoji: {id: null, name: '🔗'},
-										url: oauthURL,
-										disabled: false
-									}
-								]
+					return db.query( 'SELECT token FROM oauthusers WHERE userid = $1 AND site = $2', [msg.author.id, ( result.oauth[1] || result.oauth[0] )] ).then( ({rows: [row]}) => {
+						if ( row?.token ) return got.post( wiki + 'rest.php/oauth2/access_token', {
+							form: {
+								grant_type: 'refresh_token', refresh_token: row.token,
+								redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
+								client_id: process.env['oauth_' + ( result.oauth[1] || result.oauth[0] )],
+								client_secret: process.env['oauth_' + ( result.oauth[1] || result.oauth[0] ) + '_secret']
 							}
-						]
-					} ).then( message => {
-						msg.reactEmoji('📩');
-						allowDelete(message, msg.author.id);
-						msg.delete({timeout: 60000, reason: lang.get('verify.footer')}).catch(log_error);
-					}, error => {
-						if ( error?.code === 50007 ) { // CANNOT_MESSAGE_USER
-							return msg.replyMsg( lang.get('verify.oauth_private') );
+						} ).then( response => {
+							var body = response.body;
+							if ( response.statusCode !== 200 || !body?.access_token ) {
+								console.log( '- ' + response.statusCode + ': Error while refreshing the mediawiki token: ' + ( body?.message || body?.error ) );
+								return Promise.reject(row);
+							}
+							if ( body?.refresh_token ) db.query( 'UPDATE oauthusers SET token = $1 WHERE userid = $2 AND site = $3', [body.refresh_token, msg.author.id, ( result.oauth[1] || result.oauth[0] )] ).then( () => {
+								console.log( '- Dashboard: OAuth2 token for ' + msg.author.id + ' successfully updated.' );
+							}, dberror => {
+								console.log( '- Dashboard: Error while updating the OAuth2 token for ' + msg.author.id + ': ' + dberror );
+							} );
+							return global.verifyOauthUser('', body.access_token, {
+								wiki: wiki.href, channel: msg.channel,
+								user: msg.author.id, sourceMessage: msg,
+								fail: () => msg.replyMsg( lang.get('verify.error_reply'), false, false ).then( message => {
+									if ( message ) message.reactEmoji('error');
+								} )
+							});
+						}, error => {
+							console.log( '- Error while refreshing the mediawiki token: ' + error );
+							return Promise.reject(row);
+						} );
+						return Promise.reject(row);
+					}, dberror => {
+						console.log( '- Error while getting the OAuth2 token: ' + dberror );
+						return Promise.reject();
+					} ).catch( row => {
+						if ( row ) {
+							if ( !row?.hasOwnProperty?.('token') ) console.log( '- Error while checking the OAuth2 refresh token: ' + row );
+							else if ( row.token ) db.query( 'DELETE FROM oauthusers WHERE userid = $1 AND site = $2', [msg.author.id, ( result.oauth[1] || result.oauth[0] )] ).then( () => {
+								console.log( '- Dashboard: OAuth2 token for ' + msg.author.id + ' successfully deleted.' );
+							}, dberror => {
+								console.log( '- Dashboard: Error while deleting the OAuth2 token for ' + msg.author.id + ': ' + dberror );
+							} );
 						}
-						log_error(error);
-						msg.reactEmoji('error');
+						let state = `${result.oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
+						while ( oauthVerify.has(state) ) {
+							state = `${result.oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
+						}
+						oauthVerify.set(state, {
+							state, wiki: wiki.href,
+							channel: msg.channel,
+							user: msg.author.id
+						});
+						msg.client.shard.send({id: 'verifyUser', state, user: ( row?.token === null ? '' : msg.author.id )});
+						let oauthURL = wiki + 'rest.php/oauth2/authorize?' + new URLSearchParams({
+							response_type: 'code', redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
+							client_id: process.env['oauth_' + ( result.oauth[1] || result.oauth[0] )], state
+						}).toString();
+						msg.member.send( {
+							content: lang.get('verify.oauth_message_dm', escapeFormatting(msg.guild.name)) + '\n<' + oauthURL + '>',
+							components: [new MessageActionRow().addComponents(
+								new MessageButton().setLabel(lang.get('verify.oauth_button')).setEmoji('🔗').setStyle('LINK').setURL(oauthURL)
+							)]
+						} ).then( message => {
+							msg.reactEmoji('📩');
+							allowDelete(message, msg.author.id);
+							setTimeout( () => msg.delete().catch(log_error), 60000 ).unref();
+						}, error => {
+							if ( error?.code === 50007 ) { // CANNOT_MESSAGE_USER
+								return msg.replyMsg( lang.get('verify.oauth_private') );
+							}
+							log_error(error);
+							msg.reactEmoji('error');
+						} );
 					} );
 				}
 				else if ( result.reaction ) msg.reactEmoji(result.reaction);
 				else {
-					var options = {embed: result.embed, components: []};
-					if ( result.add_button ) options.components.push({
-						type: 1,
-						components: [
-							{
-								type: 2,
-								style: 1,
-								label: lang.get('verify.button_again'),
-								emoji: {id: null, name: '🔂'},
-								custom_id: 'verify_again',
-								disabled: false
-							}
-						]
-					});
+					var options = {
+						content: msg.member.toString() + ', ' + result.content,
+						embeds: [result.embed],
+						components: [],
+						allowedMentions: {
+							users: [msg.author.id],
+							repliedUser: true
+						}
+					};
+					if ( result.add_button ) options.components.push(new MessageActionRow().addComponents(
+						new MessageButton().setLabel(lang.get('verify.button_again')).setEmoji('🔂').setStyle('PRIMARY').setCustomId('verify_again')
+					));
 					if ( result.send_private ) {
-						let dmEmbed = new MessageEmbed(options.embed);
-						dmEmbed.fields.forEach( field => {
-							field.value = field.value.replace( /<@&(\d+)>/g, (mention, id) => {
-								if ( !msg.guild.roles.cache.has(id) ) return mention;
-								return escapeFormatting('@' + msg.guild.roles.cache.get(id)?.name);
+						let dmEmbeds = [new MessageEmbed(result.embed)];
+						if ( options.embeds[0] ) {
+							dmEmbeds.push(new MessageEmbed(options.embeds[0]));
+							dmEmbeds[0].fields.forEach( field => {
+								field.value = field.value.replace( /<@&(\d+)>/g, (mention, id) => {
+									if ( !msg.guild.roles.cache.has(id) ) return mention;
+									return escapeFormatting('@' + msg.guild.roles.cache.get(id)?.name);
+								} );
 							} );
-						} );
-						msg.member.send( msg.channel.toString() + '; ' + result.content, {embed: dmEmbed, components: []} ).then( message => {
+						}
+						msg.member.send( {content: msg.channel.toString() + '; ' + result.content, embeds: dmEmbeds, components: []} ).then( message => {
 							msg.reactEmoji('📩');
 							allowDelete(message, msg.author.id);
-							msg.delete({timeout: 60000, reason: lang.get('verify.footer')}).catch(log_error);
+							setTimeout( () => msg.delete().catch(log_error), 60000 ).unref();
 						}, error => {
 							if ( error?.code === 50007 ) { // CANNOT_MESSAGE_USER
-								return msg.replyMsg( result.content, options, false, false );
+								return msg.replyMsg( options, false, false );
 							}
 							log_error(error);
 							msg.reactEmoji('error');
 						} );
 						if ( result.logging.channel && msg.guild.channels.cache.has(result.logging.channel) ) {
-							msg.guild.channels.cache.get(result.logging.channel).send(result.logging.content, {
-								embed: result.logging.embed,
-								allowedMentions: {parse: []}
-							}).catch(log_error);
+							msg.guild.channels.cache.get(result.logging.channel).send( {
+								content: result.logging.content,
+								embeds: [result.logging.embed]
+							} ).catch(log_error);
 						}
 					}
-					else msg.replyMsg( result.content, options, false, false ).then( message => {
+					else msg.replyMsg( options, false, false ).then( message => {
 						if ( !result.logging.channel || !msg.guild.channels.cache.has(result.logging.channel) ) return;
 						if ( message ) {
-							if ( result.logging.embed ) result.logging.embed.addField(message.url, '<#' + msg.channel.id + '>');
-							else result.logging.content += '\n<#' + msg.channel.id + '> – <' + message.url + '>';
+							if ( result.logging.embed ) result.logging.embed.addField(message.url, '<#' + msg.channelId + '>');
+							else result.logging.content += '\n<#' + msg.channelId + '> – <' + message.url + '>';
 						}
-						msg.guild.channels.cache.get(result.logging.channel).send(result.logging.content, {
-							embed: result.logging.embed,
-							allowedMentions: {parse: []}
-						}).catch(log_error);
+						msg.guild.channels.cache.get(result.logging.channel).send( {
+							content: result.logging.content,
+							embeds: [result.logging.embed]
+						} ).catch(log_error);
 					} );
 				}
 				if ( reaction ) reaction.removeEmoji();
 			}, error => {
 				console.log( '- Error during the verifications: ' + error );
-				msg.replyMsg( lang.get('verify.error_reply'), {}, false, false ).then( message => {
+				msg.replyMsg( lang.get('verify.error_reply'), false, false ).then( message => {
 					if ( message ) message.reactEmoji('error');
 				} );
 			} );
 		} );
 	}, dberror => {
 		console.log( '- Error while getting the verifications: ' + dberror );
-		msg.replyMsg( lang.get('verify.error_reply'), {}, false, false ).then( message => {
+		msg.replyMsg( lang.get('verify.error_reply'), false, false ).then( message => {
 			if ( message ) message.reactEmoji('error');
 		} );
 	} );

+ 13 - 13
cmds/voice.js

@@ -13,37 +13,37 @@ function cmd_voice(lang, msg, args, line, wiki) {
 	if ( msg.isAdmin() ) {
 		if ( !args.join('') ) {
 			var text = lang.get('voice.text') + '\n`' + lang.get('voice.channel') + ' – <' + lang.get('voice.name') + '>`\n';
-			text += lang.get('voice.' + ( voice[msg.guild.id] ? 'disable' : 'enable' ), ( patreons[msg.guild.id] || process.env.prefix ) + 'voice toggle');
-			return msg.replyMsg( text, {}, true );
+			text += lang.get('voice.' + ( voice[msg.guildId] ? 'disable' : 'enable' ), ( patreons[msg.guildId] || process.env.prefix ) + 'voice toggle');
+			return msg.replyMsg( text, true );
 		}
 		args[1] = args.slice(1).join(' ').trim()
 		if ( args[0].toLowerCase() === 'toggle' && !args[1] ) {
 			if ( msg.defaultSettings ) return help_setup(lang, msg);
-			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
-			var value = ( voice[msg.guild.id] ? null : 1 );
-			return db.query( 'UPDATE discord SET voice = $1 WHERE guild = $2 AND channel IS NULL', [value, msg.guild.id] ).then( () => {
+			if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, true );
+			var value = ( voice[msg.guildId] ? null : 1 );
+			return db.query( 'UPDATE discord SET voice = $1 WHERE guild = $2 AND channel IS NULL', [value, msg.guildId] ).then( () => {
 				console.log( '- Voice settings successfully updated.' );
 				if ( value ) {
-					voice[msg.guild.id] = lang.lang;
-					db.query( 'SELECT lang FROM discord WHERE guild = $1 AND channel IS NULL', [msg.guild.id] ).then( ({rows:[row]}) => {
+					voice[msg.guildId] = lang.lang;
+					db.query( 'SELECT lang FROM discord WHERE guild = $1 AND channel IS NULL', [msg.guildId] ).then( ({rows:[row]}) => {
 						console.log( '- Voice language successfully updated.' );
-						voice[msg.guild.id] = row.lang;
+						voice[msg.guildId] = row.lang;
 					}, dberror => {
 						console.log( '- Error while getting the voice language: ' + dberror );
 					} );
-					msg.replyMsg( lang.get('voice.enabled') + '\n`' + lang.get('voice.channel') + ' – <' + lang.get('voice.name') + '>`', {}, true );
+					msg.replyMsg( lang.get('voice.enabled') + '\n`' + lang.get('voice.channel') + ' – <' + lang.get('voice.name') + '>`', true );
 				}
 				else {
-					delete voice[msg.guild.id];
-					msg.replyMsg( lang.get('voice.disabled'), {}, true );
+					delete voice[msg.guildId];
+					msg.replyMsg( lang.get('voice.disabled'), true );
 				}
 			}, dberror => {
 				console.log( '- Error while editing the voice settings: ' + dberror );
-				msg.replyMsg( lang.get('settings.save_failed'), {}, true );
+				msg.replyMsg( lang.get('settings.save_failed'), true );
 			} );
 		}
 	}
-	if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
+	if ( !msg.channel.isGuild() || !pause[msg.guildId] ) this.LINK(lang, msg, line, wiki);
 }
 
 module.exports = {

+ 26 - 23
cmds/wiki/diff.js

@@ -1,7 +1,7 @@
 const {MessageEmbed} = require('discord.js');
 const logging = require('../../util/logging.js');
 const {timeoptions} = require('../../util/default.json');
-const {htmlToPlain, htmlToDiscord, escapeFormatting} = require('../../util/functions.js');
+const {got, htmlToPlain, htmlToDiscord, escapeFormatting} = require('../../util/functions.js');
 const diffParser = require('../../util/edit_diff.js');
 
 /**
@@ -12,9 +12,10 @@ const diffParser = require('../../util/edit_diff.js');
  * @param {import('../../util/wiki.js')} wiki - The wiki for the edit.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  * @param {String} spoiler - If the response is in a spoiler.
+ * @param {Boolean} noEmbed - If the response should be without an embed.
  * @param {MessageEmbed} [embed] - The embed for the page.
  */
-function gamepedia_diff(lang, msg, args, wiki, reaction, spoiler, embed) {
+function gamepedia_diff(lang, msg, args, wiki, reaction, spoiler, noEmbed, embed) {
 	if ( args[0] ) {
 		var error = false;
 		var title = '';
@@ -50,7 +51,7 @@ function gamepedia_diff(lang, msg, args, wiki, reaction, spoiler, embed) {
 			if ( reaction ) reaction.removeEmoji();
 		}
 		else if ( diff ) {
-			gamepedia_diff_send(lang, msg, [diff, revision], wiki, reaction, spoiler);
+			gamepedia_diff_send(lang, msg, [diff, revision], wiki, reaction, spoiler, noEmbed);
 		}
 		else {
 			got.get( wiki + 'api.php?action=compare&prop=ids|diff' + ( title ? '&fromtitle=' + encodeURIComponent( title ) : '&fromrev=' + revision ) + '&torelative=' + relative + '&format=json' ).then( response => {
@@ -81,7 +82,7 @@ function gamepedia_diff(lang, msg, args, wiki, reaction, spoiler, embed) {
 						msg.reactEmoji('nowiki');
 					}
 					else if ( noerror ) {
-						msg.replyMsg( lang.get('diff.badrev') );
+						msg.replyMsg( {content: lang.get('diff.badrev'), allowedMentions: {repliedUser: false}} );
 					}
 					else {
 						console.log( '- ' + response.statusCode + ': Error while getting the search results: ' + ( body && body.error && body.error.info ) );
@@ -111,7 +112,7 @@ function gamepedia_diff(lang, msg, args, wiki, reaction, spoiler, embed) {
 							else if ( ids.fromtexthidden !== undefined ) compare[0] = '__' + lang.get('diff.hidden') + '__';
 							else if ( ids.totexthidden !== undefined ) compare[1] = '__' + lang.get('diff.hidden') + '__';
 						}
-						gamepedia_diff_send(lang, msg, argids, wiki, reaction, spoiler, compare);
+						gamepedia_diff_send(lang, msg, argids, wiki, reaction, spoiler, noEmbed, compare);
 					}
 				}
 			}, error => {
@@ -129,7 +130,7 @@ function gamepedia_diff(lang, msg, args, wiki, reaction, spoiler, embed) {
 		}
 	}
 	else {
-		if ( embed ) msg.sendChannel( spoiler + '<' + embed.url + '>' + spoiler, {embed} );
+		if ( embed ) msg.sendChannel( {content: spoiler + '<' + embed.url + '>' + spoiler, embeds: ( noEmbed ? [] : [embed] )} );
 		else msg.reactEmoji('error');
 		
 		if ( reaction ) reaction.removeEmoji();
@@ -144,10 +145,11 @@ function gamepedia_diff(lang, msg, args, wiki, reaction, spoiler, embed) {
  * @param {import('../../util/wiki.js')} wiki - The wiki for the edit.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  * @param {String} spoiler - If the response is in a spoiler.
+ * @param {Boolean} noEmbed - If the response should be without an embed.
  * @param {String[]} [compare] - The edit difference.
  */
-function gamepedia_diff_send(lang, msg, args, wiki, reaction, spoiler, compare) {
-	got.get( wiki + 'api.php?action=query&meta=siteinfo&siprop=general&list=tags&tglimit=500&tgprop=displayname&prop=revisions&rvslots=main&rvprop=ids|timestamp|flags|user|size|parsedcomment|tags' + ( args.length === 1 || args[0] === args[1] ? '|content' : '' ) + '&revids=' + args.join('|') + '&format=json' ).then( response => {
+function gamepedia_diff_send(lang, msg, args, wiki, reaction, spoiler, noEmbed, compare) {
+	got.get( wiki + 'api.php?uselang=' + lang.lang + '&action=query&meta=siteinfo&siprop=general&list=tags&tglimit=500&tgprop=displayname&prop=revisions&rvslots=main&rvprop=ids|timestamp|flags|user|size|parsedcomment|tags' + ( args.length === 1 || args[0] === args[1] ? '|content' : '' ) + '&revids=' + args.join('|') + '&format=json' ).then( response => {
 		var body = response.body;
 		if ( body && body.warnings ) log_warn(body.warnings);
 		if ( response.statusCode !== 200 || !body || body.batchcomplete === undefined || !body.query ) {
@@ -163,13 +165,13 @@ function gamepedia_diff_send(lang, msg, args, wiki, reaction, spoiler, compare)
 			if ( reaction ) reaction.removeEmoji();
 		}
 		else if ( body.query.badrevids ) {
-			msg.replyMsg( lang.get('diff.badrev') );
+			msg.replyMsg( {content: lang.get('diff.badrev'), allowedMentions: {repliedUser: false}} );
 			
 			if ( reaction ) reaction.removeEmoji();
 		}
 		else if ( body.query.pages && !body.query.pages['-1'] ) {
 			wiki.updateWiki(body.query.general);
-			logging(wiki, msg.guild?.id, 'diff');
+			logging(wiki, msg.guildId, 'diff');
 			var pages = Object.values(body.query.pages);
 			if ( pages.length !== 1 ) {
 				msg.sendChannel( spoiler + '<' + wiki.toLink('Special:Diff/' + ( args[1] ? args[1] + '/' : '' ) + args[0]) + '>' + spoiler );
@@ -181,7 +183,7 @@ function gamepedia_diff_send(lang, msg, args, wiki, reaction, spoiler, compare)
 			var revisions = pages[0].revisions.sort( (first, second) => Date.parse(second.timestamp) - Date.parse(first.timestamp) );
 			var diff = revisions[0].revid;
 			var oldid = ( revisions[1] ? revisions[1].revid : 0 );
-			var editor = [lang.get('diff.info.editor'), ( revisions[0].userhidden !== undefined ? lang.get('diff.hidden') : ( msg.showEmbed() ? '[' + escapeFormatting(revisions[0].user) + '](' + wiki.toLink(( revisions[0].anon !== undefined ? 'Special:Contributions/' : 'User:' ) + revisions[0].user, '', '', true) + ')' : escapeFormatting(revisions[0].user) ) )];
+			var editor = [lang.get('diff.info.editor'), ( revisions[0].userhidden !== undefined ? lang.get('diff.hidden') : ( msg.showEmbed() && !noEmbed ? '[' + escapeFormatting(revisions[0].user) + '](' + wiki.toLink(( revisions[0].anon !== undefined ? 'Special:Contributions/' : 'User:' ) + revisions[0].user, '', '', true) + ')' : escapeFormatting(revisions[0].user) ) )];
 			try {
 				var dateformat = new Intl.DateTimeFormat(lang.get('dateformat'), Object.assign({
 					timeZone: body.query.general.timezone
@@ -192,18 +194,17 @@ function gamepedia_diff_send(lang, msg, args, wiki, reaction, spoiler, compare)
 					timeZone: 'UTC'
 				}, timeoptions));
 			}
-			var timestamp = [lang.get('diff.info.timestamp'), dateformat.format(new Date(revisions[0].timestamp))];
+			var editDate = new Date(revisions[0].timestamp);
+			var timestamp = [lang.get('diff.info.timestamp'), dateformat.format(editDate), '<t:' + Math.trunc(editDate.getTime() / 1000) + ':R>'];
 			var difference = revisions[0].size - ( revisions[1] ? revisions[1].size : 0 );
-			var size = [lang.get('diff.info.size'), lang.get('diff.info.bytes', ( difference > 0 ? '+' : '' ) + difference.toLocaleString(lang.get('dateformat')), difference, ( revisions[0].minor !== undefined ? lang.get('diff.info.minor') : '' ))];
-			var comment = [lang.get('diff.info.comment'), ( revisions[0].commenthidden !== undefined ? lang.get('diff.hidden') : ( revisions[0].parsedcomment ? ( msg.showEmbed() ? htmlToDiscord(revisions[0].parsedcomment, wiki.toLink(title), true) : htmlToPlain(revisions[0].parsedcomment) ) : lang.get('diff.nocomment') ) )];
-			if ( revisions[0].tags.length ) var tags = [lang.get('diff.info.tags'), body.query.tags.filter( tag => revisions[0].tags.includes( tag.name ) ).map( tag => tag.displayname ).join(', ')];
+			var size = [lang.get('diff.info.size'), lang.get('diff.info.bytes', ( difference > 0 ? '+' : '' ) + difference.toLocaleString(lang.get('dateformat')), difference) + ( revisions[0].minor !== undefined ? lang.get('diff.info.minor').replace( /_/g, ' ' ) : '' )];
+			var comment = [lang.get('diff.info.comment'), ( revisions[0].commenthidden !== undefined ? lang.get('diff.hidden') : ( revisions[0].parsedcomment ? ( msg.showEmbed() && !noEmbed ? htmlToDiscord(revisions[0].parsedcomment, wiki.toLink(title), true) : htmlToPlain(revisions[0].parsedcomment) ) : lang.get('diff.nocomment') ) )];
+			if ( revisions[0].tags.length ) var tags = [lang.get('diff.info.tags'), body.query.tags.filter( tag => tag.displayname && revisions[0].tags.includes( tag.name ) ).map( tag => tag.displayname || tag.name ).join(', ')];
 			
 			var pagelink = wiki.toLink(title, {diff,oldid});
 			var text = '<' + pagelink + '>';
-			var embed = null;
-			if ( msg.showEmbed() ) {
-				embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( escapeFormatting( title + '?diff=' + diff + '&oldid=' + oldid ) ).setURL( pagelink ).addField( editor[0], editor[1], true ).addField( size[0], size[1], true ).addField( comment[0], comment[1] ).setFooter( timestamp[1] );
-				if ( tags ) embed.addField( tags[0], htmlToDiscord(tags[1], pagelink) );
+			if ( msg.showEmbed() && !noEmbed ) {
+				var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( escapeFormatting( title + '?diff=' + diff + '&oldid=' + oldid ) ).setURL( pagelink ).addField( editor[0], editor[1], true ).addField( size[0], size[1], true ).addField( timestamp[0], timestamp[1] + '\n' + timestamp[2], true ).addField( comment[0], comment[1] ).setTimestamp( editDate );
 				
 				var more = '\n__' + lang.get('diff.info.more') + '__';
 				var whitespace = '__' + lang.get('diff.info.whitespace') + '__';
@@ -244,7 +245,8 @@ function gamepedia_diff_send(lang, msg, args, wiki, reaction, spoiler, compare)
 				}, error => {
 					console.log( '- Error while getting the diff: ' + error );
 				} ).finally( () => {
-					msg.sendChannel( spoiler + text + spoiler, {embed} );
+					if ( tags?.[1] ) embed.addField( tags[0], htmlToDiscord(tags[1], pagelink) );
+					msg.sendChannel( {content: spoiler + text + spoiler, embeds: [embed]} );
 					
 					if ( reaction ) reaction.removeEmoji();
 				} );
@@ -264,17 +266,18 @@ function gamepedia_diff_send(lang, msg, args, wiki, reaction, spoiler, compare)
 							embed.addField( lang.get('diff.info.added'), content, true );
 						} else embed.addField( lang.get('diff.info.added'), whitespace, true );
 					}
+					if ( tags?.[1] ) embed.addField( tags[0], htmlToDiscord(tags[1], pagelink) );
 					
-					msg.sendChannel( spoiler + text + spoiler, {embed} );
+					msg.sendChannel( {content: spoiler + text + spoiler, embeds: [embed]} );
 					
 					if ( reaction ) reaction.removeEmoji();
 				}
 			}
 			else {
 				text += '\n\n' + editor.join(' ') + '\n' + timestamp.join(' ') + '\n' + size.join(' ') + '\n' + comment.join(' ');
-				if ( tags ) text += htmlToDiscord( '\n' + tags.join(' ') );
+				if ( tags?.[1] ) text += htmlToDiscord( '\n' + tags.join(' ') );
 				
-				msg.sendChannel( spoiler + text + spoiler, {embed} );
+				msg.sendChannel( spoiler + text + spoiler );
 				
 				if ( reaction ) reaction.removeEmoji();
 			}

+ 107 - 64
cmds/wiki/general.js

@@ -2,7 +2,7 @@ const {MessageEmbed} = require('discord.js');
 const parse_page = require('../../functions/parse_page.js');
 const phabricator = require('../../functions/phabricator.js');
 const logging = require('../../util/logging.js');
-const {htmlToDiscord, escapeFormatting, partialURIdecode} = require('../../util/functions.js');
+const {got, htmlToDiscord, escapeFormatting, partialURIdecode} = require('../../util/functions.js');
 const extract_desc = require('../../util/extract_desc.js');
 const {limit: {interwiki: interwikiLimit}, wikiProjects} = require('../../util/default.json');
 const Wiki = require('../../util/wiki.js');
@@ -38,12 +38,13 @@ fs.readdir( './cmds/minecraft', (error, files) => {
  * @param {String} cmd - The command at this point.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  * @param {String} [spoiler] - If the response is in a spoiler.
+ * @param {Boolean} [noEmbed] - If the response should be without an embed.
  * @param {URLSearchParams} [querystring] - The querystring for the link.
  * @param {String} [fragment] - The section for the link.
  * @param {String} [interwiki] - The fallback interwiki link.
  * @param {Number} [selfcall] - The amount of followed interwiki links.
  */
-function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '', querystring = new URLSearchParams(), fragment = '', interwiki = '', selfcall = 0) {
+function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '', noEmbed = false, querystring = new URLSearchParams(), fragment = '', interwiki = '', selfcall = 0) {
 	var full_title = title;
 	if ( title.includes( '#' ) ) {
 		fragment = title.split('#').slice(1).join('#').trim().replace( /(?:%[\dA-F]{2})+/g, partialURIdecode );
@@ -59,15 +60,15 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 		title = title.substring(0, 250);
 		msg.reactEmoji('⚠️');
 	}
-	var invoke = title.split(' ')[0].toLowerCase();
+	var invoke = full_title.split(' ')[0].toLowerCase();
 	var aliasInvoke = ( lang.aliases[invoke] || invoke );
-	var args = title.split(' ').slice(1);
+	var args = full_title.split(' ').slice(1);
 	
 	if ( aliasInvoke === 'random' && !args.join('') && !querystring.toString() && !fragment ) {
-		return fn.random(lang, msg, wiki, reaction, spoiler);
+		return fn.random(lang, msg, wiki, reaction, spoiler, noEmbed);
 	}
 	if ( aliasInvoke === 'overview' && !args.join('') && !querystring.toString() && !fragment ) {
-		return fn.overview(lang, msg, wiki, reaction, spoiler);
+		return fn.overview(lang, msg, wiki, reaction, spoiler, noEmbed);
 	}
 	if ( aliasInvoke === 'test' && !args.join('') && !querystring.toString() && !fragment ) {
 		this.test(lang, msg, args, '', wiki);
@@ -79,16 +80,20 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 		if ( reaction ) reaction.removeEmoji();
 		return;
 	}
-	if ( aliasInvoke === 'diff' && !wiki.isFandom(false) && args.join('') && !querystring.toString() && !fragment ) {
-		return fn.diff(lang, msg, args, wiki, reaction, spoiler);
+	if ( aliasInvoke === 'diff' && args.join('') && !querystring.toString() && !fragment ) {
+		return fn.diff(lang, msg, args, wiki, reaction, spoiler, noEmbed);
 	}
 	var noRedirect = ( querystring.getAll('redirect').pop() === 'no' || ( querystring.has('action') && querystring.getAll('action').pop() !== 'view' ) );
-	var uselang = ( querystring.get('variant') || querystring.get('uselang') || lang.lang );
-	got.get( wiki + 'api.php?uselang=' + uselang + '&action=query&meta=siteinfo&siprop=general|namespaces|specialpagealiases&iwurl=true' + ( noRedirect ? '' : '&redirects=true' ) + '&prop=categoryinfo|info|pageprops|pageimages|extracts&piprop=original|name&ppprop=description|displaytitle|page_image_free|disambiguation|infoboxes&explaintext=true&exsectionformat=raw&exlimit=1&converttitles=true&titles=%1F' + encodeURIComponent( ( aliasInvoke === 'search' ? full_title.split(' ').slice(1).join(' ') : title ).replace( /\x1F/g, '\ufffd' ) ) + '&format=json' ).then( response => {
+	var uselang = lang.lang;
+	if ( querystring.has('variant') || querystring.has('uselang') ) {
+		uselang = ( querystring.getAll('variant').pop() || querystring.getAll('uselang').pop() || uselang );
+		lang = lang.uselang(querystring.getAll('variant').pop(), querystring.getAll('uselang').pop());
+	}
+	got.get( wiki + 'api.php?uselang=' + uselang + '&action=query&meta=siteinfo&siprop=general|namespaces|namespacealiases|specialpagealiases&iwurl=true' + ( noRedirect ? '' : '&redirects=true' ) + '&prop=categoryinfo|info|pageprops|pageimages|extracts&piprop=original|name&ppprop=description|displaytitle|page_image_free|disambiguation|infoboxes&explaintext=true&exsectionformat=raw&exlimit=1&converttitles=true&titles=%1F' + encodeURIComponent( ( aliasInvoke === 'search' ? full_title.split(' ').slice(1).join(' ') : title ).replace( /\x1F/g, '\ufffd' ) ) + '&format=json' ).then( response => {
 		var body = response.body;
 		if ( body && body.warnings ) log_warn(body.warnings);
 		if ( response.statusCode !== 200 || !body || body.batchcomplete === undefined || !body.query ) {
-			if ( interwiki ) msg.sendChannel( spoiler + ' ' + interwiki + ' ' + spoiler );
+			if ( interwiki ) msg.sendChannel( spoiler + ( noEmbed ? '<' : ' ' ) + interwiki + ( noEmbed ? '>' : ' ' ) + spoiler );
 			else if ( wiki.noWiki(response.url, response.statusCode) ) {
 				console.log( '- This wiki doesn\'t exist!' );
 				msg.reactEmoji('nowiki');
@@ -103,21 +108,18 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 		}
 		wiki.updateWiki(body.query.general);
 		if ( aliasInvoke === 'search' ) {
-			logging(wiki, msg.guild?.id, 'search');
-			return fn.search(lang, msg, full_title.split(' ').slice(1).join(' '), wiki, body.query, reaction, spoiler);
-		}
-		if ( aliasInvoke === 'diff' && args.join('') && !querystring.toString() && !fragment ) {
-			return fn.diff(lang, msg, args, wiki, reaction, spoiler);
+			logging(wiki, msg.guildId, 'search');
+			return fn.search(lang, msg, full_title.split(' ').slice(1).join(' '), wiki, body.query, reaction, spoiler, noEmbed);
 		}
 		if ( aliasInvoke === 'discussion' && wiki.isFandom(false) && !querystring.toString() && !fragment ) {
-			logging(wiki, msg.guild?.id, 'discussion');
-			return fn.discussion(lang, msg, wiki, args.join(' '), body.query.general.sitename, reaction, spoiler);
+			logging(wiki, msg.guildId, 'discussion');
+			return fn.discussion(lang, msg, wiki, args.join(' '), body.query.general.sitename, reaction, spoiler, noEmbed);
 		}
 		if ( !msg.notMinecraft && mcw.hasOwnProperty(wiki.href) && ( minecraft.hasOwnProperty(aliasInvoke) || invoke.startsWith( '/' ) ) && !querystring.toString() && !fragment ) {
-			logging(wiki, msg.guild?.id, 'minecraft', ( minecraft.hasOwnProperty(aliasInvoke) ? aliasInvoke : 'command' ));
+			logging(wiki, msg.guildId, 'minecraft', ( minecraft.hasOwnProperty(aliasInvoke) ? aliasInvoke : 'command' ));
 			minecraft.WIKI = this;
-			if ( minecraft.hasOwnProperty(aliasInvoke) ) minecraft[aliasInvoke](lang, msg, wiki, args, title, cmd, reaction, spoiler);
-			else minecraft.SYNTAX(lang, msg, wiki, invoke.substring(1), args, title, cmd, reaction, spoiler);
+			if ( minecraft.hasOwnProperty(aliasInvoke) ) minecraft[aliasInvoke](lang, msg, wiki, args, title, cmd, reaction, spoiler, noEmbed);
+			else minecraft.SYNTAX(lang, msg, wiki, invoke.substring(1), args, title, cmd, reaction, spoiler, noEmbed);
 			return;
 		}
 		if ( body.query.pages && body.query.pages?.['-1']?.title !== '%1F' ) {
@@ -143,7 +145,7 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 			if ( ( querypage.ns === 2 || querypage.ns === 202 || querypage.ns === 1200 ) && ( !querypage.title.includes( '/' ) || /^[^:]+:(?:(?:\d{1,3}\.){3}\d{1,3}\/\d{2}|(?:[\dA-F]{1,4}:){7}[\dA-F]{1,4}\/\d{2,3})$/.test(querypage.title) ) ) {
 				var userparts = querypage.title.split(':');
 				querypage.noRedirect = noRedirect;
-				return fn.user(lang, msg, userparts[0] + ':', userparts.slice(1).join(':'), wiki, querystring, fragment, querypage, contribs, reaction, spoiler);
+				return fn.user(lang, msg, userparts[0] + ':', userparts.slice(1).join(':'), wiki, querystring, fragment, querypage, contribs, reaction, spoiler, noEmbed);
 			}
 			if ( querypage.ns === -1 && querypage.title.startsWith( contribs ) && querypage.title.length > contribs.length ) {
 				var username = querypage.title.split('/').slice(1).join('/');
@@ -165,7 +167,7 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 							querypage.special = '';
 							querypage.uselang = uselang;
 							querypage.noRedirect = noRedirect;
-							fn.user(lang, msg, contribs, username, wiki, querystring, fragment, querypage, contribs, reaction, spoiler);
+							fn.user(lang, msg, contribs, username, wiki, querystring, fragment, querypage, contribs, reaction, spoiler, noEmbed);
 						}
 						else {
 							msg.reactEmoji('error');
@@ -181,38 +183,82 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 				} );
 			}
 			if ( wiki.isMiraheze() && querypage.ns === 0 && /^Mh:[a-z\d]+:/.test(querypage.title) ) {
-				logging(wiki, msg.guild?.id, 'interwiki', 'miraheze');
+				logging(wiki, msg.guildId, 'interwiki', 'miraheze');
 				var iw_parts = querypage.title.split(':');
 				var iw = new Wiki('https://' + iw_parts[1] + '.miraheze.org/w/');
 				var iw_link = iw.toLink(iw_parts.slice(2).join(':'), querystring, fragment);
-				var maxselfcall = interwikiLimit[( patreons[msg.guild?.id] ? 'patreon' : 'default' )];
+				var maxselfcall = interwikiLimit[( patreons[msg.guildId] ? 'patreon' : 'default' )];
 				if ( selfcall < maxselfcall ) {
 					selfcall++;
-					return this.general(lang, msg, iw_parts.slice(2).join(':'), iw, '!!' + iw.hostname + ' ', reaction, spoiler, querystring, fragment, iw_link, selfcall);
+					return this.general(lang, msg, iw_parts.slice(2).join(':'), iw, '!!' + iw.hostname + ' ', reaction, spoiler, noEmbed, querystring, fragment, iw_link, selfcall);
 				}
-				msg.sendChannel( spoiler + ' ' + iw_link + ' ' + spoiler ).then( message => {
+				msg.sendChannel( spoiler + ( noEmbed ? '<' : ' ' ) + iw_link + ( noEmbed ? '>' : ' ' ) + spoiler ).then( message => {
 					if ( message && selfcall === maxselfcall ) message.reactEmoji('⚠️');
 				} );
 				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&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');
+			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 => {
+				logging(wiki, msg.guildId, 'general', 'search');
 				var srbody = srresponse.body;
-				if ( srbody && srbody.warnings ) log_warn(srbody.warnings);
+				if ( srbody?.warnings ) log_warn(srbody.warnings);
 				if ( srresponse.statusCode !== 200 || !srbody || srbody.batchcomplete === undefined ) {
-					console.log( '- ' + srresponse.statusCode + ': Error while getting the search results: ' + ( srbody && srbody.error && srbody.error.info ) );
+					console.log( '- ' + srresponse.statusCode + ': Error while getting the search results: ' + srbody?.error?.info );
 					msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Search', {search:title}) + '>' + spoiler );
 				
 					if ( reaction ) reaction.removeEmoji();
 					return;
 				}
+				if ( querypage.ns === 12 && wiki.isFandom() ) {
+					return got.head( wiki.articleURL.href.replace( '$1', encodeURIComponent( querypage.title ) ), {
+						followRedirect: false
+					} ).then( hresponse => {
+						if ( hresponse.statusCode === 301 && /^https:\/\/[a-z\d-]{1,50}\.fandom\.com\/(?:(?!wiki\/)[a-z-]{2,12}\/)?wiki\/Help:/.test( hresponse.headers?.location ) ) {
+							var location = hresponse.headers.location.split('wiki/');
+							var maxselfcall = interwikiLimit[( patreons[msg.guildId] ? 'patreon' : 'default' )];
+							if ( selfcall < maxselfcall ) {
+								selfcall++;
+								return this.general(lang, msg, location.slice(1).join('wiki/'), new Wiki(location[0]), cmd, reaction, spoiler, noEmbed, querystring, fragment, '', selfcall);
+							}
+							msg.sendChannel( spoiler + ( noEmbed ? '<' : ' ' ) + hresponse.headers.location + ( noEmbed ? '>' : ' ' ) + spoiler ).then( message => {
+								if ( message && selfcall === maxselfcall ) message.reactEmoji('⚠️');
+							} );
+							if ( reaction ) reaction.removeEmoji();
+							return;
+						}
+						if ( srbody.query ) return srbody;
+						msg.reactEmoji('🤷');
+						
+						if ( reaction ) reaction.removeEmoji();
+					}, error => {
+						console.log( '- Error while checking the help redirect: ' + error );
+						if ( srbody.query ) return srbody;
+						msg.reactEmoji('🤷');
+						
+						if ( reaction ) reaction.removeEmoji();
+					} );
+				}
 				if ( !srbody.query ) {
-					msg.reactEmoji('🤷');
-				
-					if ( reaction ) reaction.removeEmoji();
-					return;
+					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( tsrresponse => {
+						var tsrbody = tsrresponse.body;
+						if ( tsrbody?.warnings ) log_warn(tsrbody.warnings);
+						if ( tsrresponse.statusCode !== 200 || !tsrbody || tsrbody.batchcomplete === undefined ) {
+							if ( tsrbody?.error?.code !== 'search-text-disabled' ) console.log( '- ' + tsrresponse.statusCode + ': Error while getting the text search results: ' + tsrbody?.error?.info );
+						}
+						else if ( tsrbody.query ) return tsrbody;
+						msg.reactEmoji('🤷');
+						
+						if ( reaction ) reaction.removeEmoji();
+					}, error => {
+						console.log( '- Error while getting the text search results: ' + error );
+						msg.reactEmoji('🤷');
+						
+						if ( reaction ) reaction.removeEmoji();
+					} );
 				}
+				return srbody;
+			} ).then( srbody => {
+				if ( !srbody?.query?.pages ) return;
 				querypage = Object.values(srbody.query.pages)[0];
 				querypage.uselang = uselang;
 				var pagelink = wiki.toLink(querypage.title, querystring, fragment);
@@ -240,9 +286,7 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 					if ( msg.showEmbed() && /\.(?:png|jpg|jpeg|gif)$/.test(querypage.title.toLowerCase()) ) embed.setImage( pageimage );
 					else if ( querypage.title.toLowerCase().endsWith( '.svg' ) && querypage?.original?.width && msg.showEmbed() ) {
 						embed.setImage( wiki.toLink('Special:FilePath/' + querypage.title, {width:querypage.original.width,version:Date.now()}) );
-						if ( msg.uploadFiles() ) embed.attachFiles( [{attachment:pageimage,name:( spoiler ? 'SPOILER ' : '' ) + querypage.title}] );
 					}
-					else if ( msg.uploadFiles() ) embed.attachFiles( [{attachment:pageimage,name:( spoiler ? 'SPOILER ' : '' ) + querypage.title}] );
 				}
 				else if ( querypage.title === body.query.general.mainpage ) {
 					embed.setThumbnail( new URL(body.query.general.logo, wiki).href );
@@ -255,7 +299,7 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 				}
 				else embed.setThumbnail( new URL(body.query.general.logo, wiki).href );
 				
-				var prefix = ( msg.channel.isGuild() && patreons[msg.guild.id] || process.env.prefix );
+				var prefix = ( msg.channel.isGuild() && patreons[msg.guildId] || process.env.prefix );
 				var linksuffix = ( querystring.toString() ? '?' + querystring : '' ) + ( fragment ? '#' + fragment : '' );
 				if ( title.replace( /[_-]/g, ' ' ).toLowerCase() === querypage.title.replace( /-/g, ' ' ).toLowerCase() ) {
 					text = '';
@@ -281,13 +325,13 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 					if ( querypage.categoryinfo.subcats > 0 ) {
 						category.push(lang.get('search.category.subcats', querypage.categoryinfo.subcats.toLocaleString(lang.get('dateformat')), querypage.categoryinfo.subcats));
 					}
-					if ( msg.showEmbed() ) embed.addField( category[0], category.slice(1).join('\n') );
+					if ( msg.showEmbed() && !noEmbed ) embed.addField( category[0], category.slice(1).join('\n') );
 					else text += '\n\n' + category.join('\n');
 				}
 
-				return parse_page(lang, msg, spoiler + '<' + pagelink + '>' + text + spoiler, embed, wiki, reaction, querypage, ( querypage.title === body.query.general.mainpage ? '' : new URL(body.query.general.logo, wiki).href ), fragment, pagelink);
+				return parse_page(lang, msg, spoiler + '<' + pagelink + '>' + text + spoiler, ( noEmbed ? null : embed ), wiki, reaction, querypage, ( querypage.title === body.query.general.mainpage ? '' : new URL(body.query.general.logo, wiki).href ), fragment, pagelink);
 			}, error => {
-				logging(wiki, msg.guild?.id, 'general', 'search');
+				logging(wiki, msg.guildId, 'general', 'search');
 				console.log( '- Error while getting the search results: ' + error );
 				msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Search', {search:title}) + '>' + spoiler );
 				
@@ -297,25 +341,25 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 				var specialpage = body.query.specialpagealiases.find( sp => body.query.namespaces['-1']['*'] + ':' + sp.aliases[0].replace( /\_/g, ' ' ) === querypage.title.split('/')[0] );
 				specialpage = ( specialpage ? specialpage.realname : querypage.title.replace( body.query.namespaces['-1']['*'] + ':', '' ).split('/')[0] ).toLowerCase();
 				if ( !['mylanguage'].includes( specialpage ) ) {
-					var pagelink = wiki.toLink(querypage.title, querystring, fragment);
-					var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( escapeFormatting(querypage.title) ).setURL( pagelink ).setThumbnail( new URL(body.query.general.logo, wiki).href );
-					return fn.special_page(lang, msg, querypage, specialpage, embed, wiki, reaction, spoiler);
+					return fn.special_page(lang, msg, querypage, specialpage, body.query, wiki, querystring, fragment, reaction, spoiler, noEmbed);
 				}
 			}
 			if ( querypage.ns === -2 ) {
-				logging(wiki, msg.guild?.id, 'general', 'media');
+				logging(wiki, msg.guildId, 'general', 'media');
 				var filepath = body.query.specialpagealiases.find( sp => sp.realname === 'Filepath' );
 				var pagelink = wiki.toLink(body.query.namespaces['-1']['*'] + ':' + ( filepath?.aliases?.[0] || 'FilePath' ) + querypage.title.replace( body.query.namespaces['-2']['*'] + ':', '/' ), querystring, fragment);
-				var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( escapeFormatting(querypage.title) ).setURL( pagelink ).setDescription( '[' + lang.get('search.media') + '](' + wiki.toLink(querypage.title, '', '', true) + ')' );
-				if ( msg.showEmbed() && /\.(?:png|jpg|jpeg|gif)$/.test(querypage.title.toLowerCase()) ) embed.setImage( pagelink );
-				else if ( msg.uploadFiles() ) embed.attachFiles( [{attachment:pagelink,name:( spoiler ? 'SPOILER ' : '' ) + querypage.title}] );
+				var embed = null;
+				if ( !noEmbed ) {
+					embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( escapeFormatting(querypage.title) ).setURL( pagelink ).setDescription( '[' + lang.get('search.media') + '](' + wiki.toLink(querypage.title, '', '', true) + ')' );
+					if ( msg.showEmbed() && /\.(?:png|jpg|jpeg|gif)$/.test(querypage.title.toLowerCase()) ) embed.setImage( pagelink );
+				}
 				
-				msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
+				msg.sendChannel( {content: spoiler + '<' + pagelink + '>' + spoiler, embeds: [embed]} );
 				
 				if ( reaction ) reaction.removeEmoji();
 				return;
 			}
-			logging(wiki, msg.guild?.id, 'general');
+			logging(wiki, msg.guildId, 'general');
 			var pagelink = wiki.toLink(querypage.title, querystring, ( fragment || ( body.query.redirects && body.query.redirects[0].tofragment ) || '' ));
 			var text = '';
 			var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( escapeFormatting(querypage.title) ).setURL( pagelink );
@@ -339,7 +383,6 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 			if ( querypage.ns === 6 ) {
 				var pageimage = ( querypage?.original?.source || wiki.toLink('Special:FilePath/' + querypage.title, {version:Date.now()}) );
 				if ( msg.showEmbed() && /\.(?:png|jpg|jpeg|gif)$/.test(querypage.title.toLowerCase()) ) embed.setImage( pageimage );
-				else if ( msg.uploadFiles() ) embed.attachFiles( [{attachment:pageimage,name:( spoiler ? 'SPOILER ' : '' ) + querypage.title}] );
 			}
 			else if ( querypage.title === body.query.general.mainpage ) {
 				embed.setThumbnail( new URL(body.query.general.logo, wiki).href );
@@ -365,14 +408,14 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 				if ( querypage.categoryinfo.subcats > 0 ) {
 					category.push(lang.get('search.category.subcats', querypage.categoryinfo.subcats.toLocaleString(lang.get('dateformat')), querypage.categoryinfo.subcats));
 				}
-				if ( msg.showEmbed() ) embed.addField( category[0], category.slice(1).join('\n') );
+				if ( msg.showEmbed() && !noEmbed ) embed.addField( category[0], category.slice(1).join('\n') );
 				else text += '\n\n' + category.join('\n');
 			}
 			
-			return parse_page(lang, msg, spoiler + '<' + pagelink + '>' + text + spoiler, embed, wiki, reaction, querypage, ( querypage.title === body.query.general.mainpage ? '' : new URL(body.query.general.logo, wiki).href ), ( fragment || ( body.query.redirects && body.query.redirects[0].tofragment ) || '' ), pagelink);
+			return parse_page(lang, msg, spoiler + '<' + pagelink + '>' + text + spoiler, ( noEmbed ? null : embed ), wiki, reaction, querypage, ( querypage.title === body.query.general.mainpage ? '' : new URL(body.query.general.logo, wiki).href ), ( fragment || ( body.query.redirects && body.query.redirects[0].tofragment ) || '' ), pagelink);
 		}
 		if ( body.query.interwiki ) {
-			if ( msg.channel.isGuild() && pause[msg.guild.id] ) {
+			if ( msg.channel.isGuild() && pause[msg.guildId] ) {
 				if ( reaction ) reaction.removeEmoji();
 				console.log( '- Aborted, paused.' );
 				return;
@@ -384,16 +427,16 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 			if ( fragment ) iw.hash = Wiki.toSection(fragment);
 			else fragment = iw.hash.substring(1);
 			if ( /^phabricator\.(wikimedia|miraheze)\.org$/.test(iw.hostname) ) {
-				return phabricator(lang, msg, wiki, iw, reaction, spoiler);
+				return phabricator(lang, msg, wiki, iw, reaction, spoiler, noEmbed);
 			}
-			logging(wiki, msg.guild?.id, 'interwiki');
-			var maxselfcall = interwikiLimit[( patreons[msg.guild?.id] ? 'patreon' : 'default' )];
+			logging(wiki, msg.guildId, 'interwiki');
+			var maxselfcall = interwikiLimit[( patreons[msg.guildId] ? 'patreon' : 'default' )];
 			if ( selfcall < maxselfcall && ['http:','https:'].includes( iw.protocol ) ) {
 				selfcall++;
 				if ( iw.hostname.endsWith( '.gamepedia.com' ) ) {
 					let iwtitle = decodeURIComponent( iw.pathname.substring(1) ).replace( /_/g, ' ' );
 					cmd = '!' + iw.hostname.replace( '.gamepedia.com', ' ' );
-					if ( cmd !== '!www ' ) return this.general(lang, msg, iwtitle, new Wiki(iw.origin), cmd, reaction, spoiler, iw.searchParams, fragment, iw.href, selfcall);
+					if ( cmd !== '!www ' ) return this.general(lang, msg, iwtitle, new Wiki(iw.origin), cmd, reaction, spoiler, noEmbed, iw.searchParams, fragment, iw.href, selfcall);
 				}
 				if ( iw.hostname.endsWith( '.fandom.com' ) || iw.hostname.endsWith( '.wikia.org' ) ) {
 					let regex = iw.pathname.match( /^(\/(?!wiki\/)[a-z-]{2,12})?(?:\/wiki\/|\/?$)/ );
@@ -401,7 +444,7 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 						let path = ( regex[1] || '' );
 						let iwtitle = decodeURIComponent( iw.pathname.replace( regex[0], '' ) ).replace( /_/g, ' ' );
 						cmd = ( iw.hostname.endsWith( '.wikia.org' ) ? '??' : '?' ) + ( path ? path.substring(1) + '.' : '' ) + iw.hostname.replace( /\.(?:fandom\.com|wikia\.org)/, ' ' );
-						return this.general(lang, msg, iwtitle, new Wiki(iw.origin + path + '/'), cmd, reaction, spoiler, iw.searchParams, fragment, iw.href, selfcall);
+						return this.general(lang, msg, iwtitle, new Wiki(iw.origin + path + '/'), cmd, reaction, spoiler, noEmbed, iw.searchParams, fragment, iw.href, selfcall);
 					}
 				}
 				let project = wikiProjects.find( project => iw.hostname.endsWith( project.name ) );
@@ -410,17 +453,17 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 					if ( regex ) {
 						let iwtitle = decodeURIComponent( ( iw.host + iw.pathname ).replace( regex[0], '' ) ).replace( /_/g, ' ' );
 						cmd = '!!' + regex[1] + ' ';
-						return this.general(lang, msg, iwtitle, new Wiki('https://' + regex[1] + project.scriptPath), cmd, reaction, spoiler, iw.searchParams, fragment, iw.href, selfcall);
+						return this.general(lang, msg, iwtitle, new Wiki('https://' + regex[1] + project.scriptPath), cmd, reaction, spoiler, noEmbed, iw.searchParams, fragment, iw.href, selfcall);
 					}
 				}
 			}
-			msg.sendChannel( spoiler + ' ' + iw + ' ' + spoiler ).then( message => {
+			msg.sendChannel( spoiler + ( noEmbed ? '<' : ' ' ) + iw + ( noEmbed ? '>' : ' ' ) + spoiler ).then( message => {
 				if ( message && selfcall === maxselfcall ) message.reactEmoji('⚠️');
 			} );
 			if ( reaction ) reaction.removeEmoji();
 			return;
 		}
-		logging(wiki, msg.guild?.id, 'general');
+		logging(wiki, msg.guildId, 'general');
 		var querypage = {
 			title: body.query.general.mainpage,
 			contentmodel: 'wikitext',
@@ -469,10 +512,10 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 		}, error => {
 			console.log( '- Error while getting the main page: ' + error );
 		} ).finally( () => {
-			parse_page(lang, msg, spoiler + '<' + pagelink + '>' + spoiler, embed, wiki, reaction, querypage, '', fragment, pagelink);
+			parse_page(lang, msg, spoiler + '<' + pagelink + '>' + spoiler, ( noEmbed ? null : embed ), wiki, reaction, querypage, '', fragment, pagelink);
 		} );
 	}, error => {
-		if ( interwiki ) msg.sendChannel( spoiler + ' ' + interwiki + ' ' + spoiler );
+		if ( interwiki ) msg.sendChannel( spoiler + ( noEmbed ? '<' : ' ' ) + interwiki + ( noEmbed ? '>' : ' ' ) + spoiler );
 		else if ( wiki.noWiki(error.message) ) {
 			console.log( '- This wiki doesn\'t exist!' );
 			msg.reactEmoji('nowiki');

+ 35 - 27
cmds/wiki/overview.js

@@ -1,7 +1,7 @@
 const {MessageEmbed} = require('discord.js');
 const logging = require('../../util/logging.js');
 const {timeoptions} = require('../../util/default.json');
-const {toFormatting, toPlaintext, escapeFormatting} = require('../../util/functions.js');
+const {got, toFormatting, toPlaintext, escapeFormatting} = require('../../util/functions.js');
 
 /**
  * Sends a Gamepedia wiki overview.
@@ -10,9 +10,13 @@ const {toFormatting, toPlaintext, escapeFormatting} = require('../../util/functi
  * @param {import('../../util/wiki.js')} wiki - The wiki for the overview.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  * @param {String} spoiler - If the response is in a spoiler.
+ * @param {Boolean} noEmbed - If the response should be without an embed.
+ * @param {URLSearchParams} [querystring] - The querystring for the link.
+ * @param {String} [fragment] - The section for the link.
  */
-function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
-	got.get( wiki + 'api.php?action=query&meta=siteinfo' + ( wiki.isFandom() ? '|allmessages&ammessages=custom-GamepediaNotice|custom-FandomMergeNotice&amenableparser=true' : '' ) + '&siprop=general|statistics|languages|rightsinfo' + ( wiki.isFandom() ? '|variables' : '' ) + '&siinlanguagecode=' + lang.lang + '&list=logevents&ledir=newer&lelimit=1&leprop=timestamp&titles=Special:Statistics&format=json' ).then( response => {
+function gamepedia_overview(lang, msg, wiki, reaction, spoiler, noEmbed, querystring = new URLSearchParams(), fragment = '') {
+	var uselang = ( querystring.getAll('variant').pop() || querystring.getAll('uselang').pop() || lang.lang );
+	got.get( wiki + 'api.php?uselang=' + uselang + '&action=query&meta=allmessages|siteinfo&amenableparser=true&amtitle=Special:Statistics&ammessages=statistics' + ( wiki.isFandom() ? '|custom-GamepediaNotice|custom-FandomMergeNotice' : '' ) + '&siprop=general|statistics|languages|rightsinfo' + ( wiki.isFandom() ? '|variables' : '' ) + '&siinlanguagecode=' + uselang + '&list=logevents&ledir=newer&lelimit=1&leprop=timestamp&titles=Special:Statistics&format=json' ).then( response => {
 		var body = response.body;
 		if ( body && body.warnings ) log_warn(body.warnings);
 		if ( response.statusCode !== 200 || !body || body.batchcomplete === undefined || !body.query || !body.query.pages ) {
@@ -22,17 +26,17 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 			}
 			else {
 				console.log( '- ' + response.statusCode + ': Error while getting the statistics: ' + ( body && body.error && body.error.info ) );
-				msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Statistics') + '>' + spoiler );
+				msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Statistics', querystring, fragment) + '>' + spoiler );
 			}
 			
 			if ( reaction ) reaction.removeEmoji();
 			return;
 		}
 		wiki.updateWiki(body.query.general);
-		logging(wiki, msg.guild?.id, 'overview');
+		logging(wiki, msg.guildId, 'overview');
 		var version = [lang.get('overview.version'), body.query.general.generator];
 		var creation_date = null;
-		var created = [lang.get('overview.created'), lang.get('overview.unknown')];
+		var created = [lang.get('overview.created'), lang.get('overview.unknown'), ''];
 		try {
 			var dateformat = new Intl.DateTimeFormat(lang.get('dateformat'), Object.assign({
 				timeZone: body.query.general.timezone
@@ -46,6 +50,7 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 		if ( body.query.logevents?.[0]?.timestamp ) {
 			creation_date = new Date(body.query.logevents[0].timestamp);
 			created[1] = dateformat.format(creation_date);
+			created[2] = '<t:' + Math.trunc(creation_date.getTime() / 1000) + ':R>';
 		}
 		var language = [lang.get('overview.lang'), body.query.languages.find( language => {
 			return language.code === body.query.general.lang;
@@ -64,7 +69,7 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 			
 			if ( body.query.rightsinfo.text ) {
 				let licensetext = body.query.rightsinfo.text;
-				if ( msg.showEmbed() ) {
+				if ( msg.showEmbed() && !noEmbed ) {
 					license[1] = '[' + toPlaintext(licensetext, true) + '](' + licenseurl + ')';
 				}
 				else license[1] = toPlaintext(licensetext, true) + ' (<' + licenseurl + '>)';
@@ -72,24 +77,29 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 			else license[1] = '<' + licenseurl + '>';
 		}
 		else if ( body.query.rightsinfo.text ) {
-			license[1] = toFormatting(body.query.rightsinfo.text, msg.showEmbed(), wiki, '', true);
+			license[1] = toFormatting(body.query.rightsinfo.text, ( msg.showEmbed() && !noEmbed ), wiki, '', true);
 		}
 		var misermode = [lang.get('overview.misermode'), lang.get('overview.' + ( body.query.general.misermode !== undefined ? 'yes' : 'no' ))];
 		var readonly = [lang.get('overview.readonly')];
 		if ( body.query.general.readonly !== undefined ) {
 			if ( body.query.general.readonlyreason ) {
 				let readonlyreason = body.query.general.readonlyreason;
-				readonly.push(toFormatting(readonlyreason, msg.showEmbed(), wiki, '', true));
+				readonly.push(toFormatting(readonlyreason, ( msg.showEmbed() && !noEmbed ), wiki, '', true));
 			}
 			else readonly = ['\u200b', '**' + lang.get('overview.readonly') + '**'];
 		}
 		
 		var title = body.query.pages['-1'].title;
-		var pagelink = wiki.toLink(title);
+		var pagelink = wiki.toLink(title, querystring, fragment);
 		var text = '<' + pagelink + '>';
 		var embed = null;
-		if ( msg.showEmbed() ) {
+		if ( msg.showEmbed() && !noEmbed ) {
 			embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( escapeFormatting(title) ).setURL( pagelink ).setThumbnail( new URL(body.query.general.logo, wiki).href );
+			if ( body.query.allmessages?.[0]?.['*']?.trim?.() ) {
+				let displaytitle = escapeFormatting(body.query.allmessages[0]['*'].trim());
+				if ( displaytitle.length > 250 ) displaytitle = displaytitle.substring(0, 250) + '\u2026';
+				embed.setTitle( displaytitle );
+			}
 		}
 		else {
 			text += '\n';
@@ -106,11 +116,11 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 			var manager = [lang.get('overview.manager'), ''];
 			var founder = [lang.get('overview.founder')];
 			var crossover = [lang.get('overview.crossover')];
-			if ( body.query.allmessages?.[0]?.['*'] ) {
-				crossover[1] = '<https://' + body.query.allmessages[0]['*'] + '.gamepedia.com/>';
+			if ( body.query.allmessages?.[1]?.['*']?.trim?.() ) {
+				crossover[1] = '<https://' + body.query.allmessages[1]['*'].trim() + '.gamepedia.com/>';
 			}
-			if ( body.query.allmessages?.[1]?.['*'] ) {
-				let mergeNotice = body.query.allmessages[1]['*'];
+			if ( body.query.allmessages?.[2]?.['*']?.trim?.() ) {
+				let mergeNotice = body.query.allmessages[2]['*'].trim();
 				if ( !mergeNotice.includes( '|' ) ) {
 					mergeNotice = mergeNotice.split('/');
 					crossover[1] = '<https://' + mergeNotice[0] + '.fandom.com/' + ( mergeNotice[1] ? '/' + mergeNotice[1] : '' ) + '>';
@@ -132,6 +142,7 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 				if ( site.creation_date && creation_date > new Date(site.creation_date) ) {
 					creation_date = new Date(site.creation_date);
 					created[1] = dateformat.format(creation_date);
+					created[2] = '<t:' + Math.trunc(creation_date.getTime() / 1000) + ':R>';
 				}
 				if ( site.desc ) {
 					description[1] = escapeFormatting(site.desc);
@@ -151,7 +162,7 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 						}
 						else {
 							var user = usbody.query.users[0].name;
-							if ( msg.showEmbed() ) founder[1] = '[' + user + '](' + wiki.toLink('User:' + user, '', '', true) + ')';
+							if ( msg.showEmbed() && !noEmbed ) founder[1] = '[' + user + '](' + wiki.toLink('User:' + user, '', '', true) + ')';
 							else founder[1] = user;
 						}
 					}, error => {
@@ -183,13 +194,13 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 				console.log( '- Error while getting the wiki details: ' + error );
 				return;
 			} ).finally( () => {
-				if ( msg.showEmbed() ) {
+				if ( msg.showEmbed() && !noEmbed ) {
 					if ( vertical[1] ) embed.addField( vertical[0], vertical[1], true );
 					if ( topic[1] ) embed.addField( topic[0], topic[1], true );
 					if ( official[1] ) embed.addField( official[0], official[1], true );
 					embed.addField( version[0], version[1], true ).addField( language[0], language[1], true );
 					if ( rtl[1] ) embed.addField( rtl[0], rtl[1], true );
-					embed.addField( created[0], created[1], true ).addField( articles[0], articles[1], true ).addField( pages[0], pages[1], true ).addField( edits[0], edits[1], true );
+					embed.addField( created[0], created[1] + '\n' + created[2], true ).addField( articles[0], articles[1], true ).addField( pages[0], pages[1], true ).addField( edits[0], edits[1], true );
 					if ( posts[1] ) embed.addField( posts[0], posts[1], true );
 					if ( walls[1] ) embed.addField( walls[0], walls[1], true );
 					if ( comments[1] ) embed.addField( comments[0], comments[1], true );
@@ -218,23 +229,20 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 					if ( crossover[1] ) text += '\n' + crossover.join(' ');
 					text += '\n' + license.join(' ') + '\n' + misermode.join(' ');
 					if ( description[1] ) text += '\n' + description.join(' ');
-					if ( image[1] ) {
-						text += '\n' + image.join(' ');
-						if ( msg.uploadFiles() ) embed.files = [{attachment:image[1],name:( spoiler ? 'SPOILER ' : '' ) + body.query.general.sitename + image[1].substring(image[1].lastIndexOf('.'))}];
-					}
+					if ( image[1] ) text += '\n' + image.join(' ');
 					if ( readonly[1] ) text += '\n\n' + ( readonly[0] === '\u200b' ? readonly[1] : readonly.join('\n') );
 					text += '\n\n*' + lang.get('overview.inaccurate') + '*';
 				}
 				
-				msg.sendChannel( spoiler + text + spoiler, {embed} );
+				msg.sendChannel( {content: spoiler + text + spoiler, embeds: [embed]} );
 				
 				if ( reaction ) reaction.removeEmoji();
 			} );
 		}
-		if ( msg.showEmbed() ) {
+		if ( msg.showEmbed() && !noEmbed ) {
 			embed.addField( version[0], version[1], true ).addField( language[0], language[1], true );
 			if ( rtl[1] ) embed.addField( rtl[0], rtl[1], true );
-			embed.addField( created[0], created[1], true ).addField( articles[0], articles[1], true ).addField( pages[0], pages[1], true ).addField( edits[0], edits[1], true ).addField( users[0], users[1], true ).addField( admins[0], admins[1], true ).addField( license[0], license[1], true ).addField( misermode[0], misermode[1], true ).setFooter( lang.get('overview.inaccurate') );
+			embed.addField( created[0], created[1] + '\n' + created[2], true ).addField( articles[0], articles[1], true ).addField( pages[0], pages[1], true ).addField( edits[0], edits[1], true ).addField( users[0], users[1], true ).addField( admins[0], admins[1], true ).addField( license[0], license[1], true ).addField( misermode[0], misermode[1], true ).setFooter( lang.get('overview.inaccurate') );
 			if ( readonly[1] ) embed.addField( readonly[0], readonly[1] );
 		}
 		else {
@@ -245,7 +253,7 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 			text += '\n\n*' + lang.get('overview.inaccurate') + '*';
 		}
 		
-		msg.sendChannel( spoiler + text + spoiler, {embed} );
+		msg.sendChannel( {content: spoiler + text + spoiler, embeds: [embed]} );
 		
 		if ( reaction ) reaction.removeEmoji();
 	}, error => {
@@ -255,7 +263,7 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 		}
 		else {
 			console.log( '- Error while getting the statistics: ' + error );
-			msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Statistics') + '>' + spoiler );
+			msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Statistics', querystring, fragment) + '>' + spoiler );
 		}
 		
 		if ( reaction ) reaction.removeEmoji();

+ 66 - 13
cmds/wiki/random.js

@@ -1,7 +1,7 @@
 const {MessageEmbed} = require('discord.js');
 const parse_page = require('../../functions/parse_page.js');
 const logging = require('../../util/logging.js');
-const {htmlToDiscord, escapeFormatting} = require('../../util/functions.js');
+const {got, toMarkdown, htmlToDiscord, escapeFormatting} = require('../../util/functions.js');
 const extract_desc = require('../../util/extract_desc.js');
 
 /**
@@ -11,43 +11,79 @@ const extract_desc = require('../../util/extract_desc.js');
  * @param {import('../../util/wiki.js')} wiki - The wiki for the page.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  * @param {String} spoiler - If the response is in a spoiler.
+ * @param {Boolean} noEmbed - If the response should be without an embed.
+ * @param {String[]} [namespace] - The namespace to get a random page of.
+ * @param {URLSearchParams} [querystring] - The querystring for the link.
+ * @param {String} [fragment] - The section for the link.
  */
-function gamepedia_random(lang, msg, wiki, reaction, spoiler) {
-	got.get( wiki + 'api.php?uselang=' + lang.lang + '&action=query&meta=siteinfo&siprop=general&prop=info|pageprops|pageimages|extracts&piprop=original|name&ppprop=description|displaytitle|page_image_free|disambiguation|infoboxes&explaintext=true&exsectionformat=raw&exlimit=1&generator=random&grnnamespace=0&format=json' ).then( response => {
+function gamepedia_random(lang, msg, wiki, reaction, spoiler, noEmbed, namespace = ['0', '*'], querystring = new URLSearchParams(), fragment = '') {
+	var uselang = ( querystring.getAll('variant').pop() || querystring.getAll('uselang').pop() || lang.lang );
+	got.get( wiki + 'api.php?uselang=' + uselang + '&action=query&meta=allmessages|siteinfo&amenableparser=true&amtitle=Special:Random&ammessages=randompage|randompage-nopages&amargs=%1F' + namespace[1] + '%1F' + namespace[0].split('|').length + '&siprop=general&prop=categoryinfo|info|pageprops|pageimages|extracts&piprop=original|name&ppprop=description|displaytitle|page_image_free|disambiguation|infoboxes&explaintext=true&exsectionformat=raw&exlimit=1&converttitles=true&generator=random&grnfilterredir=nonredirects&grnlimit=1&grnnamespace=' + namespace[0] + '&format=json' ).then( response => {
 		var body = response.body;
 		if ( body && body.warnings ) log_warn(body.warnings);
-		if ( response.statusCode !== 200 || !body || body.batchcomplete === undefined || !body.query || !body.query.pages ) {
+		if ( response.statusCode !== 200 || !body || body.batchcomplete === undefined || !body.query || !body.query.general ) {
 			if ( wiki.noWiki(response.url, response.statusCode) ) {
 				console.log( '- This wiki doesn\'t exist!' );
 				msg.reactEmoji('nowiki');
 			}
 			else {
 				console.log( '- ' + response.statusCode + ': Error while getting the search results: ' + ( body && body.error && body.error.info ) );
-				msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Random') + '>' + spoiler );
+				msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Random', querystring, fragment) + '>' + spoiler );
 			}
 			if ( reaction ) reaction.removeEmoji();
 			return;
 		}
 		wiki.updateWiki(body.query.general);
-		logging(wiki, msg.guild?.id, 'random');
+		logging(wiki, msg.guildId, 'random');
+		if ( !body.query.pages ) {
+			var title = 'Special:Random';
+			if ( namespace[0] !== '0' && namespace[0].split('|').length === 1 ) title += '/' + namespace[1];
+			var pagelink = wiki.toLink(title, querystring, fragment);
+			var embed = null;
+			if ( msg.showEmbed() && !noEmbed ) {
+				embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( escapeFormatting(title) ).setURL( pagelink ).setThumbnail( new URL(body.query.general.logo, wiki).href );
+				if ( body.query.allmessages?.[0]?.['*']?.trim?.() ) {
+					let displaytitle = escapeFormatting(body.query.allmessages[0]['*'].trim());
+					if ( displaytitle.length > 250 ) displaytitle = displaytitle.substring(0, 250) + '\u2026';
+					embed.setTitle( displaytitle );
+				}
+				if ( body.query.allmessages?.[1]?.['*']?.trim?.() ) {
+					var description = toMarkdown(body.query.allmessages[1]['*'], wiki, title, true);
+					if ( description.length > 1000 ) description = description.substring(0, 1000) + '\u2026';
+					embed.setDescription( description );
+				}
+			}
+			msg.sendChannel( {content: spoiler + '<' + pagelink + '>' + spoiler, embeds: [embed]} );
+			
+			if ( reaction ) reaction.removeEmoji();
+			return;
+		}
 		var querypage = Object.values(body.query.pages)[0];
-		var pagelink = wiki.toLink(querypage.title);
+		var pagelink = wiki.toLink(querypage.title, querystring, fragment);
+		var text = '';
 		var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( escapeFormatting(querypage.title) ).setURL( pagelink );
 		if ( querypage.pageprops && querypage.pageprops.displaytitle ) {
 			var displaytitle = htmlToDiscord( querypage.pageprops.displaytitle );
 			if ( displaytitle.length > 250 ) displaytitle = displaytitle.substring(0, 250) + '\u2026';
 			embed.setTitle( displaytitle );
 		}
+		if ( querypage.extract ) {
+			var extract = extract_desc(querypage.extract, fragment);
+			embed.backupDescription = extract[0];
+			if ( extract[1].length && extract[2].length ) {
+				embed.backupField = {name: extract[1], value: extract[2]};
+			}
+		}
 		if ( querypage.pageprops && querypage.pageprops.description ) {
 			var description = htmlToDiscord( querypage.pageprops.description );
 			if ( description.length > 1000 ) description = description.substring(0, 1000) + '\u2026';
 			embed.backupDescription = description;
 		}
-		else if ( querypage.extract ) {
-			var extract = extract_desc(querypage.extract);
-			embed.backupDescription = extract[0];
+		if ( querypage.ns === 6 ) {
+			var pageimage = ( querypage?.original?.source || wiki.toLink('Special:FilePath/' + querypage.title, {version:Date.now()}) );
+			if ( msg.showEmbed() && /\.(?:png|jpg|jpeg|gif)$/.test(querypage.title.toLowerCase()) ) embed.setImage( pageimage );
 		}
-		if ( querypage.title === body.query.general.mainpage ) {
+		else if ( querypage.title === body.query.general.mainpage ) {
 			embed.setThumbnail( new URL(body.query.general.logo, wiki).href );
 		}
 		else if ( querypage.pageimage && querypage.original ) {
@@ -57,8 +93,25 @@ function gamepedia_random(lang, msg, wiki, reaction, spoiler) {
 			embed.setThumbnail( wiki.toLink('Special:FilePath/' + querypage.pageprops.page_image_free, {version:Date.now()}) );
 		}
 		else embed.setThumbnail( new URL(body.query.general.logo, wiki).href );
+		if ( querypage.categoryinfo ) {
+			var category = [lang.get('search.category.content')];
+			if ( querypage.categoryinfo.size === 0 ) {
+				category.push(lang.get('search.category.empty'));
+			}
+			if ( querypage.categoryinfo.pages > 0 ) {
+				category.push(lang.get('search.category.pages', querypage.categoryinfo.pages.toLocaleString(lang.get('dateformat')), querypage.categoryinfo.pages));
+			}
+			if ( querypage.categoryinfo.files > 0 ) {
+				category.push(lang.get('search.category.files', querypage.categoryinfo.files.toLocaleString(lang.get('dateformat')), querypage.categoryinfo.files));
+			}
+			if ( querypage.categoryinfo.subcats > 0 ) {
+				category.push(lang.get('search.category.subcats', querypage.categoryinfo.subcats.toLocaleString(lang.get('dateformat')), querypage.categoryinfo.subcats));
+			}
+			if ( msg.showEmbed() && !noEmbed ) embed.addField( category[0], category.slice(1).join('\n') );
+			else text += '\n\n' + category.join('\n');
+		}
 		
-		parse_page(lang, msg, '🎲 ' + spoiler + '<' + pagelink + '>' + spoiler, embed, wiki, reaction, querypage, ( querypage.title === body.query.general.mainpage ? '' : new URL(body.query.general.logo, wiki).href ));
+		return parse_page(lang, msg, '🎲 ' + spoiler + '<' + pagelink + '>' + text + spoiler, ( noEmbed ? null : embed ), wiki, reaction, querypage, ( querypage.title === body.query.general.mainpage ? '' : new URL(body.query.general.logo, wiki).href ), fragment, pagelink);
 	}, error => {
 		if ( wiki.noWiki(error.message) ) {
 			console.log( '- This wiki doesn\'t exist!' );
@@ -66,7 +119,7 @@ function gamepedia_random(lang, msg, wiki, reaction, spoiler) {
 		}
 		else {
 			console.log( '- Error while getting the search results: ' + error );
-			msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Random') + '>' + spoiler );
+			msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Random', querystring, fragment) + '>' + spoiler );
 		}
 		if ( reaction ) reaction.removeEmoji();
 	} );

+ 107 - 61
cmds/wiki/search.js

@@ -1,4 +1,5 @@
 const {MessageEmbed, Util} = require('discord.js');
+const {got, escapeFormatting} = require('../../util/functions.js');
 const {limit: {search: searchLimit}} = require('../../util/default.json');
 
 /**
@@ -10,95 +11,140 @@ const {limit: {search: searchLimit}} = require('../../util/default.json');
  * @param {Object} query - The siteinfo from the wiki.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  * @param {String} spoiler - If the response is in a spoiler.
+ * @param {Boolean} noEmbed - If the response should be without an embed.
  */
-function gamepedia_search(lang, msg, searchterm, wiki, query, reaction, spoiler) {
+function gamepedia_search(lang, msg, searchterm, wiki, query, reaction, spoiler, noEmbed) {
 	if ( searchterm.length > 250 ) {
 		searchterm = searchterm.substring(0, 250);
 		msg.reactEmoji('⚠️');
 	}
+	if ( !searchterm.trim() ) return this.special_page(lang, msg, {title: 'Special:Search'}, 'search', query, wiki, new URLSearchParams(), '', reaction, spoiler, noEmbed);
 	var pagelink = wiki.toLink('Special:Search', {search:searchterm,fulltext:1});
-	var embed = new MessageEmbed().setAuthor( query.general.sitename ).setTitle( '`' + searchterm + '`' ).setURL( pagelink );
-	if ( !searchterm.trim() ) {
-		pagelink = wiki.toLink('Special:Search');
-		embed.setTitle( 'Special:Search' ).setURL( pagelink );
-	}
+	var resultText = '<' + pagelink + '>';
+	var embed = null;
+	if ( msg.showEmbed() && !noEmbed ) embed = new MessageEmbed().setAuthor( query.general.sitename ).setTitle( '`' + searchterm + '`' ).setURL( pagelink );
+	else resultText += '\n\n**`' + searchterm + '`**';
 	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&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 limit = searchLimit[( patreons[msg.guildId] ? '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 => {
 		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 ) {
-			return console.log( '- ' + response.statusCode + ': Error while getting the search results: ' + ( body && body.error && body.error.info ) );
+		if ( body?.warnings ) log_warn(body.warnings);
+		if ( response.statusCode !== 200 || !body?.query?.search || body.batchcomplete === undefined ) {
+			return console.log( '- ' + response.statusCode + ': Error while getting the search results: ' + body?.error?.info );
 		}
-		if ( body.query.pages && body.query.pages['-1'] && body.query.pages['-1'].title ) {
-			if ( searchterm.trim() ) {
-				pagelink = wiki.toLink(body.query.pages['-1'].title, {search:searchterm,fulltext:1});
-				embed.setURL( pagelink );
-			}
-			else {
-				pagelink = wiki.toLink(body.query.pages['-1'].title);
-				embed.setTitle( body.query.pages['-1'].title ).setURL( pagelink );
-			}
+		if ( body.query.search.length < limit ) {
+			return got.get( wiki + 'api.php?action=query&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( tresponse => {
+				var tbody = tresponse.body;
+				if ( tbody?.warnings ) log_warn(tbody.warnings);
+				if ( tresponse.statusCode !== 200 || !tbody?.query?.search || tbody.batchcomplete === undefined ) {
+					return console.log( '- ' + tresponse.statusCode + ': Error while getting the text search results: ' + tbody?.error?.info );
+				}
+				body.query.search.push(...tbody.query.search.filter( tresult => {
+					return !body.query.search.some( result => result.pageid === tresult.pageid );
+				} ).slice(0, limit - body.query.search.length));
+				if ( body.query.searchinfo && tbody.query.searchinfo ) body.query.searchinfo.totalhits += tbody.query.searchinfo.totalhits;
+			}, error => {
+				console.log( '- Error while getting the text search results: ' + error );
+			} ).then( () => {
+				return body;
+			} );
 		}
-		if ( searchterm.trim() ) {
-			var hasExactMatch = false;
-			body.query.search.forEach( result => {
-				let text = '• ';
-				let bold = '';
-				if ( result.title.replace( /[_-]/g, ' ' ).toLowerCase() === querypage.title.replace( /-/g, ' ' ).toLowerCase() ) {
-					bold = '**';
-					hasExactMatch = true;
-					if ( query.redirects?.[0] ) {
-						if ( query.redirects[0].tofragment && !result.sectiontitle ) {
-							result.sectiontitle = query.redirects[0].tofragment;
-						}
-						if ( !result.redirecttitle ) result.redirecttitle = query.redirects[0].from;
+		return body;
+	} ).then( body => {
+		if ( !body?.query?.search ) return;
+		if ( body.query.pages?.['-1']?.title ) {
+			pagelink = wiki.toLink(body.query.pages['-1'].title, {search:searchterm,fulltext:1});
+			resultText = '<' + pagelink + '>';
+			if ( msg.showEmbed() && !noEmbed ) embed.setURL( pagelink );
+			else resultText += '\n\n**`' + searchterm + '`**';
+		}
+		var hasExactMatch = false;
+		var description = [];
+		body.query.search.forEach( result => {
+			let text = '• ';
+			let bold = '';
+			if ( result.title.replace( /[_-]/g, ' ' ).toLowerCase() === querypage.title.replace( /-/g, ' ' ).toLowerCase() ) {
+				bold = '**';
+				hasExactMatch = true;
+				if ( query.redirects?.[0] ) {
+					if ( query.redirects[0].tofragment && !result.sectiontitle ) {
+						result.sectiontitle = query.redirects[0].tofragment;
 					}
+					if ( !result.redirecttitle ) result.redirecttitle = query.redirects[0].from;
 				}
-				text += bold;
-				text += '[' + result.title + '](' + wiki.toLink(result.title, '', '', true) + ')';
+			}
+			text += bold;
+			if ( msg.showEmbed() && !noEmbed ) {
+				text += '[' + escapeFormatting(result.title) + '](' + wiki.toLink(result.title, '', '', true) + ')';
 				if ( result.sectiontitle ) {
-					text += ' § [' + result.sectiontitle + '](' + wiki.toLink(result.title, '', result.sectiontitle, true) + ')';
+					text += ' § [' + escapeFormatting(result.sectiontitle) + '](' + wiki.toLink(result.title, '', result.sectiontitle, true) + ')';
 				}
 				if ( result.redirecttitle ) {
-					text += ' (⤷ [' + result.redirecttitle + '](' + wiki.toLink(result.redirecttitle, 'redirect=no', '', true) + '))';
+					text += ' (⤷ [' + escapeFormatting(result.redirecttitle) + '](' + wiki.toLink(result.redirecttitle, 'redirect=no', '', true) + '))';
 				}
-				text += bold;
-				description.push( text );
-			} );
-			if ( !hasExactMatch ) {
-				if ( query.interwiki?.[0] ) {
-					let text = '• **⤷ ';
-					text += '__[' + query.interwiki[0].title + '](' + query.interwiki[0].url.replace( /[()]/g, '\\$&' ) + ')__';
+			}
+			else {
+				text += '<' + wiki.toLink(result.title) + '>';
+				if ( result.sectiontitle ) text += ' § ' + escapeFormatting(result.sectiontitle);
+				if ( result.redirecttitle ) text += ' (⤷ ' + escapeFormatting(result.redirecttitle) + ')';
+			}
+			text += bold;
+			description.push( text );
+		} );
+		if ( !hasExactMatch ) {
+			if ( query.interwiki?.[0] ) {
+				let text = '• **⤷ ';
+				if ( msg.showEmbed() && !noEmbed ) {
+					text += '__[' + escapeFormatting(query.interwiki[0].title) + '](' + query.interwiki[0].url.replace( /[()]/g, '\\$&' ) + ')__';
 					if ( query.redirects?.[0] ) {
-						text += ' (⤷ [' + query.redirects[0].from + '](' + wiki.toLink(query.redirects[0].from, 'redirect=no', '', true) + '))';
+						text += ' (⤷ [' + escapeFormatting(query.redirects[0].from) + '](' + wiki.toLink(query.redirects[0].from, 'redirect=no', '', true) + '))';
 					}
-					text += '**';
-					description.unshift( text );
 				}
-				else if ( querypage.invalid === undefined && ( querypage.missing === undefined || querypage.known !== undefined ) ) {
-					let text = '• **';
-					text += '[' + querypage.title + '](' + wiki.toLink(querypage.title, '', '', true) + ')';
+				else {
+					text += '__<' + query.interwiki[0].url.replace( /[()]/g, '\\$&' ) + '>__';
+					if ( query.redirects?.[0] ) text += ' (⤷ ' + escapeFormatting(query.redirects[0].from) + ')';
+				}
+				text += '**';
+				description.unshift( text );
+			}
+			else if ( querypage.invalid === undefined && ( querypage.missing === undefined || querypage.known !== undefined ) ) {
+				let text = '• **';
+				if ( msg.showEmbed() && !noEmbed ) {
+					text += '[' + escapeFormatting(querypage.title) + '](' + wiki.toLink(querypage.title, '', '', true) + ')';
 					if ( query.redirects?.[0] ) {
 						if ( query.redirects[0].tofragment ) {
-							text += ' § [' + query.redirects[0].tofragment + '](' + wiki.toLink(querypage.title, '', query.redirects[0].tofragment, true) + ')';
+							text += ' § [' + escapeFormatting(query.redirects[0].tofragment) + '](' + wiki.toLink(querypage.title, '', query.redirects[0].tofragment, true) + ')';
 						}
-						text += ' (⤷ [' + query.redirects[0].from + '](' + wiki.toLink(query.redirects[0].from, 'redirect=no', '', true) + '))';
+						text += ' (⤷ [' + escapeFormatting(query.redirects[0].from) + '](' + wiki.toLink(query.redirects[0].from, 'redirect=no', '', true) + '))';
 					}
-					text += '**';
-					description.unshift( text );
 				}
+				else {
+					text += '<' + wiki.toLink(querypage.title) + '>';
+					if ( query.redirects?.[0] ) {
+						if ( query.redirects[0].tofragment ) text += ' § ' + escapeFormatting(query.redirects[0].tofragment);
+						text += ' (⤷ ' + escapeFormatting(query.redirects[0].from) + ')';
+					}
+				}
+				text += '**';
+				description.unshift( text );
 			}
-			if ( body.query.searchinfo ) {
-				embed.setFooter( lang.get('search.results', body.query.searchinfo.totalhits.toLocaleString(lang.get('dateformat')), body.query.searchinfo.totalhits) );
-			}
+		}
+		var footer = '';
+		if ( body.query.searchinfo ) {
+			footer = lang.get('search.results', body.query.searchinfo.totalhits.toLocaleString(lang.get('dateformat')), body.query.searchinfo.totalhits);
+		}
+		if ( msg.showEmbed() && !noEmbed ) {
+			embed.setDescription( Util.splitMessage( description.join('\n') )[0] );
+			if ( footer ) embed.setFooter( footer );
+		}
+		else {
+			if ( description.length ) resultText += '\n' + Util.splitMessage( description.join('\n'), {maxLength: 1995 - resultText.length - footer.length} )[0];
+			if ( footer ) resultText += '\n' + footer;
 		}
 	}, error => {
 		console.log( '- Error while getting the search results.' + error );
-	} ).finally( () => {
-		embed.setDescription( Util.splitMessage( description.join('\n') )[0] );
-		msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
+	} ).then( () => {
+		msg.sendChannel( {content: '🔍 ' + spoiler + resultText + spoiler, embeds: [embed]} );
 		
 		if ( reaction ) reaction.removeEmoji();
 	} );

+ 34 - 32
cmds/wiki/user.js

@@ -5,7 +5,7 @@ const parse_page = require('../../functions/parse_page.js');
 const logging = require('../../util/logging.js');
 const extract_desc = require('../../util/extract_desc.js');
 const {timeoptions, usergroups} = require('../../util/default.json');
-const {toMarkdown, toPlaintext, htmlToDiscord, escapeFormatting} = require('../../util/functions.js');
+const {got, toMarkdown, toPlaintext, htmlToDiscord, escapeFormatting} = require('../../util/functions.js');
 
 /**
  * Processes a Gamepedia user.
@@ -20,10 +20,11 @@ const {toMarkdown, toPlaintext, htmlToDiscord, escapeFormatting} = require('../.
  * @param {String} contribs - The contributions page on the wiki.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  * @param {String} spoiler - If the response is in a spoiler.
+ * @param {Boolean} noEmbed - If the response should be without an embed.
  */
-function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragment, querypage, contribs, reaction, spoiler) {
+function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragment, querypage, contribs, reaction, spoiler, noEmbed) {
 	if ( /^(?:(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{2})?|(?:[\dA-F]{1,4}:){7}[\dA-F]{1,4}(?:\/\d{2,3})?)$/.test(username) ) return got.get( wiki + 'api.php?action=query&meta=siteinfo&siprop=general&list=blocks&bkprop=user|by|timestamp|expiry|reason&bkip=' + encodeURIComponent( username ) + '&format=json' ).then( response => {
-		logging(wiki, msg.guild?.id, 'user', 'ip');
+		logging(wiki, msg.guildId, 'user', 'ip');
 		var body = response.body;
 		if ( body && body.warnings ) log_warn(body.warnings);
 		if ( response.statusCode !== 200 || !body || body.batchcomplete === undefined || !body.query || !body.query.blocks || fragment ) {
@@ -61,7 +62,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 						embed.setThumbnail( wiki.toLink('Special:FilePath/' + querypage.pageprops.page_image_free, {version:Date.now()}) );
 					}
 					
-					return parse_page(lang, msg, spoiler + '<' + pagelink + '>' + spoiler, embed, wiki, reaction, querypage, new URL(body.query.general.logo, wiki).href, fragment, pagelink);
+					return parse_page(lang, msg, spoiler + '<' + pagelink + '>' + spoiler, ( noEmbed ? null : embed ), wiki, reaction, querypage, new URL(body.query.general.logo, wiki).href, fragment, pagelink);
 				}
 			}
 			else {
@@ -140,7 +141,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 			}
 			if ( isBlocked ) {
 				var text = 'user.block.' + ( isIndef ? 'indef_' : '' ) + ( block.reason ? 'text' : 'noreason' );
-				if ( msg.showEmbed() ) {
+				if ( msg.showEmbed() && !noEmbed ) {
 					text = lang.get(text, dateformat.format(blockedtimestamp), blockduration, blockexpiry, '[' + escapeFormatting(block.by) + '](' + wiki.toLink('User:' + block.by, '', '', true) + ')', toMarkdown(block.reason, wiki));
 				}
 				else {
@@ -191,7 +192,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 			var pagelink = wiki.toLink(namespace + username, querystring, fragment);
 			var text = '<' + pagelink + '>';
 			var embed = null;
-			if ( msg.showEmbed() ) {
+			if ( msg.showEmbed() && !noEmbed ) {
 				embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( username ).setURL( pagelink ).addField( editcount[0], '[' + editcount[1] + '](' + wiki.toLink(contribs + username, '', '', true) + ')' );
 				embed.forceTitle = true;
 				if ( querypage.pageprops && querypage.pageprops.description ) {
@@ -214,13 +215,13 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 				} );
 			}
 			
-			if ( msg.channel.isGuild() && patreons[msg.guild?.id] && wiki.isFandom() ) {
-				if ( msg.showEmbed() ) embed.addField( '\u200b', '<a:loading:641343250661113886> **' + lang.get('user.info.loading') + '**' );
+			if ( msg.channel.isGuild() && patreons[msg.guildId] && wiki.isFandom() ) {
+				if ( msg.showEmbed() && !noEmbed ) embed.addField( '\u200b', '<a:loading:641343250661113886> **' + lang.get('user.info.loading') + '**' );
 				else text += '\n\n<a:loading:641343250661113886> **' + lang.get('user.info.loading') + '**';
 
-				parse_page(lang, msg, spoiler + text + spoiler, embed, wiki, reaction, querypage).then( message => global_block(lang, message, username, text, embed, wiki, spoiler) );
+				parse_page(lang, msg, spoiler + text + spoiler, ( noEmbed ? null : embed ), wiki, reaction, querypage).then( message => global_block(lang, message, username, text, ( noEmbed ? null : embed ), wiki, spoiler) );
 			}
-			else parse_page(lang, msg, spoiler + text + spoiler, embed, wiki, reaction, querypage);
+			else parse_page(lang, msg, spoiler + text + spoiler, ( noEmbed ? null : embed ), wiki, reaction, querypage);
 		}, error => {
 			if ( rangeprefix && !username.includes( '/' ) ) username = rangeprefix;
 			console.log( '- Error while getting the search results: ' + error );
@@ -228,14 +229,14 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 			if ( reaction ) reaction.removeEmoji();
 		} );
 	}, error => {
-		logging(wiki, msg.guild?.id, 'user', 'ip');
+		logging(wiki, msg.guildId, 'user', 'ip');
 		console.log( '- Error while getting the search results: ' + error );
 		msg.sendChannelError( spoiler + '<' + wiki.toLink(( querypage.noRedirect ? namespace : contribs ) + username, querystring, fragment) + '>' + spoiler );
 		
 		if ( reaction ) reaction.removeEmoji();
 	} );
 
-	logging(wiki, msg.guild?.id, 'user');
+	logging(wiki, msg.guildId, 'user');
 	got.get( wiki + 'api.php?action=query&meta=siteinfo' + ( wiki.hasCentralAuth() ? '|globaluserinfo&guiprop=groups|editcount|merged&guiuser=' + encodeURIComponent( username ) + '&' : '' ) + '&siprop=general&prop=revisions&rvprop=content|user&rvslots=main&titles=User:' + encodeURIComponent( username ) + '/Discord&list=users&usprop=blockinfo|groups|editcount|registration|gender&ususers=' + encodeURIComponent( username ) + '&format=json' ).then( response => {
 		var body = response.body;
 		if ( body && body.warnings ) log_warn(body.warnings);
@@ -277,7 +278,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 				}
 				else embed.setThumbnail( new URL(body.query.general.logo, wiki).href );
 				
-				return parse_page(lang, msg, spoiler + '<' + pagelink + '>' + spoiler, embed, wiki, reaction, querypage, new URL(body.query.general.logo, wiki).href, fragment, pagelink);
+				return parse_page(lang, msg, spoiler + '<' + pagelink + '>' + spoiler, ( noEmbed ? null : embed ), wiki, reaction, querypage, new URL(body.query.general.logo, wiki).href, fragment, pagelink);
 			}
 			
 			if ( reaction ) reaction.removeEmoji();
@@ -306,7 +307,8 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 				timeZone: 'UTC'
 			}, timeoptions));
 		}
-		var registration = [lang.get('user.info.registration'), dateformat.format(new Date(queryuser.registration))];
+		let registrationDate = new Date(queryuser.registration);
+		var registration = [lang.get('user.info.registration'), dateformat.format(registrationDate), '<t:' + Math.trunc(registrationDate.getTime() / 1000) + ':R>'];
 		var editcount = [lang.get('user.info.editcount'), queryuser.editcount.toLocaleString(lang.get('dateformat'))];
 		var groups = queryuser.groups.filter( group => !usergroups.ignored.includes( group ) );
 		var globalgroups = [];
@@ -334,7 +336,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 			console.log( '- Error while getting the group names: ' + error );
 		} ).finally( () => {
 			var group = [lang.get('user.info.group', ( groups.filter( usergroup => {
-				return !['autoconfirmed', 'user'].includes( usergroup )
+				return !['autoconfirmed', 'emailconfirmed', 'user'].includes( usergroup )
 			} ).length || 1 ))];
 			for ( var i = 0; i < usergroups.sorted.length; i++ ) {
 				let usergroup = usergroups.sorted[i];
@@ -345,7 +347,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 						return groupnames[groups.indexOf(customgroup)];
 					} ));
 				}
-				else if ( groups.includes( usergroup ) && ( group.length === 1 || !['autoconfirmed', 'user'].includes( usergroup ) ) ) {
+				else if ( groups.includes( usergroup ) && ( group.length === 1 || !['autoconfirmed', 'emailconfirmed', 'user'].includes( usergroup ) ) ) {
 					group.push(groupnames[groups.indexOf(usergroup)]);
 				}
 			}
@@ -421,7 +423,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 			}
 			if ( isBlocked ) {
 				var blockedtext = 'user.block.' + ( isIndef ? 'indef_' : '' ) + ( queryuser.blockreason ? 'text' : 'noreason' );
-				if ( msg.showEmbed() ) {
+				if ( msg.showEmbed() && !noEmbed ) {
 					blockedtext = lang.get(blockedtext, ( blockedtimestamp ? dateformat.format(blockedtimestamp) : 'Invalid Date' ), blockduration, blockexpiry, '[' + escapeFormatting(queryuser.blockedby) + '](' + wiki.toLink('User:' + queryuser.blockedby, '', '', true) + ')', toMarkdown(queryuser.blockreason, wiki));
 				}
 				else {
@@ -435,7 +437,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 			var pagelink = wiki.toLink(namespace + username, querystring, fragment);
 			var text = '<' + pagelink + '>';
 			var embed = null;
-			if ( msg.showEmbed() ) {
+			if ( msg.showEmbed() && !noEmbed ) {
 				embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( escapeFormatting(username) ).setURL( pagelink ).addField( editcount[0], '[' + editcount[1] + '](' + wiki.toLink(contribs + username, '', '', true) + ')', true );
 				embed.forceTitle = true;
 				if ( wiki.hasCentralAuth() ) {
@@ -445,7 +447,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 				if ( globalgroup.length > 1 ) {
 					embed.addField( globalgroup[0], globalgroup.slice(1).join(',\n'), true );
 				}
-				embed.addField( gender[0], gender[1], true ).addField( registration[0], registration[1], true );
+				embed.addField( gender[0], gender[1], true ).addField( registration[0], registration[1] + '\n' + registration[2], true );
 				
 				if ( querypage.pageprops && querypage.pageprops.description ) {
 					var description = htmlToDiscord( querypage.pageprops.description );
@@ -473,7 +475,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 					console.log( '- ' + presponse.statusCode + ': Error while getting the user profile.' );
 					return;
 				}
-				if ( msg.showEmbed() ) {
+				if ( msg.showEmbed() && !noEmbed ) {
 					embed.spliceFields(0, 1, {
 						name: editcount[0],
 						value: '[' + pbody.userData.localEdits.toLocaleString(lang.get('dateformat')) + '](' + wiki.toLink(contribs + username, '', '', true) + ')',
@@ -523,7 +525,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 						var discordname = [lang.get('user.info.discord'),discord];
 						if ( discordmember ) discordname[1] = discordmember.toString();
 						
-						if ( msg.showEmbed() ) embed.addField( discordname[0], discordname[1], true );
+						if ( msg.showEmbed() && !noEmbed ) embed.addField( discordname[0], discordname[1], true );
 						else text += '\n' + discordname.join(' ');
 					}
 					if ( cpbody.profile['favwiki'] ) {
@@ -533,7 +535,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 								console.log( '- ' + favresponse.statusCode + ': Error while getting the favorite wiki: ' + ( favbody && ( favbody.error && favbody.error.info || favbody.errormsg ) ) );
 								return;
 							}
-							if ( msg.showEmbed() ) embed.addField( lang.get('user.info.favwiki'), '[' + favbody.data.wiki_name_display + '](<' + favbody.data.wiki_url + '>)', true );
+							if ( msg.showEmbed() && !noEmbed ) embed.addField( lang.get('user.info.favwiki'), '[' + favbody.data.wiki_name_display + '](<' + favbody.data.wiki_url + '>)', true );
 							else text += '\n' + lang.get('user.info.favwiki') + ' <' + favbody.data.wiki_url + '>';
 						}, error => {
 							console.log( '- Error while getting the favorite wiki: ' + error );
@@ -551,24 +553,24 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 					let discordname = [lang.get('user.info.discord'),discord];
 					if ( discordmember ) discordname[1] = discordmember.toString();
 					
-					if ( msg.showEmbed() ) embed.addField( discordname[0], discordname[1], true );
+					if ( msg.showEmbed() && !noEmbed ) embed.addField( discordname[0], discordname[1], true );
 					else text += '\n' + discordname.join(' ');
 				}
 			}, error => {
 				console.log( '- Error while getting the user profile: ' + error );
 			} ).finally( () => {
 				if ( isBlocked ) {
-					if ( msg.showEmbed() ) embed.addField( block.header, block.text );
+					if ( msg.showEmbed() && !noEmbed ) embed.addField( block.header, block.text );
 					else text += '\n\n**' + block.header + '**\n' + block.text;
 				}
 				
-				if ( msg.channel.isGuild() && patreons[msg.guild?.id] ) {
-					if ( msg.showEmbed() ) embed.addField( '\u200b', '<a:loading:641343250661113886> **' + lang.get('user.info.loading') + '**' );
+				if ( msg.channel.isGuild() && patreons[msg.guildId] ) {
+					if ( msg.showEmbed() && !noEmbed ) embed.addField( '\u200b', '<a:loading:641343250661113886> **' + lang.get('user.info.loading') + '**' );
 					else text += '\n\n<a:loading:641343250661113886> **' + lang.get('user.info.loading') + '**';
 					
-					parse_page(lang, msg, spoiler + text + spoiler, embed, wiki, reaction, querypage).then( message => global_block(lang, message, username, text, embed, wiki, spoiler, queryuser.gender) );
+					parse_page(lang, msg, spoiler + text + spoiler, ( noEmbed ? null : embed ), wiki, reaction, querypage).then( message => global_block(lang, message, username, text, ( noEmbed ? null : embed ), wiki, spoiler, queryuser.gender) );
 				}
-				else parse_page(lang, msg, spoiler + text + spoiler, embed, wiki, reaction, querypage);
+				else parse_page(lang, msg, spoiler + text + spoiler, ( noEmbed ? null : embed ), wiki, reaction, querypage);
 			} );
 			if ( body.query.pages ) {
 				let revision = Object.values(body.query.pages)[0]?.revisions?.[0];
@@ -581,20 +583,20 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 					let discordname = [lang.get('user.info.discord'),escapeFormatting(discord)];
 					if ( discordmember ) discordname[1] = discordmember.toString();
 					
-					if ( msg.showEmbed() ) embed.addField( discordname[0], discordname[1], true );
+					if ( msg.showEmbed() && !noEmbed ) embed.addField( discordname[0], discordname[1], true );
 					else text += '\n' + discordname.join(' ');
 				}
 			}
 			if ( isBlocked ) {
-				if ( msg.showEmbed() ) embed.addField( block.header, block.text );
+				if ( msg.showEmbed() && !noEmbed ) embed.addField( block.header, block.text );
 				else text += '\n\n**' + block.header + '**\n' + block.text;
 			}
 			if ( wiki.hasCentralAuth() && body.query.globaluserinfo.locked !== undefined ) {
-				if ( msg.showEmbed() ) embed.addField( '\u200b', '**' + lang.get('user.gblock.header', escapeFormatting(username), gender) + '**' );
+				if ( msg.showEmbed() && !noEmbed ) embed.addField( '\u200b', '**' + lang.get('user.gblock.header', escapeFormatting(username), gender) + '**' );
 				else text += '\n\n**' + lang.get('user.gblock.header', escapeFormatting(username), gender) + '**';
 			}
 			
-			parse_page(lang, msg, spoiler + text + spoiler, embed, wiki, reaction, querypage);
+			parse_page(lang, msg, spoiler + text + spoiler, ( noEmbed ? null : embed ), wiki, reaction, querypage);
 		} );
 	}, error => {
 		console.log( '- Error while getting the search results: ' + error );

+ 45 - 7
dashboard/guilds.js

@@ -2,9 +2,10 @@ const cheerio = require('cheerio');
 const {defaultPermissions} = require('../util/default.json');
 const Lang = require('./i18n.js');
 const allLangs = Lang.allLangs().names;
-const {oauth, settingsData, addWidgets, createNotice} = require('./util.js');
+const {oauth, enabledOAuth2, settingsData, addWidgets, createNotice} = require('./util.js');
 
 const forms = {
+	user: require('./user.js').get,
 	settings: require('./settings.js').get,
 	verification: require('./verification.js').get,
 	rcscript: require('./rcscript.js').get,
@@ -24,7 +25,7 @@ const file = require('fs').readFileSync('./dashboard/index.html');
  * @param {String[]} [actionArgs] - The arguments for the action
  */
 function dashboard_guilds(res, dashboardLang, theme, userSession, reqURL, action, actionArgs) {
-	reqURL.pathname = reqURL.pathname.replace( /^(\/(?:guild\/\d+(?:\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new|notice))?)?)?)(?:\/.*)?$/, '$1' );
+	reqURL.pathname = reqURL.pathname.replace( /^(\/(?:user|guild\/\d+(?:\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new|notice))?)?)?)(?:\/.*)?$/, '$1' );
 	var args = reqURL.pathname.split('/');
 	var settings = settingsData.get(userSession.user_id);
 	if ( reqURL.searchParams.get('owner') && process.env.owner.split('|').includes(userSession.user_id) ) {
@@ -56,7 +57,15 @@ function dashboard_guilds(res, dashboardLang, theme, userSession, reqURL, action
 	$('#support span').text(dashboardLang.get('general.support'));
 	$('#logout').attr('alt', dashboardLang.get('general.logout'));
 	if ( process.env.READONLY ) createNotice($, 'readonly', dashboardLang);
-	if ( action ) createNotice($, action, dashboardLang, actionArgs);
+	if ( action ) {
+		if ( action === 'oauthother' && !actionArgs ) actionArgs = [
+			oauth.generateAuthUrl( {
+				scope: ['identify', 'guilds'],
+				prompt: 'consent', state: userSession.state
+			} )
+		];
+		createNotice($, action, dashboardLang, actionArgs);
+	}
 	$('head').append(
 		$('<script>').text(`history.replaceState(null, null, '${reqURL.pathname}');`)
 	);
@@ -98,6 +107,27 @@ function dashboard_guilds(res, dashboardLang, theme, userSession, reqURL, action
 		} );
 	}
 
+	if ( args[1] === 'user' && enabledOAuth2.length ) {
+		$('head title').text(`${settings.user.username} #${settings.user.discriminator} – ` + $('head title').text());
+		$('#channellist').empty();
+		$('#channellist').append(
+			$('<a class="channel channel-header">').attr('href', '/').append(
+				$('<img>').attr('src', '/src/settings.svg'),
+				$('<div>').text(dashboardLang.get('selector.title'))
+			).attr('title', dashboardLang.get('selector.title')),
+			$('<a class="channel channel-header selected">').attr('href', '/user').append(
+				$('<img>').attr('src', '/src/settings.svg'),
+				$('<div>').text(dashboardLang.get('selector.user'))
+			).attr('title', dashboardLang.get('selector.user')),
+			...enabledOAuth2.map( oauthSite => {
+				return $('<a class="channel">').attr('href', '#oauth-' + oauthSite.id).append(
+					$('<img>').attr('src', '/src/channel.svg'),
+					$('<div>').text(oauthSite.name)
+				).attr('title', oauthSite.name);
+			} )
+		)
+		return forms.user(res, $, settings.user, dashboardLang);
+	}
 	let id = args[2];
 	if ( id ) $(`.guild#${id}`).addClass('selected');
 	if ( settings.guilds.isMember.has(id) ) {
@@ -123,8 +153,8 @@ function dashboard_guilds(res, dashboardLang, theme, userSession, reqURL, action
 		res.setHeader('Set-Cookie', [`guild="${guild.id}/settings"; SameSite=Lax; Path=/`]);
 		let url = oauth.generateAuthUrl( {
 			scope: ['identify', 'guilds', 'bot', 'applications.commands'],
-			permissions: defaultPermissions,
-			guildId: guild.id, state: userSession.state
+			permissions: defaultPermissions, guildId: guild.id,
+			disableGuildSelect: true, state: userSession.state
 		} );
 		$('#channellist').empty();
 		$('<a class="channel channel-header">').attr('href', url).append(
@@ -164,6 +194,10 @@ function dashboard_guilds(res, dashboardLang, theme, userSession, reqURL, action
 	else {
 		$('head title').text(dashboardLang.get('selector.title') + ' – ' + $('head title').text());
 		$('#channellist').empty();
+		$('<a class="channel channel-header selected">').attr('href', '/').append(
+			$('<img>').attr('src', '/src/settings.svg'),
+			$('<div>').text(dashboardLang.get('selector.title'))
+		).attr('title', dashboardLang.get('selector.title')).appendTo('#channellist');
 		$('<p>').html(dashboardLang.get('selector.desc', true, $('<code>'))).appendTo('#text .description');
 		if ( settings.guilds.isMember.size ) {
 			$('<h2 id="with-wikibot">').text(dashboardLang.get('selector.with')).appendTo('#text');
@@ -202,8 +236,8 @@ function dashboard_guilds(res, dashboardLang, theme, userSession, reqURL, action
 				scope: ['identify', 'guilds'],
 				prompt: 'consent', state: userSession.state
 			} );
-			$('<a class="channel channel-header">').attr('href', url).append(
-				$('<img>').attr('src', '/src/settings.svg'),
+			$('<a class="channel">').attr('href', url).append(
+				$('<img>').attr('src', '/src/channel.svg'),
 				$('<div>').text(dashboardLang.get('selector.switch'))
 			).attr('title', dashboardLang.get('selector.switch')).appendTo('#channellist');
 			$('#text .description').append(
@@ -213,6 +247,10 @@ function dashboard_guilds(res, dashboardLang, theme, userSession, reqURL, action
 				)
 			);
 		}
+		if ( enabledOAuth2.length ) $('<a class="channel channel-header">').attr('href', '/user').append(
+			$('<img>').attr('src', '/src/settings.svg'),
+			$('<div>').text(dashboardLang.get('selector.user'))
+		).attr('title', dashboardLang.get('selector.user')).appendTo('#channellist');
 		addWidgets($, dashboardLang);
 	}
 	let body = $.html();

+ 37 - 0
dashboard/i18n/en.json

@@ -102,6 +102,27 @@
             "text": "The restrictions for the $1 command can't be changed without verifications being set up.",
             "title": "Verifications are not set up!"
         },
+        "oauth": {
+            "text": "Your wiki account has been successfully connected with Wiki-Bot.",
+            "title": "Account successfully connected!"
+        },
+        "oauthfail": {
+            "text": "Your wiki account could not be connected, please try again.",
+            "title": "Connection failed!"
+        },
+        "oauthlogin": {
+            "text": "Please log in if you don't want Wiki-Bot to remember your wiki account.",
+            "title": "Your connected wiki account has been saved!"
+        },
+        "oauthother": {
+            "note": "Switch accounts.",
+            "text": "Please note that your wiki account got connected with a different Discord account than the one you are currently logged in as.",
+            "title": "Account successfully connected!"
+        },
+        "oauthverify": {
+            "text": "Your wiki account has been successfully verified.",
+            "title": "Account successfully verified!"
+        },
         "readonly": {
             "text": "You can currently only view your settings, but not change them.",
             "title": "Read-only database!"
@@ -144,6 +165,21 @@
             "title": "Wiki is blocked!"
         }
     },
+    "oauth": {
+        "desc": "These are your OAuth2 settings for connecting wiki accounts:",
+        "failed": "Failed to load the OAuth2 settings!",
+        "form": {
+            "connect": "Connect account.",
+            "connected": "Connected",
+            "current": "Current Status:",
+            "default": "Wiki Account Connections",
+            "disable": "Don't remember my account!",
+            "disabled": "Disabled",
+            "disconnect": "Disconnect account.",
+            "enable": "Remember my account.",
+            "unconnected": "Unconnected"
+        }
+    },
     "rcscript": {
         "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>",
@@ -175,6 +211,7 @@
         "none": "You currently don't have the [Manage Server]($1) permission on any servers, are you logged into the correct account?",
         "switch": "Switch Accounts",
         "title": "Server Selector",
+        "user": "Wiki Accounts",
         "with": "Servers with Wiki-Bot",
         "without": "Servers without Wiki-Bot"
     },

+ 18 - 2
dashboard/i18n/es.json

@@ -30,8 +30,12 @@
         "welcome": "<h2> Bienvenido al Panel de Control de Wiki-Bot. </h2> \n<p> Wiki-Bot es un bot de Discord creado para unir servidores de Discord y wikis de MediaWiki. Ayuda a vincular páginas wiki, verificar usuarios wiki, informar sobre los últimos cambios en el wiki y más. <a href=\"https://wiki.wikibot.de/wiki/Wiki-Bot_Wiki\" target=\"_blank\"> [Más información] </a> </p>\n<p> Aquí puedes cambiar diferentes configuraciones de bot para los servidores en los que tienes el permiso de administrar el servidor. Para comenzar, debes autenticar tu cuenta de Discord, lo puedes hacer con este botón: </p>"
     },
     "indexjs": {
+        "avatar": {
+            "content_type": "El enlace proporcionado tiene el tipo de contenido $1, pero solo se permiten los siguientes tipos de contenido:",
+            "invalid_url": "La URL no se pudo resolver en un archivo de imagen válido."
+        },
         "invalid": {
-            "note_http": "¡El sitio web proporcionado no utiliza HTTPS!",
+            "note_http": "¡El sitio web proporcionado no tiene un certificado TLS/SSL válido! Por razones de seguridad, solo se admiten wikis que utilizan HTTPS.",
             "note_private": "¡El wiki proporcionado es privado!",
             "note_timeout": "¡El enlace proporcionado tardó demasiado en responder!",
             "text": "¡La URL no se pudo convertir a un sitio válido de MediaWiki!",
@@ -115,7 +119,7 @@
             "title": "¡Se guardó la configuración!"
         },
         "savefail": {
-            "note_http": "¡El sitio web proporcionado no utiliza HTTPS!",
+            "note_http": "¡El sitio web proporcionado no tiene un certificado TLS/SSL válido! Por razones de seguridad, solo se admiten wikis que utilizan HTTPS.",
             "note_private": "¡El wiki proporcionado es privado!",
             "note_timeout": "¡El enlace proporcionado tardó demasiado en responder!",
             "text": "No se pudo guardar la configuración, por favor vuelve a intentarlo.",
@@ -129,6 +133,11 @@
             "text": "Inicia sesión para cambiar cualquier configuración.",
             "title": "¡No has iniciado sesión!"
         },
+        "webhookfail": {
+            "note": "¡El webhook de Discord no se pudo cambiar!",
+            "text": "La configuración solo se ha actualizado parcialmente.",
+            "title": "¡Configuración parcialmente guardada!"
+        },
         "wikiblocked": {
             "note": "Motivo:",
             "text": "Se ha bloqueado la adición de $1 como webhook de cambios recientes.",
@@ -139,6 +148,8 @@
         "desc": "Estos son los webhooks de cambios recientes para $1:",
         "explanation": "<h2>Webhook de Cambios Recientes</h2>\n<p>Wiki-Bot puede ejecutar un webhook de cambios recientes basado en <a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a>. Los cambios recientes se pueden mostrar en mensajes de texto compactos con enlaces en la misma línea o mensajes adjuntos con etiquetas de edición y cambios de categoría.</p>\n<p>Requisitos para agregar un webhook de cambios recientes:</p>\n<ul>\n<li>El wiki necesita ejecutarse en <a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a> o superior.</li>\n<li>El mensaje del sistema <code class=\"user-select\">MediaWiki:Custom-RcGcDw</code> debe configurarse con el ID del servidor Discord <code class=\"user-select\" id=\"server-id\"></code>.</li>\n</ul>",
         "form": {
+            "avatar": "Avatar del webhook:",
+            "avatar_preview": "Vista previa",
             "channel": "Canal:",
             "confirm": "¿Realmente deseas eliminar el webhook de cambios recientes?",
             "display": "Modo de visualización:",
@@ -150,6 +161,7 @@
             "feeds": "Cambios basados en feeds:",
             "feeds_only": "Solo cambios basados en feeds:",
             "lang": "Idioma:",
+            "name": "Nombre del webhook:",
             "new": "Nuevo Webhook de Cambios Recientes",
             "select_channel": "-- Selecciona un Canal --",
             "wiki": "Wiki:",
@@ -210,6 +222,8 @@
             "confirm": "¿Realmente deseas eliminar la verificación?",
             "editcount": "Número mínimo ediciones:",
             "entry": "Verificación #$1",
+            "flag_logall": "Registrar verificaciones fallidas:",
+            "flag_private": "Respuestas de comando privadas:",
             "logging": "Canal de registro:",
             "match": "Aviso de requisitos faltantes:",
             "match_placeholder": "El Markdown de texto en las etiquetas de Discord coinciden, pero que no cumplen con ningún requisito para los roles.",
@@ -223,6 +237,8 @@
             "postcount_or": "Requerir solo número de ediciones o solo número de publicaciones.",
             "rename": "Cambiar el nombre de los usuarios:",
             "role": "Rol:",
+            "role_add": "Agregar",
+            "role_remove": "Eliminar",
             "select_channel": "-- Selecciona un Canal --",
             "select_role": "-- Selecciona un Rol --",
             "success": "Aviso de operación correcta:",

+ 12 - 0
dashboard/i18n/hi.json

@@ -30,6 +30,10 @@
         "welcome": "<h2>विकी-बॉट डैशबोर्ड पर आपका स्वागत है।</h2>\n<p>विकी-बॉट एक डिस्कॉर्ड बॉट है जिसे डिस्कॉर्ड सर्वर मीडियाविकि साइटों को साथ लाने के लिए बनाया गया है। यह विकी पृष्ठों को साथ लाने, विकी सदस्यों को वेरीफाई करने, विकी पर नए बदलावों की खबर देने और बहुत कुछ के बारे में मदद कर सकता है। <a href=\"https://wiki.wikibot.de/wiki/Wiki-Bot_Wiki\" target=\"_blank\">[और जानकारी]</a></p>\n<p>यहाँ आप उन सर्वरों के लिए बॉट के सेटिंग्स बदल सकते हैं जिनमें आपके पास Manage Server अनुमति है। शुरू करने के लिए आपको अपने डिस्कॉर्ड अकाउंट को यहाँ जोड़ना होगा जो आप इस बटन से कर सकते हैं:</p>"
     },
     "indexjs": {
+        "avatar": {
+            "content_type": "दिए लिंक में कंटेंट प्रकार $1 है, पर सिर्फ ये ही प्रकार स्वीकृत हैं:",
+            "invalid_url": "URL पर कोई स्वीकृत चित्र फाइल नहीं मिला।"
+        },
         "invalid": {
             "note_http": "इस वेबसाइट के पास स्वीकृत TLS/SSL प्रमाणपत्र नहीं है! सुरक्षा के कारण सिर्फ HTTPS का इस्तेमाल करने वाले विकियाँ ही समर्थित हैं।",
             "note_private": "यह विकि व्यक्तिगत है!",
@@ -129,6 +133,11 @@
             "text": "किसी सेटिंग को बदलने से पहले कृपया लॉगइन करें।",
             "title": "लॉग्डइन नही हैं!"
         },
+        "webhookfail": {
+            "note": "डिस्कॉर्ड वेबहुक को बदला न जा सका!",
+            "text": "सेटिंग्स को पूरी तरह से बदला नहीं गया है।",
+            "title": "सेटिंग्स को पूरी तरह से सहेजा नहीं गया है!"
+        },
         "wikiblocked": {
             "note": "कारण:",
             "text": "$1 को एक रीसेंट चेंजेस वेबहुक के तौर पर लगाए जाने से ब्लॉक कर दिया गया है।",
@@ -139,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\">मीडियाविकि १.३०</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": "डिस्प्ले मोड:",
@@ -150,6 +161,7 @@
             "feeds": "फीड-आधारित बदलाव:",
             "feeds_only": "सिर्फ फीड-आधारित बदलाव:",
             "lang": "भाषा:",
+            "name": "वेबहुक का नाम:",
             "new": "नया रीसेंट चेंजेस वेबहुक",
             "select_channel": "-- एक चैनल चुनिए --",
             "wiki": "विकी:",

+ 59 - 4
dashboard/i18n/ko.json

@@ -7,6 +7,10 @@
         " "
     ],
     "general": {
+        "botlist": {
+            "text": "봇 리스트에 투표해서 다른 사람들이 Wiki-Bot을 찾도록 도와주세요!",
+            "title": "봇 목록"
+        },
         "delete": "삭제",
         "invite": "Wiki-Bot 초대",
         "language": "언어 변경",
@@ -17,14 +21,21 @@
         "save": "저장",
         "selector": "서버 선택기",
         "settings": "설정",
+        "slash": "슬래시 명령어",
         "support": "지원 서버",
+        "theme-dark": "다크 테마 사용",
+        "theme-light": "밝은 테마 사용",
         "title": "Wiki-Bot 설정",
         "verification": "인증",
         "welcome": "<h2>Wiki-Bot 대시보드에 오신 것을 환영합니다.</h2>\n<p>Wiki-Bot은 디스코드 서버와 미디어위키 위키를 통합하기 위한 디스코드 봇입니다. 위키링크를 연결하고, 위키 사용자 계정을 인증하고, 위키의 최근 바뀜을 전달하는 것 외에 여러 작업을 수행할 수 있습니다. <a href=\"https://wiki.wikibot.de/wiki/Wiki-Bot_Wiki\" target=\"_blank\">[더 많은 정보]</a></p>\n<p>이 사이트에서 서버 관리하기 권한을 가진 서버의 봇 설정을 관리할 수 있습니다. 시작하려면 아래 버튼을 이용하여 디스코드 계정으로 로그인하세요.</p>"
     },
     "indexjs": {
+        "avatar": {
+            "content_type": "제공된 링크는 $1 콘텐츠 타입을 사용하지만, 다음 콘텐츠 타입만이 허용됩니다.",
+            "invalid_url": "URL에서 유효한 이미지 파일을 불러올 수 없었습니다."
+        },
         "invalid": {
-            "note_http": "제공된 웹사이트는 HTTPS를 사용하지 않습니다!",
+            "note_http": "제공된 웹사이트는 유효한 TLS/SSL 인증서를 제공하고 있지 않습니다! 보안을 위해 HTTPS를 이용하는 위키만 추가할 수 있습니다.",
             "note_private": "제공된 위키는 비공개입니다!",
             "note_timeout": "제공된 링크가 너무 오래 응답하지 않습니다!",
             "text": "URL에서 유효한 미디어위키 사이트로 연결할 수 없었습니다!",
@@ -82,6 +93,15 @@
             "text": "서버 설정을 먼저 정의해 주세요.",
             "title": "서버가 설정되지 않았습니다!"
         },
+        "noslash": {
+            "note": "슬래시 명령어를 활성화합니다.",
+            "text": "이 서버에서는 Wiki-Bot의 슬래시 명령어 기능이 활성화되어 있지 않습니다.",
+            "title": "슬래시 명령어가 활성화되어있지 않습니다!"
+        },
+        "noverify": {
+            "text": "인증을 설정하지 않으면 $1 명령어에 대한 제한을 변경할 수 없습니다.",
+            "title": "인증이 설정되어 있지 않습니다!"
+        },
         "readonly": {
             "text": "현재 설정을 볼 수는 있지만, 변경할 수는 없습니다.",
             "title": "데이터베이스 읽기 전용!"
@@ -99,7 +119,7 @@
             "title": "설정 저장됨!"
         },
         "savefail": {
-            "note_http": "제공된 웹사이트는 HTTPS를 사용하지 않습니다!",
+            "note_http": "제공된 웹사이트에는 유효한 TLS/SSL 인증서를 제공하고 있지 않습니다! 보안을 위해 HTTPS를 이용하는 위키만 추가할 수 있습니다.",
             "note_private": "제공된 위키는 비공개입니다!",
             "note_timeout": "제공된 링크가 너무 오래 응답하지 않습니다!",
             "text": "설정을 저장할 수 없었습니다, 다시 시도해 주세요.",
@@ -113,6 +133,11 @@
             "text": "설정을 변경하기 전에 로그인해 주세요.",
             "title": "로그인하지 않음!"
         },
+        "webhookfail": {
+            "note": "디스코드 웹훅을 변경할 수 없었습니다!",
+            "text": "설정의 일부만 업데이트했습니다.",
+            "title": "설정을 일부만 저장했습니다!"
+        },
         "wikiblocked": {
             "note": "이유:",
             "text": "$1 위키는 최근 바뀜 웹훅으로 추가되지 못하도록 차단되어 있습니다.",
@@ -121,8 +146,10 @@
     },
     "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>위키가 <a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">미디어위키 1.30</a> 또는 그 이상 버전을 지원해야 합니다.</li>\n<li><code>MediaWiki:Custom-RcGcDw</code> 시스템 메시지가 디스코드 서버 ID <code class=\"user-select\" id=\"server-id\"></code>로 설정되어야 합니다.</li>\n</ul>",
+        "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>위키가 <a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">미디어위키 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": "표시 모드:",
@@ -134,6 +161,7 @@
             "feeds": "피드 기반 변경:",
             "feeds_only": "피드 기반 변경 전용:",
             "lang": "언어:",
+            "name": "웹훅 이름:",
             "new": "새 최근 바뀜 웹훅",
             "select_channel": "-- 채널을 선택하세요 --",
             "wiki": "위키:",
@@ -170,16 +198,38 @@
         },
         "new": "새 채널 덮어쓰기"
     },
+    "slash": {
+        "desc": "$1 서버의 슬래시 명령어입니다:",
+        "explanation": "<h2>슬래시 명령어</h2>\n<p>특정 슬래시 명령어의 사용을 역할별로 제한할 수 있습니다. 이곳에서 Wiki-Bot의 슬래시 명령어 사용 제한을 설정할 수 있습니다.</p>",
+        "form": {
+            "add": "추가",
+            "allow": "허용",
+            "default": "기본값",
+            "default_allow": "기본적으로 이 명령어는 누구나 사용할 수 있습니다.",
+            "default_deny": "기본적으로 이 명령어는 아무나 사용할 수 없습니다.",
+            "deny": "거부",
+            "entry": "명령어 $1",
+            "role": "역할:",
+            "select_role": "-- 역할을 선택하세요 --"
+        }
+    },
     "verification": {
         "desc": "$1 서버의 인증입니다:",
+        "explanation": "<h2>사용자 인증</h2>\n<p><code class=\"prefix\">verify &lt;위키 사용자이름&gt;</code>명령어를 이용해, 사용자가 위키 프로필의 디스코드 태그와 일치한다는 것을 증명할 수 있습니다. 사용자 계정이 일치하고, 사용자 인증이 서버에 설정되어 있는 경우, Wiki-Bot이 해당하는 인증 역할을 부여합니다.</p>\n<p>각각의 인증 항목을 통해 인증 조건을 세부설정할 수 있습니다:</p>\n<ul>\n<li><code class=\"prefix\">verify</code> 명령어를 사용할 수 있는 채널.</li>\n<li>인증이 성공하면 얻을 역할.</li>\n<li>인증이 성공하기 위해 필요한 위키 편집 횟수.</li>\n<li>인증이 성공하기 위해 필요한 사용자 권한.</li>\n<li>인증이 성공하기 위해 필요한 계정 생성 이후 지난 시간.</li>\n<li>인증이 성공하면 디스코드 닉네임을 위키 계정과 일치하도록 변경할지 여부</li>\n</ul>",
         "form": {
             "accountage": "계정 기간 (일 기준으로):",
             "channel": "채널:",
             "confirm": "정말 이 인증을 삭제하시겠어요?",
             "editcount": "최소 편집 횟수:",
             "entry": "인증 #$1",
+            "flag_logall": "인증 실패를 기록할 채널:",
+            "flag_private": "응답을 개인 메시지로 할 지 여부:",
+            "logging": "기록 채널:",
+            "match": "요구조건 불충족 공지:",
+            "match_placeholder": "디스코드 태그가 일치하지만 역할을 받기 위한 조건을 충족하지 않습니다.",
             "more": "더 추가",
             "new": "새 인증",
+            "notice": "인증 공지",
             "postcount": "최소 게시글 갯수:",
             "postcount_and": "편집 횟수와 게시글 횟수를 모두 확인합니다.",
             "postcount_both": "편집 횟수와 게시글 횟수를 통합하여 확인합니다.",
@@ -187,11 +237,16 @@
             "postcount_or": "편집 횟수 또는 게시글 횟수 중 하나만 확인합니다.",
             "rename": "이름 변경:",
             "role": "역할:",
+            "role_add": "추가",
+            "role_remove": "삭제",
             "select_channel": "-- 채널을 선택하세요 --",
             "select_role": "-- 역할을 선택하세요 --",
+            "success": "성공 공지:",
+            "success_placeholder": "인증이 성공하면 표시할 마크다운 본문.",
             "usergroup": "위키 사용자 권한:",
             "usergroup_and": "모든 사용자 권한 강제:"
         },
-        "new": "새 인증"
+        "new": "새 인증",
+        "notice": "인증 공지"
     }
 }

+ 12 - 0
dashboard/i18n/pt-br.json

@@ -30,6 +30,10 @@
         "welcome": "<h2>Bem-vindo ao painel de controle do Wiki-Bot.</h2>\n<p>Wiki-Bot é um robô no Discord feito para reunir os servidores no Discord e as wikis do MediaWiki. Isso ajuda a vincular páginas da wiki, verificar usuários da wiki, informar sobre as últimas mudanças no a wiki e muito mais. <a href=\"https://wiki.wikibot.de/wiki/Wiki-Bot_Wiki\" target=\"_blank\">[Mais Informações]</a></p>\n<p>Aqui você pode alterar diferentes configurações de bot para servidores nos quais você tem permissão para gerenciar servidor. Para começar, você terá que autenticar sua conta do Discord, o que pode ser feito com este botão:</p>"
     },
     "indexjs": {
+        "avatar": {
+            "content_type": "O link fornecido tem o tipo de conteúdo $1, mas apenas os seguintes tipos de conteúdo são permitidos:",
+            "invalid_url": "O URL não pôde ser resolvido para um arquivo de imagem válido."
+        },
         "invalid": {
             "note_http": "O site fornecido não possui um certificado TLS/SSL válido! Por razões de segurança, apenas wikis que usam HTTPS são suportados.",
             "note_private": "A wiki fornecida é privada!",
@@ -129,6 +133,11 @@
             "text": "Entre antes de alterar as configurações.",
             "title": "Não entrou!"
         },
+        "webhookfail": {
+            "note": "Não foi possível alterar o webhook do Discord!",
+            "text": "As configurações foram atualizadas apenas parcialmente.",
+            "title": "Configurações parcialmente salvas!"
+        },
         "wikiblocked": {
             "note": "Motivo:",
             "text": "$1 foi bloqueado de ser adicionado como um webhook de alterações recentes.",
@@ -139,6 +148,8 @@
         "desc": "Estes são os webhooks de mudanças recentes para $1:",
         "explanation": "<h2>Webhook de mudanças recentes</h2>\n<p>O Wiki-Bot é capaz de executar um webhook de mudanças recentes baseado no <a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a>. As mudanças recentes podem ser exibidas em mensagens de texto compactas com links embutidos ou mensagens incorporadas com etiquetas de edição e mudanças de categoria.</p>\n<p>Requisitos para adicionar um webhook de mudanças recentes:</p>\n<ul>\n<li>A wiki precisa estar no <a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a> ou superior.</li>\n<li>A mensagem de sistema <code class=\"user-select\">MediaWiki:Custom-RcGcDw</code> precisa ser definida com o ID do servidor do Discord <code class=\"user-select\" id=\"server-id\"></code>.</li>\n</ul>",
         "form": {
+            "avatar": "Avatar do webhook:",
+            "avatar_preview": "Pré-visualização",
             "channel": "Canal:",
             "confirm": "Tem certeza de que deseja excluir o webhook de mudanças recentes?",
             "display": "Modo de exibição:",
@@ -150,6 +161,7 @@
             "feeds": "Mudanças de feeds:",
             "feeds_only": "Apenas mudanças de feeds:",
             "lang": "Idioma:",
+            "name": "Nome do webhook:",
             "new": "Webhook de novas mudanças recentes",
             "select_channel": "-- Selecionar canal --",
             "wiki": "Wiki:",

+ 61 - 6
dashboard/i18n/tr.json

@@ -7,6 +7,10 @@
         " "
     ],
     "general": {
+        "botlist": {
+            "text": "Bot listelerinde oy vererek diğer kullanıcıların da Wiki-Bot'u bulmasına yardım et:",
+            "title": "Bot Listeleri"
+        },
         "delete": "Sil",
         "invite": "Wiki-Bot'u davet et",
         "language": "Dili Değiştir",
@@ -17,14 +21,21 @@
         "save": "Kaydet",
         "selector": "Sunucu Seçici",
         "settings": "Ayarlar",
+        "slash": "Eğik Çizgi Komutları",
         "support": "Destek Sunucusu",
+        "theme-dark": "Karanlık temayı kullan",
+        "theme-light": "Aydınlık temayı kullan",
         "title": "Wiki-Bot Ayarları",
         "verification": "Doğrulamalar",
         "welcome": "<h2>Wiki-Bot Kontrol Paneli'ne hoş geldin.</h2>\n<p>Wiki-Bot, Discord sunucularını ve MediaWiki vikilerini bir araya getirmek için yapılmış bir Discord botudur. Viki sayfalarına bağlantı vermek, viki kullanıcılarını doğrulamak, vikideki son değişiklikleri bildirmek ve benzeri özellikleri ile kullanıcılara yardım eder. <a href=\"https://wiki.wikibot.de/wiki/Wiki-Bot_Wiki\" target=\"_blank\">[Daha fazla bilgi]</a></p>\n<p>Burada Sunucuyu Yönet yetkisine sahip olduğun sunucular için bot ayarı yapabilirsin. Başlamak için Discord hesabını bu buton ile doğrulaman gerekiyor:</p>"
     },
     "indexjs": {
+        "avatar": {
+            "content_type": "Sağlanan bağlantı $1 içerik türüne sahip ancak izin verilen tek içerik türleri şunlar:",
+            "invalid_url": "Bu bağlantı geçerli bir resim dosyasına varmıyor."
+        },
         "invalid": {
-            "note_http": "Belirtilen site HTTPS kullanmıyor!",
+            "note_http": "Belirtilen site geçerli bir TLS/SSL sertifikasına sahip değil! Güvenlik amacıyla sadece HTTPS kullanan vikiler desteklenmektedir.",
             "note_private": "Belirtilen viki gizli!",
             "note_timeout": "Belirtilen vikinin karşılık vermesi çok uzun sürdü!",
             "text": "URL geçerli bir MediaWiki sitesinin değil!",
@@ -82,6 +93,15 @@
             "text": "Lütfen sunucu için ayarları belirtin.",
             "title": "Sunucu kurulmamış!"
         },
+        "noslash": {
+            "note": "Eğik çizgi komutlarını etkinleştir.",
+            "text": "Wiki-Bot'un eğik çizgi komutları bu sunucuda etkin.",
+            "title": "Eğik çizgi komutları etkin değil!"
+        },
+        "noverify": {
+            "text": "$1 komutunun kısıtlamaları doğrulamalar kurulmadan önce değiştirilemez.",
+            "title": "Doğrulamalar kurulmamış!"
+        },
         "readonly": {
             "text": "Şu an ayarları okuyabilirsin ama değiştiremezsin.",
             "title": "Salt okunur veritabanı!"
@@ -99,7 +119,7 @@
             "title": "Ayarlar keydedildi!"
         },
         "savefail": {
-            "note_http": "Belirtilen site HTTPS kullanmıyor!",
+            "note_http": "Belirtilen site geçerli bir TLS/SSL seritifikasına sahip değil! Güvenlik amacıyla sadece HTTPS kullanan vikiler destekleniyor.",
             "note_private": "Belirtilen viki gizli!",
             "note_timeout": "Belirtilen bağlantının karşılık vermesi çok uzun sürdü!",
             "text": "Ayarlar kaydedilemedi, lütfen tekrar deneyin.",
@@ -113,6 +133,11 @@
             "text": "Lütfen herhangi bir ayar yapmadan önce giriş yapın.",
             "title": "Giriş yapılmamış!"
         },
+        "webhookfail": {
+            "note": "Discord webhook'u değiştirilemedi!",
+            "text": "Ayalarlar sadece kısmen güncellendi.",
+            "title": "Ayarlar kısmen kaydedildi!"
+        },
         "wikiblocked": {
             "note": "Neden:",
             "text": "$1 platformunun bir son değişiklikler webhook'u olarak eklenmesi yasaklanmış.",
@@ -121,8 +146,10 @@
     },
     "rcscript": {
         "desc": "$1 için son değişiklikler webhook'ları:",
-        "explanation": "<h2>Son Değişiklikler Webhook'u</h2>\n<p>Wiki-Bot <a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a> bazlı olarak bir son değişiklikler webhook'u sağlayabilir. Son değişiklikler; satıriçi bağlantılar ile kompakt metin mesajları veya düzenleme etiketleri ve kategori değişiklikleri ile gömülü mesajlar olarak görüntülenebilir.</p>\n<p>Bir son değişiklikler webhook'u ekleme gereksinimleri:</p>\n<ul>\n<li>Viki <a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a> veya daha güncel bir versiyona sahip olmalı.</li>\n<li>Sistem mesajı <code>MediaWiki:Custom-RcGcDw</code>, Discord sunucu ID'si <code class=\"user-select\" id=\"server-id\"></code> ile eşleşmeli.</li>\n</ul>",
+        "explanation": "<h2>Son Değişiklikler Webhook'u</h2>\n<p>Wiki-Bot <a href=\"https://gitlab.com/piotrex43/RcGcDw\" target=\"_blank\">RcGcDw</a> bazlı olarak bir son değişiklikler webhook'u sağlayabilir. Son değişiklikler; satıriçi bağlantılar ile kompakt metin mesajları veya düzenleme etiketleri ve kategori değişiklikleri ile gömülü mesajlar olarak görüntülenebilir.</p>\n<p>Bir son değişiklikler webhook'u ekleme gereksinimleri:</p>\n<ul>\n<li>Viki <a href=\"https://www.mediawiki.org/wiki/MediaWiki_1.30\" target=\"_blank\">MediaWiki 1.30</a> veya daha güncel bir versiyona sahip olmalı.</li>\n<li>Sistem mesajı <code class=\"user-select\">MediaWiki:Custom-RcGcDw</code>, Discord sunucu ID'si <code class=\"user-select\" id=\"server-id\"></code> ile eşleşmeli.</li>\n</ul>",
         "form": {
+            "avatar": "Webhook simgesi:",
+            "avatar_preview": "Ön izleme",
             "channel": "Kanal:",
             "confirm": "Bu son değişiklikler webhook'unu silmek istediğine emin misin?",
             "display": "Görüntüleme modu:",
@@ -134,6 +161,7 @@
             "feeds": "Yayın bazlı değişiklikler:",
             "feeds_only": "Sadece yayın bazlı değişiklikler:",
             "lang": "Dil:",
+            "name": "Webhook ismi:",
             "new": "Yeni Son Değişiklikler Webhook'u",
             "select_channel": "-- Bir Kanal Seç --",
             "wiki": "Viki:",
@@ -147,8 +175,8 @@
         "none": "Halihazırda hiçbir sunucuda [Sunucuyu Yönet]($1) iznine sahip değilsin, doğru hesaba giriş yaptığına emin misin?",
         "switch": "Hesap Değiştir",
         "title": "Sunucu Seçici",
-        "with": "Wiki-Bot'un olduğu sunucu",
-        "without": "WikiBot'un olmadığı sunucu"
+        "with": "Wiki-Bot'un olduğu sunucular",
+        "without": "Wiki-Bot'un olmadığı sunucular"
     },
     "settings": {
         "desc": "$1 ayarları:",
@@ -170,6 +198,21 @@
         },
         "new": "Yeni kanal üzerine yazması"
     },
+    "slash": {
+        "desc": "$1 için mevcut eğik çizgi komutları:",
+        "explanation": "<h2>Eğik Çizgi Komutları</h2>\n<p>Spesifik eğik çizgi komutlarının kullanımı role göre kısıtlanabilir. Bunu Wiki-Bot'un eğik çizgi komutları için burada düzenleyebilirsin.</p>",
+        "form": {
+            "add": "Ekle",
+            "allow": "İzin ver",
+            "default": "Varsayılan",
+            "default_allow": "Varsayılan hali ile bu komut herkes tarafından kullanılabiliyor.",
+            "default_deny": "Varsayılan hali ile kimse bu komutu kullanamıyor.",
+            "deny": "Reddet",
+            "entry": "$1 komutu",
+            "role": "Rol:",
+            "select_role": "-- Bir Rol Seç --"
+        }
+    },
     "verification": {
         "desc": "$1 doğrulamaları:",
         "explanation": "<h2>Kullanıcı Doğrulaması</h2>\n<p>Kullanıcılar <code class=\"prefix\">verify &lt;viki kullanıcı adı&gt;</code> komutunu kullanarak kendilerini viki profillerindeki Discord bölümü sayesinde belirli spesifik bir viki kullanıcısı olarak doğrulayabilirler. Eğer kullanıcı eşleşirse ve sunucuda doğrulama sistemi kuruluysa Wiki-Bot eşleşen bütün doğrulama girdileri için ilgili rolleri verecektir.</p>\n<p>Her doğrulama girdisi, kullanıcının doğrulamayı nasıl tamamlayacağı konusunda bir kısıtlama getirir:</p>\n<ul>\n<li><code class=\"prefix\">verify</code> komutunun kullanılacağı kanal.</li>\n<li>Doğrulama girdisi eşleştiğinde verilecek rol.</li>\n<li>Doğrulama girdisinin eşleşebilmesi için gerekli minimum düzenleme sayısı.</li>\n<li>Doğrulama girdisinin eşleşebilmesi için üyenin vikide üyesi olması gereken kullanıcı grupları.</li>\n<li>Doğrulama girdisinin eşleşebilmesi için hesabın gün bakımından yaşı.</li>\n<li>Doğrulama girdisi eşleştiğinde Discord kullanıcı adının viki kullanıcı adıyla değiştirilip değiştirilmeyeceği.</li>\n</ul>",
@@ -179,8 +222,14 @@
             "confirm": "Doğrulamayı silmek istediğine emin misin?",
             "editcount": "Minimal düzenleme sayısı:",
             "entry": "Doğrulama #$1",
+            "flag_logall": "Başarısız doğrulama günlüğü:",
+            "flag_private": "Özel komut yanıtları:",
+            "logging": "Günlük kanalı:",
+            "match": "Eksik gereklilik bildirileri:",
+            "match_placeholder": "Biçimlendirme metni Discord etiketleri ile uyuşuyor ancak roller için gereklilikleri karşılamıyor.",
             "more": "Daha fazla ekle",
             "new": "Yeni Doğrulama",
+            "notice": "Doğrulama Bildirileri",
             "postcount": "Asgari paylaşım sayısı:",
             "postcount_and": "Hem düzenleme hem de paylaşım gerektir.",
             "postcount_both": "Düzenleme ve paylaşım sayısı toplamı gerektir.",
@@ -188,11 +237,17 @@
             "postcount_or": "Ya düzenleme ya da paylaşım sayısı gerektir.",
             "rename": "Üyelerin kullanıcı isimlerini değiştir:",
             "role": "Rol:",
+            "role_add": "Ekle",
+            "role_remove": "Kaldır",
             "select_channel": "-- Bir Kanal Seç --",
             "select_role": "-- Bir Rol Seç --",
+            "success": "Başarı bildirisi:",
+            "success_placeholder": "Başarılı doğrulamadaki biçimlendirme metni.",
             "usergroup": "Viki kullanıcı grubu:",
             "usergroup_and": "Bütün kullanıcı gruplarını iste:"
         },
-        "new": "Yeni doğrulama"
+        "help_notice": "<p>Özel bildiriler bazı basit fonksiyon ve değişkenleri destekliyor.</p>\n<ul>\n<li><code class=\"form-button user-select\">$editcount</code> - Kullanıcının mevcut düzenleme sayısı.</li>\n<li><code class=\"form-button user-select\">$accountage</code> - Kullanıcının gün bazında hesap yaşı.</li>\n<li><code class=\"form-button user-select\">$postcount</code> - Kullanıcının tartışma gönderisi sayısı (sadece Fandom vikileri).</li>\n<li><code class=\"form-button user-select\" data-after=\" }}\" data-before=\"{{#expr: \">{{#expr:1+1}}</code> - Bir ifadenin sonucunu verir.\n<ul>\n<li>Sadece ekleme <code class=\"form-button user-select\">+</code> ve çıkarmayı <code class=\"form-button user-select\">-</code> destekler.</li>\n</ul></li>\n<li><code class=\"form-button user-select\" data-after=\" |  |  }}\" data-before=\"{{#ifexpr: \"> 1 &gt; 1 | <i>eğer doğruysa</i> <i>eğer yanlışsa</i> }}</code> - Bir ifade sonucunu baz alarak metin verir.\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\">ve</code>, <code class=\"form-button user-select\">ya da</code> destekler.</li>\n</ul></li>\n</ul>",
+        "new": "Yeni doğrulama",
+        "notice": "Doğrulama bildirileri"
     }
 }

+ 11 - 1
dashboard/i18n/vi.json

@@ -5,5 +5,15 @@
         " ",
         " ",
         " "
-    ]
+    ],
+    "general": {
+        "botlist": {
+            "text": "Giúp nhũng người dùng khác tìm thấy Wiki-Bot bằng cách vote trên các trang tổng hợp bot:",
+            "title": "Trang tổng hợp bot"
+        },
+        "delete": "Xóa",
+        "invite": "Mời Wiki-Bot",
+        "language": "Thay đổi ngôn ngữ",
+        "login": "Đăng nhập"
+    }
 }

+ 6 - 6
dashboard/i18n/zh-hant.json

@@ -21,7 +21,7 @@
         "save": "儲存",
         "selector": "選擇伺服器",
         "settings": "設定",
-        "slash": "斜指令",
+        "slash": "斜指令",
         "support": "支援伺服器",
         "theme-dark": "使用灰暗主題",
         "theme-light": "使用明亮主題",
@@ -94,9 +94,9 @@
             "title": "尚未設定伺服器!"
         },
         "noslash": {
-            "note": "啟用斜指令。",
-            "text": "Wiki-Bot的斜指令在本伺服器上未啟用。",
-            "title": "斜指令未啟用!"
+            "note": "啟用斜指令。",
+            "text": "Wiki-Bot的斜指令在本伺服器上未啟用。",
+            "title": "斜指令未啟用!"
         },
         "noverify": {
             "text": "在未設定驗證方式時,無法對指令$1設定使用限制。",
@@ -199,8 +199,8 @@
         "new": "新的頻道覆寫設定"
     },
     "slash": {
-        "desc": "以下是$1的斜指令:",
-        "explanation": "<h2>斜槓指令</h2>\n<p>可以為每個身分組設定特定斜槓指令的使用限制。在此你可以設定Wiki-Bot斜槓指令的使用限制。</p>",
+        "desc": "以下是$1的斜指令:",
+        "explanation": "<h2>斜線指令</h2>\n<p>可以為每個身分組設定特定斜線指令的使用限制。在此你可以設定Wiki-Bot斜線指令的使用限制。</p>",
         "form": {
             "add": "新增",
             "allow": "允許",

+ 1 - 1
dashboard/index.html

@@ -55,7 +55,7 @@
 		<div id="guildlist">
 			<div class="guild" id="invite">
 				<div class="bar"></div>
-				<a href="https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939904064&scope=bot%20applications.commands" alt="Invite Wiki-Bot">
+				<a href="https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939904064&scope=bot+applications.commands" alt="Invite Wiki-Bot">
 					<div class="avatar svg-avatar">
 						<svg width="24" height="24" viewBox="0 0 24 24">
 							<path fill="currentColor" d="M20 11.1111H12.8889V4H11.1111V11.1111H4V12.8889H11.1111V20H12.8889V12.8889H20V11.1111Z"></path>

+ 51 - 15
dashboard/index.js

@@ -8,6 +8,7 @@ const Lang = require('./i18n.js');
 const allLangs = Lang.allLangs();
 
 const posts = {
+	user: require('./user.js').post,
 	settings: require('./settings.js').post,
 	verification: require('./verification.js').post,
 	rcscript: require('./rcscript.js').post,
@@ -53,15 +54,15 @@ const files = new Map([
 
 const server = http.createServer( (req, res) => {
 	res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
-	if ( req.method === 'POST' && req.headers['content-type'] === 'application/x-www-form-urlencoded' && req.url.startsWith( '/guild/' ) ) {
+	if ( req.method === 'POST' && req.headers['content-type'] === 'application/x-www-form-urlencoded' && ( req.url.startsWith( '/guild/' ) || req.url === '/user' ) ) {
 		let args = req.url.split('/');
 		let state = req.headers.cookie?.split('; ')?.filter( cookie => {
 			return cookie.split('=')[0] === 'wikibot' && /^"([\da-f]+(?:-\d+)*)"$/.test(( cookie.split('=')[1] || '' ));
 		} )?.map( cookie => cookie.replace( /^wikibot="([\da-f]+(?:-\d+)*)"$/, '$1' ) )?.join();
 
-		if ( args.length === 5 && ['settings', 'verification', 'rcscript', 'slash'].includes( args[3] )
-		&& /^(?:default|new|notice|\d+)$/.test(args[4]) && sessionData.has(state) && settingsData.has(sessionData.get(state).user_id)
-		&& settingsData.get(sessionData.get(state).user_id).guilds.isMember.has(args[2]) ) {
+		if ( state && sessionData.has(state) && settingsData.has(sessionData.get(state).user_id) &&
+		( ( args.length === 5 && ['settings', 'verification', 'rcscript', 'slash'].includes( args[3] ) && /^(?:default|new|notice|\d+)$/.test(args[4])
+		&& settingsData.get(sessionData.get(state).user_id).guilds.isMember.has(args[2]) ) || req.url === '/user' ) ) {
 			if ( process.env.READONLY ) return save_response(`${req.url}?save=failed`);
 			let body = [];
 			req.on( 'data', chunk => {
@@ -85,7 +86,14 @@ const server = http.createServer( (req, res) => {
 					}
 				} );
 				if ( isDebug ) console.log( '- Dashboard:', req.url, settings, sessionData.get(state).user_id );
-				return posts[args[3]](save_response, settingsData.get(sessionData.get(state).user_id), args[2], args[4], settings);
+				if ( req.url === '/user' ) {
+					let setting = Object.keys(settings);
+					if ( setting.length === 1 && setting[0].startsWith( 'oauth_' ) && setting[0].split('_').length >= 3 ) {
+						setting = setting[0].split('_');
+						return posts.user(save_response, sessionData.get(state).user_id, setting[1], setting.slice(2).join('_'));
+					}
+				}
+				else return posts[args[3]](save_response, settingsData.get(sessionData.get(state).user_id), args[2], args[4], settings);
 			} );
 
 			/**
@@ -94,6 +102,10 @@ const server = http.createServer( (req, res) => {
 			 * @param {String[]} [actionArgs]
 			 */
 			function save_response(resURL = '/', action, ...actionArgs) {
+				if ( action === 'REDIRECT' && resURL.startsWith( 'https://' ) ) {
+					res.writeHead(303, {Location: resURL});
+					return res.end();
+				}
 				var themeCookie = ( req.headers?.cookie?.split('; ')?.find( cookie => {
 					return cookie.split('=')[0] === 'theme' && /^"(?:light|dark)"$/.test(( cookie.split('=')[1] || '' ));
 				} ) || 'dark' ).replace( /^theme="(light|dark)"$/, '$1' );
@@ -132,10 +144,6 @@ const server = http.createServer( (req, res) => {
 		return res.end();
 	}
 
-	if ( reqURL.pathname === '/oauth/mw' ) {
-		return pages.verify(res, reqURL.searchParams);
-	}
-
 	if ( reqURL.pathname === '/favicon.ico' ) reqURL.pathname = '/src/icon.png';
 	if ( files.has(reqURL.pathname) ) {
 		let file = files.get(reqURL.pathname);
@@ -165,8 +173,8 @@ const server = http.createServer( (req, res) => {
 	res.setHeader('Content-Language', [dashboardLang.lang]);
 
 	var lastGuild = req.headers?.cookie?.split('; ')?.filter( cookie => {
-		return cookie.split('=')[0] === 'guild' && /^"\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new|notice))?"$/.test(( cookie.split('=')[1] || '' ));
-	} )?.map( cookie => cookie.replace( /^guild="(\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new|notice))?)"$/, '$1' ) )?.join();
+		return cookie.split('=')[0] === 'guild' && /^"(?:user|\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new|notice))?)"$/.test(( cookie.split('=')[1] || '' ));
+	} )?.map( cookie => cookie.replace( /^guild="(user|\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new|notice))?)"$/, '$1' ) )?.join();
 	if ( lastGuild ) res.setHeader('Set-Cookie', ['guild=""; SameSite=Lax; Path=/; Max-Age=0']);
 
 	var state = req.headers.cookie?.split('; ')?.filter( cookie => {
@@ -188,14 +196,27 @@ const server = http.createServer( (req, res) => {
 		return pages.login(res, dashboardLang, themeCookie, state, 'logout');
 	}
 
+	if ( reqURL.pathname === '/oauth/mw' ) {
+		return pages.verify(res, reqURL.searchParams, sessionData.get(state)?.user_id);
+	}
+
 	if ( !state ) {
+		let action = '';
+		if ( reqURL.pathname !== '/' ) action = 'unauthorized';
 		if ( reqURL.pathname.startsWith( '/guild/' ) ) {
 			let pathGuild = reqURL.pathname.split('/').slice(2, 5).join('/');
 			if ( /^\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new|notice))?$/.test(pathGuild) ) {
 				res.setHeader('Set-Cookie', [`guild="${pathGuild}"; SameSite=Lax; Path=/`]);
 			}
 		}
-		return pages.login(res, dashboardLang, themeCookie, state, ( reqURL.pathname === '/' ? '' : 'unauthorized' ));
+		else if ( reqURL.pathname === '/user' ) {
+			if ( reqURL.searchParams.get('oauth') === 'success' ) action = 'oauth';
+			if ( reqURL.searchParams.get('oauth') === 'failed' ) action = 'oauthfail';
+			if ( reqURL.searchParams.get('oauth') === 'verified' ) action = 'oauthverify';
+			if ( reqURL.searchParams.get('oauth') === 'other' ) action = 'oauth';
+			res.setHeader('Set-Cookie', ['guild="user"; SameSite=Lax; Path=/']);
+		}
+		return pages.login(res, dashboardLang, themeCookie, state, action);
 	}
 
 	if ( reqURL.pathname === '/oauth' ) {
@@ -203,18 +224,27 @@ const server = http.createServer( (req, res) => {
 	}
 
 	if ( !sessionData.has(state) || !settingsData.has(sessionData.get(state).user_id) ) {
+		let action = '';
+		if ( reqURL.pathname !== '/' ) action = 'unauthorized';
 		if ( reqURL.pathname.startsWith( '/guild/' ) ) {
 			let pathGuild = reqURL.pathname.split('/').slice(2, 5).join('/');
 			if ( /^\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new|notice))?$/.test(pathGuild) ) {
 				res.setHeader('Set-Cookie', [`guild="${pathGuild}"; SameSite=Lax; Path=/`]);
 			}
 		}
-		return pages.login(res, dashboardLang, themeCookie, state, ( reqURL.pathname === '/' ? '' : 'unauthorized' ));
+		else if ( reqURL.pathname === '/user' ) {
+			if ( reqURL.searchParams.get('oauth') === 'success' ) action = 'oauth';
+			if ( reqURL.searchParams.get('oauth') === 'failed' ) action = 'oauthfail';
+			if ( reqURL.searchParams.get('oauth') === 'verified' ) action = 'oauthverify';
+			if ( reqURL.searchParams.get('oauth') === 'other' ) action = 'oauth';
+			res.setHeader('Set-Cookie', ['guild="user"; SameSite=Lax; Path=/']);
+		}
+		return pages.login(res, dashboardLang, themeCookie, state, action);
 	}
 
 	if ( reqURL.pathname === '/refresh' ) {
 		let returnLocation = reqURL.searchParams.get('return');
-		if ( !/^\/guild\/\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new|notice))?$/.test(returnLocation) ) {
+		if ( !/^\/(?:user|guild\/\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new|notice))?)$/.test(returnLocation) ) {
 			returnLocation = '/';
 		}
 		return pages.refresh(res, sessionData.get(state), returnLocation);
@@ -228,7 +258,13 @@ const server = http.createServer( (req, res) => {
 	let action = '';
 	if ( reqURL.searchParams.get('refresh') === 'success' ) action = 'refresh';
 	if ( reqURL.searchParams.get('refresh') === 'failed' ) action = 'refreshfail';
-	if ( reqURL.searchParams.get('slash') === 'noverify' ) action = 'noverify';
+	if ( reqURL.searchParams.get('slash') === 'noverify' && reqURL.pathname.split('/')[3] === 'slash' ) action = 'noverify';
+	if ( reqURL.pathname === '/user' ) {
+		if ( reqURL.searchParams.get('oauth') === 'success' ) action = 'oauth';
+		if ( reqURL.searchParams.get('oauth') === 'failed' ) action = 'oauthfail';
+		if ( reqURL.searchParams.get('oauth') === 'verified' ) action = 'oauthverify';
+		if ( reqURL.searchParams.get('oauth') === 'other' ) action = 'oauthother';
+	}
 	return dashboard(res, dashboardLang, themeCookie, sessionData.get(state), reqURL, action);
 } );
 

+ 2 - 2
dashboard/login.html

@@ -41,7 +41,7 @@
 				<img src="/src/settings.svg" alt="Settings">
 				<div>Login</div>
 			</a>
-			<a class="channel" id="invite-wikibot" href="https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939904064&scope=bot%20applications.commands" title="Invite Wiki-Bot">
+			<a class="channel" id="invite-wikibot" href="https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939904064&scope=bot+applications.commands" title="Invite Wiki-Bot">
 				<img src="/src/channel.svg" alt="Channel">
 				<div>Invite Wiki-Bot</div>
 			</a>
@@ -57,7 +57,7 @@
 		<div id="guildlist">
 			<div class="guild" id="invite">
 				<div class="bar"></div>
-				<a href="https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939904064&scope=bot%20applications.commands" alt="Invite Wiki-Bot">
+				<a href="https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939904064&scope=bot+applications.commands" alt="Invite Wiki-Bot">
 					<div class="avatar svg-avatar">
 						<svg width="24" height="24" viewBox="0 0 24 24">
 							<path fill="currentColor" d="M20 11.1111H12.8889V4H11.1111V11.1111H4V12.8889H11.1111V20H12.8889V12.8889H20V11.1111Z"></path>

+ 53 - 15
dashboard/oauth.js

@@ -3,7 +3,7 @@ const cheerio = require('cheerio');
 const {defaultPermissions} = require('../util/default.json');
 const Wiki = require('../util/wiki.js');
 const allLangs = require('./i18n.js').allLangs().names;
-const {got, oauth, sessionData, settingsData, oauthVerify, sendMsg, addWidgets, createNotice, hasPerm} = require('./util.js');
+const {got, db, oauth, enabledOAuth2, sessionData, settingsData, oauthVerify, sendMsg, addWidgets, createNotice, hasPerm} = require('./util.js');
 
 const file = require('fs').readFileSync('./dashboard/login.html');
 
@@ -44,9 +44,11 @@ function dashboard_login(res, dashboardLang, theme, state, action) {
 	let prompt = 'none';
 	if ( process.env.READONLY ) createNotice($, 'readonly', dashboardLang);
 	if ( action ) createNotice($, action, dashboardLang);
-	if ( action === 'unauthorized' ) $('head').append(
-		$('<script>').text('history.replaceState(null, null, "/login");')
-	);
+	if ( action === 'unauthorized' ) $('<script>').text('history.replaceState(null, null, "/login");').appendTo('head');
+	else if ( action.startsWith( 'oauth' ) ) {
+		if ( action === 'oauth' ) createNotice($, 'oauthlogin', dashboardLang);
+		$('<script>').text('history.replaceState(null, null, "/user");').appendTo('head');
+	}
 	if ( action === 'logout' ) prompt = 'consent';
 	if ( action === 'loginfail' ) responseCode = 400;
 	state = Date.now().toString(16) + randomBytes(16).toString('hex');
@@ -155,8 +157,13 @@ function dashboard_oauth(res, state, searchParams, lastGuild) {
 				if ( searchParams.has('guild_id') && !lastGuild.startsWith( searchParams.get('guild_id') + '/' ) ) {
 					lastGuild = searchParams.get('guild_id') + '/settings';
 				}
+				let returnLocation = '/';
+				if ( lastGuild ) {
+					if ( lastGuild === 'user' ) returnLocation += lastGuild;
+					else if ( /^\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new|notice))?$/.test(lastGuild) ) returnLocation += 'guild/' + lastGuild;
+				}
 				res.writeHead(302, {
-					Location: ( lastGuild && /^\d+\/(?:settings|verification|rcscript|slash)(?:\/(?:\d+|new|notice))?$/.test(lastGuild) ? `/guild/${lastGuild}` : '/' ),
+					Location: returnLocation,
 					'Set-Cookie': [`wikibot="${userSession.state}"; HttpOnly; SameSite=Lax; Path=/; Max-Age=31536000`]
 				});
 				return res.end();
@@ -318,44 +325,75 @@ function dashboard_api(res, input) {
  * Load oauth data of a wiki user
  * @param {import('http').ServerResponse} res - The server response
  * @param {URLSearchParams} searchParams - The url parameters
+ * @param {String} [user_id] - The current user
  */
-function mediawiki_oauth(res, searchParams) {
-	if ( !searchParams.get('code') || !oauthVerify.has(searchParams.get('state')) ) {
-		res.writeHead(302, {Location: '/login?action=failed'});
+function mediawiki_oauth(res, searchParams, user_id) {
+	if ( !searchParams.get('code') || !searchParams.get('state') ) {
+		res.writeHead(302, {Location: '/user?oauth=failed'});
 		return res.end();
 	}
 	var state = searchParams.get('state');
 	var site = state.split(' ');
-	got.post( 'https://' + site[0] + '/rest.php/oauth2/access_token', {
+	var oauthSite = enabledOAuth2.find( oauthSite => ( site[2] || site[0] ) === oauthSite.id );
+	if ( !oauthSite || ( !oauthVerify.has(state) && !user_id ) ) {
+		res.writeHead(302, {Location: '/user?oauth=failed'});
+		return res.end();
+	}
+	var url = oauthSite.url;
+	if ( oauthVerify.has(state) && site[2] === oauthSite.id ) url = 'https://' + site[0] + '/';
+	got.post( url + 'rest.php/oauth2/access_token', {
 		form: {
 			grant_type: 'authorization_code',
 			code: searchParams.get('code'),
 			redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
-			client_id: process.env['oauth_' + ( site[2] || site[0] )],
-			client_secret: process.env['oauth_' + ( site[2] || site[0] ) + '_secret']
+			client_id: process.env['oauth_' + oauthSite.id],
+			client_secret: process.env['oauth_' + oauthSite.id + '_secret']
 		}
 	} ).then( response => {
 		var body = response.body;
 		if ( response.statusCode !== 200 || !body?.access_token ) {
 			console.log( '- Dashboard: ' + response.statusCode + ': Error while getting the mediawiki token: ' + ( body?.message || body?.error ) );
-			res.writeHead(302, {Location: '/login?action=failed'});
+			res.writeHead(302, {Location: '/user?oauth=failed'});
 			return res.end();
 		}
+		if ( !oauthVerify.has(state) ) {
+			if ( !body?.refresh_token || !user_id ) {
+				res.writeHead(302, {Location: '/user?oauth=failed'});
+				return res.end();
+			}
+			return db.query( 'INSERT INTO oauthusers(userid, site, token) VALUES($1, $2, $3)', [user_id, oauthSite.id, body.refresh_token] ).then( () => {
+				console.log( '- Dashboard: OAuth2 token for ' + user_id + ' successfully saved.' );
+				res.writeHead(302, {Location: '/user?oauth=success'});
+				return res.end();
+			}, dberror => {
+				console.log( '- Dashboard: Error while saving the OAuth2 token for ' + user_id + ': ' + dberror );
+				res.writeHead(302, {Location: '/user?oauth=failed'});
+				return res.end();
+			} );
+		}
 		sendMsg( {
 			type: 'verifyUser', state,
 			access_token: body.access_token
 		} ).then( () => {
+			let userid = oauthVerify.get(state);
+			if ( userid && body?.refresh_token ) db.query( 'INSERT INTO oauthusers(userid, site, token) VALUES($1, $2, $3)', [userid, oauthSite.id, body.refresh_token] ).then( () => {
+				console.log( '- Dashboard: OAuth2 token for ' + userid + ' successfully saved.' );
+			}, dberror => {
+				console.log( '- Dashboard: Error while saving the OAuth2 token for ' + userid + ': ' + dberror );
+			} );
 			oauthVerify.delete(state);
-			res.writeHead(302, {Location: 'https://' + site[0] + '/index.php?title=Special:MyPage'});
+			if ( !userid ) res.writeHead(302, {Location: '/user?oauth=verified'});
+			else if ( user_id && userid !== user_id ) res.writeHead(302, {Location: '/user?oauth=other'});
+			else res.writeHead(302, {Location: '/user?oauth=success'});
 			return res.end();
 		}, error => {
 			console.log( '- Dashboard: Error while sending the mediawiki token: ' + error );
-			res.writeHead(302, {Location: '/login?action=failed'});
+			res.writeHead(302, {Location: '/user?oauth=failed'});
 			return res.end();
 		} );
 	}, error => {
 		console.log( '- Dashboard: Error while getting the mediawiki token: ' + error );
-		res.writeHead(302, {Location: '/login?action=failed'});
+		res.writeHead(302, {Location: '/user?oauth=failed'});
 		return res.end();
 	} );
 }

+ 2 - 2
dashboard/settings.js

@@ -321,7 +321,7 @@ function update_settings(res, userSettings, guild, type, settings) {
 			console.log( `- Dashboard: Settings successfully removed: ${guild}#${type}` );
 			res(`/guild/${guild}/settings`, 'save');
 			if ( !channel ) return;
-			db.query( 'SELECT channel, wiki, lang, role, inline FROM discord WHERE guild = $1 AND ( channel = $2 OR channel IS NULL ) ORDER BY channel DESC NULLS LAST', [guild, '#' + response.parentID] ).then( ({rows:[row, {lang: guildlang} = {}]}) => {
+			db.query( 'SELECT channel, wiki, lang, role, inline FROM discord WHERE guild = $1 AND ( channel = $2 OR channel IS NULL ) ORDER BY channel DESC NULLS LAST', [guild, '#' + response.parentId] ).then( ({rows:[row, {lang: guildlang} = {}]}) => {
 				var lang = new Lang(( guildlang || row.lang ));
 				var text = lang.get('settings.dashboard.removed', `<@${userSettings.user.id}>`, `<#${type}>`);
 				if ( channel.wiki !== row.wiki ) text += `\n${lang.get('settings.currentwiki')} <${channel.wiki}>`;
@@ -362,7 +362,7 @@ function update_settings(res, userSettings, guild, type, settings) {
 			}
 			return fresponse;
 		} ).then( fresponse => {
-			return db.query( 'SELECT channel, wiki, lang, role, inline, prefix FROM discord WHERE guild = $1 AND ( channel = $2 OR channel IS NULL ) ORDER BY channel DESC NULLS LAST', [guild, '#' + response.parentID] ).then( ({rows:[row, {lang: guildlang} = {}]}) => {
+			return db.query( 'SELECT channel, wiki, lang, role, inline, prefix FROM discord WHERE guild = $1 AND ( channel = $2 OR channel IS NULL ) ORDER BY channel DESC NULLS LAST', [guild, '#' + response.parentId] ).then( ({rows:[row, {lang: guildlang} = {}]}) => {
 				if ( row ) row.guildlang = ( guildlang || row.lang );
 				var body = fresponse.body;
 				if ( fresponse.statusCode !== 200 || body?.batchcomplete === undefined || !body?.query?.general ) {

+ 27 - 1
dashboard/src/index.css

@@ -62,7 +62,7 @@ a[alt]:hover:after {
 	content: "";
 	display: block;
 	height: 48px;
-	margin: -48px 0 0;
+	margin-top: -48px;
 }
 #navbar a {
 	display: flex;
@@ -589,6 +589,9 @@ fieldset textarea {
 	height: 128px;
 	background: #32353b;
 }
+.theme-light #wb-settings-avatar-preview-img {
+	background-color: #fafafa;
+}
 .wb-settings-display:first-of-type,
 .wb-settings-permission:first-of-type {
 	display: inline-block;
@@ -606,6 +609,29 @@ button.addmore:not([hidden]) {
 .wb-settings-optgroup {
 	font-weight: bold;
 }
+.wb-oauth-site {
+	margin-bottom: 30px;
+}
+.wb-oauth-site legend {
+	background-color: unset;
+	color: unset;
+}
+.wb-oauth-connected {
+	font-weight: bold;
+	color: green;
+}
+.wb-oauth-unconnected {
+	font-weight: bold;
+	color: red;
+}
+.wb-oauth-disabled {
+	font-weight: bold;
+	color: darkred;
+}
+.wb-oauth-enabled {
+	font-weight: bold;
+	color: darkgreen;
+}
 .wb-settings-error {
 	color: red;
 }

+ 10 - 1
dashboard/src/index.js

@@ -150,7 +150,7 @@ if ( wiki ) {
 			if ( regex ) wikinew = regex[1];
 			else if ( !wiki.validity.valid ) return wiki.reportValidity();
 			else {
-				wikinew = wikinew.replace( /\/(?:api|load|index)\.php(?:|\?.*)$/, '' ).replace( /\/$/, '' );
+				wikinew = wikinew.replace( /\/(?:index|api|load|rest)\.php(?:|\?.*)$/, '' ).replace( /\/$/, '' );
 			}
 			var readonly = wiki.readOnly;
 			wiki.readOnly = true;
@@ -345,7 +345,16 @@ if ( avatar ) {
 			fetch( avatar.value, {
 				method: 'HEAD',
 				referrer: ''
+			} ).catch( function(error) {
+				if ( avatar.value.startsWith( 'https://cdn.discordapp.com/attachments/' ) && error.name === 'TypeError' ) {
+					return fetch( avatar.value.replace( 'https://cdn.discordapp.com/attachments/', 'https://media.discordapp.net/attachments/' ), {
+						method: 'HEAD',
+						referrer: ''
+					} );
+				}
+				throw error;
 			} ).then( function(response) {
+				avatar.value = response.url;
 				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(', ') );

+ 129 - 0
dashboard/user.js

@@ -0,0 +1,129 @@
+const {db, enabledOAuth2, oauthVerify} = require('./util.js');
+
+/**
+ * Let a user change settings
+ * @param {import('http').ServerResponse} res - The server response
+ * @param {import('cheerio')} $ - The response body
+ * @param {import('./util.js').User} user - The current user
+ * @param {import('./i18n.js')} dashboardLang - The user language
+ */
+function dashboard_user(res, $, user, dashboardLang) {
+	db.query( 'SELECT site, token FROM oauthusers WHERE userid = $1', [user.id] ).then( ({rows}) => {
+		$('<p>').text(dashboardLang.get('oauth.desc')).appendTo('#text .description');
+		$('<form id="wb-settings" method="post" enctype="application/x-www-form-urlencoded">').append(
+			$('<h2>').text(dashboardLang.get('oauth.form.default')),
+			...enabledOAuth2.map( oauthSite => {
+				let row = rows.find( row => row.site === oauthSite.id );
+				let buttons = $('<div>');
+				if ( row ) {
+					if ( row.token === null ) buttons.append(
+						$('<span>').append(
+							$('<input type="submit">').addClass('wb-oauth-enabled').attr('name', 'oauth_enable_' + oauthSite.id).val(dashboardLang.get('oauth.form.enable'))
+						),
+						$('<span>').append(
+							$('<input type="submit">').addClass('wb-oauth-connected').attr('name', 'oauth_connect_' + oauthSite.id).val(dashboardLang.get('oauth.form.connect'))
+						)
+					);
+					else buttons.append(
+						$('<span>').append(
+							$('<input type="submit">').addClass('wb-oauth-disabled').attr('name', 'oauth_disable_' + oauthSite.id).val(dashboardLang.get('oauth.form.disable'))
+						),
+						$('<span>').append(
+							$('<input type="submit">').addClass('wb-oauth-unconnected').attr('name', 'oauth_disconnect_' + oauthSite.id).val(dashboardLang.get('oauth.form.disconnect'))
+						)
+					);
+				}
+				else buttons.append(
+					$('<span>').append(
+						$('<input type="submit">').addClass('wb-oauth-disabled').attr('name', 'oauth_disable_' + oauthSite.id).val(dashboardLang.get('oauth.form.disable'))
+					),
+					$('<span>').append(
+						$('<input type="submit">').addClass('wb-oauth-connected').attr('name', 'oauth_connect_' + oauthSite.id).val(dashboardLang.get('oauth.form.connect'))
+					)
+				);
+				return $('<div>').addClass('wb-oauth-site').attr('id', 'oauth-' + oauthSite.id).append(
+					$('<fieldset>').append(
+						$('<legend>').append(
+							$('<a target="_blank">').attr('href', oauthSite.url).text(oauthSite.name)
+						),
+						$('<div>').append(
+							$('<span>').text(dashboardLang.get('oauth.form.current')),
+							( row ? ( row.token === null ?
+								$('<span>').addClass('wb-oauth-disabled').text(dashboardLang.get('oauth.form.disabled'))
+							:
+								$('<span>').addClass('wb-oauth-connected').text(dashboardLang.get('oauth.form.connected'))
+							) :
+								$('<span>').addClass('wb-oauth-unconnected').text(dashboardLang.get('oauth.form.unconnected'))
+							)
+						),
+						buttons
+					)
+				)
+			} )
+		).attr('action', '/user').appendTo('#text');
+	}, dberror => {
+		console.log( '- Dashboard: Error while getting the OAuth2 info: ' + dberror );
+		createNotice($, 'error', dashboardLang);
+		$('<p>').text(dashboardLang.get('oauth.failed')).appendTo('#text .description');
+	} ).then( () => {
+		let body = $.html();
+		res.writeHead(200, {'Content-Length': Buffer.byteLength(body)});
+		res.write( body );
+		return res.end();
+	} );
+}
+
+/**
+ * Change settings
+ * @param {Function} res - The server response
+ * @param {String} user_id - The current user
+ * @param {String} type - The setting to change
+ * @param {String} oauth_id - The OAuth2 site to change
+ */
+function update_user(res, user_id, type, oauth_id) {
+	if ( !['connect', 'disconnect', 'disable', 'enable'].includes( type ) || !enabledOAuth2.some( oauthSite => oauthSite.id === oauth_id ) ) {
+		return res('/user', 'savefail');
+	}
+	if ( type === 'disconnect' || type === 'enable' ) return db.query( 'DELETE FROM oauthusers WHERE userid = $1 AND site = $2', [user_id, oauth_id] ).then( () => {
+		if ( type === 'disconnect' ) console.log( '- Dashboard: Successfully disconnected ' + user_id + ' from ' + oauth_id + '.' );
+		else console.log( '- Dashboard: Successfully enabled ' + oauth_id + ' for ' + user_id + '.' );
+		return res('/user', 'save');
+	}, dberror => {
+		if ( type === 'disconnect' ) console.log( '- Dashboard: Error while disconnecting ' + user_id + ' from ' + oauth_id + ': ' + dberror );
+		else console.log( '- Dashboard: Error while enabling ' + oauth_id + ' for ' + user_id + ': ' + dberror );
+		return res('/user', 'savefail');
+	} );
+	return db.query( 'SELECT FROM oauthusers WHERE userid = $1 AND site = $2', [user_id, oauth_id] ).then( ({rows:[row]}) => {
+		if ( type === 'disable' ) {
+			let sql = 'INSERT INTO oauthusers(userid, site, token) VALUES($1, $2, $3)';
+			if ( row ) sql = 'UPDATE oauthusers SET token = $3 WHERE userid = $1 AND site = $2';
+			return db.query( sql, [user_id, oauth_id, null] ).then( () => {
+				console.log( '- Dashboard: Successfully disabled ' + oauth_id + ' for ' + user_id + '.' );
+				return res('/user', 'save');
+			}, dberror => {
+				console.log( '- Dashboard: Error while disabling ' + oauth_id + ' for ' + user_id + ': ' + dberror );
+				return res('/user', 'savefail');
+			} );
+		}
+		if ( type !== 'connect' ) return res('/user', 'savefail');
+		var oauthSite = enabledOAuth2.find( oauthSite => oauthSite.id === oauth_id );
+		if ( row ) db.query( 'DELETE FROM oauthusers WHERE userid = $1 AND site = $2', [user_id, oauth_id] ).then( () => {
+			console.log( '- Dashboard: Successfully disconnected ' + user_id + ' from ' + oauth_id + ' for reconnection.' );
+		}, dberror => {
+			console.log( '- Dashboard: Error while disconnecting ' + user_id + ' from ' + oauth_id + ' for reconnection: ' + dberror );
+		} );
+		let oauthURL = oauthSite.url + 'rest.php/oauth2/authorize?' + new URLSearchParams({
+			response_type: 'code', redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
+			client_id: process.env['oauth_' + oauthSite.id], state: oauthSite.id
+		}).toString();
+		return res(oauthURL, 'REDIRECT');
+	}, dberror => {
+		console.log( '- Dashboard: Error while getting the OAuth2 info on ' + oauth_id + ' for ' + user_id + ': ' + dberror );
+		return res('/user', 'savefail');
+	} );
+}
+
+module.exports = {
+	get: dashboard_user,
+	post: update_user
+};

+ 66 - 10
dashboard/util.js

@@ -2,7 +2,7 @@ const got = require('got').extend( {
 	throwHttpErrors: false,
 	timeout: 5000,
 	headers: {
-		'User-Agent': 'Wiki-Bot/' + ( isDebug ? 'testing' : process.env.npm_package_version ) + '/dashboard (Discord; ' + process.env.npm_package_name + ')'
+		'User-Agent': 'Wiki-Bot/' + ( isDebug ? 'testing' : process.env.npm_package_version ) + '/dashboard (Discord; ' + process.env.npm_package_name + ( process.env.invite ? '; ' + process.env.invite : '' ) + ')'
 	},
 	responseType: 'json'
 } );
@@ -18,6 +18,36 @@ const oauth = new DiscordOauth2( {
 	redirectUri: process.env.dashboard
 } );
 
+const {oauthSites} = require('../util/wiki.js');
+
+const enabledOAuth2 = [
+	...oauthSites.filter( oauthSite => {
+		let site = new URL(oauthSite);
+		site = site.hostname + site.pathname.slice(0, -1);
+		return ( process.env[`oauth_${site}`] && process.env[`oauth_${site}_secret`] );
+	} ).map( oauthSite => {
+		let site = new URL(oauthSite);
+		return {
+			id: site.hostname + site.pathname.slice(0, -1),
+			name: oauthSite, url: oauthSite,
+		};
+	} )
+];
+if ( process.env.oauth_miraheze && process.env.oauth_miraheze_secret ) {
+	enabledOAuth2.unshift({
+		id: 'miraheze',
+		name: 'Miraheze',
+		url: 'https://meta.miraheze.org/w/',
+	});
+}
+if ( process.env.oauth_wikimedia && process.env.oauth_wikimedia_secret ) {
+	enabledOAuth2.unshift({
+		id: 'wikimedia',
+		name: 'Wikimedia (Wikipedia)',
+		url: 'https://meta.wikimedia.org/w/',
+	});
+}
+
 const slashCommands = require('../interactions/commands.json');
 
 got.get( `https://discord.com/api/v8/applications/${process.env.bot}/commands`, {
@@ -110,9 +140,9 @@ const sessionData = new Map();
 const settingsData = new Map();
 
 /**
- * @type {Set<String>}
+ * @type {Map<String, String>}
  */
-const oauthVerify = new Set();
+const oauthVerify = new Map();
 
 /**
  * @type {Map<Number, PromiseConstructor>}
@@ -121,7 +151,7 @@ const messages = new Map();
 var messageId = 1;
 
 process.on( 'message', message => {
-	if ( message?.id === 'verifyUser' ) return oauthVerify.add(message.state);
+	if ( message?.id === 'verifyUser' ) return oauthVerify.set(message.state, message.user);
 	if ( message?.id ) {
 		if ( message.data.error ) messages.get(message.id).reject(message.data.error);
 		else messages.get(message.id).resolve(message.data.response);
@@ -160,14 +190,14 @@ if ( process.env.botlist ) {
 			link: 'https://bots.ondiscord.xyz/bots/' + process.env.bot,
 			widget: 'https://bots.ondiscord.xyz/bots/' + process.env.bot + '/embed?theme=dark&showGuilds=true'
 		},
-		'botsfordiscord.com': {
-			link: 'https://botsfordiscord.com/bots/' + process.env.bot,
-			widget: 'https://botsfordiscord.com/api/bot/' + process.env.bot + '/widget?theme=dark'
-		},
 		'discord.boats': {
 			link: 'https://discord.boats/bot/' + process.env.bot,
 			widget: 'https://discord.boats/api/widget/' + process.env.bot
 		},
+		'discords.com': {
+			link: 'https://discords.com/bots/bot/' + process.env.bot,
+			widget: 'https://discords.com/bots/api/bot/' + process.env.bot + '/widget?theme=dark'
+		},
 		'infinitybotlist.com': {
 			link: 'https://infinitybotlist.com/bots/' + process.env.bot,
 			widget: 'https://infinitybotlist.com/bots/' + process.env.bot + '/widget?size=medium'
@@ -271,6 +301,32 @@ function createNotice($, notice, dashboardLang, args = []) {
 			text.text(dashboardLang.get('notice.mwversion.text', false, args[0], args[1]));
 			note = $('<a target="_blank">').text('https://www.mediawiki.org/wiki/MediaWiki_1.30').attr('href', 'https://www.mediawiki.org/wiki/MediaWiki_1.30');
 			break;
+		case 'oauth':
+			type = 'success';
+			title.text(dashboardLang.get('notice.oauth.title'));
+			text.text(dashboardLang.get('notice.oauth.text'));
+			break;
+		case 'oauthfail':
+			type = 'error';
+			title.text(dashboardLang.get('notice.oauthfail.title'));
+			text.text(dashboardLang.get('notice.oauthfail.text'));
+			break;
+		case 'oauthverify':
+			type = 'success';
+			title.text(dashboardLang.get('notice.oauthverify.title'));
+			text.text(dashboardLang.get('notice.oauthverify.text'));
+			break;
+		case 'oauthother':
+			type = 'info';
+			title.text(dashboardLang.get('notice.oauthother.title'));
+			text.text(dashboardLang.get('notice.oauthother.text'));
+			note = $('<a>').text(dashboardLang.get('notice.oauthother.note')).attr('href', args[0]);
+			break;
+		case 'oauthlogin':
+			type = 'info';
+			title.text(dashboardLang.get('notice.oauthlogin.title'));
+			text.text(dashboardLang.get('notice.oauthlogin.text'));
+			break;
 		case 'nochange':
 			type = 'info';
 			title.text(dashboardLang.get('notice.nochange.title'));
@@ -290,7 +346,7 @@ function createNotice($, notice, dashboardLang, args = []) {
 			type = 'error';
 			title.text(dashboardLang.get('notice.noslash.title'));
 			text.text(dashboardLang.get('notice.noslash.text'));
-			note = $('<a target="_blank">').text(dashboardLang.get('notice.noslash.note')).attr('href', `https://discord.com/api/oauth2/authorize?client_id=${process.env.bot}&scope=applications.commands&guild_id=${args[0]}`);
+			note = $('<a target="_blank">').text(dashboardLang.get('notice.noslash.note')).attr('href', `https://discord.com/api/oauth2/authorize?client_id=${process.env.bot}&scope=applications.commands&guild_id=${args[0]}&disable_guild_select=true`);
 			break;
 		case 'wikiblocked':
 			type = 'error';
@@ -381,4 +437,4 @@ function hasPerm(all = 0, ...permission) {
 	} );
 }
 
-module.exports = {got, db, oauth, slashCommands, sessionData, settingsData, oauthVerify, sendMsg, addWidgets, createNotice, escapeText, hasPerm};
+module.exports = {got, db, oauth, enabledOAuth2, slashCommands, sessionData, settingsData, oauthVerify, sendMsg, addWidgets, createNotice, escapeText, hasPerm};

+ 37 - 2
database.js

@@ -99,6 +99,21 @@ CREATE INDEX IF NOT EXISTS idx_verifynotice_guild ON verifynotice (
     guild
 );
 
+CREATE TABLE IF NOT EXISTS oauthusers (
+    userid TEXT NOT NULL,
+    site   TEXT NOT NULL,
+    token  TEXT,
+    UNIQUE (
+        userid,
+        site
+    )
+);
+
+CREATE INDEX IF NOT EXISTS idx_oauthusers_userid ON oauthusers (
+    userid,
+    site
+);
+
 CREATE TABLE IF NOT EXISTS rcgcdw (
     guild    TEXT    NOT NULL
                      REFERENCES discord (main) ON DELETE CASCADE,
@@ -142,14 +157,14 @@ CREATE INDEX IF NOT EXISTS idx_blocklist_wiki ON blocklist (
 );
 
 COMMIT TRANSACTION;
-ALTER DATABASE "${process.env.PGDATABASE}" SET my.version TO 1;
+ALTER DATABASE "${process.env.PGDATABASE}" SET my.version TO 4;
 `,`
 BEGIN TRANSACTION;
 
-CREATE TABLE IF NOT EXISTS verifynotice (
     guild      TEXT    UNIQUE
                        NOT NULL
                        REFERENCES discord (main) ON DELETE CASCADE,
+CREATE TABLE IF NOT EXISTS verifynotice (
     logchannel TEXT,
     onsuccess  TEXT,
     onmatch    TEXT
@@ -169,6 +184,26 @@ ADD COLUMN IF NOT EXISTS flags INTEGER NOT NULL DEFAULT 0;
 
 COMMIT TRANSACTION;
 ALTER DATABASE "${process.env.PGDATABASE}" SET my.version TO 3;
+`,`
+BEGIN TRANSACTION;
+
+CREATE TABLE IF NOT EXISTS oauthusers (
+    userid TEXT NOT NULL,
+    site   TEXT NOT NULL,
+    token  TEXT,
+    UNIQUE (
+        userid,
+        site
+    )
+);
+
+CREATE INDEX IF NOT EXISTS idx_oauthusers_userid ON oauthusers (
+    userid,
+    site
+);
+
+COMMIT TRANSACTION;
+ALTER DATABASE "${process.env.PGDATABASE}" SET my.version TO 4;
 `];
 
 module.exports = db.connect().then( () => {

+ 28 - 20
functions/discussion.js

@@ -1,7 +1,7 @@
 const htmlparser = require('htmlparser2');
 const {MessageEmbed, Util} = require('discord.js');
 const {limit: {discussion: discussionLimit}} = require('../util/default.json');
-const {htmlToDiscord, escapeFormatting} = require('../util/functions.js');
+const {got, htmlToDiscord, escapeFormatting} = require('../util/functions.js');
 
 /**
  * Processes discussion commands.
@@ -12,11 +12,18 @@ const {htmlToDiscord, escapeFormatting} = require('../util/functions.js');
  * @param {String} sitename - The sitename of the wiki.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  * @param {String} spoiler - If the response is in a spoiler.
+ * @param {Boolean} noEmbed - If the response should be without an embed.
  */
-function fandom_discussion(lang, msg, wiki, title, sitename, reaction, spoiler) {
-	var limit = discussionLimit[( patreons[msg.guild?.id] ? 'patreon' : 'default' )];
+function fandom_discussion(lang, msg, wiki, title, sitename, reaction, spoiler, noEmbed) {
+	var limit = discussionLimit[( patreons[msg.guildId] ? 'patreon' : 'default' )];
 	if ( !title ) {
 		var pagelink = wiki + 'f';
+		if ( !msg.showEmbed() || noEmbed ) {
+			msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler );
+			
+			if ( reaction ) reaction.removeEmoji();
+			return;
+		}
 		var embed = new MessageEmbed().setAuthor( sitename ).setTitle( lang.get('discussion.main') ).setURL( pagelink );
 		got.get( wiki + 'f', {
 			responseType: 'text'
@@ -45,7 +52,7 @@ function fandom_discussion(lang, msg, wiki, title, sitename, reaction, spoiler)
 		}, error => {
 			console.log( '- Error while getting the description: ' + error );
 		} ).finally( () => {
-			msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
+			msg.sendChannel( {content: spoiler + '<' + pagelink + '>' + spoiler, embeds: [embed]} );
 			
 			if ( reaction ) reaction.removeEmoji();
 		} );
@@ -69,7 +76,7 @@ function fandom_discussion(lang, msg, wiki, title, sitename, reaction, spoiler)
 				var embed = new MessageEmbed().setAuthor( sitename );
 				
 				if ( posts.some( post => post.id === title ) ) {
-					discussion_send(lang, msg, wiki, posts.find( post => post.id === title ), embed, spoiler);
+					discussion_send(lang, msg, wiki, posts.find( post => post.id === title ), embed, spoiler, noEmbed);
 					
 					if ( reaction ) reaction.removeEmoji();
 				}
@@ -83,7 +90,7 @@ function fandom_discussion(lang, msg, wiki, title, sitename, reaction, spoiler)
 						if ( presponse.statusCode !== 200 || !pbody || pbody.id !== title ) {
 							if ( pbody && pbody.title === 'The requested resource was not found.' ) {
 								if ( posts.some( post => post.rawContent.toLowerCase().includes( title.toLowerCase() ) ) ) {
-									discussion_send(lang, msg, wiki, posts.find( post => post.rawContent.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler);
+									discussion_send(lang, msg, wiki, posts.find( post => post.rawContent.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler, noEmbed);
 								}
 								else msg.reactEmoji('🤷');
 							}
@@ -95,7 +102,7 @@ function fandom_discussion(lang, msg, wiki, title, sitename, reaction, spoiler)
 							if ( reaction ) reaction.removeEmoji();
 						}
 						else if ( pbody.title ) {
-							discussion_send(lang, msg, wiki, pbody, embed, spoiler);
+							discussion_send(lang, msg, wiki, pbody, embed, spoiler, noEmbed);
 							
 							if ( reaction ) reaction.removeEmoji();
 						}
@@ -114,7 +121,7 @@ function fandom_discussion(lang, msg, wiki, title, sitename, reaction, spoiler)
 							console.log( '- Error while getting the thread: ' + error );
 							embed.setTitle( '~~' + pbody.threadId + '~~' );
 						} ).finally( () => {
-							discussion_send(lang, msg, wiki, pbody, embed, spoiler);
+							discussion_send(lang, msg, wiki, pbody, embed, spoiler, noEmbed);
 							
 							if ( reaction ) reaction.removeEmoji();
 						} );
@@ -126,7 +133,7 @@ function fandom_discussion(lang, msg, wiki, title, sitename, reaction, spoiler)
 					} );
 				}
 				else if ( posts.some( post => post.rawContent.toLowerCase().includes( title.toLowerCase() ) ) ) {
-					discussion_send(lang, msg, wiki, posts.find( post => post.rawContent.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler);
+					discussion_send(lang, msg, wiki, posts.find( post => post.rawContent.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler, noEmbed);
 					
 					if ( reaction ) reaction.removeEmoji();
 				}
@@ -166,27 +173,27 @@ function fandom_discussion(lang, msg, wiki, title, sitename, reaction, spoiler)
 				var embed = new MessageEmbed().setAuthor( sitename );
 				
 				if ( threads.some( thread => thread.id === title ) ) {
-					discussion_send(lang, msg, wiki, threads.find( thread => thread.id === title ), embed, spoiler);
+					discussion_send(lang, msg, wiki, threads.find( thread => thread.id === title ), embed, spoiler, noEmbed);
 					
 					if ( reaction ) reaction.removeEmoji();
 				}
 				else if ( threads.some( thread => thread.title === title ) ) {
-					discussion_send(lang, msg, wiki, threads.find( thread => thread.title === title ), embed, spoiler);
+					discussion_send(lang, msg, wiki, threads.find( thread => thread.title === title ), embed, spoiler, noEmbed);
 					
 					if ( reaction ) reaction.removeEmoji();
 				}
 				else if ( threads.some( thread => thread.title.toLowerCase() === title.toLowerCase() ) ) {
-					discussion_send(lang, msg, wiki, threads.find( thread => thread.title.toLowerCase() === title.toLowerCase() ), embed, spoiler);
+					discussion_send(lang, msg, wiki, threads.find( thread => thread.title.toLowerCase() === title.toLowerCase() ), embed, spoiler, noEmbed);
 					
 					if ( reaction ) reaction.removeEmoji();
 				}
 				else if ( threads.some( thread => thread.title.includes( title ) ) ) {
-					discussion_send(lang, msg, wiki, threads.find( thread => thread.title.includes( title ) ), embed, spoiler);
+					discussion_send(lang, msg, wiki, threads.find( thread => thread.title.includes( title ) ), embed, spoiler, noEmbed);
 					
 					if ( reaction ) reaction.removeEmoji();
 				}
 				else if ( threads.some( thread => thread.title.toLowerCase().includes( title.toLowerCase() ) ) ) {
-					discussion_send(lang, msg, wiki, threads.find( thread => thread.title.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler);
+					discussion_send(lang, msg, wiki, threads.find( thread => thread.title.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler, noEmbed);
 					
 					if ( reaction ) reaction.removeEmoji();
 				}
@@ -200,7 +207,7 @@ function fandom_discussion(lang, msg, wiki, title, sitename, reaction, spoiler)
 						if ( thresponse.statusCode !== 200 || !thbody || thbody.id !== title ) {
 							if ( thbody && thbody.status === 404 ) {
 								if (threads.some( thread => thread.rawContent.toLowerCase().includes( title.toLowerCase() ) ) ) {
-									discussion_send(lang, msg, wiki, threads.find( thread => thread.rawContent.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler);
+									discussion_send(lang, msg, wiki, threads.find( thread => thread.rawContent.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler, noEmbed);
 								}
 								else msg.reactEmoji('🤷');
 							}
@@ -209,7 +216,7 @@ function fandom_discussion(lang, msg, wiki, title, sitename, reaction, spoiler)
 								msg.sendChannelError( spoiler + '<' + wiki + 'f/p/' + title + '>' + spoiler );
 							}
 						}
-						else discussion_send(lang, msg, wiki, thbody, embed, spoiler);
+						else discussion_send(lang, msg, wiki, thbody, embed, spoiler, noEmbed);
 					}, error => {
 						console.log( '- Error while getting the thread: ' + error );
 						msg.sendChannelError( spoiler + '<' + wiki + 'f/p/' + title + '>' + spoiler );
@@ -218,7 +225,7 @@ function fandom_discussion(lang, msg, wiki, title, sitename, reaction, spoiler)
 					} );
 				}
 				else if ( threads.some( thread => thread.rawContent.toLowerCase().includes( title.toLowerCase() ) ) ) {
-					discussion_send(lang, msg, wiki, threads.find( thread => thread.rawContent.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler);
+					discussion_send(lang, msg, wiki, threads.find( thread => thread.rawContent.toLowerCase().includes( title.toLowerCase() ) ), embed, spoiler, noEmbed);
 					
 					if ( reaction ) reaction.removeEmoji();
 				}
@@ -250,8 +257,9 @@ function fandom_discussion(lang, msg, wiki, title, sitename, reaction, spoiler)
  * @param {Object} discussion - The discussion post.
  * @param {import('discord.js').MessageEmbed} embed - The embed for the page.
  * @param {String} spoiler - If the response is in a spoiler.
+ * @param {Boolean} noEmbed - If the response should be without an embed.
  */
-function discussion_send(lang, msg, wiki, discussion, embed, spoiler) {
+function discussion_send(lang, msg, wiki, discussion, embed, spoiler, noEmbed) {
 	if ( discussion.title ) {
 		embed.setTitle( escapeFormatting(discussion.title) );
 		var pagelink = wiki + 'f/p/' + ( discussion.threadId || discussion.id );
@@ -260,7 +268,7 @@ function discussion_send(lang, msg, wiki, discussion, embed, spoiler) {
 		if ( discussion._embedded.thread ) embed.setTitle( escapeFormatting(discussion._embedded.thread[0].title) );
 		var pagelink = wiki + 'f/p/' + discussion.threadId + '/r/' + discussion.id;
 	}
-	var text = '<' + pagelink + '>';
+	if ( !msg.showEmbed() || noEmbed ) msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler );
 	embed.setURL( pagelink ).setFooter( discussion.createdBy.name, discussion.createdBy.avatarUrl ).setTimestamp( discussion.creationDate.epochSecond * 1000 );
 	var description = '';
 	switch ( discussion.funnel ) {
@@ -314,7 +322,7 @@ function discussion_send(lang, msg, wiki, discussion, embed, spoiler) {
 		embed.addField( lang.get('discussion.tags'), Util.splitMessage( discussion.tags.map( tag => '[' + escapeFormatting(tag.articleTitle) + '](' + wiki.toLink(tag.articleTitle, '', '', true) + ')' ).join(', '), {char:', ',maxLength:1000} )[0], false );
 	}
 	
-	msg.sendChannel( spoiler + text + spoiler, {embed} );
+	msg.sendChannel( {content: spoiler + '<' + pagelink + '>' + spoiler, embeds: [embed]} );
 }
 
 /**

+ 9 - 9
functions/global_block.js

@@ -1,5 +1,5 @@
 const cheerio = require('cheerio');
-const {escapeFormatting} = require('../util/functions.js');
+const {got, escapeFormatting} = require('../util/functions.js');
 
 /**
  * Add global blocks to user messages.
@@ -13,7 +13,7 @@ const {escapeFormatting} = require('../util/functions.js');
  * @param {String} [gender] - The gender of the user.
  */
 function global_block(lang, msg, username, text, embed, wiki, spoiler, gender) {
-	if ( !msg || !msg.channel.isGuild() || !patreons[msg.guild?.id] || !wiki.isFandom() ) return;
+	if ( !msg || !msg.channel.isGuild() || !patreons[msg.guildId] || !wiki.isFandom() ) return;
 	
 	var isUser = true;
 	if ( !gender ) {
@@ -21,7 +21,7 @@ function global_block(lang, msg, username, text, embed, wiki, spoiler, gender) {
 		gender = 'unknown';
 	}
 	
-	if ( msg.showEmbed() ) embed.fields.pop();
+	if ( embed && msg.showEmbed() ) embed.fields.pop();
 	else {
 		let splittext = text.split('\n\n');
 		splittext.pop();
@@ -39,11 +39,11 @@ function global_block(lang, msg, username, text, embed, wiki, spoiler, gender) {
 			else {
 				let $ = cheerio.load(body);
 				if ( $('#mw-content-text .errorbox').length ) {
-					if ( msg.showEmbed() ) embed.addField( '\u200b', '**' + lang.get('user.gblock.disabled') + '**' );
+					if ( embed && msg.showEmbed() ) embed.addField( '\u200b', '**' + lang.get('user.gblock.disabled') + '**' );
 					else text += '\n\n**' + lang.get('user.gblock.disabled') + '**';
 				}
 				else if ( $('#mw-content-text .userprofile.mw-warning-with-logexcerpt').length ) {
-					if ( msg.showEmbed() ) embed.addField( '\u200b', '**' + lang.get('user.gblock.header', escapeFormatting(username), gender) + '**' );
+					if ( embed && msg.showEmbed() ) embed.addField( '\u200b', '**' + lang.get('user.gblock.header', escapeFormatting(username), gender) + '**' );
 					else text += '\n\n**' + lang.get('user.gblock.header', escapeFormatting(username), gender) + '**';
 				}
 			}
@@ -62,7 +62,7 @@ function global_block(lang, msg, username, text, embed, wiki, spoiler, gender) {
 				var wikisedited = $('.curseprofile .rightcolumn .section.stats dd').eq(0).text().replace( /[,\.]/g, '' );
 				if ( wikisedited ) {
 					wikisedited = parseInt(wikisedited, 10).toLocaleString(lang.get('dateformat'));
-					if ( msg.showEmbed() ) embed.spliceFields(1, 0, {
+					if ( embed && msg.showEmbed() ) embed.spliceFields(1, 0, {
 						name: lang.get('user.info.wikisedited'),
 						value: wikisedited,
 						inline: true
@@ -76,7 +76,7 @@ function global_block(lang, msg, username, text, embed, wiki, spoiler, gender) {
 				var globaledits = $('.curseprofile .rightcolumn .section.stats dd').eq(2).text().replace( /[,\.]/g, '' );
 				if ( globaledits ) {
 					globaledits = parseInt(globaledits, 10).toLocaleString(lang.get('dateformat'));
-					if ( msg.showEmbed() ) embed.spliceFields(1, 0, {
+					if ( embed && msg.showEmbed() ) embed.spliceFields(1, 0, {
 						name: lang.get('user.info.globaleditcount'),
 						value: globaledits,
 						inline: true
@@ -87,7 +87,7 @@ function global_block(lang, msg, username, text, embed, wiki, spoiler, gender) {
 						text = splittext.join('\n');
 					}
 				}
-				if ( msg.showEmbed() ) {
+				if ( embed && msg.showEmbed() ) {
 					let avatar = $('.curseprofile .mainavatar img').prop('src');
 					if ( avatar ) {
 						embed.setThumbnail( avatar.replace( /^(?:https?:)?\/\//, 'https://' ).replace( '?d=mm&s=96', '?d=' + encodeURIComponent( embed?.thumbnail?.url || '404' ) ) );
@@ -98,7 +98,7 @@ function global_block(lang, msg, username, text, embed, wiki, spoiler, gender) {
 			console.log( '- Error while getting the global edit count: ' + error );
 		} ) : undefined )
 	]).finally( () => {
-		msg.edit( spoiler + text + spoiler, {embed,allowedMentions:{parse:[]}} ).catch(log_error);
+		msg.edit( {content: spoiler + text + spoiler, embeds: [embed]} ).catch(log_error);
 	} );
 }
 

+ 1 - 1
functions/helpsetup.js

@@ -5,7 +5,7 @@
  */
 function help_setup(lang, msg) {
 	msg.defaultSettings = false;
-	msg.replyMsg( lang.get('general.default', '`' + process.env.prefix + 'settings`') + ( process.env.dashboard ? '\n' + new URL(`/guild/${msg.guild.id}/settings`, process.env.dashboard).href : '' ) );
+	msg.replyMsg( lang.get('general.default', '`' + process.env.prefix + 'settings`') + ( process.env.dashboard ? '\n' + new URL(`/guild/${msg.guildId}/settings`, process.env.dashboard).href : '' ) );
 }
 
 module.exports = help_setup;

+ 8 - 6
functions/parse_page.js

@@ -1,7 +1,7 @@
 const cheerio = require('cheerio');
 const {MessageEmbed} = require('discord.js');
 const {toSection} = require('../util/wiki.js');
-const {parse_infobox, htmlToPlain, htmlToDiscord, escapeFormatting, limitLength} = require('../util/functions.js');
+const {got, parse_infobox, htmlToPlain, htmlToDiscord, escapeFormatting, limitLength} = require('../util/functions.js');
 
 const parsedContentModels = [
 	'wikitext',
@@ -60,6 +60,7 @@ const removeClassesExceptions = [
 	'div.mw-highlight',
 	'div.poem',
 	'div.treeview',
+	'div.redirectMsg',
 	'div.wikibase-entityview',
 	'div.wikibase-entityview-main',
 	'div.wikibase-entitytermsview',
@@ -103,10 +104,11 @@ function parse_page(lang, msg, content, embed, wiki, reaction, {title, contentmo
 				embed.setDescription( embed.backupDescription );
 			}
 		}
-		return msg.sendChannel( content, {embed} );
+		return msg.sendChannel( {content: content, embeds: [embed]} );
 	}
-	return msg.sendChannel( content, {
-		embed: new MessageEmbed(embed).setDescription( '<a:loading:641343250661113886> **' + lang.get('search.loading') + '**' )
+	return msg.sendChannel( {
+		content,
+		embeds: [new MessageEmbed(embed).setDescription( '<a:loading:641343250661113886> **' + lang.get('search.loading') + '**' )]
 	} ).then( message => {
 		if ( !message ) return;
 		if ( !parsedContentModels.includes( contentmodel ) ) return got.get( wiki + 'api.php?action=query&prop=revisions&rvprop=content&rvslots=main&converttitles=true&titles=%1F' + encodeURIComponent( title ) + '&format=json', {
@@ -159,7 +161,7 @@ function parse_page(lang, msg, content, embed, wiki, reaction, {title, contentmo
 				embed.setDescription( embed.backupDescription );
 			}
 		} ).then( () => {
-			return message.edit( content, {embed,allowedMentions:{users:[msg.author.id]}} ).catch(log_error);
+			return message.edit( {content, embeds: [embed]} ).catch(log_error);
 		} );
 		if ( !fragment && !embed.fields.length && infoboxes ) {
 			try {
@@ -392,7 +394,7 @@ function parse_page(lang, msg, content, embed, wiki, reaction, {title, contentmo
 				embed.spliceFields( 0, 0, embed.backupField );
 			}
 		} ).then( () => {
-			return message.edit( content, {embed,allowedMentions:{users:[msg.author.id]}} ).catch(log_error);
+			return message.edit( {content, embeds: [embed]} ).catch(log_error);
 		} );
 	} );
 }

+ 15 - 11
functions/phabricator.js

@@ -1,6 +1,6 @@
 const {MessageEmbed} = require('discord.js');
 const logging = require('../util/logging.js');
-const {escapeFormatting, limitLength} = require('../util/functions.js');
+const {got, escapeFormatting, limitLength} = require('../util/functions.js');
 
 /**
  * Sends a Phabricator task.
@@ -10,35 +10,36 @@ const {escapeFormatting, limitLength} = require('../util/functions.js');
  * @param {URL} link - The link.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  * @param {String} [spoiler] - If the response is in a spoiler.
+ * @param {Boolean} [noEmbed] - If the response should be without an embed.
  */
-function phabricator_task(lang, msg, wiki, link, reaction, spoiler = '') {
+function phabricator_task(lang, msg, wiki, link, reaction, spoiler = '', noEmbed = false) {
 	var regex = /^(?:https?:)?\/\/phabricator\.(wikimedia|miraheze)\.org\/T(\d+)(?:#|$)/.exec(link.href);
 	if ( !regex || !process.env['phabricator_' + regex[1]] ) {
-		logging(wiki, msg.guild?.id, 'interwiki');
-		msg.sendChannel( spoiler + ' ' + link + ' ' + spoiler );
+		logging(wiki, msg.guildId, 'interwiki');
+		msg.sendChannel( spoiler + ( noEmbed ? '<' : ' ' ) + link + ( noEmbed ? '>' : ' ' ) + spoiler );
 		if ( reaction ) reaction.removeEmoji();
 		return;
 	}
 	var site = 'https://phabricator.' + regex[1] + '.org/';
-	logging(site, msg.guild?.id, 'phabricator', regex[1]);
+	logging(site, msg.guildId, 'phabricator', regex[1]);
 	got.get( site + 'api/maniphest.search?api.token=' + process.env['phabricator_' + regex[1]] + '&attachments[projects]=1&constraints[ids][0]=' + regex[2] ).then( response => {
 		var body = response.body;
 		if ( response.statusCode !== 200 || !body?.result?.data || body.error_code ) {
 			console.log( '- ' + response.statusCode + ': Error while getting the Phabricator task: ' + body?.error_info );
-			msg.sendChannelError( spoiler + ' ' + link + ' ' + spoiler );
+			msg.sendChannelError( spoiler + ( noEmbed ? '<' : ' ' ) + link + ( noEmbed ? '>' : ' ' ) + spoiler );
 
 			if ( reaction ) reaction.removeEmoji();
 			return;
 		}
 		if ( !body.result.data.length ) {
-			msg.sendChannel( spoiler + ' ' + link + ' ' + spoiler );
+			msg.sendChannel( spoiler + ( noEmbed ? '<' : ' ' ) + link + ( noEmbed ? '>' : ' ' ) + spoiler );
 
 			if ( reaction ) reaction.removeEmoji();
 			return;
 		}
 		var task = body.result.data[0];
 		var status = '**' + task.fields.status.name + ':** ' + escapeFormatting(task.fields.name) + '\n';
-		if ( !msg.showEmbed() ) {
+		if ( !msg.showEmbed() || noEmbed ) {
 			msg.sendChannel( spoiler + status + '<' + link + '>' + spoiler );
 			
 			if ( reaction ) reaction.removeEmoji();
@@ -80,18 +81,19 @@ function phabricator_task(lang, msg, wiki, link, reaction, spoiler = '') {
 					var content = parse_text( comment.comments[0].content.raw, site );
 					if ( content.length > 1000 ) content = limitLength(content, 1000, 20);
 					embed.spliceFields( 0, 0, {name: 'Comment', value: content} );
+					if ( embed.description.length > 500 ) embed.setDescription( limitLength(description, 500, 250) );
 				}
 			}, error => {
 				console.log( '- Error while getting the task transactions: ' + error );
 			} ) : undefined )
 		]).finally( () => {
-			msg.sendChannel( spoiler + status + '<' + link + '>' + spoiler, {embed} );
+			msg.sendChannel( {content: spoiler + status + '<' + link + '>' + spoiler, embeds: [embed]} );
 			
 			if ( reaction ) reaction.removeEmoji();
 		} );
 	}, error => {
 		console.log( '- Error while getting the Phabricator task: ' + error );
-		msg.sendChannelError( spoiler + ' ' + link + ' ' + spoiler );
+		msg.sendChannelError( spoiler + ( noEmbed ? '<' : ' ' ) + link + ( noEmbed ? '>' : ' ' ) + spoiler );
 
 		if ( reaction ) reaction.removeEmoji();
 	} );
@@ -105,9 +107,11 @@ function phabricator_task(lang, msg, wiki, link, reaction, spoiler = '') {
  */
 function parse_text(text, site) {
 	text = text.replace( /```lang=/g, '```' );
+	text = text.replace( /^>>! (.+?)$/gm, '> *$1*' );
+	text = text.replace( /^>>/gm, '> >' );
 	text = text.replace( /##(.+?)##/g, '`$1`' );
 	text = text.replace( /!!(.+?)!!/g, '`$1`' );
-	text = text.replace( /\/\/(.+?)\/\//g, '*$1*' );
+	text = text.replace( /(?<!https?:)\/\/(.+?)(?<!https?:)\/\//g, '*$1*' );
 	text = text.replace( /\[\[ ?(.+?) ?(?:\| ?(.+?) ?)?\]\]/g, (match, target, display) => {
 		var link = target;
 		if ( /^(?:(?:https?:)?\/\/|\/|#)/.test(target) ) link = new URL(target, site).href;

+ 71 - 26
functions/special_page.js

@@ -1,17 +1,37 @@
-const {Util} = require('discord.js');
+const {MessageEmbed, Util} = require('discord.js');
 const logging = require('../util/logging.js');
 const {timeoptions} = require('../util/default.json');
-const {toMarkdown, escapeFormatting} = require('../util/functions.js');
+const {got, toMarkdown, escapeFormatting} = require('../util/functions.js');
 
 const overwrites = {
-	randompage: (fn, lang, msg, wiki, reaction, spoiler) => {
-		fn.random(lang, msg, wiki, reaction, spoiler);
+	randompage: (fn, lang, msg, wiki, querystring, fragment, reaction, spoiler, noEmbed, args, embed, query) => {
+		let namespaces = Object.values(query.namespaces);
+		let contentNamespaces = namespaces.filter( ns => ns.content !== undefined );
+		let namespaceData = [contentNamespaces.map( ns => ns.id ).join('|'), contentNamespaces.map( ns => ( ns['*'] || '*' )  ).join(', ')];
+		if ( args[0] ) {
+			args[0] = args[0].replace( /_/g, ' ' ).toLowerCase().trim();
+			let namespaceMap = {};
+			namespaces.forEach( namespace => {
+				if ( namespace.id < 0 ) return;
+				if ( namespace.canonical ) namespaceMap[namespace.canonical.toLowerCase()] = namespace.id;
+				namespaceMap[namespace['*'].toLowerCase()] = namespace.id;
+			} );
+			query.namespacealiases.forEach( namespace => {
+				if ( namespace.id < 0 ) return;
+				namespaceMap[namespace['*'].toLowerCase()] = namespace.id;
+			} );
+			if ( namespaceMap.hasOwnProperty(args[0]) ) {
+				namespaceData = [namespaceMap[args[0]].toString(), ( namespaces.find( namespace => namespace.id === namespaceMap[args[0]] )?.['*'] || '*' )];
+			}
+			else if ( args[0] === '*' ) namespaceData = ['*', '*'];
+		}
+		fn.random(lang, msg, wiki, reaction, spoiler, noEmbed, namespaceData, querystring, fragment, embed);
 	},
-	statistics: (fn, lang, msg, wiki, reaction, spoiler) => {
-		fn.overview(lang, msg, wiki, reaction, spoiler);
+	statistics: (fn, lang, msg, wiki, querystring, fragment, reaction, spoiler, noEmbed) => {
+		fn.overview(lang, msg, wiki, reaction, spoiler, noEmbed, querystring, fragment);
 	},
-	diff: (fn, lang, msg, wiki, reaction, spoiler, args, embed) => {
-		fn.diff(lang, msg, args, wiki, reaction, spoiler, embed);
+	diff: (fn, lang, msg, wiki, querystring, fragment, reaction, spoiler, noEmbed, args, embed) => {
+		fn.diff(lang, msg, args, wiki, reaction, spoiler, noEmbed, embed);
 	}
 }
 
@@ -34,7 +54,7 @@ const queryfunctions = {
 	timestamp: (query, wiki, lang) => query.querypage.results.map( result => {
 		try {
 			var dateformat = new Intl.DateTimeFormat(lang.get('dateformat'), Object.assign({
-				timeZone: body.query.general.timezone
+				timeZone: query.general.timezone
 			}, timeoptions));
 		}
 		catch ( error ) {
@@ -42,7 +62,8 @@ const queryfunctions = {
 				timeZone: 'UTC'
 			}, timeoptions));
 		}
-		return dateformat.format(new Date(result.timestamp)) + ': [' + escapeFormatting(result.title) + '](' + wiki.toLink(result.title, '', '', true) + ')';
+		let lastEditDate = new Date(result.timestamp);
+		return dateformat.format(lastEditDate) + ' <t:' + Math.trunc(lastEditDate.getTime() / 1000) + ':R>: [' + escapeFormatting(result.title) + '](' + wiki.toLink(result.title, '', '', true) + ')';
 	} ).join('\n'),
 	media: (query, wiki, lang) => query.querypage.results.map( result => {
 		var ms = result.title.split(';');
@@ -107,6 +128,7 @@ const querypages = {
 }
 
 const descriptions = {
+	block: 'blockiptext&amargs=16|19',
 	checkuser: 'checkuser-summary&amargs=16|19',
 	resettokens: 'resettokens-text',
 	allmessages: 'allmessagestext',
@@ -129,42 +151,65 @@ const descriptions = {
  * @param {String} querypage.title - The title of the special page.
  * @param {String} querypage.uselang - The language of the special page.
  * @param {String} specialpage - The canonical name of the special page.
- * @param {import('discord.js').MessageEmbed} embed - The embed for the page.
+ * @param {Object} query - The siteinfo from the wiki.
  * @param {import('../util/wiki.js')} wiki - The wiki for the page.
+ * @param {URLSearchParams} querystring - The querystring for the link.
+ * @param {String} fragment - The section for the link.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
  * @param {String} spoiler - If the response is in a spoiler.
+ * @param {Boolean} noEmbed - If the response should be without an embed.
  */
-function special_page(lang, msg, {title, uselang = lang.lang}, specialpage, embed, wiki, reaction, spoiler) {
+function special_page(lang, msg, {title, uselang = lang.lang}, specialpage, query, wiki, querystring, fragment, reaction, spoiler, noEmbed) {
+	var pagelink = wiki.toLink(title, querystring, fragment);
+	var embed = new MessageEmbed().setAuthor( query.general.sitename ).setTitle( escapeFormatting(title) ).setURL( pagelink ).setThumbnail( new URL(query.general.logo, wiki).href );
 	if ( overwrites.hasOwnProperty(specialpage) ) {
 		var args = title.split('/').slice(1,3);
-		overwrites[specialpage](this, lang, msg, wiki, reaction, spoiler, args, embed);
+		overwrites[specialpage](this, lang, msg, wiki, querystring, fragment, reaction, spoiler, noEmbed, args, embed, query);
+		return;
+	}
+	logging(wiki, msg.guildId, 'general', 'special');
+	if ( !msg.showEmbed() || noEmbed ) {
+		msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler );
+		
+		if ( reaction ) reaction.removeEmoji();
 		return;
 	}
-	logging(wiki, msg.guild?.id, 'general', 'special');
 	if ( specialpage === 'recentchanges' && msg.isAdmin() ) {
-		embed.addField( lang.get('rcscript.title'), lang.get('rcscript.ad', ( patreons[msg?.guild?.id] || process.env.prefix ), '[RcGcDw](https://gitlab.com/piotrex43/RcGcDw)') );
+		embed.addField( lang.get('rcscript.title'), lang.get('rcscript.ad', ( patreons[msg.guildId] || process.env.prefix ), '[RcGcDw](https://gitlab.com/piotrex43/RcGcDw)') );
 	}
-	got.get( wiki + 'api.php?uselang=' + uselang + '&action=query&meta=allmessages|siteinfo&siprop=general&amenableparser=true&amtitle=' + encodeURIComponent( title ) + '&ammessages=' + ( descriptions.hasOwnProperty(specialpage) ? descriptions[specialpage] : encodeURIComponent( specialpage ) + '-summary' ) + ( querypages.hasOwnProperty(specialpage) ? querypages[specialpage][0] : '' ) + '&format=json' ).then( response => {
+	got.get( wiki + 'api.php?uselang=' + uselang + '&action=query&meta=allmessages|siteinfo&siprop=general&amenableparser=true&amtitle=' + encodeURIComponent( title ) + '&ammessages=' + encodeURIComponent( specialpage ) + '|' + ( descriptions.hasOwnProperty(specialpage) ? descriptions[specialpage] : encodeURIComponent( specialpage ) + '-summary' ) + ( querypages.hasOwnProperty(specialpage) ? querypages[specialpage][0] : '' ) + '&converttitles=true&titles=%1F' + encodeURIComponent( title ) + '&format=json' ).then( response => {
 		var body = response.body;
 		if ( body && body.warnings ) log_warn(body.warnings);
 		if ( response.statusCode !== 200 || body?.batchcomplete === undefined ) {
 			console.log( '- ' + response.statusCode + ': Error while getting the special page: ' + ( body && body.error && body.error.info ) );
+			return;
 		}
-		else {
-			if ( body.query.allmessages[0]['*'] ) {
-				var description = toMarkdown(body.query.allmessages[0]['*'], wiki, title, true);
-				if ( description.length > 1000 ) description = description.substring(0, 1000) + '\u2026';
-				embed.setDescription( description );
-			}
-			if ( msg.channel.isGuild() && patreons[msg.guild?.id] && querypages.hasOwnProperty(specialpage) ) {
-				var text = Util.splitMessage( querypages[specialpage][1](body.query, wiki, lang), {maxLength:1000} )[0];
-				embed.addField( lang.get('search.special'), ( text || lang.get('search.empty') ) );
+		if ( body.query.pages?.['-1']?.title ) {
+			title = body.query.pages['-1'].title;
+			pagelink = wiki.toLink(title, querystring, fragment);
+			embed.setTitle( escapeFormatting(title) );
+		}
+		if ( body.query.allmessages?.[0]?.['*']?.trim?.() ) {
+			let displaytitle = escapeFormatting(body.query.allmessages[0]['*'].trim());
+			if ( displaytitle.length > 250 ) displaytitle = displaytitle.substring(0, 250) + '\u2026';
+			embed.setTitle( displaytitle );
+		}
+		if ( body.query.allmessages?.[1]?.['*']?.trim?.() ) {
+			var description = toMarkdown(body.query.allmessages[1]['*'], wiki, title, true);
+			if ( description.length > 1000 ) description = description.substring(0, 1000) + '\u2026';
+			embed.setDescription( description );
+		}
+		if ( msg.channel.isGuild() && patreons[msg.guildId] && querypages.hasOwnProperty(specialpage) ) {
+			var text = Util.splitMessage( querypages[specialpage][1](body.query, wiki, lang), {maxLength:1000} )[0];
+			embed.addField( lang.get('search.special'), ( text || lang.get('search.empty') ) );
+			if ( body.query.querypage.cached !== undefined ) {
+				embed.setFooter( lang.get('search.cached') ).setTimestamp(new Date(body.query.querypage.cachedtimestamp));
 			}
 		}
 	}, error => {
 		console.log( '- Error while getting the special page: ' + error );
 	} ).finally( () => {
-		msg.sendChannel( spoiler + '<' + embed.url + '>' + spoiler, {embed} );
+		msg.sendChannel( {content: spoiler + '<' + pagelink + '>' + spoiler, embeds: [embed]} );
 		
 		if ( reaction ) reaction.removeEmoji();
 	} );

+ 216 - 197
functions/verify.js

@@ -1,10 +1,10 @@
 const cheerio = require('cheerio');
-const {MessageEmbed} = require('discord.js');
+const {MessageEmbed, MessageActionRow, MessageButton, Permissions: {FLAGS}} = require('discord.js');
 var db = require('../util/database.js');
 const Lang = require('../util/i18n.js');
 const Wiki = require('../util/wiki.js');
 const logging = require('../util/logging.js');
-const {oauthVerify, escapeFormatting} = require('../util/functions.js');
+const {got, oauthVerify, allowDelete, escapeFormatting} = require('../util/functions.js');
 const toTitle = require('../util/wiki.js').toTitle;
 
 /**
@@ -27,17 +27,17 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 		onmatch: rows[0].onmatch
 	};
 	verifynotice.logchannel = ( verifynotice.logchannel ? channel.guild.channels.cache.filter( logchannel => {
-		return ( logchannel.isGuild() && logchannel.permissionsFor(channel.guild.me).has(['VIEW_CHANNEL', 'SEND_MESSAGES']) );
+		return ( logchannel.isGuild() && logchannel.permissionsFor(channel.guild.me).has([FLAGS.VIEW_CHANNEL, FLAGS.SEND_MESSAGES]) );
 	} ).get(verifynotice.logchannel) : null );
 	var embed = new MessageEmbed().setFooter( lang.get('verify.footer') ).setTimestamp();
 	var result = {
 		content: '', embed,
-		add_button: channel.permissionsFor(channel.guild.me).has('EMBED_LINKS'),
+		add_button: channel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS),
 		send_private: ( (verifynotice.flags & 1 << 0) === 1 << 0 ),
 		reaction: '', oauth: [],
 		logging: {
 			channel: '',
-			content: '',
+			content: null,
 			embed: null
 		}
 	};
@@ -51,7 +51,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 			}
 			else if ( body?.error?.code === 'us400' || body?.error?.code === 'baduser_ucuser' ) {
 				// special catch for Fandom
-				if ( !old_username ) logging(wiki, channel.guild.id, 'verification');
+				if ( !old_username ) logging(wiki, channel.guildId, 'verification');
 				embed.setTitle( escapeFormatting( old_username || username ) ).setColor('#0000FF').setDescription( lang.get('verify.user_missing', escapeFormatting( old_username || username )) ).addField( lang.get('verify.notice'), lang.get('verify.help_missing') );
 				result.content = lang.get('verify.user_missing_reply', escapeFormatting( old_username || username ));
 			}
@@ -73,7 +73,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 				return;
 			}
 		}
-		if ( !old_username ) logging(wiki, channel.guild.id, 'verification');
+		if ( !old_username ) logging(wiki, channel.guildId, 'verification');
 		var queryuser = body.query.users[0];
 		embed.setAuthor( body.query.general.sitename );
 		if ( body.query.users.length !== 1 || queryuser.missing !== undefined || queryuser.invalid !== undefined ) {
@@ -101,7 +101,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 			result.content = lang.get('verify.user_blocked_reply', escapeFormatting(username), queryuser.gender);
 			if ( (verifynotice.flags & 1 << 1) === 1 << 1 && verifynotice.logchannel ) {
 				result.logging.channel = verifynotice.logchannel.id;
-				if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+				if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 					let logembed = new MessageEmbed(embed);
 					logembed.addField( lang.get('verify.discord', 'unknown'), escapeFormatting(member.user.tag) + ` (${member.toString()})`, true );
 					result.logging.embed = logembed;
@@ -179,7 +179,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 					embed.setColor('#FFFF00').setDescription( lang.get('verify.user_failed', member.toString(), '[' + escapeFormatting(username) + '](' + pagelink + ')', queryuser.gender) );
 					if ( (verifynotice.flags & 1 << 1) === 1 << 1 && verifynotice.logchannel ) {
 						result.logging.channel = verifynotice.logchannel.id;
-						if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+						if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 							result.logging.embed = new MessageEmbed(embed);
 						}
 						else {
@@ -189,8 +189,8 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 						}
 					}
 					var help_link = '';
-					if ( wiki.isGamepedia() ) help_link = lang.get('verify.help_gamepedia') + '?c=' + ( patreons[channel.guild.id] && patreons[channel.guild.id] !== process.env.prefix ? encodeURIComponent( patreons[channel.guild.id] + 'verify' ) : 'wb' ) + ( channel.name !== 'verification' ? '&ch=' + encodeURIComponent( channel.name ) : '' ) + '&user=' + toTitle(username) + '&discord=' + encodeURIComponent( member.user.username ) + '&tag=' + member.user.discriminator;
-					else if ( wiki.isFandom() ) help_link = lang.get('verify.help_fandom') + '/' + toTitle(username) + '?c=' + ( patreons[channel.guild.id] && patreons[channel.guild.id] !== process.env.prefix ? encodeURIComponent( patreons[channel.guild.id] + 'verify' ) : 'wb' ) + ( channel.name !== 'verification' ? '&ch=' + encodeURIComponent( channel.name ) : '' ) + '&user=' + encodeURIComponent( member.user.username ) + '&tag=' + member.user.discriminator + '&useskin=oasis';
+					if ( wiki.isGamepedia() ) help_link = lang.get('verify.help_gamepedia') + '?c=' + ( patreons[channel.guildId] && patreons[channel.guildId] !== process.env.prefix ? encodeURIComponent( patreons[channel.guildId] + 'verify' ) : 'wb' ) + ( channel.name !== 'verification' ? '&ch=' + encodeURIComponent( channel.name ) : '' ) + '&user=' + toTitle(username) + '&discord=' + encodeURIComponent( member.user.username ) + '&tag=' + member.user.discriminator + '&useskin=fandomdesktop';
+					else if ( wiki.isFandom() ) help_link = lang.get('verify.help_fandom') + '/' + toTitle(username) + '?c=' + ( patreons[channel.guildId] && patreons[channel.guildId] !== process.env.prefix ? encodeURIComponent( patreons[channel.guildId] + 'verify' ) : 'wb' ) + ( channel.name !== 'verification' ? '&ch=' + encodeURIComponent( channel.name ) : '' ) + '&user=' + encodeURIComponent( member.user.username ) + '&tag=' + member.user.discriminator + '&useskin=fandomdesktop';
 					if ( help_link.length ) embed.addField( lang.get('verify.notice'), lang.get('verify.help_guide', help_link, queryuser.gender) + '\n' + help_link );
 					result.content = lang.get('verify.user_failed_reply', escapeFormatting(username), queryuser.gender);
 					return;
@@ -200,7 +200,9 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 					queryuser.editcount = body.query.usercontribs.length;
 					if ( body.continue?.uccontinue ) queryuser.editcount++;
 				}
+				/** @type {[Set<String>,Set<String>]} */
 				var addRoles = [new Set(), new Set()];
+				/** @type {[Set<String>,Set<String>]} */
 				var removeRoles = [new Set(), new Set()];
 				var verified = false;
 				var rename = false;
@@ -234,35 +236,36 @@ 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);
+					/** @type {Promise[]} */
+					var verifyPromise = [];
+					var editMember = {};
+					if ( rename && member.displayName !== username.substring(0, 32) ) {
+						if ( channel.guild.me.roles.highest.comparePositionTo(member.roles.highest) > 0 ) editMember.nick = username.substring(0, 32);
+						else comment.push(lang.get('verify.failed_rename', 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]);
+					if ( !editMember.nick && addRoles[0].size + removeRoles[0].size <= 1 ) {
+						if ( removeRoles[0].size === 1 ) verifyPromise.push(member.roles.remove( [...removeRoles[0]][0], lang.get('verify.audit_reason', username) ).catch( error => {
+							log_error(error);
+							comment.push(lang.get('verify.failed_roles'));
+						} ));
+						else if ( addRoles[0].size === 1 ) verifyPromise.push(member.roles.add( [...addRoles[0]][0], lang.get('verify.audit_reason', username) ).catch( error => {
+							log_error(error);
+							comment.push(lang.get('verify.failed_roles'));
+						} ));
 					}
 					else {
-						let roles = new Set([...member.roles.cache.filter( role => {
+						if ( addRoles[0].size + removeRoles[0].size ) editMember.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[changeRoles[0]]( changeRoles[1], lang.get('verify.audit_reason', username) ).catch( error => {
+						} ).keys(), ...addRoles[0]])];
+						verifyPromise.push(member.edit( editMember, lang.get('verify.audit_reason', username) ).catch( error => {
 							log_error(error);
 							comment.push(lang.get('verify.failed_roles'));
-						} )
-					];
-					if ( rename && member.displayName !== 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);
-								comment.push(lang.get('verify.failed_rename', queryuser.gender));
-							} ));
-						}
-						else comment.push(lang.get('verify.failed_rename', queryuser.gender));
+							if ( editMember.nick ) comment.push(lang.get('verify.failed_rename', queryuser.gender));
+						} ));
 					}
-					return Promise.all(verify_promise).then( () => {
+					return Promise.all(verifyPromise).then( () => {
 						var addRolesMentions = [
 							[...addRoles[0]].map( role => '<@&' + role + '>' ),
 							[...addRoles[1]].map( role => '<@&' + role + '>' )
@@ -275,7 +278,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 						if ( verifynotice.logchannel ) {
 							useLogging = true;
 							result.logging.channel = verifynotice.logchannel.id;
-							if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+							if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 								let logembed = new MessageEmbed(embed);
 								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') );
@@ -302,7 +305,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 							accountage: Math.trunc(accountage),
 							dateformat: lang.get('dateformat')
 						}).trim() : '' );
-						if ( channel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+						if ( channel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 							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') );
@@ -329,7 +332,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 
 				if ( (verifynotice.flags & 1 << 1) === 1 << 1 && verifynotice.logchannel ) {
 					result.logging.channel = verifynotice.logchannel.id;
-					if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+					if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 						result.logging.embed = new MessageEmbed(embed);
 					}
 					else {
@@ -347,7 +350,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 					dateformat: lang.get('dateformat')
 				});
 				if ( !onmatch.trim() ) return;
-				if ( channel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) embed.addField( lang.get('verify.notice'), onmatch );
+				if ( channel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) embed.addField( lang.get('verify.notice'), onmatch );
 				else result.content += '\n\n**' + lang.get('verify.notice') + '** ' + onmatch;
 			}, error => {
 				if ( error ) console.log( '- Error while getting the Discord tag: ' + error );
@@ -360,7 +363,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 			result.content = error.reply;
 			if ( (verifynotice.flags & 1 << 1) === 1 << 1 && verifynotice.logchannel ) {
 				result.logging.channel = verifynotice.logchannel.id;
-				if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+				if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 					let logembed = new MessageEmbed(embed);
 					logembed.addField( lang.get('verify.discord', 'unknown'), escapeFormatting(member.user.tag) + ` (${member.toString()})`, true );
 					result.logging.embed = logembed;
@@ -389,7 +392,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 					result.content = lang.get('verify.user_gblocked_reply', escapeFormatting(username), queryuser.gender);
 					if ( (verifynotice.flags & 1 << 1) === 1 << 1 && verifynotice.logchannel ) {
 						result.logging.channel = verifynotice.logchannel.id;
-						if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+						if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 							let logembed = new MessageEmbed(embed);
 							logembed.addField( lang.get('verify.discord', 'unknown'), escapeFormatting(member.user.tag) + ` (${member.toString()})`, true );
 							result.logging.embed = logembed;
@@ -418,7 +421,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 				embed.setColor('#FFFF00').setDescription( lang.get('verify.user_failed', member.toString(), '[' + escapeFormatting(username) + '](' + pagelink + ')', queryuser.gender) );
 				if ( (verifynotice.flags & 1 << 1) === 1 << 1 && verifynotice.logchannel ) {
 					result.logging.channel = verifynotice.logchannel.id;
-					if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+					if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 						result.logging.embed = new MessageEmbed(embed);
 					}
 					else {
@@ -432,7 +435,9 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 				return;
 			}
 			
+			/** @type {[Set<String>,Set<String>]} */
 			var addRoles = [new Set(), new Set()];
+			/** @type {[Set<String>,Set<String>]} */
 			var removeRoles = [new Set(), new Set()];
 			var verified = false;
 			var rename = false;
@@ -462,35 +467,36 @@ 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);
+				/** @type {Promise[]} */
+				var verifyPromise = [];
+				var editMember = {};
+				if ( rename && member.displayName !== username.substring(0, 32) ) {
+					if ( channel.guild.me.roles.highest.comparePositionTo(member.roles.highest) > 0 ) editMember.nick = username.substring(0, 32);
+					else comment.push(lang.get('verify.failed_rename', 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]);
+				if ( !editMember.nick && addRoles[0].size + removeRoles[0].size <= 1 ) {
+					if ( removeRoles[0].size === 1 ) verifyPromise.push(member.roles.remove( [...removeRoles[0]][0], lang.get('verify.audit_reason', username) ).catch( error => {
+						log_error(error);
+						comment.push(lang.get('verify.failed_roles'));
+					} ));
+					else if ( addRoles[0].size === 1 ) verifyPromise.push(member.roles.add( [...addRoles[0]][0], lang.get('verify.audit_reason', username) ).catch( error => {
+						log_error(error);
+						comment.push(lang.get('verify.failed_roles'));
+					} ));
 				}
 				else {
-					let roles = new Set([...member.roles.cache.filter( role => {
+					if ( addRoles[0].size + removeRoles[0].size ) editMember.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[changeRoles[0]]( changeRoles[1], lang.get('verify.audit_reason', username) ).catch( error => {
+					} ).keys(), ...addRoles[0]])];
+					verifyPromise.push(member.edit( editMember, lang.get('verify.audit_reason', username) ).catch( error => {
 						log_error(error);
 						comment.push(lang.get('verify.failed_roles'));
-					} )
-				];
-				if ( rename && member.displayName !== 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);
-							comment.push(lang.get('verify.failed_rename', queryuser.gender));
-						} ));
-					}
-					else comment.push(lang.get('verify.failed_rename', queryuser.gender));
+						if ( editMember.nick ) comment.push(lang.get('verify.failed_rename', queryuser.gender));
+					} ));
 				}
-				return Promise.all(verify_promise).then( () => {
+				return Promise.all(verifyPromise).then( () => {
 					var addRolesMentions = [
 						[...addRoles[0]].map( role => '<@&' + role + '>' ),
 						[...addRoles[1]].map( role => '<@&' + role + '>' )
@@ -503,7 +509,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 					if ( verifynotice.logchannel ) {
 						useLogging = true;
 						result.logging.channel = verifynotice.logchannel.id;
-						if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+						if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 							var logembed = new MessageEmbed(embed);
 							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') );
@@ -529,7 +535,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 						accountage: Math.trunc(accountage),
 						dateformat: lang.get('dateformat')
 					}).trim() : '' );
-					if ( channel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+					if ( channel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 						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') );
@@ -561,7 +567,7 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
 				dateformat: lang.get('dateformat')
 			});
 			if ( !onmatch.trim() ) return;
-			if ( channel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) embed.addField( lang.get('verify.notice'), onmatch );
+			if ( channel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) embed.addField( lang.get('verify.notice'), onmatch );
 			else result.content += '\n\n**' + lang.get('verify.notice') + '** ' + onmatch;
 		}, error => {
 			console.log( '- Error while getting the Discord tag: ' + error );
@@ -586,24 +592,27 @@ function verify(lang, channel, member, username, wiki, rows, old_username = '')
  * @param {String} access_token - Access token.
  * @param {Object} [settings] - Settings to skip oauth.
  * @param {import('discord.js').TextChannel} settings.channel - The channel.
- * @param {String} settings.username - The username.
  * @param {String} settings.user - The user id.
- * @param {String} [settings.token] - The webhook token.
- * @param {Function} settings.send - The function to edit the message.
+ * @param {String} settings.wiki - The OAuth2 wiki.
+ * @param {import('discord.js').CommandInteraction|import('discord.js').ButtonInteraction} [settings.interaction] - The interaction.
+ * @param {Function} [settings.fail] - The function to call when the verifiction errors.
+ * @param {import('discord.js').Message} [settings.sourceMessage] - The source message with the command.
  */
 global.verifyOauthUser = function(state, access_token, settings) {
 	if ( state && access_token && oauthVerify.has(state) ) {
 		settings = oauthVerify.get(state);
 		oauthVerify.delete(state);
 	}
-	if ( !settings?.channel ) return;
+	if ( !settings?.channel ) return settings?.fail?.();
 	var channel = settings.channel;
-	var username = settings.username;
-	if ( !username && !channel.permissionsFor(channel.guild.me).has(['VIEW_CHANNEL', 'SEND_MESSAGES']) ) return;
+	if ( !channel.permissionsFor(channel.guild.me).has([FLAGS.VIEW_CHANNEL, FLAGS.SEND_MESSAGES]) ) return settings.fail?.();
 	Promise.all([
-		db.query( 'SELECT logchannel, flags, onsuccess, onmatch, role, editcount, postcount, usergroup, accountage, rename FROM verification LEFT JOIN verifynotice ON verification.guild = verifynotice.guild WHERE verification.guild = $1 AND channel LIKE $2 ORDER BY configid ASC', [channel.guild.id, '%|' + channel.id + '|%'] ).then( ({rows}) => {
+		db.query( 'SELECT logchannel, flags, onsuccess, onmatch, role, editcount, postcount, usergroup, accountage, rename FROM verification LEFT JOIN verifynotice ON verification.guild = verifynotice.guild WHERE verification.guild = $1 AND channel LIKE $2 ORDER BY configid ASC', [channel.guildId, '%|' + ( channel.isThread() ? channel.parentId : channel.id ) + '|%'] ).then( ({rows}) => {
 			if ( !rows.length ) return Promise.reject();
-			return db.query( 'SELECT wiki, lang FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', [channel.guild.id, channel.id, '#' + channel.parentID] ).then( ({rows: [row]}) => {
+			let sqlargs = [channel.guildId];
+			if ( channel.isThread() ) sqlargs.push(channel.parentId, '#' + channel.parent?.parentId);
+			else sqlargs.push(channel.id, '#' + channel.parentId);
+			return db.query( 'SELECT wiki, lang FROM discord WHERE guild = $1 AND (channel = $2 OR channel = $3 OR channel IS NULL) ORDER BY channel DESC NULLS LAST LIMIT 1', sqlargs ).then( ({rows: [row]}) => {
 				return {
 					rows, wiki: new Wiki(row?.wiki),
 					lang: new Lang(( row?.lang || channel?.guild?.preferredLocale ))
@@ -611,7 +620,7 @@ global.verifyOauthUser = function(state, access_token, settings) {
 			} );
 		} ),
 		channel.guild.members.fetch(settings.user),
-		( !username ? got.get( settings.wiki + 'rest.php/oauth2/resource/profile', {
+		got.get( settings.wiki + 'rest.php/oauth2/resource/profile', {
 			headers: {
 				Authorization: `Bearer ${access_token}`
 			}
@@ -621,17 +630,17 @@ global.verifyOauthUser = function(state, access_token, settings) {
 				console.log( '- ' + response.statusCode + ': Error while getting the mediawiki profile: ' + ( body?.message || body?.error ) );
 				return;
 			}
-			username = body.username;
-			console.log( channel.guild.id + ': OAuth2: ' + username );
+			console.log( channel.guildId + ': OAuth2: ' + body.username );
+			return body.username;
 		}, error => {
 			console.log( '- Error while getting the mediawiki profile: ' + error );
-		} ) : null )
-	]).then( ([{rows, wiki, lang}, member]) => {
-		if ( !username || ( settings.wiki && settings.wiki !== wiki.href ) ) return settings.send?.();
+		} )
+	]).then( ([{rows, wiki, lang}, member, username]) => {
+		if ( !username || settings.wiki !== wiki.href ) return settings.fail?.();
 		/** @type {{logchannel:import('discord.js').TextChannel,flags:Number,onsuccess:String,onmatch:String}} */
 		var verifynotice = ( rows[0] || {} );
 		verifynotice.logchannel = ( verifynotice.logchannel ? channel.guild.channels.cache.filter( logchannel => {
-			return ( logchannel.isGuild() && logchannel.permissionsFor(channel.guild.me).has(['VIEW_CHANNEL', 'SEND_MESSAGES']) );
+			return ( logchannel.isGuild() && logchannel.permissionsFor(channel.guild.me).has([FLAGS.VIEW_CHANNEL, FLAGS.SEND_MESSAGES]) );
 		} ).get(verifynotice.logchannel) : null );
 		got.get( wiki + 'api.php?action=query&meta=siteinfo|globaluserinfo&siprop=general&guiprop=groups&guiuser=' + encodeURIComponent( username ) + '&list=users&usprop=blockinfo|groups|editcount|registration|gender&ususers=' + encodeURIComponent( username ) + '&format=json' ).then( response => {
 			var body = response.body;
@@ -643,27 +652,22 @@ global.verifyOauthUser = function(state, access_token, settings) {
 				else {
 					console.log( '- ' + response.statusCode + ': Error while getting the user: ' + body?.error?.info );
 				}
-				return settings.send?.();
+				return settings.fail?.();
 			}
 			wiki.updateWiki(body.query.general);
-			logging(wiki, channel.guild.id, 'verification');
+			logging(wiki, channel.guildId, 'verification');
 			var queryuser = body.query.users[0];
-			if ( body.query.users.length !== 1 || queryuser.missing !== undefined || queryuser.invalid !== undefined ) return settings.send?.();
-			var allowedMentions = {
-				users: [
-					member.id
-				]
-			};
+			if ( body.query.users.length !== 1 || queryuser.missing !== undefined || queryuser.invalid !== undefined ) return settings.fail?.();
 			var embed = new MessageEmbed().setFooter( lang.get('verify.footer') ).setTimestamp().setAuthor( body.query.general.sitename ).addField( lang.get('verify.discord', queryuser.gender), escapeFormatting(member.user.tag), true ).addField( lang.get('verify.wiki', queryuser.gender), lang.get('verify.oauth_used'), true );
 			var pagelink = wiki.toLink('User:' + username, '', '', true);
 			embed.setTitle( escapeFormatting(username) ).setURL( pagelink );
 			if ( queryuser.blockexpiry ) {
 				embed.setColor('#FF0000').setDescription( lang.get('verify.user_blocked', '[' + escapeFormatting(username) + '](' + pagelink + ')', queryuser.gender) );
-				return sendMessage(lang.get('verify.user_blocked_reply', escapeFormatting(username), queryuser.gender), {embed, allowedMentions}).then( msg => {
+				return sendMessage( {content: lang.get('verify.user_blocked_reply', escapeFormatting(username), queryuser.gender), embeds: [embed]} ).then( msg => {
 					if ( (verifynotice.flags & 1 << 1) !== 1 << 1 || !verifynotice.logchannel ) return;
 					let logembed;
-					let logtext = '';
-					if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+					let logtext;
+					if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 						logembed = new MessageEmbed(embed);
 						logembed.addField( lang.get('verify.discord', 'unknown'), escapeFormatting(member.user.tag) + ` (${member.toString()})`, true );
 						if ( msg ) logembed.addField(msg.url, '<#' + channel.id + '>');
@@ -673,19 +677,19 @@ global.verifyOauthUser = function(state, access_token, settings) {
 						logtext += '\n<' + pagelink + '>';
 						if ( msg ) logtext += '\n<#' + channel.id + '> – <' + msg.url + '>';
 					}
-					verifynotice.logchannel.send(logtext, {
-						embed: logembed,
-						allowedMentions: {parse: []}
-					}).catch(log_error);
+					verifynotice.logchannel.send( {
+						content: logtext,
+						embeds: [logembed]
+					} ).catch(log_error);
 				}, log_error );
 			}
 			if ( body.query.globaluserinfo.locked !== undefined ) {
 				embed.setColor('#FF0000').setDescription( lang.get('verify.user_gblocked', '[' + escapeFormatting(username) + '](' + pagelink + ')', queryuser.gender) );
-				return sendMessage(lang.get('verify.user_gblocked_reply', escapeFormatting(username), queryuser.gender), {embed, allowedMentions}).then( msg => {
+				return sendMessage( {content: lang.get('verify.user_gblocked_reply', escapeFormatting(username), queryuser.gender), embeds: [embed]} ).then( msg => {
 					if ( (verifynotice.flags & 1 << 1) !== 1 << 1 || !verifynotice.logchannel ) return;
 					let logembed;
-					let logtext = '';
-					if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+					let logtext;
+					if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 						logembed = new MessageEmbed(embed);
 						logembed.addField( lang.get('verify.discord', 'unknown'), escapeFormatting(member.user.tag) + ` (${member.toString()})`, true );
 						if ( msg ) logembed.addField(msg.url, '<#' + channel.id + '>');
@@ -695,15 +699,17 @@ global.verifyOauthUser = function(state, access_token, settings) {
 						logtext += '\n<' + pagelink + '>';
 						if ( msg ) logtext += '\n<#' + channel.id + '> – <' + msg.url + '>';
 					}
-					verifynotice.logchannel.send(logtext, {
-						embed: logembed,
-						allowedMentions: {parse: []}
-					}).catch(log_error);
+					verifynotice.logchannel.send( {
+						content: logtext,
+						embeds: [logembed]
+					} ).catch(log_error);
 				}, log_error );
 			}
 			queryuser.groups.push(...body.query.globaluserinfo.groups);
 
+			/** @type {[Set<String>,Set<String>]} */
 			var addRoles = [new Set(), new Set()];
+			/** @type {[Set<String>,Set<String>]} */
 			var removeRoles = [new Set(), new Set()];
 			var verified = false;
 			var rename = false;
@@ -734,35 +740,36 @@ 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 = [];
+				/** @type {Promise[]} */
+				var verifyPromise = [];
+				var editMember = {};
+				if ( rename && member.displayName !== username.substring(0, 32) ) {
+					if ( channel.guild.me.roles.highest.comparePositionTo(member.roles.highest) > 0 ) editMember.nick = username.substring(0, 32);
+					else comment.push(lang.get('verify.failed_rename', 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]);
+				if ( !editMember.nick && addRoles[0].size + removeRoles[0].size <= 1 ) {
+					if ( removeRoles[0].size === 1 ) verifyPromise.push(member.roles.remove( [...removeRoles[0]][0], lang.get('verify.audit_reason', username) ).catch( error => {
+						log_error(error);
+						comment.push(lang.get('verify.failed_roles'));
+					} ));
+					else if ( addRoles[0].size === 1 ) verifyPromise.push(member.roles.add( [...addRoles[0]][0], lang.get('verify.audit_reason', username) ).catch( error => {
+						log_error(error);
+						comment.push(lang.get('verify.failed_roles'));
+					} ));
 				}
 				else {
-					let roles = new Set([...member.roles.cache.filter( role => {
+					if ( addRoles[0].size + removeRoles[0].size ) editMember.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[changeRoles[0]]( changeRoles[1], lang.get('verify.audit_reason', username) ).catch( error => {
+					} ).keys(), ...addRoles[0]])];
+					verifyPromise.push(member.edit( editMember, lang.get('verify.audit_reason', username) ).catch( error => {
 						log_error(error);
 						comment.push(lang.get('verify.failed_roles'));
-					} )
-				];
-				if ( rename && member.displayName !== 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);
-							comment.push(lang.get('verify.failed_rename', queryuser.gender));
-						} ));
-					}
-					else comment.push(lang.get('verify.failed_rename', queryuser.gender));
+						if ( editMember.nick ) comment.push(lang.get('verify.failed_rename', queryuser.gender));
+					} ));
 				}
-				return Promise.all(verify_promise).then( () => {
+				return Promise.all(verifyPromise).then( () => {
 					var addRolesMentions = [
 						[...addRoles[0]].map( role => '<@&' + role + '>' ),
 						[...addRoles[1]].map( role => '<@&' + role + '>' )
@@ -773,10 +780,10 @@ global.verifyOauthUser = function(state, access_token, settings) {
 					];
 					var useLogging = false;
 					var logembed;
-					var logtext = '';
+					var logtext;
 					if ( verifynotice.logchannel ) {
 						useLogging = true;
-						if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+						if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 							logembed = new MessageEmbed(embed);
 							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') );
@@ -801,7 +808,7 @@ global.verifyOauthUser = function(state, access_token, settings) {
 						accountage: Math.trunc(accountage),
 						dateformat: lang.get('dateformat')
 					}).trim() : '' );
-					if ( channel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+					if ( channel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 						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') );
@@ -818,16 +825,16 @@ global.verifyOauthUser = function(state, access_token, settings) {
 						if ( comment.length && !useLogging ) text += '\n\n' + comment.join('\n');
 						if ( onsuccess ) text += '\n\n**' + lang.get('verify.notice') + '** ' + onsuccess;
 					}
-					return sendMessage(text, {embed, allowedMentions}).then( msg => {
+					return sendMessage( {content: text, embeds: [embed]} ).then( msg => {
 						if ( !useLogging ) return;
 						if ( msg ) {
 							if ( logembed ) logembed.addField(msg.url, '<#' + channel.id + '>');
 							else logtext += '\n<#' + channel.id + '> – <' + msg.url + '>';
 						}
-						verifynotice.logchannel.send(logtext, {
-							embed: logembed,
-							allowedMentions: {parse: []}
-						}).catch(log_error);
+						verifynotice.logchannel.send( {
+							content: logtext,
+							embeds: [logembed]
+						} ).catch(log_error);
 					}, log_error );
 				}, log_error );
 			}
@@ -835,9 +842,9 @@ global.verifyOauthUser = function(state, access_token, settings) {
 			embed.setColor('#FFFF00').setDescription( lang.get('verify.user_matches', member.toString(), '[' + escapeFormatting(username) + '](' + pagelink + ')', queryuser.gender) );
 
 			let logembed;
-			let logtext = '';
+			let logtext;
 			if ( (verifynotice.flags & 1 << 1) === 1 << 1 && verifynotice.logchannel ) {
-				if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+				if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 					logembed = new MessageEmbed(embed);
 				}
 				else {
@@ -854,102 +861,114 @@ global.verifyOauthUser = function(state, access_token, settings) {
 					dateformat: lang.get('dateformat')
 				});
 				if ( onmatch.trim() ) {
-					if ( channel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) embed.addField( lang.get('verify.notice'), onmatch );
+					if ( channel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) embed.addField( lang.get('verify.notice'), onmatch );
 					else noticeContent = '\n\n**' + lang.get('verify.notice') + '** ' + onmatch;
 				}
 			}
-			var components = [
-				{
-					type: 1,
-					components: [
-						{
-							type: 2,
-							style: 1,
-							label: lang.get('verify.button_again'),
-							emoji: {id: null, name: '🔂'},
-							custom_id: 'verify_again',
-							disabled: false
-						}
-					]
-				}
-			];
-			return sendMessage(lang.get('verify.user_matches_reply', escapeFormatting(username), queryuser.gender) + noticeContent, {embed, allowedMentions, components}).then( msg => {
+			return sendMessage( {
+				content: lang.get('verify.user_matches_reply', escapeFormatting(username), queryuser.gender) + noticeContent,
+				embeds: [embed], components: [new MessageActionRow().addComponents(
+					new MessageButton().setLabel(lang.get('verify.button_again')).setEmoji('🔂').setStyle('PRIMARY').setCustomId('verify_again')
+				)]
+			} ).then( msg => {
 				if ( !logtext && !logembed ) return;
 				if ( msg ) {
-					if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has('EMBED_LINKS') ) {
+					if ( verifynotice.logchannel.permissionsFor(channel.guild.me).has(FLAGS.EMBED_LINKS) ) {
 						logembed.addField(msg.url, '<#' + channel.id + '>');
 					}
 					else logtext += '\n<#' + channel.id + '> – <' + msg.url + '>';
 				}
-				verifynotice.logchannel.send(logtext, {
-					embed: logembed,
-					allowedMentions: {parse: []}
-				}).catch(log_error);
+				verifynotice.logchannel.send( {
+					content: logtext,
+					embeds: [logembed]
+				} ).catch(log_error);
 			}, log_error );
 
-			function sendMessage(content, options) {
-				var msg;
-				if ( settings.send ) msg = settings.send(member.toString() + ', ' + content, options);
-				else if ( settings.token ) {
-					msg = channel.client.api.webhooks(channel.client.user.id, settings.token).post( {
-						data: {
-							content: member.toString() + ', ' + content,
-							allowed_mentions: options.allowed_mentions,
-							embeds: ( options.embed ? [options.embed] : [] ),
-							components: ( options.components || [] ),
-							flags: ( (verifynotice.flags & 1 << 0) === 1 << 0 ? 64 : 0 )
-						}
-					} ).then( message => {
-						if ( (verifynotice.flags & 1 << 0) === 1 << 0 ) return;
-						return channel.messages.add(message);
-					}, () => {
-						if ( (verifynotice.flags & 1 << 0) === 1 << 0 ) {
-							let dmEmbed = new MessageEmbed(options.embed);
-							dmEmbed.fields.forEach( field => {
+			/**
+			 * Send the message responding to the OAuth2 verification.
+			 * @param {import('discord.js').MessageOptions} options - The message options.
+			 */
+			function sendMessage(options) {
+				var message = {
+					content: member.toString() + ', ' + options.content,
+					embeds: ( options.embeds?.[0] ? options.embeds : [] ),
+					components: ( options.components || [] ),
+					allowedMentions: {
+						users: [member.id],
+						repliedUser: true
+					},
+					ephemeral: ( (verifynotice.flags & 1 << 0) === 1 << 0 )
+				}
+				if ( settings.interaction ) return settings.interaction.editReply( message ).then( msg => {
+					if ( settings.interaction.isButton() ) settings.interaction.followUp( {
+						content: message.content,
+						embeds: message.embeds,
+						components: [],
+						ephemeral: true
+					} ).catch(log_error);
+					if ( message.ephemeral ) return;
+					return msg;
+				}, error => {
+					log_error(error);
+					if ( message.ephemeral ) {
+						let dmEmbeds = [];
+						if ( message.embeds[0] ) {
+							dmEmbeds.push(new MessageEmbed(message.embeds[0]));
+							dmEmbeds[0].fields.forEach( field => {
 								field.value = field.value.replace( /<@&(\d+)>/g, (mention, id) => {
 									if ( !channel.guild.roles.cache.has(id) ) return mention;
 									return escapeFormatting('@' + channel.guild.roles.cache.get(id)?.name);
 								} );
 							} );
-							member.send(channel.toString() + '; ' + content, Object.assign({}, options, {embed: dmEmbed})).then( message => {
-								allowDelete(message, member.id);
-							}, error => {
-								if ( error?.code === 50007 ) { // CANNOT_MESSAGE_USER
-									return channel.send(member.toString() + ', ' + content, options);
-								}
-								log_error(error);
-							} );
 						}
-						else return channel.send(member.toString() + ', ' + content, options);
-					} );
-				}
-				else if ( (verifynotice.flags & 1 << 0) === 1 << 0 ) {
-					let dmEmbed = new MessageEmbed(options.embed);
-					dmEmbed.fields.forEach( field => {
-						field.value = field.value.replace( /<@&(\d+)>/g, (mention, id) => {
-							if ( !channel.guild.roles.cache.has(id) ) return mention;
-							return escapeFormatting('@' + channel.guild.roles.cache.get(id)?.name);
+						return member.send( {content: channel.toString() + '; ' + options.content, embeds: dmEmbeds} ).then( msg => {
+							allowDelete(msg, member.id);
+							if ( settings.sourceMessage ) {
+								settings.sourceMessage.reactEmoji('📩');
+								setTimeout( () => settings.sourceMessage.delete().catch(log_error), 60000 ).unref();
+							}
+						}, error => {
+							if ( error?.code === 50007 ) { // CANNOT_MESSAGE_USER
+								return channel.send( message ).catch(log_error);
+							}
+							log_error(error);
 						} );
-					} );
-					member.send(channel.toString() + '; ' + content, Object.assign({}, options, {embed: dmEmbed})).then( message => {
-						allowDelete(message, member.id);
+					}
+					return channel.send( message ).catch(log_error);
+				} );
+				if ( message.ephemeral ) {
+					let dmEmbeds = [];
+					if ( message.embeds[0] ) {
+						dmEmbeds.push(new MessageEmbed(message.embeds[0]));
+						dmEmbeds[0].fields.forEach( field => {
+							field.value = field.value.replace( /<@&(\d+)>/g, (mention, id) => {
+								if ( !channel.guild.roles.cache.has(id) ) return mention;
+								return escapeFormatting('@' + channel.guild.roles.cache.get(id)?.name);
+							} );
+						} );
+					}
+					return member.send( {content: channel.toString() + '; ' + options.content, embeds: dmEmbeds} ).then( msg => {
+						allowDelete(msg, member.id);
+						if ( settings.sourceMessage ) {
+							settings.sourceMessage.reactEmoji('📩');
+							setTimeout( () => settings.sourceMessage.delete().catch(log_error), 60000 ).unref();
+						}
 					}, error => {
 						if ( error?.code === 50007 ) { // CANNOT_MESSAGE_USER
-							return channel.send(member.toString() + ', ' + content, options);
+							return channel.send( message ).catch(log_error);
 						}
 						log_error(error);
 					} );
 				}
-				else msg = channel.send(member.toString() + ', ' + content, options);
-				return msg;
+				return channel.send( message ).catch(log_error);
 			}
 		}, error => {
 			console.log( '- Error while getting the user: ' + error );
-			settings.send?.();
+			settings.fail?.();
 		} );
 	}, error => {
 		if ( error ) console.log( '- Error while preparing oauth verification: ' + error );
-		settings.send?.();
+		settings.fail?.();
 	} );
 }
 

+ 2 - 2
i18n/bn.json

@@ -117,10 +117,10 @@
         "hidden": "*লুকানো হয়েছে*",
         "info": {
             "added": "লাগানো হয়েছে:",
-            "bytes": "$1 বাইট $3",
+            "bytes": "$1 বাইট",
             "comment": "কমেন্ট:",
             "editor": "সদস্য:",
-            "minor": "(অ)",
+            "minor": "_(অ)",
             "more": "আরও",
             "removed": "সরানো হয়েছে:",
             "size": "অন্তর:",

+ 3 - 2
i18n/de.json

@@ -117,10 +117,10 @@
         "hidden": "*versteckt*",
         "info": {
             "added": "Hinzugefügt:",
-            "bytes": "$1 {{PLURAL:$2|Byte|Bytes}} $3",
+            "bytes": "$1 {{PLURAL:$2|Byte|Bytes}}",
             "comment": "Zusammenfassung:",
             "editor": "Autor:",
-            "minor": "(K)",
+            "minor": "_(K)",
             "more": "Und mehr",
             "removed": "Entfernt:",
             "size": "Unterschied:",
@@ -578,6 +578,7 @@
         "wiki": "Wiki:"
     },
     "search": {
+        "cached": "Cache zuletzt aktualisiert:",
         "category": {
             "content": "Inhalt der Kategorie:",
             "empty": "*Diese Kategorie ist leer*",

+ 90 - 89
i18n/en.json

@@ -113,14 +113,14 @@
     },
     "dateformat": "en-US",
     "diff": {
-        "badrev": "at least one revision doesn't exist!",
+        "badrev": "At least one revision doesn't exist!",
         "hidden": "*hidden*",
         "info": {
             "added": "Added:",
-            "bytes": "$1 {{PLURAL:$2|byte|bytes}} $3",
+            "bytes": "$1 {{PLURAL:$2|byte|bytes}}",
             "comment": "Comment:",
             "editor": "Editor:",
-            "minor": "(m)",
+            "minor": "_(m)",
             "more": "And more",
             "removed": "Removed:",
             "size": "Difference:",
@@ -146,15 +146,15 @@
     ],
     "general": {
         "database": "⚠️ **Limited Functionality** ⚠️\nNo settings found, please contact the bot owner!",
-        "default": "this server isn't set up yet. Use $1 or the dashboard to change the settings.",
+        "default": "This server isn't set up yet. Use $1 or the dashboard to change the settings.",
         "disclaimer": "I am a small bot with the purpose to easily link and search MediaWiki sites like Wikipedia and Fandom wikis. I show short descriptions and additional info about pages and am able to resolve redirects and follow interwiki links. $1 wrote me in JavaScript.\n\nYou can support me on Patreon:",
         "experimental": "**This feature is experimental! It is not guaranteed to work correctly and may be removed in the future.**",
         "helpserver": "For questions and problems please visit my support server:",
         "limit": "🚨 **Stop, you hit a limit!** 🚨\n\n$1, your message contained too many commands!",
         "missingperm": "I'm missing some permissions for this command:",
-        "patreon": "this is a Patreon only feature!\nYou can support me on Patreon to get access to this feature:",
-        "prefix": "the prefix for this server is `$1`. You can change the prefix with `$1settings prefix`. For a list of all commands see `$1help`.",
-        "readonly": "**the database is currently in read-only mode, you can't change any settings right now!**"
+        "patreon": "This is a Patreon only feature!\nYou can support me on Patreon to get access to this feature:",
+        "prefix": "The prefix for this server is `$1`. You can change the prefix with `$1settings prefix`. For a list of all commands see `$1help`.",
+        "readonly": "**The database is currently in read-only mode, you can't change any settings right now!**"
     },
     "help": {
         "admin": "These commands can only be performed by administrators:",
@@ -389,7 +389,7 @@
                 "desc": "I will answer with a link to a matching article in the named Wikia wiki: `https://<wiki>.wikia.org/`"
             }
         },
-        "noadmin": "you need the `Manage Server` permission for these commands!",
+        "noadmin": "You need the `Manage Server` permission for these commands!",
         "pause": "**I'm currently paused on this server!**\nOnly these commands can be performed:"
     },
     "interaction": {
@@ -490,34 +490,34 @@
     "rcscript": {
         "ad": "You want recent changes directly in Discord? Use `$1rcscript` to add a recent changes webhook based on **$2** to your Discord server!",
         "add_more": "Add more recent changes webhooks:",
-        "added": "a recent changes webhook has been added for:",
-        "all_inactive": "you can't have wiki changes and feeds based changes disabled at the same time.",
+        "added": "A recent changes webhook has been added for:",
+        "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_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`!",
+        "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:",
-        "current": "these are the current recent changes webhooks for this server:",
-        "current_display": "the display mode for this webhook is:",
-        "current_lang": "the language for this webhook is:",
-        "current_selected": "this is the recent changes webhook `$1` for this server:",
-        "current_wiki": "the wiki for this webhook is:",
+        "current": "These are the current recent changes webhooks for this server:",
+        "current_display": "The display mode for this webhook is:",
+        "current_lang": "The language for this webhook is:",
+        "current_selected": "This is the recent changes webhook `$1` for this server:",
+        "current_wiki": "The wiki for this webhook is:",
         "dashboard": {
             "added": "$1 added the recent changes webhook with id `$2`.",
             "removed": "$1 removed the recent changes webhook with id `$2`.",
             "updated": "$1 updated the recent changes webhook with id `$2`."
         },
         "delete": "Delete this recent changes webhook:",
-        "deleted": "the recent changes webhook has been deleted.",
+        "deleted": "The recent changes webhook has been deleted.",
         "disabled": "disabled",
-        "disabled_feeds": "the feeds based changes, like discussions, message walls and article comments, for this webhook have been disabled.",
-        "disabled_rc": "the wiki changes for this webhook have been disabled.",
+        "disabled_feeds": "The feeds based changes, like discussions, message walls and article comments, for this webhook have been disabled.",
+        "disabled_rc": "The wiki changes for this webhook have been disabled.",
         "display": "Display mode:",
         "enabled": "enabled",
-        "enabled_feeds": "the feeds based changes, like discussions, message walls and article comments, for this webhook have been enabled.",
-        "enabled_rc": "the wiki changes for this webhook have been enabled.",
+        "enabled_feeds": "The feeds based changes, like discussions, message walls and article comments, for this webhook have been enabled.",
+        "enabled_rc": "The wiki changes for this webhook have been enabled.",
         "feeds": "Feeds based changes:",
         "help_display_compact": "Compact text messages with inline links.",
         "help_display_diff": "Embed messages with image previews and edit differences.",
@@ -527,20 +527,20 @@
         "help_lang": "Currently supported languages are:",
         "help_wiki": "Link to a MediaWiki site like `https://<wiki>.fandom.com/`",
         "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.",
+        "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.",
-        "noadmin": "you need the `Manage Webhooks` permission for this command!",
+        "no_feeds": "The wiki for this webhook has no feeds based features, like discussions, message walls or article comments, enabled.",
+        "noadmin": "You need the `Manage Webhooks` permission for this command!",
         "rc": "Wiki changes:",
-        "sysmessage": "the system message `$1` has to be the server id `$2` to add a recent changes webhook.",
+        "sysmessage": "The system message `$1` has to be the server id `$2` to add a recent changes webhook.",
         "title": "Recent changes webhook",
         "toggle": "(toggle)",
-        "updated_display": "the display mode for this webhook has been changed to:",
-        "updated_lang": "the language for this webhook has been changed to:",
-        "updated_wiki": "the wiki for this webhook has been changed to:",
+        "updated_display": "The display mode for this webhook has been changed to:",
+        "updated_lang": "The language for this webhook has been changed to:",
+        "updated_wiki": "The wiki for this webhook has been changed to:",
         "webhook": {
             "blocked": "This recent changes webhook will be deleted because the wiki has been blocked!",
             "blocked_help": "You can ask for more details on the [support server]($1).",
@@ -574,10 +574,11 @@
             "updated_lang": "The language has been changed to `$1` for this recent changes webhook.",
             "updated_wiki": "The wiki has been changed to $1 for this recent changes webhook."
         },
-        "webhook_failed": "sadly the webhook couldn't be created, please try again later.",
+        "webhook_failed": "Sadly the webhook couldn't be created, please try again later.",
         "wiki": "Wiki:"
     },
     "search": {
+        "cached": "Cache last updated:",
         "category": {
             "content": "Content of this category:",
             "empty": "*This category is empty*",
@@ -595,14 +596,14 @@
     },
     "settings": {
         "button": "Use the Dashboard",
-        "channel current": "these are the current settings for this channel:",
-        "channel lang": "the language for this channel is:",
-        "channel langchanged": "you changed the language for this channel to:",
-        "channel role": "the minimal required role for this channel is:",
-        "channel rolechanged": "you changed the minimal required role for this channel to:",
-        "channel wiki": "the default wiki for this channel is:",
-        "channel wikichanged": "you changed the default wiki for this channel to:",
-        "current": "these are the current settings for this server:",
+        "channel current": "These are the current settings for this channel:",
+        "channel lang": "The language for this channel is:",
+        "channel langchanged": "You changed the language for this channel to:",
+        "channel role": "The minimal required role for this channel is:",
+        "channel rolechanged": "You changed the minimal required role for this channel to:",
+        "channel wiki": "The default wiki for this channel is:",
+        "channel wikichanged": "You changed the default wiki for this channel to:",
+        "current": "These are the current settings for this server:",
         "currentchannel": "Channel overwrites:",
         "currentinline": "Inline commands:",
         "currentlang": "Language:",
@@ -616,42 +617,42 @@
         },
         "foundwikis": "Do you mean any of these wikis?",
         "inline disabled": {
-            "channel inline": "inline commands are currently disabled for this channel.",
-            "channel inlinechanged": "you disabled inline commands for this channel.",
+            "channel inline": "Inline commands are currently disabled for this channel.",
+            "channel inlinechanged": "You disabled inline commands for this channel.",
             "help": "Use `$1` to enable inline commands like `[[$2]]` and `{{$2}}`.",
-            "inline": "inline commands are currently disabled for this server.",
-            "inlinechanged": "you disabled inline commands for this server."
+            "inline": "Inline commands are currently disabled for this server.",
+            "inlinechanged": "You disabled inline commands for this server."
         },
         "inline enabled": {
-            "channel inline": "inline commands are currently enabled for this channel.",
-            "channel inlinechanged": "you enabled inline commands for this channel.",
+            "channel inline": "Inline commands are currently enabled for this channel.",
+            "channel inlinechanged": "You enabled inline commands for this channel.",
             "help": "Use `$1` to disable inline commands like `[[$2]]` and `{{$2}}`.",
-            "inline": "inline commands are currently enabled for this server.",
-            "inlinechanged": "you enabled inline commands for this server."
+            "inline": "Inline commands are currently enabled for this server.",
+            "inlinechanged": "You enabled inline commands for this server."
         },
-        "lang": "the language for this server is:",
-        "langchanged": "you changed the language for this server to:",
+        "lang": "The language for this server is:",
+        "langchanged": "You changed the language for this server to:",
         "langhelp": "Use `$1 <language>` to change the language.\nCurrently supported languages are:",
-        "langinvalid": "the specified language is not supported!",
-        "missing": "this server isn't set up yet. Use $1 and $2 to change the settings.",
+        "langinvalid": "The specified language is not supported!",
+        "missing": "This server isn't set up yet. Use $1 and $2 to change the settings.",
         "nochannels": "*No channel overwrites yet*",
-        "prefix": "the prefix for this server is:",
-        "prefixchanged": "you changed the prefix for this server to:",
+        "prefix": "The prefix for this server is:",
+        "prefixchanged": "You changed the prefix for this server to:",
         "prefixhelp": "Use `$1 <prefix>` to change the prefix.\nUse `_` at the end to indicate a space at the end of the prefix.\nThe prefix may not include mentions!",
-        "prefixinvalid": "the specified prefix is not supported!",
-        "role": "the minimal required role for this server is:",
-        "rolechanged": "you changed the minimal required role for this server to:",
+        "prefixinvalid": "The specified prefix is not supported!",
+        "role": "The minimal required role for this server is:",
+        "rolechanged": "You changed the minimal required role for this server to:",
         "rolehelp": "Use `$1 <role>` to change the minimal required role.",
-        "roleinvalid": "the specified role does not exist!",
-        "save_failed": "sadly the settings couldn't be saved, please try again later.",
-        "wiki": "the default wiki for this server is:",
-        "wikichanged": "you changed the default wiki for this server to:",
+        "roleinvalid": "The specified role does not exist!",
+        "save_failed": "Sadly the settings couldn't be saved, please try again later.",
+        "wiki": "The default wiki for this server is:",
+        "wikichanged": "You changed the default wiki for this server to:",
         "wikihelp": "Use `$1 <link>` to change the default wiki.\nLink to a MediaWiki site like `https://<wiki>.fandom.com/`",
-        "wikiinvalid": "please provide a valid link to a MediaWiki site, like Wikipedia or a Fandom wiki!",
-        "wikiinvalid_http": "the provided website doesn't have a valid TLS/SSL certificate! For security reasons only wikis using HTTPS are supported.\nIf you are a site administrator, you can get a certificate from a certificate authority like *Let’s Encrypt*:\n<https://letsencrypt.org/getting-started/>",
-        "wikiinvalid_private": "the provided wiki is private! Only public wikis that can be read by everyone are supported.",
-        "wikiinvalid_timeout": "the provided link took too long to respond!",
-        "wikimissing": "no default wiki is set for this server yet!"
+        "wikiinvalid": "Please provide a valid link to a MediaWiki site, like Wikipedia or a Fandom wiki!",
+        "wikiinvalid_http": "The provided website doesn't have a valid TLS/SSL certificate! For security reasons only wikis using HTTPS are supported.\nIf you are a site administrator, you can get a certificate from a certificate authority like *Let’s Encrypt*:\n<https://letsencrypt.org/getting-started/>",
+        "wikiinvalid_private": "The provided wiki is private! Only public wikis that can be read by everyone are supported.",
+        "wikiinvalid_timeout": "The provided link took too long to respond!",
+        "wikimissing": "No default wiki is set for this server yet!"
     },
     "test": {
         "MediaWiki": "Requires at least $1 for full functionality, found `$2`.",
@@ -660,7 +661,7 @@
         "text": [
             "I'm fully functional!",
             "I'm still alive!",
-            "and believe me, I am still alive.",
+            "And believe me, I am still alive.",
             "I'm doing science and I'm still alive.",
             "I feel fantastic and I'm still alive.",
             " ",
@@ -735,13 +736,13 @@
     "verification": {
         "accountage": "Account age:",
         "add_more": "Add more verifications:",
-        "added": "the verification has been added:",
+        "added": "The verification has been added:",
         "and": "and",
         "channel": "Channel:",
-        "channel_max": "you provided too many channels.",
-        "channel_missing": "the provided channel does not exist.",
-        "current": "these are the current verifications for this server:",
-        "current_selected": "this is the verification `$1` for this server:",
+        "channel_max": "You provided too many channels.",
+        "channel_missing": "The provided channel does not exist.",
+        "current": "These are the current verifications for this server:",
+        "current_selected": "This is the verification `$1` for this server:",
         "dashboard": {
             "added": "$1 added the verification with id `$2`.",
             "added_notice": "$1 added some verification notices.",
@@ -750,7 +751,7 @@
             "updated_notice": "$1 updated some verification notices."
         },
         "delete_current": "Delete this verification:",
-        "deleted": "the verification has been deleted.",
+        "deleted": "The verification has been deleted.",
         "disabled": "disabled",
         "editcount": "Edit count:",
         "enabled": "enabled",
@@ -759,15 +760,15 @@
         "indays": "(in days)",
         "logging": "Logging channel:",
         "match": "Missing requirements notice:",
-        "max_entries": "you already reached the maximal amount of verifications.",
-        "missing": "there are no verifications for this server yet.",
+        "max_entries": "You already reached the maximal amount of verifications.",
+        "missing": "There are no verifications for this server yet.",
         "new_accountage": "<new account age>",
         "new_channel": "<new channel>",
         "new_editcount": "<new edit count>",
         "new_postcount": "<new post count>",
         "new_role": "<new role>",
         "new_usergroup": "<new user group>",
-        "no_role": "please provide a role for the new verification.",
+        "no_role": "Please provide a role for the new verification.",
         "notice_embed": "Some notices include masked markdown links. Make sure the bot has the `Embed Links` permissions in all verification channels for them to work properly.",
         "or": "or",
         "postcount": "Post count (only Fandom wikis):",
@@ -777,20 +778,20 @@
         "rename_no_permission": "**$1 is missing the `Manage Nicknames` permission to force wiki usernames!**",
         "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_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.",
+        "save_failed": "Sadly the verification couldn't be saved, please try again later.",
         "success": "Success notice:",
         "toggle": "(toggle)",
-        "updated": "the verification has been updated:",
+        "updated": "The verification has been updated:",
         "usergroup": "User group:",
-        "usergroup_max": "you provided too many user groups.",
-        "usergroup_too_long": "the provided user group is too long.",
-        "value_too_high": "the provided value is too high."
+        "usergroup_max": "You provided too many user groups.",
+        "usergroup_too_long": "The provided user group is too long.",
+        "value_too_high": "The provided value is too high."
     },
     "verify": {
         "audit_reason": "Verified as \"$1\"",
@@ -799,7 +800,7 @@
         "discord": "Discord user:",
         "empty": "*empty*",
         "error": "The verification failed due to an error.",
-        "error_reply": "the verification failed due to an error, please try again.",
+        "error_reply": "The verification failed due to an error, please try again.",
         "failed_gblock": "**Check for global block failed!**",
         "failed_rename": "**Changing {{GENDER:$1|his|her|their}} Discord nickname failed!**",
         "failed_roles": "**Adding roles failed!**",
@@ -809,12 +810,12 @@
         "help_guide": "Follow [this guide]($1) to add your Discord tag to your wiki profile:",
         "help_missing": "Please make sure you are using your wiki username and the caseing is correct.",
         "help_subpage": "Please add your Discord tag ($1) to your Discord subpage on the wiki:",
-        "missing": "there are no verifications set up for this channel.",
+        "missing": "There are no verifications set up for this channel.",
         "notice": "Notice:",
         "oauth_button": "Authenticate",
-        "oauth_message": "please use [this link]($1) to authenticate your wiki account.",
+        "oauth_message": "Please use [this link]($1) to authenticate your wiki account.",
         "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_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_add": "Added to:",
         "qualified_add_error": "Can't be added to:",
@@ -840,9 +841,9 @@
     "voice": {
         "channel": "Voice channel",
         "disable": "Use `$1` to disable this function.",
-        "disabled": "you disabled the function to add roles for voice channels.",
+        "disabled": "You disabled the function to add roles for voice channels.",
         "enable": "Use `$1` to enable this function.",
-        "enabled": "you enabled the function to add roles for voice channels.",
+        "enabled": "You enabled the function to add roles for voice channels.",
         "join": "$1 joined the voice channel \"$2\".",
         "left": "$1 left the voice channel \"$2\".",
         "name": "voice channel name",

+ 24 - 2
i18n/es.json

@@ -117,10 +117,10 @@
         "hidden": "*oculto*",
         "info": {
             "added": "Añadido:",
-            "bytes": "$1 {{PLURAL:$2|Byte|Bytes}} $3",
+            "bytes": "$1 {{PLURAL:$2|Byte|Bytes}}",
             "comment": "Comentario:",
             "editor": "Editor:",
-            "minor": "(m)",
+            "minor": "_(m)",
             "more": "Y más",
             "removed": "Eliminado:",
             "size": "Diferencia:",
@@ -494,6 +494,8 @@
         "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_edit": "Actualizó el webhook de cambios recientes",
+        "avatar": "Avatar del webhook:",
         "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:",
@@ -527,6 +529,7 @@
         "lang": "Idioma:",
         "max_entries": "ya alcanzaste la cantidad máxima de webhooks de cambios recientes.",
         "missing": "todavía no hay webhooks de cambios recientes en este servidor.",
+        "name": "Nombre del webhook:",
         "new_lang": "<nuevo idioma>",
         "new_wiki": "<enlace al wiki>",
         "no_feeds": "el wiki de este webhook no tiene funciones basadas en feeds, como discusiones, muros de mensajes o comentarios de artículos, habilitadas.",
@@ -544,6 +547,7 @@
             "blocked_reason": "¡Este webhook de cambios recientes se eliminará porque el wiki ha sido bloqueado por `$1`!",
             "created": "Se ha agregado a este canal un webhook de cambios recientes para $1.",
             "dashboard": {
+                "avatar": "• Se ha cambiado el avatar del webhook.",
                 "channel": "• El webhook se ha movido a este canal.",
                 "disabled_feeds": "• Los cambios basados en feeds, como discusiones, muros de mensajes y comentarios de artículos, se han desactivado.",
                 "disabled_rc": "• Los cambios del wiki se han desactivado.",
@@ -554,6 +558,7 @@
                 "enabled_feeds": "• Se han activado los cambios basados en feeds, como discusiones, muros de mensajes y comentarios de artículos.",
                 "enabled_rc": "• Se han activado los cambios del wiki.",
                 "lang": "• El idioma se ha cambiado a $1.",
+                "name": "• El nombre del webhook se ha cambiado a \"$1\".",
                 "updated": "Este webhook de cambios recientes se ha actualizado:",
                 "wiki": "• El wiki se ha cambiado a $1."
             },
@@ -589,6 +594,7 @@
         "special": "Contenido de esta página especial:"
     },
     "settings": {
+        "button": "Usar el panel de control",
         "channel current": "estas son las configuraciones actuales en este canal:",
         "channel lang": "el idioma de este canal es:",
         "channel langchanged": "cambiaste el idioma de este canal a:",
@@ -748,6 +754,8 @@
         "disabled": "desactivada",
         "editcount": "Número de ediciones:",
         "enabled": "activada",
+        "flag_logall": "Registro de verificaciones fallidas:",
+        "flag_private": "Envío de respuestas de comandos de verificación de forma privada:",
         "indays": "(en días)",
         "logging": "Canal de registro:",
         "match": "Aviso de requisitos faltantes:",
@@ -767,10 +775,13 @@
         "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_add": "Rol a agregar:",
         "role_deleted": "**¡El rol $1 parece que ya no existe!**",
         "role_managed": "no se puede asignar el rol proporcionado.",
         "role_max": "proporcionaste demasiados roles.",
         "role_missing": "el rol proporcionado no existe.",
+        "role_none": "ninguno",
+        "role_remove": "Rol a remover:",
         "role_too_high": "**¡El rol $1 está en un rango muy alto como para asignarlo a $2!**",
         "save_failed": "lamentablemente, no se pudo guardar la verificación. Vuelve a intentarlo más tarde.",
         "success": "Aviso de operación correcta:",
@@ -783,6 +794,8 @@
     },
     "verify": {
         "audit_reason": "Verificado como \"$1\"",
+        "button_again": "Revisar otra vez",
+        "button_wrong_user": "No puedes volver a comprobar esta verificación, ¡es la verificación de $1!",
         "discord": "Usuario de Discord:",
         "empty": "*vacío*",
         "error": "La verificación falló debido a un error.",
@@ -798,6 +811,15 @@
         "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:",
+        "oauth_button": "Autenticar",
+        "oauth_message": "por favor usa [este enlace]($1) para autenticar tu cuenta wiki.",
+        "oauth_message_dm": "Por favor, utiliza este enlace para autenticar tu cuenta wiki para $1.",
+        "oauth_private": "el wiki usa OAuth2 para la verificación. Por favor, habilita los mensajes directos de este servidor o use el comando `/verify` para que pueda enviarte un enlace de autenticación de forma privada.",
+        "oauth_used": "*Verificado usando OAuth2*",
+        "qualified_add": "Agregado a:",
+        "qualified_add_error": "No se puede agregar a:",
+        "qualified_remove": "Eliminado de:",
+        "qualified_remove_error": "No se puede eliminar de:",
         "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!**",

+ 2 - 2
i18n/fr.json

@@ -117,10 +117,10 @@
         "hidden": "*masqué*",
         "info": {
             "added": "Ajoutés :",
-            "bytes": "$1 {{PLURAL:$2|octet|octets}} $3",
+            "bytes": "$1 {{PLURAL:$2|octet|octets}}",
             "comment": "Commentaire :",
             "editor": "Éditeur :",
-            "minor": "(m)",
+            "minor": "_(m)",
             "more": "Et plus",
             "removed": "Retirés :",
             "size": "Différence :",

+ 2 - 2
i18n/hi.json

@@ -117,10 +117,10 @@
         "hidden": "*छिपाया गया*",
         "info": {
             "added": "जोड़ा:",
-            "bytes": "$1 {{PLURAL:$2|बाइट|बाइट}} $3",
+            "bytes": "$1 {{PLURAL:$2|बाइट|बाइट}}",
             "comment": "कमेंट:",
             "editor": "सदस्य:",
-            "minor": "(छो)",
+            "minor": "_(छो)",
             "more": "और भी",
             "removed": "हटाया:",
             "size": "अंतर:",

+ 102 - 11
i18n/ja.json

@@ -117,10 +117,10 @@
         "hidden": "*隠された*",
         "info": {
             "added": "追加:",
-            "bytes": "$1 {{PLURAL:$2|バイト|バイト}} $3",
+            "bytes": "$1 {{PLURAL:$2|バイト|バイト}}",
             "comment": "コメント:",
             "editor": "編集者:",
-            "minor": "(細)",
+            "minor": "_(細)",
             "more": "その他にも",
             "removed": "削除:",
             "size": "差分:",
@@ -265,31 +265,31 @@
             "rcscript": {
                 "add": {
                     "cmd": "rcscript add [<wiki>]",
-                    "desc": "新しい最近の更新のウェブフックを追加します。"
+                    "desc": "新しい最近の更新のWebhookを追加します。"
                 },
                 "default": {
                     "cmd": "rcscript",
-                    "desc": "最近の更新のウェブフックを変更します。"
+                    "desc": "最近の更新のWebhookを変更します。"
                 },
                 "delete": {
                     "cmd": "rcscript delete",
-                    "desc": "Wiki-Botから最近の更新のウェブフックを削除します。"
+                    "desc": "Wiki-Botから最近の更新のWebhookを削除します。"
                 },
                 "display": {
                     "cmd": "rcscript display <新規 表示モード>",
-                    "desc": "Wiki-Botの最近の更新Webhookの表示モードを変更します。"
+                    "desc": "Wiki-Botの最近の更新Webhookの表示モードを変更します。"
                 },
                 "feeds": {
                     "cmd": "rcscript feeds",
-                    "desc": "Fandom Wikiの更新されたディスカッションのウェブフックを最近の更新のウェブフックへ切り替えます。"
+                    "desc": "Fandom Wikiの更新されたディスカッションのWebhookを最近の更新のWebhookへ切り替えます。"
                 },
                 "lang": {
                     "cmd": "rcscript lang <新規言語>",
-                    "desc": "Wiki-Botの最近の更新のウェブフックの言語を変更します。"
+                    "desc": "Wiki-Botの最近の更新のWebhookの言語を変更します。"
                 },
                 "wiki": {
                     "cmd": "rcscript wiki <新規wiki>",
-                    "desc": "Web-Botの最近の更新ウェブフックのWikiを変更します。"
+                    "desc": "Web-Botの最近の更新のWebhookのWikiを変更します。"
                 }
             },
             "search": {
@@ -385,9 +385,100 @@
                 "desc": "このボイスチャンネルの全員に特定のロールを与えるようにします。"
             },
             "wikia": {
-                "cmd": "??<wiki> <検索キーワード>"
+                "cmd": "??<wiki> <検索キーワード>",
+                "desc": "選択したWikia wikiの関連記事へのリンクを返信します:`https://<wiki>.wikia.org/`"
             }
-        }
+        },
+        "noadmin": "これらのコマンドを実行するには、`Manage Server`のパーミッションが必要です!",
+        "pause": "**このサーバーでは現在停止中です!**\nこれらのコマンドのみが実行可能です。"
+    },
+    "interaction": {
+        "dashboard": {
+            "perm_allow": "許可",
+            "perm_default": "デフォルト",
+            "perm_deny": "拒否",
+            "updated": "$1は、`$2`のスラッシュコマンドのパーミッションを上書き更新しました。"
+        },
+        "inline": "このコマンドを使用するには、参照するテキストを[[wikitext]]リンクを指定します。",
+        "nowiki": "使われているWikiは存在しません!",
+        "verify": "このコマンドでWikiのユーザー名を入力すると、DiscordアカウントとWikiアカウントが本人のものであることを確認し、Wikiアカウントに関連するロールを取得することができます。"
+    },
+    "invite": {
+        "bot": "別のサーバーに招待するには、このリンクを使ってください。"
+    },
+    "minecraft": {
+        "fixed": "{{PLURAL:$1|バージョン|バージョン}}で修正しました:",
+        "issue_link": {
+            "Blocks": {
+                "inward": "は$1でブロックされています",
+                "outward": "$1はブロックされています"
+            },
+            "Bonfire Testing": {
+                "inward": "$1のテスト中に検出された",
+                "outward": "$1のテスト中にバグが検出されました"
+            },
+            "Cloners": {
+                "inward": "は、$1でクローン化されています",
+                "outward": "クローン $1"
+            },
+            "Duplicate": {
+                "inward": "は、$1で重複しています",
+                "outward": "$1の複製"
+            },
+            "Relates": {
+                "inward": "は、$1に関連しています",
+                "outward": "は、$1に関連しています"
+            }
+        },
+        "more": "さらに$1あります。",
+        "private": "**機密性の高いエラーレポート**",
+        "status": {
+            "Awaiting Response": "回答の保留",
+            "Cannot Reproduce": "再現性がありません",
+            "Closed": "閉鎖",
+            "Done": "完了",
+            "Duplicate": "重複",
+            "Fixed": "修復済",
+            "In Progress": "進行中",
+            "Incomplete": "未完成",
+            "Invalid": "無効",
+            "Open": "公開",
+            "Postponed": "延期済",
+            "Reopened": "再開",
+            "Resolved": "解決済",
+            "Won't Fix": "修正不能",
+            "Works As Intended": "意図した通りの動作(仕様)"
+        },
+        "total": "$1 {{PLURAL:$2|問題は|問題は}} 修正されました"
+    },
+    "overview": {
+        "admins": "管理者:",
+        "articles": "記事:",
+        "comments": "記事のコメント:",
+        "created": "作成日時:",
+        "crossover": "合併先:",
+        "description": "説明:",
+        "edits": "編集:",
+        "founder": "創設者:",
+        "image": "画像:",
+        "inaccurate": "統計が不正確な場合もあります",
+        "lang": "言語:",
+        "license": "ライセンス:",
+        "manager": "Wiki管理人:",
+        "misermode": "サーバーファームの負荷分散状態(Miser mode):",
+        "no": "いいえ",
+        "none": "*無し*",
+        "official": "公式Wiki:",
+        "pages": "ページ総数:",
+        "posts": "ディスカッションの投稿:",
+        "readonly": "このWikiは現在、読み取り専用です!",
+        "rtl": "右から左へ:",
+        "talk": "討論",
+        "topic": "トピック:",
+        "unknown": "*不明*",
+        "users": "アクティブユーザー:",
+        "version": "バージョン:",
+        "vertical": "縦型:"
     },
     "test": {
         "text": [

+ 33 - 4
i18n/ko.json

@@ -120,6 +120,7 @@
             "bytes": "$1 바이트",
             "comment": "코멘트:",
             "editor": "편집자:",
+            "minor": "_(잔글)",
             "more": "내용 더 있음",
             "removed": "삭제됨:",
             "size": "차이:",
@@ -392,7 +393,15 @@
         "pause": "**저는 이 서버에서 일시중지되어 있어요!**\n지금은 이 명령어만 사용할 수 있어요."
     },
     "interaction": {
-        "inline": "이 명령어를 사용하려면 [[위키텍스트]]와 다른 텍스트를 같이 제공해야 합니다."
+        "dashboard": {
+            "perm_allow": "허용",
+            "perm_default": "기본값",
+            "perm_deny": "거부",
+            "updated": "$1 님이 `$2` 슬래시 명령어 권한을 업데이트했습니다."
+        },
+        "inline": "이 명령어를 사용하려면 [[위키텍스트]]와 다른 텍스트를 같이 제공해야 합니다.",
+        "nowiki": "언급한 위키는 존재하지 않아요!",
+        "verify": "위키 계정 이름을 명령어 뒤에 입력하여 디스코드 계정과 위키 계정을 인증하고 위키 계정에 해당하는 역할을 받으세요."
     },
     "invite": {
         "bot": "저를 다른 서버에 초대하려면 이 링크를 사용하세요."
@@ -485,6 +494,8 @@
         "all_inactive": "위키 최근 바뀜과 피드 기반 바뀜이 동시에 비활성화되면 안 됩니다.",
         "audit_reason": "\"$1\" 위키의 최근 바뀜 웹훅",
         "audit_reason_delete": "삭제된 최근 바뀜 웹훅",
+        "audit_reason_edit": "최근 바뀜 웹후크를 업데이트했습니다",
+        "avatar": "웹후크 아바타:",
         "blocked": "이 위키는 최근 바뀜 웹훅으로 추가되지 못하도록 차단되어 있습니다!",
         "blocked_reason": "이 위키는 `$1` 때문에 최근 바뀜 웹훅으로 추가되지 못하도록 차단되어 있습니다!",
         "channel": "채널:",
@@ -518,6 +529,7 @@
         "lang": "언어:",
         "max_entries": "최근 바뀜 웹훅 최대치에 도달했습니다.",
         "missing": "이 서버에 아직 최근 바뀜 웹훅이 없습니다.",
+        "name": "웹후크 이름:",
         "new_lang": "<새 언어>",
         "new_wiki": "<위키의 링크>",
         "no_feeds": "이 웹훅의 위키는 토론, 메시지 담벼락, 문서 댓글과 같은 피드 기반 기능이 활성화되어있지 않습니다.",
@@ -535,6 +547,7 @@
             "blocked_reason": "이 위키가 `$1` 때문에 차단되었기 때문에 최근 바뀜 웹훅이 삭제되었습니다!",
             "created": "$1 최근 바뀜 웹후크가 채널에 추가되었습니다.",
             "dashboard": {
+                "avatar": "• 웹후크 아바타가 변경되었습니다.",
                 "channel": "• 웹훅이 이 채널로 이동되었습니다.",
                 "disabled_feeds": "• 토론, 메시지 담벼락, 문서 댓글과 같은 피드 기반 변경이 비활성화되었습니다.",
                 "disabled_rc": "• 위키 최근 바뀜이 비활성화되었습니다.",
@@ -545,6 +558,7 @@
                 "enabled_feeds": "• 토론, 메시지 담벼락, 문서 댓글과 같은 피드 기반 바뀜 기능이 활성화되었습니다.",
                 "enabled_rc": "• 위키 최근 바뀜이 활성화되었습니다.",
                 "lang": "• $1 언어로 변경되었습니다.",
+                "name": "• \"$1\" 웹후크의 이름이 변경되었습니다.",
                 "updated": "이 최근 바뀜 웹훅이 업데이트되었습니다:",
                 "wiki": "• $1 위키로 위키가 변경되었습니다."
             },
@@ -580,6 +594,7 @@
         "special": "이 특수 문서의 내용:"
     },
     "settings": {
+        "button": "대시보드 사용하기",
         "channel current": "이 채널의 현재 설정은 다음과 같습니다.",
         "channel lang": "이 채널의 언어는 다음과 같습니다:",
         "channel langchanged": "이 채널의 언어를 다음 언어로 변경하였습니다:",
@@ -633,7 +648,7 @@
         "wikichanged": "이 서버의 기본 위키를 다음 위키로 변경했습니다:",
         "wikihelp": "`$1 <링크>` 명령어로 기본 위키를 변경하세요.\n`https://<위키>.fandom.com` 과 같은 미디어위키 사이트 링크를 입력하세요",
         "wikiinvalid": "위키백과나 팬덤 위키와 같은 유효한 미디어위키 사이트로 가는 링크를 제공해 주세요!",
-        "wikiinvalid_http": "제공된 웹사이트에 TLS/SSL 연결을 할 수 없습니다! 보안 문제로 HTTPS를 사용하는 위키만 지원됩니다.\n사이트 관리자라면 *Let's Encrypt*와 같은 인증 기관에서 인증서를 받을 수 있습니다.\n<https://letsencrypt.org/ko/getting-started/>",
+        "wikiinvalid_http": "제공된 웹사이트에 해당하는 TLS/SSL 인증서를 찾을 수 없습니다! 보안상의 이유로 HTTPS를 사용하는 위키만 지원됩니다.\n사이트 관리자라면 *Let's Encrypt*와 같은 인증 기관에서 인증서를 받을 수 있습니다.\n<https://letsencrypt.org/ko/getting-started/>",
         "wikiinvalid_private": "제공된 위키는 비공개 위키입니다! 모두가 읽을 수 있는 공개 위키만 지원하고 있습니다.",
         "wikiinvalid_timeout": "이 링크는 응답하기에는 너무 깁니다!",
         "wikimissing": "이 서버의 기본 위키가 설정되지 않았습니다!"
@@ -678,7 +693,18 @@
     },
     "user": {
         "block": {
-            "header": "$1 사용자는 현재 차단되어 있어요!"
+            "duration": {
+                "days": "$1일",
+                "hours": "$1시간",
+                "minutes": "$1분",
+                "months": "$1개월",
+                "separator": ",_",
+                "weeks": "$1주",
+                "years": "$1년"
+            },
+            "header": "$1 사용자는 현재 차단되어 있어요!",
+            "indef_noreason": "$4 사용자가 $1에 무기한 차단했어요.",
+            "indef_text": "$4 사용자가 \"$5\" 이유로 $1 에 무기한 차단했어요."
         },
         "gblock": {
             "disabled": "이 계정은 비활성화되어 있어요!",
@@ -715,14 +741,17 @@
         "current_selected": "이 서버의 `$1` 인증 상태예요:",
         "dashboard": {
             "added": "$1 님이 `$2` 인증을 추가했어요.",
+            "added_notice": "$1 님이 인증 알림을 추가했어요.",
             "removed": "$1 님이 `$2` 인증을 제거했어요.",
-            "updated": "$1 님이 `$2` 인증을 수정했어요."
+            "updated": "$1 님이 `$2` 인증을 수정했어요.",
+            "updated_notice": "$1 님이 인증 알림을 업데이트했어요."
         },
         "delete_current": "이 인증을 삭제할까요?",
         "deleted": "인증을 삭제했습니다.",
         "disabled": "비활성화됨",
         "editcount": "편집 수:",
         "enabled": "활성화됨",
+        "flag_logall": "인증 실패 기록:",
         "indays": "(날짜로)",
         "max_entries": "설정할 수 있는 인증 최대치에 도달했어요.",
         "missing": "이 서버에 아직 인증이 설정되지 않았어요.",

+ 3 - 2
i18n/pl.json

@@ -117,10 +117,10 @@
         "hidden": "*ukryte*",
         "info": {
             "added": "Dodano:",
-            "bytes": "$1 {{PLURAL:$2|bajt|bajty|bajtów}} $3",
+            "bytes": "$1 {{PLURAL:$2|bajt|bajty|bajtów}}",
             "comment": "Komentarz:",
             "editor": "Autor:",
-            "minor": "(d)",
+            "minor": "_(d)",
             "more": "Oraz więcej",
             "removed": "Usunięto:",
             "size": "Wielkość zmiany:",
@@ -783,6 +783,7 @@
         "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.",
+        "success": "Powiadomienie o sukcesie:",
         "toggle": "(przełącz)",
         "updated": "weryfikacja została zaktualizowana:",
         "usergroup": "Grupa użytkownika:",

+ 7 - 2
i18n/pt-br.json

@@ -117,10 +117,10 @@
         "hidden": "*oculto*",
         "info": {
             "added": "Adicionado:",
-            "bytes": "$1 {{PLURAL:$2|byte|bytes}} $3",
+            "bytes": "$1 {{PLURAL:$2|byte|bytes}}",
             "comment": "Resumo:",
             "editor": "Editor:",
-            "minor": "(m)",
+            "minor": "_(m)",
             "more": "E mais",
             "removed": "Removido:",
             "size": "Diferença:",
@@ -494,6 +494,8 @@
         "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_edit": "Webhook de mudanças recentes atualizado",
+        "avatar": "Avatar do webhook:",
         "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:",
@@ -527,6 +529,7 @@
         "lang": "Idioma:",
         "max_entries": "você já atingiu a quantidade máxima de webhooks de mudanças recentes.",
         "missing": "ainda não há webhooks de mudanças recentes para este servidor.",
+        "name": "Nome do webhook:",
         "new_lang": "<novo idioma>",
         "new_wiki": "<link para a wiki>",
         "no_feeds": "a wiki deste webhook não possui recursos baseados em feeds, como discussões, murais de mensagens ou comentários de artigos, ativados.",
@@ -544,6 +547,7 @@
             "blocked_reason": "Este webhook de mudanças recentes será excluído porque a wiki foi bloqueado por `$1`!",
             "created": "Um webhook de mudanças recentes para $1 foi adicionado a este canal.",
             "dashboard": {
+                "avatar": "• O avatar do webhook foi alterado.",
                 "channel": "• O webhook foi movido para esse canal.",
                 "disabled_feeds": "• As mudanças baseadas em feeds, como discussões, mural de mensagens e comentários de artigos, foram desabilitadas.",
                 "disabled_rc": "As mudanças da wiki foram desativadas.",
@@ -554,6 +558,7 @@
                 "enabled_feeds": "• As mudanças baseadas em feeds, como discussões, mural de mensagens e comentários de artigos, foram habilitadas.",
                 "enabled_rc": "• As mudanças da wiki foram habilitadas.",
                 "lang": "• O idioma foi alterado para $1.",
+                "name": "• O nome do webhook foi alterado para \"$1\".",
                 "updated": "Este webhook de mudanças recentes foi atualizado:",
                 "wiki": "• A wiki foi alterada para $1."
             },

+ 3 - 2
i18n/ru.json

@@ -117,10 +117,10 @@
         "hidden": "*скрытая*",
         "info": {
             "added": "Добавлено:",
-            "bytes": "$1 {{PLURAL:$2|байт|байта|байтов}} $3",
+            "bytes": "$1 {{PLURAL:$2|байт|байта|байтов}}",
             "comment": "Комментарий:",
             "editor": "Редактор:",
-            "minor": "(м)",
+            "minor": "_(м)",
             "more": "И более",
             "removed": "Удалено:",
             "size": "Разница:",
@@ -578,6 +578,7 @@
         "wiki": "Вики:"
     },
     "search": {
+        "cached": "Кэш в последний раз обновлён:",
         "category": {
             "content": "Содержимое этой категории:",
             "empty": "*Эта категория пуста*",

+ 2 - 2
i18n/sv.json

@@ -117,10 +117,10 @@
         "hidden": "*gömd*",
         "info": {
             "added": "Tillagd:",
-            "bytes": "$1 {{PLURAL:$2|byte|bytes}} $3",
+            "bytes": "$1 {{PLURAL:$2|byte|bytes}}",
             "comment": "Kommentar:",
             "editor": "Redigerare:",
-            "minor": "(m)",
+            "minor": "_(m)",
             "more": "Och mer",
             "removed": "Raderade:",
             "size": "Skillnad:",

+ 2 - 2
i18n/th.json

@@ -117,10 +117,10 @@
         "hidden": "*ซ่อนอยู่*",
         "info": {
             "added": "เพิ่ม:",
-            "bytes": "$1 ไบต์ $3",
+            "bytes": "$1 ไบต์",
             "comment": "คำอธิบายอย่างย่อ:",
             "editor": "ผู้แก้ไข:",
-            "minor": "(ล)",
+            "minor": "_(ล)",
             "more": "และอื่น ๆ",
             "removed": "ลบ:",
             "size": "แตกต่าง:",

+ 61 - 8
i18n/tr.json

@@ -117,9 +117,10 @@
         "hidden": "*gizli*",
         "info": {
             "added": "Eklendi:",
-            "bytes": "$1 Bayt",
+            "bytes": "$1 bayt",
             "comment": "Yorum:",
             "editor": "Editör:",
+            "minor": "_(k)",
             "more": "Daha fazlası",
             "removed": "Kaldırıldı:",
             "size": "Fark:",
@@ -144,8 +145,9 @@
         " "
     ],
     "general": {
+        "database": "⚠️ **Kısıtlı İşlevsellik** ⚠️\nAyar bulunamadı. Lütfen bot sahibi ile iletişim geç!",
         "default": "bu sunucu daha kurulmamış. $1 ile ya da kontrol panelini kullanarak ayarları değiştirin.",
-        "disclaimer": "Ben, Gamepedia ve Fandom Wikilerinin bağlantılarını göndermek ile görevlendirilmiş küçük bir botum. Sayfalar için kısa açıklamalar ve ek bilgiler verebilir ve interwiki bağlantıları ile yönlendirmeleri takip edebilirim. $1, beni JavaScript kullanarak yazdı.\n\nBeni Patreon'da da destekleyebilirsiniz:",
+        "disclaimer": "Ben, Vikipedi ve Fandom vikileri gibi MediaWiki sitelerine kolayca bağlantı vermek ve arama yapmak ile görevlendirilmiş küçük bir botum. Sayfalar için kısa açıklamalar ve ek bilgiler verebilir ve interwiki bağlantıları ile yönlendirmeleri takip edebilirim. $1, beni JavaScript kullanarak yazdı.\n\nBeni Patreon'da da destekleyebilirsiniz:",
         "experimental": "**Bu özellik deneme aşamasında! Çalışması garanti değil ve gelecekte kaldırılabilir.**",
         "helpserver": "Sorular ve sorunlarınız için lütfen destek sunucumu ziyaret edin:",
         "limit": "🚨 **Dur, limite ulaştın!** 🚨\n\n$1, mesajın çok fazla komut içeriyor!",
@@ -391,7 +393,15 @@
         "pause": "**Şu an bu sunucuda duraklatılmış durumdayım!**\nSadece bu komutlar gerçekleştirilebilir:"
     },
     "interaction": {
-        "inline": "Bu komutu kullanmak için [[wikitext]] ile bir metin girin."
+        "dashboard": {
+            "perm_allow": "İzin ver",
+            "perm_default": "Varsayılan",
+            "perm_deny": "Reddet",
+            "updated": "$1, `$2` eğik çizgi komutu yetkilerini güncelledi."
+        },
+        "inline": "Bu komutu kullanmak için [[wikitext]] ile bir metin girin.",
+        "nowiki": "Kullanılan viki mevcut değil!",
+        "verify": "Discord hesabını viki hesabın ile doğrulamak ve hesabın ile uyumlu rolleri almak için viki kullanıcı adını sağlayarak bu komutu kullan."
     },
     "invite": {
         "bot": "Beni başka bir sunucuya davet etmek için bu bağlantıyı kullan:"
@@ -484,6 +494,8 @@
         "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_edit": "Son değişiklikler webhook'u güncellendi",
+        "avatar": "Webhook simgesi:",
         "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:",
@@ -513,10 +525,11 @@
         "help_display_image": "Görüntü ön izlemeleri ile gömülü mesajlar.",
         "help_feeds": "(tartışmalar, mesaj duvarları, makale yorumları)",
         "help_lang": "Halihazırda desteklenen diller:",
-        "help_wiki": "`https://<wiki>.gamepedia.com/` ya da `https://<wiki>.fandom.com` gibi bir MediaWiki sitesine bağlantı verin",
+        "help_wiki": "`https://<wiki>.fandom.com/` gibi bir MediaWiki sitesine bağlantı ver",
         "lang": "Dil:",
         "max_entries": "azami son değişiklikler webhook'u sayısına ulaştın.",
         "missing": "bunu sunucuda halihazırda bir son değişiklikler webhook'u yok.",
+        "name": "Webhook adı:",
         "new_lang": "<yeni dil>",
         "new_wiki": "<vikiye bağlantı>",
         "no_feeds": "bu webhook'un vikide tartışmalar, mesaj duvarları ya da makale yorumları gibi yayın bazlı özellikler etkin değil.",
@@ -534,6 +547,7 @@
             "blocked_reason": "Bu son değişiklikler webhook'u silinecek çünkü viki `$1` nedeniyle engellendi!",
             "created": "Bu kanala $1 için bir son değişiklikler webhook'u eklendi.",
             "dashboard": {
+                "avatar": "• Webhook simgesi değiştirildi.",
                 "channel": "•Webhook bu kanala taşındı.",
                 "disabled_feeds": "•Tartışmalar, mesaj duvarları ve makale yorumları gibi yayın bazlı değişiklikler devre dışı bırakıldı.",
                 "disabled_rc": "•Viki düzenlemeleri devre dışı bırakıldı.",
@@ -544,6 +558,7 @@
                 "enabled_feeds": "•Tartışmalar, mesaj duvarları ve makale yorumları gibi yayın bazlı düzenlemeler etkinleştirildi.",
                 "enabled_rc": "•Viki düzenlemeleri etkinleştirildi.",
                 "lang": "•Dil $1 ile değiştirildi.",
+                "name": "• Webhook adı \"$1\" yapıldı.",
                 "updated": "Bu son değişiklikler webhook'u güncellendi:",
                 "wiki": "•Viki $1 ile değiştirildi."
             },
@@ -579,6 +594,7 @@
         "special": "Bu özel sayfanın içeriği:"
     },
     "settings": {
+        "button": "Kontrol panelini kullan",
         "channel current": "kanalın mevcut ayarları:",
         "channel lang": "bu kanalın dili:",
         "channel langchanged": "kanalın dilini şuna değiştirdin:",
@@ -630,8 +646,8 @@
         "save_failed": "ne yazık ki ayarlar kaydedilemedi, lütfen daha sonra tekrar deneyin.",
         "wiki": "bu sunucunun varsayılan wikisi:",
         "wikichanged": "bu sunucunun varsayılan wikisini şuna değiştirdiniz:",
-        "wikihelp": "`$1 <wiki>` kullanarak varsayılan wikiyi değiştirebilirsin.\nWiki bağlantısı: `https://<wiki>.gamepedia.com/` veya `https://<wiki>.fandom.com/`",
-        "wikiinvalid": "lütfen Gamepedia veya Fandom wikiye geçerli bir bağlantı saylağın!",
+        "wikihelp": "`$1 <wiki>` kullanarak varsayılan vikiyi değiştirebilirsin.\n`https://<wiki>.fandom.com/` gibi bir MediaWiki sitesine bağlantı ver",
+        "wikiinvalid": "lütfen Vikipedi veya bir Fandom vikisinin geçerli bağlantısını verin!",
         "wikiinvalid_http": "belirtilen site geçerli bir TLS/SSL sertifikasına sahip değil! Güvenlik amacıyla sadece HTTPS kullanan vikiler desteklenmektedir.\nEğer bir site yöneticisiysen *Let’s Encrypt* gibi bir sertifika yetkilisinden sertifika alabilirsin:\n<https://letsencrypt.org/getting-started/>",
         "wikiinvalid_private": "belirtilen viki gizli! Sadece herkes tarafından okunabilen halka açık vikiler desteklenmektedir.",
         "wikiinvalid_timeout": "belirtilen bağlantının cevap vermesi çok uzun sürdü!",
@@ -677,7 +693,21 @@
     },
     "user": {
         "block": {
-            "header": "$1 şu anda engelli!"
+            "duration": {
+                "days": "$1 gün",
+                "hours": "$1 saat",
+                "minutes": "$1 dakika",
+                "months": "$1 ay",
+                "separator": ",_",
+                "separator_last": "_",
+                "weeks": "$1 hafta",
+                "years": "$1 yıl"
+            },
+            "header": "$1 şu anda engelli!",
+            "indef_noreason": "$1 tarihinde $4 tarafından sonsuza kadar engellendi.",
+            "indef_text": "$1 tarihinde \"$5\" nedeniyle $4 tarafından sonsuza engellendi.",
+            "noreason": "$4 tarafından $1 tarihinde $3 tarihine kadar $2 süreliğine engellendi.",
+            "text": "$4 tarafından \"$5\" nedeniyle $1 tarihinde $3 tarihine kadar $2 süreliğine engellendi."
         },
         "gblock": {
             "disabled": "Bu hesap şu anda devre dışı!",
@@ -714,15 +744,21 @@
         "current_selected": "sunucu doğrulaması `$1`:",
         "dashboard": {
             "added": "$1, `$2` ID'si ile doğrulama ekledi.",
+            "added_notice": "$1 birkaç doğrulama bildirisi ekledi.",
             "removed": "$1, `$2` ID'li doğrulamayı kaldırdı.",
-            "updated": "$1, `$2` ID'li doğrulamayı güncelledi."
+            "updated": "$1, `$2` ID'li doğrulamayı güncelledi.",
+            "updated_notice": "$1 birkaç doğrulama bildirisini güncelledi."
         },
         "delete_current": "Bu doğrulamayı sil:",
         "deleted": "doğrulama silindi.",
         "disabled": "devre dışı",
         "editcount": "Düzenleme sayısı:",
         "enabled": "etkin",
+        "flag_logall": "Başarısız doğrulamalar günlüğü:",
+        "flag_private": "Doğrulama komutu yanıtlarını özel olarak gönderme:",
         "indays": "(gün)",
+        "logging": "Günlük kanalı:",
+        "match": "Eksik gereklilik bildirisi:",
         "max_entries": "halihazırda azami doğrulama sayısına sahipsin.",
         "missing": "bu sunucuda halihazırda bir doğrulama yok.",
         "new_accountage": "<yeni hesap yaşı>",
@@ -732,18 +768,23 @@
         "new_role": "<yeni rol>",
         "new_usergroup": "<yeni kullanıcı grubu>",
         "no_role": "lütfen yeni doğrulama için bir rol belirtin.",
+        "notice_embed": "Bazı bildiriler maskeli biçimlendirme bağlantılarına sahip. Lütfen çalışabilmeleri için botun bütün doğrulama kanallarında `Bağlantı Yerleştir` yetkisine sahip olduğundan emin ol.",
         "or": "ya da",
         "postcount": "Gönderi sayısı (sadece Fandom vikileri):",
         "postcount_or": "(düzenleme sayısına alternatif)",
         "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_add": "Eklenecek rol:",
         "role_deleted": "**$1 rolü artık yok gibi duruyor!**",
         "role_managed": "belirtilen rol atanamıyor.",
         "role_max": "çok fazla rol belirttin.",
         "role_missing": "belirtilen rol mevcut değil.",
+        "role_none": "hiçbiri",
+        "role_remove": "Kaldırılacak rol:",
         "role_too_high": "**$1 rolü $2 tarafından atanmak için çok yüksekte!**",
         "save_failed": "ne yazık ki doğrulama kaydedilemedi, lütfen daha sonra tekrar deneyin.",
+        "success": "Başarı bildirisi:",
         "toggle": "(değiştir)",
         "updated": "doğrulama güncellendi:",
         "usergroup": "Kullanıcı grubu:",
@@ -753,6 +794,8 @@
     },
     "verify": {
         "audit_reason": "\"$1\" olarak doğrulandı",
+        "button_again": "Tekrar dene",
+        "button_wrong_user": "Bu doğrulamayı tekrar deneyemezsin. Doğrulama $1 kullanıcısına ait!",
         "discord": "Discord kullanıcısı:",
         "empty": "*boş*",
         "error": "Doğrulama bir hata sonucu başarısız oldu.",
@@ -764,9 +807,19 @@
         "help_fandom": "https://community.fandom.com/wiki/Special:VerifyUser",
         "help_gamepedia": "https://help.fandom.com/wiki/Gamepedia_Help_Wiki:Discord_verification",
         "help_guide": "Viki profiline Discord etiketi eklemek için [bu rehberi]($1) oku:",
+        "help_missing": "Lütfen viki kullanıcı adını ve büyük küçük harf uyumunu doğru kullandığına emin ol.",
         "help_subpage": "Lütfen vikideki Discord alt sayfana Discord etiketini ($1) ekle:",
         "missing": "kanalda halihazırda bir doğrulama yok.",
         "notice": "Uyarı:",
+        "oauth_button": "Doğrula",
+        "oauth_message": "lütfen [bu bağlantıyı]($1) kullanarak viki hesabını doğrula.",
+        "oauth_message_dm": "$1 sunucusu için viki hesabını doğrulamak için lütfen bu bağlantıyı kullan.",
+        "oauth_private": "bu viki doğrulama için OAuth2 kullanıyor. Lütfen sunucudan gelen direkt mesajları etkinleştir ve `/verify` komutunu kullan. Böylece doğrulama bağlantısını sana özel mesaj olarak gönderebilirim.",
+        "oauth_used": "*OAuth2 kullanılarak doğrulandı*",
+        "qualified_add": "Eklendi:",
+        "qualified_add_error": "Eklenemedi:",
+        "qualified_remove": "Kaldırıldı:",
+        "qualified_remove_error": "Kaldırılamadı:",
         "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ışı!**",

+ 13 - 5
i18n/uk.json

@@ -117,10 +117,10 @@
         "hidden": "*приховано*",
         "info": {
             "added": "Додано:",
-            "bytes": "$1 {{PLURAL:$2|байт|байта|байтів}} $3",
+            "bytes": "$1 {{PLURAL:$2|байт|байта|байтів}}",
             "comment": "Коментар:",
             "editor": "Редактор:",
-            "minor": "(м)",
+            "minor": "_(м)",
             "more": "І більше",
             "removed": "Видалено:",
             "size": "Різниця:",
@@ -281,7 +281,7 @@
                 },
                 "feeds": {
                     "cmd": "rcscript feeds",
-                    "desc": "Я буду перемикати обговорення змін на Фендом-вікі для вебхука останніх правок."
+                    "desc": "Я буду перемикати обговорення змін на вікі Фендома для вебхука останніх правок."
                 },
                 "lang": {
                     "cmd": "rcscript lang <мова>",
@@ -400,7 +400,8 @@
             "updated": "$1 оновив перезаписі дозволів на використання слеш-команди `$2`."
         },
         "inline": "Будь ласка, надайте деякий текст з посиланнями [[wikitext]], щоб використовувати цю команду.",
-        "nowiki": "Використовувана вікі не існує!"
+        "nowiki": "Використовувана вікі не існує!",
+        "verify": "Будь ласка, вкажіть своє ім'я користувача вікі, щоб використовувати цю команду для перевірки вашого облікового запису Discord з вашим обліковим записом вікі і отримання ролей, відповідних вашого облікового запису вікі."
     },
     "invite": {
         "bot": "Використовуйте це посилання, щоб запросити мене на інший сервер:"
@@ -409,8 +410,15 @@
         "fixed": "Виправлено {{PLURAL:$1|Version|Versions}}:",
         "issue_link": {
             "Blocks": {
-                "inward": "блокується $1"
+                "inward": "блокується $1",
+                "outward": "блокуєт $1"
             }
+        },
+        "status": {
+            "Awaiting Response": "В Очікуванні відповіді",
+            "Cannot Reproduce": "Не вдається відтворити",
+            "Closed": "Закрито",
+            "Done": "Зроблено"
         }
     },
     "test": {

+ 122 - 2
i18n/vi.json

@@ -1,6 +1,6 @@
 {
     "__translator": [
-        "",
+        "Song Ngư Kaze",
         " ",
         " ",
         " ",
@@ -104,13 +104,39 @@
             " "
         ],
         "verify": [
-            "Kiểm Chứng",
+            "Xác Thực",
             " ",
             " ",
             " ",
             " "
         ]
     },
+    "dateformat": "vi-VN",
+    "diff": {
+        "badrev": "ít nhất một phiên bản chưa tồn tại!",
+        "hidden": "*bị ẩn*",
+        "info": {
+            "added": "Đã thêm:",
+            "bytes": "$1 {{PLURAL:$2|byte|byte}}",
+            "comment": "Miêu tả:",
+            "editor": "Người thực hiện:",
+            "minor": "_(n)",
+            "more": "Và thêm nữa",
+            "removed": "Đã loại bỏ:",
+            "size": "Độ khác nhau:",
+            "tags": "Thẻ:",
+            "timestamp": "Ngày sửa đổi:",
+            "whitespace": "Chỉ chứa khoảng trắng"
+        },
+        "nocomment": "*Không có miêu tả nào được cung cấp*"
+    },
+    "discussion": {
+        "image": "Xem hình ảnh",
+        "main": "Thảo luận",
+        "post": "bài",
+        "tags": "Thẻ:",
+        "votes": "$1 {{PLURAL:$2|bình chọn|bình chọn}} ($3%)"
+    },
     "fallback": [
         "en",
         " ",
@@ -118,6 +144,100 @@
         " ",
         " "
     ],
+    "general": {
+        "database": "⚠️ **Tính năng giới hạn** ⚠️\nKhông có cài đặt nào như vậy cả, vui lòng liên hệ với chủ sở hữu của bot!",
+        "default": "máy chủ này vẫn chưa sẵn sàng. Dùng $1 hoặc trang quản lý để thay đổi cài đặt.",
+        "disclaimer": "Tôi là một con bot được tạo ra với mục đích làm cho việc liên kết và tìm kiếm các trang wiki website chạy MediaWiki như Wikipedia, Fandom, Miraheze và tương tự thế. Tôi hiển thị một vài miêu tả nhỏ và vài ba thông tin bổ sung về các trang và thậm chí, tôi cũng có thể xử lý các trang đổi hướng hay liên kết liên wiki. Tôi được viết bằng JavaScript bởi $1.\n\nBạn có thể hỗ trợ tôi trên Patreon:",
+        "experimental": "**Tính năng này vẫn đang trong quá trình thử nghiệm! Chúng tôi không đảm bảo rằng nó sẽ hoạt động đúng cách và nó cũng có thể sẽ bị loại bỏ trong tương lai.**",
+        "helpserver": "Các câu hỏi và vấn đề với bot vui lòng điều hướng đến máy chủ hỗ trợ của tôi:",
+        "limit": "🚨 **Dừng lại, quá giới hạn rồi!** 🚨\n\n$1, đoạn tin nhắn này có chứa quá nhiều lệnh!",
+        "missingperm": "Có vẻ như tôi đang không có các quyền cần thiết cho câu lệnh này:",
+        "patreon": "đây là tính năng chỉ dành cho Patreon!\nBạn có thể hỗ trợ tôi trên Patreon để được truy cập vào tính năng này:",
+        "prefix": "tiền tố của máy chủ hiện tại là `$1`. Bạn có thể thay đổi tiền tố với lệnh `$1settings prefix`. Danh sách lệnh đầy đủ sẽ có khi bạn dùng `$1help`.",
+        "readonly": "**Ối! Cơ sở dữ liệu hiện tại đang ở chế độ chỉ-đọc, bạn không thể thay đổi cài đặt ngay được!**"
+    },
+    "help": {
+        "admin": "Những câu lệnh này chỉ có thể được chạy bởi những người có quyền quản lý:",
+        "adminfooter": "Người quản lý cũng có thể dùng trang quản lý để thay đổi cài đặt của bot tại:",
+        "all": "Vậy là bạn muốn biết tôi có thể làm gì đúng không? Thế thì đây là danh sách câu lệnh mà tôi có thể hiểu:",
+        "footer": "Nếu bạn gặp phải một lời phản hồi không mong muốn, bạn có thể react với 🗑️ (`:wastebasket:`) vào tin nhắn của tôi và tôi sẽ tự tay xóa nó.",
+        "list": {
+            "default": {
+                "cmd": "<từ khóa>",
+                "desc": "Tôi sẽ trả lời bạn đường liên kết tương ứng với bài viết có sẵn trên wiki."
+            },
+            "diff": {
+                "id": {
+                    "cmd": "diff <diff> [<oldid>]",
+                    "desc": "Tôi sẽ trả lời bạn đường liên kết tới trang so sánh phiên bản tương ứng trên Wiki."
+                },
+                "name": {
+                    "cmd": "diff <tên trang>",
+                    "desc": "Tôi sẽ trả lời bạn đường liên kết tới trang so sánh phiên bản cuối cùng của bài viết tương ứng trên Wiki."
+                }
+            },
+            "discussion": {
+                "post": {
+                    "cmd": "discussion post <từ khóa>",
+                    "desc": "Tôi sẽ trả lời bạn đường liên kết tới bài thảo luận tương ứng trên wiki Fandom."
+                },
+                "thread": {
+                    "cmd": "discussion <từ khóa>",
+                    "desc": "Tôi sẽ trả lời bạn đường liên kết tới bài thảo luận tương ứng trên wiki Fandom."
+                }
+            },
+            "fandom": {
+                "cmd": "?<tiền tố wiki> <từ khóa>",
+                "desc": "Tôi sẽ trả lời bạn đường liên kết tới trang bài viết trên wiki Fandom tương ứng: ."
+            },
+            "gamepedia": {
+                "cmd": "!<tiền tố wiki> <từ khóa>",
+                "desc": "Tôi sẽ trả lời bạn đường liên kết tới bài viết trên wiki Gamepedia tương ứng: ``"
+            },
+            "help": {
+                "admin": {
+                    "cmd": "help admin",
+                    "desc": "Tôi sẽ liệt kê toàn bộ các câu lệnh quản trị."
+                },
+                "command": {
+                    "cmd": "help <câu lệnh bot>",
+                    "desc": "Thắc mắc cách một lệnh hoạt động? Để tôi giải thích nó cho bạn!"
+                },
+                "default": {
+                    "cmd": "help",
+                    "desc": "Tôi sẽ liệt kê hết những câu lệnh mà tôi có thể hiểu."
+                }
+            },
+            "info": {
+                "cmd": "info",
+                "desc": "Tôi sẽ giới thiệu bản thân."
+            },
+            "inline": {
+                "link": {
+                    "cmd": "[[<tên trang>]]",
+                    "desc": "Tôi sẽ trả lời với đường dẫn trực tiếp tới bài viết tương ứng trên wiki."
+                },
+                "template": {
+                    "cmd": "{{<tên trang>}}",
+                    "desc": "Tôi sẽ trả lời với đường dẫn tới bài viết tương ứng trên wiki."
+                }
+            },
+            "minecraft": {
+                "bug": {
+                    "cmd": "bug <bug trong Minecraft>",
+                    "desc": "Tôi sẽ cung cấp cho bạn liên kết tới trang theo dõi lỗi của Minecraft."
+                },
+                "command": {
+                    "cmd": "command <lệnh trong Minecraft>",
+                    "desc": "Tôi sẽ cung cấp cho bạn liên kết tới trang về câu lệnh tương ứng trên Minecraft Wiki."
+                },
+                "default": {
+                    "cmd": "/<lệnh trong Minecraft>",
+                    "desc": "Tôi sẽ cung cấp cho bạn liên kết tới trang về câu lệnh tương ứng trên Minecraft Wiki."
+                }
+            }
+        }
+    },
     "test": {
         "text": [
             "",

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


+ 3 - 2
i18n/zh-hans.json

@@ -117,10 +117,10 @@
         "hidden": "*隐藏*",
         "info": {
             "added": "添加:",
-            "bytes": "$1 字节 $3",
+            "bytes": "$1 字节",
             "comment": "摘要:",
             "editor": "编辑者:",
-            "minor": "(小)",
+            "minor": "_(小)",
             "more": "更多",
             "removed": "删除:",
             "size": "差异:",
@@ -578,6 +578,7 @@
         "wiki": "wiki:"
     },
     "search": {
+        "cached": "缓存上次更新:",
         "category": {
             "content": "分类下的内容:",
             "empty": "*空白分类*",

+ 4 - 3
i18n/zh-hant.json

@@ -117,10 +117,10 @@
         "hidden": "*隱藏*",
         "info": {
             "added": "增加:",
-            "bytes": "$1位元$3",
+            "bytes": "$1位元",
             "comment": "摘要:",
             "editor": "編輯者:",
-            "minor": "(小)",
+            "minor": "_(小)",
             "more": "更多",
             "removed": "移除:",
             "size": "差異:",
@@ -397,7 +397,7 @@
             "perm_allow": "允許",
             "perm_default": "預設",
             "perm_deny": "拒絕",
-            "updated": "$1更新了`$2`的斜指令權限覆寫。"
+            "updated": "$1更新了`$2`的斜指令權限覆寫。"
         },
         "inline": "請提供一些有 [[wikitext]] 的文字來使用此指令。",
         "nowiki": "所使用的wiki不存在!",
@@ -578,6 +578,7 @@
         "wiki": "wiki:"
     },
     "search": {
+        "cached": "快取上次更新:",
         "category": {
             "content": "此分類的內容:",
             "empty": "*空白分類*",

+ 31 - 51
interactions/inline.js

@@ -1,45 +1,33 @@
+const {Permissions: {FLAGS}} = require('discord.js');
 const logging = require('../util/logging.js');
 const Wiki = require('../util/wiki.js');
-const {limitLength, partialURIdecode, sendMessage} = require('../util/functions.js');
+const {got, limitLength, partialURIdecode, sendMessage} = require('../util/functions.js');
 
 /**
  * Post a message with inline wiki links.
- * @param {Object} interaction - The interaction.
- * @param {import('discord.js').Client} interaction.client - The client of the interaction.
+ * @param {import('discord.js').CommandInteraction} interaction - The interaction.
  * @param {import('../util/i18n.js')} lang - The user language.
  * @param {import('../util/wiki.js')} wiki - The wiki for the interaction.
- * @param {import('discord.js').TextChannel} [channel] - The channel for the interaction.
  */
-function slash_inline(interaction, lang, wiki, channel) {
-	var text = ( interaction.data.options?.[0]?.value || '' ).replace( /\]\(/g, ']\\(' );
+function slash_inline(interaction, lang, wiki) {
+	var text = ( interaction.options.getString('text') || '' ).replace( /\]\(/g, ']\\(' );
 	text = text.replace( /\x1F/g, '' ).replace( /(?<!@)\u200b/g, '' ).trim();
 	if ( !text.includes( '{{' ) && !( text.includes( '[[' ) && text.includes( ']]' ) ) && !text.includes( 'PMID' ) && !text.includes( 'RFC' ) && !text.includes( 'ISBN' ) ) {
-		return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {
-				type: 4,
-				data: {
-					content: lang.get('interaction.inline'),
-					allowed_mentions: {
-						parse: []
-					},
-					flags: 64
-				}
-			}
-		} ).catch(log_error);
+		return interaction.reply( {content: lang.get('interaction.inline'), ephemeral: true} ).catch(log_error);
 	}
-	var allowed_mentions = {
+	var allowedMentions = {
 		parse: ['users']
 	};
-	if ( interaction.guild_id ) {
-		if ( interaction.member.permissions.has('MENTION_EVERYONE') ) {
-			allowed_mentions.parse = ['users', 'roles', 'everyone'];
+	if ( interaction.inGuild() ) {
+		if ( interaction.member.permissions.has(FLAGS.MENTION_EVERYONE) ) {
+			allowedMentions.parse = ['users', 'roles', 'everyone'];
 		}
-		else if ( channel?.guild ) {
-			allowed_mentions.roles = channel.guild.roles.cache.filter( role => role.mentionable ).map( role => role.id ).slice(0, 100);
+		else if ( interaction.guild ) {
+			allowedMentions.roles = interaction.guild.roles.cache.filter( role => role.mentionable ).map( role => role.id ).slice(0, 100);
 		}
-		if ( channel?.guild && !interaction.member.permissions.has('USE_EXTERNAL_EMOJIS') ) {
+		if ( interaction.guild && !interaction.member.permissions.has(FLAGS.USE_EXTERNAL_EMOJIS) ) {
 			text = text.replace( /(?<!\\)<a?(:\w+:)\d+>/g, (replacement, emoji, id) => {
-				if ( channel.guild.emojis.cache.has(id) ) {
+				if ( interaction.guild.emojis.cache.has(id) ) {
 					return replacement;
 				}
 				return emoji;
@@ -49,22 +37,14 @@ function slash_inline(interaction, lang, wiki, channel) {
 	if ( text.length > 1800 ) text = text.substring(0, 1800) + '\u2026';
 	var message = {
 		content: text.replace( /(?<!\\)<a?(:\w+:)\d+>/g, (replacement, emoji, id) => {
-			if ( channel?.guild?.emojis.cache.has(id) ) {
+			if ( interaction.guild?.emojis.cache.has(id) ) {
 				return replacement;
 			}
 			return emoji;
 		} ),
-		allowed_mentions
+		allowedMentions
 	};
-	return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-		data: {
-			type: 5,
-			data: {
-				allowed_mentions,
-				flags: 0
-			}
-		}
-	} ).then( () => {
+	return interaction.deferReply().then( () => {
 		var textReplacement = [];
 		var magiclinks = [];
 		var replacedText = text.replace( /(?<!\\)(?:<a?(:\w+:)\d+>|<#(\d+)>|<@!?(\d+)>|<@&(\d+)>|```.+?```|``.+?``|`.+?`)/gs, (replacement, emoji, textchannel, user, role) => {
@@ -73,18 +53,18 @@ function slash_inline(interaction, lang, wiki, channel) {
 			if ( emoji ) arg = emoji;
 			if ( textchannel ) {
 				let tempchannel = interaction.client.channels.cache.get(textchannel);
-				if ( tempchannel ) arg = '#' + tempchannel.name;
+				if ( tempchannel ) arg = '#' + ( tempchannel.name || 'deleted-channel' );
 			}
 			if ( user ) {
-				let tempuser = channel?.guild?.members.cache.get(user);
-				if ( tempuser ) arg = '@' + tempuser.displayName;
+				let tempmember = interaction.guild?.members.cache.get(user);
+				if ( tempmember ) arg = '@' + tempmember.displayName;
 				else {
-					tempuser = interaction.client.users.cache.get(user);
+					let tempuser = interaction.client.users.cache.get(user);
 					if ( tempuser ) arg = '@' + tempuser.username;
 				}
 			}
 			if ( role ) {
-				let temprole = channel?.guild?.roles.cache.get(role);
+				let temprole = interaction.guild?.roles.cache.get(role);
 				if ( temprole ) arg = '@' + temprole.name;
 			}
 			return '\x1F<replacement\x1F' + textReplacement.length + ( arg ? '\x1F' + arg : '' ) + '>\x1F';
@@ -127,7 +107,7 @@ function slash_inline(interaction, lang, wiki, channel) {
 			}
 		} );
 		if ( !templates.length && !links.length && !magiclinks.length ) {
-			return sendMessage(interaction, message, channel);
+			return sendMessage(interaction, message);
 		}
 		return got.get( wiki + 'api.php?action=query&meta=siteinfo' + ( magiclinks.length ? '|allmessages&ammessages=pubmedurl|rfcurl&amenableparser=true' : '' ) + '&siprop=general&iwurl=true&titles=' + encodeURIComponent( [
 			...templates.map( link => link.title + '|' + link.template ),
@@ -142,9 +122,9 @@ function slash_inline(interaction, lang, wiki, channel) {
 				else {
 					console.log( '- ' + response.statusCode + ': Error while following the links: ' + body?.error?.info );
 				}
-				return sendMessage(interaction, message, channel);
+				return sendMessage(interaction, message);
 			}
-			logging(wiki, interaction.guild_id, 'slash', 'inline');
+			logging(wiki, interaction.guildId, 'slash', 'inline');
 			wiki.updateWiki(body.query.general);
 			if ( body.query.normalized ) {
 				body.query.normalized.forEach( title => {
@@ -211,7 +191,7 @@ function slash_inline(interaction, lang, wiki, channel) {
 						link.url = wiki.toLink(title + '/' + link.isbn, '', '', true);
 					}
 					if ( link.url ) {
-						console.log( ( interaction.guild_id || '@' + interaction.user.id ) + ': Slash: ' + link.type + ' ' + link.id );
+						console.log( ( interaction.guildId || '@' + interaction.user.id ) + ': Slash: ' + link.type + ' ' + link.id );
 						textReplacement[link.replacementId] = '[' + link.type + ' ' + link.id + '](<' + link.url + '>)';
 					}
 				} );
@@ -229,7 +209,7 @@ function slash_inline(interaction, lang, wiki, channel) {
 							title = title.replace( /(?:%[\dA-F]{2})+/g, partialURIdecode ).replace( /\x1F<replacement\x1F\d+\x1F(.+?)>\x1F/g, '$1' ).trim();
 							let link = templates.find( link => link.raw === title );
 							if ( !link ) return fullLink;
-							console.log( ( interaction.guild_id || '@' + interaction.user.id ) + ': Slash: ' + fullLink );
+							console.log( ( interaction.guildId || '@' + interaction.user.id ) + ': Slash: ' + fullLink );
 							if ( title.startsWith( 'int:' ) ) {
 								title = title.replace( /^int:\s*/, replacement => {
 									linkprefix += replacement;
@@ -245,7 +225,7 @@ function slash_inline(interaction, lang, wiki, channel) {
 							title = title.replace( /(?:%[\dA-F]{2})+/g, partialURIdecode ).replace( /\x1F<replacement\x1F\d+\x1F(.+?)>\x1F/g, '$1' ).split('#')[0].trim();
 							let link = links.find( link => link.raw === title );
 							if ( !link ) return fullLink;
-							console.log( ( interaction.guild_id || '@' + interaction.user.id ) + ': Slash: ' + fullLink );
+							console.log( ( interaction.guildId || '@' + interaction.user.id ) + ': Slash: ' + fullLink );
 							if ( display === undefined ) display = title.replace( /^\s*:?/, '' );
 							if ( !display.trim() ) {
 								display = title.replace( /^\s*:/, '' );
@@ -267,9 +247,9 @@ function slash_inline(interaction, lang, wiki, channel) {
 				} );
 				if ( text.length > 1900 ) text = limitLength(text, 1900, 100);
 				message.content = text;
-				return sendMessage(interaction, message, channel);
+				return sendMessage(interaction, message);
 			}
-			else return sendMessage(interaction, message, channel);
+			else return sendMessage(interaction, message);
 		}, error => {
 			if ( wiki.noWiki(error.message) ) {
 				console.log( '- This wiki doesn\'t exist!' );
@@ -277,7 +257,7 @@ function slash_inline(interaction, lang, wiki, channel) {
 			else {
 				console.log( '- Error while following the links: ' + error );
 			}
-			return sendMessage(interaction, message, channel);
+			return sendMessage(interaction, message);
 		} );
 	}, log_error );
 }

+ 340 - 414
interactions/verify.js

@@ -1,112 +1,109 @@
 const {randomBytes} = require('crypto');
+const {MessageActionRow, MessageButton, Permissions: {FLAGS}} = require('discord.js');
 var db = require('../util/database.js');
 var verify = require('../functions/verify.js');
-const {oauthVerify, sendMessage} = require('../util/functions.js');
+const {got, oauthVerify, sendMessage} = require('../util/functions.js');
 
 /**
  * Wiki user verification.
- * @param {Object} interaction - The interaction.
- * @param {import('discord.js').Client} interaction.client - The client of the interaction.
+ * @param {import('discord.js').CommandInteraction} interaction - The interaction.
  * @param {import('../util/i18n.js')} lang - The user language.
  * @param {import('../util/wiki.js')} wiki - The wiki for the interaction.
- * @param {import('discord.js').TextChannel} [channel] - The channel for the interaction.
  */
-function slash_verify(interaction, lang, wiki, channel) {
-	var reply = '<@' + ( interaction.member?.nick ? '!' : '' ) + interaction.user.id + '>, ';
-	var allowed_mentions = {
-		users: [interaction.user.id]
-	};
-	if ( !channel?.guild ) return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-		data: {
-			type: 4,
-			data: {
-				content: reply + lang.get('verify.missing'),
-				allowed_mentions,
-				flags: 64
-			}
-		}
-	} ).catch(log_error);
-	if ( !channel.guild.me.permissions.has('MANAGE_ROLES') ) {
-		console.log( channel.guild.id + ': Missing permissions - MANAGE_ROLES' );
-		return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {
-				type: 4,
-				data: {
-					content: reply + lang.get('general.missingperm') + ' `MANAGE_ROLES`',
-					allowed_mentions,
-					flags: 64
-				}
-			}
-		} ).catch(log_error);
+function slash_verify(interaction, lang, wiki) {
+	if ( !interaction.guild ) return interaction.reply( {content: lang.get('verify.missing'), ephemeral: true} ).catch(log_error);
+	if ( !interaction.guild.me.permissions.has(FLAGS.MANAGE_ROLES) ) {
+		console.log( interaction.guildId + ': Missing permissions - MANAGE_ROLES' );
+		return interaction.reply( {content: lang.get('general.missingperm') + ' `MANAGE_ROLES`', ephemeral: true} ).catch(log_error);
 	}
 	
-	return db.query( 'SELECT logchannel, flags, onsuccess, onmatch, role, editcount, postcount, usergroup, accountage, rename FROM verification LEFT JOIN verifynotice ON verification.guild = verifynotice.guild WHERE verification.guild = $1 AND channel LIKE $2 ORDER BY configid ASC', [interaction.guild_id, '%|' + interaction.channel_id + '|%'] ).then( ({rows}) => {
-		if ( !rows.length ) return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {
-				type: 4,
-				data: {
-					content: reply + lang.get('verify.missing') + ( interaction.member.permissions.has('MANAGE_GUILD') && process.env.dashboard ? '\n' + new URL(`/guild/${interaction.guild_id}/verification`, process.env.dashboard).href : '' ),
-					allowed_mentions,
-					flags: 64
-				}
-			}
-		} ).catch(log_error);
+	return db.query( 'SELECT logchannel, flags, onsuccess, onmatch, role, editcount, postcount, usergroup, accountage, rename FROM verification LEFT JOIN verifynotice ON verification.guild = verifynotice.guild WHERE verification.guild = $1 AND channel LIKE $2 ORDER BY configid ASC', [interaction.guildId, '%|' + ( interaction.channel?.isThread() ? interaction.channel.parentId : interaction.channelId ) + '|%'] ).then( ({rows}) => {
+		if ( !rows.length ) return interaction.reply( {content: lang.get('verify.missing') + ( interaction.member.permissions.has(FLAGS.MANAGE_GUILD) && process.env.dashboard ? '\n' + new URL(`/guild/${interaction.guildId}/verification`, process.env.dashboard).href : '' ), ephemeral: true} ).catch(log_error);
 
 		if ( wiki.hasOAuth2() && process.env.dashboard ) {
 			let oauth = [wiki.hostname + wiki.pathname.slice(0, -1)];
 			if ( wiki.isWikimedia() ) oauth.push('wikimedia');
 			if ( wiki.isMiraheze() ) oauth.push('miraheze');
 			if ( process.env['oauth_' + ( oauth[1] || oauth[0] )] && process.env['oauth_' + ( oauth[1] || oauth[0] ) + '_secret'] ) {
-				let state = `${oauth[0]} ${global.shardId}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
-				while ( oauthVerify.has(state) ) {
-					state = `${oauth[0]} ${global.shardId}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
-				}
-				oauthVerify.set(state, {
-					state, wiki: wiki.href, channel,
-					user: interaction.user.id,
-					token: interaction.token
-				});
-				interaction.client.shard.send({id: 'verifyUser', state});
-				let oauthURL = wiki + 'rest.php/oauth2/authorize?' + new URLSearchParams({
-					response_type: 'code', redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
-					client_id: process.env['oauth_' + ( oauth[1] || oauth[0] )], state
-				}).toString();
-				return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-					data: {
-						type: 4,
-						data: {
-							content: reply + lang.get('verify.oauth_message', '<' + oauthURL + '>'),
-							allowed_mentions,
-							components: [
-								{
-									type: 1,
-									components: [
-										{
-											type: 2,
-											style: 5,
-											label: lang.get('verify.oauth_button'),
-											emoji: {id: null, name: '🔗'},
-											url: oauthURL,
-											disabled: false
-										}
-									]
-								}
-							],
-							flags: 64
+				return interaction.deferReply( {ephemeral: ( (rows[0].flags & 1 << 0) === 1 << 0 )} ).then( () => {
+					return db.query( 'SELECT token FROM oauthusers WHERE userid = $1 AND site = $2', [interaction.user.id, ( oauth[1] || oauth[0] )] ).then( ({rows: [row]}) => {
+						if ( row?.token ) return got.post( wiki + 'rest.php/oauth2/access_token', {
+							form: {
+								grant_type: 'refresh_token', refresh_token: row.token,
+								redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
+								client_id: process.env['oauth_' + ( oauth[1] || oauth[0] )],
+								client_secret: process.env['oauth_' + ( oauth[1] || oauth[0] ) + '_secret']
+							}
+						} ).then( response => {
+							var body = response.body;
+							if ( response.statusCode !== 200 || !body?.access_token ) {
+								console.log( '- ' + response.statusCode + ': Error while refreshing the mediawiki token: ' + ( body?.message || body?.error ) );
+								return Promise.reject(row);
+							}
+							if ( body?.refresh_token ) db.query( 'UPDATE oauthusers SET token = $1 WHERE userid = $2 AND site = $3', [body.refresh_token, interaction.user.id, ( oauth[1] || oauth[0] )] ).then( () => {
+								console.log( '- Dashboard: OAuth2 token for ' + interaction.user.id + ' successfully updated.' );
+							}, dberror => {
+								console.log( '- Dashboard: Error while updating the OAuth2 token for ' + interaction.user.id + ': ' + dberror );
+							} );
+							return global.verifyOauthUser('', body.access_token, {
+								wiki: wiki.href, channel: interaction.channel,
+								user: interaction.user.id, interaction,
+								fail: () => sendMessage(interaction, lang.get('verify.error_reply'))
+							});
+						}, error => {
+							console.log( '- Error while refreshing the mediawiki token: ' + error );
+							return Promise.reject(row);
+						} );
+						return Promise.reject(row);
+					}, dberror => {
+						console.log( '- Error while getting the OAuth2 token: ' + dberror );
+						return Promise.reject();
+					} ).catch( row => {
+						if ( row ) {
+							if ( !row?.hasOwnProperty?.('token') ) console.log( '- Error while checking the OAuth2 refresh token: ' + row );
+							else if ( row.token ) db.query( 'DELETE FROM oauthusers WHERE userid = $1 AND site = $2', [interaction.user.id, ( oauth[1] || oauth[0] )] ).then( () => {
+								console.log( '- Dashboard: OAuth2 token for ' + interaction.user.id + ' successfully deleted.' );
+							}, dberror => {
+								console.log( '- Dashboard: Error while deleting the OAuth2 token for ' + interaction.user.id + ': ' + dberror );
+							} );
 						}
-					}
-				} ).catch(log_error);
+						let state = `${oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
+						while ( oauthVerify.has(state) ) {
+							state = `${oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
+						}
+						oauthVerify.set(state, {
+							state, wiki: wiki.href, channel: interaction.channel,
+							user: interaction.user.id, interaction
+						});
+						interaction.client.shard.send({id: 'verifyUser', state, user: ( row?.token === null ? '' : interaction.user.id )});
+						let oauthURL = wiki + 'rest.php/oauth2/authorize?' + new URLSearchParams({
+							response_type: 'code', redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
+							client_id: process.env['oauth_' + ( oauth[1] || oauth[0] )], state
+						}).toString();
+						let message = {
+							content: lang.get('verify.oauth_message', '<' + oauthURL + '>'),
+							components: [new MessageActionRow().addComponents(
+								new MessageButton().setLabel(lang.get('verify.oauth_button')).setEmoji('🔗').setStyle('LINK').setURL(oauthURL)
+							)],
+							ephemeral: true
+						};
+						if ( (rows[0].flags & 1 << 0) === 1 << 0 ) return sendMessage(interaction, message, false);
+						return interaction.deleteReply().then( () => {
+							return interaction.followUp( message ).catch(log_error);
+						}, log_error );
+					} );
+				}, log_error );
 			}
 		}
 		
-		var username = ( interaction.data.options?.[0]?.value || '' ).replace( /^\s*<@!?(\d+)>\s*$/, (mention, id) => {
+		var username = ( interaction.options.getString('username') || '' ).replace( /^\s*<@!?(\d+)>\s*$/, (mention, id) => {
 			if ( id === interaction.user.id ) {
-				return ( interaction.member?.nick || interaction.user.username );
+				return interaction.member.displayName;
 			}
-			let user = channel.guild.members.cache.get(id);
-			if ( user ) return user.displayName;
+			let member = interaction.guild.members.cache.get(id);
+			if ( member ) return member.displayName;
 			else {
-				user = interaction.client.users.cache.get(user);
+				let user = interaction.client.users.cache.get(id);
 				if ( user ) return user.username;
 			}
 			return mention;
@@ -116,391 +113,320 @@ function slash_verify(interaction, lang, wiki, channel) {
 		}
 		if ( wiki.isGamepedia() ) username = username.replace( /^userprofile\s*:\s*/i, '' );
 		
-		if ( !username.trim() ) return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {
-				type: 4,
-				data: {
-					content: lang.get('interaction.verify'),
-					allowed_mentions,
-					flags: 64
-				}
-			}
-		} ).catch(log_error);
+		if ( !username.trim() ) return interaction.reply( {content: lang.get('interaction.verify'), ephemeral: true} ).catch(log_error);
 
-		return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {
-				type: 5,
-				data: {
-					allowed_mentions,
-					flags: ( (rows[0].flags & 1 << 0) === 1 << 0 ? 64 : 0 )
-				}
-			}
-		} ).then( () => {
-			return channel.guild.members.fetch(interaction.user.id).then( member => {
-				return verify(lang, channel, member, username, wiki, rows).then( result => {
-					if ( result.oauth.length ) {
-						let state = `${result.oauth[0]} ${global.shardId}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
+		return interaction.deferReply( {ephemeral: ( (rows[0].flags & 1 << 0) === 1 << 0 )} ).then( () => {
+			return verify(lang, interaction.channel, interaction.member, username, wiki, rows).then( result => {
+				if ( result.oauth.length ) {
+					return db.query( 'SELECT token FROM oauthusers WHERE userid = $1 AND site = $2', [interaction.user.id, ( result.oauth[1] || result.oauth[0] )] ).then( ({rows: [row]}) => {
+						if ( row?.token ) return got.post( wiki + 'rest.php/oauth2/access_token', {
+							form: {
+								grant_type: 'refresh_token', refresh_token: row.token,
+								redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
+								client_id: process.env['oauth_' + ( result.oauth[1] || result.oauth[0] )],
+								client_secret: process.env['oauth_' + ( result.oauth[1] || result.oauth[0] ) + '_secret']
+							}
+						} ).then( response => {
+							var body = response.body;
+							if ( response.statusCode !== 200 || !body?.access_token ) {
+								console.log( '- ' + response.statusCode + ': Error while refreshing the mediawiki token: ' + ( body?.message || body?.error ) );
+								return Promise.reject(row);
+							}
+							if ( body?.refresh_token ) db.query( 'UPDATE oauthusers SET token = $1 WHERE userid = $2 AND site = $3', [body.refresh_token, interaction.user.id, ( result.oauth[1] || result.oauth[0] )] ).then( () => {
+								console.log( '- Dashboard: OAuth2 token for ' + interaction.user.id + ' successfully updated.' );
+							}, dberror => {
+								console.log( '- Dashboard: Error while updating the OAuth2 token for ' + interaction.user.id + ': ' + dberror );
+							} );
+							return global.verifyOauthUser('', body.access_token, {
+								wiki: wiki.href, channel: interaction.channel,
+								user: interaction.user.id, interaction,
+								fail: () => sendMessage(interaction, lang.get('verify.error_reply'))
+							});
+						}, error => {
+							console.log( '- Error while refreshing the mediawiki token: ' + error );
+							return Promise.reject(row);
+						} );
+						return Promise.reject(row);
+					}, dberror => {
+						console.log( '- Error while getting the OAuth2 token: ' + dberror );
+						return Promise.reject();
+					} ).catch( row => {
+						if ( row ) {
+							if ( !row?.hasOwnProperty?.('token') ) console.log( '- Error while checking the OAuth2 refresh token: ' + row );
+							else if ( row.token ) db.query( 'DELETE FROM oauthusers WHERE userid = $1 AND site = $2', [interaction.user.id, ( result.oauth[1] || result.oauth[0] )] ).then( () => {
+								console.log( '- Dashboard: OAuth2 token for ' + interaction.user.id + ' successfully deleted.' );
+							}, dberror => {
+								console.log( '- Dashboard: Error while deleting the OAuth2 token for ' + interaction.user.id + ': ' + dberror );
+							} );
+						}
+						let state = `${result.oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
 						while ( oauthVerify.has(state) ) {
-							state = `${result.oauth[0]} ${global.shardId}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
+							state = `${result.oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
 						}
 						oauthVerify.set(state, {
-							state, wiki: wiki.href, channel,
-							user: interaction.user.id,
-							token: interaction.token
+							state, wiki: wiki.href, channel: interaction.channel,
+							user: interaction.user.id, interaction
 						});
-						interaction.client.shard.send({id: 'verifyUser', state});
+						interaction.client.shard.send({id: 'verifyUser', state, user: ( row?.token === null ? '' : interaction.user.id )});
 						let oauthURL = wiki + 'rest.php/oauth2/authorize?' + new URLSearchParams({
 							response_type: 'code', redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
 							client_id: process.env['oauth_' + ( result.oauth[1] || result.oauth[0] )], state
 						}).toString();
 						let message = {
-							content: reply + lang.get('verify.oauth_message', '<' + oauthURL + '>'),
-							allowed_mentions,
-							components: [
-								{
-									type: 1,
-									components: [
-										{
-											type: 2,
-											style: 5,
-											label: lang.get('verify.oauth_button'),
-											emoji: {id: null, name: '🔗'},
-											url: oauthURL,
-											disabled: false
-										}
-									]
-								}
-							]
+							content: lang.get('verify.oauth_message', '<' + oauthURL + '>'),
+							components: [new MessageActionRow().addComponents(
+								new MessageButton().setLabel(lang.get('verify.oauth_button')).setEmoji('🔗').setStyle('LINK').setURL(oauthURL)
+							)],
+							ephemeral: true
 						}
-						if ( result.send_private ) return sendMessage(interaction, message, channel, false);
-						message.flags = 64;
-						return interaction.client.api.webhooks(interaction.application_id, interaction.token).messages('@original').delete().then( () => {
-							return interaction.client.api.webhooks(interaction.application_id, interaction.token).post( {
-								data: message
-							} ).catch(log_error);
+						if ( result.send_private ) return sendMessage(interaction, message, false);
+						return interaction.deleteReply().then( () => {
+							return interaction.followUp( message ).catch(log_error);
 						}, log_error );
+					} );
+				}
+				var message = {
+					content: interaction.member.toString() + ', ' + result.content,
+					embeds: [result.embed],
+					components: [],
+					allowedMentions: {
+						users: [interaction.user.id],
+						repliedUser: true
 					}
-					var message = {
-						content: reply + result.content,
-						embeds: [result.embed],
-						allowed_mentions,
-						components: []
-					};
-					if ( result.add_button && !result.send_private ) message.components.push({
-						type: 1,
-						components: [
-							{
-								type: 2,
-								style: 1,
-								label: lang.get('verify.button_again'),
-								emoji: {id: null, name: '🔂'},
-								custom_id: 'verify_again',
-								disabled: false
-							}
-						]
-					});
-					if ( result.reaction ) {
-						if ( result.reaction === 'nowiki' ) message.content = lang.get('interaction.nowiki');
-						else message.content = reply + lang.get('verify.error_reply');
-						message.embeds = [];
+				};
+				if ( result.reaction ) {
+					if ( result.reaction === 'nowiki' ) message.content = lang.get('interaction.nowiki');
+					else message.content = lang.get('verify.error_reply');
+					message.embeds = [];
+				}
+				else if ( result.add_button && !result.send_private ) message.components.push(new MessageActionRow().addComponents(
+					new MessageButton().setLabel(lang.get('verify.button_again')).setEmoji('🔂').setStyle('PRIMARY').setCustomId('verify_again')
+				));
+				return sendMessage(interaction, message, false).then( msg => {
+					if ( !result.logging.channel || !interaction.guild.channels.cache.has(result.logging.channel) ) return;
+					if ( msg && !result.send_private ) {
+						if ( result.logging.embed ) result.logging.embed.addField(msg.url, '<#' + interaction.channelId + '>');
+						else result.logging.content += '\n<#' + interaction.channelId + '> – <' + msg.url + '>';
 					}
-					return sendMessage(interaction, message, channel, false).then( msg => {
-						if ( !result.logging.channel || !channel.guild.channels.cache.has(result.logging.channel) ) return;
-						if ( msg && !result.send_private ) {
-							if ( result.logging.embed ) result.logging.embed.addField(msg.url, '<#' + channel.id + '>');
-							else result.logging.content += '\n<#' + channel.id + '> – <' + msg.url + '>';
-						}
-						channel.guild.channels.cache.get(result.logging.channel).send(result.logging.content, {
-							embed: result.logging.embed,
-							allowedMentions: {parse: []}
-						}).catch(log_error);
-					} );
-				}, error => {
-					console.log( '- Error during the verifications: ' + error );
-					return sendMessage(interaction, {
-						content: reply + lang.get('verify.error_reply'),
-						allowed_mentions
-					}, channel);
+					interaction.guild.channels.cache.get(result.logging.channel).send( {
+						content: result.logging.content,
+						embeds: [result.logging.embed]
+					} ).catch(log_error);
 				} );
 			}, error => {
-				console.log( '- Error while getting the member: ' + error );
-				return sendMessage(interaction, {
-					content: reply + lang.get('verify.error_reply'),
-					allowed_mentions
-				}, channel);
+				console.log( '- Error during the verifications: ' + error );
+				return sendMessage(interaction, lang.get('verify.error_reply'));
 			} );
 		}, log_error );
 	}, dberror => {
 		console.log( '- Error while getting the verifications: ' + dberror );
-		return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {
-				type: 4,
-				data: {
-					content: reply + lang.get('verify.error_reply'),
-					allowed_mentions,
-					flags: 64
-				}
-			}
-		} ).catch(log_error);
+		return interaction.reply( {content: lang.get('verify.error_reply'), ephemeral: true} ).catch(log_error);
 	} );
 }
 
 /**
  * Wiki user verification.
- * @param {Object} interaction - The interaction.
- * @param {import('discord.js').Client} interaction.client - The client of the interaction.
+ * @param {import('discord.js').ButtonInteraction} interaction - The interaction.
  * @param {import('../util/i18n.js')} lang - The user language.
  * @param {import('../util/wiki.js')} wiki - The wiki for the interaction.
- * @param {import('discord.js').TextChannel} [channel] - The channel for the interaction.
  */
- function button_verify(interaction, lang, wiki, channel) {
-	var username = interaction?.message?.embeds?.[0]?.title?.replace( /\\(\\)?/g, '$1' );
-	if ( !username || !channel?.guild || !interaction.message?.mentions?.[0]?.id ) {
-		interaction.message.allowed_mentions = {
-			users: [interaction.user.id]
-		};
-		interaction.message.components = [];
-		return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {
-				type: 7,
-				data: interaction.message
-			}
-		} ).catch(log_error);
+ function button_verify(interaction, lang, wiki) {
+	var username = interaction.message?.embeds?.[0]?.title?.replace( /\\(\\)?/g, '$1' );
+	if ( !username || !interaction.guild || !interaction.message.mentions?.users?.size ) {
+		return interaction.update( {components: []} ).catch(log_error);
 	}
-	if ( interaction.user.id !== interaction.message.mentions[0].id ) {
-		return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {type: 6}
-		} ).then( () => {
-			interaction.client.api.webhooks(interaction.application_id, interaction.token).post( {
-				data: {
-					content: lang.get('verify.button_wrong_user', `<@${interaction.message.mentions[0].id}>`),
-					allowed_mentions: {
-						parse: []
-					},
-					flags: 64
-				}
-			} ).catch(log_error);
-		}, log_error);
+	if ( !interaction.message.mentions.users.has(interaction.user.id) ) {
+		return interaction.reply( {content: lang.get('verify.button_wrong_user', interaction.message.mentions.users.first().toString()), ephemeral: true} ).catch(log_error);
 	}
-	return db.query( 'SELECT logchannel, flags, onsuccess, onmatch, role, editcount, postcount, usergroup, accountage, rename FROM verification LEFT JOIN verifynotice ON verification.guild = verifynotice.guild WHERE verification.guild = $1 AND channel LIKE $2 ORDER BY configid ASC', [interaction.guild_id, '%|' + interaction.channel_id + '|%'] ).then( ({rows}) => {
-		if ( !rows.length || !channel.guild.me.permissions.has('MANAGE_ROLES') ) {
-			return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-				data: {type: 6}
-			} ).catch(log_error);
-		}
-		var reply = '<@' + ( interaction.member?.nick ? '!' : '' ) + interaction.user.id + '>, ';
-		var allowed_mentions = {
-			users: [interaction.user.id]
-		};
-		interaction.message.allowed_mentions = allowed_mentions;
+	return db.query( 'SELECT logchannel, flags, onsuccess, onmatch, role, editcount, postcount, usergroup, accountage, rename FROM verification LEFT JOIN verifynotice ON verification.guild = verifynotice.guild WHERE verification.guild = $1 AND channel LIKE $2 ORDER BY configid ASC', [interaction.guildId, '%|' + ( interaction.channel?.isThread() ? interaction.channel.parentId : interaction.channelId ) + '|%'] ).then( ({rows}) => {
+		if ( !rows.length || !interaction.guild.me.permissions.has(FLAGS.MANAGE_ROLES) ) return interaction.update( {components: []} ).catch(log_error);
 
-		if ( interaction?.message?.embeds?.[0]?.fields?.[1]?.value === lang.get('verify.oauth_used') && interaction?.message?.embeds?.[0]?.url?.startsWith( wiki.origin ) ) {
-			console.log( interaction.guild_id + ': Button: ' + interaction.data.custom_id + ': OAuth2: ' + username );
-			interaction.message.components[0].components[0].disabled = true;
-			return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-				data: {
-					type: 7,
-					data: interaction.message
-				}
-			} ).then( () => {
-				return global.verifyOauthUser('', '', {
-					channel, username, user: interaction.user.id,
-					send: function(content, options) {
-						if ( !content && !options ) {
-							interaction.message.components = [];
-							return sendMessage(interaction, interaction.message, channel, false);
-						}
-						var message = {
-							content, allowed_mentions,
-							embeds: ( options.embed ? [options.embed] : [] ),
-							components: ( options.components ? options.components : [] )
-						};
-						var msg = sendMessage(interaction, message, channel, false);
-						interaction.client.api.webhooks(interaction.application_id, interaction.token).post( {
-							data: {
-								content, allowed_mentions,
-								embeds: ( options.embed ? [options.embed] : [] ),
-								components: [],
-								flags: 64
-							}
-						} ).catch(log_error);
-						return msg;
-					}
-				});
-			}, log_error );
-		}
 		if ( wiki.hasOAuth2() && process.env.dashboard ) {
 			let oauth = [wiki.hostname + wiki.pathname.slice(0, -1)];
 			if ( wiki.isWikimedia() ) oauth.push('wikimedia');
 			if ( wiki.isMiraheze() ) oauth.push('miraheze');
 			if ( process.env['oauth_' + ( oauth[1] || oauth[0] )] && process.env['oauth_' + ( oauth[1] || oauth[0] ) + '_secret'] ) {
-				console.log( interaction.guild_id + ': Button: ' + interaction.data.custom_id + ': OAuth2' );
-				let state = `${oauth[0]} ${global.shardId}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
-				while ( oauthVerify.has(state) ) {
-					state = `${oauth[0]} ${global.shardId}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
-				}
-				oauthVerify.set(state, {
-					state, wiki: wiki.href, channel,
-					user: interaction.user.id,
-					token: interaction.token
-				});
-				interaction.client.shard.send({id: 'verifyUser', state});
-				let oauthURL = wiki + 'rest.php/oauth2/authorize?' + new URLSearchParams({
-					response_type: 'code', redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
-					client_id: process.env['oauth_' + ( oauth[1] || oauth[0] )], state
-				}).toString();
-				interaction.message.components = [];
-				interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-					data: {
-						type: 7,
-						data: interaction.message
-					}
-				} ).catch(log_error);
-				return interaction.client.api.webhooks(interaction.application_id, interaction.token).post( {
-					data: {
-						content: reply + lang.get('verify.oauth_message', '<' + oauthURL + '>'),
-						allowed_mentions,
-						components: [
-							{
-								type: 1,
-								components: [
-									{
-										type: 2,
-										style: 5,
-										label: lang.get('verify.oauth_button'),
-										emoji: {id: null, name: '🔗'},
-										url: oauthURL,
-										disabled: false
-									}
-								]
+				console.log( interaction.guildId + ': Button: ' + interaction.customId + ': OAuth2' );
+				return interaction.update( {components: [new MessageActionRow().addComponents(
+					new MessageButton(interaction.message.components[0].components[0]).setDisabled()
+				)]} ).then( () => {
+					return db.query( 'SELECT token FROM oauthusers WHERE userid = $1 AND site = $2', [interaction.user.id, ( oauth[1] || oauth[0] )] ).then( ({rows: [row]}) => {
+						if ( row?.token ) return got.post( wiki + 'rest.php/oauth2/access_token', {
+							form: {
+								grant_type: 'refresh_token', refresh_token: row.token,
+								redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
+								client_id: process.env['oauth_' + ( oauth[1] || oauth[0] )],
+								client_secret: process.env['oauth_' + ( oauth[1] || oauth[0] ) + '_secret']
 							}
-						],
-						flags: 64
-					}
-				} ).catch(log_error);
+						} ).then( response => {
+							var body = response.body;
+							if ( response.statusCode !== 200 || !body?.access_token ) {
+								console.log( '- ' + response.statusCode + ': Error while refreshing the mediawiki token: ' + ( body?.message || body?.error ) );
+								return Promise.reject(row);
+							}
+							if ( body?.refresh_token ) db.query( 'UPDATE oauthusers SET token = $1 WHERE userid = $2 AND site = $3', [body.refresh_token, interaction.user.id, ( oauth[1] || oauth[0] )] ).then( () => {
+								console.log( '- Dashboard: OAuth2 token for ' + interaction.user.id + ' successfully updated.' );
+							}, dberror => {
+								console.log( '- Dashboard: Error while updating the OAuth2 token for ' + interaction.user.id + ': ' + dberror );
+							} );
+							return global.verifyOauthUser('', body.access_token, {
+								wiki: wiki.href, channel: interaction.channel,
+								user: interaction.user.id, interaction,
+								fail: () => sendMessage(interaction, {components: []}, false)
+							});
+						}, error => {
+							console.log( '- Error while refreshing the mediawiki token: ' + error );
+							return Promise.reject(row);
+						} );
+						return Promise.reject(row);
+					}, dberror => {
+						console.log( '- Error while getting the OAuth2 token: ' + dberror );
+						return Promise.reject();
+					} ).catch( row => {
+						if ( row ) {
+							if ( !row?.hasOwnProperty?.('token') ) console.log( '- Error while checking the OAuth2 refresh token: ' + row );
+							else if ( row.token ) db.query( 'DELETE FROM oauthusers WHERE userid = $1 AND site = $2', [interaction.user.id, ( oauth[1] || oauth[0] )] ).then( () => {
+								console.log( '- Dashboard: OAuth2 token for ' + interaction.user.id + ' successfully deleted.' );
+							}, dberror => {
+								console.log( '- Dashboard: Error while deleting the OAuth2 token for ' + interaction.user.id + ': ' + dberror );
+							} );
+						}
+						let state = `${oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
+						while ( oauthVerify.has(state) ) {
+							state = `${oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( oauth[1] ? ` ${oauth[1]}` : '' );
+						}
+						oauthVerify.set(state, {
+							state, wiki: wiki.href, channel: interaction.channel,
+							user: interaction.user.id, interaction
+						});
+						interaction.client.shard.send({id: 'verifyUser', state, user: ( row?.token === null ? '' : interaction.user.id )});
+						let oauthURL = wiki + 'rest.php/oauth2/authorize?' + new URLSearchParams({
+							response_type: 'code', redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
+							client_id: process.env['oauth_' + ( oauth[1] || oauth[0] )], state
+						}).toString();
+						sendMessage(interaction, {components: []}, false);
+						return interaction.followUp( {
+							content: lang.get('verify.oauth_message', '<' + oauthURL + '>'),
+							components: [new MessageActionRow().addComponents(
+								new MessageButton().setLabel(lang.get('verify.oauth_button')).setEmoji('🔗').setStyle('LINK').setURL(oauthURL)
+							)],
+							ephemeral: true
+						} ).catch(log_error);
+					} );
+				}, log_error );
 			}
 		}
 
-		interaction.message.components[0].components[0].disabled = true;
-		return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {
-				type: 7,
-				data: interaction.message
-			}
-		} ).then( () => {
-			return channel.guild.members.fetch(interaction.user.id).then( member => {
-				console.log( interaction.guild_id + ': Button: ' + interaction.data.custom_id + ' ' + username );
-				return verify(lang, channel, member, username, wiki, rows).then( result => {
-					if ( result.oauth.length ) {
-						let state = `${result.oauth[0]} ${global.shardId}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
+		return interaction.update( {components: [new MessageActionRow().addComponents(
+			new MessageButton(interaction.message.components[0].components[0]).setDisabled()
+		)]} ).then( () => {
+			console.log( interaction.guildId + ': Button: ' + interaction.customId + ' ' + username );
+			return verify(lang, interaction.channel, interaction.member, username, wiki, rows).then( result => {
+				if ( result.oauth.length ) {
+					return db.query( 'SELECT token FROM oauthusers WHERE userid = $1 AND site = $2', [interaction.user.id, ( result.oauth[1] || result.oauth[0] )] ).then( ({rows: [row]}) => {
+						if ( row?.token ) return got.post( wiki + 'rest.php/oauth2/access_token', {
+							form: {
+								grant_type: 'refresh_token', refresh_token: row.token,
+								redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
+								client_id: process.env['oauth_' + ( result.oauth[1] || result.oauth[0] )],
+								client_secret: process.env['oauth_' + ( result.oauth[1] || result.oauth[0] ) + '_secret']
+							}
+						} ).then( response => {
+							var body = response.body;
+							if ( response.statusCode !== 200 || !body?.access_token ) {
+								console.log( '- ' + response.statusCode + ': Error while refreshing the mediawiki token: ' + ( body?.message || body?.error ) );
+								return Promise.reject(row);
+							}
+							if ( body?.refresh_token ) db.query( 'UPDATE oauthusers SET token = $1 WHERE userid = $2 AND site = $3', [body.refresh_token, interaction.user.id, ( result.oauth[1] || result.oauth[0] )] ).then( () => {
+								console.log( '- Dashboard: OAuth2 token for ' + interaction.user.id + ' successfully updated.' );
+							}, dberror => {
+								console.log( '- Dashboard: Error while updating the OAuth2 token for ' + interaction.user.id + ': ' + dberror );
+							} );
+							return global.verifyOauthUser('', body.access_token, {
+								wiki: wiki.href, channel: interaction.channel,
+								user: interaction.user.id, interaction,
+								fail: () => sendMessage(interaction, {components: []}, false)
+							});
+						}, error => {
+							console.log( '- Error while refreshing the mediawiki token: ' + error );
+							return Promise.reject(row);
+						} );
+						return Promise.reject(row);
+					}, dberror => {
+						console.log( '- Error while getting the OAuth2 token: ' + dberror );
+						return Promise.reject();
+					} ).catch( row => {
+						if ( row ) {
+							if ( !row?.hasOwnProperty?.('token') ) console.log( '- Error while checking the OAuth2 refresh token: ' + row );
+							else if ( row.token ) db.query( 'DELETE FROM oauthusers WHERE userid = $1 AND site = $2', [interaction.user.id, ( result.oauth[1] || result.oauth[0] )] ).then( () => {
+								console.log( '- Dashboard: OAuth2 token for ' + interaction.user.id + ' successfully deleted.' );
+							}, dberror => {
+								console.log( '- Dashboard: Error while deleting the OAuth2 token for ' + interaction.user.id + ': ' + dberror );
+							} );
+						}
+						let state = `${result.oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
 						while ( oauthVerify.has(state) ) {
-							state = `${result.oauth[0]} ${global.shardId}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
+							state = `${result.oauth[0]} ${process.env.SHARDS}` + Date.now().toString(16) + randomBytes(16).toString('hex') + ( result.oauth[1] ? ` ${result.oauth[1]}` : '' );
 						}
 						oauthVerify.set(state, {
-							state, wiki: wiki.href, channel,
-							user: interaction.user.id,
-							token: interaction.token
+							state, wiki: wiki.href, channel: interaction.channel,
+							user: interaction.user.id, interaction
 						});
-						interaction.client.shard.send({id: 'verifyUser', state});
+						interaction.client.shard.send({id: 'verifyUser', state, user: ( row?.token === null ? '' : interaction.user.id )});
 						let oauthURL = wiki + 'rest.php/oauth2/authorize?' + new URLSearchParams({
 							response_type: 'code', redirect_uri: new URL('/oauth/mw', process.env.dashboard).href,
 							client_id: process.env['oauth_' + ( result.oauth[1] || result.oauth[0] )], state
 						}).toString();
-						interaction.message.components = [];
-						interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-							data: {
-								type: 7,
-								data: interaction.message
-							}
+						sendMessage(interaction, {components: []}, false);
+						return interaction.followUp( {
+							content: lang.get('verify.oauth_message', '<' + oauthURL + '>'),
+							components: [new MessageActionRow().addComponents(
+								new MessageButton().setLabel(lang.get('verify.oauth_button')).setEmoji('🔗').setStyle('LINK').setURL(oauthURL)
+							)],
+							ephemeral: true
 						} ).catch(log_error);
-						return interaction.client.api.webhooks(interaction.application_id, interaction.token).post( {
-							data: {
-								content: reply + lang.get('verify.oauth_message', '<' + oauthURL + '>'),
-								allowed_mentions,
-								components: [
-									{
-										type: 1,
-										components: [
-											{
-												type: 2,
-												style: 5,
-												label: lang.get('verify.oauth_button'),
-												emoji: {id: null, name: '🔗'},
-												url: oauthURL,
-												disabled: false
-											}
-										]
-									}
-								],
-								flags: 64
-							}
-						} ).catch(log_error);
-					}
-					var message = {
-						content: reply + result.content,
-						embeds: [result.embed],
-						allowed_mentions,
-						components: []
-					};
-					if ( result.reaction ) {
-						if ( result.reaction === 'nowiki' ) message.content = lang.get('interaction.nowiki');
-						else message.content = reply + lang.get('verify.error_reply');
-						message.embeds = [];
-					}
-					else if ( result.add_button ) message.components.push({
-						type: 1,
-						components: [
-							{
-								type: 2,
-								style: 1,
-								label: lang.get('verify.button_again'),
-								emoji: {id: null, name: '🔂'},
-								custom_id: 'verify_again',
-								disabled: false
-							}
-						]
-					});
-					sendMessage(interaction, message, channel, false);
-					if ( result.logging.channel && channel.guild.channels.cache.has(result.logging.channel) ) {
-						if ( !result.send_private ) {
-							let msg_url = `https://discord.com/channels/${channel.guild.id}/${channel.id}/${interaction.message.id}`;
-							if ( result.logging.embed ) result.logging.embed.addField(msg_url, '<#' + channel.id + '>');
-							else result.logging.content += '\n<#' + channel.id + '> – <' + msg_url + '>';
-						}
-						channel.guild.channels.cache.get(result.logging.channel).send(result.logging.content, {
-							embed: result.logging.embed,
-							allowedMentions: {parse: []}
-						}).catch(log_error);
+					} );
+				}
+				var message = {
+					content: interaction.member.toString() + ', ' + result.content,
+					embeds: [result.embed],
+					components: [],
+					allowedMentions: {
+						users: [interaction.user.id],
+						repliedUser: true
 					}
-					interaction.client.api.webhooks(interaction.application_id, interaction.token).post( {
-						data: {
-							content: message.content,
-							embeds: message.embeds,
-							allowed_mentions,
-							components: [],
-							flags: 64
-						}
+				};
+				if ( result.reaction ) {
+					if ( result.reaction === 'nowiki' ) message.content = lang.get('interaction.nowiki');
+					else message.content = lang.get('verify.error_reply');
+					message.embeds = [];
+				}
+				else if ( result.add_button ) message.components.push(new MessageActionRow().addComponents(
+					new MessageButton().setLabel(lang.get('verify.button_again')).setEmoji('🔂').setStyle('PRIMARY').setCustomId('verify_again')
+				));
+				sendMessage(interaction, message, false);
+				if ( result.logging.channel && interaction.guild.channels.cache.has(result.logging.channel) ) {
+					if ( result.logging.embed ) result.logging.embed.addField(interaction.message.url, '<#' + interaction.channelId + '>');
+					else result.logging.content += '\n<#' + interaction.channelId + '> – <' + interaction.message.url + '>';
+					interaction.guild.channels.cache.get(result.logging.channel).send( {
+						content: result.logging.content,
+						embeds: [result.logging.embed]
 					} ).catch(log_error);
-				}, error => {
-					console.log( '- Error during the verifications: ' + error );
-					return sendMessage(interaction, {
-						content: reply + lang.get('verify.error_reply'),
-						allowed_mentions
-					}, channel);
-				} );
+				}
+				interaction.followUp( {
+					content: message.content,
+					embeds: message.embeds,
+					components: [],
+					ephemeral: true
+				} ).catch(log_error);
 			}, error => {
-				console.log( '- Error while getting the member: ' + error );
-				return sendMessage(interaction, {
-					content: reply + lang.get('verify.error_reply'),
-					allowed_mentions
-				}, channel);
+				console.log( '- Error during the verifications: ' + error );
+				return sendMessage(interaction, {components: []});
 			} );
 		}, log_error);
 	}, dberror => {
 		console.log( '- Error while getting the verifications: ' + dberror );
-		return interaction.client.api.interactions(interaction.id, interaction.token).callback.post( {
-			data: {type: 6}
-		} ).catch(log_error);
+		return interaction.reply( {content: lang.get('verify.error_reply'), ephemeral: true} ).catch(log_error);
 	} );
 }
 

+ 169 - 152
main.js

@@ -11,11 +11,11 @@ const got = require('got').extend( {
 	throwHttpErrors: false,
 	timeout: 30000,
 	headers: {
-		'User-Agent': 'Wiki-Bot/' + ( isDebug ? 'testing' : process.env.npm_package_version ) + ' (Discord; ' + process.env.npm_package_name + ')'
+		'User-Agent': 'Wiki-Bot/' + ( isDebug ? 'testing' : process.env.npm_package_version ) + ' (Discord; ' + process.env.npm_package_name + ( process.env.invite ? '; ' + process.env.invite : '' ) + ')'
 	},
 	responseType: 'json'
 } );
-const {ShardingManager, ShardClientUtil: {shardIDForGuildID}} = require('discord.js');
+const {ShardingManager, ShardClientUtil: {shardIdForGuildId}} = require('discord.js');
 const manager = new ShardingManager( './bot.js', {
 	execArgv: ['--icu-data-dir=node_modules/full-icu'],
 	shardArgs: ( isDebug ? ['debug'] : [] ),
@@ -26,13 +26,8 @@ var diedShards = 0;
 manager.on( 'shardCreate', shard => {
 	console.log( `- Shard[${shard.id}]: Launched` );
 	
-	shard.on( 'spawn', message => {
+	shard.on( 'spawn', () => {
 		console.log( `- Shard[${shard.id}]: Spawned` );
-		shard.send( {
-			shard: {
-				id: shard.id
-			}
-		} );
 	} );
 	
 	shard.on( 'message', message => {
@@ -47,7 +42,9 @@ manager.on( 'shardCreate', shard => {
 		if ( message === 'toggleDebug' ) {
 			console.log( '\n- Toggle debug logging for all shards!\n' );
 			isDebug = !isDebug;
-			manager.broadcastEval( `global.isDebug = !global.isDebug` );
+			manager.broadcastEval( () => {
+				global.isDebug = !global.isDebug;
+			} );
 			if ( typeof server !== 'undefined' ) server.send( 'toggleDebug' );
 		}
 		if ( message === 'postStats' && process.env.botlist ) postStats();
@@ -65,7 +62,7 @@ manager.on( 'shardCreate', shard => {
 	} );
 } );
 
-manager.spawn().then( shards => {
+manager.spawn({timeout: 60000}).then( shards => {
 	if ( !isDebug && process.env.botlist ) {
 		var botList = JSON.parse(process.env.botlist);
 		for ( let [key, value] of Object.entries(botList) ) {
@@ -77,11 +74,19 @@ manager.spawn().then( shards => {
 	}
 }, error => {
 	console.error( '- Error while spawning the shards: ' + error );
+	manager.shards.filter( shard => shard.process && !shard.process.killed ).forEach( shard => shard.kill() );
 	if ( isDebug ) {
+		manager.respawn = false;
 		if ( typeof server !== 'undefined' && !server.killed ) server.kill();
 		process.exit(1);
 	}
-	else manager.respawnAll();
+	else manager.spawn({timeout: -1}).catch( error2 => {
+		console.error( '- Error while spawning the shards: ' + error2 );
+		manager.respawn = false;
+		manager.shards.filter( shard => shard.process && !shard.process.killed ).forEach( shard => shard.kill() );
+		if ( typeof server !== 'undefined' && !server.killed ) server.kill();
+		process.exit(1);
+	} );
 } );
 
 var server;
@@ -89,6 +94,147 @@ if ( process.env.dashboard ) {
 	const dashboard = child_process.fork('./dashboard/index.js', ( isDebug ? ['debug'] : [] ));
 	server = dashboard;
 
+	const evalFunctions = {
+		getGuilds: (discordClient, evalData) => {
+			return Promise.all(
+				evalData.guilds.map( id => {
+					if ( discordClient.guilds.cache.has(id) ) {
+						let guild = discordClient.guilds.cache.get(id);
+						return guild.members.fetch(evalData.member).then( member => {
+							return {
+								patreon: global.patreons.hasOwnProperty(guild.id),
+								memberCount: guild.memberCount,
+								botPermissions: guild.me.permissions.bitfield.toString(),
+								channels: guild.channels.cache.filter( channel => {
+									return ( channel.isGuild(false) || channel.type === 'GUILD_CATEGORY' );
+								} ).sort( (a, b) => {
+									let aVal = a.rawPosition + 1;
+									if ( a.type === 'GUILD_CATEGORY' ) aVal *= 1000;
+									else if ( !a.parent ) aVal -= 1000;
+									else aVal += ( a.parent.rawPosition + 1 ) * 1000;
+									let bVal = b.rawPosition + 1;
+									if ( b.type === 'GUILD_CATEGORY' ) bVal *= 1000;
+									else if ( !b.parent ) bVal -= 1000;
+									else bVal += ( b.parent.rawPosition + 1 ) * 1000;
+									return aVal - bVal;
+								} ).map( channel => {
+									return {
+										id: channel.id,
+										name: channel.name,
+										isCategory: ( channel.type === 'GUILD_CATEGORY' ),
+										userPermissions: member.permissionsIn(channel).bitfield.toString(),
+										botPermissions: guild.me.permissionsIn(channel).bitfield.toString()
+									};
+								} ),
+								roles: guild.roles.cache.filter( role => {
+									return ( role.id !== guild.id );
+								} ).sort( (a, b) => {
+									return b.rawPosition - a.rawPosition;
+								} ).map( role => {
+									return {
+										id: role.id,
+										name: role.name,
+										lower: ( guild.me.roles.highest.comparePositionTo(role) > 0 && !role.managed )
+									};
+								} ),
+								locale: guild.preferredLocale
+							};
+						}, error => {
+							return 'noMember';
+						} );
+					}
+				} )
+			)
+		},
+		getMember: (discordClient, evalData) => {
+			if ( discordClient.guilds.cache.has(evalData.guild) ) {
+				let guild = discordClient.guilds.cache.get(evalData.guild);
+				return guild.members.fetch(evalData.member).then( member => {
+					var response = {
+						patreon: global.patreons.hasOwnProperty(guild.id),
+						userPermissions: member.permissions.bitfield.toString(),
+						botPermissions: guild.me.permissions.bitfield.toString()
+					};
+					if ( evalData.channel ) {
+						let channel = guild.channels.cache.get(evalData.channel);
+						if ( channel?.isGuild(false) || ( response.patreon && evalData.allowCategory && channel?.type === 'GUILD_CATEGORY' ) ) {
+							response.userPermissions = channel.permissionsFor(member).bitfield.toString();
+							response.botPermissions = channel.permissionsFor(guild.me).bitfield.toString();
+							response.isCategory = ( channel.type === 'GUILD_CATEGORY' );
+							response.parentId = channel.parentId;
+						}
+						else response.message = 'noChannel';
+					}
+					if ( evalData.newchannel ) {
+						let newchannel = guild.channels.cache.get(evalData.newchannel);
+						if ( newchannel?.isGuild(false) ) {
+							response.userPermissionsNew = newchannel.permissionsFor(member).bitfield.toString();
+							response.botPermissionsNew = newchannel.permissionsFor(guild.me).bitfield.toString();
+						}
+						else response.message = 'noChannel';
+					}
+					return response;
+				}, error => {
+					return 'noMember';
+				} );
+			}
+		},
+		notifyGuild: (discordClient, evalData) => {
+			if ( evalData.prefix ) {
+				global.patreons[evalData.guild] = evalData.prefix;
+			}
+			if ( evalData.voice && global.voice.hasOwnProperty(evalData.guild) ) {
+				global.voice[evalData.guild] = evalData.voice;
+			}
+			if ( discordClient.guilds.cache.has(evalData.guild) ) {
+				let channel = discordClient.guilds.cache.get(evalData.guild).publicUpdatesChannel;
+				if ( channel ) channel.send( {
+					content: evalData.text,
+					embeds: [evalData.embed],
+					files: evalData.file,
+					allowedMentions: {parse: []}
+				} ).catch(log_error);
+			}
+		},
+		createWebhook: (discordClient, evalData) => {
+			if ( discordClient.guilds.cache.has(evalData.guild) ) {
+				let channel = discordClient.guilds.cache.get(evalData.guild).channels.cache.get(evalData.channel);
+				if ( channel ) return channel.createWebhook( evalData.name, {
+					avatar: ( evalData.avatar || discordClient.user.displayAvatarURL({format:'png',size:4096}) ),
+					reason: evalData.reason
+				} ).then( webhook => {
+					console.log( `- Dashboard: Webhook successfully created: ${evalData.guild}#${evalData.channel}` );
+					webhook.send( evalData.text ).catch(log_error);
+					return webhook.id + '/' + webhook.token;
+				}, error => {
+					console.log( '- Dashboard: Error while creating the webhook: ' + error );
+				} );
+			}
+		},
+		editWebhook: (discordClient, evalData) => {
+			if ( discordClient.guilds.cache.has(evalData.guild) ) {
+				return discordClient.fetchWebhook(...evalData.webhook.split('/')).then( webhook => {
+					var changes = {};
+					if ( evalData.channel ) changes.channel = evalData.channel;
+					if ( evalData.name ) changes.name = evalData.name;
+					if ( evalData.avatar ) changes.avatar = evalData.avatar;
+					return webhook.edit( changes, evalData.reason ).then( newwebhook => {
+						console.log( `- Dashboard: Webhook successfully edited: ${evalData.guild}#` + ( evalData.channel || webhook.channelId ) );
+						webhook.send( evalData.text ).catch(log_error);
+						return true;
+					}, error => {
+						console.log( '- Dashboard: Error while editing the webhook: ' + error );
+					} );
+				}, error => {
+					console.log( '- Dashboard: Error while editing the webhook: ' + error );
+				} );
+			}
+		},
+		verifyUser: (discordClient, evalData) => {
+			global.verifyOauthUser(evalData.state, evalData.access_token);
+		}
+	};
+
 	dashboard.on( 'message', message => {
 		if ( message.id ) {
 			var data = {
@@ -98,55 +244,7 @@ if ( process.env.dashboard ) {
 			};
 			switch ( message.data.type ) {
 				case 'getGuilds':
-					return manager.broadcastEval(`Promise.all(
-						${JSON.stringify(message.data.guilds)}.map( id => {
-							if ( this.guilds.cache.has(id) ) {
-								let guild = this.guilds.cache.get(id);
-								return guild.members.fetch(${JSON.stringify(message.data.member)}).then( member => {
-									return {
-										patreon: global.patreons.hasOwnProperty(guild.id),
-										memberCount: guild.memberCount,
-										botPermissions: guild.me.permissions.bitfield,
-										channels: guild.channels.cache.filter( channel => {
-											return ( channel.isGuild() || channel.type === 'category' );
-										} ).sort( (a, b) => {
-											let aVal = a.rawPosition + 1;
-											if ( a.type === 'category' ) aVal *= 1000;
-											else if ( !a.parent ) aVal -= 1000;
-											else aVal += ( a.parent.rawPosition + 1 ) * 1000;
-											let bVal = b.rawPosition + 1;
-											if ( b.type === 'category' ) bVal *= 1000;
-											else if ( !b.parent ) bVal -= 1000;
-											else bVal += ( b.parent.rawPosition + 1 ) * 1000;
-											return aVal - bVal;
-										} ).map( channel => {
-											return {
-												id: channel.id,
-												name: channel.name,
-												isCategory: ( channel.type === 'category' ),
-												userPermissions: member.permissionsIn(channel).bitfield,
-												botPermissions: guild.me.permissionsIn(channel).bitfield
-											};
-										} ),
-										roles: guild.roles.cache.filter( role => {
-											return ( role.id !== guild.id );
-										} ).sort( (a, b) => {
-											return b.rawPosition - a.rawPosition;
-										} ).map( role => {
-											return {
-												id: role.id,
-												name: role.name,
-												lower: ( guild.me.roles.highest.comparePositionTo(role) > 0 && !role.managed )
-											};
-										} ),
-										locale: guild.preferredLocale
-									};
-								}, error => {
-									return 'noMember';
-								} )
-							}
-						} )
-					)`).then( results => {
+					return manager.broadcastEval( evalFunctions.getGuilds, {context: message.data} ).then( results => {
 						data.response = message.data.guilds.map( (guild, i) => {
 							return results.find( result => result[i] )?.[i];
 						} );
@@ -157,78 +255,12 @@ if ( process.env.dashboard ) {
 					} );
 					break;
 				case 'getMember':
-					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 ( ${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' );
-									response.parentID = channel.parentID;
-								}
-								else response.message = 'noChannel';
-							}
-							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;
-								}
-								else response.message = 'noChannel';
-							}
-							return response;
-						}, error => {
-							return 'noMember';
-						} );
-					}`, shardIDForGuildID(message.data.guild, manager.totalShards)).then( result => {
-						data.response = result;
-					}, error => {
-						data.error = error.toString();
-					} ).finally( () => {
-						return dashboard.send( {id: message.id, data} );
-					} );
-					break;
-				case 'notifyGuild':
-					return manager.broadcastEval(`if ( ${JSON.stringify(message.data.prefix)} ) {
-						global.patreons[${JSON.stringify(message.data.guild)}] = ${JSON.stringify(message.data.prefix)};
-					}
-					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(${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
-						} ).catch( error => {} );
-					}`).catch( error => {
-						data.error = error.toString();
-					} ).finally( () => {
-						return dashboard.send( {id: message.id, data} );
-					} );
-					break;
 				case 'createWebhook':
-					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( ${JSON.stringify(message.data.text)} ).catch(log_error);
-							return webhook.id + '/' + webhook.token;
-						}, error => {
-							console.log( '- Dashboard: Error while creating the webhook: ' + error );
-						} );
-					}`, shardIDForGuildID(message.data.guild, manager.totalShards)).then( result => {
+				case 'editWebhook':
+					return manager.broadcastEval( evalFunctions[message.data.type], {
+						context: message.data,
+						shard: shardIdForGuildId(message.data.guild, manager.totalShards)
+					} ).then( result => {
 						data.response = result;
 					}, error => {
 						data.error = error.toString();
@@ -236,33 +268,18 @@ if ( process.env.dashboard ) {
 						return dashboard.send( {id: message.id, data} );
 					} );
 					break;
-				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 editing the webhook: ' + error );
-							} );
-						}, error => {
-							console.log( '- Dashboard: Error while editing the webhook: ' + error );
-						} );
-					}`, shardIDForGuildID(message.data.guild, manager.totalShards)).then( result => {
-						data.response = result;
-					}, error => {
+				case 'notifyGuild':
+					return manager.broadcastEval( evalFunctions.notifyGuild, {context: message.data} ).catch( error => {
 						data.error = error.toString();
 					} ).finally( () => {
 						return dashboard.send( {id: message.id, data} );
 					} );
 					break;
 				case 'verifyUser':
-					return manager.broadcastEval(`global.verifyOauthUser(${JSON.stringify(message.data.state)}, ${JSON.stringify(message.data.access_token)})`, message.data.state.split(' ')[1][0]).catch( error => {
+					return manager.broadcastEval( evalFunctions.verifyUser, {
+						context: message.data,
+						shard: message.data.state.split(' ')[1][0]
+					} ).catch( error => {
 						data.error = error.toString();
 					} ).finally( () => {
 						return dashboard.send( {id: message.id, data} );

File diff suppressed because it is too large
+ 329 - 203
package-lock.json


+ 6 - 6
package.json

@@ -1,6 +1,6 @@
 {
   "name": "discord-wiki-bot",
-  "version": "4.0.0",
+  "version": "4.2.0",
   "description": "Wiki-Bot is a bot with the purpose to easily search for and link to wiki pages. Wiki-Bot shows short descriptions and additional info about pages and is able to resolve redirects and follow interwiki links.",
   "main": "main.js",
   "scripts": {
@@ -12,19 +12,19 @@
   "author": "MarkusRost",
   "license": "ISC",
   "engines": {
-    "node": ">=14.0.0"
+    "node": ">=16.6.0"
   },
   "dependencies": {
     "cheerio": "^1.0.0-rc.10",
     "datetime-difference": "^1.0.2",
-    "discord-oauth2": "^2.6.0",
-    "discord.js": "^12.5.3",
+    "discord-oauth2": "^2.7.1",
+    "discord.js": "^13.1.0",
     "dotenv": "^10.0.0",
     "full-icu": "^1.3.4",
     "got": "^11.8.2",
     "htmlparser2": "^6.1.0",
-    "npm": "^7.17.0",
-    "pg": "^8.6.0"
+    "npm": "^7.20.6",
+    "pg": "^8.7.1"
   },
   "repository": {
     "type": "git",

Some files were not shown because too many files changed in this diff