aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuca Matei Pintilie <lucafulger@gmail.com>2022-07-02 20:39:59 +0000
committerLuca Matei Pintilie <lucafulger@gmail.com>2022-07-02 20:39:59 +0000
commit3eb2e3502f497a18d8b445516c004e245dbc90ad (patch)
tree2f4c16bdd391971d519a18e48b4eebe019c35f05
parentcda94e3571d1ad7d2415204ec1651f0a747a40d1 (diff)
downloadspeedrunbot-slash-3eb2e3502f497a18d8b445516c004e245dbc90ad.tar
speedrunbot-slash-3eb2e3502f497a18d8b445516c004e245dbc90ad.tar.gz
speedrunbot-slash-3eb2e3502f497a18d8b445516c004e245dbc90ad.tar.bz2
speedrunbot-slash-3eb2e3502f497a18d8b445516c004e245dbc90ad.tar.lz
speedrunbot-slash-3eb2e3502f497a18d8b445516c004e245dbc90ad.tar.xz
speedrunbot-slash-3eb2e3502f497a18d8b445516c004e245dbc90ad.tar.zst
speedrunbot-slash-3eb2e3502f497a18d8b445516c004e245dbc90ad.zip
Refactor code a bit and switch from fresh to nanojsx
-rw-r--r--.github/workflows/deno.yml5
-rw-r--r--CONTRIBUTING.md7
-rw-r--r--deps_frontend.ts1
-rw-r--r--deps_general.ts7
-rw-r--r--deps_server.ts20
-rwxr-xr-xmain.ts39
-rwxr-xr-xsrc/client.ts39
-rw-r--r--src/config.ts7
-rw-r--r--src/pages/admin.tsx252
-rw-r--r--src/pages/api/discord/bot_info.ts18
-rw-r--r--src/pages/api/discord/interactions.ts45
-rw-r--r--src/pages/api/discord/reload.ts67
-rw-r--r--src/pages/api/discord/token.ts42
-rw-r--r--src/pages/index.tsx16
-rw-r--r--src/pages/logout.ts11
-rw-r--r--src/pages/mod.tsx60
-rwxr-xr-xsrc/srcom/categories.ts22
-rwxr-xr-xsrc/srcom/categories_played.ts3
-rwxr-xr-xsrc/srcom/game_info.ts8
-rwxr-xr-xsrc/srcom/games.ts3
-rwxr-xr-xsrc/srcom/world_record.ts6
-rw-r--r--src/standalone_client.ts9
-rw-r--r--src/utils.ts36
-rw-r--r--tests/srcom/world_records.test.ts4
24 files changed, 431 insertions, 296 deletions
diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml
index 97a98fd..076b8e6 100644
--- a/.github/workflows/deno.yml
+++ b/.github/workflows/deno.yml
@@ -25,6 +25,9 @@ jobs:
- name: Run linter
run: deno lint
+ - name: Run type checker
+ run: deno check main.ts
+
- name: Run tests
run: deno test -A
@@ -46,4 +49,4 @@ jobs:
deno-version: v1.x
- name: Update commands
- run: ./src/client.ts -u
+ run: ./main.ts -u
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1f28c80..3e608c5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -30,14 +30,17 @@ project and put in the following
```sh
TOKEN="YOUR BOT'S DISCORD TOKEN"
TEST_SERVER="12345" # Optional discord guild/server ID
+# For the website part
+CLIENT_SECRET="abcd"
+PUBLIC_KEY="abcd"
```
To run the discord bot development mode use the following command
```sh
-deno run --allow-net=www.speedrun.com,gateway.discord.gg,discord.com --allow-env --allow-read=. src/client.ts
+deno run --allow-net=www.speedrun.com,gateway.discord.gg,discord.com --allow-env --allow-read=. ./mod.ts
# Or if on unix
-src/client.ts
+./mod.ts
```
The permissions required are as follows:
diff --git a/deps_frontend.ts b/deps_frontend.ts
deleted file mode 100644
index 56b96fc..0000000
--- a/deps_frontend.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "https://raw.githubusercontent.com/lucacasonato/fresh/d1076b0ad1b420aec689324b3342e543c0d5a591/runtime.ts";
diff --git a/deps_general.ts b/deps_general.ts
index e5c0eaa..effcc7e 100644
--- a/deps_general.ts
+++ b/deps_general.ts
@@ -1,6 +1,5 @@
export { TimeDelta } from "https://esm.sh/@silane/datetime@1.2.1";
-export { parse } from "https://deno.land/std@0.128.0/flags/mod.ts";
-export { delay } from "https://deno.land/std@0.128.0/async/mod.ts";
-export { bold } from "https://deno.land/std@0.128.0/fmt/colors.ts";
-export { groupBy } from "https://deno.land/std@0.128.0/collections/mod.ts";
+export { delay } from "https://deno.land/std@0.146.0/async/mod.ts";
+export { bold } from "https://deno.land/std@0.146.0/fmt/colors.ts";
+export { groupBy } from "https://deno.land/std@0.146.0/collections/group_by.ts";
export { Moogle } from "https://deno.land/x/moogle@v1.0.0/mod.ts";
diff --git a/deps_server.ts b/deps_server.ts
index 825acd1..cc22b93 100644
--- a/deps_server.ts
+++ b/deps_server.ts
@@ -17,10 +17,18 @@ export type {
SlashCommandPartial,
} from "https://code.harmony.rocks/08dc7b99089d94d62d6c87bf26e279746dfe418a/mod.ts";
-// Fresh, the web framework
-export { start } from "https://raw.githubusercontent.com/lucacasonato/fresh/d1076b0ad1b420aec689324b3342e543c0d5a591/server.ts";
-export type { HandlerContext } from "https://raw.githubusercontent.com/lucacasonato/fresh/d1076b0ad1b420aec689324b3342e543c0d5a591/server.ts";
+export { config } from "https://deno.land/std@0.146.0/dotenv/mod.ts";
-// Auto load the .env file
-// Which contains `TOKEN` and `TEST_SERVER`
-import "https://deno.land/x/dotenv@v3.2.0/load.ts";
+export {
+ deleteCookie,
+ getCookies,
+ serve,
+ setCookie,
+ Status,
+} from "https://deno.land/std@0.146.0/http/mod.ts";
+
+export {
+ h,
+ Helmet,
+ renderSSR,
+} from "https://deno.land/x/nano_jsx@v0.0.33/mod.ts";
diff --git a/main.ts b/main.ts
index 3686610..6ca0bb7 100755
--- a/main.ts
+++ b/main.ts
@@ -1,18 +1,35 @@
-#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --allow-run --no-check
-import { SpeedRunBot } from "./src/client.ts";
-import { start } from "./deps_server.ts";
-import { client } from "./src/pages/api/discord/interactions.ts";
-import routes from "./src/routes.gen.ts";
-import { SpeedrunCom } from "./src/srcom/slash_commands.ts";
+#!/usr/bin/env -S deno run --allow-net --allow-read=.env --no-check
+import { serve } from "./deps_server.ts";
+import { config } from "./src/config.ts";
+import { handler } from "./src/pages/mod.tsx";
+import { commands } from "./src/srcom/slash_commands.ts";
+import { SpeedRunBot } from "./src/standalone_client.ts";
+import { isDeployed } from "./src/utils.ts";
if (import.meta.main) {
- if (!Deno.env.get("DENO_DEPLOYMENT_ID")) {
+ if (!isDeployed) {
const client = new SpeedRunBot({
intents: [],
- token: Deno.env.get("TOKEN"),
+ token: config.TOKEN,
});
- client.connect();
+ console.log("Connecting...");
+ await client.connect();
+ console.log("Connected!");
+ console.log("Editting commands...");
+ await client.interactions.commands.bulkEdit(
+ commands,
+ config.TEST_SERVER,
+ );
+
+ console.log("Editted!");
+
+ if (Deno.args.includes("--update") || Deno.args.includes("-u")) {
+ console.log("Goodbye!");
+ await client.close();
+ Deno.exit(0);
+ }
}
- start(routes);
- client.loadModule(new SpeedrunCom());
+ if ((await Deno.permissions.query({ name: "net" })).state === "granted") {
+ serve(handler);
+ } else console.warn("Webserver is NOT running!");
}
diff --git a/src/client.ts b/src/client.ts
index 5077aec..f1f894c 100755
--- a/src/client.ts
+++ b/src/client.ts
@@ -1,33 +1,10 @@
-#!/usr/bin/env -S deno run --allow-net=www.speedrun.com,gateway.discord.gg,discord.com --allow-env --allow-read=. --no-check
-import { commands, SpeedrunCom } from "./srcom/slash_commands.ts";
-import { Client, event } from "../deps_server.ts";
-import { parse } from "../deps_general.ts";
+import { SpeedrunCom } from "./srcom/slash_commands.ts";
+import { InteractionsClient } from "../deps_server.ts";
+import { config } from "./config.ts";
-export class SpeedRunBot extends Client {
- @event()
- ready() {
- this.interactions.loadModule(new SpeedrunCom());
- }
-}
+export const client = new InteractionsClient({
+ token: config.TOKEN,
+ publicKey: config.PUBLIC_KEY,
+});
-if (import.meta.main) {
- const client = new SpeedRunBot({
- intents: [],
- token: Deno.env.get("TOKEN"),
- });
- console.log("Connecting...");
- await client.connect();
- console.log("Connected!");
- console.log("Editting commands...");
- await client.interactions.commands.bulkEdit(
- commands,
- Deno.env.get("TEST_SERVER"),
- );
- console.log("Editted!");
- const args = parse(Deno.args);
- if (args.update || args.u) {
- console.log("Goodbye!");
- await client.close();
- Deno.exit(0);
- }
-}
+client.loadModule(new SpeedrunCom());
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..af43738
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,7 @@
+import { config as dotenvConfig } from "../deps_server.ts";
+
+export const config = await dotenvConfig({
+ export: false,
+ // @ts-ignore This can be false trust me dude
+ defaults: false,
+});
diff --git a/src/pages/admin.tsx b/src/pages/admin.tsx
index 5044051..ea69274 100644
--- a/src/pages/admin.tsx
+++ b/src/pages/admin.tsx
@@ -1,81 +1,205 @@
/** @jsx h */
-import { h, IS_BROWSER, useEffect, useState } from "../../deps_frontend.ts";
-import type { PageConfig } from "../../deps_frontend.ts";
-import { DISCORD_URL } from "../utils.ts";
-import type { BotInfo } from "./api/discord/bot_info.ts";
-import type { Token } from "./api/discord/token.ts";
-
-export const config: PageConfig = { runtimeJS: true };
-
-export default function Admin() {
- const [botInfo, setBotInfo] = useState<BotInfo | null>(null);
- const [code, setCode] = useState("");
- const [token, setToken] = useState("");
- const [status, setStatus] = useState("");
- useEffect(() => {
- setStatus("Fetching bot info");
- fetch("/api/discord/bot_info").then((res) => res.json()).then((data) =>
- setBotInfo(data)
- ).then(() => setStatus("Fetched bot info!"));
- }, [setBotInfo, setStatus]);
-
- useEffect(() => {
- if (IS_BROWSER) {
- const url = new URL(location.href);
- if (url.searchParams.has("code")) setCode(url.searchParams.get("code")!);
+import {
+ deleteCookie,
+ getCookies,
+ h,
+ setCookie,
+ Status,
+} from "../../deps_server.ts";
+import { DISCORD_URL, renderPage } from "../utils.ts";
+import { client } from "../client.ts";
+import { config } from "../config.ts";
+import { SpeedRunBot } from "../standalone_client.ts";
+import { commands } from "../srcom/slash_commands.ts";
+
+// User structure that comes from discord
+interface User {
+ id: string;
+ username: string;
+ discriminator: string;
+ avatar: string;
+ verified: boolean;
+ email: string;
+ flags: number;
+ banner: string;
+ accent_color: number;
+ premium_type: number;
+ public_flags: number;
+}
+
+// Access token response from discord
+interface AccessTokenResponse {
+ access_token: string;
+ token_type: "Bearer";
+ expires_in: number;
+ refresh_token: string;
+ scope: string;
+}
+
+// The application has the team/owner of the bot
+// Which is used to see if the client has permission to reload the commands
+const application = await client.fetchApplication();
+
+export default async (req: Request): Promise<Response> => {
+ const { pathname, searchParams, origin } = new URL(req.url);
+ const location = `${origin}${pathname}`;
+ const code = searchParams.get("code");
+ const access_token = getCookies(req.headers).access_token;
+
+ if (req.method === "DELETE") {
+ const headers = new Headers();
+ deleteCookie(headers, "access_token");
+
+ return renderPage(
+ <Admin location={location} message={"Logged out!"} />,
+ );
+ } // The user has triggered a reload
+ else if (req.method === "POST") {
+ if (!access_token) {
+ return renderPage(
+ <Admin location={location} message={"Not logged in"} />,
+ );
+ }
+ const user = await getUserFromToken(access_token);
+ if (isUserAnOwner(user)) {
+ // reload commands
+ const client = new SpeedRunBot({
+ intents: [],
+ token: config.TOKEN,
+ });
+ await client.connect();
+ await client.interactions.commands.bulkEdit(
+ commands,
+ config.TEST_SERVER,
+ );
+ return renderPage(
+ <Admin location={location} message={"Commands succesfully updated!"} />,
+ );
+ } else {
+ return renderPage(
+ <Admin location={location} message={"Access denied"} />,
+ );
}
- }, [setCode]);
-
- useEffect(() => {
- if (!code) return;
- setStatus("Fetching token");
- fetch(`/api/discord/token`, {
- method: "POST",
- body: JSON.stringify({
- code,
- redirect_uri: location.origin + location.pathname,
- }),
- }).then(async (res) => {
- const data = await res.json() as Token;
- if (res.ok) {
- setToken(data.access_token);
- } else {
- // @ts-ignore if it's an error the response will be unexpected
- setStatus(`Error: ${data.error_description}`);
- setCode("");
- }
+ } // The user just came back from discord
+ // And authorized the app
+ else if (code) {
+ const headers = new Headers({
+ "Location": location,
+ });
+
+ const access_token = await exchangeCodeForToken(code, location);
+
+ setCookie(headers, {
+ name: "access_token",
+ httpOnly: true,
+ value: access_token,
});
- }, [code, setToken]);
-
- const reload = () => {
- if (!token) return;
- setStatus("Reloading commands");
- fetch("/api/discord/reload", {
- method: "POST",
- body: JSON.stringify({ access_token: token }),
- headers: {
- "Content-Type": "application/json",
- },
- }).then((res) => res.json()).then((data) => setStatus(data.message));
- };
+ return new Response(null, {
+ headers,
+ status: Status.TemporaryRedirect,
+ });
+ } // The user is now logged in
+ else if (access_token) {
+ const user = await getUserFromToken(access_token);
+ return renderPage(<Admin location={location} user={user} />);
+ } else return renderPage(<Admin location={location} />);
+};
+
+export function Admin(
+ { user, location, message }: {
+ user?: User;
+ location: string;
+ message?: string;
+ },
+) {
return (
<div>
- <h1>Speedrun.bot admin panel</h1>
- <p>{status}</p>
- {!code
- ? (botInfo?.id
+ <h1>
+ Speedrun.bot admin panel
+ </h1>
+
+ <h2>
+ {message}
+ </h2>
+
+ {!user
+ ? (client.id
? (
<a
- href={`${DISCORD_URL}/oauth2/authorize?client_id=${botInfo.id}&scope=identify&response_type=code&redirect_uri=${
- encodeURIComponent(location.origin + location.pathname)
+ href={`${DISCORD_URL}/oauth2/authorize?client_id=${client.id}&scope=identify&response_type=code&redirect_uri=${
+ encodeURIComponent(location)
}`}
>
Click here to log in
</a>
)
: "Loading...")
- : <button onClick={reload}>Reload commands?</button>}
+ : (
+ // If the authorized user is in the team
+ // Or is the owner of the application
+ // Allow them to reload commands
+ isUserAnOwner(user)
+ ? (
+ <form method="POST">
+ <button type="submit">
+ Reload commands?
+ </button>
+ </form>
+ )
+ : "Access denied"
+ )}
+ {user && (
+ <form action="/logout">
+ <button type="submit">
+ Log out
+ </button>
+ </form>
+ )}
</div>
);
}
+
+async function exchangeCodeForToken(
+ code: string,
+ redirect_uri: string,
+): Promise<string> {
+ const client_secret = config.CLIENT_SECRET;
+ if (!client_secret) {
+ throw new Error(
+ "CLIENT_SECRET is not set!!! Tell an admin about this. Wait, you're the admin?",
+ );
+ }
+
+ // https://discord.com/developers/docs/topics/oauth2#authorization-code-grant-access-token-response
+ const res = await fetch(`${DISCORD_URL}/oauth2/token`, {
+ method: "POST",
+ body: new URLSearchParams({
+ "client_id": typeof client.id === "function" ? client.id() : client.id,
+ client_secret,
+ "grant_type": "authorization_code",
+ code,
+ redirect_uri,
+ }).toString(),
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ });
+ const data = await res.json() as AccessTokenResponse;
+ return data.access_token;
+}
+
+async function getUserFromToken(access_token: string): Promise<User> {
+ const res = await fetch(`${DISCORD_URL}/users/@me`, {
+ headers: {
+ "Authorization": `Bearer ${access_token}`,
+ },
+ });
+ const user = await res.json() as User;
+ return user;
+}
+
+function isUserAnOwner(user: User): boolean {
+ return application.team?.members.map((user) => user.id).includes(user.id) ||
+ application.owner?.id === user.id;
+}
diff --git a/src/pages/api/discord/bot_info.ts b/src/pages/api/discord/bot_info.ts
deleted file mode 100644
index efd62e3..0000000
--- a/src/pages/api/discord/bot_info.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { client } from "./interactions.ts";
-
-export interface BotInfo {
- id: string;
- owners: string[];
-}
-
-export const handler = {
- async GET(): Promise<Response> {
- const applicationInfo = await client.fetchApplication();
- return new Response(JSON.stringify({
- id: client.getID(),
- owners: applicationInfo.team
- ? applicationInfo.team.members.map((user) => user.id)
- : [applicationInfo.owner?.id],
- }));
- },
-};
diff --git a/src/pages/api/discord/interactions.ts b/src/pages/api/discord/interactions.ts
index c7cd798..4d48c39 100644
--- a/src/pages/api/discord/interactions.ts
+++ b/src/pages/api/discord/interactions.ts
@@ -1,27 +1,24 @@
-import { HandlerContext, InteractionsClient } from "../../../../deps_server.ts";
-import { SpeedrunCom } from "../../../srcom/slash_commands.ts";
+import { Status } from "../../../../deps_server.ts";
+import { client } from "../../../client.ts";
-export const client = new InteractionsClient({
- token: Deno.env.get("TOKEN"),
- publicKey: Deno.env.get("PUBLIC_KEY"),
-});
-
-client.loadModule(new SpeedrunCom());
-
-export const handler = {
- POST(ctx: HandlerContext): Promise<Response> {
- const req = ctx.req;
- // deno-lint-ignore no-async-promise-executor
- return new Promise(async (res) => {
- const interaction = await client.verifyFetchEvent({
- respondWith: res,
- request: req,
- });
- if (interaction === false) {
- return res(new Response(null, { status: 401 }));
- }
- if (interaction.type === 1) return interaction.respond({ type: 1 });
- await client._process(interaction);
+export default (req: Request): Promise<Response> | Response => {
+ if (req.method !== "POST") {
+ return Response.json({
+ message: "Bad method",
+ }, {
+ status: Status.NotFound,
+ });
+ }
+ // deno-lint-ignore no-async-promise-executor
+ return new Promise(async (res) => {
+ const interaction = await client.verifyFetchEvent({
+ respondWith: res,
+ request: req,
});
- },
+ if (interaction === false) {
+ return res(new Response(null, { status: Status.Unauthorized }));
+ }
+ if (interaction.type === 1) return interaction.respond({ type: 1 });
+ await client._process(interaction);
+ });
};
diff --git a/src/pages/api/discord/reload.ts b/src/pages/api/discord/reload.ts
deleted file mode 100644
index f867dfd..0000000
--- a/src/pages/api/discord/reload.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { client } from "./interactions.ts";
-import { DISCORD_URL } from "../../../utils.ts";
-import { commands } from "../../../srcom/slash_commands.ts";
-import { HandlerContext } from "../../../../deps_server.ts";
-
-interface ReqBody {
- // deno-lint-ignore camelcase
- access_token: string;
-}
-
-const bad = new Response(
- JSON.stringify({
- code: 400,
- message: "Bad request",
- }),
- {
- status: 400,
- },
-);
-
-export const handler = {
- async POST(ctx: HandlerContext): Promise<Response> {
- const req = ctx.req;
- const admin = await isAdmin(req);
- if (admin) {
- await client.commands.bulkEdit(commands);
- return new Response(JSON.stringify({
- code: 200,
- message: "Updated commands",
- }));
- } else {
- return new Response(
- JSON.stringify({
- code: 403,
- message: "You are not this bot's owner",
- }),
- {
- status: 403,
- },
- );
- }
- },
-};
-
-async function isAdmin(input: string | Request): Promise<boolean> {
- // deno-lint-ignore camelcase
- let access_token: string;
- if (input instanceof Request) {
- if (input.method !== "POST") {
- throw bad;
- }
- if (!input.headers.get("content-type")?.includes("json")) throw bad;
- const data = await input.json() as ReqBody;
- if (!("access_token" in data)) throw bad;
- access_token = data.access_token;
- } else access_token = input;
-
- const userReq = await fetch(`${DISCORD_URL}/users/@me`, {
- headers: {
- "authorization": `Bearer ${access_token}`,
- },
- });
- const user = await userReq.json();
- const applicationInfo = await client.fetchApplication();
- return (applicationInfo.team?.members.includes(user.id) ||
- applicationInfo.owner?.id === user.id);
-}
diff --git a/src/pages/api/discord/token.ts b/src/pages/api/discord/token.ts
deleted file mode 100644
index a4b488d..0000000
--- a/src/pages/api/discord/token.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-// deno-lint-ignore-file camelcase
-import { HandlerContext } from "../../../../deps_server.ts";
-import { DISCORD_URL } from "../../../utils.ts";
-import { client } from "./interactions.ts";
-
-export interface Token {
- access_token: string;
- refresh_token: string;
- expires_in: number;
- token_type: string;
- scope: string;
-}
-
-export const handler = {
- async POST(ctx: HandlerContext): Promise<Response> {
- const req = ctx.req;
- const { code, redirect_uri } = await req.json();
- const body = new URLSearchParams({
- client_id: client.getID(),
- client_secret: Deno.env.get("CLIENT_SECRET")!,
- grant_type: "authorization_code",
- redirect_uri,
- code,
- });
-
- const res = await fetch(`${DISCORD_URL}/oauth2/token`, {
- method: "POST",
- body: body.toString(),
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- },
- });
- const resData = await res.json() as Token;
- return new Response(
- JSON.stringify(resData),
- {
- status: res.status,
- statusText: res.statusText,
- },
- );
- },
-};
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 51b6652..ed085de 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,16 +1,12 @@
/** @jsx h */
-import { h, useEffect, useState } from "../../deps_frontend.ts";
-import { DISCORD_URL } from "../utils.ts";
+import { h } from "../../deps_server.ts";
+import { client } from "../client.ts";
+import { DISCORD_URL, renderPage } from "../utils.ts";
-export const config = { runtimeJS: true };
+export default () => renderPage(<Home />);
-export default function Home() {
- const [id, setId] = useState("");
- useEffect(() => {
- fetch("/api/discord/bot_info").then((res) => res.json()).then((data) =>
- setId(data.id)
- );
- }, [setId]);
+export function Home() {
+ const id = client.id;
return (
<div>
<h1>Speedrun.bot</h1>
diff --git a/src/pages/logout.ts b/src/pages/logout.ts
new file mode 100644
index 0000000..1fc6b2d
--- /dev/null
+++ b/src/pages/logout.ts
@@ -0,0 +1,11 @@
+import { deleteCookie, Status } from "../../deps_server.ts";
+
+export default (req: Request): Response => {
+ const headers = new Headers();
+ headers.set("Location", req.headers.get("referer") || "/");
+ deleteCookie(headers, "access_token");
+ return new Response(null, {
+ headers,
+ status: Status.TemporaryRedirect,
+ });
+};
diff --git a/src/pages/mod.tsx b/src/pages/mod.tsx
new file mode 100644
index 0000000..aba743d
--- /dev/null
+++ b/src/pages/mod.tsx
@@ -0,0 +1,60 @@
+/** @jsx h */
+import { h, Helmet, renderSSR, Status } from "../../deps_server.ts";
+
+// Frontend
+import Index from "./index.tsx";
+import Admin from "./admin.tsx";
+// API
+import DiscordInteractions from "./api/discord/interactions.ts";
+import logout from "./logout.ts";
+import { renderPage } from "../utils.ts";
+
+const routes: Record<
+ string,
+ (req: Request) => Response | Promise<Response>
+> = {
+ "/api/discord/interactions": DiscordInteractions,
+ "/": Index,
+ "/admin": Admin,
+ "/logout": logout,
+};
+
+export function handler(req: Request): Response | Promise<Response> {
+ const { pathname } = new URL(req.url);
+
+ // Check if the requested route is available
+ if (Object.keys(routes).includes(pathname)) {
+ return routes[pathname](req);
+ } // If the route is not available
+ // And the client expects an api route
+ // Return as JSON
+ else if (pathname.includes("api")) {
+ return Response.json({
+ message: "Not found",
+ }, {
+ status: Status.NotFound,
+ });
+ } // Else return a nice website
+ else return renderPage(<NotFound pathname={pathname} />);
+}
+
+function NotFound({ pathname }: {
+ pathname: string;
+}) {
+ return (
+ <div>
+ <h1>
+ Not found
+ </h1>
+ <p>
+ Sorry, but we couldn't find {pathname}.
+ </p>
+ <p>
+ Want to go to the
+ <a href="/">
+ Home page?
+ </a>
+ </p>
+ </div>
+ );
+}
diff --git a/src/srcom/categories.ts b/src/srcom/categories.ts
index 2c0348e..4385a04 100755
--- a/src/srcom/categories.ts
+++ b/src/srcom/categories.ts
@@ -41,11 +41,27 @@ export async function categories(
}
});
output.push(`Categories - ${game}`);
- output.push(`${fmt.bold("Fullgame")}: ${fullGameCategories.length ? fullGameCategories.join(", ") : "No Full Game categories."}`);
output.push(
- `${fmt.bold("Individual Level")}: ${individualLevelCategories.length ? individualLevelCategories.join(", ") : "No Individual Level categories."}`,
+ `${fmt.bold("Fullgame")}: ${
+ fullGameCategories.length
+ ? fullGameCategories.join(", ")
+ : "No Full Game categories."
+ }`,
+ );
+ output.push(
+ `${fmt.bold("Individual Level")}: ${
+ individualLevelCategories.length
+ ? individualLevelCategories.join(", ")
+ : "No Individual Level categories."
+ }`,
+ );
+ output.push(
+ `${fmt.bold("Miscellaneous")}: ${
+ miscCategories.length
+ ? miscCategories.join(", ")
+ : "No Miscellaneous categories."
+ }`,
);
- output.push(`${fmt.bold("Miscellaneous")}: ${miscCategories.length ? miscCategories.join(", ") : "No Miscellaneous categories."}`);
return output.join("\n");
}
diff --git a/src/srcom/categories_played.ts b/src/srcom/categories_played.ts
index ea58a09..2efdfa1 100755
--- a/src/srcom/categories_played.ts
+++ b/src/srcom/categories_played.ts
@@ -1,5 +1,5 @@
#!/usr/bin/env -S deno run --allow-net=www.speedrun.com --allow-env=NO_COLOR --no-check
-import { Format, MarkupType } from "./fmt.ts";
+import { MarkupType } from "./fmt.ts";
import { CommandError, getUser, SRC_API } from "./utils.ts";
import type { Opts } from "./utils.ts";
import type { SpeedrunCom } from "./types.d.ts";
@@ -24,7 +24,6 @@ export async function categoriesPlayed(
{ outputType = "markdown" }: Opts = {},
): Promise<string | CategoriesObject> {
games = games.filter((a) => !!a);
- const fmt = new Format(outputType);
const output: string[] = [];
const categories: string[] = [];
const user = await getUser(username);
diff --git a/src/srcom/game_info.ts b/src/srcom/game_info.ts
index d6a9c79..621b160 100755
--- a/src/srcom/game_info.ts
+++ b/src/srcom/game_info.ts
@@ -60,8 +60,12 @@ export async function gameInfo(
);
if (genres.length) output.push(`${fmt.bold("Genres")}: ${genres.join(", ")}`);
- output.push(`${fmt.bold("Moderators")}: ${moderators.length ? moderators.join(", ") : "No moderators found"}`);
-
+ output.push(
+ `${fmt.bold("Moderators")}: ${
+ moderators.length ? moderators.join(", ") : "No moderators found"
+ }`,
+ );
+
output.push(
`${fmt.bold("Rules")}: Timing method - ${
gameObj.ruleset["default-time"]
diff --git a/src/srcom/games.ts b/src/srcom/games.ts
index b7c77d3..71b166c 100755
--- a/src/srcom/games.ts
+++ b/src/srcom/games.ts
@@ -1,5 +1,5 @@
#!/usr/bin/env -S deno run --allow-net=www.speedrun.com --allow-env=NO_COLOR --no-check
-import { Format, MarkupType } from "./fmt.ts";
+import { MarkupType } from "./fmt.ts";
import { CommandError, getUser, SRC_API } from "./utils.ts";
import type { Opts } from "./utils.ts";
import type { SpeedrunCom } from "./types.d.ts";
@@ -20,7 +20,6 @@ export async function games(
username: string,
{ outputType = "markdown" }: Opts = {},
): Promise<string | GamesObject> {
- const fmt = new Format(outputType);
const output: string[] = [];
const games: string[] = [];
const user = await getUser(username);
diff --git a/src/srcom/world_record.ts b/src/srcom/world_record.ts
index 9ea9f98..f76f807 100755
--- a/src/srcom/world_record.ts
+++ b/src/srcom/world_record.ts
@@ -18,7 +18,6 @@ export async function worldRecord(
category?: string,
subcategory?: string,
// For consistency sake
- // deno-lint-ignore no-unused-vars
{ outputType = "markdown" }: Opts = {},
): Promise<string> {
const output: string[] = [];
@@ -130,13 +129,12 @@ export async function worldRecord(
`The category '${category}' is an IL category, not level`,
);
}
- const playersTasks: Promise<string>[] = wr.players.map((player) =>
+ const playersTasks: (Promise<string> | string)[] = wr.players.map((player) =>
player.rel === "user"
? getUser(player.id).then((user) =>
user ? fmt.link(user.weblink, user.names.international) : ""
)
- : // @ts-ignore A player can be a guest
- player.name
+ : player.name
);
const players = await Promise.all(playersTasks);
diff --git a/src/standalone_client.ts b/src/standalone_client.ts
new file mode 100644
index 0000000..555da26
--- /dev/null
+++ b/src/standalone_client.ts
@@ -0,0 +1,9 @@
+import { Client, event } from "../deps_server.ts";
+import { SpeedrunCom } from "./srcom/slash_commands.ts";
+
+export class SpeedRunBot extends Client {
+ @event()
+ ready() {
+ this.interactions.loadModule(new SpeedrunCom());
+ }
+}
diff --git a/src/utils.ts b/src/utils.ts
index dafb604..8b4b4f5 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1 +1,37 @@
+import { Helmet, renderSSR } from "../deps_server.ts";
+
export const DISCORD_URL = "https://discord.com/api/v9";
+
+export const isDeployed = (await Deno.permissions.query({
+ name: "env",
+ variable: "DENO_DEPLOYMENT_ID",
+ })).state === "granted" && !!Deno.env.get("DENO_DEPLOYMENT_ID");
+
+export function renderPage(page: JSX.IntrinsicElements, init?: ResponseInit) {
+ const { body, head, footer, attributes } = Helmet.SSR(renderSSR(
+ page,
+ ));
+ return new Response(
+ `
+<!DOCTYPE html>
+<html ${attributes.html.toString()}>
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ ${head.join("\n")}
+ </head>
+ <body ${attributes.body.toString()}>
+ ${body}
+ ${footer.join("\n")}
+ </body>
+</html>
+`,
+ {
+ ...init,
+ headers: {
+ "content-type": "text/html",
+ ...init?.headers,
+ },
+ },
+ );
+}
diff --git a/tests/srcom/world_records.test.ts b/tests/srcom/world_records.test.ts
index 0a805e2..507decc 100644
--- a/tests/srcom/world_records.test.ts
+++ b/tests/srcom/world_records.test.ts
@@ -7,7 +7,7 @@ Deno.test("Get world records by username", async () => {
});
const expected = `World Record Count: 1
Fullgame: 0
-Individual Level: 10
-Total: 10`;
+Individual Level: 9
+Total: 9`;
assertEquals(res, expected);
});