diff options
-rw-r--r-- | .github/workflows/deno.yml | 5 | ||||
-rw-r--r-- | CONTRIBUTING.md | 7 | ||||
-rw-r--r-- | deps_frontend.ts | 1 | ||||
-rw-r--r-- | deps_general.ts | 7 | ||||
-rw-r--r-- | deps_server.ts | 20 | ||||
-rwxr-xr-x | main.ts | 39 | ||||
-rwxr-xr-x | src/client.ts | 39 | ||||
-rw-r--r-- | src/config.ts | 7 | ||||
-rw-r--r-- | src/pages/admin.tsx | 252 | ||||
-rw-r--r-- | src/pages/api/discord/bot_info.ts | 18 | ||||
-rw-r--r-- | src/pages/api/discord/interactions.ts | 45 | ||||
-rw-r--r-- | src/pages/api/discord/reload.ts | 67 | ||||
-rw-r--r-- | src/pages/api/discord/token.ts | 42 | ||||
-rw-r--r-- | src/pages/index.tsx | 16 | ||||
-rw-r--r-- | src/pages/logout.ts | 11 | ||||
-rw-r--r-- | src/pages/mod.tsx | 60 | ||||
-rwxr-xr-x | src/srcom/categories.ts | 22 | ||||
-rwxr-xr-x | src/srcom/categories_played.ts | 3 | ||||
-rwxr-xr-x | src/srcom/game_info.ts | 8 | ||||
-rwxr-xr-x | src/srcom/games.ts | 3 | ||||
-rwxr-xr-x | src/srcom/world_record.ts | 6 | ||||
-rw-r--r-- | src/standalone_client.ts | 9 | ||||
-rw-r--r-- | src/utils.ts | 36 | ||||
-rw-r--r-- | tests/srcom/world_records.test.ts | 4 |
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"; @@ -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); }); |