diff --git a/deno.lock b/deno.lock deleted file mode 100644 index 8ddf6cb..0000000 --- a/deno.lock +++ /dev/null @@ -1,118 +0,0 @@ -{ - "version": "3", - "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" - } -} 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/lib/api/api.ts b/lib/api/api.ts deleted file mode 100644 index 233d1a1..0000000 --- a/lib/api/api.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as discord from "lc-dailies/lib/discord/mod.ts"; -import * as lc from "lc-dailies/lib/lc/mod.ts"; -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"; - -/** - * 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() - .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, - ), - ) - .get( - new URLPattern({ pathname: "/invite" }), - () => - Promise.resolve( - Response.redirect(makeInviteURL(discordApplicationID)), - ), - ) - .get( - new URLPattern({ pathname: "/source" }), - () => - Promise.resolve( - Response.redirect("https://github.com/acmcsufoss/lc-dailies"), - ), - ) - .get( - new URLPattern({ pathname: "/seasons" }), - withCORS(makeSeasonsGetHandler(leaderboardClient)), - ) - .get( - new URLPattern({ pathname: "/seasons/:season_id.txt" }), - withCORS(makeSeasonTxtGetHandler(leaderboardClient)), - ) - .get( - new URLPattern({ pathname: "/seasons/:season_id" }), - withCORS(makeSeasonGetHandler(leaderboardClient)), - ); -} - -/** - * makeOnLoad creates a function which is called when the server is - * loaded. - */ -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, - }); - - console.log( - "- Discord application information:", - `https://discord.com/developers/applications/${discordApplicationID}/`, - ); - console.log( - "- Interaction endpoint:", - `http://127.0.0.1:${port}/`, - ); - console.log( - "- Invite LC-Dailies to your server:", - `http://127.0.0.1:${port}/invite`, - ); - console.log( - "- Latest season:", - `http://127.0.0.1:${port}/seasons/latest`, - ); - }; -} - -function makeInviteURL(applicationID: string) { - return `https://discord.com/api/oauth2/authorize?client_id=${applicationID}&scope=applications.commands`; -} - -/** - * withCORS wraps a handler 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", - ); - return response; - }; -} diff --git a/lib/api/dailies.ts b/lib/api/dailies.ts deleted file mode 100644 index 277354b..0000000 --- a/lib/api/dailies.ts +++ /dev/null @@ -1,215 +0,0 @@ -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 * as lc from "lc-dailies/lib/lc/mod.ts"; -import * as leaderboard from "lc-dailies/lib/leaderboard/mod.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( - lcClient: lc.LCClient, - leaderboardClient: leaderboard.LeaderboardClient, - webhookURL: string, - 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 - ? await leaderboardClient.getSeason(seasonID) - : await leaderboardClient.getLatestSeason(); - - // If the season is ongoing, then sync it. - const referenceDate = new Date(); - - let isLatestSeason = false; - if (storedSeason) { - const seasonStartDate = new Date(storedSeason.start_date).getTime(); - const seasonEndDate = seasonStartDate + WEEK; - isLatestSeason = leaderboard.checkDateBetween( - seasonStartDate, - seasonEndDate, - referenceDate.getTime(), - ); - } - - // Sync the season if it is ongoing and not synced. - const syncedSeason = isLatestSeason && storedSeason - ? await leaderboardClient - .sync(storedSeason.id) - .then((response) => response.season) - : null; - - // Format the webhook embed. - const embeds = makeDailyWebhookEmbeds({ - question, - questionDate, - season: isSunday ? (syncedSeason ?? storedSeason) : null, - }); - - // Execute the webhook. - await discord.executeWebhook({ - url: webhookURL, - data: { embeds }, - }); - - // If the season is not synced, then sync it to set up the next season. - if (!syncedSeason) { - await leaderboardClient.sync(undefined, referenceDate); - } - - // Acknowledge the request. - return new Response("OK"); -} - -/** - * DailyWebhookOptions are the options for makeDailyWebhookEmbeds. - */ -export interface DailyWebhookOptions { - /** - * question is the daily question. - */ - question: api.Question; - - /** - * questionDate is the date of the question. - */ - questionDate: Date; - - /** - * season is the season to recap. - */ - season: api.Season | null; -} - -/** - * makeDailyWebhookEmbeds formats a daily webhook. - */ -export function makeDailyWebhookEmbeds( - options: DailyWebhookOptions, -): APIEmbed[] { - const embed: APIEmbed = { - title: options.question.title, - url: options.question.url, - description: `Daily Leetcode question for ${options.question.date}.`, - color: getColorByDifficulty(options.question.difficulty), - fields: [ - { - name: "Difficulty", - value: options.question.difficulty, - inline: true, - }, - { - name: "Here is a snack to get your brain working!", - value: snacks.pickRandom(options.questionDate), - inline: true, - }, - { - name: - "Register to play by typing `/lc register YOUR_LC_USERNAME` below!", - value: "[See more…](https://acmcsuf.com/lc-dailies-handbook)", - }, - ], - }; - - if (options.season) { - embed.fields?.push({ - name: `Leaderboard for week of ${options.season.start_date}`, - value: [ - "```", - leaderboard.formatScores(options.season), - "```", - ].join("\n"), - }); - } - - return [embed]; -} - -/** - * getColorByDifficulty returns a color for a difficulty. - */ -export function getColorByDifficulty(difficulty: string): number | undefined { - switch (difficulty) { - case "Easy": { - return 0x339933; - } - - case "Medium": { - return 0xff6600; - } - - case "Hard": { - return 0xe91e63; - } - } -} 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/mod.ts b/lib/api/discord_app/mod.ts deleted file mode 100644 index 05bbef7..0000000 --- a/lib/api/discord_app/mod.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./app.ts"; 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/snacks.ts b/lib/api/snacks.ts deleted file mode 100644 index 03a5c36..0000000 --- a/lib/api/snacks.ts +++ /dev/null @@ -1,71 +0,0 @@ -const SNACKS = [ - "πŸ•", - "🍫", - "🍬", - "🍩", - "🍟", - "πŸ₯", - "πŸ₯¨", - "πŸ₯ͺ", - "πŸ₯ ", - "πŸ₯‘", - "🍦", - "🍧", - "πŸͺ", - "🧁", - "πŸ§ƒ", - "🍌", - "πŸ₯œ", - "πŸ₯•", - "πŸ₯”", - "πŸ‰", - "🍊", - "πŸ‡", - "🍎", - "πŸ₯­", - "β˜•", - "🍭", - "πŸ™", - "πŸ§‡", -]; - -const HALLOWEEN_SNACKS = [ - "🍬", - "🍭", - "🍫", - "πŸͺ", - "🧁", - "πŸŽƒ", -]; - -const WINTER_SNACKS = [ - "πŸͺ", - "🧁", - "🍫", - "🍬", - "🍭", - "🍩", - "🍧", - "❄", -]; - -/** - * pickRandom picks a random snack from the list of snacks. - */ -export function pickRandom(date: Date): string { - const snacks = getSnacksByMonth(date.getMonth()); - const randomIndex = Math.floor(date.getTime() % snacks.length); - return snacks[randomIndex]; -} - -function getSnacksByMonth(month: number) { - if (month === 9) { - return HALLOWEEN_SNACKS; - } - - if (month === 11) { - return WINTER_SNACKS; - } - - return SNACKS; -} diff --git a/lib/api/types.ts b/lib/api/types.ts deleted file mode 100644 index bfeccc8..0000000 --- a/lib/api/types.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Player is a registered player from Leetcode. - */ -export interface Player { - /** - * discord_user_id is the Discord user ID of the player. - */ - discord_user_id: string; - - /** - * lc_username is the Leetcode username of the player. - */ - lc_username: string; -} - -/** - * Players is a map of players by Discord user ID. - */ -export interface Players { - [discord_user_id: string]: Player; -} - -/** - * Submission is a Leetcode submission. - */ -export interface Submission { - /** - * id is the ID of the submission. - */ - id: string; - - /** - * date is the timestamp of the Leetcode submission. - */ - date: string; -} - -/** - * Submissions is a map of submissions by question name - * by Discord user ID. - */ -export interface Submissions { - [discord_user_id: string]: { - [question_name: string]: Submission; - }; -} - -/** - * Question is a Leetcode question. - */ -export interface Question { - /** - * name is the name of the daily question. - */ - name: string; - - /** - * date is the date the daily question was posted in the format of YYYY-MM-DD. - */ - date: string; - - /** - * title is the title of the daily question. - */ - title: string; - - /** - * difficulty is the difficulty of the daily question. - */ - difficulty: string; - - /** - * url is the link of the daily question. - */ - url: string; -} - -/** - * Questions is a map of questions by question name. - */ -export interface Questions { - [question_name: string]: Question; -} - -/** - * Scores is a map of scores by Discord user ID. - */ -export interface Scores { - [discord_user_id: string]: number; -} - -/** - * Season is a season of the leaderboard. - */ -export interface Season { - /** - * id is the ID of the season. - */ - id: string; - - /** - * start_date is the start date of the season. - */ - start_date: string; - - /** - * scores is the map of scores in the season. - */ - scores: Scores; - - /** - * players is the map of players in the season. - */ - players: Players; - - /** - * questions is the map of questions in the season. - */ - questions: Questions; - - /** - * submissions is the map of submissions in the season. - */ - submissions: Submissions; - - /** - * synced_at is the date the season was synced. - * - * The field is undefined if the season has not been synced. - */ - synced_at?: string; -} - -/** - * RegisterResponse is the response for the register subcommand. - */ -export interface RegisterResponse { - /** - * ok is whether the registration was successful. - */ - ok: boolean; -} - -/** - * SyncResponse is the response for the sync subcommand. - */ -export interface SyncResponse { - /** - * season is the season that was synced. - */ - season: Season; -} 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 deleted file mode 100644 index b84b351..0000000 --- a/lib/lc/client.ts +++ /dev/null @@ -1,143 +0,0 @@ -import type { - LCClientInterface, - LCQuestion, - LCSubmission, -} from "./client_interface.ts"; -import { makeQuestionURL } from "./urls.ts"; -import { gql } from "./gql.ts"; - -/** - * LCClient is the client for Leetcode. - */ -export class LCClient implements LCClientInterface { - constructor( - private readonly fetch: typeof window.fetch = window.fetch.bind(window), - ) {} - - /** - * verifyUser verifies the user by username. - */ - public async verifyUser(username: string): Promise { - const response = await this.fetch(`https://leetcode.com/${username}/`); - return response.status === 200; - } - - /** - * getDailyQuestion gets the daily question from Leetcode. - */ - public async getDailyQuestion(): Promise { - const date = new Date(); - const [question] = await this.listDailyQuestions( - date.getFullYear(), - date.getMonth() + 1, - 1, - ); - if (!question) { - throw new Error("No daily question found"); - } - - return question; - } - - /** - * listDailyQuestions gets the last `amount` of daily questions from Leetcode since `asOfYear` and `asOfMonth`. - */ - public async listDailyQuestions( - asOfYear: number, - asOfMonth: number, - limit = 10, - ): Promise { - const dailies: LCQuestion[] = []; - let currentYear = asOfYear; - let currentMonth = asOfMonth; - - while (dailies.length < limit) { - const response = await gql( - 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 ", - variables: { year: currentYear, month: currentMonth }, - }), - ); - const json = await response.json(); - const challenges = json.data.dailyCodingChallengeV2.challenges - .reverse() as Array<{ - date: string; - question: { - title: string; - titleSlug: string; - difficulty: string; - }; - }>; - for (const challenge of challenges) { - if (dailies.length === limit) { - break; - } - - dailies.push({ - name: challenge.question.titleSlug, - date: challenge.date, - title: challenge.question.title, - difficulty: challenge.question.difficulty, - url: makeQuestionURL(challenge.question.titleSlug), - }); - } - - currentMonth--; - if (currentMonth === 0) { - currentMonth = 12; - currentYear--; - } - } - - return dailies; - } - - /** - * getRecentAcceptedSubmissions gets the recent accepted submissions from - * Leetcode by username. - */ - public async getRecentAcceptedSubmissions( - username: string, - limit = MAX_SUBMISSIONS_LIMIT, - ): Promise { - if (limit > MAX_SUBMISSIONS_LIMIT) { - limit = MAX_SUBMISSIONS_LIMIT; - } - - return await gql( - JSON.stringify({ - operationName: "recentAcSubmissions", - query: - "\n query recentAcSubmissions($username: String!, $limit: Int!) {\n recentAcSubmissionList(username: $username, limit: $limit) {\n id\n title\n titleSlug\n timestamp\n }\n}\n ", - variables: { username, limit }, - }), - ) - .then((response) => response.json()) - /** - * Map the result of the graphql query into the shape of a LCDailyQuestion instance. - */ - .then((json) => - json.data.recentAcSubmissionList - ?.map(( - acSubmission: { - id: string; - title: string; - timestamp: string; - titleSlug: string; - }, - ): LCSubmission => ({ - id: acSubmission.id, - name: acSubmission.titleSlug, - title: acSubmission.title, - timestamp: acSubmission.timestamp, - })) ?? [] - ); - } -} - -/** - * MAX_SUBMISSIONS_LIMIT is the maximum amount of submissions to fetch from Leetcode. - */ -export const MAX_SUBMISSIONS_LIMIT = 20; diff --git a/lib/lc/client_interface.ts b/lib/lc/client_interface.ts deleted file mode 100644 index 1ea0e97..0000000 --- a/lib/lc/client_interface.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { Question } from "lc-dailies/lib/api/mod.ts"; - -/** - * LCQuestion is an alias interface for a Leetcode question. - */ -export type LCQuestion = Question; - -/** - * LCSubmission is the representation of Leetcode's recent submission per user. - */ -export interface LCSubmission { - /** - * id is the id details of the submission. - */ - id: string; - - /** - * name is the name of the question of the submission. - */ - name: string; - - /** - * title is the title of the question of the submission. - */ - title: string; - - /** - * timestamp is the time the submission was submitted. - */ - timestamp: string; -} - -/** - * LCClientInterface is the client interface for Leetcode. - */ -export interface LCClientInterface { - /** - * verifyUser verifies the user by username. - */ - verifyUser(username: string): Promise; - - /** - * getDailyQuestion gets the daily question from Leetcode. - */ - getDailyQuestion(): Promise; - - /** - * listDailyQuestions gets the last `amount` of daily questions from Leetcode since `asOfYear` and `asOfMonth`. - */ - listDailyQuestions( - asOfYear: number, - asOfMonth: number, - limit?: number, - ): Promise; - - /** - * getRecentAcceptedSubmissions gets the recent accepted submissions from - * Leetcode by username. - */ - getRecentAcceptedSubmissions( - username: string, - limit?: number, - ): Promise; -} diff --git a/lib/lc/client_test.ts b/lib/lc/client_test.ts deleted file mode 100644 index ed2324a..0000000 --- a/lib/lc/client_test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { assertEquals } from "lc-dailies/deps.ts"; -import { makeQuestionURL, parseSubmissionID } from "./urls.ts"; - -Deno.test("makeQuestionURL", () => { - assertEquals( - makeQuestionURL("implement-stack-using-queues"), - "https://leetcode.com/problems/implement-stack-using-queues/", - ); -}); - -Deno.test("parseSubmissionID full URL", () => { - assertEquals( - parseSubmissionID( - "https://leetcode.com/problems/implement-stack-using-queues/submissions/1035629181/", - ), - "1035629181", - ); -}); - -Deno.test("parseSubmissionID detail URL", () => { - assertEquals( - parseSubmissionID( - "https://leetcode.com/submissions/detail/1035629181/", - ), - "1035629181", - ); -}); - -Deno.test("parseSubmissionID submission ID ignores search params", () => { - assertEquals( - parseSubmissionID( - "https://leetcode.com/problems/unique-paths/submissions/1039832006/?envType=daily-question&envId=2023-09-03", - ), - "1039832006", - ); -}); - -Deno.test("parseSubmissionID submission ID", () => { - assertEquals( - parseSubmissionID("1035629181"), - "1035629181", - ); -}); diff --git a/lib/lc/fake_client.ts b/lib/lc/fake_client.ts deleted file mode 100644 index 7775e4d..0000000 --- a/lib/lc/fake_client.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { - LCClientInterface, - LCQuestion, - LCSubmission, -} from "./client_interface.ts"; - -export const FAKE_LC_USERNAME = "fake_lc_username"; -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 = { - name: FAKE_LC_QUESTION_NAME, - title: FAKE_LC_QUESTION_TITLE, - url: FAKE_LC_QUESTION_URL, - difficulty: FAKE_LC_QUESTION_DIFFICULTY, - date: FAKE_LC_QUESTION_DATE, -}; -export const FAKE_LC_QUESTIONS: LCQuestion[] = [FAKE_LC_QUESTION]; -export const FAKE_RECENT_SUBMISSION_ID = "1031839418"; -export const FAKE_RECENT_SUBMISSION_TIMESTAMP = "1690761600"; -export const FAKE_RECENT_SUBMISSION: LCSubmission = { - id: FAKE_RECENT_SUBMISSION_ID, - name: FAKE_LC_QUESTION_NAME, - title: FAKE_LC_QUESTION_TITLE, - timestamp: FAKE_RECENT_SUBMISSION_TIMESTAMP, -}; -export const FAKE_RECENT_SUBMISSIONS: LCSubmission[] = [ - FAKE_RECENT_SUBMISSION, -]; - -/** - * FakeLCClient is a fake implementation of LCClient. - */ -export class FakeLCClient implements LCClientInterface { - public verifyUser(username: string): Promise { - return Promise.resolve(username === FAKE_LC_USERNAME); - } - - public listDailyQuestions( - _: number, - __: number, - ___: number, - ): Promise { - return Promise.resolve(FAKE_LC_QUESTIONS); - } - - public getRecentAcceptedSubmissions( - _: string, - __: number, - ): Promise { - return Promise.resolve(FAKE_RECENT_SUBMISSIONS); - } - - public getDailyQuestion(): Promise { - return Promise.resolve(FAKE_LC_QUESTION); - } -} diff --git a/lib/lc/gql.ts b/lib/lc/gql.ts deleted file mode 100644 index ca67481..0000000 --- a/lib/lc/gql.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * gql executes a query to Leetcode's GraphQL API. - */ -export async function gql(body: string): Promise { - return await fetch("https://leetcode.com/graphql/", { - method: "POST", - headers: { - accept: "*/*", - "accept-language": "en-US,en;q=0.9", - authorization: "", - "content-type": "application/json", - }, - body, - }); -} diff --git a/lib/lc/mod.ts b/lib/lc/mod.ts deleted file mode 100644 index 77239a1..0000000 --- a/lib/lc/mod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./client_interface.ts"; -export * from "./client.ts"; -export * from "./urls.ts"; diff --git a/lib/lc/urls.ts b/lib/lc/urls.ts deleted file mode 100644 index 52f89cf..0000000 --- a/lib/lc/urls.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * makeQuestionURL makes a Leetcode question URL from the title slug. - */ -export function makeQuestionURL(titleSlug: string): string { - return `https://leetcode.com/problems/${titleSlug}/`; -} - -/** - * parseSubmissionID parses the submission ID from the submission URL. - */ -export function parseSubmissionID(submissionURLOrID: string): string { - let submissionID = submissionURLOrID; - try { - const url = new URL(submissionURLOrID); - if (LEETCODE_SUBMISSIONS_PATHNAME_PATTERN.test(url.pathname)) { - submissionID = url.pathname - .replace(LEETCODE_SUBMISSIONS_PATHNAME_PATTERN, "") - .replace(/\/$/, ""); - } - } catch { - // noop - } - return submissionID; -} - -/** - * LEETCODE_SUBMISSIONS_PATHNAME_PATTERN is the pattern for Leetcode's - * submission URLs. - * - * Valid submission URLs: This entails a full URL or the direct submission ID. - * https://leetcode.com/problems/implement-stack-using-queues/submissions/1035629181/ - * https://leetcode.com/submissions/detail/1035629181/ - * 1035629181 - * https://leetcode.com/problems/unique-paths/submissions/1039832006/?envType=daily-question&envId=2023-09-03 - */ -const LEETCODE_SUBMISSIONS_PATHNAME_PATTERN = - /^\/(problems\/.*\/submissions\/|submissions\/detail\/)/; diff --git a/lib/leaderboard/denokv/denokv_leaderboard_client.ts b/lib/leaderboard/denokv/denokv_leaderboard_client.ts deleted file mode 100644 index 86610fa..0000000 --- a/lib/leaderboard/denokv/denokv_leaderboard_client.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { DAY, ulid, WEEK } from "lc-dailies/deps.ts"; -import type * as api from "lc-dailies/lib/api/mod.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"; - -/** - * DenoKvLeaderboardClient is the client for the leaderboard. - */ -export class DenoKvLeaderboardClient implements LeaderboardClient { - public constructor( - /** - * kv is the key-value store for the leaderboard. - */ - private readonly kv: Deno.Kv, - /** - * lc is the Leetcode client. - */ - private readonly lc: LCClientInterface, - /** - * restartMS is the milliseconds to restart the leaderboard. - * - * 0 is Sunday 12:00:000 AM UTC. - * 604_799_999 is Saturday 11:59:999 PM UTC. - */ - private readonly restartMs = 0, - ) {} - - /** - * listPlayers lists all the registered players. - */ - private async listPlayers(): Promise { - const players: api.Players = {}; - const entries = this.kv - .list({ prefix: [LeaderboardKvPrefix.PLAYERS] }); - for await (const entry of entries) { - const playerID = entry.key[1] as string; - players[playerID] = entry.value; - } - - return players; - } - - /** - * getLatestSeasonFromKv reads the latest season from Deno KV. - */ - private async getLatestSeasonFromKv(): Promise< - Deno.KvEntryMaybe | null - > { - // Get the latest season ID. - const latestSeasonIDResult = await this.kv - .get([LeaderboardKvPrefix.SEASON_ID]); - if (!latestSeasonIDResult.value) { - return null; - } - - // Get the current season. - const seasonResult = await this.kv.get([ - LeaderboardKvPrefix.SEASONS, - latestSeasonIDResult.value, - ]); - if (!seasonResult.value) { - throw new Error("Season not found"); - } - - return seasonResult; - } - - /** - * updateLatestSeason updates the latest season in Deno KV. - */ - private async updateLatestSeason(season: api.Season): Promise { - // Update the season. - const updateSeasonOp = this.kv.atomic(); - - // Update the season. - const updateSeasonResult = await updateSeasonOp.set( - [LeaderboardKvPrefix.SEASONS, season.id], - season, - ).commit(); - if (!updateSeasonResult.ok) { - throw new Error("Failed to update season"); - } - - // Update the current season ID. - const updateSeasonIDResult = await this.kv.set( - [LeaderboardKvPrefix.SEASON_ID], - season.id, - ); - if (!updateSeasonIDResult.ok) { - throw new Error("Failed to update season ID"); - } - - return; - } - - public async register( - playerID: string, - lcUsername: string, - ): Promise { - const key: Deno.KvKey = [LeaderboardKvPrefix.PLAYERS, playerID]; - const playerResult = await this.kv.get(key); - if (playerResult.value) { - throw new Error("Player already registered"); - } - - // Verify the user with Leetcode. - const isVerified = await this.lc.verifyUser(lcUsername); - if (!isVerified) { - throw new Error("Failed to verify user with Leetcode"); - } - - // Register the player. - const registerResult = await this.kv - .atomic() - .check(playerResult) - .set( - key, - { discord_user_id: playerID, lc_username: lcUsername }, - ) - .commit(); - if (!registerResult.ok) { - throw new Error("Failed to register player"); - } - - return { ok: true }; - } - - public async sync( - seasonID?: string, - referenceDate = new Date(), - ): Promise { - // startOfWeekUTC is the start of the season in UTC. - const startOfWeekUTC = getStartOfWeek(this.restartMs, referenceDate); - const startOfWeekDate = new Date(startOfWeekUTC); - - // Get the season. - let season: api.Season; - let seasonStartDate: Date; - let isLatestSeason: boolean; - let seasonResult: Deno.KvEntryMaybe | null; - const players = await this.listPlayers(); - if (seasonID) { - seasonResult = await this.kv.get([ - LeaderboardKvPrefix.SEASONS, - seasonID, - ]); - if (!seasonResult.value) { - throw new Error("Season not found"); - } - - season = seasonResult.value; - seasonStartDate = new Date(season.start_date); - isLatestSeason = startOfWeekDate.getTime() === seasonStartDate.getTime(); - } else { - seasonResult = await this.getLatestSeasonFromKv(); - const isPresentAndLatest = seasonResult?.value && - new Date(seasonResult.value.start_date).getTime() === - startOfWeekDate.getTime(); - - if (!isPresentAndLatest && seasonResult?.value) { - // Sync old season. - const oldSeason = await sync({ - lcClient: this.lc, - players, - season: seasonResult.value, - }); - oldSeason.synced_at = referenceDate.toUTCString(); - - // Store the synced old season. - await this.kv.set( - [LeaderboardKvPrefix.SEASONS, oldSeason.id], - oldSeason, - ); - } - - season = isPresentAndLatest - ? seasonResult?.value! - : makeEmptySeason(startOfWeekDate); - seasonStartDate = new Date(season.start_date); - isLatestSeason = true; - } - - // Sync the season. - season = await sync({ lcClient: this.lc, players, season }); - season.synced_at = referenceDate.toUTCString(); - - // Store the synced season. - await this.kv.set( - [LeaderboardKvPrefix.SEASONS, season.id], - season, - ); - - // Update the season if it is the latest season. - startOfWeekDate.getTime() === seasonStartDate.getTime(); - if (isLatestSeason) { - await this.updateLatestSeason(season); - } - - // Return a sync response. - return { season }; - } - - public async getSeason( - seasonID: string, - ): Promise { - const seasonResult = await this.kv.get([ - LeaderboardKvPrefix.SEASONS, - seasonID, - ]); - return seasonResult.value; - } - - public async listSeasons(): Promise { - const seasons: api.Season[] = []; - const entries = this.kv - .list({ prefix: [LeaderboardKvPrefix.SEASONS] }); - for await (const entry of entries) { - seasons.push(entry.value); - } - - return seasons; - } - - public async getLatestSeason(): Promise { - const seasonResult = await this.getLatestSeasonFromKv(); - return seasonResult?.value ?? null; - } -} - -/** - * LeaderboardKvPrefix is the key prefix for the leaderboard key-value store. - */ -export enum LeaderboardKvPrefix { - /** Collection of players. */ - PLAYERS = "leaderboard_players", - /** Collection of seasons. */ - SEASONS = "leaderboard_seasons", - /** Latest season ID. */ - SEASON_ID = "leaderboard_season_id", -} - -/** - * makeEmptySeason creates an empty season. - */ -export function makeEmptySeason(startOfWeek: Date): api.Season { - return { - id: ulid(startOfWeek.getTime()), - start_date: startOfWeek.toUTCString(), - scores: {}, - players: {}, - questions: {}, - submissions: {}, - }; -} - -function getStartOfWeek(restartMs = 0, date = new Date()): number { - let startOfWeek = new Date(date).setUTCHours(0, 0, 0, 0) - - (DAY * (date.getUTCDay())) + - restartMs; - if (startOfWeek > date.getTime()) { - startOfWeek -= WEEK; - } - - return startOfWeek; -} diff --git a/lib/leaderboard/denokv/denokv_leaderboard_client_test.ts b/lib/leaderboard/denokv/denokv_leaderboard_client_test.ts deleted file mode 100644 index dabedcc..0000000 --- a/lib/leaderboard/denokv/denokv_leaderboard_client_test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { assertEquals, assertRejects, DAY, WEEK } from "lc-dailies/deps.ts"; -import * as fake_lc from "lc-dailies/lib/lc/fake_client.ts"; -import type { Season } from "lc-dailies/lib/api/mod.ts"; -import { DenoKvLeaderboardClient } from "./denokv_leaderboard_client.ts"; - -const FAKE_DISCORD_USER_ID = "fake_discord_user_id"; -const FAKE_SEASON_START_DATE = new Date("2023-07-30"); -const FAKE_SEASON: Season = { - id: "", - start_date: FAKE_SEASON_START_DATE.toUTCString(), - players: { - [FAKE_DISCORD_USER_ID]: { - discord_user_id: FAKE_DISCORD_USER_ID, - lc_username: fake_lc.FAKE_LC_USERNAME, - }, - }, - scores: { - [FAKE_DISCORD_USER_ID]: 50, - }, - questions: { - [fake_lc.FAKE_LC_QUESTION_NAME]: fake_lc.FAKE_LC_QUESTION, - }, - submissions: { - [FAKE_DISCORD_USER_ID]: { - [fake_lc.FAKE_LC_QUESTION_NAME]: { - id: fake_lc.FAKE_RECENT_SUBMISSION_ID, - date: "Mon, 31 Jul 2023 00:00:00 GMT", - }, - }, - }, -}; - -Deno.test("DenoKvLeaderboardClient", async (t) => { - const kv = await Deno.openKv(":memory:"); - const client = new DenoKvLeaderboardClient( - kv, - new fake_lc.FakeLCClient(), - ); - - await t.step("register", async () => { - const result = await client.register( - FAKE_DISCORD_USER_ID, - fake_lc.FAKE_LC_USERNAME, - ); - assertEquals(result.ok, true); - }); - - await t.step("register same username", () => { - assertRejects(async () => { - await client.register( - FAKE_DISCORD_USER_ID, - fake_lc.FAKE_LC_USERNAME, - ); - }); - }); - - await t.step("sync", async () => { - const twoDaysAfterFakeSeasonStartDate = new Date( - FAKE_SEASON_START_DATE.getTime() + 2 * DAY, - ); - const syncResponse = await client - .sync(undefined, twoDaysAfterFakeSeasonStartDate); - assertSeasonsEquals(syncResponse.season, FAKE_SEASON); - }); - - let seasonID: string | undefined; - await t.step("getLatestSeason", async () => { - const season = await client.getLatestSeason(); - seasonID = season?.id; - assertSeasonsEquals(season, FAKE_SEASON); - }); - - await t.step("sync again", async () => { - const weekAfterFakeSeasonStartDate = new Date( - FAKE_SEASON_START_DATE.getTime() + WEEK, - ); - const syncResponse = await client - .sync(undefined, weekAfterFakeSeasonStartDate); - assertEquals( - syncResponse.season.start_date, - "Sun, 06 Aug 2023 00:00:00 GMT", - ); - assertEquals(syncResponse.season.submissions, {}); - }); - - await t.step("listSeasons", async () => { - const seasons = await client.listSeasons(); - assertEquals(seasons.length, 2); - - const season = seasons[0]; - assertSeasonsEquals(season, FAKE_SEASON); - }); - - await t.step("getSeason", async () => { - const season = await client.getSeason(seasonID!); - assertSeasonsEquals(season, FAKE_SEASON); - }); - - // Dispose of the resource. - kv.close(); -}); - -function assertSeasonsEquals( - actualSeason: Season | null, - expectedSeason: Season, -): void { - assertEquals(actualSeason?.start_date, expectedSeason.start_date); - assertEquals(actualSeason?.scores, expectedSeason.scores); - assertEquals(actualSeason?.players, expectedSeason.players); - assertEquals(actualSeason?.questions, expectedSeason.questions); - assertEquals(actualSeason?.submissions, expectedSeason.submissions); -} diff --git a/lib/leaderboard/denokv/mod.ts b/lib/leaderboard/denokv/mod.ts deleted file mode 100644 index 444b6c3..0000000 --- a/lib/leaderboard/denokv/mod.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./denokv_leaderboard_client.ts"; diff --git a/lib/leaderboard/leaderboard_client.ts b/lib/leaderboard/leaderboard_client.ts deleted file mode 100644 index 9473ad9..0000000 --- a/lib/leaderboard/leaderboard_client.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type * as api from "lc-dailies/lib/api/mod.ts"; - -/** - * LeaderboardClient is the client interface for the leaderboard. - */ -export interface LeaderboardClient { - /** - * register registers a new player to the leaderboard. - */ - register( - discord_user_id: string, - lc_username: string, - ): Promise; - - /** - * sync syncs the leaderboard with Leetcode. - * - * Throws an error if the season is unable to be synced. - * - * Returns the synced season. - */ - sync(season_id?: string, reference_date?: Date): Promise; - - /** - * getLatestSeason gets the latest season. - */ - getLatestSeason(): Promise; - - /** - * getSeason gets a season of the leaderboard by ID. - */ - getSeason(season_id: string): Promise; - - /** - * listSeasons gets a list of season IDs of the leaderboard. - */ - listSeasons(): Promise; -} diff --git a/lib/leaderboard/mod.ts b/lib/leaderboard/mod.ts deleted file mode 100644 index 4eb2c75..0000000 --- a/lib/leaderboard/mod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./leaderboard_client.ts"; -export * from "./scores.ts"; -export * from "./sync.ts"; diff --git a/lib/leaderboard/scores.ts b/lib/leaderboard/scores.ts deleted file mode 100644 index 3a86483..0000000 --- a/lib/leaderboard/scores.ts +++ /dev/null @@ -1,258 +0,0 @@ -import type * as api from "lc-dailies/lib/api/types.ts"; - -/** - * CalculateScoresOptions is the options for calculateScores. - */ -export interface CalculateScoresOptions { - /** - * submissions are the submissions in the season. - */ - submissions: api.Submissions; - - /** - * questions are the questions in the season. - */ - questions: api.Questions; - - /** - * players are the players in the season. - */ - players: api.Players; - - /** - * possibleHighestScore is the highest possible score per question. - */ - possibleHighestScore: number; - - /** - * possibleLowestScore is the lowest possible score per question. - */ - possibleLowestScore: number; - - /** - * duration is the amount of milliseconds it takes to linearly interpolate - * between the highest and lowest possible scores. - */ - duration: number; - - /** - * modifyScore modifies the score of a player. - */ - modifyScore?: (score: number) => number; -} - -/** - * calculateSubmissionScore calculates the score of a submission. - */ -export function calculateSubmissionScore( - submission: api.Submission, - question: api.Question, - options: CalculateScoresOptions, -): number { - const questionDate = new Date(`${question.date} GMT`); - const submissionDate = new Date(submission.date); - const msElapsed = submissionDate.getTime() - questionDate.getTime(); - const ratio = 1 - Math.min(Math.max(msElapsed / options.duration, 0), 1); - const score = ((options.possibleHighestScore - options.possibleLowestScore) * - ratio) + options.possibleLowestScore; - if (!options.modifyScore) { - return score; - } - - return options.modifyScore(score); -} - -/** - * calculatePlayerScore calculates the score of a player in a season. - * - * See: - * https://github.com/acmcsufoss/lc-dailies/issues/32#issuecomment-1728904942 - */ -export function calculatePlayerScore( - playerID: string, - options: CalculateScoresOptions, -): number { - const submissions = options.submissions[playerID]; - if (!submissions) { - return 0; - } - - const scoreSum = Object.entries(submissions) - .reduce((score, [questionID, submission]) => { - const question = options.questions[questionID]; - if (!question) { - return score; - } - - return score + calculateSubmissionScore( - submission, - question, - options, - ); - }, 0); - - return scoreSum; -} - -/** - * calculateScores calculates the scores of all players in a season. - * - * Returns a map of player ID to score. - */ -export function calculateScores( - options: CalculateScoresOptions, -): api.Scores { - return Object.keys(options.players) - .reduce((scores, playerID) => { - const score = calculatePlayerScore(playerID, options); - if (score > 0) { - scores[playerID] = score; - } - - return scores; - }, {} as api.Scores); -} - -/** - * makeDefaultCalculateScoresOptions creates a default CalculateScoresOptions. - */ -export function makeDefaultCalculateScoresOptions( - players: api.Players, - questions: api.Questions, - submissions: api.Submissions, -): CalculateScoresOptions { - return { - players, - questions, - submissions, - possibleHighestScore: 100, - possibleLowestScore: 50, - duration: 1_000 * 60 * 60 * 24, // 1 day. - modifyScore: defaultModifyScore, - }; -} - -/** - * defaultModifyScore is the default score modifier. - */ -export function defaultModifyScore(score: number): number { - return Math.ceil(score); -} - -/** - * 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})`; - }) - .join("\n"); -} - -/** - * formatSubmissions formats the submissions of a player in a season. - */ -export function formatSubmissions( - season: api.Season, - playerID: string, -): string { - const daysToQuestionIDsMap = Object.entries(season.questions) - .reduce((questions, [questionID, question]) => { - const date = new Date(question.date + " GMT"); - questions[date.getUTCDay()] = questionID; - return questions; - }, {} as Record); - - let result = ""; - for (let i = 0; i < 7; i++) { - const questionID = daysToQuestionIDsMap[i]; - if (!questionID) { - result += formatDifficulty(); - continue; - } - - const submission = season.submissions[playerID]?.[questionID]; - if (!submission) { - result += formatDifficulty(); - continue; - } - - const question = season.questions[questionID]; - result += formatDifficulty(question.difficulty); - } - - return result; -} - -/** - * formatRank formats the rank of a player in a season. - */ -export function formatRank(rank: number): string { - switch (rank) { - case 1: { - return "πŸ₯‡"; - } - - case 2: { - return "πŸ₯ˆ"; - } - - case 3: { - return "πŸ₯‰"; - } - - case 11: - case 12: - case 13: { - return `${rank}th`; - } - } - - const lastDigit = rank % 10; - switch (lastDigit) { - case 1: { - return `${rank}st`; - } - - case 2: { - return `${rank}nd`; - } - - case 3: { - return `${rank}rd`; - } - - default: { - return `${rank}th`; - } - } -} - -/** - * formatDifficulty formats the difficulty of a question. - */ -export function formatDifficulty(difficulty?: string): string { - switch (difficulty) { - case "Easy": { - return "🟒"; - } - - case "Medium": { - return "🟠"; - } - - case "Hard": { - return "πŸ”΄"; - } - - default: { - return "Β·"; - } - } -} diff --git a/lib/leaderboard/scores_test.ts b/lib/leaderboard/scores_test.ts deleted file mode 100644 index a40e0b4..0000000 --- a/lib/leaderboard/scores_test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { assertEquals } from "lc-dailies/deps.ts"; -import { - calculatePlayerScore, - calculateScores, - makeDefaultCalculateScoresOptions, -} from "./scores.ts"; - -const FAKE_SEASON = { - "id": "01H8T4MM00BQHHK7VTTEJE1WAS", - "start_date": "Sun, 27 Aug 2023 00:00:00 GMT", - "players": { - "redacted_discord_id_00": { - "discord_user_id": "redacted_discord_id_00", - "lc_username": "EthanThatOneKid", - }, - "redacted_discord_id_01": { - "discord_user_id": "redacted_discord_id_01", - "lc_username": "PillowGit", - }, - }, - "questions": { - "implement-stack-using-queues": { - "name": "implement-stack-using-queues", - "date": "2023-08-28", - "title": "Implement Stack using Queues", - "difficulty": "Easy", - "url": "https://leetcode.com/problems/implement-stack-using-queues/", - }, - "counting-bits": { - "name": "counting-bits", - "date": "2023-09-01", - "title": "Counting Bits", - "difficulty": "Easy", - "url": "https://leetcode.com/problems/counting-bits/", - }, - }, - "submissions": { - "redacted_discord_id_00": { - "implement-stack-using-queues": { - "id": "1035629181", - "date": "Wed, 30 Aug 2023 04:10:39 GMT", - }, - "counting-bits": { - "id": "1037337123", - "date": "Fri, 01 Sep 2023 04:18:58 GMT", - }, - }, - "redacted_discord_id_01": { - "counting-bits": { - "id": "1037327504", - "date": "Fri, 01 Sep 2023 04:01:36 GMT", - }, - "implement-stack-using-queues": { - "id": "1034291152", - "date": "Mon, 28 Aug 2023 17:06:37 GMT", - }, - }, - }, -}; - -Deno.test("calculatePlayerScore calculates the score of a player", () => { - assertEquals( - calculatePlayerScore( - "redacted_discord_id_00", - makeDefaultCalculateScoresOptions( - FAKE_SEASON.players, - FAKE_SEASON.questions, - FAKE_SEASON.submissions, - ), - ), - 159, - ); -}); - -Deno.test("calculateSeasonScores calculates the scores of a season", () => { - const seasonScores = calculateScores( - makeDefaultCalculateScoresOptions( - FAKE_SEASON.players, - FAKE_SEASON.questions, - FAKE_SEASON.submissions, - ), - ); - - assertEquals( - seasonScores["redacted_discord_id_00"], - 159, - ); - assertEquals( - seasonScores["redacted_discord_id_01"], - 145, - ); -}); diff --git a/lib/leaderboard/sync.ts b/lib/leaderboard/sync.ts deleted file mode 100644 index 9f01ba3..0000000 --- a/lib/leaderboard/sync.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { SECOND, WEEK } from "lc-dailies/deps.ts"; -import type * as api from "lc-dailies/lib/api/mod.ts"; -import type { LCClientInterface } from "lc-dailies/lib/lc/mod.ts"; -import { - calculateScores, - makeDefaultCalculateScoresOptions, -} from "lc-dailies/lib/leaderboard/mod.ts"; - -/** - * SyncOptions are the required options for the sync operation. - */ -export interface SyncOptions { - /** - * season is the season to sync. - */ - season: api.Season; - - /** - * players are the registered players. - */ - players: api.Players; - - /** - * lcClient is the Leetcode client. - */ - lcClient: LCClientInterface; - - /** - * questionsFetchAmount is the amount of questions to fetch from Leetcode. - * - * If not specified, it will be set to 10. - */ - questionsFetchAmount?: number; -} - -/** - * sync creates a season given a list of players, an LCCient, and a - * date range. - */ -export async function sync(options: SyncOptions): Promise { - // Fetch the daily questions of the season. - const seasonStartDate = new Date(options.season.start_date); - const seasonEndDate = new Date(seasonStartDate.getTime() + WEEK); - const recentDailyQuestions = await options.lcClient.listDailyQuestions( - seasonEndDate.getUTCFullYear(), - seasonEndDate.getUTCMonth() + 1, - options.questionsFetchAmount, - ); - - // Fetch the submissions of the players. - for (const playerID in options.players) { - // Get the submissions of the player. - const player = options.players[playerID]; - const lcSubmissions = await options.lcClient - .getRecentAcceptedSubmissions(player.lc_username); - - // Store the submissions in the season. - for (const lcSubmission of lcSubmissions) { - const questionName = lcSubmission.name; - - // Skip if the submission is not the earliest submission. - const submissionDate = fromLCTimestamp(lcSubmission.timestamp); - const storedSubmission: api.Submission | undefined = options.season - .submissions[playerID]?.[questionName]; - if ( - storedSubmission && new Date(storedSubmission.date) < submissionDate - ) { - continue; - } - - // Skip if the submission is not in the season. - const isSubmissionInSeason = checkDateBetween( - seasonStartDate.getTime(), - seasonEndDate.getTime(), - submissionDate.getTime(), - ); - if (!isSubmissionInSeason) { - continue; - } - - // Fetch the question if it is not in the season. - const storedQuestion: api.Question | undefined = - options.season.questions[questionName]; - if (!storedQuestion) { - // Skip if the question is not found. - const recentDailyQuestion = recentDailyQuestions - .find((q) => q.name === questionName); - if (!recentDailyQuestion) { - continue; - } - - // Skip if the question is not in the season. - const questionDate = new Date(recentDailyQuestion.date); - const isQuestionInSeason = checkDateBetween( - seasonStartDate.getTime(), - seasonEndDate.getTime(), - questionDate.getTime(), - ); - if (!isQuestionInSeason) { - continue; - } - - // Store the question in the season. - options.season.questions[questionName] ??= recentDailyQuestion; - } - - // Store the earliest submission of the player. - options.season.submissions[playerID] ??= {}; - options.season.submissions[playerID][questionName] = { - id: lcSubmission.id, - date: submissionDate.toUTCString(), - }; - - // Store the player in the season if it is not in the season. - options.season.players[playerID] ??= player; - } - } - - // Calculate the scores of the players. - options.season.scores = calculateScores( - makeDefaultCalculateScoresOptions( - options.season.players, - options.season.questions, - options.season.submissions, - ), - ); - - return options.season; -} - -/** - * fromLCTimestamp converts a Leetcode timestamp to a Date. - */ -export function fromLCTimestamp(timestamp: string): Date { - const utcSeconds = parseInt(timestamp); - return new Date(utcSeconds * SECOND); -} - -/** - * checkDateBetween checks if a date is in a given duration. - */ -export function checkDateBetween( - start: number, - end: number, - date: number, -): boolean { - return date >= start && date < end; -} diff --git a/lib/leaderboard/sync_test.ts b/lib/leaderboard/sync_test.ts deleted file mode 100644 index a8d1bd2..0000000 --- a/lib/leaderboard/sync_test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { assertEquals } from "lc-dailies/deps.ts"; -import type { LCSubmission } from "lc-dailies/lib/lc/mod.ts"; -// import type { SyncOptions } from "./sync.ts"; -import { sync } from "./sync.ts"; - -const FAKE_UNSYNCED_SEASON = { - "id": "01H8T4MM00BQHHK7VTTEJE1WAS", - "start_date": "Sun, 27 Aug 2023 00:00:00 GMT", - "players": { - "redacted_discord_id_00": { - "discord_user_id": "redacted_discord_id_00", - "lc_username": "EthanThatOneKid", - }, - "redacted_discord_id_01": { - "discord_user_id": "redacted_discord_id_01", - "lc_username": "PillowGit", - }, - }, - "questions": { - "implement-stack-using-queues": { - "name": "implement-stack-using-queues", - "date": "2023-08-28", - "title": "Implement Stack using Queues", - "difficulty": "Easy", - "url": "https://leetcode.com/problems/implement-stack-using-queues/", - }, - "counting-bits": { - "name": "counting-bits", - "date": "2023-09-01", - "title": "Counting Bits", - "difficulty": "Easy", - "url": "https://leetcode.com/problems/counting-bits/", - }, - }, - "submissions": { - "redacted_discord_id_00": { - "implement-stack-using-queues": { - "id": "1035629181", - "date": "Wed, 30 Aug 2023 04:10:39 GMT", - }, - "counting-bits": { - "id": "1037337123", - "date": "Fri, 01 Sep 2023 04:18:58 GMT", - }, - }, - "redacted_discord_id_01": { - "counting-bits": { - "id": "1037327504", - "date": "Fri, 01 Sep 2023 04:01:36 GMT", - }, - "implement-stack-using-queues": { - "id": "1034291152", - "date": "Mon, 28 Aug 2023 17:06:37 GMT", - }, - }, - }, - "scores": {}, -}; - -const FAKE_QUESTION = { - name: "reverse-integer", - date: "2023-09-02", - title: "Reverse Integer", - difficulty: "Easy", - url: "https://leetcode.com/problems/reverse-integer/", -}; - -const FAKE_SUBMISSION = { - id: "8008569420", - name: "reverse-integer", - title: "Reverse Integer", - timestamp: "1693627483", -}; - -const FAKE_SYNCED_SEASON = { - "id": "01H8T4MM00BQHHK7VTTEJE1WAS", - "start_date": "Sun, 27 Aug 2023 00:00:00 GMT", - "players": { - "redacted_discord_id_00": { - "discord_user_id": "redacted_discord_id_00", - "lc_username": "EthanThatOneKid", - }, - "redacted_discord_id_01": { - "discord_user_id": "redacted_discord_id_01", - "lc_username": "PillowGit", - }, - }, - "questions": { - "implement-stack-using-queues": { - "name": "implement-stack-using-queues", - "date": "2023-08-28", - "title": "Implement Stack using Queues", - "difficulty": "Easy", - "url": "https://leetcode.com/problems/implement-stack-using-queues/", - }, - "counting-bits": { - "name": "counting-bits", - "date": "2023-09-01", - "title": "Counting Bits", - "difficulty": "Easy", - "url": "https://leetcode.com/problems/counting-bits/", - }, - "reverse-integer": FAKE_QUESTION, - }, - "submissions": { - "redacted_discord_id_00": { - "implement-stack-using-queues": { - "id": "1035629181", - "date": "Wed, 30 Aug 2023 04:10:39 GMT", - }, - "counting-bits": { - "id": "1037337123", - "date": "Fri, 01 Sep 2023 04:18:58 GMT", - }, - }, - "redacted_discord_id_01": { - "counting-bits": { - "id": "1037327504", - "date": "Fri, 01 Sep 2023 04:01:36 GMT", - }, - "implement-stack-using-queues": { - "id": "1034291152", - "date": "Mon, 28 Aug 2023 17:06:37 GMT", - }, - "reverse-integer": { - "id": FAKE_SUBMISSION.id, - "date": "Sat, 02 Sep 2023 04:04:43 GMT", - }, - }, - }, - "scores": { - "redacted_discord_id_00": 159, - "redacted_discord_id_01": 204, - }, -}; - -Deno.test("sync syncs a season with Leetcode", async () => { - const actual = await sync({ - season: FAKE_UNSYNCED_SEASON, - players: FAKE_UNSYNCED_SEASON.players, - lcClient: { - verifyUser(_: string) { - throw new Error("Not implemented"); - }, - getDailyQuestion() { - throw new Error("Not implemented"); - }, - listDailyQuestions(_: number, __: number, ___: number) { - return Promise.resolve([FAKE_QUESTION]); - }, - getRecentAcceptedSubmissions(username: string, _?: number) { - const result: LCSubmission[] = []; - const fakeUsername = - FAKE_UNSYNCED_SEASON.players.redacted_discord_id_01.lc_username; - if (username === fakeUsername) { - result.push(FAKE_SUBMISSION); - } - - return Promise.resolve(result); - }, - }, - }); - - assertEquals(actual, FAKE_SYNCED_SEASON); -}); 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 deleted file mode 100644 index b457e72..0000000 --- a/main.ts +++ /dev/null @@ -1,52 +0,0 @@ -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"; - -if (import.meta.main) { - await main(); -} - -async function main() { - const kv = await Deno.openKv(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, - leaderboardClient, - ); - - await Router.serve( - { - port: PORT, - onListen: api.makeOnListen( - PORT, - DISCORD_APPLICATION_ID, - DISCORD_TOKEN, - ), - }, - r, - ) - .finished - .finally(() => { - kv.close(); - }); -} 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(); -}