소스 검색

Make wiki type of URL

also code cleanup
closes #55
Markus-Rost 4 년 전
부모
커밋
79443cd0b8

+ 16 - 86
bot.js

@@ -22,7 +22,7 @@ const Lang = require('./util/i18n.js');
 const newMessage = require('./util/newMessage.js');
 global.patreons = {};
 global.voice = {};
-var db = require('./util/database.js');
+const db = require('./util/database.js');
 
 const Discord = require('discord.js');
 const client = new Discord.Client( {
@@ -63,20 +63,6 @@ client.on( 'ready', () => {
 } );
 
 
-String.prototype.noWiki = function(href) {
-	if ( !href ) return true;
-	else if ( this.startsWith( 'https://www.' ) && ( this.endsWith( '.gamepedia.com/' ) || this.isFandom() ) ) return true;
-	else if ( this.isFandom() ) return [
-		this.replace( /^https:\/\/([a-z\d-]{1,50}\.(?:fandom\.com|wikia\.org))\/(?:[a-z-]{1,8}\/)?$/, 'https://community.fandom.com/wiki/Community_Central:Not_a_valid_community?from=$1' ),
-		this + 'language-wikis'
-	].includes( href.replace( /Unexpected token < in JSON at position 0 in "([^ ]+)"/, '$1' ) );
-	else return false;
-};
-
-String.prototype.isFandom = function() {
-	return /^https:\/\/[a-z\d-]{1,50}\.(?:fandom\.com|wikia\.org)\/(?:[a-z-]{1,8}\/)?$/.test(this);
-};
-
 String.prototype.isMention = function(guild) {
 	var text = this.trim();
 	return text === '@' + client.user.username || text.replace( /^<@!?(\d+)>$/, '$1' ) === client.user.id || ( guild && text === '@' + guild.me.displayName );
@@ -98,63 +84,6 @@ Discord.Message.prototype.uploadFiles = function() {
 	return this.channel.type !== 'text' || this.channel.permissionsFor(client.user).has('ATTACH_FILES');
 };
 
-String.prototype.toLink = function(title = '', querystring = '', fragment = '', {server: serverURL, articlepath: articlePath} = {}, isMarkdown = false) {
-	var linksuffix = ( querystring ? '?' + querystring : '' ) + ( fragment ? '#' + fragment.toSection() : '' );
-	if ( serverURL && articlePath ) return serverURL.replace( /^(?:https?:)?\/\//, 'https://' ) + articlePath.replaceSave( '$1', title.toTitle(isMarkdown, articlePath.includes( '?' )) ) + ( articlePath.includes( '?' ) && linksuffix.startsWith( '?' ) ? '&' + linksuffix.substring(1) : linksuffix );
-	if ( this.endsWith( '.gamepedia.com/' ) ) return this + title.toTitle(isMarkdown) + linksuffix;
-	if ( this.isFandom() ) return this + 'wiki/' + title.toTitle(isMarkdown) + linksuffix;
-	let project = wikiProjects.find( project => this.split('/')[2].endsWith( project.name ) );
-	if ( project ) {
-		let regex = this.match( new RegExp( '^https://' + project.regex + project.scriptPath + '$' ) );
-		if ( regex ) return 'https://' + regex[1] + project.articlePath + title.toTitle(isMarkdown, project.articlePath.includes( '?' )) + ( project.articlePath.includes( '?' ) && linksuffix.startsWith( '?' ) ? '&' + linksuffix.substring(1) : linksuffix );
-	}
-	return this + 'index.php?title=' + title.toTitle(isMarkdown, true) + ( linksuffix.startsWith( '?' ) ? '&' + linksuffix.substring(1) : linksuffix );
-};
-
-String.prototype.toDescLink = function(title = '') {
-	return this + 'wiki/' + encodeURIComponent( title.replace( / /g, '_' ) );
-};
-
-String.prototype.toTitle = function(isMarkdown = false, inQuery = false) {
-	var title = this.replace( / /g, '_' ).replace( /\%/g, '%25' ).replace( /\\/g, '%5C' ).replace( /\?/g, '%3F' ).replace( /@(here|everyone)/g, '%40$1' );
-	if ( inQuery ) title = title.replace( /\&/g, '%26' );
-	if ( isMarkdown ) title = title.replace( /([\(\)])/g, '\\$1' );
-	return title;
-};
-
-String.prototype.toSearch = function() {
-	return encodeURIComponent( this ).replace( /%20/g, '+' );
-};
-
-String.prototype.toSection = function() {
-	return encodeURIComponent( this.replace( / /g, '_' ) ).replace( /\'/g, '%27' ).replace( /\(/g, '%28' ).replace( /\)/g, '%29' ).replace( /\%/g, '.' );
-};
-
-String.prototype.toFormatting = function(showEmbed = false, ...args) {
-	if ( showEmbed ) return this.toMarkdown(...args);
-	else return this.toPlaintext();
-};
-
-String.prototype.toMarkdown = function(wiki, path, title = '') {
-	var text = this.replace( /[\(\)\\]/g, '\\$&' );
-	var link = null;
-	var regex = /\[\[(?:([^\|\]]+)\|)?([^\]]+)\]\]([a-z]*)/g;
-	while ( ( link = regex.exec(text) ) !== null ) {
-		var pagetitle = ( link[1] || link[2] );
-		var page = wiki.toLink(( /^[#\/]/.test(pagetitle) ? title + ( pagetitle.startsWith( '/' ) ? pagetitle : '' ) : pagetitle ), '', ( pagetitle.startsWith( '#' ) ? pagetitle.substring(1) : '' ), path, true);
-		text = text.replaceSave( link[0], '[' + link[2] + link[3] + '](' + page + ')' );
-	}
-	regex = /\/\*\s*([^\*]+?)\s*\*\/\s*(.)?/g;
-	while ( title !== '' && ( link = regex.exec(text) ) !== null ) {
-		text = text.replaceSave( link[0], '[→' + link[1] + '](' + wiki.toLink(title, '', link[1], path, true) + ')' + ( link[2] ? ': ' + link[2] : '' ) );
-	}
-	return text.escapeFormatting(true);
-};
-
-String.prototype.toPlaintext = function() {
-	return this.replace( /\[\[(?:[^\|\]]+\|)?([^\]]+)\]\]/g, '$1' ).replace( /\/\*\s*([^\*]+?)\s*\*\//g, '→$1:' ).escapeFormatting();
-};
-
 String.prototype.escapeFormatting = function(isMarkdown) {
 	var text = this;
 	if ( !isMarkdown ) text = text.replace( /[\(\)\\]/g, '\\$&' );
@@ -193,8 +122,8 @@ Discord.Message.prototype.sendChannel = function(content, options = {}, ignorePa
 	if ( this.channel.type !== 'text' || !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 => message.allowDelete(this.author.id) );
-			else msg.allowDelete(this.author.id);
+			if ( msg.length ) msg.forEach( message => allowDelete(message, this.author.id) );
+			else allowDelete(msg, this.author.id);
 			return msg;
 		}, error => {
 			log_error(error);
@@ -211,11 +140,11 @@ Discord.Message.prototype.sendChannelError = function(content, options = {}) {
 	return this.channel.send(content, options).then( msg => {
 		if ( msg.length ) msg.forEach( message => {
 			message.reactEmoji('error');
-			message.allowDelete(this.author.id);
+			allowDelete(message, this.author.id);
 		} );
 		else {
 			msg.reactEmoji('error');
-			msg.allowDelete(this.author.id);
+			allowDelete(msg, this.author.id);
 		}
 		return msg;
 	}, error => {
@@ -229,8 +158,8 @@ Discord.Message.prototype.replyMsg = function(content, options = {}, ignorePause
 		if ( !options.allowedMentions ) options.allowedMentions = {users:[this.author.id]};
 		return this.reply(content, options).then( msg => {
 			if ( allowDelete ) {
-				if ( msg.length ) msg.forEach( message => message.allowDelete(this.author.id) );
-				else msg.allowDelete(this.author.id);
+				if ( msg.length ) msg.forEach( message => allowDelete(message, this.author.id) );
+				else allowDelete(msg, this.author.id);
 			}
 			return msg;
 		}, error => {
@@ -243,14 +172,15 @@ Discord.Message.prototype.replyMsg = function(content, options = {}, ignorePause
 	}
 };
 
-Discord.Message.prototype.deleteMsg = function(timeout = 0) {
-	return this.delete({timeout}).catch(log_error);
-};
-
-Discord.Message.prototype.allowDelete = function(author) {
-	return this.awaitReactions( (reaction, user) => reaction.emoji.name === '🗑️' && user.id === author, {max:1,time:120000} ).then( reaction => {
+/**
+ * All users to delete their command responses.
+ * @param {Discord.Message} msg - The response.
+ * @param {String} author - The user.
+ */
+function allowDelete(msg, author) {
+	msg.awaitReactions( (reaction, user) => reaction.emoji.name === '🗑️' && user.id === author, {max:1,time:120000} ).then( reaction => {
 		if ( reaction.size ) {
-			this.deleteMsg();
+			msg.delete().catch(log_error);
 		}
 	} );
 };
@@ -412,7 +342,7 @@ global.log_warn = function(warning, api = true) {
 
 /**
  * End the process gracefully.
- * @param {String} signal - The signal received.
+ * @param {NodeJS.Signals} signal - The signal received.
  */
 async function graceful(signal) {
 	isStop = true;

+ 1 - 1
cmds/eval.js

@@ -12,7 +12,7 @@ var db = require('../util/database.js');
  * @param {Discord.Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  * @async
  */
 async function cmd_eval(lang, msg, args, line, wiki) {

+ 1 - 1
cmds/get.js

@@ -8,7 +8,7 @@ var db = require('../util/database.js');
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  * @async
  */
 async function cmd_get(lang, msg, args, line, wiki) {

+ 2 - 2
cmds/help.js

@@ -76,12 +76,12 @@ const restrictions = {
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  */
 function cmd_help(lang, msg, args, line, wiki) {
 	if ( msg.channel.type === 'text' && pause[msg.guild.id] && ( args.join('') || !msg.isAdmin() ) ) return;
 	if ( msg.isAdmin() && msg.defaultSettings ) help_server(lang, msg);
-	var isMinecraft = ( wiki === lang.get('minecraft.link') );
+	var isMinecraft = ( wiki.href === lang.get('minecraft.link') );
 	if ( args.join('') ) {
 		if ( args.join(' ').isMention(msg.guild) ) {
 			if ( !( msg.isAdmin() && msg.defaultSettings ) ) help_server(lang, msg);

+ 1 - 1
cmds/info.js

@@ -6,7 +6,7 @@ const help_server = require('../functions/helpserver.js');
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  */
 function cmd_info(lang, msg, args, line, wiki) {
 	if ( args.join('') ) this.LINK(lang, msg, line, wiki);

+ 1 - 1
cmds/invite.js

@@ -6,7 +6,7 @@ const {defaultPermissions} = require('../util/default.json');
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  */
 function cmd_invite(lang, msg, args, line, wiki) {
 	if ( args.join('') ) {

+ 1 - 1
cmds/link.js

@@ -10,7 +10,7 @@ const help_setup = require('../functions/helpsetup.js');
  * @param {import('../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String} title - The page title.
- * @param {String} wiki - The wiki for the page.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the page.
  * @param {String} [cmd] - The command at this point.
  */
 function cmd_link(lang, msg, title, wiki, cmd = '') {

+ 8 - 5
cmds/minecraft/bug.js

@@ -1,4 +1,5 @@
 const {MessageEmbed} = require('discord.js');
+const Wiki = require('../../util/wiki.js');
 
 /**
  * Sends a Minecraft issue.
@@ -7,7 +8,7 @@ const {MessageEmbed} = require('discord.js');
  * @param {String[]} args - The command arguments.
  * @param {String} title - The page title.
  * @param {String} cmd - The command at this point.
- * @param {String} querystring - The querystring for the link.
+ * @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.
@@ -74,9 +75,11 @@ function minecraft_bug(lang, msg, args, title, cmd, querystring, fragment, react
 		} );
 	}
 	else if ( invoke && invoke.toLowerCase() === 'version' && args.length && args.join(' ').length < 100 ) {
-		var jql = 'fixVersion="' + args.join(' ').replace( /(["\\])/g, '\\$1' ).toSearch() + '"+order+by+key';
-		var link = 'https://bugs.mojang.com/issues/?jql=' + jql;
-		got.get( 'https://bugs.mojang.com/rest/api/2/search?fields=summary,resolution,status&jql=' + jql + '&maxResults=25' ).then( response => {
+		var jql = new URLSearchParams({
+			jql: 'fixVersion="' + args.join(' ').replace( /["\\]/g, '\\$&' ) + '" order by key'
+		});
+		var link = 'https://bugs.mojang.com/issues/?' + jql;
+		got.get( 'https://bugs.mojang.com/rest/api/2/search?fields=summary,resolution,status&' + jql + '&maxResults=25' ).then( response => {
 			var body = response.body;
 			if ( response.statusCode !== 200 || !body || body['status-code'] === 404 || body.errorMessages || body.errors ) {
 				if ( body && body.errorMessages ) {
@@ -121,7 +124,7 @@ function minecraft_bug(lang, msg, args, title, cmd, querystring, fragment, react
 	}
 	else {
 		msg.notMinecraft = true;
-		this.WIKI.gamepedia(lang, msg, title, lang.get('minecraft.link'), cmd, reaction, spoiler, querystring, fragment);
+		this.WIKI.gamepedia(lang, msg, title, new Wiki(lang.get('minecraft.link')), cmd, reaction, spoiler, querystring, fragment);
 	}
 }
 

+ 4 - 2
cmds/minecraft/command.js

@@ -1,3 +1,5 @@
+const Wiki = require('../../util/wiki.js');
+
 /**
  * Processes Minecraft commands.
  * @param {import('../../util/i18n.js')} lang - The user language.
@@ -5,7 +7,7 @@
  * @param {String[]} args - The command arguments.
  * @param {String} title - The page title.
  * @param {String} cmd - The command at this point.
- * @param {String} querystring - The querystring for the link.
+ * @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.
@@ -17,7 +19,7 @@ function minecraft_command(lang, msg, args, title, cmd, querystring, fragment, r
 	}
 	else {
 		msg.notMinecraft = true;
-		this.WIKI.gamepedia(lang, msg, title, lang.get('minecraft.link'), cmd, reaction, spoiler, querystring, fragment);
+		this.WIKI.gamepedia(lang, msg, title, new Wiki(lang.get('minecraft.link')), cmd, reaction, spoiler, querystring, fragment);
 	}
 }
 

+ 3 - 2
cmds/minecraft/syntax.js

@@ -1,3 +1,4 @@
+const Wiki = require('../../util/wiki.js');
 const commands = require('./commands.json');
 
 /**
@@ -8,7 +9,7 @@ const commands = require('./commands.json');
  * @param {String[]} args - The command arguments.
  * @param {String} title - The page title.
  * @param {String} cmd - The command at this point.
- * @param {String} querystring - The querystring for the link.
+ * @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.
@@ -40,7 +41,7 @@ function minecraft_syntax(lang, msg, mccmd, args, title, cmd, querystring, fragm
 	else {
 		msg.reactEmoji('❓');
 		msg.notMinecraft = true;
-		this.WIKI.gamepedia(lang, msg, title, lang.get('minecraft.link'), cmd, reaction, spoiler, querystring, fragment);
+		this.WIKI.gamepedia(lang, msg, title, new Wiki(lang.get('minecraft.link')), cmd, reaction, spoiler, querystring, fragment);
 	}
 }
 

+ 1 - 1
cmds/patreon.js

@@ -7,7 +7,7 @@ var db = require('../util/database.js');
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @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('') ) ) {

+ 1 - 1
cmds/pause.js

@@ -4,7 +4,7 @@
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  */
 function cmd_pause(lang, msg, args, line, wiki) {
 	if ( msg.channel.type === 'text' && args.join(' ').split('\n')[0].isMention(msg.guild) && ( msg.isAdmin() || msg.isOwner() ) ) {

+ 37 - 35
cmds/rcscript.js

@@ -2,6 +2,7 @@ const cheerio = require('cheerio');
 const {limit: {rcgcdw: rcgcdwLimit}, defaultSettings, wikiProjects} = require('../util/default.json');
 const Lang = require('../util/i18n.js');
 const allLangs = Lang.allLangs(true);
+const Wiki = require('../util/wiki.js');
 var db = require('../util/database.js');
 
 const fs = require('fs');
@@ -20,11 +21,11 @@ const display_types = [
 
 /**
  * Processes the "rcscript" command.
- * @param {import('../util/i18n.js')} lang - The user language.
+ * @param {Lang} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {Wiki} wiki - The wiki for the message.
  */
 function cmd_rcscript(lang, msg, args, line, wiki) {
 	if ( args[0] === 'block' && msg.isOwner() ) return blocklist(msg, args.slice(1));
@@ -57,17 +58,17 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 			if ( rows.length >= limit ) return msg.replyMsg( lang.get('rcscript.max_entries'), {}, true );
 
 			var wikiinvalid = lang.get('settings.wikiinvalid') + '\n`' + prefix + 'rcscript add ' + lang.get('rcscript.new_wiki') + '`\n' + lang.get('rcscript.help_wiki');
-			var wikinew = args.slice(1).join(' ').toLowerCase().trim().replace( /^<\s*(.*?)\s*>$/, '$1' );
-			if ( !wikinew ) wikinew = wiki;
-			else {
-				wikinew = input_to_wiki(wikinew.replace( /^(?:https?:)?\/\//, 'https://' ));
+			var input = args.slice(1).join(' ').toLowerCase().trim().replace( /^<\s*(.*?)\s*>$/, '$1' );
+			var wikinew = new Wiki(wiki);
+			if ( input ) {
+				wikinew = input_to_wiki(input.replace( /^(?:https?:)?\/\//, 'https://' ));
 				if ( !wikinew ) return msg.replyMsg( wikiinvalid, {}, 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' ).then( response => {
 				if ( response.statusCode === 404 && typeof response.body === 'string' ) {
 					let api = cheerio.load(response.body)('head link[rel="EditURI"]').prop('href');
 					if ( api ) {
-						wikinew = api.replace( /^(?:https?:)?\/\//, 'https://' ).split('api.php?')[0];
+						wikinew = new Wiki(api.split('api.php?')[0], wikinew);
 						return got.get( wikinew + 'api.php?action=query&meta=allmessages|siteinfo&ammessages=custom-RcGcDw|recentchanges&amenableparser=true&siprop=general&titles=Special:RecentChanges&format=json' );
 					}
 				}
@@ -80,7 +81,7 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 					msg.reactEmoji('nowiki', true);
 					return msg.replyMsg( wikiinvalid, {}, true );
 				}
-				wikinew = body.query.general.server.replace( /^(?:https?:)?\/\//, 'https://' ) + body.query.general.scriptpath + '/';
+				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();
@@ -88,9 +89,9 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 				}
 				if ( body.query.allmessages[0]['*'] !== msg.guild.id ) {
 					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', '', body.query.general) + '>', {}, true );
+					return msg.replyMsg( lang.get('rcscript.sysmessage', 'MediaWiki:Custom-RcGcDw', msg.guild.id) + '\n<' + wikinew.toLink('MediaWiki:Custom-RcGcDw', 'action=edit') + '>', {}, true );
 				}
-				return db.get( 'SELECT reason FROM blocklist WHERE wiki = ?', [wikinew], (blerror, row) => {
+				return db.get( 'SELECT reason FROM blocklist WHERE wiki = ?', [wikinew.href], (blerror, row) => {
 					if ( blerror ) {
 						console.log( '- Error while getting the blocklist: ' + blerror );
 						if ( reaction ) reaction.removeEmoji();
@@ -143,13 +144,13 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 						} ).then( webhook => {
 							console.log( '- Webhook successfully created.' );
 							var webhook_lang = new Lang(( allLangs.map[lang.lang] || allLangs.map[body.query.general.lang] || defaultSettings.lang ), 'rcscript.webhook');
-							webhook.send( webhook_lang.get('created', body.query.general.sitename) + '\n<' + wikinew.toLink(body.query.pages['-1'].title, '', '', body.query.general) + ( wikiid ? '>\n<' + wikinew + 'f' : '' ) + '>' ).catch(log_error);
+							webhook.send( webhook_lang.get('created', body.query.general.sitename) + '\n<' + wikinew.toLink(body.query.pages['-1'].title) + ( wikiid ? '>\n<' + wikinew + 'f' : '' ) + '>' ).catch(log_error);
 							var new_configid = 1;
 							for ( let i of rows.map( row => row.configid ) ) {
 								if ( new_configid === i ) new_configid++;
 								else break;
 							}
-							db.run( 'INSERT INTO rcgcdw(guild, configid, webhook, wiki, lang, display, wikiid) VALUES(?, ?, ?, ?, ?, ?, ?)', [msg.guild.id, new_configid, webhook.id + '/' + webhook.token, wikinew, webhook_lang.lang, ( msg.showEmbed() ? 1 : 0 ), wikiid], function (error) {
+							db.run( 'INSERT INTO rcgcdw(guild, configid, webhook, wiki, lang, display, wikiid) VALUES(?, ?, ?, ?, ?, ?, ?)', [msg.guild.id, new_configid, webhook.id + '/' + webhook.token, wikinew.href, webhook_lang.lang, ( msg.showEmbed() ? 1 : 0 ), wikiid], function (error) {
 								if ( error ) {
 									console.log( '- Error while adding the RcGcDw: ' + error );
 									if ( reaction ) reaction.removeEmoji();
@@ -236,7 +237,7 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 					if ( response.statusCode === 404 && typeof response.body === 'string' ) {
 						let api = cheerio.load(response.body)('head link[rel="EditURI"]').prop('href');
 						if ( api ) {
-							wikinew = api.replace( /^(?:https?:)?\/\//, 'https://' ).split('api.php?')[0];
+							wikinew = new Wiki(api.split('api.php?')[0], wikinew);
 							return got.get( wikinew + 'api.php?action=query&meta=allmessages|siteinfo&ammessages=custom-RcGcDw&amenableparser=true&siprop=general&titles=Special:RecentChanges&format=json' );
 						}
 					}
@@ -249,7 +250,7 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 						msg.reactEmoji('nowiki', true);
 						return msg.replyMsg( wikiinvalid, {}, true );
 					}
-					wikinew = body.query.general.server.replace( /^(?:https?:)?\/\//, 'https://' ) + body.query.general.scriptpath + '/';
+					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();
@@ -257,9 +258,9 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 					}
 					if ( body.query.allmessages[0]['*'] !== msg.guild.id ) {
 						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', '', body.query.general) + '>', {}, true );
+						return msg.replyMsg( lang.get('rcscript.sysmessage', 'MediaWiki:Custom-RcGcDw', msg.guild.id) + '\n<' + wikinew.toLink('MediaWiki:Custom-RcGcDw', 'action=edit') + '>', {}, true );
 					}
-					return db.get( 'SELECT reason FROM blocklist WHERE wiki = ?', [wikinew], (blerror, row) => {
+					return db.get( 'SELECT reason FROM blocklist WHERE wiki = ?', [wikinew.href], (blerror, row) => {
 						if ( blerror ) {
 							console.log( '- Error while getting the blocklist: ' + blerror );
 							if ( reaction ) reaction.removeEmoji();
@@ -307,9 +308,9 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 						 */
 						function updateWiki(wikiid = null) {
 							msg.client.fetchWebhook(...selected_row.webhook.split('/')).then( webhook => {
-								webhook.send( webhook_lang.get('updated_wiki', body.query.general.sitename) + '\n<' + wikinew.toLink(body.query.pages['-1'].title, '', '', body.query.general) + ( wikiid ? '>\n<' + wikinew + 'f' : '' ) + '>' ).catch(log_error);
+								webhook.send( webhook_lang.get('updated_wiki', body.query.general.sitename) + '\n<' + wikinew.toLink(body.query.pages['-1'].title) + ( wikiid ? '>\n<' + wikinew + 'f' : '' ) + '>' ).catch(log_error);
 							}, log_error );
-							db.run( 'UPDATE rcgcdw SET wiki = ?, wikiid = ?, rcid = ?, postid = ? WHERE webhook = ?', [wikinew, wikiid, null, null, selected_row.webhook], function (error) {
+							db.run( 'UPDATE rcgcdw SET wiki = ?, wikiid = ?, rcid = ?, postid = ? WHERE webhook = ?', [wikinew.href, wikiid, null, null, selected_row.webhook], function (error) {
 								if ( error ) {
 									console.log( '- Error while updating the RcGcDw: ' + error );
 									if ( reaction ) reaction.removeEmoji();
@@ -574,7 +575,7 @@ function blocklist(msg, args) {
 		let wiki = input_to_wiki(input.replace( /^(?:https?:)?\/\//, 'https://' ));
 		if ( !wiki ) return msg.replyMsg( '`' + prefix + 'rcscript block add <wiki> [<reason>]`', {}, true );
 		let reason = ( args.slice(2).join(' ').trim() || null );
-		return db.run( 'INSERT INTO blocklist(wiki, reason) VALUES(?, ?)', [wiki, reason], function (error) {
+		return db.run( 'INSERT INTO blocklist(wiki, reason) VALUES(?, ?)', [wiki.href, reason], function (error) {
 			if ( error ) {
 				if ( error.message === 'SQLITE_CONSTRAINT: UNIQUE constraint failed: blocklist.wiki' ) {
 					return msg.replyMsg( '`' + wiki + '` is already on the blocklist.\n`' + prefix + 'rcscript block <' + wiki + '>`', {}, true );
@@ -584,7 +585,7 @@ function blocklist(msg, args) {
 				return error;
 			}
 			console.log( '- Successfully added to the blocklist.' );
-			db.each( 'SELECT webhook, lang FROM rcgcdw WHERE wiki = ?', [wiki], (dberror, row) => {
+			db.each( 'SELECT webhook, lang FROM rcgcdw WHERE wiki = ?', [wiki.href], (dberror, row) => {
 				if ( dberror ) return dberror;
 				msg.client.fetchWebhook(...row.webhook.split('/')).then( webhook => {
 					var lang = new Lang(row.lang, 'rcscript.webhook');
@@ -594,7 +595,7 @@ function blocklist(msg, args) {
 				}, log_error );
 			}, (dberror, count) => {
 				if ( dberror ) console.log( '- Error while deleting the webhooks: ' + dberror );
-				db.run( 'DELETE FROM rcgcdw WHERE wiki = ?', [wiki], function (delerror) {
+				db.run( 'DELETE FROM rcgcdw WHERE wiki = ?', [wiki.href], function (delerror) {
 					if ( delerror ) {
 						console.log( '- Error while removing the webhooks: ' + delerror );
 						msg.replyMsg( 'I added `' + wiki + '` to the blocklist for `' + reason + '` but got an error while removing the webhooks: ' + delerror, {}, true );
@@ -607,10 +608,10 @@ function blocklist(msg, args) {
 		} );
 	}
 	if ( args[0] === 'remove' ) {
-		let wiki = args.slice(1).join(' ').toLowerCase().trim().replace( /^<\s*(.*?)\s*>$/, '$1' );
-		wiki = input_to_wiki(wiki.replace( /^(?:https?:)?\/\//, 'https://' ));
+		let input = args.slice(1).join(' ').toLowerCase().trim().replace( /^<\s*(.*?)\s*>$/, '$1' );
+		let wiki = input_to_wiki(input.replace( /^(?:https?:)?\/\//, 'https://' ));
 		if ( !wiki ) return msg.replyMsg( '`' + prefix + 'rcscript block remove <wiki>`', {}, true );
-		return db.run( 'DELETE FROM blocklist WHERE wiki = ?', [wiki], function (error) {
+		return db.run( 'DELETE FROM blocklist WHERE wiki = ?', [wiki.href], function (error) {
 			if ( error ) {
 				console.log( '- Error while removing from the blocklist: ' + error );
 				msg.replyMsg( 'I got an error while removing from the blocklist: ' + error, {}, true );
@@ -621,10 +622,10 @@ function blocklist(msg, args) {
 		} );
 	}
 	if ( args.length ) {
-		let wiki = args.join(' ').toLowerCase().trim().replace( /^<\s*(.*?)\s*>$/, '$1' );
-		wiki = input_to_wiki(wiki.replace( /^(?:https?:)?\/\//, 'https://' ));
+		let input = args.join(' ').toLowerCase().trim().replace( /^<\s*(.*?)\s*>$/, '$1' );
+		let wiki = input_to_wiki(input.replace( /^(?:https?:)?\/\//, 'https://' ));
 		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.get( 'SELECT reason FROM blocklist WHERE wiki = ?', [wiki], function (error, row) {
+		return db.get( 'SELECT reason FROM blocklist WHERE wiki = ?', [wiki.href], function (error, row) {
 			if ( error ) {
 				console.log( '- Error while checking the blocklist: ' + error );
 				msg.replyMsg( 'I got an error while checking the blocklist: ' + error, {}, true );
@@ -648,31 +649,32 @@ function blocklist(msg, args) {
 /**
  * Turn user input into a wiki.
  * @param {String} input - The user input referring to a wiki.
+ * @returns {Wiki}
  */
 function input_to_wiki(input) {
-	var regex = input.match( /^(?:https:\/\/)?([a-z\d-]{1,50}\.(?:gamepedia\.com|(?:fandom\.com|wikia\.org)(?:(?!\/wiki\/)\/[a-z-]{2,12})?))(?:\/|$)/ );
-	if ( regex ) return 'https://' + regex[1] + '/';
+	var regex = input.match( /^(?:https:\/\/)?([a-z\d-]{1,50}\.(?:gamepedia\.com|(?:fandom\.com|wikia\.org)(?:(?!\/(?:wiki|api)\/)\/[a-z-]{2,12})?))(?:\/|$)/ );
+	if ( regex ) return new Wiki('https://' + regex[1] + '/');
 	if ( input.startsWith( 'https://' ) ) {
 		let project = wikiProjects.find( project => input.split('/')[2].endsWith( project.name ) );
 		if ( project ) {
 			regex = input.match( new RegExp( project.regex + `(?:${project.articlePath}|${project.scriptPath}|/?$)` ) );
-			if ( regex ) return 'https://' + regex[1] + project.scriptPath;
+			if ( regex ) return new Wiki('https://' + regex[1] + project.scriptPath);
 		}
 		let wiki = input.replace( /\/(?:api|load|index)\.php(?:|\?.*)$/, '/' );
 		if ( !wiki.endsWith( '/' ) ) wiki += '/';
-		return wiki;
+		return new Wiki(wiki);
 	}
 	let project = wikiProjects.find( project => input.split('/')[0].endsWith( project.name ) );
 	if ( project ) {
 		regex = input.match( new RegExp( project.regex + `(?:${project.articlePath}|${project.scriptPath}|/?$)` ) );
-		if ( regex ) return 'https://' + regex[1] + project.scriptPath;
+		if ( regex ) return new Wiki('https://' + regex[1] + project.scriptPath);
 	}
 	if ( allSites.some( site => site.wiki_domain === input + '.gamepedia.com' ) ) {
-		return 'https://' + input + '.gamepedia.com/';
+		return new Wiki('https://' + input + '.gamepedia.com/');
 	}
 	if ( /^(?:[a-z-]{2,12}\.)?[a-z\d-]{1,50}$/.test(input) ) {
-		if ( !input.includes( '.' ) ) return 'https://' + input + '.fandom.com/';
-		else return 'https://' + input.split('.')[1] + '.fandom.com/' + input.split('.')[0] + '/';
+		if ( !input.includes( '.' ) ) return new Wiki('https://' + input + '.fandom.com/');
+		else return new Wiki('https://' + input.split('.')[1] + '.fandom.com/' + input.split('.')[0] + '/');
 	}
 	return;
 }

+ 2 - 2
cmds/say.js

@@ -4,7 +4,7 @@
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  */
 function cmd_say(lang, msg, args, line, wiki) {
 	var text = args.join(' ');
@@ -23,7 +23,7 @@ function cmd_say(lang, msg, args, line, wiki) {
 		var allowedMentions = {parse:['users']};
 		if ( msg.member.hasPermission(['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.deleteMsg(), error => {
+		msg.channel.send( text, {allowedMentions,files:imgs} ).then( () => msg.delete().catch(log_error), error => {
 			log_error(error);
 			msg.reactEmoji('error', true);
 		} );

+ 27 - 25
cmds/settings.js

@@ -3,6 +3,7 @@ const {MessageEmbed} = require('discord.js');
 const {defaultSettings, wikiProjects} = require('../util/default.json');
 const Lang = require('../util/i18n.js');
 const allLangs = Lang.allLangs();
+const Wiki = require('../util/wiki.js');
 var db = require('../util/database.js');
 
 var allSites = [];
@@ -15,7 +16,7 @@ getAllSites.then( sites => allSites = sites );
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {Wiki} wiki - The wiki for the message.
  */
 function cmd_settings(lang, msg, args, line, wiki) {
 	if ( !allSites.length ) getAllSites.update();
@@ -91,16 +92,16 @@ function cmd_settings(lang, msg, args, line, wiki) {
 				}
 				return msg.replyMsg( text, {split:true}, true );
 			}
-			if ( wikinew.endsWith( '.gamepedia.com/' ) && !isForced ) {
-				let site = allSites.find( site => site.wiki_domain === wikinew.replace( /^https:\/\/([a-z\d-]{1,50}\.gamepedia\.com)\/$/, '$1' ) );
-				if ( site ) wikinew = 'https://' + ( site.wiki_crossover || site.wiki_domain ) + '/';
+			if ( wikinew.isGamepedia() && !isForced ) {
+				let site = allSites.find( site => site.wiki_domain === wikinew.hostname );
+				if ( site ) wikinew = new Wiki('https://' + ( site.wiki_crossover || site.wiki_domain ) + '/');
 			}
 			return msg.reactEmoji('⏳', true).then( reaction => {
 				got.get( wikinew + 'api.php?&action=query&meta=allmessages|siteinfo&ammessages=custom-GamepediaNotice|custom-FandomMergeNotice&amenableparser=true&siprop=general|extensions&format=json' ).then( response => {
 					if ( !isForced && response.statusCode === 404 && typeof response.body === 'string' ) {
 						let api = cheerio.load(response.body)('head link[rel="EditURI"]').prop('href');
 						if ( api ) {
-							wikinew = api.replace( /^(?:https?:)?\/\//, 'https://' ).split('api.php?')[0];
+							wikinew = new Wiki(api.split('api.php?')[0], wikinew);
 							return got.get( wikinew + 'api.php?action=query&meta=allmessages|siteinfo&ammessages=custom-GamepediaNotice|custom-FandomMergeNotice&amenableparser=true&siprop=general|extensions&format=json' );
 						}
 					}
@@ -113,10 +114,10 @@ function cmd_settings(lang, msg, args, line, wiki) {
 						msg.reactEmoji('nowiki', true);
 						return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {}, true );
 					}
-					if ( !isForced ) wikinew = body.query.general.server.replace( /^(?:https?:)?\/\//, 'https://' ) + body.query.general.scriptpath + '/';
-					if ( wikinew.endsWith( '.gamepedia.com/' ) && !isForced ) {
-						let site = allSites.find( site => site.wiki_domain === wikinew );
-						if ( site ) wikinew = 'https://' + ( site.wiki_crossover || site.wiki_domain ) + '/';
+					if ( !isForced ) wikinew.updateWiki(body.query.general);
+					if ( wikinew.isGamepedia() && !isForced ) {
+						let site = allSites.find( site => site.wiki_domain === wikinew.hostname );
+						if ( site ) wikinew = new Wiki('https://' + ( site.wiki_crossover || site.wiki_domain ) + '/');
 					}
 					else if ( wikinew.isFandom() && !isForced ) {
 						let crossover = '';
@@ -127,10 +128,10 @@ function cmd_settings(lang, msg, args, line, wiki) {
 							let merge = body.query.allmessages[1]['*'].split('/');
 							crossover = 'https://' + merge[0] + '.fandom.com/' + ( merge[1] ? merge[1] + '/' : '' );
 						}
-						if ( crossover ) wikinew = crossover;
+						if ( crossover ) wikinew = new Wiki(crossover);
 					}
 					var embed;
-					if ( !wikinew.isFandom() && !wikinew.endsWith( '.gamepedia.com/' ) ) {
+					if ( !wikinew.isFandom() && !wikinew.isGamepedia() ) {
 						var notice = [];
 						if ( body.query.general.generator.replace( /^MediaWiki 1\.(\d\d).*$/, '$1' ) <= 30 ) {
 							console.log( '- This wiki is using ' + body.query.general.generator + '.' );
@@ -158,7 +159,7 @@ function cmd_settings(lang, msg, args, line, wiki) {
 						}
 					}
 					var sql = 'UPDATE discord SET wiki = ? WHERE guild = ? AND wiki = ?';
-					var sqlargs = [wikinew, msg.guild.id, guild.wiki];
+					var sqlargs = [wikinew.href, msg.guild.id, guild.wiki];
 					if ( !rows.length ) {
 						sql = 'INSERT INTO discord(wiki, guild) VALUES(?, ?)';
 						sqlargs.pop();
@@ -167,7 +168,7 @@ function cmd_settings(lang, msg, args, line, wiki) {
 						sql = 'UPDATE discord SET wiki = ? WHERE guild = ? AND channel = ?';
 						sqlargs[2] = msg.channel.id;
 						if ( !rows.includes( channel ) ) {
-							if ( channel.wiki === wikinew ) {
+							if ( channel.wiki === wikinew.href ) {
 								if ( reaction ) reaction.removeEmoji();
 								return msg.replyMsg( lang.get('settings.' + prelang + 'changed') + ' ' + channel.wiki + wikihelp, {embed}, true );
 							}
@@ -183,14 +184,14 @@ function cmd_settings(lang, msg, args, line, wiki) {
 							return dberror;
 						}
 						console.log( '- Settings successfully updated.' );
-						if ( channel ) channel.wiki = wikinew;
+						if ( channel ) channel.wiki = wikinew.href;
 						else {
 							rows.forEach( row => {
-								if ( row.channel && row.wiki === guild.wiki ) row.wiki = wikinew;
+								if ( row.channel && row.wiki === guild.wiki ) row.wiki = wikinew.href;
 							} );
-							guild.wiki = wikinew;
+							guild.wiki = wikinew.href;
 						}
-						if ( channel || !rows.some( row => row.channel === msg.channel.id ) ) wiki = wikinew;
+						if ( channel || !rows.some( row => row.channel === msg.channel.id ) ) wiki = new Wiki(wikinew);
 						if ( reaction ) reaction.removeEmoji();
 						msg.replyMsg( lang.get('settings.' + prelang + 'changed') + ' ' + wikinew + wikihelp, {embed}, true );
 						var channels = rows.filter( row => row.channel && row.lang === guild.lang && row.wiki === guild.wiki && row.prefix === guild.prefix && row.inline === guild.inline ).map( row => row.channel );
@@ -353,31 +354,32 @@ function cmd_settings(lang, msg, args, line, wiki) {
 /**
  * Turn user input into a wiki.
  * @param {String} input - The user input referring to a wiki.
+ * @returns {Wiki}
  */
 function input_to_wiki(input) {
-	var regex = input.match( /^(?:https:\/\/)?([a-z\d-]{1,50}\.(?:gamepedia\.com|(?:fandom\.com|wikia\.org)(?:(?!\/wiki\/)\/[a-z-]{2,12})?))(?:\/|$)/ );
-	if ( regex ) return 'https://' + regex[1] + '/';
+	var regex = input.match( /^(?:https:\/\/)?([a-z\d-]{1,50}\.(?:gamepedia\.com|(?:fandom\.com|wikia\.org)(?:(?!\/(?:wiki|api)\/)\/[a-z-]{2,12})?))(?:\/|$)/ );
+	if ( regex ) return new Wiki('https://' + regex[1] + '/');
 	if ( input.startsWith( 'https://' ) ) {
 		let project = wikiProjects.find( project => input.split('/')[2].endsWith( project.name ) );
 		if ( project ) {
 			regex = input.match( new RegExp( project.regex + `(?:${project.articlePath}|${project.scriptPath}|/?$)` ) );
-			if ( regex ) return 'https://' + regex[1] + project.scriptPath;
+			if ( regex ) return new Wiki('https://' + regex[1] + project.scriptPath);
 		}
 		let wiki = input.replace( /\/(?:api|load|index)\.php(?:|\?.*)$/, '/' );
 		if ( !wiki.endsWith( '/' ) ) wiki += '/';
-		return wiki;
+		return new Wiki(wiki);
 	}
 	let project = wikiProjects.find( project => input.split('/')[0].endsWith( project.name ) );
 	if ( project ) {
 		regex = input.match( new RegExp( project.regex + `(?:${project.articlePath}|${project.scriptPath}|/?$)` ) );
-		if ( regex ) return 'https://' + regex[1] + project.scriptPath;
+		if ( regex ) return new Wiki('https://' + regex[1] + project.scriptPath);
 	}
 	if ( allSites.some( site => site.wiki_domain === input + '.gamepedia.com' ) ) {
-		return 'https://' + input + '.gamepedia.com/';
+		return new Wiki('https://' + input + '.gamepedia.com/');
 	}
 	if ( /^(?:[a-z-]{2,12}\.)?[a-z\d-]{1,50}$/.test(input) ) {
-		if ( !input.includes( '.' ) ) return 'https://' + input + '.fandom.com/';
-		else return 'https://' + input.split('.')[1] + '.fandom.com/' + input.split('.')[0] + '/';
+		if ( !input.includes( '.' ) ) return new Wiki('https://' + input + '.fandom.com/');
+		else return new Wiki('https://' + input.split('.')[1] + '.fandom.com/' + input.split('.')[0] + '/');
 	}
 	return;
 }

+ 1 - 1
cmds/stop.js

@@ -4,7 +4,7 @@
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  * @async
  */
 async function cmd_stop(lang, msg, args, line, wiki) {

+ 4 - 3
cmds/test.js

@@ -19,7 +19,7 @@ const wsStatus = [
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  */
 function cmd_test(lang, msg, args, line, wiki) {
 	if ( args.join('') ) {
@@ -39,6 +39,7 @@ function cmd_test(lang, msg, args, line, wiki) {
 				then = Date.now();
 				var body = response.body;
 				if ( body && body.warnings ) log_warn(body.warnings);
+				if ( body?.query?.general ) wiki.updateWiki(body.query.general);
 				var ping = ( then - now ) + 'ms';
 				var notice = [];
 				if ( response.statusCode !== 200 || !body?.query?.general || !body?.query?.extensions ) {
@@ -51,7 +52,7 @@ function cmd_test(lang, msg, args, line, wiki) {
 						ping += ' <:error:505887261200613376>';
 					}
 				}
-				else if ( ( msg.isAdmin() || msg.isOwner() ) && !wiki.isFandom() ) {
+				else if ( ( msg.isAdmin() || msg.isOwner() ) && !wiki.isFandom() && !wiki.isGamepedia() ) {
 					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));
@@ -65,7 +66,7 @@ function cmd_test(lang, msg, args, line, wiki) {
 						notice.push(lang.get('test.PageImages', '[PageImages](https://www.mediawiki.org/wiki/Extension:PageImages)'));
 					}
 				}
-				embed.addField( wiki.toLink( '', '', '', body?.query?.general ), ping );
+				embed.addField( wiki, ping );
 				if ( notice.length ) embed.addField( lang.get('test.notice'), notice.join('\n') );
 			}, error => {
 				then = Date.now();

+ 3 - 3
cmds/verification.js

@@ -7,7 +7,7 @@ var db = require('../util/database.js');
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  */
 function cmd_verification(lang, msg, args, line, wiki) {
 	if ( !msg.isAdmin() ) {
@@ -178,7 +178,7 @@ function cmd_verification(lang, msg, args, line, wiki) {
 						if ( wiki.noWiki(response.url) || response.statusCode === 410 ) console.log( '- This wiki doesn\'t exist!' );
 						else console.log( '- ' + response.statusCode + ': Error while getting the usergroups: ' + ( body && body.error && body.error.info ) );
 					}
-					var groups = body.query.allmessages.filter( group => !/\.(?:css|js)$/.test(group.name) && group.name !== 'group-all' ).map( group => {
+					var groups = body.query.allmessages.filter( group => !['group-all','group-membership-link-with-expiry'].includes( group.name ) && !/\.(?:css|js)$/.test(group.name) ).map( group => {
 						return {
 							name: group.name.replace( /^group-/, '' ).replace( /-member$/, '' ),
 							content: group['*'].replace( / /g, '_' ).toLowerCase()
@@ -187,7 +187,7 @@ function cmd_verification(lang, msg, args, line, wiki) {
 					usergroups = usergroups.map( usergroup => {
 						if ( groups.some( group => group.name === usergroup ) ) return usergroup;
 						if ( groups.some( group => group.content === usergroup ) ) return groups.find( group => group.content === usergroup ).name;
-						if ( /^admins?$/.test(usergroup) ) return 'sysop'
+						if ( /^admins?$/.test(usergroup) ) return 'sysop';
 						return usergroup;
 					} );
 				}, error => {

+ 14 - 12
cmds/verify.js

@@ -2,6 +2,7 @@ const htmlparser = require('htmlparser2');
 const cheerio = require('cheerio');
 const {MessageEmbed} = require('discord.js');
 const {timeoptions} = require('../util/default.json');
+const toTitle = require('../util/wiki.js').toTitle;
 var db = require('../util/database.js');
 
 /**
@@ -10,7 +11,7 @@ var db = require('../util/database.js');
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  * @param {String} [old_username] - The username before the search.
  */
 function cmd_verify(lang, msg, args, line, wiki, old_username = '') {
@@ -27,7 +28,7 @@ function cmd_verify(lang, msg, args, line, wiki, old_username = '') {
 	if ( /^(?:https?:)?\/\/([a-z\d-]{1,50})\.(?:gamepedia\.com\/|(?:fandom\.com|wikia\.org)\/(?:[a-z-]{1,8}\/)?wiki\/)/.test(username) ) {
 		username = decodeURIComponent( username.replace( /^(?:https?:)?\/\/([a-z\d-]{1,50})\.(?:gamepedia\.com\/|(?:fandom\.com|wikia\.org)\/(?:[a-z-]{1,8}\/)?wiki\/)/, '' ) );
 	}
-	if ( wiki.endsWith( '.gamepedia.com/' ) ) username = username.replace( /^userprofile\s*:/i, '' );
+	if ( wiki.isGamepedia() ) username = username.replace( /^userprofile\s*:/i, '' );
 	
 	var embed = new MessageEmbed().setFooter( lang.get('verify.footer') ).setTimestamp();
 	db.all( 'SELECT role, editcount, usergroup, accountage, rename FROM verification WHERE guild = ? AND channel LIKE ? ORDER BY configid ASC', [msg.guild.id, '%|' + msg.channel.id + '|%'], (dberror, rows) => {
@@ -37,7 +38,7 @@ function cmd_verify(lang, msg, args, line, wiki, old_username = '') {
 			msg.replyMsg( lang.get('verify.error_reply'), {embed}, false, false ).then( message => message.reactEmoji('error') );
 			return dberror;
 		}
-		if ( !rows.length ) return msg.replyMsg( lang.get('verify.missing') );
+		if ( !rows.length ) return msg.replyMsg( lang.get('verify.missing') + ( msg.isAdmin() ? '\n`' + ( patreons[msg.guild.id] || process.env.prefix ) + lang.localNames.verification + '`' : '' ) );
 		
 		if ( !username.trim() ) {
 			args[0] = line.split(' ')[0];
@@ -65,12 +66,13 @@ function cmd_verify(lang, msg, args, line, wiki, old_username = '') {
 				if ( reaction ) reaction.removeEmoji();
 				return;
 			}
+			wiki.updateWiki(body.query.general);
 			var queryuser = body.query.users[0];
 			embed.setAuthor( body.query.general.sitename );
 			if ( body.query.users.length !== 1 || queryuser.missing !== undefined || queryuser.invalid !== undefined ) {
 				username = ( body.query.users.length === 1 ? queryuser.name : username );
 				embed.setTitle( ( old_username || username ).escapeFormatting() ).setColor('#0000FF').setDescription( lang.get('verify.user_missing', ( old_username || username ).escapeFormatting()) );
-				if ( ( wiki.isFandom() || wiki.endsWith( '.gamepedia.com/' ) ) && !old_username ) return got.get( 'https://community.fandom.com/api/v1/User/UsersByName?limit=1&query=' + encodeURIComponent( username ) + '&format=json' ).then( wsresponse => {
+				if ( ( wiki.isFandom() || wiki.isGamepedia() ) && !old_username ) return got.get( 'https://community.fandom.com/api/v1/User/UsersByName?limit=1&query=' + encodeURIComponent( username ) + '&format=json' ).then( wsresponse => {
 					var wsbody = wsresponse.body;
 					if ( wsresponse.statusCode !== 200 || wsbody?.exception || wsbody?.users?.[0]?.name?.length !== username.length ) {
 						if ( !wsbody?.users ) console.log( '- ' + wsresponse.statusCode + ': Error while searching the user: ' + wsbody?.exception?.details );
@@ -92,7 +94,7 @@ function cmd_verify(lang, msg, args, line, wiki, old_username = '') {
 				return;
 			}
 			username = queryuser.name;
-			var pagelink = wiki.toLink('User:' + username, '', '', body.query.general, true);
+			var pagelink = wiki.toLink('User:' + username, '', '', true);
 			embed.setTitle( username.escapeFormatting() ).setURL( pagelink );
 			if ( queryuser.blockexpiry ) {
 				embed.setColor('#FF0000').setDescription( lang.get('verify.user_blocked', '[' + username.escapeFormatting() + '](' + pagelink + ')', queryuser.gender) );
@@ -104,7 +106,7 @@ function cmd_verify(lang, msg, args, line, wiki, old_username = '') {
 			
 			var comment = [];
 			var url = '';
-			if ( wiki.endsWith( '.gamepedia.com/' ) ) {
+			if ( wiki.isGamepedia() ) {
 				url = 'https://help.gamepedia.com/Special:GlobalBlockList/' + encodeURIComponent( username ) + '?uselang=qqx&cache=' + Date.now();
 			}
 			else if ( wiki.isFandom() ) {
@@ -119,7 +121,7 @@ function cmd_verify(lang, msg, args, line, wiki, old_username = '') {
 				}
 				else {
 					let $ = cheerio.load(gbresponse.body);
-					if ( wiki.endsWith( '.gamepedia.com/' ) ) {
+					if ( wiki.isGamepedia() ) {
 						if ( $('.mw-blocklist').length ) {
 							return Promise.reject({
 								desc: lang.get('verify.user_gblocked', '[' + username.escapeFormatting() + '](' + pagelink + ')', queryuser.gender),
@@ -147,7 +149,7 @@ function cmd_verify(lang, msg, args, line, wiki, old_username = '') {
 				comment.push(lang.get('verify.failed_gblock'));
 			} ).then( async () => {
 				// async check for editcount on Gamepedia, workaround for https://gitlab.com/hydrawiki/hydra/-/issues/5054
-				if ( wiki.endsWith( '.gamepedia.com/' ) ) {
+				if ( wiki.isGamepedia() || ( wiki.isFandom() && body.query.general.generator.startsWith( 'MediaWiki 1.3' ) ) ) {
 					try {
 						let ucresponse = await got.get( wiki + 'api.php?action=query&list=usercontribs&ucprop=&uclimit=500&ucuser=' + encodeURIComponent( username ) + '&format=json' );
 						if ( !ucresponse.body.continue ) queryuser.editcount = ucresponse.body.query.usercontribs.length;
@@ -157,7 +159,7 @@ function cmd_verify(lang, msg, args, line, wiki, old_username = '') {
 				}
 				
 				var options = {};
-				if ( wiki.endsWith( '.gamepedia.com/' ) ) {
+				if ( wiki.isGamepedia() ) {
 					url = wiki + 'api.php?action=profile&do=getPublicProfile&user_name=' + encodeURIComponent( username ) + '&format=json&cache=' + Date.now();
 				}
 				else if ( wiki.isFandom() ) {
@@ -185,8 +187,8 @@ function cmd_verify(lang, msg, args, line, wiki, old_username = '') {
 					if ( msg.author.tag.escapeFormatting() !== discordname ) {
 						embed.setColor('#FFFF00').setDescription( lang.get('verify.user_failed', msg.member.toString(), '[' + username.escapeFormatting() + '](' + pagelink + ')', queryuser.gender) );
 						var help_link = '';
-						if ( wiki.endsWith( '.gamepedia.com/' ) ) help_link = lang.get('verify.help_gamepedia') + '?c=' + ( msg.guild.id in patreons && patreons[msg.guild.id] !== process.env.prefix ? encodeURIComponent( patreons[msg.guild.id] + ' verify' ) : 'wb' ) + ( msg.channel.name !== 'verification' ? '&ch=' + encodeURIComponent( msg.channel.name ) : '' ) + '&user=' + username.toTitle(true, true) + '&discord=' + encodeURIComponent( msg.author.username ) + '&tag=' + msg.author.discriminator;
-						else if ( wiki.isFandom() ) help_link = lang.get('verify.help_fandom') + '/' + username.toTitle(true) + '?c=' + ( msg.guild.id in patreons && patreons[msg.guild.id] !== process.env.prefix ? encodeURIComponent( patreons[msg.guild.id] + ' verify' ) : 'wb' ) + ( msg.channel.name !== 'verification' ? '&ch=' + encodeURIComponent( msg.channel.name ) : '' ) + '&user=' + encodeURIComponent( msg.author.username ) + '&tag=' + msg.author.discriminator;
+						if ( wiki.isGamepedia() ) help_link = lang.get('verify.help_gamepedia') + '?c=' + ( msg.guild.id in patreons && patreons[msg.guild.id] !== process.env.prefix ? encodeURIComponent( patreons[msg.guild.id] + ' verify' ) : 'wb' ) + ( msg.channel.name !== 'verification' ? '&ch=' + encodeURIComponent( msg.channel.name ) : '' ) + '&user=' + toTitle(username) + '&discord=' + encodeURIComponent( msg.author.username ) + '&tag=' + msg.author.discriminator;
+						else if ( wiki.isFandom() ) help_link = lang.get('verify.help_fandom') + '/' + toTitle(username) + '?c=' + ( msg.guild.id in patreons && patreons[msg.guild.id] !== process.env.prefix ? encodeURIComponent( patreons[msg.guild.id] + ' verify' ) : 'wb' ) + ( msg.channel.name !== 'verification' ? '&ch=' + encodeURIComponent( msg.channel.name ) : '' ) + '&user=' + encodeURIComponent( msg.author.username ) + '&tag=' + msg.author.discriminator;
 						if ( help_link.length ) embed.addField( lang.get('verify.notice'), lang.get('verify.help_guide', help_link, queryuser.gender) + '\n' + help_link );
 						msg.replyMsg( lang.get('verify.user_failed_reply', username.escapeFormatting(), queryuser.gender), {embed}, false, false );
 						
@@ -293,7 +295,7 @@ function cmd_verify(lang, msg, args, line, wiki, old_username = '') {
 				embed.addField( lang.get('verify.discord', ( msg.author.tag.escapeFormatting() === discordname ? queryuser.gender : 'unknown' )), msg.author.tag.escapeFormatting(), true ).addField( lang.get('verify.wiki', queryuser.gender), ( discordname || lang.get('verify.empty') ), true );
 				if ( msg.author.tag.escapeFormatting() !== discordname ) {
 					embed.setColor('#FFFF00').setDescription( lang.get('verify.user_failed', msg.member.toString(), '[' + username.escapeFormatting() + '](' + pagelink + ')', queryuser.gender) );
-					embed.addField( lang.get('verify.notice'), lang.get('verify.help_subpage', '**`' + msg.author.tag + '`**', queryuser.gender) + '\n' + wiki.toLink('Special:MyPage/Discord', 'action=edit', '', body.query.general) );
+					embed.addField( lang.get('verify.notice'), lang.get('verify.help_subpage', '**`' + msg.author.tag + '`**', queryuser.gender) + '\n' + wiki.toLink('Special:MyPage/Discord', 'action=edit') );
 					msg.replyMsg( lang.get('verify.user_failed_reply', username.escapeFormatting(), queryuser.gender), {embed}, false, false );
 					
 					if ( reaction ) reaction.removeEmoji();

+ 1 - 1
cmds/voice.js

@@ -7,7 +7,7 @@ var db = require('../util/database.js');
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
  * @param {String} line - The command as plain text.
- * @param {String} wiki - The wiki for the message.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  */
 function cmd_voice(lang, msg, args, line, wiki) {
 	if ( msg.isAdmin() ) {

+ 413 - 405
cmds/wiki/fandom.js

@@ -1,6 +1,7 @@
 const htmlparser = require('htmlparser2');
 const {MessageEmbed} = require('discord.js');
 const {limit: {interwiki: interwikiLimit}, wikiProjects} = require('../../util/default.json');
+const Wiki = require('../../util/wiki.js');
 
 const fs = require('fs');
 var fn = {
@@ -20,16 +21,16 @@ fs.readdir( './cmds/wiki/fandom', (error, files) => {
  * @param {import('../../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String} title - The page title.
- * @param {String} wiki - The wiki for the page.
+ * @param {Wiki} wiki - The wiki for the page.
  * @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 {String} [querystring] - The querystring for the link.
+ * @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 fandom_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '', querystring = '', fragment = '', interwiki = '', selfcall = 0) {
+function fandom_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '', querystring = new URLSearchParams(), fragment = '', interwiki = '', selfcall = 0) {
 	var full_title = title;
 	if ( title.includes( '#' ) ) {
 		fragment = title.split('#').slice(1).join('#');
@@ -37,7 +38,7 @@ function fandom_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '',
 	}
 	if ( /\?\w+=/.test(title) ) {
 		var querystart = title.search(/\?\w+=/);
-		querystring = title.substring(querystart + 1) + ( querystring ? '&' + querystring : '' );
+		querystring = new URLSearchParams(querystring + '&' + title.substring(querystart + 1));
 		title = title.substring(0, querystart);
 	}
 	if ( title.length > 250 ) {
@@ -48,457 +49,464 @@ function fandom_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '',
 	var aliasInvoke = ( lang.aliases[invoke] || invoke );
 	var args = title.split(' ').slice(1);
 	
-	if ( aliasInvoke === 'random' && !args.join('') && !querystring && !fragment ) fn.random(lang, msg, wiki, reaction, spoiler);
-	else if ( aliasInvoke === 'overview' && !args.join('') && !querystring && !fragment ) fn.overview(lang, msg, wiki, reaction, spoiler);
-	else if ( aliasInvoke === 'test' && !args.join('') && !querystring && !fragment ) {
+	if ( aliasInvoke === 'random' && !args.join('') && !querystring.toString() && !fragment ) {
+		return fn.random(lang, msg, wiki, reaction, spoiler);
+	}
+	if ( aliasInvoke === 'overview' && !args.join('') && !querystring.toString() && !fragment ) {
+		return fn.overview(lang, msg, wiki, reaction, spoiler);
+	}
+	if ( aliasInvoke === 'test' && !args.join('') && !querystring.toString() && !fragment ) {
 		this.test(lang, msg, args, '', wiki);
 		if ( reaction ) reaction.removeEmoji();
+		return;
 	}
-	else if ( aliasInvoke === 'page' ) {
-		msg.sendChannel( spoiler + '<' + wiki.toLink(args.join('_'), querystring.toTitle(), fragment) + '>' + spoiler );
+	if ( aliasInvoke === 'page' ) {
+		msg.sendChannel( spoiler + '<' + wiki.toLink(args.join('_'), querystring, fragment) + '>' + spoiler );
 		if ( reaction ) reaction.removeEmoji();
+		return;
 	}
-	else if ( aliasInvoke === 'diff' && args.join('') && !querystring && !fragment ) fn.diff(lang, msg, args, wiki, reaction, spoiler);
-	else {
-		var noRedirect = ( /(?:^|&)redirect=no(?:&|$)/.test(querystring) || /(?:^|&)action=(?!view(?:&|$))/.test(querystring) );
-		got.get( wiki + 'api.php?action=query&meta=allmessages|siteinfo&ammessages=description&amenableparser=true&siprop=general|namespaces|specialpagealiases|wikidesc&iwurl=true' + ( noRedirect ? '' : '&redirects=true' ) + '&prop=imageinfo|categoryinfo&titles=' + encodeURIComponent( title.replace( /\|/g, '\ufffd' ) ) + '&format=json' ).then( response => {
-			var body = response.body;
-			if ( body && body.warnings ) log_warn(body.warnings);
-			if ( response.statusCode !== 200 || !body || !body.query ) {
-				if ( interwiki ) msg.sendChannel( spoiler + ' ' + interwiki.replace( /@(here|everyone)/g, '%40$1' ) + fragment + ' ' + spoiler );
-				else if ( wiki.noWiki(response.url) || response.statusCode === 410 ) {
-					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(( querystring || fragment || !title ? title : 'Special:Search' ), ( querystring || fragment || !title ? querystring.toTitle() : 'search=' + title.toSearch() ), fragment) + '>' + spoiler );
-				}
-				
-				if ( reaction ) reaction.removeEmoji();
+	if ( aliasInvoke === 'diff' && args.join('') && !querystring.toString() && !fragment ) {
+		return fn.diff(lang, msg, args, wiki, reaction, spoiler);
+	}
+	var noRedirect = ( querystring.getAll('redirect').pop() === 'no' || ( querystring.has('action') && querystring.getAll('action').pop() !== 'view' ) );
+	got.get( wiki + 'api.php?action=query&meta=allmessages|siteinfo&ammessages=description&amenableparser=true&siprop=general|namespaces|specialpagealiases|wikidesc&iwurl=true' + ( noRedirect ? '' : '&redirects=true' ) + '&prop=imageinfo|categoryinfo&converttitles=true&titles=' + encodeURIComponent( title.replace( /\|/g, '\ufffd' ) ) + '&format=json' ).then( response => {
+		var body = response.body;
+		if ( body && body.warnings ) log_warn(body.warnings);
+		if ( response.statusCode !== 200 || !body || !body.query ) {
+			if ( interwiki ) msg.sendChannel( spoiler + ' ' + interwiki + ' ' + spoiler );
+			else if ( wiki.noWiki(response.url) || response.statusCode === 410 ) {
+				console.log( '- This wiki doesn\'t exist!' );
+				msg.reactEmoji('nowiki');
 			}
-			else if ( body.query.general.generator.startsWith( 'MediaWiki 1.3' ) ) {
-				return this.gamepedia(lang, msg, title, wiki, cmd, reaction, spoiler, querystring, fragment, selfcall);
+			else {
+				console.log( '- ' + response.statusCode + ': Error while getting the search results: ' + ( body && body.error && body.error.info ) );
+				msg.sendChannelError( spoiler + '<' + wiki.toLink(( querystring.toString() || fragment || !title ? title : 'Special:Search' ), ( querystring.toString() || fragment || !title ? querystring : {search:title} ), fragment) + '>' + spoiler );
 			}
-			else if ( aliasInvoke === 'search' ) {
-				fn.search(lang, msg, full_title.split(' ').slice(1).join(' '), wiki, body.query, reaction, spoiler);
+			
+			if ( reaction ) reaction.removeEmoji();
+			return;
+		}
+		wiki.updateWiki(body.query.general);
+		if ( body.query.general.generator.startsWith( 'MediaWiki 1.3' ) ) {
+			return this.gamepedia(lang, msg, title, wiki, cmd, reaction, spoiler, querystring, fragment, selfcall);
+		}
+		if ( aliasInvoke === 'search' ) {
+			return fn.search(lang, msg, full_title.split(' ').slice(1).join(' '), wiki, body.query, reaction, spoiler);
+		}
+		if ( aliasInvoke === 'discussion' && !querystring.toString() && !fragment ) {
+			return fn.discussion(lang, msg, wiki, args.join(' '), body.query, reaction, spoiler);
+		}
+		if ( body.query.pages ) {
+			var querypages = Object.values(body.query.pages);
+			var querypage = querypages[0];
+			if ( body.query.redirects && body.query.redirects[0].from.split(':')[0] === body.query.namespaces['-1']['*'] && body.query.specialpagealiases.filter( sp => ['Mypage','Mytalk','MyLanguage'].includes( sp.realname ) ).map( sp => sp.aliases[0] ).includes( body.query.redirects[0].from.split(':').slice(1).join(':').split('/')[0].replace( / /g, '_' ) ) ) {
+				querypage.title = body.query.redirects[0].from;
+				delete body.query.redirects[0].tofragment;
+				delete querypage.missing;
+				querypage.ns = -1;
+				querypage.special = '';
 			}
-			else if ( aliasInvoke === 'discussion' && !querystring && !fragment ) {
-				fn.discussion(lang, msg, wiki, args.join(' '), body.query, reaction, spoiler);
+			
+			var contribs = body.query.namespaces['-1']['*'] + ':' + body.query.specialpagealiases.find( sp => sp.realname === 'Contributions' ).aliases[0] + '/';
+			if ( querypage.ns === 2 && ( !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;
+				fn.user(lang, msg, userparts[0] + ':', userparts.slice(1).join(':'), wiki, querystring, fragment, querypage, contribs, reaction, spoiler);
 			}
-			else {
-				if ( body.query.pages ) {
-					var querypages = Object.values(body.query.pages);
-					var querypage = querypages[0];
-					if ( body.query.redirects && body.query.redirects[0].from.split(':')[0] === body.query.namespaces['-1']['*'] && body.query.specialpagealiases.filter( sp => ['Mypage','Mytalk','MyLanguage'].includes( sp.realname ) ).map( sp => sp.aliases[0] ).includes( body.query.redirects[0].from.split(':').slice(1).join(':').split('/')[0].replace( / /g, '_' ) ) ) {
-						querypage.title = body.query.redirects[0].from;
-						delete body.query.redirects[0].tofragment;
-						delete querypage.missing;
-						querypage.ns = -1;
-						querypage.special = '';
+			else if ( querypage.ns === -1 && querypage.title.startsWith( contribs ) && querypage.title.length > contribs.length ) {
+				var username = querypage.title.split('/').slice(1).join('/');
+				got.get( wiki + 'api.php?action=query&titles=User:' + encodeURIComponent( username ) + '&format=json' ).then( uresponse => {
+					var ubody = uresponse.body;
+					if ( uresponse.statusCode !== 200 || !ubody || !ubody.query ) {
+						console.log( '- ' + uresponse.statusCode + ': Error while getting the user: ' + ( ubody && ubody.error && ubody.error.info ) );
+						msg.sendChannelError( spoiler + '<' + wiki.toLink(contribs + username, querystring, fragment) + '>' + spoiler );
+						
+						if ( reaction ) reaction.removeEmoji();
+					}
+					else {
+						querypage = Object.values(ubody.query.pages)[0];
+						if ( querypage.ns === 2 ) {
+							username = querypage.title.split(':').slice(1).join(':');
+							querypage.title = contribs + username;
+							delete querypage.missing;
+							querypage.ns = -1;
+							querypage.special = '';
+							querypage.noRedirect = noRedirect;
+							fn.user(lang, msg, contribs, username, wiki, querystring, fragment, querypage, contribs, reaction, spoiler);
+						}
+						else {
+							msg.reactEmoji('error');
+							
+							if ( reaction ) reaction.removeEmoji();
+						}
 					}
+				}, error => {
+					console.log( '- Error while getting the user: ' + error );
+					msg.sendChannelError( spoiler + '<' + wiki.toLink(contribs + username, querystring, fragment) + '>' + spoiler );
 					
-					var contribs = body.query.namespaces['-1']['*'] + ':' + body.query.specialpagealiases.find( sp => sp.realname === 'Contributions' ).aliases[0] + '/';
-					if ( querypage.ns === 2 && ( !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;
-						fn.user(lang, msg, userparts[0].toTitle() + ':', userparts.slice(1).join(':'), wiki, querystring, fragment, querypage, contribs.toTitle(), reaction, spoiler);
+					if ( reaction ) reaction.removeEmoji();
+				} );
+			}
+			else if ( querypage.ns === 1201 && querypage.missing !== undefined ) {
+				var thread = querypage.title.split(':');
+				got.get( wiki + 'api.php?action=query&prop=revisions&rvprop=user&rvdir=newer&rvlimit=1&pageids=' + thread.slice(1).join(':') + '&format=json' ).then( thresponse => {
+					var thbody = thresponse.body;
+					if ( thresponse.statusCode !== 200 || !thbody || !thbody.query || !thbody.query.pages ) {
+						console.log( '- ' + thresponse.statusCode + ': Error while getting the thread: ' + ( thbody && thbody.error && thbody.error.info ) );
+						msg.sendChannelError( spoiler + '<' + wiki.toLink(querypage.title, querystring, fragment) + '>' + spoiler );
+						
+						if ( reaction ) reaction.removeEmoji();
 					}
-					else if ( querypage.ns === -1 && querypage.title.startsWith( contribs ) && querypage.title.length > contribs.length ) {
-						var username = querypage.title.split('/').slice(1).join('/');
-						got.get( wiki + 'api.php?action=query&titles=User:' + encodeURIComponent( username ) + '&format=json' ).then( uresponse => {
-							var ubody = uresponse.body;
-							if ( uresponse.statusCode !== 200 || !ubody || !ubody.query ) {
-								console.log( '- ' + uresponse.statusCode + ': Error while getting the user: ' + ( ubody && ubody.error && ubody.error.info ) );
-								msg.sendChannelError( spoiler + '<' + wiki.toLink(contribs + username, querystring.toTitle(), fragment, body.query.general) + '>' + spoiler );
-								
-								if ( reaction ) reaction.removeEmoji();
-							}
-							else {
-								querypage = Object.values(ubody.query.pages)[0];
-								if ( querypage.ns === 2 ) {
-									username = querypage.title.split(':').slice(1).join(':');
-									querypage.title = contribs + username;
-									delete querypage.missing;
-									querypage.ns = -1;
-									querypage.special = '';
-									querypage.noRedirect = noRedirect;
-									fn.user(lang, msg, contribs.toTitle(), username, wiki, querystring, fragment, querypage, contribs.toTitle(), reaction, spoiler);
-								}
-								else {
-									msg.reactEmoji('error');
-									
-									if ( reaction ) reaction.removeEmoji();
-								}
-							}
-						}, error => {
-							console.log( '- Error while getting the user: ' + error );
-							msg.sendChannelError( spoiler + '<' + wiki.toLink(contribs + username, querystring.toTitle(), fragment, body.query.general) + '>' + spoiler );
+					else {
+						querypage = thbody.query.pages[thread.slice(1).join(':')];
+						if ( querypage.missing !== undefined ) {
+							msg.reactEmoji('🤷');
 							
 							if ( reaction ) reaction.removeEmoji();
-						} );
-					}
-					else if ( querypage.ns === 1201 && querypage.missing !== undefined ) {
-						var thread = querypage.title.split(':');
-						got.get( wiki + 'api.php?action=query&prop=revisions&rvprop=user&rvdir=newer&rvlimit=1&pageids=' + thread.slice(1).join(':') + '&format=json' ).then( thresponse => {
-							var thbody = thresponse.body;
-							if ( thresponse.statusCode !== 200 || !thbody || !thbody.query || !thbody.query.pages ) {
-								console.log( '- ' + thresponse.statusCode + ': Error while getting the thread: ' + ( thbody && thbody.error && thbody.error.info ) );
-								msg.sendChannelError( spoiler + '<' + wiki.toLink(querypage.title, querystring.toTitle(), fragment, body.query.general) + '>' + spoiler );
-								
-								if ( reaction ) reaction.removeEmoji();
-							}
-							else {
-								querypage = thbody.query.pages[thread.slice(1).join(':')];
-								if ( querypage.missing !== undefined ) {
-									msg.reactEmoji('🤷');
-									
-									if ( reaction ) reaction.removeEmoji();
-								}
-								else {
-									var pagelink = wiki.toLink(thread.join(':'), querystring.toTitle(), fragment, body.query.general);
-									var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( thread.join(':').escapeFormatting() ).setURL( pagelink ).setFooter( querypage.revisions[0].user );
-									got.get( wiki.toDescLink(querypage.title), {
-										responseType: 'text'
-									} ).then( descresponse => {
-										var descbody = descresponse.body;
-										if ( descresponse.statusCode !== 200 || !descbody ) {
-											console.log( '- ' + descresponse.statusCode + ': Error while getting the description.' );
-										} else {
-											var thumbnail = wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', body.query.general);
-											var parser = new htmlparser.Parser( {
-												onopentag: (tagname, attribs) => {
-													if ( tagname === 'meta' && attribs.property === 'og:description' ) {
-														var description = attribs.content.escapeFormatting();
-														if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
-														embed.setDescription( description );
-													}
-													if ( tagname === 'meta' && attribs.property === 'og:image' ) {
-														thumbnail = attribs.content;
-													}
-												}
-											}, {decodeEntities:true} );
-											parser.write( descbody );
-											parser.end();
-											embed.setThumbnail( thumbnail );
+						}
+						else {
+							var pagelink = wiki.toLink(thread.join(':'), querystring, fragment);
+							var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( thread.join(':').escapeFormatting() ).setURL( pagelink ).setFooter( querypage.revisions[0].user );
+							got.get( wiki.toDescLink(querypage.title), {
+								responseType: 'text'
+							} ).then( descresponse => {
+								var descbody = descresponse.body;
+								if ( descresponse.statusCode !== 200 || !descbody ) {
+									console.log( '- ' + descresponse.statusCode + ': Error while getting the description.' );
+								} else {
+									var thumbnail = wiki.toLink('Special:FilePath/Wiki-wordmark.png');
+									var parser = new htmlparser.Parser( {
+										onopentag: (tagname, attribs) => {
+											if ( tagname === 'meta' && attribs.property === 'og:description' ) {
+												var description = attribs.content.escapeFormatting();
+												if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
+												embed.setDescription( description );
+											}
+											if ( tagname === 'meta' && attribs.property === 'og:image' ) {
+												thumbnail = attribs.content;
+											}
 										}
-									}, error => {
-										console.log( '- Error while getting the description: ' + error );
-									} ).finally( () => {
-										msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
-										
-										if ( reaction ) reaction.removeEmoji();
-									} );
+									}, {decodeEntities:true} );
+									parser.write( descbody );
+									parser.end();
+									embed.setThumbnail( thumbnail );
 								}
-							}
-						}, error => {
-							console.log( '- Error while getting the thread: ' + error );
-							msg.sendChannelError( spoiler + '<' + wiki.toLink(querypage.title, querystring.toTitle(), fragment, body.query.general) + '>' + spoiler );
-							
-							if ( reaction ) reaction.removeEmoji();
-						} );
+							}, error => {
+								console.log( '- Error while getting the description: ' + error );
+							} ).finally( () => {
+								msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
+								
+								if ( reaction ) reaction.removeEmoji();
+							} );
+						}
 					}
-					else if ( ( querypage.missing !== undefined && querypage.known === undefined && !( noRedirect || querypage.categoryinfo ) ) || querypage.invalid !== undefined ) {
-						got.get( wiki + 'api/v1/Search/List?minArticleQuality=0&namespaces=4,12,14,' + Object.values(body.query.namespaces).filter( ns => ns.content !== undefined ).map( ns => ns.id ).join(',') + '&limit=1&query=' + encodeURIComponent( title ) + '&format=json&cache=' + Date.now() ).then( wsresponse => {
-							var wsbody = wsresponse.body;
-							if ( wsresponse.statusCode !== 200 || !wsbody || wsbody.exception || !wsbody.total || !wsbody.items || !wsbody.items.length ) {
-								if ( wsbody && ( !wsbody.total || ( wsbody.items && !wsbody.items.length ) || ( wsbody.exception && wsbody.exception.code === 404 ) ) ) msg.reactEmoji('🤷');
-								else {
-									console.log( '- ' + wsresponse.statusCode + ': Error while getting the search results: ' + ( wsbody && wsbody.exception && wsbody.exception.details ) );
-									msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Search', 'search=' + title.toSearch(), '', body.query.general) + '>' + spoiler );
-								}
+				}, error => {
+					console.log( '- Error while getting the thread: ' + error );
+					msg.sendChannelError( spoiler + '<' + wiki.toLink(querypage.title, querystring, fragment) + '>' + spoiler );
+					
+					if ( reaction ) reaction.removeEmoji();
+				} );
+			}
+			else if ( ( querypage.missing !== undefined && querypage.known === undefined && !( noRedirect || querypage.categoryinfo ) ) || querypage.invalid !== undefined ) {
+				got.get( wiki + 'api/v1/Search/List?minArticleQuality=0&namespaces=4,12,14,' + Object.values(body.query.namespaces).filter( ns => ns.content !== undefined ).map( ns => ns.id ).join(',') + '&limit=1&query=' + encodeURIComponent( title ) + '&format=json&cache=' + Date.now() ).then( wsresponse => {
+					var wsbody = wsresponse.body;
+					if ( wsresponse.statusCode !== 200 || !wsbody || wsbody.exception || !wsbody.total || !wsbody.items || !wsbody.items.length ) {
+						if ( wsbody && ( !wsbody.total || ( wsbody.items && !wsbody.items.length ) || ( wsbody.exception && wsbody.exception.code === 404 ) ) ) msg.reactEmoji('🤷');
+						else {
+							console.log( '- ' + wsresponse.statusCode + ': Error while getting the search results: ' + ( wsbody && wsbody.exception && wsbody.exception.details ) );
+							msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Search', {search:title}) + '>' + spoiler );
+						}
+						
+						if ( reaction ) reaction.removeEmoji();
+					}
+					else {
+						querypage = wsbody.items[0];
+						if ( querypage.ns && !querypage.title.startsWith( body.query.namespaces[querypage.ns]['*'] + ':' ) ) {
+							querypage.title = body.query.namespaces[querypage.ns]['*'] + ':' + querypage.title;
+						}
+						
+						var text = '';
+						var prefix = ( msg.channel.type === 'text' && patreons[msg.guild.id] || process.env.prefix );
+						var linksuffix = ( querystring.toString() ? '?' + querystring : '' ) + ( fragment ? '#' + fragment : '' );
+						if ( title.replace( /[_-]/g, ' ' ).toLowerCase() === querypage.title.replace( /-/g, ' ' ).toLowerCase() ) {
+							text = '';
+						}
+						else if ( wsbody.total === 1 ) {
+							text = '\n' + lang.get('search.infopage', '`' + prefix + cmd + ( lang.localNames.page || 'page' ) + ' ' + title + linksuffix + '`');
+						}
+						else {
+							text = '\n' + lang.get('search.infosearch', '`' + prefix + cmd + ( lang.localNames.page || 'page' ) + ' ' + title + linksuffix + '`', '`' + prefix + cmd + ( lang.localNames.search || 'search' ) + ' ' + title + linksuffix + '`');
+						}
+						got.get( wiki + 'api.php?action=query&prop=imageinfo|categoryinfo&titles=' + encodeURIComponent( querypage.title ) + '&format=json' ).then( srresponse => {
+							var srbody = srresponse.body;
+							if ( srbody && srbody.warnings ) log_warn(srbody.warnings);
+							if ( srresponse.statusCode !== 200 || !srbody || !srbody.query || !srbody.query.pages ) {
+								console.log( '- ' + srresponse.statusCode + ': Error while getting the search results: ' + ( srbody && srbody.error && srbody.error.info ) );
+								msg.sendChannelError( spoiler + '<' + wiki.toLink(querypage.title, querystring, fragment) + '>' + spoiler );
 								
 								if ( reaction ) reaction.removeEmoji();
 							}
 							else {
-								querypage = wsbody.items[0];
-								if ( querypage.ns && !querypage.title.startsWith( body.query.namespaces[querypage.ns]['*'] + ':' ) ) {
-									querypage.title = body.query.namespaces[querypage.ns]['*'] + ':' + querypage.title;
-								}
-								
-								var text = '';
-								var prefix = ( msg.channel.type === 'text' && patreons[msg.guild.id] || process.env.prefix );
-								var linksuffix = ( querystring ? '?' + querystring : '' ) + ( fragment ? '#' + fragment : '' );
-								if ( title.replace( /\-/g, ' ' ).toTitle().toLowerCase() === querypage.title.replace( /\-/g, ' ' ).toTitle().toLowerCase() ) {
-									text = '';
+								querypage = Object.values(srbody.query.pages)[0];
+								var pagelink = wiki.toLink(querypage.title, querystring, fragment);
+								var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink );
+								if ( querypage.imageinfo ) {
+									var filename = querypage.title.replace( body.query.namespaces['6']['*'] + ':', '' );
+									var pageimage = wiki.toLink('Special:FilePath/' + filename, {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 ' : '' ) + filename}] );
 								}
-								else if ( wsbody.total === 1 ) {
-									text = '\n' + lang.get('search.infopage', '`' + prefix + cmd + ( lang.localNames.page || 'page' ) + ' ' + title + linksuffix + '`');
+								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));
+									}
+									if ( querypage.categoryinfo.files > 0 ) {
+										category.push(lang.get('search.category.files', querypage.categoryinfo.files));
+									}
+									if ( querypage.categoryinfo.subcats > 0 ) {
+										category.push(lang.get('search.category.subcats', querypage.categoryinfo.subcats));
+									}
+									if ( msg.showEmbed() ) embed.addField( category[0], category.slice(1).join('\n') );
+									else text += '\n\n' + category.join('\n');
 								}
-								else {
-									text = '\n' + lang.get('search.infosearch', '`' + prefix + cmd + ( lang.localNames.page || 'page' ) + ' ' + title + linksuffix + '`', '`' + prefix + cmd + ( lang.localNames.search || 'search' ) + ' ' + title + linksuffix + '`');
+								
+								if ( querypage.title === body.query.general.mainpage && body.query.allmessages[0]['*'] ) {
+									embed.setDescription( body.query.allmessages[0]['*'] );
+									embed.setThumbnail( wiki.toLink('Special:FilePath/Wiki-wordmark.png') );
+									
+									msg.sendChannel( spoiler + '<' + pagelink + '>' + text + spoiler, {embed} );
+									
+									if ( reaction ) reaction.removeEmoji();
 								}
-								got.get( wiki + 'api.php?action=query&prop=imageinfo|categoryinfo&titles=' + encodeURIComponent( querypage.title ) + '&format=json' ).then( srresponse => {
-									var srbody = srresponse.body;
-									if ( srbody && srbody.warnings ) log_warn(srbody.warnings);
-									if ( srresponse.statusCode !== 200 || !srbody || !srbody.query || !srbody.query.pages ) {
-										console.log( '- ' + srresponse.statusCode + ': Error while getting the search results: ' + ( srbody && srbody.error && srbody.error.info ) );
-										msg.sendChannelError( spoiler + '<' + wiki.toLink(querypage.title, querystring.toTitle(), fragment, body.query.general) + '>' + spoiler );
-										
-										if ( reaction ) reaction.removeEmoji();
-									}
-									else {
-										querypage = Object.values(srbody.query.pages)[0];
-										var pagelink = wiki.toLink(querypage.title, querystring.toTitle(), fragment, body.query.general);
-										var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink );
-										if ( querypage.imageinfo ) {
-											var filename = querypage.title.replace( body.query.namespaces['6']['*'] + ':', '' );
-											var pageimage = wiki.toLink('Special:FilePath/' + filename, 'v=' + Date.now(), '', body.query.general);
-											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 ' : '' ) + filename}] );
-										}
-										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));
-											}
-											if ( querypage.categoryinfo.files > 0 ) {
-												category.push(lang.get('search.category.files', querypage.categoryinfo.files));
-											}
-											if ( querypage.categoryinfo.subcats > 0 ) {
-												category.push(lang.get('search.category.subcats', querypage.categoryinfo.subcats));
-											}
-											if ( msg.showEmbed() ) embed.addField( category[0], category.slice(1).join('\n') );
-											else text += '\n\n' + category.join('\n');
-										}
-										
-										if ( querypage.title === body.query.general.mainpage && body.query.allmessages[0]['*'] ) {
-											embed.setDescription( body.query.allmessages[0]['*'] );
-											embed.setThumbnail( wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', body.query.general) );
-											
-											msg.sendChannel( spoiler + '<' + pagelink + '>' + text + spoiler, {embed} );
-											
-											if ( reaction ) reaction.removeEmoji();
-										}
-										else got.get( wiki.toDescLink(querypage.title), {
-											responseType: 'text'
-										} ).then( descresponse => {
-											var descbody = descresponse.body;
-											if ( descresponse.statusCode !== 200 || !descbody ) {
-												console.log( '- ' + descresponse.statusCode + ': Error while getting the description.' );
-											} else {
-												var thumbnail = wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', body.query.general);
-												var parser = new htmlparser.Parser( {
-													onopentag: (tagname, attribs) => {
-														if ( tagname === 'meta' && attribs.property === 'og:description' ) {
-															var description = attribs.content.escapeFormatting();
-															if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
-															embed.setDescription( description );
-														}
-														if ( tagname === 'meta' && attribs.property === 'og:image' && querypage.title !== body.query.general.mainpage ) {
-															thumbnail = attribs.content;
-														}
-													}
-												}, {decodeEntities:true} );
-												parser.write( descbody );
-												parser.end();
-												if ( !querypage.imageinfo ) embed.setThumbnail( thumbnail );
+								else got.get( wiki.toDescLink(querypage.title), {
+									responseType: 'text'
+								} ).then( descresponse => {
+									var descbody = descresponse.body;
+									if ( descresponse.statusCode !== 200 || !descbody ) {
+										console.log( '- ' + descresponse.statusCode + ': Error while getting the description.' );
+									} else {
+										var thumbnail = wiki.toLink('Special:FilePath/Wiki-wordmark.png');
+										var parser = new htmlparser.Parser( {
+											onopentag: (tagname, attribs) => {
+												if ( tagname === 'meta' && attribs.property === 'og:description' ) {
+													var description = attribs.content.escapeFormatting();
+													if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
+													embed.setDescription( description );
+												}
+												if ( tagname === 'meta' && attribs.property === 'og:image' && querypage.title !== body.query.general.mainpage ) {
+													thumbnail = attribs.content;
+												}
 											}
-										}, error => {
-											console.log( '- Error while getting the description: ' + error );
-										} ).finally( () => {
-											msg.sendChannel( spoiler + '<' + pagelink + '>' + text + spoiler, {embed} );
-											
-											if ( reaction ) reaction.removeEmoji();
-										} );
+										}, {decodeEntities:true} );
+										parser.write( descbody );
+										parser.end();
+										if ( !querypage.imageinfo ) embed.setThumbnail( thumbnail );
 									}
 								}, error => {
-									console.log( '- Error while getting the search results: ' + error );
-									msg.sendChannelError( spoiler + '<' + wiki.toLink(querypage.title, querystring.toTitle(), fragment, body.query.general) + '>' + spoiler );
+									console.log( '- Error while getting the description: ' + error );
+								} ).finally( () => {
+									msg.sendChannel( spoiler + '<' + pagelink + '>' + text + spoiler, {embed} );
 									
 									if ( reaction ) reaction.removeEmoji();
 								} );
 							}
 						}, error => {
 							console.log( '- Error while getting the search results: ' + error );
-							msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Search', 'search=' + title.toSearch(), '', body.query.general) + '>' + spoiler );
-							
-							if ( reaction ) reaction.removeEmoji();
-						} );
-					}
-					else if ( querypage.ns === -1 ) {
-						var pagelink = wiki.toLink(querypage.title, querystring.toTitle(), fragment, body.query.general);
-						var embed =  new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink ).setThumbnail( wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', body.query.general) );
-						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();
-						fn.special_page(lang, msg, querypage.title, specialpage, embed, wiki, reaction, spoiler);
-					}
-					else {
-						var pagelink = wiki.toLink(querypage.title, querystring.toTitle(), ( fragment || ( body.query.redirects && body.query.redirects[0].tofragment || '' ) ), body.query.general);
-						var text = '';
-						var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink );
-						if ( querypage.imageinfo ) {
-							var filename = querypage.title.replace( body.query.namespaces['6']['*'] + ':', '' );
-							var pageimage = wiki.toLink('Special:FilePath/' + filename, 'v=' + Date.now(), '', body.query.general);
-							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 ' : '' ) + filename}] );
-						}
-						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));
-							}
-							if ( querypage.categoryinfo.files > 0 ) {
-								category.push(lang.get('search.category.files', querypage.categoryinfo.files));
-							}
-							if ( querypage.categoryinfo.subcats > 0 ) {
-								category.push(lang.get('search.category.subcats', querypage.categoryinfo.subcats));
-							}
-							if ( msg.showEmbed() ) embed.addField( category[0], category.slice(1).join('\n') );
-							else text += '\n\n' + category.join('\n');
-						}
-						
-						if ( querypage.title === body.query.general.mainpage && body.query.allmessages[0]['*'] ) {
-							embed.setDescription( body.query.allmessages[0]['*'] );
-							embed.setThumbnail( wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', body.query.general) );
-							
-							msg.sendChannel( spoiler + '<' + pagelink + '>' + text + spoiler, {embed} );
-							
-							if ( reaction ) reaction.removeEmoji();
-						}
-						else got.get( wiki.toDescLink(querypage.title), {
-							responseType: 'text'
-						} ).then( descresponse => {
-							var descbody = descresponse.body;
-							if ( descresponse.statusCode !== 200 || !descbody ) {
-								console.log( '- ' + descresponse.statusCode + ': Error while getting the description.' );
-							} else {
-								var thumbnail = wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', body.query.general);
-								var parser = new htmlparser.Parser( {
-									onopentag: (tagname, attribs) => {
-										if ( tagname === 'meta' && attribs.property === 'og:description' ) {
-											var description = attribs.content.escapeFormatting();
-											if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
-											embed.setDescription( description );
-										}
-										if ( tagname === 'meta' && attribs.property === 'og:image' && querypage.title !== body.query.general.mainpage ) {
-											thumbnail = attribs.content;
-										}
-									}
-								}, {decodeEntities:true} );
-								parser.write( descbody );
-								parser.end();
-								if ( !querypage.imageinfo ) embed.setThumbnail( thumbnail );
-							}
-						}, error => {
-							console.log( '- Error while getting the description: ' + error );
-						} ).finally( () => {
-							msg.sendChannel( spoiler + '<' + pagelink + '>' + text + spoiler, {embed} );
+							msg.sendChannelError( spoiler + '<' + wiki.toLink(querypage.title, querystring, fragment) + '>' + spoiler );
 							
 							if ( reaction ) reaction.removeEmoji();
 						} );
 					}
+				}, error => {
+					console.log( '- Error while getting the search results: ' + error );
+					msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Search', {search:title}) + '>' + spoiler );
+					
+					if ( reaction ) reaction.removeEmoji();
+				} );
+			}
+			else if ( querypage.ns === -1 ) {
+				var pagelink = wiki.toLink(querypage.title, querystring, fragment);
+				var embed =  new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink ).setThumbnail( wiki.toLink('Special:FilePath/Wiki-wordmark.png') );
+				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();
+				fn.special_page(lang, msg, querypage.title, specialpage, embed, wiki, reaction, spoiler);
+			}
+			else {
+				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( querypage.title.escapeFormatting() ).setURL( pagelink );
+				if ( querypage.imageinfo ) {
+					var filename = querypage.title.replace( body.query.namespaces['6']['*'] + ':', '' );
+					var pageimage = wiki.toLink('Special:FilePath/' + filename, {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 ' : '' ) + filename}] );
 				}
-				else if ( body.query.interwiki ) {
-					if ( msg.channel.type === 'text' && pause[msg.guild.id] ) {
-						if ( reaction ) reaction.removeEmoji();
-						console.log( '- Aborted, paused.' );
-						return;
+				if ( querypage.categoryinfo ) {
+					var category = [lang.get('search.category.content')];
+					if ( querypage.categoryinfo.size === 0 ) {
+						category.push(lang.get('search.category.empty'));
 					}
-					interwiki = body.query.interwiki[0].url;
-					var maxselfcall = interwikiLimit[( msg?.guild?.id in patreons ? 'patreon' : 'default' )];
-					if ( selfcall < maxselfcall && /^(?:https?:)?\/\//.test(interwiki) ) {
-						selfcall++;
-						var regex = interwiki.match( /^(?:https?:)?\/\/(([a-z\d-]{1,50})\.(?:fandom\.com|wikia\.org)(?:(?!\/wiki\/)\/([a-z-]{2,12}))?)(?:\/wiki\/|\/?$)/ );
-						if ( regex ) {
-							let iwtitle = decodeURIComponent( interwiki.replace( regex[0], '' ) ).replace( /\_/g, ' ' );
-							this.fandom(lang, msg, iwtitle, 'https://' + regex[1] + '/', ( regex[1].includes( '.wikia.org' ) ? '??' : '?' ) + ( regex[3] ? regex[3] + '.' : '' ) + regex[2] + ' ', reaction, spoiler, querystring, fragment, interwiki, selfcall);
-							return;
-						}
-						regex = interwiki.match( /^(?:https?:)?\/\/([a-z\d-]{1,50})\.gamepedia\.com(?:\/|$)/ );
-						if ( regex ) {
-							let iwtitle = decodeURIComponent( interwiki.replace( regex[0], '' ) ).replace( /\_/g, ' ' );
-							this.gamepedia(lang, msg, iwtitle, 'https://' + regex[1] + '.gamepedia.com/', '!' + regex[1] + ' ', reaction, spoiler, querystring, fragment, interwiki, selfcall);
-							return;
-						}
-						let project = wikiProjects.find( project => interwiki.split('/')[2].endsWith( project.name ) );
-						if ( project ) {
-							regex = interwiki.match( new RegExp( '^(?:https?:)?//' + project.regex + `(?:${project.articlePath}|/?$)` ) );
-							if ( regex ) {
-								let iwtitle = decodeURIComponent( interwiki.replace( regex[0], '' ) ).replace( /\_/g, ' ' );
-								this.gamepedia(lang, msg, iwtitle, 'https://' + regex[1] + project.scriptPath, cmd + body.query.interwiki[0].iw + ':', reaction, spoiler, querystring, fragment, interwiki, selfcall);
-								return;
-							}
-						}
+					if ( querypage.categoryinfo.pages > 0 ) {
+						category.push(lang.get('search.category.pages', querypage.categoryinfo.pages));
 					}
-					if ( fragment ) fragment = '#' + fragment.toSection();
-					if ( interwiki.includes( '#' ) ) {
-						if ( !fragment ) fragment = '#' + interwiki.split('#').slice(1).join('#');
-						interwiki = interwiki.split('#')[0];
+					if ( querypage.categoryinfo.files > 0 ) {
+						category.push(lang.get('search.category.files', querypage.categoryinfo.files));
 					}
-					if ( querystring ) interwiki += ( interwiki.includes( '?' ) ? '&' : '?' ) + querystring.toTitle();
-					msg.sendChannel( spoiler + ' ' + interwiki.replace( /@(here|everyone)/g, '%40$1' ) + fragment + ' ' + spoiler ).then( message => {
-						if ( message && selfcall === maxselfcall ) message.reactEmoji('⚠️');
-					} );
-					if ( reaction ) reaction.removeEmoji();
+					if ( querypage.categoryinfo.subcats > 0 ) {
+						category.push(lang.get('search.category.subcats', querypage.categoryinfo.subcats));
+					}
+					if ( msg.showEmbed() ) embed.addField( category[0], category.slice(1).join('\n') );
+					else text += '\n\n' + category.join('\n');
 				}
-				else if ( body.query.redirects ) {
-					var pagelink = wiki.toLink(body.query.redirects[0].to, querystring.toTitle(), ( fragment || body.query.redirects[0].tofragment || '' ) );
-					var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( body.query.redirects[0].to.escapeFormatting() ).setURL( pagelink ).setThumbnail( wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', body.query.general) );
+				
+				if ( querypage.title === body.query.general.mainpage && body.query.allmessages[0]['*'] ) {
+					embed.setDescription( body.query.allmessages[0]['*'] );
+					embed.setThumbnail( wiki.toLink('Special:FilePath/Wiki-wordmark.png') );
 					
-					msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
+					msg.sendChannel( spoiler + '<' + pagelink + '>' + text + spoiler, {embed} );
 					
-					if ( reaction ) reaction.removeEmoji();;
+					if ( reaction ) reaction.removeEmoji();
 				}
-				else {
-					var pagelink = wiki.toLink(body.query.general.mainpage, querystring.toTitle(), fragment, body.query.general);
-					var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( body.query.general.mainpage.escapeFormatting() ).setURL( pagelink ).setThumbnail( wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', body.query.general) );
-					
-					if ( body.query.allmessages[0]['*'] ) {
-						embed.setDescription( body.query.allmessages[0]['*'] );
-						
-						msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
-						
-						if ( reaction ) reaction.removeEmoji();
-					}
-					else got.get( wiki.toDescLink(body.query.general.mainpage), {
-						responseType: 'text'
-					} ).then( descresponse => {
-						var descbody = descresponse.body;
-						if ( descresponse.statusCode !== 200 || !descbody ) {
-							console.log( '- ' + descresponse.statusCode + ': Error while getting the description.' );
-						} else {
-							var parser = new htmlparser.Parser( {
-								onopentag: (tagname, attribs) => {
-									if ( tagname === 'meta' && attribs.property === 'og:description' ) {
-										var description = attribs.content.escapeFormatting();
-										if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
-										embed.setDescription( description );
-									}
+				else got.get( wiki.toDescLink(querypage.title), {
+					responseType: 'text'
+				} ).then( descresponse => {
+					var descbody = descresponse.body;
+					if ( descresponse.statusCode !== 200 || !descbody ) {
+						console.log( '- ' + descresponse.statusCode + ': Error while getting the description.' );
+					} else {
+						var thumbnail = wiki.toLink('Special:FilePath/Wiki-wordmark.png');
+						var parser = new htmlparser.Parser( {
+							onopentag: (tagname, attribs) => {
+								if ( tagname === 'meta' && attribs.property === 'og:description' ) {
+									var description = attribs.content.escapeFormatting();
+									if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
+									embed.setDescription( description );
 								}
-							}, {decodeEntities:true} );
-							parser.write( descbody );
-							parser.end();
-						}
-					}, error => {
-						console.log( '- Error while getting the description: ' + error );
-					} ).finally( () => {
-						msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
-						
-						if ( reaction ) reaction.removeEmoji();
-					} );
-				}
+								if ( tagname === 'meta' && attribs.property === 'og:image' && querypage.title !== body.query.general.mainpage ) {
+									thumbnail = attribs.content;
+								}
+							}
+						}, {decodeEntities:true} );
+						parser.write( descbody );
+						parser.end();
+						if ( !querypage.imageinfo ) embed.setThumbnail( thumbnail );
+					}
+				}, error => {
+					console.log( '- Error while getting the description: ' + error );
+				} ).finally( () => {
+					msg.sendChannel( spoiler + '<' + pagelink + '>' + text + spoiler, {embed} );
+					
+					if ( reaction ) reaction.removeEmoji();
+				} );
 			}
-		}, error => {
-			if ( interwiki ) msg.sendChannel( spoiler + ' ' + interwiki.replace( /@(here|everyone)/g, '%40$1' ) + fragment + ' ' + spoiler );
-			else if ( wiki.noWiki(error.message) ) {
-				console.log( '- This wiki doesn\'t exist!' );
-				msg.reactEmoji('nowiki');
+		}
+		else if ( body.query.interwiki ) {
+			if ( msg.channel.type === 'text' && pause[msg.guild.id] ) {
+				if ( reaction ) reaction.removeEmoji();
+				console.log( '- Aborted, paused.' );
+				return;
 			}
-			else {
-				console.log( '- Error while getting the search results: ' + error );
-				msg.sendChannelError( spoiler + '<' + wiki.toLink(( querystring || fragment || !title ? title : 'Special:Search' ), ( querystring || fragment || !title ? querystring.toTitle() : 'search=' + title.toSearch() ), fragment) + '>' + spoiler );
+			var iw = new URL(body.query.interwiki[0].url.replace( /\\/g, '%5C' ).replace( /@(here|everyone)/g, '%40$1' ), wiki);
+			querystring.forEach( (value, name) => {
+				iw.searchParams.append(name, value);
+			} );
+			if ( fragment ) iw.hash = Wiki.toSection(fragment);
+			else fragment = iw.hash.substring(1);
+			var maxselfcall = interwikiLimit[( msg?.guild?.id in patreons ? 'patreon' : 'default' )];
+			if ( selfcall < maxselfcall && ['http:','https:'].includes( iw.protocol ) ) {
+				selfcall++;
+				if ( iw.hostname.endsWith( '.fandom.com' ) || iw.hostname.endsWith( '.wikia.org' ) ) {
+					let regex = iw.pathname.match( /^(\/(?!wiki\/)[a-z-]{2,12})?(?:\/wiki\/|\/?$)/ );
+					if ( regex ) {
+						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.fandom(lang, msg, iwtitle, new Wiki(iw.origin + path + '/'), cmd, reaction, spoiler, iw.searchParams, fragment, iw.href, 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.gamepedia(lang, msg, iwtitle, new Wiki(iw.origin), cmd, reaction, spoiler, iw.searchParams, fragment, iw.href, selfcall);
+				}
+				let project = wikiProjects.find( project => iw.hostname.endsWith( project.name ) );
+				if ( project ) {
+					let regex = ( iw.host + iw.pathname ).match( new RegExp( '^' + project.regex + '(?:' + project.articlePath + '|/?$)' ) );
+					if ( regex ) {
+						let iwtitle = decodeURIComponent( iw.pathname.replace( regex[0], '' ) ).replace( /_/g, ' ' );
+						cmd = '!!' + regex[1] + ' ';
+						return this.gamepedia(lang, msg, iwtitle, new Wiki(iw.origin + project.scriptPath), cmd, reaction, spoiler, iw.searchParams, fragment, iw.href, selfcall);
+					}
+				}
 			}
-			
+			msg.sendChannel( spoiler + ' ' + iw + ' ' + spoiler ).then( message => {
+				if ( message && selfcall === maxselfcall ) message.reactEmoji('⚠️');
+			} );
 			if ( reaction ) reaction.removeEmoji();
-		} );
-	}
+		}
+		else if ( body.query.redirects ) {
+			var pagelink = wiki.toLink(body.query.redirects[0].to, querystring, ( fragment || body.query.redirects[0].tofragment || '' ));
+			var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( body.query.redirects[0].to.escapeFormatting() ).setURL( pagelink ).setThumbnail( wiki.toLink('Special:FilePath/Wiki-wordmark.png') );
+			
+			msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
+			
+			if ( reaction ) reaction.removeEmoji();;
+		}
+		else {
+			var pagelink = wiki.toLink(body.query.general.mainpage, querystring, fragment);
+			var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( body.query.general.mainpage.escapeFormatting() ).setURL( pagelink ).setThumbnail( wiki.toLink('Special:FilePath/Wiki-wordmark.png') );
+			
+			if ( body.query.allmessages[0]['*'] ) {
+				embed.setDescription( body.query.allmessages[0]['*'] );
+				
+				msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
+				
+				if ( reaction ) reaction.removeEmoji();
+			}
+			else got.get( wiki.toDescLink(body.query.general.mainpage), {
+				responseType: 'text'
+			} ).then( descresponse => {
+				var descbody = descresponse.body;
+				if ( descresponse.statusCode !== 200 || !descbody ) {
+					console.log( '- ' + descresponse.statusCode + ': Error while getting the description.' );
+				} else {
+					var parser = new htmlparser.Parser( {
+						onopentag: (tagname, attribs) => {
+							if ( tagname === 'meta' && attribs.property === 'og:description' ) {
+								var description = attribs.content.escapeFormatting();
+								if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
+								embed.setDescription( description );
+							}
+						}
+					}, {decodeEntities:true} );
+					parser.write( descbody );
+					parser.end();
+				}
+			}, error => {
+				console.log( '- Error while getting the description: ' + error );
+			} ).finally( () => {
+				msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
+				
+				if ( reaction ) reaction.removeEmoji();
+			} );
+		}
+	}, error => {
+		if ( interwiki ) msg.sendChannel( spoiler + ' ' + interwiki + ' ' + spoiler );
+		else if ( wiki.noWiki(error.message) ) {
+			console.log( '- This wiki doesn\'t exist!' );
+			msg.reactEmoji('nowiki');
+		}
+		else {
+			console.log( '- Error while getting the search results: ' + error );
+			msg.sendChannelError( spoiler + '<' + wiki.toLink(( querystring.toString() || fragment || !title ? title : 'Special:Search' ), ( querystring.toString() || fragment || !title ? querystring : {search:title} ), fragment) + '>' + spoiler );
+		}
+		
+		if ( reaction ) reaction.removeEmoji();
+	} );
 }
 
 module.exports = fandom_check_wiki;

+ 174 - 174
cmds/wiki/fandom/diff.js

@@ -1,13 +1,14 @@
 const htmlparser = require('htmlparser2');
 const {MessageEmbed} = require('discord.js');
 const {timeoptions} = require('../../../util/default.json');
+const {toFormatting} = require('../../../util/functions.js');
 
 /**
  * Processes a Fandom edit.
  * @param {import('../../../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
- * @param {String} wiki - The wiki for the edit.
+ * @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 {MessageEmbed} [embed] - The embed for the page.
@@ -61,7 +62,7 @@ function fandom_diff(lang, msg, args, wiki, reaction, spoiler, embed) {
 					}
 					else {
 						console.log( '- ' + response.statusCode + ': Error while getting the search results: ' + ( body && body.error && body.error.info ) );
-						msg.sendChannelError( spoiler + '<' + wiki.toLink(title, 'diff=' + diff + ( title ? '' : '&oldid=' + revision )) + '>' + spoiler );
+						msg.sendChannelError( spoiler + '<' + wiki.toLink(title, ( title ? {diff} : {diff,oldid:revision} )) + '>' + spoiler );
 					}
 					
 					if ( reaction ) reaction.removeEmoji();
@@ -182,7 +183,7 @@ function fandom_diff(lang, msg, args, wiki, reaction, spoiler, embed) {
 				}
 				else {
 					console.log( '- Error while getting the search results: ' + error );
-					msg.sendChannelError( spoiler + '<' + wiki.toLink(title, 'diff=' + diff + ( title ? '' : '&oldid=' + revision )) + '>' + spoiler );
+					msg.sendChannelError( spoiler + '<' + wiki.toLink(title, ( title ? {diff} : {diff,oldid:revision} )) + '>' + spoiler );
 				}
 				
 				if ( reaction ) reaction.removeEmoji();
@@ -202,7 +203,7 @@ function fandom_diff(lang, msg, args, wiki, reaction, spoiler, embed) {
  * @param {import('../../../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
- * @param {String} wiki - The wiki for the edit.
+ * @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 {String[]} [compare] - The edit difference.
@@ -223,201 +224,200 @@ function fandom_diff_send(lang, msg, args, wiki, reaction, spoiler, compare) {
 			
 			if ( reaction ) reaction.removeEmoji();
 		}
-		else {
-			if ( body.query.badrevids ) {
-				msg.replyMsg( lang.get('diff.badrev') );
+		else if ( body.query.badrevids ) {
+			msg.replyMsg( lang.get('diff.badrev') );
+			
+			if ( reaction ) reaction.removeEmoji();
+		}
+		else if ( body.query.pages && !body.query.pages['-1'] ) {
+			wiki.updateWiki(body.query.general);
+			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 );
 				
 				if ( reaction ) reaction.removeEmoji();
 			}
-			else if ( body.query.pages && !body.query.pages['-1'] ) {
-				var pages = Object.values(body.query.pages);
-				if ( pages.length !== 1 ) {
-					msg.sendChannel( spoiler + '<' + wiki.toLink('Special:Diff/' + ( args[1] ? args[1] + '/' : '' ) + args[0], '', '', body.query.general) + '>' + spoiler );
-					
-					if ( reaction ) reaction.removeEmoji();
-				}
-				else {
-					var title = pages[0].title;
-					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') : revisions[0].user )];
-					var timestamp = [lang.get('diff.info.timestamp'), new Date(revisions[0].timestamp).toLocaleString(lang.get('dateformat'), timeoptions)];
-					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)];
-					var comment = [lang.get('diff.info.comment'), ( revisions[0].commenthidden !== undefined ? lang.get('diff.hidden') : ( revisions[0].comment ? revisions[0].comment.toFormatting(msg.showEmbed(), wiki, body.query.general, title) : 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(', ')];
+			else {
+				var title = pages[0].title;
+				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') : revisions[0].user )];
+				var timestamp = [lang.get('diff.info.timestamp'), new Date(revisions[0].timestamp).toLocaleString(lang.get('dateformat'), timeoptions)];
+				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)];
+				var comment = [lang.get('diff.info.comment'), ( revisions[0].commenthidden !== undefined ? lang.get('diff.hidden') : ( revisions[0].comment ? toFormatting(revisions[0].comment, msg.showEmbed(), wiki, title) : 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 pagelink = wiki.toLink(title, {diff,oldid});
+				if ( msg.showEmbed() ) {
+					var text = '<' + pagelink + '>';
+					var editorlink = '[' + editor[1] + '](' + wiki.toLink('User:' + editor[1], '', '', true) + ')';
+					if ( revisions[0].anon !== undefined ) {
+						editorlink = '[' + editor[1] + '](' + wiki.toLink('Special:Contributions/' + editor[1], '', '', true) + ')';
+					}
+					if ( editor[1] === lang.get('diff.hidden') ) editorlink = editor[1];
+					var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( ( title + '?diff=' + diff + '&oldid=' + oldid ).escapeFormatting() ).setURL( pagelink ).addField( editor[0], editorlink, true ).addField( size[0], size[1], true ).addField( comment[0], comment[1] ).setFooter( timestamp[1] );
+					if ( tags ) {
+						var taglink = '';
+						var tagtext = '';
+						var tagparser = new htmlparser.Parser( {
+							onopentag: (tagname, attribs) => {
+								if ( tagname === 'a' ) taglink = attribs.href;
+							},
+							ontext: (htmltext) => {
+								if ( taglink ) tagtext += '[' + htmltext.escapeFormatting() + '](' + taglink + ')'
+								else tagtext += htmltext.escapeFormatting();
+							},
+							onclosetag: (tagname) => {
+								if ( tagname === 'a' ) taglink = '';
+							}
+						}, {decodeEntities:true} );
+						tagparser.write( tags[1] );
+						tagparser.end();
+						embed.addField( tags[0], tagtext );
+					}
 					
-					var pagelink = wiki.toLink(title, 'diff=' + diff + '&oldid=' + oldid, '', body.query.general);
-					if ( msg.showEmbed() ) {
-						var text = '<' + pagelink + '>';
-						var editorlink = '[' + editor[1] + '](' + wiki.toLink('User:' + editor[1], '', '', body.query.general, true) + ')';
-						if ( revisions[0].anon !== undefined ) {
-							editorlink = '[' + editor[1] + '](' + wiki.toLink('Special:Contributions/' + editor[1], '', '', body.query.general, true) + ')';
-						}
-						if ( editor[1] === lang.get('diff.hidden') ) editorlink = editor[1];
-						var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( ( title + '?diff=' + diff + '&oldid=' + oldid ).escapeFormatting() ).setURL( pagelink ).addField( editor[0], editorlink, true ).addField( size[0], size[1], true ).addField( comment[0], comment[1] ).setFooter( timestamp[1] );
-						if ( tags ) {
-							var taglink = '';
-							var tagtext = '';
-							var tagparser = new htmlparser.Parser( {
-								onopentag: (tagname, attribs) => {
-									if ( tagname === 'a' ) taglink = attribs.href;
-								},
-								ontext: (htmltext) => {
-									if ( taglink ) tagtext += '[' + htmltext.escapeFormatting() + '](' + taglink + ')'
-									else tagtext += htmltext.escapeFormatting();
-								},
-								onclosetag: (tagname) => {
-									if ( tagname === 'a' ) taglink = '';
-								}
-							}, {decodeEntities:true} );
-							tagparser.write( tags[1] );
-							tagparser.end();
-							embed.addField( tags[0], tagtext );
+					var more = '\n__' + lang.get('diff.info.more') + '__';
+					if ( !compare && oldid ) got.get( wiki + 'api.php?action=query&prop=revisions&rvprop=&revids=' + oldid + '&rvdiffto=' + diff + '&format=json' ).then( cpresponse => {
+						var cpbody = cpresponse.body;
+						if ( cpbody && cpbody.warnings ) log_warn(cpbody.warnings);
+						if ( cpresponse.statusCode !== 200 || !cpbody || !cpbody.query || cpbody.query.badrevids || !cpbody.query.pages && cpbody.query.pages[-1] ) {
+							console.log( '- ' + cpresponse.statusCode + ': Error while getting the diff: ' + ( cpbody && cpbody.error && cpbody.error.info ) );
 						}
-						
-						var more = '\n__' + lang.get('diff.info.more') + '__';
-						if ( !compare && oldid ) got.get( wiki + 'api.php?action=query&prop=revisions&rvprop=&revids=' + oldid + '&rvdiffto=' + diff + '&format=json' ).then( cpresponse => {
-							var cpbody = cpresponse.body;
-							if ( cpbody && cpbody.warnings ) log_warn(cpbody.warnings);
-							if ( cpresponse.statusCode !== 200 || !cpbody || !cpbody.query || cpbody.query.badrevids || !cpbody.query.pages && cpbody.query.pages[-1] ) {
-								console.log( '- ' + cpresponse.statusCode + ': Error while getting the diff: ' + ( cpbody && cpbody.error && cpbody.error.info ) );
-							}
-							else {
-								var revision = Object.values(cpbody.query.pages)[0].revisions[0];
-								if ( revision.texthidden === undefined && revision.diff && revision.diff['*'] !== undefined ) {
-									var current_tag = '';
-									var small_prev_ins = '';
-									var small_prev_del = '';
-									var ins_length = more.length;
-									var del_length = more.length;
-									var added = false;
-									var parser = new htmlparser.Parser( {
-										onopentag: (tagname, attribs) => {
-											if ( tagname === 'ins' || tagname == 'del' ) {
-												current_tag = tagname;
-											}
-											if ( tagname === 'td' && attribs.class === 'diff-addedline' ) {
-												current_tag = tagname+'a';
-											}
-											if ( tagname === 'td' && attribs.class === 'diff-deletedline' ) {
-												current_tag = tagname+"d";
-											}
-											if ( tagname === 'td' && attribs.class === 'diff-marker' ) {
-												added = true;
-											}
-										},
-										ontext: (htmltext) => {
-											if ( current_tag === 'ins' && ins_length <= 1000 ) {
-												ins_length += ( '**' + htmltext.escapeFormatting() + '**' ).length;
-												if ( ins_length <= 1000 ) small_prev_ins += '**' + htmltext.escapeFormatting() + '**';
+						else {
+							var revision = Object.values(cpbody.query.pages)[0].revisions[0];
+							if ( revision.texthidden === undefined && revision.diff && revision.diff['*'] !== undefined ) {
+								var current_tag = '';
+								var small_prev_ins = '';
+								var small_prev_del = '';
+								var ins_length = more.length;
+								var del_length = more.length;
+								var added = false;
+								var parser = new htmlparser.Parser( {
+									onopentag: (tagname, attribs) => {
+										if ( tagname === 'ins' || tagname == 'del' ) {
+											current_tag = tagname;
+										}
+										if ( tagname === 'td' && attribs.class === 'diff-addedline' ) {
+											current_tag = tagname+'a';
+										}
+										if ( tagname === 'td' && attribs.class === 'diff-deletedline' ) {
+											current_tag = tagname+"d";
+										}
+										if ( tagname === 'td' && attribs.class === 'diff-marker' ) {
+											added = true;
+										}
+									},
+									ontext: (htmltext) => {
+										if ( current_tag === 'ins' && ins_length <= 1000 ) {
+											ins_length += ( '**' + htmltext.escapeFormatting() + '**' ).length;
+											if ( ins_length <= 1000 ) small_prev_ins += '**' + htmltext.escapeFormatting() + '**';
+											else small_prev_ins += more;
+										}
+										if ( current_tag === 'del' && del_length <= 1000 ) {
+											del_length += ( '~~' + htmltext.escapeFormatting() + '~~' ).length;
+											if ( del_length <= 1000 ) small_prev_del += '~~' + htmltext.escapeFormatting() + '~~';
+											else small_prev_del += more;
+										}
+										if ( ( current_tag === 'afterins' || current_tag === 'tda') && ins_length <= 1000 ) {
+											ins_length += htmltext.escapeFormatting().length;
+											if ( ins_length <= 1000 ) small_prev_ins += htmltext.escapeFormatting();
+											else small_prev_ins += more;
+										}
+										if ( ( current_tag === 'afterdel' || current_tag === 'tdd') && del_length <= 1000 ) {
+											del_length += htmltext.escapeFormatting().length;
+											if ( del_length <= 1000 ) small_prev_del += htmltext.escapeFormatting();
+											else small_prev_del += more;
+										}
+										if ( added ) {
+											if ( htmltext === '+' && ins_length <= 1000 ) {
+												ins_length++;
+												if ( ins_length <= 1000 ) small_prev_ins += '\n';
 												else small_prev_ins += more;
 											}
-											if ( current_tag === 'del' && del_length <= 1000 ) {
-												del_length += ( '~~' + htmltext.escapeFormatting() + '~~' ).length;
-												if ( del_length <= 1000 ) small_prev_del += '~~' + htmltext.escapeFormatting() + '~~';
+											if ( htmltext === '−' && del_length <= 1000 ) {
+												del_length++;
+												if ( del_length <= 1000 ) small_prev_del += '\n';
 												else small_prev_del += more;
 											}
-											if ( ( current_tag === 'afterins' || current_tag === 'tda') && ins_length <= 1000 ) {
-												ins_length += htmltext.escapeFormatting().length;
-												if ( ins_length <= 1000 ) small_prev_ins += htmltext.escapeFormatting();
-												else small_prev_ins += more;
-											}
-											if ( ( current_tag === 'afterdel' || current_tag === 'tdd') && del_length <= 1000 ) {
-												del_length += htmltext.escapeFormatting().length;
-												if ( del_length <= 1000 ) small_prev_del += htmltext.escapeFormatting();
-												else small_prev_del += more;
-											}
-											if ( added ) {
-												if ( htmltext === '+' && ins_length <= 1000 ) {
-													ins_length++;
-													if ( ins_length <= 1000 ) small_prev_ins += '\n';
-													else small_prev_ins += more;
-												}
-												if ( htmltext === '−' && del_length <= 1000 ) {
-													del_length++;
-													if ( del_length <= 1000 ) small_prev_del += '\n';
-													else small_prev_del += more;
-												}
-												added = false;
-											}
-										},
-										onclosetag: (tagname) => {
-											if ( tagname === 'ins' ) {
-												current_tag = 'afterins';
-											} else if ( tagname === 'del' ) {
-												current_tag = 'afterdel';
-											} else {
-												current_tag = '';
-											}
+											added = false;
+										}
+									},
+									onclosetag: (tagname) => {
+										if ( tagname === 'ins' ) {
+											current_tag = 'afterins';
+										} else if ( tagname === 'del' ) {
+											current_tag = 'afterdel';
+										} else {
+											current_tag = '';
 										}
-									}, {decodeEntities:true} );
-									parser.write( revision.diff['*'] );
-									parser.end();
-									if ( small_prev_del.length ) {
-										if ( small_prev_del.replace( /\~\~/g, '' ).trim().length ) {
-											embed.addField( lang.get('diff.info.removed'), small_prev_del.replace( /\~\~\~\~/g, '' ), true );
-										} else embed.addField( lang.get('diff.info.removed'), '__' + lang.get('diff.info.whitespace') + '__', true );
-									}
-									if ( small_prev_ins.length ) {
-										if ( small_prev_ins.replace( /\*\*/g, '' ).trim().length ) {
-											embed.addField( lang.get('diff.info.added'), small_prev_ins.replace( /\*\*\*\*/g, '' ), true );
-										} else embed.addField( lang.get('diff.info.added'), '__' + lang.get('diff.info.whitespace') + '__', true );
 									}
+								}, {decodeEntities:true} );
+								parser.write( revision.diff['*'] );
+								parser.end();
+								if ( small_prev_del.length ) {
+									if ( small_prev_del.replace( /\~\~/g, '' ).trim().length ) {
+										embed.addField( lang.get('diff.info.removed'), small_prev_del.replace( /\~\~\~\~/g, '' ), true );
+									} else embed.addField( lang.get('diff.info.removed'), '__' + lang.get('diff.info.whitespace') + '__', true );
 								}
-								else if ( revision.texthidden !== undefined ) {
-									embed.addField( lang.get('diff.info.added'), '__' + lang.get('diff.hidden') + '__', true );
-								}
-								else if ( revision.diff && revision.diff['*'] === undefined ) {
-									embed.addField( lang.get('diff.info.removed'), '__' + lang.get('diff.hidden') + '__', true );
+								if ( small_prev_ins.length ) {
+									if ( small_prev_ins.replace( /\*\*/g, '' ).trim().length ) {
+										embed.addField( lang.get('diff.info.added'), small_prev_ins.replace( /\*\*\*\*/g, '' ), true );
+									} else embed.addField( lang.get('diff.info.added'), '__' + lang.get('diff.info.whitespace') + '__', true );
 								}
 							}
-						}, error => {
-							console.log( '- Error while getting the diff: ' + error );
-						} ).finally( () => {
-							msg.sendChannel( spoiler + text + spoiler, {embed} );
-							
-							if ( reaction ) reaction.removeEmoji();
-						} );
-						else {
-							if ( compare ) {
-								if ( compare[0].length ) embed.addField( lang.get('diff.info.removed'), compare[0], true );
-								if ( compare[1].length ) embed.addField( lang.get('diff.info.added'), compare[1], true );
+							else if ( revision.texthidden !== undefined ) {
+								embed.addField( lang.get('diff.info.added'), '__' + lang.get('diff.hidden') + '__', true );
 							}
-							else if ( revisions[0]['*'] ) {
-								var content = revisions[0]['*'].escapeFormatting();
-								if ( content.trim().length ) {
-									if ( content.length <= 1000 ) content = '**' + content + '**';
-									else {
-										content = content.substring(0, 1000 - more.length);
-										content = '**' + content.substring(0, content.lastIndexOf('\n')) + '**' + more;
-									}
-									embed.addField( lang.get('diff.info.added'), content, true );
-								} else embed.addField( lang.get('diff.info.added'), '__' + lang.get('diff.info.whitespace') + '__', true );
+							else if ( revision.diff && revision.diff['*'] === undefined ) {
+								embed.addField( lang.get('diff.info.removed'), '__' + lang.get('diff.hidden') + '__', true );
 							}
-							
-							msg.sendChannel( spoiler + text + spoiler, {embed} );
-							
-							if ( reaction ) reaction.removeEmoji();
 						}
-					}
+					}, error => {
+						console.log( '- Error while getting the diff: ' + error );
+					} ).finally( () => {
+						msg.sendChannel( spoiler + text + spoiler, {embed} );
+						
+						if ( reaction ) reaction.removeEmoji();
+					} );
 					else {
-						var embed = {};
-						var text = '<' + pagelink + '>\n\n' + editor.join(' ') + '\n' + timestamp.join(' ') + '\n' + size.join(' ') + '\n' + comment.join(' ');
-						if ( tags ) text += htmlToPlain( '\n' + tags.join(' ') );
+						if ( compare ) {
+							if ( compare[0].length ) embed.addField( lang.get('diff.info.removed'), compare[0], true );
+							if ( compare[1].length ) embed.addField( lang.get('diff.info.added'), compare[1], true );
+						}
+						else if ( revisions[0]['*'] ) {
+							var content = revisions[0]['*'].escapeFormatting();
+							if ( content.trim().length ) {
+								if ( content.length <= 1000 ) content = '**' + content + '**';
+								else {
+									content = content.substring(0, 1000 - more.length);
+									content = '**' + content.substring(0, content.lastIndexOf('\n')) + '**' + more;
+								}
+								embed.addField( lang.get('diff.info.added'), content, true );
+							} else embed.addField( lang.get('diff.info.added'), '__' + lang.get('diff.info.whitespace') + '__', true );
+						}
 						
 						msg.sendChannel( spoiler + text + spoiler, {embed} );
 						
 						if ( reaction ) reaction.removeEmoji();
 					}
 				}
+				else {
+					var embed = {};
+					var text = '<' + pagelink + '>\n\n' + editor.join(' ') + '\n' + timestamp.join(' ') + '\n' + size.join(' ') + '\n' + comment.join(' ');
+					if ( tags ) text += htmlToPlain( '\n' + tags.join(' ') );
+					
+					msg.sendChannel( spoiler + text + spoiler, {embed} );
+					
+					if ( reaction ) reaction.removeEmoji();
+				}
 			}
-			else {
-				msg.reactEmoji('error');
-				
-				if ( reaction ) reaction.removeEmoji();
-			}
+		}
+		else {
+			msg.reactEmoji('error');
+			
+			if ( reaction ) reaction.removeEmoji();
 		}
 	}, error => {
 		if ( wiki.noWiki(error.message) ) {

+ 9 - 8
cmds/wiki/fandom/overview.js

@@ -11,7 +11,7 @@ getAllSites.then( sites => allSites = sites );
  * Sends a Fandom wiki overview.
  * @param {import('../../../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
- * @param {String} wiki - The wiki for the overview.
+ * @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.
  */
@@ -36,10 +36,11 @@ function fandom_overview(lang, msg, wiki, reaction, spoiler) {
 			return gamepedia_overview(lang, msg, wiki, reaction, spoiler);
 		}
 		else got.get( 'https://community.fandom.com/api/v1/Wikis/Details?ids=' + body.query.wikidesc.id + '&format=json&cache=' + Date.now() ).then( ovresponse => {
+			wiki.updateWiki(body.query.general);
 			var ovbody = ovresponse.body;
 			if ( ovresponse.statusCode !== 200 || !ovbody || ovbody.exception || !ovbody.items || !ovbody.items[body.query.wikidesc.id] ) {
 				console.log( '- ' + ovresponse.statusCode + ': Error while getting the wiki details: ' + ( ovbody && ovbody.exception && ovbody.exception.details ) );
-				msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Statistics', '', '', body.query.general) + '>' + spoiler );
+				msg.sendChannelError( spoiler + '<' + wiki.toLink(body.query.pages['-1'].title) + '>' + spoiler );
 				
 				if ( reaction ) reaction.removeEmoji();
 			}
@@ -70,13 +71,13 @@ function fandom_overview(lang, msg, wiki, reaction, spoiler) {
 					description[1] = description[1].escapeFormatting();
 					if ( description[1].length > 1000 ) description[1] = description[1].substring(0, 1000) + '\u2026';
 				}
-				if ( image[1] && image[1].startsWith( '/' ) ) image[1] = wiki.substring(0, wiki.length - 1) + image[1];
+				if ( image[1] && image[1].startsWith( '/' ) ) image[1] = new URL(image[1], wiki).href;
 				
 				var title = body.query.pages['-1'].title;
-				var pagelink = wiki.toLink(title, '', '', body.query.general);
+				var pagelink = wiki.toLink(title);
 				if ( msg.showEmbed() ) {
 					var text = '<' + pagelink + '>';
-					var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( title.escapeFormatting() ).setURL( pagelink ).setThumbnail( ( site.wordmark.startsWith( 'data:' ) ? wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', body.query.general) : site.wordmark ) ).addField( vertical[0], vertical[1], true );
+					var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( title.escapeFormatting() ).setURL( pagelink ).setThumbnail( ( site.wordmark.startsWith( 'data:' ) ? wiki.toLink('Special:FilePath/Wiki-wordmark.png') : site.wordmark ) ).addField( vertical[0], vertical[1], true );
 					if ( topic[1] ) embed.addField( topic[0], topic[1], true );
 				}
 				else {
@@ -93,7 +94,7 @@ function fandom_overview(lang, msg, wiki, reaction, spoiler) {
 					}
 					else {
 						var user = usbody.query.users[0].name;
-						if ( msg.showEmbed() ) founder[1] = '[' + user + '](' + wiki.toLink('User:' + user, '', '', body.query.general, true) + ')';
+						if ( msg.showEmbed() ) founder[1] = '[' + user + '](' + wiki.toLink('User:' + user, '', '', true) + ')';
 						else founder[1] = user;
 					}
 				}, error => {
@@ -102,7 +103,7 @@ function fandom_overview(lang, msg, wiki, reaction, spoiler) {
 				} ).finally( () => {
 					if ( msg.showEmbed() ) {
 						embed.addField( founder[0], founder[1], true );
-						if ( manager[1] ) embed.addField( manager[0], '[' + manager[1] + '](' + wiki.toLink('User:' + manager[1], '', '', body.query.general, true) + ') ([' + lang.get('overview.talk') + '](' + wiki.toLink('User talk:' + manager[1], '', '', body.query.general, true) + '))', true );
+						if ( manager[1] ) embed.addField( manager[0], '[' + manager[1] + '](' + wiki.toLink('User:' + manager[1], '', '', true) + ') ([' + lang.get('overview.talk') + '](' + wiki.toLink('User talk:' + manager[1], '', '', true) + '))', 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 ).setFooter( lang.get('overview.inaccurate') );
 						if ( crossover[1] ) {
 							var crossoverSite = allSites.find( site => '<https://' + site.wiki_domain + '/>' === crossover[1] );
@@ -157,7 +158,7 @@ function fandom_overview(lang, msg, wiki, reaction, spoiler) {
 			}
 		}, error => {
 			console.log( '- Error while getting the wiki details: ' + error );
-			msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Statistics', '', '', body.query.general) + '>' + spoiler );
+			msg.sendChannelError( spoiler + '<' + wiki.updateWiki(body.query.general).toLink(body.query.pages['-1'].title) + '>' + spoiler );
 			
 			if ( reaction ) reaction.removeEmoji();
 		} );

+ 4 - 4
cmds/wiki/fandom/random.js

@@ -6,7 +6,7 @@ const gamepedia_random = require('../gamepedia/random.js').run;
  * Sends a random Fandom page.
  * @param {import('../../../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
- * @param {String} wiki - The wiki for the page.
+ * @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.
  */
@@ -31,11 +31,11 @@ function fandom_random(lang, msg, wiki, reaction, spoiler) {
 		}
 		else {
 			var querypage = Object.values(body.query.pages)[0];
-			var pagelink = wiki.toLink(querypage.title, '', '', body.query.general);
+			var pagelink = wiki.updateWiki(body.query.general).toLink(querypage.title);
 			var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink );
 			if ( querypage.title === body.query.general.mainpage && body.query.allmessages[0]['*'] ) {
 				embed.setDescription( body.query.allmessages[0]['*'] );
-				embed.setThumbnail( wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', body.query.general) );
+				embed.setThumbnail( wiki.toLink('Special:FilePath/Wiki-wordmark.png') );
 				
 				msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
 				
@@ -48,7 +48,7 @@ function fandom_random(lang, msg, wiki, reaction, spoiler) {
 				if ( descresponse.statusCode !== 200 || !descbody ) {
 					console.log( '- ' + descresponse.statusCode + ': Error while getting the description.' );
 				} else {
-					var thumbnail = wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', body.query.general);
+					var thumbnail = wiki.toLink('Special:FilePath/Wiki-wordmark.png');
 					var parser = new htmlparser.Parser( {
 						onopentag: (tagname, attribs) => {
 							if ( tagname === 'meta' && attribs.property === 'og:description' ) {

+ 4 - 4
cmds/wiki/fandom/search.js

@@ -5,7 +5,7 @@ const {MessageEmbed, Util} = require('discord.js');
  * @param {import('../../../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String} searchterm - The searchterm.
- * @param {String} wiki - The wiki for the search.
+ * @param {import('../../../util/wiki.js')} wiki - The wiki for the search.
  * @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.
@@ -15,10 +15,10 @@ function fandom_search(lang, msg, searchterm, wiki, query, reaction, spoiler) {
 		searchterm = searchterm.substring(0, 250);
 		msg.reactEmoji('⚠️');
 	}
-	var pagelink = wiki.toLink('Special:Search', 'search=' + searchterm.toSearch(), '', query.general);
+	var pagelink = wiki.toLink('Special:Search', {search:searchterm});
 	var embed = new MessageEmbed().setAuthor( query.general.sitename ).setTitle( '`' + searchterm + '`' ).setURL( pagelink );
 	if ( !searchterm.trim() ) {
-		pagelink = wiki.toLink('Special:Search', '', '', query.general);
+		pagelink = wiki.toLink('Special:Search');
 		embed.setTitle( 'Special:Search' ).setURL( pagelink );
 		msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
 		
@@ -35,7 +35,7 @@ function fandom_search(lang, msg, searchterm, wiki, query, reaction, spoiler) {
 			return;
 		}
 		body.items.forEach( result => {
-			description.push( '• [' + result.title + '](' + wiki.toLink(result.title, '', '', query.general, true) + ')' );
+			description.push( '• [' + result.title + '](' + wiki.toLink(result.title, '', '', true) + ')' );
 		} );
 		embed.setFooter( lang.get('search.results', body.total) );
 	}, error => {

+ 24 - 22
cmds/wiki/fandom/user.js

@@ -2,6 +2,7 @@ const htmlparser = require('htmlparser2');
 const {MessageEmbed} = require('discord.js');
 const global_block = require('../../../functions/global_block.js');
 const {timeoptions, usergroups} = require('../../../util/default.json');
+const {toMarkdown, toPlaintext} = require('../../../util/functions.js');
 
 /**
  * Processes a Fandom user.
@@ -9,8 +10,8 @@ const {timeoptions, usergroups} = require('../../../util/default.json');
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String} namespace - The user namespace on the wiki.
  * @param {String} username - The name of the user.
- * @param {String} wiki - The wiki for the page.
- * @param {String} querystring - The querystring for the link.
+ * @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 {Object} querypage - The user page on the wiki.
  * @param {String} contribs - The contributions page on the wiki.
@@ -30,7 +31,7 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 						if ( reaction ) reaction.removeEmoji();
 					}
 					else {
-						var pagelink = wiki.toLink(querypage.title, querystring.toTitle(), fragment);
+						var pagelink = wiki.toLink(querypage.title, querystring, fragment);
 						var embed = new MessageEmbed().setTitle( querypage.title.escapeFormatting() ).setURL( pagelink );
 						got.get( wiki.toDescLink(querypage.title), {
 							responseType: 'text'
@@ -67,7 +68,7 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 				}
 				else {
 					console.log( '- ' + response.statusCode + ': Error while getting the search results: ' + ( body && body.error && body.error.info ) );
-					msg.sendChannelError( spoiler + '<' + wiki.toLink(( querypage.noRedirect ? namespace : contribs ) + username, querystring.toTitle(), fragment) + '>' + spoiler );
+					msg.sendChannelError( spoiler + '<' + wiki.toLink(( querypage.noRedirect ? namespace : contribs ) + username, querystring, fragment) + '>' + spoiler );
 					
 					if ( reaction ) reaction.removeEmoji();
 				}
@@ -112,7 +113,7 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 						else if ( range >= 16 ) rangeprefix = username.replace( /^((?:\d{1,3}\.){2}).+$/, '$1' );
 					}
 				}
-				got.get( wiki + 'api.php?action=query&list=usercontribs&ucprop=&uclimit=50&ucuser=' + encodeURIComponent( username ) + '&format=json' ).then( ucresponse => {
+				got.get( wiki.updateWiki(body.query.general) + 'api.php?action=query&list=usercontribs&ucprop=&uclimit=50&ucuser=' + encodeURIComponent( username ) + '&format=json' ).then( ucresponse => {
 					var ucbody = ucresponse.body;
 					if ( rangeprefix && !username.includes( '/' ) ) username = rangeprefix;
 					if ( ucbody && ucbody.warnings ) log_warn(ucbody.warnings);
@@ -122,19 +123,19 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 						}
 						else {
 							console.log( '- ' + ucresponse.statusCode + ': Error while getting the search results: ' + ( ucbody && ucbody.error && ucbody.error.info ) );
-							msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring.toTitle(), fragment, body.query.general) + '>' + spoiler );
+							msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring, fragment) + '>' + spoiler );
 						}
 					}
 					else {
 						var editcount = [lang.get('user.info.editcount'), ( username.includes( '/' ) ? '~' : '' ) + ucbody.query.usercontribs.length + ( ucbody.continue ? '+' : '' )];
 						
-						var pagelink = wiki.toLink(namespace + username, querystring.toTitle(), fragment, body.query.general);
+						var pagelink = wiki.toLink(namespace + username, querystring, fragment);
 						if ( msg.showEmbed() ) {
 							var text = '<' + pagelink + '>';
-							var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( username ).setURL( pagelink ).addField( editcount[0], '[' + editcount[1] + '](' + wiki.toLink(contribs + username, '', '', body.query.general, true) + ')' );
+							var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( username ).setURL( pagelink ).addField( editcount[0], '[' + editcount[1] + '](' + wiki.toLink(contribs + username, '', '', true) + ')' );
 							if ( blocks.length ) {
-								block.text = block.text.replaceSave( /\$3/g, '[' + block.by.escapeFormatting() + '](' + wiki.toLink('User:' + block.by, '', '', body.query.general, true) + ')' );
-								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, block.reason.toMarkdown(wiki, body.query.general) );
+								block.text = block.text.replaceSave( /\$3/g, '[' + block.by.escapeFormatting() + '](' + wiki.toLink('User:' + block.by, '', '', true) + ')' );
+								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, toMarkdown(block.reason, wiki) );
 								embed.addField( block.header, block.text );
 							}
 						}
@@ -143,7 +144,7 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 							var text = '<' + pagelink + '>\n\n' + editcount.join(' ');
 							if ( blocks.length ) {
 								block.text = block.text.replaceSave( /\$3/g, block.by.escapeFormatting() );
-								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, block.reason.toPlaintext() );
+								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, toPlaintext(block.reason) );
 								text += '\n\n**' + block.header + '**\n' + block.text;
 							}
 						}
@@ -159,14 +160,14 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 				}, error => {
 					if ( rangeprefix && !username.includes( '/' ) ) username = rangeprefix;
 					console.log( '- Error while getting the search results: ' + error );
-					msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring.toTitle(), fragment, body.query.general) + '>' + spoiler );
+					msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring, fragment) + '>' + spoiler );
 				} ).finally( () => {
 					if ( reaction ) reaction.removeEmoji();
 				} );
 			}
 		}, error => {
 			console.log( '- Error while getting the search results: ' + error );
-			msg.sendChannelError( spoiler + '<' + wiki.toLink(( querypage.noRedirect ? namespace : contribs ) + username, querystring.toTitle(), fragment) + '>' + spoiler );
+			msg.sendChannelError( spoiler + '<' + wiki.toLink(( querypage.noRedirect ? namespace : contribs ) + username, querystring, fragment) + '>' + spoiler );
 			
 			if ( reaction ) reaction.removeEmoji();
 		} );
@@ -176,11 +177,12 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 			if ( body && body.warnings ) log_warn(body.warnings);
 			if ( response.statusCode !== 200 || !body || !body.query || !body.query.users ) {
 				console.log( '- ' + response.statusCode + ': Error while getting the search results: ' + ( body && body.error && body.error.info ) );
-				msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring.toTitle(), fragment) + '>' + spoiler );
+				msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring, fragment) + '>' + spoiler );
 				
 				if ( reaction ) reaction.removeEmoji();
 			}
 			else {
+				wiki.updateWiki(body.query.general);
 				var queryuser = body.query.users[0];
 				if ( !queryuser ) {
 					if ( querypage.missing !== undefined || querypage.ns === -1 ) {
@@ -189,7 +191,7 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 						if ( reaction ) reaction.removeEmoji();
 					}
 					else {
-						var pagelink = wiki.toLink(querypage.title, querystring.toTitle(), fragment, body.query.general);
+						var pagelink = wiki.toLink(querypage.title, querystring, fragment);
 						var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink );
 						got.get( wiki.toDescLink(querypage.title), {
 							responseType: 'text'
@@ -198,7 +200,7 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 							if ( descresponse.statusCode !== 200 || !descbody ) {
 								console.log( '- ' + descresponse.statusCode + ': Error while getting the description.' );
 							} else {
-								var thumbnail = wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', body.query.general);
+								var thumbnail = wiki.toLink('Special:FilePath/Wiki-wordmark.png');
 								var parser = new htmlparser.Parser( {
 									onopentag: (tagname, attribs) => {
 										if ( tagname === 'meta' && attribs.property === 'og:description' ) {
@@ -268,10 +270,10 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 						reason: blockreason
 					};
 					
-					var pagelink = wiki.toLink(namespace + username, querystring.toTitle(), fragment, body.query.general);
+					var pagelink = wiki.toLink(namespace + username, querystring, fragment);
 					if ( msg.showEmbed() ) {
 						var text = '<' + pagelink + '>';
-						var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( username.escapeFormatting() ).setURL( pagelink ).addField( editcount[0], '[' + editcount[1] + '](' + wiki.toLink(contribs + username, '', '', body.query.general, true) + ')', true ).addField( group[0], group.slice(1).join(',\n'), true ).addField( gender[0], gender[1], true ).addField( registration[0], registration[1], true );
+						var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( username.escapeFormatting() ).setURL( pagelink ).addField( editcount[0], '[' + editcount[1] + '](' + wiki.toLink(contribs + username, '', '', true) + ')', true ).addField( group[0], group.slice(1).join(',\n'), true ).addField( gender[0], gender[1], true ).addField( registration[0], registration[1], true );
 					}
 					else {
 						var embed = {};
@@ -320,15 +322,15 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 					} ).finally( () => {
 						if ( msg.showEmbed() ) {
 							if ( isBlocked ) {
-								block.text = block.text.replaceSave( /\$3/g, '[' + block.by.escapeFormatting() + '](' + wiki.toLink('User:' + block.by, '', '', body.query.general, true) + ')' );
-								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, block.reason.toMarkdown(wiki, body.query.general) );
+								block.text = block.text.replaceSave( /\$3/g, '[' + block.by.escapeFormatting() + '](' + wiki.toLink('User:' + block.by, '', '', true) + ')' );
+								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, toMarkdown(block.reason, wiki) );
 								embed.addField( block.header, block.text );
 							}
 						}
 						else {
 							if ( isBlocked ) {
 								block.text = block.text.replaceSave( /\$3/g, block.by.escapeFormatting() );
-								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, block.reason.toPlaintext() );
+								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, toPlaintext(block.reason) );
 								text += '\n\n**' + block.header + '**\n' + block.text;
 							}
 						}
@@ -347,7 +349,7 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 			}
 		}, error => {
 			console.log( '- Error while getting the search results: ' + error );
-			msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring.toTitle(), fragment) + '>' + spoiler );
+			msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring, fragment) + '>' + spoiler );
 			
 			if ( reaction ) reaction.removeEmoji();
 		} );

+ 319 - 306
cmds/wiki/gamepedia.js

@@ -3,6 +3,7 @@ const {MessageEmbed} = require('discord.js');
 const parse_page = require('../../functions/parse_page.js');
 const extract_desc = require('../../util/extract_desc.js');
 const {limit: {interwiki: interwikiLimit}, wikiProjects} = require('../../util/default.json');
+const Wiki = require('../../util/wiki.js');
 
 const fs = require('fs');
 var fn = {
@@ -30,24 +31,24 @@ fs.readdir( './cmds/minecraft', (error, files) => {
  * @param {import('../../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String} title - The page title.
- * @param {String} wiki - The wiki for the page.
+ * @param {Wiki} wiki - The wiki for the page.
  * @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 {String} [querystring] - The querystring for the link.
+ * @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 = '', fragment = '', interwiki = '', selfcall = 0) {
+function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '', querystring = new URLSearchParams(), fragment = '', interwiki = '', selfcall = 0) {
 	var full_title = title;
 	if ( title.includes( '#' ) ) {
 		fragment = title.split('#').slice(1).join('#');
 		title = title.split('#')[0];
 	}
 	if ( /\?\w+=/.test(title) ) {
-		var querystart = title.search(/\?\w+=/);
-		querystring = title.substring(querystart + 1) + ( querystring ? '&' + querystring : '' );
+		let querystart = title.search(/\?\w+=/);
+		querystring = new URLSearchParams(querystring + '&' + title.substring(querystart + 1));
 		title = title.substring(0, querystart);
 	}
 	if ( title.length > 250 ) {
@@ -58,298 +59,124 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 	var aliasInvoke = ( lang.aliases[invoke] || invoke );
 	var args = title.split(' ').slice(1);
 	
-	if ( !msg.notMinecraft && wiki === lang.get('minecraft.link') && ( aliasInvoke in minecraft || invoke.startsWith( '/' ) ) ) {
+	if ( !msg.notMinecraft && wiki.href === lang.get('minecraft.link') && ( aliasInvoke in minecraft || invoke.startsWith( '/' ) ) ) {
 		minecraft.WIKI = this;
 		if ( aliasInvoke in minecraft ) minecraft[aliasInvoke](lang, msg, args, title, cmd, querystring, fragment, reaction, spoiler);
 		else minecraft.SYNTAX(lang, msg, invoke.substring(1), args, title, cmd, querystring, fragment, reaction, spoiler);
+		return;
 	}
-	else if ( aliasInvoke === 'random' && !args.join('') && !querystring && !fragment ) fn.random(lang, msg, wiki, reaction, spoiler);
-	else if ( aliasInvoke === 'overview' && !args.join('') && !querystring && !fragment ) fn.overview(lang, msg, wiki, reaction, spoiler);
-	else if ( aliasInvoke === 'test' && !args.join('') && !querystring && !fragment ) {
+	if ( aliasInvoke === 'random' && !args.join('') && !querystring.toString() && !fragment ) {
+		return fn.random(lang, msg, wiki, reaction, spoiler);
+	}
+	if ( aliasInvoke === 'overview' && !args.join('') && !querystring.toString() && !fragment ) {
+		return fn.overview(lang, msg, wiki, reaction, spoiler);
+	}
+	if ( aliasInvoke === 'test' && !args.join('') && !querystring.toString() && !fragment ) {
 		this.test(lang, msg, args, '', wiki);
 		if ( reaction ) reaction.removeEmoji();
+		return;
 	}
-	else if ( aliasInvoke === 'page' ) {
-		msg.sendChannel( spoiler + '<' + wiki.toLink(args.join('_'), querystring.toTitle(), fragment) + '>' + spoiler );
+	if ( aliasInvoke === 'page' ) {
+		msg.sendChannel( spoiler + '<' + wiki.toLink(args.join('_'), querystring, fragment) + '>' + spoiler );
 		if ( reaction ) reaction.removeEmoji();
+		return;
 	}
-	else if ( aliasInvoke === 'diff' && args.join('') && !querystring && !fragment ) fn.diff(lang, msg, args, wiki, reaction, spoiler);
-	else {
-		var noRedirect = ( /(?:^|&)redirect=no(?:&|$)/.test(querystring) || /(?:^|&)action=(?!view(?:&|$))/.test(querystring) );
-		got.get( wiki + 'api.php?action=query&meta=siteinfo&siprop=general|namespaces|specialpagealiases&iwurl=true' + ( noRedirect ? '' : '&redirects=true' ) + '&prop=pageimages|categoryinfo|pageprops|extracts&piprop=original|name&ppprop=description|displaytitle&explaintext=true&exsectionformat=raw&exlimit=1&titles=%1F' + encodeURIComponent( 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.replace( /@(here|everyone)/g, '%40$1' ) + fragment + ' ' + spoiler );
-				else if ( wiki.noWiki(response.url) || response.statusCode === 410 ) {
-					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( ( querystring || fragment || !title ? title : 'Special:Search' ), ( querystring || fragment || !title ? querystring.toTitle() : 'search=' + title.toSearch() ), fragment) + '>' + spoiler );
-				}
-				
-				if ( reaction ) reaction.removeEmoji();
+	if ( aliasInvoke === 'diff' && args.join('') && !querystring.toString() && !fragment ) {
+		return fn.diff(lang, msg, args, wiki, reaction, spoiler);
+	}
+	var noRedirect = ( querystring.getAll('redirect').pop() === 'no' || ( querystring.has('action') && querystring.getAll('action').pop() !== 'view' ) );
+	got.get( wiki + 'api.php?action=query&meta=siteinfo&siprop=general|namespaces|specialpagealiases&iwurl=true' + ( noRedirect ? '' : '&redirects=true' ) + '&prop=pageimages|categoryinfo|pageprops|extracts&piprop=original|name&ppprop=description|displaytitle|page_image_free&explaintext=true&exsectionformat=raw&exlimit=1&converttitles=true&titles=%1F' + encodeURIComponent( 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 );
+			else if ( wiki.noWiki(response.url) || response.statusCode === 410 ) {
+				console.log( '- This wiki doesn\'t exist!' );
+				msg.reactEmoji('nowiki');
 			}
-			else if ( aliasInvoke === 'search' ) {
-				fn.search(lang, msg, full_title.split(' ').slice(1).join(' '), wiki, body.query, reaction, spoiler);
+			else {
+				console.log( '- ' + response.statusCode + ': Error while getting the search results: ' + ( body && body.error && body.error.info ) );
+				msg.sendChannelError( spoiler + '<' + wiki.toLink( ( querystring.toString() || fragment || !title ? title : 'Special:Search' ), ( querystring.toString() || fragment || !title ? querystring : {search:title} ), fragment) + '>' + spoiler );
 			}
-			else if ( aliasInvoke === 'discussion' && wiki.isFandom() && !querystring && !fragment ) {
-				fn.discussion(lang, msg, wiki, args.join(' '), body.query, reaction, spoiler);
+			
+			if ( reaction ) reaction.removeEmoji();
+			return;
+		}
+		wiki.updateWiki(body.query.general);
+		if ( aliasInvoke === 'search' ) {
+			return fn.search(lang, msg, full_title.split(' ').slice(1).join(' '), wiki, body.query, reaction, spoiler);
+		}
+		if ( aliasInvoke === 'discussion' && wiki.isFandom() && !querystring.toString() && !fragment ) {
+			return fn.discussion(lang, msg, wiki, args.join(' '), body.query, reaction, spoiler);
+		}
+		if ( body.query.pages ) {
+			var querypages = Object.values(body.query.pages);
+			var querypage = querypages[0];
+			if ( body.query.redirects && body.query.redirects[0].from.split(':')[0] === body.query.namespaces['-1']['*'] && body.query.specialpagealiases.filter( sp => ['Mypage','Mytalk','MyLanguage'].includes( sp.realname ) ).map( sp => sp.aliases[0] ).includes( body.query.redirects[0].from.split(':').slice(1).join(':').split('/')[0].replace( / /g, '_' ) ) ) {
+				querypage.title = body.query.redirects[0].from;
+				delete body.query.redirects[0].tofragment;
+				delete querypage.missing;
+				querypage.ns = -1;
+				querypage.special = '';
 			}
-			else {
-				if ( body.query.pages ) {
-					var querypages = Object.values(body.query.pages);
-					var querypage = querypages[0];
-					if ( body.query.redirects && body.query.redirects[0].from.split(':')[0] === body.query.namespaces['-1']['*'] && body.query.specialpagealiases.filter( sp => ['Mypage','Mytalk','MyLanguage'].includes( sp.realname ) ).map( sp => sp.aliases[0] ).includes( body.query.redirects[0].from.split(':').slice(1).join(':').split('/')[0].replace( / /g, '_' ) ) ) {
-						querypage.title = body.query.redirects[0].from;
-						delete body.query.redirects[0].tofragment;
-						delete querypage.missing;
-						querypage.ns = -1;
-						querypage.special = '';
-					}
-					
-					var contribs = body.query.namespaces['-1']['*'] + ':' + body.query.specialpagealiases.find( sp => sp.realname === 'Contributions' ).aliases[0] + '/';
-					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;
-						fn.user(lang, msg, userparts[0].toTitle() + ':', userparts.slice(1).join(':'), wiki, querystring, fragment, querypage, contribs.toTitle(), reaction, spoiler);
-					}
-					else if ( querypage.ns === -1 && querypage.title.startsWith( contribs ) && querypage.title.length > contribs.length ) {
-						var username = querypage.title.split('/').slice(1).join('/');
-						got.get( wiki + 'api.php?action=query&titles=User:' + encodeURIComponent( username ) + '&format=json' ).then( uresponse => {
-							var ubody = uresponse.body;
-							if ( uresponse.statusCode !== 200 || !ubody || ubody.batchcomplete === undefined || !ubody.query ) {
-								console.log( '- ' + uresponse.statusCode + ': Error while getting the user: ' + ( ubody && ubody.error && ubody.error.info ) );
-								msg.sendChannelError( spoiler + '<' + wiki.toLink(contribs + username, querystring.toTitle(), fragment, body.query.general) + '>' + spoiler );
-								
-								if ( reaction ) reaction.removeEmoji();
-							}
-							else {
-								querypage = Object.values(ubody.query.pages)[0];
-								if ( querypage.ns === 2 ) {
-									username = querypage.title.split(':').slice(1).join(':');
-									querypage.title = contribs + username;
-									delete querypage.missing;
-									querypage.ns = -1;
-									querypage.special = '';
-									querypage.noRedirect = noRedirect;
-									fn.user(lang, msg, contribs.toTitle(), username, wiki, querystring, fragment, querypage, contribs.toTitle(), reaction, spoiler);
-								}
-								else {
-									msg.reactEmoji('error');
-									
-									if ( reaction ) reaction.removeEmoji();
-								}
-							}
-						}, error => {
-							console.log( '- Error while getting the user: ' + error );
-							msg.sendChannelError( spoiler + '<' + wiki.toLink(contribs + username, querystring.toTitle(), fragment, body.query.general) + '>' + spoiler );
-							
-							if ( reaction ) reaction.removeEmoji();
-						} );
-					}
-					else if ( ( querypage.missing !== undefined && querypage.known === undefined && !( noRedirect || querypage.categoryinfo ) ) || querypage.invalid !== undefined ) {
-						got.get( wiki + 'api.php?action=query&prop=pageimages|categoryinfo|pageprops|extracts&piprop=original|name&ppprop=description|displaytitle&explaintext=true&exsectionformat=raw&exlimit=1&generator=search&gsrnamespace=4|12|14|' + Object.values(body.query.namespaces).filter( ns => ns.content !== undefined ).map( ns => ns.id ).join('|') + '&gsrlimit=1&gsrsearch=' + encodeURIComponent( title ) + '&format=json' ).then( srresponse => {
-							var srbody = srresponse.body;
-							if ( srbody && 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 ) );
-								msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Search', 'search=' + title.toSearch(), '', body.query.general) + '>' + spoiler );
-							}
-							else {
-								if ( !srbody.query ) {
-									msg.reactEmoji('🤷');
-								}
-								else {
-									querypage = Object.values(srbody.query.pages)[0];
-									var pagelink = wiki.toLink(querypage.title, querystring.toTitle(), fragment, body.query.general);
-									var text = '';
-									var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).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.setDescription( extract[0] );
-										if ( extract[2].length ) embed.addField( extract[1], extract[2] );
-									}
-									if ( querypage.pageprops && querypage.pageprops.description ) {
-										var description = htmlToPlain( querypage.pageprops.description );
-										if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
-										embed.setDescription( description );
-									}
-									if ( querypage.pageimage && querypage.original && querypage.title !== body.query.general.mainpage ) {
-										var pageimage = querypage.original.source;
-										if ( querypage.ns === 6 ) {
-											if ( msg.showEmbed() && /\.(?:png|jpg|jpeg|gif)$/.test(querypage.pageimage.toLowerCase()) ) embed.setImage( pageimage );
-											else if ( msg.uploadFiles() ) embed.attachFiles( [{attachment:pageimage,name:( spoiler ? 'SPOILER ' : '' ) + querypage.pageimage}] );
-										} else embed.setThumbnail( pageimage );
-									} else embed.setThumbnail( logoToURL(body.query.general) );
-									
-									var prefix = ( msg.channel.type === 'text' && patreons[msg.guild.id] || process.env.prefix );
-									var linksuffix = ( querystring ? '?' + querystring : '' ) + ( fragment ? '#' + fragment : '' );
-									if ( title.replace( /\-/g, ' ' ).toTitle().toLowerCase() === querypage.title.replace( /\-/g, ' ' ).toTitle().toLowerCase() ) {
-										text = '';
-									}
-									else if ( !srbody.continue ) {
-										text = '\n' + lang.get('search.infopage', '`' + prefix + cmd + ( lang.localNames.page || 'page' ) + ' ' + title + linksuffix + '`');
-									}
-									else {
-										text = '\n' + lang.get('search.infosearch', '`' + prefix + cmd + ( lang.localNames.page || 'page' ) + ' ' + title + linksuffix + '`', '`' + prefix + cmd + ( lang.localNames.search || 'search' ) + ' ' + title + linksuffix + '`');
-									}
-									
-									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));
-										}
-										if ( querypage.categoryinfo.files > 0 ) {
-											category.push(lang.get('search.category.files', querypage.categoryinfo.files));
-										}
-										if ( querypage.categoryinfo.subcats > 0 ) {
-											category.push(lang.get('search.category.subcats', querypage.categoryinfo.subcats));
-										}
-										if ( msg.showEmbed() ) embed.addField( category[0], category.slice(1).join('\n') );
-										else text += '\n\n' + category.join('\n');
-									}
-						
-									msg.sendChannel( spoiler + '<' + pagelink + '>' + text + spoiler, {embed} ).then( message => parse_page(message, querypage.title, embed, wiki, ( querypage.title === body.query.general.mainpage ? '' : logoToURL(body.query.general) )) );
-								}
-							}
-						}, error => {
-							console.log( '- Error while getting the search results: ' + error );
-							msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Search', 'search=' + title.toSearch(), '', body.query.general) + '>' + spoiler );
-						} ).finally( () => {
-							if ( reaction ) reaction.removeEmoji();
-						} );
-					}
-					else if ( querypage.ns === -1 ) {
-						var pagelink = wiki.toLink(querypage.title, querystring.toTitle(), fragment, body.query.general);
-						var embed =  new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink ).setThumbnail( logoToURL(body.query.general) );
-						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();
-						fn.special_page(lang, msg, querypage.title, specialpage, embed, wiki, reaction, spoiler);
-					}
-					else if ( querypage.ns === -2 ) {
-						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.toTitle(), fragment, body.query.general);
-						var embed =  new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink ).setDescription( '[' + lang.get('search.media') + '](' + wiki.toLink(querypage.title, '', '', body.query.general, 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}] );
-						
-						msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
+			
+			var contribs = body.query.namespaces['-1']['*'] + ':' + body.query.specialpagealiases.find( sp => sp.realname === 'Contributions' ).aliases[0] + '/';
+			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;
+				fn.user(lang, msg, userparts[0] + ':', userparts.slice(1).join(':'), wiki, querystring, fragment, querypage, contribs, reaction, spoiler);
+			}
+			else if ( querypage.ns === -1 && querypage.title.startsWith( contribs ) && querypage.title.length > contribs.length ) {
+				var username = querypage.title.split('/').slice(1).join('/');
+				got.get( wiki + 'api.php?action=query&titles=User:' + encodeURIComponent( username ) + '&format=json' ).then( uresponse => {
+					var ubody = uresponse.body;
+					if ( uresponse.statusCode !== 200 || !ubody || ubody.batchcomplete === undefined || !ubody.query ) {
+						console.log( '- ' + uresponse.statusCode + ': Error while getting the user: ' + ( ubody && ubody.error && ubody.error.info ) );
+						msg.sendChannelError( spoiler + '<' + wiki.toLink(contribs + username, querystring, fragment) + '>' + spoiler );
 						
 						if ( reaction ) reaction.removeEmoji();
 					}
 					else {
-						var pagelink = wiki.toLink(querypage.title, querystring.toTitle(), ( fragment || ( body.query.redirects && body.query.redirects[0].tofragment ) || '' ), body.query.general);
-						var text = '';
-						var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).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 );
+						querypage = Object.values(ubody.query.pages)[0];
+						if ( querypage.ns === 2 ) {
+							username = querypage.title.split(':').slice(1).join(':');
+							querypage.title = contribs + username;
+							delete querypage.missing;
+							querypage.ns = -1;
+							querypage.special = '';
+							querypage.noRedirect = noRedirect;
+							fn.user(lang, msg, contribs, username, wiki, querystring, fragment, querypage, contribs, reaction, spoiler);
 						}
-						if ( querypage.extract ) {
-							var extract = extract_desc(querypage.extract, ( fragment || ( body.query.redirects && body.query.redirects[0].tofragment ) || '' ));
-							embed.setDescription( extract[0] );
-							if ( extract[2].length ) embed.addField( extract[1], extract[2] );
-						}
-						if ( querypage.pageprops && querypage.pageprops.description ) {
-							var description = htmlToPlain( querypage.pageprops.description );
-							if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
-							embed.setDescription( description );
-						}
-						if ( querypage.pageimage && querypage.original && querypage.title !== body.query.general.mainpage ) {
-							var pageimage = querypage.original.source;
-							if ( querypage.ns === 6 ) {
-								if ( msg.showEmbed() && /\.(?:png|jpg|jpeg|gif)$/.test(querypage.pageimage.toLowerCase()) ) embed.setImage( pageimage );
-								else if ( msg.uploadFiles() ) embed.attachFiles( [{attachment:pageimage,name:( spoiler ? 'SPOILER ' : '' ) + querypage.pageimage}] );
-							} else embed.setThumbnail( pageimage );
-						} else embed.setThumbnail( logoToURL(body.query.general) );
-						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));
-							}
-							if ( querypage.categoryinfo.files > 0 ) {
-								category.push(lang.get('search.category.files', querypage.categoryinfo.files));
-							}
-							if ( querypage.categoryinfo.subcats > 0 ) {
-								category.push(lang.get('search.category.subcats', querypage.categoryinfo.subcats));
-							}
-							if ( msg.showEmbed() ) embed.addField( category[0], category.slice(1).join('\n') );
-							else text += '\n\n' + category.join('\n');
+						else {
+							msg.reactEmoji('error');
+							
+							if ( reaction ) reaction.removeEmoji();
 						}
-						
-						msg.sendChannel( spoiler + '<' + pagelink + '>' + text + spoiler, {embed} ).then( message => parse_page(message, querypage.title, embed, wiki, ( querypage.title === body.query.general.mainpage ? '' : logoToURL(body.query.general) )) );
-						
-						if ( reaction ) reaction.removeEmoji();
 					}
-				}
-				else if ( body.query.interwiki ) {
-					if ( msg.channel.type === 'text' && pause[msg.guild.id] ) {
-						if ( reaction ) reaction.removeEmoji();
-						console.log( '- Aborted, paused.' );
-						return;
+				}, error => {
+					console.log( '- Error while getting the user: ' + error );
+					msg.sendChannelError( spoiler + '<' + wiki.toLink(contribs + username, querystring, fragment) + '>' + spoiler );
+					
+					if ( reaction ) reaction.removeEmoji();
+				} );
+			}
+			else if ( ( querypage.missing !== undefined && querypage.known === undefined && !( noRedirect || querypage.categoryinfo ) ) || querypage.invalid !== undefined ) {
+				got.get( wiki + 'api.php?action=query&prop=pageimages|categoryinfo|pageprops|extracts&piprop=original|name&ppprop=description|displaytitle|page_image_free&explaintext=true&exsectionformat=raw&exlimit=1&generator=search&gsrnamespace=4|12|14|' + Object.values(body.query.namespaces).filter( ns => ns.content !== undefined ).map( ns => ns.id ).join('|') + '&gsrlimit=1&gsrsearch=' + encodeURIComponent( title ) + '&format=json' ).then( srresponse => {
+					var srbody = srresponse.body;
+					if ( srbody && 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 ) );
+						msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Search', {search:title}) + '>' + spoiler );
 					}
-					interwiki = body.query.interwiki[0].url;
-					var maxselfcall = interwikiLimit[( msg?.guild?.id in patreons ? 'patreon' : 'default' )];
-					if ( selfcall < maxselfcall && /^(?:https?:)?\/\//.test(interwiki) ) {
-						selfcall++;
-						var regex = interwiki.match( /^(?:https?:)?\/\/([a-z\d-]{1,50})\.gamepedia\.com(?:\/|$)/ );
-						if ( regex ) {
-							let iwtitle = decodeURIComponent( interwiki.replace( regex[0], '' ) ).replace( /\_/g, ' ' );
-							this.gamepedia(lang, msg, iwtitle, 'https://' + regex[1] + '.gamepedia.com/', '!' + regex[1] + ' ', reaction, spoiler, querystring, fragment, interwiki, selfcall);
-							return;
-						}
-						regex = interwiki.match( /^(?:https?:)?\/\/(([a-z\d-]{1,50})\.(?:fandom\.com|wikia\.org)(?:(?!\/wiki\/)\/([a-z-]{2,12}))?)(?:\/wiki\/|\/?$)/ );
-						if ( regex ) {
-							let iwtitle = decodeURIComponent( interwiki.replace( regex[0], '' ) ).replace( /\_/g, ' ' );
-							this.fandom(lang, msg, iwtitle, 'https://' + regex[1] + '/', ( regex[1].includes( '.wikia.org' ) ? '??' : '?' ) + ( regex[3] ? regex[3] + '.' : '' ) + regex[2] + ' ', reaction, spoiler, querystring, fragment, interwiki, selfcall);
-							return;
-						}
-						let project = wikiProjects.find( project => interwiki.split('/')[2].endsWith( project.name ) );
-						if ( project ) {
-							regex = interwiki.match( new RegExp( '^(?:https?:)?//' + project.regex + `(?:${project.articlePath}|/?$)` ) );
-							if ( regex ) {
-								let iwtitle = decodeURIComponent( interwiki.replace( regex[0], '' ) ).replace( /\_/g, ' ' );
-								this.gamepedia(lang, msg, iwtitle, 'https://' + regex[1] + project.scriptPath, cmd + body.query.interwiki[0].iw + ':', reaction, spoiler, querystring, fragment, interwiki, selfcall);
-								return;
-							}
+					else {
+						if ( !srbody.query ) {
+							msg.reactEmoji('🤷');
 						}
-					}
-					if ( fragment ) fragment = '#' + fragment.toSection();
-					if ( interwiki.includes( '#' ) ) {
-						if ( !fragment ) fragment = '#' + interwiki.split('#').slice(1).join('#');
-						interwiki = interwiki.split('#')[0];
-					}
-					if ( querystring ) interwiki += ( interwiki.includes( '?' ) ? '&' : '?' ) + querystring.toTitle();
-					msg.sendChannel( spoiler + ' ' + interwiki.replace( /@(here|everyone)/g, '%40$1' ) + fragment + ' ' + spoiler ).then( message => {
-						if ( message && selfcall === maxselfcall ) message.reactEmoji('⚠️');
-					} );
-					if ( reaction ) reaction.removeEmoji();
-				}
-				else {
-					var pagelink = wiki.toLink(body.query.general.mainpage, querystring.toTitle(), fragment, body.query.general);
-					var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( body.query.general.mainpage.escapeFormatting() ).setURL( pagelink ).setThumbnail( logoToURL(body.query.general) );
-					got.get( wiki + 'api.php?action=query' + ( noRedirect ? '' : '&redirects=true' ) + '&prop=pageprops|extracts&ppprop=description|displaytitle&explaintext=true&exsectionformat=raw&exlimit=1&titles=' + encodeURIComponent( body.query.general.mainpage ) + '&format=json' ).then( mpresponse => {
-						var mpbody = mpresponse.body;
-						if ( mpbody && mpbody.warnings ) log_warn(body.warnings);
-						if ( mpresponse.statusCode !== 200 || !mpbody || mpbody.batchcomplete === undefined || !mpbody.query ) {
-							console.log( '- ' + mpresponse.statusCode + ': Error while getting the main page: ' + ( mpbody && mpbody.error && mpbody.error.info ) );
-						} else {
-							var querypage = Object.values(mpbody.query.pages)[0];
+						else {
+							querypage = Object.values(srbody.query.pages)[0];
+							var pagelink = wiki.toLink(querypage.title, querystring, fragment);
+							var text = '';
+							var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink );
 							if ( querypage.pageprops && querypage.pageprops.displaytitle ) {
 								var displaytitle = htmlToDiscord( querypage.pageprops.displaytitle );
 								if ( displaytitle.length > 250 ) displaytitle = displaytitle.substring(0, 250) + '\u2026';
@@ -365,42 +192,228 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 								if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
 								embed.setDescription( description );
 							}
+							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 );
+							}
+							else if ( querypage.pageimage && querypage.original ) {
+								embed.setThumbnail( querypage.original.source );
+							}
+							else if ( querypage.pageprops && querypage.pageprops.page_image_free ) {
+								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 );
+							
+							var prefix = ( msg.channel.type === 'text' && patreons[msg.guild.id] || process.env.prefix );
+							var linksuffix = ( querystring.toString() ? '?' + querystring : '' ) + ( fragment ? '#' + fragment : '' );
+							if ( title.replace( /[_-]/g, ' ' ).toLowerCase() === querypage.title.replace( /-/g, ' ' ).toLowerCase() ) {
+								text = '';
+							}
+							else if ( !srbody.continue ) {
+								text = '\n' + lang.get('search.infopage', '`' + prefix + cmd + ( lang.localNames.page || 'page' ) + ' ' + title + linksuffix + '`');
+							}
+							else {
+								text = '\n' + lang.get('search.infosearch', '`' + prefix + cmd + ( lang.localNames.page || 'page' ) + ' ' + title + linksuffix + '`', '`' + prefix + cmd + ( lang.localNames.search || 'search' ) + ' ' + title + linksuffix + '`');
+							}
+							
+							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));
+								}
+								if ( querypage.categoryinfo.files > 0 ) {
+									category.push(lang.get('search.category.files', querypage.categoryinfo.files));
+								}
+								if ( querypage.categoryinfo.subcats > 0 ) {
+									category.push(lang.get('search.category.subcats', querypage.categoryinfo.subcats));
+								}
+								if ( msg.showEmbed() ) embed.addField( category[0], category.slice(1).join('\n') );
+								else text += '\n\n' + category.join('\n');
+							}
+				
+							msg.sendChannel( spoiler + '<' + pagelink + '>' + text + spoiler, {embed} ).then( message => parse_page(message, querypage.title, embed, wiki, ( querypage.title === body.query.general.mainpage ? '' : new URL(body.query.general.logo, wiki).href )) );
 						}
-					}, error => {
-						console.log( '- Error while getting the main page: ' + error );
-					} ).finally( () => {
-						msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} ).then( message => parse_page(message, body.query.general.mainpage, embed, wiki, '') );
-						
-						if ( reaction ) reaction.removeEmoji();
-					} );
-				}
+					}
+				}, error => {
+					console.log( '- Error while getting the search results: ' + error );
+					msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Search', {search:title}) + '>' + spoiler );
+				} ).finally( () => {
+					if ( reaction ) reaction.removeEmoji();
+				} );
 			}
-		}, error => {
-			if ( interwiki ) msg.sendChannel( spoiler + ' ' + interwiki.replace( /@(here|everyone)/g, '%40$1' ) + fragment + ' ' + spoiler );
-			else if ( wiki.noWiki(error.message) ) {
-				console.log( '- This wiki doesn\'t exist!' );
-				msg.reactEmoji('nowiki');
+			else if ( querypage.ns === -1 ) {
+				var pagelink = wiki.toLink(querypage.title, querystring, fragment);
+				var embed =  new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink ).setThumbnail( new URL(body.query.general.logo, wiki).href );
+				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();
+				fn.special_page(lang, msg, querypage.title, specialpage, embed, wiki, reaction, spoiler);
+			}
+			else if ( querypage.ns === -2 ) {
+				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( querypage.title.escapeFormatting() ).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}] );
+				
+				msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} );
+				
+				if ( reaction ) reaction.removeEmoji();
 			}
 			else {
-				console.log( '- Error while getting the search results: ' + error );
-				msg.sendChannelError( spoiler + '<' + wiki.toLink( ( querystring || fragment || !title ? title : 'Special:Search' ), ( querystring || fragment || !title ? querystring.toTitle() : 'search=' + title.toSearch() ), fragment) + '>' + spoiler );
+				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( querypage.title.escapeFormatting() ).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 || ( body.query.redirects && body.query.redirects[0].tofragment ) || '' ));
+					embed.setDescription( extract[0] );
+					if ( extract[2].length ) embed.addField( extract[1], extract[2] );
+				}
+				if ( querypage.pageprops && querypage.pageprops.description ) {
+					var description = htmlToPlain( querypage.pageprops.description );
+					if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
+					embed.setDescription( description );
+				}
+				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 );
+				}
+				else if ( querypage.pageimage && querypage.original ) {
+					embed.setThumbnail( querypage.original.source );
+				}
+				else if ( querypage.pageprops && querypage.pageprops.page_image_free ) {
+					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));
+					}
+					if ( querypage.categoryinfo.files > 0 ) {
+						category.push(lang.get('search.category.files', querypage.categoryinfo.files));
+					}
+					if ( querypage.categoryinfo.subcats > 0 ) {
+						category.push(lang.get('search.category.subcats', querypage.categoryinfo.subcats));
+					}
+					if ( msg.showEmbed() ) embed.addField( category[0], category.slice(1).join('\n') );
+					else text += '\n\n' + category.join('\n');
+				}
+				
+				msg.sendChannel( spoiler + '<' + pagelink + '>' + text + spoiler, {embed} ).then( message => parse_page(message, querypage.title, embed, wiki, ( querypage.title === body.query.general.mainpage ? '' : new URL(body.query.general.logo, wiki).href )) );
+				
+				if ( reaction ) reaction.removeEmoji();
 			}
-			
+		}
+		else if ( body.query.interwiki ) {
+			if ( msg.channel.type === 'text' && pause[msg.guild.id] ) {
+				if ( reaction ) reaction.removeEmoji();
+				console.log( '- Aborted, paused.' );
+				return;
+			}
+			var iw = new URL(body.query.interwiki[0].url.replace( /\\/g, '%5C' ).replace( /@(here|everyone)/g, '%40$1' ), wiki);
+			querystring.forEach( (value, name) => {
+				iw.searchParams.append(name, value);
+			} );
+			if ( fragment ) iw.hash = Wiki.toSection(fragment);
+			else fragment = iw.hash.substring(1);
+			var maxselfcall = interwikiLimit[( msg?.guild?.id in patreons ? '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.gamepedia(lang, msg, iwtitle, new Wiki(iw.origin), cmd, reaction, spoiler, 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\/|\/?$)/ );
+					if ( regex ) {
+						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.fandom(lang, msg, iwtitle, new Wiki(iw.origin + path + '/'), cmd, reaction, spoiler, iw.searchParams, fragment, iw.href, selfcall);
+					}
+				}
+				let project = wikiProjects.find( project => iw.hostname.endsWith( project.name ) );
+				if ( project ) {
+					let regex = ( iw.host + iw.pathname ).match( new RegExp( '^' + project.regex + '(?:' + project.articlePath + '|/?$)' ) );
+					if ( regex ) {
+						let iwtitle = decodeURIComponent( iw.pathname.replace( regex[0], '' ) ).replace( /_/g, ' ' );
+						cmd = '!!' + regex[1] + ' ';
+						return this.gamepedia(lang, msg, iwtitle, new Wiki(iw.origin + project.scriptPath), cmd, reaction, spoiler, iw.searchParams, fragment, iw.href, selfcall);
+					}
+				}
+			}
+			msg.sendChannel( spoiler + ' ' + iw + ' ' + spoiler ).then( message => {
+				if ( message && selfcall === maxselfcall ) message.reactEmoji('⚠️');
+			} );
 			if ( reaction ) reaction.removeEmoji();
-		} );
-	}
-}
-
-/**
- * Turns the siteinfo logo into an URL.
- * @param {Object} arg - The siteinfo from the wiki.
- * @param {String} arg.logo - The logo from the wiki.
- * @param {String} arg.server - The server URL from the wiki.
- * @returns {String}
- */
-function logoToURL({logo, server: serverURL}) {
-	if ( !/^(?:https?:)?\/\//.test(logo) ) logo = serverURL + ( logo.startsWith( '/' ) ? '' : '/' ) + logo;
-	return logo.replace( /^(?:https?:)?\/\//, 'https://' );
+		}
+		else {
+			var pagelink = wiki.toLink(body.query.general.mainpage, querystring, fragment);
+			var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( body.query.general.mainpage.escapeFormatting() ).setURL( pagelink ).setThumbnail( new URL(body.query.general.logo, wiki).href );
+			got.get( wiki + 'api.php?action=query' + ( noRedirect ? '' : '&redirects=true' ) + '&prop=pageprops|extracts&ppprop=description|displaytitle&explaintext=true&exsectionformat=raw&exlimit=1&titles=' + encodeURIComponent( body.query.general.mainpage ) + '&format=json' ).then( mpresponse => {
+				var mpbody = mpresponse.body;
+				if ( mpbody && mpbody.warnings ) log_warn(body.warnings);
+				if ( mpresponse.statusCode !== 200 || !mpbody || mpbody.batchcomplete === undefined || !mpbody.query ) {
+					console.log( '- ' + mpresponse.statusCode + ': Error while getting the main page: ' + ( mpbody && mpbody.error && mpbody.error.info ) );
+				} else {
+					var querypage = Object.values(mpbody.query.pages)[0];
+					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.setDescription( extract[0] );
+						if ( extract[2].length ) embed.addField( extract[1], extract[2] );
+					}
+					if ( querypage.pageprops && querypage.pageprops.description ) {
+						var description = htmlToPlain( querypage.pageprops.description );
+						if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
+						embed.setDescription( description );
+					}
+				}
+			}, error => {
+				console.log( '- Error while getting the main page: ' + error );
+			} ).finally( () => {
+				msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} ).then( message => parse_page(message, body.query.general.mainpage, embed, wiki, '') );
+				
+				if ( reaction ) reaction.removeEmoji();
+			} );
+		}
+	}, error => {
+		if ( interwiki ) msg.sendChannel( spoiler + ' ' + interwiki + ' ' + spoiler );
+		else if ( wiki.noWiki(error.message) ) {
+			console.log( '- This wiki doesn\'t exist!' );
+			msg.reactEmoji('nowiki');
+		}
+		else {
+			console.log( '- Error while getting the search results: ' + error );
+			msg.sendChannelError( spoiler + '<' + wiki.toLink( ( querystring.toString() || fragment || !title ? title : 'Special:Search' ), ( querystring.toString() || fragment || !title ? querystring : {search:title} ), fragment) + '>' + spoiler );
+		}
+		
+		if ( reaction ) reaction.removeEmoji();
+	} );
 }
 
 /**

+ 181 - 181
cmds/wiki/gamepedia/diff.js

@@ -1,13 +1,14 @@
 const htmlparser = require('htmlparser2');
 const {MessageEmbed} = require('discord.js');
 const {timeoptions} = require('../../../util/default.json');
+const {toFormatting} = require('../../../util/functions.js');
 
 /**
  * Processes a Gamepedia edit.
  * @param {import('../../../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
- * @param {String} wiki - The wiki for the edit.
+ * @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 {MessageEmbed} [embed] - The embed for the page.
@@ -83,7 +84,7 @@ function gamepedia_diff(lang, msg, args, wiki, reaction, spoiler, embed) {
 					}
 					else {
 						console.log( '- ' + response.statusCode + ': Error while getting the search results: ' + ( body && body.error && body.error.info ) );
-						msg.sendChannelError( spoiler + '<' + wiki.toLink(title, 'diff=' + relative + ( title ? '' : '&oldid=' + revision )) + '>' + spoiler );
+						msg.sendChannelError( spoiler + '<' + wiki.toLink(title, ( title ? {diff} : {diff,oldid:revision} )) + '>' + spoiler );
 					}
 					
 					if ( reaction ) reaction.removeEmoji();
@@ -215,7 +216,7 @@ function gamepedia_diff(lang, msg, args, wiki, reaction, spoiler, embed) {
  * @param {import('../../../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String[]} args - The command arguments.
- * @param {String} wiki - The wiki for the edit.
+ * @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 {String[]} [compare] - The edit difference.
@@ -236,211 +237,210 @@ 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') );
+		else if ( body.query.badrevids ) {
+			msg.replyMsg( lang.get('diff.badrev') );
+			
+			if ( reaction ) reaction.removeEmoji();
+		}
+		else if ( body.query.pages && !body.query.pages['-1'] ) {
+			wiki.updateWiki(body.query.general);
+			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 );
 				
 				if ( reaction ) reaction.removeEmoji();
 			}
-			else if ( body.query.pages && !body.query.pages['-1'] ) {
-				var pages = Object.values(body.query.pages);
-				if ( pages.length !== 1 ) {
-					msg.sendChannel( spoiler + '<' + wiki.toLink('Special:Diff/' + ( args[1] ? args[1] + '/' : '' ) + args[0], '', '', body.query.general) + '>' + spoiler );
-					
-					if ( reaction ) reaction.removeEmoji();
-				}
-				else {
-					var title = pages[0].title;
-					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') : revisions[0].user )];
-					var timestamp = [lang.get('diff.info.timestamp'), new Date(revisions[0].timestamp).toLocaleString(lang.get('dateformat'), timeoptions)];
-					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)];
-					var comment = [lang.get('diff.info.comment'), ( revisions[0].commenthidden !== undefined ? lang.get('diff.hidden') : ( revisions[0].comment ? revisions[0].comment.toFormatting(msg.showEmbed(), wiki, body.query.general, title) : 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(', ')];
+			else {
+				var title = pages[0].title;
+				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') : revisions[0].user )];
+				var timestamp = [lang.get('diff.info.timestamp'), new Date(revisions[0].timestamp).toLocaleString(lang.get('dateformat'), timeoptions)];
+				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)];
+				var comment = [lang.get('diff.info.comment'), ( revisions[0].commenthidden !== undefined ? lang.get('diff.hidden') : ( revisions[0].comment ? toFormatting(revisions[0].comment, msg.showEmbed(), wiki, title) : 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 pagelink = wiki.toLink(title, {diff,oldid});
+				if ( msg.showEmbed() ) {
+					var text = '<' + pagelink + '>';
+					var editorlink = '[' + editor[1] + '](' + wiki.toLink('User:' + editor[1], '', '', true) + ')';
+					if ( revisions[0].anon !== undefined ) {
+						editorlink = '[' + editor[1] + '](' + wiki.toLink('Special:Contributions/' + editor[1], '', '', true) + ')';
+					}
+					if ( editor[1] === lang.get('diff.hidden') ) editorlink = editor[1];
+					var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( ( title + '?diff=' + diff + '&oldid=' + oldid ).escapeFormatting() ).setURL( pagelink ).addField( editor[0], editorlink, true ).addField( size[0], size[1], true ).addField( comment[0], comment[1] ).setFooter( timestamp[1] );
+					if ( tags ) {
+						var taglink = '';
+						var tagtext = '';
+						var tagparser = new htmlparser.Parser( {
+							onopentag: (tagname, attribs) => {
+								if ( tagname === 'a' ) taglink = attribs.href;
+							},
+							ontext: (htmltext) => {
+								if ( taglink ) tagtext += '[' + htmltext.escapeFormatting() + '](' + taglink + ')'
+								else tagtext += htmltext.escapeFormatting();
+							},
+							onclosetag: (tagname) => {
+								if ( tagname === 'a' ) taglink = '';
+							}
+						}, {decodeEntities:true} );
+						tagparser.write( tags[1] );
+						tagparser.end();
+						embed.addField( tags[0], tagtext );
+					}
 					
-					var pagelink = wiki.toLink(title, 'diff=' + diff + '&oldid=' + oldid, '', body.query.general);
-					if ( msg.showEmbed() ) {
-						var text = '<' + pagelink + '>';
-						var editorlink = '[' + editor[1] + '](' + wiki.toLink('User:' + editor[1], '', '', body.query.general, true) + ')';
-						if ( revisions[0].anon !== undefined ) {
-							editorlink = '[' + editor[1] + '](' + wiki.toLink('Special:Contributions/' + editor[1], '', '', body.query.general, true) + ')';
+					var more = '\n__' + lang.get('diff.info.more') + '__';
+					if ( !compare && oldid ) got.get( wiki + 'api.php?action=compare&prop=diff&fromrev=' + oldid + '&torev=' + diff + '&format=json' ).then( cpresponse => {
+						var cpbody = cpresponse.body;
+						if ( cpbody && cpbody.warnings ) log_warn(cpbody.warnings);
+						if ( cpresponse.statusCode !== 200 || !cpbody || !cpbody.compare || cpbody.compare['*'] === undefined ) {
+							var noerror = false;
+							if ( cpbody && cpbody.error ) {
+								switch ( cpbody.error.code ) {
+									case 'nosuchrevid':
+										noerror = true;
+										break;
+									case 'missingcontent':
+										noerror = true;
+										break;
+									default:
+										noerror = false;
+								}
+							}
+							if ( !noerror ) console.log( '- ' + cpresponse.statusCode + ': Error while getting the diff: ' + ( cpbody && cpbody.error && cpbody.error.info ) );
 						}
-						if ( editor[1] === lang.get('diff.hidden') ) editorlink = editor[1];
-						var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( ( title + '?diff=' + diff + '&oldid=' + oldid ).escapeFormatting() ).setURL( pagelink ).addField( editor[0], editorlink, true ).addField( size[0], size[1], true ).addField( comment[0], comment[1] ).setFooter( timestamp[1] );
-						if ( tags ) {
-							var taglink = '';
-							var tagtext = '';
-							var tagparser = new htmlparser.Parser( {
+						else if ( cpbody.compare.fromtexthidden === undefined && cpbody.compare.totexthidden === undefined && cpbody.compare.fromarchive === undefined && cpbody.compare.toarchive === undefined ) {
+							var current_tag = '';
+							var small_prev_ins = '';
+							var small_prev_del = '';
+							var ins_length = more.length;
+							var del_length = more.length;
+							var added = false;
+							var parser = new htmlparser.Parser( {
 								onopentag: (tagname, attribs) => {
-									if ( tagname === 'a' ) taglink = attribs.href;
+									if ( tagname === 'ins' || tagname == 'del' ) {
+										current_tag = tagname;
+									}
+									if ( tagname === 'td' && attribs.class === 'diff-addedline' ) {
+										current_tag = tagname+'a';
+									}
+									if ( tagname === 'td' && attribs.class === 'diff-deletedline' ) {
+										current_tag = tagname+"d";
+									}
+									if ( tagname === 'td' && attribs.class === 'diff-marker' ) {
+										added = true;
+									}
 								},
 								ontext: (htmltext) => {
-									if ( taglink ) tagtext += '[' + htmltext.escapeFormatting() + '](' + taglink + ')'
-									else tagtext += htmltext.escapeFormatting();
-								},
-								onclosetag: (tagname) => {
-									if ( tagname === 'a' ) taglink = '';
-								}
-							}, {decodeEntities:true} );
-							tagparser.write( tags[1] );
-							tagparser.end();
-							embed.addField( tags[0], tagtext );
-						}
-						
-						var more = '\n__' + lang.get('diff.info.more') + '__';
-						if ( !compare && oldid ) got.get( wiki + 'api.php?action=compare&prop=diff&fromrev=' + oldid + '&torev=' + diff + '&format=json' ).then( cpresponse => {
-							var cpbody = cpresponse.body;
-							if ( cpbody && cpbody.warnings ) log_warn(cpbody.warnings);
-							if ( cpresponse.statusCode !== 200 || !cpbody || !cpbody.compare || cpbody.compare['*'] === undefined ) {
-								var noerror = false;
-								if ( cpbody && cpbody.error ) {
-									switch ( cpbody.error.code ) {
-										case 'nosuchrevid':
-											noerror = true;
-											break;
-										case 'missingcontent':
-											noerror = true;
-											break;
-										default:
-											noerror = false;
+									if ( current_tag === 'ins' && ins_length <= 1000 ) {
+										ins_length += ( '**' + htmltext.escapeFormatting() + '**' ).length;
+										if ( ins_length <= 1000 ) small_prev_ins += '**' + htmltext.escapeFormatting() + '**';
+										else small_prev_ins += more;
 									}
-								}
-								if ( !noerror ) console.log( '- ' + cpresponse.statusCode + ': Error while getting the diff: ' + ( cpbody && cpbody.error && cpbody.error.info ) );
-							}
-							else if ( cpbody.compare.fromtexthidden === undefined && cpbody.compare.totexthidden === undefined && cpbody.compare.fromarchive === undefined && cpbody.compare.toarchive === undefined ) {
-								var current_tag = '';
-								var small_prev_ins = '';
-								var small_prev_del = '';
-								var ins_length = more.length;
-								var del_length = more.length;
-								var added = false;
-								var parser = new htmlparser.Parser( {
-									onopentag: (tagname, attribs) => {
-										if ( tagname === 'ins' || tagname == 'del' ) {
-											current_tag = tagname;
-										}
-										if ( tagname === 'td' && attribs.class === 'diff-addedline' ) {
-											current_tag = tagname+'a';
-										}
-										if ( tagname === 'td' && attribs.class === 'diff-deletedline' ) {
-											current_tag = tagname+"d";
-										}
-										if ( tagname === 'td' && attribs.class === 'diff-marker' ) {
-											added = true;
-										}
-									},
-									ontext: (htmltext) => {
-										if ( current_tag === 'ins' && ins_length <= 1000 ) {
-											ins_length += ( '**' + htmltext.escapeFormatting() + '**' ).length;
-											if ( ins_length <= 1000 ) small_prev_ins += '**' + htmltext.escapeFormatting() + '**';
+									if ( current_tag === 'del' && del_length <= 1000 ) {
+										del_length += ( '~~' + htmltext.escapeFormatting() + '~~' ).length;
+										if ( del_length <= 1000 ) small_prev_del += '~~' + htmltext.escapeFormatting() + '~~';
+										else small_prev_del += more;
+									}
+									if ( ( current_tag === 'afterins' || current_tag === 'tda') && ins_length <= 1000 ) {
+										ins_length += htmltext.escapeFormatting().length;
+										if ( ins_length <= 1000 ) small_prev_ins += htmltext.escapeFormatting();
+										else small_prev_ins += more;
+									}
+									if ( ( current_tag === 'afterdel' || current_tag === 'tdd') && del_length <= 1000 ) {
+										del_length += htmltext.escapeFormatting().length;
+										if ( del_length <= 1000 ) small_prev_del += htmltext.escapeFormatting();
+										else small_prev_del += more;
+									}
+									if ( added ) {
+										if ( htmltext === '+' && ins_length <= 1000 ) {
+											ins_length++;
+											if ( ins_length <= 1000 ) small_prev_ins += '\n';
 											else small_prev_ins += more;
 										}
-										if ( current_tag === 'del' && del_length <= 1000 ) {
-											del_length += ( '~~' + htmltext.escapeFormatting() + '~~' ).length;
-											if ( del_length <= 1000 ) small_prev_del += '~~' + htmltext.escapeFormatting() + '~~';
+										if ( htmltext === '−' && del_length <= 1000 ) {
+											del_length++;
+											if ( del_length <= 1000 ) small_prev_del += '\n';
 											else small_prev_del += more;
 										}
-										if ( ( current_tag === 'afterins' || current_tag === 'tda') && ins_length <= 1000 ) {
-											ins_length += htmltext.escapeFormatting().length;
-											if ( ins_length <= 1000 ) small_prev_ins += htmltext.escapeFormatting();
-											else small_prev_ins += more;
-										}
-										if ( ( current_tag === 'afterdel' || current_tag === 'tdd') && del_length <= 1000 ) {
-											del_length += htmltext.escapeFormatting().length;
-											if ( del_length <= 1000 ) small_prev_del += htmltext.escapeFormatting();
-											else small_prev_del += more;
-										}
-										if ( added ) {
-											if ( htmltext === '+' && ins_length <= 1000 ) {
-												ins_length++;
-												if ( ins_length <= 1000 ) small_prev_ins += '\n';
-												else small_prev_ins += more;
-											}
-											if ( htmltext === '−' && del_length <= 1000 ) {
-												del_length++;
-												if ( del_length <= 1000 ) small_prev_del += '\n';
-												else small_prev_del += more;
-											}
-											added = false;
-										}
-									},
-									onclosetag: (tagname) => {
-										if ( tagname === 'ins' ) {
-											current_tag = 'afterins';
-										} else if ( tagname === 'del' ) {
-											current_tag = 'afterdel';
-										} else {
-											current_tag = '';
-										}
+										added = false;
+									}
+								},
+								onclosetag: (tagname) => {
+									if ( tagname === 'ins' ) {
+										current_tag = 'afterins';
+									} else if ( tagname === 'del' ) {
+										current_tag = 'afterdel';
+									} else {
+										current_tag = '';
 									}
-								}, {decodeEntities:true} );
-								parser.write( cpbody.compare['*'] );
-								parser.end();
-								if ( small_prev_del.length ) {
-									if ( small_prev_del.replace( /\~\~/g, '' ).trim().length ) {
-										embed.addField( lang.get('diff.info.removed'), small_prev_del.replace( /\~\~\~\~/g, '' ), true );
-									} else embed.addField( lang.get('diff.info.removed'), '__' + lang.get('diff.info.whitespace') + '__', true );
-								}
-								if ( small_prev_ins.length ) {
-									if ( small_prev_ins.replace( /\*\*/g, '' ).trim().length ) {
-										embed.addField( lang.get('diff.info.added'), small_prev_ins.replace( /\*\*\*\*/g, '' ), true );
-									} else embed.addField( lang.get('diff.info.added'), '__' + lang.get('diff.info.whitespace') + '__', true );
 								}
+							}, {decodeEntities:true} );
+							parser.write( cpbody.compare['*'] );
+							parser.end();
+							if ( small_prev_del.length ) {
+								if ( small_prev_del.replace( /\~\~/g, '' ).trim().length ) {
+									embed.addField( lang.get('diff.info.removed'), small_prev_del.replace( /\~\~\~\~/g, '' ), true );
+								} else embed.addField( lang.get('diff.info.removed'), '__' + lang.get('diff.info.whitespace') + '__', true );
 							}
-							else if ( cpbody.compare.fromtexthidden !== undefined ) {
-								embed.addField( lang.get('diff.info.removed'), '__' + lang.get('diff.hidden') + '__', true );
-							}
-							else if ( cpbody.compare.totexthidden !== undefined ) {
-								embed.addField( lang.get('diff.info.added'), '__' + lang.get('diff.hidden') + '__', true );
-							}
-						}, error => {
-							console.log( '- Error while getting the diff: ' + error );
-						} ).finally( () => {
-							msg.sendChannel( spoiler + text + spoiler, {embed} );
-							
-							if ( reaction ) reaction.removeEmoji();
-						} );
-						else {
-							if ( compare ) {
-								if ( compare[0].length ) embed.addField( lang.get('diff.info.removed'), compare[0], true );
-								if ( compare[1].length ) embed.addField( lang.get('diff.info.added'), compare[1], true );
-							}
-							else if ( revisions[0]['*'] ) {
-								var content = revisions[0]['*'].escapeFormatting();
-								if ( content.trim().length ) {
-									if ( content.length <= 1000 ) content = '**' + content + '**';
-									else {
-										content = content.substring(0, 1000 - more.length);
-										content = '**' + content.substring(0, content.lastIndexOf('\n')) + '**' + more;
-									}
-									embed.addField( lang.get('diff.info.added'), content, true );
+							if ( small_prev_ins.length ) {
+								if ( small_prev_ins.replace( /\*\*/g, '' ).trim().length ) {
+									embed.addField( lang.get('diff.info.added'), small_prev_ins.replace( /\*\*\*\*/g, '' ), true );
 								} else embed.addField( lang.get('diff.info.added'), '__' + lang.get('diff.info.whitespace') + '__', true );
 							}
-							
-							msg.sendChannel( spoiler + text + spoiler, {embed} );
-							
-							if ( reaction ) reaction.removeEmoji();
 						}
-					}
+						else if ( cpbody.compare.fromtexthidden !== undefined ) {
+							embed.addField( lang.get('diff.info.removed'), '__' + lang.get('diff.hidden') + '__', true );
+						}
+						else if ( cpbody.compare.totexthidden !== undefined ) {
+							embed.addField( lang.get('diff.info.added'), '__' + lang.get('diff.hidden') + '__', true );
+						}
+					}, error => {
+						console.log( '- Error while getting the diff: ' + error );
+					} ).finally( () => {
+						msg.sendChannel( spoiler + text + spoiler, {embed} );
+						
+						if ( reaction ) reaction.removeEmoji();
+					} );
 					else {
-						var embed = {};
-						var text = '<' + pagelink + '>\n\n' + editor.join(' ') + '\n' + timestamp.join(' ') + '\n' + size.join(' ') + '\n' + comment.join(' ');
-						if ( tags ) text += htmlToPlain( '\n' + tags.join(' ') );
+						if ( compare ) {
+							if ( compare[0].length ) embed.addField( lang.get('diff.info.removed'), compare[0], true );
+							if ( compare[1].length ) embed.addField( lang.get('diff.info.added'), compare[1], true );
+						}
+						else if ( revisions[0]['*'] ) {
+							var content = revisions[0]['*'].escapeFormatting();
+							if ( content.trim().length ) {
+								if ( content.length <= 1000 ) content = '**' + content + '**';
+								else {
+									content = content.substring(0, 1000 - more.length);
+									content = '**' + content.substring(0, content.lastIndexOf('\n')) + '**' + more;
+								}
+								embed.addField( lang.get('diff.info.added'), content, true );
+							} else embed.addField( lang.get('diff.info.added'), '__' + lang.get('diff.info.whitespace') + '__', true );
+						}
 						
 						msg.sendChannel( spoiler + text + spoiler, {embed} );
 						
 						if ( reaction ) reaction.removeEmoji();
 					}
 				}
+				else {
+					var embed = {};
+					var text = '<' + pagelink + '>\n\n' + editor.join(' ') + '\n' + timestamp.join(' ') + '\n' + size.join(' ') + '\n' + comment.join(' ');
+					if ( tags ) text += htmlToPlain( '\n' + tags.join(' ') );
+					
+					msg.sendChannel( spoiler + text + spoiler, {embed} );
+					
+					if ( reaction ) reaction.removeEmoji();
+				}
 			}
-			else {
-				msg.reactEmoji('error');
-				
-				if ( reaction ) reaction.removeEmoji();
-			}
+		}
+		else {
+			msg.reactEmoji('error');
+			
+			if ( reaction ) reaction.removeEmoji();
 		}
 	}, error => {
 		if ( wiki.noWiki(error.message) ) {

+ 19 - 14
cmds/wiki/gamepedia/overview.js

@@ -9,7 +9,7 @@ getAllSites.then( sites => allSites = sites );
  * Sends a Gamepedia wiki overview.
  * @param {import('../../../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
- * @param {String} wiki - The wiki for the overview.
+ * @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.
  */
@@ -31,9 +31,10 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 			if ( reaction ) reaction.removeEmoji();
 		}
 		else {
+			wiki.updateWiki(body.query.general);
 			var site = false;
-			if ( allSites.some( site => site.wiki_domain === body.query.general.servername ) ) {
-				site = allSites.find( site => site.wiki_domain === body.query.general.servername );
+			if ( allSites.some( site => site.wiki_domain === body.hostname ) ) {
+				site = allSites.find( site => site.wiki_domain === body.hostname );
 				
 				var name = [lang.get('overview.name'), site.wiki_display_name];
 				var created = [lang.get('overview.created'), new Date(parseInt(site.created + '000', 10)).toLocaleString(lang.get('dateformat'), timeoptions)];
@@ -47,7 +48,7 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 					description[1] = description[1].escapeFormatting();
 					if ( description[1].length > 1000 ) description[1] = description[1].substring(0, 1000) + '\u2026';
 				}
-				if ( image[1] && image[1].startsWith( '/' ) ) image[1] = wiki.substring(0, wiki.length - 1) + image[1];
+				if ( image[1] && image[1].startsWith( '/' ) ) image[1] = new URL(image[1], wiki).href;
 			}
 			var articles = [lang.get('overview.articles'), body.query.statistics.articles];
 			var pages = [lang.get('overview.pages'), body.query.statistics.pages];
@@ -55,11 +56,11 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 			var users = [lang.get('overview.users'), body.query.statistics.activeusers];
 			
 			var title = body.query.pages['-1'].title;
-			var pagelink = wiki.toLink(title, '', '', body.query.general);
+			var pagelink = wiki.toLink(title);
 			
 			if ( msg.showEmbed() ) {
 				var text = '<' + pagelink + '>';
-				var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( title.escapeFormatting() ).setURL( pagelink ).setThumbnail( ( /^(?:https?:)?\/\//.test(body.query.general.logo) ? body.query.general.logo.replace( /^(?:https?:)?\/\//, 'https://' ) : body.query.general.server + ( body.query.general.logo.startsWith( '/' ) ? '' : '/' ) + body.query.general.logo ) );
+				var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( title.escapeFormatting() ).setURL( pagelink ).setThumbnail( new URL(body.query.general.logo, wiki).href );
 			}
 			else {
 				var embed = {};
@@ -79,12 +80,16 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 				var ovbody = ovresponse.body;
 				if ( ovresponse.statusCode !== 200 || !ovbody || ovbody.exception || !ovbody.items || !ovbody.items.length ) {
 					console.log( '- ' + ovresponse.statusCode + ': Error while getting the wiki details: ' + ( ovbody && ovbody.exception && ovbody.exception.details ) );
-					msg.sendChannelError( spoiler + '<' + wiki.toLink('Special:Statistics', '', '', body.query.general) + '>' + spoiler );
+
+					if ( msg.showEmbed() ) embed.addField( articles[0], articles[1], true ).addField( pages[0], pages[1], true ).addField( edits[0], edits[1], true ).addField( users[0], users[1], true ).setTimestamp( msg.client.readyTimestamp ).setFooter( lang.get('overview.inaccurate') );
+					else text = articles.join(' ') + '\n' + pages.join(' ') + '\n' + edits.join(' ') + '\n' + users.join(' ') + '\n\n*' + lang.get('overview.inaccurate') + '*';
+	
+					msg.sendChannelError( spoiler + text + spoiler, {embed} );
 					
 					if ( reaction ) reaction.removeEmoji();
 				}
-				else if ( ovbody.items.some( site => site.url === body.query.general.server + ( body.query.general.scriptpath ? body.query.general.scriptpath + '/' : '' ) ) ) {
-					site = ovbody.items.find( site => site.url === body.query.general.server + ( body.query.general.scriptpath ? body.query.general.scriptpath + '/' : '' ) );
+				else if ( ovbody.items.some( site => site.url === wiki.href ) ) {
+					site = ovbody.items.find( site => site.url === wiki.href );
 					
 					var vertical = [lang.get('overview.vertical'), site.hub];
 					var topic = [lang.get('overview.topic'), site.topic];
@@ -100,7 +105,7 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 						description[1] = description[1].escapeFormatting();
 						if ( description[1].length > 1000 ) description[1] = description[1].substring(0, 1000) + '\u2026';
 					}
-					if ( image[1] && image[1].startsWith( '/' ) ) image[1] = wiki.substring(0, wiki.length - 1) + image[1];
+					if ( image[1] && image[1].startsWith( '/' ) ) image[1] = new URL(image[1], wiki).href;
 					
 					if ( msg.showEmbed() ) {
 						embed.addField( vertical[0], vertical[1], true );
@@ -118,7 +123,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, '', '', body.query.general, true) + ')';
+								if ( msg.showEmbed() ) founder[1] = '[' + user + '](' + wiki.toLink('User:' + user, '', '', true) + ')';
 								else founder[1] = user;
 							}
 						}, error => {
@@ -146,7 +151,7 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 					]).finally( () => {
 						if ( msg.showEmbed() ) {
 							embed.addField( founder[0], founder[1], true );
-							if ( manager[1] ) embed.addField( manager[0], '[' + manager[1] + '](' + wiki.toLink('User:' + manager[1], '', '', body.query.general, true) + ') ([' + lang.get('overview.talk') + '](' + wiki.toLink('User talk:' + manager[1], '', '', body.query.general, true) + '))', true );
+							if ( manager[1] ) embed.addField( manager[0], '[' + manager[1] + '](' + wiki.toLink('User:' + manager[1], '', '', true) + ') ([' + lang.get('overview.talk') + '](' + wiki.toLink('User talk:' + manager[1], '', '', true) + '))', 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 );
 							if ( posts[1] ) embed.addField( posts[0], posts[1], true );
 							if ( walls[1] ) embed.addField( walls[0], walls[1], true );
@@ -182,7 +187,7 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 				}
 				else {
 					if ( msg.showEmbed() ) {
-						if ( manager[1] ) embed.addField( manager[0], '[' + manager[1] + '](' + wiki.toLink('User:' + manager[1], '', '', body.query.general, true) + ') ([' + lang.get('overview.talk') + '](' + wiki.toLink('User talk:' + manager[1], '', '', body.query.general, true) + '))', true );
+						if ( manager[1] ) embed.addField( manager[0], '[' + manager[1] + '](' + wiki.toLink('User:' + manager[1], '', '', true) + ') ([' + lang.get('overview.talk') + '](' + wiki.toLink('User talk:' + manager[1], '', '', true) + '))', true );
 						embed.addField( articles[0], articles[1], true ).addField( pages[0], pages[1], true ).addField( edits[0], edits[1], true ).addField( users[0], users[1], true ).setTimestamp( msg.client.readyTimestamp ).setFooter( lang.get('overview.inaccurate') );
 						if ( crossover[1] ) {
 							var crossoverSite = allSites.find( site => '<https://' + site.wiki_domain + '/>' === crossover[1] );
@@ -212,7 +217,7 @@ function gamepedia_overview(lang, msg, wiki, reaction, spoiler) {
 			else {
 				if ( msg.showEmbed() ) {
 					if ( site ) {
-						var managerlist = manager[1].map( wm => '[' + wm + '](' + wiki.toLink('User:' + wm, '', '', body.query.general, true) + ') ([' + lang.get('overview.talk') + '](' + wiki.toLink('User talk:' + wm, '', '', body.query.general, true) + '))' ).join('\n');
+						var managerlist = manager[1].map( wm => '[' + wm + '](' + wiki.toLink('User:' + wm, '', '', true) + ') ([' + lang.get('overview.talk') + '](' + wiki.toLink('User talk:' + wm, '', '', true) + '))' ).join('\n');
 						embed.addField( name[0], name[1], true ).addField( created[0], created[1], true ).addField( manager[0], ( managerlist || lang.get('overview.none') ), true ).addField( official[0], official[1], true );
 					}
 					embed.addField( articles[0], articles[1], true ).addField( pages[0], pages[1], true ).addField( edits[0], edits[1], true ).addField( users[0], users[1], true ).setTimestamp( msg.client.readyTimestamp ).setFooter( lang.get('overview.inaccurate') );

+ 12 - 18
cmds/wiki/gamepedia/random.js

@@ -7,12 +7,12 @@ const extract_desc = require('../../../util/extract_desc.js');
  * Sends a random Gamepedia page.
  * @param {import('../../../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
- * @param {String} wiki - The wiki for the page.
+ * @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.
  */
 function gamepedia_random(lang, msg, wiki, reaction, spoiler) {
-	got.get( wiki + 'api.php?action=query&meta=siteinfo&siprop=general&prop=pageimages|pageprops|extracts&piprop=original|name&ppprop=description|displaytitle&explaintext=true&exsectionformat=raw&exlimit=1&generator=random&grnnamespace=0&format=json' ).then( response => {
+	got.get( wiki + 'api.php?action=query&meta=siteinfo&siprop=general&prop=pageimages|pageprops|extracts&piprop=original|name&ppprop=description|displaytitle|page_image_free&explaintext=true&exsectionformat=raw&exlimit=1&generator=random&grnnamespace=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 ) {
@@ -27,7 +27,7 @@ function gamepedia_random(lang, msg, wiki, reaction, spoiler) {
 		}
 		else {
 			var querypage = Object.values(body.query.pages)[0];
-			var pagelink = wiki.toLink(querypage.title, '', '', body.query.general);
+			var pagelink = wiki.updateWiki(body.query.general).toLink(querypage.title);
 			var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink );
 			if ( querypage.pageprops && querypage.pageprops.displaytitle ) {
 				var displaytitle = htmlToDiscord( querypage.pageprops.displaytitle );
@@ -43,12 +43,18 @@ function gamepedia_random(lang, msg, wiki, reaction, spoiler) {
 				var extract = extract_desc(querypage.extract);
 				embed.setDescription( extract[0] );
 			}
-			if ( querypage.pageimage && querypage.original && querypage.title !== body.query.general.mainpage ) {
+			if ( querypage.title === body.query.general.mainpage ) {
+				embed.setThumbnail( new URL(body.query.general.logo, wiki).href );
+			}
+			else if ( querypage.pageimage && querypage.original ) {
 				embed.setThumbnail( querypage.original.source );
 			}
-			else embed.setThumbnail( logoToURL(body.query.general) );
+			else if ( querypage.pageprops && querypage.pageprops.page_image_free ) {
+				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 );
 			
-			msg.sendChannel( '🎲 ' + spoiler + '<' + pagelink + '>' + spoiler, {embed} ).then( message => parse_page(message, querypage.title, embed, wiki, ( querypage.title === body.query.general.mainpage ? '' : logoToURL(body.query.general) )) );
+			msg.sendChannel( '🎲 ' + spoiler + '<' + pagelink + '>' + spoiler, {embed} ).then( message => parse_page(message, querypage.title, embed, wiki, ( querypage.title === body.query.general.mainpage ? '' : new URL(body.query.general.logo, wiki).href )) );
 		}
 	}, error => {
 		if ( wiki.noWiki(error.message) ) {
@@ -64,18 +70,6 @@ function gamepedia_random(lang, msg, wiki, reaction, spoiler) {
 	} );
 }
 
-/**
- * Turns the siteinfo logo into an URL.
- * @param {Object} arg - The siteinfo from the wiki.
- * @param {String} arg.logo - The logo from the wiki.
- * @param {String} arg.server - The server URL from the wiki.
- * @returns {String}
- */
-function logoToURL({logo, server: serverURL}) {
-	if ( !/^(?:https?:)?\/\//.test(logo) ) logo = serverURL + ( logo.startsWith( '/' ) ? '' : '/' ) + logo;
-	return logo.replace( /^(?:https?:)?\/\//, 'https://' );
-}
-
 /**
  * Change HTML text to plain text.
  * @param {String} html - The text in HTML.

+ 6 - 6
cmds/wiki/gamepedia/search.js

@@ -5,7 +5,7 @@ const {MessageEmbed, Util} = require('discord.js');
  * @param {import('../../../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String} searchterm - The searchterm.
- * @param {String} wiki - The wiki for the search.
+ * @param {import('../../../util/wiki.js')} wiki - The wiki for the search.
  * @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.
@@ -15,10 +15,10 @@ function gamepedia_search(lang, msg, searchterm, wiki, query, reaction, spoiler)
 		searchterm = searchterm.substring(0, 250);
 		msg.reactEmoji('⚠️');
 	}
-	var pagelink = wiki.toLink('Special:Search', 'search=' + searchterm.toSearch() + '&fulltext=1', '', query.general);
+	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', '', '', query.general);
+		pagelink = wiki.toLink('Special:Search');
 		embed.setTitle( 'Special:Search' ).setURL( pagelink );
 	}
 	var description = [];
@@ -30,17 +30,17 @@ function gamepedia_search(lang, msg, searchterm, wiki, query, reaction, spoiler)
 		}
 		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.toSearch() + '&fulltext=1', '', query.general);
+				pagelink = wiki.toLink(body.query.pages['-1'].title, {search:searchterm,fulltext:1});
 				embed.setURL( pagelink );
 			}
 			else {
-				pagelink = wiki.toLink(body.query.pages['-1'].title, '', '', query.general);
+				pagelink = wiki.toLink(body.query.pages['-1'].title);
 				embed.setTitle( body.query.pages['-1'].title ).setURL( pagelink );
 			}
 		}
 		if ( searchterm.trim() ) {
 			body.query.search.forEach( result => {
-				description.push( '• [' + result.title + '](' + wiki.toLink(result.title, '', '', query.general, true) + ')' + ( result.sectiontitle ? ' § [' + result.sectiontitle + '](' + wiki.toLink(result.title, '', result.sectiontitle, query.general, true) + ')' : '' ) + ( result.redirecttitle ? ' (⤷ [' + result.redirecttitle + '](' + wiki.toLink(result.redirecttitle, '', '', query.general, true) + '))' : '' ) );
+				description.push( '• [' + result.title + '](' + wiki.toLink(result.title, '', '', true) + ')' + ( result.sectiontitle ? ' § [' + result.sectiontitle + '](' + wiki.toLink(result.title, '', result.sectiontitle, true) + ')' : '' ) + ( result.redirecttitle ? ' (⤷ [' + result.redirecttitle + '](' + wiki.toLink(result.redirecttitle, 'redirect=no', '', true) + '))' : '' ) );
 			} );
 			if ( body.query.searchinfo ) {
 				embed.setFooter( lang.get('search.results', body.query.searchinfo.totalhits) );

+ 51 - 51
cmds/wiki/gamepedia/user.js

@@ -4,6 +4,7 @@ const global_block = require('../../../functions/global_block.js');
 const parse_page = require('../../../functions/parse_page.js');
 const extract_desc = require('../../../util/extract_desc.js');
 const {timeoptions, usergroups} = require('../../../util/default.json');
+const {toMarkdown, toPlaintext} = require('../../../util/functions.js');
 
 var allSites = [];
 const getAllSites = require('../../../util/allSites.js');
@@ -15,8 +16,8 @@ getAllSites.then( sites => allSites = sites );
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String} namespace - The user namespace on the wiki.
  * @param {String} username - The name of the user.
- * @param {String} wiki - The wiki for the page.
- * @param {String} querystring - The querystring for the link.
+ * @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 {Object} querypage - The user page on the wiki.
  * @param {String} contribs - The contributions page on the wiki.
@@ -33,8 +34,13 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 				if ( body && body.error && ( body.error.code === 'param_ip' || body.error.code === 'cidrtoobroad' ) || fragment ) {
 					if ( querypage.missing !== undefined || querypage.ns === -1 ) msg.reactEmoji('error');
 					else {
-						var pagelink = wiki.toLink(querypage.title, querystring.toTitle(), fragment);
-						var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink );
+						var pagelink = wiki.toLink(querypage.title, querystring, fragment);
+						var embed = new MessageEmbed().setTitle( querypage.title.escapeFormatting() ).setURL( pagelink );
+						if ( body?.query?.general ) {
+							wiki.updateWiki(body.query.general);
+							embed.setAuthor( body.query.general.sitename );
+							embed.setThumbnail( new URL(body.query.general.logo, wiki).href );
+						}
 						if ( querypage.pageprops && querypage.pageprops.displaytitle ) {
 							var displaytitle = htmlToDiscord( querypage.pageprops.displaytitle );
 							if ( displaytitle.length > 250 ) displaytitle = displaytitle.substring(0, 250) + '\u2026';
@@ -51,16 +57,18 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 							embed.setDescription( description );
 						}
 						if ( querypage.pageimage && querypage.original ) {
-							var pageimage = querypage.original.source;
-							embed.setThumbnail( pageimage );
-						} else embed.setThumbnail( logoToURL(body.query.general) );
+							embed.setThumbnail( querypage.original.source );
+						}
+						else if ( querypage.pageprops && querypage.pageprops.page_image_free ) {
+							embed.setThumbnail( wiki.toLink('Special:FilePath/' + querypage.pageprops.page_image_free, {version:Date.now()}) );
+						}
 						
-						msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} ).then( message => parse_page(message, querypage.title, embed, wiki, ( querypage.title === body.query.general.mainpage ? '' : logoToURL(body.query.general) )) );
+						msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} ).then( message => parse_page(message, querypage.title, embed, wiki, '') );
 					}
 				}
 				else {
 					console.log( '- ' + response.statusCode + ': Error while getting the search results: ' + ( body && body.error && body.error.info ) );
-					msg.sendChannelError( spoiler + '<' + wiki.toLink(( querypage.noRedirect ? namespace : contribs ) + username, querystring.toTitle(), fragment) + '>' + spoiler );
+					msg.sendChannelError( spoiler + '<' + wiki.toLink(( querypage.noRedirect ? namespace : contribs ) + username, querystring, fragment) + '>' + spoiler );
 				}
 				
 				if ( reaction ) reaction.removeEmoji();
@@ -105,7 +113,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 						else if ( range >= 16 ) rangeprefix = username.replace( /^((?:\d{1,3}\.){2}).+$/, '$1' );
 					}
 				}
-				got.get( wiki + 'api.php?action=query&list=usercontribs&ucprop=&uclimit=50' + ( username.includes( '/' ) ? '&ucuserprefix=' + encodeURIComponent( rangeprefix ) : '&ucuser=' + encodeURIComponent( username ) ) + '&format=json' ).then( ucresponse => {
+				got.get( wiki.updateWiki(body.query.general) + 'api.php?action=query&list=usercontribs&ucprop=&uclimit=50' + ( username.includes( '/' ) ? '&ucuserprefix=' + encodeURIComponent( rangeprefix ) : '&ucuser=' + encodeURIComponent( username ) ) + '&format=json' ).then( ucresponse => {
 					var ucbody = ucresponse.body;
 					if ( rangeprefix && !username.includes( '/' ) ) username = rangeprefix;
 					if ( ucbody && ucbody.warnings ) log_warn(ucbody.warnings);
@@ -115,16 +123,16 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 						}
 						else {
 							console.log( '- ' + ucresponse.statusCode + ': Error while getting the search results: ' + ( ucbody && ucbody.error && ucbody.error.info ) );
-							msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring.toTitle(), fragment, body.query.general) + '>' + spoiler );
+							msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring, fragment) + '>' + spoiler );
 						}
 					}
 					else {
 						var editcount = [lang.get('user.info.editcount'), ( username.includes( '/' ) && ( ( username.includes( ':' ) && range % 16 ) || range % 8 ) ? '~' : '' ) + ucbody.query.usercontribs.length + ( ucbody.continue ? '+' : '' )];
 						
-						var pagelink = wiki.toLink(namespace + username, querystring.toTitle(), fragment, body.query.general);
+						var pagelink = wiki.toLink(namespace + username, querystring, fragment);
 						if ( msg.showEmbed() ) {
 							var text = '<' + pagelink + '>';
-							var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( username ).setURL( pagelink ).addField( editcount[0], '[' + editcount[1] + '](' + wiki.toLink(contribs + username, '', '', body.query.general, true) + ')' );
+							var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( username ).setURL( pagelink ).addField( editcount[0], '[' + editcount[1] + '](' + wiki.toLink(contribs + username, '', '', true) + ')' );
 							if ( querypage.pageprops && querypage.pageprops.description ) {
 								var description = htmlToPlain( querypage.pageprops.description );
 								if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
@@ -135,8 +143,8 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 								embed.setDescription( extract[0] );
 							}
 							if ( blocks.length ) blocks.forEach( block => {
-								block.text = block.text.replaceSave( /\$3/g, '[' + block.by.escapeFormatting() + '](' + wiki.toLink('User:' + block.by, '', '', body.query.general, true) + ')' );
-								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, block.reason.toMarkdown(wiki, body.query.general) );
+								block.text = block.text.replaceSave( /\$3/g, '[' + block.by.escapeFormatting() + '](' + wiki.toLink('User:' + block.by, '', '', true) + ')' );
+								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, toMarkdown(block.reason, wiki) );
 								embed.addField( block.header, block.text );
 							} );
 						}
@@ -145,12 +153,12 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 							var text = '<' + pagelink + '>\n\n' + editcount.join(' ');
 							if ( blocks.length ) blocks.forEach( block => {
 								block.text = block.text.replaceSave( /\$3/g, block.by.escapeFormatting() );
-								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, block.reason.toPlaintext() );
+								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, toPlaintext(block.reason) );
 								text += '\n\n**' + block.header + '**\n' + block.text;
 							} );
 						}
 						
-						if ( msg.channel.type === 'text' && msg.guild.id in patreons && ( wiki.isFandom() || wiki.endsWith( '.gamepedia.com/' ) ) ) {
+						if ( msg.channel.type === 'text' && msg.guild.id in patreons && ( wiki.isFandom() || wiki.isGamepedia() ) ) {
 							if ( msg.showEmbed() ) embed.addField( '\u200b', '<a:loading:641343250661113886> **' + lang.get('user.info.loading') + '**' );
 							else text += '\n\n<a:loading:641343250661113886> **' + lang.get('user.info.loading') + '**';
 
@@ -161,14 +169,14 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 				}, error => {
 					if ( rangeprefix && !username.includes( '/' ) ) username = rangeprefix;
 					console.log( '- Error while getting the search results: ' + error );
-					msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring.toTitle(), fragment, body.query.general) + '>' + spoiler );
+					msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring, fragment) + '>' + spoiler );
 				} ).finally( () => {
 					if ( reaction ) reaction.removeEmoji();
 				} );
 			}
 		}, error => {
 			console.log( '- Error while getting the search results: ' + error );
-			msg.sendChannelError( spoiler + '<' + wiki.toLink(( querypage.noRedirect ? namespace : contribs ) + username, querystring.toTitle(), fragment) + '>' + spoiler );
+			msg.sendChannelError( spoiler + '<' + wiki.toLink(( querypage.noRedirect ? namespace : contribs ) + username, querystring, fragment) + '>' + spoiler );
 			
 			if ( reaction ) reaction.removeEmoji();
 		} );
@@ -178,16 +186,17 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 			if ( body && body.warnings ) log_warn(body.warnings);
 			if ( response.statusCode !== 200 || !body || body.batchcomplete === undefined || !body.query || !body.query.users || !body.query.users[0] ) {
 				console.log( '- ' + response.statusCode + ': Error while getting the search results: ' + ( body && body.error && body.error.info ) );
-				msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring.toTitle(), fragment) + '>' + spoiler );
+				msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring, fragment) + '>' + spoiler );
 				
 				if ( reaction ) reaction.removeEmoji();
 			}
 			else {
+				wiki.updateWiki(body.query.general);
 				var queryuser = body.query.users[0];
 				if ( queryuser.missing !== undefined || queryuser.invalid !== undefined || fragment ) {
 					if ( querypage.missing !== undefined || querypage.ns === -1 ) msg.reactEmoji('🤷');
 					else {
-						var pagelink = wiki.toLink(querypage.title, querystring.toTitle(), fragment, body.query.general);
+						var pagelink = wiki.toLink(querypage.title, querystring, fragment);
 						var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( querypage.title.escapeFormatting() ).setURL( pagelink );
 						if ( querypage.pageprops && querypage.pageprops.displaytitle ) {
 							var displaytitle = htmlToDiscord( querypage.pageprops.displaytitle );
@@ -205,11 +214,14 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 							embed.setDescription( description );
 						}
 						if ( querypage.pageimage && querypage.original ) {
-							var pageimage = querypage.original.source;
-							embed.setThumbnail( pageimage );
-						} else embed.setThumbnail( logoToURL(body.query.general) );
+							embed.setThumbnail( querypage.original.source );
+						}
+						else if ( querypage.pageprops && querypage.pageprops.page_image_free ) {
+							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 );
 						
-						msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} ).then( message => parse_page(message, querypage.title, embed, wiki, ( querypage.title === body.query.general.mainpage ? '' : logoToURL(body.query.general) )) );
+						msg.sendChannel( spoiler + '<' + pagelink + '>' + spoiler, {embed} ).then( message => parse_page(message, querypage.title, embed, wiki, new URL(body.query.general.logo, wiki).href) );
 					}
 					
 					if ( reaction ) reaction.removeEmoji();
@@ -234,7 +246,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 					var group = [lang.get('user.info.group')];
 					for ( var i = 0; i < usergroups.length; i++ ) {
 						if ( groups.includes( usergroups[i] ) && ( group.length === 1 || !['autoconfirmed', 'user'].includes( usergroups[i] ) ) ) {
-							let thisSite = allSites.find( site => site.wiki_domain === body.query.general.servername );
+							let thisSite = allSites.find( site => site.wiki_domain === wiki.hostname );
 							if ( usergroups[i] === 'wiki_manager' && thisSite && thisSite.wiki_managers.includes( username ) ) {
 								group.push('**' + lang.get('user.groups.' + usergroups[i], queryuser.gender) + '**');
 							}
@@ -266,10 +278,10 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 						reason: blockreason
 					};
 					
-					var pagelink = wiki.toLink(namespace + username, querystring.toTitle(), fragment, body.query.general);
+					var pagelink = wiki.toLink(namespace + username, querystring, fragment);
 					if ( msg.showEmbed() ) {
 						var text = '<' + pagelink + '>';
-						var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( username.escapeFormatting() ).setURL( pagelink ).addField( editcount[0], '[' + editcount[1] + '](' + wiki.toLink(contribs + username, '', '', body.query.general, true) + ')', true ).addField( group[0], group.slice(1).join(',\n'), true ).addField( gender[0], gender[1], true ).addField( registration[0], registration[1], true );
+						var embed = new MessageEmbed().setAuthor( body.query.general.sitename ).setTitle( username.escapeFormatting() ).setURL( pagelink ).addField( editcount[0], '[' + editcount[1] + '](' + wiki.toLink(contribs + username, '', '', true) + ')', true ).addField( group[0], group.slice(1).join(',\n'), true ).addField( gender[0], gender[1], true ).addField( registration[0], registration[1], true );
 						
 						if ( querypage.pageprops && querypage.pageprops.description ) {
 							var description = htmlToPlain( querypage.pageprops.description );
@@ -285,7 +297,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 						var embed = {};
 						var text = '<' + pagelink + '>\n\n' + gender.join(' ') + '\n' + registration.join(' ') + '\n' + editcount.join(' ') + '\n' + group[0] + ' ' + group.slice(1).join(', ');
 					}
-					if ( wiki.endsWith( '.gamepedia.com/' ) ) got.get( wiki + 'api.php?action=profile&do=getPublicProfile&user_name=' + encodeURIComponent( username ) + '&format=json&cache=' + Date.now() ).then( presponse => {
+					if ( wiki.isGamepedia() ) got.get( wiki + 'api.php?action=profile&do=getPublicProfile&user_name=' + encodeURIComponent( username ) + '&format=json&cache=' + Date.now() ).then( presponse => {
 						var pbody = presponse.body;
 						if ( presponse.statusCode !== 200 || !pbody || pbody.error || pbody.errormsg || !pbody.profile ) {
 							console.log( '- ' + presponse.statusCode + ': Error while getting the user profile: ' + ( pbody && ( pbody.error && pbody.error.info || pbody.errormsg ) ) );
@@ -315,15 +327,15 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 					} ).finally( () => {
 						if ( msg.showEmbed() ) {
 							if ( isBlocked ) {
-								block.text = block.text.replaceSave( /\$3/g, '[' + block.by.escapeFormatting() + '](' + wiki.toLink('User:' + block.by, '', '', body.query.general, true) + ')' );
-								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, block.reason.toMarkdown(wiki, body.query.general) );
+								block.text = block.text.replaceSave( /\$3/g, '[' + block.by.escapeFormatting() + '](' + wiki.toLink('User:' + block.by, '', '', true) + ')' );
+								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, toMarkdown(block.reason, wiki) );
 								embed.addField( block.header, block.text );
 							}
 						}
 						else {
 							if ( isBlocked ) {
 								block.text = block.text.replaceSave( /\$3/g, block.by.escapeFormatting() );
-								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, block.reason.toPlaintext() );
+								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, toPlaintext(block.reason) );
 								text += '\n\n**' + block.header + '**\n' + block.text;
 							}
 						}
@@ -380,15 +392,15 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 					} ).finally( () => {
 						if ( msg.showEmbed() ) {
 							if ( isBlocked ) {
-								block.text = block.text.replaceSave( /\$3/g, '[' + block.by.escapeFormatting() + '](' + wiki.toLink('User:' + block.by, '', '', body.query.general, true) + ')' );
-								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, block.reason.toMarkdown(wiki, body.query.general) );
+								block.text = block.text.replaceSave( /\$3/g, '[' + block.by.escapeFormatting() + '](' + wiki.toLink('User:' + block.by, '', '', true) + ')' );
+								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, toMarkdown(block.reason, wiki) );
 								embed.addField( block.header, block.text );
 							}
 						}
 						else {
 							if ( isBlocked ) {
 								block.text = block.text.replaceSave( /\$3/g, block.by.escapeFormatting() );
-								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, block.reason.toPlaintext() );
+								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, toPlaintext(block.reason) );
 								text += '\n\n**' + block.header + '**\n' + block.text;
 							}
 						}
@@ -406,13 +418,13 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 					else {
 						if ( isBlocked ) {
 							if ( msg.showEmbed() ) {
-								block.text = block.text.replaceSave( /\$3/g, '[' + block.by.escapeFormatting() + '](' + wiki.toLink('User:' + block.by, '', '', body.query.general, true) + ')' );
-								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, block.reason.toMarkdown(wiki, body.query.general) );
+								block.text = block.text.replaceSave( /\$3/g, '[' + block.by.escapeFormatting() + '](' + wiki.toLink('User:' + block.by, '', '', true) + ')' );
+								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, toMarkdown(block.reason, wiki) );
 								embed.addField( block.header, block.text );
 							}
 							else {
 								block.text = block.text.replaceSave( /\$3/g, block.by.escapeFormatting() );
-								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, block.reason.toPlaintext() );
+								if ( block.reason ) block.text = block.text.replaceSave( /\$4/g, toPlaintext(block.reason) );
 								text += '\n\n**' + block.header + '**\n' + block.text;
 							}
 						}
@@ -425,25 +437,13 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 			}
 		}, error => {
 			console.log( '- Error while getting the search results: ' + error );
-			msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring.toTitle(), fragment) + '>' + spoiler );
+			msg.sendChannelError( spoiler + '<' + wiki.toLink(namespace + username, querystring, fragment) + '>' + spoiler );
 			
 			if ( reaction ) reaction.removeEmoji();
 		} );
 	}
 }
 
-/**
- * Turns the siteinfo logo into an URL.
- * @param {Object} arg - The siteinfo from the wiki.
- * @param {String} arg.logo - The logo from the wiki.
- * @param {String} arg.server - The server URL from the wiki.
- * @returns {String}
- */
-function logoToURL({logo, server: serverURL}) {
-	if ( !/^(?:https?:)?\/\//.test(logo) ) logo = serverURL + ( logo.startsWith( '/' ) ? '' : '/' ) + logo;
-	return logo.replace( /^(?:https?:)?\/\//, 'https://' );
-}
-
 /**
  * Change HTML text to plain text.
  * @param {String} html - The text in HTML.

+ 4 - 4
functions/discussion.js

@@ -6,7 +6,7 @@ const {limit: {discussion: discussionLimit}} = require('../util/default.json');
  * Processes discussion commands.
  * @param {import('../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
- * @param {String} wiki - The wiki for the page.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the page.
  * @param {String} title - The title of the discussion post.
  * @param {Object} query - The siteinfo from the wiki.
  * @param {import('discord.js').MessageReaction} reaction - The reaction on the message.
@@ -24,7 +24,7 @@ function fandom_discussion(lang, msg, wiki, title, query, reaction, spoiler) {
 			if ( descresponse.statusCode !== 200 || !descbody ) {
 				console.log( '- ' + descresponse.statusCode + ': Error while getting the description.' );
 			} else {
-				var thumbnail = wiki.toLink('Special:FilePath/Wiki-wordmark.png', '', '', query.general);
+				var thumbnail = wiki.toLink('Special:FilePath/Wiki-wordmark.png');
 				var parser = new htmlparser.Parser( {
 					onopentag: (tagname, attribs) => {
 						if ( tagname === 'meta' && attribs.property === 'og:description' ) {
@@ -270,7 +270,7 @@ function fandom_discussion(lang, msg, wiki, title, query, reaction, spoiler) {
  * Send discussion posts.
  * @param {import('../util/i18n.js')} lang - The user language.
  * @param {import('discord.js').Message} msg - The Discord message.
- * @param {String} wiki - The wiki for the page.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the page.
  * @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.
@@ -355,7 +355,7 @@ function discussion_send(lang, msg, wiki, discussion, embed, spoiler) {
 	if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
 	embed.setDescription( description );
 	if ( discussion.tags?.length ) {
-		embed.addField( lang.get('discussion.tags'), Util.splitMessage( discussion.tags.map( tag => '[' + tag.articleTitle.escapeFormatting() + '](' + wiki.toLink(tag.articleTitle, '', '', {}, true) + ')' ).join(', '), {char:', ',maxLength:1000} )[0], false );
+		embed.addField( lang.get('discussion.tags'), Util.splitMessage( discussion.tags.map( tag => '[' + tag.articleTitle.escapeFormatting() + '](' + wiki.toLink(tag.articleTitle, '', '', true) + ')' ).join(', '), {char:', ',maxLength:1000} )[0], false );
 	}
 	
 	msg.sendChannel( spoiler + text + spoiler, {embed} );

+ 6 - 5
functions/global_block.js

@@ -1,5 +1,6 @@
 const cheerio = require('cheerio');
 const {timeoptions} = require('../util/default.json');
+const toTitle = require('../util/wiki.js').toTitle;
 
 /**
  * Add global blocks to user messages.
@@ -8,7 +9,7 @@ const {timeoptions} = require('../util/default.json');
  * @param {String} username - The name of the user.
  * @param {String} text - The text of the response.
  * @param {import('discord.js').MessageEmbed} embed - The embed for the page.
- * @param {String} wiki - The wiki for the page.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the page.
  * @param {String} spoiler - If the response is in a spoiler.
  * @param {String} [gender] - The gender of the user.
  */
@@ -63,7 +64,7 @@ function global_block(lang, msg, username, text, embed, wiki, spoiler, gender) {
 				if ( globaledits ) {
 					if ( msg.showEmbed() ) embed.spliceFields(1, 0, {
 						name: lang.get('user.info.globaleditcount'),
-						value: '[' + globaledits + '](https://community.fandom.com/wiki/Special:Editcount/' + username.toTitle(true) + ')',
+						value: '[' + globaledits + '](https://community.fandom.com/wiki/Special:Editcount/' + toTitle(username) + ')',
 						inline: true
 					});
 					else {
@@ -79,7 +80,7 @@ function global_block(lang, msg, username, text, embed, wiki, spoiler, gender) {
 	]).finally( () => {
 		msg.edit( spoiler + text + spoiler, {embed,allowedMentions:{parse:[]}} ).catch(log_error);
 	} );
-	else if ( wiki.endsWith( '.gamepedia.com/' ) ) Promise.all([
+	else if ( wiki.isGamepedia() ) Promise.all([
 		got.get( 'https://help.gamepedia.com/Special:GlobalBlockList/' + encodeURIComponent( username ) + '?uselang=qqx', {
 			responseType: 'text'
 		} ).then( response => {
@@ -104,7 +105,7 @@ function global_block(lang, msg, username, text, embed, wiki, spoiler, gender) {
 						if ( globalblock ) globalblock.name = gblocktitle;
 						else {
 							var block_wiki = reason[3].replace( /Special:BlockList$/, '' );
-							var gblocktext = lang.get('user.gblock.' + ( reason.length > 4 ? 'text' : 'noreason' ), timestamp, expiry, '[' + reason[1] + '](' + block_wiki + 'User:' + reason[1].toTitle(true) + ')', '[' + reason[2] + '](' + block_wiki + 'Special:Contribs/' + username.toTitle(true) + ')', reason.slice(4).join(', ').escapeFormatting());
+							var gblocktext = lang.get('user.gblock.' + ( reason.length > 4 ? 'text' : 'noreason' ), timestamp, expiry, '[' + reason[1] + '](' + block_wiki + 'User:' + toTitle(reason[1]) + ')', '[' + reason[2] + '](' + block_wiki + 'Special:Contribs/' + toTitle(username) + ')', reason.slice(4).join(', ').escapeFormatting());
 							embed.addField( gblocktitle, gblocktext );
 						}
 					}
@@ -145,7 +146,7 @@ function global_block(lang, msg, username, text, embed, wiki, spoiler, gender) {
 				if ( globaledits ) {
 					if ( msg.showEmbed() ) embed.spliceFields(1, 0, {
 						name: lang.get('user.info.globaleditcount'),
-						value: '[' + globaledits + '](https://help.gamepedia.com/Gamepedia_Help_Wiki:Global_user_tracker#' + wiki.replace( /^https:\/\/([a-z\d-]{1,50})\.gamepedia\.com\/$/, '$1/' ) + username.toTitle(true) + ')',
+						value: '[' + globaledits + '](https://help.gamepedia.com/Gamepedia_Help_Wiki:Global_user_tracker#' + wiki.hostname.replace( '.gamepedia.com', '/' ) + toTitle(username) + ')',
 						inline: true
 					});
 					else {

+ 14 - 6
functions/parse_page.js

@@ -17,17 +17,22 @@ const removeClasses = [
 	'.sortkey'
 ];
 
+const keepMainPageTag = [
+	'div.main-page-tag-lcs',
+	'div.lcs-container'
+];
+
 /**
  * Parses a wiki page to get it's description.
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {String} title - The title of the page.
  * @param {import('discord.js').MessageEmbed} embed - The embed for the page.
- * @param {String} wiki - The wiki for the page.
+ * @param {import('../util/wiki.js')} wiki - The wiki for the page.
  * @param {String} thumbnail - The default thumbnail for the wiki.
  */
 function parse_page(msg, title, embed, wiki, thumbnail) {
 	if ( !msg || ( embed.description && embed.thumbnail?.url !== thumbnail ) ) return;
-	got.get(wiki + 'api.php?action=parse&prop=text|images|parsewarnings&section=0&disablelimitreport=true&disableeditsection=true&disabletoc=true&page=' + encodeURIComponent( title ) + '&format=json').then( response => {
+	got.get( wiki + 'api.php?action=parse&prop=text|images|parsewarnings&section=0&disablelimitreport=true&disableeditsection=true&disabletoc=true&page=' + encodeURIComponent( title ) + '&format=json' ).then( response => {
 		if ( response?.body?.parse?.parsewarnings?.length ) log_warn(response.body.parse.parsewarnings);
 		if ( response.statusCode !== 200 || !response?.body?.parse?.text ) {
 			console.log( '- ' + response.statusCode + ': Error while parsing the page: ' + response?.body?.error?.info );
@@ -38,8 +43,11 @@ function parse_page(msg, title, embed, wiki, thumbnail) {
 		if ( embed.thumbnail?.url === thumbnail ) {
 			var image = response.body.parse.images.find( pageimage => ( /\.(?:png|jpg|jpeg|gif)$/.test(pageimage.toLowerCase()) && pageimage.toLowerCase().includes( title.toLowerCase().replace( / /g, '_' ) ) ) );
 			if ( !image ) {
-				thumbnail = $('img').first().prop('src');
-				if ( thumbnail && !/^(?:https?:)?\/\//.test(thumbnail) ) image = response.body.parse.images.find( pageimage => ( /\.(?:png|jpg|jpeg|gif)$/.test(pageimage.toLowerCase()) ) );
+				thumbnail = $('img').filter( (i, img) => {
+					img = $(img).prop('src')?.toLowerCase();
+					return ( /^(?:https?:)?\/\//.test(img) && /\.(?:png|jpg|jpeg|gif)(?:\/|\?|$)/.test(img) );
+				} ).first().prop('src');
+				if ( !thumbnail ) image = response.body.parse.images.find( pageimage => ( /\.(?:png|jpg|jpeg|gif)$/.test(pageimage.toLowerCase()) ) );
 			}
 			if ( image ) thumbnail = wiki.toLink('Special:FilePath/' + image);
 			if ( thumbnail ) {
@@ -50,8 +58,8 @@ function parse_page(msg, title, embed, wiki, thumbnail) {
 		if ( !embed.description ) {
 			$('h1, h2, h3, h4, h5, h6').nextAll().remove();
 			$('h1, h2, h3, h4, h5, h6').remove();
-			$(removeClasses.join(', '), $('.mw-parser-output')).remove();
-			var description = $.text().trim().escapeFormatting();
+			$(removeClasses.join(', '), $('.mw-parser-output')).not(keepMainPageTag.join(', ')).remove();
+			var description = $.text().trim().replace( /\n{3,}/g, '\n\n' ).escapeFormatting();
 			if ( description ) {
 				if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
 				embed.setDescription( description );

+ 12 - 11
functions/special_page.js

@@ -1,5 +1,6 @@
 const {Util} = require('discord.js');
 const {timeoptions} = require('../util/default.json');
+const {toPlaintext} = require('../util/functions.js');
 
 const overwrites = {
 	randompage: (fn, lang, msg, wiki, reaction, spoiler) => {
@@ -15,36 +16,36 @@ const overwrites = {
 
 const queryfunctions = {
 	title: (query, wiki) => query.querypage.results.map( result => {
-		return '[' + result.title.escapeFormatting() + '](' + wiki.toLink(result.title, '', '', query.general, true) + ')';
+		return '[' + result.title.escapeFormatting() + '](' + wiki.toLink(result.title, '', '', true) + ')';
 	} ).join('\n'),
 	times: (query, wiki) => query.querypage.results.map( result => {
-		return result.value + '× [' + result.title.escapeFormatting() + '](' + wiki.toLink(result.title, '', '', query.general, true) + ')';
+		return result.value + '× [' + result.title.escapeFormatting() + '](' + wiki.toLink(result.title, '', '', true) + ')';
 	} ).join('\n'),
 	size: (query, wiki) => query.querypage.results.map( result => {
-		return result.value + ' bytes: [' + result.title.escapeFormatting() + '](' + wiki.toLink(result.title, '', '', query.general, true) + ')';
+		return result.value + ' bytes: [' + result.title.escapeFormatting() + '](' + wiki.toLink(result.title, '', '', true) + ')';
 	} ).join('\n'),
 	redirect: (query, wiki) => query.querypage.results.map( result => {
-		return '[' + result.title.replace( / /g, '_' ).escapeFormatting() + '](' + wiki.toLink(result.title, 'redirect=no', '', query.general, true) + ')' + ( result.databaseResult && result.databaseResult.rd_title ? ' → ' + result.databaseResult.rd_title.escapeFormatting() : '' );
+		return '[' + result.title.replace( / /g, '_' ).escapeFormatting() + '](' + wiki.toLink(result.title, 'redirect=no', '', true) + ')' + ( result.databaseResult && result.databaseResult.rd_title ? ' → ' + result.databaseResult.rd_title.escapeFormatting() : '' );
 	} ).join('\n'),
 	doubleredirect: (query, wiki) => query.querypage.results.map( result => {
-		return '[' + result.title.replace( / /g, '_' ).escapeFormatting() + '](' + wiki.toLink(result.title, 'redirect=no', '', query.general, true) + ')' + ( result.databaseResult && result.databaseResult.b_title && result.databaseResult.c_title ? ' → ' + result.databaseResult.b_title.escapeFormatting() + ' → ' + result.databaseResult.c_title.escapeFormatting() : '' );
+		return '[' + result.title.replace( / /g, '_' ).escapeFormatting() + '](' + wiki.toLink(result.title, 'redirect=no', '', true) + ')' + ( result.databaseResult && result.databaseResult.b_title && result.databaseResult.c_title ? ' → ' + result.databaseResult.b_title.escapeFormatting() + ' → ' + result.databaseResult.c_title.escapeFormatting() : '' );
 	} ).join('\n'),
 	timestamp: (query, wiki) => query.querypage.results.map( result => {
-		return new Date(result.timestamp).toLocaleString(lang.get('dateformat'), timeoptions).escapeFormatting() + ': [' + result.title.escapeFormatting() + '](' + wiki.toLink(result.title, '', '', query.general, true) + ')';
+		return new Date(result.timestamp).toLocaleString(lang.get('dateformat'), timeoptions).escapeFormatting() + ': [' + result.title.escapeFormatting() + '](' + wiki.toLink(result.title, '', '', true) + ')';
 	} ).join('\n'),
 	media: (query) => query.querypage.results.map( result => {
 		var ms = result.title.split(';');
 		return '**' + ms[1] + '**: ' + ms[2] + ' files (' + ms[3] + ' bytes)';
 	} ).join('\n'),
 	category: (query, wiki) => query.querypage.results.map( result => {
-		return result.value + '× [' + result.title.escapeFormatting() + '](' + wiki.toLink('Category:' + result.title, '', '', query.general, true) + ')';
+		return result.value + '× [' + result.title.escapeFormatting() + '](' + wiki.toLink('Category:' + result.title, '', '', true) + ')';
 	} ).join('\n'),
 	gadget: (query) => query.querypage.results.map( result => {
 		result.title = result.title.replace( /^(?:.*:)?gadget-/, '' );
 		return '**' + result.title.escapeFormatting() + '**: ' + result.value + ' users (' + result.ns + ' active)';
 	} ).join('\n'),
 	recentchanges: (query, wiki) => query.recentchanges.map( result => {
-		return '[' + result.title.escapeFormatting() + '](' + wiki.toLink(result.title, ( result.type === 'edit' ? 'diff=' + result.revid + '&oldid=' + result.old_revid : '' ), '', query.general, true) + ')';
+		return '[' + result.title.escapeFormatting() + '](' + wiki.toLink(result.title, ( result.type === 'edit' ? {diff:result.revid,oldid:result.old_revid} : '' ), '', true) + ')';
 	} ).join('\n')
 }
 
@@ -116,7 +117,7 @@ const descriptions = {
  * @param {String} title - The title 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 {String} wiki - The wiki for the page.
+ * @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.
  */
@@ -129,7 +130,7 @@ function special_page(lang, msg, title, specialpage, embed, wiki, reaction, spoi
 	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)') );
 	}
-	got.get( wiki + 'api.php?action=query&meta=siteinfo|allmessages&siprop=general&amenableparser=true&amtitle=' + encodeURIComponent( title ) + '&ammessages=' + ( specialpage in descriptions ? descriptions[specialpage] : encodeURIComponent( specialpage ) + '-summary' ) + ( specialpage in querypages ? querypages[specialpage][0] : '' ) + '&format=json' ).then( response => {
+	got.get( wiki + 'api.php?action=query&meta=allmessages&amenableparser=true&amtitle=' + encodeURIComponent( title ) + '&ammessages=' + ( specialpage in descriptions ? descriptions[specialpage] : encodeURIComponent( specialpage ) + '-summary' ) + ( specialpage in querypages ? querypages[specialpage][0] : '' ) + '&format=json' ).then( response => {
 		var body = response.body;
 		if ( body && body.warnings ) log_warn(body.warnings);
 		if ( response.statusCode !== 200 || !body ) {
@@ -137,7 +138,7 @@ function special_page(lang, msg, title, specialpage, embed, wiki, reaction, spoi
 		}
 		else {
 			if ( body.query.allmessages[0]['*'] ) {
-				var description = body.query.allmessages[0]['*'].toPlaintext();
+				var description = toPlaintext(body.query.allmessages[0]['*']);
 				if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
 				embed.setDescription( description );
 			}

+ 61 - 0
util/functions.js

@@ -0,0 +1,61 @@
+/**
+ * Make wikitext formatting usage.
+ * @param {String} [text] - The text to modify.
+ * @param {Boolean} [showEmbed] - If the text is used in an embed.
+ * @param {import('./wiki.js')|String} [args] - The text contains markdown links.
+ * @returns {String}
+ */
+function toFormatting(text = '', showEmbed = false, ...args) {
+	if ( showEmbed ) return toMarkdown(text, ...args);
+	else return toPlaintext(text);
+};
+
+/**
+ * Turns wikitext formatting into markdown.
+ * @param {String} [text] - The text to modify.
+ * @param {import('./wiki.js')} [wiki] - The wiki.
+ * @param {String} [title] - The page title.
+ * @returns {String}
+ */
+function toMarkdown(text = '', wiki, title = '') {
+	text = text.replace( /[()\\]/g, '\\$&' );
+	var link = null;
+	var regex = /\[\[(?:([^\|\]]+)\|)?([^\]]+)\]\]([a-z]*)/g;
+	while ( ( link = regex.exec(text) ) !== null ) {
+		var pagetitle = ( link[1] || link[2] );
+		var page = wiki.toLink(( /^[#\/]/.test(pagetitle) ? title + ( pagetitle.startsWith( '/' ) ? pagetitle : '' ) : pagetitle ), '', ( pagetitle.startsWith( '#' ) ? pagetitle.substring(1) : '' ), true);
+		text = text.replaceSave( link[0], '[' + link[2] + link[3] + '](' + page + ')' );
+	}
+	regex = /\/\*\s*([^\*]+?)\s*\*\/\s*(.)?/g;
+	while ( title !== '' && ( link = regex.exec(text) ) !== null ) {
+		text = text.replaceSave( link[0], '[→' + link[1] + '](' + wiki.toLink(title, '', link[1], true) + ')' + ( link[2] ? ': ' + link[2] : '' ) );
+	}
+	return escapeFormatting(text, true);
+};
+
+/**
+ * Removes wikitext formatting.
+ * @param {String} [text] - The text to modify.
+ * @returns {String}
+ */
+function toPlaintext(text = '') {
+	return escapeFormatting(text.replace( /\[\[(?:[^\|\]]+\|)?([^\]]+)\]\]/g, '$1' ).replace( /\/\*\s*([^\*]+?)\s*\*\//g, '→$1:' ));
+};
+
+/**
+ * Escapes formatting.
+ * @param {String} [text] - The text to modify.
+ * @param {Boolean} [isMarkdown] - The text contains markdown links.
+ * @returns {String}
+ */
+function escapeFormatting(text = '', isMarkdown = false) {
+	if ( !isMarkdown ) text = text.replace( /[()\\]/g, '\\$&' );
+	return text.replace( /[`_*~:<>{}@|]|\/\//g, '\\$&' );
+};
+
+module.exports = {
+	toFormatting,
+	toMarkdown,
+	toPlaintext,
+	escapeFormatting
+};

+ 14 - 10
util/newMessage.js

@@ -1,5 +1,6 @@
 const {Util} = require('discord.js');
 const {limit: {command: commandLimit}, defaultSettings, wikiProjects} = require('./default.json');
+const Wiki = require('./wiki.js');
 const check_wiki = {
 	fandom: require('../cmds/wiki/fandom.js'),
 	gamepedia: require('../cmds/wiki/gamepedia.js'),
@@ -24,12 +25,13 @@ fs.readdir( './cmds', (error, files) => {
  * Processes new messages.
  * @param {import('discord.js').Message} msg - The Discord message.
  * @param {import('./i18n.js')} lang - The user language.
- * @param {String} [wiki] - The default wiki.
+ * @param {Wiki} [wiki] - The default wiki.
  * @param {String} [prefix] - The prefix for the message.
  * @param {Boolean} [noInline] - Parse inline commands?
  * @param {String} [content] - Overwrite for the message content.
  */
 function newMessage(msg, lang, wiki = defaultSettings.wiki, prefix = process.env.prefix, noInline = null, content = '') {
+	wiki = new Wiki(wiki);
 	msg.noInline = noInline;
 	var cont = ( content || msg.content );
 	var cleanCont = ( content && Util.cleanContent(content, msg) || msg.cleanContent );
@@ -73,25 +75,25 @@ function newMessage(msg, lang, wiki = defaultSettings.wiki, prefix = process.env
 		if ( pausecmd ) return pausecmdmap[aliasInvoke](lang, msg, args, line, wiki);
 		if ( aliasInvoke in cmdmap ) return cmdmap[aliasInvoke](lang, msg, args, line, wiki);
 		if ( /^![a-z\d-]{1,50}$/.test(invoke) ) {
-			return cmdmap.LINK(lang, msg, args.join(' '), 'https://' + invoke.substring(1) + '.gamepedia.com/', invoke + ' ');
+			return cmdmap.LINK(lang, msg, args.join(' '), new Wiki('https://' + invoke.substring(1) + '.gamepedia.com/'), invoke + ' ');
 		}
 		if ( /^\?(?:[a-z-]{2,12}\.)?[a-z\d-]{1,50}$/.test(invoke) ) {
 			let invokeWiki = wiki;
 			if ( invoke.includes( '.' ) ) invokeWiki = 'https://' + invoke.split('.')[1] + '.fandom.com/' + invoke.substring(1).split('.')[0] + '/';
 			else invokeWiki = 'https://' + invoke.substring(1) + '.fandom.com/';
-			return cmdmap.LINK(lang, msg, args.join(' '), invokeWiki, invoke + ' ');
+			return cmdmap.LINK(lang, msg, args.join(' '), new Wiki(invokeWiki), invoke + ' ');
 		}
 		if ( /^\?\?(?:[a-z-]{2,12}\.)?[a-z\d-]{1,50}$/.test(invoke) ) {
 			let invokeWiki = wiki;
 			if ( invoke.includes( '.' ) ) invokeWiki = 'https://' + invoke.split('.')[1] + '.wikia.org/' + invoke.substring(2).split('.')[0] + '/';
 			else invokeWiki = 'https://' + invoke.substring(2) + '.wikia.org/';
-			return cmdmap.LINK(lang, msg, args.join(' '), invokeWiki, invoke + ' ');
+			return cmdmap.LINK(lang, msg, args.join(' '), new Wiki(invokeWiki), invoke + ' ');
 		}
 		if ( /^!!(?:[a-z\d-]{1,50}\.)?[a-z\d-]{1,50}\.[a-z\d-]{1,10}(?:\/|$)/.test(invoke) ) {
 			let project = wikiProjects.find( project => invoke.split('/')[0].endsWith( project.name ) );
 			if ( project ) {
 				let regex = invoke.match( new RegExp( project.regex ) );
-				if ( regex && invoke === '!!' + regex[1] ) return cmdmap.LINK(lang, msg, args.join(' '), 'https://' + regex[1] + project.scriptPath, invoke + ' ');
+				if ( regex && invoke === '!!' + regex[1] ) return cmdmap.LINK(lang, msg, args.join(' '), new Wiki('https://' + regex[1] + project.scriptPath), invoke + ' ');
 			}
 		}
 		return cmdmap.LINK(lang, msg, line, wiki);
@@ -159,12 +161,13 @@ function newMessage(msg, lang, wiki = defaultSettings.wiki, prefix = process.env
 				console.log( '- ' + response.statusCode + ': Error while following the links: ' + ( body && body.error && body.error.info ) );
 				return;
 			}
+			wiki.updateWiki(body.query.general);
 			if ( body.query.normalized ) {
 				body.query.normalized.forEach( title => links.filter( link => link.title === title.from ).forEach( link => link.title = title.to ) );
 			}
 			if ( body.query.interwiki ) {
 				body.query.interwiki.forEach( interwiki => links.filter( link => link.title === interwiki.title ).forEach( link => {
-					link.url = interwiki.url + ( link.section ? '#' + link.section.toSection() : '' );
+					link.url = ( link.section ? interwiki.url.split('#')[0] + Wiki.toSection(link.section) : interwiki.url );
 				} ) );
 			}
 			if ( body.query.pages ) {
@@ -174,10 +177,10 @@ function newMessage(msg, lang, wiki = defaultSettings.wiki, prefix = process.env
 				} ) );
 				querypages.filter( page => page.missing !== undefined && page.known === undefined ).forEach( page => links.filter( link => link.title === page.title ).forEach( link => {
 					if ( ( page.ns === 2 || page.ns === 202 ) && !page.title.includes( '/' ) ) return;
-					link.url = wiki.toLink(link.title, 'action=edit&redlink=1', '', body.query.general);
+					link.url = wiki.toLink(link.title, 'action=edit&redlink=1');
 				} ) );
 			}
-			if ( links.length ) msg.sendChannel( links.map( link => link.spoiler + '<' + ( link.url || wiki.toLink(link.title, '', link.section, body.query.general) ) + '>' + link.spoiler ).join('\n'), {split:true} );
+			if ( links.length ) msg.sendChannel( links.map( link => link.spoiler + '<' + ( link.url || wiki.toLink(link.title, '', link.section) ) + '>' + link.spoiler ).join('\n'), {split:true} );
 		}, error => {
 			if ( wiki.noWiki(error.message) ) {
 				console.log( '- This wiki doesn\'t exist!' );
@@ -199,6 +202,7 @@ function newMessage(msg, lang, wiki = defaultSettings.wiki, prefix = process.env
 				console.log( '- ' + response.statusCode + ': Error while following the links: ' + ( body && body.error && body.error.info ) );
 				return;
 			}
+			wiki.updateWiki(body.query.general);
 			if ( body.query.normalized ) {
 				body.query.normalized.forEach( title => embeds.filter( embed => embed.title === title.from ).forEach( embed => embed.title = title.to ) );
 			}
@@ -213,12 +217,12 @@ function newMessage(msg, lang, wiki = defaultSettings.wiki, prefix = process.env
 					embeds.splice(embeds.indexOf(embed), 1);
 					if ( page.ns === 0 && !embed.section ) {
 						var template = querypages.find( template => template.ns === 10 && template.title.split(':').slice(1).join(':') === embed.title );
-						if ( template && template.missing === undefined ) embed.template = wiki.toLink(template.title, '', '', body.query.general);
+						if ( template && template.missing === undefined ) embed.template = wiki.toLink(template.title);
 					}
 					if ( embed.template || !body.query.variables || !body.query.variables.some( variable => variable.toUpperCase() === embed.title ) ) missing.push(embed);
 				} ) );
 				if ( missing.length ) {
-					msg.sendChannel( missing.map( embed => embed.spoiler + '<' + ( embed.template || wiki.toLink(embed.title, 'action=edit&redlink=1', '', body.query.general) ) + '>' + embed.spoiler ).join('\n'), {split:true} );
+					msg.sendChannel( missing.map( embed => embed.spoiler + '<' + ( embed.template || wiki.toLink(embed.title, 'action=edit&redlink=1') ) + '>' + embed.spoiler ).join('\n'), {split:true} );
 				}
 			}
 			if ( embeds.length ) {

+ 214 - 0
util/wiki.js

@@ -0,0 +1,214 @@
+const util = require('util');
+const {defaultSettings, wikiProjects} = require('./default.json');
+
+/**
+ * A wiki.
+ * @class Wiki
+ */
+class Wiki extends URL {
+	/**
+	 * Creates a new wiki.
+	 * @param {String|URL|Wiki} [wiki] - The wiki script path.
+	 * @param {String|URL|Wiki} [base] - The base for the wiki.
+	 * @constructs Wiki
+	 */
+	constructor(wiki = defaultSettings.wiki, base) {
+		super(wiki, base);
+		this.protocol = 'https';
+		let articlepath = '/index.php?title=$1';
+		if ( this.isFandom() ) articlepath = this.pathname + 'wiki/$1';
+		if ( this.isGamepedia() ) articlepath = '/$1';
+		let project = wikiProjects.find( project => this.hostname.endsWith( project.name ) );
+		if ( project ) {
+			let regex = ( this.host + this.pathname ).match( new RegExp( '^' + project.regex + project.scriptPath + '$' ) );
+			if ( regex ) articlepath = 'https://' + regex[1] + project.articlePath + '$1';
+		}
+		this.articlepath = articlepath;
+		this.mainpage = '';
+	}
+
+	/**
+	 * @type {String}
+	 */
+	get articlepath() {
+		return this.articleURL.pathname + this.articleURL.search;
+	}
+	set articlepath(path) {
+		this.articleURL = new articleURL(path, this);
+	}
+
+	/**
+	 * @type {String}
+	 */
+	get mainpage() {
+		return this.articleURL.mainpage;
+	}
+	set mainpage(title) {
+		this.articleURL.mainpage = title;
+	}
+
+	/**
+	 * Updates the wiki url.
+	 * @param {Object} siteinfo - Siteinfo from the wiki API.
+	 * @param {String} siteinfo.server - Server of the wiki with protocol. (For legacy Fandom wikis)
+	 * @param {String} siteinfo.servername - Hostname of the wiki.
+	 * @param {String} siteinfo.scriptpath - Scriptpath of the wiki.
+	 * @param {String} siteinfo.articlepath - Articlepath of the wiki.
+	 * @param {String} siteinfo.mainpage - Main page of the wiki.
+	 * @returns {Wiki}
+	 */
+	updateWiki({server, servername, scriptpath, articlepath, mainpage}) {
+		if ( servername ) this.hostname = servername;
+		else this.hostname = server.replace( /^(?:https?:)?\/\//, '' );
+		this.pathname = scriptpath + '/';
+		this.articlepath = articlepath;
+		this.mainpage = mainpage;
+		return this;
+	}
+
+	/**
+	 * Check for a Fandom wiki.
+	 * @returns {Boolean}
+	 */
+	isFandom() {
+		return ( this.hostname.endsWith( '.fandom.com' ) || this.hostname.endsWith( '.wikia.org' ) );
+	}
+
+	/**
+	 * Check for a Gamepedia wiki.
+	 * @returns {Boolean}
+	 */
+	isGamepedia() {
+		return this.hostname.endsWith( '.gamepedia.com' );
+	}
+
+	/**
+	 * Check if a wiki is missing.
+	 * @param {String} [message] - Error message or response url.
+	 * @returns {Boolean}
+	 */
+	noWiki(message = '') {
+		if ( !( this.isGamepedia() || this.isFandom() ) ) return false;
+		if ( this.hostname.startsWith( 'www.' ) || message.startsWith( 'https://www.' ) ) return true;
+		return [
+			'https://community.fandom.com/wiki/Community_Central:Not_a_valid_community?from=' + this.hostname,
+			this + 'language-wikis'
+		].includes( message.replace( /Unexpected token < in JSON at position 0 in "([^ ]+)"/, '$1' ) );
+	}
+
+	/**
+	 * Get an URI encoded link.
+	 * @param {String} [title] - Name of the page.
+	 * @returns {String}
+	 */
+	toDescLink(title = this.mainpage) {
+		return this.articleURL.href.replace( '$1', encodeURIComponent( title.replace( / /g, '_' ) ) );
+	}
+
+	/**
+	 * Get a page link.
+	 * @param {String} [title] - Name of the page.
+	 * @param {URLSearchParams} [querystring] - Query arguments of the page.
+	 * @param {String} [fragment] - Fragment of the page.
+	 * @param {Boolean} [isMarkdown] - Use the link in markdown.
+	 * @returns {String}
+	 */
+	toLink(title = '', querystring = '', fragment = '', isMarkdown = false) {
+		querystring = new URLSearchParams(querystring);
+		if ( !querystring.toString().length ) title = ( title || this.mainpage );
+		title = title.replace( / /g, '_' );
+		let link = new URL(this.articleURL);
+		link.pathname = link.pathname.replace( '$1', title.replace( /\\/g, '%5C' ) );
+		link.searchParams.forEach( (value, name, searchParams) => {
+			if ( value.includes( '$1' ) ) {
+				if ( !title ) searchParams.delete(name);
+				else searchParams.set(name, value.replace( '$1', title ));
+			}
+		} );
+		querystring.forEach( (value, name) => {
+			link.searchParams.append(name, value);
+		} );
+		let output = decodeURI( link ).replace( /\\/g, '%5C' ).replace( /@(here|everyone)/g, '%40$1' );
+		if ( isMarkdown ) output = output.replace( /([\(\)])/g, '\\$1' );
+		return output + Wiki.toSection(fragment);
+	}
+
+	/**
+	 * Encode a page title.
+	 * @param {String} [title] - Title of the page.
+	 * @returns {String}
+	 * @static
+	 */
+	static toTitle(title = '') {
+		return title.replace( / /g, '_' ).replace( /[?&%\\]/g, (match) => {
+			return '%' + match.charCodeAt().toString(16).toUpperCase();
+		} ).replace( /@(here|everyone)/g, '%40$1' ).replace( /[()]/g, '\\$&' );
+	};
+
+	/**
+	 * Encode a link section.
+	 * @param {String} [fragment] - Fragment of the page.
+	 * @returns {String}
+	 * @static
+	 */
+	static toSection(fragment = '') {
+		if ( !fragment ) return '';
+		fragment = fragment.replace( / /g, '_' );
+		if ( !/['"`^{}<>|\\]|@(everyone|here)/.test(fragment) ) return '#' + fragment;
+		return '#' + encodeURIComponent( fragment ).replace( /[!'()*~]/g, (match) => {
+			return '%' + match.charCodeAt().toString(16).toUpperCase();
+		} ).replace( /%3A/g, ':' ).replace( /%/g, '.' );
+	}
+
+	[util.inspect.custom](depth, opts) {
+		if ( typeof depth === 'number' && depth < 0 ) return this;
+		const wiki = {
+			href: this.href,
+			origin: this.origin,
+			protocol: this.protocol,
+			username: this.username,
+			password: this.password,
+			host: this.host,
+			hostname: this.hostname,
+			port: this.port,
+			pathname: this.pathname,
+			search: this.search,
+			searchParams: this.searchParams,
+			hash: this.hash,
+			articlepath: this.articlepath,
+			articleURL: this.articleURL,
+			mainpage: this.mainpage
+		}
+		return 'Wiki ' + util.inspect(wiki, opts);
+	}
+}
+
+/**
+ * An article URL.
+ * @class articleURL
+ */
+class articleURL extends URL {
+	/**
+	 * Creates a new article URL.
+	 * @param {String|URL|Wiki} [articlepath] - The article path.
+	 * @param {String|URL|Wiki} [wiki] - The wiki.
+	 * @constructs articleURL
+	 */
+	constructor(articlepath = '/index.php?title=$1', wiki) {
+		super(articlepath, wiki);
+		this.protocol = 'https';
+		this.mainpage = '';
+	}
+
+	[util.inspect.custom](depth, opts) {
+		if ( typeof depth === 'number' && depth < 0 ) return this;
+		if ( typeof depth === 'number' && depth < 2 ) {
+			var link = this.href;
+			var mainpage = link.replace( '$1', ( this.mainpage || 'Main Page' ).replace( / /g, '_' ) );
+			return 'articleURL { ' + util.inspect(link, opts) + ' => ' + util.inspect(mainpage, opts) + ' }';
+		}
+		return super[util.inspect.custom](depth, opts);
+	}
+}
+
+module.exports = Wiki;