play from arbitrary files

This commit is contained in:
minish 2025-01-12 21:01:03 -05:00
parent 44c1e5ec70
commit b2cbe50d74
Signed by: min
GPG Key ID: FEECFF24EF0CE9E9
10 changed files with 207 additions and 64 deletions

View File

@ -5,6 +5,7 @@
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"start": "pnpm build && node .", "start": "pnpm build && node .",
"watch": "pnpm build watch",
"build": "node build.mjs", "build": "node build.mjs",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
@ -12,12 +13,14 @@
"author": "", "author": "",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"dependencies": { "dependencies": {
"@discordjs/voice": "^0.18.0", "@discordjs/voice": "^0.17.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"libsodium-wrappers": "^0.7.15",
"oceanic.js": "^1.11.2" "oceanic.js": "^1.11.2"
}, },
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a", "packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a",
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.18.0",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"esbuild": "0.24.2" "esbuild": "0.24.2"
} }

View File

@ -9,15 +9,21 @@ importers:
.: .:
dependencies: dependencies:
'@discordjs/voice': '@discordjs/voice':
specifier: ^0.18.0 specifier: ^0.17.0
version: 0.18.0 version: 0.17.0
dotenv: dotenv:
specifier: ^16.4.7 specifier: ^16.4.7
version: 16.4.7 version: 16.4.7
libsodium-wrappers:
specifier: ^0.7.15
version: 0.7.15
oceanic.js: oceanic.js:
specifier: ^1.11.2 specifier: ^1.11.2
version: 1.11.2 version: 1.11.2
devDependencies: devDependencies:
'@eslint/js':
specifier: ^9.18.0
version: 9.18.0
'@types/node': '@types/node':
specifier: ^22.10.5 specifier: ^22.10.5
version: 22.10.5 version: 22.10.5
@ -32,10 +38,6 @@ packages:
engines: {node: '>=16.11.0'} engines: {node: '>=16.11.0'}
deprecated: This version uses deprecated encryption modes. Please use a newer version. 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': '@esbuild/aix-ppc64@0.24.2':
resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -186,15 +188,16 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] 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': '@types/node@22.10.5':
resolution: {integrity: sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==} resolution: {integrity: sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==}
'@types/ws@8.5.13': '@types/ws@8.5.13':
resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} 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: discord-api-types@0.37.83:
resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==} resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==}
@ -207,6 +210,12 @@ packages:
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true 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: oceanic.js@1.11.2:
resolution: {integrity: sha512-kXMoZiIrIFq0QCfsZGJ+eOK+1IFkVsTpvwcQ3nfY6ioDKGVdHQjVIXNJAYvJb5c5la//2OU8WGpqsmp/bAP8aw==} resolution: {integrity: sha512-kXMoZiIrIFq0QCfsZGJ+eOK+1IFkVsTpvwcQ3nfY6ioDKGVdHQjVIXNJAYvJb5c5la//2OU8WGpqsmp/bAP8aw==}
engines: {node: '>=18.13.0'} engines: {node: '>=18.13.0'}
@ -262,22 +271,6 @@ snapshots:
- node-opus - node-opus
- opusscript - opusscript
- utf-8-validate - 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': '@esbuild/aix-ppc64@0.24.2':
optional: true optional: true
@ -354,6 +347,8 @@ snapshots:
'@esbuild/win32-x64@0.24.2': '@esbuild/win32-x64@0.24.2':
optional: true optional: true
'@eslint/js@9.18.0': {}
'@types/node@22.10.5': '@types/node@22.10.5':
dependencies: dependencies:
undici-types: 6.20.0 undici-types: 6.20.0
@ -362,10 +357,7 @@ snapshots:
dependencies: dependencies:
'@types/node': 22.10.5 '@types/node': 22.10.5
discord-api-types@0.37.115: {} discord-api-types@0.37.83: {}
discord-api-types@0.37.83:
optional: true
dotenv@16.4.7: {} dotenv@16.4.7: {}
@ -397,6 +389,12 @@ snapshots:
'@esbuild/win32-ia32': 0.24.2 '@esbuild/win32-ia32': 0.24.2
'@esbuild/win32-x64': 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: oceanic.js@1.11.2:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1

29
src/commands/help.ts Normal file
View File

@ -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;

View File

@ -1,19 +1,15 @@
import { Message } from "oceanic.js"; import { Message } from "oceanic.js";
import { Command } from "."; import { Command } from ".";
const hi: Command = { const cmdHi: Command = {
name: "hi", names: ["hi", "hello"],
desc: "test command", desc: "test command",
async checkPerm(msg: Message): Promise<boolean> { async checkPerm(msg: Message) {
if (msg.member === undefined) return false; return msg.member?.roles.includes("1327065762031992924");
return msg.member.roles.includes("1327065762031992924");
}, },
async handler(msg: Message) { async handler(msg: Message, { say }) {
msg.channel?.createMessage({ await say("Test message");
content: "Test message",
});
}, },
}; };
export default hi; export default cmdHi;

View File

@ -1,13 +1,21 @@
import { Message } from "oceanic.js"; import { Message } from "oceanic.js";
import hi from "./hi"; import cmdHelp from "./help";
import cmdHi from "./hi";
import cmdPlay from "./play";
export type Command = { export type Command = {
name: string; names: string[];
desc?: string; desc: string;
checkPerm(msg: Message): Promise<boolean>; checkPerm(msg: Message): Promise<boolean | undefined>;
handler(msg: Message): Promise<void>; handler(
msg: Message,
replies: {
reply: (text: string) => Promise<void>;
say: (text: string) => Promise<void>;
}
): Promise<void>;
}; };
const commands: Command[] = [hi]; const commands: Command[] = [cmdHelp, cmdHi, cmdPlay];
export default commands; export default commands;

63
src/commands/play.ts Normal file
View File

@ -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;

1
src/constants.ts Normal file
View File

@ -0,0 +1 @@
export const PREFIX = "!!";

View File

@ -1,15 +1,20 @@
import { Client } from "oceanic.js"; import { Client } from "oceanic.js";
import "dotenv/config"; import "dotenv/config";
import commands from "./commands"; import commands from "./commands";
import { PREFIX } from "./constants";
const token = process.env.DISCORD_TOKEN; function token() {
if (!token) { return (
console.error("No `DISCORD_TOKEN` was specified!!"); process.env.DISCORD_TOKEN ??
process.exit(1); (() => {
console.error("No `DISCORD_TOKEN` was specified!!");
process.exit(1);
})()
);
} }
const client = new Client({ const client = new Client({
auth: `Bot ${token}`, auth: `Bot ${token()}`,
gateway: { gateway: {
intents: ["ALL"], intents: ["ALL"],
}, },
@ -21,25 +26,37 @@ client.on("ready", async () => {
console.log("Ready as", client.user.tag); console.log("Ready as", client.user.tag);
}); });
const PREFIX = "!!";
client.on("messageCreate", async (msg) => { client.on("messageCreate", async (msg) => {
if (msg.author.bot) return; if (msg.author.bot) return;
let first = msg.content.split(/\s/).shift(); const content = msg.content.toLowerCase().trim();
if (first?.startsWith(PREFIX) === false) return; if (!content.startsWith(PREFIX)) return;
let cmdName = first!.slice(PREFIX.length);
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) { if (await cmd.checkPerm(msg)) {
await msg.createReaction("❌"); const reply = async (text: string) => {
return; 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) => { client.on("error", (err) => {

7
src/util.ts Normal file
View File

@ -0,0 +1,7 @@
export async function filterAsync<T>(
arr: T[],
predicate: (v) => Promise<boolean>
): Promise<T[]> {
const results = await Promise.all(arr.map(predicate));
return arr.filter((_v, i) => results[i]);
}

21
tsconfig.json Normal file
View File

@ -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"]
}
}