瀏覽代碼

add basic forms

Markus-Rost 4 年之前
父節點
當前提交
12acf8aa38
共有 15 個文件被更改,包括 711 次插入164 次删除
  1. 56 13
      dashboard/guilds.js
  2. 30 32
      dashboard/index.html
  3. 43 5
      dashboard/index.js
  4. 33 21
      dashboard/login.html
  5. 90 57
      dashboard/oauth.js
  6. 36 0
      dashboard/rcscript.js
  7. 181 0
      dashboard/settings.js
  8. 56 12
      dashboard/src/index.css
  9. 88 0
      dashboard/src/index.js
  10. 3 0
      dashboard/src/settings.svg
  11. 21 11
      dashboard/util.js
  12. 36 0
      dashboard/verification.js
  13. 34 9
      main.js
  14. 3 3
      package-lock.json
  15. 1 1
      package.json

+ 56 - 13
dashboard/guilds.js

@@ -1,6 +1,12 @@
 const cheerio = require('cheerio');
 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 oauth = new DiscordOauth2( {
@@ -12,17 +18,46 @@ const oauth = new DiscordOauth2( {
 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 {String} state - The user state
  * @param {URL} reqURL - The used url
  */
 function dashboard_guilds(res, state, reqURL) {
-	var arguments = reqURL.pathname.split('/');
+	var args = reqURL.pathname.split('/');
 	var settings = settingsData.get(state);
 	var $ = cheerio.load(file);
+	if ( reqURL.searchParams.get('refresh') === 'success' ) {
+		createNotice($, {
+			type: 'success',
+			title: 'Refresh successful!',
+			text: 'Your server list has been successfully refeshed.'
+		}).prependTo('#text');
+	}
+	if ( reqURL.searchParams.get('refresh') === 'failed' ) {
+		createNotice($, {
+			type: 'error',
+			title: 'Refresh failed!',
+			text: 'You server list could not be refreshed, please try again.'
+		}).prependTo('#text');
+	}
+	if ( reqURL.searchParams.get('save') === 'success' ) {
+		createNotice($, {
+			type: 'success',
+			title: 'Settings saved!',
+			text: 'The settings have been updated successfully.'
+		}).prependTo('#text');
+	}
+	if ( reqURL.searchParams.get('save') === 'failed' ) {
+		createNotice($, {
+			type: 'error',
+			title: 'Save failed!',
+			text: 'The settings could not be saved, please try again.'
+		}).prependTo('#text');
+	}
 	if ( process.env.READONLY ) {
 		createNotice($, {
+			type: 'info',
 			title: 'Read-only database!',
 			text: 'You can currently only view your settings but not change them.'
 		}).prependTo('#text');
@@ -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) ) {
-			$(`.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=/`]);
-			$('<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) ) {
-			$(`.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=/`]);
+			res.setHeader('Set-Cookie', [`guild="${guild.id}"; HttpOnly; Path=/`]);
 			let url = oauth.generateAuthUrl( {
 				scope: ['identify', 'guilds', 'bot'],
 				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 {
+			$('head title').text('Unknown Server – ' + $('head title').text());
 			$('#text .description').text('You are missing the <code>MANAGE_GUILD</code> permission.');
 		}
 	}
 	else {
+		$('head title').text('Server Selector – ' + $('head title').text());
 		$('#channellist').empty();
 		$('#text .description').text('This is a list of all servers you can change settings on. Please select a server:');
 		if ( settings.guilds.isMember.size ) {
@@ -127,7 +167,10 @@ function dashboard_guilds(res, state, reqURL) {
 		}
 		if ( !settings.guilds.count ) {
 			$('#text .description').text('You currently don\'t have the MANAGE_SERVER permission on any servers, are you logged into the correct account?');
-			$('<a class="channel">').attr('href', '/logout').append(
+			$('<a class="channel">').attr('href', oauth.generateAuthUrl( {
+				scope: ['identify', 'guilds'],
+				prompt: 'consent', state
+			} )).append(
 				$('<img>').attr('src', '/src/channel.svg'),
 				$('<div>').text('Switch accounts')
 			).appendTo('#channellist');

+ 30 - 32
dashboard/index.html

@@ -11,23 +11,41 @@
 	<meta property="og:site_name" content="Wiki-Bot Settings">
 	<meta itemprop="author" content="MarkusRost">
 	<link rel="stylesheet" type="text/css" href="/src/index.css">
+	<!--<script src="/src/index.js"></script>-->
 </head>
 <body>
 	<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>
-		<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="channellist">
+			<a class="channel channel-header" id="settings">
+				<img src="/src/settings.svg" alt="Settings">
+				<div>Settings</div>
+			</a>
+			<a class="channel channel-header" id="verification">
+				<img src="/src/settings.svg" alt="Settings">
+				<div>Verifications</div>
+			</a>
+			<a class="channel channel-header" id="rcgcdb">
+				<img src="/src/settings.svg" alt="Settings">
+				<div>Recent Changes</div>
+			</a>
+		</div>
+		<div id="navbar">
+			<a id="selector" href="/" style="width: 230px;">
+				<img class="avatar" src="https://discord.com/assets/f8389ca1a741a115313bede9ac02e2c0.svg" alt="Discord">
+				<span>Server Selector</span>
+			</a>
+			<a id="support" href="https://discord.gg/v77RTk5" target="_blank" alt="Help with Wiki-Bot">
+				<img class="avatar" src="https://cdn.discordapp.com/icons/464084451165732868/c6a8b9fc902b09545de8194a911e6045.png?size=64" alt="Wiki-Bot">
+				<span>Support Server</span>
+			</a>
+			<a id="logout" href="/logout" alt="Logout">
+				<img class="avatar" src="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=64" alt="Logout">
+				<span>Wiki-Bot #2998</span>
+			</a>
+		</div>
 		<div id="guildlist">
 			<div class="guild" id="invite">
 				<div class="bar"></div>
@@ -44,7 +62,7 @@
 			</div>
 			<div class="guild" id="refresh">
 				<div class="bar"></div>
-				<a href="/refresh" alt="Refresh guild list">
+				<a href="/refresh" alt="Refresh server list">
 					<div class="avatar svg-avatar">
 						<svg width="20" height="23" viewBox="0 1.35 17.3 20" style="overflow: visible;">
 							<path fill="currentColor" d="M8.6,2.7V0.4c0-0.4-0.4-0.5-0.7-0.3L4.2,3.9C4,4,4,4.3,4.2,4.4l3.7,3.7c0.3,0.3,0.7,0.1,0.7-0.3V5.6c3.4,0,6.1,2.9,5.7,6.4  c-0.3,2.6-2.4,4.8-5.1,5.1c-3.5,0.4-6.4-2.3-6.4-5.7c0-1.1,0.3-2.1,0.8-2.9c0.1-0.2,0.1-0.4-0.1-0.5L2.1,6.5  C1.9,6.3,1.6,6.3,1.5,6.5C0.5,8-0.1,9.8,0,11.8c0.2,4.4,3.8,8.1,8.3,8.2c4.9,0.2,9-3.7,9-8.6C17.3,6.6,13.4,2.7,8.6,2.7z"></path>
@@ -53,26 +71,6 @@
 				</a>
 			</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>
 </body>
 </html>

+ 43 - 5
dashboard/index.js

@@ -1,8 +1,15 @@
 const http = require('http');
+const {parse} = require('querystring');
 const pages = require('./oauth.js');
 const dashboard = require('./guilds.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 path = require('path');
 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) => {
+	if ( req.method === 'POST' && req.url.startsWith( '/guild/' ) ) {
+		let args = req.url.split('/');
+		let state = req.headers.cookie?.split('; ')?.filter( cookie => {
+			return cookie.split('=')[0] === 'wikibot';
+		} )?.map( cookie => cookie.replace( /^wikibot="(\w*(?:-\d+)?)"$/, '$1' ) )?.join();
+
+		if ( args.length <= 4 && ['settings', 'verification', 'rcscript'].incluses( args[3] ) 
+		&& settingsData.has(state) && settingsData.get(state).guilds.isMember.has(args[2]) ) {
+			let body = '';
+			req.on( 'data', chunk => {
+				body += chunk.toString();
+			} );
+			req.on( 'error', () => {
+				console.log( error );
+				res.end('error');
+			} );
+			return req.on( 'end', () => {
+				console.log( parse(body) );
+				//return posts[args[3]](res, settingsData.get(state).user.id, args[2], parse(body));
+				res.writeHead(302, {Location: req.url});
+				res.end();
+			} );
+		}
+	}
+
 	if ( req.method !== 'GET' ) {
 		let body = '<img width="400" src="https://http.cat/418"><br><strong>' + http.STATUS_CODES[418] + '</strong>';
-		res.writeHead(418, {'Content-Length': body.length});
+		res.writeHead(418, {
+			'Content-Type': 'text/html',
+			'Content-Length': body.length
+		});
 		res.write( body );
 		return res.end();
 	}
@@ -59,12 +94,12 @@ const server = http.createServer((req, res) => {
 
 	var lastGuild = req.headers?.cookie?.split('; ')?.filter( cookie => {
 		return cookie.split('=')[0] === 'guild';
-	} )?.map( cookie => cookie.replace( /^guild="(\w+)"$/, '$1' ) )?.join();
-	if ( lastGuild ) res.setHeader('Set-Cookie', [`guild="${lastGuild}"; Max-Age=0; HttpOnly; Path=/`]);
+	} )?.map( cookie => cookie.replace( /^guild="(\w*)"$/, '$1' ) )?.join();
+	if ( lastGuild ) res.setHeader('Set-Cookie', ['guild=""; HttpOnly; Path=/; Max-Age=0']);
 
 	var state = req.headers.cookie?.split('; ')?.filter( cookie => {
 		return cookie.split('=')[0] === 'wikibot';
-	} )?.map( cookie => cookie.replace( /^wikibot="(\w+(?:-\d+)?)"$/, '$1' ) )?.join();
+	} )?.map( cookie => cookie.replace( /^wikibot="(\w*(?:-\d+)?)"$/, '$1' ) )?.join();
 
 	if ( reqURL.pathname === '/login' ) {
 		return pages.login(res, state, reqURL.searchParams.get('action'));
@@ -74,7 +109,10 @@ const server = http.createServer((req, res) => {
 		settingsData.delete(state);
 		res.writeHead(302, {
 			Location: '/login?action=logout',
-			'Set-Cookie': [`wikibot="${state}"; Max-Age=0; HttpOnly`]
+			'Set-Cookie': [
+				...( res.getHeader('Set-Cookie') || [] ),
+				'wikibot=""; HttpOnly; Path=/; Max-Age=0'
+			]
 		});
 		return res.end();
 	}

+ 33 - 21
dashboard/login.html

@@ -10,20 +10,44 @@
 	<meta property="og:title" content="Login – Wiki-Bot Settings">
 	<meta property="og:site_name" content="Wiki-Bot Settings">
 	<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>
 <body>
 	<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 class="scrollbar" id="sidebar">
+		<div class="scrollbar" id="channellist">
+			<a class="channel channel-header" id="login">
+				<img src="src/settings.svg" alt="Settings">
+				<div>Login</div>
+			</a>
+			<a class="channel" id="invite-wikibot" href="https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939912256&scope=bot">
+				<img src="src/channel.svg" alt="Channel">
+				<div>Invite Wiki-Bot</div>
+			</a>
+		</div>
+		<div id="navbar">
+			<div></div>
+			<a id="support" href="https://discord.gg/v77RTk5" target="_blank" alt="Help with Wiki-Bot">
+				<img class="avatar" src="https://cdn.discordapp.com/icons/464084451165732868/c6a8b9fc902b09545de8194a911e6045.png?size=64" alt="Wiki-Bot">
+				<span>Support Server</span>
+			</a>
+			<div></div>
+		</div>
 		<div id="guildlist">
 			<div class="guild" id="invite">
 				<div class="bar"></div>
@@ -36,18 +60,6 @@
 				</a>
 			</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>
 </body>
 </html>

+ 90 - 57
dashboard/oauth.js

@@ -1,7 +1,7 @@
 const crypto = require('crypto');
 const cheerio = require('cheerio');
 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 oauth = new DiscordOauth2( {
@@ -19,54 +19,66 @@ const file = require('fs').readFileSync('./dashboard/login.html');
  * @param {String} [action] - The action the user made
  */
 function dashboard_login(res, state, action) {
-	if ( state ) {
-		if ( settingsData.has(state) ) {
+	if ( state && settingsData.has(state) ) {
+		if ( !action ) {
 			res.writeHead(302, {Location: '/'});
 			return res.end();
 		}
-		res.setHeader('Set-Cookie', [`wikibot="${state}"; Max-Age=0; HttpOnly`]);
+		settingsData.delete(state);
 	}
 	var $ = cheerio.load(file);
 	let invite = oauth.generateAuthUrl( {
 		scope: ['identify', 'guilds', 'bot'],
 		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 prompt = 'none';
+	if ( action === 'unauthorized' ) {
+		createNotice($, {
+			type: 'info',
+			title: 'Not logged in!',
+			text: 'Please login before you can change any settings.'
+		}).prependTo('#text');
+	}
 	if ( action === 'failed' ) {
 		responseCode = 400;
 		createNotice($, {
+			type: 'error',
 			title: 'Login failed!',
 			text: 'An error occurred while logging you in, please try again.'
 		}).prependTo('#text');
 	}
-	if ( action === 'unauthorized' ) {
-		responseCode = 401;
-		createNotice($, {
-			title: 'Not logged in!',
-			text: 'Please login before you can change any settings.'
-		}).prependTo('#text');
-	}
 	if ( action === 'logout' ) {
+		prompt = 'consent';
 		createNotice($, {
+			type: 'success',
 			title: 'Successfully logged out!',
 			text: 'You have been successfully logged out. To change any settings you need to login again.'
 		}).prependTo('#text');
 	}
+	if ( process.env.READONLY ) {
+		createNotice($, {
+			type: 'info',
+			title: 'Read-only database!',
+			text: 'You can currently only view your settings but not change them.'
+		}).prependTo('#text');
+	}
 	state = crypto.randomBytes(16).toString("hex");
 	while ( settingsData.has(state) ) {
 		state = crypto.randomBytes(16).toString("hex");
 	}
 	let url = oauth.generateAuthUrl( {
 		scope: ['identify', 'guilds'],
-		prompt: '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();
 	res.writeHead(responseCode, {
-		'Set-Cookie': [`wikibot="${state}"; HttpOnly`],
+		'Set-Cookie': [
+			...( res.getHeader('Set-Cookie') || [] ),
+			`wikibot="${state}"; HttpOnly; Path=/`
+		],
 		'Content-Length': body.length
 	});
 	res.write( body );
@@ -81,14 +93,11 @@ function dashboard_login(res, state, action) {
  * @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'});
+		res.writeHead(302, {Location: '/login?action=failed'});
 		return res.end();
 	}
+	settingsData.delete(state);
 	return oauth.tokenRequest( {
 		scope: ['identify', 'guilds'],
 		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, '' ),
 					icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
 					+ ( 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( {
-				type: 'isMemberAll',
+				type: 'getGuilds',
+				member: user.id,
 				guilds: guilds.map( guild => guild.id )
 			} ).then( response => {
-				let isMember = new Map();
-				let notMember = new Map();
 				response.forEach( (guild, i) => {
-					if ( guild ) isMember.set(guilds[i].id, 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, {
 					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();
 			}, 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 => {
 			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, '' ),
 				icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
 				+ ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) : null ),
-				permissions: guild.permissions
+				userPermissions: guild.permissions
 			};
 		} );
 		sendMsg( {
-			type: 'isMemberAll',
+			type: 'getGuilds',
+			member: settings.user.id,
 			guilds: guilds.map( guild => guild.id )
 		} ).then( response => {
 			let isMember = new Map();
 			let notMember = new Map();
 			response.forEach( (guild, i) => {
-				if ( guild ) isMember.set(guilds[i].id, guilds[i]);
+				if ( guild ) isMember.set(guilds[i].id, Object.assign(guilds[i], guild));
 				else notMember.set(guilds[i].id, guilds[i]);
 			} );
 			settings.guilds = {count: guilds.length, isMember, notMember};
-			res.writeHead(302, {Location: returnLocation});
+			res.writeHead(302, {Location: returnLocation + '?refresh=success'});
 			return res.end();
 		}, 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();
 		} );
 	}, 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();
 	} );
 }

+ 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;
 	margin: 0 auto 1em;
 	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 {
 	display: flex;
@@ -88,12 +99,13 @@ a:hover .description {
 	display: flex;
 	align-items: center;
 	justify-content: space-between;
-	height: 47px;
-	padding: 0 0 0 8px;
+	height: 48px;
 	font-size: 16px;
 	line-height: 20px;
 	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 {
 	content: "";
@@ -104,6 +116,7 @@ a:hover .description {
 #navbar a {
 	display: flex;
 	align-items: center;
+	justify-content: center;
 	height: 100%;
 	padding-left: 10px;
 }
@@ -143,7 +156,6 @@ a:hover .description {
 	flex: 1 1 auto;
 	min-height: calc(100% - 24px);
 	width: 72px;
-	z-index: 1;
 }
 .guild {
 	margin: 0 0 8px;
@@ -226,7 +238,6 @@ a:hover .description {
 	top: 48px;
 	left: 72px;
 	bottom: 0;
-	z-index: 0;
 }
 .channel {
 	padding: 0 8px;
@@ -237,9 +248,6 @@ a:hover .description {
 	align-items: center;
 	color: #8e9297;
 }
-.channel:hover {
-	background: rgba(79,84,92,0.16);
-}
 .channel img {
 	margin-right: 6px;
 	width: 20px;
@@ -252,6 +260,12 @@ a:hover .description {
 	white-space: nowrap;
 	overflow: hidden;
 }
+.channel:hover {
+	background: rgba(79,84,92,0.16);
+}
+.channel.selected {
+	background: rgba(79,84,92,0.32);
+}
 .channel:hover div {
 	color: #dcddde;
 }
@@ -262,8 +276,7 @@ a:hover .description {
 	margin-left: 8px;
 	height: 44px;
 }
-.channel-header svg {
-	margin-right: 6px;
+.channel-header img {
 	width: 24px;
 	height: 24px;
 }
@@ -272,4 +285,35 @@ a:hover .description {
 	line-height: 24px;
 	font-weight: bold;
 	text-shadow: none;
+}
+fieldset div {
+	margin: 10px 0;
+}
+fieldset label {
+	display: inline-block;
+	min-width: 20%;
+}
+fieldset input[type="url"] {
+	min-width: 30%;
+	margin-right: 5px;
+}
+#login-button {
+	display: flex;
+	margin: 20px auto;
+	padding: 20px 50px;
+	width: fit-content;
+	justify-content: center;
+	align-items: center;
+	font-size: 300%;
+	background: #2f3136;
+	border: 5px solid #202225;
+	border-radius: 30px;
+}
+#login-button:hover {
+	background-color:#36393f;
+}
+#login-button img {
+	width: 60px;
+	height: 60px;
+	padding-right: 10px;
 }

+ 88 - 0
dashboard/src/index.js

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

+ 3 - 0
dashboard/src/settings.svg

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

+ 21 - 11
dashboard/util.js

@@ -34,7 +34,9 @@ const db = new sqlite3.Database( './wikibot.db', mode, dberror => {
  * @property {String} name
  * @property {String} acronym
  * @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 {String} notice.title - The title of the notice
  * @param {String} notice.text - The text of the notice
+ * @param {String} [notice.type] - The type of the notice
  * @returns {Cheerio}
  */
 function createNotice($, notice) {
+	var type = ( notice.type ? `notice-${notice.type}` : '' );
 	return $('<div class="notice">').append(
 		$('<b>').text(notice.title),
 		$('<div>').text(notice.text)
-	);
+	).addClass(type);
 }
 
 const permissions = {
 	ADMINISTRATOR: 1 << 3,
 	MANAGE_CHANNELS: 1 << 4,
 	MANAGE_GUILD: 1 << 5,
+	ADD_REACTIONS: 1 << 6,
+	VIEW_CHANNEL: 1 << 10,
+	SEND_MESSAGES: 1 << 11,
 	MANAGE_MESSAGES: 1 << 13,
-	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_ROLES: 1 << 28,
-	MANAGE_WEBHOOKS: 1 << 29,
-	MANAGE_EMOJIS: 1 << 30
+	MANAGE_WEBHOOKS: 1 << 29
 }
 
 /**
  * Check if a permission is included in the BitField
  * @param {String|Number} all - BitField of multiple permissions
- * @param {String} permission - Name of the permission to check for
- * @param {Boolean} [admin] - If administrator permission can overwrite
+ * @param {String[]} permission - Name of the permission to check for
  * @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};

+ 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
 			};
 			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 => {
 						data.error = error;
 					} ).finally( () => {
 						return dashboard.send( {id: message.id, data} );
 					} );
 					break;
-				case 'isMemberAll':
-					return manager.broadcastEval(`${JSON.stringify(message.data.guilds)}.map( guild => {
-						return this.guilds.cache.has(guild);
-					} )`).then( results => {
-						data.response = message.data.guilds.map( (guild, i) => {
-							return results.map( result => result[i] ).includes( true );
+				case 'getMember':
+					return manager.broadcastEval(`if ( this.guilds.cache.has('${message.data.guild}') ) {
+						let guild = this.guilds.cache.get('${message.data.guild}');
+						guild.members.fetch('${message.data.member}').then( member => {
+							return member.permissions.bitfield;
 						} );
+					}`).then( results => {
+						data.response = results.find( result => result );
 					}, error => {
 						data.error = error;
 					} ).finally( () => {

+ 3 - 3
package-lock.json

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

+ 1 - 1
package.json

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