Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add KDH #298

Merged
merged 7 commits into from Sep 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintrc.json
Expand Up @@ -2,5 +2,8 @@
"extends": "bamboo",
"rules": {
"no-throw-literal": 0
},
"parserOptions": {
"project": "./tsconfig.json"
}
}
18 changes: 17 additions & 1 deletion config.ts.example
Expand Up @@ -2,11 +2,19 @@

import { KlasaClientOptions } from 'klasa';
import { RPoolConnectionOptions } from 'rethinkdb-ts';
import { ServerOptions } from 'http';
import { APIWebhookData } from './src/lib/types/DiscordAPI';
import ApiRequest from './src/lib/structures/api/ApiRequest';
import ApiResponse from './src/lib/structures/api/ApiResponse';

export const DEV = 'DEV' in process.env ? process.env.DEV === 'true' : !('PM2_HOME' in process.env);
export const DEV_LAVALINK = 'DEV_LAVALINK' in process.env ? process.env.DEV_LAVALINK === 'true' : DEV;

const DASHBOARD_SERVER_OPTIONS: ServerOptions = {
IncomingMessage: ApiRequest,
ServerResponse: ApiResponse
};

export const DATABASE_DEVELOPMENT: RPoolConnectionOptions = { db: 'test' };
export const DATABASE_PRODUCTION: RPoolConnectionOptions = {
db: '',
Expand Down Expand Up @@ -91,11 +99,19 @@ export const CLIENT_OPTIONS: KlasaClientOptions = {
schedule: { interval: 5000 },
slowmode: 750,
slowmodeAggressive: true,
typing: false
typing: false,
/* Klasa Dashboard Hooks */
dashboardHooks: {
apiPrefix: '/',
port: 1234,
serverOptions: DASHBOARD_SERVER_OPTIONS
}
};

export const NAME = 'Skyra';
export const CLIENT_ID = DEV ? '365184854914236416' : '266624760782258186';
export const CLIENT_SECRET = '';

export const WEBHOOK_ERROR: APIWebhookData = DEV
? {
avatar: '7d52ea85e9ffe07ac8b15d4b60cf29d7',
Expand Down
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -48,6 +48,8 @@
"fs-nextra": "^0.4.5",
"gifencoder": "^2.0.1",
"klasa": "github:dirigeants/klasa#settings",
"klasa-dashboard-hooks": "github:kyranet/klasa-dashboard-hooks#master",
"klasa-decorators": "^0.0.1",
"lavalink": "^2.8.1",
"node-fetch": "^2.6.0",
"rethinkdb-ts": "^2.4.0-rc.16",
Expand Down
3 changes: 3 additions & 0 deletions src/Skyra.ts
Expand Up @@ -11,6 +11,9 @@ export const rootFolder = join(__dirname, '..', '..');
export const assetsFolder = join(rootFolder, 'assets');
export const cdnFolder = DEV ? join(assetsFolder, 'public') : join('/var', 'www', 'assets');

import klasaDashboardHooks = require('klasa-dashboard-hooks');
SkyraClient.use(klasaDashboardHooks);

const { FLAGS } = Permissions;

// Canvas setup
Expand Down
26 changes: 26 additions & 0 deletions src/lib/structures/api/ApiRequest.ts
@@ -0,0 +1,26 @@
import { IncomingMessage } from 'http';
import { Socket } from 'net';

export interface UserAuthObject {
token: string;
user_id: string;
}

export default class ApiRequest extends IncomingMessage {

public constructor(socket: Socket) {
super(socket);
}

}

export default interface ApiRequest {
originalUrl: string;
path: string;
search: string;
query: Record<string, string | string[]>;
params: Record<string, string>;
body?: unknown;
length?: number;
auth?: UserAuthObject;
}
22 changes: 22 additions & 0 deletions src/lib/structures/api/ApiResponse.ts
@@ -0,0 +1,22 @@
import { ServerResponse, STATUS_CODES } from 'http';

export default class ApiResponse extends ServerResponse {

public error(error: number | string): void {
if (typeof error === 'string') {
return this.status(500).json({ error });
}

return this.status(error).json({ error: STATUS_CODES[error] });
}

public status(code: number): this {
this.statusCode = code;
return this;
}

public json(data: any): void {
this.end(JSON.stringify(data));
}

}
8 changes: 8 additions & 0 deletions src/lib/types/Augments.d.ts
Expand Up @@ -41,3 +41,11 @@ declare module 'klasa' {
}

}

declare module 'klasa-dashboard-hooks' {

interface AuthData {
user_id: string;
}

}
17 changes: 17 additions & 0 deletions src/lib/util/util.ts
Expand Up @@ -13,6 +13,11 @@ import { util } from 'klasa';
import { Mutable } from '../types/util';
import { api } from './Models/Api';
import { Events } from '../types/Enums';
import { createFunctionInhibitor } from 'klasa-decorators';
import { Util } from 'klasa-dashboard-hooks';
import { CLIENT_SECRET } from '../../../config';
import ApiRequest from '../structures/api/ApiRequest';
import ApiResponse from '../structures/api/ApiResponse';

const REGEX_FCUSTOM_EMOJI = /<a?:\w{2,32}:\d{17,18}>/;
const REGEX_PCUSTOM_EMOJI = /a?:\w{2,32}:\d{17,18}/;
Expand Down Expand Up @@ -528,6 +533,18 @@ export function enumerable(value: boolean) {
};
}

export const authenticated = createFunctionInhibitor(
(request: ApiRequest) => {
if (!request.headers.authorization) return false;
request.auth = Util.decrypt(request.headers.authorization, CLIENT_SECRET);
if (!request.auth!.user_id || !request.auth!.token) return false;
return true;
},
(_request: ApiRequest, response: ApiResponse) => {
response.error(403);
}
);

interface UtilOneToTenEntry {
emoji: string;
color: number;
Expand Down
19 changes: 19 additions & 0 deletions src/middlewares/headers.ts
@@ -0,0 +1,19 @@
import { Middleware, MiddlewareStore } from 'klasa-dashboard-hooks';
import ApiRequest from '../lib/structures/api/ApiRequest';
import ApiResponse from '../lib/structures/api/ApiResponse';

export default class extends Middleware {

public constructor(store: MiddlewareStore, file: string[], directory: string) {
super(store, file, directory, { priority: 10 });
}

public run(request: ApiRequest, response: ApiResponse) {
response.setHeader('Access-Control-Allow-Origin', '*');
response.setHeader('Access-Control-Allow-Methods', 'DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT');
response.setHeader('Access-Control-Allow-Headers', 'Authorization, User-Agent, Content-Type');
response.setHeader('Content-Type', 'application/json; charset=utf-8');
if (request.method === 'OPTIONS') response.end('{"success":true}');
}

}
41 changes: 41 additions & 0 deletions src/middlewares/json.ts
@@ -0,0 +1,41 @@
import { createGunzip, createInflate } from 'zlib';
import { KlasaIncomingMessage, Middleware, MiddlewareStore } from 'klasa-dashboard-hooks';

export default class extends Middleware {

public constructor(store: MiddlewareStore, file: string[], directory: string) {
super(store, file, directory, { priority: 20 });
}

public async run(request: KlasaIncomingMessage) {
if (request.method !== 'POST') return;

const stream = this.contentStream(request);
let body = '';

for await (const chunk of stream) body += chunk;

const data = JSON.parse(body);
request.body = data;
}

public contentStream(request: KlasaIncomingMessage) {
const length = request.headers['content-length'];
let stream;
switch ((request.headers['content-encoding'] || 'identity').toLowerCase()) {
case 'deflate':
stream = createInflate();
request.pipe(stream);
break;
case 'gzip':
stream = createGunzip();
request.pipe(stream);
break;
case 'identity':
stream = request;
stream.length = length;
}
return stream;
}

}
47 changes: 47 additions & 0 deletions src/routes/authenticated/userGuilds.ts
@@ -0,0 +1,47 @@
import { Route, RouteStore } from 'klasa-dashboard-hooks';
import { Permissions } from 'discord.js';
import ApiRequest from '../../lib/structures/api/ApiRequest';
import ApiResponse from '../../lib/structures/api/ApiResponse';
import { Events } from '../../lib/types/Enums';
import { inspect } from 'util';

const { FLAGS: { MANAGE_GUILD } } = Permissions;

export default class extends Route {

public constructor(store: RouteStore, file: string[], directory: string) {
super(store, file, directory, { route: '/guilds/:guild/settings', authenticated: true });
}


public async post(request: ApiRequest, response: ApiResponse) {
const requestBody = request.body as Record<string, string>;

if (!requestBody.guild_id || !requestBody.data) {
return response.error(400);
}

const botGuild = this.client.guilds.get(requestBody.guild_id);
if (!botGuild) return response.error(400);

const member = await botGuild.members.fetch(request.auth!.user_id).catch(() => null);
if (!member) return response.error(400);

const canManage = member.permissions.has(MANAGE_GUILD);
if (!canManage) return response.error(401);

const { updated, errors } = await botGuild.settings.update(requestBody.data, { action: 'overwrite' });

if (errors.length > 0) {
this.client.emit(Events.Error,
`${botGuild.name}[${botGuild.id}] failed guild settings update:\n${inspect(errors)}`);

return response.error(500);
}


return response.json(updated);

}

}
21 changes: 21 additions & 0 deletions src/routes/main.ts
@@ -0,0 +1,21 @@
import { Route, RouteStore } from 'klasa-dashboard-hooks';
import { authenticated } from '../lib/util/util';
import ApiRequest from '../lib/structures/api/ApiRequest';
import ApiResponse from '../lib/structures/api/ApiResponse';

export default class extends Route {

public constructor(store: RouteStore, file: string[], directory: string) {
super(store, file, directory, { route: '' });
}

public get(_request: ApiRequest, response: ApiResponse) {
response.json({ message: 'Hello World' });
}

@authenticated
public post(_request: ApiRequest, response: ApiResponse) {
response.json({ message: 'Hello World' });
}

}
58 changes: 58 additions & 0 deletions src/routes/oauth/oauthcallback.ts
@@ -0,0 +1,58 @@
import fetch from 'node-fetch';
import { URL } from 'url';
import { Route, RouteStore, Util } from 'klasa-dashboard-hooks';
import ApiRequest from '../../lib/structures/api/ApiRequest';
import ApiResponse from '../../lib/structures/api/ApiResponse';
import OauthUser from './oauthuser';

export default class extends Route {

public constructor(store: RouteStore, file: string[], directory: string) {
super(store, file, directory, { route: 'oauth/callback' });
}

public async post(request: ApiRequest, response: ApiResponse) {
const requestBody = request.body as Record<string, string>;
if (!requestBody.code) {
response.error(400);
return;
}

const url = new URL('https://discordapp.com/api/oauth2/token');
url.searchParams.append('grant_type', 'authorization_code');
url.searchParams.append('redirect_uri', requestBody.redirect_uri);
url.searchParams.append('code', requestBody.code);

const res = await fetch(url as URL, {
headers: { Authorization: `Basic ${Buffer.from(`${this.client.options.clientID}:${this.client.options.clientSecret}`).toString('base64')}` },
method: 'POST'
});

if (!res.ok) {
console.log(await res.text());
response.error('There was an error fetching the token.');
return;
}

const oauthUser = this.store.get('oauthuser') as unknown as OauthUser;

if (!oauthUser) {
response.error(500);
return;
}

const body = await res.json();
const user = await oauthUser.api(body.access_token);

response.json(({
access_token: Util.encrypt({
user_id: user.id,
token: body.access_token
},
this.client.options.clientSecret),
user
}));

}

}
20 changes: 20 additions & 0 deletions src/routes/oauth/oauthuser.ts
@@ -0,0 +1,20 @@
import fetch from 'node-fetch';
import { Route, RouteStore } from 'klasa-dashboard-hooks';

export default class extends Route {


public constructor(store: RouteStore, file: string[], directory: string) {
super(store, file, directory, { route: 'oauth/user' });
}

public async api(token: string) {
token = `Bearer ${token}`;
const user = await fetch('https://discordapp.com/api/users/@me', { headers: { Authorization: token } })
.then(result => result.json());
user.guilds = await fetch('https://discordapp.com/api/users/@me/guilds', { headers: { Authorization: token } })
.then(result => result.json());
return this.client.dashboardUsers.add(user);
}

}
16 changes: 15 additions & 1 deletion tsconfig.json
Expand Up @@ -3,9 +3,23 @@
"compilerOptions": {
"noImplicitAny": false,
"outDir": "./dist",
"types": ["node", "node-fetch"]
"types": [
"node",
"node-fetch",
"discord.js",
"klasa",
"klasa-dashboard-hooks"
]
},
"include": [
"./src/**/*"
],
"exclude": [
"**/node_modules",
"**/build",
"**/bin",
"**/out",
"**/dist",
"**/coverage"
]
}