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
JavaScript
"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();