From 221fcc0f3168ed921df40555bc599011b5b93045 Mon Sep 17 00:00:00 2001 From: Michelle Bergeron Date: Fri, 3 Oct 2025 15:49:08 -0400 Subject: [PATCH 1/2] new components --- .../sinch/actions/send-fax/send-fax.mjs | 86 +++++++++++ .../actions/send-message/send-message.mjs | 88 ++++++++++++ components/sinch/common/constants.mjs | 20 +++ components/sinch/package.json | 8 +- components/sinch/sinch.app.mjs | 134 +++++++++++++++++- components/sinch/sources/common/base.mjs | 28 ++++ .../new-fax-received/new-fax-received.mjs | 43 ++++++ .../sources/new-fax-received/test-event.mjs | 21 +++ .../new-message-received.mjs | 55 +++++++ .../new-message-received/test-event.mjs | 20 +++ 10 files changed, 496 insertions(+), 7 deletions(-) create mode 100644 components/sinch/actions/send-fax/send-fax.mjs create mode 100644 components/sinch/actions/send-message/send-message.mjs create mode 100644 components/sinch/common/constants.mjs create mode 100644 components/sinch/sources/common/base.mjs create mode 100644 components/sinch/sources/new-fax-received/new-fax-received.mjs create mode 100644 components/sinch/sources/new-fax-received/test-event.mjs create mode 100644 components/sinch/sources/new-message-received/new-message-received.mjs create mode 100644 components/sinch/sources/new-message-received/test-event.mjs diff --git a/components/sinch/actions/send-fax/send-fax.mjs b/components/sinch/actions/send-fax/send-fax.mjs new file mode 100644 index 0000000000000..8f0bd61793acb --- /dev/null +++ b/components/sinch/actions/send-fax/send-fax.mjs @@ -0,0 +1,86 @@ +import sinch from "../../sinch.app.mjs"; +import { getFileStreamAndMetadata } from "@pipedream/platform"; +import FormData from "form-data"; + +export default { + key: "sinch-send-fax", + name: "Send Fax", + description: "Send a fax to a contact. [See the documentation](https://developers.sinch.com/docs/fax/api-reference/fax/tag/Faxes/#tag/Faxes/operation/sendFax)", + version: "0.0.1", + type: "action", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + props: { + sinch, + to: { + type: "string", + label: "To", + description: "The phone number to send the fax to", + }, + file: { + type: "string", + label: "File Path or URL", + description: "Provide either a file URL or a path to a file in the /tmp directory (for example, /tmp/myFile.pdf).", + }, + from: { + type: "string", + label: "From", + description: "The phone number of the sender", + optional: true, + }, + headerText: { + type: "string", + label: "Header Text", + description: "Text that will be displayed at the top of each page of the fax. 50 characters maximum.", + optional: true, + }, + retryDelaySeconds: { + type: "integer", + label: "Retry Delay Seconds", + description: "The number of seconds to wait between retries if the fax is not yet completed", + optional: true, + }, + syncDir: { + type: "dir", + accessMode: "read", + sync: true, + optional: true, + }, + }, + async run({ $ }) { + const data = new FormData(); + + const { + stream, metadata, + } = await getFileStreamAndMetadata(this.file); + + data.append("file", stream, { + contentType: metadata.contentType, + knownLength: metadata.size, + filename: metadata.name, + }); + + data.append("to", this.to); + if (this.from) { + data.append("from", this.from); + } + if (this.headerText) { + data.append("headerText", this.headerText); + } + if (this.retryDelaySeconds) { + data.append("retryDelaySeconds", this.retryDelaySeconds); + } + + const response = await this.sinch.sendFax({ + $, + data, + headers: data.getHeaders(), + }); + + $.export("$summary", `Successfully sent fax to ${this.to}`); + return response; + }, +}; diff --git a/components/sinch/actions/send-message/send-message.mjs b/components/sinch/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..47ee6065eb409 --- /dev/null +++ b/components/sinch/actions/send-message/send-message.mjs @@ -0,0 +1,88 @@ +import sinch from "../../sinch.app.mjs"; +import constants from "../../common/constants.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "sinch-send-message", + name: "Send Message", + description: "Send a message to a contact. [See the documentation](https://developers.sinch.com/docs/conversation/api-reference/conversation/tag/Messages/#tag/Messages/operation/Messages_SendMessage)", + version: "0.0.1", + type: "action", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + props: { + sinch, + appId: { + propDefinition: [ + sinch, + "appId", + ], + }, + message: { + type: "string", + label: "Message", + description: "The message to send", + }, + contactId: { + propDefinition: [ + sinch, + "contactId", + ], + description: "The ID of the recipient. Overrides channel and identity", + optional: true, + }, + channel: { + type: "string", + label: "Channel", + description: "The channel to send the message to", + options: constants.CHANNELS, + optional: true, + }, + identity: { + type: "string", + label: "Identity", + description: "The channel identity. This will differ from channel to channel. For example, a phone number for SMS, WhatsApp, and Viber Business.", + optional: true, + }, + }, + async run({ $ }) { + if (!this.contactId && !this.identity) { + throw new ConfigurationError("You must provide either a contact ID or an identity."); + } + + if (this.identity && !this.channel) { + throw new ConfigurationError("You must provide a channel when providing an identity."); + } + + const response = await this.sinch.sendMessage({ + $, + data: { + app_id: this.appId, + recipient: this.contactId + ? { + contact_id: this.contactId, + } + : { + identified_by: { + channel_identities: [ + { + channel: this.channel, + identity: this.identity, + }, + ], + }, + }, + message: { + text_message: { + text: this.message, + }, + }, + }, + }); + $.export("$summary", "Successfully sent message"); + return response; + }, +}; diff --git a/components/sinch/common/constants.mjs b/components/sinch/common/constants.mjs new file mode 100644 index 0000000000000..92f4ab7a6861e --- /dev/null +++ b/components/sinch/common/constants.mjs @@ -0,0 +1,20 @@ +const CHANNELS = [ + "WHATSAPP", + "RCS", + "SMS", + "MESSENGER", + "VIBER", + "VIBERBM", + "MMS", + "INSTAGRAM", + "TELEGRAM", + "KAKAOTALK", + "KAKAOTALKCHAT", + "LINE", + "WECHAT", + "APPLEBC", +]; + +export default { + CHANNELS, +}; diff --git a/components/sinch/package.json b/components/sinch/package.json index 669592145504b..6d061de1c6323 100644 --- a/components/sinch/package.json +++ b/components/sinch/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/sinch", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Sinch Components", "main": "sinch.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.1.0", + "form-data": "^4.0.4" } -} \ No newline at end of file +} diff --git a/components/sinch/sinch.app.mjs b/components/sinch/sinch.app.mjs index 489aa82a8edf9..e99205ba8f0f6 100644 --- a/components/sinch/sinch.app.mjs +++ b/components/sinch/sinch.app.mjs @@ -1,11 +1,135 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "sinch", - propDefinitions: {}, + propDefinitions: { + appId: { + type: "string", + label: "App ID", + description: "The ID of an app", + async options() { + const { apps } = await this.listApps(); + return apps?.map(({ + id: value, display_name: label, + }) => ({ + label, + value, + })) || []; + }, + }, + contactId: { + type: "string", + label: "Contact ID", + description: "The ID of a contact", + async options({ prevContext }) { + const { + contacts, next_page_token: next, + } = await this.listContacts({ + params: { + page_token: prevContext?.nextPageToken, + }, + }); + return { + options: contacts?.map(({ + id: value, display_name: label, + }) => ({ + label, + value, + })) || [], + context: { + nextPageToken: next, + }, + }; + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _getBaseUrl(api) { + if (api === "conversation") { + return `https://${this.$auth.region}.conversation.api.sinch.com/v1`; + } + if (api === "fax") { + return "https://fax.api.sinch.com/v3"; + } + }, + _makeRequest({ + $ = this, path, api = "conversation", headers, ...opts + }) { + return axios($, { + url: `${this._getBaseUrl(api)}/projects/${this.$auth.project_id}${path}`, + headers: { + ...headers, + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + api: "conversation", + path: "/webhooks", + ...opts, + }); + }, + deleteWebhook({ + webhookId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + api: "conversation", + path: `/webhooks/${webhookId}`, + ...opts, + }); + }, + createService(opts = {}) { + return this._makeRequest({ + method: "POST", + api: "fax", + path: "/services", + ...opts, + }); + }, + deleteService({ + serviceId, ...opts + }) { + return this._makeRequest({ + method: "DELETE", + api: "fax", + path: `/services/${serviceId}`, + ...opts, + }); + }, + listApps(opts = {}) { + return this._makeRequest({ + api: "conversation", + path: "/apps", + ...opts, + }); + }, + listContacts(opts = {}) { + return this._makeRequest({ + api: "conversation", + path: "/contacts", + ...opts, + }); + }, + sendMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + api: "conversation", + path: "/messages:send", + ...opts, + }); + }, + sendFax(opts = {}) { + return this._makeRequest({ + method: "POST", + api: "fax", + path: "/faxes", + ...opts, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/sinch/sources/common/base.mjs b/components/sinch/sources/common/base.mjs new file mode 100644 index 0000000000000..d46261e478791 --- /dev/null +++ b/components/sinch/sources/common/base.mjs @@ -0,0 +1,28 @@ +import sinch from "../../sinch.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + props: { + sinch, + db: "$.service.db", + http: "$.interface.http", + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + generateMeta() { + throw new ConfigurationError("generateMeta is not implemented"); + }, + }, + async run(event) { + const { body } = event; + if (!body) { + return; + } + this.$emit(body, this.generateMeta(body)); + }, +}; diff --git a/components/sinch/sources/new-fax-received/new-fax-received.mjs b/components/sinch/sources/new-fax-received/new-fax-received.mjs new file mode 100644 index 0000000000000..7fcb026cd8d4a --- /dev/null +++ b/components/sinch/sources/new-fax-received/new-fax-received.mjs @@ -0,0 +1,43 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "sinch-new-fax-received", + name: "New Fax Received (Instant)", + description: "Emit new event when a new fax is received. [See the documentation](https://developers.sinch.com/docs/fax/api-reference/fax/tag/Services/#tag/Services/operation/createService)", + version: "0.0.1", + type: "source", + dedupe: "unique", + hooks: { + async activate() { + const { id } = await this.sinch.createService({ + data: { + incomingWebhookUrl: this.http.endpoint, + webhookContentType: "application/json", + defaultForProject: true, + }, + }); + this._setHookId(id); + }, + async deactivate() { + const serviceId = this._getHookId(); + if (serviceId) { + await this.sinch.deleteService({ + serviceId, + }); + } + }, + }, + methods: { + ...common.methods, + generateMeta(event) { + return { + id: event.fax.id, + summary: `New fax from: ${event.fax.from}`, + ts: Date.parse(event.eventTime), + }; + }, + }, + sampleEmit, +}; diff --git a/components/sinch/sources/new-fax-received/test-event.mjs b/components/sinch/sources/new-fax-received/test-event.mjs new file mode 100644 index 0000000000000..37d25a100c309 --- /dev/null +++ b/components/sinch/sources/new-fax-received/test-event.mjs @@ -0,0 +1,21 @@ +export default { + "event": "INCOMING_FAX", + "eventTime": "2021-11-01T23:25:67Z", + "fax": { + "id": "01HDFHACK1YN7CCDYRA6ZRMA8Z", + "direction": "INBOUND", + "from": "+14155552222", + "to": "+14155553333", + "numberOfPages": 1, + "status": "COMPLETED", + "price": { + "amount": "0.00", + "currencyCode": "USD" + }, + "createTime": "2021-11-01T23:25:67Z", + "completedTime": "2021-11-01T23:25:67Z", + "projectId": "YOUR_PROJECT_ID", + "serviceId": "YOUR_SERVICE_ID" + }, + "file": "{application/pdf}" +} \ No newline at end of file diff --git a/components/sinch/sources/new-message-received/new-message-received.mjs b/components/sinch/sources/new-message-received/new-message-received.mjs new file mode 100644 index 0000000000000..ae477edc2dd55 --- /dev/null +++ b/components/sinch/sources/new-message-received/new-message-received.mjs @@ -0,0 +1,55 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "sinch-new-message-received", + name: "New Message Received (Instant)", + description: "Emit new event when a new message is received. [See the documentation](https://developers.sinch.com/docs/conversation/api-reference/conversation/tag/Webhooks/#tag/Webhooks/operation/Webhooks_CreateWebhook)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + appId: { + propDefinition: [ + common.props.sinch, + "appId", + ], + }, + }, + hooks: { + async activate() { + const { id } = await this.sinch.createWebhook({ + data: { + target: this.http.endpoint, + app_id: this.appId, + triggers: [ + "MESSAGE_INBOUND", + ], + target_type: "HTTP", + }, + }); + this._setHookId(id); + }, + async deactivate() { + const webhookId = this._getHookId(); + if (webhookId) { + await this.sinch.deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + ...common.methods, + generateMeta(event) { + return { + id: event.message_delivery_report.message_id, + summary: `New message with ID: ${event.message_delivery_report.message_id}`, + ts: Date.parse(event.event_time), + }; + }, + }, + sampleEmit, +}; diff --git a/components/sinch/sources/new-message-received/test-event.mjs b/components/sinch/sources/new-message-received/test-event.mjs new file mode 100644 index 0000000000000..3dc2014d75732 --- /dev/null +++ b/components/sinch/sources/new-message-received/test-event.mjs @@ -0,0 +1,20 @@ +export default { + "app_id": "01EB37HMH1M6SV18BSNS3G135H", + "accepted_time": "2020-11-17T15:09:11.659Z", + "event_time": "2020-11-17T15:09:13.267185Z", + "project_id": "c36f3d3d-1513-2edd-ae42-11995557ff61", + "message_delivery_report": { + "message_id": "01EQBC1A3BEK731GY4YXEN0C2R", + "conversation_id": "01EPYATA64TMNZ1FV02JKF12JF", + "status": "QUEUED_ON_CHANNEL", + "channel_identity": { + "channel": "MESSENGER", + "identity": "2734085512340733", + "app_id": "01EB27HMH1M6SV18ASNS3G135H" + }, + "contact_id": "01EXA07N79THJ20WSN6AS30TMW", + "metadata": "", + "processing_mode": "CONVERSATION" + }, + "message_metadata": "" +} \ No newline at end of file From 442205954c0c8fb1556a0125a806173e43153fa4 Mon Sep 17 00:00:00 2001 From: Michelle Bergeron Date: Fri, 3 Oct 2025 15:49:44 -0400 Subject: [PATCH 2/2] pnpm-lock.yaml --- pnpm-lock.yaml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22215e9439823..f686711b10b3e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -623,8 +623,7 @@ importers: specifier: ^2.30.1 version: 2.30.1 - components/airtop: - specifiers: {} + components/airtop: {} components/aitable_ai: dependencies: @@ -7684,8 +7683,7 @@ importers: specifier: ^3.1.0 version: 3.1.0 - components/kudosity: - specifiers: {} + components/kudosity: {} components/kustomer: dependencies: @@ -13295,7 +13293,13 @@ importers: version: 3.0.3 components/sinch: - specifiers: {} + dependencies: + '@pipedream/platform': + specifier: ^3.1.0 + version: 3.1.0 + form-data: + specifier: ^4.0.4 + version: 4.0.4 components/sinch_messagemedia: {}