Browse Source

Move async helpers to shared

ghorsington 3 years ago
parent
commit
cc53dd75a0

+ 3 - 2
bot/src/logging.ts

@@ -2,8 +2,9 @@ import winston from "winston";
 import nodemailer from "nodemailer";
 import TransportStream from "winston-transport";
 import Mail from "nodemailer/lib/mailer";
-import { isHttpError, hasStackTrace } from "./util";
+import { hasStackTrace, isHttpError } from "@shared/common/async_utils";
 import { inspect } from "util";
+import { HTTPError } from "got/dist/source";
 
 export const logger = winston.createLogger({
     level: "debug",
@@ -23,7 +24,7 @@ export const logger = winston.createLogger({
 
 process.on("unhandledRejection", (reason) => {
     
-    if (isHttpError(reason))
+    if (isHttpError<HTTPError>(reason))
         throw new Error(`HTTPError: ${reason.request.requestUrl} failed because ${reason}\nStack trace: ${reason.stack}`);
     if (hasStackTrace(reason))
         throw new Error(`Unhandled rejection: ${reason}\nFull stack trace: ${reason.stack}`);

+ 1 - 1
bot/src/main.ts

@@ -5,8 +5,8 @@ import "./environment";
 import * as path from "path";
 import { client } from "./client";
 import { createConnection, getConnectionOptions } from "typeorm";
-import { assertOk } from "./util";
 import { DB_ENTITIES } from "@shared/db/entities";
+import { assertOk } from "@shared/common/async_utils";
 import { logger } from "./logging";
 import { PluginManager } from "./plugin_manager";
 import { startRpcServer } from "./rpc";

+ 1 - 1
bot/src/plugins/inspire.ts

@@ -1,7 +1,7 @@
 import got from "got";
 import { logger } from "src/logging";
 import { Command, ICommandData, Plugin } from "src/model/plugin";
-import { tryDo } from "src/util";
+import { tryDo } from "@shared/common/async_utils";
 
 @Plugin
 export class Inspire {

+ 1 - 1
bot/src/plugins/news_aggregator.ts

@@ -12,7 +12,7 @@ import { KnownChannel } from "@shared/db/entity/KnownChannel";
 import { AggroNewsItem } from "@shared/db/entity/AggroNewsItem";
 import { logger } from "src/logging";
 import { Plugin } from "src/model/plugin";
-import { assertOk, tryDo } from "src/util";
+import { assertOk, tryDo } from "@shared/common/async_utils";
 
 const UPDATE_INTERVAL = process.env.NODE_ENV == "dev" ? 60 : 5;
 const AGGREGATOR_MANAGER_CHANNEL = "aggregatorManager";

+ 1 - 1
bot/src/plugins/rcg.ts

@@ -2,7 +2,7 @@ import { Message } from "discord.js";
 import got from "got";
 import { logger } from "src/logging";
 import { Command, ICommandData, Plugin } from "src/model/plugin";
-import { tryDo } from "src/util";
+import { tryDo } from "@shared/common/async_utils";
 
 const rcgRe = /<input id="rcg_image".+value="([^"]+)".*\/>/i;
 

+ 1 - 1
bot/src/plugins/stickers.ts

@@ -2,8 +2,8 @@ import { Plugin, Event, BotEventData, Command, ICommandData } from "src/model/pl
 import { readdirSync, statSync } from "fs";
 import { join, basename, extname } from "path";
 import { Message } from "discord.js";
-import { tryDo } from "src/util";
 import { logger } from "src/logging";
+import { tryDo } from "@shared/common/async_utils";
 
 const STICKERS_PATH = "./stickers";
 const STICKERS_PREFIX = "!";

+ 2 - 1
bot/src/plugins/violation.ts

@@ -1,5 +1,5 @@
 import { Plugin, ICommandData, Command, Event, BotEventData } from "src/model/plugin";
-import { parseArgs, tryDo, parseDuration, UNIT_MEASURES, Option, isAuthorisedAsync } from "src/util";
+import { parseArgs, parseDuration, UNIT_MEASURES, isAuthorisedAsync } from "src/util";
 import { GuildMember, Guild, MessageEmbed, Message, TextChannel, PartialGuildMember, User } from "discord.js";
 import { logger } from "src/logging";
 import { client } from "src/client";
@@ -9,6 +9,7 @@ import { GuildViolationSettings } from "@shared/db/entity/GuildViolationSettings
 import { Mute, Violation } from "@shared/db/entity/Violation";
 import { scheduleJob, Job, rescheduleJob } from "node-schedule";
 import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
+import { tryDo, Option } from "@shared/common/async_utils";
 
 const MENTION_PATTERN = /<@!?(\d+)>/;
 

+ 1 - 35
bot/src/util.ts

@@ -1,7 +1,6 @@
 import { GuildMember } from "discord.js";
 import { getRepository, In } from "typeorm";
 import { KnownUser } from "@shared/db/entity/KnownUser";
-import { HTTPError } from "got/dist/source";
 import humanizeDuration from "humanize-duration";
 
 const VALID_EXTENSIONS = new Set([
@@ -79,35 +78,6 @@ export function formatString(str: string, vars: Record<string, string>): string
     return Object.keys(vars).filter(s => Object.prototype.hasOwnProperty.call(vars, s)).reduce((s, cur) => s.replace(`{${cur}}`, vars[cur]), str);
 }
 
-export async function tryDo<TResult>(promise: Promise<TResult>) : Promise<Option<{result: TResult}, {error: unknown}>> {
-    try {
-        return {ok: true, result: await promise};
-    } catch(err) {
-        return {ok: false, error: err};
-    }
-}
-
-export async function assertOk<T>(promise: Promise<T>): Promise<T> {
-    try {
-        return await promise;
-    } catch (err) {
-        if (hasStackTrace(err)) {
-            const trace: {stack?: string} = {};
-            Error.captureStackTrace(trace);
-            err.stack = `${err.stack}\nCaused by: ${trace.stack}`;
-        }
-        throw err;
-    }
-}
-
-export function isHttpError(err?: unknown): err is HTTPError {
-    return err && Object.prototype.hasOwnProperty.call(err, "response");
-}
-
-export function hasStackTrace(reason?: unknown): reason is {stack: unknown} {
-    return reason && Object.prototype.hasOwnProperty.call(reason, "stack");
-}
-
 export function parseArgs(str: string): string[] {
     const result: string[] = [];
 
@@ -173,8 +143,4 @@ export function parseDuration(s: string): number | undefined {
     if (buffer.length != 0)
         return undefined;
     return result;
-}
-
-export type Ok<T> = T & { ok: true };
-export type Error<T> = T & { ok: false };
-export type Option<T, U = unknown> = Ok<T> | Error<U>;
+}

+ 4 - 3
bot/src/xenforo.ts

@@ -1,5 +1,6 @@
-import { Dict, tryDo, isHttpError, assertOk } from "./util";
-import got, { Method } from "got";
+import { Dict } from "./util";
+import got, { Method, HTTPError } from "got";
+import { tryDo, assertOk, isHttpError } from "@shared/common/async_utils";
 
 export interface RequestError {
     code: string;
@@ -25,7 +26,7 @@ export class XenforoClient {
         }));
 
         if (!result.ok) {
-            if (isHttpError(result.error))
+            if (isHttpError<HTTPError>(result.error))
                 throw result.error.response.body as RequestErrorSet;
             else
                 throw { errors: [{ code: "UNK", message: "Unkown error" }] } as RequestErrorSet;

+ 32 - 0
shared/src/common/async_utils.ts

@@ -0,0 +1,32 @@
+export async function tryDo<TResult>(promise: Promise<TResult>) : Promise<Option<{result: TResult}, {error: unknown}>> {
+    try {
+        return {ok: true, result: await promise};
+    } catch(err) {
+        return {ok: false, error: err};
+    }
+}
+
+export async function assertOk<T>(promise: Promise<T>): Promise<T> {
+    try {
+        return await promise;
+    } catch (err) {
+        if (hasStackTrace(err)) {
+            const trace: {stack?: string} = {};
+            Error.captureStackTrace(trace);
+            err.stack = `${err.stack}\nCaused by: ${trace.stack}`;
+        }
+        throw err;
+    }
+}
+
+export function hasStackTrace(reason?: unknown): reason is {stack: unknown} {
+    return reason && Object.prototype.hasOwnProperty.call(reason, "stack");
+}
+
+export function isHttpError<T>(err?: unknown): err is T {
+    return err && Object.prototype.hasOwnProperty.call(err, "response");
+}
+
+export type Ok<T> = T & { ok: true };
+export type Error<T> = T & { ok: false };
+export type Option<T, U = unknown> = Ok<T> | Error<U>;

+ 4 - 1
web/package.json

@@ -36,7 +36,8 @@
 		"make-error": "^1.3.6",
 		"node-fetch": "^2.6.0",
 		"rpc_ts": "^2.1.0",
-		"sirv": "^1.0.5"
+		"sirv": "^1.0.5",
+		"svelte-awesome": "^2.3.0"
 	},
 	"devDependencies": {
 		"@babel/core": "^7.11.1",
@@ -44,6 +45,8 @@
 		"@babel/plugin-transform-runtime": "^7.11.0",
 		"@babel/preset-env": "^7.11.0",
 		"@babel/runtime": "^7.11.2",
+		"@fortawesome/free-brands-svg-icons": "^5.14.0",
+		"@fortawesome/free-solid-svg-icons": "^5.14.0",
 		"@rollup/plugin-alias": "^3.1.1",
 		"@rollup/plugin-babel": "^5.1.0",
 		"@rollup/plugin-commonjs": "^14.0.0",

+ 16 - 0
web/src/environment.ts

@@ -14,3 +14,19 @@ if (process.env.NODE_ENV === "development") {
     process.env.TYPEORM_PASSWORD = process.env.DB_PASSWORD;
     process.env.TYPEORM_DATABASE = process.env.DB_NAME;
 }
+
+export interface BotEnvironment {
+    clientId: string;
+    redirectUrl: string;
+    clientSecret: string;
+}
+
+export const ENVIRONMENT: BotEnvironment = {
+    clientId: process.env.BOT_CLIENT_ID ?? "",
+    redirectUrl: process.env.WEB_AUTH_URI ?? "",
+    clientSecret: process.env.BOT_CLIENT_SECRET ?? "",
+};
+
+export const IS_VALID = process.env.BOT_CLIENT_ID !== undefined
+                        && process.env.WEB_AUTH_URI !== undefined
+                        && process.env.BOT_CLIENT_SECRET !== undefined;

+ 12 - 27
web/src/routes/auth.ts

@@ -1,42 +1,27 @@
 import { Request as ExpressRequest, Response as ExpressResponse } from "express";
-import got from "got";
-
-const TOKEN_API = "https://discord.com/api/oauth2/token";
+import { OAuth2 } from "src/util";
+import { ENVIRONMENT } from "src/environment";
 
 interface CodeResponse {
     code?: string;
 }
 
-interface AccessTokenResponse {
-    // eslint-disable-next-line camelcase
-    access_token: string;
-    // eslint-disable-next-line camelcase
-    token_type: string;
-    // eslint-disable-next-line camelcase
-    expires_in: number;
-    // eslint-disable-next-line camelcase
-    refresh_token: string;
-    // eslint-disable-next-line camelcase
-    scope: string;
-}
-
 export const get = async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
     const data = req.query as CodeResponse;
     if (!data.code) {
         res.redirect("/");
         return;
     }
-    const result = await got<AccessTokenResponse>(TOKEN_API, {
-        method: "post",
-        form: {
-            client_id: process.env.BOT_CLIENT_ID,
-            client_secret: process.env.BOT_CLIENT_SECRET,
-            grant_type: "authorization_code",
-            code: data.code,
-            scope: "identify",
-            redirect_uri: process.env.WEB_AUTH_URI,
-        },
+    const result = await OAuth2.getToken({
+        client_id: ENVIRONMENT.clientId,
+        client_secret: ENVIRONMENT.clientSecret,
+        grant_type: "authorization_code",
+        code: data.code,
+        scope: "identify",
+        redirect_uri: ENVIRONMENT.redirectUrl,
     });
-    console.log(result.body);
+    if (result.ok) {
+        console.log(result.access_token);
+    }
     res.redirect("/");
 };

+ 7 - 1
web/src/routes/index.svelte

@@ -1,12 +1,18 @@
 <script lang="typescript">
+	import Icon from "svelte-awesome/components/Icon.svelte";
+	import { faDiscord } from "@fortawesome/free-brands-svg-icons";
 	import ExampleComponent from "../components/ExampleComponent.svelte";
+
+	const discordIcon = faDiscord as unknown as undefined;
 </script>
 
 <div class="flex items-center justify-center bg-gray-900 h-screen w-screen">
 	<div class="bg-gray-800 rounded-sm text-center px-20 py-5 shadow-lg">
 		<h1 class="text-white text-4xl font-light">Login</h1>
 		<p class="px-10 py-5">
-			<a class="text-sm bg-blue-700 hover:bg-blue-800 p-2 text-white hover:text-gray-100 rounded-sm" href="/login">Login with Discord</a>
+			<a class="text-sm bg-blue-700 hover:bg-blue-800 p-2 text-white hover:text-gray-100 rounded-sm" href="/login">
+				<Icon data={discordIcon} /> Login with Discord
+			</a>
 		</p>
 	</div>
 </div>

+ 6 - 7
web/src/routes/login.ts

@@ -1,14 +1,13 @@
 import { Request as ExpressRequest, Response as ExpressResponse } from "express";
 import { stringify } from "querystring";
-
-const AUTHORIZE_URL = "https://discord.com/api/oauth2/authorize";
+import { OAuth2 } from "src/util";
+import { ENVIRONMENT } from "src/environment";
 
 export const get = async (req: ExpressRequest, res: ExpressResponse): Promise<void> => {
-    const params = stringify({
-        client_id: process.env.BOT_CLIENT_ID,
-        redirect_url: process.env.WEB_AUTH_URI,
+    res.redirect(OAuth2.getAuthUrl({
+        client_id: ENVIRONMENT.clientId,
+        redirect_url: ENVIRONMENT.redirectUrl,
         response_type: "code",
         scope: "identify",
-    });
-    res.redirect(`${AUTHORIZE_URL}?${params}`);
+    }));
 };

+ 55 - 0
web/src/util.ts

@@ -0,0 +1,55 @@
+/* eslint-disable camelcase */
+import * as querystring from "querystring";
+import { Option, tryDo, isHttpError } from "@shared/common/async_utils";
+import got, { HTTPError } from "got";
+
+const OAUTH_API = "https://discord.com/api/oauth2";
+
+export interface AccessTokenResponse {
+    access_token: string;
+    token_type: string;
+    expires_in: number;
+    refresh_token: string;
+    scope: string;
+}
+
+type GrantType = "authorization_code";
+type TokenType = "code" | "token";
+
+export interface AccessTokenRequest {
+    client_id: string;
+    client_secret: string;
+    grant_type: GrantType;
+    code: string;
+    scope: string;
+    redirect_uri: string;
+}
+
+export type AuthorizeRequest = {
+    client_id: string,
+    redirect_url: string,
+    response_type: TokenType,
+    scope: string,
+}
+
+export class OAuth2 {
+    static getAuthUrl(opts: AuthorizeRequest): string {
+        return `${OAUTH_API}/authorize?${querystring.stringify(opts)}`;
+    }
+
+    static async getToken(opts: AccessTokenRequest):
+                        Promise<Option<AccessTokenResponse, { error: string }>> {
+        const result = await tryDo(got<AccessTokenResponse>(`${OAUTH_API}/token`, {
+            method: "post",
+            form: opts,
+        }));
+
+        if (!result.ok) {
+            if (isHttpError<HTTPError>(result.error)) {
+                return { error: `Failed to authenticate. Error: ${result.error.message}`, ok: false };
+            }
+            return { error: "Unexpected error", ok: false };
+        }
+        return { ok: true, ...result.result.body };
+    }
+}