Selaa lähdekoodia

Merge pull request #88 from Markus-Rost/v2.3.0

Continue Dashboard, fix crash
MarkusRost 4 vuotta sitten
vanhempi
sitoutus
1e88dd4320

+ 12 - 8
bot.js

@@ -68,8 +68,12 @@ String.prototype.isMention = function(guild) {
 	return text === '@' + client.user.username || text.replace( /^<@!?(\d+)>$/, '$1' ) === client.user.id || ( guild && text === '@' + guild.me.displayName );
 };
 
+Discord.Channel.prototype.isGuild = function() {
+	return ['text', 'news'].includes( this.type );
+}
+
 Discord.Message.prototype.isAdmin = function() {
-	return this.channel.type === 'text' && this.member && ( this.member.permissions.has('MANAGE_GUILD') || ( this.isOwner() && this.evalUsed ) );
+	return this.channel.isGuild() && this.member && ( this.member.permissions.has('MANAGE_GUILD') || ( this.isOwner() && this.evalUsed ) );
 };
 
 Discord.Message.prototype.isOwner = function() {
@@ -77,11 +81,11 @@ Discord.Message.prototype.isOwner = function() {
 };
 
 Discord.Message.prototype.showEmbed = function() {
-	return this.channel.type !== 'text' || this.channel.permissionsFor(client.user).has('EMBED_LINKS');
+	return !this.channel.isGuild() || this.channel.permissionsFor(client.user).has('EMBED_LINKS');
 };
 
 Discord.Message.prototype.uploadFiles = function() {
-	return this.channel.type !== 'text' || this.channel.permissionsFor(client.user).has('ATTACH_FILES');
+	return !this.channel.isGuild() || this.channel.permissionsFor(client.user).has('ATTACH_FILES');
 };
 
 String.prototype.escapeFormatting = function(isMarkdown) {
@@ -95,7 +99,7 @@ String.prototype.replaceSave = function(pattern, replacement) {
 };
 
 Discord.Message.prototype.reactEmoji = function(name, ignorePause = false) {
-	if ( this.channel.type !== 'text' || !pause[this.guild.id] || ( ignorePause && ( this.isAdmin() || this.isOwner() ) ) ) {
+	if ( !this.channel.isGuild() || !pause[this.guild.id] || ( ignorePause && ( this.isAdmin() || this.isOwner() ) ) ) {
 		var emoji = ':error:440871715938238494';
 		switch ( name ) {
 			case 'nowiki':
@@ -119,7 +123,7 @@ Discord.MessageReaction.prototype.removeEmoji = function() {
 };
 
 Discord.Message.prototype.sendChannel = function(content, options = {}, ignorePause = false) {
-	if ( this.channel.type !== 'text' || !pause[this.guild.id] || ( ignorePause && ( this.isAdmin() || this.isOwner() ) ) ) {
+	if ( !this.channel.isGuild() || !pause[this.guild.id] || ( ignorePause && ( this.isAdmin() || this.isOwner() ) ) ) {
 		if ( !options.allowedMentions ) options.allowedMentions = {users:[this.author.id]};
 		return this.channel.send(content, options).then( msg => {
 			if ( msg.length ) msg.forEach( message => allowDelete(message, this.author.id) );
@@ -154,7 +158,7 @@ Discord.Message.prototype.sendChannelError = function(content, options = {}) {
 };
 
 Discord.Message.prototype.replyMsg = function(content, options = {}, ignorePause = false, letDelete = true) {
-	if ( this.channel.type !== 'text' || !pause[this.guild.id] || ( ignorePause && ( this.isAdmin() || this.isOwner() ) ) ) {
+	if ( !this.channel.isGuild() || !pause[this.guild.id] || ( ignorePause && ( this.isAdmin() || this.isOwner() ) ) ) {
 		if ( !options.allowedMentions ) options.allowedMentions = {users:[this.author.id]};
 		return this.reply(content, options).then( msg => {
 			if ( letDelete ) {
@@ -197,7 +201,7 @@ String.prototype.hasPrefix = function(prefix, flags = '') {
 
 client.on( 'message', msg => {
 	if ( isStop || msg.type !== 'DEFAULT' || msg.system || msg.webhookID || msg.author.bot || msg.author.id === msg.client.user.id ) return;
-	if ( !msg.content.hasPrefix(( msg.channel.type === 'text' && patreons[msg.guild.id] || process.env.prefix ), 'm') ) {
+	if ( !msg.content.hasPrefix(( msg.channel.isGuild() && patreons[msg.guild.id] || process.env.prefix ), 'm') ) {
 		if ( msg.content === process.env.prefix + 'help' && ( msg.isAdmin() || msg.isOwner() ) ) {
 			if ( msg.channel.permissionsFor(msg.client.user).has('SEND_MESSAGES') ) {
 				console.log( msg.guild.name + ': ' + msg.content );
@@ -209,7 +213,7 @@ client.on( 'message', msg => {
 		}
 		if ( !( msg.content.includes( '[[' ) && msg.content.includes( ']]' ) ) && !( msg.content.includes( '{{' ) && msg.content.includes( '}}' ) ) ) return;
 	}
-	if ( msg.channel.type === 'text' ) {
+	if ( msg.channel.isGuild() ) {
 		var permissions = msg.channel.permissionsFor(msg.client.user);
 		var missing = permissions.missing(['SEND_MESSAGES','ADD_REACTIONS','USE_EXTERNAL_EMOJIS','READ_MESSAGE_HISTORY']);
 		if ( missing.length ) {

+ 1 - 1
cmds/eval.js

@@ -151,7 +151,7 @@ function removePatreons(guild, msg) {
 function removeSettings(msg) {
 	if ( !msg ) return 'removeSettings(msg) – No message provided!';
 	try {
-		msg.client.shard.broadcastEval( `[[...this.guilds.cache.keys()], [...this.channels.cache.filter( channel => channel.type === 'text' ).keys()]]` ).then( results => {
+		msg.client.shard.broadcastEval( `[[...this.guilds.cache.keys()], [...this.channels.cache.filter( channel => channel.isGuild() ).keys()]]` ).then( results => {
 			var all_guilds = results.map( result => result[0] ).reduce( (acc, val) => acc.concat(val), [] );
 			var all_channels = results.map( result => result[1] ).reduce( (acc, val) => acc.concat(val), [] );
 			var guilds = [];

+ 2 - 2
cmds/get.js

@@ -64,7 +64,7 @@ async function cmd_get(lang, msg, args, line, wiki) {
 			} );
 		}
 		
-		var channel = await msg.client.shard.broadcastEval( `if ( this.channels.cache.filter( channel => channel.type === 'text' ).has('${id}') ) {
+		var channel = await msg.client.shard.broadcastEval( `if ( this.channels.cache.filter( channel => channel.isGuild() ).has('${id}') ) {
 			var {name, id, guild: {name: guild, id: guildID, me}} = this.channels.cache.get('${id}');
 			( {
 				name, id, guild, guildID,
@@ -137,7 +137,7 @@ async function cmd_get(lang, msg, args, line, wiki) {
 		}
 		
 		msg.replyMsg( 'I couldn\'t find a result for `' + id + '`', {}, true );
-	} else if ( msg.channel.type !== 'text' || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
+	} else if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
 }
 
 module.exports = {

+ 5 - 5
cmds/help.js

@@ -79,7 +79,7 @@ const restrictions = {
  * @param {import('../util/wiki.js')} wiki - The wiki for the message.
  */
 function cmd_help(lang, msg, args, line, wiki) {
-	if ( msg.channel.type === 'text' && pause[msg.guild.id] && ( args.join('') || !msg.isAdmin() ) ) return;
+	if ( msg.channel.isGuild() && pause[msg.guild.id] && ( args.join('') || !msg.isAdmin() ) ) return;
 	if ( msg.isAdmin() && msg.defaultSettings ) help_server(lang, msg);
 	var isMinecraft = ( wiki.href === lang.get('minecraft.link') );
 	if ( args.join('') ) {
@@ -90,7 +90,7 @@ function cmd_help(lang, msg, args, line, wiki) {
 		var invoke = args[0].toLowerCase();
 		var cmd = ( lang.aliases[invoke] || invoke );
 		if ( cmd === 'admin' ) {
-			if ( msg.channel.type !== 'text' || msg.isAdmin() ) {
+			if ( !msg.channel.isGuild() || msg.isAdmin() ) {
 				var cmdlist = lang.get('help.admin') + '\n';
 				cmdlist += formathelp(helplist.admin, msg, lang);
 				msg.sendChannel( cmdlist, {split:{char:'🔹',prepend:'🔹'}} );
@@ -139,12 +139,12 @@ function cmd_help(lang, msg, args, line, wiki) {
  * @param {import('../util/i18n.js')} lang - The user language.
  */
 function formathelp(messages, msg, lang) {
-	var prefix = ( msg.channel.type === 'text' && patreons[msg.guild.id] || process.env.prefix );
-	var mention = '@' + ( msg.channel.type === 'text' ? msg.guild.me.displayName : msg.client.user.username );
+	var prefix = ( msg.channel.isGuild() && patreons[msg.guild.id] || process.env.prefix );
+	var mention = '@' + ( msg.channel.isGuild() ? msg.guild.me.displayName : msg.client.user.username );
 	return messages.filter( message => {
 		if ( restrictions.inline.includes( message ) && msg.noInline ) return false;
 		if ( !restrictions.patreon.includes( message ) ) return true;
-		return ( msg.channel.type === 'text' && msg.guild.id in patreons );
+		return ( msg.channel.isGuild() && msg.guild.id in patreons );
 	} ).map( message => {
 		var cmd = message.split('.')[0];
 		var intro = ( restrictions.inline.includes( message ) ? '' : prefix );

+ 2 - 2
cmds/patreon.js

@@ -11,7 +11,7 @@ var db = require('../util/database.js');
  */
 function cmd_patreon(lang, msg, args, line, wiki) {
 	if ( !( process.env.channel.split('|').includes( msg.channel.id ) && args.join('') ) ) {
-		if ( msg.channel.type !== 'text' || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
+		if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
 		return;
 	}
 	
@@ -275,7 +275,7 @@ function cmd_patreon(lang, msg, args, line, wiki) {
 		} );
 	} );
 	
-	if ( msg.channel.type !== 'text' || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
+	if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
 }
 
 module.exports = {

+ 2 - 2
cmds/pause.js

@@ -7,7 +7,7 @@
  * @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() ) ) {
+	if ( msg.channel.isGuild() && args.join(' ').split('\n')[0].isMention(msg.guild) && ( msg.isAdmin() || msg.isOwner() ) ) {
 		if ( pause[msg.guild.id] ) {
 			delete pause[msg.guild.id];
 			console.log( '- Pause ended.' );
@@ -17,7 +17,7 @@ function cmd_pause(lang, msg, args, line, wiki) {
 			console.log( '- Pause started.' );
 			pause[msg.guild.id] = true;
 		}
-	} else if ( msg.channel.type !== 'text' || !pause[msg.guild.id] ) {
+	} else if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) {
 		this.LINK(lang, msg, line, wiki);
 	}
 }

+ 3 - 3
cmds/rcscript.js

@@ -377,7 +377,7 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 					msg.replyMsg( lang.get('rcscript.updated_display') + ' `' + args[1] + '`\n`' + cmd + '`', {}, true );
 				} );
 			}
-			if ( selected_row.wiki.isFandom() && args[0] === 'feeds' ) {
+			if ( new Wiki(selected_row.wiki).isFandom() && args[0] === 'feeds' ) {
 				if ( process.env.READONLY ) return msg.replyMsg( lang.get('general.readonly') + '\n' + process.env.invite, {}, true );
 				if ( args[1] === 'only' ) {
 					if ( selected_row.rcid === -1 ) {
@@ -507,7 +507,7 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 					text += '\n' + lang.get('rcscript.rc') + ' *`' + lang.get('rcscript.disabled' ) + '`*';
 					text += '\n`' + cmd + ' feeds only` ' + lang.get('rcscript.toggle') + '\n';
 				}
-				if ( selected_row.wiki.isFandom() ) {
+				if ( new Wiki(selected_row.wiki).isFandom() ) {
 					text += '\n' + lang.get('rcscript.feeds') + ' *`' + lang.get('rcscript.' + ( selected_row.wikiid ? 'enabled' : 'disabled' )) + '`*';
 					text += '\n' + lang.get('rcscript.help_feeds') + '\n`' + cmd + ' feeds` ' + lang.get('rcscript.toggle') + '\n';
 				}
@@ -554,7 +554,7 @@ function cmd_rcscript(lang, msg, args, line, wiki) {
 					row_text += '\n' + lang.get('rcscript.rc') + ' *`' + lang.get('rcscript.disabled' ) + '`*';
 					if ( only ) row_text += '\n`' + cmd + ' feeds only` ' + lang.get('rcscript.toggle') + '\n';
 				}
-				if ( row.wiki.isFandom() ) {
+				if ( new Wiki(row.wiki).isFandom() ) {
 					row_text += '\n' + lang.get('rcscript.feeds') + ' *`' + lang.get('rcscript.' + ( row.wikiid ? 'enabled' : 'disabled' )) + '`*';
 					if ( only ) row_text += '\n' + lang.get('rcscript.help_feeds') + '\n`' + cmd + ' feeds` ' + lang.get('rcscript.toggle') + '\n';
 				}

+ 1 - 1
cmds/stop.js

@@ -15,7 +15,7 @@ async function cmd_stop(lang, msg, args, line, wiki) {
 		await msg.replyMsg( 'I\'ll restart myself now!', {}, true );
 		console.log( '\n- Restarting all shards!\n\n' );
 		await msg.client.shard.respawnAll();
-	} else if ( msg.channel.type !== 'text' || !pause[msg.guild.id] ) {
+	} else if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) {
 		this.LINK(lang, msg, line, wiki);
 	}
 }

+ 5 - 3
cmds/test.js

@@ -23,8 +23,9 @@ const wsStatus = [
  */
 function cmd_test(lang, msg, args, line, wiki) {
 	if ( args.join('') ) {
-		if ( msg.channel.type !== 'text' || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
-	} else if ( msg.channel.type !== 'text' || !pause[msg.guild.id] ) {
+		if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
+	}
+	else if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) {
 		if ( msg.isAdmin() && msg.defaultSettings ) help_setup(lang, msg);
 		let textList = lang.get('test.text').filter( text => text.trim() );
 		var text = textList[Math.floor(Math.random() * ( textList.length * 5 ))] || lang.get('test.text.0');
@@ -92,7 +93,8 @@ function cmd_test(lang, msg, args, line, wiki) {
 				message.edit( message.content, {embed,allowedMentions:{users:[msg.author.id]}} ).catch(log_error);
 			} );
 		} );
-	} else {
+	}
+	else {
 		console.log( '- Test: Paused!' );
 		msg.replyMsg( lang.get('test.pause'), {}, true );
 	}

+ 4 - 4
cmds/verification.js

@@ -11,7 +11,7 @@ var db = require('../util/database.js');
  */
 function cmd_verification(lang, msg, args, line, wiki) {
 	if ( !msg.isAdmin() ) {
-		if ( msg.channel.type === 'text' && !pause[msg.guild.id] ) this.verify(lang, msg, args, line, wiki);
+		if ( msg.channel.isGuild() && !pause[msg.guild.id] ) this.verify(lang, msg, args, line, wiki);
 		else msg.reactEmoji('❌');
 		return;
 	}
@@ -110,9 +110,9 @@ function cmd_verification(lang, msg, args, line, wiki) {
 				if ( channels.length > 10 ) return msg.replyMsg( lang.get('verification.channel_max'), {}, true );
 				channels = channels.map( channel => {
 					var new_channel = '';
-					if ( /^\d+$/.test(channel) ) new_channel = msg.guild.channels.cache.filter( tc => tc.type === 'text' ).get(channel);
-					if ( !new_channel ) new_channel = msg.guild.channels.cache.filter( gc => gc.type === 'text' ).find( gc => gc.name === channel.replace( /^#/, '' ) );
-					if ( !new_channel ) new_channel = msg.guild.channels.cache.filter( gc => gc.type === 'text' ).find( gc => gc.name.toLowerCase() === channel.toLowerCase().replace( /^#/, '' ) );
+					if ( /^\d+$/.test(channel) ) new_channel = msg.guild.channels.cache.filter( tc => tc.isGuild() ).get(channel);
+					if ( !new_channel ) new_channel = msg.guild.channels.cache.filter( gc => gc.isGuild() ).find( gc => gc.name === channel.replace( /^#/, '' ) );
+					if ( !new_channel ) new_channel = msg.guild.channels.cache.filter( gc => gc.isGuild() ).find( gc => gc.name.toLowerCase() === channel.toLowerCase().replace( /^#/, '' ) );
 					return new_channel;
 				} );
 				if ( channels.some( channel => !channel ) ) return msg.replyMsg( lang.get('verification.channel_missing'), {}, true );

+ 1 - 1
cmds/verify.js

@@ -15,7 +15,7 @@ var db = require('../util/database.js');
  * @param {String} [old_username] - The username before the search.
  */
 function cmd_verify(lang, msg, args, line, wiki, old_username = '') {
-	if ( msg.channel.type !== 'text' ) return this.LINK(lang, msg, line, wiki);
+	if ( !msg.channel.isGuild() ) return this.LINK(lang, msg, line, wiki);
 	if ( !msg.guild.me.permissions.has('MANAGE_ROLES') ) {
 		if ( msg.isAdmin() || msg.isOwner() ) {
 			console.log( msg.guild.id + ': Missing permissions - MANAGE_ROLES' );

+ 1 - 1
cmds/voice.js

@@ -56,7 +56,7 @@ function cmd_voice(lang, msg, args, line, wiki) {
 			} );
 		}
 	}
-	if ( msg.channel.type !== 'text' || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
+	if ( !msg.channel.isGuild() || !pause[msg.guild.id] ) this.LINK(lang, msg, line, wiki);
 }
 
 module.exports = {

+ 2 - 2
cmds/wiki/fandom.js

@@ -226,7 +226,7 @@ function fandom_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '',
 						}
 						
 						var text = '';
-						var prefix = ( msg.channel.type === 'text' && patreons[msg.guild.id] || process.env.prefix );
+						var prefix = ( msg.channel.isGuild() && 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 = '';
@@ -405,7 +405,7 @@ function fandom_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '',
 			}
 		}
 		else if ( body.query.interwiki ) {
-			if ( msg.channel.type === 'text' && pause[msg.guild.id] ) {
+			if ( msg.channel.isGuild() && pause[msg.guild.id] ) {
 				if ( reaction ) reaction.removeEmoji();
 				console.log( '- Aborted, paused.' );
 				return;

+ 3 - 3
cmds/wiki/fandom/user.js

@@ -149,7 +149,7 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 							}
 						}
 						
-						if ( msg.channel.type === 'text' && msg.guild.id in patreons ) {
+						if ( msg.channel.isGuild() && msg.guild.id in patreons ) {
 							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') + '**';
 							
@@ -299,7 +299,7 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 							if ( discordfield && discordfield.value ) {
 								discordfield.value = htmlToPlain( discordfield.value ).replace( /^\s*([^@#:]{2,32}?)\s*#(\d{4,6})\s*$/, '$1#$2' );
 								if ( discordfield.value.length > 50 ) discordfield.value = discordfield.value.substring(0, 50) + '\u2026';
-								if ( msg.channel.type === 'text' ) var discordmember = msg.guild.members.cache.find( member => {
+								if ( msg.channel.isGuild() ) var discordmember = msg.guild.members.cache.find( member => {
 									return member.user.tag.escapeFormatting() === discordfield.value;
 								} );
 								var discordname = [lang.get('user.info.discord'),discordfield.value];
@@ -335,7 +335,7 @@ function fandom_user(lang, msg, namespace, username, wiki, querystring, fragment
 							}
 						}
 						
-						if ( msg.channel.type === 'text' && msg.guild.id in patreons ) {
+						if ( msg.channel.isGuild() && msg.guild.id in patreons ) {
 							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') + '**';
 							

+ 2 - 2
cmds/wiki/gamepedia.js

@@ -208,7 +208,7 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 							}
 							else embed.setThumbnail( new URL(body.query.general.logo, wiki).href );
 							
-							var prefix = ( msg.channel.type === 'text' && patreons[msg.guild.id] || process.env.prefix );
+							var prefix = ( msg.channel.isGuild() && 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 = '';
@@ -324,7 +324,7 @@ function gamepedia_check_wiki(lang, msg, title, wiki, cmd, reaction, spoiler = '
 			}
 		}
 		else if ( body.query.interwiki ) {
-			if ( msg.channel.type === 'text' && pause[msg.guild.id] ) {
+			if ( msg.channel.isGuild() && pause[msg.guild.id] ) {
 				if ( reaction ) reaction.removeEmoji();
 				console.log( '- Aborted, paused.' );
 				return;

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

@@ -158,7 +158,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 							} );
 						}
 						
-						if ( msg.channel.type === 'text' && msg.guild.id in patreons && ( wiki.isFandom() || wiki.isGamepedia() ) ) {
+						if ( msg.channel.isGuild() && 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') + '**';
 
@@ -305,7 +305,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 						else {
 							if ( pbody.profile['link-discord'] ) {
 								if ( pbody.profile['link-discord'].length > 50 ) pbody.profile['link-discord'] = pbody.profile['link-discord'].substring(0, 50) + '\u2026';
-								if ( msg.channel.type === 'text' ) var discordmember = msg.guild.members.cache.find( member => {
+								if ( msg.channel.isGuild() ) var discordmember = msg.guild.members.cache.find( member => {
 									return member.user.tag === pbody.profile['link-discord'].replace( /^\s*([^@#:]{2,32}?)\s*#(\d{4,6})\s*$/, '$1#$2' );
 								} );
 								var discordname = [lang.get('user.info.discord'),pbody.profile['link-discord'].escapeFormatting()];
@@ -340,7 +340,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 							}
 						}
 						
-						if ( msg.channel.type === 'text' && msg.guild.id in patreons ) {
+						if ( msg.channel.isGuild() && msg.guild.id in patreons ) {
 							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') + '**';
 							
@@ -369,7 +369,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 							if ( pbody.userData.discordHandle ) {
 								let discord = pbody.userData.discordHandle.replace( /^\s*([^@#:]{2,32}?)\s*#(\d{4,6})\s*$/, '$1#$2' );
 								if ( discord.length > 50 ) discord = discord.substring(0, 50) + '\u2026';
-								if ( msg.channel.type === 'text' ) var discordmember = msg.guild.members.cache.find( member => {
+								if ( msg.channel.isGuild() ) var discordmember = msg.guild.members.cache.find( member => {
 									return member.user.tag.escapeFormatting() === discord;
 								} );
 								let discordname = [lang.get('user.info.discord'),discord];
@@ -405,7 +405,7 @@ function gamepedia_user(lang, msg, namespace, username, wiki, querystring, fragm
 							}
 						}
 						
-						if ( msg.channel.type === 'text' && msg.guild.id in patreons ) {
+						if ( msg.channel.isGuild() && msg.guild.id in patreons ) {
 							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') + '**';
 							

+ 185 - 0
dashboard/guilds.js

@@ -0,0 +1,185 @@
+const cheerio = require('cheerio');
+const {defaultPermissions} = require('../util/default.json');
+const {settingsData, createNotice} = require('./util.js');
+
+const forms = {
+	settings: require('./settings.js').get,
+	verification: require('./verification.js').get,
+	rcscript: require('./rcscript.js').get
+};
+
+const DiscordOauth2 = require('discord-oauth2');
+const oauth = new DiscordOauth2( {
+	clientId: process.env.bot,
+	clientSecret: process.env.secret,
+	redirectUri: process.env.dashboard
+} );
+
+const file = require('fs').readFileSync('./dashboard/index.html');
+
+/**
+ * Let a user view settings
+ * @param {import('http').ServerResponse} res - The server response
+ * @param {String} state - The user state
+ * @param {URL} reqURL - The used url
+ */
+function dashboard_guilds(res, state, reqURL) {
+	var args = reqURL.pathname.split('/');
+	var settings = settingsData.get(state);
+	var $ = cheerio.load(file);
+	if ( reqURL.searchParams.get('refresh') === 'success' ) {
+		createNotice($, {
+			type: 'success',
+			title: 'Refresh successful!',
+			text: 'Your server list has been successfully refeshed.'
+		}).prependTo('#text');
+	}
+	if ( reqURL.searchParams.get('refresh') === 'failed' ) {
+		createNotice($, {
+			type: 'error',
+			title: 'Refresh failed!',
+			text: 'You server list could not be refreshed, please try again.'
+		}).prependTo('#text');
+	}
+	if ( reqURL.searchParams.get('save') === 'success' ) {
+		createNotice($, {
+			type: 'success',
+			title: 'Settings saved!',
+			text: 'The settings have been updated successfully.'
+		}).prependTo('#text');
+	}
+	if ( reqURL.searchParams.get('save') === 'failed' ) {
+		createNotice($, {
+			type: 'error',
+			title: 'Save failed!',
+			text: 'The settings could not be saved, please try again.'
+		}).prependTo('#text');
+	}
+	if ( process.env.READONLY ) {
+		createNotice($, {
+			type: 'info',
+			title: 'Read-only database!',
+			text: 'You can currently only view your settings but not change them.'
+		}).prependTo('#text');
+	}
+	$('#logout img').attr('src', settings.user.avatar);
+	$('#logout span').text(`${settings.user.username} #${settings.user.discriminator}`);
+	$('.guild#invite a').attr('href', oauth.generateAuthUrl( {
+		scope: ['identify', 'guilds', 'bot'],
+		permissions: defaultPermissions, state
+	} ));
+	$('.guild#refresh a').attr('href', '/refresh?return=' + reqURL.pathname);
+	if ( settings.guilds.isMember.size ) {
+		$('<div class="guild">').append(
+			$('<div class="separator">')
+		).insertBefore('.guild#last-separator');
+		settings.guilds.isMember.forEach( guild => {
+			$('<div class="guild">').attr('id', guild.id).append(
+				$('<div class="bar">'),
+				$('<a>').attr('href', `/guild/${guild.id}`).attr('alt', guild.name).append(
+					( guild.icon ? 
+						$('<img class="avatar">').attr('src', `${guild.icon}?size=64`).attr('alt', guild.name)
+					 : $('<div class="avatar noicon">').text(guild.acronym) )
+				)
+			).insertBefore('.guild#last-separator');
+		} );
+	}
+	if ( settings.guilds.notMember.size ) {
+		$('<div class="guild">').append(
+			$('<div class="separator">')
+		).insertBefore('.guild#last-separator');
+		settings.guilds.notMember.forEach( guild => {
+			$('<div class="guild">').attr('id', guild.id).append(
+				$('<div class="bar">'),
+				$('<a>').attr('href', `/guild/${guild.id}`).attr('alt', guild.name).append(
+					( guild.icon ? 
+						$('<img class="avatar">').attr('src', `${guild.icon}?size=64`).attr('alt', guild.name)
+					 : $('<div class="avatar noicon">').text(guild.acronym) )
+				)
+			).insertBefore('.guild#last-separator');
+		} );
+	}
+
+	if ( args[1] === 'guild' ) {
+		let id = args[2];
+		$(`.guild#${id}`).addClass('selected');
+		if ( settings.guilds.isMember.has(id) ) {
+			let guild = settings.guilds.isMember.get(id);
+			$('head title').text(`${guild.name} – ` + $('head title').text());
+			$('.channel#settings').attr('href', `/guild/${guild.id}`);
+			$('.channel#verification').attr('href', `/guild/${guild.id}/verification`);
+			$('.channel#rcgcdb').attr('href', `/guild/${guild.id}/rcscript`);
+			if ( args[3] === 'rcscript' ) return forms.rcscript(res, $, guild, args);
+			if ( args[3] === 'verification' ) return forms.verification(res, $, guild, args);
+			return forms.settings(res, $, guild, args);
+		}
+		else if ( settings.guilds.notMember.has(id) ) {
+			let guild = settings.guilds.notMember.get(id);
+			$('head title').text(`${guild.name} – ` + $('head title').text());
+			res.setHeader('Set-Cookie', [`guild="${guild.id}"; HttpOnly; Path=/`]);
+			let url = oauth.generateAuthUrl( {
+				scope: ['identify', 'guilds', 'bot'],
+				permissions: defaultPermissions,
+				guildId: guild.id, state
+			} );
+			$('<a>').attr('href', url).text(guild.name).appendTo('#text .description');
+		}
+		else {
+			$('head title').text('Unknown Server – ' + $('head title').text());
+			$('#text .description').text('You are missing the <code>MANAGE_GUILD</code> permission.');
+		}
+	}
+	else {
+		$('head title').text('Server Selector – ' + $('head title').text());
+		$('#channellist').empty();
+		$('#text .description').text('This is a list of all servers you can change settings on. Please select a server:');
+		if ( settings.guilds.isMember.size ) {
+			$('<h2 id="with-wikibot">').text('Server with Wiki-Bot').appendTo('#text');
+			$('<a class="channel">').attr('href', '#with-wikibot').append(
+				$('<img>').attr('src', '/src/channel.svg'),
+				$('<div>').text('Server with Wiki-Bot')
+			).appendTo('#channellist');
+			$('<div class="server-selector" id="isMember">').appendTo('#text');
+			settings.guilds.isMember.forEach( guild => {
+				$('<a class="server">').attr('href', `/guild/${guild.id}`).append(
+					( guild.icon ? 
+						$('<img class="avatar">').attr('src', `${guild.icon}?size=256`).attr('alt', guild.name)
+					 : $('<div class="avatar noicon">').text(guild.acronym) ),
+					$('<div class="server-name description">').text(guild.name)
+				).appendTo('.server-selector#isMember');
+			} );
+		}
+		if ( settings.guilds.notMember.size ) {
+			$('<h2 id="without-wikibot">').text('Server without Wiki-Bot').appendTo('#text');
+			$('<a class="channel">').attr('href', '#without-wikibot').append(
+				$('<img>').attr('src', '/src/channel.svg'),
+				$('<div>').text('Server without Wiki-Bot')
+			).appendTo('#channellist');
+			$('<div class="server-selector" id="notMember">').appendTo('#text');
+			settings.guilds.notMember.forEach( guild => {
+				$('<a class="server">').attr('href', `/guild/${guild.id}`).append(
+					( guild.icon ? 
+						$('<img class="avatar">').attr('src', `${guild.icon}?size=256`).attr('alt', guild.name)
+					 : $('<div class="avatar noicon">').text(guild.acronym) ),
+					$('<div class="server-name description">').text(guild.name)
+				).appendTo('.server-selector#notMember');
+			} );
+		}
+		if ( !settings.guilds.count ) {
+			$('#text .description').text('You currently don\'t have the MANAGE_SERVER permission on any servers, are you logged into the correct account?');
+			$('<a class="channel">').attr('href', oauth.generateAuthUrl( {
+				scope: ['identify', 'guilds'],
+				prompt: 'consent', state
+			} )).append(
+				$('<img>').attr('src', '/src/channel.svg'),
+				$('<div>').text('Switch accounts')
+			).appendTo('#channellist');
+		}
+	}
+	let body = $.html();
+	res.writeHead(200, {'Content-Length': body.length});
+	res.write( body );
+	return res.end();
+}
+
+module.exports = dashboard_guilds;

+ 38 - 190
dashboard/index.html

@@ -5,199 +5,48 @@
 	<title>Wiki-Bot Settings</title>
 	<link rel="shortcut icon" href="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=64">
 	<meta name="description" content="Wiki-Bot is a bot with the purpose to easily search for and link to wiki pages. Wiki-Bot shows short descriptions and additional info about pages and is able to resolve redirects and follow interwiki links.">
+	<meta property="og:description" content="Wiki-Bot is a bot with the purpose to easily search for and link to wiki pages. Wiki-Bot shows short descriptions and additional info about pages and is able to resolve redirects and follow interwiki links.">
 	<meta property="og:image" content="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=1024">
 	<meta property="og:title" content="Wiki-Bot Settings">
 	<meta property="og:site_name" content="Wiki-Bot Settings">
 	<meta itemprop="author" content="MarkusRost">
-	<style>
-		html {
-			height: calc(100% - 48px);
-		}
-		body {
-			font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
-			text-rendering: optimizeLegibility;
-			background: #36393f;
-			color: #dcddde;
-			position: relative;
-			min-height: 100%;
-			margin: 0;
-		}
-		a {
-			text-decoration: none;
-			color: inherit;
-		}
-		.text a {
-			color: #00b0f4;
-		}
-		.text a:hover {
-			text-decoration: underline;
-		}
-		.text {
-			position: relative;
-			padding: 8px;
-			width: calc(100% - 88px);
-			top: 48px;
-			left: 72px;
-		}
-		.notice {
-			padding: 5px 10px;
-			line-height: 1.6;
-			text-align: center;
-			margin: 0 auto 1em;
-			width: fit-content;
-			background: #200;
-			border: 2px solid #500;
-		}
-		.navbar {
-			background: #2f3136;
-			position: fixed;
-			top: 0;
-			left: 72px;
-			right: 0;
-			display: flex;
-			align-items: center;
-			justify-content: space-between;
-			height: 48px;
-			padding: 0 0 0 8px;
-			font-size: 16px;
-			line-height: 20px;
-			font-weight: bold;
-		}
-		.navbar a {
-			display: flex;
-			align-items: center;
-			height: 100%;
-			padding-left: 10px;
-		}
-		.navbar a:hover {
-			background: #202225;
-		}
-		.navbar .avatar {
-			width: 32px;
-			height: 32px;
-		}
-		.navbar span {
-			padding: 0 10px;
-		}
-		.sidebar {
-			background: #202225;
-			position: absolute;
-			top: 0;
-			left: 0;
-			min-height: calc(100% + 48px);
-			width: 72px;
-			display: flex;
-		}
-		.guildlist {
-			padding: 12px 0;
-			position: relative;
-			flex: 1 1 auto;
-		}
-		.guild {
-			margin: 0 0 8px;
-			position: relative;
-			display: flex;
-			justify-content: center;
-		}
-		.avatar {
-			border-radius: 50%;
-			width: 48px;
-			height: 48px;
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			font-weight: 500;
-			line-height: 1.2em;
-			white-space: nowrap;
-			overflow: hidden;
-			color: #dcddde;
-			font-weight: bold;
-		}
-		.navbar a:hover .avatar,
-		.guild.selected .avatar,
-		.guild:hover .avatar {
-			border-radius: 30%;
-			color: #ffffff;
-		}
-		.noicon {
-			width: 48px;
-			height: 48px;
-			background: #36393f;
-		}
-		.guild.selected .noicon,
-		.guild:hover .noicon {
-			background-color: #7289da;
-		}
-		.svg-avatar {
-			color: #43b581;
-			background: #36393f;
-		}
-		.guild:hover .svg-avatar {
-			color: #ffffff;
-			background-color: #43b581;
-		}
-		.separator {
-			height: 2px;
-			width: 32px;
-			border-radius: 1px;
-			background-color: rgba(255,255,255,0.06);;
-		}
-		.bar {
-			position: absolute;
-			left: 0;
-			top: 0;
-			display: block;
-			width: 8px;
-			border-radius: 0 4px 4px 0;
-			margin-left: -4px;
-			background-color: #ffffff;
-		}
-		.guild:hover .bar {
-			margin-top: 14px;
-			height: 20px;
-		}
-		.guild.selected .bar {
-			margin-top: 4px;
-			height: 40px;
-		}
-		a[alt]:hover:after {
-			content: attr(alt);
-			position: absolute;
-			background: #000000;
-			color: #dcddde;
-			font-weight: bold;
-			font-size: 90%;
-			white-space: nowrap;
-			border-radius: 4px;
-			padding: 8px;
-		}
-		.guild a[alt]:hover:after {
-			top: 20%;
-			left: 72px;
-		}
-		.navbar a[alt]:hover:after {
-			top: 48px;
-		}
-	</style>
+	<link rel="stylesheet" type="text/css" href="/src/index.css">
+	<!--<script src="/src/index.js"></script>-->
 </head>
-<body class="settings">
-	<div class="text">
-		<replace id="notice">Some text here</replace>
-		<replace id="text">Some text here</replace>
+<body>
+	<div id="text">
+		<div class="description"></div>
 	</div>
-	<div class="navbar">
-		<div style="width: 150px;"></div>
-		<a id="support" href="https://discord.gg/v77RTk5" target="_blank" alt="Support server">
-			<img class="avatar" src="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=64" alt="Support server" width="32" height="32">
-			<span>Support server</span>
-		</a>
-		<a id="logout" href="/logout" alt="Logout">
-			<img class="avatar" src="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=64" alt="Logout" width="32" height="32">
-			<span>Wiki-Bot #2998</span>
-		</a>
-	</div>
-	<div class="sidebar">
-		<div class="guildlist">
+	<div class="scrollbar" id="sidebar">
+		<div class="scrollbar" id="channellist">
+			<a class="channel channel-header" id="settings">
+				<img src="/src/settings.svg" alt="Settings">
+				<div>Settings</div>
+			</a>
+			<a class="channel channel-header" id="verification">
+				<img src="/src/settings.svg" alt="Settings">
+				<div>Verifications</div>
+			</a>
+			<a class="channel channel-header" id="rcgcdb">
+				<img src="/src/settings.svg" alt="Settings">
+				<div>Recent Changes</div>
+			</a>
+		</div>
+		<div id="navbar">
+			<a id="selector" href="/" style="width: 230px;">
+				<img class="avatar" src="https://discord.com/assets/f8389ca1a741a115313bede9ac02e2c0.svg" alt="Discord">
+				<span>Server Selector</span>
+			</a>
+			<a id="support" href="https://discord.gg/v77RTk5" target="_blank" alt="Help with Wiki-Bot">
+				<img class="avatar" src="https://cdn.discordapp.com/icons/464084451165732868/c6a8b9fc902b09545de8194a911e6045.png?size=64" alt="Wiki-Bot">
+				<span>Support Server</span>
+			</a>
+			<a id="logout" href="/logout" alt="Logout">
+				<img class="avatar" src="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=64" alt="Logout">
+				<span>Wiki-Bot #2998</span>
+			</a>
+		</div>
+		<div id="guildlist">
 			<div class="guild" id="invite">
 				<div class="bar"></div>
 				<a href="https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939912256&scope=bot" alt="Invite Wiki-Bot">
@@ -208,13 +57,12 @@
 					</div>
 				</a>
 			</div>
-			<replace id="guilds">List of guilds here</replace>
-			<div class="guild">
+			<div class="guild" id="last-separator">
 				<div class="separator"></div>
 			</div>
 			<div class="guild" id="refresh">
 				<div class="bar"></div>
-				<a href="/refresh" alt="Refresh guild list">
+				<a href="/refresh" alt="Refresh server list">
 					<div class="avatar svg-avatar">
 						<svg width="20" height="23" viewBox="0 1.35 17.3 20" style="overflow: visible;">
 							<path fill="currentColor" d="M8.6,2.7V0.4c0-0.4-0.4-0.5-0.7-0.3L4.2,3.9C4,4,4,4.3,4.2,4.4l3.7,3.7c0.3,0.3,0.7,0.1,0.7-0.3V5.6c3.4,0,6.1,2.9,5.7,6.4  c-0.3,2.6-2.4,4.8-5.1,5.1c-3.5,0.4-6.4-2.3-6.4-5.7c0-1.1,0.3-2.1,0.8-2.9c0.1-0.2,0.1-0.4-0.1-0.5L2.1,6.5  C1.9,6.3,1.6,6.3,1.5,6.5C0.5,8-0.1,9.8,0,11.8c0.2,4.4,3.8,8.1,8.3,8.2c4.9,0.2,9-3.7,9-8.6C17.3,6.6,13.4,2.7,8.6,2.7z"></path>

+ 87 - 372
dashboard/index.js

@@ -1,177 +1,118 @@
 const http = require('http');
-const crypto = require('crypto');
-const cheerio = require('cheerio');
-const {defaultPermissions} = require('../util/default.json');
+const {parse} = require('querystring');
+const pages = require('./oauth.js');
+const dashboard = require('./guilds.js');
+const {db, settingsData} = require('./util.js');
 
-const sqlite3 = require('sqlite3').verbose();
-const mode = ( process.env.READONLY ? sqlite3.OPEN_READONLY : sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE );
-const db = new sqlite3.Database( './wikibot.db', mode, dberror => {
-	if ( dberror ) {
-		console.log( '- Dashboard: Error while connecting to the database: ' + dberror );
-		return dberror;
-	}
-	console.log( '- Dashboard: Connected to the database.' );
-} );
-
-const DiscordOauth2 = require('discord-oauth2');
-const oauth = new DiscordOauth2( {
-	clientId: process.env.bot,
-	clientSecret: process.env.secret,
-	redirectUri: process.env.dashboard
-} );
+const posts = {
+	settings: require('./settings.js').post,
+	verification: require('./verification.js').post,
+	rcscript: require('./rcscript.js').post
+};
 
 const fs = require('fs');
-const files = {
-	index: fs.readFileSync('./dashboard/index.html'),
-	login: fs.readFileSync('./dashboard/login.html')
-}
-
-/**
- * @type {Map<Number, PromiseConstructor>}
- */
-var messages = new Map();
-var messageId = 1;
-
-process.on( 'message', message => {
-	if ( message.id ) {
-		if ( message.data.error ) messages.get(message.id).reject(message.data.error);
-		else messages.get(message.id).resolve(message.data.response);
-		return messages.delete(message.id);
+const path = require('path');
+const files = new Map(fs.readdirSync( './dashboard/src' ).map( file => {
+	let contentType = 'text/html';
+	switch ( path.extname(file) ) {
+		case '.css':
+			contentType = 'text/css';
+			break;
+		case '.js':
+			contentType = 'text/javascript';
+			break;
+		case '.json':
+			contentType = 'application/json';
+			break;
+		case '.svg':
+			contentType = 'image/svg+xml';
+			break;
+		case '.png':
+			contentType = 'image/png';
+			break;
+		case '.jpg':
+			contentType = 'image/jpg';
+			break;
 	}
-	console.log( '- [Dashboard]: Message received!', message );
-} );
-
-/**
- * Send messages to the manager.
- * @param {Object} [message] - The message.
- * @returns {Promise<Object>}
- */
-function sendMsg(message) {
-	var id = messageId++;
-	var promise = new Promise( (resolve, reject) => {
-		messages.set(id, {resolve, reject});
-		process.send( {id, data: message} );
-	} );
-	return promise;
-}
-
-/**
- * @typedef Settings
- * @property {String} state
- * @property {String} access_token
- * @property {User} user
- * @property {Object} guilds
- * @property {Map<String, Guild>} guilds.isMember
- * @property {Map<String, Guild>} guilds.notMember
- */
-
-/**
- * @typedef User
- * @property {String} id
- * @property {String} username
- * @property {String} discriminator
- * @property {String} avatar
- * @property {String} locale
- */
-
-/**
- * @typedef Guild
- * @property {String} id
- * @property {String} name
- * @property {String} acronym
- * @property {String} [icon]
- * @property {String} permissions
- */
-
-/**
- * @type {Map<String, Settings>}
- */
-var settingsData = new Map();
+	return [`/src/${file}`, {
+		name: file, contentType,
+		path: `./dashboard/src/${file}`
+	}];
+} ));
 
 const server = http.createServer((req, res) => {
+	if ( req.method === 'POST' && req.url.startsWith( '/guild/' ) ) {
+		let args = req.url.split('/');
+		let state = req.headers.cookie?.split('; ')?.filter( cookie => {
+			return cookie.split('=')[0] === 'wikibot';
+		} )?.map( cookie => cookie.replace( /^wikibot="(\w*(?:-\d+)?)"$/, '$1' ) )?.join();
+
+		if ( args.length <= 4 && ['settings', 'verification', 'rcscript'].incluses( args[3] ) 
+		&& settingsData.has(state) && settingsData.get(state).guilds.isMember.has(args[2]) ) {
+			let body = '';
+			req.on( 'data', chunk => {
+				body += chunk.toString();
+			} );
+			req.on( 'error', () => {
+				console.log( error );
+				res.end('error');
+			} );
+			return req.on( 'end', () => {
+				console.log( parse(body) );
+				//return posts[args[3]](res, settingsData.get(state).user.id, args[2], parse(body));
+				res.writeHead(302, {Location: req.url});
+				res.end();
+			} );
+		}
+	}
+
 	if ( req.method !== 'GET' ) {
 		let body = '<img width="400" src="https://http.cat/418"><br><strong>' + http.STATUS_CODES[418] + '</strong>';
-		res.writeHead(418, {'Content-Length': body.length});
+		res.writeHead(418, {
+			'Content-Type': 'text/html',
+			'Content-Length': body.length
+		});
 		res.write( body );
 		return res.end();
 	}
 
-	if ( req.url === '/favicon.ico' ) {
+	var reqURL = new URL(req.url, process.env.dashboard);
+
+	if ( reqURL.pathname === '/favicon.ico' ) {
 		res.writeHead(302, {Location: 'https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=64'});
 		return res.end();
 	}
 
+	if ( files.has(reqURL.pathname) ) {
+		let file = files.get(reqURL.pathname);
+		res.writeHead(200, {'Content-Type': file.contentType});
+		return fs.createReadStream(file.path).pipe(res);
+	}
+
 	res.setHeader('Content-Type', 'text/html');
 	res.setHeader('Content-Language', ['en']);
 
 	var lastGuild = req.headers?.cookie?.split('; ')?.filter( cookie => {
 		return cookie.split('=')[0] === 'guild';
-	} )?.map( cookie => cookie.replace( /^guild="(\w+)"$/, '$1' ) )?.join();
-	if ( lastGuild ) res.setHeader('Set-Cookie', [`guild="${lastGuild}"; Max-Age=0; HttpOnly; Path=/`]);
+	} )?.map( cookie => cookie.replace( /^guild="(\w*)"$/, '$1' ) )?.join();
+	if ( lastGuild ) res.setHeader('Set-Cookie', ['guild=""; HttpOnly; Path=/; Max-Age=0']);
 
 	var state = req.headers.cookie?.split('; ')?.filter( cookie => {
 		return cookie.split('=')[0] === 'wikibot';
-	} )?.map( cookie => cookie.replace( /^wikibot="(\w+(?:-\d+)?)"$/, '$1' ) )?.join();
-
-	var reqURL = new URL(req.url, process.env.dashboard);
+	} )?.map( cookie => cookie.replace( /^wikibot="(\w*(?:-\d+)?)"$/, '$1' ) )?.join();
 
 	if ( reqURL.pathname === '/login' ) {
-		if ( settingsData.has(state) ) {
-			res.writeHead(302, {Location: '/'});
-			return res.end();
-		}
-		if ( state ) res.setHeader('Set-Cookie', [`wikibot="${state}"; Max-Age=0; HttpOnly`]);
-		var $ = cheerio.load(files.login);
-		$('.guild#invite a').attr('href', oauth.generateAuthUrl( {
-			scope: ['identify', 'guilds', 'bot'],
-			permissions: defaultPermissions, state
-		} ));
-		let responseCode = 200;
-		let notice = '';
-		if ( reqURL.searchParams.get('action') === 'failed' ) {
-			responseCode = 400;
-			notice = createNotice($, {
-				title: 'Login failed!',
-				text: 'An error occurred while logging you in, please try again.'
-			});
-		}
-		if ( reqURL.searchParams.get('action') === 'unauthorized' ) {
-			responseCode = 401;
-			notice = createNotice($, {
-				title: 'Not logged in!',
-				text: 'Please login before you can change any settings.'
-			});
-		}
-		if ( reqURL.searchParams.get('action') === 'logout' ) {
-			notice = createNotice($, {
-				title: 'Successfully logged out!',
-				text: 'You have been successfully logged out. To change any settings you need to login again.'
-			});
-		}
-		$('replace#notice').replaceWith(notice);
-		state = crypto.randomBytes(16).toString("hex");
-		while ( settingsData.has(state) ) {
-			state = crypto.randomBytes(16).toString("hex");
-		}
-		let url = oauth.generateAuthUrl( {
-			scope: ['identify', 'guilds'],
-			prompt: 'none', state
-		} );
-		$('replace#text').replaceWith(`<a href="${url}">Login</a>`);
-		let body = $.html();
-		res.writeHead(responseCode, {
-			'Set-Cookie': [`wikibot="${state}"; HttpOnly`],
-			'Content-Length': body.length
-		});
-		res.write( body );
-		return res.end();
+		return pages.login(res, state, reqURL.searchParams.get('action'));
 	}
 
 	if ( reqURL.pathname === '/logout' ) {
 		settingsData.delete(state);
 		res.writeHead(302, {
 			Location: '/login?action=logout',
-			'Set-Cookie': [`wikibot="${state}"; Max-Age=0; HttpOnly`]
+			'Set-Cookie': [
+				...( res.getHeader('Set-Cookie') || [] ),
+				'wikibot=""; HttpOnly; Path=/; Max-Age=0'
+			]
 		});
 		return res.end();
 	}
@@ -184,83 +125,7 @@ const server = http.createServer((req, res) => {
 	}
 
 	if ( reqURL.pathname === '/oauth' ) {
-		if ( settingsData.has(state) ) {
-			res.writeHead(302, {Location: '/'});
-			return res.end();
-		}
-		if ( state !== reqURL.searchParams.get('state') || !reqURL.searchParams.get('code') ) {
-			res.writeHead(302, {Location: '/login?action=unauthorized'});
-			return res.end();
-		}
-		return oauth.tokenRequest( {
-			scope: ['identify', 'guilds'],
-			code: reqURL.searchParams.get('code'),
-			grantType: 'authorization_code'
-		} ).then( ({access_token}) => {
-			return Promise.all([
-				oauth.getUser(access_token),
-				oauth.getUserGuilds(access_token)
-			]).then( ([user, guilds]) => {
-				guilds = guilds.filter( guild => {
-					return ( guild.owner || hasPerm(guild.permissions, 'MANAGE_GUILD') );
-				} ).map( guild => {
-					return {
-						id: guild.id,
-						name: guild.name,
-						acronym: guild.name.replace( /'s /g, ' ' ).replace( /\w+/g, e => e[0] ).replace( /\s/g, '' ),
-						icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
-						+ ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) + '?size=128' : null ),
-						permissions: guild.permissions
-					};
-				} );
-				sendMsg( {
-					type: 'isMemberAll',
-					guilds: guilds.map( guild => guild.id )
-				} ).then( response => {
-					let isMember = new Map();
-					let notMember = new Map();
-					response.forEach( (guild, i) => {
-						if ( guild ) isMember.set(guilds[i].id, guilds[i]);
-						else notMember.set(guilds[i].id, guilds[i]);
-					} );
-					settingsData.set(`${state}-${user.id}`, {
-						state: `${state}-${user.id}`,
-						access_token,
-						user: {
-							id: user.id,
-							username: user.username,
-							discriminator: user.discriminator,
-							avatar: 'https://cdn.discordapp.com/' + ( user.avatar ? 
-								`avatars/${user.id}/${user.avatar}.` + 
-								( user.avatar.startsWith( 'a_' ) ? 'gif' : 'png' ) : 
-								`embed/avatars/${user.discriminator % 5}.png` ) + '?size=64',
-							locale: user.locale
-						},
-						guilds: {isMember, notMember}
-					});
-					res.writeHead(302, {
-						Location: ( lastGuild ? '/guild/' + lastGuild : '/' ),
-						'Set-Cookie': [
-							`wikibot="${state}"; Max-Age=0; HttpOnly`,
-							`wikibot="${state}-${user.id}"; HttpOnly`
-						]
-					});
-					return res.end();
-				}, error => {
-					console.log( '- Dashboard: Error while checking the guilds:', error );
-					res.writeHead(302, {Location: '/login?action=failed'});
-					return res.end();
-				} );
-			}, error => {
-				console.log( '- Dashboard: Error while getting user and guilds: ' + error );
-				res.writeHead(302, {Location: '/login?action=failed'});
-				return res.end();
-			} );
-		}, error => {
-			console.log( '- Dashboard: Error while getting the token: ' + error );
-			res.writeHead(302, {Location: '/login?action=failed'});
-			return res.end();
-		} );
+		return pages.oauth(res, state, reqURL.searchParams, lastGuild);
 	}
 
 	if ( !settingsData.has(state) ) {
@@ -269,126 +134,16 @@ const server = http.createServer((req, res) => {
 		});
 		return res.end();
 	}
-	var settings = settingsData.get(state);
 
 	if ( reqURL.pathname === '/refresh' ) {
-		return oauth.getUserGuilds(settings.access_token).then( guilds => {
-			guilds = guilds.filter( guild => {
-				return ( guild.owner || hasPerm(guild.permissions, 'MANAGE_GUILD') );
-			} ).map( guild => {
-				return {
-					id: guild.id,
-					name: guild.name,
-					acronym: guild.name.replace( /'s /g, ' ' ).replace( /\w+/g, e => e[0] ).replace( /\s/g, '' ),
-					icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
-					+ ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) + '?size=128' : null ),
-					permissions: guild.permissions
-				};
-			} );
-			sendMsg( {
-				type: 'isMemberAll',
-				guilds: guilds.map( guild => guild.id )
-			} ).then( response => {
-				let isMember = new Map();
-				let notMember = new Map();
-				response.forEach( (guild, i) => {
-					if ( guild ) isMember.set(guilds[i].id, guilds[i]);
-					else notMember.set(guilds[i].id, guilds[i]);
-				} );
-				settings.guilds = {isMember, notMember};
-				res.writeHead(302, {
-					Location: ( reqURL.searchParams.get('return') || '/' )
-				});
-				return res.end();
-			}, error => {
-				console.log( '- Dashboard: Error while checking refreshed guilds:', error );
-				res.writeHead(302, {Location: '/login?action=failed'});
-				return res.end();
-			} );
-		}, error => {
-			console.log( '- Dashboard: Error while refreshing guilds: ' + error );
-			res.writeHead(302, {Location: '/login?action=failed'});
-			return res.end();
-		} );
+		return pages.refresh(res, state, reqURL.searchParams.get('return'));
 	}
 
-	var $ = cheerio.load(files.index);
-	let notice = '';
-	if ( process.env.READONLY ) {
-		notice = createNotice($, {
-			title: 'Read-only database!',
-			text: 'You can currently only view your settings but not change them.'
-		});
-	}
-	$('replace#notice').replaceWith(notice);
-	$('.navbar #logout img').attr('src', settings.user.avatar);
-	$('.navbar #logout span').text(`${settings.user.username} #${settings.user.discriminator}`);
-	$('.guild#invite a').attr('href', oauth.generateAuthUrl( {
-		scope: ['identify', 'guilds', 'bot'],
-		permissions: defaultPermissions, state
-	} ));
-	$('.guild#refresh a').attr('href', '/refresh?return=' + reqURL.pathname);
-	let guilds = $('<div>');
-	if ( settings.guilds.isMember.size ) {
-		$('<div class="guild">').append(
-			$('<div class="separator">')
-		).appendTo(guilds);
-		settings.guilds.isMember.forEach( guild => {
-			$('<div class="guild">').attr('id', guild.id).append(
-				$('<div class="bar">'),
-				$('<a>').attr('href', `/guild/${guild.id}`).attr('alt', guild.name).append(
-					( guild.icon ? 
-						$('<img class="avatar" width="48" height="48">').attr('src', guild.icon).attr('alt', guild.name)
-					 : $('<div class="avatar noicon">').text(guild.acronym) )
-				)
-			).appendTo(guilds);
-		} );
+	if ( reqURL.pathname === '/' || reqURL.pathname.startsWith( '/guild/' ) ) {
+		return dashboard(res, state, reqURL);
 	}
-	if ( settings.guilds.notMember.size ) {
-		$('<div class="guild">').append(
-			$('<div class="separator">')
-		).appendTo(guilds);
-		settings.guilds.notMember.forEach( guild => {
-			$('<div class="guild">').attr('id', guild.id).append(
-				$('<div class="bar">'),
-				$('<a>').attr('href', `/guild/${guild.id}`).attr('alt', guild.name).append(
-					( guild.icon ? 
-						$('<img class="avatar" width="48" height="48">').attr('src', guild.icon).attr('alt', guild.name)
-					 : $('<div class="avatar noicon">').text(guild.acronym) )
-				)
-			).appendTo(guilds);
-		} );
-	}
-	$('replace#guilds').replaceWith(guilds.children());
 
-	if ( reqURL.pathname.startsWith( '/guild/' ) ) {
-		let id = reqURL.pathname.replace( '/guild/', '' );
-		if ( settings.guilds.isMember.has(id) ) {
-			$(`.guild#${id}`).addClass('selected');
-			let guild = settings.guilds.isMember.get(id);
-			$('head title').text(`${guild.name} – ` + $('head title').text());
-			res.setHeader('Set-Cookie', [`guild="${id}"; HttpOnly; Path=/`]);
-			$('replace#text').replaceWith(`${guild.permissions}`);
-		}
-		if ( settings.guilds.notMember.has(id) ) {
-			$(`.guild#${id}`).addClass('selected');
-			let guild = settings.guilds.notMember.get(id);
-			$('head title').text(`${guild.name} – ` + $('head title').text());
-			res.setHeader('Set-Cookie', [`guild="${id}"; HttpOnly; Path=/`]);
-			let url = oauth.generateAuthUrl( {
-				scope: ['identify', 'guilds', 'bot'],
-				permissions: defaultPermissions,
-				guild_id: id, state
-			} );
-			$('replace#text').replaceWith($('<a>').attr('href', url).text(guild.permissions));
-		}
-		$('replace#text').replaceWith('You are missing the <code>MANAGE_GUILD</code> permission.');
-	}
-
-	$('replace#text').replaceWith('Keks');
-	let body = $.html();
-	res.writeHead(200, {'Content-Length': body.length});
-	res.write( body );
+	res.writeHead(302, {Location: '/'});
 	return res.end();
 });
 
@@ -396,46 +151,6 @@ server.listen(8080, 'localhost', () => {
 	console.log( '- Dashboard: Server running at http://localhost:8080/' );
 });
 
-/**
- * Create a red notice
- * @param {CheerioStatic} $ - The cheerio static
- * @param {{title: String, text: String}[]} notices - The notices to create
- * @returns {Cheerio}
- */
-function createNotice($, ...notices) {
-	return notices.map( notice => {
-		return $('<div class="notice">').append(
-			$('<b>').text(notice.title),
-			$('<div>').text(notice.text)
-		);
-	} );
-}
-
-const permissions = {
-	ADMINISTRATOR: 1 << 3,
-	MANAGE_CHANNELS: 1 << 4,
-	MANAGE_GUILD: 1 << 5,
-	MANAGE_MESSAGES: 1 << 13,
-	MENTION_EVERYONE: 1 << 17,
-	MANAGE_NICKNAMES: 1 << 27,
-	MANAGE_ROLES: 1 << 28,
-	MANAGE_WEBHOOKS: 1 << 29,
-	MANAGE_EMOJIS: 1 << 30
-}
-
-/**
- * Check if a permission is included in the BitField
- * @param {String|Number} all - BitField of multiple permissions
- * @param {String} permission - Name of the permission to check for
- * @param {Boolean} [admin] - If administrator permission can overwrite
- * @returns {Boolean}
- */
-function hasPerm(all, permission, admin = true) {
-	var bit = permissions[permission];
-	var adminOverwrite = ( admin && (all & permissions.ADMINISTRATOR) === permissions.ADMINISTRATOR );
-	return ( adminOverwrite || (all & bit) === bit )
-}
-
 
 /**
  * End the process gracefully.

+ 38 - 183
dashboard/login.html

@@ -5,195 +5,50 @@
 	<title>Login – Wiki-Bot Settings</title>
 	<link rel="shortcut icon" href="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=64">
 	<meta name="description" content="Wiki-Bot is a bot with the purpose to easily search for and link to wiki pages. Wiki-Bot shows short descriptions and additional info about pages and is able to resolve redirects and follow interwiki links.">
+	<meta property="og:description" content="Wiki-Bot is a bot with the purpose to easily search for and link to wiki pages. Wiki-Bot shows short descriptions and additional info about pages and is able to resolve redirects and follow interwiki links.">
 	<meta property="og:image" content="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=1024">
 	<meta property="og:title" content="Login – Wiki-Bot Settings">
 	<meta property="og:site_name" content="Wiki-Bot Settings">
 	<meta itemprop="author" content="MarkusRost">
-	<style>
-		html {
-			height: calc(100% - 48px);
-		}
-		body {
-			font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
-			text-rendering: optimizeLegibility;
-			background: #36393f;
-			color: #dcddde;
-			position: relative;
-			min-height: 100%;
-			margin: 0;
-		}
-		a {
-			text-decoration: none;
-			color: inherit;
-		}
-		.text a {
-			color: #00b0f4;
-		}
-		.text a:hover {
-			text-decoration: underline;
-		}
-		.text {
-			position: relative;
-			padding: 8px;
-			width: calc(100% - 88px);
-			top: 48px;
-			left: 72px;
-		}
-		.notice {
-			padding: 5px 10px;
-			line-height: 1.6;
-			text-align: center;
-			margin: 0 auto 1em;
-			width: fit-content;
-			background: #200;
-			border: 2px solid #500;
-		}
-		.navbar {
-			background: #2f3136;
-			position: fixed;
-			top: 0;
-			left: 72px;
-			right: 0;
-			display: flex;
-			align-items: center;
-			justify-content: space-between;
-			height: 48px;
-			padding: 0 0 0 8px;
-			font-size: 16px;
-			line-height: 20px;
-			font-weight: bold;
-		}
-		.navbar a {
-			display: flex;
-			align-items: center;
-			height: 100%;
-			padding-left: 10px;
-		}
-		.navbar a:hover {
-			background: #202225;
-		}
-		.navbar .avatar {
-			width: 32px;
-			height: 32px;
-		}
-		.navbar span {
-			padding: 0 10px;
-		}
-		.sidebar {
-			background: #202225;
-			position: absolute;
-			top: 0;
-			left: 0;
-			min-height: calc(100% + 48px);
-			width: 72px;
-			display: flex;
-		}
-		.guildlist {
-			padding: 12px 0;
-			position: relative;
-			flex: 1 1 auto;
-		}
-		.guild {
-			margin: 0 0 8px;
-			position: relative;
-			display: flex;
-			justify-content: center;
-		}
-		.avatar {
-			border-radius: 50%;
-			width: 48px;
-			height: 48px;
-			display: flex;
-			align-items: center;
-			justify-content: center;
-			font-weight: 500;
-			line-height: 1.2em;
-			white-space: nowrap;
-			overflow: hidden;
-			color: #dcddde;
-			font-weight: bold;
-		}
-		.navbar a:hover .avatar,
-		.guild.selected .avatar,
-		.guild:hover .avatar {
-			border-radius: 30%;
-			color: #ffffff;
-		}
-		.noicon {
-			width: 48px;
-			height: 48px;
-			background: #36393f;
-		}
-		.guild.selected .noicon,
-		.guild:hover .noicon {
-			background-color: #7289da;
-		}
-		.svg-avatar {
-			color: #43b581;
-			background: #36393f;
-		}
-		.guild:hover .svg-avatar {
-			color: #ffffff;
-			background-color: #43b581;
-		}
-		.separator {
-			height: 2px;
-			width: 32px;
-			border-radius: 1px;
-			background-color: rgba(255,255,255,0.06);;
-		}
-		.bar {
-			position: absolute;
-			left: 0;
-			top: 0;
-			display: block;
-			width: 8px;
-			border-radius: 0 4px 4px 0;
-			margin-left: -4px;
-			background-color: #ffffff;
-		}
-		.guild:hover .bar {
-			margin-top: 14px;
-			height: 20px;
-		}
-		.guild.selected .bar {
-			margin-top: 4px;
-			height: 40px;
-		}
-		a[alt]:hover:after {
-			content: attr(alt);
-			position: absolute;
-			background: #000000;
-			color: #dcddde;
-			font-weight: bold;
-			font-size: 90%;
-			white-space: nowrap;
-			border-radius: 4px;
-			padding: 8px;
-		}
-		.guild a[alt]:hover:after {
-			top: 20%;
-			left: 72px;
-		}
-		.navbar a[alt]:hover:after {
-			top: 48px;
-		}
-	</style>
+	<link rel="stylesheet" type="text/css" href="src/index.css">
 </head>
-<body class="settings">
-	<div class="text">
-		<replace id="notice">Some text here</replace>
-		<replace id="text">Some text here</replace>
-	</div>
-	<div class="navbar">
-		<div style="width: 150px;"></div>
-		<a id="support" href="https://discord.gg/v77RTk5" target="_blank" alt="Support server">
-			<img class="avatar" src="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=64" alt="Support server" width="32" height="32">
-			<span>Support server</span>
-		</a>
+<body>
+	<div id="text">
+		<div class="description">
+			<h2>Welcome on Wiki-Bot Dashboard.</h2>
+			<p>Wiki-Bot is a Discord bot made to bring Discord servers and MediaWiki wikis together. It helps with linking wiki pages, verifying wiki users, informing about latest changes on the wiki and more.</p>
+			<p>Here you can change different bot settings for servers you have Manage Server permission on. To begin, you will have to authenticate your Discord account which you can do with this button:</p>
+			<a id="login-button">
+				<img src="https://discord.com/assets/f8389ca1a741a115313bede9ac02e2c0.svg" alt="Discord">
+				Login
+			</a>
+			<h3>Enjoy a little story?</h3>
+			<p>A long time ago, when the world was still figuring out itself, a new faction was born, Gamepedia its name. The kingdom grew quickly, people wanted their own place in the kingdom, it was said the kingdom was a better place than any other kingdoms, which were ridden with very old infrastructure, the taxes enforced by their kings were too high and the kings not too merciful. On the other side, the new kingdom was blossoming, people from all over the world wanted to live there, have their own place there and to do that, they joined existing guilds of people who share their interests. The king here really cared about his most devoted citizens giving them tax exemption status and helping all of them so each of the guilds in the kingdom can prosper and spread the good word about the kingdom.</p>
+			<p>Soon enough the first bigger guilds were joining the kingdom, seeing the greatness of it they joined the kingdom along with their huge tracts. The momentum of growth became a sign of change, a change for the better future. One of the first great King's advisors was Wyn. She was passionate and very talented in all fields needed to manage the kingdom. She enthusiastically  welcomed new guilds and made sure there is nothing on their way to be a fully functioning guilds on Gamepedia.</p>
+			<p>At first the biggest guilds in the kingdom included a guild which consisted of people who devoted their lives to punching the trees with their bare fists, …</p>
+			<a onclick="alert('Not available yet…')">[Read more]</a>
+		</div>
 	</div>
-	<div class="sidebar">
-		<div class="guildlist">
+	<div class="scrollbar" id="sidebar">
+		<div class="scrollbar" id="channellist">
+			<a class="channel channel-header" id="login">
+				<img src="src/settings.svg" alt="Settings">
+				<div>Login</div>
+			</a>
+			<a class="channel" id="invite-wikibot" href="https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939912256&scope=bot">
+				<img src="src/channel.svg" alt="Channel">
+				<div>Invite Wiki-Bot</div>
+			</a>
+		</div>
+		<div id="navbar">
+			<div></div>
+			<a id="support" href="https://discord.gg/v77RTk5" target="_blank" alt="Help with Wiki-Bot">
+				<img class="avatar" src="https://cdn.discordapp.com/icons/464084451165732868/c6a8b9fc902b09545de8194a911e6045.png?size=64" alt="Wiki-Bot">
+				<span>Support Server</span>
+			</a>
+			<div></div>
+		</div>
+		<div id="guildlist">
 			<div class="guild" id="invite">
 				<div class="bar"></div>
 				<a href="https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939912256&scope=bot" alt="Invite Wiki-Bot">

+ 226 - 0
dashboard/oauth.js

@@ -0,0 +1,226 @@
+const crypto = require('crypto');
+const cheerio = require('cheerio');
+const {defaultPermissions} = require('../util/default.json');
+const {db, settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
+
+const DiscordOauth2 = require('discord-oauth2');
+const oauth = new DiscordOauth2( {
+	clientId: process.env.bot,
+	clientSecret: process.env.secret,
+	redirectUri: process.env.dashboard
+} );
+
+const file = require('fs').readFileSync('./dashboard/login.html');
+
+/**
+ * Let a user login
+ * @param {import('http').ServerResponse} res - The server response
+ * @param {String} [state] - The user state
+ * @param {String} [action] - The action the user made
+ */
+function dashboard_login(res, state, action) {
+	if ( state && settingsData.has(state) ) {
+		if ( !action ) {
+			res.writeHead(302, {Location: '/'});
+			return res.end();
+		}
+		settingsData.delete(state);
+	}
+	var $ = cheerio.load(file);
+	let invite = oauth.generateAuthUrl( {
+		scope: ['identify', 'guilds', 'bot'],
+		permissions: defaultPermissions, state
+	} );
+	$('.guild#invite a, .channel#invite-wikibot').attr('href', invite);
+	let responseCode = 200;
+	let prompt = 'none';
+	if ( action === 'unauthorized' ) {
+		createNotice($, {
+			type: 'info',
+			title: 'Not logged in!',
+			text: 'Please login before you can change any settings.'
+		}).prependTo('#text');
+	}
+	if ( action === 'failed' ) {
+		responseCode = 400;
+		createNotice($, {
+			type: 'error',
+			title: 'Login failed!',
+			text: 'An error occurred while logging you in, please try again.'
+		}).prependTo('#text');
+	}
+	if ( action === 'logout' ) {
+		prompt = 'consent';
+		createNotice($, {
+			type: 'success',
+			title: 'Successfully logged out!',
+			text: 'You have been successfully logged out. To change any settings you need to login again.'
+		}).prependTo('#text');
+	}
+	if ( process.env.READONLY ) {
+		createNotice($, {
+			type: 'info',
+			title: 'Read-only database!',
+			text: 'You can currently only view your settings but not change them.'
+		}).prependTo('#text');
+	}
+	state = crypto.randomBytes(16).toString("hex");
+	while ( settingsData.has(state) ) {
+		state = crypto.randomBytes(16).toString("hex");
+	}
+	let url = oauth.generateAuthUrl( {
+		scope: ['identify', 'guilds'],
+		prompt, state
+	} );
+	$('.channel#login, #login-button').attr('href', url);
+	let body = $.html();
+	res.writeHead(responseCode, {
+		'Set-Cookie': [
+			...( res.getHeader('Set-Cookie') || [] ),
+			`wikibot="${state}"; HttpOnly; Path=/`
+		],
+		'Content-Length': body.length
+	});
+	res.write( body );
+	return res.end();
+}
+
+/**
+ * Load oauth data of a user
+ * @param {import('http').ServerResponse} res - The server response
+ * @param {String} state - The user state
+ * @param {URLSearchParams} searchParams - The url parameters
+ * @param {String} [lastGuild] - The guild to return to
+ */
+function dashboard_oauth(res, state, searchParams, lastGuild) {
+	if ( state !== searchParams.get('state') || !searchParams.get('code') ) {
+		res.writeHead(302, {Location: '/login?action=failed'});
+		return res.end();
+	}
+	settingsData.delete(state);
+	return oauth.tokenRequest( {
+		scope: ['identify', 'guilds'],
+		code: searchParams.get('code'),
+		grantType: 'authorization_code'
+	} ).then( ({access_token}) => {
+		return Promise.all([
+			oauth.getUser(access_token),
+			oauth.getUserGuilds(access_token)
+		]).then( ([user, guilds]) => {
+			guilds = guilds.filter( guild => {
+				return ( guild.owner || hasPerm(guild.permissions, 'MANAGE_GUILD') );
+			} ).map( guild => {
+				return {
+					id: guild.id,
+					name: guild.name,
+					acronym: guild.name.replace( /'s /g, ' ' ).replace( /\w+/g, e => e[0] ).replace( /\s/g, '' ),
+					icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
+					+ ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) : null ),
+					userPermissions: guild.permissions
+				};
+			} );
+			sendMsg( {
+				type: 'getGuilds',
+				member: user.id,
+				guilds: guilds.map( guild => guild.id )
+			} ).then( response => {
+				var settings = {
+					state: `${state}-${user.id}`,
+					access_token,
+					user: {
+						id: user.id,
+						username: user.username,
+						discriminator: user.discriminator,
+						avatar: 'https://cdn.discordapp.com/' + ( user.avatar ? 
+							`avatars/${user.id}/${user.avatar}.` + 
+							( user.avatar.startsWith( 'a_' ) ? 'gif' : 'png' ) : 
+							`embed/avatars/${user.discriminator % 5}.png` ) + '?size=64',
+						locale: user.locale
+					},
+					guilds: {
+						count: guilds.length,
+						isMember: new Map(),
+						notMember: new Map()
+					}
+				};
+				response.forEach( (guild, i) => {
+					if ( guild ) {
+						settings.guilds.isMember.set(guilds[i].id, Object.assign(guilds[i], guild));
+					}
+					else settings.guilds.notMember.set(guilds[i].id, guilds[i]);
+				} );
+				settingsData.set(settings.state, settings);
+				res.writeHead(302, {
+					Location: ( lastGuild ? '/guild/' + lastGuild : '/' ),
+					'Set-Cookie': [`wikibot="${settings.state}"; HttpOnly; Path=/`]
+				});
+				return res.end();
+			}, error => {
+				console.log( '- Dashboard: Error while getting the guilds:', error );
+				res.writeHead(302, {Location: '/login?action=failed'});
+				return res.end();
+			} );
+		}, error => {
+			console.log( '- Dashboard: Error while getting user and guilds: ' + error );
+			res.writeHead(302, {Location: '/login?action=failed'});
+			return res.end();
+		} );
+	}, error => {
+		console.log( '- Dashboard: Error while getting the token: ' + error );
+		res.writeHead(302, {Location: '/login?action=failed'});
+		return res.end();
+	} );
+}
+
+/**
+ * Reload the guild of a user
+ * @param {import('http').ServerResponse} res - The server response
+ * @param {String} state - The user state
+ * @param {String} [returnLocation] - The return location
+ */
+function dashboard_refresh(res, state, returnLocation = '/') {
+	var settings = settingsData.get(state);
+	return oauth.getUserGuilds(settings.access_token).then( guilds => {
+		guilds = guilds.filter( guild => {
+			return ( guild.owner || hasPerm(guild.permissions, 'MANAGE_GUILD') );
+		} ).map( guild => {
+			return {
+				id: guild.id,
+				name: guild.name,
+				acronym: guild.name.replace( /'s /g, ' ' ).replace( /\w+/g, e => e[0] ).replace( /\s/g, '' ),
+				icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
+				+ ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) : null ),
+				userPermissions: guild.permissions
+			};
+		} );
+		sendMsg( {
+			type: 'getGuilds',
+			member: settings.user.id,
+			guilds: guilds.map( guild => guild.id )
+		} ).then( response => {
+			let isMember = new Map();
+			let notMember = new Map();
+			response.forEach( (guild, i) => {
+				if ( guild ) isMember.set(guilds[i].id, Object.assign(guilds[i], guild));
+				else notMember.set(guilds[i].id, guilds[i]);
+			} );
+			settings.guilds = {count: guilds.length, isMember, notMember};
+			res.writeHead(302, {Location: returnLocation + '?refresh=success'});
+			return res.end();
+		}, error => {
+			console.log( '- Dashboard: Error while getting the refreshed guilds:', error );
+			res.writeHead(302, {Location: returnLocation + '?refresh=failed'});
+			return res.end();
+		} );
+	}, error => {
+		console.log( '- Dashboard: Error while refreshing guilds: ' + error );
+		res.writeHead(302, {Location: returnLocation + '?refresh=failed'});
+		return res.end();
+	} );
+}
+
+module.exports = {
+	login: dashboard_login,
+	oauth: dashboard_oauth,
+	refresh: dashboard_refresh
+};

+ 36 - 0
dashboard/rcscript.js

@@ -0,0 +1,36 @@
+const {db, settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
+
+/**
+ * Let a user change recent changes scripts
+ * @param {import('http').ServerResponse} res - The server response
+ * @param {CheerioStatic} $ - The response body
+ * @param {import('./util.js').Guild} guild - The current guild
+ * @param {String[]} args - The url parts
+ */
+function dashboard_rcscript(res, $, guild, args) {
+	$('.channel#rcgcdb').addClass('selected');
+	db.all( 'SELECT configid, wiki, lang, display, wikiid, rcid FROM rcgcdw WHERE guild = ? ORDER BY configid ASC', [guild.id], function(dberror, rows) {
+		if ( dberror ) {
+			console.log( '- Dashboard: Error while getting the RcGcDw: ' + dberror );
+			$('#text .description').text('Failed to load the recent changes webhooks!');
+			let body = $.html();
+			res.writeHead(200, {'Content-Length': body.length});
+			res.write( body );
+			return res.end();
+		}
+		$('<pre>').text(JSON.stringify(rows, null, '\t')).appendTo('#text .description');
+		let body = $.html();
+		res.writeHead(200, {'Content-Length': body.length});
+		res.write( body );
+		return res.end();
+	} );
+}
+
+function update_rcscript() {
+	
+}
+
+module.exports = {
+	get: dashboard_rcscript,
+	post: update_rcscript
+};

+ 181 - 0
dashboard/settings.js

@@ -0,0 +1,181 @@
+const {defaultSettings} = require('../util/default.json');
+const {allLangs: {names: allLangs}} = require('../i18n/allLangs.json');
+const {db, settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
+
+const fieldset = {
+	channel: '<label for="wb-settings-channel">Channel:</label>'
+	+ '<select id="wb-settings-channel" name="channel" required></select>',
+	wiki: '<label for="wb-settings-wiki">Default Wiki:</label>'
+	+ '<input type="url" id="wb-settings-wiki" name="wiki" required>',
+	//+ '<button type="button" id="wb-settings-wiki-search" class="collapsible">Search wiki</button>'
+	//+ '<fieldset style="display: none;">'
+	//+ '<legend>Wiki search</legend>'
+	//+ '</fieldset>',
+	lang: '<label for="wb-settings-lang">Language:</label>'
+	+ '<select id="wb-settings-lang" name="lang" required>'
+	+ Object.keys(allLangs).map( lang => {
+		return `<option id="wb-settings-lang-${lang}" value="${lang}">${allLangs[lang]}</option>`
+	} ).join('\n')
+	+ '</select>',
+	prefix: '<label for="wb-settings-prefix">Prefix:</label>'
+	+ '<input type="text" id="wb-settings-prefix" name="prefix" pattern="^[^ \`]+$" required>'
+	+ '<br>'
+	+ '<label for="wb-settings-prefix-space">Prefix ends with space:</label>'
+	+ '<input type="checkbox" id="wb-settings-prefix-space" name="prefix-space">',
+	inline: '<label for="wb-settings-inline">Inline commands:</label>'
+	+ '<input type="checkbox" id="wb-settings-inline" name="inline">',
+	voice: '<label for="wb-settings-voice">Voice channels:</label>'
+	+ '<input type="checkbox" id="wb-settings-voice" name="voice">'
+};
+
+/**
+ * Let a user change settings
+ * @param {CheerioStatic} $ - The response body
+ */
+function createForm($, header, settings, guildChannels) {
+	var readonly = ( process.env.READONLY ? true : false );
+	var fields = [];
+	if ( settings.channel ) {
+		let channel = $('<div>').append(fieldset.channel);
+		channel.find('#wb-settings-channel').append(
+			...guildChannels.map( guildChannel => {
+				return $(`<option id="wb-settings-channel-${guildChannel.id}">`).val(guildChannel.id).text(`${guildChannel.id} – #${guildChannel.name}`)
+			} )
+		);
+		if ( guildChannels.length === 1 ) {
+			channel.find(`#wb-settings-channel-${settings.channel}`).attr('selected', '');
+			if ( !hasPerm(guildChannels[0].permissions, 'VIEW_CHANNEL', 'SEND_MESSAGES') ) {
+				readonly = true;
+			}
+		}
+		else channel.find('#wb-settings-channel').prepend(
+			$(`<option id="wb-settings-channel-default" selected>`).val('').text('-- Select a Channel --')
+		);
+		fields.push(channel);
+	}
+	let wiki = $('<div>').append(fieldset.wiki);
+	wiki.find('#wb-settings-wiki').val(settings.wiki);
+	fields.push(wiki);
+	if ( !settings.channel || settings.patreon ) {
+		let lang = $('<div>').append(fieldset.lang);
+		lang.find(`#wb-settings-lang-${settings.lang}`).attr('selected', '');
+		fields.push(lang);
+		let inline = $('<div>').append(fieldset.inline);
+		if ( !settings.inline ) inline.find('#wb-settings-inline').attr('checked', '');
+		fields.push(inline);
+	}
+	if ( settings.patreon && !settings.channel ) {
+		let prefix = $('<div>').append(fieldset.prefix);
+		prefix.find('#wb-settings-prefix').val(settings.prefix.trim());
+		if ( settings.prefix.endsWith( ' ' ) ) {
+			prefix.find('#wb-settings-prefix-space').attr('checked', '');
+		}
+		fields.push(prefix);
+	}
+	if ( !settings.channel ) {
+		let voice = $('<div>').append(fieldset.voice);
+		if ( settings.voice ) voice.find('#wb-settings-voice').attr('checked', '');
+		fields.push(voice);
+	}
+	var form = $('<fieldset>').append(...fields, '<input type="submit">');
+	if ( readonly ) {
+		form.find('input').attr('readonly', '');
+		form.find('input[type="submit"], input[type="checkbox"], option').attr('disabled', '');
+	}
+	return $('<form id="wb-settings" method="post" enctype="application/x-www-form-urlencoded">').append(
+		$('<h2>').text(header),
+		form
+	);
+}
+
+/**
+ * Let a user change settings
+ * @param {import('http').ServerResponse} res - The server response
+ * @param {CheerioStatic} $ - The response body
+ * @param {import('./util.js').Guild} guild - The current guild
+ * @param {String[]} args - The url parts
+ */
+function dashboard_settings(res, $, guild, args) {
+	db.all( 'SELECT channel, lang, wiki, prefix, inline, voice, patreon FROM discord WHERE guild = ? ORDER BY channel ASC', [guild.id], function(dberror, rows) {
+		if ( dberror ) {
+			console.log( '- Dashboard: Error while getting the settings: ' + dberror );
+			$('#text .description').text('Failed to load the settings!');
+			let body = $.html();
+			res.writeHead(200, {'Content-Length': body.length});
+			res.write( body );
+			return res.end();
+		}
+		$('#text .description').text(`These are the settings for "${guild.name}":`);
+		if ( !rows.length ) {
+			$('.channel#settings').addClass('selected');
+			createForm($, 'Server-wide Settings', Object.assign({
+				prefix: process.env.prefix
+			}, defaultSettings)).attr('action', `/guild/${guild.id}`).appendTo('#text');
+			let body = $.html();
+			res.writeHead(200, {'Content-Length': body.length});
+			res.write( body );
+			return res.end();
+		}
+		let isPatreon = rows.some( row => row.patreon );
+		let channellist = rows.filter( row => row.channel ).map( row => {
+			let channel = guild.channels.find( channel => channel.id === row.channel );
+			return ( channel || {id: row.channel, name: 'UNKNOWN', permissions: 0} );
+		} ).sort( (a, b) => {
+			return guild.channels.indexOf(a) - guild.channels.indexOf(b);
+		} );
+		$('#channellist #settings').after(
+			...channellist.map( channel => {
+				return $('<a class="channel">').attr('href', `/guild/${guild.id}/${channel.id}`).append(
+					$('<img>').attr('src', '/src/channel.svg'),
+					$('<div>').text(channel.name)
+				).attr('id', `channel-${channel.id}`).attr('title', channel.id);
+			} ),
+			( process.env.READONLY ? '' :
+			$('<a class="channel" id="channel-new">').attr('href', `/guild/${guild.id}/new`).append(
+				$('<img>').attr('src', '/src/channel.svg'),
+				$('<div>').text('New channel overwrite')
+			) )
+		);
+		if ( args[3] === 'new' ) {
+			$('.channel#channel-new').addClass('selected');
+			createForm($, 'New channel overwrite', Object.assign({}, rows.find( row => !row.channel ), {
+				patreon: isPatreon,
+				channel: 'new'
+			}), guild.channels.filter( channel => {
+				return hasPerm(channel.permissions, 'VIEW_CHANNEL', 'SEND_MESSAGES');
+			} )).attr('action', `/guild/${guild.id}`).appendTo('#text');
+			let body = $.html();
+			res.writeHead(200, {'Content-Length': body.length});
+			res.write( body );
+			return res.end();
+		}
+		if ( channellist.some( channel => channel.id === args[3] ) ) {
+			let channel = channellist.find( channel => channel.id === args[3] );
+			$(`.channel#channel-${channel.id}`).addClass('selected');
+			createForm($, `#${channel.name} Settings`, Object.assign({}, rows.find( row => {
+				return row.channel === channel.id;
+			} ), {
+				patreon: isPatreon
+			}), [channel]).attr('action', `/guild/${guild.id}`).appendTo('#text');
+			let body = $.html();
+			res.writeHead(200, {'Content-Length': body.length});
+			res.write( body );
+			return res.end();
+		}
+		$('.channel#settings').addClass('selected');
+		createForm($, 'Server-wide Settings', rows.find( row => !row.channel )).attr('action', `/guild/${guild.id}`).appendTo('#text');
+		let body = $.html();
+		res.writeHead(200, {'Content-Length': body.length});
+		res.write( body );
+		return res.end();
+	} );
+}
+
+function update_settings(user, guild, settings) {
+	
+}
+
+module.exports = {
+	get: dashboard_settings,
+	post: update_settings
+};

+ 3 - 0
dashboard/src/channel.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 24 24">
+	<path fill="#8e9297" fill-rule="evenodd" clip-rule="evenodd" d="M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z"></path>
+</svg>

+ 319 - 0
dashboard/src/index.css

@@ -0,0 +1,319 @@
+html {
+	height: calc(100% - 48px);
+}
+body {
+	font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
+	text-rendering: optimizeLegibility;
+	background: #36393f;
+	color: #dcddde;
+	position: relative;
+	min-height: 100%;
+	margin: 0;
+}
+a {
+	text-decoration: none;
+	color: inherit;
+}
+a[alt]:hover:after {
+	content: attr(alt);
+	position: absolute;
+	background: #000000;
+	color: #dcddde;
+	font-weight: bold;
+	font-size: 90%;
+	word-break: break-word;
+	border-radius: 4px;
+	padding: 8px;
+}
+.description a,
+a .description {
+	color: #00b0f4;
+}
+.description a:hover,
+a:hover .description {
+	text-decoration: underline;
+}
+#text {
+	position: relative;
+	padding: 8px;
+	width: calc(100% - 328px);
+	top: 48px;
+	left: 312px;
+}
+.notice {
+	padding: 5px 10px;
+	line-height: 1.6;
+	text-align: center;
+	margin: 0 auto 1em;
+	width: fit-content;
+	border: 2px solid;
+}
+.notice-error {
+	background-color: #200;
+	border-color: #500;
+}
+.notice-info {
+	background-color: #220;
+	border-color: #550;
+}
+.notice-success {
+	background-color: #020;
+	border-color: #050;
+}
+.server-selector {
+	display: flex;
+	flex-wrap: wrap;
+}
+.server {
+	background-color: rgba(0,0,0,0.5);
+	text-align: center;
+	border-radius: 10%;
+	width: 200px;
+	margin: 5px;
+}
+.server:hover {
+	background: rgba(0,0,0,0.3);
+	filter: brightness(1.2);
+}
+.server .avatar {
+	border-radius: 10%;
+	width: 200px;
+	height: 200px;
+}
+.server .noicon {
+	font-size: 416%;
+	background-color: unset;
+}
+.server .server-name {
+	padding: 8px 12px;
+	word-break: break-word;
+	font-weight: bold;
+	font-size: 90%;
+}
+#navbar {
+	background: #2f3136;
+	position: fixed;
+	top: 0;
+	left: 72px;
+	right: 0;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	height: 48px;
+	font-size: 16px;
+	line-height: 20px;
+	font-weight: bold;
+	box-shadow: 0 1px 0 rgba(4,4,5,0.2),
+				0 1.5px 0 rgba(6,6,7,0.05),
+				0 2px 0 rgba(4,4,5,0.05);
+}
+:target::before {
+	content: "";
+	display: block;
+	height: 48px;
+	margin: -48px 0 0;
+}
+#navbar a {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	height: 100%;
+	padding-left: 10px;
+}
+#navbar a:hover {
+	background: #202225;
+}
+#navbar a[alt]:hover:after {
+	top: 48px;
+}
+#navbar .avatar {
+	width: 32px;
+	height: 32px;
+}
+#navbar span {
+	padding: 0 10px;
+}
+.scrollbar {
+	-ms-overflow-style: none;
+	scrollbar-width: none;
+}
+.scrollbar::-webkit-scrollbar {
+	display: none;
+}
+#sidebar {
+	position: fixed;
+	display: flex;
+	top: 0;
+	left: 0;
+	bottom: 0;
+	width: 312px;
+	overflow: visible scroll;
+}
+#guildlist {
+	background: #202225;
+	position: absolute;
+	padding: 12px 0;
+	flex: 1 1 auto;
+	min-height: calc(100% - 24px);
+	width: 72px;
+}
+.guild {
+	margin: 0 0 8px;
+	position: relative;
+	display: flex;
+	justify-content: center;
+}
+.avatar {
+	border-radius: 50%;
+	width: 48px;
+	height: 48px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	line-height: 1.2em;
+	white-space: nowrap;
+	overflow: hidden;
+	color: #dcddde;
+	font-weight: bold;
+}
+#navbar a:hover .avatar,
+.guild.selected .avatar,
+.guild:hover .avatar {
+	border-radius: 30%;
+	color: #ffffff;
+}
+.noicon {
+	width: 48px;
+	height: 48px;
+	background: #36393f;
+}
+.guild.selected .noicon,
+.guild:hover .noicon {
+	background-color: #7289da;
+}
+.svg-avatar {
+	color: #43b581;
+	background: #36393f;
+}
+.guild:hover .svg-avatar {
+	color: #ffffff;
+	background-color: #43b581;
+}
+.separator {
+	height: 2px;
+	width: 32px;
+	border-radius: 1px;
+	background-color: rgba(255,255,255,0.06);
+}
+.bar {
+	position: absolute;
+	left: 0;
+	top: 0;
+	display: block;
+	width: 8px;
+	border-radius: 0 4px 4px 0;
+	margin-left: -4px;
+	background-color: #ffffff;
+}
+.guild:hover .bar {
+	margin-top: 14px;
+	height: 20px;
+}
+.guild.selected .bar {
+	margin-top: 4px;
+	height: 40px;
+}
+.guild a[alt]:hover:after {
+	top: 20%;
+	left: 72px;
+	max-width: 224px;
+	width: max-content;
+}
+#channellist {
+	background: #2f3136;
+	position: fixed;
+	overflow: hidden scroll;
+	padding: 16px 0;
+	width: 240px;
+	top: 48px;
+	left: 72px;
+	bottom: 0;
+}
+.channel {
+	padding: 0 8px;
+	margin: 0 8px 2px 12px;
+	height: 32px;
+	border-radius: 4px;
+	display: flex;
+	align-items: center;
+	color: #8e9297;
+}
+.channel img {
+	margin-right: 6px;
+	width: 20px;
+	height: 20px;
+}
+.channel div {
+	font-size: 16px;
+	line-height: 20px;
+	text-shadow: 0 0 1px;
+	white-space: nowrap;
+	overflow: hidden;
+}
+.channel:hover {
+	background: rgba(79,84,92,0.16);
+}
+.channel.selected {
+	background: rgba(79,84,92,0.32);
+}
+.channel:hover div {
+	color: #dcddde;
+}
+.channel.selected div {
+	color: #ffffff;
+}
+.channel-header {
+	margin-left: 8px;
+	height: 44px;
+}
+.channel-header img {
+	width: 24px;
+	height: 24px;
+}
+.channel-header div {
+	font-size: 20px;
+	line-height: 24px;
+	font-weight: bold;
+	text-shadow: none;
+}
+fieldset div {
+	margin: 10px 0;
+}
+fieldset label {
+	display: inline-block;
+	min-width: 20%;
+}
+fieldset input[type="url"] {
+	min-width: 30%;
+	margin-right: 5px;
+}
+#login-button {
+	display: flex;
+	margin: 20px auto;
+	padding: 20px 50px;
+	width: fit-content;
+	justify-content: center;
+	align-items: center;
+	font-size: 300%;
+	background: #2f3136;
+	border: 5px solid #202225;
+	border-radius: 30px;
+}
+#login-button:hover {
+	background-color:#36393f;
+}
+#login-button img {
+	width: 60px;
+	height: 60px;
+	padding-right: 10px;
+}

+ 88 - 0
dashboard/src/index.js

@@ -0,0 +1,88 @@
+const wiki = document.getElementById('wb-settings-wiki');
+if ( wiki ) wiki.addEventListener( 'input', function (event) {
+	if ( wiki.validity.valid ) {
+		wiki.setCustomValidity('I am expecting an e-mail address!');
+	}
+	else {
+		wiki.setCustomValidity();
+	}
+} );
+
+const prefix = document.getElementById('wb-settings-prefix');
+if ( prefix ) prefix.addEventListener( 'input', function (event) {
+	if ( prefix.validity.patternMismatch ) {
+		prefix.setCustomValidity('The prefix may not include spaces or code markdown!');
+	}
+	else {
+		prefix.setCustomValidity();
+	}
+} );
+
+const form = document.getElementById('wb-settings');
+if ( form ) form.addEventListener( 'submit', function (event) {
+	if ( prefix && prefix.validity.patternMismatch ) {
+		prefix.setCustomValidity('The prefix may not include spaces or code markdown!');
+		event.preventDefault();
+	}
+	else if ( wiki && wiki.validity.valid ) {
+		wiki.value
+		fetch()/*
+		got.get( wikinew + 'api.php?&action=query&meta=siteinfo&siprop=general&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 = new Wiki(api.split('api.php?')[0], wikinew);
+					return got.get( wikinew + 'api.php?action=query&meta=siteinfo&siprop=generals&format=json' );
+				}
+			}
+			return response;
+		} ).then( response => {
+			var body = response.body;
+			if ( response.statusCode !== 200 || !body?.query?.allmessages || !body?.query?.general || !body?.query?.extensions ) {
+				console.log( '- ' + response.statusCode + ': Error while testing the wiki: ' + body?.error?.info );
+				if ( reaction ) reaction.removeEmoji();
+				msg.reactEmoji('nowiki', true);
+				return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {}, true );
+			}
+			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 = '';
+				if ( body.query.allmessages[0]['*'] ) {
+					crossover = 'https://' + body.query.allmessages[0]['*'] + '.gamepedia.com/';
+				}
+				else if ( body.query.allmessages[1]['*'] ) {
+					let merge = body.query.allmessages[1]['*'].split('/');
+					crossover = 'https://' + merge[0] + '.fandom.com/' + ( merge[1] ? merge[1] + '/' : '' );
+				}
+				if ( crossover ) wikinew = new Wiki(crossover);
+			}
+		}, ferror => {
+			console.log( '- Error while testing the wiki: ' + ferror );
+			if ( reaction ) reaction.removeEmoji();
+			msg.reactEmoji('nowiki', true);
+			return msg.replyMsg( lang.get('settings.wikiinvalid') + wikihelp, {}, true );
+		} );*/
+	}
+	else form.dispatchEvent(new Event('submit'));
+} );
+
+var collapsible = document.getElementsByClassName('collapsible');
+for ( var i = 0; i < collapsible.length; i++ ) {
+	collapsible[i].onclick = function() {
+		this.classList.toggle('active');
+		if ( this.id === 'wb-settings-wiki-search' ) {
+			wiki.toggleAttribute('readonly');
+		}
+		var content = this.nextElementSibling;
+		if ( content.style.display === 'block' ) {
+			content.style.display = 'none';
+		}
+		else {
+			content.style.display = 'block';
+		}
+	}
+}

+ 3 - 0
dashboard/src/settings.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 24 24">
+	<path fill="#8e9297" fill-rule="evenodd" clip-rule="evenodd" d="M19.738 10H22V14H19.739C19.498 14.931 19.1 15.798 18.565 16.564L20 18L18 20L16.565 18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069 19.498 8.203 19.099 7.436 18.564L6 20L4 18L5.436 16.564C4.901 15.799 4.502 14.932 4.262 14H2V10H4.262C4.502 9.068 4.9 8.202 5.436 7.436L4 6L6 4L7.436 5.436C8.202 4.9 9.068 4.502 10 4.262V2H14V4.261C14.932 4.502 15.797 4.9 16.565 5.435L18 3.999L20 5.999L18.564 7.436C19.099 8.202 19.498 9.069 19.738 10ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16Z"></path>
+</svg>

+ 125 - 0
dashboard/util.js

@@ -0,0 +1,125 @@
+const sqlite3 = require('sqlite3').verbose();
+const mode = ( process.env.READONLY ? sqlite3.OPEN_READONLY : sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE );
+const db = new sqlite3.Database( './wikibot.db', mode, dberror => {
+	if ( dberror ) {
+		console.log( '- Dashboard: Error while connecting to the database: ' + dberror );
+		return dberror;
+	}
+	console.log( '- Dashboard: Connected to the database.' );
+} );
+
+/**
+ * @typedef Settings
+ * @property {String} state
+ * @property {String} access_token
+ * @property {User} user
+ * @property {Object} guilds
+ * @property {Number} guilds.count
+ * @property {Map<String, Guild>} guilds.isMember
+ * @property {Map<String, Guild>} guilds.notMember
+ */
+
+/**
+ * @typedef User
+ * @property {String} id
+ * @property {String} username
+ * @property {String} discriminator
+ * @property {String} avatar
+ * @property {String} locale
+ */
+
+/**
+ * @typedef Guild
+ * @property {String} id
+ * @property {String} name
+ * @property {String} acronym
+ * @property {String} [icon]
+ * @property {String} userPermissions
+ * @property {String} [botPermissions]
+ * @property {{id: String, name: String, permissions: Number}[]} [channels]
+ * @property {{id: String, name: String, lower: Boolean}[]} [roles]
+ */
+
+/**
+ * @type {Map<String, Settings>}
+ */
+const settingsData = new Map();
+
+/**
+ * @type {Map<Number, PromiseConstructor>}
+ */
+const messages = new Map();
+var messageId = 1;
+
+process.on( 'message', message => {
+	if ( message.id ) {
+		if ( message.data.error ) messages.get(message.id).reject(message.data.error);
+		else messages.get(message.id).resolve(message.data.response);
+		return messages.delete(message.id);
+	}
+	console.log( '- [Dashboard]: Message received!', message );
+} );
+
+/**
+ * Send messages to the manager.
+ * @param {Object} [message] - The message.
+ * @returns {Promise<Object>}
+ */
+function sendMsg(message) {
+	var id = messageId++;
+	var promise = new Promise( (resolve, reject) => {
+		messages.set(id, {resolve, reject});
+		process.send( {id, data: message} );
+	} );
+	return promise;
+}
+
+/**
+ * Create a red notice
+ * @param {CheerioStatic} $ - The cheerio static
+ * @param {Object} notice - The notices to create
+ * @param {String} notice.title - The title of the notice
+ * @param {String} notice.text - The text of the notice
+ * @param {String} [notice.type] - The type of the notice
+ * @returns {Cheerio}
+ */
+function createNotice($, notice) {
+	var type = ( notice.type ? `notice-${notice.type}` : '' );
+	return $('<div class="notice">').append(
+		$('<b>').text(notice.title),
+		$('<div>').text(notice.text)
+	).addClass(type);
+}
+
+const permissions = {
+	ADMINISTRATOR: 1 << 3,
+	MANAGE_CHANNELS: 1 << 4,
+	MANAGE_GUILD: 1 << 5,
+	ADD_REACTIONS: 1 << 6,
+	VIEW_CHANNEL: 1 << 10,
+	SEND_MESSAGES: 1 << 11,
+	MANAGE_MESSAGES: 1 << 13,
+	EMBED_LINKS: 1 << 14,
+	ATTACH_FILES: 1 << 15,
+	READ_MESSAGE_HISTORY: 1 << 16,
+	USE_EXTERNAL_EMOJIS: 1 << 18,
+	MANAGE_NICKNAMES: 1 << 27,
+	MANAGE_ROLES: 1 << 28,
+	MANAGE_WEBHOOKS: 1 << 29
+}
+
+/**
+ * Check if a permission is included in the BitField
+ * @param {String|Number} all - BitField of multiple permissions
+ * @param {String[]} permission - Name of the permission to check for
+ * @returns {Boolean}
+ */
+function hasPerm(all, ...permission) {
+	if ( (all & permissions.ADMINISTRATOR) === permissions.ADMINISTRATOR ) return true;
+	return permission.map( perm => {
+		let bit = permissions[perm];
+		return ( (all & bit) === bit );
+	} ).every( perm => perm );
+}
+
+module.exports = {db, settingsData, sendMsg, createNotice, hasPerm};

+ 36 - 0
dashboard/verification.js

@@ -0,0 +1,36 @@
+const {db, settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
+
+/**
+ * Let a user change verifications
+ * @param {import('http').ServerResponse} res - The server response
+ * @param {CheerioStatic} $ - The response body
+ * @param {import('./util.js').Guild} guild - The current guild
+ * @param {String[]} args - The url parts
+ */
+function dashboard_verification(res, $, guild, args) {
+	$('.channel#verification').addClass('selected');
+	db.all( 'SELECT configid, channel, role, editcount, usergroup, accountage, rename FROM verification WHERE guild = ? ORDER BY configid ASC', [guild.id], function(dberror, rows) {
+		if ( dberror ) {
+			console.log( '- Dashboard: Error while getting the verifications: ' + dberror );
+			$('#text .description').text('Failed to load the verifications!');
+			let body = $.html();
+			res.writeHead(200, {'Content-Length': body.length});
+			res.write( body );
+			return res.end();
+		}
+		$('<pre>').text(JSON.stringify(rows, null, '\t')).appendTo('#text .description');
+		let body = $.html();
+		res.writeHead(200, {'Content-Length': body.length});
+		res.write( body );
+		return res.end();
+	} );
+}
+
+function update_verification() {
+	
+}
+
+module.exports = {
+	get: dashboard_verification,
+	post: update_verification
+};

+ 1 - 1
functions/global_block.js

@@ -14,7 +14,7 @@ const toTitle = require('../util/wiki.js').toTitle;
  * @param {String} [gender] - The gender of the user.
  */
 function global_block(lang, msg, username, text, embed, wiki, spoiler, gender) {
-	if ( !msg || msg.channel.type !== 'text' || !( msg.guild.id in patreons ) ) return;
+	if ( !msg || !msg.channel.isGuild() || !( msg.guild.id in patreons ) ) return;
 	
 	var isUser = true;
 	if ( !gender ) {

+ 1 - 1
functions/special_page.js

@@ -142,7 +142,7 @@ function special_page(lang, msg, title, specialpage, embed, wiki, reaction, spoi
 				if ( description.length > 2000 ) description = description.substring(0, 2000) + '\u2026';
 				embed.setDescription( description );
 			}
-			if ( msg.channel.type === 'text' && msg.guild.id in patreons && specialpage in querypages ) {
+			if ( msg.channel.isGuild() && msg.guild.id in patreons && specialpage in querypages ) {
 				var text = Util.splitMessage( querypages[specialpage][1](body.query, wiki), {maxLength:1000} )[0];
 				embed.addField( lang.get('search.special'), ( text || lang.get('search.empty') ) );
 			}

+ 45 - 9
main.js

@@ -80,22 +80,58 @@ if ( process.env.dashboard ) {
 				error: null
 			};
 			switch ( message.data.type ) {
-				case 'isMember':
-					return manager.broadcastEval(`this.guilds.cache.has('${message.data.guild}')`).then( results => {
-						data.response = results.includes( true );
+				case 'getGuilds':
+					return manager.broadcastEval(`Promise.all(
+						${JSON.stringify(message.data.guilds)}.map( id => {
+							if ( this.guilds.cache.has(id) ) {
+								let guild = this.guilds.cache.get(id);
+								return guild.members.fetch('${message.data.member}').then( member => {
+									return {
+										botPermissions: guild.me.permissions.bitfield,
+										channels: guild.channels.cache.filter( channel => {
+											return channel.isGuild();
+										} ).sort( (a, b) => {
+											return a.rawPosition - b.rawPosition;
+										} ).map( channel => {
+											return {
+												id: channel.id,
+												name: channel.name,
+												permissions: member.permissionsIn(channel).bitfield
+											};
+										} ),
+										roles: guild.roles.cache.filter( role => {
+											return ( role.id !== guild.id );
+										} ).sort( (a, b) => {
+											return b.rawPosition - a.rawPosition;
+										} ).map( role => {
+											return {
+												id: role.id,
+												name: role.name,
+												lower: ( guild.me.roles.highest.comparePositionTo(role) > 0 && !role.managed )
+											};
+										} )
+									};
+								} )
+							}
+						} )
+					)`).then( results => {
+						data.response = message.data.guilds.map( (guild, i) => {
+							return results.find( result => result[i] )?.[i];
+						} );
 					}, error => {
 						data.error = error;
 					} ).finally( () => {
 						return dashboard.send( {id: message.id, data} );
 					} );
 					break;
-				case 'isMemberAll':
-					return manager.broadcastEval(`${JSON.stringify(message.data.guilds)}.map( guild => {
-						return this.guilds.cache.has(guild);
-					} )`).then( results => {
-						data.response = message.data.guilds.map( (guild, i) => {
-							return results.map( result => result[i] ).includes( true );
+				case 'getMember':
+					return manager.broadcastEval(`if ( this.guilds.cache.has('${message.data.guild}') ) {
+						let guild = this.guilds.cache.get('${message.data.guild}');
+						guild.members.fetch('${message.data.member}').then( member => {
+							return member.permissions.bitfield;
 						} );
+					}`).then( results => {
+						data.response = results.find( result => result );
 					}, error => {
 						data.error = error;
 					} ).finally( () => {

+ 3 - 3
package-lock.json

@@ -387,9 +387,9 @@
       "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
     },
     "discord-oauth2": {
-      "version": "2.4.0",
-      "resolved": "https://registry.npmjs.org/discord-oauth2/-/discord-oauth2-2.4.0.tgz",
-      "integrity": "sha512-UhS44esXxcOAmPj2c10jjh8w6GUIvizAO7Nnt/Msz2a3zAiH0iHc4/Tu96Pr/OAzqc+BbL9B2Q6zPifCOHDR3A=="
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/discord-oauth2/-/discord-oauth2-2.5.0.tgz",
+      "integrity": "sha512-CFbc2jtALlzbsNw8yBxBWWQUxVAUqa3+8ZbhD0Vrrlj+9mh3PC5Itn7r4ibndwUCTYLKPcVKDAgLEL2WdYdSAQ=="
     },
     "discord.js": {
       "version": "12.3.1",

+ 1 - 1
package.json

@@ -16,7 +16,7 @@
   },
   "dependencies": {
     "cheerio": "^1.0.0-rc.3",
-    "discord-oauth2": "^2.4.0",
+    "discord-oauth2": "^2.5.0",
     "discord.js": "^12.3.1",
     "dotenv": "^8.2.0",
     "full-icu": "^1.3.1",

+ 6 - 6
util/newMessage.js

@@ -44,7 +44,7 @@ function newMessage(msg, lang, wiki = defaultSettings.wiki, prefix = process.env
 			cont = cont.substring(prefix.length);
 			let args = cont.split(' ').slice(1);
 			if ( cont.split(' ')[0].split('\n')[1] ) args.unshift( '', cont.split(' ')[0].split('\n')[1] );
-			console.log( ( channel.type === 'text' ? msg.guild.id : '@' + author.id ) + ': ' + prefix + cont );
+			console.log( ( channel.isGuild() ? msg.guild.id : '@' + author.id ) + ': ' + prefix + cont );
 			return ownercmdmap[aliasInvoke](lang, msg, args, cont, wiki);
 		}
 	}
@@ -67,10 +67,10 @@ function newMessage(msg, lang, wiki = defaultSettings.wiki, prefix = process.env
 		var aliasInvoke = ( lang.aliases[invoke] || invoke );
 		var ownercmd = ( msg.isOwner() && aliasInvoke in ownercmdmap );
 		var pausecmd = ( msg.isAdmin() && pause[msg.guild.id] && aliasInvoke in pausecmdmap );
-		if ( channel.type === 'text' && pause[msg.guild.id] && !( pausecmd || ownercmd ) ) {
+		if ( channel.isGuild() && pause[msg.guild.id] && !( pausecmd || ownercmd ) ) {
 			return console.log( msg.guild.id + ': Paused' );
 		}
-		console.log( ( channel.type === 'text' ? msg.guild.id : '@' + author.id ) + ': ' + prefix + line );
+		console.log( ( channel.isGuild() ? msg.guild.id : '@' + author.id ) + ': ' + prefix + line );
 		if ( ownercmd ) return ownercmdmap[aliasInvoke](lang, msg, args, line, wiki);
 		if ( pausecmd ) return pausecmdmap[aliasInvoke](lang, msg, args, line, wiki);
 		if ( aliasInvoke in cmdmap ) return cmdmap[aliasInvoke](lang, msg, args, line, wiki);
@@ -99,7 +99,7 @@ function newMessage(msg, lang, wiki = defaultSettings.wiki, prefix = process.env
 		return cmdmap.LINK(lang, msg, line, wiki);
 	} );
 	
-	if ( ( channel.type !== 'text' || !pause[msg.guild.id] ) && !noInline && ( cont.includes( '[[' ) || cont.includes( '{{' ) ) ) {
+	if ( ( !channel.isGuild() || !pause[msg.guild.id] ) && !noInline && ( cont.includes( '[[' ) || cont.includes( '{{' ) ) ) {
 		var links = [];
 		var embeds = [];
 		var linkcount = 0;
@@ -115,7 +115,7 @@ function newMessage(msg, lang, wiki = defaultSettings.wiki, prefix = process.env
 				while ( ( entry = regex.exec(line) ) !== null ) {
 					if ( linkcount < linkmaxcount ) {
 						linkcount++;
-						console.log( ( channel.type === 'text' ? msg.guild.id : '@' + author.id ) + ': ' + entry[0] );
+						console.log( ( channel.isGuild() ? msg.guild.id : '@' + author.id ) + ': ' + entry[0] );
 						let title = entry[2].split('#')[0];
 						let section = ( entry[2].includes( '#' ) ? entry[2].split('#').slice(1).join('#') : '' )
 						links.push({title,section,spoiler:entry[1]});
@@ -135,7 +135,7 @@ function newMessage(msg, lang, wiki = defaultSettings.wiki, prefix = process.env
 				while ( ( entry = regex.exec(line) ) !== null ) {
 					if ( count < maxcount ) {
 						count++;
-						console.log( ( channel.type === 'text' ? msg.guild.id : '@' + author.id ) + ': ' + entry[0] );
+						console.log( ( channel.isGuild() ? msg.guild.id : '@' + author.id ) + ': ' + entry[0] );
 						let title = entry[2].split('#')[0];
 						let section = ( entry[2].includes( '#' ) ? entry[2].split('#').slice(1).join('#') : '' )
 						embeds.push({title,section,spoiler:entry[1]});