Initial implementation of python client with socket.io IPC
This commit is contained in:
@@ -1,148 +0,0 @@
|
||||
import {
|
||||
NoSubscriberBehavior,
|
||||
StreamType,
|
||||
createAudioPlayer,
|
||||
createAudioResource,
|
||||
entersState,
|
||||
AudioPlayerStatus,
|
||||
VoiceConnectionStatus,
|
||||
joinVoiceChannel,
|
||||
getVoiceConnection,
|
||||
} from '@discordjs/voice';
|
||||
|
||||
import { GatewayIntentBits } from 'discord-api-types/v10';
|
||||
|
||||
import { Client, Events, ActivityType } from 'discord.js';
|
||||
|
||||
import prism_media from 'prism-media';
|
||||
const { FFmpeg } = prism_media;
|
||||
|
||||
// Import the DAB settings from the dynamic settings file
|
||||
import {device, maxTransmissionGap, type} from './dabSettings.mjs'
|
||||
|
||||
const player = createAudioPlayer({
|
||||
behaviors: {
|
||||
noSubscriber: NoSubscriberBehavior.Play,
|
||||
maxMissedFrames: Math.round(maxTransmissionGap / 20),
|
||||
},
|
||||
});
|
||||
|
||||
function attachRecorder() {
|
||||
player.play(
|
||||
createAudioResource(
|
||||
new FFmpeg({
|
||||
args: [
|
||||
'-analyzeduration',
|
||||
'0',
|
||||
'-loglevel',
|
||||
'0',
|
||||
'-f',
|
||||
type,
|
||||
'-i',
|
||||
type === 'dshow' ? `audio=${device}` : device,
|
||||
'-acodec',
|
||||
'libopus',
|
||||
'-f',
|
||||
'opus',
|
||||
'-ar',
|
||||
'48000',
|
||||
'-ac',
|
||||
'2',
|
||||
],
|
||||
}),
|
||||
{
|
||||
inputType: StreamType.OggOpus,
|
||||
},
|
||||
),
|
||||
);
|
||||
console.log('Attached recorder - ready to go!');
|
||||
}
|
||||
|
||||
player.on('stateChange', (oldState, newState) => {
|
||||
if (oldState.status === AudioPlayerStatus.Idle && newState.status === AudioPlayerStatus.Playing) {
|
||||
console.log('Playing audio output on audio player');
|
||||
} else if (newState.status === AudioPlayerStatus.Idle) {
|
||||
console.log('Playback has stopped. Attempting to restart.');
|
||||
attachRecorder();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} channel
|
||||
* @returns {any}
|
||||
*/
|
||||
export async function connectToChannel(channel) {
|
||||
const connection = joinVoiceChannel({
|
||||
channelId: channel.id,
|
||||
guildId: channel.guild.id,
|
||||
adapterCreator: channel.guild.voiceAdapterCreator,
|
||||
});
|
||||
try {
|
||||
await entersState(connection, VoiceConnectionStatus.Ready, 30_000);
|
||||
await connection.subscribe(player);
|
||||
return connection;
|
||||
} catch (error) {
|
||||
connection.destroy();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVoiceChannelFromID(client, channelID) {
|
||||
return client.channels.cache.get(channelID)
|
||||
}
|
||||
|
||||
export async function checkIfConnectedToVC(guildId) {
|
||||
const connection = getVoiceConnection(guildId)
|
||||
console.log("Connection!", connection);
|
||||
return connection
|
||||
}
|
||||
|
||||
export const getVoiceConnectionFromGuild = async (guildId) => {
|
||||
return getVoiceConnection(guildId);
|
||||
}
|
||||
|
||||
export async function initDiscordBotClient(token, systemName, readyCallback) {
|
||||
const client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.MessageContent],
|
||||
});
|
||||
|
||||
client.on(Events.ClientReady, () => {
|
||||
console.log('discord.js client is ready!');
|
||||
|
||||
// Attach the recorder to the VC connection
|
||||
attachRecorder();
|
||||
|
||||
// Set the activity of the bot user
|
||||
client.user.setPresence({
|
||||
activities: [{ name: `${systemName}`, type: ActivityType.Listening }],
|
||||
});
|
||||
|
||||
//
|
||||
readyCallback(client);
|
||||
});
|
||||
|
||||
/* on event create
|
||||
// TODO - Implement methods for discord users to interact directly with the bots for realtime info (last talked ID/TG, etc.)
|
||||
client.on(Events.MessageCreate, async (message) => {
|
||||
if (!message.guild) return;
|
||||
console.log(`New Message:`, message.content);
|
||||
if (message.content === '-join') {
|
||||
const channel = message.member?.voice.channel;
|
||||
if (channel) {
|
||||
try {
|
||||
const connection = await connectToChannel(channel);
|
||||
connection.subscribe(player);
|
||||
await message.reply('Playing now!');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
await message.reply('Join a voice channel then try again!');
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
client.login(token);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { executeCommand } from '../modules/cliHandler.mjs';
|
||||
import { extractValue } from '../modules/baseUtils.mjs';
|
||||
import os from 'os';
|
||||
|
||||
// Defaults to Windows values for testing
|
||||
let device = "VoiceMeeter VAIO3 Output (VB-Audio VoiceMeeter VAIO3)";
|
||||
let maxTransmissionGap = 500;
|
||||
let type = "dshow";
|
||||
|
||||
// Set Linux values for use in production
|
||||
if (os.platform() === 'linux') {
|
||||
await new Promise((res) => {
|
||||
executeCommand("pactl", ['list', 'short', 'sources']).then(cliOutput => {
|
||||
const extracted_device_details = extractValue(cliOutput, '(?:\\d(?:\\t|[\\s]{1,9})(?<device>[\\w\\d\\-\\.\\_]+?)(?:\\t|[\\s]{1,9})(?<card>[\\w\\d\\-\\.\\_]+)(?:\\t|[\\s]{1,9})(?:(?<device_type>[\\w\\d\\-\\.\\_]+?) (?<channels>\\dch) (?<frequency>\\d+Hz))(?:\\t|[\\s]{1,9})(?:IDLE|RUNNING|SUSPENDED))')
|
||||
device = extracted_device_details.groups.device;
|
||||
console.log("Device:", device);
|
||||
res();
|
||||
});
|
||||
});
|
||||
type = "pulse"; // Replace with appropriate type for Linux
|
||||
}
|
||||
|
||||
export { device, maxTransmissionGap, type };
|
||||
@@ -1,135 +0,0 @@
|
||||
import { connectToChannel, checkIfConnectedToVC, initDiscordBotClient, getVoiceChannelFromID, getVoiceConnectionFromGuild } from './dab.mjs';
|
||||
import { openOP25, closeOP25 } from '../op25Handler/op25Handler.mjs';
|
||||
|
||||
let activeDiscordClient = undefined;
|
||||
const activeDiscordVoiceConnections = {};
|
||||
|
||||
/**
|
||||
* Join the requested server VC and listen to the requested system
|
||||
* @param {object} joinData The object containing all the information to join the server
|
||||
*/
|
||||
export const joinDiscordVC = async (joinData) => {
|
||||
console.log("Join requested: ", joinData)
|
||||
const connection = await new Promise((res) => {
|
||||
// Check if a client already exists
|
||||
if (!activeDiscordClient) {
|
||||
// Open a new client and join the requested channel with the requested ID
|
||||
initDiscordBotClient(joinData.clientID, joinData.system, client => {
|
||||
// Open an instance of OP25
|
||||
openOP25(joinData.system);
|
||||
|
||||
getVoiceChannelFromID(client, joinData.channelID).then(vc => {
|
||||
// Add the client object to the IO instance
|
||||
activeDiscordClient = client;
|
||||
const connection = connectToChannel(vc);
|
||||
activeDiscordVoiceConnections[vc.guild.id] = connection;
|
||||
console.log("Bot Connected to VC");
|
||||
res(connection);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Join the requested channel with the requested ID
|
||||
getVoiceChannelFromID(activeDiscordClient, joinData.channelID).then(vc => {
|
||||
// Add the client object to the IO instance
|
||||
const connection = connectToChannel(vc);
|
||||
activeDiscordVoiceConnections[vc.guild.id] = connection;
|
||||
console.log("Bot Connected to VC");
|
||||
res(connection);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave VC on the requested server
|
||||
* @param {string} guildId The guild ID to disconnect from VC
|
||||
*/
|
||||
export const leaveDiscordVC = async (guildId) => {
|
||||
console.log("Leave requested");
|
||||
if (await checkIfConnectedToVC(guildId)) {
|
||||
const connection = await getVoiceConnectionFromGuild(guildId);
|
||||
if (connection) {
|
||||
console.log("There is an open VC connection, closing it now");
|
||||
// Destroy the open VC connection
|
||||
connection.destroy();
|
||||
|
||||
// Remove the connection from the object
|
||||
delete activeDiscordVoiceConnections[guildId];
|
||||
|
||||
// Check if this was the last open VC connection
|
||||
if (Object.keys(activeDiscordVoiceConnections).length == 0) {
|
||||
console.log("No more open VC connections, closing the client-side discord client and OP25")
|
||||
// Close the active client if there are no open VCs after this one
|
||||
activeDiscordClient.destroy();
|
||||
activeDiscordClient = undefined;
|
||||
|
||||
// Close OP25
|
||||
await closeOP25();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the bot is connected to a discord VC in the given server
|
||||
* @param {string} guildId The guild id to check the connection status in
|
||||
* @returns {boolean} If the node is connected to VC in the given guild
|
||||
*/
|
||||
export const checkIfDiscordVCConnected = async (guildId) => {
|
||||
console.log("Requested status check");
|
||||
if (await checkIfConnectedToVC(guildId)) {
|
||||
console.log("There is an open VC connection");
|
||||
return (true);
|
||||
} else {
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the username of the bot in a given guild
|
||||
* (there may be a server nickname given to the bot in a certain guild)
|
||||
* @param {string} guildId The guild id to check the connection status in
|
||||
* @returns {string} The username of the bot in the given guild's VC
|
||||
*/
|
||||
export const getDiscordUsername = async (guildId) => {
|
||||
console.log("Requested username");
|
||||
if (activeDiscordClient) {
|
||||
// Fetch the guild
|
||||
const guild = await activeDiscordClient.guilds.fetch(guildId);
|
||||
|
||||
// Fetch the bot member in the guild
|
||||
const botMember = await guild.members.fetch(activeDiscordClient.user.id);
|
||||
|
||||
// Return bot's nickname if available, otherwise return username
|
||||
return botMember.nickname || botMember.user.username;
|
||||
}
|
||||
else return (undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the currently running bot
|
||||
* @returns {string} The ID of the active client
|
||||
*/
|
||||
export const getDiscordID = async () => {
|
||||
console.log("Requested username");
|
||||
if (activeDiscordClient) {
|
||||
return (activeDiscordClient.user.id);
|
||||
}
|
||||
else return (undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if there is an open discord client
|
||||
* @returns {boolean} If the client is open or not
|
||||
*/
|
||||
export const checkIfClientIsOpen = async () => {
|
||||
if (activeDiscordClient) {
|
||||
return (true);
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
97
client/discordAudioBot/pdabHandler.mjs
Normal file
97
client/discordAudioBot/pdabHandler.mjs
Normal file
@@ -0,0 +1,97 @@
|
||||
// server.js
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
import { launchProcess } from '../modules/subprocessHandler.mjs';
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server);
|
||||
let pdabProcess = false;
|
||||
|
||||
export const initDiscordBotClient = (clientId, callback) => {
|
||||
const port = process.env.PDAB_PORT || 3000;
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log('A user connected');
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('User disconnected');
|
||||
});
|
||||
|
||||
// Listen for the discord client ready event
|
||||
socket.on('discord_ready', (message) => {
|
||||
console.log("Message from local client", message);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port, async () => {
|
||||
console.log(`Server is running on port ${port}`);
|
||||
|
||||
launchProcess("python", ["./discordAduioBot/pdab/main.py", process.env.AUDIO_DEVICE_ID, clientId, port], false);
|
||||
pdabProcess = true; // TODO - Make this more dynamic
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Function to emit a command to join a voice channel
|
||||
export const connectToChannel = (channelId) => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('join_server', { channelId: channelId }, (status, value) => {
|
||||
console.log("Status returned from bot:", status, value);
|
||||
res(value[0]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Function to emit a command to leave a voice channel
|
||||
export const leaveVoiceChannel = (guildId) => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('leave_server', { guild_id: guildId }, (status, clientRemainsOpen) => {
|
||||
console.log("Discord client remains open?", clientRemainsOpen);
|
||||
res(clientRemainsOpen)
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Placeholder functions (replace with actual implementation)
|
||||
export const checkIfConnectedToVC = async (guildId) => {
|
||||
console.log("Pdab process var:", pdabProcess);
|
||||
|
||||
if (!pdabProcess) return false;
|
||||
|
||||
return await new Promise((res) => {
|
||||
io.timeout(25000).emit('check_discord_vc_connected', { guild_id: guildId }, (status, result) => {
|
||||
console.log(`Discord VC connected for guild ${guildId}: ${result}`);
|
||||
res((result[0]));
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
export const requestDiscordUsername = (guildId) => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('request_discord_username', { guild_id: guildId }, (status, result) => {
|
||||
console.log(`Discord username: ${result[0]}`);
|
||||
res(result[0]);
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
export const checkIfClientIsOpen = () => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('check_client_is_open', (status, result) => {
|
||||
console.log(`Client is open: ${result}`);
|
||||
res(result[0])
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const requestDiscordID = () => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('request_discord_id', (status, result) => {
|
||||
console.log(`Discord ID: ${result}`);
|
||||
res(result[0]);
|
||||
});
|
||||
});
|
||||
};
|
||||
106
client/discordAudioBot/pdabWrappers.mjs
Normal file
106
client/discordAudioBot/pdabWrappers.mjs
Normal file
@@ -0,0 +1,106 @@
|
||||
import { connectToChannel, leaveVoiceChannel, checkIfConnectedToVC, initDiscordBotClient, requestDiscordUsername, requestDiscordID } from './pdabHandler.mjs';
|
||||
import { openOP25, closeOP25 } from '../op25Handler/op25Handler.mjs';
|
||||
|
||||
let activeDiscordClient = undefined;
|
||||
|
||||
/**
|
||||
* Join the requested server VC and listen to the requested system
|
||||
* @param {object} joinData The object containing all the information to join the server
|
||||
*/
|
||||
export const joinDiscordVC = async (joinData) => {
|
||||
console.log("Join requested: ", joinData)
|
||||
const connection = await new Promise(async (res) => {
|
||||
// Check if a client already exists
|
||||
if (!await checkIfClientIsOpen()) {
|
||||
// Open a new client and join the requested channel with the requested ID
|
||||
initDiscordBotClient(joinData.clientID, () => {
|
||||
// Open an instance of OP25
|
||||
// TODO DELTE comment DEV ONLY openOP25(joinData.system);
|
||||
|
||||
// Add the client object to the IO instance
|
||||
connectToChannel(joinData.channelID, (connectionStatus) => {
|
||||
console.log("Bot Connected to VC:", connectionStatus);
|
||||
res(connectionStatus);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Join the requested channel with the requested ID
|
||||
// Add the client object to the IO instance
|
||||
const connection = connectToChannel(joinData.channelID);
|
||||
console.log("Bot Connected to VC::");
|
||||
res(connection);
|
||||
}
|
||||
});
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave VC on the requested server
|
||||
* @param {string} guildId The guild ID to disconnect from VC
|
||||
*/
|
||||
export const leaveDiscordVC = async (guildId) => {
|
||||
console.log("Leave requested");
|
||||
if (await checkIfConnectedToVC(guildId)) {
|
||||
await leaveVoiceChannel(guildId, async (clientRemainsOpen) => {
|
||||
if (!clientRemainsOpen) {
|
||||
console.log("There are no open VC connections");
|
||||
// TODO DELETE comment DEV ONLY await closeOP25();
|
||||
|
||||
// TODO Close the python client
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the bot is connected to a discord VC in the given server
|
||||
* @param {string} guildId The guild id to check the connection status in
|
||||
* @returns {boolean} If the node is connected to VC in the given guild
|
||||
*/
|
||||
export const checkIfDiscordVCConnected = async (guildId) => {
|
||||
console.log("Requested status check");
|
||||
if (await checkIfConnectedToVC(guildId)) {
|
||||
console.log("There is an open VC connection");
|
||||
return (true);
|
||||
} else {
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the username of the bot in a given guild
|
||||
* (there may be a server nickname given to the bot in a certain guild)
|
||||
* @param {string} guildId The guild id to check the connection status in
|
||||
* @returns {string} The username of the bot in the given guild's VC
|
||||
*/
|
||||
export const getDiscordUsername = async (guildId) => {
|
||||
console.log("Requested username");
|
||||
if (checkIfClientIsOpen()) {
|
||||
return await requestDiscordUsername(guildId)
|
||||
} else return (undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the currently running bot
|
||||
* @returns {string} The ID of the active client
|
||||
*/
|
||||
export const getDiscordID = async () => {
|
||||
console.log("Requested ID");
|
||||
if (checkIfClientIsOpen()) {
|
||||
return await requestDiscordID();
|
||||
}
|
||||
else return (undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if there is an open discord client
|
||||
* @returns {boolean} If the client is open or not
|
||||
*/
|
||||
export const checkIfClientIsOpen = async () => {
|
||||
if (activeDiscordClient) {
|
||||
return (true);
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
Reference in New Issue
Block a user