From f85b15c0c024b4aad2cb58afc15bd601ff3cb338 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Thu, 17 Jul 2025 16:16:58 -0300 Subject: [PATCH 1/2] [Components] lark #17281 Sources - New User Added To Group Chat Actions - Create New Base Table Record - Create New Lark Document - Create Lark Spreadsheet - Add User To Lark Group Chat - Update Base Table Record - Create New Lark Folder - Create New Lark Calendar Event --- .../add-user-to-lark-group-chat.mjs | 46 +++ .../create-new-base-table-record.mjs | 52 +++ .../create-new-lark-calendar-event.mjs | 220 +++++++++++ .../create-new-lark-document.mjs | 37 ++ .../create-new-lark-folder.mjs | 34 ++ .../create-new-lark-spreadsheet.mjs | 37 ++ .../create-new-lark-task.mjs | 102 +++++ .../update-base-table-record.mjs | 67 ++++ components/lark/common/constants.mjs | 95 +++++ components/lark/common/utils.mjs | 24 ++ components/lark/lark.app.mjs | 356 +++++++++++++++++- components/lark/package.json | 7 +- .../new-user-added-to-group-chat.mjs | 77 ++++ .../test-event.mjs | 6 + 14 files changed, 1154 insertions(+), 6 deletions(-) create mode 100644 components/lark/actions/add-user-to-lark-group-chat/add-user-to-lark-group-chat.mjs create mode 100644 components/lark/actions/create-new-base-table-record/create-new-base-table-record.mjs create mode 100644 components/lark/actions/create-new-lark-calendar-event/create-new-lark-calendar-event.mjs create mode 100644 components/lark/actions/create-new-lark-document/create-new-lark-document.mjs create mode 100644 components/lark/actions/create-new-lark-folder/create-new-lark-folder.mjs create mode 100644 components/lark/actions/create-new-lark-spreadsheet/create-new-lark-spreadsheet.mjs create mode 100644 components/lark/actions/create-new-lark-task/create-new-lark-task.mjs create mode 100644 components/lark/actions/update-base-table-record/update-base-table-record.mjs create mode 100644 components/lark/common/constants.mjs create mode 100644 components/lark/common/utils.mjs create mode 100644 components/lark/sources/new-user-added-to-group-chat/new-user-added-to-group-chat.mjs create mode 100644 components/lark/sources/new-user-added-to-group-chat/test-event.mjs diff --git a/components/lark/actions/add-user-to-lark-group-chat/add-user-to-lark-group-chat.mjs b/components/lark/actions/add-user-to-lark-group-chat/add-user-to-lark-group-chat.mjs new file mode 100644 index 0000000000000..25d80515cea00 --- /dev/null +++ b/components/lark/actions/add-user-to-lark-group-chat/add-user-to-lark-group-chat.mjs @@ -0,0 +1,46 @@ +import { ConfigurationError } from "@pipedream/platform"; +import lark from "../../lark.app.mjs"; + +export default { + key: "lark-add-user-to-lark-group-chat", + name: "Add User to Lark Group Chat", + description: "Add a user to a Lark group chat. [See the documentation](https://open.larksuite.com/document/server-docs/historic-version/im-chat/group-chat/add-users-to-a-group-chat)", + version: "0.0.1", + type: "action", + props: { + lark, + chatId: { + propDefinition: [ + lark, + "chatId", + ], + }, + userId: { + propDefinition: [ + lark, + "userId", + ], + }, + }, + async run({ $ }) { + const response = await this.lark.addUserToGroupChat({ + $, + chatId: this.chatId, + data: { + id_list: [ + this.userId, + ], + }, + params: { + member_id_type: "user_id", + }, + }); + + if (response.code !== 0) { + throw new ConfigurationError(`Failed to add user to group chat: ${response.msg}`); + } + + $.export("$summary", `Successfully added user ${this.userId} to group chat with ID ${this.chatId}`); + return response; + }, +}; diff --git a/components/lark/actions/create-new-base-table-record/create-new-base-table-record.mjs b/components/lark/actions/create-new-base-table-record/create-new-base-table-record.mjs new file mode 100644 index 0000000000000..56db3ce1381b4 --- /dev/null +++ b/components/lark/actions/create-new-base-table-record/create-new-base-table-record.mjs @@ -0,0 +1,52 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import lark from "../../lark.app.mjs"; + +export default { + key: "lark-create-new-base-table-record", + name: "Create New Base Table Record", + description: "Creates a new record in a Lark base table. [See the documentation](https://open.larksuite.com/document/server-docs/docs/bitable-v1/app-table-record/create)", + version: "0.0.1", + type: "action", + props: { + lark, + baseToken: { + propDefinition: [ + lark, + "baseToken", + ], + }, + tableId: { + propDefinition: [ + lark, + "tableId", + ({ baseToken }) => ({ + baseToken, + }), + ], + }, + fields: { + propDefinition: [ + lark, + "fields", + ], + }, + }, + async run({ $ }) { + const response = await this.lark.createRecord({ + $, + baseToken: this.baseToken, + tableId: this.tableId, + data: { + fields: parseObject(this.fields), + }, + }); + + if (response.error) { + throw new ConfigurationError(response.msg); + } + + $.export("$summary", `Successfully created a new record in the table with ID ${response.data.record.record_id}`); + return response; + }, +}; diff --git a/components/lark/actions/create-new-lark-calendar-event/create-new-lark-calendar-event.mjs b/components/lark/actions/create-new-lark-calendar-event/create-new-lark-calendar-event.mjs new file mode 100644 index 0000000000000..dda5f54fe0d6e --- /dev/null +++ b/components/lark/actions/create-new-lark-calendar-event/create-new-lark-calendar-event.mjs @@ -0,0 +1,220 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + ATTENDEE_ABILITY_OPTIONS, + FREE_BUSY_STATUS_OPTIONS, + ICON_TYPE_OPTIONS, + VC_TYPE_OPTIONS, + VISIBILITY_OPTIONS, +} from "../../common/constants.mjs"; +import lark from "../../lark.app.mjs"; + +export default { + key: "lark-create-new-lark-calendar-event", + name: "Create New Lark Calendar Event", + description: "Creates a new event in Lark's calendar. [See the documentation](https://open.larksuite.com/document/server-docs/calendar-v4/calendar-event/create)", + version: "0.0.1", + type: "action", + props: { + lark, + calendarId: { + propDefinition: [ + lark, + "calendarId", + ], + }, + summary: { + type: "string", + label: "Summary", + description: "Event title.", + }, + description: { + type: "string", + label: "Description", + description: "Event description. Supports parsing HTML tags. **Note:** While HTML tags can be used to achieve some rich text formatting, the rich text formatting generated by the client is not implemented using HTML tags. If you update the description through the API after generating rich text formatting on the client side, it may result in the loss of the original rich text formatting applied by the client.", + optional: true, + }, + needNotification: { + type: "boolean", + label: "Need Notification", + description: "Whether to send Bot notifications to event participants when updating the event.", + optional: true, + }, + startTimeDate: { + type: "string", + label: "Start Time Date", + description: "Start time, only use this field for all-day events, [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339) format, for example, 2018-09-01.", + optional: true, + }, + startTimeTimestamp: { + type: "string", + label: "Start Time Timestamp", + description: "Second-level timestamp, used to set a specific start time. For example, 1602504000 means 2020/10/12 20:00:00 (UTC +8 time zone).", + optional: true, + }, + startTimeTimezone: { + type: "string", + label: "Start Time Timezone", + description: "Time zone. Use IANA Time Zone Database standards such as Asia/Shanghai.", + optional: true, + }, + endTimeDate: { + type: "string", + label: "End Time Date", + description: "End time, only use this field for all-day events, [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339) format, for example, 2018-09-01.", + optional: true, + }, + endTimeTimestamp: { + type: "string", + label: "End Time Timestamp", + description: "Second-level timestamp, used to set a specific end time. For example, 1602504000 means 2020/10/12 20:00:00 (UTC +8 time zone).", + optional: true, + }, + endTimeTimezone: { + type: "string", + label: "End Time Timezone", + description: "Time zone. Use IANA Time Zone Database standards such as Asia/Shanghai.", + optional: true, + }, + vcType: { + type: "string", + label: "VC Type", + description: "Video conferencing type. If video conferencing is not required, no_meeting must be passed. **Default value:** empty, which means the Lark video conference URL will be automatically generated when a event participant is added for the first time.", + options: VC_TYPE_OPTIONS, + optional: true, + }, + iconType: { + type: "string", + label: "Icon Type", + description: "The icon type of the video conferencing.", + options: ICON_TYPE_OPTIONS, + optional: true, + }, + vchatDescription: { + type: "string", + label: "VChat Description", + description: "Video conference copywriting.", + optional: true, + }, + meetingUrl: { + type: "string", + label: "Meeting URL", + description: "Video conference URL.", + optional: true, + }, + visibility: { + type: "string", + label: "Visibility", + description: "Event visibility. The value is Default for new events by default. This is valid for all invitees only when an event is created. Any changes to the property are valid only for the current identity.", + options: VISIBILITY_OPTIONS, + optional: true, + }, + attendeeAbility: { + type: "string", + label: "Attendee Ability", + description: "Event invitees' scopes.", + options: ATTENDEE_ABILITY_OPTIONS, + optional: true, + }, + freeBusyStatus: { + type: "string", + label: "Free/Busy Status", + description: "Event availability. The value is Busy for new events by default. This is valid for all invitees only when an event is created. Any changes to the property are valid only for the current identity.", + options: FREE_BUSY_STATUS_OPTIONS, + optional: true, + }, + locationName: { + type: "string", + label: "Location Name", + description: "Name of the location.", + optional: true, + }, + locationAddress: { + type: "string", + label: "Location Address", + description: "Address of the location.", + optional: true, + }, + locationLatitude: { + type: "string", + label: "Location Latitude", + description: "Latitude coordinate of the location. Locations within China Mainland should comply with the GCJ-02 standard, and locations outside China Mainland should comply with the WGS84 standard.", + optional: true, + }, + locationLongitude: { + type: "string", + label: "Location Longitude", + description: "Longitude coordinate of the location. Locations within China Mainland should comply with the GCJ-02 standard, and locations outside China Mainland should comply with the WGS84 standard.", + optional: true, + }, + color: { + type: "integer", + label: "Color", + description: "Event color, the value is represented by an int32 color RGB value.", + optional: true, + }, + recurrence: { + type: "string", + label: "Recurrence", + description: "Recurrence rule for recurring events. For details, see [RFC 5545](https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10). E.g. `FREQ=DAILY;INTERVAL=1`.", + optional: true, + }, + }, + async run({ $ }) { + if (!this.startTimeDate && !this.startTimeTimestamp) { + throw new ConfigurationError("You must provide at least one of `start time date` or `start time timestamp`"); + } + if (this.startTimeDate && this.startTimeTimestamp) { + throw new ConfigurationError("You must provide either `start time date` or `start time timestamp`, but not both"); + } + if (!this.endTimeDate && !this.endTimeTimestamp) { + throw new ConfigurationError("You must provide at least one of `end time date` or `end time timestamp`"); + } + if (this.endTimeDate && this.endTimeTimestamp) { + throw new ConfigurationError("You must provide either `end time date` or `end time timestamp`, but not both"); + } + + const response = await this.lark.createCalendarEvent({ + $, + calendarId: this.calendarId, + data: { + summary: this.summary, + description: this.description, + need_notification: this.needNotification, + start_time: { + date: this.startTimeDate, + timestamp: this.startTimeTimestamp, + timezone: this.startTimeTimezone, + }, + end_time: { + date: this.endTimeDate, + timestamp: this.endTimeTimestamp, + timezone: this.endTimeTimezone, + }, + vchat: { + vc_type: this.vcType, + icon_type: this.iconType, + description: this.vchatDescription, + meeting_url: this.meetingUrl, + }, + visibility: this.visibility, + attendee_ability: this.attendeeAbility, + free_busy_status: this.freeBusyStatus, + location: { + name: this.locationName, + address: this.locationAddress, + latitude: this.locationLatitude + ? parseFloat(this.locationLatitude) + : undefined, + longitude: this.locationLongitude + ? parseFloat(this.locationLongitude) + : undefined, + }, + color: this.color, + recurrence: this.recurrence, + }, + }); + + $.export("$summary", `Successfully created calendar event with ID: ${response.data.event.event_id}`); + return response; + }, +}; diff --git a/components/lark/actions/create-new-lark-document/create-new-lark-document.mjs b/components/lark/actions/create-new-lark-document/create-new-lark-document.mjs new file mode 100644 index 0000000000000..0abf78f3b1223 --- /dev/null +++ b/components/lark/actions/create-new-lark-document/create-new-lark-document.mjs @@ -0,0 +1,37 @@ +import lark from "../../lark.app.mjs"; + +export default { + key: "lark-create-new-lark-document", + name: "Create New Lark Document", + description: "Creates a new Lark document. [See the documentation](https://open.larksuite.com/document/server-docs/docs/docs/docx-v1/document/create)", + version: "0.0.1", + type: "action", + props: { + lark, + documentTitle: { + type: "string", + label: "Document Title", + description: "The title of the new Lark document", + optional: true, + }, + folderToken: { + propDefinition: [ + lark, + "folderToken", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.lark.createDocument({ + $, + data: { + title: this.documentTitle, + folder_token: this.folderToken, + }, + }); + + $.export("$summary", `Successfully created document with ID: ${response.data.document.document_id}`); + return response; + }, +}; diff --git a/components/lark/actions/create-new-lark-folder/create-new-lark-folder.mjs b/components/lark/actions/create-new-lark-folder/create-new-lark-folder.mjs new file mode 100644 index 0000000000000..2b28dd0d33150 --- /dev/null +++ b/components/lark/actions/create-new-lark-folder/create-new-lark-folder.mjs @@ -0,0 +1,34 @@ +import lark from "../../lark.app.mjs"; + +export default { + key: "lark-create-new-lark-folder", + name: "Create New Lark Folder", + description: "Creates a new, empty Lark folder. [See the documentation](https://open.larksuite.com/document/server-docs/docs/drive-v1/folder/create_folder)", + version: "0.0.1", + type: "action", + props: { + lark, + name: { + type: "string", + label: "Name", + description: "The name of the new Lark folder", + }, + folderToken: { + propDefinition: [ + lark, + "folderToken", + ], + }, + }, + async run({ $ }) { + const response = await this.lark.createFolder({ + $, + data: { + name: this.name, + folder_token: this.folderToken, + }, + }); + $.export("$summary", `Successfully created folder with token: ${response.data.token}`); + return response; + }, +}; diff --git a/components/lark/actions/create-new-lark-spreadsheet/create-new-lark-spreadsheet.mjs b/components/lark/actions/create-new-lark-spreadsheet/create-new-lark-spreadsheet.mjs new file mode 100644 index 0000000000000..44b6c6de22598 --- /dev/null +++ b/components/lark/actions/create-new-lark-spreadsheet/create-new-lark-spreadsheet.mjs @@ -0,0 +1,37 @@ +import lark from "../../lark.app.mjs"; + +export default { + key: "lark-create-new-lark-spreadsheet", + name: "Create New Lark Spreadsheet", + description: "Create a new spreadsheet in Lark. [See the documentation](https://open.larksuite.com/document/server-docs/docs/sheets-v3/spreadsheet/create)", + version: "0.0.1", + type: "action", + props: { + lark, + spreadsheetTitle: { + type: "string", + label: "Spreadsheet Title", + description: "The title of the new Lark spreadsheet", + optional: true, + }, + folderToken: { + propDefinition: [ + lark, + "folderToken", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.lark.createSpreadsheet({ + $, + data: { + title: this.spreadsheetTitle, + folder_token: this.folderToken, + }, + }); + + $.export("$summary", `Successfully created spreadsheet with ID: ${response.data.spreadsheet.spreadsheet_token}`); + return response; + }, +}; diff --git a/components/lark/actions/create-new-lark-task/create-new-lark-task.mjs b/components/lark/actions/create-new-lark-task/create-new-lark-task.mjs new file mode 100644 index 0000000000000..777daea7db5b2 --- /dev/null +++ b/components/lark/actions/create-new-lark-task/create-new-lark-task.mjs @@ -0,0 +1,102 @@ +import { MODE_OPTIONS } from "../../common/constants.mjs"; +import lark from "../../lark.app.mjs"; + +export default { + key: "lark-create-new-lark-task", + name: "Create New Lark Task", + description: "Creates a new task in Lark. [See the documentation](https://open.larksuite.com/document/uajlw4cm/uktmuktmuktm/task-v2/task/create)", + version: "0.0.1", + type: "action", + props: { + lark, + summary: { + type: "string", + label: "Summary", + description: "Task summary. Empty is not allowed; supports up to 3000 utf8 characters.", + }, + description: { + type: "string", + label: "Description", + description: "Task description. Supports up to 3000 utf8 characters.", + optional: true, + }, + dueTimestamp: { + type: "string", + label: "Due Timestamp", + description: "The timestamp of the due time/date, in milliseconds from 1970-01-01 00:00:00 UTC. If the expiration time is a date, you need to convert the date to timestamp and set `Is All Day` to true.", + }, + isAllDay: { + type: "boolean", + label: "Due Is All Day", + description: "Whether to due on a date. If set to true, only the date part of the timestamp will be parsed and stored.", + optional: true, + }, + extra: { + type: "string", + label: "Extra", + description: "Any data that the caller can pass in attached to the task. It will be returned as it is when getting the task details. If it is binary data, it can be encoded with Base64.", + optional: true, + }, + completedAt: { + type: "string", + label: "Completed At", + description: "The completion time timestamp (ms) of the task. Fill in or set to 0 to create an unfinished task; fill in a specific timestamp to create a completed task.", + optional: true, + }, + repeatRule: { + type: "string", + label: "Repeat Rule", + description: "Task repeat rule. If set, the task is \"recurring task\". Please refer to the [How to Use Recurring Tasks?](https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/task-v2/task/overview) section in Task Feature Overview. E.g. **FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR**", + optional: true, + }, + startTimestamp: { + type: "string", + label: "Start Timestamp", + description: "The timestamp of the start time/date in milliseconds from 1970-01-01 00:00:00. If the start time is a date, you need to convert the date to timestamp and set `Due Is All Day` to true. If you set both the start time and the deadline for the task, the start time must be < = deadline, and the `Is All Day` settings for the start/deadline must be the same.", + }, + startIsAllDay: { + type: "boolean", + label: "Start Is All Day", + description: "Whether to start on a date. If set to true, only the date part of the timestamp will be parsed and stored.", + optional: true, + }, + mode: { + type: "integer", + label: "Mode", + description: "The mode of the task. If set to `auto_complete`, the task will be completed automatically when the due time is reached.", + options: MODE_OPTIONS, + optional: true, + }, + isMilestone: { + type: "boolean", + label: "Is Milestone", + description: "Is it a milestone task", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.lark.createTask({ + $, + data: { + summary: this.summary, + description: this.description, + due: { + timestamp: this.dueTimestamp, + is_all_day: this.isAllDay, + }, + extra: this.extra, + completed_at: this.completedAt, + repeat_rule: this.repeatRule, + start: { + timestamp: this.startTimestamp, + is_all_day: this.startIsAllDay, + }, + mode: this.mode, + is_milestone: this.isMilestone, + }, + }); + + $.export("$summary", `Successfully created task with GUID "${response.data.task.guid}"`); + return response; + }, +}; diff --git a/components/lark/actions/update-base-table-record/update-base-table-record.mjs b/components/lark/actions/update-base-table-record/update-base-table-record.mjs new file mode 100644 index 0000000000000..709398818c081 --- /dev/null +++ b/components/lark/actions/update-base-table-record/update-base-table-record.mjs @@ -0,0 +1,67 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import lark from "../../lark.app.mjs"; + +export default { + key: "lark-update-base-table-record", + name: "Update Base Table Record", + description: "Update an existing record in a base table. [See the documentation](https://open.larksuite.com/document/server-docs/docs/bitable-v1/app-table-record/update)", + version: "0.0.1", + type: "action", + props: { + lark, + baseToken: { + propDefinition: [ + lark, + "baseToken", + ], + }, + tableId: { + propDefinition: [ + lark, + "tableId", + ({ baseToken }) => ({ + baseToken, + }), + ], + }, + recordId: { + propDefinition: [ + lark, + "recordId", + ({ + baseToken, + tableId, + }) => ({ + baseToken, + tableId, + }), + ], + }, + fields: { + propDefinition: [ + lark, + "fields", + ], + description: "The fields to update the record with.", + }, + }, + async run({ $ }) { + const response = await this.lark.updateRecord({ + $, + baseToken: this.baseToken, + tableId: this.tableId, + recordId: this.recordId, + data: { + fields: parseObject(this.fields), + }, + }); + + if (response.error) { + throw new ConfigurationError(response.msg); + } + + $.export("$summary", `Successfully updated record with ID ${this.recordId}`); + return response; + }, +}; diff --git a/components/lark/common/constants.mjs b/components/lark/common/constants.mjs new file mode 100644 index 0000000000000..74f03ebf21eb7 --- /dev/null +++ b/components/lark/common/constants.mjs @@ -0,0 +1,95 @@ +export const LIMIT = 200; + +export const MODE_OPTIONS = [ + { + label: "Countersign Task", + value: 1, + }, + { + label: "Or-sign Task", + value: 2, + }, +]; + +export const VC_TYPE_OPTIONS = [ + { + label: "Lark video conferencing. When this type is selected, other fields of `vchat` are invalid.", + value: "vc", + }, + { + label: "Third-party linked video conferencing. When this type is selected, only the `Icon Type`, `Description`, and `Meeting URL` fields of `vchat` will take effect.", + value: "third_party", + }, + { + label: "No video conferencing. When this type is selected, other fields of `vchat` are invalid.", + value: "no_meeting", + }, + { + label: "Lark Live. Read-only enumeration value, used only on the client side, not supported through API calls.", + value: "lark_live", + }, + { + label: "Unknown type. Read-only enumeration value, only used for client compatibility, does not support API calls.", + value: "unknown", + }, +]; + +export const ICON_TYPE_OPTIONS = [ + { + label: "Lark Video Conference icon.", + value: "vc", + }, + { + label: "Livestream video conference icon.", + value: "live", + }, + { + label: "Default icon.", + value: "default", + }, +]; + +export const VISIBILITY_OPTIONS = [ + { + label: "Default range, which depends on the calendar visibility. Only the availability status is visible to other people by default.", + value: "default", + }, + { + label: "Public. Event details are displayed.", + value: "public", + }, + { + label: "Private. Details are visible only to the current identity.", + value: "private", + }, +]; + +export const ATTENDEE_ABILITY_OPTIONS = [ + { + label: "Cannot edit events, cannot invite others, and cannot view event invitee list.", + value: "none", + }, + { + label: "Cannot edit events, can invite others, and can view event invitee list.", + value: "can_see_others", + }, + { + label: "Cannot edit events, can invite others, and can view event invitee list.", + value: "can_invite_others", + }, + { + label: "Can edit events, can invite others, and can view event invitee list.", + value: "can_modify_event", + }, +]; + +export const FREE_BUSY_STATUS_OPTIONS = [ + { + label: "Busy. The event is busy.", + value: "busy", + }, + { + label: "Free. The event is free.", + value: "free", + }, +]; diff --git a/components/lark/common/utils.mjs b/components/lark/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/lark/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/lark/lark.app.mjs b/components/lark/lark.app.mjs index a434364213871..d22e83b520f82 100644 --- a/components/lark/lark.app.mjs +++ b/components/lark/lark.app.mjs @@ -1,11 +1,359 @@ +import { axios } from "@pipedream/platform"; +import { LIMIT } from "./common/constants.mjs"; + export default { type: "app", app: "lark", - propDefinitions: {}, + propDefinitions: { + folderToken: { + type: "string", + label: "Folder Token", + description: "The token of the parent folder", + async options() { + const { data: { token: rootToken } } = await this.getRootFolder(); + + const response = [ + { + label: "Root", + value: rootToken, + }, + ]; + + response.push(...await this.serealizeFolderTree({ + token: rootToken, + })); + return response; + }, + }, + calendarId: { + type: "string", + label: "Calendar ID", + description: "The ID of the calendar to create the event in.", + async options({ prevContext }) { + const { data } = await this.listCalendars({ + params: { + page_token: prevContext?.page_token, + }, + }); + return { + options: data.calendar_list.map(({ + calendar_id: value, summary: label, + }) => ({ + label, + value, + })), + context: { + page_token: data.page_token, + }, + }; + }, + }, + tableId: { + type: "string", + label: "Table ID", + description: "The ID of the table to create the record in.", + async options({ + prevContext, baseToken, + }) { + const { data } = await this.listTables({ + baseToken, + params: { + page_token: prevContext?.page_token, + }, + }); + return { + options: data.items.map(({ + table_id: value, name: label, + }) => ({ + label, + value, + })), + context: { + page_token: data.page_token, + }, + }; + }, + }, + recordId: { + type: "string", + label: "Record ID", + description: "The ID of the record to update.", + async options({ + prevContext, baseToken, tableId, + }) { + const { data } = await this.listRecords({ + baseToken, + tableId, + params: { + page_token: prevContext?.page_token, + }, + }); + return { + options: data.items.map(({ record_id }) => record_id), + context: { + page_token: data.page_token, + }, + }; + }, + }, + chatId: { + type: "string", + label: "Chat ID", + description: "The ID of the Lark chat", + async options({ prevContext }) { + const { data } = await this.listGroupChats({ + params: { + page_token: prevContext?.page_token, + }, + }); + return { + options: data.items.map(({ + chat_id: value, name: label, + }) => ({ + label, + value, + })), + context: { + page_token: data.page_token, + }, + }; + }, + }, + userId: { + type: "string", + label: "User ID", + description: "The ID of the user to add to the group chat", + async options({ prevContext }) { + const { data } = await this.listUsers({ + params: { + page_token: prevContext?.page_token, + }, + }); + return { + options: data.items.map(({ + user_id: value, name, email, + }) => ({ + label: `${name} (${email})`, + value, + })), + context: { + page_token: data.page_token, + }, + }; + }, + }, + baseToken: { + type: "string", + label: "Base Token", + description: "The token for the base to create the record in. [See the documentation](https://open.larksuite.com/document/server-docs/docs/bitable-v1/notification#dcf71789) to check how to get the Base Token.", + }, + fields: { + type: "object", + label: "Fields", + description: "The fields to create the record with.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://open.larksuite.com/open-apis"; + }, + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + async serealizeFolderTree({ + token, pageToken, parentName = "", + }) { + const response = []; + let hasMore = true; + + while (hasMore) { + const { data } = await this.listFolders({ + params: { + page_size: LIMIT, + page_token: pageToken, + folder_token: token, + }, + }); + + await Promise.all(data.files + .filter(({ type }) => type === "folder") + .sort((a, b) => a.created_time - b.created_time) + .map(async ({ + token: value, name: label, + }) => { + response.push({ + label: `${parentName}${parentName + ? " / " + : ""}${label}`, + value, + }); + response.push(...await this.serealizeFolderTree({ + token: value, + pageToken: data.next_page_token, + parentName: `${parentName}${parentName + ? " / " + : ""}${label}`, + })); + })); + hasMore = data.has_more; + } + return response; + }, + getRootFolder(opts = {}) { + return this._makeRequest({ + path: "/drive/explorer/v2/root_folder/meta", + ...opts, + }); + }, + listFolders(opts = {}) { + return this._makeRequest({ + path: "/drive/v1/files", + ...opts, + }); + }, + createDocument(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/docx/v1/documents", + ...opts, + }); + }, + listCalendars(opts = {}) { + return this._makeRequest({ + path: "/calendar/v4/calendars", + ...opts, + }); + }, + listTables({ + baseToken, ...opts + }) { + return this._makeRequest({ + path: `/bitable/v1/apps/${baseToken}/tables`, + ...opts, + }); + }, + listRecords({ + baseToken, tableId, ...opts + }) { + return this._makeRequest({ + path: `/bitable/v1/apps/${baseToken}/tables/${tableId}/records`, + ...opts, + }); + }, + createRecord({ + baseToken, tableId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/bitable/v1/apps/${baseToken}/tables/${tableId}/records`, + ...opts, + }); + }, + updateRecord({ + baseToken, tableId, recordId, ...opts + }) { + return this._makeRequest({ + method: "PUT", + path: `/bitable/v1/apps/${baseToken}/tables/${tableId}/records/${recordId}`, + ...opts, + }); + }, + createSpreadsheet(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/sheets/v3/spreadsheets", + ...opts, + }); + }, + listGroupChats(opts = {}) { + return this._makeRequest({ + path: "/im/v1/chats", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/contact/v3/users", + ...opts, + }); + }, + addUserToGroupChat({ + chatId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/im/v1/chats/${chatId}/members`, + ...opts, + }); + }, + listChatMembers({ + chatId, ...opts + }) { + return this._makeRequest({ + path: `/im/v1/chats/${chatId}/members`, + ...opts, + }); + }, + createFolder(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/drive/v1/files/create_folder", + ...opts, + }); + }, + createTask(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/task/v2/tasks", + ...opts, + }); + }, + createCalendarEvent({ + calendarId, ...opts + }) { + return this._makeRequest({ + method: "POST", + path: `/calendar/v4/calendars/${calendarId}/events`, + ...opts, + }); + }, + async *paginate({ + fn, params = {}, ...opts + }) { + let hasMore = false; + let nextPageToken = null; + + do { + params.page_token = nextPageToken; + const { + data: { + has_more, + items, + page_token, + }, + } = await fn({ + params, + ...opts, + }); + for (const d of items) { + yield d; + } + + nextPageToken = page_token; + hasMore = has_more; + + } while (hasMore); }, }, }; diff --git a/components/lark/package.json b/components/lark/package.json index 579c470c26ea0..71fa594e7d838 100644 --- a/components/lark/package.json +++ b/components/lark/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/lark", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Lark Components", "main": "lark.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.1.0" } -} \ No newline at end of file +} diff --git a/components/lark/sources/new-user-added-to-group-chat/new-user-added-to-group-chat.mjs b/components/lark/sources/new-user-added-to-group-chat/new-user-added-to-group-chat.mjs new file mode 100644 index 0000000000000..0e3196a737875 --- /dev/null +++ b/components/lark/sources/new-user-added-to-group-chat/new-user-added-to-group-chat.mjs @@ -0,0 +1,77 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import lark from "../../lark.app.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + key: "lark-new-user-added-to-group-chat", + name: "New User Added to Group Chat", + description: "Emit new event when a new user is added to a group chat. [See the documentation](https://open.larksuite.com/document/server-docs/group/chat-member/get)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + lark, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + chatId: { + propDefinition: [ + lark, + "chatId", + ], + }, + }, + methods: { + _getMemberIds() { + return this.db.get("memberIds") || []; + }, + _setMemberIds(memberIds) { + this.db.set("memberIds", memberIds); + }, + async emitEvent(maxResults = false) { + const memberIds = this._getMemberIds(); + + const response = this.lark.paginate({ + fn: this.lark.listChatMembers, + chatId: this.chatId, + }); + + let responseArray = []; + for await (const item of response) { + responseArray.push(item); + } + + const newMembers = responseArray + .filter(({ member_id }) => !memberIds.includes(member_id)) + .reverse(); + + if (maxResults && (newMembers.length > maxResults)) { + newMembers.length = maxResults; + } + + const allMemberIds = responseArray.map(({ member_id }) => member_id); + this._setMemberIds(allMemberIds); + + for (const member of newMembers.reverse()) { + this.$emit(member, { + id: member.member_id, + summary: `New member added: ${member.name}`, + ts: Date.now(), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, + sampleEmit, +}; diff --git a/components/lark/sources/new-user-added-to-group-chat/test-event.mjs b/components/lark/sources/new-user-added-to-group-chat/test-event.mjs new file mode 100644 index 0000000000000..1efd15871de3a --- /dev/null +++ b/components/lark/sources/new-user-added-to-group-chat/test-event.mjs @@ -0,0 +1,6 @@ +export default { + "member_id": "ou_5385a389daf18d1db774bef0f509f738", + "member_id_type": "open_id", + "name": "Contact Name", + "tenant_key": "10ff7714195b3eb" +}; \ No newline at end of file From 953940c665e4f18c9d0d7a371e4a5bf821d9b018 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Thu, 17 Jul 2025 16:23:49 -0300 Subject: [PATCH 2/2] pnpm update --- pnpm-lock.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c8424a80275c..3f4fcffed0e8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1872,8 +1872,7 @@ importers: specifier: ^3.0.0 version: 3.0.3 - components/bright_data: - specifiers: {} + components/bright_data: {} components/brilliant_directories: dependencies: @@ -7366,7 +7365,11 @@ importers: specifier: ^3.0.0 version: 3.0.3 - components/lark: {} + components/lark: + dependencies: + '@pipedream/platform': + specifier: ^3.1.0 + version: 3.1.0 components/lastpass: dependencies: