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",
"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"
}

View File

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

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 { Command } from ".";
const hi: Command = {
name: "hi",
const cmdHi: Command = {
names: ["hi", "hello"],
desc: "test command",
async checkPerm(msg: Message): Promise<boolean> {
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;

View File

@ -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<boolean>;
handler(msg: Message): Promise<void>;
names: string[];
desc: string;
checkPerm(msg: Message): Promise<boolean | undefined>;
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;

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 "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) => {

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