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 * 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();
|
||||
|
||||
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