n8n-nodes-feishu-lark
Version:
n8n custom nodes for n8n to interact with Feishu/Lark, including Lark Bot, Lark MCP, and Lark Trigger.
757 lines • 27.8 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.larkApiRequestSheetList = larkApiRequestSheetList;
exports.larkApiRequestCalendarEventList = larkApiRequestCalendarEventList;
exports.larkApiRequestCalendarList = larkApiRequestCalendarList;
exports.larkApiRequestBaseRoleList = larkApiRequestBaseRoleList;
exports.larkApiRequestTableFieldList = larkApiRequestTableFieldList;
exports.larkApiRequestTableViewList = larkApiRequestTableViewList;
exports.larkApiRequestTableList = larkApiRequestTableList;
exports.larkApiRequestMessageResourceData = larkApiRequestMessageResourceData;
exports.getFileList = getFileList;
exports.sanitizeCustomCss = sanitizeCustomCss;
exports.createDescriptionMetadata = createDescriptionMetadata;
exports.prepareFormData = prepareFormData;
exports.getResolvables = getResolvables;
exports.resolveRawData = resolveRawData;
exports.addFormResponseDataToReturnItem = addFormResponseDataToReturnItem;
exports.prepareFormReturnItem = prepareFormReturnItem;
exports.sendAndWaitWebhook = sendAndWaitWebhook;
exports.escapeHtml = escapeHtml;
exports.getSendAndWaitConfig = getSendAndWaitConfig;
exports.createSendAndWaitMessageBody = createSendAndWaitMessageBody;
exports.configureWaitTillDate = configureWaitTillDate;
const n8n_workflow_1 = require("n8n-workflow");
const isbot_1 = require("isbot");
const sanitize_html_1 = __importDefault(require("sanitize-html"));
const luxon_1 = require("luxon");
const RequestUtils_1 = __importDefault(require("../help/utils/RequestUtils"));
const templates_1 = require("../help/templates");
const INPUT_FIELD_IDENTIFIER = 'field-0';
async function larkApiRequestSheetList(spreadsheetId) {
const { data: { sheets }, } = await RequestUtils_1.default.request.call(this, {
method: 'GET',
url: `/open-apis/sheets/v3/spreadsheets/${spreadsheetId}/sheets/query`,
});
return sheets;
}
async function larkApiRequestCalendarEventList(options) {
const { calendarId, query, user_id_type } = options;
const allEvents = [];
let hasMore = false;
let pageToken = '';
do {
const { data: { page_token, items }, } = await RequestUtils_1.default.request.call(this, {
method: 'POST',
url: `/open-apis/calendar/v4/calendars/${calendarId}/events/search`,
qs: {
user_id_type,
...(pageToken && { page_token: pageToken }),
page_size: 100,
},
body: {
query,
},
});
hasMore = page_token ? true : false;
pageToken = page_token;
if (items) {
allEvents.push(...items);
}
} while (hasMore);
return allEvents;
}
async function larkApiRequestCalendarList() {
const allCalendars = [];
let hasMore = false;
let pageToken = '';
do {
const { data: { has_more, page_token, calendar_list }, } = await RequestUtils_1.default.request.call(this, {
method: 'GET',
url: `/open-apis/calendar/v4/calendars`,
qs: {
...(pageToken && { page_token: pageToken }),
page_size: 1000,
},
});
hasMore = has_more;
pageToken = page_token;
if (calendar_list) {
allCalendars.push(...calendar_list);
}
} while (hasMore);
return allCalendars;
}
async function larkApiRequestBaseRoleList(options) {
const allRoles = [];
let hasMore = false;
let pageToken = '';
const { app_token } = options;
do {
const { data: { has_more, page_token, items }, } = await RequestUtils_1.default.request.call(this, {
method: 'GET',
url: `/open-apis/base/v2/apps/${app_token}/roles`,
qs: {
...(pageToken && { page_token: pageToken }),
page_size: 100,
},
});
hasMore = has_more;
pageToken = page_token;
if (items) {
allRoles.push(...items);
}
} while (hasMore);
return allRoles;
}
async function larkApiRequestTableFieldList(options) {
const allFields = [];
let hasMore = false;
let pageToken = '';
const { app_token, table_id } = options;
do {
const { data: { has_more, page_token, items }, } = await RequestUtils_1.default.request.call(this, {
method: 'GET',
url: `/open-apis/bitable/v1/apps/${app_token}/tables/${table_id}/fields`,
qs: {
page_token: pageToken,
page_size: 100,
},
});
hasMore = has_more;
pageToken = page_token;
if (items) {
allFields.push(...items);
}
} while (hasMore);
return allFields;
}
async function larkApiRequestTableViewList(options) {
const allViews = [];
let hasMore = false;
let pageToken = '';
const { app_token, table_id, user_id_type } = options;
do {
const { data: { has_more, page_token, items }, } = await RequestUtils_1.default.request.call(this, {
method: 'GET',
url: `/open-apis/bitable/v1/apps/${app_token}/tables/${table_id}/views`,
qs: {
page_token: pageToken,
page_size: 100,
user_id_type,
},
});
hasMore = has_more;
pageToken = page_token;
if (items) {
allViews.push(...items);
}
} while (hasMore);
return allViews;
}
async function larkApiRequestTableList(appToken) {
const allTables = [];
let hasMore = false;
let pageToken = '';
do {
const { data: { has_more, page_token, items }, } = await RequestUtils_1.default.request.call(this, {
method: 'GET',
url: `/open-apis/bitable/v1/apps/${appToken}/tables`,
qs: {
page_token: pageToken,
page_size: 100,
},
});
hasMore = has_more;
pageToken = page_token;
if (items) {
allTables.push(...items);
}
} while (hasMore);
return allTables;
}
async function larkApiRequestMessageResourceData(options) {
const { type, messageId, key } = options;
const data = await RequestUtils_1.default.request.call(this, {
method: 'GET',
url: `/open-apis/im/v1/messages/${messageId}/resources/${key}`,
qs: {
type,
},
encoding: 'arraybuffer',
json: false,
});
return Buffer.from(data).toString(n8n_workflow_1.BINARY_ENCODING);
}
async function getFileList(type, order_by = 'EditedTime', direction = 'DESC', user_id_type = 'open_id', folderToken) {
let hasMore = true;
let pageToken = '';
const allFiles = [];
while (hasMore) {
const response = await RequestUtils_1.default.request.call(this, {
method: 'GET',
url: `/open-apis/drive/v1/files`,
qs: {
page_size: 100,
page_token: pageToken,
folder_token: folderToken,
order_by,
direction,
user_id_type,
},
});
const responseData = response.data;
const { files, has_more, next_page_token: nextPageToken } = responseData;
hasMore = has_more;
pageToken = nextPageToken || '';
for (const file of files) {
if (type.includes(file.type)) {
allFiles.push(file);
}
if (file.type === "folder") {
const results = await getFileList.call(this, type, order_by, direction, user_id_type, file.token);
allFiles.push(...results);
}
}
}
return allFiles;
}
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;
}
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') {
const data = this.getBodyData().data;
return {
webhookResponse: templates_1.ACTION_RECORDED_PAGE,
workflowData: [[{ json: { data: { text: data[INPUT_FIELD_IDENTIFIER] } } }]],
};
}
}
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') {
const returnItem = await prepareFormReturnItem(this, fields, 'production', true);
const json = returnItem.json;
delete json.submittedAt;
delete json.formMode;
returnItem.json = { data: json };
return {
webhookResponse: templates_1.ACTION_RECORDED_PAGE,
workflowData: [[returnItem]],
};
}
}
const query = req.query;
const approved = query.approved === 'true';
return {
webhookResponse: templates_1.ACTION_RECORDED_PAGE,
workflowData: [[{ json: { data: { approved } } }]],
};
}
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(context.getNodeParameter('message', 0, '').trim())
.replace(/\\n/g, '\n')
.replace(/<br>/g, '\n');
const subject = escapeHtml(context.getNodeParameter('subject', 0, ''));
const approvalOptions = context.getNodeParameter('approvalOptions.values', 0, {});
const options = context.getNodeParameter('options', 0, {});
const config = {
title: subject,
message,
options: [],
appendAttribution: options === null || options === void 0 ? void 0 : options.appendAttribution,
};
const responseType = context.getNodeParameter('responseType', 0, 'approval');
context.setSignatureValidationRequired();
const approvedSignedResumeUrl = context.getSignedResumeUrl({ approved: 'true' });
if (responseType === 'freeText' || responseType === 'customForm') {
const label = context.getNodeParameter('options.messageButtonLabel', 0, '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) {
return options.map((option) => {
return {
tag: 'button',
text: {
tag: 'plain_text',
content: option.label,
i18n_content: {
en_us: option.label,
},
},
type: option.style,
width: 'default',
size: 'medium',
behaviors: [
{
type: 'open_url',
default_url: option.url,
},
],
};
});
}
function createSendAndWaitMessageBody(context) {
const config = getSendAndWaitConfig(context);
const subject = config.title;
const message = config.message;
const content = {
schema: '2.0',
config: {
locales: ['en_us'],
style: {
text_size: {
normal_v2: {
default: 'normal',
pc: 'normal',
mobile: 'heading',
},
},
},
},
body: {
direction: 'vertical',
padding: '12px 12px 12px 12px',
elements: [
{
tag: 'markdown',
content: message,
i18n_content: {
en_us: 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),
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}`,
i18n_content: {
en_us: `${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;
}
//# sourceMappingURL=GenericFunctions.js.map