import videoRecordingService from "./videoRecording.js"; import webcamRecordingService from "./webcamRecording.js"; import { VIDEO_CONFIG } from "../config/videoConfig.js"; class CommandHandler { constructor() { this.commands = new Map(); this._registerCommands(); } _registerCommands() { // DM pickup command this.commands.set("dm_pickup", { trigger: (msg) => msg.channel.type === "DM" && msg.content.toLowerCase().includes("teto"), execute: async (msg, context) => { msg.channel.send("I'm here! What do you need?"); return { action: { message: `Picked up DM from ${msg.author.tag}`, channel: "DM", icon: "📞", }, }; }, }); // Xbox record that command this.commands.set("start_recording", { trigger: (msg) => msg.content.toLowerCase() === "xbox record that", execute: async (msg, context) => { const { client } = context; // Check if user is in a voice channel const member = msg.guild?.members.cache.get(msg.author.id); const voiceChannel = member?.voice?.channel; if (!voiceChannel) { msg.channel.send(VIDEO_CONFIG.ERRORS.NO_VOICE_CHANNEL); return null; } msg.channel.send( VIDEO_CONFIG.MESSAGES.RECORDING_STARTING.replace( "{channelName}", voiceChannel.name ) ); const result = await videoRecordingService.startRecording({ client, voiceChannel, user: msg.author, textChannel: msg.channel, }); if (result.success) { msg.channel.send(VIDEO_CONFIG.MESSAGES.RECORDING_STARTED); return { action: { message: `Started video recording ${msg.author.tag} in ${voiceChannel.name}`, channel: `#${msg.channel.name}`, icon: "đŸŽĨ", }, }; } else { if (result.message === VIDEO_CONFIG.ERRORS.ALREADY_RECORDING) { msg.channel.send(result.message); } else { msg.channel.send(VIDEO_CONFIG.ERRORS.VOICE_CONNECTION_FAILED); } return null; } }, }); // Stop recording command this.commands.set("stop_recording", { trigger: (msg) => msg.content.toLowerCase() === "stop recording" || msg.content.toLowerCase() === "xbox stop", execute: async (msg, context) => { const member = msg.guild?.members.cache.get(msg.author.id); const voiceChannel = member?.voice?.channel; if ( !voiceChannel || !videoRecordingService.isRecording(voiceChannel.id) ) { msg.channel.send(VIDEO_CONFIG.ERRORS.NO_ACTIVE_RECORDING); return null; } const result = await videoRecordingService.stopRecording( voiceChannel.id, msg.channel, VIDEO_CONFIG.MESSAGES.RECORDING_STOPPED ); if (result.success) { return { action: { message: `Stopped video recording by ${msg.author.tag} in ${voiceChannel.name}`, channel: `#${msg.channel.name}`, icon: "âšī¸", }, }; } return null; }, }); // Status command this.commands.set("status", { trigger: (msg) => msg.content.toLowerCase() === "teto status", execute: async (msg, context) => { const videoStatus = videoRecordingService.getStatus(); const webcamStatus = webcamRecordingService.getStatus(); const statusMsg = [ VIDEO_CONFIG.STATUS_TEMPLATE.TITLE, VIDEO_CONFIG.STATUS_TEMPLATE.ACTIVE_RECORDINGS.replace( "{count}", videoStatus.activeRecordings ), `đŸŽĨ Active webcam recordings: ${webcamStatus.activeWebcamRecordings}`, VIDEO_CONFIG.STATUS_TEMPLATE.CONTAINER_OUTPUT.replace( "{path}", videoStatus.outputDir ), VIDEO_CONFIG.STATUS_TEMPLATE.HOST_OUTPUT, VIDEO_CONFIG.STATUS_TEMPLATE.VIDEO_FILES.replace( "{count}", videoStatus.videoFiles ), `đŸŽĨ Webcam files: ${webcamStatus.webcamFiles}`, videoStatus.videoFiles > 0 ? VIDEO_CONFIG.STATUS_TEMPLATE.RECENT_FILES.replace( "{files}", videoStatus.recentFiles.join(", ") ) : "", webcamStatus.webcamFiles > 0 ? `📋 Recent webcam files: ${webcamStatus.recentWebcamFiles.join( ", " )}` : "", videoStatus.activeRecordings > 0 ? VIDEO_CONFIG.STATUS_TEMPLATE.CURRENTLY_RECORDING.replace( "{channels}", videoStatus.activeRecordingsList .map((r) => r.voiceChannelName) .join(", ") ) : "", webcamStatus.activeWebcamRecordings > 0 ? `đŸŽĨ Currently recording webcams in: ${webcamStatus.activeWebcamRecordingsList .map((r) => r.voiceChannelName) .join(", ")}` : "", VIDEO_CONFIG.STATUS_TEMPLATE.UPTIME.replace( "{uptime}", Math.round(process.uptime()) ), VIDEO_CONFIG.STATUS_TEMPLATE.DOCKER_INFO, ] .filter((line) => line) .join("\n"); msg.channel.send(statusMsg); return { action: { message: `Status requested by ${msg.author.tag}`, channel: `#${msg.channel.name || "DM"}`, icon: "📊", }, }; }, }); // Start webcam recording command this.commands.set("start_webcam_recording", { trigger: (msg) => msg.content.toLowerCase() === "record webcam" || msg.content.toLowerCase() === "xbox record webcam", execute: async (msg, context) => { const { client } = context; // Check if user is in a voice channel const member = msg.guild?.members.cache.get(msg.author.id); const voiceChannel = member?.voice?.channel; if (!voiceChannel) { msg.channel.send(VIDEO_CONFIG.ERRORS.NO_VOICE_CHANNEL); return null; } msg.channel.send( `đŸŽĨ Trying to start webcam recording... in ${voiceChannel.name}` ); const result = await webcamRecordingService.startRecording({ client, voiceChannel, user: msg.author, textChannel: msg.channel, }); if (result.success) { msg.channel.send("đŸŽĨ Starting webcam recording... 📹"); return { action: { message: `Started webcam recording ${msg.author.tag} in ${voiceChannel.name}`, channel: `#${msg.channel.name}`, icon: "đŸŽĨ", }, }; } else { if (result.message.includes("Already recording")) { msg.channel.send(result.message); } else if (result.message.includes("does not have webcam")) { msg.channel.send(result.message); } else { msg.channel.send( "Failed to start webcam recording. Check container logs for details!" ); } return null; } }, }); // Stop webcam recording command this.commands.set("stop_webcam_recording", { trigger: (msg) => msg.content.toLowerCase() === "stop webcam" || msg.content.toLowerCase() === "xbox stop webcam", execute: async (msg, context) => { const member = msg.guild?.members.cache.get(msg.author.id); const voiceChannel = member?.voice?.channel; if ( !voiceChannel || !webcamRecordingService.isRecording(voiceChannel.id) ) { msg.channel.send("No active webcam recording in your voice channel!"); return null; } const result = await webcamRecordingService.stopRecording( voiceChannel.id, msg.channel, "đŸŽĨ Webcam recording stopped by user." ); if (result.success) { return { action: { message: `Stopped webcam recording by ${msg.author.tag} in ${voiceChannel.name}`, channel: `#${msg.channel.name}`, icon: "âšī¸", }, }; } return null; }, }); // Enhanced status command to include webcam recordings this.commands.set("webcam_status", { trigger: (msg) => msg.content.toLowerCase() === "webcam status", execute: async (msg, context) => { const videoStatus = videoRecordingService.getStatus(); const webcamStatus = webcamRecordingService.getStatus(); const statusMsg = [ "🤖 **Teto Recording Status (Docker)**", `📹 Active stream recordings: ${videoStatus.activeRecordings}`, `đŸŽĨ Active webcam recordings: ${webcamStatus.activeWebcamRecordings}`, `📁 Container output: ${videoStatus.outputDir}`, "📁 Host output: ./output (volume mounted)", `đŸŽŦ Stream video files: ${videoStatus.videoFiles}`, `đŸŽĨ Webcam video files: ${webcamStatus.webcamFiles}`, videoStatus.videoFiles > 0 ? `📋 Recent stream files: ${videoStatus.recentFiles.join(", ")}` : "", webcamStatus.webcamFiles > 0 ? `📋 Recent webcam files: ${webcamStatus.recentWebcamFiles.join( ", " )}` : "", videoStatus.activeRecordings > 0 ? `🔴 Currently recording streams in: ${videoStatus.activeRecordingsList .map((r) => r.voiceChannelName) .join(", ")}` : "", webcamStatus.activeWebcamRecordings > 0 ? `đŸŽĨ Currently recording webcams in: ${webcamStatus.activeWebcamRecordingsList .map((r) => r.voiceChannelName) .join(", ")}` : "", `⏰ Bot uptime: ${Math.round(process.uptime())}s`, "đŸŗ Running in Docker container", ] .filter((line) => line) .join("\n"); msg.channel.send(statusMsg); return { action: { message: `Webcam status requested by ${msg.author.tag}`, channel: `#${msg.channel.name || "DM"}`, icon: "📊", }, }; }, }); // Debug voice state command this.commands.set("debug_voice", { trigger: (msg) => msg.content.toLowerCase() === "debug voice", execute: async (msg, context) => { const member = msg.guild?.members.cache.get(msg.author.id); const voiceChannel = member?.voice?.channel; if (!voiceChannel) { msg.channel.send( "❌ You need to be in a voice channel for voice state debugging!" ); return null; } const voiceState = voiceChannel.guild?.voiceStates.cache.get(msg.author.id) || context.client.voiceStates.cache.get(msg.author.id); // Get voice connection and SSRC info if available const voiceConnection = context.client.voice.connections.get( voiceChannel.guild.id ); let ssrcInfo = []; if (voiceConnection && voiceConnection.ssrcMap) { ssrcInfo = [ "", "**🔊 SSRC Mapping:**", `â€ĸ Connection status: ${voiceConnection.status}`, `â€ĸ Total mapped SSRCs: ${voiceConnection.ssrcMap.size}`, ...Array.from(voiceConnection.ssrcMap.entries()).map( ([ssrc, data]) => { const streamType = data.streamType ? ` (${data.streamType})` : ""; const quality = data.quality ? ` Q:${data.quality}` : ""; return `â€ĸ SSRC ${ssrc}: ${data.userId}${streamType}${quality}`; } ), ]; } const debugInfo = [ "🔍 **Voice State Debug Info**", `📍 Channel: ${voiceChannel.name} (${voiceChannel.id})`, `👤 User: ${msg.author.tag} (${msg.author.id})`, "", "**Voice State Properties:**", `â€ĸ Has voice state: ${!!voiceState}`, `â€ĸ Self video: ${voiceState?.selfVideo ?? "undefined"}`, `â€ĸ Streaming: ${voiceState?.streaming ?? "undefined"}`, `â€ĸ Server mute: ${voiceState?.serverMute ?? "undefined"}`, `â€ĸ Server deaf: ${voiceState?.serverDeaf ?? "undefined"}`, `â€ĸ Self mute: ${voiceState?.selfMute ?? "undefined"}`, `â€ĸ Self deaf: ${voiceState?.selfDeaf ?? "undefined"}`, `â€ĸ Channel ID: ${voiceState?.channelId ?? "undefined"}`, `â€ĸ Session ID: ${voiceState?.sessionId ?? "undefined"}`, "", "**Other Users in Channel:**", ...voiceChannel.members.map((m) => { const otherState = voiceChannel.guild?.voiceStates.cache.get(m.id); return `â€ĸ ${m.user.tag}: selfVideo=${otherState?.selfVideo}, streaming=${otherState?.streaming}`; }), ...ssrcInfo, ].join("\n"); msg.channel.send(debugInfo); return { action: { message: `Voice debug requested by ${msg.author.tag}`, channel: `#${msg.channel.name}`, icon: "🔍", }, }; }, }); // Test voice connectivity command this.commands.set("test_voice", { trigger: (msg) => msg.content.toLowerCase() === "test voice", execute: async (msg, context) => { const member = msg.guild?.members.cache.get(msg.author.id); const voiceChannel = member?.voice?.channel; if (!voiceChannel) { msg.channel.send( "❌ You need to be in a voice channel to test voice connectivity!" ); return null; } msg.channel.send("🔧 Testing voice server connectivity..."); try { const testStart = Date.now(); const connection = await context.client.voice.joinChannel( voiceChannel.id, { selfMute: true, selfDeaf: true, selfVideo: false, } ); const connectTime = Date.now() - testStart; msg.channel.send( `✅ Voice connection successful!\n` + `â€ĸ Connect time: ${connectTime}ms\n` + `â€ĸ Voice server: ${connection.voiceServerURL || "Unknown"}\n` + `â€ĸ Status: ${connection.status}\n` + `â€ĸ Channel: ${voiceChannel.name}` ); // Disconnect after test setTimeout(() => { connection.disconnect(); }, 2000); return { action: { message: `Voice connectivity test by ${msg.author.tag}`, channel: `#${msg.channel.name}`, icon: "🔧", }, }; } catch (error) { msg.channel.send( `❌ Voice connection failed!\n` + `â€ĸ Error: ${error.message}\n` + `â€ĸ This may indicate Discord voice server issues\n` + `â€ĸ Try again in a few minutes` ); return { action: { message: `Voice connectivity test failed by ${msg.author.tag}`, channel: `#${msg.channel.name}`, icon: "❌", }, }; } }, }); // Help command this.commands.set("help", { trigger: (msg) => msg.content.toLowerCase() === "teto help" || msg.content.toLowerCase() === "help", execute: async (msg, context) => { const helpMsg = [ "🤖 **Teto Recording Bot Commands**", "", "**📹 Stream Recording:**", "`xbox record that` - Start recording screen share", "`stop recording` or `xbox stop` - Stop stream recording", "", "**đŸŽĨ Webcam Recording:**", "`record webcam` or `xbox record webcam` - Start recording webcam", "`stop webcam` or `xbox stop webcam` - Stop webcam recording", "", "**📊 Status & Info:**", "`teto status` - Show overall recording status", "`webcam status` - Show detailed webcam status", "`debug voice` - Show voice state debug info", "`test voice` - Test voice server connectivity", "`teto help` or `help` - Show this help message", "", "**đŸ’Ŧ General:**", "`hello teto` - Say hello", "", "**Notes:**", "â€ĸ You must be in a voice channel to start recordings", "â€ĸ For webcam recording, you must have your camera turned ON in Discord", "â€ĸ Recordings auto-stop after 30 seconds (demo limit)", "â€ĸ Both stream and webcam can be recorded simultaneously", "â€ĸ Files are saved to ./output/ directory", ].join("\n"); msg.channel.send(helpMsg); return { action: { message: `Help requested by ${msg.author.tag}`, channel: `#${msg.channel.name || "DM"}`, icon: "❓", }, }; }, }); // Hello teto command this.commands.set("hello", { trigger: (msg) => msg.content.toLowerCase().includes("hello teto"), execute: async (msg, context) => { msg.channel.send("Hello there!"); return { action: { message: `Responded to ${msg.author.tag}`, channel: `#${msg.channel.name || "DM"}`, icon: "đŸ’Ŧ", }, }; }, }); } /** * Process a message and execute matching commands * @param {Object} msg - Discord message object * @param {Object} context - Context object with client, logItem, etc. * @returns {Promise} Array of actions to log */ async processMessage(msg, context) { const actions = []; for (const [commandName, command] of this.commands) { if (command.trigger(msg)) { try { const result = await command.execute(msg, context); if (result && result.action) { actions.push(result.action); } // Only execute the first matching command break; } catch (error) { console.error(`Error executing command ${commandName}:`, error); msg.channel.send("An error occurred while processing your command."); } } } return actions; } /** * Get list of available commands * @returns {Array} Array of command names */ getAvailableCommands() { return Array.from(this.commands.keys()); } /** * Add a new command * @param {string} name - Command name * @param {Object} command - Command object with trigger and execute functions */ addCommand(name, command) { this.commands.set(name, command); } /** * Remove a command * @param {string} name - Command name to remove */ removeCommand(name) { return this.commands.delete(name); } } // Export singleton instance export const commandHandler = new CommandHandler(); export default commandHandler;