n8n-nodes-feishu-lark
Version:
n8n custom nodes for n8n to interact with Feishu/Lark, including Lark Bot, Lark MCP, and Lark Trigger.
598 lines • 22.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeCustomCss = sanitizeCustomCss;
exports.createDescriptionMetadata = createDescriptionMetadata;
exports.prepareFormData = prepareFormData;
exports.getResolvables = getResolvables;
exports.resolveRawData = resolveRawData;
exports.addFormResponseDataToReturnItem = addFormResponseDataToReturnItem;
exports.prepareFormReturnItem = prepareFormReturnItem;
exports.escapeHtml = escapeHtml;
exports.getSendAndWaitConfig = getSendAndWaitConfig;
exports.createSendAndWaitMessageBody = createSendAndWaitMessageBody;
exports.configureWaitTillDate = configureWaitTillDate;
exports.sendAndWaitWebhook = sendAndWaitWebhook;
const isbot_1 = require("isbot");
const luxon_1 = require("luxon");
const n8n_workflow_1 = require("n8n-workflow");
const sanitize_html_1 = __importDefault(require("sanitize-html"));
const templates_1 = require("../templates");
const cache_1 = require("../../lark-sdk/handler/cache");
const RequestUtils_1 = __importDefault(require("./RequestUtils"));
const node_1 = __importDefault(require("./node"));
const INPUT_FIELD_IDENTIFIER = 'field-0';
const getFormResponseCustomizations = (context) => {
const message = context.getNodeParameter('message', '');
const options = context.getNodeParameter('options', {});
let formTitle = '';
if (options.responseFormTitle) {
formTitle = options.responseFormTitle;
}
let formDescription = message;
if (options.responseFormDescription) {
formDescription = options.responseFormDescription;
}
formDescription = formDescription.replace(/\\n/g, '\n').replace(/<br>/g, '\n');
let buttonLabel = 'Submit';
if (options.responseFormButtonLabel) {
buttonLabel = options.responseFormButtonLabel;
}
return {
formTitle,
formDescription,
buttonLabel,
customCss: options.responseFormCustomCss,
};
};
function sanitizeCustomCss(css) {
if (!css)
return undefined;
return (0, sanitize_html_1.default)(css, {
allowedTags: [],
allowedAttributes: {},
});
}
function createDescriptionMetadata(description) {
return description === ''
? 'n8n form'
: description.replace(/^\s*\n+|<\/?[^>]+(>|$)/g, '').slice(0, 150);
}
function prepareFormData({ formTitle, formDescription, formSubmittedHeader, formSubmittedText, redirectUrl, formFields, testRun, query, instanceId, useResponseData, appendAttribution = true, buttonLabel, customCss, }) {
var _a, _b, _c, _d, _e;
const utm_campaign = instanceId ? `&utm_campaign=${instanceId}` : '';
const n8nWebsiteLink = `https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger${utm_campaign}`;
if (formSubmittedText === undefined) {
formSubmittedText = 'Your response has been recorded';
}
const formData = {
testRun,
formTitle,
formDescription,
formDescriptionMetadata: createDescriptionMetadata(formDescription),
formSubmittedHeader,
formSubmittedText,
n8nWebsiteLink,
formFields: [],
useResponseData,
appendAttribution,
buttonLabel,
dangerousCustomCss: sanitizeCustomCss(customCss),
};
if (redirectUrl) {
if (!redirectUrl.includes('://')) {
redirectUrl = `http://${redirectUrl}`;
}
formData.redirectUrl = redirectUrl;
}
for (const [index, field] of formFields.entries()) {
const { fieldType, requiredField, multiselect, placeholder } = field;
const input = {
id: `field-${index}`,
errorId: `error-field-${index}`,
label: field.fieldLabel,
inputRequired: requiredField ? 'form-required' : '',
defaultValue: (_a = query[field.fieldLabel]) !== null && _a !== void 0 ? _a : '',
placeholder,
};
if (multiselect) {
input.isMultiSelect = true;
input.multiSelectOptions =
(_c = (_b = field.fieldOptions) === null || _b === void 0 ? void 0 : _b.values.map((e, i) => ({
id: `option${i}_${input.id}`,
label: e.option,
}))) !== null && _c !== void 0 ? _c : [];
}
else if (fieldType === 'file') {
input.isFileInput = true;
input.acceptFileTypes = field.acceptFileTypes;
input.multipleFiles = field.multipleFiles ? 'multiple' : '';
}
else if (fieldType === 'dropdown') {
input.isSelect = true;
const fieldOptions = (_e = (_d = field.fieldOptions) === null || _d === void 0 ? void 0 : _d.values) !== null && _e !== void 0 ? _e : [];
input.selectOptions = fieldOptions.map((e) => e.option);
}
else if (fieldType === 'textarea') {
input.isTextarea = true;
}
else if (fieldType === 'html') {
input.isHtml = true;
input.html = field.html;
}
else if (fieldType === 'hiddenField') {
input.isHidden = true;
input.hiddenName = field.fieldName;
input.hiddenValue =
input.defaultValue === '' ? field.fieldValue : input.defaultValue;
}
else {
input.isInput = true;
input.type = fieldType;
}
formData.formFields.push(input);
}
return formData;
}
function getResolvables(expression) {
if (!expression)
return [];
const resolvables = [];
const resolvableRegex = /({{[\s\S]*?}})/g;
let match;
while ((match = resolvableRegex.exec(expression)) !== null) {
if (match[1]) {
resolvables.push(match[1]);
}
}
return resolvables;
}
function resolveRawData(context, rawData) {
const resolvables = getResolvables(rawData);
let returnData = rawData;
if (returnData.startsWith('=')) {
returnData = returnData.replace(/^=+/, '');
}
else {
return returnData;
}
if (resolvables.length) {
for (const resolvable of resolvables) {
const resolvedValue = context.evaluateExpression(`${resolvable}`);
if (typeof resolvedValue === 'object' && resolvedValue !== null) {
returnData = returnData.replace(resolvable, JSON.stringify(resolvedValue));
}
else {
returnData = returnData.replace(resolvable, resolvedValue);
}
}
}
return returnData;
}
function addFormResponseDataToReturnItem(returnItem, formFields, bodyData) {
var _a, _b;
for (const [index, field] of formFields.entries()) {
const key = `field-${index}`;
const name = (_a = field.fieldLabel) !== null && _a !== void 0 ? _a : field.fieldName;
let value = (_b = bodyData[key]) !== null && _b !== void 0 ? _b : null;
if (value === null) {
returnItem.json[name] = null;
continue;
}
if (field.fieldType === 'html') {
if (field.elementName) {
returnItem.json[field.elementName] = value;
}
continue;
}
if (field.fieldType === 'number') {
value = Number(value);
}
if (field.fieldType === 'text') {
value = String(value).trim();
}
if (field.multiselect && typeof value === 'string') {
value = (0, n8n_workflow_1.jsonParse)(value);
}
if (field.fieldType === 'date' && value && field.formatDate !== '') {
value = luxon_1.DateTime.fromFormat(String(value), 'yyyy-mm-dd').toFormat(field.formatDate);
}
if (field.fieldType === 'file' && field.multipleFiles && !Array.isArray(value)) {
value = [value];
}
returnItem.json[name] = value;
}
}
async function prepareFormReturnItem(context, formFields, mode, useWorkflowTimezone = false) {
var _a, _b, _c;
const bodyData = (_a = context.getBodyData().data) !== null && _a !== void 0 ? _a : {};
const files = (_b = context.getBodyData().files) !== null && _b !== void 0 ? _b : {};
const returnItem = {
json: {},
};
if (files && Object.keys(files).length) {
returnItem.binary = {};
}
for (const key of Object.keys(files)) {
const processFiles = [];
let multiFile = false;
const filesInput = files[key];
if (Array.isArray(filesInput)) {
bodyData[key] = filesInput.map((file) => ({
filename: file.originalFilename,
mimetype: file.mimetype,
size: file.size,
}));
processFiles.push(...filesInput);
multiFile = true;
}
else {
bodyData[key] = {
filename: filesInput.originalFilename,
mimetype: filesInput.mimetype,
size: filesInput.size,
};
processFiles.push(filesInput);
}
const entryIndex = Number(key.replace(/field-/g, ''));
const fieldLabel = isNaN(entryIndex) ? key : formFields[entryIndex].fieldLabel;
let fileCount = 0;
for (const file of processFiles) {
let binaryPropertyName = fieldLabel.replace(/\W/g, '_');
if (multiFile) {
binaryPropertyName += `_${fileCount++}`;
}
returnItem.binary[binaryPropertyName] = await context.nodeHelpers.copyBinaryFile(file.filepath, (_c = file.originalFilename) !== null && _c !== void 0 ? _c : file.newFilename, file.mimetype);
}
}
addFormResponseDataToReturnItem(returnItem, formFields, bodyData);
const timezone = useWorkflowTimezone ? context.getTimezone() : 'UTC';
returnItem.json.submittedAt = luxon_1.DateTime.now().setZone(timezone).toISO();
returnItem.json.formMode = mode;
if (context.getNode().type === n8n_workflow_1.FORM_TRIGGER_NODE_TYPE &&
Object.keys(context.getRequestObject().query || {}).length) {
returnItem.json.formQueryParameters = context.getRequestObject().query;
}
return returnItem;
}
function escapeHtml(text) {
if (!text)
return '';
return text.replace(/&|<|>|'|"/g, (match) => {
switch (match) {
case '&':
return '&';
case '<':
return '<';
case '>':
return '>';
case ''':
return "'";
case '"':
return '"';
default:
return match;
}
});
}
function getSendAndWaitConfig(context) {
const message = escapeHtml(node_1.default.getNodeParameter(context, 'message', '').trim())
.replace(/\\n/g, '\n')
.replace(/<br>/g, '\n');
const subject = escapeHtml(node_1.default.getNodeParameter(context, 'subject', ''));
const approvalOptions = node_1.default.getNodeParameter(context, 'approvalOptions.values', {});
const options = node_1.default.getNodeParameter(context, 'options', {});
const config = {
title: subject,
message,
options: [],
appendAttribution: options === null || options === void 0 ? void 0 : options.appendAttribution,
};
const responseType = node_1.default.getNodeParameter(context, 'responseType', 'approval');
if (typeof context.setSignatureValidationRequired === 'function') {
context.setSignatureValidationRequired();
}
const approvedSignedResumeUrl = context.getSignedResumeUrl({ approved: 'true' });
if (responseType === 'freeText' || responseType === 'customForm') {
const label = node_1.default.getNodeParameter(context, 'options.messageButtonLabel', 'Respond');
config.options.push({
label,
url: approvedSignedResumeUrl,
style: 'primary_filled',
});
}
else if (approvalOptions.approvalType === 'double') {
const approveLabel = escapeHtml(approvalOptions.approveLabel || 'Approve');
const buttonApprovalStyle = approvalOptions.buttonApprovalStyle || 'primary_filled';
const disapproveLabel = escapeHtml(approvalOptions.disapproveLabel || 'Disapprove');
const buttonDisapprovalStyle = approvalOptions.buttonDisapprovalStyle || 'danger_filled';
const disapprovedSignedResumeUrl = context.getSignedResumeUrl({ approved: 'false' });
config.options.push({
label: approveLabel,
url: approvedSignedResumeUrl,
style: buttonApprovalStyle,
});
config.options.push({
label: disapproveLabel,
url: disapprovedSignedResumeUrl,
style: buttonDisapprovalStyle,
});
}
else {
const label = escapeHtml(approvalOptions.approveLabel || 'Approve');
const style = approvalOptions.buttonApprovalStyle || 'primary_filled';
config.options.push({
label,
url: approvedSignedResumeUrl,
style,
});
}
return config;
}
function createButtonElements(options, disableValues) {
return options.map((option) => {
return {
tag: 'button',
text: {
tag: 'plain_text',
content: option.label,
},
type: option.style,
width: 'default',
size: 'medium',
disabled: disableValues.disabled,
disabled_tips: {
tag: 'plain_text',
content: disableValues.disabledTips,
},
behaviors: [
{
type: 'open_url',
default_url: option.url,
},
],
};
});
}
function createSendAndWaitMessageBody(context, disableValues = { disabled: false, disabledTips: '' }) {
const config = getSendAndWaitConfig(context);
const subject = config.title;
const message = config.message;
console.log('config value: ', config);
const content = {
schema: '2.0',
config: {
style: {
text_size: {
normal_v2: {
default: 'normal',
pc: 'normal',
mobile: 'heading',
},
},
},
},
body: {
direction: 'vertical',
padding: '12px 12px 12px 12px',
elements: [
{
tag: 'markdown',
content: message,
text_align: 'left',
text_size: 'normal_v2',
margin: '0px 0px 0px 0px',
},
{
tag: 'hr',
margin: '0px 0px 0px 0px',
},
{
tag: 'column_set',
horizontal_align: 'left',
columns: [
{
tag: 'column',
width: 'weighted',
elements: createButtonElements(config.options, disableValues),
direction: 'horizontal',
vertical_spacing: '8px',
horizontal_align: 'left',
vertical_align: 'top',
weight: 1,
},
],
margin: '0px 0px 0px 0px',
},
],
},
header: {
title: {
tag: 'plain_text',
content: `${subject}`,
},
template: 'blue',
icon: {
tag: 'standard_icon',
token: 'approval_colorful',
},
padding: '12px 12px 12px 12px',
},
};
return JSON.stringify(content);
}
function configureWaitTillDate(context) {
let waitTill = n8n_workflow_1.WAIT_INDEFINITELY;
const limitOptions = context.getNodeParameter('options.limitWaitTime.values', 0, {});
if (Object.keys(limitOptions).length) {
try {
if (limitOptions.limitType === 'afterTimeInterval') {
let waitAmount = limitOptions.resumeAmount;
if (limitOptions.resumeUnit === 'minutes') {
waitAmount *= 60;
}
if (limitOptions.resumeUnit === 'hours') {
waitAmount *= 60 * 60;
}
if (limitOptions.resumeUnit === 'days') {
waitAmount *= 60 * 60 * 24;
}
waitAmount *= 1000;
waitTill = new Date(new Date().getTime() + waitAmount);
}
else {
waitTill = new Date(limitOptions.maxDateAndTime);
}
if (isNaN(waitTill.getTime())) {
throw new n8n_workflow_1.ApplicationError('Invalid date format');
}
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Could not configure Limit Wait Time', {
description: error.message,
});
}
}
return waitTill;
}
async function getMessageId(context) {
return await cache_1.internalCache.get('sendAndWaitMessageId', {
namespace: `${context.getWorkflow().id}-${context.getExecutionId()}`,
});
}
async function editCardMessage(context) {
const disableButton = context.getNodeParameter('disableButton', false);
if (!disableButton) {
return;
}
const disabledTips = context.getNodeParameter('disableTips', '');
const messageId = await getMessageId(context);
if (!messageId) {
return;
}
await RequestUtils_1.default.request.call(context, {
method: 'PATCH',
url: `/open-apis/im/v1/messages/${messageId}`,
body: {
content: createSendAndWaitMessageBody(context, {
disabled: true,
disabledTips,
}),
},
});
}
async function sendAndWaitWebhook() {
const method = this.getRequestObject().method;
const res = this.getResponseObject();
const req = this.getRequestObject();
const responseType = this.getNodeParameter('responseType', 'approval');
if (responseType === 'approval' && (0, isbot_1.isbot)(req.headers['user-agent'])) {
res.send('');
return { noWebhookResponse: true };
}
if (responseType === 'freeText') {
if (method === 'GET') {
const { formTitle, formDescription, buttonLabel, customCss } = getFormResponseCustomizations(this);
const data = prepareFormData({
formTitle,
formDescription,
formSubmittedHeader: 'Got it, thanks',
formSubmittedText: 'This page can be closed now',
buttonLabel,
redirectUrl: undefined,
formFields: [
{
fieldLabel: 'Response',
fieldType: 'textarea',
requiredField: true,
},
],
testRun: false,
query: {},
customCss,
});
res.render('form-trigger', data);
return {
noWebhookResponse: true,
};
}
if (method === 'POST') {
await editCardMessage(this);
const data = this.getBodyData().data;
return {
webhookResponse: templates_1.ACTION_RECORDED_PAGE,
workflowData: [
[
{
json: {
data: { text: data[INPUT_FIELD_IDENTIFIER], messageId: await getMessageId(this) },
},
},
],
],
};
}
}
if (responseType === 'customForm') {
const defineForm = this.getNodeParameter('defineForm', 'fields');
let fields = [];
if (defineForm === 'json') {
try {
const jsonOutput = this.getNodeParameter('jsonOutput', '', {
rawExpressions: true,
});
fields = (0, n8n_workflow_1.tryToParseJsonToFormFields)(resolveRawData(this, jsonOutput));
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, {
description: error.message,
});
}
}
else {
fields = this.getNodeParameter('formFields.values', []);
}
if (method === 'GET') {
const { formTitle, formDescription, buttonLabel, customCss } = getFormResponseCustomizations(this);
const data = prepareFormData({
formTitle,
formDescription,
formSubmittedHeader: 'Got it, thanks',
formSubmittedText: 'This page can be closed now',
buttonLabel,
redirectUrl: undefined,
formFields: fields,
testRun: false,
query: {},
customCss,
});
res.render('form-trigger', data);
return {
noWebhookResponse: true,
};
}
if (method === 'POST') {
await editCardMessage(this);
const returnItem = await prepareFormReturnItem(this, fields, 'production', true);
const json = returnItem.json;
json.messageId = await getMessageId(this);
delete json.submittedAt;
delete json.formMode;
returnItem.json = { data: json };
return {
webhookResponse: templates_1.ACTION_RECORDED_PAGE,
workflowData: [[returnItem]],
};
}
}
await editCardMessage(this);
const query = req.query;
const approved = query.approved === 'true';
return {
webhookResponse: templates_1.ACTION_RECORDED_PAGE,
workflowData: [[{ json: { data: { approved, messageId: await getMessageId(this) } } }]],
};
}
//# sourceMappingURL=webhook.js.map