From 36b5862bc1eed22c8861c9b52c5cef1fc42e4cf1 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Tue, 12 Nov 2024 13:12:21 -0500 Subject: [PATCH 1/6] init --- .../actions/add-subscriber/add-subscriber.mjs | 37 ++++++ .../send-smart-transactional-email.mjs | 43 +++++++ .../actions/unsubscribe/unsubscribe.mjs | 28 +++++ .../campaign_monitor/campaign_monitor.app.mjs | 117 +++++++++++++++++- .../sources/new-bounce/new-bounce.mjs | 53 ++++++++ .../sources/new-email-open/new-email-open.mjs | 61 +++++++++ .../sources/new-subscriber/new-subscriber.mjs | 58 +++++++++ 7 files changed, 392 insertions(+), 5 deletions(-) create mode 100644 components/campaign_monitor/actions/add-subscriber/add-subscriber.mjs create mode 100644 components/campaign_monitor/actions/send-smart-transactional-email/send-smart-transactional-email.mjs create mode 100644 components/campaign_monitor/actions/unsubscribe/unsubscribe.mjs create mode 100644 components/campaign_monitor/sources/new-bounce/new-bounce.mjs create mode 100644 components/campaign_monitor/sources/new-email-open/new-email-open.mjs create mode 100644 components/campaign_monitor/sources/new-subscriber/new-subscriber.mjs diff --git a/components/campaign_monitor/actions/add-subscriber/add-subscriber.mjs b/components/campaign_monitor/actions/add-subscriber/add-subscriber.mjs new file mode 100644 index 0000000000000..22656fb82445e --- /dev/null +++ b/components/campaign_monitor/actions/add-subscriber/add-subscriber.mjs @@ -0,0 +1,37 @@ +import campaignMonitor from "../../campaign_monitor.app.mjs"; + +export default { + key: "campaign_monitor-add-subscriber", + name: "Add Subscriber", + description: "Creates a new subscriber on a specific list. [See the documentation](https://www.campaignmonitor.com/api/subscribers/#adding-a-subscriber)", + version: "0.0.{{ts}}", + type: "action", + props: { + campaignMonitor, + email: { + propDefinition: [ + campaignMonitor, + "email", + ], + }, + listId: { + propDefinition: [ + campaignMonitor, + "listId", + ], + }, + name: { + propDefinition: [ + campaignMonitor, + "name", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = + await this.campaignMonitor.createSubscriber(this.email, this.listId, this.name); + $.export("$summary", `Successfully added subscriber ${this.email} to list ${this.listId}`); + return response; + }, +}; diff --git a/components/campaign_monitor/actions/send-smart-transactional-email/send-smart-transactional-email.mjs b/components/campaign_monitor/actions/send-smart-transactional-email/send-smart-transactional-email.mjs new file mode 100644 index 0000000000000..26f0b7acc34ec --- /dev/null +++ b/components/campaign_monitor/actions/send-smart-transactional-email/send-smart-transactional-email.mjs @@ -0,0 +1,43 @@ +import campaignMonitor from "../../campaign_monitor.app.mjs"; + +export default { + key: "campaign_monitor-send-smart-transactional-email", + name: "Send Smart Transactional Email", + description: "Sends an intelligent transactional email to a specified recipient.", + version: "0.0.{{ts}}", + type: "action", + props: { + campaignMonitor, + email: { + type: "string", + label: "Email", + description: "The email of the recipient", + }, + subject: { + type: "string", + label: "Subject", + description: "The subject of the email", + }, + content: { + type: "string", + label: "Content", + description: "The content of the email", + }, + listId: { + type: "string", + label: "List ID", + description: "The ID of the list", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.campaignMonitor.sendIntelligentEmail( + this.email, + this.subject, + this.content, + this.listId, + ); + $.export("$summary", "Successfully sent smart transactional email"); + return response; + }, +}; diff --git a/components/campaign_monitor/actions/unsubscribe/unsubscribe.mjs b/components/campaign_monitor/actions/unsubscribe/unsubscribe.mjs new file mode 100644 index 0000000000000..a766d53f7ff1c --- /dev/null +++ b/components/campaign_monitor/actions/unsubscribe/unsubscribe.mjs @@ -0,0 +1,28 @@ +import campaignMonitor from "../../campaign_monitor.app.mjs"; + +export default { + key: "campaign_monitor-unsubscribe", + name: "Unsubscribe", + description: "Removes a subscriber from a mailing list given their email address.", + version: "0.0.{{ts}}", + type: "action", + props: { + campaignMonitor, + email: { + type: "string", + label: "Email", + description: "The email of the subscriber to remove", + }, + listId: { + type: "string", + label: "List ID", + description: "The ID of the mailing list from which to remove the subscriber", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.campaignMonitor.removeSubscriber(this.email, this.listId); + $.export("$summary", `Successfully unsubscribed ${this.email}`); + return response; + }, +}; diff --git a/components/campaign_monitor/campaign_monitor.app.mjs b/components/campaign_monitor/campaign_monitor.app.mjs index 87a1742c0dad1..986b68d37663f 100644 --- a/components/campaign_monitor/campaign_monitor.app.mjs +++ b/components/campaign_monitor/campaign_monitor.app.mjs @@ -1,11 +1,118 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "campaign_monitor", - propDefinitions: {}, + propDefinitions: { + campaignId: { + type: "string", + label: "Campaign ID", + description: "The ID of the campaign", + }, + subscriberId: { + type: "string", + label: "Subscriber ID", + description: "The ID of the subscriber", + optional: true, + }, + listId: { + type: "string", + label: "List ID", + description: "The ID of the list", + }, + email: { + type: "string", + label: "Email", + description: "The email of the recipient", + }, + subject: { + type: "string", + label: "Subject", + description: "The subject of the email", + }, + content: { + type: "string", + label: "Content", + description: "The content of the email", + }, + name: { + type: "string", + label: "Name", + description: "The name of the subscriber", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.createsend.com/api/v3.3"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, + method = "GET", + path, + headers, + ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + method, + url: this._baseUrl() + path, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.api_key}`, + }, + }); + }, + async emitCampaignEmailBounce(campaignId) { + return this._makeRequest({ + path: `/campaigns/${campaignId}/bounces`, + }); + }, + async emitCampaignEmailOpen(campaignId, subscriberId) { + return this._makeRequest({ + path: `/campaigns/${campaignId}/opens`, + params: { + subscriber_id: subscriberId, + }, + }); + }, + async emitNewSubscriber(listId) { + return this._makeRequest({ + path: `/lists/${listId}/active`, + }); + }, + async sendIntelligentEmail(email, subject, content, listId) { + return this._makeRequest({ + method: "POST", + path: "/transactional/send", + data: { + email, + subject, + content, + list_id: listId, + }, + }); + }, + async removeSubscriber(email, listId) { + return this._makeRequest({ + method: "DELETE", + path: `/subscribers/${listId}`, + params: { + email, + }, + }); + }, + async createSubscriber(email, listId, name) { + return this._makeRequest({ + method: "POST", + path: `/subscribers/${listId}`, + data: { + email, + list_id: listId, + name, + }, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/campaign_monitor/sources/new-bounce/new-bounce.mjs b/components/campaign_monitor/sources/new-bounce/new-bounce.mjs new file mode 100644 index 0000000000000..4cb57edee777b --- /dev/null +++ b/components/campaign_monitor/sources/new-bounce/new-bounce.mjs @@ -0,0 +1,53 @@ +import campaignMonitor from "../../campaign_monitor.app.mjs"; + +export default { + key: "campaign_monitor-new-bounce", + name: "New Bounce", + description: "Emits an event when a campaign email bounces", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + campaignMonitor: { + type: "app", + app: "campaign_monitor", + }, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: 60 * 15, // 15 minutes + }, + }, + campaignId: { + propDefinition: [ + campaignMonitor, + "campaignId", + ], + }, + }, + hooks: { + async deploy() { + // Get the most recent bounces to initialize the checkpoint. + const bounces = await this.campaignMonitor.emitCampaignEmailBounce(this.campaignId); + if (bounces && bounces.length > 0) { + this.db.set("lastBounceDate", bounces[0].Date); + } + }, + }, + async run() { + const lastBounceDate = this.db.get("lastBounceDate"); + const bounces = await this.campaignMonitor.emitCampaignEmailBounce(this.campaignId); + + for (const bounce of bounces) { + if (!lastBounceDate || new Date(bounce.Date) > new Date(lastBounceDate)) { + this.$emit(bounce, { + id: bounce.EmailAddress, + summary: `New bounce for ${bounce.EmailAddress}`, + ts: Date.parse(bounce.Date), + }); + this.db.set("lastBounceDate", bounce.Date); + } + } + }, +}; diff --git a/components/campaign_monitor/sources/new-email-open/new-email-open.mjs b/components/campaign_monitor/sources/new-email-open/new-email-open.mjs new file mode 100644 index 0000000000000..b156bac8b35cf --- /dev/null +++ b/components/campaign_monitor/sources/new-email-open/new-email-open.mjs @@ -0,0 +1,61 @@ +import campaignMonitor from "../../campaign_monitor.app.mjs"; + +export default { + key: "campaign_monitor-new-email-open", + name: "New Email Open", + description: "Emits a new event when an email from a campaign is opened", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + campaignMonitor: { + type: "app", + app: "campaign_monitor", + }, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: 60 * 15, // 15 minutes + }, + }, + campaignId: { + propDefinition: [ + campaignMonitor, + "campaignId", + ], + }, + subscriberId: { + propDefinition: [ + campaignMonitor, + "subscriberId", + ], + optional: true, + }, + }, + methods: { + _getEventMeta(event) { + const ts = +new Date(event.Date); + const summary = `New email open: ${event.EmailAddress}`; + const id = `${event.EmailAddress}-${ts}`; + return { + id, + summary, + ts, + }; + }, + }, + async run() { + const since = this.db.get("since"); + const { Results: events } = + await this.campaignMonitor.emitCampaignEmailOpen(this.campaignId, this.subscriberId); + for (const event of events) { + if (since && new Date(event.Date) <= new Date(since)) { + console.log("This email open event is old, skipping"); + continue; + } + this.$emit(event, this._getEventMeta(event)); + } + this.db.set("since", new Date()); + }, +}; diff --git a/components/campaign_monitor/sources/new-subscriber/new-subscriber.mjs b/components/campaign_monitor/sources/new-subscriber/new-subscriber.mjs new file mode 100644 index 0000000000000..144b271ff7b09 --- /dev/null +++ b/components/campaign_monitor/sources/new-subscriber/new-subscriber.mjs @@ -0,0 +1,58 @@ +import campaignMonitor from "../../campaign_monitor.app.mjs"; + +export default { + key: "campaign_monitor-new-subscriber", + name: "New Subscriber", + description: "Emits an event when a new subscriber is added to a specific list", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + campaignMonitor: { + type: "app", + app: "campaign_monitor", + }, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: 60 * 15, // 15 minutes + }, + }, + listId: { + propDefinition: [ + campaignMonitor, + "listId", + ], + }, + }, + methods: { + _getLastEvent() { + return this.db.get("lastEvent") || null; + }, + _setLastEvent(lastEvent) { + this.db.set("lastEvent", lastEvent); + }, + }, + async run() { + const subscribers = await this.campaignMonitor.emitNewSubscriber(this.listId); + if (!subscribers || subscribers.length === 0) { + console.log("No new subscribers found"); + return; + } + + let lastEvent = this._getLastEvent(); + subscribers.forEach((subscriber) => { + if (!lastEvent || new Date(subscriber.Date) > new Date(lastEvent)) { + lastEvent = subscriber.Date; + } + this.$emit(subscriber, { + id: subscriber.EmailAddress, + summary: `New subscriber ${subscriber.EmailAddress}`, + ts: Date.parse(subscriber.Date), + }); + }); + + this._setLastEvent(lastEvent); + }, +}; From 381c7b4482b1f6144dea8ebb0ad385d3632fea4d Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Tue, 12 Nov 2024 16:25:31 -0500 Subject: [PATCH 2/6] new components --- .../actions/add-subscriber/add-subscriber.mjs | 63 ++++- .../send-smart-transactional-email.mjs | 63 +++-- .../actions/unsubscribe/unsubscribe.mjs | 43 ++- .../campaign_monitor/campaign_monitor.app.mjs | 248 ++++++++++++++---- components/campaign_monitor/package.json | 7 +- .../campaign_monitor/sources/common/base.mjs | 82 ++++++ .../sources/new-bounce/new-bounce.mjs | 71 +++-- .../sources/new-email-open/new-email-open.mjs | 69 +++-- .../sources/new-subscriber/new-subscriber.mjs | 67 ++--- 9 files changed, 498 insertions(+), 215 deletions(-) create mode 100644 components/campaign_monitor/sources/common/base.mjs diff --git a/components/campaign_monitor/actions/add-subscriber/add-subscriber.mjs b/components/campaign_monitor/actions/add-subscriber/add-subscriber.mjs index 22656fb82445e..4d5e440692e1d 100644 --- a/components/campaign_monitor/actions/add-subscriber/add-subscriber.mjs +++ b/components/campaign_monitor/actions/add-subscriber/add-subscriber.mjs @@ -3,35 +3,82 @@ import campaignMonitor from "../../campaign_monitor.app.mjs"; export default { key: "campaign_monitor-add-subscriber", name: "Add Subscriber", - description: "Creates a new subscriber on a specific list. [See the documentation](https://www.campaignmonitor.com/api/subscribers/#adding-a-subscriber)", - version: "0.0.{{ts}}", + description: "Creates a new subscriber on a specific list. [See the documentation](https://www.campaignmonitor.com/api/v3-3/subscribers/)", + version: "0.0.1", type: "action", props: { campaignMonitor, - email: { + clientId: { propDefinition: [ campaignMonitor, - "email", + "clientId", ], }, listId: { propDefinition: [ campaignMonitor, "listId", + (c) => ({ + clientId: c.clientId, + }), ], }, + email: { + type: "string", + label: "Email", + description: "The email address of the subscriber", + }, name: { + type: "string", + label: "Name", + description: "The name of the subscriber", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number of the subscriber. Example: `+5012398752`", + optional: true, + }, + consentToTrack: { propDefinition: [ campaignMonitor, - "name", + "consentToTrack", + ], + }, + consentToSendSMS: { + type: "string", + label: "Consent to Send SMS", + description: "Indicates if consent has been granted by the subscriber to receive Sms", + options: [ + "Yes", + "No", + "Unchanged", ], + default: "Unchanged", + optional: true, + }, + resubscribe: { + type: "boolean", + label: "Resubscribe", + description: "Resubscribe if the email address has previously been unsubscribed", optional: true, }, }, async run({ $ }) { - const response = - await this.campaignMonitor.createSubscriber(this.email, this.listId, this.name); - $.export("$summary", `Successfully added subscriber ${this.email} to list ${this.listId}`); + const response = await this.campaignMonitor.createSubscriber({ + $, + listId: this.listId, + data: { + EmailAddress: this.email, + Name: this.name, + MobileNumber: this.phone, + ConsentToTrack: this.consentToTrack, + ConsentToSendSms: this.consentToSendSMS, + Resubscribe: this.resubscribe, + }, + }); + $.export("$summary", `Successfully added subscriber ${this.email}`); return response; }, }; diff --git a/components/campaign_monitor/actions/send-smart-transactional-email/send-smart-transactional-email.mjs b/components/campaign_monitor/actions/send-smart-transactional-email/send-smart-transactional-email.mjs index 26f0b7acc34ec..3507386a189f6 100644 --- a/components/campaign_monitor/actions/send-smart-transactional-email/send-smart-transactional-email.mjs +++ b/components/campaign_monitor/actions/send-smart-transactional-email/send-smart-transactional-email.mjs @@ -3,40 +3,61 @@ import campaignMonitor from "../../campaign_monitor.app.mjs"; export default { key: "campaign_monitor-send-smart-transactional-email", name: "Send Smart Transactional Email", - description: "Sends an intelligent transactional email to a specified recipient.", - version: "0.0.{{ts}}", + description: "Sends an intelligent transactional email to a specified recipient. [See the documentation](https://www.campaignmonitor.com/api/v3-3/transactional/#send-smart-email)", + version: "0.0.1", type: "action", props: { campaignMonitor, - email: { - type: "string", - label: "Email", - description: "The email of the recipient", + clientId: { + propDefinition: [ + campaignMonitor, + "clientId", + ], + }, + smartEmailId: { + propDefinition: [ + campaignMonitor, + "smartEmailId", + (c) => ({ + clientId: c.clientId, + }), + ], }, - subject: { + to: { type: "string", - label: "Subject", - description: "The subject of the email", + label: "To", + description: "An array of email addresses to send the email to", }, - content: { + cc: { type: "string", - label: "Content", - description: "The content of the email", + label: "CC", + description: "An array of email address to carbon copy the email to", + optional: true, }, - listId: { + bcc: { type: "string", - label: "List ID", - description: "The ID of the list", + label: "BCC", + description: "An array of email address to blind carbon copy the email to", optional: true, }, + consentToTrack: { + propDefinition: [ + campaignMonitor, + "consentToTrack", + ], + }, }, async run({ $ }) { - const response = await this.campaignMonitor.sendIntelligentEmail( - this.email, - this.subject, - this.content, - this.listId, - ); + const response = await this.campaignMonitor.sendSmartEmail({ + $, + smartEmailId: this.smartEmailId, + data: { + To: this.to, + CC: this.cc, + BCC: this.bcc, + ConsentToTrack: this.consentToTrack, + }, + }); $.export("$summary", "Successfully sent smart transactional email"); return response; }, diff --git a/components/campaign_monitor/actions/unsubscribe/unsubscribe.mjs b/components/campaign_monitor/actions/unsubscribe/unsubscribe.mjs index a766d53f7ff1c..fb5d562932f90 100644 --- a/components/campaign_monitor/actions/unsubscribe/unsubscribe.mjs +++ b/components/campaign_monitor/actions/unsubscribe/unsubscribe.mjs @@ -3,26 +3,45 @@ import campaignMonitor from "../../campaign_monitor.app.mjs"; export default { key: "campaign_monitor-unsubscribe", name: "Unsubscribe", - description: "Removes a subscriber from a mailing list given their email address.", - version: "0.0.{{ts}}", + description: "Removes a subscriber from a mailing list given their email address. [See the documentation](https://www.campaignmonitor.com/api/v3-3/subscribers/#unsubscribing-a-subscriber)", + version: "0.0.1", type: "action", props: { campaignMonitor, - email: { - type: "string", - label: "Email", - description: "The email of the subscriber to remove", + clientId: { + propDefinition: [ + campaignMonitor, + "clientId", + ], }, listId: { - type: "string", - label: "List ID", - description: "The ID of the mailing list from which to remove the subscriber", - optional: true, + propDefinition: [ + campaignMonitor, + "listId", + (c) => ({ + clientId: c.clientId, + }), + ], + }, + subscriber: { + propDefinition: [ + campaignMonitor, + "subscriber", + (c) => ({ + listId: c.listId, + }), + ], }, }, async run({ $ }) { - const response = await this.campaignMonitor.removeSubscriber(this.email, this.listId); - $.export("$summary", `Successfully unsubscribed ${this.email}`); + const response = await this.campaignMonitor.unsubscribeSubscriber({ + $, + listId: this.listId, + data: { + EmailAddress: this.subscriber, + }, + }); + $.export("$summary", `Successfully unsubscribed ${this.subscriber}`); return response; }, }; diff --git a/components/campaign_monitor/campaign_monitor.app.mjs b/components/campaign_monitor/campaign_monitor.app.mjs index 986b68d37663f..9260e0af3bc39 100644 --- a/components/campaign_monitor/campaign_monitor.app.mjs +++ b/components/campaign_monitor/campaign_monitor.app.mjs @@ -1,44 +1,103 @@ import { axios } from "@pipedream/platform"; +const DEFAULT_PAGE_SIZE = 1000; export default { type: "app", app: "campaign_monitor", propDefinitions: { + clientId: { + type: "string", + label: "Client ID", + description: "The ID of the client", + async options() { + const clients = await this.listClients(); + return clients?.map(({ + ClientID: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, + }, campaignId: { type: "string", label: "Campaign ID", - description: "The ID of the campaign", + description: "The ID of a sent or scheduled campaign", + async options({ clientId }) { + const campaigns = await this.listCampaigns(clientId); + return campaigns?.map(({ + CampaignID: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, }, - subscriberId: { + subscriber: { type: "string", - label: "Subscriber ID", - description: "The ID of the subscriber", - optional: true, + label: "Subscriber", + description: "The email address of the subscriber", + async options({ + listId, page, + }) { + const { Results: subscribers } = await this.listSubscribers({ + listId, + params: { + page: page + 1, + }, + }); + return subscribers?.map(({ + EmailAddress: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, }, listId: { type: "string", label: "List ID", description: "The ID of the list", + async options({ clientId }) { + const lists = await this.listLists({ + clientId, + }); + return lists?.map(({ + ListID: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, }, - email: { - type: "string", - label: "Email", - description: "The email of the recipient", - }, - subject: { + smartEmailId: { type: "string", - label: "Subject", - description: "The subject of the email", + label: "Smart Email ID", + description: "The ID of the smart email to send", + async options({ clientId }) { + const emails = await this.listSmartEmails({ + params: { + clientId, + }, + }); + return emails?.map(({ + ID: value, Name: label, + }) => ({ + value, + label, + })) || []; + }, }, - content: { + consentToTrack: { type: "string", - label: "Content", - description: "The content of the email", - }, - name: { - type: "string", - label: "Name", - description: "The name of the subscriber", + label: "Consent to Track", + description: "Whether the subscriber has given permission to have their email opens and clicks tracked", + options: [ + "Yes", + "No", + "Unchanged", + ], + default: "Unchanged", optional: true, }, }, @@ -46,73 +105,148 @@ export default { _baseUrl() { return "https://api.createsend.com/api/v3.3"; }, - async _makeRequest(opts = {}) { + _makeRequest(opts = {}) { const { $ = this, - method = "GET", path, - headers, ...otherOpts } = opts; return axios($, { ...otherOpts, - method, - url: this._baseUrl() + path, + url: `${this._baseUrl()}${path}`, headers: { - ...headers, - Authorization: `Bearer ${this.$auth.api_key}`, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, }, }); }, - async emitCampaignEmailBounce(campaignId) { + listClients(opts = {}) { return this._makeRequest({ - path: `/campaigns/${campaignId}/bounces`, + path: "/clients.json", + ...opts, }); }, - async emitCampaignEmailOpen(campaignId, subscriberId) { - return this._makeRequest({ - path: `/campaigns/${campaignId}/opens`, + async listCampaigns(clientId) { + const scheduledCampaigns = await this.listScheduledCampaigns({ + clientId, + }); + const { Results: sentCampaigns } = await this.listSentCampaigns({ + clientId, params: { - subscriber_id: subscriberId, + pagesize: DEFAULT_PAGE_SIZE, }, }); + return [ + ...scheduledCampaigns, + ...sentCampaigns, + ]; + }, + listSentCampaigns({ + clientId, ...opts + }) { + return this._makeRequest({ + path: `/clients/${clientId}/campaigns.json`, + ...opts, + }); + }, + listScheduledCampaigns({ + clientId, ...opts + }) { + return this._makeRequest({ + path: `/clients/${clientId}/scheduled.json`, + ...opts, + }); + }, + listBounces({ + campaignId, ...opts + }) { + return this._makeRequest({ + path: `/campaigns/${campaignId}/bounces.json`, + ...opts, + }); }, - async emitNewSubscriber(listId) { + listOpens({ + campaignId, ...opts + }) { return this._makeRequest({ - path: `/lists/${listId}/active`, + path: `/campaigns/${campaignId}/opens.json`, + ...opts, }); }, - async sendIntelligentEmail(email, subject, content, listId) { + listSubscribers({ + listId, ...opts + }) { + return this._makeRequest({ + path: `/lists/${listId}/active.json`, + ...opts, + }); + }, + listLists({ + clientId, ...opts + }) { + return this._makeRequest({ + path: `/clients/${clientId}/lists.json`, + ...opts, + }); + }, + listSmartEmails(opts = {}) { + return this._makeRequest({ + path: "/transactional/smartEmail", + ...opts, + }); + }, + sendSmartEmail({ + smartEmailId, ...opts + }) { return this._makeRequest({ method: "POST", - path: "/transactional/send", - data: { - email, - subject, - content, - list_id: listId, - }, + path: `/transactional/smartEmail/${smartEmailId}/send`, + ...opts, }); }, - async removeSubscriber(email, listId) { + unsubscribeSubscriber({ + listId, ...opts + }) { return this._makeRequest({ - method: "DELETE", - path: `/subscribers/${listId}`, - params: { - email, - }, + method: "POST", + path: `/subscribers/${listId}/unsubscribe.json`, + ...opts, }); }, - async createSubscriber(email, listId, name) { + createSubscriber({ + listId, ...opts + }) { return this._makeRequest({ method: "POST", - path: `/subscribers/${listId}`, - data: { - email, - list_id: listId, - name, - }, + path: `/subscribers/${listId}.json`, + ...opts, }); }, + async *paginate({ + fn, + args, + max, + }) { + args = { + ...args, + params: { + ...args?.params, + pagesize: DEFAULT_PAGE_SIZE, + }, + }; + let hasMore, count = 0; + do { + const { + Results: results, NumberOfPages: numPages, + } = await fn(args); + for (const item of results) { + yield item; + if (max && ++count >= max) { + return; + } + hasMore = args.params.page < numPages; + args.params.page++; + } + } while (hasMore); + }, }, }; diff --git a/components/campaign_monitor/package.json b/components/campaign_monitor/package.json index d3f633809b89e..2a5c8328cfbcc 100644 --- a/components/campaign_monitor/package.json +++ b/components/campaign_monitor/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/campaign_monitor", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Campaign Monitor Components", "main": "campaign_monitor.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } -} \ No newline at end of file +} diff --git a/components/campaign_monitor/sources/common/base.mjs b/components/campaign_monitor/sources/common/base.mjs new file mode 100644 index 0000000000000..df120d4c3fe96 --- /dev/null +++ b/components/campaign_monitor/sources/common/base.mjs @@ -0,0 +1,82 @@ +import campaignMonitor from "../../campaign_monitor.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + campaignMonitor, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + clientId: { + propDefinition: [ + campaignMonitor, + "clientId", + ], + }, + }, + hooks: { + async deploy() { + await this.processEvent(25); + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + getArgs() { + return {}; + }, + getTsField() { + return "Date"; + }, + getResourceFn() { + throw new Error("getResourceFn is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + async processEvent(max) { + const lastTs = this._getLastTs(); + const fn = this.getResourceFn(); + const args = this.getArgs(); + const tsField = this.getTsField(); + + const results = this.campaignMonitor.paginate({ + fn, + args, + max, + }); + + const items = []; + for await (const item of results) { + const ts = Date.parse(item[tsField]); + if (ts >= lastTs) { + items.push(item); + } else { + break; + } + } + + if (!items?.length) { + return; + } + + this._setLastTs(Date.parse(items[0][tsField])); + + items.forEach((item) => { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }); + }, + }, + async run() { + await this.processEvent(); + }, +}; diff --git a/components/campaign_monitor/sources/new-bounce/new-bounce.mjs b/components/campaign_monitor/sources/new-bounce/new-bounce.mjs index 4cb57edee777b..6b9b825f50382 100644 --- a/components/campaign_monitor/sources/new-bounce/new-bounce.mjs +++ b/components/campaign_monitor/sources/new-bounce/new-bounce.mjs @@ -1,53 +1,52 @@ -import campaignMonitor from "../../campaign_monitor.app.mjs"; +import common from "../common/base.mjs"; export default { + ...common, key: "campaign_monitor-new-bounce", name: "New Bounce", - description: "Emits an event when a campaign email bounces", - version: "0.0.{{ts}}", + description: "Emit new event when a campaign email bounces", + version: "0.0.1", type: "source", dedupe: "unique", props: { - campaignMonitor: { - type: "app", - app: "campaign_monitor", - }, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: 60 * 15, // 15 minutes - }, + ...common.props, + clientId: { + propDefinition: [ + common.props.campaignMonitor, + "clientId", + ], }, campaignId: { propDefinition: [ - campaignMonitor, + common.props.campaignMonitor, "campaignId", + (c) => ({ + clientId: c.clientId, + }), ], }, }, - hooks: { - async deploy() { - // Get the most recent bounces to initialize the checkpoint. - const bounces = await this.campaignMonitor.emitCampaignEmailBounce(this.campaignId); - if (bounces && bounces.length > 0) { - this.db.set("lastBounceDate", bounces[0].Date); - } + methods: { + ...common.methods, + getResourceFn() { + return this.campaignMonitor.listBounces; + }, + getArgs() { + return { + campaignId: this.campaignId, + params: { + orderfield: "date", + orderdirection: "desc", + }, + }; + }, + generateMeta(bounce) { + const ts = Date.parse(bounce[this.getTsField()]); + return { + id: `${bounce.EmailAddress}-${ts}`, + summary: `New Bounce: ${bounce.EmailAddress}`, + ts, + }; }, - }, - async run() { - const lastBounceDate = this.db.get("lastBounceDate"); - const bounces = await this.campaignMonitor.emitCampaignEmailBounce(this.campaignId); - - for (const bounce of bounces) { - if (!lastBounceDate || new Date(bounce.Date) > new Date(lastBounceDate)) { - this.$emit(bounce, { - id: bounce.EmailAddress, - summary: `New bounce for ${bounce.EmailAddress}`, - ts: Date.parse(bounce.Date), - }); - this.db.set("lastBounceDate", bounce.Date); - } - } }, }; diff --git a/components/campaign_monitor/sources/new-email-open/new-email-open.mjs b/components/campaign_monitor/sources/new-email-open/new-email-open.mjs index b156bac8b35cf..222f4a214565f 100644 --- a/components/campaign_monitor/sources/new-email-open/new-email-open.mjs +++ b/components/campaign_monitor/sources/new-email-open/new-email-open.mjs @@ -1,61 +1,52 @@ -import campaignMonitor from "../../campaign_monitor.app.mjs"; +import common from "../common/base.mjs"; export default { + ...common, key: "campaign_monitor-new-email-open", name: "New Email Open", - description: "Emits a new event when an email from a campaign is opened", + description: "Emit new event when an email from a campaign is opened", version: "0.0.1", type: "source", dedupe: "unique", props: { - campaignMonitor: { - type: "app", - app: "campaign_monitor", - }, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: 60 * 15, // 15 minutes - }, - }, - campaignId: { + ...common.props, + clientId: { propDefinition: [ - campaignMonitor, - "campaignId", + common.props.campaignMonitor, + "clientId", ], }, - subscriberId: { + campaignId: { propDefinition: [ - campaignMonitor, - "subscriberId", + common.props.campaignMonitor, + "campaignId", + (c) => ({ + clientId: c.clientId, + }), ], - optional: true, }, }, methods: { - _getEventMeta(event) { - const ts = +new Date(event.Date); - const summary = `New email open: ${event.EmailAddress}`; - const id = `${event.EmailAddress}-${ts}`; + ...common.methods, + getResourceFn() { + return this.campaignMonitor.listOpens; + }, + getArgs() { return { - id, - summary, + campaignId: this.campaignId, + params: { + orderfield: "date", + orderdirection: "desc", + }, + }; + }, + generateMeta(open) { + const ts = Date.parse(open[this.getTsField()]); + return { + id: `${open.EmailAddress}-${ts}`, + summary: `New Email Open: ${open.EmailAddress}`, ts, }; }, }, - async run() { - const since = this.db.get("since"); - const { Results: events } = - await this.campaignMonitor.emitCampaignEmailOpen(this.campaignId, this.subscriberId); - for (const event of events) { - if (since && new Date(event.Date) <= new Date(since)) { - console.log("This email open event is old, skipping"); - continue; - } - this.$emit(event, this._getEventMeta(event)); - } - this.db.set("since", new Date()); - }, }; diff --git a/components/campaign_monitor/sources/new-subscriber/new-subscriber.mjs b/components/campaign_monitor/sources/new-subscriber/new-subscriber.mjs index 144b271ff7b09..5149782b83839 100644 --- a/components/campaign_monitor/sources/new-subscriber/new-subscriber.mjs +++ b/components/campaign_monitor/sources/new-subscriber/new-subscriber.mjs @@ -1,58 +1,45 @@ -import campaignMonitor from "../../campaign_monitor.app.mjs"; +import common from "../common/base.mjs"; export default { + ...common, key: "campaign_monitor-new-subscriber", - name: "New Subscriber", - description: "Emits an event when a new subscriber is added to a specific list", - version: "0.0.{{ts}}", + name: "New Subscriber Added", + description: "Emit new event when a new subscriber is added to a specific list", + version: "0.0.1", type: "source", dedupe: "unique", props: { - campaignMonitor: { - type: "app", - app: "campaign_monitor", - }, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: 60 * 15, // 15 minutes - }, - }, + ...common.props, listId: { propDefinition: [ - campaignMonitor, + common.props.campaignMonitor, "listId", + (c) => ({ + clientId: c.clientId, + }), ], }, }, methods: { - _getLastEvent() { - return this.db.get("lastEvent") || null; + ...common.methods, + getResourceFn() { + return this.campaignMonitor.listSubscribers; }, - _setLastEvent(lastEvent) { - this.db.set("lastEvent", lastEvent); + getArgs() { + return { + listId: this.listId, + params: { + orderfield: "date", + orderdirection: "desc", + }, + }; }, - }, - async run() { - const subscribers = await this.campaignMonitor.emitNewSubscriber(this.listId); - if (!subscribers || subscribers.length === 0) { - console.log("No new subscribers found"); - return; - } - - let lastEvent = this._getLastEvent(); - subscribers.forEach((subscriber) => { - if (!lastEvent || new Date(subscriber.Date) > new Date(lastEvent)) { - lastEvent = subscriber.Date; - } - this.$emit(subscriber, { + generateMeta(subscriber) { + return { id: subscriber.EmailAddress, - summary: `New subscriber ${subscriber.EmailAddress}`, - ts: Date.parse(subscriber.Date), - }); - }); - - this._setLastEvent(lastEvent); + summary: `New Subscriber: ${subscriber.EmailAddress}`, + ts: Date.parse(subscriber[this.getTsField()]), + }; + }, }, }; From 9bd98dfa67fac7c4df10282d730d19a13bb5201e Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Tue, 12 Nov 2024 16:28:06 -0500 Subject: [PATCH 3/6] pnpm-lock.yaml --- pnpm-lock.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7315a9f70c3f..d5a384ebbea32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1485,7 +1485,10 @@ importers: '@pipedream/platform': 1.5.1 components/campaign_monitor: - specifiers: {} + specifiers: + '@pipedream/platform': ^3.0.3 + dependencies: + '@pipedream/platform': 3.0.3 components/campaignhq: specifiers: {} From 24c7853e5735137a5d381a39fa51fd54595b931a Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Tue, 12 Nov 2024 16:45:16 -0500 Subject: [PATCH 4/6] fix pagination --- components/campaign_monitor/campaign_monitor.app.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/campaign_monitor/campaign_monitor.app.mjs b/components/campaign_monitor/campaign_monitor.app.mjs index 9260e0af3bc39..e0d6f8b46db0b 100644 --- a/components/campaign_monitor/campaign_monitor.app.mjs +++ b/components/campaign_monitor/campaign_monitor.app.mjs @@ -243,9 +243,9 @@ export default { if (max && ++count >= max) { return; } - hasMore = args.params.page < numPages; - args.params.page++; } + hasMore = args.params.page < numPages; + args.params.page++; } while (hasMore); }, }, From c1ea1f4d30b771b42691a221facef9dceeff0430 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Wed, 13 Nov 2024 11:31:03 -0500 Subject: [PATCH 5/6] remove redundant clientId prop --- .../campaign_monitor/sources/new-bounce/new-bounce.mjs | 6 ------ .../sources/new-email-open/new-email-open.mjs | 6 ------ 2 files changed, 12 deletions(-) diff --git a/components/campaign_monitor/sources/new-bounce/new-bounce.mjs b/components/campaign_monitor/sources/new-bounce/new-bounce.mjs index 6b9b825f50382..868af3f4d7f31 100644 --- a/components/campaign_monitor/sources/new-bounce/new-bounce.mjs +++ b/components/campaign_monitor/sources/new-bounce/new-bounce.mjs @@ -10,12 +10,6 @@ export default { dedupe: "unique", props: { ...common.props, - clientId: { - propDefinition: [ - common.props.campaignMonitor, - "clientId", - ], - }, campaignId: { propDefinition: [ common.props.campaignMonitor, diff --git a/components/campaign_monitor/sources/new-email-open/new-email-open.mjs b/components/campaign_monitor/sources/new-email-open/new-email-open.mjs index 222f4a214565f..199bf6ed132ee 100644 --- a/components/campaign_monitor/sources/new-email-open/new-email-open.mjs +++ b/components/campaign_monitor/sources/new-email-open/new-email-open.mjs @@ -10,12 +10,6 @@ export default { dedupe: "unique", props: { ...common.props, - clientId: { - propDefinition: [ - common.props.campaignMonitor, - "clientId", - ], - }, campaignId: { propDefinition: [ common.props.campaignMonitor, From 90b4b2b233dd3ccae51730f95238a57c768692d6 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Thu, 14 Nov 2024 12:46:59 -0500 Subject: [PATCH 6/6] add retry --- .../campaign_monitor/campaign_monitor.app.mjs | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/components/campaign_monitor/campaign_monitor.app.mjs b/components/campaign_monitor/campaign_monitor.app.mjs index e0d6f8b46db0b..ead348f6733b7 100644 --- a/components/campaign_monitor/campaign_monitor.app.mjs +++ b/components/campaign_monitor/campaign_monitor.app.mjs @@ -105,19 +105,37 @@ export default { _baseUrl() { return "https://api.createsend.com/api/v3.3"; }, - _makeRequest(opts = {}) { + async _makeRequest(opts = {}) { const { $ = this, path, ...otherOpts } = opts; - return axios($, { - ...otherOpts, - url: `${this._baseUrl()}${path}`, - headers: { - Authorization: `Bearer ${this.$auth.oauth_access_token}`, - }, - }); + const requestFn = async () => { + return await axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }; + return await this.retryWithExponentialBackoff(requestFn); + }, + // The API has been observed to occasionally return - + // {"Code":120,"Message":"Invalid OAuth Token"} + // Retry if a 401 Unauthorized or 429 (Rate limit exceeded) + // status is returned + async retryWithExponentialBackoff(requestFn, retries = 3, backoff = 500) { + try { + return await requestFn(); + } catch (error) { + if (retries > 0 && (error.response?.status === 401 || error.response?.status === 429)) { + await new Promise((resolve) => setTimeout(resolve, backoff)); + return this.retryWithExponentialBackoff(requestFn, retries - 1, backoff * 2); + } + throw error; + } }, listClients(opts = {}) { return this._makeRequest({