Initial implementation of python client with socket.io IPC

This commit is contained in:
Logan Cusano
2024-04-03 02:24:21 -04:00
parent be5943e9d4
commit c78ed89707
11 changed files with 1022 additions and 308 deletions

View File

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

View File

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

View File

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

View 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]);
});
});
};

View 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);
}