diff --git a/bun.lock b/bun.lock index 7bf3f7e..8224434 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,9 @@ "@discordjs/core": "^2.0.1", "@sapphire/discord.js-utilities": "^7.3.2", "cheerio": "^1.0.0", + "deepl-node": "^1.16.0", "discord.js": "^14.17.3", + "execa": "^9.5.2", "file-type": "^20.1.0", "ky": "^1.7.4", "lru-cache": "^11.0.2", @@ -133,6 +135,8 @@ "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + "@toil/translate": ["@toil/translate@1.0.2", "", { "peerDependencies": { "typescript": "^5.6.2" } }, "sha512-awxtAuyhNpogagl20Ic/BV1XY68voGWU0w/5kzxlBN5sxUPrMtSqluDSPm4XvHRfji4v45r1p1NAxLISCQfhpA=="], "@tokenizer/inflate": ["@tokenizer/inflate@0.2.6", "", { "dependencies": { "debug": "^4.3.7", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-SdR/i05U7Xhnsq36iyIq/ZiGGw4PKzw4ww3bOq80Pjj4wyXpqyTcgrgdDdGlcatnlvzNJx8CQw3hp6QZvkUwhA=="], @@ -181,6 +185,10 @@ "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], @@ -211,6 +219,8 @@ "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -223,6 +233,10 @@ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "deepl-node": ["deepl-node@1.16.0", "", { "dependencies": { "@types/node": ">=12.0", "axios": "^1.7.4", "form-data": "^3.0.0", "loglevel": ">=1.6.2" } }, "sha512-sYrh8UngVHKBrgffLPl+ng065mlC+Ep3LBdSN3xqDBd4KdBZzgvdxHiHd9Pm9xQCI5pwyuCHyq0cDUW1T3vQUg=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="], "discord-api-types": ["discord-api-types@0.37.119", "", {}, "sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg=="], @@ -261,7 +275,7 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + "execa": ["execa@9.5.2", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.3", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.0", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.0.0" } }, "sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -277,6 +291,8 @@ "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "file-type": ["file-type@20.1.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-XoxU+lETfCf+bYK3SXkxFusAvmtYQl1u/ZC4zw1DBLEsHUvh339uwYucgQnnSMz1mRCWYJrCzsbJJ95hsQbZ8A=="], @@ -291,6 +307,10 @@ "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], + + "form-data": ["form-data@3.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ=="], + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -303,7 +323,7 @@ "htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="], - "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + "human-signals": ["human-signals@8.0.0", "", {}, "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA=="], "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -329,7 +349,11 @@ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], @@ -363,6 +387,8 @@ "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + "loglevel": ["loglevel@1.9.2", "", {}, "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg=="], + "lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="], "magic-bytes.js": ["magic-bytes.js@1.10.0", "", {}, "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ=="], @@ -373,6 +399,10 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -383,7 +413,7 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], @@ -399,6 +429,8 @@ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "parse5": ["parse5@7.2.1", "", { "dependencies": { "entities": "^4.5.0" } }, "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ=="], "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], @@ -415,6 +447,10 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -449,7 +485,7 @@ "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], @@ -485,6 +521,8 @@ "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -507,6 +545,8 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="], + "zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="], "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], @@ -521,32 +561,44 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "axios/form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="], + + "clipboardy/execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + "color-string/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "colorspace/color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], "discord.js/@discordjs/ws": ["@discordjs/ws@1.2.0", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.4.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.37.114", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg=="], - "execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], - - "execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "get-stream/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "winston/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "ya-ocr/file-type": ["file-type@19.6.0", "", { "dependencies": { "get-stream": "^9.0.1", "strtok3": "^9.0.1", "token-types": "^6.0.0", "uint8array-extras": "^1.3.0" } }, "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "clipboardy/execa/get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + + "clipboardy/execa/human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "clipboardy/execa/is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "clipboardy/execa/npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + + "clipboardy/execa/strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + "colorspace/color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "discord.js/@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], "ya-ocr/file-type/strtok3": ["strtok3@9.1.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^5.3.1" } }, "sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw=="], + "clipboardy/execa/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "colorspace/color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "ya-ocr/file-type/strtok3/peek-readable": ["peek-readable@5.4.2", "", {}, "sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg=="], diff --git a/package.json b/package.json index cc38035..fbad7f8 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "@discordjs/core": "^2.0.1", "@sapphire/discord.js-utilities": "^7.3.2", "cheerio": "^1.0.0", + "deepl-node": "^1.16.0", "discord.js": "^14.17.3", + "execa": "^9.5.2", "file-type": "^20.1.0", "ky": "^1.7.4", "lru-cache": "^11.0.2", diff --git a/src/commands/index.ts b/src/commands/index.ts index f912ffa..046f344 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,5 +1,5 @@ -import type { Command } from "../types/command"; +import type { Command, CommandBuilder } from "../types/command"; -export function defineCommand(command: Command) { +export function defineCommand(command: Command) { return command; } diff --git a/src/commands/language/translate.ts b/src/commands/language/translate.ts new file mode 100644 index 0000000..11f7f9a --- /dev/null +++ b/src/commands/language/translate.ts @@ -0,0 +1,130 @@ +import { + Attachment, + AutocompleteInteraction, + SlashCommandBuilder, + type InteractionEditReplyOptions, +} from "discord.js"; +import { defineCommand } from ".."; +import { + getSourceLanguages, + getTargetLanguages, + isSourceLanguageSupported, + isTargetLanguageSupported, + languageCodeToName, + translate, +} from "../../utils/deepl"; +import { abort } from "../../utils/error"; + +export async function translateAutocompleteImpl( + interaction: AutocompleteInteraction +) { + const option = interaction.options.getFocused(true); + const languages = + option.name === "source" + ? await getSourceLanguages() + : await getTargetLanguages(); + const choices = languages + .filter((language) => + language.name.toLowerCase().includes(option.value.toLowerCase()) + ) + .map((language) => ({ + name: language.name, + value: language.code, + })) + .slice(0, 25); + await interaction.respond(choices); +} + +export async function translateImpl( + text: string, + source: string | null, + target: string, + attachment?: Attachment +): Promise { + const { + text: translatedText, + detectedSourceLang, + billedCharacters, + } = await translate({ + text, + source, + target, + }); + + const displaySource = await languageCodeToName(detectedSourceLang); + const displayTarget = await languageCodeToName(target); + + if (translatedText.length > 4096) { + return { + files: [ + { + name: `${displaySource}-${displayTarget}.txt`, + attachment: `--- From ${displaySource} to ${displayTarget} ---\n${translatedText}`, + }, + ...(attachment ? [attachment] : []), + ], + }; + } + + return { + embeds: [ + { + title: `From ${displaySource} to ${displayTarget}`, + description: translatedText, + color: 0x0f2b46, + author: { + name: "DeepL", + icon_url: "https://www.google.com/s2/favicons?domain=deepl.com&sz=64", + }, + footer: { + text: `Billed characters: ${billedCharacters}`, + }, + }, + ], + files: attachment ? [attachment] : [], + }; +} + +export default defineCommand({ + data: new SlashCommandBuilder() + .setName("translate") + .setDescription("Translates text using DeepL") + .addStringOption((option) => + option + .setName("text") + .setDescription("The text to translate") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("source") + .setDescription("Source language of the text") + .setAutocomplete(true) + ) + .addStringOption((option) => + option + .setName("target") + .setDescription("Target language of the text") + .setAutocomplete(true) + ), + + autocomplete: translateAutocompleteImpl, + + async execute(interaction) { + const text = interaction.options.getString("text", true); + const source = interaction.options.getString("source") ?? null; + const target = interaction.options.getString("target") ?? "en-US"; + + await interaction.deferReply(); + + if (source && !(await isSourceLanguageSupported(source))) { + abort("Source language not supported"); + } + if (target && !(await isTargetLanguageSupported(target))) { + abort("Target language not supported"); + } + + const payload = await translateImpl(text, source, target); + await interaction.editReply(payload); + }, +}); diff --git a/src/commands/language/translateMenu.ts b/src/commands/language/translateMenu.ts new file mode 100644 index 0000000..8072041 --- /dev/null +++ b/src/commands/language/translateMenu.ts @@ -0,0 +1,19 @@ +import { ApplicationCommandType, ContextMenuCommandBuilder } from "discord.js"; +import { defineCommand } from ".."; +import { translateImpl } from "./translate"; + +export default defineCommand({ + data: new ContextMenuCommandBuilder() + .setName("Translate to English") + .setType(ApplicationCommandType.Message), + + async execute(interaction) { + if (!interaction.isMessageContextMenuCommand()) return; + + const text = interaction.targetMessage.content; + await interaction.deferReply(); + + const payload = await translateImpl(text, null, "en-US"); + await interaction.editReply(payload); + }, +}); diff --git a/src/commands/language/wiktionary.ts b/src/commands/language/wiktionary.ts index d41397f..296d7a7 100644 --- a/src/commands/language/wiktionary.ts +++ b/src/commands/language/wiktionary.ts @@ -50,6 +50,8 @@ export default defineCommand({ async execute(interaction) { let term = interaction.options.getString("term", true); + await interaction.deferReply(); + // autocomplete value vs user value if (term.startsWith(":")) { term = term.slice(1); diff --git a/src/commands/ocr/ocr.ts b/src/commands/ocr/ocr.ts new file mode 100644 index 0000000..501d722 --- /dev/null +++ b/src/commands/ocr/ocr.ts @@ -0,0 +1,87 @@ +import { + Attachment, + codeBlock, + inlineCode, + SlashCommandBuilder, + type InteractionEditReplyOptions, +} from "discord.js"; +import { defineCommand } from ".."; +import { downloadFile } from "../../utils/http"; +import { abort } from "../../utils/error"; +import { yandexOcr } from "../../utils/ocr"; +import sharp from "sharp"; + +export function buildOcrPayload( + text: string, + detected_lang: string, + attachment?: Attachment +): InteractionEditReplyOptions { + const languageName = + new Intl.DisplayNames(["en"], { type: "language" }).of(detected_lang) ?? + "unknown"; + + const content = `Detected language: ${inlineCode(languageName)}\n${codeBlock( + text + )}`; + + if (content.length > 2000) { + return { + content: `Detected language: ${inlineCode(languageName)}`, + files: [ + { + name: "ocr.txt", + attachment: text, + }, + ...(attachment ? [attachment] : []), + ], + }; + } + + return { + content, + files: attachment ? [attachment] : [], + }; +} + +export async function ocrImpl(attachment: Attachment) { + const { data, type } = await downloadFile(attachment.url); + if (!type?.mime.startsWith("image/")) { + abort("The file must be an image!"); + } + + const compressed = await sharp(data) + .resize(1000) + .jpeg({ quality: 90 }) + .toBuffer(); + + return yandexOcr(compressed, type.mime); +} + +export default defineCommand({ + data: new SlashCommandBuilder() + .setName("ocr") + .setDescription("OCR an image using Yandex") + .addAttachmentOption((option) => + option + .setName("image") + .setDescription("The image to OCR") + .setRequired(true) + ), + + async execute(interaction) { + const attachment = interaction.options.getAttachment("image", true); + if (!attachment.contentType?.startsWith("image/")) { + abort("The file must be an image!"); + } + + await interaction.deferReply(); + + const result = await ocrImpl(attachment); + const payload = buildOcrPayload( + result.text, + result.detected_lang, + attachment + ); + await interaction.editReply(payload); + }, +}); diff --git a/src/commands/ocr/ocrMenu.ts b/src/commands/ocr/ocrMenu.ts new file mode 100644 index 0000000..7960751 --- /dev/null +++ b/src/commands/ocr/ocrMenu.ts @@ -0,0 +1,28 @@ +import { ApplicationCommandType, ContextMenuCommandBuilder } from "discord.js"; +import { defineCommand } from ".."; +import { abort } from "../../utils/error"; +import { buildOcrPayload, ocrImpl } from "./ocr"; + +export default defineCommand({ + data: new ContextMenuCommandBuilder() + .setName("OCR") + .setType(ApplicationCommandType.Message), + + async execute(interaction) { + if (!interaction.isMessageContextMenuCommand()) return; + + const attachment = interaction.targetMessage.attachments.first(); + if (!attachment) { + abort("No attachment found"); + } + if (!attachment.contentType?.startsWith("image/")) { + abort("The file must be an image!"); + } + + await interaction.deferReply(); + + const result = await ocrImpl(attachment); + const payload = buildOcrPayload(result.text, result.detected_lang); + await interaction.editReply(payload); + }, +}); diff --git a/src/commands/ocr/ocrTranslate.ts b/src/commands/ocr/ocrTranslate.ts new file mode 100644 index 0000000..0a1adeb --- /dev/null +++ b/src/commands/ocr/ocrTranslate.ts @@ -0,0 +1,63 @@ +import { SlashCommandBuilder } from "discord.js"; +import { defineCommand } from ".."; +import { abort } from "../../utils/error"; +import { + isSourceLanguageSupported, + isTargetLanguageSupported, +} from "../../utils/deepl"; +import { + translateAutocompleteImpl, + translateImpl, +} from "../language/translate"; +import { ocrImpl } from "./ocr"; + +export default defineCommand({ + data: new SlashCommandBuilder() + .setName("ocrtranslate") + .setDescription( + "OCR an image using Yandex and translate the result using DeepL" + ) + .addAttachmentOption((option) => + option + .setName("image") + .setDescription("The image to OCR") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("source") + .setDescription("Source language of the text") + .setAutocomplete(true) + ) + .addStringOption((option) => + option + .setName("target") + .setDescription("Target language of the text") + .setAutocomplete(true) + ), + + autocomplete: translateAutocompleteImpl, + + async execute(interaction) { + const attachment = interaction.options.getAttachment("image", true); + const source = interaction.options.getString("source") ?? null; + const target = interaction.options.getString("target") ?? "en-US"; + + if (!attachment.contentType?.startsWith("image/")) { + abort("The file must be an image!"); + } + + await interaction.deferReply(); + + if (source && !(await isSourceLanguageSupported(source))) { + abort("Source language not supported"); + } + if (target && !(await isTargetLanguageSupported(target))) { + abort("Target language not supported"); + } + + const { text } = await ocrImpl(attachment); + const payload = await translateImpl(text, source, target, attachment); + await interaction.editReply(payload); + }, +}); diff --git a/src/commands/ocr/ocrTranslateMenu.ts b/src/commands/ocr/ocrTranslateMenu.ts new file mode 100644 index 0000000..07204ba --- /dev/null +++ b/src/commands/ocr/ocrTranslateMenu.ts @@ -0,0 +1,29 @@ +import { ApplicationCommandType, ContextMenuCommandBuilder } from "discord.js"; +import { defineCommand } from ".."; +import { abort } from "../../utils/error"; +import { translateImpl } from "../language/translate"; +import { ocrImpl } from "./ocr"; + +export default defineCommand({ + data: new ContextMenuCommandBuilder() + .setName("OCR and translate to English") + .setType(ApplicationCommandType.Message), + + async execute(interaction) { + if (!interaction.isMessageContextMenuCommand()) return; + + const attachment = interaction.targetMessage.attachments.first(); + if (!attachment) { + abort("No attachment found"); + } + if (!attachment.contentType?.startsWith("image/")) { + abort("The file must be an image!"); + } + + await interaction.deferReply(); + + const { text } = await ocrImpl(attachment); + const payload = await translateImpl(text, null, "en-US"); + await interaction.editReply(payload); + }, +}); diff --git a/src/commands/owner/restart.ts b/src/commands/owner/restart.ts index 4671386..a7b8c26 100644 --- a/src/commands/owner/restart.ts +++ b/src/commands/owner/restart.ts @@ -14,6 +14,6 @@ export default defineCommand({ flags: MessageFlags.Ephemeral, }); - await restart(interaction.token); + await restart({ token: interaction.token }); }, }); diff --git a/src/commands/owner/update.ts b/src/commands/owner/update.ts new file mode 100644 index 0000000..2df6cad --- /dev/null +++ b/src/commands/owner/update.ts @@ -0,0 +1,65 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + codeBlock, + ComponentType, + SlashCommandBuilder, +} from "discord.js"; +import { defineCommand } from ".."; +import { restart } from "../../utils/restart"; +import { shell } from "../../utils/functions"; + +export default defineCommand({ + data: new SlashCommandBuilder() + .setName("update") + .setDescription("Updates the bot"), + isOwnerOnly: true, + + async execute(interaction) { + const response = await interaction.deferReply({ withResponse: true }); + const result = await shell`git pull`; + const output = result.stdout + result.stderr; + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("restart") + .setLabel("Restart") + .setStyle(ButtonStyle.Success) + ); + + const isUpToDate = output.trim() === "Already up to date."; + + await interaction.editReply({ + components: isUpToDate || result.failed ? [] : [row], + embeds: [ + { + description: codeBlock(output), + color: isUpToDate ? 0x00ff00 : 0xff0000, + }, + ], + }); + + if (!isUpToDate && !result.failed) { + response.resource?.message + ?.awaitMessageComponent({ + componentType: ComponentType.Button, + time: 30000, + filter: (i) => i.user.id === interaction.user.id, + dispose: true, + }) + .then(async (interaction) => { + await interaction.update({ + components: [], + }); + await interaction.message.react("🔄"); + await restart({ + message: { + id: interaction.message.id, + channelId: interaction.message.channelId, + }, + }); + }); + } + }, +}); diff --git a/src/commands/utility/ocr.ts b/src/commands/utility/ocr.ts deleted file mode 100644 index aea330e..0000000 --- a/src/commands/utility/ocr.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { codeBlock, inlineCode, SlashCommandBuilder } from "discord.js"; -import { defineCommand } from ".."; -import { downloadFile } from "../../utils/http"; -import { abort } from "../../utils/error"; -import { yandexOcr } from "../../utils/ocr"; -import sharp from "sharp"; - -export default defineCommand({ - data: new SlashCommandBuilder() - .setName("ocr") - .setDescription("OCR an image using Yandex") - .addAttachmentOption((option) => - option - .setName("image") - .setDescription("The image to OCR") - .setRequired(true) - ), - - async execute(interaction) { - const attachment = interaction.options.getAttachment("image", true); - if (!attachment.contentType?.startsWith("image/")) { - abort("The file must be an image!"); - } - - await interaction.deferReply(); - - const { data, type } = await downloadFile(attachment.url); - if (!type?.mime.startsWith("image/")) { - abort("The file must be an image!"); - } - - const compressed = await sharp(data) - .resize(1000) - .jpeg({ quality: 90 }) - .toBuffer(); - - const { text, detected_lang } = await yandexOcr(compressed, type.mime); - - const languageName = - new Intl.DisplayNames(["en"], { type: "language" }).of(detected_lang) ?? - "unknown"; - - const content = `Detected language: ${inlineCode( - languageName - )}\n${codeBlock(text)}`; - - if (content.length > 2000) { - await interaction.editReply({ - content: `Detected language: ${inlineCode(languageName)}`, - files: [ - { - name: "ocr.txt", - attachment: text, - }, - attachment, - ], - }); - return; - } - - await interaction.editReply({ - content, - files: [attachment], - }); - }, -}); diff --git a/src/env.ts b/src/env.ts index 3867ff5..3b66f12 100644 --- a/src/env.ts +++ b/src/env.ts @@ -10,6 +10,7 @@ const envSchema = z.object({ .default("development"), DEV_GUILD_ID: z.string(), DEV_CHANNEL_ID: z.string(), + DEEPL_API_KEY: z.string(), }); export const env = envSchema.parse(process.env); diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 5a04466..7b4ad4a 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,15 +1,24 @@ import { AutocompleteInteraction, ChatInputCommandInteraction, + ContextMenuCommandBuilder, Events, inlineCode, + MessageContextMenuCommandInteraction, MessageFlags, + SlashCommandBuilder, + UserContextMenuCommandInteraction, } from "discord.js"; import { client } from "../client"; import { log } from "../utils/logger"; import { defineEvent } from "."; -import { isExplicitCommandError, notifyError } from "../utils/error"; +import { + isCombinedError, + isExplicitCommandError, + notifyError, +} from "../utils/error"; import { nanoid } from "../utils/functions"; +import type { Command } from "../types/command"; const running = new Map(); const getRunning = (command: string) => running.get(command) ?? 0; @@ -18,7 +27,10 @@ export default defineEvent({ name: Events.InteractionCreate, async execute(interaction) { - if (interaction.isChatInputCommand()) { + if ( + interaction.isChatInputCommand() || + interaction.isContextMenuCommand() + ) { await handleChatInputCommand(interaction); } else if (interaction.isAutocomplete()) { await handleAutocomplete(interaction); @@ -27,9 +39,14 @@ export default defineEvent({ }); async function handleChatInputCommand( - interaction: ChatInputCommandInteraction + interaction: + | ChatInputCommandInteraction + | MessageContextMenuCommandInteraction + | UserContextMenuCommandInteraction ) { - const command = client.commands.get(interaction.commandName); + const command = client.commands.get(interaction.commandName) as Command< + SlashCommandBuilder | ContextMenuCommandBuilder + >; if (!command) return; if (command.isOwnerOnly && interaction.user.id !== client.ownerId) { @@ -61,7 +78,13 @@ async function handleChatInputCommand( if (!isExplicitCommandError(err)) { const trace = nanoid(); content += `\ntrace: ${inlineCode(trace)}`; - log.error("Unhandled Command Error", { trace, err }); + if (isCombinedError(err)) { + err.errors.forEach((error) => { + log.error("Unhandled Command Error", { trace, err: error }); + }); + } else { + log.error("Unhandled Command Error", { trace, err }); + } notifyError(trace, err); } diff --git a/src/scripts/test.ts b/src/scripts/test.ts index 69abd8e..58118c3 100644 --- a/src/scripts/test.ts +++ b/src/scripts/test.ts @@ -1,7 +1,8 @@ -import { getDefinitions } from "../utils/wiktionary"; +import { ArtemisClient } from "../client"; +import { env } from "../env"; -const definitions = await getDefinitions("appl"); - -if (!definitions) process.exit(1); - -console.dir(definitions, { depth: null }); +const client = new ArtemisClient(); +await client.api.applicationCommands.bulkOverwriteGlobalCommands( + env.APPLICATION_ID, + [] +); diff --git a/src/types/command.ts b/src/types/command.ts index be5650c..a6b6a0c 100644 --- a/src/types/command.ts +++ b/src/types/command.ts @@ -1,13 +1,29 @@ import type { AutocompleteInteraction, ChatInputCommandInteraction, + ContextMenuCommandBuilder, + MessageContextMenuCommandInteraction, SlashCommandBuilder, SlashCommandOptionsOnlyBuilder, + UserContextMenuCommandInteraction, } from "discord.js"; -export interface Command { - data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder; - execute(interaction: ChatInputCommandInteraction): Promise; +export type CommandBuilder = + | SlashCommandBuilder + | SlashCommandOptionsOnlyBuilder + | ContextMenuCommandBuilder; + +type InferInteraction = B extends + | SlashCommandBuilder + | SlashCommandOptionsOnlyBuilder + ? ChatInputCommandInteraction + : B extends ContextMenuCommandBuilder + ? MessageContextMenuCommandInteraction | UserContextMenuCommandInteraction + : never; + +export interface Command { + data: B; + execute(interaction: InferInteraction): Promise; autocomplete?(interaction: AutocompleteInteraction): Promise; category?: string; maxConcurrency?: number; diff --git a/src/utils/components.ts b/src/utils/components.ts index cdb39ce..81cb278 100644 --- a/src/utils/components.ts +++ b/src/utils/components.ts @@ -43,6 +43,6 @@ export async function confirmPrompt( }); }); - msg.delete(); + interaction.deleteReply(); return confirmation?.customId === "confirm"; } diff --git a/src/utils/deepl.ts b/src/utils/deepl.ts new file mode 100644 index 0000000..73c988b --- /dev/null +++ b/src/utils/deepl.ts @@ -0,0 +1,57 @@ +import { + Translator, + type SourceLanguageCode, + type TargetLanguageCode, +} from "deepl-node"; +import { env } from "../env"; +import { lazy } from "./functions"; + +const translator = new Translator(env.DEEPL_API_KEY); +export const getSourceLanguages = lazy(() => translator.getSourceLanguages()); +export const getTargetLanguages = lazy(() => translator.getTargetLanguages()); + +type TranslateOptions = { + text: string; + source?: string | null; + target?: string; +}; + +export async function translate({ + text, + source = null, + target = "en-US", +}: TranslateOptions) { + return translator.translateText( + text, + source as SourceLanguageCode, + target as TargetLanguageCode + ); +} + +export async function getUsage() { + return translator.getUsage(); +} + +export async function getLanguages() { + return (await getSourceLanguages()).concat(await getTargetLanguages()); +} + +export async function languageCodeToName(code: string) { + return (await getLanguages()).find((l) => l.code === code)?.name; +} + +export async function isSourceLanguageSupported(code: string) { + return ( + (await getSourceLanguages()).find( + (l) => l.code.toLowerCase() === code.toLowerCase() + ) !== undefined + ); +} + +export async function isTargetLanguageSupported(code: string) { + return ( + (await getTargetLanguages()).find( + (l) => l.code.toLowerCase() === code.toLowerCase() + ) !== undefined + ); +} diff --git a/src/utils/error.ts b/src/utils/error.ts index bf60009..fd7e7ae 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -14,6 +14,10 @@ export function isExplicitCommandError( return error instanceof ExplicitCommandError; } +export function isCombinedError(error: any): error is { errors: any[] } { + return typeof error === "object" && "errors" in error; +} + export async function notifyError(trace: string, error: any) { return (client.channels.cache.get(env.DEV_CHANNEL_ID) as TextChannel).send({ content: trace, diff --git a/src/utils/functions.ts b/src/utils/functions.ts index 692c634..ea2fda2 100644 --- a/src/utils/functions.ts +++ b/src/utils/functions.ts @@ -1,7 +1,9 @@ import * as cheerio from "cheerio"; +import { execa } from "execa"; import { customAlphabet } from "nanoid"; export const nanoid = customAlphabet("1234567890abcdef"); +export const shell = execa({ reject: false }); export function noop() {} @@ -51,3 +53,9 @@ export function dedent(parts: TemplateStringsArray, ...values: unknown[]) { .join("") .replace(/(\n)\s+/g, "$1"); } + +export function lazy(cb: () => T) { + let defaultValue: T; + + return () => (defaultValue ??= cb()); +} diff --git a/src/utils/restart.ts b/src/utils/restart.ts index bf1d0e5..87947c7 100644 --- a/src/utils/restart.ts +++ b/src/utils/restart.ts @@ -1,34 +1,60 @@ -import { Client, InteractionWebhook, MessageFlags } from "discord.js"; +import { + Client, + InteractionWebhook, + MessageFlags, + TextChannel, +} from "discord.js"; import { silently } from "./functions"; import { client } from "../client"; import { env } from "../env"; import { readFile, rm, writeFile } from "node:fs/promises"; import { log } from "./logger"; -export async function restart(state: string) { +type RestartState = { + token?: string; + message?: { + id: string; + channelId: string; + }; +}; + +export async function restart(state: RestartState) { log.info("Shutting down..."); - await writeFile("./data/temp/restart", state); + await writeFile("./data/temp/restart", JSON.stringify(state)); await client.destroy(); process.exit(0); } export async function maybeSendRestarted() { - const restartToken = await silently(readFile("./data/temp/restart", "utf-8")); + const content = await silently(readFile("./data/temp/restart", "utf-8")); + if (!content) return; + const state = JSON.parse(content) as RestartState; - if (restartToken) { + if (state.token) { const webhook = new InteractionWebhook( client as Client, env.APPLICATION_ID, - restartToken + state.token ); - await webhook.send({ - content: "Successfully restarted!", - flags: MessageFlags.Ephemeral, - }); + await silently( + webhook.send({ + content: "Successfully restarted!", + flags: MessageFlags.Ephemeral, + }) + ); + } else if (state.message) { + const channel = client.channels.cache.get( + state.message.channelId + ) as TextChannel; + if (!channel) return; - await silently(rm("./data/temp/restart")); + await silently( + channel.messages.fetch(state.message.id).then((msg) => msg.react("☑️")) + ); } + + await silently(rm("./data/temp/restart")); }