diff --git a/package.json b/package.json index 76d9324..d446f83 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "dist/index.js", "scripts": { "start": "pnpm build && node .", + "watch": "pnpm build watch", "build": "node build.mjs", "test": "echo \"Error: no test specified\" && exit 1" }, @@ -12,12 +13,14 @@ "author": "", "license": "GPL-3.0-only", "dependencies": { - "@discordjs/voice": "^0.18.0", + "@discordjs/voice": "^0.17.0", "dotenv": "^16.4.7", + "libsodium-wrappers": "^0.7.15", "oceanic.js": "^1.11.2" }, "packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a", "devDependencies": { + "@eslint/js": "^9.18.0", "@types/node": "^22.10.5", "esbuild": "0.24.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf5a7ed..bb5848a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,15 +9,21 @@ importers: .: dependencies: '@discordjs/voice': - specifier: ^0.18.0 - version: 0.18.0 + specifier: ^0.17.0 + version: 0.17.0 dotenv: specifier: ^16.4.7 version: 16.4.7 + libsodium-wrappers: + specifier: ^0.7.15 + version: 0.7.15 oceanic.js: specifier: ^1.11.2 version: 1.11.2 devDependencies: + '@eslint/js': + specifier: ^9.18.0 + version: 9.18.0 '@types/node': specifier: ^22.10.5 version: 22.10.5 @@ -32,10 +38,6 @@ packages: engines: {node: '>=16.11.0'} deprecated: This version uses deprecated encryption modes. Please use a newer version. - '@discordjs/voice@0.18.0': - resolution: {integrity: sha512-BvX6+VJE5/vhD9azV9vrZEt9hL1G+GlOdsQaVl5iv9n87fkXjf3cSwllhR3GdaUC8m6dqT8umXIWtn3yCu4afg==} - engines: {node: '>=18'} - '@esbuild/aix-ppc64@0.24.2': resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} engines: {node: '>=18'} @@ -186,15 +188,16 @@ packages: cpu: [x64] os: [win32] + '@eslint/js@9.18.0': + resolution: {integrity: sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@types/node@22.10.5': resolution: {integrity: sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==} '@types/ws@8.5.13': resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} - discord-api-types@0.37.115: - resolution: {integrity: sha512-ivPnJotSMrXW8HLjFu+0iCVs8zP6KSliMelhr7HgcB2ki1QzpORkb26m71l1pzSnnGfm7gb5n/VtRTtpw8kXFA==} - discord-api-types@0.37.83: resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==} @@ -207,6 +210,12 @@ packages: engines: {node: '>=18'} hasBin: true + libsodium-wrappers@0.7.15: + resolution: {integrity: sha512-E4anqJQwcfiC6+Yrl01C1m8p99wEhLmJSs0VQqST66SbQXXBoaJY0pF4BNjRYa/sOQAxx6lXAaAFIlx+15tXJQ==} + + libsodium@0.7.15: + resolution: {integrity: sha512-sZwRknt/tUpE2AwzHq3jEyUU5uvIZHtSssktXq7owd++3CSgn8RGrv6UZJJBpP7+iBghBqe7Z06/2M31rI2NKw==} + oceanic.js@1.11.2: resolution: {integrity: sha512-kXMoZiIrIFq0QCfsZGJ+eOK+1IFkVsTpvwcQ3nfY6ioDKGVdHQjVIXNJAYvJb5c5la//2OU8WGpqsmp/bAP8aw==} engines: {node: '>=18.13.0'} @@ -262,22 +271,6 @@ snapshots: - node-opus - opusscript - utf-8-validate - optional: true - - '@discordjs/voice@0.18.0': - dependencies: - '@types/ws': 8.5.13 - discord-api-types: 0.37.115 - prism-media: 1.3.5 - tslib: 2.8.1 - ws: 8.18.0 - transitivePeerDependencies: - - '@discordjs/opus' - - bufferutil - - ffmpeg-static - - node-opus - - opusscript - - utf-8-validate '@esbuild/aix-ppc64@0.24.2': optional: true @@ -354,6 +347,8 @@ snapshots: '@esbuild/win32-x64@0.24.2': optional: true + '@eslint/js@9.18.0': {} + '@types/node@22.10.5': dependencies: undici-types: 6.20.0 @@ -362,10 +357,7 @@ snapshots: dependencies: '@types/node': 22.10.5 - discord-api-types@0.37.115: {} - - discord-api-types@0.37.83: - optional: true + discord-api-types@0.37.83: {} dotenv@16.4.7: {} @@ -397,6 +389,12 @@ snapshots: '@esbuild/win32-ia32': 0.24.2 '@esbuild/win32-x64': 0.24.2 + libsodium-wrappers@0.7.15: + dependencies: + libsodium: 0.7.15 + + libsodium@0.7.15: {} + oceanic.js@1.11.2: dependencies: tslib: 2.8.1 diff --git a/src/commands/help.ts b/src/commands/help.ts new file mode 100644 index 0000000..16ba605 --- /dev/null +++ b/src/commands/help.ts @@ -0,0 +1,29 @@ +import commands, { Command } from "."; +import { filterAsync } from "../util"; + +const cmdHelp: Command = { + names: ["help", "cmds"], + desc: "list out commands you have access to", + checkPerm: async () => true, + async handler(msg, { reply }) { + const availCmds = await filterAsync( + commands, + async (cmd) => await cmd.checkPerm(msg) + ); + + let text = `// List of commands (${availCmds.length} count)\n`; + + for (let cmd of availCmds) { + text += `${cmd.names[0]} `; + if (cmd.names.length > 1) { + text += `(aka: ${cmd.names.slice(1).join(", ")}) `; + } + text += `- ${cmd.desc}\n`; + } + + text = "```\n" + text + "\n```"; + await reply(text); + }, +}; + +export default cmdHelp; diff --git a/src/commands/hi.ts b/src/commands/hi.ts index 916ca10..c872311 100644 --- a/src/commands/hi.ts +++ b/src/commands/hi.ts @@ -1,19 +1,15 @@ import { Message } from "oceanic.js"; import { Command } from "."; -const hi: Command = { - name: "hi", +const cmdHi: Command = { + names: ["hi", "hello"], desc: "test command", - async checkPerm(msg: Message): Promise { - if (msg.member === undefined) return false; - - return msg.member.roles.includes("1327065762031992924"); + async checkPerm(msg: Message) { + return msg.member?.roles.includes("1327065762031992924"); }, - async handler(msg: Message) { - msg.channel?.createMessage({ - content: "Test message", - }); + async handler(msg: Message, { say }) { + await say("Test message"); }, }; -export default hi; +export default cmdHi; diff --git a/src/commands/index.ts b/src/commands/index.ts index 45cb943..0db6f6d 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,13 +1,21 @@ import { Message } from "oceanic.js"; -import hi from "./hi"; +import cmdHelp from "./help"; +import cmdHi from "./hi"; +import cmdPlay from "./play"; export type Command = { - name: string; - desc?: string; - checkPerm(msg: Message): Promise; - handler(msg: Message): Promise; + names: string[]; + desc: string; + checkPerm(msg: Message): Promise; + handler( + msg: Message, + replies: { + reply: (text: string) => Promise; + say: (text: string) => Promise; + } + ): Promise; }; -const commands: Command[] = [hi]; +const commands: Command[] = [cmdHelp, cmdHi, cmdPlay]; export default commands; diff --git a/src/commands/play.ts b/src/commands/play.ts new file mode 100644 index 0000000..4ba172a --- /dev/null +++ b/src/commands/play.ts @@ -0,0 +1,63 @@ +import { + AudioPlayerStatus, + createAudioPlayer, + createAudioResource, + VoiceConnectionStatus, +} from "@discordjs/voice"; +import { Command } from "."; +import { existsSync } from "node:fs"; + +const cmdPlay: Command = { + names: ["play", "soundtest"], + desc: "test voice features", + async checkPerm(msg) { + return msg.author.id === "778441081883983893"; + }, + async handler(msg, { reply, say }) { + const voiceState = msg.member?.voiceState; + if (!voiceState?.channelID) + return await reply("You are not in a voice channel"); + + if (msg.guildID !== null && msg.client.getVoiceConnection(msg.guildID)) { + return await reply("I am already connected"); + } + + const conn = voiceState.channel?.join({ + selfMute: false, + }); + if (!conn) return await reply("Failed to join channel"); + + conn.on(VoiceConnectionStatus.Disconnected, () => { + console.log("Rejoining from disconnection!!"); + conn.rejoin(); + }); + + const player = createAudioPlayer(); + conn.subscribe(player); + + conn.on("stateChange", (oldSt, newSt) => { + console.log(`Moving from ${oldSt.status} to ${newSt.status}`); + }); + conn.on("error", (err) => { + console.error("Connection experienced error!", err); + }); + + player.once(AudioPlayerStatus.Playing, async () => { + await say("Playing audio"); + }); + player.on(AudioPlayerStatus.Idle, async () => { + await say("Audio finished playing"); + }); + + const name = msg.content.match(/\s(.*)$/)?.[1]; + if (!name || !existsSync(name)) { + return await reply("Audio file not found"); + } + console.log(name); + + const audio = createAudioResource(name); + player.play(audio); + }, +}; + +export default cmdPlay; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..5949fdf --- /dev/null +++ b/src/constants.ts @@ -0,0 +1 @@ +export const PREFIX = "!!"; diff --git a/src/index.ts b/src/index.ts index c652c3e..1ba9501 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,20 @@ import { Client } from "oceanic.js"; import "dotenv/config"; import commands from "./commands"; +import { PREFIX } from "./constants"; -const token = process.env.DISCORD_TOKEN; -if (!token) { - console.error("No `DISCORD_TOKEN` was specified!!"); - process.exit(1); +function token() { + return ( + process.env.DISCORD_TOKEN ?? + (() => { + console.error("No `DISCORD_TOKEN` was specified!!"); + process.exit(1); + })() + ); } const client = new Client({ - auth: `Bot ${token}`, + auth: `Bot ${token()}`, gateway: { intents: ["ALL"], }, @@ -21,25 +26,37 @@ client.on("ready", async () => { console.log("Ready as", client.user.tag); }); -const PREFIX = "!!"; - client.on("messageCreate", async (msg) => { if (msg.author.bot) return; - let first = msg.content.split(/\s/).shift(); - if (first?.startsWith(PREFIX) === false) return; - let cmdName = first!.slice(PREFIX.length); + const content = msg.content.toLowerCase().trim(); + if (!content.startsWith(PREFIX)) return; - let command = commands.find((c) => c.name === cmdName); + const cmdName = content.split(/\s/)[0]?.slice(PREFIX.length) ?? ""; + const cmd = commands.find((cmd) => cmd.names.includes(cmdName)); - if (command === undefined) return; + if (cmd === undefined) return; - if ((await command.checkPerm(msg)) === false) { - await msg.createReaction("❌"); - return; - } + if (await cmd.checkPerm(msg)) { + const reply = async (text: string) => { + await msg.channel?.createMessage({ + content: text, + allowedMentions: { + repliedUser: false, + }, + messageReference: { + messageID: msg.id, + }, + }); + }; + const say = async (text: string) => { + await msg.channel?.createMessage({ + content: text, + }); + }; - await command?.handler(msg); + await cmd?.handler(msg, { reply, say }); + } else await msg.createReaction("❌"); }); client.on("error", (err) => { diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..3ec891f --- /dev/null +++ b/src/util.ts @@ -0,0 +1,7 @@ +export async function filterAsync( + arr: T[], + predicate: (v) => Promise +): Promise { + const results = await Promise.all(arr.map(predicate)); + return arr.filter((_v, i) => results[i]); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c77a0e7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "ESNext", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + "module": "NodeNext", + "outDir": "dist", + "sourceMap": true, + + "lib": ["ESNext"] + } +}