Browse Source

Write rules to file

ghorsington 3 years ago
parent
commit
f8e9e9cc0f

+ 29 - 19
bot/src/rpc.ts

@@ -8,36 +8,46 @@ import { client } from "./client";
 import { tryDo } from "@shared/common/async_utils";
 import { getRepository } from "typeorm";
 import { GuildVerification } from "@shared/db/entity/GuildVerification";
+import { isAuthorisedAsync } from "./util";
+import { GuildMember } from "discord.js";
 
 const PORT = +(process.env.RPC_PORT ?? "8181");
 
 const app = express();
 
+async function checkUser(userId: string, check: (user: GuildMember) => Promise<boolean>): Promise<boolean> {
+    const verificationGuildRepo = getRepository(GuildVerification);
+    const guilds = await verificationGuildRepo.find({
+        select: [
+            "guildId"
+        ]
+    });
+    for (const guild of guilds) {
+        const guildInstance = await tryDo(client.bot.guilds.fetch(guild.guildId));
+        if (!guildInstance.ok) {
+            logger.error("Failed to fetch guild instance for guild %s: %s", guild.guildId, guildInstance.error);
+            continue;
+        }
+        const user = await tryDo(guildInstance.result.members.fetch(userId));
+        if (user.ok && await check(user.result)) {
+            return true;
+        }
+    }
+    return false;
+}
+
 const handler: ModuleRpcServer.ServiceHandlerFor<typeof NoctBotService> = {
     async getPing({ ping }): Promise<{ text: string }> {
         return { text: `pong: ${ping}` };
     },
 
     async userInServer({ userId }): Promise<{ exists: boolean }> {
-        const verificationGuildRepo = getRepository(GuildVerification);
-        const guilds = await verificationGuildRepo.find({
-            select: [
-                "guildId"
-            ]
-        });
-        for (const guild of guilds) {
-            const guildInstance = await tryDo(client.bot.guilds.fetch(guild.guildId));
-            if (!guildInstance.ok) {
-                logger.error("Failed to fetch guild instance for guild %s: %s", guild.guildId, guildInstance.error);
-                continue;
-            }
-            const user = await tryDo(guildInstance.result.members.fetch(userId));
-            if (user.ok) {
-                return { exists: true };
-            }
-        }
-        return { exists: false };
-    }
+        return { exists: await checkUser(userId, async () => true) };
+    },
+
+    async userAuthorised({ userId }): Promise<{ authorised: boolean }> {
+        return { authorised: await checkUser(userId, (user) => isAuthorisedAsync(user)) };
+    },
 };
 
 app.use(ModuleRpcProtocolServer.registerRpcRoutes(NoctBotService, handler));

+ 4 - 1
docker-compose.yml

@@ -51,6 +51,8 @@ services:
       TYPEORM_DATABASE: ${DB_NAME}
     ports:
       - 3020:3000
+    volumes:
+      - web-data:/web_data
 
   db:
     image: postgres
@@ -70,4 +72,5 @@ services:
       - 3030:8080
 
 volumes:
-  db-data:
+  db-data:
+  web-data:

+ 4 - 0
shared/src/rpc/backend.ts

@@ -6,5 +6,9 @@ export const NoctBotService = {
     userInServer: {
         request: {} as { userId: string },
         response: {} as { exists: boolean },
+    },
+    userAuthorised: {
+        request: {} as { userId: string },
+        response: {} as { authorised: boolean },
     }
 };

+ 118 - 22
web/src/routes/rules/create.svelte

@@ -1,35 +1,131 @@
+<script lang="typescript" context="module">
+  import type { AppSession, PageData, PreloadContext } from "src/utils/session";
+  export async function preload(
+    this: PreloadContext,
+    { path }: PageData,
+    session: AppSession
+  ) {
+    const result = await this.fetch("/rules/md");
+    const md = (await result.json()) as Option<MDText, { error: string }>;
+    if (md.ok) {
+      return { rulesText: md.text };
+    }
+    return { rulesText: "" };
+  }
+</script>
+
 <script lang="typescript">
-    import { onMount } from "svelte";
-    import "easymde/dist/easymde.min.css";
-
-    let vpHeight: number;
-    let textArea!: HTMLElement;
-    onMount(async () => {
-        const EasyMDE = (await import("easymde")).default;
-        let mde = new EasyMDE({
-            element: textArea,
-            maxHeight: `50vh`,
-            minHeight: `50vh`,
-            previewClass: "md-dark md-editor-preview md-body",
-        });
+  import { onMount } from "svelte";
+  import "easymde/dist/easymde.min.css";
+  import { fade } from "svelte/transition";
+  import type { MDText } from "./md_interfaces";
+  import { Option } from "@shared/common/async_utils";
+
+  export let rulesText!: string;
+  let vpHeight: number;
+  let textArea!: HTMLTextAreaElement;
+  let statusMessage: string = "";
+  let error = false;
+  onMount(async () => {
+    const EasyMDE = (await import("easymde")).default;
+    let mde = new EasyMDE({
+      element: textArea,
+      previewClass: "md-dark md-editor-preview md-body",
+      forceSync: true,
+      initialValue: rulesText,
+    });
+  });
+
+  async function save() {
+    const result = await fetch("/rules/md", {
+      method: "post",
+      headers: {
+        Accept: "application/json",
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify({ text: textArea.value } as MDText),
     });
+    const opt = (await result.json()) as Option<unknown, { error: string }>;
+
+    let waitTime = 2000;
+    if (opt.ok) {
+      statusMessage = "Saved!";
+      error = false;
+    } else {
+      statusMessage = opt.error;
+      error = true;
+      waitTime = 10000;
+    }
+    setTimeout(() => {
+      statusMessage = "";
+      error = false;
+    }, waitTime);
+  }
 </script>
 
 <style>
-    @import "./markdown-dark.css";
-    @import "./easymde-style.css";
+  @import "./markdown-dark.css";
+  @import "./easymde-style.css";
+
+  .viewport {
+    @apply bg-gray-800 rounded-sm px-20 py-5 shadow-lg w-screen;
+    height: 100vh;
+  }
+
+  span.status {
+    @apply text-white px-4;
 
+    &.error {
+      @apply text-red-600;
+    }
+  }
+
+  .save-button {
+    @apply text-base bg-green-600 p-2 px-3 text-white rounded-sm;
+
+    &:hover {
+      @apply bg-green-800 text-gray-100 cursor-pointer;
+    }
+  }
+
+  :global .CodeMirror-scroll {
+    max-height: calc(90vh - 200px) !important;
+    min-height: calc(90vh - 200px) !important;
+  }
+
+  @screen lg {
     .viewport {
-        @apply bg-gray-800 rounded-sm px-20 py-5 shadow-lg w-3/4;
-        height: 80vh;
+      @apply w-3/4;
+      height: 80vh;
+    }
+
+    :global .CodeMirror-scroll {
+      max-height: calc(70vh - 200px) !important;
+      min-height: calc(70vh - 200px) !important;
     }
+  }
+
+  #easy-mde {
+    visibility: hidden;
+  }
 </style>
 
+<svelte:head>
+  <title>Edit rules</title>
+</svelte:head>
 <div class="viewport" bind:clientHeight={vpHeight}>
-    <h1 class="text-white text-4xl font-light py-2">Edit rules</h1>
+  <h1 class="text-white text-4xl font-light py-2">Edit rules</h1>
+  <form on:submit|preventDefault={save}>
     <p class="text-white md-editor">
-        <textarea id="easy-mde" bind:this={textArea}></textarea>
+      <textarea id="easy-mde" bind:this={textArea} />
     </p>
-    <a class="text-md float-right bg-green-600 hover:bg-green-800 p-2 px-3 text-white hover:text-gray-100 rounded-sm" href="#">Save</a>
-  </div>
-
+    <span class="float-right">
+      {#if statusMessage}
+        <span
+          transition:fade={{ duration: 100 }}
+          class="status {error ? 'error' : ''}">{statusMessage}</span>
+      {/if}
+      <input type="submit" class="save-button" value="Save" />
+    </span>
+  </form>
+</div>

+ 56 - 0
web/src/routes/rules/md.ts

@@ -0,0 +1,56 @@
+import { Request as ExpressRequest, Response as ExpressResponse } from "express";
+import { existsSync, promises } from "fs";
+import { join } from "path";
+import { ENVIRONMENT } from "src/utils/environment";
+import { Option } from "@shared/common/async_utils";
+import { rpcClient } from "src/utils/rpc";
+import { MDText } from "./md_interfaces";
+
+const FILE_PATH = join(ENVIRONMENT.dataPath, "rules.md");
+
+type GetResult = Promise<ExpressResponse<Option<MDText, { error: string }>>>;
+export const get = async (req: ExpressRequest, res: ExpressResponse): GetResult => {
+    if (!existsSync(FILE_PATH)) {
+        return res.json({
+            ok: true,
+            text: "",
+        });
+    }
+    const fileData = await promises.readFile(FILE_PATH);
+    return res.json({
+        ok: true,
+        text: fileData.toString("utf-8"),
+    });
+};
+
+type PostResult = Promise<ExpressResponse<Option<unknown, { error: string }>>>;
+export const post = async (req: ExpressRequest, res: ExpressResponse): PostResult => {
+    const isText = (body: unknown):
+        body is MDText => (body as Record<string, unknown>).text !== undefined;
+
+    if (!isText(req.body)) {
+        return res.json({
+            ok: false,
+            error: "No text",
+        });
+    }
+
+    if (!req.session?.userId) {
+        return res.json({
+            ok: false,
+            error: "Not logged in, please log in",
+        });
+    }
+
+    const { authorised } = await rpcClient.userAuthorised({ userId: req.session.userId });
+    if (!authorised) {
+        return res.json({
+            ok: false,
+            error: "Not authorised, please log in",
+        });
+    }
+
+    await promises.writeFile(FILE_PATH, req.body.text);
+
+    return res.json({ ok: true });
+};

+ 3 - 0
web/src/routes/rules/md_interfaces.ts

@@ -0,0 +1,3 @@
+export interface MDText {
+    text: string;
+}

+ 1 - 0
web/src/server.ts

@@ -33,6 +33,7 @@ const createSapperServer = async (): Promise<Express> => {
 
     const app = express();
 
+    app.use(express.json());
     app.use(
         session({
             secret: key,

+ 5 - 1
web/src/utils/environment.ts

@@ -10,6 +10,7 @@ if (process.env.NODE_ENV === "development") {
 
     process.env.TYPEORM_HOST = "localhost";
     process.env.NOCTBOT_ADDR = "localhost";
+    process.env.WEB_DATA_PATH = "./web_data";
     process.env.TYPEORM_USERNAME = process.env.DB_USERNAME;
     process.env.TYPEORM_PASSWORD = process.env.DB_PASSWORD;
     process.env.TYPEORM_DATABASE = process.env.DB_NAME;
@@ -19,14 +20,17 @@ export interface BotEnvironment {
     clientId: string;
     redirectUrl: string;
     clientSecret: string;
+    dataPath: string;
 }
 
 export const ENVIRONMENT: BotEnvironment = {
     clientId: process.env.BOT_CLIENT_ID ?? "",
     redirectUrl: process.env.WEB_AUTH_URI ?? "",
     clientSecret: process.env.BOT_CLIENT_SECRET ?? "",
+    dataPath: process.env.WEB_DATA_PATH ?? "",
 };
 
 export const IS_VALID = process.env.BOT_CLIENT_ID !== undefined
                         && process.env.WEB_AUTH_URI !== undefined
-                        && process.env.BOT_CLIENT_SECRET !== undefined;
+                        && process.env.BOT_CLIENT_SECRET !== undefined
+                        && process.env.WEB_DATA_PATH !== undefined;

+ 3 - 0
web/web_data/rules.md

@@ -0,0 +1,3 @@
+# Hello, world!
+
+ads