Discord bot to open dong files

Compare changes

Choose any two refs to compare.

+2 -1
.env.template
···
-
TOKEN="<Your Discord Bot Token>"
+
TOKEN="<Your Discord Bot Token>"
+
CLIENT="app id"
+7
.gitignore
···
# Finder (MacOS) folder config
.DS_Store
+
lib/index.js
+
lib/sync.js
+
lib/commands/dong/create.js
+
lib/commands/dong/open.js
+
lib/commands/util/ping.js
+
lib/lib/dong-io.js
+
lib/lib/download.js
+3
.vscode/settings.json
···
+
{
+
"deno.enable": true
+
}
+7
Dockerfile
···
+
FROM denoland/deno:latest
+
WORKDIR /app
+
+
COPY ./ /app
+
RUN deno install
+
+
CMD deno --env-file --allow-env --allow-read=./ --allow-net=discord.com,gateway.discord.gg,cdn.discordapp.com src/index.ts
-15
README.md
···
-
# discord
-
-
To install dependencies:
-
-
```bash
-
bun install
-
```
-
-
To run:
-
-
```bash
-
bun run index.ts
-
```
-
-
This project was created using `bun init` in bun v1.2.4. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
-74
bun.lock
···
-
{
-
"lockfileVersion": 1,
-
"workspaces": {
-
"": {
-
"name": "discord",
-
"dependencies": {
-
"discord.js": "^14.18.0",
-
},
-
"devDependencies": {
-
"@types/bun": "latest",
-
},
-
"peerDependencies": {
-
"typescript": "^5",
-
},
-
},
-
},
-
"packages": {
-
"@discordjs/builders": ["@discordjs/builders@1.10.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.0", "@discordjs/util": "^1.1.1", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.37.119", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-OWo1fY4ztL1/M/DUyRPShB4d/EzVfuUvPTRRHRIt/YxBrUYSz0a+JicD5F5zHFoNs2oTuWavxCOVFV1UljHTng=="],
-
-
"@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="],
-
-
"@discordjs/formatters": ["@discordjs/formatters@0.6.0", "", { "dependencies": { "discord-api-types": "^0.37.114" } }, "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw=="],
-
-
"@discordjs/rest": ["@discordjs/rest@2.4.3", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.37.119", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.1" } }, "sha512-+SO4RKvWsM+y8uFHgYQrcTl/3+cY02uQOH7/7bKbVZsTfrfpoE62o5p+mmV+s7FVhTX82/kQUGGbu4YlV60RtA=="],
-
-
"@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="],
-
-
"@discordjs/ws": ["@discordjs/ws@1.2.1", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.4.3", "@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.119", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-PBvenhZG56a6tMWF/f4P6f4GxZKJTBG95n7aiGSPTnodmz4N5g60t79rSIAq7ywMbv8A4jFtexMruH+oe51aQQ=="],
-
-
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
-
-
"@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
-
-
"@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
-
-
"@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="],
-
-
"@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
-
-
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
-
-
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="],
-
-
"bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="],
-
-
"discord-api-types": ["discord-api-types@0.37.119", "", {}, "sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg=="],
-
-
"discord.js": ["discord.js@14.18.0", "", { "dependencies": { "@discordjs/builders": "^1.10.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.0", "@discordjs/rest": "^2.4.3", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.1", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.37.119", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "tslib": "^2.6.3", "undici": "6.21.1" } }, "sha512-SvU5kVUvwunQhN2/+0t55QW/1EHfB1lp0TtLZUSXVHDmyHTrdOj5LRKdR0zLcybaA15F+NtdWuWmGOX9lE+CAw=="],
-
-
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
-
-
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
-
-
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
-
-
"magic-bytes.js": ["magic-bytes.js@1.10.0", "", {}, "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ=="],
-
-
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
-
-
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
-
-
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
-
-
"undici": ["undici@6.21.1", "", {}, "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ=="],
-
-
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
-
-
"ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="],
-
-
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
-
-
"@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
-
}
-
}
+12
deno.json
···
+
{
+
"tasks": {
+
"dev": "deno run --watch --env-file --allow-env --allow-read=./ --allow-net=discord.com,gateway.discord.gg,cdn.discordapp.com src/index.ts",
+
"main": "deno run --env-file --allow-env --allow-read=./ --allow-net=discord.com,gateway.discord.gg,cdn.discordapp.com src/index.ts",
+
"sync": "deno run --env-file --allow-env --allow-read=./src --allow-net=discord.com src/sync.js"
+
},
+
"imports": {
+
"@std/assert": "jsr:@std/assert@1",
+
"discord.js": "npm:discord.js@^14.25.1",
+
"mime": "npm:mime@^4.1.0"
+
}
+
}
+167
deno.lock
···
+
{
+
"version": "5",
+
"specifiers": {
+
"jsr:@std/assert@1": "1.0.16",
+
"jsr:@std/internal@^1.0.12": "1.0.12",
+
"npm:discord.js@^14.25.1": "14.25.1",
+
"npm:mime@^4.1.0": "4.1.0"
+
},
+
"jsr": {
+
"@std/assert@1.0.16": {
+
"integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532",
+
"dependencies": [
+
"jsr:@std/internal"
+
]
+
},
+
"@std/internal@1.0.12": {
+
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
+
}
+
},
+
"npm": {
+
"@discordjs/builders@1.13.1": {
+
"integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==",
+
"dependencies": [
+
"@discordjs/formatters",
+
"@discordjs/util",
+
"@sapphire/shapeshift",
+
"discord-api-types",
+
"fast-deep-equal",
+
"ts-mixer",
+
"tslib"
+
]
+
},
+
"@discordjs/collection@1.5.3": {
+
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="
+
},
+
"@discordjs/collection@2.1.1": {
+
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="
+
},
+
"@discordjs/formatters@0.6.2": {
+
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
+
"dependencies": [
+
"discord-api-types"
+
]
+
},
+
"@discordjs/rest@2.6.0": {
+
"integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==",
+
"dependencies": [
+
"@discordjs/collection@2.1.1",
+
"@discordjs/util",
+
"@sapphire/async-queue",
+
"@sapphire/snowflake",
+
"@vladfrangu/async_event_emitter",
+
"discord-api-types",
+
"magic-bytes.js",
+
"tslib",
+
"undici"
+
]
+
},
+
"@discordjs/util@1.2.0": {
+
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
+
"dependencies": [
+
"discord-api-types"
+
]
+
},
+
"@discordjs/ws@1.2.3": {
+
"integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
+
"dependencies": [
+
"@discordjs/collection@2.1.1",
+
"@discordjs/rest",
+
"@discordjs/util",
+
"@sapphire/async-queue",
+
"@types/ws",
+
"@vladfrangu/async_event_emitter",
+
"discord-api-types",
+
"tslib",
+
"ws"
+
]
+
},
+
"@sapphire/async-queue@1.5.5": {
+
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="
+
},
+
"@sapphire/shapeshift@4.0.0": {
+
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
+
"dependencies": [
+
"fast-deep-equal",
+
"lodash"
+
]
+
},
+
"@sapphire/snowflake@3.5.3": {
+
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="
+
},
+
"@types/node@22.15.15": {
+
"integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==",
+
"dependencies": [
+
"undici-types"
+
]
+
},
+
"@types/ws@8.18.1": {
+
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+
"dependencies": [
+
"@types/node"
+
]
+
},
+
"@vladfrangu/async_event_emitter@2.4.7": {
+
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="
+
},
+
"discord-api-types@0.38.36": {
+
"integrity": "sha512-qrbUbjjwtyeBg5HsAlm1C859epfOyiLjPqAOzkdWlCNsZCWJrertnETF/NwM8H+waMFU58xGSc5eXUfXah+WTQ=="
+
},
+
"discord.js@14.25.1": {
+
"integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==",
+
"dependencies": [
+
"@discordjs/builders",
+
"@discordjs/collection@1.5.3",
+
"@discordjs/formatters",
+
"@discordjs/rest",
+
"@discordjs/util",
+
"@discordjs/ws",
+
"@sapphire/snowflake",
+
"discord-api-types",
+
"fast-deep-equal",
+
"lodash.snakecase",
+
"magic-bytes.js",
+
"tslib",
+
"undici"
+
]
+
},
+
"fast-deep-equal@3.1.3": {
+
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+
},
+
"lodash.snakecase@4.1.1": {
+
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="
+
},
+
"lodash@4.17.21": {
+
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+
},
+
"magic-bytes.js@1.12.1": {
+
"integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="
+
},
+
"mime@4.1.0": {
+
"integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==",
+
"bin": true
+
},
+
"ts-mixer@6.0.4": {
+
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="
+
},
+
"tslib@2.8.1": {
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
+
},
+
"undici-types@6.21.0": {
+
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
+
},
+
"undici@6.21.3": {
+
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="
+
},
+
"ws@8.18.3": {
+
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="
+
}
+
},
+
"workspace": {
+
"dependencies": [
+
"jsr:@std/assert@1",
+
"npm:discord.js@^14.25.1",
+
"npm:mime@^4.1.0"
+
]
+
}
+
}
-18
package.json
···
-
{
-
"name": "discord",
-
"module": "index.ts",
-
"type": "module",
-
"private": true,
-
"scripts": {
-
"dev": "bun --watch src/index.ts"
-
},
-
"devDependencies": {
-
"@types/bun": "latest"
-
},
-
"peerDependencies": {
-
"typescript": "^5"
-
},
-
"dependencies": {
-
"discord.js": "^14.18.0"
-
}
-
}
+68
src/commands/dong/create.ts
···
+
import {
+
AttachmentBuilder,
+
ChatInputCommandInteraction,
+
SlashCommandBuilder,
+
} from "discord.js";
+
import type { customClient } from "../../index.ts";
+
import { createDong } from "../../lib/dong-io.ts";
+
import { download } from "../../lib/download.ts";
+
import { Buffer } from "node:buffer";
+
+
export const data = new SlashCommandBuilder()
+
.setName("create")
+
.setDescription("Create a dong file!")
+
.addAttachmentOption((opt) =>
+
opt
+
.setName("image")
+
.setDescription("The image of the dong file")
+
.setRequired(true)
+
)
+
.addAttachmentOption((opt) =>
+
opt
+
.setName("audio")
+
.setDescription("The audio of the dong file")
+
.setRequired(true)
+
)
+
.addStringOption((opt) =>
+
opt
+
.setName("name")
+
.setDescription("Filename of the dong file")
+
.setRequired(true)
+
);
+
export const execute = async (
+
interaction: ChatInputCommandInteraction & { client: customClient }
+
) => {
+
const filename = interaction.options.getString("name", true) + ".dong";
+
const image = interaction.options.getAttachment("image", true);
+
const audio = interaction.options.getAttachment("audio", true);
+
+
if (!image.contentType?.startsWith("image/")) {
+
await interaction.reply("Image is not a valid image!");
+
return;
+
}
+
if (!audio.contentType?.startsWith("audio/")) {
+
await interaction.reply("Audio is not a valid audio!");
+
return;
+
}
+
+
await interaction.deferReply();
+
+
const downloaded = {
+
image: await download(image),
+
audio: await download(audio),
+
};
+
+
const dong = new File(
+
[await createDong(downloaded.image, downloaded.audio)],
+
filename,
+
{ type: "application/prs.vielle.dong" }
+
);
+
+
await interaction.editReply({
+
files: [
+
new AttachmentBuilder(Buffer.from(await dong.arrayBuffer()), {
+
name: dong.name,
+
}),
+
],
+
});
+
};
+57
src/commands/dong/open.ts
···
+
import {
+
AttachmentBuilder,
+
ChatInputCommandInteraction,
+
MessageFlags,
+
SlashCommandBuilder,
+
} from "discord.js";
+
import type { customClient } from "../../index.ts";
+
import { download } from "../../lib/download.ts";
+
import { readDong } from "../../lib/dong-io.ts";
+
import { Mime } from "mime";
+
import standardTypes from "mime/types/standard.js";
+
import otherTypes from "mime/types/other.js";
+
import { Buffer } from "node:buffer";
+
+
const mime = new Mime(standardTypes, otherTypes);
+
mime.define({ "audio/mpeg": ["mp3"] });
+
+
export const data = new SlashCommandBuilder()
+
.setName("open")
+
.setDescription("Open a dong file!")
+
.addAttachmentOption((opt) =>
+
opt.setName("dong").setDescription("The dong file").setRequired(true)
+
);
+
export const execute = async (
+
interaction: ChatInputCommandInteraction & { client: customClient }
+
) => {
+
const dong = interaction.options.getAttachment("dong", true);
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
+
+
const downloadedDong = await download(dong);
+
+
const output = await readDong(downloadedDong);
+
if (typeof output === "string") {
+
await interaction.editReply(output);
+
return;
+
}
+
const { image, audio } = output;
+
+
await interaction.editReply({
+
files: [
+
(() => {
+
const img = new AttachmentBuilder(Buffer.from(image.data.buffer), {
+
name: `${dong.name.match(/^.*(?=\.dong$)/gm)}.${mime.getExtension(
+
image.mime
+
)}`,
+
});
+
img.setSpoiler(true);
+
return img;
+
})(),
+
new AttachmentBuilder(Buffer.from(audio.data.buffer), {
+
name: `${dong.name.match(/^.*(?=\.dong$)/gm)}.${mime.getExtension(
+
audio.mime
+
)}`,
+
}),
+
],
+
});
+
};
+11
src/commands/util/ping.ts
···
+
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
+
import type { customClient } from "../../index.ts";
+
+
export const data = new SlashCommandBuilder()
+
.setName("ping")
+
.setDescription("Replies with pong!");
+
export const execute = async (
+
interaction: ChatInputCommandInteraction & { client: customClient }
+
) => {
+
await interaction.reply("Pong!");
+
};
+100 -1
src/index.ts
···
-
console.log("Hello via Bun!");
+
import {
+
ChatInputCommandInteraction,
+
Client,
+
Collection,
+
Events,
+
GatewayIntentBits,
+
MessageFlags,
+
type Interaction,
+
} from "discord.js";
+
import { glob } from "node:fs/promises";
+
+
const token = Deno.env.get("TOKEN");
+
if (!token) throw new Error("Token required. Please fill in TOKEN in .env");
+
console.log("Token Valid!");
+
+
const __dirname = import.meta.dirname;
+
+
// client typing
+
export type customClient = Client & {
+
// add command typing
+
commands: Collection<
+
string,
+
{
+
data: { name: string };
+
execute: (
+
interaction: ChatInputCommandInteraction & { client: customClient }
+
) => Promise<void>;
+
}
+
>;
+
};
+
// new client
+
const client: customClient = new Client({
+
intents: [GatewayIntentBits.Guilds],
+
// as customclient as i cannot add .commands till this is done
+
}) as customClient;
+
+
// setup commands
+
client.commands = new Collection();
+
for await (const file of glob("src/commands/**/*.ts", {
+
exclude: ["node_modules"],
+
})) {
+
console.log(file);
+
const command = await import(`../${file}`);
+
// check command contains all required properties
+
if (
+
"data" in command &&
+
"execute" in command &&
+
typeof command.data === "object" &&
+
command.data !== null &&
+
"name" in command.data &&
+
typeof command.data.name === "string"
+
) {
+
client.commands.set(command.data.name, command);
+
console.log("[loaded]", file);
+
} else {
+
// log missing features
+
console.log(`[WARNING] ${file} is not a valid command!`, command);
+
}
+
}
+
+
// when client is ready do this once
+
client.once(Events.ClientReady, (ready) => {
+
console.log(`Ready! Logged in as ${ready.user.tag}`);
+
});
+
+
// _interaction so we can cast types properly
+
// we need access to client.commands
+
// we could just do it as a global but this makes it go wherever the command goes soooo
+
client.on(Events.InteractionCreate, async (_interaction) => {
+
const interaction = _interaction as Interaction & { client: customClient };
+
if (!interaction.isChatInputCommand()) return;
+
const command = interaction.client.commands.get(interaction.commandName);
+
if (!command) {
+
console.error(`No command ${interaction.commandName}`);
+
return;
+
}
+
+
console.log(
+
`Got command /${interaction.commandName} from @${interaction.user.username}`
+
);
+
+
try {
+
await command.execute(interaction);
+
} catch (e) {
+
console.error(e);
+
if (interaction.replied || interaction.deferred) {
+
await interaction.followUp({
+
content: "There was an error while executing this command!",
+
flags: MessageFlags.Ephemeral,
+
});
+
} else {
+
await interaction.reply({
+
content: "There was an error while executing this command!",
+
flags: MessageFlags.Ephemeral,
+
});
+
}
+
}
+
});
+
+
client.login(token);
+109
src/lib/dong-io.ts
···
+
declare global {
+
interface Uint8Array {
+
toBase64(): string;
+
}
+
}
+
+
const blobBytes = (blob: Blob) => {
+
if ("bytes" in blob) return blob.bytes();
+
return new Response(blob).arrayBuffer().then((buffer) => {
+
const uint = new Uint8Array(buffer);
+
return uint;
+
});
+
};
+
+
export const createDong = async (
+
image: File,
+
audio: File
+
): Promise<Blob | string> => {
+
if (
+
image.type === "" ||
+
!image.type.startsWith("image/") ||
+
audio.type === "" ||
+
!audio.type.startsWith("audio/")
+
)
+
return "Mime types invalid";
+
return new Blob([
+
// version
+
(() => {
+
const version = new Int8Array(new ArrayBuffer(2));
+
version[0] = 0xd0;
+
version[1] = 2;
+
return version;
+
})(),
+
// image type
+
image.type,
+
// 00 padding
+
new ArrayBuffer(256 - image.type.length),
+
// image size
+
(() => {
+
const value = new Uint32Array(1);
+
value[0] = image.size;
+
return value;
+
})(),
+
// audio type
+
audio.type,
+
// 00 padding
+
new ArrayBuffer(256 - audio.type.length),
+
// audio size
+
(() => {
+
const value = new Uint32Array(1);
+
value[0] = audio.size;
+
return value;
+
})(),
+
// image data
+
await blobBytes(image),
+
// audio data
+
await blobBytes(audio),
+
]);
+
};
+
+
export async function readDong(dongFile: File): Promise<
+
| {
+
image: { data: Uint8Array; mime: string };
+
audio: { data: Uint8Array; mime: string };
+
}
+
| string
+
> {
+
// get first 2 bytes and verify
+
const version = new Uint8Array(await blobBytes(dongFile.slice(0, 2)));
+
if (version[0] !== 0xd0 || version[1] !== 2) return "Invalid file";
+
+
// get next 256 bytes and get mime type
+
const imgMimeType: string | undefined = (
+
await dongFile.slice(2, 258).text()
+
).match(/[a-zA-Z0-9.]+\/[a-zA-Z0-9.]+/gm)?.[0];
+
if (!imgMimeType) return "Image mime type parse failed";
+
+
// get next 4 bytes and get image size
+
const imgSize = new Uint32Array(
+
(await blobBytes(dongFile.slice(258, 262))).buffer
+
)[0];
+
+
// get next 256 bytes and get mime type
+
const audMimeType: string | undefined = (
+
await dongFile.slice(262, 518).text()
+
).match(/[a-zA-Z0-9.]+\/[a-zA-Z0-9.]+/gm)?.[0];
+
if (!audMimeType) return "Audio mime type parse failed";
+
+
// get next 4 bytes and get image size
+
const audSize = new Uint32Array(
+
(await blobBytes(dongFile.slice(518, 522))).buffer
+
)[0];
+
+
const imageBytes = await blobBytes(dongFile.slice(522, 522 + imgSize));
+
const audioBytes = await blobBytes(
+
dongFile.slice(522 + imgSize, 522 + imgSize + audSize)
+
);
+
+
return {
+
image: {
+
mime: imgMimeType,
+
data: imageBytes,
+
},
+
audio: {
+
mime: audMimeType,
+
data: audioBytes,
+
},
+
};
+
}
+14
src/lib/download.ts
···
+
import type { Attachment } from "discord.js";
+
+
export const download = async (file: Attachment): Promise<File> =>
+
new File(
+
[
+
await fetch(file.url).then((res) => {
+
return res.blob();
+
}),
+
],
+
file.name,
+
{
+
type: file.contentType ?? "application/octet-stream",
+
}
+
);
+58
src/sync.js
···
+
import { REST, Routes } from "discord.js";
+
import fs from "node:fs";
+
import path from "node:path";
+
+
const clientId = Deno.env.get("CLIENT");
+
const token = Deno.env.get("TOKEN");
+
if (!clientId) throw "CLIENT not defined";
+
if (!token) throw "TOKEN not defined";
+
+
const commands = [];
+
// Grab all the command folders from the commands directory you created earlier
+
const foldersPath = path.join(import.meta.dirname, "commands");
+
const commandFolders = fs.readdirSync(foldersPath);
+
+
for (const folder of commandFolders) {
+
// Grab all the command files from the commands directory you created earlier
+
const commandsPath = path.join(foldersPath, folder);
+
const commandFiles = fs
+
.readdirSync(commandsPath)
+
.filter((file) => file.endsWith(".ts") || file.endsWith(".js"));
+
// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
+
for (const file of commandFiles) {
+
const filePath = path.join(commandsPath, file);
+
const command = await import(filePath);
+
if ("data" in command && "execute" in command) {
+
commands.push(command.data.toJSON());
+
} else {
+
console.log(
+
`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`
+
);
+
}
+
}
+
}
+
+
// Construct and prepare an instance of the REST module
+
const rest = new REST().setToken(token);
+
+
// and deploy your commands!
+
(async () => {
+
try {
+
console.log(
+
`Started refreshing ${commands.length} application (/) commands.`
+
);
+
+
// The put method is used to fully refresh all commands with the current set
+
const data = await rest.put(Routes.applicationCommands(clientId), {
+
body: commands,
+
});
+
+
console.log(
+
`Successfully reloaded ${data.length} application (/) commands.`
+
);
+
} catch (error) {
+
// And of course, make sure you catch and log any errors!
+
console.error(error);
+
process.stdout.write(JSON.stringify(error) + "\n");
+
}
+
})();
-27
tsconfig.json
···
-
{
-
"compilerOptions": {
-
// Enable latest features
-
"lib": ["ESNext", "DOM"],
-
"target": "ESNext",
-
"module": "ESNext",
-
"moduleDetection": "force",
-
"jsx": "react-jsx",
-
"allowJs": true,
-
-
// Bundler mode
-
"moduleResolution": "bundler",
-
"allowImportingTsExtensions": true,
-
"verbatimModuleSyntax": true,
-
"noEmit": true,
-
-
// Best practices
-
"strict": true,
-
"skipLibCheck": true,
-
"noFallthroughCasesInSwitch": true,
-
-
// Some stricter flags (disabled by default)
-
"noUnusedLocals": false,
-
"noUnusedParameters": false,
-
"noPropertyAccessFromIndexSignature": false
-
}
-
}