소스 검색

more work on dashboard

Markus-Rost 4 년 전
부모
커밋
8ddaffa91a
7개의 변경된 파일292개의 추가작업 그리고 70개의 파일을 삭제
  1. 1 0
      .github/workflows/nodejs.yml
  2. 1 1
      .github/workflows/translations.yml
  3. 0 0
      dashboard/index.css
  4. 158 0
      dashboard/index.html
  5. 129 68
      dashboard/index.js
  6. 1 0
      main.js
  7. 2 1
      util/database.js

+ 1 - 0
.github/workflows/nodejs.yml

@@ -19,6 +19,7 @@ jobs:
     - run: npm ci
     - run: npm run build --if-present
     - run: sed -i -e 's/"<Discord bot token>"/"${{secrets.DISCORD_TOKEN}}"/g' .env
+    - run: sed -i -e 's/"<Discord client secret>"/"${{secrets.DISCORD_SECRET}}"/g' .env
     - run: sed -i -e 's/"!wiki "/"!test "/g' .env
     - run: npm test -- --timeout:60
       timeout-minutes: 5

+ 1 - 1
.github/workflows/translations.yml

@@ -39,6 +39,6 @@ jobs:
       uses: repo-sync/pull-request@v2
       with:
         pr_title: "Update translations"
-        pr_body: "Update translations from https://weblate.frisk.space/engage/wiki-bot/"
+        pr_body: "Update translations from https://translate.wikibot.de/engage/wiki-bot/"
         pr_label: "translation"
         github_token: ${{ secrets.WIKIBOT_TOKEN }}

+ 0 - 0
dashboard/index.css


+ 158 - 0
dashboard/index.html

@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+	<meta charset="UTF-8">
+	<title>Wiki-Bot Settings</title>
+	<link rel="shortcut icon" href="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=64">
+	<meta name="description" content="Wiki-Bot is a bot with the purpose to easily search for and link to wiki pages. Wiki-Bot shows short descriptions and additional info about pages and is able to resolve redirects and follow interwiki links.">
+	<meta property="og:image" content="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=1024">
+	<meta property="og:title" content="Wiki-Bot Settings">
+	<meta property="og:site_name" content="Wiki-Bot Settings">
+	<meta itemprop="author" content="MarkusRost">
+	<style>
+		body {
+			background: #36393f;
+			font-family: Whitney,Helvetica Neue,Helvetica,Arial,sans-serif;
+			text-rendering: optimizeLegibility;
+			color: #dcddde;
+		}
+		.text {
+			position: relative;
+			display: inline-block;
+			left: 72px;
+		}
+		.sidebar {
+			background: #202225;
+			position: absolute;
+			top: 0;
+			left: 0;
+			min-height: 100%;
+			display: flex;
+			width: 72px;
+		}
+		.guildlist {
+			padding: 12px 0 0;
+			position: relative;
+			flex: 1 1 auto;
+		}
+		.guild {
+			margin: 0 0 8px;
+			position: relative;
+			display: flex;
+			justify-content: center;
+		}
+		.guild a {
+			text-decoration: none;
+		}
+		.avatar {
+			border-radius: 50%;
+			width: 48px;
+			height: 48px;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			font-weight: 500;
+			line-height: 1.2em;
+			white-space: nowrap;
+			overflow: hidden;
+			color: #dcddde;
+			font-weight: bold;
+		}
+		.guild.selected .avatar,
+		.guild:hover .avatar {
+			border-radius: 30%;
+			color: #ffffff;
+		}
+		.noicon {
+			width: 48px;
+			height: 48px;
+			background: #36393f;
+		}
+		.guild.selected .noicon,
+		.guild:hover .noicon {
+			background-color: #7289da;
+		}
+		#invite .avatar {
+			color: #43b581;
+			background: #36393f;
+		}
+		#invite:hover .avatar {
+			color: #ffffff;
+			background-color: #43b581;
+		}
+		.separator {
+			height: 2px;
+			width: 32px;
+			border-radius: 1px;
+			background-color: rgba(255,255,255,0.06);;
+		}
+		.bar {
+			position: absolute;
+			left: 0;
+			top: 0;
+			display: block;
+			width: 8px;
+			border-radius: 0 4px 4px 0;
+			margin-left: -4px;
+			background-color: #ffffff;
+		}
+		.guild:hover .bar {
+			margin-top: 14px;
+			height: 20px;
+		}
+		.guild.selected .bar {
+			margin-top: 4px;
+			height: 40px;
+		}
+		.guild a[alt]:hover:after {
+			content: attr(alt);
+			position: absolute;
+			top: 20%;
+			left: 72px;
+			background: #000000;
+			color: #dcddde;
+			font-weight: bold;
+			font-size: 90%;
+			white-space: nowrap;
+			border-radius: 4px;
+			padding: 8px;
+		}
+	</style>
+</head>
+<body class="settings">
+	<div class="text"><replace id="text">Some text here</replace></div>
+	<div class="sidebar">
+		<div class="guildlist">
+			<div class="guild" id="refresh">
+				<div class="bar"></div>
+				<a href="/refresh" alt="Refresh guild list">
+					<img class="avatar" src="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=128" alt="Refresh guild list" width="48" height="48">
+				</a>
+			</div>
+			<div class="guild">
+				<div class="separator"></div>
+			</div>
+			<div class="guild" id="invite">
+				<div class="bar"></div>
+				<a href="https://discord.com/oauth2/authorize?client_id=461189216198590464&permissions=939912256&scope=bot" alt="Invite Wiki-Bot">
+					<div class="avatar">
+						<svg width="24" height="24" viewBox="0 0 24 24">
+							<path fill="currentColor" d="M20 11.1111H12.8889V4H11.1111V11.1111H4V12.8889H11.1111V20H12.8889V12.8889H20V11.1111Z"></path>
+						</svg>
+					</div>
+				</a>
+			</div>
+			<replace id="guilds">List of guilds here</replace>
+			<div class="guild">
+				<div class="separator"></div>
+			</div>
+			<div class="guild" id="logout">
+				<div class="bar"></div>
+				<a href="/logout" alt="Logout">
+					<img class="avatar" src="https://cdn.discordapp.com/avatars/461189216198590464/f69cdc197791aed829882b64f9760dbb.png?size=128" alt="Logout" width="48" height="48">
+				</a>
+			</div>
+		</div>
+	</div>
+</body>
+</html>

+ 129 - 68
dashboard/index.js

@@ -1,6 +1,18 @@
 const http = require('http');
 const crypto = require('crypto');
-const db = require('./util/database.js');
+const cheerio = require('cheerio');
+const {defaultPermissions} = require('../util/default.json');
+
+const sqlite3 = require('sqlite3').verbose();
+const mode = ( process.env.READONLY ? sqlite3.OPEN_READONLY : sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE );
+const db = new sqlite3.Database( './wikibot.db', mode, dberror => {
+	if ( dberror ) {
+		console.log( '- Dashboard: Error while connecting to the database: ' + dberror );
+		return dberror;
+	}
+	console.log( '- Dashboard: Connected to the database.' );
+} );
+
 const DiscordOauth2 = require('discord-oauth2');
 const oauth = new DiscordOauth2( {
 	clientId: process.env.bot,
@@ -8,6 +20,9 @@ const oauth = new DiscordOauth2( {
 	redirectUri: process.env.dashboard
 } );
 
+const fs = require('fs');
+const file = fs.readFileSync('./dashboard/index.html');
+
 var messageId = 1;
 var messages = new Map();
 
@@ -46,7 +61,8 @@ function sendMsg(message) {
  * @typedef Guild
  * @property {String} id
  * @property {String} name
- * @property {String} icon
+ * @property {String} acronym
+ * @property {String} [icon]
  * @property {String} permissions
  */
 
@@ -56,26 +72,46 @@ function sendMsg(message) {
 var settingsData = new Map();
 
 const server = http.createServer((req, res) => {
+	res.setHeader('Content-Type', 'text/html');
+	res.setHeader('Content-Language', ['en']);
+
 	if ( req.method !== 'GET' ) {
-		res.writeHead(418, {'Content-Type': 'text/html'});
-		res.write( '<img width="400" src="https://http.cat/418"><br><strong>' + http.STATUS_CODES[418] + '</strong>' );
+		let notice = '<img width="400" src="https://http.cat/418"><br><strong>' + http.STATUS_CODES[418] + '</strong>';
+		res.writeHead(418, {'Content-Length': notice.length});
+		res.write( notice );
 		return res.end();
 	}
-	res.setHeader('Content-Type', 'text/html');
-	res.setHeader('Content-Language', ['en']);
+	
+	var guild = req.headers?.cookie?.split('; ')?.filter( cookie => {
+		return cookie.split('=')[0] === 'guild';
+	} )?.map( cookie => cookie.replace( /^guild="(\w+)"$/, '$1' ) )?.join();
+	if ( guild ) res.setHeader('Set-Cookie', [`guild="${guild}"; Max-Age=0; HttpOnly; Path=/oauth`]);
+
+	var state = req.headers.cookie?.split('; ')?.filter( cookie => {
+		return cookie.split('=')[0] === 'wikibot';
+	} )?.map( cookie => cookie.replace( /^wikibot="(\w+)"$/, '$1' ) )?.join();
+
 	var reqURL = new URL(req.url, process.env.dashboard);
+
 	if ( reqURL.pathname === '/login' ) {
+		if ( settingsData.has(state) ) {
+			res.writeHead(302, {Location: '/'});
+			return res.end();
+		}
 		let responseCode = 200;
 		let notice = '';
 		if ( reqURL.searchParams.get('action') === 'failed' ) {
 			responseCode = 400;
 			notice = '<img width="400" src="https://http.cat/' + responseCode + '"><br><strong>Login failed, please try again!</strong><br><br>';
 		}
-		if ( reqURL.searchParams.get('action') === 'missing' ) {
-			responseCode = 404;
-			notice = '<img width="400" src="https://http.cat/' + responseCode + '"><br><strong>404, could not find the page!</strong><br><br>';
+		if ( reqURL.searchParams.get('action') === 'unauthorized' ) {
+			responseCode = 401;
+			notice = '<img width="400" src="https://http.cat/' + responseCode + '"><br><strong>Please login first!</strong><br><br>';
 		}
-		let state = crypto.randomBytes(16).toString("hex");
+		if ( reqURL.searchParams.get('action') === 'logout' ) {
+			notice = '<img width="400" src="https://http.cat/' + responseCode + '"><br><strong>Successfully logged out!</strong><br><br>';
+		}
+		state = crypto.randomBytes(16).toString("hex");
 		while ( settingsData.has(state) ) {
 			state = crypto.randomBytes(16).toString("hex");
 		}
@@ -83,28 +119,33 @@ const server = http.createServer((req, res) => {
 			scope: ['identify', 'guilds'],
 			promt: 'none', state
 		} );
+		notice += `<a href="${url}">Login</a>`;
 		res.writeHead(responseCode, {
-			'Set-Cookie': [`wikibot="${state}"`, 'HttpOnly', 'SameSite=Strict']
+			'Set-Cookie': [`wikibot="${state}"; HttpOnly`],
+			'Content-Length': notice.length
 		});
-		res.write( notice + `<a href="${url}">Login</a>` );
+		res.write( notice );
 		return res.end();
 	}
-	var state = req.headers?.cookie?.split('; ')?.filter( cookie => {
-		return cookie.split('=')[0] === 'wikibot';
-	} )?.map( cookie => cookie.replace( /^wikibot="(\w+)"$/, '$1' ) )?.join();
+
 	if ( reqURL.pathname === '/logout' ) {
 		settingsData.delete(state);
 		res.writeHead(302, {
-			Location: '/?action=logout',
-			'Set-Cookie': [`wikibot="${state}"`, 'Max-Age=0', 'HttpOnly', 'SameSite=Strict']
+			Location: '/login?action=logout',
+			'Set-Cookie': [`wikibot="${state}"; Max-Age=0; HttpOnly`]
 		});
 		return res.end();
 	}
+
+	if ( !state ) {
+		res.writeHead(302, {Location: '/login?action=unauthorized'});
+		return res.end();
+	}
+
 	if ( reqURL.pathname === '/oauth' ) {
+		console.log(req.url)
 		if ( state !== reqURL.searchParams.get('state') || !reqURL.searchParams.get('code') ) {
-			res.writeHead(302, {
-				Location: '/login?action=failed'
-			});
+			res.writeHead(302, {Location: '/login?action=unauthorized'});
 			return res.end();
 		}
 		return oauth.tokenRequest( {
@@ -124,7 +165,8 @@ const server = http.createServer((req, res) => {
 						discriminator: user.discriminator,
 						avatar: 'https://cdn.discordapp.com/' + ( user.avatar ? 
 							`embed/avatars/${user.discriminator % 5}.png` : 
-							`avatars/${user.id}/${user.avatar}.webp` ),
+							`avatars/${user.id}/${user.avatar}.` + 
+							( user.avatar.startsWith( 'a_' ) ? 'gif' : 'png' ) ) + '?size=128',
 						locale: user.locale
 					},
 					guilds: new Map(guilds.filter( guild => {
@@ -132,85 +174,97 @@ const server = http.createServer((req, res) => {
 					} ).map( guild => [guild.id, {
 						id: guild.id,
 						name: guild.name,
-						icon: `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp`,
+						acronym: guild.name.replace( /'s /g, ' ' ).replace( /\w+/g, e => e[0] ).replace( /\s/g, '' ),
+						icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
+						 + ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) + '?size=128' : null ),
 						permissions: guild.permissions
 					}] ))
 				});
 				res.writeHead(302, {
-					Location: '/'
+					Location: ( guild ? '/guild/' + guild : '/' )
 				});
 				return res.end();
 			}, error => {
 				console.log( '- Dashboard: Error while getting user and guilds: ' + error );
-				res.writeHead(302, {
-					Location: '/login?action=failed'
-				});
+				res.writeHead(302, {Location: '/login?action=failed'});
 				return res.end();
 			} );
 		}, error => {
 			console.log( '- Dashboard: Error while getting the token: ' + error );
-			res.writeHead(302, {
-				Location: '/login?action=failed'
-			});
+			res.writeHead(302, {Location: '/login?action=failed'});
 			return res.end();
 		} );
 	}
+
+	if ( !settingsData.has(state) ) {
+		res.writeHead(302, {Location: '/login?action=unauthorized'});
+		return res.end();
+	}
+	var settings = settingsData.get(state);
+
 	if ( reqURL.pathname === '/refresh' ) {
-		if ( !settingsData.has(state) ) {
-			res.writeHead(302, {
-				Location: '/login?action=failed'
-			});
-			return res.end();
-		}
-		let settings = settingsData.get(state)
 		return oauth.getUserGuilds(settings.access_token).then( guilds => {
 			settings.guilds = new Map(guilds.filter( guild => {
 				return ( guild.owner || hasPerm(guild.permissions, 'MANAGE_GUILD') );
 			} ).map( guild => [guild.id, {
 				id: guild.id,
 				name: guild.name,
-				icon: guild.icon,
+				acronym: guild.name.replace( /'s /g, ' ' ).replace( /\w+/g, e => e[0] ).replace( /\s/g, '' ),
+				icon: ( guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.`
+				 + ( guild.icon.startsWith( 'a_' ) ? 'gif' : 'png' ) + '?size=128' : null ),
 				permissions: guild.permissions
 			}] ));
 			res.writeHead(302, {
-				Location: ( reqURL.searchParams.get('returnTo') || '/' )
+				Location: ( reqURL.searchParams.get('return') || '/' )
 			});
 			return res.end();
 		}, error => {
 			console.log( '- Dashboard: Error while refreshing guilds: ' + error );
-			res.writeHead(302, {
-				Location: '/login?action=failed'
-			});
+			res.writeHead(302, {Location: '/login?action=failed'});
 			return res.end();
 		} );
 	}
-	if ( reqURL.pathname === '/' ) {
-		if ( !settingsData.has(state) ) {
-			let notice = '';
-			if ( reqURL.searchParams.get('action') === 'logout' ) {
-				notice = '<strong>Successfully logged out!</strong><br><br>';
-			}
-			res.write( notice + '<a href="/login">Login</a>' );
-			return res.end();
+
+	var $ = cheerio.load(file);
+	$('.guild#refresh a').attr('href', '/refresh?return=' + reqURL.pathname);
+	$('.guild#invite a').attr('href', oauth.generateAuthUrl( {
+		scope: ['identify', 'guilds', 'bot'],
+		permissions: defaultPermissions, state
+	} ));
+	$('.guild#logout img').attr('src', settings.user.avatar);
+	let guilds = '';
+	settings.guilds.forEach( guild => {
+		guilds += `<div class="guild" id="${guild.id}">
+			<div class="bar"></div>
+			<a href="/guild/${guild.id}" alt="${guild.name}">` + ( guild.icon ? 
+				`<img class="avatar" src="${guild.icon}" alt="${guild.acronym}" width="48" height="48">`
+				 : `<div class="avatar noicon">${guild.acronym}</div>` ) + 
+			`</a>
+		</div>`
+	} );
+	$('replace#guilds').replaceWith(guilds);
+
+	if ( reqURL.pathname.startsWith( '/guild/' ) ) {
+		let id = reqURL.pathname.replace( '/guild/', '' );
+		if ( settings.guilds.has(id) ) {
+			$('.guild#' + id).addClass('selected');
+			let guild = settings.guilds.get(id);
+			$('head title').text(guild.name + ' – ' + $('head title').text());
+			let url = oauth.generateAuthUrl( {
+				scope: ['identify', 'guilds', 'bot'],
+				permissions: defaultPermissions,
+				guild_id: id, state
+			} );
+			$('replace#text').replaceWith(`<a href="${url}">${guild.permissions} Keks</a>`);
+			res.setHeader('Set-Cookie', [`guild="${id}"; HttpOnly; Path=/oauth`]);
 		}
-		let notice = 'Guilds:';
-		settingsData.get(state)?.guilds.forEach( guild => {
-			notice += '\n\n' + guild.name;
-		} );
-		res.write( '<a href="/refresh">Refresh guild list.</a><pre>' + notice.replace( /</g, '&lt;' ) + '</pre>' );
-		return res.end();
-	}
-	if ( /^\/guild\/\d+$/.test(reqURL.pathname) && settingsData.get(state)?.guilds?.has(reqURL.pathname.replace( '/guild/', '' )) ) {
-		res.write( settingsData.get(state).guilds.get(reqURL.pathname.replace( '/guild/', '' )).name.replace( /</g, '&lt;' ) );
-		return res.end();
+		$('replace#text').replaceWith('You are missing the <code>MANAGE_GUILD</code> permission.');
 	}
-	if ( reqURL.pathname === '/guild' || reqURL.pathname.startsWith( '/guild/' ) ) {
-		res.writeHead(302, {
-			Location: '/'
-		});
-		return res.end();
-	}
-	res.writeHead(302, {'Location': '/login?action=missing'});
+
+	$('replace#text').replaceWith('Keks');
+	let notice = $.html();
+	res.writeHead(200, {'Content-Length': notice.length});
+	res.write( notice );
 	return res.end();
 });
 
@@ -233,7 +287,7 @@ const permissions = {
 /**
  * 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 {String?} permission - Name of the permission to check for
  * @param {Boolean} [admin] - If administrator permission can overwrite
  * @returns {Boolean}
  */
@@ -250,9 +304,16 @@ function hasPerm(all, permission, admin = true) {
  */
 async function graceful(signal) {
 	console.log( '- Dashboard: ' + signal + ': Closing the dashboard...' );
-	server.close( () => {
+	await server.close( () => {
 		console.log( '- Dashboard: ' + signal + ': Closed the dashboard server.' );
 	} );
+	await db.close( dberror => {
+		if ( dberror ) {
+			console.log( '- Dashboard: ' + signal + ': Error while closing the database connection: ' + dberror );
+			return dberror;
+		}
+		console.log( '- Dashboard: ' + signal + ': Closed the database connection.' );
+	} );
 }
 
 process.once( 'SIGINT', graceful );

+ 1 - 0
main.js

@@ -2,6 +2,7 @@ require('dotenv').config();
 const child_process = require('child_process');
 
 const isDebug = ( process.argv[2] === 'debug' );
+process.env.READONLY = ( process.argv[2] === 'readonly' );
 const got = require('got').extend( {
 	throwHttpErrors: false,
 	timeout: 30000,

+ 2 - 1
util/database.js

@@ -1,6 +1,7 @@
 const {defaultSettings} = require('../util/default.json');
 const sqlite3 = require('sqlite3').verbose();
-var db = new sqlite3.Database( './wikibot.db', sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, dberror => {
+const mode = ( process.env.READONLY ? sqlite3.OPEN_READONLY : sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE );
+const db = new sqlite3.Database( './wikibot.db', mode, dberror => {
 	if ( dberror ) {
 		console.log( '- ' + shardId + ': Error while connecting to the database: ' + dberror );
 		return dberror;