From 4e246637fbb86ee1cdc6cd2ef66ca834253f44b1 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:18:35 +0800 Subject: [PATCH] refactor --- src/config.ts | 24 +++++++ src/discordService.ts | 92 ++++++++++++++++++++++++ src/index.ts | 158 ++++++------------------------------------ src/types.ts | 16 +++++ src/udpBridge.ts | 30 ++++++++ 5 files changed, 185 insertions(+), 135 deletions(-) create mode 100644 src/config.ts create mode 100644 src/discordService.ts create mode 100644 src/types.ts create mode 100644 src/udpBridge.ts diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..ddf47fe --- /dev/null +++ b/src/config.ts @@ -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); +} diff --git a/src/discordService.ts b/src/discordService.ts new file mode 100644 index 0000000..b41a899 --- /dev/null +++ b/src/discordService.ts @@ -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(); diff --git a/src/index.ts b/src/index.ts index 03f59c3..ecb624e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,105 +1,59 @@ -import * as RPC from 'discord-rpc'; -import * as dgram from 'dgram'; -import axios from 'axios'; -import { config } from 'dotenv'; +import { EspCommand, VoiceState } from './types'; +import { startUdpServer, sendToEsp } from './udpBridge'; +import { discordService } from './discordService'; -config(); - -// --- 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) => { +startUdpServer(async (msg) => { const command = msg.toString().trim(); console.log(`Received from ESP32: ${command}`); + if (!discordService) return; + if (command === EspCommand.TOGGLE) { - if (!loggedIn) { + if (!discordService.isLoggedIn()) { console.warn('Cannot toggle: Not authenticated yet.'); return; } try { - // Fetch current state to toggle accurately - const settings = await client.getVoiceSettings(); + const client = discordService.getClient(); + const settings: any = await client.getVoiceSettings(); const currentMute = settings.mute; const { output, input, mode, ...restSettings } = settings; await client.setVoiceSettings({ - ...restSettings, // Preserve other settings - mute: settings.deaf || !currentMute, // if deaf is true, mute should stay true, otherwise nothing changes + ...restSettings, + mute: settings.deaf || !currentMute, }); console.log(`Toggled mute to: ${!currentMute}`); } catch (err) { console.error('Error toggling mute:', err); } } else if (command === EspCommand.MUTE || command === EspCommand.UNMUTE) { - if (!loggedIn) { + if (!discordService.isLoggedIn()) { console.warn('Cannot set mute: Not authenticated yet.'); return; } + try { 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; await client.setVoiceSettings({ - ...restSettings, // Preserve other settings - mute: muteState || settings.deaf, // if deaf is true, mute should stay true + ...restSettings, + mute: muteState || settings.deaf, }); console.log(`Set mute to: ${muteState}`); } catch (err) { console.error('Error setting mute state:', err); } } else if (command === EspCommand.STATUS) { - if (!loggedIn) { + if (!discordService.isLoggedIn()) { console.warn('Cannot fetch status: Not authenticated yet.'); return; } try { - const settings = await client.getVoiceSettings(); + const client = discordService.getClient(); + const settings: any = await client.getVoiceSettings(); sendToEsp(settings.mute ? VoiceState.MUTED : VoiceState.UNMUTED); } catch (err) { console.error('Error fetching voice settings:', err); @@ -109,76 +63,10 @@ udpServer.on('message', async (msg, rinfo) => { } }); -udpServer.bind(CONFIG.LISTEN_PORT, () => { - 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 }) => { +discordService.onVoiceSettingsUpdate((data) => { console.log(`Voice settings updated. Mute: ${data.mute}, Deaf: ${data.deaf}`); sendToEsp(data.mute ? VoiceState.MUTED : VoiceState.UNMUTED); }); -let isLoginInProgress = false; - -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(); +// Start auth flow +discordService.login(); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..af3acdb --- /dev/null +++ b/src/types.ts @@ -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; +}; diff --git a/src/udpBridge.ts b/src/udpBridge.ts new file mode 100644 index 0000000..0f6ff88 --- /dev/null +++ b/src/udpBridge.ts @@ -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 };