UNPKG

n8n-nodes-enhanced-chat

Version:

Enhanced Chat UI for n8n - Complete interactive chat solution with Enhanced Chat Trigger, Chat UI Renderer, HTML Renderer, and Callback Handler nodes. Features dynamic forms, media, WebRTC, and interactive workflows.

645 lines (644 loc) 29.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CallbackHandler = void 0; const n8n_workflow_1 = require("n8n-workflow"); class CallbackHandler { constructor() { this.description = { displayName: 'Callback Handler', name: 'callbackHandler', icon: 'file:callbackHandler.svg', group: ['trigger'], version: 1, description: 'Handles form submissions and user interactions from HTML Renderer content', defaults: { name: 'Callback Handler', }, inputs: [], outputs: ["main" /* NodeConnectionType.Main */], webhooks: [ { name: 'default', httpMethod: 'POST', responseMode: 'onReceived', path: 'callback', }, ], properties: [ // === CALLBACK CONFIGURATION === { displayName: 'Callback Type', name: 'callbackType', type: 'options', options: [ { name: 'Form Submission', value: 'form', description: 'Handle form data submissions', }, { name: 'Media Interaction', value: 'media', description: 'Handle video/audio player events', }, { name: 'WebRTC Event', value: 'webrtc', description: 'Handle WebRTC communication events', }, { name: 'Generic Interaction', value: 'generic', description: 'Handle any user interaction event', }, ], default: 'form', description: 'Type of callback to handle', }, // === RESPONSE CONFIGURATION === { displayName: 'Response Configuration', name: 'responseConfig', type: 'collection', placeholder: 'Add Response Setting', default: {}, options: [ { displayName: 'Response Mode', name: 'responseMode', type: 'options', options: [ { name: 'JSON Response', value: 'json', description: 'Send JSON response back to frontend', }, { name: 'HTML Response', value: 'html', description: 'Send HTML content as response', }, { name: 'Redirect', value: 'redirect', description: 'Redirect to another URL', }, { name: 'Success Message', value: 'message', description: 'Send success/error message', }, ], default: 'json', }, { displayName: 'Success Message', name: 'successMessage', type: 'string', default: 'Thank you! Your submission has been received.', description: 'Message to display on successful submission', displayOptions: { show: { responseMode: ['message', 'json'], }, }, }, { displayName: 'Error Message', name: 'errorMessage', type: 'string', default: 'Sorry, there was an error processing your submission.', description: 'Message to display on error', displayOptions: { show: { responseMode: ['message', 'json'], }, }, }, { displayName: 'Redirect URL', name: 'redirectUrl', type: 'string', default: '', description: 'URL to redirect to after processing', displayOptions: { show: { responseMode: ['redirect'], }, }, }, { displayName: 'Custom HTML Response', name: 'customHtml', type: 'string', typeOptions: { rows: 5, }, default: '<div class="success-message"><h3>Success!</h3><p>Your submission has been processed.</p></div>', description: 'Custom HTML to return as response', displayOptions: { show: { responseMode: ['html'], }, }, }, ], }, // === VALIDATION & SECURITY === { displayName: 'Validation & Security', name: 'validationConfig', type: 'collection', placeholder: 'Add Validation Setting', default: {}, options: [ { displayName: 'Validate Session ID', name: 'validateSession', type: 'boolean', default: true, description: 'Require valid session ID for processing', }, { displayName: 'Allowed Origins', name: 'allowedOrigins', type: 'string', default: '*', description: 'Comma-separated list of allowed origins for CORS', }, { displayName: 'Rate Limiting', name: 'rateLimiting', type: 'options', options: [ { name: 'None', value: 'none' }, { name: 'Basic (10/min)', value: 'basic' }, { name: 'Moderate (5/min)', value: 'moderate' }, { name: 'Strict (2/min)', value: 'strict' }, ], default: 'basic', description: 'Rate limiting for callback requests', }, { displayName: 'Required Fields', name: 'requiredFields', type: 'string', default: '', description: 'Comma-separated list of required field names (for form callbacks)', displayOptions: { show: { callbackType: ['form'], }, }, }, { displayName: 'Max File Size (MB)', name: 'maxFileSize', type: 'number', default: 10, description: 'Maximum file size for uploads in MB', }, ], }, // === DATA PROCESSING === { displayName: 'Data Processing', name: 'dataProcessing', type: 'collection', placeholder: 'Add Processing Setting', default: {}, options: [ { displayName: 'Store Submissions', name: 'storeSubmissions', type: 'boolean', default: true, description: 'Store callback data for later retrieval', }, { displayName: 'Auto-parse JSON', name: 'autoParseJson', type: 'boolean', default: true, description: 'Automatically parse JSON data in request body', }, { displayName: 'Include Headers', name: 'includeHeaders', type: 'boolean', default: false, description: 'Include request headers in output data', }, { displayName: 'Include Query Parameters', name: 'includeQuery', type: 'boolean', default: true, description: 'Include URL query parameters in output data', }, { displayName: 'Data Transformation', name: 'dataTransform', type: 'options', options: [ { name: 'None', value: 'none' }, { name: 'Flatten Object', value: 'flatten' }, { name: 'Key-Value Pairs', value: 'keyvalue' }, { name: 'Form Data Format', value: 'formdata' }, ], default: 'none', description: 'How to transform the incoming data', }, ], }, // === NOTIFICATION SETTINGS === { displayName: 'Notification Settings', name: 'notificationConfig', type: 'collection', placeholder: 'Add Notification Setting', default: {}, options: [ { displayName: 'Send Email Notification', name: 'sendEmail', type: 'boolean', default: false, description: 'Send email notification on callback', }, { displayName: 'Email Recipients', name: 'emailRecipients', type: 'string', default: '', description: 'Comma-separated email addresses for notifications', displayOptions: { show: { sendEmail: [true], }, }, }, { displayName: 'Webhook URL', name: 'webhookUrl', type: 'string', default: '', description: 'External webhook URL to notify on callback', }, { displayName: 'Slack Webhook', name: 'slackWebhook', type: 'string', default: '', description: 'Slack webhook URL for notifications', }, ], }, ], }; } trigger() { return __awaiter(this, void 0, void 0, function* () { const webhookUrl = this.getInstanceBaseUrl() + '/webhook/' + this.getNode().id; // Get configuration const callbackType = this.getNodeParameter('callbackType'); const responseConfig = this.getNodeParameter('responseConfig'); const validationConfig = this.getNodeParameter('validationConfig'); const dataProcessing = this.getNodeParameter('dataProcessing'); const notificationConfig = this.getNodeParameter('notificationConfig'); // Store configuration for webhook handler CallbackHandler.callbackSessions.set(this.getNode().id, { callbackType, responseConfig, validationConfig, dataProcessing, notificationConfig, webhookUrl, }); return { manualTriggerFunction: () => __awaiter(this, void 0, void 0, function* () { // Manual trigger just shows the webhook URL console.log('Callback Handler is ready', { webhookUrl, callbackType }); }), }; }); } webhook() { return __awaiter(this, void 0, void 0, function* () { const nodeId = this.getNode().id; const sessionConfig = CallbackHandler.callbackSessions.get(nodeId); if (!sessionConfig) { return { webhookResponse: { status: 500, body: { error: 'Callback handler not configured' }, }, workflowData: [[]], }; } try { // Get request data const body = this.getBodyData(); const headers = this.getHeaderData(); const query = this.getQueryData(); const method = this.getRequestObject().method; // Validate CORS and extract session const origin = headers.origin || ''; const allowedOrigins = sessionConfig.validationConfig.allowedOrigins || '*'; const sessionId = (body === null || body === void 0 ? void 0 : body.sessionId) || (query === null || query === void 0 ? void 0 : query.sessionId) || ''; // Basic validation if (sessionConfig.validationConfig.validateSession && !sessionId) { return CallbackHandler.buildErrorResponse('Session ID required', 400, allowedOrigins); } // Process form submission (simplified) let processedData = { type: sessionConfig.callbackType, data: body, sessionId: sessionId, validated: true, }; // Store submission if enabled if (sessionConfig.dataProcessing.storeSubmissions && sessionId) { if (!CallbackHandler.submissionHistory.has(sessionId)) { CallbackHandler.submissionHistory.set(sessionId, []); } CallbackHandler.submissionHistory.get(sessionId).push({ timestamp: new Date().toISOString(), data: processedData, origin: origin, }); } // Build response const response = CallbackHandler.buildSuccessResponse(processedData, sessionConfig.responseConfig, allowedOrigins); return { webhookResponse: response.webhookResponse, workflowData: [ [ { json: { callbackType: sessionConfig.callbackType, sessionId: sessionId, timestamp: new Date().toISOString(), origin: origin, data: processedData, metadata: { method: method, userAgent: headers['user-agent'], ip: headers['x-forwarded-for'] || headers['x-real-ip'] || 'unknown', }, }, }, ], ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return this.buildErrorResponse(`Callback processing failed: ${errorMessage}`, 500, '*'); } }); } // Process different types of callbacks processCallback(callbackType, requestData, config) { return __awaiter(this, void 0, void 0, function* () { const { body, headers, query, method } = requestData; switch (callbackType) { case 'form': return this.processFormCallback(body, config); case 'media': return this.processMediaCallback(body, config); case 'webrtc': return this.processWebRTCCallback(body, config); case 'generic': default: return this.processGenericCallback(body, headers, query, config); } }); } processFormCallback(body, config) { return __awaiter(this, void 0, void 0, function* () { const formData = body; // Validate required fields const requiredFields = config.validationConfig.requiredFields || ''; if (requiredFields) { const required = requiredFields.split(',').map(f => f.trim()); for (const field of required) { if (!formData[field] || formData[field] === '') { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Required field missing: ${field}`); } } } // Transform data if needed const transformType = config.dataProcessing.dataTransform; let processedData = formData; if (transformType === 'flatten') { processedData = this.flattenObject(formData); } else if (transformType === 'keyvalue') { processedData = this.convertToKeyValue(formData); } else if (transformType === 'formdata') { processedData = this.formatAsFormData(formData); } return { type: 'form_submission', fields: processedData, fieldCount: Object.keys(processedData).length, validated: true, }; }); } processMediaCallback(body, config) { return __awaiter(this, void 0, void 0, function* () { const mediaData = body; return { type: 'media_interaction', event: mediaData.event || 'unknown', mediaType: mediaData.mediaType || 'unknown', currentTime: mediaData.currentTime || 0, duration: mediaData.duration || 0, volume: mediaData.volume || 1, data: mediaData, }; }); } processWebRTCCallback(body, config) { return __awaiter(this, void 0, void 0, function* () { const webrtcData = body; return { type: 'webrtc_event', event: webrtcData.event || 'unknown', roomId: webrtcData.roomId || '', peerId: webrtcData.peerId || '', connectionState: webrtcData.connectionState || 'unknown', mediaConstraints: webrtcData.mediaConstraints || {}, data: webrtcData, }; }); } processGenericCallback(body, headers, query, config) { return __awaiter(this, void 0, void 0, function* () { let data = Object.assign({}, body); // Include headers if configured if (config.dataProcessing.includeHeaders) { data.headers = headers; } // Include query if configured if (config.dataProcessing.includeQuery) { data.query = query; } return { type: 'generic_interaction', data: data, }; }); } // Helper methods validateOrigin(origin, allowedOrigins) { if (allowedOrigins === '*') return true; const allowed = allowedOrigins.split(',').map(o => o.trim()); return allowed.includes(origin); } buildSuccessResponse(data, responseConfig, allowedOrigins) { const responseMode = responseConfig.responseMode || 'json'; const successMessage = responseConfig.successMessage || 'Success'; const headers = { 'Access-Control-Allow-Origin': allowedOrigins, 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }; switch (responseMode) { case 'html': return { webhookResponse: { status: 200, headers: Object.assign(Object.assign({}, headers), { 'Content-Type': 'text/html' }), body: responseConfig.customHtml || '<div>Success!</div>', }, }; case 'redirect': return { webhookResponse: { status: 302, headers: Object.assign(Object.assign({}, headers), { Location: responseConfig.redirectUrl || '/' }), body: '', }, }; case 'message': return { webhookResponse: { status: 200, headers: Object.assign(Object.assign({}, headers), { 'Content-Type': 'application/json' }), body: { success: true, message: successMessage, timestamp: new Date().toISOString(), }, }, }; case 'json': default: return { webhookResponse: { status: 200, headers: Object.assign(Object.assign({}, headers), { 'Content-Type': 'application/json' }), body: { success: true, message: successMessage, data: data, timestamp: new Date().toISOString(), }, }, }; } } buildErrorResponse(message, status, allowedOrigins) { return { webhookResponse: { status: status, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': allowedOrigins, 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, body: { success: false, error: message, timestamp: new Date().toISOString(), }, }, workflowData: [[]], }; } sendNotifications(data, notificationConfig) { return __awaiter(this, void 0, void 0, function* () { // Email notifications if (notificationConfig.sendEmail && notificationConfig.emailRecipients) { // Email notification logic would go here console.log('Email notification:', { recipients: notificationConfig.emailRecipients, data }); } // Webhook notifications if (notificationConfig.webhookUrl) { // External webhook notification logic would go here console.log('Webhook notification:', { url: notificationConfig.webhookUrl, data }); } // Slack notifications if (notificationConfig.slackWebhook) { // Slack notification logic would go here console.log('Slack notification:', { webhook: notificationConfig.slackWebhook, data }); } }); } // Data transformation helpers flattenObject(obj, prefix = '') { const flattened = {}; for (const key in obj) { const newKey = prefix ? `${prefix}.${key}` : key; if (obj[key] !== null && typeof obj[key] === 'object' && !Array.isArray(obj[key])) { Object.assign(flattened, this.flattenObject(obj[key], newKey)); } else { flattened[newKey] = obj[key]; } } return flattened; } convertToKeyValue(obj) { const keyValue = { keys: Object.keys(obj), values: Object.values(obj), pairs: Object.entries(obj).map(([key, value]) => ({ key, value })), }; return keyValue; } formatAsFormData(obj) { const formData = {}; for (const [key, value] of Object.entries(obj)) { if (Array.isArray(value)) { formData[key] = value.join(', '); } else if (typeof value === 'object' && value !== null) { formData[key] = JSON.stringify(value); } else { formData[key] = String(value); } } return formData; } } exports.CallbackHandler = CallbackHandler; // Store callback sessions and data CallbackHandler.callbackSessions = new Map(); CallbackHandler.submissionHistory = new Map();