From 5f8526e611df3bd017d360345e3c5f53f617b32d Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Tue, 8 Aug 2023 19:46:33 -0700 Subject: [PATCH 1/5] Update README.md --- README.md | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/README.md b/README.md index 155daf3..975b6dd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,109 @@ # shorter + 🔗 Discord slash command URL shortener. + +## Development + +### Run the server + +> **NOTE** +> +> Be sure to set the environment variables before running the server. +> +> Be sure to [install Deno](https://deno.land/install) before running the +> server. + +You will need to use two terminal windows; one for the HTTP server and one for +Ngrok. + +#### Terminal 1 + +To run the server, run the following command: + +```bash +deno task start +``` + +#### Terminal 2 + +> **NOTE** You will need to have +> [Ngrok](https://dashboard.ngrok.com/get-started/setup) installed and in your +> path. + +```bash +deno task ngrok +``` + +In **Terminal 2**, copy the URL that is generated under **Forwarding**. + +- The URL should look similar to this: + `https://ab01-23-456-78-910.ngrok-free.app` + +Set this new URL as the **Interactions Endpoint URL** in the **General** tab of +your Discord application. Find your application +[here](https://discord.com/developers/applications). + +### Deploy + +This server is deployed on [Deno Deploy](https://deno.com/deploy). To deploy, +set the entrypoint file to `main.ts` and set the environment variables in the +Deno Deploy dashboard. + +### Slash command usage + +To understand the usage of the slash command, refer to the source code in +[`bot/app/app.ts`](bot/app/app.ts). + +### Discord Application Command setup + +1. [Create a Discord application](https://discord.com/developers/applications). +1. Create a bot for the application. +1. Copy the bot token and set it as the `DISCORD_TOKEN` environment variable. +1. Copy the public key and set it as the `DISCORD_PUBLIC_KEY` environment + variable. +1. Copy the client ID and set it as the `DISCORD_CLIENT_ID` environment + variable. +1. Spin up the server. Set the Discord interactions endpoint URL to the URL of + the server (Ngrok or Deno Deploy). + +### Environment variables + +Refer to `.env.example` for a list of environment variables that need to be set. + +#### `DISCORD_PUBLIC_KEY` + +The public key of the Discord application. This is used to verify that the +request is coming from Discord. + +#### `DISCORD_CLIENT_ID` + +The client ID of the Discord application. This is used to generate the OAuth2 +URL. + +#### `DISCORD_TOKEN` + +The bot token of the Discord application. + +#### `DISCORD_ROLE_ID` + +The ID of the role that is allowed to use the slash command. Board members are +intended to use this command. + +#### `GITHUB_TOKEN` + +The GitHub personal access token. This is used to access the GitHub API via +[Codemod](https://deno.land/x/codemod). + +#### `PORT` + +**Not required**. The port that the server will listen on. + +#### `DEV` + +**Not required**. If set to `true`, the server will spin up an Ngrok tunnel and +print the URL to the console. This is intended for development. Paste this URL +into the Discord application's Discord interactions endpoint URL. + +--- + +Programmed with ❤️ by [**@acmcsufoss**](https://oss.acmcsuf.com/). From e364a813e0b1942fc2a7944242414a6027f29a77 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Tue, 8 Aug 2023 20:09:52 -0700 Subject: [PATCH 2/5] Create settings.json --- .vscode/settings.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7b3ce44 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "deno.enable": true, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[typescript]": { + "editor.defaultFormatter": "denoland.vscode-deno" + } +} From 1db4b14691320775c9e64479c199496ebb3c608f Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Tue, 8 Aug 2023 20:09:58 -0700 Subject: [PATCH 3/5] Create deno.jsonc --- deno.jsonc | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 deno.jsonc diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..133a90d --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,12 @@ +{ + "lock": "./deno.lock", + "tasks": { + "lint": "deno lint", + "fmt": "deno fmt", + "udd": "deno run -r --allow-read=. --allow-write=. --allow-net https://deno.land/x/udd/main.ts deps.ts && deno task lock", + "lock": "deno cache --lock=deno.lock --lock-write deps.ts", + "all": "deno task udd && deno task lint && deno task fmt", + "start": "deno run -A main.ts", + "ngrok": "ngrok http 8080" + } +} From f891b4d03246c326f66403c5ce83bc70796d396a Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Tue, 8 Aug 2023 20:10:04 -0700 Subject: [PATCH 4/5] Create LICENSE --- LICENSE | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c57e48b --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. From ae92a1fafa993fbb5f13d2e66b8323e0b7968a72 Mon Sep 17 00:00:00 2001 From: Ethan Davidson <31261035+EthanThatOneKid@users.noreply.github.com> Date: Wed, 9 Aug 2023 16:19:51 -0700 Subject: [PATCH 5/5] =?UTF-8?q?pop=20off=20=F0=9F=92=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 9 ++ .github/workflows/check.yaml | 25 ++++ .gitignore | 2 + app/app.ts | 35 ++++++ app/mod.ts | 1 + deno.jsonc | 2 - deno.lock | 118 ++++++++++++++++++ deps.ts | 5 + discord/discord_api_client.ts | 81 ++++++++++++ discord/discord_api_client_interface.ts | 48 ++++++++ discord/mod.ts | 3 + discord/verify.ts | 66 ++++++++++ env.ts | 58 +++++++++ main.ts | 157 ++++++++++++++++++++++++ shorter.ts | 125 +++++++++++++++++++ 15 files changed, 733 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 .github/workflows/check.yaml create mode 100644 .gitignore create mode 100644 app/app.ts create mode 100644 app/mod.ts create mode 100644 deno.lock create mode 100644 deps.ts create mode 100644 discord/discord_api_client.ts create mode 100644 discord/discord_api_client_interface.ts create mode 100644 discord/mod.ts create mode 100644 discord/verify.ts create mode 100644 env.ts create mode 100644 main.ts create mode 100644 shorter.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1bc2639 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Required: +DISCORD_PUBLIC_KEY="" +DISCORD_CLIENT_ID="" +DISCORD_TOKEN="" +DISCORD_ROLE_ID="" +GITHUB_TOKEN="" + +# Optional: +PORT="8080" diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml new file mode 100644 index 0000000..fed7b8f --- /dev/null +++ b/.github/workflows/check.yaml @@ -0,0 +1,25 @@ +name: check + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + + strategy: + matrix: + deno-version: [canary] + + steps: + - uses: actions/checkout@v2 + + - uses: denoland/setup-deno@v1 + with: + deno-version: ${{ matrix.deno-version }} + + - run: deno lint && git diff-index --quiet HEAD + - run: deno fmt && git diff-index --quiet HEAD diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..514ceb7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +ngrok.exe diff --git a/app/app.ts b/app/app.ts new file mode 100644 index 0000000..54f635e --- /dev/null +++ b/app/app.ts @@ -0,0 +1,35 @@ +import { discord } from "../deps.ts"; + +export const SHORTER = "shorter"; +export const SHORTER_DESCRIPTION = "Manage acmcsuf.com shortlinks."; + +export const SHORTER_ALIAS = "alias"; +export const SHORTER_ALIAS_DESCRIPTION = + "The alias of the shortlink (e.g. `discord`, `example/a/b/c`, etc.)."; + +export const SHORTER_DESTINATION = "destination"; +export const SHORTER_DESTINATION_DESCRIPTION = + "The destination of the shortlink."; + +/** + * APP_SHORTER is the top-level command for the Shorter Application Command. + */ +export const APP_SHORTER: discord.RESTPostAPIApplicationCommandsJSONBody = { + type: discord.ApplicationCommandType.ChatInput, + name: SHORTER, + description: SHORTER_DESCRIPTION, + options: [ + { + type: discord.ApplicationCommandOptionType.String, + name: SHORTER_ALIAS, + description: SHORTER_ALIAS_DESCRIPTION, + required: true, + }, + { + type: discord.ApplicationCommandOptionType.String, + name: SHORTER_DESTINATION, + description: SHORTER_DESTINATION_DESCRIPTION, + required: true, + }, + ], +}; diff --git a/app/mod.ts b/app/mod.ts new file mode 100644 index 0000000..05bbef7 --- /dev/null +++ b/app/mod.ts @@ -0,0 +1 @@ +export * from "./app.ts"; diff --git a/deno.jsonc b/deno.jsonc index 133a90d..bc89228 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,8 +1,6 @@ { "lock": "./deno.lock", "tasks": { - "lint": "deno lint", - "fmt": "deno fmt", "udd": "deno run -r --allow-read=. --allow-write=. --allow-net https://deno.land/x/udd/main.ts deps.ts && deno task lock", "lock": "deno cache --lock=deno.lock --lock-write deps.ts", "all": "deno task udd && deno task lint && deno task fmt", diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..de155f7 --- /dev/null +++ b/deno.lock @@ -0,0 +1,118 @@ +{ + "version": "2", + "remote": { + "https://deno.land/std@0.191.0/collections/filter_values.ts": "5b9feaf17b9a6e5ffccdd36cf6f38fa4ffa94cff2602d381c2ad0c2a97929652", + "https://deno.land/std@0.191.0/collections/without_all.ts": "a89f5da0b5830defed4f59666e188df411d8fece35a5f6ca69be6ca71a95c185", + "https://deno.land/std@0.191.0/dotenv/mod.ts": "f5a8123741d1561ae8184a7f043bc097b15132c5171c651142b804b6dbc21853", + "https://deno.land/std@0.191.0/http/http_errors.ts": "b9a18ef97d6c5966964de95e04d1f9f88a0f8bd8577c26fd402d9d632fb03a42", + "https://deno.land/std@0.191.0/http/http_status.ts": "8a7bcfe3ac025199ad804075385e57f63d055b2aed539d943ccc277616d6f932", + "https://deno.land/std@0.197.0/collections/filter_values.ts": "16e1fc456a7969e770ec5b89edf5ac97b295ca534b47c1a83f061b409aad7814", + "https://deno.land/std@0.197.0/collections/without_all.ts": "1e3cccb1ed0659455b473c0766d9414b7710d8cef48862c899f445178f66b779", + "https://deno.land/std@0.197.0/dotenv/mod.ts": "ff7acf1c97ba57af512ecb6f9094fa96e1f63cca1960a7687616fa86bab7e356", + "https://deno.land/x/codemod@0.0.5/deps.ts": "5e27f88433fb872ab1873b104c4301825d2d16c5c37165a7b989a71e62ae908a", + "https://deno.land/x/codemod@0.0.5/github/api/github_api_client.ts": "0574f97e834c215f6151e420cf821bceb745750a7a4d4fcc60eb897b486d1f98", + "https://deno.land/x/codemod@0.0.5/github/api/github_api_client_interface.ts": "16e7466539ad7f50dc6fa2a3d1fcdee4407336ffb94643ab3e783e76f03e309b", + "https://deno.land/x/codemod@0.0.5/github/api/github_api_client_urls.ts": "9aea4e8cf89f3dd15450af60537baf513ba03fd58fe0623982ac5c9e7d2f79f4", + "https://deno.land/x/codemod@0.0.5/github/api/mod.ts": "c77e6ef8e4622259ce8bd9b225199480b919b41f1ac71fd044353a8ae4a0d1bf", + "https://deno.land/x/codemod@0.0.5/github/branch/github_create_branch_builder.ts": "e91487ce95364284469e1364367556697fdf7a15fac3f5f78151ba0985d6459b", + "https://deno.land/x/codemod@0.0.5/github/branch/github_create_branch_builder_interface.ts": "3c1485f68878da423e716a521173ea4538f6b574de2fb783ab50b6acb51d99c1", + "https://deno.land/x/codemod@0.0.5/github/branch/github_update_branch_builder.ts": "30c72baa9d421d18572b9fbab4c82ba614c4fa0e52e64c43b028464904ea0a49", + "https://deno.land/x/codemod@0.0.5/github/branch/github_update_branch_builder_interface.ts": "5b8e71c2e0bfa37fea024a7715c5efcde6ab6696e6fd06abd6fcaea28b209ab5", + "https://deno.land/x/codemod@0.0.5/github/branch/mod.ts": "0ca628d55e514f0c86d68bbb1eee19dabd40ea6057b422bea70bb885d94d7dc2", + "https://deno.land/x/codemod@0.0.5/github/codemod/github_codemod_builder.ts": "b107ba6e24b895b286120869c8ab7d11e2089eb66a5ecbff68b4b93859948762", + "https://deno.land/x/codemod@0.0.5/github/codemod/github_codemod_builder_interface.ts": "c3de3bd4372c822bf03e64e105a69face76528b0dc07acef7a78448d848f0b4f", + "https://deno.land/x/codemod@0.0.5/github/codemod/mod.ts": "88fecc575c58adbcc87a3c177000d27e4f44621966a28484bd2d20a1e8a1f68d", + "https://deno.land/x/codemod@0.0.5/github/commit/github_create_commit_builder.ts": "173e1263d39c8a4d5f091704c385edd59a3f69d908346b18639201a5f7c5e1b0", + "https://deno.land/x/codemod@0.0.5/github/commit/github_create_commit_builder_interface.ts": "9e116dc37b6b9e024a966274598d1bde6bd84972ad1487662bc98adb15f72915", + "https://deno.land/x/codemod@0.0.5/github/commit/mod.ts": "77c22d1e2ce4198dc9e873c4e8636a4685e56b263e3ea3b971a2dca9ed5d6db8", + "https://deno.land/x/codemod@0.0.5/github/create_codemod.ts": "726884836873978a5df719daed7a290558cd55af23591fd19dbf052ffd7ad8d8", + "https://deno.land/x/codemod@0.0.5/github/mod.ts": "66de36157ae91be9dc3e829ab9a79d0434657821abd2b94f0511f0c33ea0cf0c", + "https://deno.land/x/codemod@0.0.5/github/pr/github_create_pr_builder.ts": "db092fcfa9d355d43c419b925c0b53dba98129ab3bd13c53fe339907a6249098", + "https://deno.land/x/codemod@0.0.5/github/pr/github_create_pr_builder_interface.ts": "07571893f91713a7ea917f6f3e55e1a1323340b370e119ede3f691c179cb294e", + "https://deno.land/x/codemod@0.0.5/github/pr/github_update_pr_builder.ts": "a98fb814b3a32e51ee9192b4106d915bc41502cdf4714d2ed0f1858a6a46865a", + "https://deno.land/x/codemod@0.0.5/github/pr/github_update_pr_builder_interface.ts": "d8645ecdc0796f04085045ddf8ad9bf9d2ba8ceebb0c74dc9d4e4b6ec7bc37d5", + "https://deno.land/x/codemod@0.0.5/github/pr/mod.ts": "0ee439570efcca82f321fca5ac356aac847f3e202f5c8a57483fc647d7837de4", + "https://deno.land/x/codemod@0.0.5/github/shared/append.ts": "14fd7a3c056d569b6faa83f765b9fe66f97e2de470ed72f23906cc0ef609a069", + "https://deno.land/x/codemod@0.0.5/github/shared/generate.ts": "a483aec1f47974846533dd903ea91b5d8a1e1a9b94a66148ecd85e095bab4f88", + "https://deno.land/x/codemod@0.0.5/github/tree/base64.ts": "4854bba36ec3fc501693d27cece4f5725797b522cd7dfe0c721befedefd956f3", + "https://deno.land/x/codemod@0.0.5/github/tree/github_create_tree_builder.ts": "cc3d4efa23ecce94eb1b3dbaf8cdd16dff3a14065335302365009df5a20a7c17", + "https://deno.land/x/codemod@0.0.5/github/tree/github_create_tree_builder_interface.ts": "0623f3c8a5285b58898555c2718cd2f7ec6e1d5619ee30405ef3297d5ea8802b", + "https://deno.land/x/discord_api_types@0.37.52/gateway/common.ts": "fb67003adda424df76c2726e0624d709c5a16e3694d6b75facd587d121fe121f", + "https://deno.land/x/discord_api_types@0.37.52/gateway/v10.ts": "9d9a0bbc57e0159d3c1a4ec4cf6b5b72c1f86db86849e5650628ac0fffa76e9c", + "https://deno.land/x/discord_api_types@0.37.52/globals.ts": "7d8879654c4741ac071668ad52f2659bcdb66694cfe7da306c8437ec752807a7", + "https://deno.land/x/discord_api_types@0.37.52/payloads/common.ts": "7c42a7965f38d82d9a425cf732ff0d4d2b5370568168d5fa41f138fd8ab4c693", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/_chatInput/attachment.ts": "c66dccd54c1b84d073f2e1caa466e551b8045a84a2e8a88a1bfbc7e2c64a703d", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/_chatInput/base.ts": "f6a2556e14d489e1f0e5ddeb3a0303e2603e25330530dd9263e176013c5f51eb", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/_chatInput/boolean.ts": "65e29561b61785ca4ede4b1b4a88c5fc0696cfdf1fa74d5197588c196ee7ae98", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/_chatInput/channel.ts": "73c7fc49de242e1ce3be958375fd810750aed83553ef3860e3cddf858f9eb464", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/_chatInput/integer.ts": "a25d24a3e54d647c7039b99e3208fc0fc2228d174f6dcc421e93919b8154a011", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/_chatInput/mentionable.ts": "742e42857465866e0c08b587d7fb5ccd81d4705c61ce4cd6b97ad5692e88e969", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/_chatInput/number.ts": "be974ea68f5fdf55d7a7f5d3faf48a3193777d432b1aa9087afc204bcb916284", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/_chatInput/role.ts": "a57114d0f7eeee4ab7cf217a865dd9dbd9096d007c556aec6185d64257100f41", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/_chatInput/shared.ts": "9e2d3b3530280f6de5f9b6de1bb81e8a905998e058f784a9b041e48a96cd93d2", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/_chatInput/string.ts": "33ab12dab64544a70729b9b66b5a9790964ea779f05d4ab1a1e190e7c1b59e98", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/_chatInput/subcommand.ts": "0013737da6d2b54e2f413fbf31cf9c84ea51bd9204b615cb4fd19b420f856cb2", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/_chatInput/subcommandGroup.ts": "db5cc701bcb3d68c094de409da39c9a2b8834dc0d5038e5f963c96e5eaf412ac", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/_chatInput/user.ts": "ed2871693744298225ba53ddfb18d3e7afff20a34f413822d5b1193918aea27f", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/chatInput.ts": "46362da4e56c99cc69331481330d6e95c31d5e46f4cc36ec23f03cafbb687d52", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/contextMenu.ts": "89aed5f05f75d482e40259f55d0172143a90c1980d060d16545bdc14b68b29c4", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/internals.ts": "5eb5ea13a1247c73c0611886dea09ab8d632a9c5555ff0f33d44cd379fd75a08", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/_applicationCommands/permissions.ts": "ddca14b62e6afd418c1417117ffcc7cfb2ea5e5cc5353b4a0598435bdea45fb5", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/applicationCommands.ts": "b0646f2930d38113389bd1ecf8c605ac5af8fc40f93fafd17f968150419fac14", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/autocomplete.ts": "821ae50ff9845cac4b03169dbd4c4b187d8399765eb1f0d658d477c68e4c6136", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/base.ts": "c839d953bf7f7912aa7202e7586d65aafb7f90d22aedd21a13586295c62f2bdf", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/messageComponents.ts": "08faa77d1c1d9a33359a962b78b695304e27cb6435af319b41e9a9d3b395adb2", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/modalSubmit.ts": "3a02d2d7df5bcdb1ffcd089f15e0d82ab65dcc0cefa904c6e0621f46edac041e", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/ping.ts": "096ce582e9af373649fd5355cccd7424adceaffb73367b5301f1594ef5a3c264", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/_interactions/responses.ts": "0a607a78dd08a498c3b523a922c51ec9465021d98b3a21b7c7551524d49d70ce", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/application.ts": "69e8470289fcde7ec1483600540085e402a11b21cfeab2191db23dd387452ee7", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/auditLog.ts": "bee4a1333028c970265255c713378570cc92e92962654af68cff1125d4ea596b", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/autoModeration.ts": "9ccb4408f1c6392d9619fac159997e08e660080b3f9567a1619163a40329e3a2", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/channel.ts": "cb41960857b663ad65027889caddc911bf7c448a6f41ac037b8cb551e36a2190", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/emoji.ts": "b9a30b16e1ec4dc15d6149e59aa48b02ad57a51335b7be5a7f5368db0491b3dd", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/gateway.ts": "e2375d4da7c2c61dff55527bbc4e00a9f243d07c51d1ee63bfd469cffca9e9a5", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/guild.ts": "536ce0d73d055290855febb556d6ffcc3308f5824887879d28f49d0e4a676fa6", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/guildScheduledEvent.ts": "bf506b7807501b71077751ec793e719c5515e1bb405dec5cc4371a61b03cf8b9", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/interactions.ts": "17dabe94016dad3d0d7fdc0aa812bf5b0b366465dd72cd0b01168880778cc60d", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/invite.ts": "92c09f549482a4e2ad5a3c1062debfb262c6fe4b6740581175a0b8108873ab01", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/mod.ts": "733090e3f67bb812dc0a3986d476e62483e88dfa0ce362d048a669b54b94c625", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/oauth2.ts": "dfb9f09fb44bf5faaa73ad4488ebe408905907d5fd46404895317f4e7c378489", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/permissions.ts": "bf185ff286a02a3e583fd938c54273233cd62ff2f646c0e3666189a7139f8a9c", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/stageInstance.ts": "f0b9ee8c24c67298086fa32cb0595f6c29710d81b6fe85b958d48e6c549c4cb8", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/sticker.ts": "a917cbd294fe78dabe8414a86bdb3829491aad809b30896d7c205bd8a1c77909", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/teams.ts": "908ffbcb2389fdd50181e51c3147453d51a0f5c5490f3124161f904b7c240f73", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/template.ts": "c6bee171ed0ce61fc8b59de42541a023bdcde62718deb42325397e5c82efdc27", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/user.ts": "59706244399f29512688c0813ec7dbaabd2fd5f583471564ff513e87213d2d13", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/voice.ts": "62d03a540f2e78e5f3989f71a0ee1ec682ef7306a4fa096f89118cbd82351d47", + "https://deno.land/x/discord_api_types@0.37.52/payloads/v10/webhook.ts": "7fc370f40a84f12a6e57ddda7cf2814f15039d6320b46979db0b49d5b91e303b", + "https://deno.land/x/discord_api_types@0.37.52/rest/common.ts": "0bfbcf97482fe86daeb68c57885bd04a7d6504ba9037f7e533ea466825afd051", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/application.ts": "5b44ce90cf739aac76e2f01c15a3d4a03724703695235aec4f800fdd08782ec0", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/auditLog.ts": "39a0914b6c51445023d82c3e3e66c9866cbb3cb6774d3e7eac63414ea9bcbfec", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/autoModeration.ts": "3d388fd9a91c34f04b5e3e1b6ffe12029fb48b511f37ff88042325ec6cbc6605", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/channel.ts": "b8242d365c6193a400fa136d457bc5946275ab8d8eaa929457abd25e2628fa3c", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/emoji.ts": "9f694a1bd63886c62a87b4320f3bfa5d4f534b2d87c317d77d572d10522df3aa", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/gateway.ts": "747cb95c9a8bca4e52423c780d5fc492fb0dab2b6015cd7e51e890e8d51acf29", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/guild.ts": "ba51e20983b4b10e7ddd4472b44c03eefbaf0def9f1d99a87f5a1f04950c10a2", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/guildScheduledEvent.ts": "29d361f395d8cd1ecb47550615d19e10793d513cc5ad8d32895da2cc9cd0fd89", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/interactions.ts": "2b6decdfff921b6aa8f0e6e5c61d38469a0178c5ecf1a18dd17ad6738143e662", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/invite.ts": "28f8e740bdaa782c9d9d504049323762b5c1180348019dc5f9e0a900ec11213e", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/mod.ts": "c926a288b4a911857e1c5a5b98c3763bf55f86cef2ae70a903a3ae6dd08229ff", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/oauth2.ts": "b659a35654c17767480d142c46c36f5fe2544346875745e3f654c5e7c3d9f3f9", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/stageInstance.ts": "d5d96446a0275860f136d40546bf0519d5d8d4171d03058598559df095dbf899", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/sticker.ts": "656aae6b4263a602524098bcb09ad503a51dc09310165a575340e55d5a9c06cb", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/template.ts": "0ad41c3c85571d3c5b0bec3914c678a21f376ec162ef0d3f1f7731a8d1d1009c", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/user.ts": "60cfa227426c791021e9e8f769287e997477e722db5a3c577a9ec54078aaffca", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/voice.ts": "cdbe9d6c39c8f44635d8632bf62a95b8c15877b92c56ddf69df2072bb1a74edc", + "https://deno.land/x/discord_api_types@0.37.52/rest/v10/webhook.ts": "8d6b5eed9f46d803a8e32ed6db275fc788e65dedaf67b38ea66b675eee16a9a2", + "https://deno.land/x/discord_api_types@0.37.52/rpc/common.ts": "a693352ffd86ae9e995fb3fbfbfd2be30896257ecb83c5611050f060b08de4ef", + "https://deno.land/x/discord_api_types@0.37.52/rpc/v10.ts": "fbaad9f3d73fce88e76b0e52ad5345093f18077e4293937c9ec0ee24415b9a93", + "https://deno.land/x/discord_api_types@0.37.52/utils/internals.ts": "cb70895ba89f7947c38f7fa447b0190cb14b5585be323414cda53d2ccb19b16c", + "https://deno.land/x/discord_api_types@0.37.52/utils/v10.ts": "056bd036f8c65365ff28eb63ec6897811d51921cca6d068392dd1ca5b397ae62", + "https://deno.land/x/discord_api_types@0.37.52/v10.ts": "f3f23492c59e77859aba5b34431edf3668c37f722d7f70c2e1ef7ba4bcda3010", + "https://deno.land/x/github_api_types@2023-05-17-05-41/mod.ts": "dc3a5cd3176c78085b49601e9c3fccac24809b037929230293255edabafbd0bb", + "https://esm.sh/fast-json-patch@3.1.1": "fd59bab1fabdb3e7e2ce8204aa92113dc451708797021fff6f96b8ff265faedf", + "https://esm.sh/tweetnacl@1.0.3": "fe5086fc2857018dc6118351ec0790016454d787af13301d65331c9a08eabb6a", + "https://esm.sh/v124/fast-json-patch@3.1.1/deno/fast-json-patch.mjs": "19183b256388e7af3ffb65022a634e00109c308fe79071cd550dd5ab1f0a571b", + "https://esm.sh/v125/tweetnacl@1.0.3/denonext/tweetnacl.mjs": "e3f326d197a7d2dc580dc57db86b5adb6374d9a64de2d5b6a1711612c930c1c7" + } +} diff --git a/deps.ts b/deps.ts new file mode 100644 index 0000000..1fc022b --- /dev/null +++ b/deps.ts @@ -0,0 +1,5 @@ +export * as dotenv from "https://deno.land/std@0.197.0/dotenv/mod.ts"; +export * as discord from "https://deno.land/x/discord_api_types@0.37.52/v10.ts"; +export * from "https://deno.land/x/codemod@0.0.5/github/mod.ts"; +export type { GitHubAPIClientOptions } from "https://deno.land/x/codemod@0.0.5/github/api/mod.ts"; +export { default as nacl } from "https://esm.sh/tweetnacl@1.0.3"; diff --git a/discord/discord_api_client.ts b/discord/discord_api_client.ts new file mode 100644 index 0000000..5c921c9 --- /dev/null +++ b/discord/discord_api_client.ts @@ -0,0 +1,81 @@ +import type { + DiscordAPIClientInterface, + EditOriginalInteractionResponseOptions, + RegisterCommandOptions, +} from "./discord_api_client_interface.ts"; + +/** + * DiscordAPIClient is a client for the Discord API. + */ +export class DiscordAPIClient implements DiscordAPIClientInterface { + async registerCommand(options: RegisterCommandOptions): Promise { + const url = makeRegisterCommandsURL(options.botID); + const response = await fetch(url, { + method: "POST", + headers: new Headers([ + ["Content-Type", "application/json"], + ["Authorization", makeBotAuthorization(options.botToken)], + ]), + body: JSON.stringify(options.app), + }); + if (!response.ok) { + console.error("text:", await response.text()); + throw new Error( + `Failed to register command: ${response.status} ${response.statusText}`, + ); + } + } + + async editOriginalInteractionResponse( + options: EditOriginalInteractionResponseOptions, + ): Promise { + const url = makeEditOriginalInteractionResponseURL( + options.botID, + options.interactionToken, + ); + const response = await fetch(url, { + method: "PATCH", + headers: new Headers([["Content-Type", "application/json"]]), + body: JSON.stringify({ content: options.content }), + }); + if (!response.ok) { + console.error("text:", await response.text()); + throw new Error( + `Failed to edit original interaction response: ${response.status} ${response.statusText}`, + ); + } + } +} + +/** + * makeBotAuthorization makes the Authorization header for a bot. + */ +export function makeBotAuthorization(botToken: string) { + return botToken.startsWith("Bot ") ? botToken : `Bot ${botToken}`; +} + +/** + * makeRegisterCommandsURL makes the URL to register a Discord application command. + */ +export function makeRegisterCommandsURL( + clientID: string, + base = DISCORD_API_URL, +) { + return new URL(`${base}/applications/${clientID}/commands`); +} + +/** + * makeEditOriginalInteractionResponseURL makes the URL to edit the original interaction response. + */ +export function makeEditOriginalInteractionResponseURL( + clientID: string, + interactionToken: string, + base = DISCORD_API_URL, +) { + return `${base}/webhooks/${clientID}/${interactionToken}/messages/@original`; +} + +/** + * DISCORD_API_URL is the base URL for the Discord API. + */ +export const DISCORD_API_URL = "https://discord.com/api/v10"; diff --git a/discord/discord_api_client_interface.ts b/discord/discord_api_client_interface.ts new file mode 100644 index 0000000..cdcb1b0 --- /dev/null +++ b/discord/discord_api_client_interface.ts @@ -0,0 +1,48 @@ +import type { discord } from "../deps.ts"; + +/** + * DiscordAPIClientInterface is the interface for the Discord API Client. + */ +export interface DiscordAPIClientInterface { + /** + * registerCommand overwrites the Discord Slash Commands associated with the server. + * + * Based on this cURL command: + * ```bash + * BOT_TOKEN='replace_me_with_bot_token' + * CLIENT_ID='replace_me_with_client_id' + * curl -X POST \ + * -H 'Content-Type: application/json' \ + * -H "Authorization: Bot $BOT_TOKEN" \ + * -d '{"name":"hello","description":"Greet a person","options":[{"name":"name","description":"The name of the person","type":3,"required":true}]}' \ + * "https://discord.com/api/v8/applications/$CLIENT_ID/commands" + * ``` + */ + registerCommand(o: RegisterCommandOptions): Promise; + + /** + * editOriginalInteractionResponse edits the original interaction response. + */ + editOriginalInteractionResponse( + o: EditOriginalInteractionResponseOptions, + ): Promise; +} + +/** + * RegisterCommandOptions is the initialization to register a Discord application command. + */ +export interface RegisterCommandOptions { + botID: string; + botToken: string; + app: discord.RESTPostAPIApplicationCommandsJSONBody; +} + +/** + * EditOriginalInteractionResponseOptions is the initialization to edit the original interaction response. + */ +export interface EditOriginalInteractionResponseOptions { + botID: string; + botToken: string; + interactionToken: string; + content: string; +} diff --git a/discord/mod.ts b/discord/mod.ts new file mode 100644 index 0000000..b1bc6a9 --- /dev/null +++ b/discord/mod.ts @@ -0,0 +1,3 @@ +export * from "./discord_api_client_interface.ts"; +export * from "./discord_api_client.ts"; +export * from "./verify.ts"; diff --git a/discord/verify.ts b/discord/verify.ts new file mode 100644 index 0000000..6df4aea --- /dev/null +++ b/discord/verify.ts @@ -0,0 +1,66 @@ +import { nacl } from "../deps.ts"; + +/** + * verify verifies whether the request is coming from Discord. + */ +export async function verify( + request: Request, + publicKey: string, +): Promise<{ error: Response; body: null } | { error: null; body: string }> { + if (request.method !== "POST") { + return { + error: new Response("Method not allowed", { status: 405 }), + body: null, + }; + } + + if (request.headers.get("content-type") !== "application/json") { + return { + error: new Response("Unsupported Media Type", { status: 415 }), + body: null, + }; + } + + const signature = request.headers.get("X-Signature-Ed25519"); + if (!signature) { + return { + error: new Response("Missing header X-Signature-Ed25519", { + status: 401, + }), + body: null, + }; + } + + const timestamp = request.headers.get("X-Signature-Timestamp"); + if (!timestamp) { + return { + error: new Response("Missing header X-Signature-Timestamp", { + status: 401, + }), + body: null, + }; + } + + const body = await request.text(); + const valid = nacl.sign.detached.verify( + new TextEncoder().encode(timestamp + body), + hexToUint8Array(signature), + hexToUint8Array(publicKey), + ); + + // When the request's signature is not valid, we return a 401 and this is + // important as Discord sends invalid requests to test our verification. + if (!valid) { + return { + error: new Response("Invalid request", { status: 401 }), + body: null, + }; + } + + return { body, error: null }; +} + +/** hexToUint8Array converts a hexadecimal string to Uint8Array. */ +function hexToUint8Array(hex: string) { + return new Uint8Array(hex.match(/.{1,2}/g)!.map((val) => parseInt(val, 16))); +} diff --git a/env.ts b/env.ts new file mode 100644 index 0000000..4d5760e --- /dev/null +++ b/env.ts @@ -0,0 +1,58 @@ +import { dotenv } from "./deps.ts"; + +await dotenv.load({ export: true }); + +/** + * PORT is the port to listen on. + */ +export const PORT = parseInt(Deno.env.get("PORT") || "8080"); + +const RAW_DISCORD_PUBLIC_KEY = Deno.env.get("DISCORD_PUBLIC_KEY"); +if (!RAW_DISCORD_PUBLIC_KEY) { + throw new Error("DISCORD_PUBLIC_KEY environment variable is required"); +} + +/** + * DISCORD_PUBLIC_KEY is the Discord bot public key. + */ +export const DISCORD_PUBLIC_KEY = RAW_DISCORD_PUBLIC_KEY; + +const RAW_DISCORD_CLIENT_ID = Deno.env.get("DISCORD_CLIENT_ID"); +if (!RAW_DISCORD_CLIENT_ID) { + throw new Error("DISCORD_CLIENT_ID environment variable is required"); +} + +/** + * DISCORD_CLIENT_ID is the Discord bot client ID. + */ +export const DISCORD_CLIENT_ID = RAW_DISCORD_CLIENT_ID; + +const RAW_DISCORD_TOKEN = Deno.env.get("DISCORD_TOKEN"); +if (!RAW_DISCORD_TOKEN) { + throw new Error("DISCORD_TOKEN environment variable is required"); +} + +/** + * DISCORD_TOKEN is the Discord bot token. + */ +export const DISCORD_TOKEN = RAW_DISCORD_TOKEN; + +const RAW_DISCORD_ROLE_ID = Deno.env.get("DISCORD_ROLE_ID"); +if (!RAW_DISCORD_ROLE_ID) { + throw new Error("DISCORD_ROLE_ID environment variable is required"); +} + +/** + * DISCORD_ROLE_ID is the Discord board role ID. + */ +export const DISCORD_ROLE_ID = RAW_DISCORD_ROLE_ID; + +const RAW_GITHUB_TOKEN = Deno.env.get("GITHUB_TOKEN"); +if (!RAW_GITHUB_TOKEN) { + throw new Error("GITHUB_TOKEN environment variable is required"); +} + +/** + * GITHUB_TOKEN is the GitHub personal access token. + */ +export const GITHUB_TOKEN = RAW_GITHUB_TOKEN; diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..bdc8e4e --- /dev/null +++ b/main.ts @@ -0,0 +1,157 @@ +// Run: +// deno task start +// +// deno task ngrok +// + +import { discord } from "./deps.ts"; +import { DiscordAPIClient, verify } from "./discord/mod.ts"; +import { APP_SHORTER, SHORTER_ALIAS } from "./app/mod.ts"; +import type { ShorterOptions } from "./shorter.ts"; +import { shorter } from "./shorter.ts"; +import * as env from "./env.ts"; + +const api = new DiscordAPIClient(); + +if (import.meta.main) { + await main(); +} + +/** + * main is the entrypoint for the Shorter application command. + */ +export function main() { + // Start the server. + Deno.serve({ port: env.PORT, onListen }, handle); +} + +async function onListen() { + // Overwrite the Discord Application Command. + await api.registerCommand({ + app: APP_SHORTER, + botID: env.DISCORD_CLIENT_ID, + botToken: env.DISCORD_TOKEN, + }); + + // Log the invite URL. + console.log( + "Invite Shorter to a server:", + `https://discord.com/api/oauth2/authorize?client_id=${env.DISCORD_CLIENT_ID}&scope=applications.commands`, + ); + + // Log the application information. + console.log( + "Discord application information:", + `https://discord.com/developers/applications/${env.DISCORD_CLIENT_ID}/bot`, + ); +} + +/** + * handle is the HTTP handler for the Shorter application command. + */ +export async function handle(request: Request): Promise { + const { error, body } = await verify(request, env.DISCORD_PUBLIC_KEY); + if (error !== null) { + return error; + } + + // Parse the incoming request as JSON. + const interaction = await JSON.parse(body) as discord.APIInteraction; + switch (interaction.type) { + case discord.InteractionType.Ping: { + return Response.json({ type: discord.InteractionResponseType.Pong }); + } + + case discord.InteractionType.ApplicationCommand: { + if ( + !discord.Utils.isChatInputApplicationCommandInteraction(interaction) + ) { + return new Response("Invalid request", { status: 400 }); + } + + if (!interaction.member?.user) { + return new Response("Invalid request", { status: 400 }); + } + + if ( + !interaction.member.roles.some((role) => env.DISCORD_ROLE_ID === role) + ) { + return new Response("Invalid request", { status: 400 }); + } + + // Make the Shorter options. + const options = makeShorterOptions( + interaction.member, + interaction.data, + ); + + // Invoke the Shorter operation. + shorter(options) + .then((result) => + api.editOriginalInteractionResponse({ + botID: env.DISCORD_CLIENT_ID, + botToken: env.DISCORD_TOKEN, + interactionToken: interaction.token, + content: + `Created commit [${result.message}](https://acmcsuf.com/code/commit/${result.sha})!`, + }) + ) + .catch((error) => { + if (error instanceof Error) { + api.editOriginalInteractionResponse({ + botID: env.DISCORD_CLIENT_ID, + botToken: env.DISCORD_TOKEN, + interactionToken: interaction.token, + content: error.message, + }); + } + + console.error(error); + }); + + // Acknowledge the interaction. + return Response.json( + { + type: + discord.InteractionResponseType.DeferredChannelMessageWithSource, + } satisfies discord.APIInteractionResponseDeferredChannelMessageWithSource, + ); + } + + default: { + return new Response("Invalid request", { status: 400 }); + } + } +} + +/** + * makeShorterOptions makes the Shorter options from the Discord interaction. + */ +export function makeShorterOptions( + member: discord.APIInteractionGuildMember, + data: discord.APIChatInputApplicationCommandInteractionData, +): ShorterOptions { + const aliasOption = data.options + ?.find((option) => option.name === SHORTER_ALIAS); + if (aliasOption?.type !== discord.ApplicationCommandOptionType.String) { + throw new Error("Invalid alias"); + } + + const destinationOption = data.options + ?.find((option) => option.name === SHORTER_ALIAS); + if (destinationOption?.type !== discord.ApplicationCommandOptionType.String) { + throw new Error("Invalid destination"); + } + + return { + githubPAT: env.GITHUB_TOKEN, + actor: { + tag: member.user.username, + nick: member.nick || undefined, + }, + data: { + alias: aliasOption.value, + destination: destinationOption.value, + }, + }; +} diff --git a/shorter.ts b/shorter.ts new file mode 100644 index 0000000..cee97c4 --- /dev/null +++ b/shorter.ts @@ -0,0 +1,125 @@ +import { createCodemod } from "./deps.ts"; + +/** + * shorter executes the code modification to shorten a URL. + */ +export async function shorter(options: ShorterOptions): Promise { + const message = formatCommitMessage(options); + const codemodResult = await createCodemod((codemod) => + codemod + .createTree((tree) => + tree + .baseRef(ACMCSUF_MAIN_BRANCH) + .text(ACMCSUF_LINKS_PATH, (text) => { + const data = JSON.parse(text); + data[options.data.alias] = options.data.destination; + return JSON.stringify(data, null, 2) + "\n"; + }) + ) + .createCommit(({ 0: tree }) => ({ + message, + tree: tree.sha, + }), (commit) => commit.parentRef(ACMCSUF_MAIN_BRANCH)) + .createOrUpdateBranch(({ 1: commit }) => ({ + ref: ACMCSUF_MAIN_BRANCH, + sha: commit.sha, + })), { + owner: ACMCSUF_OWNER, + repo: ACMCSUF_REPO, + token: options.githubPAT, + }); + + return { + sha: codemodResult[1].sha, + message, + }; +} + +/** + * ShorterOptions are the options for the shorter API. + */ +export interface ShorterOptions { + /** + * githubPAT is the GitHub personal access token to use to authenticate with + * the GitHub API. + */ + githubPAT: string; + + /** + * actor is the user who invoked the slash command. + */ + actor: { + /** + * tag is the Discord tag of the actor. + */ + tag: string; + + /** + * nick is the nickname of the actor. + */ + nick?: string; + }; + + /** + * data is the data to shorten a URL. + */ + data: { + /** + * alias is the alias of the destination URL. + */ + alias: string; + + /** + * destination is the destination location. + */ + destination: string; + }; +} + +/** + * ShorterResult is the result of the shorter function. + */ +export interface ShorterResult { + /** + * sha is the SHA of the commit that was created on the main branch. + */ + sha: string; + + /** + * message is the commit message. + */ + message: string; +} + +/** + * ShorterError is an error that occurs during shortening. + */ +export class ShorterError extends Error { + public constructor(message: string) { + super(message); + } +} + +function formatCommitMessage(options: ShorterOptions): string { + return `update \`/${options.data.alias}\` shortlink`; +} + +/** + * ACMCSUF_OWNER is the owner of the acmcsuf.com GitHub repository. + */ +export const ACMCSUF_OWNER = "EthanThatOneKid"; + +/** + * ACMCSUF_REPO is the name of the acmcsuf.com GitHub repository. + */ +export const ACMCSUF_REPO = "acmcsuf.com"; + +/** + * ACMCSUF_MAIN_BRANCH is the name of the main branch of the acmcsuf.com GitHub + */ +export const ACMCSUF_MAIN_BRANCH = "main"; + +/** + * ACMCSUF_LINKS_PATH is the path to the board data file. + */ +export const ACMCSUF_LINKS_PATH = "src/lib/public/links/links.json";