refactor
This commit is contained in:
24
src/config.ts
Normal file
24
src/config.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { config as dotenvConfig } from 'dotenv';
|
||||||
|
|
||||||
|
dotenvConfig();
|
||||||
|
|
||||||
|
export const CONFIG: {
|
||||||
|
CLIENT_ID: string;
|
||||||
|
CLIENT_SECRET: string;
|
||||||
|
REDIRECT_URI: string;
|
||||||
|
ESP_IP: string;
|
||||||
|
ESP_PORT: number;
|
||||||
|
LISTEN_PORT: number;
|
||||||
|
} = {
|
||||||
|
CLIENT_ID: process.env.CLIENT_ID ?? '',
|
||||||
|
CLIENT_SECRET: process.env.CLIENT_SECRET ?? '',
|
||||||
|
REDIRECT_URI: process.env.REDIRECT_URI || 'http://localhost',
|
||||||
|
ESP_IP: process.env.ESP_IP || '192.168.50.201',
|
||||||
|
ESP_PORT: Number(process.env.ESP_PORT) || 4210,
|
||||||
|
LISTEN_PORT: Number(process.env.LISTEN_PORT) || 4211,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!CONFIG.CLIENT_ID || !CONFIG.CLIENT_SECRET) {
|
||||||
|
console.error('Error: CLIENT_ID and CLIENT_SECRET must be set in the environment variables.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
92
src/discordService.ts
Normal file
92
src/discordService.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import * as RPC from 'discord-rpc';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { CONFIG } from './config';
|
||||||
|
import { VoiceSettingsPayload } from './types';
|
||||||
|
|
||||||
|
class DiscordService {
|
||||||
|
private client: RPC.Client;
|
||||||
|
private loggedIn = false;
|
||||||
|
private isLoginInProgress = false;
|
||||||
|
private voiceUpdateCallbacks: Array<(d: VoiceSettingsPayload) => void> = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.client = new RPC.Client({ transport: 'ipc' });
|
||||||
|
this.setupEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupEvents() {
|
||||||
|
this.client.on('ready', () => {
|
||||||
|
console.log(`Logged in as ${this.client.user?.username || 'Unknown User'}`);
|
||||||
|
this.client.subscribe('VOICE_SETTINGS_UPDATE', undefined);
|
||||||
|
|
||||||
|
this.client
|
||||||
|
.getVoiceSettings()
|
||||||
|
.then((settings) => {
|
||||||
|
if (settings) {
|
||||||
|
this.callVoiceCallbacks({ mute: settings.mute, deaf: settings.deaf });
|
||||||
|
} else {
|
||||||
|
console.warn('Could not fetch initial voice settings.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('VOICE_SETTINGS_UPDATE', (data: VoiceSettingsPayload) => {
|
||||||
|
this.callVoiceCallbacks(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('disconnected', () => {
|
||||||
|
console.warn('Disconnected from Discord RPC.');
|
||||||
|
this.loggedIn = false;
|
||||||
|
if (this.isLoginInProgress) return;
|
||||||
|
this.isLoginInProgress = true;
|
||||||
|
this.login().finally(() => (this.isLoginInProgress = false));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('error', (err) => console.error('Discord RPC Error:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
private callVoiceCallbacks(data: VoiceSettingsPayload) {
|
||||||
|
for (const cb of this.voiceUpdateCallbacks) cb(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onVoiceSettingsUpdate(cb: (d: VoiceSettingsPayload) => void) {
|
||||||
|
this.voiceUpdateCallbacks.push(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async login() {
|
||||||
|
if (this.loggedIn) return;
|
||||||
|
try {
|
||||||
|
console.log('Waiting for authorization (Check Discord popup)...');
|
||||||
|
this.isLoginInProgress = true;
|
||||||
|
|
||||||
|
await this.client.login({
|
||||||
|
clientId: CONFIG.CLIENT_ID,
|
||||||
|
clientSecret: CONFIG.CLIENT_SECRET,
|
||||||
|
scopes: ['rpc', 'rpc.voice.read', 'rpc.voice.write'],
|
||||||
|
redirectUri: CONFIG.REDIRECT_URI,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loggedIn = true;
|
||||||
|
console.log('Authenticated successfully with Discord!');
|
||||||
|
} catch (error: any) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
console.error('Token Exchange Failed:', error.response?.data || error.message);
|
||||||
|
} else {
|
||||||
|
console.error('Authentication Failed:', error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isLoginInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isLoggedIn() {
|
||||||
|
return this.loggedIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getClient() {
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const discordService = new DiscordService();
|
||||||
158
src/index.ts
158
src/index.ts
@@ -1,105 +1,59 @@
|
|||||||
import * as RPC from 'discord-rpc';
|
import { EspCommand, VoiceState } from './types';
|
||||||
import * as dgram from 'dgram';
|
import { startUdpServer, sendToEsp } from './udpBridge';
|
||||||
import axios from 'axios';
|
import { discordService } from './discordService';
|
||||||
import { config } from 'dotenv';
|
|
||||||
|
|
||||||
config();
|
startUdpServer(async (msg) => {
|
||||||
|
|
||||||
// --- CONFIGURATION ---
|
|
||||||
const CONFIG = {
|
|
||||||
CLIENT_ID: process.env.CLIENT_ID || 'YOUR_CLIENT_ID', // From Discord Dev Portal
|
|
||||||
CLIENT_SECRET: process.env.CLIENT_SECRET || 'YOUR_CLIENT_SECRET', // From Discord Dev Portal
|
|
||||||
REDIRECT_URI: process.env.REDIRECT_URI || 'http://localhost',
|
|
||||||
ESP_IP: process.env.ESP_IP || '192.168.50.201', // IP of your ESP32
|
|
||||||
ESP_PORT: 4210, // Port ESP32 listens on
|
|
||||||
LISTEN_PORT: 4211, // Port this script listens on
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- TYPES ---
|
|
||||||
enum VoiceState {
|
|
||||||
MUTED = 'MUTED',
|
|
||||||
UNMUTED = 'UNMUTED',
|
|
||||||
}
|
|
||||||
|
|
||||||
enum EspCommand {
|
|
||||||
TOGGLE = 'TOGGLE',
|
|
||||||
STATUS = 'STATUS',
|
|
||||||
MUTE = 'MUTE',
|
|
||||||
UNMUTE = 'UNMUTE',
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- UDP SERVER SETUP ---
|
|
||||||
const udpServer = dgram.createSocket('udp4');
|
|
||||||
|
|
||||||
udpServer.on('error', (err) => {
|
|
||||||
console.error(`UDP server error:\n${err.stack}`);
|
|
||||||
udpServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper to send messages to ESP32
|
|
||||||
const sendToEsp = (msg: string): void => {
|
|
||||||
const message = Buffer.from(msg);
|
|
||||||
udpServer.send(message, CONFIG.ESP_PORT, CONFIG.ESP_IP, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(`UDP Send Error to ${CONFIG.ESP_IP}:`, err);
|
|
||||||
} else {
|
|
||||||
console.log(`Sent to ESP32: ${msg}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- DISCORD RPC SETUP ---
|
|
||||||
const client = new RPC.Client({ transport: 'ipc' });
|
|
||||||
let loggedIn = false;
|
|
||||||
|
|
||||||
// Handle incoming UDP messages from ESP32
|
|
||||||
udpServer.on('message', async (msg, rinfo) => {
|
|
||||||
const command = msg.toString().trim();
|
const command = msg.toString().trim();
|
||||||
console.log(`Received from ESP32: ${command}`);
|
console.log(`Received from ESP32: ${command}`);
|
||||||
|
|
||||||
|
if (!discordService) return;
|
||||||
|
|
||||||
if (command === EspCommand.TOGGLE) {
|
if (command === EspCommand.TOGGLE) {
|
||||||
if (!loggedIn) {
|
if (!discordService.isLoggedIn()) {
|
||||||
console.warn('Cannot toggle: Not authenticated yet.');
|
console.warn('Cannot toggle: Not authenticated yet.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch current state to toggle accurately
|
const client = discordService.getClient();
|
||||||
const settings = await client.getVoiceSettings();
|
const settings: any = await client.getVoiceSettings();
|
||||||
const currentMute = settings.mute;
|
const currentMute = settings.mute;
|
||||||
const { output, input, mode, ...restSettings } = settings;
|
const { output, input, mode, ...restSettings } = settings;
|
||||||
await client.setVoiceSettings({
|
await client.setVoiceSettings({
|
||||||
...restSettings, // Preserve other settings
|
...restSettings,
|
||||||
mute: settings.deaf || !currentMute, // if deaf is true, mute should stay true, otherwise nothing changes
|
mute: settings.deaf || !currentMute,
|
||||||
});
|
});
|
||||||
console.log(`Toggled mute to: ${!currentMute}`);
|
console.log(`Toggled mute to: ${!currentMute}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error toggling mute:', err);
|
console.error('Error toggling mute:', err);
|
||||||
}
|
}
|
||||||
} else if (command === EspCommand.MUTE || command === EspCommand.UNMUTE) {
|
} else if (command === EspCommand.MUTE || command === EspCommand.UNMUTE) {
|
||||||
if (!loggedIn) {
|
if (!discordService.isLoggedIn()) {
|
||||||
console.warn('Cannot set mute: Not authenticated yet.');
|
console.warn('Cannot set mute: Not authenticated yet.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const muteState = command === EspCommand.MUTE;
|
const muteState = command === EspCommand.MUTE;
|
||||||
const settings = await client.getVoiceSettings();
|
const client = discordService.getClient();
|
||||||
|
const settings: any = await client.getVoiceSettings();
|
||||||
const { output, input, mode, ...restSettings } = settings;
|
const { output, input, mode, ...restSettings } = settings;
|
||||||
await client.setVoiceSettings({
|
await client.setVoiceSettings({
|
||||||
...restSettings, // Preserve other settings
|
...restSettings,
|
||||||
mute: muteState || settings.deaf, // if deaf is true, mute should stay true
|
mute: muteState || settings.deaf,
|
||||||
});
|
});
|
||||||
console.log(`Set mute to: ${muteState}`);
|
console.log(`Set mute to: ${muteState}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error setting mute state:', err);
|
console.error('Error setting mute state:', err);
|
||||||
}
|
}
|
||||||
} else if (command === EspCommand.STATUS) {
|
} else if (command === EspCommand.STATUS) {
|
||||||
if (!loggedIn) {
|
if (!discordService.isLoggedIn()) {
|
||||||
console.warn('Cannot fetch status: Not authenticated yet.');
|
console.warn('Cannot fetch status: Not authenticated yet.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const settings = await client.getVoiceSettings();
|
const client = discordService.getClient();
|
||||||
|
const settings: any = await client.getVoiceSettings();
|
||||||
sendToEsp(settings.mute ? VoiceState.MUTED : VoiceState.UNMUTED);
|
sendToEsp(settings.mute ? VoiceState.MUTED : VoiceState.UNMUTED);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching voice settings:', err);
|
console.error('Error fetching voice settings:', err);
|
||||||
@@ -109,76 +63,10 @@ udpServer.on('message', async (msg, rinfo) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
udpServer.bind(CONFIG.LISTEN_PORT, () => {
|
discordService.onVoiceSettingsUpdate((data) => {
|
||||||
console.log(`UDP Bridge listening on port ${CONFIG.LISTEN_PORT}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- DISCORD EVENTS ---
|
|
||||||
client.on('ready', () => {
|
|
||||||
console.log(`Logged in as ${client.user?.username || 'Unknown User'}`);
|
|
||||||
|
|
||||||
// Subscribe to voice setting changes
|
|
||||||
client.subscribe('VOICE_SETTINGS_UPDATE', {});
|
|
||||||
|
|
||||||
// Initial fetch to sync ESP32 state on startup
|
|
||||||
client
|
|
||||||
.getVoiceSettings()
|
|
||||||
.then((settings) => {
|
|
||||||
if (settings) {
|
|
||||||
sendToEsp(settings.mute ? VoiceState.MUTED : VoiceState.UNMUTED);
|
|
||||||
} else {
|
|
||||||
console.warn('Could not fetch initial voice settings.');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('VOICE_SETTINGS_UPDATE', (data: { mute: boolean; deaf: boolean }) => {
|
|
||||||
console.log(`Voice settings updated. Mute: ${data.mute}, Deaf: ${data.deaf}`);
|
console.log(`Voice settings updated. Mute: ${data.mute}, Deaf: ${data.deaf}`);
|
||||||
sendToEsp(data.mute ? VoiceState.MUTED : VoiceState.UNMUTED);
|
sendToEsp(data.mute ? VoiceState.MUTED : VoiceState.UNMUTED);
|
||||||
});
|
});
|
||||||
|
|
||||||
let isLoginInProgress = false;
|
// Start auth flow
|
||||||
|
discordService.login();
|
||||||
client.on('disconnected', () => {
|
|
||||||
console.warn('Disconnected from Discord RPC.');
|
|
||||||
loggedIn = false;
|
|
||||||
if (isLoginInProgress) return; // Prevent multiple login attempts
|
|
||||||
isLoginInProgress = true;
|
|
||||||
login().finally(() => {
|
|
||||||
isLoginInProgress = false;
|
|
||||||
}); // Attempt to re-login
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('error', (error) => {
|
|
||||||
console.error('Discord RPC Error:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- AUTHENTICATION FLOW ---
|
|
||||||
async function login() {
|
|
||||||
try {
|
|
||||||
console.log('Waiting for authorization (Check Discord popup)...');
|
|
||||||
|
|
||||||
// 1. Authorize (Popup)
|
|
||||||
// RPC.Client.authorize returns a Promise that resolves to { code }
|
|
||||||
await client.login({
|
|
||||||
clientId: CONFIG.CLIENT_ID,
|
|
||||||
clientSecret: CONFIG.CLIENT_SECRET,
|
|
||||||
scopes: ['rpc', 'rpc.voice.read', 'rpc.voice.write'],
|
|
||||||
redirectUri: CONFIG.REDIRECT_URI,
|
|
||||||
});
|
|
||||||
|
|
||||||
loggedIn = true;
|
|
||||||
|
|
||||||
console.log('Authenticated successfully with Discord!');
|
|
||||||
} catch (error: any) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
console.error('Token Exchange Failed:', error.response?.data || error.message);
|
|
||||||
} else {
|
|
||||||
console.error('Authentication Failed:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the flow
|
|
||||||
login();
|
|
||||||
|
|||||||
16
src/types.ts
Normal file
16
src/types.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export enum VoiceState {
|
||||||
|
MUTED = 'MUTED',
|
||||||
|
UNMUTED = 'UNMUTED',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EspCommand {
|
||||||
|
TOGGLE = 'TOGGLE',
|
||||||
|
STATUS = 'STATUS',
|
||||||
|
MUTE = 'MUTE',
|
||||||
|
UNMUTE = 'UNMUTE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VoiceSettingsPayload = {
|
||||||
|
mute: boolean;
|
||||||
|
deaf?: boolean;
|
||||||
|
};
|
||||||
30
src/udpBridge.ts
Normal file
30
src/udpBridge.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import * as dgram from 'dgram';
|
||||||
|
import { CONFIG } from './config';
|
||||||
|
import { VoiceState } from './types';
|
||||||
|
|
||||||
|
const udpServer = dgram.createSocket('udp4');
|
||||||
|
|
||||||
|
udpServer.on('error', (err) => {
|
||||||
|
console.error(`UDP server error:\n${err.stack}`);
|
||||||
|
udpServer.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const startUdpServer = (onMessage: (msg: Buffer, rinfo: dgram.RemoteInfo) => void) => {
|
||||||
|
udpServer.on('message', onMessage);
|
||||||
|
udpServer.bind(CONFIG.LISTEN_PORT, () => {
|
||||||
|
console.log(`UDP Bridge listening on port ${CONFIG.LISTEN_PORT}`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendToEsp = (msg: string): void => {
|
||||||
|
const message = Buffer.from(msg);
|
||||||
|
udpServer.send(message, CONFIG.ESP_PORT, CONFIG.ESP_IP, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`UDP Send Error to ${CONFIG.ESP_IP}:`, err);
|
||||||
|
} else {
|
||||||
|
console.log(`Sent to ESP32: ${msg}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { dgram };
|
||||||
Reference in New Issue
Block a user