From a981fcfe832aa4a12929f44736bf0afab66506ac Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Thu, 7 Nov 2024 10:46:35 -0300 Subject: [PATCH] New Components - openphone (#14505) * openphone init * [Components] openphone #14493 Sources - New Call Recording Completed (Instant) - New Outgoing Call Completed (Instant) - New Incoming Call Completed (Instant) Actions - Send Message - Create Contact - Update Contact * pnpm update * fix source --- .../actions/create-contact/create-contact.mjs | 80 ++++++++++++ .../actions/send-message/send-message.mjs | 57 +++++++++ .../actions/update-contact/update-contact.mjs | 87 +++++++++++++ components/openphone/common/utils.mjs | 26 ++++ components/openphone/openphone.app.mjs | 118 +++++++++++++++++- components/openphone/package.json | 7 +- components/openphone/sources/common/base.mjs | 62 +++++++++ .../new-call-recording-completed-instant.mjs | 24 ++++ .../test-event.mjs | 30 +++++ .../new-incoming-call-completed-instant.mjs | 27 ++++ .../test-event.mjs | 28 +++++ .../new-outgoing-call-completed-instant.mjs | 27 ++++ .../test-event.mjs | 28 +++++ pnpm-lock.yaml | 5 +- 14 files changed, 599 insertions(+), 7 deletions(-) create mode 100644 components/openphone/actions/create-contact/create-contact.mjs create mode 100644 components/openphone/actions/send-message/send-message.mjs create mode 100644 components/openphone/actions/update-contact/update-contact.mjs create mode 100644 components/openphone/common/utils.mjs create mode 100644 components/openphone/sources/common/base.mjs create mode 100644 components/openphone/sources/new-call-recording-completed-instant/new-call-recording-completed-instant.mjs create mode 100644 components/openphone/sources/new-call-recording-completed-instant/test-event.mjs create mode 100644 components/openphone/sources/new-incoming-call-completed-instant/new-incoming-call-completed-instant.mjs create mode 100644 components/openphone/sources/new-incoming-call-completed-instant/test-event.mjs create mode 100644 components/openphone/sources/new-outgoing-call-completed-instant/new-outgoing-call-completed-instant.mjs create mode 100644 components/openphone/sources/new-outgoing-call-completed-instant/test-event.mjs diff --git a/components/openphone/actions/create-contact/create-contact.mjs b/components/openphone/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..be3f6626c2ad4 --- /dev/null +++ b/components/openphone/actions/create-contact/create-contact.mjs @@ -0,0 +1,80 @@ +import { parseObject } from "../../common/utils.mjs"; +import openphone from "../../openphone.app.mjs"; + +export default { + key: "openphone-create-contact", + name: "Create Contact", + description: "Create a new contact in OpenPhone. [See the documentation](https://www.openphone.com/docs/api-reference/contacts/create-a-contact)", + version: "0.0.1", + type: "action", + props: { + openphone, + firstName: { + propDefinition: [ + openphone, + "firstName", + ], + }, + lastName: { + propDefinition: [ + openphone, + "lastName", + ], + optional: true, + }, + company: { + propDefinition: [ + openphone, + "company", + ], + optional: true, + }, + role: { + propDefinition: [ + openphone, + "role", + ], + optional: true, + }, + emails: { + propDefinition: [ + openphone, + "emails", + ], + optional: true, + }, + phoneNumbers: { + propDefinition: [ + openphone, + "phoneNumbers", + ], + optional: true, + }, + customFields: { + propDefinition: [ + openphone, + "customFields", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.openphone.createContact({ + $, + data: { + defaultFields: { + firstName: this.firstName, + lastName: this.lastName, + company: this.company, + role: this.role, + emails: parseObject(this.emails), + phoneNumbers: parseObject(this.phoneNumbers), + }, + customFields: parseObject(this.customFields), + }, + }); + + $.export("$summary", `Successfully created contact with ID: ${response.data.id}`); + return response; + }, +}; diff --git a/components/openphone/actions/send-message/send-message.mjs b/components/openphone/actions/send-message/send-message.mjs new file mode 100644 index 0000000000000..c44ccd8ea68ae --- /dev/null +++ b/components/openphone/actions/send-message/send-message.mjs @@ -0,0 +1,57 @@ +import { ConfigurationError } from "@pipedream/platform"; +import openphone from "../../openphone.app.mjs"; + +export default { + key: "openphone-send-message", + name: "Send a Text Message via OpenPhone", + description: "Send a text message from your OpenPhone number to a recipient. [See the documentation](https://www.openphone.com/docs/api-reference/messages/send-a-text-message)", + version: "0.0.1", + type: "action", + props: { + openphone, + from: { + propDefinition: [ + openphone, + "from", + ], + }, + to: { + type: "string", + label: "To", + description: "Recipient phone number in E.164 format.", + }, + content: { + type: "string", + label: "Content", + description: "The text content of the message to be sent.", + }, + }, + async run({ $ }) { + try { + const response = await this.openphone.sendTextMessage({ + $, + data: { + content: this.content, + from: this.from, + to: [ + this.to, + ], + setInboxStatus: "done", + }, + }); + $.export("$summary", `Successfully sent message to ${this.to}`); + return response; + + } catch ({ response }) { + let errorMessage = ""; + + if (response.data.errors) { + errorMessage = `Prop: ${response.data.errors[0].path} - ${response.data.errors[0].message}`; + } else { + errorMessage = response.data.message; + } + + throw new ConfigurationError(errorMessage); + } + }, +}; diff --git a/components/openphone/actions/update-contact/update-contact.mjs b/components/openphone/actions/update-contact/update-contact.mjs new file mode 100644 index 0000000000000..d685eb793b0d9 --- /dev/null +++ b/components/openphone/actions/update-contact/update-contact.mjs @@ -0,0 +1,87 @@ +import { parseObject } from "../../common/utils.mjs"; +import openphone from "../../openphone.app.mjs"; + +export default { + key: "openphone-update-contact", + name: "Update Contact", + description: "Update an existing contact on OpenPhone. [See the documentation](https://www.openphone.com/docs/api-reference/contacts/update-a-contact-by-id)", + version: "0.0.1", + type: "action", + props: { + openphone, + contactId: { + type: "string", + label: "Contact ID", + description: "The unique identifier of the contact.", + }, + firstName: { + propDefinition: [ + openphone, + "firstName", + ], + optional: true, + }, + lastName: { + propDefinition: [ + openphone, + "lastName", + ], + optional: true, + }, + company: { + propDefinition: [ + openphone, + "company", + ], + optional: true, + }, + role: { + propDefinition: [ + openphone, + "role", + ], + optional: true, + }, + emails: { + propDefinition: [ + openphone, + "emails", + ], + optional: true, + }, + phoneNumbers: { + propDefinition: [ + openphone, + "phoneNumbers", + ], + optional: true, + }, + customFields: { + propDefinition: [ + openphone, + "customFields", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.openphone.updateContact({ + $, + contactId: this.contactId, + data: { + defaultFields: { + firstName: this.firstName, + lastName: this.lastName, + company: this.company, + role: this.role, + emails: parseObject(this.emails), + phoneNumbers: parseObject(this.phoneNumbers), + }, + customFields: parseObject(this.customFields), + }, + }); + + $.export("$summary", `Successfully updated contact with ID ${this.contactId}`); + return response; + }, +}; diff --git a/components/openphone/common/utils.mjs b/components/openphone/common/utils.mjs new file mode 100644 index 0000000000000..e7c267cec0d5e --- /dev/null +++ b/components/openphone/common/utils.mjs @@ -0,0 +1,26 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + let parsedObj = obj; + if (typeof obj === "string") { + try { + parsedObj = JSON.parse(obj); + } catch (e) { + return obj; + } + } + + if (Array.isArray(parsedObj)) { + return parsedObj.map((item) => parseObject(item)); + } + if (typeof parsedObj === "object") { + for (const [ + key, + value, + ] of Object.entries(parsedObj)) { + parsedObj[key] = parseObject(value); + } + } + + return parsedObj; +}; diff --git a/components/openphone/openphone.app.mjs b/components/openphone/openphone.app.mjs index 954b90d9e14c3..7d126dd084736 100644 --- a/components/openphone/openphone.app.mjs +++ b/components/openphone/openphone.app.mjs @@ -1,11 +1,121 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "openphone", - propDefinitions: {}, + propDefinitions: { + from: { + type: "string", + label: "From", + description: "The sender's phone number. Can be either your OpenPhone phone number ID or the full phone number in E.164 format.", + async options() { + const { data } = await this.listPhoneNumbers(); + return data.map(({ + id: value, name, formattedNumber, + }) => ({ + label: `${name} - ${formattedNumber}`, + value, + })); + }, + }, + firstName: { + type: "string", + label: "First Name", + description: "The contact's first name.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The contact's last name.", + optional: true, + }, + company: { + type: "string", + label: "Company", + description: "The contact's company name.", + optional: true, + }, + role: { + type: "string", + label: "Role", + description: "The contact's role.", + optional: true, + }, + emails: { + type: "string[]", + label: "Emails", + description: "Array of objects of contact's emails. **Example:** `{\"name\": \"Company Email\", \"value\": \"abc@example.com\"}`.", + }, + phoneNumbers: { + type: "string[]", + label: "Phone Numbers", + description: "Array of objects of contact's phone numbers. **Example:** `{\"name\": \"Company Phone\", \"value\": \"+12345678901\"}`.", + }, + customFields: { + type: "string[]", + label: "Custom Fields", + description: "Array of objects of custom fields for the contact. **Example:** `{\"key\": \"inbound-lead\", \"value\": \"[\"option1\", \"option2\"]\"}`.", + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.openphone.com/v1"; + }, + _headers() { + return { + Authorization: `${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, + }); + }, + listPhoneNumbers(opts = {}) { + return this._makeRequest({ + path: "/phone-numbers", + ...opts, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhooks/calls", + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + }, + sendTextMessage(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/messages", + ...opts, + }); + }, + createContact(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/contacts", + ...opts, + }); + }, + updateContact({ + contactId, ...opts + }) { + return this._makeRequest({ + method: "PATCH", + path: `/contacts/${contactId}`, + ...opts, + }); }, }, }; diff --git a/components/openphone/package.json b/components/openphone/package.json index a51350e9e59c9..e7fae1c54b357 100644 --- a/components/openphone/package.json +++ b/components/openphone/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/openphone", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream OpenPhone Components", "main": "openphone.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/openphone/sources/common/base.mjs b/components/openphone/sources/common/base.mjs new file mode 100644 index 0000000000000..75921c5b6df7d --- /dev/null +++ b/components/openphone/sources/common/base.mjs @@ -0,0 +1,62 @@ +import openphone from "../../openphone.app.mjs"; + +export default { + props: { + openphone, + http: "$.interface.http", + db: "$.service.db", + resourceIds: { + propDefinition: [ + openphone, + "from", + ], + type: "string[]", + label: "Resource IDs", + description: "The unique identifiers of phone numbers associated with the webhook.", + optional: true, + }, + label: { + type: "string", + label: "Label", + description: "Webhook's label", + optional: true, + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getEventFilter() { + return true; + }, + }, + hooks: { + async activate() { + const response = await this.openphone.createWebhook({ + data: { + url: this.http.endpoint, + events: this.getEvent(), + resourceIds: this.resourceIds, + label: this.label, + }, + }); + this._setHookId(response.data.id); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.openphone.deleteWebhook(webhookId); + }, + }, + async run({ body }) { + if (this.getEventFilter(body)) { + this.$emit(body, { + id: body.id, + summary: this.getSummary(body), + ts: Date.parse(body.data.object.completedAt), + }); + } + }, +}; diff --git a/components/openphone/sources/new-call-recording-completed-instant/new-call-recording-completed-instant.mjs b/components/openphone/sources/new-call-recording-completed-instant/new-call-recording-completed-instant.mjs new file mode 100644 index 0000000000000..9a0098f0fbe39 --- /dev/null +++ b/components/openphone/sources/new-call-recording-completed-instant/new-call-recording-completed-instant.mjs @@ -0,0 +1,24 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "openphone-new-call-recording-completed-instant", + name: "New Call Recording Completed", + description: "Emit new event when a call recording has finished.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return [ + "call.recording.completed", + ]; + }, + getEmit(body) { + return `New call recording completed for call ID: ${body.data.object.id}`; + }, + }, + sampleEmit, +}; diff --git a/components/openphone/sources/new-call-recording-completed-instant/test-event.mjs b/components/openphone/sources/new-call-recording-completed-instant/test-event.mjs new file mode 100644 index 0000000000000..bd109bda4fb0f --- /dev/null +++ b/components/openphone/sources/new-call-recording-completed-instant/test-event.mjs @@ -0,0 +1,30 @@ +export default { + "apiVersion": "v3", + "createdAt": "2022-01-24T19:30:55.400Z", + "data": { + "object": { + "answeredAt": "2022-01-24T19:30:38.000Z", + "completedAt": "2022-01-24T19:30:48.000Z", + "conversationId": "CN78ba0373683c48fd8fd96bc836c51f79", + "createdAt": "2022-01-24T19:30:34.675Z", + "direction": "incoming", + "from": "+18005550100", + "media": [ + { + "duration":7, + "type": "audio/mpeg", + "url": "https://storage.googleapis.com/opstatics-dev/a5f839bc72a24b33a7fc032f78777146.mp3" + } + ], + "object": "call", + "phoneNumberId": "PNtoDbDhuz", + "status": "completed", + "to": "+18885550101", + "userId": "USu5AsEHuQ", + "voicemail":null + } + }, + "id": "EVda6e196255814311aaac1983005fa2d9", + "object": "event", + "type": "call.recording.completed" +} \ No newline at end of file diff --git a/components/openphone/sources/new-incoming-call-completed-instant/new-incoming-call-completed-instant.mjs b/components/openphone/sources/new-incoming-call-completed-instant/new-incoming-call-completed-instant.mjs new file mode 100644 index 0000000000000..dade14952b2a6 --- /dev/null +++ b/components/openphone/sources/new-incoming-call-completed-instant/new-incoming-call-completed-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "openphone-new-incoming-call-completed-instant", + name: "New Incoming Call Completed (Instant)", + description: "Emit new event when an incoming call is completed, including calls not picked up or voicemails left.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return [ + "call.completed", + ]; + }, + getSummary() { + return "New Incoming Call Completed"; + }, + getEventFilter(body) { + return body.data.object.direction === "incoming"; + }, + }, + sampleEmit, +}; diff --git a/components/openphone/sources/new-incoming-call-completed-instant/test-event.mjs b/components/openphone/sources/new-incoming-call-completed-instant/test-event.mjs new file mode 100644 index 0000000000000..350b4db7cad36 --- /dev/null +++ b/components/openphone/sources/new-incoming-call-completed-instant/test-event.mjs @@ -0,0 +1,28 @@ +export default { + "apiVersion": "v3", + "createdAt": "2022-01-24T19:22:25.427Z", + "data": { + "object": { + "answeredAt": null, + "completedAt": "2022-01-24T19:22:19.000Z", + "conversationId": "CN78ba0373683c48fd8fd96bc836c51f79", + "createdAt": "2022-01-24T19:21:59.545Z", + "direction": "incoming", + "from": "+18005550100", + "media": [], + "object": "call", + "phoneNumberId": "PNtoDbDhuz", + "status": "completed", + "to": "+18885550101", + "userId": "USu5AsEHuQ", + "voicemail": { + "duration": 7, + "type": "audio/mpeg", + "url": "https://m.openph.one/static/15ad4740be6048e4a80efb268d347482.mp3" + } + } + }, + "id": "EVd39d3c8d6f244d21a9131de4fc9350d0", + "object": "event", + "type": "call.completed" +} \ No newline at end of file diff --git a/components/openphone/sources/new-outgoing-call-completed-instant/new-outgoing-call-completed-instant.mjs b/components/openphone/sources/new-outgoing-call-completed-instant/new-outgoing-call-completed-instant.mjs new file mode 100644 index 0000000000000..cdd01cfa2a1cd --- /dev/null +++ b/components/openphone/sources/new-outgoing-call-completed-instant/new-outgoing-call-completed-instant.mjs @@ -0,0 +1,27 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "openphone-new-outgoing-call-completed-instant", + name: "New Outgoing Call Completed (Instant)", + description: "Emit new event when an outgoing call has ended.", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getEvent() { + return [ + "call.completed", + ]; + }, + getSummary() { + return "New Outgoing Call Completed"; + }, + getEventFilter(body) { + return body.data.object.direction === "outgoing"; + }, + }, + sampleEmit, +}; diff --git a/components/openphone/sources/new-outgoing-call-completed-instant/test-event.mjs b/components/openphone/sources/new-outgoing-call-completed-instant/test-event.mjs new file mode 100644 index 0000000000000..3451381272c21 --- /dev/null +++ b/components/openphone/sources/new-outgoing-call-completed-instant/test-event.mjs @@ -0,0 +1,28 @@ +export default { + "apiVersion": "v3", + "createdAt": "2022-01-24T19:22:25.427Z", + "data": { + "object": { + "answeredAt": null, + "completedAt": "2022-01-24T19:22:19.000Z", + "conversationId": "CN78ba0373683c48fd8fd96bc836c51f79", + "createdAt": "2022-01-24T19:21:59.545Z", + "direction": "outgoing", + "from": "+18005550100", + "media": [], + "object": "call", + "phoneNumberId": "PNtoDbDhuz", + "status": "completed", + "to": "+18885550101", + "userId": "USu5AsEHuQ", + "voicemail": { + "duration": 7, + "type": "audio/mpeg", + "url": "https://m.openph.one/static/15ad4740be6048e4a80efb268d347482.mp3" + } + } + }, + "id": "EVd39d3c8d6f244d21a9131de4fc9350d0", + "object": "event", + "type": "call.completed" +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f71144c429b6a..59189c8dba229 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7052,7 +7052,10 @@ importers: '@pipedream/platform': 3.0.1 components/openphone: - specifiers: {} + specifiers: + '@pipedream/platform': ^3.0.3 + dependencies: + '@pipedream/platform': 3.0.3 components/opensea: specifiers: