ソースを参照

split dashboard code

Markus-Rost 4 年 前
コミット
d74f23c44f
9 ファイル変更508 行追加412 行削除
  1. 103 0
      dashboard/guilds.js
  2. 14 8
      dashboard/index.html
  3. 37 392
      dashboard/index.js
  4. 1 3
      dashboard/login.html
  5. 213 0
      dashboard/oauth.js
  6. 3 0
      dashboard/src/channel.svg
  7. 23 4
      dashboard/src/index.css
  8. 113 0
      dashboard/util.js
  9. 1 5
      main.js

+ 103 - 0
dashboard/guilds.js

@@ -0,0 +1,103 @@
+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/index.html');
+
+/**
+ * Let a user change 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 arguments = reqURL.pathname.split('/');
+	var settings = settingsData.get(state);
+	var $ = cheerio.load(file);
+	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 ( 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 );
+	return res.end();
+}
+
+module.exports = dashboard_guilds;

+ 14 - 8
dashboard/index.html

@@ -56,17 +56,23 @@
 			</div>
 		</div>
 		<div class="channellist">
-			<a class="channel">
-				<svg width="20" height="20" viewBox="0 0 24 24">
-					<path fill="currentColor" 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>
+			<a class="channel channel-header" id="settings">
+				<svg width="24" height="24" viewBox="0 0 24 24">
+					<path fill="currentColor" 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>
-				<div>Keks</div>
+				<div>Settings</div>
 			</a>
-			<a class="channel">
-				<svg width="20" height="20" viewBox="0 0 24 24">
-					<path fill="currentColor" 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>
+			<a class="channel channel-header" id="verification">
+				<svg width="24" height="24" viewBox="0 0 24 24">
+					<path fill="currentColor" 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>
-				<div>Keks</div>
+				<div>Verifications</div>
+			</a>
+			<a class="channel channel-header" id="rcgcdb">
+				<svg width="24" height="24" viewBox="0 0 24 24">
+					<path fill="currentColor" 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>
+				<div>Recent Changes</div>
 			</a>
 		</div>
 	</div>

+ 37 - 392
dashboard/index.js

@@ -1,117 +1,37 @@
 const http = require('http');
-const crypto = require('crypto');
-const cheerio = require('cheerio');
-const {defaultPermissions} = require('../util/default.json');
-
-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 pages = require('./oauth.js');
+const dashboard = require('./guilds.js');
+const {db, settingsData} = require('./util.js');
 
 const fs = require('fs');
 const path = require('path');
-const files = {
-	index: fs.readFileSync('./dashboard/index.html'),
-	login: fs.readFileSync('./dashboard/login.html'),
-	src: 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 '.png':
-				contentType = 'image/png';
-				break;
-			case '.jpg':
-				contentType = 'image/jpg';
-				break;
-		}
-		return [`/src/${file}`, {
-			name: file, contentType,
-			path: `./dashboard/src/${file}`
-		}];
-	} ))
-}
-
-/**
- * @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 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 !== 'GET' ) {
@@ -128,8 +48,8 @@ const server = http.createServer((req, res) => {
 		return res.end();
 	}
 
-	if ( files.src.has(reqURL.pathname) ) {
-		let file = files.src.get(reqURL.pathname);
+	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);
 	}
@@ -147,56 +67,7 @@ const server = http.createServer((req, res) => {
 	} )?.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
-		} );
-		$('a#login').attr('href', url);
-		$('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' ) {
@@ -216,83 +87,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) ) {
@@ -301,126 +96,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();
 });
 
@@ -428,46 +113,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.

+ 1 - 3
dashboard/login.html

@@ -39,9 +39,7 @@
 		</div>
 		<div class="channellist">
 			<a class="channel" id="login">
-				<svg width="20" height="20" viewBox="0 0 24 24">
-					<path fill="currentColor" 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>
+				<img src="/src/channel.svg" width="20" height="20">
 				<div>Login</div>
 			</a>
 		</div>

+ 213 - 0
dashboard/oauth.js

@@ -0,0 +1,213 @@
+const crypto = require('crypto');
+const cheerio = require('cheerio');
+const {defaultPermissions} = require('../util/default.json');
+const {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 ) {
+		if ( settingsData.has(state) ) {
+			res.writeHead(302, {Location: '/'});
+			return res.end();
+		}
+		res.setHeader('Set-Cookie', [`wikibot="${state}"; Max-Age=0; HttpOnly`]);
+	}
+	var $ = cheerio.load(file);
+	$('.guild#invite a').attr('href', oauth.generateAuthUrl( {
+		scope: ['identify', 'guilds', 'bot'],
+		permissions: defaultPermissions, state
+	} ));
+	let responseCode = 200;
+	let notice = '';
+	if ( action === 'failed' ) {
+		responseCode = 400;
+		notice = createNotice($, {
+			title: 'Login failed!',
+			text: 'An error occurred while logging you in, please try again.'
+		});
+	}
+	if ( action === 'unauthorized' ) {
+		responseCode = 401;
+		notice = createNotice($, {
+			title: 'Not logged in!',
+			text: 'Please login before you can change any settings.'
+		});
+	}
+	if ( 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
+	} );
+	$('a#login').attr('href', url);
+	$('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();
+}
+
+/**
+ * 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 ( settingsData.has(state) ) {
+		res.writeHead(302, {Location: '/'});
+		return res.end();
+	}
+	if ( state !== searchParams.get('state') || !searchParams.get('code') ) {
+		res.writeHead(302, {Location: '/login?action=unauthorized'});
+		return res.end();
+	}
+	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' ) + '?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();
+	} );
+}
+
+/**
+ * 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' ) + '?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: returnLocation});
+			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();
+	} );
+}
+
+module.exports = {
+	login: dashboard_login,
+	oauth: dashboard_oauth,
+	refresh: dashboard_refresh
+};

+ 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" width="24" height="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>

+ 23 - 4
dashboard/src/index.css

@@ -183,7 +183,7 @@ a[alt]:hover:after {
 }
 .channel {
 	padding: 0 8px;
-	margin: 0 8px 2px;
+	margin: 0 8px 2px 12px;
 	height: 32px;
 	border-radius: 4px;
 	display: flex;
@@ -193,17 +193,36 @@ a[alt]:hover:after {
 .channel:hover {
 	background: rgba(79,84,92,0.16);
 }
-.channel svg {
+.channel img {
 	margin-right: 6px;
-	width: 20;
-	height: 20;
+	width: 20px;
+	height: 20px;
 }
 .channel div {
 	font-size: 16px;
 	line-height: 20px;
 	text-shadow: 0 0 1px;
 	white-space: nowrap;
+	overflow: hidden;
 }
 .channel:hover div {
 	color: #dcddde;
+}
+.channel.selected div {
+	color: #ffffff;
+}
+.channel-header {
+	margin-left: 8px;
+	height: 44px;
+}
+.channel-header svg {
+	margin-right: 6px;
+	width: 24px;
+	height: 24px;
+}
+.channel-header div {
+	font-size: 20px;
+	line-height: 24px;
+	font-weight: bold;
+	text-shadow: none;
 }

+ 113 - 0
dashboard/util.js

@@ -0,0 +1,113 @@
+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 {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>}
+ */
+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 {{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 )
+}
+
+module.exports = {db, settingsData, sendMsg, createNotice, hasPerm};

+ 1 - 5
main.js

@@ -177,9 +177,5 @@ if ( isDebug && process.argv[3]?.startsWith( '--timeout:' ) ) {
 		console.log( `\n- Running for ${timeout} seconds, closing process!\n` );
 		manager.shards.forEach( shard => shard.kill() );
 		if ( typeof server !== 'undefined' ) server.kill();
-		setTimeout( () => {
-			console.log( `\n- Process stayed open, exiting maually!\n` );
-			process.exit(0);
-		}, timeout  * 1000 ).unref();
-	}, timeout  * 1000 ).unref();
+	}, timeout * 1000 ).unref();
 }