ソースを参照

continue dashboard prep

Markus-Rost 4 年 前
コミット
031674eb2a
4 ファイル変更535 行追加107 行削除
  1. 97 27
      dashboard/index.html
  2. 186 72
      dashboard/index.js
  3. 210 0
      dashboard/login.html
  4. 42 8
      main.js

+ 97 - 27
dashboard/index.html

@@ -10,28 +10,86 @@
 	<meta property="og:site_name" content="Wiki-Bot Settings">
 	<meta itemprop="author" content="MarkusRost">
 	<style>
+		html {
+			height: calc(100% - 48px);
+		}
 		body {
-			background: #36393f;
 			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;
-			display: inline-block;
+			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: 100%;
-			display: flex;
+			min-height: calc(100% + 48px);
 			width: 72px;
+			display: flex;
 		}
 		.guildlist {
-			padding: 12px 0 0;
+			padding: 12px 0;
 			position: relative;
 			flex: 1 1 auto;
 		}
@@ -41,9 +99,6 @@
 			display: flex;
 			justify-content: center;
 		}
-		.guild a {
-			text-decoration: none;
-		}
 		.avatar {
 			border-radius: 50%;
 			width: 48px;
@@ -58,6 +113,7 @@
 			color: #dcddde;
 			font-weight: bold;
 		}
+		.navbar a:hover .avatar,
 		.guild.selected .avatar,
 		.guild:hover .avatar {
 			border-radius: 30%;
@@ -72,11 +128,11 @@
 		.guild:hover .noicon {
 			background-color: #7289da;
 		}
-		#invite .avatar {
+		.svg-avatar {
 			color: #43b581;
 			background: #36393f;
 		}
-		#invite:hover .avatar {
+		.guild:hover .svg-avatar {
 			color: #ffffff;
 			background-color: #43b581;
 		}
@@ -104,11 +160,9 @@
 			margin-top: 4px;
 			height: 40px;
 		}
-		.guild a[alt]:hover:after {
+		a[alt]:hover:after {
 			content: attr(alt);
 			position: absolute;
-			top: 20%;
-			left: 72px;
 			background: #000000;
 			color: #dcddde;
 			font-weight: bold;
@@ -117,25 +171,37 @@
 			border-radius: 4px;
 			padding: 8px;
 		}
+		.guild a[alt]:hover:after {
+			top: 20%;
+			left: 72px;
+		}
+		.navbar a[alt]:hover:after {
+			top: 48px;
+		}
 	</style>
 </head>
 <body class="settings">
-	<div class="text"><replace id="text">Some text here</replace></div>
+	<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>
+		<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>MarkusRost #1234</span>
+		</a>
+	</div>
 	<div class="sidebar">
 		<div class="guildlist">
-			<div class="guild" id="refresh">
-				<div class="bar"></div>
-				<a href="/refresh" alt="Refresh guild list">
-					<img class="avatar" src="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=128" alt="Refresh guild list" width="48" height="48">
-				</a>
-			</div>
-			<div class="guild">
-				<div class="separator"></div>
-			</div>
 			<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">
-					<div class="avatar">
+					<div class="avatar svg-avatar">
 						<svg width="24" height="24" viewBox="0 0 24 24">
 							<path fill="currentColor" d="M20 11.1111H12.8889V4H11.1111V11.1111H4V12.8889H11.1111V20H12.8889V12.8889H20V11.1111Z"></path>
 						</svg>
@@ -146,10 +212,14 @@
 			<div class="guild">
 				<div class="separator"></div>
 			</div>
-			<div class="guild" id="logout">
+			<div class="guild" id="refresh">
 				<div class="bar"></div>
-				<a href="/logout" alt="Logout">
-					<img class="avatar" src="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=128" alt="Logout" width="48" height="48">
+				<a href="/refresh" alt="Refresh guild 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>
+						</svg>
+					</div>
 				</a>
 			</div>
 		</div>

+ 186 - 72
dashboard/index.js

@@ -21,16 +21,31 @@ const oauth = new DiscordOauth2( {
 } );
 
 const fs = require('fs');
-const file = fs.readFileSync('./dashboard/index.html');
+const files = {
+	index: fs.readFileSync('./dashboard/index.html'),
+	login: fs.readFileSync('./dashboard/login.html')
+}
 
-var messageId = 1;
+/**
+ * @type {Map<Number, PromiseConstructor>}
+ */
 var messages = new Map();
+var messageId = 1;
 
 process.on( 'message', message => {
-	messages.get(message.id).resolve(message.data);
-	messages.delete(message.id);
+	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) => {
@@ -45,7 +60,9 @@ function sendMsg(message) {
  * @property {String} state
  * @property {String} access_token
  * @property {User} user
- * @property {Map<String, Guild>} guilds
+ * @property {Object} guilds
+ * @property {Map<String, Guild>} guilds.isMember
+ * @property {Map<String, Guild>} guilds.notMember
  */
 
 /**
@@ -72,24 +89,29 @@ function sendMsg(message) {
 var settingsData = new Map();
 
 const server = http.createServer((req, res) => {
-	res.setHeader('Content-Type', 'text/html');
-	res.setHeader('Content-Language', ['en']);
-
 	if ( req.method !== 'GET' ) {
 		let notice = '<img width="400" src="https://http.cat/418"><br><strong>' + http.STATUS_CODES[418] + '</strong>';
 		res.writeHead(418, {'Content-Length': notice.length});
 		res.write( notice );
 		return res.end();
 	}
-	
-	var guild = req.headers?.cookie?.split('; ')?.filter( cookie => {
+
+	if ( req.url === '/favicon.ico' ) {
+		res.writeHead(302, {Location: 'https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=64'});
+		return res.end();
+	}
+
+	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 ( guild ) res.setHeader('Set-Cookie', [`guild="${guild}"; Max-Age=0; HttpOnly; Path=/oauth`]);
+	if ( lastGuild ) res.setHeader('Set-Cookie', [`guild="${lastGuild}"; Max-Age=0; HttpOnly; Path=/`]);
 
 	var state = req.headers.cookie?.split('; ')?.filter( cookie => {
 		return cookie.split('=')[0] === 'wikibot';
-	} )?.map( cookie => cookie.replace( /^wikibot="(\w+)"$/, '$1' ) )?.join();
+	} )?.map( cookie => cookie.replace( /^wikibot="(\w+(?:-\d+)?)"$/, '$1' ) )?.join();
 
 	var reqURL = new URL(req.url, process.env.dashboard);
 
@@ -98,28 +120,44 @@ const server = http.createServer((req, res) => {
 			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 = '<img width="400" src="https://http.cat/' + responseCode + '"><br><strong>Login failed, please try again!</strong><br><br>';
+			$('replace#notice').replaceWith(`<div class="notice">
+				<b>Login failed!</b>
+				<div>An error occurred while logging you in, please try again.</div>
+			</div>`);
 		}
 		if ( reqURL.searchParams.get('action') === 'unauthorized' ) {
 			responseCode = 401;
-			notice = '<img width="400" src="https://http.cat/' + responseCode + '"><br><strong>Please login first!</strong><br><br>';
+			$('replace#notice').replaceWith(`<div class="notice">
+				<b>Not logged in!</b>
+				<div>Please login before you can change any settings.</div>
+			</div>`);
 		}
 		if ( reqURL.searchParams.get('action') === 'logout' ) {
-			notice = '<img width="400" src="https://http.cat/' + responseCode + '"><br><strong>Successfully logged out!</strong><br><br>';
+			$('replace#notice').replaceWith(`<div class="notice">
+				<b>Successfully logged out!</b>
+				<div>You have been successfully logged out. To change any settings you need to login again.</div>
+			</div>`);
 		}
+		$('replace#notice').replaceWith('');
 		state = crypto.randomBytes(16).toString("hex");
 		while ( settingsData.has(state) ) {
 			state = crypto.randomBytes(16).toString("hex");
 		}
 		let url = oauth.generateAuthUrl( {
 			scope: ['identify', 'guilds'],
-			promt: 'none', state
+			prompt: 'none', state
 		} );
-		notice += `<a href="${url}">Login</a>`;
+		$('replace#text').replaceWith(`<a href="${url}">Login</a>`);
+		let notice = $.html();
 		res.writeHead(responseCode, {
 			'Set-Cookie': [`wikibot="${state}"; HttpOnly`],
 			'Content-Length': notice.length
@@ -138,12 +176,17 @@ const server = http.createServer((req, res) => {
 	}
 
 	if ( !state ) {
-		res.writeHead(302, {Location: '/login?action=unauthorized'});
+		res.writeHead(302, {
+			Location: ( reqURL.pathname === '/' ? '/login' : '/login?action=unauthorized' )
+		});
 		return res.end();
 	}
 
 	if ( reqURL.pathname === '/oauth' ) {
-		console.log(req.url)
+		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();
@@ -157,33 +200,56 @@ const server = http.createServer((req, res) => {
 				oauth.getUser(access_token),
 				oauth.getUserGuilds(access_token)
 			]).then( ([user, guilds]) => {
-				settingsData.set(state, {
-					state, access_token,
-					user: {
-						id: user.id,
-						username: user.username,
-						discriminator: user.discriminator,
-						avatar: 'https://cdn.discordapp.com/' + ( user.avatar ? 
-							`embed/avatars/${user.discriminator % 5}.png` : 
-							`avatars/${user.id}/${user.avatar}.` + 
-							( user.avatar.startsWith( 'a_' ) ? 'gif' : 'png' ) ) + '?size=128',
-						locale: user.locale
-					},
-					guilds: new Map(guilds.filter( guild => {
-						return ( guild.owner || hasPerm(guild.permissions, 'MANAGE_GUILD') );
-					} ).map( guild => [guild.id, {
+				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 ),
+						+ ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) + '?size=128' : null ),
 						permissions: guild.permissions
-					}] ))
-				});
-				res.writeHead(302, {
-					Location: ( guild ? '/guild/' + guild : '/' )
-				});
-				return res.end();
+					};
+				} );
+				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'});
@@ -197,27 +263,47 @@ const server = http.createServer((req, res) => {
 	}
 
 	if ( !settingsData.has(state) ) {
-		res.writeHead(302, {Location: '/login?action=unauthorized'});
+		res.writeHead(302, {
+			Location: ( reqURL.pathname === '/' ? '/login' : '/login?action=unauthorized' )
+		});
 		return res.end();
 	}
 	var settings = settingsData.get(state);
 
 	if ( reqURL.pathname === '/refresh' ) {
 		return oauth.getUserGuilds(settings.access_token).then( guilds => {
-			settings.guilds = new Map(guilds.filter( guild => {
+			guilds = guilds.filter( guild => {
 				return ( guild.owner || hasPerm(guild.permissions, 'MANAGE_GUILD') );
-			} ).map( guild => [guild.id, {
-				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
-			}] ));
-			res.writeHead(302, {
-				Location: ( reqURL.searchParams.get('return') || '/' )
-			});
-			return res.end();
+			} ).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'});
@@ -225,38 +311,66 @@ const server = http.createServer((req, res) => {
 		} );
 	}
 
-	var $ = cheerio.load(file);
-	$('.guild#refresh a').attr('href', '/refresh?return=' + reqURL.pathname);
+	var $ = cheerio.load(files.index);
+	$('replace#notice').replaceWith('');
+	$('.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#logout img').attr('src', settings.user.avatar);
+	$('.guild#refresh a').attr('href', '/refresh?return=' + reqURL.pathname);
 	let guilds = '';
-	settings.guilds.forEach( guild => {
-		guilds += `<div class="guild" id="${guild.id}">
-			<div class="bar"></div>
-			<a href="/guild/${guild.id}" alt="${guild.name}">` + ( guild.icon ? 
-				`<img class="avatar" src="${guild.icon}" alt="${guild.acronym}" width="48" height="48">`
-				 : `<div class="avatar noicon">${guild.acronym}</div>` ) + 
-			`</a>
-		</div>`
-	} );
+	if ( settings.guilds.isMember.size ) {
+		guilds += `<div class="guild">
+			<div class="separator"></div>
+		</div>`;
+		settings.guilds.isMember.forEach( guild => {
+			guilds += `<div class="guild" id="${guild.id}">
+				<div class="bar"></div>
+				<a href="/guild/${guild.id}" alt="${guild.name}">` + ( guild.icon ? 
+					`<img class="avatar" src="${guild.icon}" alt="${guild.acronym}" width="48" height="48">`
+					: `<div class="avatar noicon">${guild.acronym}</div>` ) + 
+				`</a>
+			</div>`;
+		} );
+	}
+	if ( settings.guilds.notMember.size ) {
+		guilds += `<div class="guild">
+			<div class="separator"></div>
+		</div>`;
+		settings.guilds.notMember.forEach( guild => {
+			guilds += `<div class="guild" id="${guild.id}">
+				<div class="bar"></div>
+				<a href="/guild/${guild.id}" alt="${guild.name}">` + ( guild.icon ? 
+					`<img class="avatar" src="${guild.icon}" alt="${guild.acronym}" width="48" height="48">`
+					: `<div class="avatar noicon">${guild.acronym}</div>` ) + 
+				`</a>
+			</div>`;
+		} );
+	}
 	$('replace#guilds').replaceWith(guilds);
 
 	if ( reqURL.pathname.startsWith( '/guild/' ) ) {
 		let id = reqURL.pathname.replace( '/guild/', '' );
-		if ( settings.guilds.has(id) ) {
+		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.get(id);
+			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 href="${url}">${guild.permissions} Keks</a>`);
-			res.setHeader('Set-Cookie', [`guild="${id}"; HttpOnly; Path=/oauth`]);
+			$('replace#text').replaceWith(`<a href="${url}">${guild.permissions}</a>`);
 		}
 		$('replace#text').replaceWith('You are missing the <code>MANAGE_GUILD</code> permission.');
 	}

+ 210 - 0
dashboard/login.html

@@ -0,0 +1,210 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+	<meta charset="UTF-8">
+	<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: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>
+</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>
+	</div>
+	<div class="sidebar">
+		<div class="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">
+					<div class="avatar svg-avatar">
+						<svg width="24" height="24" viewBox="0 0 24 24">
+							<path fill="currentColor" d="M20 11.1111H12.8889V4H11.1111V11.1111H4V12.8889H11.1111V20H12.8889V12.8889H20V11.1111Z"></path>
+						</svg>
+					</div>
+				</a>
+			</div>
+		</div>
+	</div>
+</body>
+</html>

+ 42 - 8
main.js

@@ -68,18 +68,52 @@ manager.spawn().then( shards => {
 
 if ( process.env.dashboard ) {
 	const dashboard = child_process.fork('./dashboard/index.js', ( isDebug ? ['debug'] : [] ));
-	dashboard.on( 'exit', (code) => {
-		if ( code ) console.log( '- [Dashboard]: Process exited!', code );
+
+	dashboard.on( 'message', message => {
+		if ( message.id ) {
+			var data = {
+				type: message.data.type,
+				response: null,
+				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 );
+					}, 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 );
+						} );
+					}, error => {
+						data.error = error;
+					} ).finally( () => {
+						return dashboard.send( {id: message.id, data} );
+					} );
+					break;
+				default:
+					console.log( '- [Dashboard]: Unknown message received!', message.data );
+					data.error = 'Unknown message type: ' + message.data.type;
+					return dashboard.send( {id: message.id, data} );
+			}
+		}
+		console.log( '- [Dashboard]: Message received!', message );
 	} );
+
 	dashboard.on( 'error', error => {
 		console.log( '- [Dashboard]: Error received!', error );
 	} );
-	dashboard.on( 'message', message => {
-		if ( message.id ) {
-			message.data = message.data;
-			dashboard.send( message );
-		}
-		console.log( '- Dashboard: Message received!', message );
+
+	dashboard.on( 'exit', (code) => {
+		if ( code ) console.log( '- [Dashboard]: Process exited!', code );
 	} );
 }