Browse Source

add basic forms

Markus-Rost 4 years ago
parent
commit
12acf8aa38

+ 56 - 13
dashboard/guilds.js

@@ -1,6 +1,12 @@
 const cheerio = require('cheerio');
 const cheerio = require('cheerio');
 const {defaultPermissions} = require('../util/default.json');
 const {defaultPermissions} = require('../util/default.json');
-const {db, settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
+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 DiscordOauth2 = require('discord-oauth2');
 const oauth = new DiscordOauth2( {
 const oauth = new DiscordOauth2( {
@@ -12,17 +18,46 @@ const oauth = new DiscordOauth2( {
 const file = require('fs').readFileSync('./dashboard/index.html');
 const file = require('fs').readFileSync('./dashboard/index.html');
 
 
 /**
 /**
- * Let a user change settings
+ * Let a user view settings
  * @param {import('http').ServerResponse} res - The server response
  * @param {import('http').ServerResponse} res - The server response
  * @param {String} state - The user state
  * @param {String} state - The user state
  * @param {URL} reqURL - The used url
  * @param {URL} reqURL - The used url
  */
  */
 function dashboard_guilds(res, state, reqURL) {
 function dashboard_guilds(res, state, reqURL) {
-	var arguments = reqURL.pathname.split('/');
+	var args = reqURL.pathname.split('/');
 	var settings = settingsData.get(state);
 	var settings = settingsData.get(state);
 	var $ = cheerio.load(file);
 	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 ) {
 	if ( process.env.READONLY ) {
 		createNotice($, {
 		createNotice($, {
+			type: 'info',
 			title: 'Read-only database!',
 			title: 'Read-only database!',
 			text: 'You can currently only view your settings but not change them.'
 			text: 'You can currently only view your settings but not change them.'
 		}).prependTo('#text');
 		}).prependTo('#text');
@@ -65,32 +100,37 @@ function dashboard_guilds(res, state, reqURL) {
 		} );
 		} );
 	}
 	}
 
 
-	if ( arguments[1] === 'guild' ) {
-		let id = arguments[2];
+	if ( args[1] === 'guild' ) {
+		let id = args[2];
+		$(`.guild#${id}`).addClass('selected');
 		if ( settings.guilds.isMember.has(id) ) {
 		if ( settings.guilds.isMember.has(id) ) {
-			$(`.guild#${id}`).addClass('selected');
 			let guild = settings.guilds.isMember.get(id);
 			let guild = settings.guilds.isMember.get(id);
 			$('head title').text(`${guild.name} – ` + $('head title').text());
 			$('head title').text(`${guild.name} – ` + $('head title').text());
-			res.setHeader('Set-Cookie', [`guild="${id}"; HttpOnly; Path=/`]);
-			$('<a>').text(`${guild.permissions}`).appendTo('#text .description');
+			$('.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) ) {
 		else if ( settings.guilds.notMember.has(id) ) {
-			$(`.guild#${id}`).addClass('selected');
 			let guild = settings.guilds.notMember.get(id);
 			let guild = settings.guilds.notMember.get(id);
 			$('head title').text(`${guild.name} – ` + $('head title').text());
 			$('head title').text(`${guild.name} – ` + $('head title').text());
-			res.setHeader('Set-Cookie', [`guild="${id}"; HttpOnly; Path=/`]);
+			res.setHeader('Set-Cookie', [`guild="${guild.id}"; HttpOnly; Path=/`]);
 			let url = oauth.generateAuthUrl( {
 			let url = oauth.generateAuthUrl( {
 				scope: ['identify', 'guilds', 'bot'],
 				scope: ['identify', 'guilds', 'bot'],
 				permissions: defaultPermissions,
 				permissions: defaultPermissions,
-				guild_id: id, state
+				guildId: guild.id, state
 			} );
 			} );
-			$('<a>').attr('href', url).text(guild.permissions).appendTo('#text .description');
+			$('<a>').attr('href', url).text(guild.name).appendTo('#text .description');
 		}
 		}
 		else {
 		else {
+			$('head title').text('Unknown Server – ' + $('head title').text());
 			$('#text .description').text('You are missing the <code>MANAGE_GUILD</code> permission.');
 			$('#text .description').text('You are missing the <code>MANAGE_GUILD</code> permission.');
 		}
 		}
 	}
 	}
 	else {
 	else {
+		$('head title').text('Server Selector – ' + $('head title').text());
 		$('#channellist').empty();
 		$('#channellist').empty();
 		$('#text .description').text('This is a list of all servers you can change settings on. Please select a server:');
 		$('#text .description').text('This is a list of all servers you can change settings on. Please select a server:');
 		if ( settings.guilds.isMember.size ) {
 		if ( settings.guilds.isMember.size ) {
@@ -127,7 +167,10 @@ function dashboard_guilds(res, state, reqURL) {
 		}
 		}
 		if ( !settings.guilds.count ) {
 		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?');
 			$('#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', '/logout').append(
+			$('<a class="channel">').attr('href', oauth.generateAuthUrl( {
+				scope: ['identify', 'guilds'],
+				prompt: 'consent', state
+			} )).append(
 				$('<img>').attr('src', '/src/channel.svg'),
 				$('<img>').attr('src', '/src/channel.svg'),
 				$('<div>').text('Switch accounts')
 				$('<div>').text('Switch accounts')
 			).appendTo('#channellist');
 			).appendTo('#channellist');

+ 30 - 32
dashboard/index.html

@@ -11,23 +11,41 @@
 	<meta property="og:site_name" content="Wiki-Bot Settings">
 	<meta property="og:site_name" content="Wiki-Bot Settings">
 	<meta itemprop="author" content="MarkusRost">
 	<meta itemprop="author" content="MarkusRost">
 	<link rel="stylesheet" type="text/css" href="/src/index.css">
 	<link rel="stylesheet" type="text/css" href="/src/index.css">
+	<!--<script src="/src/index.js"></script>-->
 </head>
 </head>
 <body>
 <body>
 	<div id="text">
 	<div id="text">
 		<div class="description"></div>
 		<div class="description"></div>
 	</div>
 	</div>
-	<div id="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/icons/464084451165732868/c6a8b9fc902b09545de8194a911e6045.png?size=128" alt="Support server">
-			<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 class="scrollbar" id="sidebar">
 	<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 id="guildlist">
 			<div class="guild" id="invite">
 			<div class="guild" id="invite">
 				<div class="bar"></div>
 				<div class="bar"></div>
@@ -44,7 +62,7 @@
 			</div>
 			</div>
 			<div class="guild" id="refresh">
 			<div class="guild" id="refresh">
 				<div class="bar"></div>
 				<div class="bar"></div>
-				<a href="/refresh" alt="Refresh guild list">
+				<a href="/refresh" alt="Refresh server list">
 					<div class="avatar svg-avatar">
 					<div class="avatar svg-avatar">
 						<svg width="20" height="23" viewBox="0 1.35 17.3 20" style="overflow: visible;">
 						<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>
 							<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>
@@ -53,26 +71,6 @@
 				</a>
 				</a>
 			</div>
 			</div>
 		</div>
 		</div>
-		<div class="scrollbar" id="channellist">
-			<a class="channel channel-header" id="settings">
-				<svg 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>Settings</div>
-			</a>
-			<a class="channel channel-header" id="verification">
-				<svg 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>Verifications</div>
-			</a>
-			<a class="channel channel-header" id="rcgcdb">
-				<svg 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>
 	</div>
 </body>
 </body>
 </html>
 </html>

+ 43 - 5
dashboard/index.js

@@ -1,8 +1,15 @@
 const http = require('http');
 const http = require('http');
+const {parse} = require('querystring');
 const pages = require('./oauth.js');
 const pages = require('./oauth.js');
 const dashboard = require('./guilds.js');
 const dashboard = require('./guilds.js');
 const {db, settingsData} = require('./util.js');
 const {db, settingsData} = require('./util.js');
 
 
+const posts = {
+	settings: require('./settings.js').post,
+	verification: require('./verification.js').post,
+	rcscript: require('./rcscript.js').post
+};
+
 const fs = require('fs');
 const fs = require('fs');
 const path = require('path');
 const path = require('path');
 const files = new Map(fs.readdirSync( './dashboard/src' ).map( file => {
 const files = new Map(fs.readdirSync( './dashboard/src' ).map( file => {
@@ -34,9 +41,37 @@ const files = new Map(fs.readdirSync( './dashboard/src' ).map( file => {
 } ));
 } ));
 
 
 const server = http.createServer((req, res) => {
 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' ) {
 	if ( req.method !== 'GET' ) {
 		let body = '<img width="400" src="https://http.cat/418"><br><strong>' + http.STATUS_CODES[418] + '</strong>';
 		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 );
 		res.write( body );
 		return res.end();
 		return res.end();
 	}
 	}
@@ -59,12 +94,12 @@ const server = http.createServer((req, res) => {
 
 
 	var lastGuild = req.headers?.cookie?.split('; ')?.filter( cookie => {
 	var lastGuild = req.headers?.cookie?.split('; ')?.filter( cookie => {
 		return cookie.split('=')[0] === 'guild';
 		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 => {
 	var state = req.headers.cookie?.split('; ')?.filter( cookie => {
 		return cookie.split('=')[0] === 'wikibot';
 		return cookie.split('=')[0] === 'wikibot';
-	} )?.map( cookie => cookie.replace( /^wikibot="(\w+(?:-\d+)?)"$/, '$1' ) )?.join();
+	} )?.map( cookie => cookie.replace( /^wikibot="(\w*(?:-\d+)?)"$/, '$1' ) )?.join();
 
 
 	if ( reqURL.pathname === '/login' ) {
 	if ( reqURL.pathname === '/login' ) {
 		return pages.login(res, state, reqURL.searchParams.get('action'));
 		return pages.login(res, state, reqURL.searchParams.get('action'));
@@ -74,7 +109,10 @@ const server = http.createServer((req, res) => {
 		settingsData.delete(state);
 		settingsData.delete(state);
 		res.writeHead(302, {
 		res.writeHead(302, {
 			Location: '/login?action=logout',
 			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();
 		return res.end();
 	}
 	}

+ 33 - 21
dashboard/login.html

@@ -10,20 +10,44 @@
 	<meta property="og:title" content="Login – Wiki-Bot Settings">
 	<meta property="og:title" content="Login – Wiki-Bot Settings">
 	<meta property="og:site_name" content="Wiki-Bot Settings">
 	<meta property="og:site_name" content="Wiki-Bot Settings">
 	<meta itemprop="author" content="MarkusRost">
 	<meta itemprop="author" content="MarkusRost">
-	<link rel="stylesheet" type="text/css" href="/src/index.css">
+	<link rel="stylesheet" type="text/css" href="src/index.css">
 </head>
 </head>
 <body>
 <body>
 	<div id="text">
 	<div id="text">
-		<div class="description"></div>
-	</div>
-	<div id="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/icons/464084451165732868/c6a8b9fc902b09545de8194a911e6045.png?size=128" alt="Support server">
-			<span>Support server</span>
-		</a>
+		<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>
 	<div class="scrollbar" id="sidebar">
 	<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 id="guildlist">
 			<div class="guild" id="invite">
 			<div class="guild" id="invite">
 				<div class="bar"></div>
 				<div class="bar"></div>
@@ -36,18 +60,6 @@
 				</a>
 				</a>
 			</div>
 			</div>
 		</div>
 		</div>
-		<div class="scrollbar" id="channellist">
-			<a class="channel channel-header" id="login">
-				<svg 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>Login</div>
-			</a>
-			<a class="channel" id="invite-wikibot">
-				<img src="/src/channel.svg" width="20" height="20">
-				<div>Invite Wiki-Bot</div>
-			</a>
-		</div>
 	</div>
 	</div>
 </body>
 </body>
 </html>
 </html>

+ 90 - 57
dashboard/oauth.js

@@ -1,7 +1,7 @@
 const crypto = require('crypto');
 const crypto = require('crypto');
 const cheerio = require('cheerio');
 const cheerio = require('cheerio');
 const {defaultPermissions} = require('../util/default.json');
 const {defaultPermissions} = require('../util/default.json');
-const {settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
+const {db, settingsData, sendMsg, createNotice, hasPerm} = require('./util.js');
 
 
 const DiscordOauth2 = require('discord-oauth2');
 const DiscordOauth2 = require('discord-oauth2');
 const oauth = new DiscordOauth2( {
 const oauth = new DiscordOauth2( {
@@ -19,54 +19,66 @@ const file = require('fs').readFileSync('./dashboard/login.html');
  * @param {String} [action] - The action the user made
  * @param {String} [action] - The action the user made
  */
  */
 function dashboard_login(res, state, action) {
 function dashboard_login(res, state, action) {
-	if ( state ) {
-		if ( settingsData.has(state) ) {
+	if ( state && settingsData.has(state) ) {
+		if ( !action ) {
 			res.writeHead(302, {Location: '/'});
 			res.writeHead(302, {Location: '/'});
 			return res.end();
 			return res.end();
 		}
 		}
-		res.setHeader('Set-Cookie', [`wikibot="${state}"; Max-Age=0; HttpOnly`]);
+		settingsData.delete(state);
 	}
 	}
 	var $ = cheerio.load(file);
 	var $ = cheerio.load(file);
 	let invite = oauth.generateAuthUrl( {
 	let invite = oauth.generateAuthUrl( {
 		scope: ['identify', 'guilds', 'bot'],
 		scope: ['identify', 'guilds', 'bot'],
 		permissions: defaultPermissions, state
 		permissions: defaultPermissions, state
 	} );
 	} );
-	$('.guild#invite a').attr('href', invite);
-	$('.channel#invite-wikibot').attr('href', invite);
+	$('.guild#invite a, .channel#invite-wikibot').attr('href', invite);
 	let responseCode = 200;
 	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' ) {
 	if ( action === 'failed' ) {
 		responseCode = 400;
 		responseCode = 400;
 		createNotice($, {
 		createNotice($, {
+			type: 'error',
 			title: 'Login failed!',
 			title: 'Login failed!',
 			text: 'An error occurred while logging you in, please try again.'
 			text: 'An error occurred while logging you in, please try again.'
 		}).prependTo('#text');
 		}).prependTo('#text');
 	}
 	}
-	if ( action === 'unauthorized' ) {
-		responseCode = 401;
-		createNotice($, {
-			title: 'Not logged in!',
-			text: 'Please login before you can change any settings.'
-		}).prependTo('#text');
-	}
 	if ( action === 'logout' ) {
 	if ( action === 'logout' ) {
+		prompt = 'consent';
 		createNotice($, {
 		createNotice($, {
+			type: 'success',
 			title: 'Successfully logged out!',
 			title: 'Successfully logged out!',
 			text: 'You have been successfully logged out. To change any settings you need to login again.'
 			text: 'You have been successfully logged out. To change any settings you need to login again.'
 		}).prependTo('#text');
 		}).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");
 	state = crypto.randomBytes(16).toString("hex");
 	while ( settingsData.has(state) ) {
 	while ( settingsData.has(state) ) {
 		state = crypto.randomBytes(16).toString("hex");
 		state = crypto.randomBytes(16).toString("hex");
 	}
 	}
 	let url = oauth.generateAuthUrl( {
 	let url = oauth.generateAuthUrl( {
 		scope: ['identify', 'guilds'],
 		scope: ['identify', 'guilds'],
-		prompt: 'none', state
+		prompt, state
 	} );
 	} );
-	$('.channel#login').attr('href', url);
-	$('<a>').attr('href', url).text('Login').appendTo('#text .description');
+	$('.channel#login, #login-button').attr('href', url);
 	let body = $.html();
 	let body = $.html();
 	res.writeHead(responseCode, {
 	res.writeHead(responseCode, {
-		'Set-Cookie': [`wikibot="${state}"; HttpOnly`],
+		'Set-Cookie': [
+			...( res.getHeader('Set-Cookie') || [] ),
+			`wikibot="${state}"; HttpOnly; Path=/`
+		],
 		'Content-Length': body.length
 		'Content-Length': body.length
 	});
 	});
 	res.write( body );
 	res.write( body );
@@ -81,14 +93,11 @@ function dashboard_login(res, state, action) {
  * @param {String} [lastGuild] - The guild to return to
  * @param {String} [lastGuild] - The guild to return to
  */
  */
 function dashboard_oauth(res, state, searchParams, lastGuild) {
 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') ) {
 	if ( state !== searchParams.get('state') || !searchParams.get('code') ) {
-		res.writeHead(302, {Location: '/login?action=unauthorized'});
+		res.writeHead(302, {Location: '/login?action=failed'});
 		return res.end();
 		return res.end();
 	}
 	}
+	settingsData.delete(state);
 	return oauth.tokenRequest( {
 	return oauth.tokenRequest( {
 		scope: ['identify', 'guilds'],
 		scope: ['identify', 'guilds'],
 		code: searchParams.get('code'),
 		code: searchParams.get('code'),
@@ -107,46 +116,69 @@ function dashboard_oauth(res, state, searchParams, lastGuild) {
 					acronym: guild.name.replace( /'s /g, ' ' ).replace( /\w+/g, e => e[0] ).replace( /\s/g, '' ),
 					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}.`
 					icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
 					+ ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) : null ),
 					+ ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) : null ),
-					permissions: guild.permissions
+					userPermissions: guild.permissions
 				};
 				};
 			} );
 			} );
+			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()
+				}
+			};
 			sendMsg( {
 			sendMsg( {
-				type: 'isMemberAll',
+				type: 'getGuilds',
+				member: user.id,
 				guilds: guilds.map( guild => guild.id )
 				guilds: guilds.map( guild => guild.id )
 			} ).then( response => {
 			} ).then( response => {
-				let isMember = new Map();
-				let notMember = new Map();
 				response.forEach( (guild, i) => {
 				response.forEach( (guild, i) => {
-					if ( guild ) isMember.set(guilds[i].id, guilds[i]);
-					else notMember.set(guilds[i].id, guilds[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(`${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: {count: guilds.length, isMember, notMember}
-				});
+				settingsData.set(settings.state, settings);
 				res.writeHead(302, {
 				res.writeHead(302, {
 					Location: ( lastGuild ? '/guild/' + lastGuild : '/' ),
 					Location: ( lastGuild ? '/guild/' + lastGuild : '/' ),
-					'Set-Cookie': [
-						`wikibot="${state}"; Max-Age=0; HttpOnly`,
-						`wikibot="${state}-${user.id}"; HttpOnly`
-					]
+					'Set-Cookie': [`wikibot="${settings.state}"; HttpOnly; Path=/`]
 				});
 				});
 				return res.end();
 				return res.end();
 			}, error => {
 			}, error => {
-				console.log( '- Dashboard: Error while checking the guilds:', error );
-				res.writeHead(302, {Location: '/login?action=failed'});
-				return res.end();
+				console.log( '- Dashboard: Error while getting the guilds:', error );
+				db.all( 'SELECT guild FROM discord WHERE guild IN (' + guilds.map( guild => '?' ).join(', ') + ') AND channel IS NULL', guilds.map( guild => guild.id ), (dberror, rows) => {
+					if ( dberror ) {
+						console.log( '- Error while checking for settings: ' + dberror );
+						res.writeHead(302, {Location: '/login?action=failed'});
+						return res.end();
+					}
+					guilds.forEach( guild => {
+						if ( rows.some( row => row.guild === guild.id ) ) {
+							settings.guilds.isMember.set(guild.id, Object.assign(guild, {
+								botPermissions: 0,
+								channels: []
+							}));
+						}
+						else settings.guilds.notMember.set(guild.id, guild);
+					} );
+					settingsData.set(settings.state, settings);
+					res.writeHead(302, {
+						Location: ( lastGuild ? '/guild/' + lastGuild : '/' ),
+						'Set-Cookie': [`wikibot="${settings.state}"; HttpOnly; Path=/`]
+					});
+					return res.end();
+				} );
 			} );
 			} );
 		}, error => {
 		}, error => {
 			console.log( '- Dashboard: Error while getting user and guilds: ' + error );
 			console.log( '- Dashboard: Error while getting user and guilds: ' + error );
@@ -178,30 +210,31 @@ function dashboard_refresh(res, state, returnLocation = '/') {
 				acronym: guild.name.replace( /'s /g, ' ' ).replace( /\w+/g, e => e[0] ).replace( /\s/g, '' ),
 				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}.`
 				icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
 				+ ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) : null ),
 				+ ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) : null ),
-				permissions: guild.permissions
+				userPermissions: guild.permissions
 			};
 			};
 		} );
 		} );
 		sendMsg( {
 		sendMsg( {
-			type: 'isMemberAll',
+			type: 'getGuilds',
+			member: settings.user.id,
 			guilds: guilds.map( guild => guild.id )
 			guilds: guilds.map( guild => guild.id )
 		} ).then( response => {
 		} ).then( response => {
 			let isMember = new Map();
 			let isMember = new Map();
 			let notMember = new Map();
 			let notMember = new Map();
 			response.forEach( (guild, i) => {
 			response.forEach( (guild, i) => {
-				if ( guild ) isMember.set(guilds[i].id, guilds[i]);
+				if ( guild ) isMember.set(guilds[i].id, Object.assign(guilds[i], guild));
 				else notMember.set(guilds[i].id, guilds[i]);
 				else notMember.set(guilds[i].id, guilds[i]);
 			} );
 			} );
 			settings.guilds = {count: guilds.length, isMember, notMember};
 			settings.guilds = {count: guilds.length, isMember, notMember};
-			res.writeHead(302, {Location: returnLocation});
+			res.writeHead(302, {Location: returnLocation + '?refresh=success'});
 			return res.end();
 			return res.end();
 		}, error => {
 		}, error => {
-			console.log( '- Dashboard: Error while checking refreshed guilds:', error );
-			res.writeHead(302, {Location: '/login?action=failed'});
+			console.log( '- Dashboard: Error while getting the refreshed guilds:', error );
+			res.writeHead(302, {Location: returnLocation + '?refresh=failed'});
 			return res.end();
 			return res.end();
 		} );
 		} );
 	}, error => {
 	}, error => {
 		console.log( '- Dashboard: Error while refreshing guilds: ' + error );
 		console.log( '- Dashboard: Error while refreshing guilds: ' + error );
-		res.writeHead(302, {Location: '/login?action=failed'});
+		res.writeHead(302, {Location: returnLocation + '?refresh=failed'});
 		return res.end();
 		return res.end();
 	} );
 	} );
 }
 }

+ 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 * 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
+};

+ 56 - 12
dashboard/src/index.css

@@ -46,8 +46,19 @@ a:hover .description {
 	text-align: center;
 	text-align: center;
 	margin: 0 auto 1em;
 	margin: 0 auto 1em;
 	width: fit-content;
 	width: fit-content;
-	background: #200;
-	border: 2px solid #500;
+	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 {
 .server-selector {
 	display: flex;
 	display: flex;
@@ -88,12 +99,13 @@ a:hover .description {
 	display: flex;
 	display: flex;
 	align-items: center;
 	align-items: center;
 	justify-content: space-between;
 	justify-content: space-between;
-	height: 47px;
-	padding: 0 0 0 8px;
+	height: 48px;
 	font-size: 16px;
 	font-size: 16px;
 	line-height: 20px;
 	line-height: 20px;
 	font-weight: bold;
 	font-weight: bold;
-	border-bottom: 1px solid rgba(4,4,5,0.2);
+	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 {
 :target::before {
 	content: "";
 	content: "";
@@ -104,6 +116,7 @@ a:hover .description {
 #navbar a {
 #navbar a {
 	display: flex;
 	display: flex;
 	align-items: center;
 	align-items: center;
+	justify-content: center;
 	height: 100%;
 	height: 100%;
 	padding-left: 10px;
 	padding-left: 10px;
 }
 }
@@ -143,7 +156,6 @@ a:hover .description {
 	flex: 1 1 auto;
 	flex: 1 1 auto;
 	min-height: calc(100% - 24px);
 	min-height: calc(100% - 24px);
 	width: 72px;
 	width: 72px;
-	z-index: 1;
 }
 }
 .guild {
 .guild {
 	margin: 0 0 8px;
 	margin: 0 0 8px;
@@ -226,7 +238,6 @@ a:hover .description {
 	top: 48px;
 	top: 48px;
 	left: 72px;
 	left: 72px;
 	bottom: 0;
 	bottom: 0;
-	z-index: 0;
 }
 }
 .channel {
 .channel {
 	padding: 0 8px;
 	padding: 0 8px;
@@ -237,9 +248,6 @@ a:hover .description {
 	align-items: center;
 	align-items: center;
 	color: #8e9297;
 	color: #8e9297;
 }
 }
-.channel:hover {
-	background: rgba(79,84,92,0.16);
-}
 .channel img {
 .channel img {
 	margin-right: 6px;
 	margin-right: 6px;
 	width: 20px;
 	width: 20px;
@@ -252,6 +260,12 @@ a:hover .description {
 	white-space: nowrap;
 	white-space: nowrap;
 	overflow: hidden;
 	overflow: hidden;
 }
 }
+.channel:hover {
+	background: rgba(79,84,92,0.16);
+}
+.channel.selected {
+	background: rgba(79,84,92,0.32);
+}
 .channel:hover div {
 .channel:hover div {
 	color: #dcddde;
 	color: #dcddde;
 }
 }
@@ -262,8 +276,7 @@ a:hover .description {
 	margin-left: 8px;
 	margin-left: 8px;
 	height: 44px;
 	height: 44px;
 }
 }
-.channel-header svg {
-	margin-right: 6px;
+.channel-header img {
 	width: 24px;
 	width: 24px;
 	height: 24px;
 	height: 24px;
 }
 }
@@ -272,4 +285,35 @@ a:hover .description {
 	line-height: 24px;
 	line-height: 24px;
 	font-weight: bold;
 	font-weight: bold;
 	text-shadow: none;
 	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>

+ 21 - 11
dashboard/util.js

@@ -34,7 +34,9 @@ const db = new sqlite3.Database( './wikibot.db', mode, dberror => {
  * @property {String} name
  * @property {String} name
  * @property {String} acronym
  * @property {String} acronym
  * @property {String} [icon]
  * @property {String} [icon]
- * @property {String} permissions
+ * @property {String} userPermissions
+ * @property {String} [botPermissions]
+ * @property {{id: String, name: String, permissions: Number}[]} [channels]
  */
  */
 
 
 /**
 /**
@@ -77,38 +79,46 @@ function sendMsg(message) {
  * @param {Object} notice - The notices to create
  * @param {Object} notice - The notices to create
  * @param {String} notice.title - The title of the notice
  * @param {String} notice.title - The title of the notice
  * @param {String} notice.text - The text of the notice
  * @param {String} notice.text - The text of the notice
+ * @param {String} [notice.type] - The type of the notice
  * @returns {Cheerio}
  * @returns {Cheerio}
  */
  */
 function createNotice($, notice) {
 function createNotice($, notice) {
+	var type = ( notice.type ? `notice-${notice.type}` : '' );
 	return $('<div class="notice">').append(
 	return $('<div class="notice">').append(
 		$('<b>').text(notice.title),
 		$('<b>').text(notice.title),
 		$('<div>').text(notice.text)
 		$('<div>').text(notice.text)
-	);
+	).addClass(type);
 }
 }
 
 
 const permissions = {
 const permissions = {
 	ADMINISTRATOR: 1 << 3,
 	ADMINISTRATOR: 1 << 3,
 	MANAGE_CHANNELS: 1 << 4,
 	MANAGE_CHANNELS: 1 << 4,
 	MANAGE_GUILD: 1 << 5,
 	MANAGE_GUILD: 1 << 5,
+	ADD_REACTIONS: 1 << 6,
+	VIEW_CHANNEL: 1 << 10,
+	SEND_MESSAGES: 1 << 11,
 	MANAGE_MESSAGES: 1 << 13,
 	MANAGE_MESSAGES: 1 << 13,
-	MENTION_EVERYONE: 1 << 17,
+	EMBED_LINKS: 1 << 14,
+	ATTACH_FILES: 1 << 15,
+	READ_MESSAGE_HISTORY: 1 << 16,
+	USE_EXTERNAL_EMOJIS: 1 << 18,
 	MANAGE_NICKNAMES: 1 << 27,
 	MANAGE_NICKNAMES: 1 << 27,
 	MANAGE_ROLES: 1 << 28,
 	MANAGE_ROLES: 1 << 28,
-	MANAGE_WEBHOOKS: 1 << 29,
-	MANAGE_EMOJIS: 1 << 30
+	MANAGE_WEBHOOKS: 1 << 29
 }
 }
 
 
 /**
 /**
  * Check if a permission is included in the BitField
  * Check if a permission is included in the BitField
  * @param {String|Number} all - BitField of multiple permissions
  * @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
+ * @param {String[]} permission - Name of the permission to check for
  * @returns {Boolean}
  * @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 )
+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};
 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 * 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
+};

+ 34 - 9
main.js

@@ -80,22 +80,47 @@ if ( process.env.dashboard ) {
 				error: null
 				error: null
 			};
 			};
 			switch ( message.data.type ) {
 			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.type === 'text' );
+										} ).sort( (a, b) => {
+											return a.rawPosition - b.rawPosition;
+										} ).map( channel => {
+											return {
+												id: channel.id,
+												name: channel.name,
+												permissions: member.permissionsIn(channel).bitfield
+											};
+										} )
+									};
+								} )
+							}
+						} )
+					)`).then( results => {
+						data.response = message.data.guilds.map( (guild, i) => {
+							return results.find( result => result[i] )?.[i];
+						} );
 					}, error => {
 					}, error => {
 						data.error = error;
 						data.error = error;
 					} ).finally( () => {
 					} ).finally( () => {
 						return dashboard.send( {id: message.id, data} );
 						return dashboard.send( {id: message.id, data} );
 					} );
 					} );
 					break;
 					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 => {
 					}, error => {
 						data.error = error;
 						data.error = error;
 					} ).finally( () => {
 					} ).finally( () => {

+ 3 - 3
package-lock.json

@@ -387,9 +387,9 @@
       "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
       "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
     },
     },
     "discord-oauth2": {
     "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": {
     "discord.js": {
       "version": "12.3.1",
       "version": "12.3.1",

+ 1 - 1
package.json

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