diff --git a/components/instantly/actions/add-lead-campaign/add-lead-campaign.mjs b/components/instantly/actions/add-lead-campaign/add-lead-campaign.mjs index d85fc5392ffba..310071be2d0ba 100644 --- a/components/instantly/actions/add-lead-campaign/add-lead-campaign.mjs +++ b/components/instantly/actions/add-lead-campaign/add-lead-campaign.mjs @@ -3,16 +3,16 @@ import instantly from "../../instantly.app.mjs"; export default { key: "instantly-add-lead-campaign", - name: "Add Lead to Campaign", - description: "Adds a lead to a campaign for tracking or further actions. [See the documentation](https://developer.instantly.ai/lead/add-leads-to-a-campaign)", - version: "0.0.1", + name: "Add Leads to Campaign", + description: "Adds a lead or leads to a campaign for tracking or further actions. [See the documentation](https://developer.instantly.ai/api/v2/lead/moveleads)", + version: "0.0.2", type: "action", props: { instantly, - leads: { + leadIds: { propDefinition: [ instantly, - "leads", + "leadIds", ], }, campaignId: { @@ -21,30 +21,42 @@ export default { "campaignId", ], }, - skipIfInWorkspace: { - type: "boolean", - label: "Skip if in Workspace", - description: "Skip lead if it exists in any campaigns in the workspace", - optional: true, - }, skipIfInCampaign: { type: "boolean", label: "Skip if in Campaign", description: "Skip lead if it exists in the campaign", optional: true, }, + waitForCompletion: { + type: "boolean", + label: "Wait for Completion", + description: "Set to `true` to poll the API in 3-second intervals until the background job is completed", + optional: true, + }, }, async run({ $ }) { - const response = await this.instantly.addLeadsToCampaign({ + let response = await this.instantly.addLeadsToCampaign({ $, data: { - leads: parseObject(this.leads), - campaign_id: this.campaignId, - skip_if_in_workspace: this.skipIfInWorkspace, - skip_if_in_campaign: this.skipIfInCampaign, + ids: parseObject(this.leadIds), + to_campaign_id: this.campaignId, + check_duplicates_in_campaigns: this.skipIfInCampaign, }, }); - $.export("$summary", `Added ${response.leads_uploaded} leads to campaign ${this.campaignId}`); + + if (this.waitForCompletion) { + const jobId = response.id; + const timer = (ms) => new Promise((res) => setTimeout(res, ms)); + while (response.status === "pending" || response.status === "in-progress") { + response = await this.instantly.getBackgroundJob({ + $, + jobId, + }); + await timer(3000); + } + } + + $.export("$summary", `Added ${this.leadIds.length} lead(s) to campaign ${this.campaignId}`); return response; }, }; diff --git a/components/instantly/actions/add-tags-campaign/add-tags-campaign.mjs b/components/instantly/actions/add-tags-campaign/add-tags-campaign.mjs index 6b59814d975ef..29a8ad0678636 100644 --- a/components/instantly/actions/add-tags-campaign/add-tags-campaign.mjs +++ b/components/instantly/actions/add-tags-campaign/add-tags-campaign.mjs @@ -4,8 +4,8 @@ import instantly from "../../instantly.app.mjs"; export default { key: "instantly-add-tags-campaign", name: "Add Tags to Campaign", - description: "Adds tags to a specific campaign. [See the documentation](https://developer.instantly.ai/tags/assign-or-unassign-a-tag)", - version: "0.0.1", + description: "Adds tags to a specific campaign. [See the documentation](https://developer.instantly.ai/api/v2/customtag/toggletagresource)", + version: "0.0.2", type: "action", props: { instantly, @@ -15,6 +15,7 @@ export default { "campaignId", ], type: "string[]", + description: "The campaign IDs to assign tags to", }, tagIds: { propDefinition: [ @@ -34,7 +35,7 @@ export default { resource_ids: parseObject(this.campaignIds), }, }); - $.export("$summary", response.message); + $.export("$summary", "Successfully added tags to campaign(s)"); return response; }, }; diff --git a/components/instantly/actions/update-lead-status/update-lead-status.mjs b/components/instantly/actions/update-lead-status/update-lead-status.mjs index db5bf892e2042..97ee2eeb1a65b 100644 --- a/components/instantly/actions/update-lead-status/update-lead-status.mjs +++ b/components/instantly/actions/update-lead-status/update-lead-status.mjs @@ -4,8 +4,8 @@ import instantly from "../../instantly.app.mjs"; export default { key: "instantly-update-lead-status", name: "Update Lead Status", - description: "Updates the status of a lead in a campaign. [See the documentation](https://developer.instantly.ai/lead/update-lead-status)", - version: "0.0.1", + description: "Updates the interest status of a lead in a campaign. [See the documentation](https://developer.instantly.ai/api/v2/customtag/toggletagresource)", + version: "0.0.2", type: "action", props: { instantly, @@ -18,8 +18,14 @@ export default { email: { propDefinition: [ instantly, - "email", + "leadIds", + () => ({ + valueKey: "email", + }), ], + type: "string", + label: "Lead Email", + description: "Email address of the lead to update", }, newStatus: { propDefinition: [ @@ -33,12 +39,12 @@ export default { const response = await this.instantly.updateLeadStatus({ $, data: { - email: this.email, - new_status: this.newStatus, + lead_email: this.email, + interest_value: this.newStatus, campaign_id: this.campaignId, }, }); - $.export("$summary", `Updated lead ${this.email} to status '${this.newStatus}'`); + $.export("$summary", `Updated status of lead: ${this.email}`); return response; } catch ({ response }) { throw new ConfigurationError(response.data.error); diff --git a/components/instantly/common/constants.mjs b/components/instantly/common/constants.mjs index b0e103c9c6726..745b98031cf60 100644 --- a/components/instantly/common/constants.mjs +++ b/components/instantly/common/constants.mjs @@ -1,77 +1,36 @@ export const LIMIT = 100; -export const EVENT_TYPE_OPTIONS = [ - { - label: "Email Sent", - value: "email_sent", - }, - { - label: "Email Bounced", - value: "email_bounced", - }, - { - label: "Email Opened", - value: "email_opened", - }, - { - label: "Email Link Clicked", - value: "email_link_clicked", - }, - { - label: "Reply Received", - value: "reply_received", - }, - { - label: "Lead Unsubscribed", - value: "lead_unsubscribed", - }, - { - label: "Campaign Completed", - value: "campaign_completed", - }, +export const NEW_STATUS_OPTIONS = [ { - label: "Account Error", - value: "account_error", + label: "Out of Office", + value: "0", }, { - label: "Lead Not Interested", - value: "lead_not_interested", + label: "Interested", + value: "1", }, { - label: "Lead Neutral", - value: "lead_neutral", + label: "Meeting Booked", + value: "2", }, { - label: "Lead Meeting Booked", - value: "lead_meeting_booked", + label: "Meeting Completed", + value: "3", }, { - label: "Lead Meeting Completed", - value: "lead_meeting_completed", + label: "Closed", + value: "4", }, { - label: "Lead Closed", - value: "lead_closed", + label: "Not Interested", + value: "-1", }, { - label: "Lead Out of Office", - value: "lead_out_of_office", + label: "Wrong Person", + value: "-2", }, { - label: "Lead Wrong Person", - value: "lead_wrong_person", + label: "Lost", + value: "-3", }, ]; - -export const NEW_STATUS_OPTIONS = [ - "Active", - "Completed", - "Unsubscribed", - "Interested", - "Meeting Booked", - "Meeting Completed", - "Closed", - "Out of Office", - "Not Interested", - "Wrong Person", -]; diff --git a/components/instantly/instantly.app.mjs b/components/instantly/instantly.app.mjs index 2877295fecddc..36ff866fc9687 100644 --- a/components/instantly/instantly.app.mjs +++ b/components/instantly/instantly.app.mjs @@ -1,6 +1,5 @@ import { axios } from "@pipedream/platform"; import { - EVENT_TYPE_OPTIONS, LIMIT, NEW_STATUS_OPTIONS, } from "./common/constants.mjs"; @@ -13,140 +12,193 @@ export default { type: "string", label: "Campaign ID", description: "The ID of the campaign", - async options({ page }) { - const campaigns = await this.listCampaigns({ + async options({ prevContext }) { + const { + items, next_starting_after: next, + } = await this.listCampaigns({ params: { limit: LIMIT, - skip: LIMIT * page, + starting_after: prevContext?.next, }, }); - return campaigns.map(({ - id: value, name: label, - }) => ({ - label, - value, - })); + return { + options: items?.map(({ + id: value, name: label, + }) => ({ + label, + value, + })) || [], + context: { + next, + }, + }; }, }, tagIds: { type: "string[]", - label: "Tags Id", - description: "List of tag Ids to add", - async options({ page }) { - const { data } = await this.listTags({ + label: "Tags ID", + description: "List of tag IDs to add", + async options({ prevContext }) { + const { + items, next_starting_after: next, + } = await this.listTags({ params: { limit: LIMIT, - skip: LIMIT * page, + starting_after: prevContext?.next, }, }); - return data.map(({ - id: value, label, - }) => ({ - label, - value, - })); + return { + options: items?.map(({ + id: value, label, + }) => ({ + label, + value, + })) || [], + context: { + next, + }, + }; }, }, - leads: { + leadIds: { type: "string[]", - label: "Leads", - description: "An array of lead objects to add to the campaign. **Example: [{ \"email\":\"john2@abc.com\", \"first_name\":\"John\", \"last_name\":\"Doe\", \"company_name\":\"Instantly\", \"personalization\":\"Loved your latest post\", \"phone\":\"123456789\", \"website\":\"instantly.ai\", \"custom_variables\":{ \"favorite_restaurant\":\"Chipotle\", \"language\":\"English\"}}]**", - }, - skipIfInWorkspace: { - type: "boolean", - label: "Skip if in Workspace", - description: "Skip lead if it exists in any campaigns in the workspace", - optional: true, - }, - skipIfInCampaign: { - type: "boolean", - label: "Skip if in Campaign", - description: "Skip lead if it exists in the campaign", - optional: true, - }, - eventType: { - type: "string", - label: "Event Type", - description: "Type of event to filter", - options: EVENT_TYPE_OPTIONS, - }, - email: { - type: "string", - label: "Lead Email", - description: "Email address of the lead", + label: "Lead IDs", + description: "The array of lead IDs to include", + async options({ + prevContext, valueKey = "id", + }) { + const { + items, next_starting_after: next, + } = await this.listLeads({ + params: { + limit: LIMIT, + starting_after: prevContext?.next, + }, + }); + return { + options: items?.map((lead) => ({ + label: (`${lead?.first_name} ${lead?.last_name}`).trim(), + value: lead[valueKey], + })) || [], + context: { + next, + }, + }; + }, }, newStatus: { type: "string", label: "New Status", - description: "New status to assign to the lead", + description: "Lead interest status. It can be either a static value, or a custom status interest value.", options: NEW_STATUS_OPTIONS, }, }, methods: { _baseUrl() { - return "https://api.instantly.ai/api/v1"; + return "https://api.instantly.ai/api/v2"; }, - _params(params = {}) { + _headers(headers = {}) { return { - ...params, - api_key: `${this.$auth.api_key}`, + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, }; }, _makeRequest({ - $ = this, params, path, ...opts + $ = this, headers, path, ...opts }) { return axios($, { url: this._baseUrl() + path, - params: this._params(params), + headers: this._headers(headers), ...opts, }); }, listCampaigns(opts = {}) { return this._makeRequest({ - path: "/campaign/list", + path: "/campaigns", ...opts, }); }, listTags(opts = {}) { return this._makeRequest({ - path: "/custom-tag", + path: "/custom-tags", ...opts, }); }, - addTagsToCampaign(opts = {}) { + listLeads(opts = {}) { return this._makeRequest({ method: "POST", - path: "/custom-tag/toggle-tag-resource", + path: "/leads/list", ...opts, }); }, - addLeadsToCampaign(opts = {}) { + listEmails(opts = {}) { return this._makeRequest({ - method: "POST", - path: "/lead/add", + path: "/emails", ...opts, }); }, - updateLeadStatus(opts = {}) { + listBackgroundJobs(opts = {}) { + return this._makeRequest({ + path: "/background-jobs", + ...opts, + }); + }, + getBackgroundJob({ + jobId, ...opts + }) { + return this._makeRequest({ + path: `/background-jobs/${jobId}`, + ...opts, + }); + }, + addTagsToCampaign(opts = {}) { return this._makeRequest({ method: "POST", - path: "/lead/update/status", + path: "/custom-tags/toggle-resource", ...opts, }); }, - createWebhook(opts = {}) { + addLeadsToCampaign(opts = {}) { return this._makeRequest({ method: "POST", - path: "/webhook/subscribe", + path: "/leads/move", ...opts, }); }, - deleteWebhook(opts = {}) { + updateLeadStatus(opts = {}) { return this._makeRequest({ method: "POST", - path: "/webhook/unsubscribe", + path: "/leads/update-interest-status", ...opts, }); }, + async *paginate({ + fn, args = {}, max, + }) { + const optsKey = args?.data + ? "data" + : "params"; + + args[optsKey] = { + ...args[optsKey], + limit: LIMIT, + }; + + let total, count = 0; + + do { + const { + items, next_starting_after: next, + } = await fn(args); + for (const item of items) { + yield item; + if (max && ++count >= max) { + return; + } + } + total = items?.length; + args[optsKey].starting_after = next; + } while (total === args[optsKey].limit); + }, }, }; diff --git a/components/instantly/package.json b/components/instantly/package.json index cca202a1b1e66..5bba0c90211a8 100644 --- a/components/instantly/package.json +++ b/components/instantly/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/instantly", - "version": "0.1.0", + "version": "0.1.1", "description": "Pipedream Instantly Components", "main": "instantly.app.mjs", "keywords": [ diff --git a/components/instantly/sources/common/base.mjs b/components/instantly/sources/common/base.mjs new file mode 100644 index 0000000000000..c5c820c6572eb --- /dev/null +++ b/components/instantly/sources/common/base.mjs @@ -0,0 +1,91 @@ +import instantly from "../../instantly.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + instantly, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getArgs() { + return {}; + }, + generateMeta(item) { + return { + id: item.id, + summary: this.getSummary(item), + ts: Date.parse(item[this.getTsField()]), + }; + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + + const resourceFn = this.getResourceFn(); + const args = this.getArgs(); + const tsField = this.getTsField(); + const isSortedDesc = args?.sort_order === "desc"; + + const items = this.instantly.paginate({ + fn: resourceFn, + args, + max: isSortedDesc && max, + }); + + let results = []; + for await (const item of items) { + const ts = Date.parse(item[tsField]); + if (ts >= lastTs) { + results.push(item); + maxTs = Math.max(ts, maxTs); + } else if (isSortedDesc) { + break; + } + } + + if (!results.length) { + return; + } + + if (max && !isSortedDesc) { + results = results.slice(-1 * max).reverse(); + } + + results.forEach((item) => { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }); + + this._setLastTs(maxTs); + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + getTsField() { + throw new Error("getTsField is not implemented"); + }, + getSummary() { + throw new Error("getSummary is not implemented"); + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/instantly/sources/new-background-job-completed/new-background-job-completed.mjs b/components/instantly/sources/new-background-job-completed/new-background-job-completed.mjs new file mode 100644 index 0000000000000..862036eb15fb7 --- /dev/null +++ b/components/instantly/sources/new-background-job-completed/new-background-job-completed.mjs @@ -0,0 +1,32 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "instantly-new-background-job-completed", + name: "New Background Job Completed", + description: "Emit new event when a new background job has completed. [See the documentation](https://developer.instantly.ai/api/v2/backgroundjob/listbackgroundjob)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.instantly.listBackgroundJobs; + }, + getArgs() { + return { + params: { + status: "success,failed", + sort_column: "updated_at", + sort_order: "desc", + }, + }; + }, + getTsField() { + return "updated_at"; + }, + getSummary(item) { + return `Background Job Completed with ID: ${item.id}`; + }, + }, +}; diff --git a/components/instantly/sources/new-email-received/new-email-received.mjs b/components/instantly/sources/new-email-received/new-email-received.mjs new file mode 100644 index 0000000000000..8312e326ee4ae --- /dev/null +++ b/components/instantly/sources/new-email-received/new-email-received.mjs @@ -0,0 +1,31 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "instantly-new-email-received", + name: "New Email Received", + description: "Emit new event when a new email is received. [See the documentation](https://developer.instantly.ai/api/v2/email/listemail)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.instantly.listEmails; + }, + getArgs() { + return { + params: { + email_type: "received", + sort_order: "desc", + }, + }; + }, + getTsField() { + return "timestamp_created"; + }, + getSummary(item) { + return `New Email with ID: ${item.id}`; + }, + }, +}; diff --git a/components/instantly/sources/new-event-instant/new-event-instant.mjs b/components/instantly/sources/new-event-instant/new-event-instant.mjs deleted file mode 100644 index e6bed8eda4e29..0000000000000 --- a/components/instantly/sources/new-event-instant/new-event-instant.mjs +++ /dev/null @@ -1,67 +0,0 @@ -import instantly from "../../instantly.app.mjs"; -import sampleEmit from "./test-event.mjs"; - -export default { - key: "instantly-new-event-instant", - name: "New Event in Instantly (Instant)", - description: "Emit new event when an activity occurs in your Instantly workspace.", - version: "0.0.1", - type: "source", - dedupe: "unique", - props: { - instantly, - http: "$.interface.http", - db: "$.service.db", - campaignId: { - propDefinition: [ - instantly, - "campaignId", - ], - optional: true, - }, - eventType: { - propDefinition: [ - instantly, - "eventType", - ], - optional: true, - }, - }, - methods: { - _getHookId() { - return this.db.get("hookId"); - }, - _setHookId(hookId) { - this.db.set("hookId", hookId); - }, - }, - hooks: { - async activate() { - const response = await this.instantly.createWebhook({ - data: { - hookUrl: this.http.endpoint, - event_type: this.eventType, - campaign: this.campaignId, - }, - }); - this._setHookId(response.id); - }, - async deactivate() { - const webhookId = this._getHookId(); - await this.instantly.deleteWebhook({ - data: { - hook_id: webhookId, - }, - }); - }, - }, - async run({ body }) { - const ts = Date.parse(new Date()); - this.$emit(body, { - id: `${body.resource}-${ts}`, - summary: `New event from ${body.lead_email} for campaign ${body.campaign_name}`, - ts: ts, - }); - }, - sampleEmit, -}; diff --git a/components/instantly/sources/new-event-instant/test-event.mjs b/components/instantly/sources/new-event-instant/test-event.mjs deleted file mode 100644 index da174700f93f5..0000000000000 --- a/components/instantly/sources/new-event-instant/test-event.mjs +++ /dev/null @@ -1,18 +0,0 @@ -export default { - "timestamp": "2025-01-08T22:06:16.129Z", - "event_type": "lead_not_interested", - "workspace": "c1b30c69-7fcd-88df-e1151d8f7ec7", - "campaign_id": "c7c1103b-0185-ba08-e925bc5ca574", - "unibox_url": null, - "campaign_name": "My Campaign", - "lead_email": "john@abc.com", - "email": "john@abc.com", - "phone": "123456789", - "website": "instantly.ai", - "language": "English", - "lastName": "Doe", - "firstName": "John", - "companyName": "Instantly", - "personalization": "Loved your latest post", - "favorite_restaurant": "Chipotle" -} \ No newline at end of file diff --git a/components/instantly/sources/new-lead-created/new-lead-created.mjs b/components/instantly/sources/new-lead-created/new-lead-created.mjs new file mode 100644 index 0000000000000..094b4f67b7dca --- /dev/null +++ b/components/instantly/sources/new-lead-created/new-lead-created.mjs @@ -0,0 +1,28 @@ +import common from "../common/base.mjs"; + +export default { + ...common, + key: "instantly-new-lead-created", + name: "New Lead Created", + description: "Emit new event when a new lead is created. [See the documentation](https://developer.instantly.ai/api/v2/lead/listleads)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getResourceFn() { + return this.instantly.listLeads; + }, + getArgs() { + return { + data: {}, + }; + }, + getTsField() { + return "timestamp_created"; + }, + getSummary(item) { + return `New Lead with ID: ${item.id}`; + }, + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb857b64d7a65..24fa63d0aad0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -962,8 +962,7 @@ importers: specifier: ^3.0.0 version: 3.0.3 - components/arlo: - specifiers: {} + components/arlo: {} components/aroflo: dependencies: