UNPKG

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
"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(/&amp;|&lt;|&gt;|&#39;|&quot;/g, (match) => { switch (match) { case '&amp;': return '&'; case '&lt;': return '<'; case '&gt;': return '>'; case '&#39;': return "'"; case '&quot;': 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