diff --git a/.env.example b/.env.example index f354a87..73f3d6c 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ PORT=8080 -WEBHOOK_TOKEN="❤" DISCORD_APPLICATION_ID="❤" DISCORD_TOKEN="❤" DISCORD_PUBLIC_KEY="❤" DISCORD_WEBHOOK_URL="❤" DISCORD_CHANNEL_ID="❤" +KV_URL="❤" diff --git a/.github/workflows/cf.yaml b/.github/workflows/cf.yaml deleted file mode 100644 index 0445f78..0000000 --- a/.github/workflows/cf.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: cf push - -on: - workflow_dispatch: - push: - branches: [main] - -jobs: - push: - runs-on: ubuntu-latest - - strategy: - matrix: - deno-version: [canary] - - steps: - - uses: actions/checkout@v3 - - - uses: denoland/setup-deno@v1 - with: - deno-version: ${{ matrix.deno-version }} - - - name: Push to Cloudflare - run: deno task cf:push - env: - CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} - CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }} - WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }} diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index fbae6eb..13f48c1 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -27,5 +27,5 @@ jobs: - name: Lint run: deno lint && git diff-index --quiet HEAD - - name: Test - run: deno task test + # - name: Test + # run: deno task test diff --git a/README.md b/README.md index d9dcd8c..526a7d4 100644 --- a/README.md +++ b/README.md @@ -24,29 +24,7 @@ The server is automatically deployed on push to the `main` branch via ### Daily webhook invocation -The daily webhook is invoked by making a POST request to the `/webhook/:token` -endpoint. - -Set up a cron job to make the request at the desired time. Supabase supports -cron jobs via the -[pg_cron](https://supabase.com/docs/guides/database/extensions/pg_cron) -extension. - -```sql -select cron.unschedule('lc-daily'); - -select - cron.schedule( - 'lc-daily', - '0 0 * * *', -- https://crontab.guru/#0_0_*_*_* - $$ - select - net.http_post( - url:='...', - ) as request_id; - $$ - ); -``` +The daily webhook is invoked by a daily Deno Cron job. --- diff --git a/deno.jsonc b/deno.jsonc index 234ed21..b337ef5 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,17 +1,17 @@ { - "lock": "./deno.lock", "tasks": { - "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-write deps.ts", - "all": "deno task udd && deno lint && deno fmt", - "test": "deno test --unstable", - "start": "deno run -A --unstable main.ts", + "test": "deno test --unstable-kv", + "start": "deno run -A --env --unstable-kv main.ts", "ngrok": "ngrok http 8080", - "cf:push": "deno run -A tasks/cf/dailies/push/main.ts", - "cf:serve": "deno run -A tasks/cf/dailies/serve/main.ts", "dnt": "deno run -A tasks/dnt/main.ts" }, "imports": { + "@discord-applications/app": "jsr:@discord-applications/app@^0.0.4", + "@fartlabs/rt": "jsr:@fartlabs/rt@^0.0.3", + "@std/assert": "jsr:@std/assert@^0.225.3", + "@std/datetime": "jsr:@std/datetime@^0.224.0", + "@std/ulid": "jsr:@std/ulid@^0.224.0", + "discord-api-types": "npm:discord-api-types@^0.37.93", "lc-dailies/": "./" }, "fmt": { diff --git a/deno.lock b/deno.lock index 8ddf6cb..43c9762 100644 --- a/deno.lock +++ b/deno.lock @@ -1,118 +1,262 @@ { "version": "3", + "packages": { + "specifiers": { + "jsr:@discord-applications/app@^0.0.4": "jsr:@discord-applications/app@0.0.4", + "jsr:@fartlabs/rt@^0.0.3": "jsr:@fartlabs/rt@0.0.3", + "jsr:@std/assert@^0.225.3": "jsr:@std/assert@0.225.3", + "jsr:@std/datetime@^0.224.0": "jsr:@std/datetime@0.224.0", + "jsr:@std/internal@^1.0.0": "jsr:@std/internal@1.0.0", + "jsr:@std/ulid@^0.224.0": "jsr:@std/ulid@0.224.0", + "npm:discord-api-types@0.37.79": "npm:discord-api-types@0.37.79", + "npm:discord-api-types@^0.37.93": "npm:discord-api-types@0.37.93", + "npm:tweetnacl@^1.0.3": "npm:tweetnacl@1.0.3" + }, + "jsr": { + "@discord-applications/app@0.0.4": { + "integrity": "fa95279d13d2ad07636799d12dd637d930a6443a0d4223e9f65493a58cb47dd5", + "dependencies": [ + "npm:discord-api-types@0.37.79", + "npm:tweetnacl@^1.0.3" + ] + }, + "@fartlabs/rt@0.0.3": { + "integrity": "a94851f8a185f64b884120bb1d53fb203b62e8f98a27879ffa615560ef6ba806" + }, + "@std/assert@0.225.3": { + "integrity": "b3c2847aecf6955b50644cdb9cf072004ea3d1998dd7579fc0acb99dbb23bd4f", + "dependencies": [ + "jsr:@std/internal@^1.0.0" + ] + }, + "@std/datetime@0.224.0": { + "integrity": "7b2e41f44a241a2205a113d2931c536fa384e79d58298bd133ae003ef06637bb" + }, + "@std/internal@1.0.0": { + "integrity": "ac6a6dfebf838582c4b4f61a6907374e27e05bedb6ce276e0f1608fe84e7cd9a" + }, + "@std/ulid@0.224.0": { + "integrity": "1b5622ac78ccdbca72dd8da40b2b3b29e6062e1eb3267973f2e60e871e6683de" + } + }, + "npm": { + "discord-api-types@0.37.79": { + "integrity": "sha512-jblKMZL5f9t/pfUyhHNey8Lb9yVCcBVIPxz/JTY0raAmfj7CuFXdl9m5o/+iiB7E0vv1Kz9V7Ao5HtLRc2gH1Q==", + "dependencies": {} + }, + "discord-api-types@0.37.93": { + "integrity": "sha512-M5jn0x3bcXk8EI2c6F6V6LeOWq10B/cJf+YJSyqNmg7z4bdXK+Z7g9zGJwHS0h9Bfgs0nun2LQISFOzwck7G9A==", + "dependencies": {} + }, + "tweetnacl@1.0.3": { + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "dependencies": {} + } + } + }, "remote": { "https://cdn.skypack.dev/-/tweetnacl@v1.0.3-G4yM3nQ8lnXXlGGQADqJ/dist=es2019,mode=imports/optimized/tweetnacl.js": "d26554516df57e5cb58954e90c633c8871b4e66016b9fe4e07a36db5430bc8c7", "https://cdn.skypack.dev/tweetnacl@1.0.3": "6610aad2ac175c2d575995fc7de8ed552c2e5e05aef80ed8588cf3c6e2db61d7", - "https://deno.land/std@0.211.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", - "https://deno.land/std@0.211.0/assert/_diff.ts": "6a2d68f2c42d73a1e31818a4195f40598d672c7f02ac75c7f1b1e6789852c2bc", - "https://deno.land/std@0.211.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4", - "https://deno.land/std@0.211.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", - "https://deno.land/std@0.211.0/assert/assert_almost_equals.ts": "648ea72678296a5ad86d3bbb66904335fa97de3133223f44ca4596b225cdcbef", - "https://deno.land/std@0.211.0/assert/assert_array_includes.ts": "dbb461c20681807a884ad84d873f9e4daead380859531b1e7f27fa4e8f8bf431", - "https://deno.land/std@0.211.0/assert/assert_equals.ts": "b3b33ae8a85ae22a0754c61a7486d4ae870e8938830a94f5cacecba3a9b0442a", - "https://deno.land/std@0.211.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9", - "https://deno.land/std@0.211.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769", - "https://deno.land/std@0.211.0/assert/assert_greater.ts": "8dfcf082d2bcffcaab3bd0dab48d41e41c26266529567246de47bd6864936f6d", - "https://deno.land/std@0.211.0/assert/assert_greater_or_equal.ts": "9e02ef89f32563f539f7e66556930033418728847aefcca4e3806a735b5f122e", - "https://deno.land/std@0.211.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444", - "https://deno.land/std@0.211.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2", - "https://deno.land/std@0.211.0/assert/assert_less.ts": "91a6fed705f9c39bbd683b62aa9dfc42547bc886c29f696997e681cafb886b16", - "https://deno.land/std@0.211.0/assert/assert_less_or_equal.ts": "7a3c2e554eb20aa6af9dd4a410e550bcee9e8a28102d51f5f40cb1b8d141e4e1", - "https://deno.land/std@0.211.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1", - "https://deno.land/std@0.211.0/assert/assert_not_equals.ts": "cb78bf9a4357d69673c87b634491bc6b840412c8b55efe472af9877ef6f0a29b", - "https://deno.land/std@0.211.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931", - "https://deno.land/std@0.211.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f", - "https://deno.land/std@0.211.0/assert/assert_not_strict_equals.ts": "89ba25e1da5233404ac4c01651c088759b7977c51034eefc6050fe3fc2d10c46", - "https://deno.land/std@0.211.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49", - "https://deno.land/std@0.211.0/assert/assert_rejects.ts": "e9e0c8d9c3e164c7ac962c37b3be50577c5a2010db107ed272c4c1afb1269f54", - "https://deno.land/std@0.211.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366", - "https://deno.land/std@0.211.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7", - "https://deno.land/std@0.211.0/assert/assert_throws.ts": "edddd86b39606c342164b49ad88dd39a26e72a26655e07545d172f164b617fa7", - "https://deno.land/std@0.211.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", - "https://deno.land/std@0.211.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2", - "https://deno.land/std@0.211.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c", - "https://deno.land/std@0.211.0/assert/mod.ts": "325df8c0683ad83a873b9691aa66b812d6275fc9fec0b2d180ac68a2c5efed3b", - "https://deno.land/std@0.211.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd", - "https://deno.land/std@0.211.0/assert/unreachable.ts": "38cfecb95d8b06906022d2f9474794fca4161a994f83354fd079cac9032b5145", - "https://deno.land/std@0.211.0/datetime/constants.ts": "5c198b3b47fbcc4d913e61dcae1c37e053937affc2c9a6a5ad7e5473bab3e4a6", - "https://deno.land/std@0.211.0/dotenv/mod.ts": "bcfa9c102d5ce6218ec0c4a69aa0fc1009fc309a85448f98a8debaa75866a3d7", - "https://deno.land/std@0.211.0/dotenv/parse.ts": "ddcb04a8a7198918cfc52efbebb0d4fc51abfea913649912fcc1cf4c52bd81a2", - "https://deno.land/std@0.211.0/dotenv/stringify.ts": "74521a8e907adffff19f132765902a8c51743e386107b91cc934bae154031e84", - "https://deno.land/std@0.211.0/fmt/colors.ts": "be082d6a6bbb2980ae7b2bf8c23c6bb2811ba90a06a9bcb861344a71784c5a99", - "https://deno.land/x/discord_api_types@0.37.67/gateway/common.ts": "fb67003adda424df76c2726e0624d709c5a16e3694d6b75facd587d121fe121f", - "https://deno.land/x/discord_api_types@0.37.67/gateway/v10.ts": "f3a491ee47369c71d09f8710f25ed0164615d905bf8ce6a47ea4b908a50672c2", - "https://deno.land/x/discord_api_types@0.37.67/globals.ts": "7d8879654c4741ac071668ad52f2659bcdb66694cfe7da306c8437ec752807a7", - "https://deno.land/x/discord_api_types@0.37.67/payloads/common.ts": "4449a87e8c1cf6d091f667370be3a42609c1a4f44cbe5f9881f7fc0e6f6920cc", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/_chatInput/attachment.ts": "c66dccd54c1b84d073f2e1caa466e551b8045a84a2e8a88a1bfbc7e2c64a703d", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/_chatInput/base.ts": "f6a2556e14d489e1f0e5ddeb3a0303e2603e25330530dd9263e176013c5f51eb", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/_chatInput/boolean.ts": "65e29561b61785ca4ede4b1b4a88c5fc0696cfdf1fa74d5197588c196ee7ae98", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/_chatInput/channel.ts": "73c7fc49de242e1ce3be958375fd810750aed83553ef3860e3cddf858f9eb464", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/_chatInput/integer.ts": "a25d24a3e54d647c7039b99e3208fc0fc2228d174f6dcc421e93919b8154a011", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/_chatInput/mentionable.ts": "742e42857465866e0c08b587d7fb5ccd81d4705c61ce4cd6b97ad5692e88e969", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/_chatInput/number.ts": "be974ea68f5fdf55d7a7f5d3faf48a3193777d432b1aa9087afc204bcb916284", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/_chatInput/role.ts": "a57114d0f7eeee4ab7cf217a865dd9dbd9096d007c556aec6185d64257100f41", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/_chatInput/shared.ts": "9e2d3b3530280f6de5f9b6de1bb81e8a905998e058f784a9b041e48a96cd93d2", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/_chatInput/string.ts": "33ab12dab64544a70729b9b66b5a9790964ea779f05d4ab1a1e190e7c1b59e98", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/_chatInput/subcommand.ts": "0013737da6d2b54e2f413fbf31cf9c84ea51bd9204b615cb4fd19b420f856cb2", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/_chatInput/subcommandGroup.ts": "db5cc701bcb3d68c094de409da39c9a2b8834dc0d5038e5f963c96e5eaf412ac", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/_chatInput/user.ts": "ed2871693744298225ba53ddfb18d3e7afff20a34f413822d5b1193918aea27f", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/chatInput.ts": "46362da4e56c99cc69331481330d6e95c31d5e46f4cc36ec23f03cafbb687d52", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/contextMenu.ts": "89aed5f05f75d482e40259f55d0172143a90c1980d060d16545bdc14b68b29c4", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/internals.ts": "5eb5ea13a1247c73c0611886dea09ab8d632a9c5555ff0f33d44cd379fd75a08", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/_applicationCommands/permissions.ts": "ddca14b62e6afd418c1417117ffcc7cfb2ea5e5cc5353b4a0598435bdea45fb5", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/applicationCommands.ts": "b0646f2930d38113389bd1ecf8c605ac5af8fc40f93fafd17f968150419fac14", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/autocomplete.ts": "821ae50ff9845cac4b03169dbd4c4b187d8399765eb1f0d658d477c68e4c6136", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/base.ts": "ca2df849ee55f2fbd5fa9626c48811de7cc9b69979838c6c54982a5e3a44219a", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/messageComponents.ts": "08faa77d1c1d9a33359a962b78b695304e27cb6435af319b41e9a9d3b395adb2", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/modalSubmit.ts": "3a02d2d7df5bcdb1ffcd089f15e0d82ab65dcc0cefa904c6e0621f46edac041e", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/ping.ts": "096ce582e9af373649fd5355cccd7424adceaffb73367b5301f1594ef5a3c264", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/_interactions/responses.ts": "c1b0fc1ecca7858de08720e0222660e23935ebdd563e1964878a525eb29f062b", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/application.ts": "7ec267bf4b809534c8c5e919a7b1da7b33190f9d545445146e007ddb9d0554f5", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/auditLog.ts": "ced9d28a20b2bb201761c37ef266ace325a808a405b7472d70ad6df3b56c4d87", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/autoModeration.ts": "9ccb4408f1c6392d9619fac159997e08e660080b3f9567a1619163a40329e3a2", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/channel.ts": "47af2e40deb2ce6dd00438cd41d67d00f311ac330db6c27aed74da104776baf8", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/emoji.ts": "b9a30b16e1ec4dc15d6149e59aa48b02ad57a51335b7be5a7f5368db0491b3dd", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/gateway.ts": "4ce715fa94eadf5e2ba6adfc4a3bf99bac5d19c4787794ee1774b645a324db72", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/guild.ts": "4c03e054fc07d8db0ee7fa2b6f645f57f7642de0d28a704aa15047a51544f710", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/guildScheduledEvent.ts": "bf506b7807501b71077751ec793e719c5515e1bb405dec5cc4371a61b03cf8b9", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/interactions.ts": "17dabe94016dad3d0d7fdc0aa812bf5b0b366465dd72cd0b01168880778cc60d", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/invite.ts": "92c09f549482a4e2ad5a3c1062debfb262c6fe4b6740581175a0b8108873ab01", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/mod.ts": "83d68247652307f1587d71ac6983fd795ad7b9d5c92540a65207ea9293b09812", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/monetization.ts": "9a91c8dbb4f4c505e561630f0205f821e6877a5ea74faf4eaad9c154f5cc0d02", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/oauth2.ts": "dfb9f09fb44bf5faaa73ad4488ebe408905907d5fd46404895317f4e7c378489", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/permissions.ts": "5e7990e6dad3e35f8c130dc52de4bbf63afe5d6ad98e1c56b09da3ead94ad5a4", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/stageInstance.ts": "f0b9ee8c24c67298086fa32cb0595f6c29710d81b6fe85b958d48e6c549c4cb8", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/sticker.ts": "8511b5cdbe8289ce13fd51c2e96d24548345111b8d9f9c907dd3336f10e795bf", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/teams.ts": "101044d8c48a3cbabb60048eff9f69588bdd1ea84d1825de0372b4e23ad7ccbe", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/template.ts": "c6bee171ed0ce61fc8b59de42541a023bdcde62718deb42325397e5c82efdc27", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/user.ts": "361b93e3683fcf611c021c4f39c7501ca1482e999887d97a4b0a09398b4618c5", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/voice.ts": "62d03a540f2e78e5f3989f71a0ee1ec682ef7306a4fa096f89118cbd82351d47", - "https://deno.land/x/discord_api_types@0.37.67/payloads/v10/webhook.ts": "7fc370f40a84f12a6e57ddda7cf2814f15039d6320b46979db0b49d5b91e303b", - "https://deno.land/x/discord_api_types@0.37.67/rest/common.ts": "36f7f83f8c1b95d68d9a55bc00b1d54eb4672186960962684826581f13ef9643", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/application.ts": "9f1c388bb5ccba192a57923c0aabe525f47de2d5ef8710a5d9a0cd0a3fa55317", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/auditLog.ts": "39a0914b6c51445023d82c3e3e66c9866cbb3cb6774d3e7eac63414ea9bcbfec", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/autoModeration.ts": "3d388fd9a91c34f04b5e3e1b6ffe12029fb48b511f37ff88042325ec6cbc6605", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/channel.ts": "421ab1178e83765fe403c75b8e481823bd10a9288c1487163c4f1b5049d2ca7c", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/emoji.ts": "9f694a1bd63886c62a87b4320f3bfa5d4f534b2d87c317d77d572d10522df3aa", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/gateway.ts": "747cb95c9a8bca4e52423c780d5fc492fb0dab2b6015cd7e51e890e8d51acf29", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/guild.ts": "6822854ad9026c2be656acc81e8161d45d650d44170e664762ec7e67b1f15e4e", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/guildScheduledEvent.ts": "29d361f395d8cd1ecb47550615d19e10793d513cc5ad8d32895da2cc9cd0fd89", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/interactions.ts": "2b6decdfff921b6aa8f0e6e5c61d38469a0178c5ecf1a18dd17ad6738143e662", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/invite.ts": "28f8e740bdaa782c9d9d504049323762b5c1180348019dc5f9e0a900ec11213e", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/mod.ts": "de48b0db06354fc8d37034a6cc5052a56fa38f4105db4df2157cfca8c164fbba", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/monetization.ts": "93f066371177fc847576df6875cd5019b116bc7ad0b2559395452d75920085a4", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/oauth2.ts": "b659a35654c17767480d142c46c36f5fe2544346875745e3f654c5e7c3d9f3f9", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/stageInstance.ts": "a090ff8b54f77188323af5d06cdef9c42738edf9a9b0eba8aad3c89d5ac5569f", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/sticker.ts": "1f7f2729308a0ec1fe373b5df7ac71bafc200132f1a06df7e75bf5ce1d1069c1", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/template.ts": "0ad41c3c85571d3c5b0bec3914c678a21f376ec162ef0d3f1f7731a8d1d1009c", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/user.ts": "60cfa227426c791021e9e8f769287e997477e722db5a3c577a9ec54078aaffca", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/voice.ts": "cdbe9d6c39c8f44635d8632bf62a95b8c15877b92c56ddf69df2072bb1a74edc", - "https://deno.land/x/discord_api_types@0.37.67/rest/v10/webhook.ts": "3e16c7d6b2517411a66b79edceaea64c761a7b4cf13cbc03eb3cb441c45a26e7", - "https://deno.land/x/discord_api_types@0.37.67/rpc/common.ts": "a693352ffd86ae9e995fb3fbfbfd2be30896257ecb83c5611050f060b08de4ef", - "https://deno.land/x/discord_api_types@0.37.67/rpc/v10.ts": "fbaad9f3d73fce88e76b0e52ad5345093f18077e4293937c9ec0ee24415b9a93", - "https://deno.land/x/discord_api_types@0.37.67/utils/internals.ts": "cb70895ba89f7947c38f7fa447b0190cb14b5585be323414cda53d2ccb19b16c", - "https://deno.land/x/discord_api_types@0.37.67/utils/v10.ts": "056bd036f8c65365ff28eb63ec6897811d51921cca6d068392dd1ca5b397ae62", - "https://deno.land/x/discord_api_types@0.37.67/v10.ts": "f3f23492c59e77859aba5b34431edf3668c37f722d7f70c2e1ef7ba4bcda3010", - "https://deno.land/x/ulid@v0.3.0/mod.ts": "f7ff065b66abd485051fc68af23becef6ccc7e81f7774d7fcfd894a4b2da1984" + "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", + "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49", + "https://deno.land/std@0.140.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d", + "https://deno.land/std@0.140.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9", + "https://deno.land/std@0.140.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf", + "https://deno.land/std@0.140.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37", + "https://deno.land/std@0.140.0/fs/_util.ts": "0fb24eb4bfebc2c194fb1afdb42b9c3dda12e368f43e8f2321f84fc77d42cb0f", + "https://deno.land/std@0.140.0/fs/ensure_dir.ts": "9dc109c27df4098b9fc12d949612ae5c9c7169507660dcf9ad90631833209d9d", + "https://deno.land/std@0.140.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b", + "https://deno.land/std@0.140.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", + "https://deno.land/std@0.140.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", + "https://deno.land/std@0.140.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b", + "https://deno.land/std@0.140.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", + "https://deno.land/std@0.140.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", + "https://deno.land/std@0.140.0/path/mod.ts": "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d", + "https://deno.land/std@0.140.0/path/posix.ts": "293cdaec3ecccec0a9cc2b534302dfe308adb6f10861fa183275d6695faace44", + "https://deno.land/std@0.140.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", + "https://deno.land/std@0.140.0/path/win32.ts": "31811536855e19ba37a999cd8d1b62078235548d67902ece4aa6b814596dd757", + "https://deno.land/std@0.140.0/streams/conversion.ts": "712585bfa0172a97fb68dd46e784ae8ad59d11b88079d6a4ab098ff42e697d21", + "https://deno.land/std@0.181.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.181.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.181.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e", + "https://deno.land/std@0.181.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32", + "https://deno.land/std@0.181.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688", + "https://deno.land/std@0.181.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40", + "https://deno.land/std@0.181.0/fs/expand_glob.ts": "e4f56259a0a70fe23f05215b00de3ac5e6ba46646ab2a06ebbe9b010f81c972a", + "https://deno.land/std@0.181.0/fs/walk.ts": "ea95ffa6500c1eda6b365be488c056edc7c883a1db41ef46ec3bf057b1c0fe32", + "https://deno.land/std@0.181.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.181.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.181.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", + "https://deno.land/std@0.181.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.181.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.181.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c", + "https://deno.land/std@0.181.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", + "https://deno.land/std@0.181.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.181.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", + "https://deno.land/std@0.218.2/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", + "https://deno.land/std@0.218.2/assert/_diff.ts": "dcc63d94ca289aec80644030cf88ccbf7acaa6fbd7b0f22add93616b36593840", + "https://deno.land/std@0.218.2/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4", + "https://deno.land/std@0.218.2/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", + "https://deno.land/std@0.218.2/assert/assert_almost_equals.ts": "8b96b7385cc117668b0720115eb6ee73d04c9bcb2f5d2344d674918c9113688f", + "https://deno.land/std@0.218.2/assert/assert_array_includes.ts": "1688d76317fd45b7e93ef9e2765f112fdf2b7c9821016cdfb380b9445374aed1", + "https://deno.land/std@0.218.2/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e", + "https://deno.land/std@0.218.2/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9", + "https://deno.land/std@0.218.2/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769", + "https://deno.land/std@0.218.2/assert/assert_greater.ts": "4945cf5729f1a38874d7e589e0fe5cc5cd5abe5573ca2ddca9d3791aa891856c", + "https://deno.land/std@0.218.2/assert/assert_greater_or_equal.ts": "573ed8823283b8d94b7443eb69a849a3c369a8eb9666b2d1db50c33763a5d219", + "https://deno.land/std@0.218.2/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444", + "https://deno.land/std@0.218.2/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2", + "https://deno.land/std@0.218.2/assert/assert_less.ts": "2b4b3fe7910f65f7be52212f19c3977ecb8ba5b2d6d0a296c83cde42920bb005", + "https://deno.land/std@0.218.2/assert/assert_less_or_equal.ts": "b93d212fe669fbde959e35b3437ac9a4468f2e6b77377e7b6ea2cfdd825d38a0", + "https://deno.land/std@0.218.2/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1", + "https://deno.land/std@0.218.2/assert/assert_not_equals.ts": "ac86413ab70ffb14fdfc41740ba579a983fe355ba0ce4a9ab685e6b8e7f6a250", + "https://deno.land/std@0.218.2/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931", + "https://deno.land/std@0.218.2/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f", + "https://deno.land/std@0.218.2/assert/assert_not_strict_equals.ts": "da0b8ab60a45d5a9371088378e5313f624799470c3b54c76e8b8abeec40a77be", + "https://deno.land/std@0.218.2/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49", + "https://deno.land/std@0.218.2/assert/assert_rejects.ts": "5206ac37d883797d9504e3915a0c7b692df6efcdefff3889cc14bb5a325641dd", + "https://deno.land/std@0.218.2/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366", + "https://deno.land/std@0.218.2/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7", + "https://deno.land/std@0.218.2/assert/assert_throws.ts": "31f3c061338aec2c2c33731973d58ccd4f14e42f355501541409ee958d2eb8e5", + "https://deno.land/std@0.218.2/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", + "https://deno.land/std@0.218.2/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2", + "https://deno.land/std@0.218.2/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c", + "https://deno.land/std@0.218.2/assert/mod.ts": "325df8c0683ad83a873b9691aa66b812d6275fc9fec0b2d180ac68a2c5efed3b", + "https://deno.land/std@0.218.2/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd", + "https://deno.land/std@0.218.2/assert/unreachable.ts": "3670816a4ab3214349acb6730e3e6f5299021234657eefe05b48092f3848c270", + "https://deno.land/std@0.218.2/datetime/constants.ts": "5c198b3b47fbcc4d913e61dcae1c37e053937affc2c9a6a5ad7e5473bab3e4a6", + "https://deno.land/std@0.218.2/dotenv/mod.ts": "0180eaeedaaf88647318811cdaa418cc64dc51fb08354f91f5f480d0a1309f7d", + "https://deno.land/std@0.218.2/dotenv/parse.ts": "a49ed243bd62ec26f38614042cda4e5e3a47b18337c93b0f9f8556a5c918884b", + "https://deno.land/std@0.218.2/dotenv/stringify.ts": "0047ad7068289735d08964046aea267a750c141b494ca0e38831b89be6c020c2", + "https://deno.land/std@0.218.2/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a", + "https://deno.land/x/code_block_writer@12.0.0/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5", + "https://deno.land/x/code_block_writer@12.0.0/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff", + "https://deno.land/x/deno_cache@0.6.2/auth_tokens.ts": "5d1d56474c54a9d152e44d43ea17c2e6a398dd1e9682c69811a313567c01ee1e", + "https://deno.land/x/deno_cache@0.6.2/cache.ts": "58b53c128b742757efcad10af9a3871f23b4e200674cb5b0ddf61164fb9b2fe7", + "https://deno.land/x/deno_cache@0.6.2/deno_dir.ts": "1ea355b8ba11c630d076b222b197cfc937dd81e5a4a260938997da99e8ff93a0", + "https://deno.land/x/deno_cache@0.6.2/deps.ts": "12cca94516cf2d3ed42fccd4b721ecd8060679253f077d83057511045b0081aa", + "https://deno.land/x/deno_cache@0.6.2/dirs.ts": "009c6f54e0b610914d6ce9f72f6f6ccfffd2d47a79a19061e0a9eb4253836069", + "https://deno.land/x/deno_cache@0.6.2/disk_cache.ts": "66a1e604a8d564b6dd0500326cac33d08b561d331036bf7272def80f2f7952aa", + "https://deno.land/x/deno_cache@0.6.2/file_fetcher.ts": "4f3e4a2c78a5ca1e4812099e5083f815a8525ab20d389b560b3517f6b1161dd6", + "https://deno.land/x/deno_cache@0.6.2/http_cache.ts": "407135eaf2802809ed373c230d57da7ef8dff923c4abf205410b9b99886491fd", + "https://deno.land/x/deno_cache@0.6.2/lib/deno_cache_dir.generated.js": "59f8defac32e8ebf2a30f7bc77e9d88f0e60098463fb1b75e00b9791a4bbd733", + "https://deno.land/x/deno_cache@0.6.2/lib/snippets/deno_cache_dir-a2aecaa9536c9402/fs.js": "cbe3a976ed63c72c7cb34ef845c27013033a3b11f9d8d3e2c4aa5dda2c0c7af6", + "https://deno.land/x/deno_cache@0.6.2/mod.ts": "b4004287e1c6123d7f07fe9b5b3e94ce6d990c4102949a89c527c68b19627867", + "https://deno.land/x/deno_cache@0.6.2/util.ts": "f3f5a0cfc60051f09162942fb0ee87a0e27b11a12aec4c22076e3006be4cc1e2", + "https://deno.land/x/deno_graph@0.53.0/deno_graph_wasm.generated.js": "2cbaec012743f138172c0aff377c589ca1dd25331b77acada8ea4aafd6ec8bb4", + "https://deno.land/x/deno_graph@0.53.0/loader.ts": "a2e757383908f4a51659fe1b1203386887ebb17756bac930a64856d613d8d57d", + "https://deno.land/x/deno_graph@0.53.0/media_type.ts": "a89a1b38d07c160e896de9ceb99285ba8391940140558304171066b5c3ef7609", + "https://deno.land/x/deno_graph@0.53.0/mod.ts": "e4bdddf09d8332394ac4b2e7084f7f4fbbbf09dff344cac9bd60f5e20b4f12e0", + "https://deno.land/x/dir@1.5.1/data_local_dir/mod.ts": "91eb1c4bfadfbeda30171007bac6d85aadacd43224a5ed721bbe56bc64e9eb66", + "https://deno.land/x/discord_api_types@0.37.71/gateway/common.ts": "fb67003adda424df76c2726e0624d709c5a16e3694d6b75facd587d121fe121f", + "https://deno.land/x/discord_api_types@0.37.71/gateway/v10.ts": "f3a491ee47369c71d09f8710f25ed0164615d905bf8ce6a47ea4b908a50672c2", + "https://deno.land/x/discord_api_types@0.37.71/globals.ts": "7d8879654c4741ac071668ad52f2659bcdb66694cfe7da306c8437ec752807a7", + "https://deno.land/x/discord_api_types@0.37.71/payloads/common.ts": "4449a87e8c1cf6d091f667370be3a42609c1a4f44cbe5f9881f7fc0e6f6920cc", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/_chatInput/attachment.ts": "c66dccd54c1b84d073f2e1caa466e551b8045a84a2e8a88a1bfbc7e2c64a703d", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/_chatInput/base.ts": "f6a2556e14d489e1f0e5ddeb3a0303e2603e25330530dd9263e176013c5f51eb", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/_chatInput/boolean.ts": "65e29561b61785ca4ede4b1b4a88c5fc0696cfdf1fa74d5197588c196ee7ae98", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/_chatInput/channel.ts": "73c7fc49de242e1ce3be958375fd810750aed83553ef3860e3cddf858f9eb464", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/_chatInput/integer.ts": "a25d24a3e54d647c7039b99e3208fc0fc2228d174f6dcc421e93919b8154a011", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/_chatInput/mentionable.ts": "742e42857465866e0c08b587d7fb5ccd81d4705c61ce4cd6b97ad5692e88e969", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/_chatInput/number.ts": "be974ea68f5fdf55d7a7f5d3faf48a3193777d432b1aa9087afc204bcb916284", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/_chatInput/role.ts": "a57114d0f7eeee4ab7cf217a865dd9dbd9096d007c556aec6185d64257100f41", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/_chatInput/shared.ts": "9e2d3b3530280f6de5f9b6de1bb81e8a905998e058f784a9b041e48a96cd93d2", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/_chatInput/string.ts": "33ab12dab64544a70729b9b66b5a9790964ea779f05d4ab1a1e190e7c1b59e98", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/_chatInput/subcommand.ts": "0013737da6d2b54e2f413fbf31cf9c84ea51bd9204b615cb4fd19b420f856cb2", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/_chatInput/subcommandGroup.ts": "db5cc701bcb3d68c094de409da39c9a2b8834dc0d5038e5f963c96e5eaf412ac", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/_chatInput/user.ts": "ed2871693744298225ba53ddfb18d3e7afff20a34f413822d5b1193918aea27f", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/chatInput.ts": "46362da4e56c99cc69331481330d6e95c31d5e46f4cc36ec23f03cafbb687d52", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/contextMenu.ts": "89aed5f05f75d482e40259f55d0172143a90c1980d060d16545bdc14b68b29c4", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/internals.ts": "5eb5ea13a1247c73c0611886dea09ab8d632a9c5555ff0f33d44cd379fd75a08", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/_applicationCommands/permissions.ts": "ddca14b62e6afd418c1417117ffcc7cfb2ea5e5cc5353b4a0598435bdea45fb5", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/applicationCommands.ts": "b0646f2930d38113389bd1ecf8c605ac5af8fc40f93fafd17f968150419fac14", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/autocomplete.ts": "821ae50ff9845cac4b03169dbd4c4b187d8399765eb1f0d658d477c68e4c6136", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/base.ts": "ca2df849ee55f2fbd5fa9626c48811de7cc9b69979838c6c54982a5e3a44219a", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/messageComponents.ts": "08faa77d1c1d9a33359a962b78b695304e27cb6435af319b41e9a9d3b395adb2", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/modalSubmit.ts": "3a02d2d7df5bcdb1ffcd089f15e0d82ab65dcc0cefa904c6e0621f46edac041e", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/ping.ts": "096ce582e9af373649fd5355cccd7424adceaffb73367b5301f1594ef5a3c264", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/_interactions/responses.ts": "c1b0fc1ecca7858de08720e0222660e23935ebdd563e1964878a525eb29f062b", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/application.ts": "7ec267bf4b809534c8c5e919a7b1da7b33190f9d545445146e007ddb9d0554f5", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/auditLog.ts": "ced9d28a20b2bb201761c37ef266ace325a808a405b7472d70ad6df3b56c4d87", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/autoModeration.ts": "9ccb4408f1c6392d9619fac159997e08e660080b3f9567a1619163a40329e3a2", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/channel.ts": "e8ba5dc1ad08dcf4f8ff64af2b325a55bdb466068d10528fe230cd96b803eb40", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/emoji.ts": "b9a30b16e1ec4dc15d6149e59aa48b02ad57a51335b7be5a7f5368db0491b3dd", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/gateway.ts": "4ce715fa94eadf5e2ba6adfc4a3bf99bac5d19c4787794ee1774b645a324db72", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/guild.ts": "4c03e054fc07d8db0ee7fa2b6f645f57f7642de0d28a704aa15047a51544f710", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/guildScheduledEvent.ts": "bf506b7807501b71077751ec793e719c5515e1bb405dec5cc4371a61b03cf8b9", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/interactions.ts": "17dabe94016dad3d0d7fdc0aa812bf5b0b366465dd72cd0b01168880778cc60d", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/invite.ts": "92c09f549482a4e2ad5a3c1062debfb262c6fe4b6740581175a0b8108873ab01", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/mod.ts": "83d68247652307f1587d71ac6983fd795ad7b9d5c92540a65207ea9293b09812", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/monetization.ts": "9a91c8dbb4f4c505e561630f0205f821e6877a5ea74faf4eaad9c154f5cc0d02", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/oauth2.ts": "dfb9f09fb44bf5faaa73ad4488ebe408905907d5fd46404895317f4e7c378489", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/permissions.ts": "5e7990e6dad3e35f8c130dc52de4bbf63afe5d6ad98e1c56b09da3ead94ad5a4", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/stageInstance.ts": "f0b9ee8c24c67298086fa32cb0595f6c29710d81b6fe85b958d48e6c549c4cb8", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/sticker.ts": "8511b5cdbe8289ce13fd51c2e96d24548345111b8d9f9c907dd3336f10e795bf", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/teams.ts": "101044d8c48a3cbabb60048eff9f69588bdd1ea84d1825de0372b4e23ad7ccbe", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/template.ts": "c6bee171ed0ce61fc8b59de42541a023bdcde62718deb42325397e5c82efdc27", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/user.ts": "361b93e3683fcf611c021c4f39c7501ca1482e999887d97a4b0a09398b4618c5", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/voice.ts": "62d03a540f2e78e5f3989f71a0ee1ec682ef7306a4fa096f89118cbd82351d47", + "https://deno.land/x/discord_api_types@0.37.71/payloads/v10/webhook.ts": "7fc370f40a84f12a6e57ddda7cf2814f15039d6320b46979db0b49d5b91e303b", + "https://deno.land/x/discord_api_types@0.37.71/rest/common.ts": "f9a033daf3d11aaeea1917e6fc51405efcfa786be59fc64e4854c4b74a41cd97", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/application.ts": "9f1c388bb5ccba192a57923c0aabe525f47de2d5ef8710a5d9a0cd0a3fa55317", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/auditLog.ts": "39a0914b6c51445023d82c3e3e66c9866cbb3cb6774d3e7eac63414ea9bcbfec", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/autoModeration.ts": "3d388fd9a91c34f04b5e3e1b6ffe12029fb48b511f37ff88042325ec6cbc6605", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/channel.ts": "caeb142faffd74b6534a5d3dbf0e1aa7b339e67f954a5da6f5578bbb49335272", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/emoji.ts": "9f694a1bd63886c62a87b4320f3bfa5d4f534b2d87c317d77d572d10522df3aa", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/gateway.ts": "747cb95c9a8bca4e52423c780d5fc492fb0dab2b6015cd7e51e890e8d51acf29", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/guild.ts": "6822854ad9026c2be656acc81e8161d45d650d44170e664762ec7e67b1f15e4e", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/guildScheduledEvent.ts": "29d361f395d8cd1ecb47550615d19e10793d513cc5ad8d32895da2cc9cd0fd89", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/interactions.ts": "2b6decdfff921b6aa8f0e6e5c61d38469a0178c5ecf1a18dd17ad6738143e662", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/invite.ts": "28f8e740bdaa782c9d9d504049323762b5c1180348019dc5f9e0a900ec11213e", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/mod.ts": "963c8060f610b2215ce4fda950000246895d57315facdb9ae86bac7657078541", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/monetization.ts": "93f066371177fc847576df6875cd5019b116bc7ad0b2559395452d75920085a4", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/oauth2.ts": "b659a35654c17767480d142c46c36f5fe2544346875745e3f654c5e7c3d9f3f9", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/stageInstance.ts": "a090ff8b54f77188323af5d06cdef9c42738edf9a9b0eba8aad3c89d5ac5569f", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/sticker.ts": "1f7f2729308a0ec1fe373b5df7ac71bafc200132f1a06df7e75bf5ce1d1069c1", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/template.ts": "0ad41c3c85571d3c5b0bec3914c678a21f376ec162ef0d3f1f7731a8d1d1009c", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/user.ts": "60cfa227426c791021e9e8f769287e997477e722db5a3c577a9ec54078aaffca", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/voice.ts": "cdbe9d6c39c8f44635d8632bf62a95b8c15877b92c56ddf69df2072bb1a74edc", + "https://deno.land/x/discord_api_types@0.37.71/rest/v10/webhook.ts": "3e16c7d6b2517411a66b79edceaea64c761a7b4cf13cbc03eb3cb441c45a26e7", + "https://deno.land/x/discord_api_types@0.37.71/rpc/common.ts": "a693352ffd86ae9e995fb3fbfbfd2be30896257ecb83c5611050f060b08de4ef", + "https://deno.land/x/discord_api_types@0.37.71/rpc/v10.ts": "fbaad9f3d73fce88e76b0e52ad5345093f18077e4293937c9ec0ee24415b9a93", + "https://deno.land/x/discord_api_types@0.37.71/utils/internals.ts": "cb70895ba89f7947c38f7fa447b0190cb14b5585be323414cda53d2ccb19b16c", + "https://deno.land/x/discord_api_types@0.37.71/utils/v10.ts": "056bd036f8c65365ff28eb63ec6897811d51921cca6d068392dd1ca5b397ae62", + "https://deno.land/x/discord_api_types@0.37.71/v10.ts": "f3f23492c59e77859aba5b34431edf3668c37f722d7f70c2e1ef7ba4bcda3010", + "https://deno.land/x/dnt@0.40.0/lib/compiler.ts": "7f4447531581896348b8a379ab94730856b42ae50d99043f2468328360293cb1", + "https://deno.land/x/dnt@0.40.0/lib/compiler_transforms.ts": "f21aba052f5dcf0b0595c734450842855c7f572e96165d3d34f8fed2fc1f7ba1", + "https://deno.land/x/dnt@0.40.0/lib/mod.deps.ts": "8d6123c8e1162037e58aa8126686a03d1e2cffb250a8757bf715f80242097597", + "https://deno.land/x/dnt@0.40.0/lib/npm_ignore.ts": "57fbb7e7b935417d225eec586c6aa240288905eb095847d3f6a88e290209df4e", + "https://deno.land/x/dnt@0.40.0/lib/package_json.ts": "607b0a4f44acad071a4c8533b312a27d6671eac8e6a23625c8350ce29eadb2ba", + "https://deno.land/x/dnt@0.40.0/lib/pkg/dnt_wasm.generated.js": "2694546844a50861d6d1610859afbf5130baca4dc6cf304541b7ec2d6d998142", + "https://deno.land/x/dnt@0.40.0/lib/pkg/snippets/dnt-wasm-a15ef721fa5290c5/helpers.js": "aba69a019a6da6f084898a6c7b903b8b583bc0dbd82bfb338449cf0b5bce58fd", + "https://deno.land/x/dnt@0.40.0/lib/shims.ts": "39e5c141f0315c0faf30b479b53f92b9078d92e1fd67ee34cc60b701d8e68dab", + "https://deno.land/x/dnt@0.40.0/lib/test_runner/get_test_runner_code.ts": "4dc7a73a13b027341c0688df2b29a4ef102f287c126f134c33f69f0339b46968", + "https://deno.land/x/dnt@0.40.0/lib/test_runner/test_runner.ts": "4d0da0500ec427d5f390d9a8d42fb882fbeccc92c92d66b6f2e758606dbd40e6", + "https://deno.land/x/dnt@0.40.0/lib/transform.deps.ts": "2e159661e1c5c650de9a573babe0e319349fe493105157307ec2ad2f6a52c94e", + "https://deno.land/x/dnt@0.40.0/lib/types.ts": "b8e228b2fac44c2ae902fbb73b1689f6ab889915bd66486c8a85c0c24255f5fb", + "https://deno.land/x/dnt@0.40.0/lib/utils.ts": "224f15f33e7226a2fd991e438d0291d7ed8c7889807efa2e1ecb67d2d1db6720", + "https://deno.land/x/dnt@0.40.0/mod.ts": "ae1890fbe592e4797e7dd88c1e270f22b8334878e9bf187c4e11ae75746fe778", + "https://deno.land/x/dnt@0.40.0/transform.ts": "f68743a14cf9bf53bfc9c81073871d69d447a7f9e3453e0447ca2fb78926bb1d", + "https://deno.land/x/ts_morph@20.0.0/bootstrap/mod.ts": "b53aad517f106c4079971fcd4a81ab79fadc40b50061a3ab2b741a09119d51e9", + "https://deno.land/x/ts_morph@20.0.0/bootstrap/ts_morph_bootstrap.js": "6645ac03c5e6687dfa8c78109dc5df0250b811ecb3aea2d97c504c35e8401c06", + "https://deno.land/x/ts_morph@20.0.0/common/DenoRuntime.ts": "6a7180f0c6e90dcf23ccffc86aa8271c20b1c4f34c570588d08a45880b7e172d", + "https://deno.land/x/ts_morph@20.0.0/common/mod.ts": "01985d2ee7da8d1caee318a9d07664774fbee4e31602bc2bb6bb62c3489555ed", + "https://deno.land/x/ts_morph@20.0.0/common/ts_morph_common.js": "2325f94f61dc5f3f98a1dab366dc93048d11b1433d718b10cfc6ee5a1cfebe8f", + "https://deno.land/x/ts_morph@20.0.0/common/typescript.js": "b9edf0a451685d13e0467a7ed4351d112b74bd1e256b915a2b941054e31c1736", + "https://deno.land/x/ulid@v0.3.0/mod.ts": "f7ff065b66abd485051fc68af23becef6ccc7e81f7774d7fcfd894a4b2da1984", + "https://deno.land/x/wasmbuild@0.14.1/cache.ts": "89eea5f3ce6035a1164b3e655c95f21300498920575ade23161421f5b01967f4", + "https://deno.land/x/wasmbuild@0.14.1/loader.ts": "d98d195a715f823151cbc8baa3f32127337628379a02d9eb2a3c5902dbccfc02", + "https://deno.land/x/wasmbuild@0.15.1/cache.ts": "9d01b5cb24e7f2a942bbd8d14b093751fa690a6cde8e21709ddc97667e6669ed", + "https://deno.land/x/wasmbuild@0.15.1/loader.ts": "8c2fc10e21678e42f84c5135d8ab6ab7dc92424c3f05d2354896a29ccfd02a63" + }, + "workspace": { + "dependencies": [ + "jsr:@discord-applications/app@^0.0.4", + "jsr:@fartlabs/rt@^0.0.3", + "jsr:@std/assert@^0.225.3", + "jsr:@std/datetime@^0.224.0", + "jsr:@std/ulid@^0.224.0", + "npm:discord-api-types@^0.37.93" + ] } } diff --git a/deps.ts b/deps.ts deleted file mode 100644 index 5d2dc5d..0000000 --- a/deps.ts +++ /dev/null @@ -1,27 +0,0 @@ -export { - assertEquals, - assertRejects, -} from "https://deno.land/std@0.211.0/assert/mod.ts"; -export { load } from "https://deno.land/std@0.211.0/dotenv/mod.ts"; -export * from "https://deno.land/std@0.211.0/datetime/constants.ts"; -export { ulid } from "https://deno.land/x/ulid@v0.3.0/mod.ts"; -export type { - APIApplicationCommandInteractionDataOption, - APIApplicationCommandOption, - APIEmbed, - APIInteraction, - APIInteractionResponse, - APIInteractionResponseChannelMessageWithSource, - APIInteractionResponseDeferredChannelMessageWithSource, - APIUser, - RESTPostAPIApplicationCommandsJSONBody, - RESTPostAPIWebhookWithTokenJSONBody, -} from "https://deno.land/x/discord_api_types@0.37.67/v10.ts"; -export { - ApplicationCommandOptionType, - InteractionResponseType, - InteractionType, - MessageFlags, - Utils, -} from "https://deno.land/x/discord_api_types@0.37.67/v10.ts"; -export { default as nacl } from "https://cdn.skypack.dev/tweetnacl@1.0.3"; diff --git a/env.ts b/env.ts deleted file mode 100644 index f6c84ef..0000000 --- a/env.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { load } from "lc-dailies/deps.ts"; - -await load({ export: true }); - -/** - * PORT is the port to listen on. - */ -export const PORT = parseInt(Deno.env.get("PORT") || "8080"); - -/** - * WEBHOOK_TOKEN is used to authenticate requests to execute our webhook. - * - * Usage: POST /daily/:token - */ -export const WEBHOOK_TOKEN = Deno.env.get("WEBHOOK_TOKEN")!; - -/** - * DISCORD_APPLICATION_ID is the application ID of the Discord application. - */ -export const DISCORD_APPLICATION_ID = Deno.env.get("DISCORD_APPLICATION_ID")!; - -/** - * DISCORD_TOKEN is the token of the Discord application. - */ -export const DISCORD_TOKEN = Deno.env.get("DISCORD_TOKEN")!; - -/** - * DISCORD_PUBLIC_KEY is the public key of the Discord application. - */ -export const DISCORD_PUBLIC_KEY = Deno.env.get("DISCORD_PUBLIC_KEY")!; - -/** - * DISCORD_WEBHOOK_URL is the webhook URL of the Discord application. - */ -export const DISCORD_WEBHOOK_URL = Deno.env.get("DISCORD_WEBHOOK_URL")!; - -/** - * DISCORD_CHANNEL_ID is the channel ID specified for the application. - */ -export const DISCORD_CHANNEL_ID = Deno.env.get("DISCORD_CHANNEL_ID")!; - -/** - * KV_URL is the URL of the KV store. - */ -export const KV_URL = Deno.env.get("KV_URL"); diff --git a/lib/api/api.ts b/lib/api/api.ts index 233d1a1..688229f 100644 --- a/lib/api/api.ts +++ b/lib/api/api.ts @@ -1,83 +1,89 @@ -import * as discord from "lc-dailies/lib/discord/mod.ts"; -import * as lc from "lc-dailies/lib/lc/mod.ts"; +import { createRouter } from "@fartlabs/rt"; import * as leaderboard from "lc-dailies/lib/leaderboard/mod.ts"; -import * as router from "lc-dailies/lib/router/mod.ts"; -import * as discord_app from "./discord_app/mod.ts"; -import { - makeDailyWebhookPostHandler, - makeManualDailyWebhookPostHandler, -} from "./dailies.ts"; -import { - makeSeasonGetHandler, - makeSeasonsGetHandler, - makeSeasonTxtGetHandler, -} from "./seasons.ts"; +import * as discord_app from "./discord/mod.ts"; +import type { Season } from "./types.ts"; + +export * from "./types.ts"; + +/** + * APIRouterOptions are the options for the API router. + */ +export interface APIRouterOptions { + leaderboardClient: leaderboard.LeaderboardClient; + discordChannelID: string; + discordApplicationID: string; + discordPublicKey: string; + discordToken: string; +} /** * makeAPIRouter creates a router which handles requests on the * LC-Dailies API. */ -export function makeAPIRouter( - discordApplicationID: string, - discordPublicKey: string, - discordChannelID: string, - webhookURL: string, - webhookToken: string, - lcClient: lc.LCClient, - leaderboardClient: leaderboard.LeaderboardClient, -) { - return new router.Router() +export async function makeAPIRouter(options: APIRouterOptions) { + const app = await discord_app.makeDiscordAppHandler( + options.leaderboardClient, + options.discordApplicationID, + options.discordChannelID, + options.discordPublicKey, + options.discordToken, + ); + return createRouter() .post( - new URLPattern({ pathname: "/" }), - discord_app.withErrorResponse( - discord_app.makeDiscordAppHandler( - leaderboardClient, - discordPublicKey, - discordChannelID, - ), - ), - ) - .post( - new URLPattern({ pathname: "/webhook" }), - makeManualDailyWebhookPostHandler( - lcClient, - leaderboardClient, - ), - ) - .post( - new URLPattern({ pathname: "/webhook/:token" }), - makeDailyWebhookPostHandler( - lcClient, - leaderboardClient, - webhookURL, - webhookToken, - ), + "/", + (ctx) => discord_app.withErrorResponse(app)(ctx.request), ) .get( - new URLPattern({ pathname: "/invite" }), - () => - Promise.resolve( - Response.redirect(makeInviteURL(discordApplicationID)), - ), + "/invite", + () => Response.redirect(makeInviteURL(options.discordApplicationID)), ) .get( - new URLPattern({ pathname: "/source" }), - () => - Promise.resolve( - Response.redirect("https://github.com/acmcsufoss/lc-dailies"), - ), + "/source", + () => Response.redirect("https://github.com/acmcsufoss/lc-dailies"), ) .get( - new URLPattern({ pathname: "/seasons" }), - withCORS(makeSeasonsGetHandler(leaderboardClient)), + "/seasons", + async () => { + const seasons = await options.leaderboardClient.listSeasons(); + return withCORS(new Response(JSON.stringify(seasons))); + }, ) - .get( - new URLPattern({ pathname: "/seasons/:season_id.txt" }), - withCORS(makeSeasonTxtGetHandler(leaderboardClient)), + .get<"season_id">( + "/seasons/:season_id.txt", + async (ctx) => { + const seasonID = ctx.params["season_id"]; + if (!seasonID) { + return new Response("Missing season ID", { status: 400 }); + } + + const season = await getSeasonByIDOrLatest( + options.leaderboardClient, + seasonID, + ); + if (!season) { + return new Response("Season not found", { status: 404 }); + } + + const text = leaderboard.formatScores(season); + return withCORS( + new Response(text, { headers: { "Content-Type": "text/plain" } }), + ); + }, ) - .get( - new URLPattern({ pathname: "/seasons/:season_id" }), - withCORS(makeSeasonGetHandler(leaderboardClient)), + .get<"season_id">( + "/seasons/:season_id", + async (ctx) => { + const seasonID = ctx.params["season_id"]; + if (!seasonID) { + return new Response("Missing season ID", { status: 400 }); + } + + const season = await getSeasonByIDOrLatest( + options.leaderboardClient, + seasonID, + ); + return withCORS(new Response(JSON.stringify(season))); + }, ); } @@ -88,19 +94,11 @@ export function makeAPIRouter( export function makeOnListen( port: number, discordApplicationID: string, - discordToken: string, ) { /** * onLoad is callback which is called when the server starts listening. */ - return async function onLoad() { - // Overwrite the Discord Application Command. - await discord.registerCommand({ - app: discord_app.APP_LC, - applicationID: discordApplicationID, - botToken: discordToken, - }); - + return function onLoad() { console.log( "- Discord application information:", `https://discord.com/developers/applications/${discordApplicationID}/`, @@ -125,19 +123,38 @@ function makeInviteURL(applicationID: string) { } /** - * withCORS wraps a handler with common CORS headers. + * withCORS wraps a Response with common CORS headers. */ -function withCORS( - handle: router.RouterHandler["handle"], -): router.RouterHandler["handle"] { - return async function (request: router.RouterRequest) { - const response = await handle(request); - response.headers.set("Access-Control-Allow-Origin", "*"); - response.headers.set("Access-Control-Allow-Methods", "GET, POST"); - response.headers.set( - "Access-Control-Allow-Headers", - "Content-Type, Authorization", +function withCORS(response: Response): Response { + response.headers.set("Access-Control-Allow-Origin", "*"); + response.headers.set("Access-Control-Allow-Methods", "GET, POST"); + response.headers.set( + "Access-Control-Allow-Headers", + "Content-Type, Authorization", + ); + + return response; +} + +/** + * getSeasonByIDOrLatest gets a season by ID or the latest season. + */ +async function getSeasonByIDOrLatest( + leaderboardClient: leaderboard.LeaderboardClient, + seasonID: string | undefined, +): Promise { + const season = !seasonID || seasonID === "latest" + ? await leaderboardClient.getLatestSeason() + : await leaderboardClient.getSeason(seasonID); + if (season && !season.scores) { + season.scores = await leaderboard.calculateScores( + leaderboard.makeDefaultCalculateScoresOptions( + season.players, + season.questions, + season.submissions, + ), ); - return response; - }; + } + + return season; } diff --git a/lib/api/dailies.ts b/lib/api/dailies.ts index 71911a3..caf6848 100644 --- a/lib/api/dailies.ts +++ b/lib/api/dailies.ts @@ -1,87 +1,19 @@ -import { type APIEmbed, WEEK } from "lc-dailies/deps.ts"; -import * as api from "./mod.ts"; -import * as discord from "lc-dailies/lib/discord/mod.ts"; -import * as router from "lc-dailies/lib/router/mod.ts"; +import type { APIEmbed } from "discord-api-types"; +import { WEEK } from "@std/datetime"; import * as lc from "lc-dailies/lib/lc/mod.ts"; import * as leaderboard from "lc-dailies/lib/leaderboard/mod.ts"; +import * as api from "./api.ts"; import * as snacks from "./snacks.ts"; -/** - * makeDailyWebhookPostHandler creates a handler for daily webhook POST requests. - */ -export function makeDailyWebhookPostHandler( - lcClient: lc.LCClient, - leaderboardClient: leaderboard.LeaderboardClient, - webhookURL: string, - webhookToken?: string, -) { - /** - * handlePostDailyWebhook handles POST requests to the daily webhook. - */ - return async function handlePostDailyWebhook( - request: router.RouterRequest, - ): Promise { - // Override the webhook URL if applicable. - const overrideWebhookURL = request.url.searchParams.get("webhook_url"); - if (overrideWebhookURL) { - webhookURL = overrideWebhookURL; - } - - // Check the webhook token. - const token = request.params["token"]; - if (!overrideWebhookURL && webhookToken && token !== webhookToken) { - return new Response("Invalid token", { status: 401 }); - } - - // Get the season ID if applicable. - const seasonID = request.url.searchParams.get("season_id"); - - // Execute the webhook. - return await executeDailyWebhook( - lcClient, - leaderboardClient, - webhookURL, - seasonID, - ); - }; -} - -/** - * makeManualDailyWebhookPostHandler creates a handler for any variable - * webhook URL POST requests. - */ -export function makeManualDailyWebhookPostHandler( - lcClient: lc.LCClient, - leaderboardClient: leaderboard.LeaderboardClient, -) { - return async function handleManualPostDailyWebhook( - request: router.RouterRequest, - ): Promise { - const seasonID = request.url.searchParams.get("season_id"); - const webhookURL = request.url.searchParams.get("webhook_url"); - if (!webhookURL) { - return new Response("Missing webhook_url", { status: 400 }); - } - - return await executeDailyWebhook( - lcClient, - leaderboardClient, - webhookURL, - seasonID, - ); - }; -} - -async function executeDailyWebhook( +export async function executeDailyWebhook( lcClient: lc.LCClient, leaderboardClient: leaderboard.LeaderboardClient, webhookURL: string, - seasonID: string | null, + seasonID?: string | null, ): Promise { // Get the daily question. const question = await lcClient.getDailyQuestion(); const questionDate = new Date(`${question.date} GMT`); - const isSunday = questionDate.getDay() === 0; // Get the stored season. const storedSeason = seasonID @@ -113,13 +45,16 @@ async function executeDailyWebhook( const embeds = makeDailyWebhookEmbeds({ question, questionDate, - season: isSunday ? (syncedSeason ?? storedSeason) : null, + season: syncedSeason ?? storedSeason, }); // Execute the webhook. - await discord.executeWebhook({ - url: webhookURL, - data: { embeds }, + await fetch(webhookURL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ embeds }), }); // If the season is not synced, then sync it to set up the next season. @@ -158,7 +93,7 @@ export function makeDailyWebhookEmbeds( options: DailyWebhookOptions, ): APIEmbed[] { const embed: APIEmbed = { - title: options.question.title, + title: `${options.question.number}. ${options.question.title}`, url: options.question.url, description: `Daily Leetcode question for ${options.question.date}.`, color: getColorByDifficulty(options.question.difficulty), diff --git a/lib/api/discord/app.ts b/lib/api/discord/app.ts new file mode 100644 index 0000000..056cca8 --- /dev/null +++ b/lib/api/discord/app.ts @@ -0,0 +1,196 @@ +import { SECOND } from "@std/datetime"; +import type { + APIInteractionResponse, + AppSchema, +} from "@discord-applications/app"; +import { + ApplicationCommandOptionType, + createApp, + InteractionResponseType, + MessageFlags, +} from "@discord-applications/app"; +import * as api from "lc-dailies/lib/api/api.ts"; +import * as leaderboard from "lc-dailies/lib/leaderboard/mod.ts"; + +export const lcSchema = { + chatInput: { + name: "lc", + description: "Set of commands to register and submit Leetcode solutions.", + subcommands: { + register: { + description: "Register your Leetcode account", + options: { + lc_username: { + description: "Your Leetcode username", + type: ApplicationCommandOptionType.String, + required: true, + }, + }, + }, + unregister: { + description: "Unregister your Leetcode account", + options: {}, + }, + sync: { + description: "Sync the leaderboard with the latest submissions", + options: { + season_id: { + description: "The season ID to sync", + type: ApplicationCommandOptionType.String, + required: true, + }, + }, + }, + }, + }, +} as const satisfies AppSchema; + +/** + * makeDiscordAppHandler creates a handler for Discord application command interactions. + */ +export function makeDiscordAppHandler( + leaderboardClient: leaderboard.LeaderboardClient, + applicationID: string, + channelID: string, + publicKey: string, + token: string, +) { + return createApp( + { + schema: lcSchema, + applicationID, + publicKey, + token, + invite: { path: "/invite", scopes: ["applications.commands"] }, + register: true, + }, + { + async register(interaction) { + if (interaction.channel.id !== channelID) { + return { + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: + "This command is only available in the LC-Dailies channel.", + flags: MessageFlags.Ephemeral, + }, + }; + } + + const registerResponse = await leaderboardClient.register( + interaction.member!.user.id, + interaction.data.parsedOptions.lc_username, + ); + + return makeRegisterInteractionResponse(registerResponse); + }, + async unregister(interaction) { + if (interaction.channel.id !== channelID) { + return { + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: + "This command is only available in the LC-Dailies channel.", + flags: MessageFlags.Ephemeral, + }, + }; + } + + const unregisterResponse = await leaderboardClient.unregister( + interaction.member!.user.id, + ); + + return { + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: `Your Leetcode username was ${ + unregisterResponse.ok ? "unregistered" : "not unregistered" + }.`, + }, + }; + }, + async sync(interaction) { + const syncResponse = await leaderboardClient.sync( + interaction.data.parsedOptions.season_id, + ); + + return makeSyncInteractionResponse(syncResponse); + }, + }, + ); +} + +/** + * withErrorResponse wraps around the Discord app handler to catch any errors + * and return a response using the error message. + */ +export function withErrorResponse( + oldHandle: (request: Request) => Promise, +): (request: Request) => Promise { + return async function handle(request: Request): Promise { + return await oldHandle(request) + .catch((error) => { + if (!(error instanceof Error)) { + throw error; + } + + return Response.json( + { + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: `Error: ${error.message}`, + flags: MessageFlags.Ephemeral, + }, + } satisfies APIInteractionResponse, + ); + }); + }; +} + +/** + * makeSyncInteractionResponse makes the interaction response for the sync subcommand. + */ +export function makeSyncInteractionResponse( + r: api.SyncResponse, +): APIInteractionResponse { + return { + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: [ + `# Synced leaderboard [\`${r.season.id}\`](https://lc-dailies.deno.dev/seasons/${r.season.id}) for week of ${r.season.start_date} synced ${ + toDiscordTimestamp(new Date(r.season.synced_at!)) + }`, + "```", + leaderboard.formatScores(r.season), + "```", + ].join("\n"), + }, + }; +} + +/** + * toDiscordTimestamp converts a date to a Discord timestamp. + * + * Reference: + * - https://gist.github.com/LeviSnoot/d9147767abeef2f770e9ddcd91eb85aa + * - https://github.com/acmcsufoss/shorter/blob/dbaac9a020a621be0c349a8b9a870b936b988265/main.ts#L235 + */ +function toDiscordTimestamp(date: Date) { + return ``; +} + +/** + * makeRegisterInteractionResponse makes the interaction response for the register subcommand. + */ +export function makeRegisterInteractionResponse( + r: api.RegisterResponse, +): APIInteractionResponse { + return { + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: `Your Leetcode username was ${ + r.ok ? "registered" : "not registered" + }.`, + }, + }; +} diff --git a/lib/api/discord_app/mod.ts b/lib/api/discord/mod.ts similarity index 100% rename from lib/api/discord_app/mod.ts rename to lib/api/discord/mod.ts diff --git a/lib/api/discord_app/app.ts b/lib/api/discord_app/app.ts deleted file mode 100644 index dfc78c3..0000000 --- a/lib/api/discord_app/app.ts +++ /dev/null @@ -1,205 +0,0 @@ -import type { - APIInteraction, - APIInteractionResponse, - APIUser, - RESTPostAPIApplicationCommandsJSONBody, -} from "lc-dailies/deps.ts"; -import { - ApplicationCommandOptionType, - InteractionResponseType, - InteractionType, - MessageFlags, - Utils, -} from "lc-dailies/deps.ts"; -import * as router from "lc-dailies/lib/router/mod.ts"; -import * as discord from "lc-dailies/lib/discord/mod.ts"; -import * as leaderboard from "lc-dailies/lib/leaderboard/mod.ts"; -import { - makeRegisterInteractionResponse, - parseRegisterOptions, - REGISTER, - SUB_REGISTER, -} from "./sub/register.ts"; -import { - makeSyncInteractionResponse, - parseSyncOptions, - SUB_SYNC, - SYNC, -} from "./sub/sync.ts"; - -export const LC = "lc"; -export const LC_DESCRIPTION = - "Set of commands to register and submit Leetcode solutions."; - -/** - * APP_LC is the top-level command for the LC Application Commands. - */ -export const APP_LC: RESTPostAPIApplicationCommandsJSONBody = { - name: LC, - description: LC_DESCRIPTION, - options: [SUB_REGISTER, SUB_SYNC], -}; - -/** - * makeDiscordAppHandler creates a handler for Discord application command interactions. - */ -export function makeDiscordAppHandler( - leaderboardClient: leaderboard.LeaderboardClient, - discordPublicKey: string, - discordChannelID: string, -) { - return async function handleDiscordApp( - request: router.RouterRequest, - ): Promise { - // Verify the request is coming from Discord. - const { error, body } = await discord.verify( - request.request, - discordPublicKey, - ); - if (error !== null) { - return error; - } - - // Parse the incoming request as JSON. - const interaction = await JSON.parse(body) as APIInteraction; - switch (interaction.type) { - case InteractionType.Ping: { - return Response.json({ type: InteractionResponseType.Pong }); - } - - case InteractionType.ApplicationCommand: { - // Assert the interaction is a context menu interaction. - if ( - !Utils.isChatInputApplicationCommandInteraction(interaction) - ) { - return new Response("Invalid request", { status: 400 }); - } - - // Assert the interaction is within the specified channel. - if (interaction.channel?.id !== discordChannelID) { - return new Response("Invalid request", { status: 400 }); - } - - // Assert the interaction is from a member. - if (!interaction.member?.user) { - return new Response("Invalid request", { status: 400 }); - } - - // Assert the interaction has options. - if ( - !interaction.data.options || interaction.data.options.length === 0 - ) { - throw new Error("No options provided"); - } - - // Assert the interaction has a subcommand. - const { 0: { name, type } } = interaction.data.options; - if (type !== ApplicationCommandOptionType.Subcommand) { - throw new Error("Invalid option type"); - } - - // Assert the interaction has a subcommand. - if (!interaction.member) { - throw new Error("No user provided"); - } - - // Handle the subcommand. - switch (name) { - case REGISTER: { - const registerResponse = await handleRegisterSubcommand( - leaderboardClient, - interaction.member.user, - parseRegisterOptions(interaction.data.options), - ); - - return Response.json(registerResponse); - } - - case SYNC: { - const syncResponse = await handleSyncSubcommand( - leaderboardClient, - parseSyncOptions(interaction.data.options), - ); - - return Response.json(syncResponse); - } - - default: { - throw new Error("Invalid subcommand"); - } - } - } - - default: { - return new Response("Invalid request", { status: 400 }); - } - } - }; -} - -/** - * handleRegisterSubcommand handles the register subcommand. - */ -async function handleRegisterSubcommand( - leaderboardClient: leaderboard.LeaderboardClient, - user: APIUser, - options: ReturnType, -): Promise { - const registerResponse = await leaderboardClient.register( - user.id, - options.lc_username, - ); - - return makeRegisterInteractionResponse(registerResponse); -} - -/** - * handleSyncSubcommand handles the sync subcommand. - */ -async function handleSyncSubcommand( - leaderboardClient: leaderboard.LeaderboardClient, - options: ReturnType, -): Promise { - try { - const syncResponse = await leaderboardClient.sync( - options.season_id, - ); - - const interactionResponse = makeSyncInteractionResponse(syncResponse); - console.log(syncResponse); - console.log(interactionResponse); - return interactionResponse; - } catch (error) { - console.error(error); - throw error; - } -} - -/** - * withErrorResponse wraps around the Discord app handler to catch any errors - * and return a response using the error message. - */ -export function withErrorResponse( - oldHandle: router.RouterHandler["handle"], -): router.RouterHandler["handle"] { - return async function handle( - request: router.RouterRequest, - ): Promise { - return await oldHandle(request) - .catch((error) => { - if (!(error instanceof Error)) { - throw error; - } - - return Response.json( - { - type: InteractionResponseType.ChannelMessageWithSource, - data: { - content: `Error: ${error.message}`, - flags: MessageFlags.Ephemeral, - }, - } satisfies APIInteractionResponse, - ); - }); - }; -} diff --git a/lib/api/discord_app/sub/register.ts b/lib/api/discord_app/sub/register.ts deleted file mode 100644 index 7522be4..0000000 --- a/lib/api/discord_app/sub/register.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { - APIApplicationCommandInteractionDataOption, - APIApplicationCommandOption, - APIInteractionResponse, -} from "lc-dailies/deps.ts"; -import { - ApplicationCommandOptionType, - InteractionResponseType, -} from "lc-dailies/deps.ts"; -import type * as api from "../../mod.ts"; - -export const REGISTER = "register"; -export const REGISTER_DESCRIPTION = "Register your Leetcode account"; -export const REGISTER_LC_USERNAME = "lc_username"; -export const REGISTER_LC_USERNAME_DESCRIPTION = "Your Leetcode username"; - -/** - * SUB_REGISTER is the subcommand for the LC-Dailies command. - */ -export const SUB_REGISTER: APIApplicationCommandOption = { - name: REGISTER, - description: REGISTER_DESCRIPTION, - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: REGISTER_LC_USERNAME, - description: REGISTER_LC_USERNAME_DESCRIPTION, - type: ApplicationCommandOptionType.String, - required: true, - }, - ], -}; - -/** - * parseRegisterOptions parses the options for the register subcommand. - */ -export function parseRegisterOptions( - options: APIApplicationCommandInteractionDataOption[], -) { - const registerOption = options.find((option) => option.name === REGISTER); - if (!registerOption) { - throw new Error("No options provided"); - } - if ( - registerOption.type !== ApplicationCommandOptionType.Subcommand - ) { - throw new Error("Invalid option type"); - } - if (!registerOption.options) { - throw new Error("No options provided"); - } - - const usernameOption = registerOption.options.find((option) => - option.name === REGISTER_LC_USERNAME - ); - if (usernameOption?.type !== ApplicationCommandOptionType.String) { - throw new Error("Expected a string for the username option."); - } - - return { [REGISTER_LC_USERNAME]: usernameOption.value }; -} - -/** - * makeRegisterInteractionResponse makes the interaction response for the register subcommand. - */ -export function makeRegisterInteractionResponse( - r: api.RegisterResponse, -): APIInteractionResponse { - return { - type: InteractionResponseType.ChannelMessageWithSource, - data: { - content: `Your Leetcode username was ${ - r.ok ? "registered" : "not registered" - }.`, - }, - }; -} diff --git a/lib/api/discord_app/sub/sync.ts b/lib/api/discord_app/sub/sync.ts deleted file mode 100644 index 49216f7..0000000 --- a/lib/api/discord_app/sub/sync.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { - APIApplicationCommandInteractionDataOption, - APIApplicationCommandOption, - APIInteractionResponse, -} from "lc-dailies/deps.ts"; -import { - ApplicationCommandOptionType, - InteractionResponseType, - SECOND, -} from "lc-dailies/deps.ts"; -import * as api from "../../mod.ts"; -import { formatScores } from "lc-dailies/lib/leaderboard/mod.ts"; - -export const SYNC = "sync"; -export const SYNC_DESCRIPTION = "Sync and display your season scores"; -export const SEASON_ID = "season_id"; -export const SEASON_ID_DESCRIPTION = "The season ID to sync"; - -/** - * SUB_SYNC is the subcommand for the LC-Dailies command for syncing a season. - */ -export const SUB_SYNC: APIApplicationCommandOption = { - name: SYNC, - description: SYNC_DESCRIPTION, - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: SEASON_ID, - description: SEASON_ID_DESCRIPTION, - type: ApplicationCommandOptionType.String, - }, - ], -}; - -/** - * parseSyncOptions parses the options for the sync subcommand. - */ -export function parseSyncOptions( - options: APIApplicationCommandInteractionDataOption[], -) { - const syncOption = options.find((option) => option.name === SYNC); - if (!syncOption) { - throw new Error("No options provided"); - } - if ( - syncOption.type !== ApplicationCommandOptionType.Subcommand - ) { - throw new Error("Invalid option type"); - } - if (!syncOption.options) { - throw new Error("No options provided"); - } - - const seasonIDOption = syncOption.options.find((option) => - option.name === SEASON_ID - ); - if ( - seasonIDOption && - seasonIDOption.type !== ApplicationCommandOptionType.String - ) { - throw new Error("Expected a string for the season ID option."); - } - - return { [SEASON_ID]: seasonIDOption?.value }; -} - -/** - * makeSyncInteractionResponse makes the interaction response for the sync subcommand. - */ -export function makeSyncInteractionResponse( - r: api.SyncResponse, -): APIInteractionResponse { - return { - type: InteractionResponseType.ChannelMessageWithSource, - data: { - content: [ - `# Synced leaderboard [\`${r.season.id}\`](https://lc-dailies.deno.dev/seasons/${r.season.id}) for week of ${r.season.start_date} synced ${ - toDiscordTimestamp(new Date(r.season.synced_at!)) - }`, - "```", - formatScores(r.season), - "```", - ].join("\n"), - }, - }; -} - -/** - * toDiscordTimestamp converts a date to a Discord timestamp. - * - * Reference: - * - https://gist.github.com/LeviSnoot/d9147767abeef2f770e9ddcd91eb85aa - * - https://github.com/acmcsufoss/shorter/blob/dbaac9a020a621be0c349a8b9a870b936b988265/main.ts#L235 - */ -function toDiscordTimestamp(date: Date) { - return ``; -} diff --git a/lib/api/mod.ts b/lib/api/mod.ts deleted file mode 100644 index 9811231..0000000 --- a/lib/api/mod.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./api.ts"; -export * from "./types.ts"; diff --git a/lib/api/seasons.ts b/lib/api/seasons.ts deleted file mode 100644 index bc1947b..0000000 --- a/lib/api/seasons.ts +++ /dev/null @@ -1,90 +0,0 @@ -import * as api from "lc-dailies/lib/api/mod.ts"; -import * as leaderboard from "lc-dailies/lib/leaderboard/mod.ts"; -import * as router from "lc-dailies/lib/router/mod.ts"; - -/** - * makeSeasonsGetHandler makes a handler that returns a list of seasons. - */ -export function makeSeasonsGetHandler( - leaderboardClient: leaderboard.LeaderboardClient, -) { - /** - * handleGetSeasons handles GET requests to the seasons endpoint. - */ - return async function handleGetSeasons(): Promise { - const seasons = await leaderboardClient.listSeasons(); - return new Response(JSON.stringify(seasons)); - }; -} - -/** - * makeSeasonGetHandler makes a handler that returns a season. - */ -export function makeSeasonGetHandler( - leaderboardClient: leaderboard.LeaderboardClient, -) { - /** - * handleGetSeason handles GET requests to the season endpoint. - */ - return async function handleGetSeason( - request: router.RouterRequest, - ): Promise { - const seasonID = request.params["season_id"]; - if (!seasonID) { - return new Response("Missing season ID", { status: 400 }); - } - - const season = await getSeasonByIDOrLatest(leaderboardClient, seasonID); - return new Response(JSON.stringify(season)); - }; -} - -/** - * makeSeasonTxtGetHandler makes a handler that returns a plaintext - * representation of a season. - */ -export function makeSeasonTxtGetHandler( - leaderboardClient: leaderboard.LeaderboardClient, -) { - /** - * handleGetSeasonTxt handles GET requests to the season.txt endpoint. - */ - return async function handleGetSeasonTxt( - request: router.RouterRequest, - ): Promise { - const seasonID = request.params["season_id"]; - if (!seasonID) { - return new Response("Missing season ID", { status: 400 }); - } - - const season = await getSeasonByIDOrLatest(leaderboardClient, seasonID); - if (!season) { - return new Response("Season not found", { status: 404 }); - } - - const text = leaderboard.formatScores(season); - return new Response(text, { - headers: { "Content-Type": "text/plain" }, - }); - }; -} - -async function getSeasonByIDOrLatest( - leaderboardClient: leaderboard.LeaderboardClient, - seasonID: string | undefined, -): Promise { - const season = !seasonID || seasonID === "latest" - ? await leaderboardClient.getLatestSeason() - : await leaderboardClient.getSeason(seasonID); - if (season && !season.scores) { - season.scores = await leaderboard.calculateScores( - leaderboard.makeDefaultCalculateScoresOptions( - season.players, - season.questions, - season.submissions, - ), - ); - } - - return season; -} diff --git a/lib/api/types.ts b/lib/api/types.ts index bfeccc8..6e20c70 100644 --- a/lib/api/types.ts +++ b/lib/api/types.ts @@ -73,6 +73,11 @@ export interface Question { * url is the link of the daily question. */ url: string; + + /** + * number is the incremental ID of the Leetcode question. + */ + number: number; } /** @@ -141,6 +146,16 @@ export interface RegisterResponse { ok: boolean; } +/** + * UnregisterResponse is the response for the unregister subcommand. + */ +export interface UnregisterResponse { + /** + * ok is whether the unregistration was successful. + */ + ok: boolean; +} + /** * SyncResponse is the response for the sync subcommand. */ diff --git a/lib/denoflare/denoflare.ts b/lib/denoflare/denoflare.ts deleted file mode 100644 index 0d9117b..0000000 --- a/lib/denoflare/denoflare.ts +++ /dev/null @@ -1,68 +0,0 @@ -const DENOFLARE_CONFIG_FILENAME = ".denoflare"; - -/** - * DENOFLARE_VERSION_TAG is the version tag of denoflare. - */ -export const DENOFLARE_VERSION_TAG = "v0.5.12"; - -/** - * DenoflareOptions is the options for denoflare. - */ -export interface DenoflareOptions { - versionTag: string; - scriptName: string; - path: string; - cfAccountID: string; - cfAPIToken: string; - localPort: number; - args: string[]; -} - -/** - * denoflare is a helper for interfacing with the denoflare CLI. - * - * See: https://denoflare.dev/cli/ - */ -export async function denoflare(options: DenoflareOptions) { - const moduleURL = `https://deno.land/x/denoflare@${options.versionTag}`; - const config = { - $schema: `${moduleURL}/common/config.schema.json`, - scripts: { - [options.scriptName]: { - path: options.path, - localPort: options.localPort, - }, - }, - profiles: { - profile: { - accountId: options.cfAccountID, - apiToken: options.cfAPIToken, - }, - }, - }; - await Deno.writeTextFile( - DENOFLARE_CONFIG_FILENAME, - JSON.stringify(config), - ); - - try { - // Create a child process running denoflare CLI. - const child = new Deno.Command(Deno.execPath(), { - args: [ - "run", - "-A", - "--unstable", - `${moduleURL}/cli/cli.ts`, - ...options.args, - ], - stdin: "piped", - stdout: "piped", - }).spawn(); - - // Pipe the child process stdout to stdout. - await child.stdout.pipeTo(Deno.stdout.writable); - } finally { - // Delete the temporary config file. - await Deno.remove(DENOFLARE_CONFIG_FILENAME); - } -} diff --git a/lib/denoflare/mod.ts b/lib/denoflare/mod.ts deleted file mode 100644 index 660ca8b..0000000 --- a/lib/denoflare/mod.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./denoflare.ts"; diff --git a/lib/discord/mod.ts b/lib/discord/mod.ts deleted file mode 100644 index 2b317c5..0000000 --- a/lib/discord/mod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./register.ts"; -export * from "./verify.ts"; -export * from "./webhook.ts"; diff --git a/lib/discord/register.ts b/lib/discord/register.ts deleted file mode 100644 index 78d9b94..0000000 --- a/lib/discord/register.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { RESTPostAPIApplicationCommandsJSONBody } from "lc-dailies/deps.ts"; - -/** - * RegisterCommandOptions is the initialization to register a Discord application command. - */ -export interface RegisterCommandOptions { - applicationID: string; - botToken: string; - app: RESTPostAPIApplicationCommandsJSONBody; -} - -/** - * makeRegisterCommandsURL makes the URL to register a Discord application command. - */ -export function makeRegisterCommandsURL( - applicationID: string, - base = DISCORD_API_URL, -) { - return new URL(`${base}/applications/${applicationID}/commands`); -} - -/** - * makeBotAuthorization makes the Authorization header for a bot. - */ -export function makeBotAuthorization(botToken: string) { - return botToken.startsWith("Bot ") ? botToken : `Bot ${botToken}`; -} - -/** - * registerCommand registers a Discord application command. - */ -export async function registerCommand( - options: RegisterCommandOptions, -): Promise { - const url = makeRegisterCommandsURL(options.applicationID); - 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}`, - ); - } -} - -/** - * DISCORD_API_URL is the base URL for the Discord API. - */ -export const DISCORD_API_URL = "https://discord.com/api/v10"; diff --git a/lib/discord/verify.ts b/lib/discord/verify.ts deleted file mode 100644 index e56ccca..0000000 --- a/lib/discord/verify.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { nacl } from "lc-dailies/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/lib/discord/webhook.ts b/lib/discord/webhook.ts deleted file mode 100644 index 6da6b88..0000000 --- a/lib/discord/webhook.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { RESTPostAPIWebhookWithTokenJSONBody } from "lc-dailies/deps.ts"; - -/** - * ExecuteWebhookOptions are the options for a webhook message. - */ -export interface ExecuteWebhookOptions { - /** - * url is the webhook url. - */ - url: string; - - /** - * data is the webhook data. - */ - data: RESTPostAPIWebhookWithTokenJSONBody; -} - -/** - * @see https://discord.com/developers/docs/resources/webhook#execute-webhook - */ -export function executeWebhook(o: ExecuteWebhookOptions) { - return fetch(o.url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(o.data), - }); -} diff --git a/lib/lc/client.ts b/lib/lc/client.ts index b84b351..80f5fff 100644 --- a/lib/lc/client.ts +++ b/lib/lc/client.ts @@ -11,14 +11,19 @@ import { gql } from "./gql.ts"; */ export class LCClient implements LCClientInterface { constructor( - private readonly fetch: typeof window.fetch = window.fetch.bind(window), + private readonly fetch: typeof globalThis.fetch = globalThis.fetch.bind( + globalThis, + ), ) {} /** * verifyUser verifies the user by username. */ public async verifyUser(username: string): Promise { - const response = await this.fetch(`https://leetcode.com/${username}/`); + const response = await this.fetch( + `https://leetcode.com/${username}/`, + { headers: { "priority": "u=0, i" } }, + ); return response.status === 200; } @@ -56,7 +61,7 @@ export class LCClient implements LCClientInterface { JSON.stringify({ operationName: "dailyCodingQuestionRecords", query: - "\n query dailyCodingQuestionRecords($year: Int!, $month: Int!) {\n dailyCodingChallengeV2(year: $year, month: $month) {\n challenges {\n date\n userStatus\n link\n question {\n questionFrontendId\n title\n titleSlug\n difficulty\n }\n }\n weeklyChallenges {\n date\n userStatus\n link\n question {\n questionFrontendId\n title\n titleSlug\n }\n }\n }\n}\n ", + "\n query dailyCodingQuestionRecords($year: Int!, $month: Int!) {\n dailyCodingChallengeV2(year: $year, month: $month) {\n challenges {\n date\n userStatus\n link\n question {\n questionFrontendId\n title\n titleSlug\n difficulty\n }\n }\n weeklyChallenges {\n date\n userStatus\n link\n question {\n questionFrontendId\n title\n titleSlug\n difficulty\n }\n }\n }\n}\n ", variables: { year: currentYear, month: currentMonth }, }), ); @@ -68,6 +73,7 @@ export class LCClient implements LCClientInterface { title: string; titleSlug: string; difficulty: string; + questionFrontendId: string; }; }>; for (const challenge of challenges) { @@ -81,6 +87,7 @@ export class LCClient implements LCClientInterface { title: challenge.question.title, difficulty: challenge.question.difficulty, url: makeQuestionURL(challenge.question.titleSlug), + number: parseInt(challenge.question.questionFrontendId), }); } diff --git a/lib/lc/client_interface.ts b/lib/lc/client_interface.ts index 1ea0e97..af0bf01 100644 --- a/lib/lc/client_interface.ts +++ b/lib/lc/client_interface.ts @@ -1,4 +1,4 @@ -import type { Question } from "lc-dailies/lib/api/mod.ts"; +import type { Question } from "lc-dailies/lib/api/api.ts"; /** * LCQuestion is an alias interface for a Leetcode question. diff --git a/lib/lc/client_test.ts b/lib/lc/client_test.ts index ed2324a..ee19d8a 100644 --- a/lib/lc/client_test.ts +++ b/lib/lc/client_test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "lc-dailies/deps.ts"; +import { assertEquals } from "@std/assert"; import { makeQuestionURL, parseSubmissionID } from "./urls.ts"; Deno.test("makeQuestionURL", () => { diff --git a/lib/lc/fake_client.ts b/lib/lc/fake_client.ts index 7775e4d..0bb5fb9 100644 --- a/lib/lc/fake_client.ts +++ b/lib/lc/fake_client.ts @@ -5,12 +5,14 @@ import type { } from "./client_interface.ts"; export const FAKE_LC_USERNAME = "fake_lc_username"; +export const FAKE_LC_QUESTION_NUMBER = 0; export const FAKE_LC_QUESTION_NAME = "fake_lc_question_name"; export const FAKE_LC_QUESTION_TITLE = "fake_lc_question_title"; export const FAKE_LC_QUESTION_URL = "fake_lc_question_url"; export const FAKE_LC_QUESTION_DIFFICULTY = "fake_lc_question_difficulty"; export const FAKE_LC_QUESTION_DATE = "2023-07-31"; export const FAKE_LC_QUESTION: LCQuestion = { + number: FAKE_LC_QUESTION_NUMBER, name: FAKE_LC_QUESTION_NAME, title: FAKE_LC_QUESTION_TITLE, url: FAKE_LC_QUESTION_URL, diff --git a/lib/leaderboard/denokv/denokv_leaderboard_client.ts b/lib/leaderboard/denokv/denokv_leaderboard_client.ts index 86610fa..99854cb 100644 --- a/lib/leaderboard/denokv/denokv_leaderboard_client.ts +++ b/lib/leaderboard/denokv/denokv_leaderboard_client.ts @@ -1,5 +1,6 @@ -import { DAY, ulid, WEEK } from "lc-dailies/deps.ts"; -import type * as api from "lc-dailies/lib/api/mod.ts"; +import { DAY, WEEK } from "@std/datetime"; +import { ulid } from "@std/ulid"; +import type * as api from "lc-dailies/lib/api/api.ts"; import type { LeaderboardClient } from "lc-dailies/lib/leaderboard/mod.ts"; import { sync } from "lc-dailies/lib/leaderboard/mod.ts"; import type { LCClientInterface } from "lc-dailies/lib/lc/mod.ts"; @@ -126,6 +127,23 @@ export class DenoKvLeaderboardClient implements LeaderboardClient { return { ok: true }; } + public async unregister(playerID: string): Promise { + const playerResult = await this.kv.get([ + LeaderboardKvPrefix.PLAYERS, + playerID, + ]); + if (!playerResult.value) { + throw new Error("Player not registered"); + } + + // Unregister the player. + await this.kv.delete([ + LeaderboardKvPrefix.PLAYERS, + playerID, + ]); + return { ok: true }; + } + public async sync( seasonID?: string, referenceDate = new Date(), diff --git a/lib/leaderboard/denokv/denokv_leaderboard_client_test.ts b/lib/leaderboard/denokv/denokv_leaderboard_client_test.ts index dabedcc..3806091 100644 --- a/lib/leaderboard/denokv/denokv_leaderboard_client_test.ts +++ b/lib/leaderboard/denokv/denokv_leaderboard_client_test.ts @@ -1,6 +1,7 @@ -import { assertEquals, assertRejects, DAY, WEEK } from "lc-dailies/deps.ts"; +import { assertEquals, assertRejects } from "@std/assert"; +import { DAY, WEEK } from "@std/datetime"; import * as fake_lc from "lc-dailies/lib/lc/fake_client.ts"; -import type { Season } from "lc-dailies/lib/api/mod.ts"; +import type { Season } from "lc-dailies/lib/api/api.ts"; import { DenoKvLeaderboardClient } from "./denokv_leaderboard_client.ts"; const FAKE_DISCORD_USER_ID = "fake_discord_user_id"; diff --git a/lib/leaderboard/leaderboard_client.ts b/lib/leaderboard/leaderboard_client.ts index 9473ad9..68c7d4d 100644 --- a/lib/leaderboard/leaderboard_client.ts +++ b/lib/leaderboard/leaderboard_client.ts @@ -1,4 +1,4 @@ -import type * as api from "lc-dailies/lib/api/mod.ts"; +import type * as api from "lc-dailies/lib/api/api.ts"; /** * LeaderboardClient is the client interface for the leaderboard. @@ -12,6 +12,11 @@ export interface LeaderboardClient { lc_username: string, ): Promise; + /** + * unregister unregisters a player from the leaderboard. + */ + unregister(discord_user_id: string): Promise; + /** * sync syncs the leaderboard with Leetcode. * diff --git a/lib/leaderboard/scores.ts b/lib/leaderboard/scores.ts index 3a86483..4214505 100644 --- a/lib/leaderboard/scores.ts +++ b/lib/leaderboard/scores.ts @@ -143,15 +143,35 @@ export function defaultModifyScore(score: number): number { * formatScores formats the scores of all players in a season. */ export function formatScores(season: api.Season): string { - return Object.entries(season.scores) - .sort(({ 1: scoreA }, { 1: scoreB }) => scoreB - scoreA) - .map(([playerID, score], i) => { - const player = season.players[playerID]; - const formattedScore = String(score).padStart(3, " "); - const formattedSubmissions = formatSubmissions(season, playerID); - const formattedRank = formatRank(i + 1); - return `${formattedScore} ${formattedSubmissions} ${player.lc_username} (${formattedRank})`; - }) + return Object.entries( + Object.groupBy( + Object.entries(season.scores), + ([, score]) => score, + ), + ) + .toSorted((a, b) => b[1]![0][1] - a[1]![0][1]) + .reduce( + ({ result, rank }, [score, entries]) => { + if (entries === undefined) { + return { result, rank }; + } + + const formattedScore = score.padStart(3, " "); + return { + result: result.concat( + entries.map(([playerID]) => { + const player = season.players[playerID]; + const formattedSubmissions = formatSubmissions(season, playerID); + const formattedRank = formatRank(rank); + return `${formattedScore} ${formattedSubmissions} ${player.lc_username} (${formattedRank})`; + }), + ), + rank: rank + entries.length, + }; + }, + { result: [], rank: 1 } as { result: string[]; rank: number }, + ) + .result .join("\n"); } @@ -240,19 +260,19 @@ export function formatRank(rank: number): string { export function formatDifficulty(difficulty?: string): string { switch (difficulty) { case "Easy": { - return "🟢"; + return "🟩"; } case "Medium": { - return "🟠"; + return "🟧"; } case "Hard": { - return "🔴"; + return "🟥"; } default: { - return "·"; + return "🔲"; } } } diff --git a/lib/leaderboard/scores_test.ts b/lib/leaderboard/scores_test.ts index a40e0b4..f6bd756 100644 --- a/lib/leaderboard/scores_test.ts +++ b/lib/leaderboard/scores_test.ts @@ -1,11 +1,12 @@ -import { assertEquals } from "lc-dailies/deps.ts"; +import { assertEquals } from "@std/assert"; import { calculatePlayerScore, calculateScores, makeDefaultCalculateScoresOptions, } from "./scores.ts"; +import type * as api from "lc-dailies/lib/api/types.ts"; -const FAKE_SEASON = { +const FAKE_SEASON: api.Season = { "id": "01H8T4MM00BQHHK7VTTEJE1WAS", "start_date": "Sun, 27 Aug 2023 00:00:00 GMT", "players": { @@ -20,6 +21,7 @@ const FAKE_SEASON = { }, "questions": { "implement-stack-using-queues": { + "number": 225, "name": "implement-stack-using-queues", "date": "2023-08-28", "title": "Implement Stack using Queues", @@ -27,6 +29,7 @@ const FAKE_SEASON = { "url": "https://leetcode.com/problems/implement-stack-using-queues/", }, "counting-bits": { + "number": 338, "name": "counting-bits", "date": "2023-09-01", "title": "Counting Bits", @@ -56,6 +59,7 @@ const FAKE_SEASON = { }, }, }, + scores: {}, }; Deno.test("calculatePlayerScore calculates the score of a player", () => { diff --git a/lib/leaderboard/sync.ts b/lib/leaderboard/sync.ts index 9f01ba3..ec34c94 100644 --- a/lib/leaderboard/sync.ts +++ b/lib/leaderboard/sync.ts @@ -1,5 +1,5 @@ -import { SECOND, WEEK } from "lc-dailies/deps.ts"; -import type * as api from "lc-dailies/lib/api/mod.ts"; +import { SECOND, WEEK } from "@std/datetime"; +import type * as api from "lc-dailies/lib/api/api.ts"; import type { LCClientInterface } from "lc-dailies/lib/lc/mod.ts"; import { calculateScores, diff --git a/lib/leaderboard/sync_test.ts b/lib/leaderboard/sync_test.ts index a8d1bd2..8434b27 100644 --- a/lib/leaderboard/sync_test.ts +++ b/lib/leaderboard/sync_test.ts @@ -1,9 +1,9 @@ -import { assertEquals } from "lc-dailies/deps.ts"; -import type { LCSubmission } from "lc-dailies/lib/lc/mod.ts"; -// import type { SyncOptions } from "./sync.ts"; +import { assertEquals } from "@std/assert"; +import type { LCQuestion, LCSubmission } from "lc-dailies/lib/lc/mod.ts"; import { sync } from "./sync.ts"; +import type * as api from "lc-dailies/lib/api/types.ts"; -const FAKE_UNSYNCED_SEASON = { +const FAKE_UNSYNCED_SEASON: api.Season = { "id": "01H8T4MM00BQHHK7VTTEJE1WAS", "start_date": "Sun, 27 Aug 2023 00:00:00 GMT", "players": { @@ -18,6 +18,7 @@ const FAKE_UNSYNCED_SEASON = { }, "questions": { "implement-stack-using-queues": { + "number": 225, "name": "implement-stack-using-queues", "date": "2023-08-28", "title": "Implement Stack using Queues", @@ -25,6 +26,7 @@ const FAKE_UNSYNCED_SEASON = { "url": "https://leetcode.com/problems/implement-stack-using-queues/", }, "counting-bits": { + "number": 338, "name": "counting-bits", "date": "2023-09-01", "title": "Counting Bits", @@ -57,7 +59,8 @@ const FAKE_UNSYNCED_SEASON = { "scores": {}, }; -const FAKE_QUESTION = { +const FAKE_QUESTION: LCQuestion = { + number: 7, name: "reverse-integer", date: "2023-09-02", title: "Reverse Integer", @@ -65,14 +68,14 @@ const FAKE_QUESTION = { url: "https://leetcode.com/problems/reverse-integer/", }; -const FAKE_SUBMISSION = { +const FAKE_SUBMISSION: LCSubmission = { id: "8008569420", name: "reverse-integer", title: "Reverse Integer", timestamp: "1693627483", }; -const FAKE_SYNCED_SEASON = { +const FAKE_SYNCED_SEASON: api.Season = { "id": "01H8T4MM00BQHHK7VTTEJE1WAS", "start_date": "Sun, 27 Aug 2023 00:00:00 GMT", "players": { @@ -87,6 +90,7 @@ const FAKE_SYNCED_SEASON = { }, "questions": { "implement-stack-using-queues": { + "number": 225, "name": "implement-stack-using-queues", "date": "2023-08-28", "title": "Implement Stack using Queues", @@ -94,6 +98,7 @@ const FAKE_SYNCED_SEASON = { "url": "https://leetcode.com/problems/implement-stack-using-queues/", }, "counting-bits": { + "number": 338, "name": "counting-bits", "date": "2023-09-01", "title": "Counting Bits", diff --git a/lib/router/mod.ts b/lib/router/mod.ts deleted file mode 100644 index d18015d..0000000 --- a/lib/router/mod.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./router.ts"; diff --git a/lib/router/router.ts b/lib/router/router.ts deleted file mode 100644 index 98f44ef..0000000 --- a/lib/router/router.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * RouterHandler is a function which can be registered to a specific route in the - * router. The router will call the handler with the request object and any - * matched parameters. - */ -export interface RouterHandler { - /** - * method is the HTTP method to match on. - */ - method: "get" | "post" | "put" | "delete" | "options" | "head"; - - /** - * handle is the function which will be called when a request matches the - * route. - */ - handle: (r: RouterRequest) => Promise; -} - -/** - * RouterRequest is a structure which contains the request object and any - * matched parameters. - */ -export interface RouterRequest { - /** - * request is the original request object. - */ - request: Request; - - /** - * url is the parsed fully qualified URL of the request. - */ - url: URL; - - /** - * params is a map of matched parameters from the URL pattern. - */ - params: { [key: string]: string }; -} - -/** - * RouterHandlerMap is a map of URL patterns to handlers. The router will use - * this map to find a handler for a given request. - */ -export type RouterHandlerMap = Map; - -/** - * Router is a simple HTTP server which can be configured with handlers for - * specific routes. - */ -export class Router { - constructor( - public handlerMap: RouterHandlerMap = new Map(), - public readonly response404: Response = new Response("Not found", { - status: 404, - }), - ) {} - - /** - * get registers a handler for the "get" HTTP method. - */ - public get(pattern: URLPattern, fn: RouterHandler["handle"]): this { - this.handlerMap.set(pattern, { method: "get", handle: fn }); - return this; - } - - /** - * post registers a handler for the "post" HTTP method. - */ - public post(pattern: URLPattern, fn: RouterHandler["handle"]): this { - this.handlerMap.set(pattern, { method: "post", handle: fn }); - return this; - } - - /** - * put registers a handler for the "put" HTTP method. - */ - public put(pattern: URLPattern, fn: RouterHandler["handle"]): this { - this.handlerMap.set(pattern, { method: "put", handle: fn }); - return this; - } - - /** - * delete registers a handler for the "delete" HTTP method. - */ - public delete(pattern: URLPattern, fn: RouterHandler["handle"]): this { - this.handlerMap.set(pattern, { method: "delete", handle: fn }); - return this; - } - - /** - * options registers a handler for the "options" HTTP method. - */ - public options(pattern: URLPattern, fn: RouterHandler["handle"]): this { - this.handlerMap.set(pattern, { method: "options", handle: fn }); - return this; - } - - /** - * head registers a handler for the "head" HTTP method. - */ - public head(pattern: URLPattern, fn: RouterHandler["handle"]): this { - this.handlerMap.set(pattern, { method: "head", handle: fn }); - return this; - } - - /** - * execute is a method that runs the router with the given request. If a - * handler is found, it will be called and the response will be returned. - */ - public async execute(request: Request): Promise { - for (const [pattern, handler] of this.handlerMap) { - const match = pattern.exec(request.url); - if ( - !match || - handler.method !== request.method.toLowerCase() - ) { - continue; - } - - const url = new URL(request.url); - const params = Object.entries(match.pathname.groups) - .reduce((acc, [key, value]) => { - if (value) acc[key] = value; - return acc; - }, {} as { [key: string]: string }); - const response = await handler.handle({ - request, - url, - params, - }); - - return response; - } - - return this.response404; - } - - /** - * serve starts the server on the given port. If onListen is provided, it will - * be called with the hostname and port that the server is listening on. - */ - public static serve( - serveOptions: Deno.ServeOptions, - router: Router, - ): Deno.Server { - return Deno.serve( - serveOptions, - router.execute.bind(router), - ); - } -} diff --git a/main.ts b/main.ts index b457e72..92ccff9 100644 --- a/main.ts +++ b/main.ts @@ -1,52 +1,60 @@ import { DenoKvLeaderboardClient } from "lc-dailies/lib/leaderboard/denokv/mod.ts"; -import { Router } from "lc-dailies/lib/router/mod.ts"; import * as lc from "lc-dailies/lib/lc/mod.ts"; -import * as api from "lc-dailies/lib/api/mod.ts"; -import { - DISCORD_APPLICATION_ID, - DISCORD_CHANNEL_ID, - DISCORD_PUBLIC_KEY, - DISCORD_TOKEN, - DISCORD_WEBHOOK_URL, - KV_URL, - PORT, - WEBHOOK_TOKEN, -} from "lc-dailies/env.ts"; +import * as api from "lc-dailies/lib/api/api.ts"; +import { executeDailyWebhook } from "lc-dailies/lib/api/dailies.ts"; if (import.meta.main) { await main(); } async function main() { - const kv = await Deno.openKv(KV_URL); + const kv = await Deno.openKv(Deno.env.get("KV_URL")!); const lcClient = new lc.LCClient(); const leaderboardClient = new DenoKvLeaderboardClient( kv, lcClient, ); - const r = api.makeAPIRouter( - DISCORD_APPLICATION_ID, - DISCORD_PUBLIC_KEY, - DISCORD_CHANNEL_ID, - DISCORD_WEBHOOK_URL, - WEBHOOK_TOKEN, - lcClient, + const discordApplicationID = Deno.env.get("DISCORD_APPLICATION_ID")!; + const discordChannelID = Deno.env.get("DISCORD_CHANNEL_ID")!; + const discordPublicKey = Deno.env.get("DISCORD_PUBLIC_KEY")!; + const discordToken = Deno.env.get("DISCORD_TOKEN")!; + const router = await api.makeAPIRouter({ leaderboardClient, + discordApplicationID, + discordChannelID, + discordPublicKey, + discordToken, + }); + + Deno.cron( + "sync leaderboard", + // Sync at 5 minutes before every hour. + "55 * * * *", + async () => { + await leaderboardClient.sync(); + }, ); - await Router.serve( + Deno.cron( + "execute daily webhook", + // Execute every day at 12:00:000 AM UTC. + "0 0 * * *", + async () => { + const webhookURL = Deno.env.get("DISCORD_WEBHOOK_URL")!; + await executeDailyWebhook( + lcClient, + leaderboardClient, + webhookURL, + ); + }, + ); + + const port = Number(Deno.env.get("PORT")); + Deno.serve( { - port: PORT, - onListen: api.makeOnListen( - PORT, - DISCORD_APPLICATION_ID, - DISCORD_TOKEN, - ), + port, + onListen: api.makeOnListen(port, discordApplicationID), }, - r, - ) - .finished - .finally(() => { - kv.close(); - }); + (request) => router.fetch(request), + ); } diff --git a/tasks/cf/dailies/dailies.ts b/tasks/cf/dailies/dailies.ts deleted file mode 100644 index 47616d3..0000000 --- a/tasks/cf/dailies/dailies.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * ScheduledEvent is the expected Cloudflare event for this worker. - */ -export interface ScheduledEvent { - cron: string; -} - -/** - * Env is the expected environment variables for this worker. - */ -export interface Env { - WEBHOOK_URL: string; -} - -/** - * Ctx is the expected context for this worker. - */ -interface Ctx { - waitUntil(promise: Promise): void; -} - -export default { - /** - * fetch is executed on every request. - */ - async fetch(request: Request, env: Env) { - if (request.method !== "GET") { - return new Response("Method not allowed", { status: 405 }); - } - - const url = new URL(request.url); - if (url.pathname !== "/__scheduled") { - return new Response("Not found", { status: 404 }); - } - - const cron = url.searchParams.get("cron"); - if (cron !== CRON_EXPRESSION) { - return new Response("Unexpected cron expression", { status: 400 }); - } - - const seasonID = url.searchParams.get("season_id"); - return await execute(env.WEBHOOK_URL, seasonID); - }, - - /** - * scheduled is executed daily at 12:00 AM UTC. - * - * See: - * - - */ - scheduled(event: ScheduledEvent, env: Env, ctx: Ctx) { - if (event.cron !== CRON_EXPRESSION) { - return; - } - - ctx.waitUntil(execute(env.WEBHOOK_URL)); - }, -}; - -function execute(webhookURL: string | URL, seasonID?: string | null) { - if (seasonID) { - webhookURL = new URL(webhookURL); - webhookURL.searchParams.set("season_id", seasonID); - } - - return fetch(webhookURL, { method: "POST" }); -} - -/** - * CRON_EXPRESSION is the cron expression for the scheduled event. - * - * See: - * - - */ -const CRON_EXPRESSION = "0 0 * * *"; diff --git a/tasks/cf/dailies/env.ts b/tasks/cf/dailies/env.ts deleted file mode 100644 index 50d7b64..0000000 --- a/tasks/cf/dailies/env.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { load } from "lc-dailies/deps.ts"; -import { - denoflare, - DENOFLARE_VERSION_TAG, -} from "lc-dailies/lib/denoflare/mod.ts"; - -await load({ export: true, examplePath: null }); - -const CF_ACCOUNT_ID = Deno.env.get("CF_ACCOUNT_ID")!; -const CF_API_TOKEN = Deno.env.get("CF_API_TOKEN")!; -const WEBHOOK_URL = Deno.env.get("WEBHOOK_URL")!; - -const DENOFLARE_SCRIPT_NAME = "lc-dailies"; -const DENOFLARE_SCRIPT_SPECIFIER = "cf/dailies/dailies.ts"; - -async function daily(...args: string[]) { - return await denoflare({ - versionTag: DENOFLARE_VERSION_TAG, - scriptName: DENOFLARE_SCRIPT_NAME, - path: DENOFLARE_SCRIPT_SPECIFIER, - cfAccountID: CF_ACCOUNT_ID, - cfAPIToken: CF_API_TOKEN, - localPort: 8080, - args, - }); -} - -export async function serve() { - return await daily( - "serve", - DENOFLARE_SCRIPT_NAME, - "--secret-binding", - `WEBHOOK_URL:${WEBHOOK_URL}`, - ); -} - -export async function push() { - return await daily( - "push", - DENOFLARE_SCRIPT_NAME, - "--secret-binding", - `WEBHOOK_URL:${WEBHOOK_URL}`, - ); -} diff --git a/tasks/cf/dailies/push/main.ts b/tasks/cf/dailies/push/main.ts deleted file mode 100644 index 93dc4fd..0000000 --- a/tasks/cf/dailies/push/main.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { push } from "../env.ts"; - -if (import.meta.main) { - await push(); -} diff --git a/tasks/cf/dailies/serve/main.ts b/tasks/cf/dailies/serve/main.ts deleted file mode 100644 index 49f6627..0000000 --- a/tasks/cf/dailies/serve/main.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { serve } from "../env.ts"; - -if (import.meta.main) { - await serve(); -} diff --git a/tasks/dnt/main.ts b/tasks/dnt/main.ts index b2a9fde..57df109 100644 --- a/tasks/dnt/main.ts +++ b/tasks/dnt/main.ts @@ -1,7 +1,7 @@ // Run: // deno run -A tasks/dnt/main.ts $VERSION -import { build, emptyDir } from "https://deno.land/x/dnt@0.38.1/mod.ts"; +import { build, emptyDir } from "https://deno.land/x/dnt@0.40.0/mod.ts"; await emptyDir("./npm");