// src/integrations/webex/BotClient.js import axios from 'axios'; import FormData from 'form-data'; import { logger } from '../../utils/logger.js'; class BotClient { static #instance = null; constructor() { if (BotClient.#instance) { return BotClient.#instance; } const token = process.env.WEBEX_BOT_TOKEN; const baseURL = process.env.WEBEX_BASE_URL || 'https://webexapis.com/v1'; if (!token) { logger('webex:bot', 'WEBEX_BOT_TOKEN is missing – messaging will fail', 'error'); throw new Error('Missing WEBEX_BOT_TOKEN environment variable'); } this.axios = axios.create({ baseURL, timeout: 15000, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }); // Global error interceptor for better logging this.axios.interceptors.response.use( (response) => response, (err) => { const msg = err.response ? `${err.response.status} - ${JSON.stringify(err.response.data || {})}` : err.message; logger('webex:bot', `API error: ${msg}`, 'error'); return Promise.reject(err); } ); BotClient.#instance = this; logger('webex:bot', 'BotClient initialized with bot token from environment'); } async sendMarkdown(roomId, markdown, textFallback = null) { if (!roomId || !markdown) { logger('webex:bot', 'sendMarkdown called with missing roomId or markdown', 'warn'); return null; } const payload = { roomId, markdown }; if (textFallback) payload.text = textFallback; try { const response = await this.axios.post('/messages', payload); logger('webex:bot', `Sent markdown message to room ${roomId.slice(0, 8)}...`); return response.data; } catch (err) { // Error already logged by interceptor throw err; } } async sendWithAttachment(roomId, buffer, fileName, contentType = 'application/octet-stream', text = 'Attached file') { if (!roomId || !buffer || !fileName) { logger('webex:bot', 'sendWithAttachment called with missing parameters', 'warn'); return null; } try { const form = new FormData(); form.append('roomId', roomId); form.append('text', text); form.append('files', buffer, { filename: fileName, contentType: contentType }); const response = await this.axios.post('/messages', form, { headers: form.getHeaders(), // Let form-data set the correct Content-Type with boundary }); logger('webex:bot', `Sent attachment "${fileName}" to room ${roomId.slice(0, 8)}...`); return response.data; } catch (err) { logger('webex:bot', `Failed to send attachment ${fileName}: ${err.message}`, 'error'); throw err; } } async getRoomDetails(roomId) { try { const response = await this.axios.get(`/rooms/${roomId}`); logger('webex:bot', `Retrieved details for room ${roomId}`); return response.data; } catch (err) { logger('webex:bot', `Failed to get room details for ${roomId}: ${err.message}`, 'warn'); return null; } } async addMemberToRoom(roomId, personEmail) { try { const response = await this.axios.post('/memberships', { roomId, personEmail }); logger('webex:bot', `Added member ${personEmail} to room ${roomId}`); return response.data; } catch (err) { logger('webex:bot', `Failed to add member ${personEmail} to room ${roomId}: ${err.message}`, 'error'); throw err; } } } // Export singleton instance export default new BotClient();