diff --git a/components/heyy/actions/create-contact/create-contact.mjs b/components/heyy/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..a377a2636dff7 --- /dev/null +++ b/components/heyy/actions/create-contact/create-contact.mjs @@ -0,0 +1,102 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../heyy.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "heyy-create-contact", + name: "Create Contact", + description: "Creates a new contact for the business. [See the documentation](https://documenter.getpostman.com/view/27408936/2sA2r3a6DW#a1249b8d-10cf-446a-be35-eb8793ffa967).", + version: "0.0.1", + type: "action", + props: { + app, + phoneNumber: { + propDefinition: [ + app, + "phoneNumber", + ], + }, + firstName: { + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + labels: { + propDefinition: [ + app, + "labels", + ], + }, + attributes: { + propDefinition: [ + app, + "attributes", + ], + }, + }, + methods: { + createContact(args = {}) { + return this.app.post({ + path: "/contacts", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createContact, + phoneNumber, + firstName, + lastName, + email, + labels, + attributes, + } = this; + + if (!utils.isPhoneNumberValid(phoneNumber)) { + throw new ConfigurationError(`The phone number \`${phoneNumber}\` is invalid. Please provide a valid phone number.`); + } + + const response = await createContact({ + $, + data: { + phoneNumber, + firstName, + lastName, + email, + ...(labels?.length && { + labels: labels.map((name) => ({ + name, + })), + }), + attributes: + attributes && Object.entries(attributes) + .reduce((acc, [ + externalId, + value, + ]) => ([ + ...acc, + { + externalId, + value, + }, + ]), []), + }, + }); + $.export("$summary", `Successfully created contact with ID \`${response.data.id}\`.`); + return response; + }, +}; diff --git a/components/heyy/actions/send-whatsapp-message/send-whatsapp-message.mjs b/components/heyy/actions/send-whatsapp-message/send-whatsapp-message.mjs new file mode 100644 index 0000000000000..7ea702a060f37 --- /dev/null +++ b/components/heyy/actions/send-whatsapp-message/send-whatsapp-message.mjs @@ -0,0 +1,161 @@ +import app from "../../heyy.app.mjs"; +import constants from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "heyy-send-whatsapp-message", + name: "Send WhatsApp Message", + description: "Sends a WhatsApp message to a contact. [See the documentation](https://documenter.getpostman.com/view/27408936/2sa2r3a6dw)", + version: "0.0.1", + type: "action", + props: { + app, + channelId: { + propDefinition: [ + app, + "channelId", + ], + }, + phoneNumber: { + label: "Phone Number", + description: "The phone number of the contact.", + propDefinition: [ + app, + "contactId", + () => ({ + mapper: ({ + firstName, phoneNumber: value, + }) => ({ + label: firstName || value, + value, + }), + }), + ], + }, + msgType: { + type: "string", + label: "Message Type", + description: "The type of message to send.", + options: Object.values(constants.MSG_TYPE), + reloadProps: true, + }, + }, + additionalProps() { + const { msgType } = this; + + const bodyText = { + type: "string", + label: "Body Text", + description: "The text of the message to send.", + }; + + if (msgType === constants.MSG_TYPE.TEXT) { + return { + bodyText, + }; + } + + if (msgType === constants.MSG_TYPE.IMAGE) { + return { + bodyText, + fileId: { + type: "string", + label: "File ID", + description: "The ID of the file to attach to the message.", + }, + }; + } + + if (msgType === constants.MSG_TYPE.TEMPLATE) { + return { + messageTemplateId: { + type: "string", + label: "Message Template ID", + description: "The ID of the message template to use.", + optional: true, + options: async ({ page }) => { + const { data: { messageTemplates } } = await this.app.getMessageTemplates({ + params: { + page, + sortBy: "updatedAt", + order: "DESC", + }, + }); + return messageTemplates.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + }; + } + + if (msgType === constants.MSG_TYPE.INTERACTIVE) { + return { + bodyText, + buttons: { + type: "string[]", + label: "Buttons", + description: "The buttons to include in the message. Each row should have a JSON formated string. Eg. `{ \"id\": \"STRING\", \"title\": \"STRING\" }`.", + }, + headerText: { + type: "string", + label: "Header Text", + description: "The header text of the message to send.", + optional: true, + }, + footerText: { + type: "string", + label: "Footer Text", + description: "The footer text of the message to send.", + optional: true, + }, + }; + } + + return {}; + }, + methods: { + sendWhatsappMessage({ + channelId, ...args + } = {}) { + return this.app.post({ + path: `/${channelId}/whatsapp_messages/send`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + sendWhatsappMessage, + channelId, + phoneNumber, + msgType, + bodyText, + fileId, + messageTemplateId, + headerText, + footerText, + buttons, + } = this; + + const response = await sendWhatsappMessage({ + $, + channelId, + data: { + phoneNumber, + type: msgType, + bodyText, + fileId, + messageTemplateId, + headerText, + footerText, + buttons: utils.parseArray(buttons), + }, + }); + $.export("$summary", "Succesfully sent WhatsApp message."); + return response; + }, +}; diff --git a/components/heyy/actions/update-contact/update-contact.mjs b/components/heyy/actions/update-contact/update-contact.mjs new file mode 100644 index 0000000000000..156c5669b2099 --- /dev/null +++ b/components/heyy/actions/update-contact/update-contact.mjs @@ -0,0 +1,115 @@ +import { ConfigurationError } from "@pipedream/platform"; +import app from "../../heyy.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "heyy-update-contact", + name: "Update Contact", + description: "Updates the details of a contact under your business. [See the documentation](https://documenter.getpostman.com/view/27408936/2sA2r3a6DW#5a5ee22b-c16e-4d46-ae5d-3844b6501a34).", + version: "0.0.1", + type: "action", + props: { + app, + contactId: { + propDefinition: [ + app, + "contactId", + ], + }, + phoneNumber: { + optional: true, + propDefinition: [ + app, + "phoneNumber", + ], + }, + firstName: { + optional: true, + propDefinition: [ + app, + "firstName", + ], + }, + lastName: { + propDefinition: [ + app, + "lastName", + ], + }, + email: { + propDefinition: [ + app, + "email", + ], + }, + labels: { + propDefinition: [ + app, + "labels", + ], + }, + attributes: { + propDefinition: [ + app, + "attributes", + ], + }, + }, + methods: { + updateContact({ + contactId, ...args + } = {}) { + return this.app.put({ + path: `/contacts/${contactId}`, + ...args, + }); + }, + }, + async run({ $ }) { + const { + updateContact, + contactId, + phoneNumber, + firstName, + lastName, + email, + labels, + attributes, + } = this; + + if (phoneNumber && !utils.isPhoneNumberValid(phoneNumber)) { + throw new ConfigurationError(`The phone number \`${phoneNumber}\` is invalid. Please provide a valid phone number.`); + } + + const response = await updateContact({ + $, + contactId, + data: { + phoneNumber, + firstName, + lastName, + email, + ...(labels?.length && { + labels: labels.map((name) => ({ + name, + })), + }), + attributes: + attributes && Object.entries(attributes) + .reduce((acc, [ + externalId, + value, + ]) => ([ + ...acc, + { + externalId, + value, + }, + ]), []), + }, + }); + + $.export("$summary", `Successfully updated contact with ID \`${response.data.id}\`.`); + return response; + }, +}; diff --git a/components/heyy/actions/upload-file/upload-file.mjs b/components/heyy/actions/upload-file/upload-file.mjs new file mode 100644 index 0000000000000..6869a3a417ee5 --- /dev/null +++ b/components/heyy/actions/upload-file/upload-file.mjs @@ -0,0 +1,62 @@ +import { createReadStream } from "fs"; +import FormData from "form-data"; +import app from "../../heyy.app.mjs"; + +export default { + key: "heyy-upload-file", + name: "Upload File", + description: "Uploads a file. [See the documentation](https://documenter.getpostman.com/view/27408936/2sA2r3a6DW#67e41b81-318c-4ed0-be78-e92fd39f3530).", + version: "0.0.1", + type: "action", + props: { + app, + filePath: { + type: "string", + label: "File Path", + description: "The file to be uploaded, please provide a file from `/tmp`. To upload a file to `/tmp` folder, please follow the doc [here](https://pipedream.com/docs/code/nodejs/working-with-files/#writing-a-file-to-tmp)", + }, + format: { + type: "string", + label: "Format", + description: "The format of the file to be uploaded.", + options: [ + "IMAGE", + "VIDEO", + "DOCUMENT", + ], + }, + }, + methods: { + uploadFile(args = {}) { + return this.app.post({ + path: "/upload_file", + ...args, + }); + }, + }, + async run({ $ }) { + const { + uploadFile, + filePath, + format, + } = this; + + const file = filePath.startsWith("/tmp") + ? filePath + : `/tmp/${filePath}`; + + const data = new FormData(); + data.append("file", createReadStream(file)); + data.append("format", format); + + const response = await uploadFile({ + $, + headers: { + "Content-Type": "multipart/form-data", + }, + data, + }); + $.export("$summary", `Succesfully uploaded file with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/heyy/common/constants.mjs b/components/heyy/common/constants.mjs new file mode 100644 index 0000000000000..a137b766d45b6 --- /dev/null +++ b/components/heyy/common/constants.mjs @@ -0,0 +1,21 @@ +const BASE_URL = "https://api.hey-y.io"; +const VERSION_PATH = "/api/v2.0"; +const LAST_CREATED_AT = "lastCreatedAt"; +const DEFAULT_MAX = 600; +const WEBHOOK_ID = "webhookId"; + +const MSG_TYPE = { + TEXT: "TEXT", + IMAGE: "IMAGE", + TEMPLATE: "TEMPLATE", + INTERACTIVE: "INTERACTIVE", +}; + +export default { + BASE_URL, + VERSION_PATH, + DEFAULT_MAX, + LAST_CREATED_AT, + MSG_TYPE, + WEBHOOK_ID, +}; diff --git a/components/heyy/common/utils.mjs b/components/heyy/common/utils.mjs new file mode 100644 index 0000000000000..9c78ff9021a00 --- /dev/null +++ b/components/heyy/common/utils.mjs @@ -0,0 +1,60 @@ +import { ConfigurationError } from "@pipedream/platform"; + +function isJson(value) { + try { + JSON.parse(value); + } catch (e) { + return false; + } + + return true; +} + +function parse(value) { + if (!Object.keys(value).length) { + throw new ConfigurationError("Please provide at least one object property."); + } + + if (typeof(value) === "object") { + return value; + } + + if (isJson(value)) { + return JSON.parse(value); + } + + throw new ConfigurationError("Make sure the custom expression contains a valid object"); +} + +function parseArray(value) { + try { + if (!value) { + return; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +function isPhoneNumberValid(phoneNumber) { + const pattern = new RegExp("^\\+[1-9]{1}[0-9]{0,2}[2-9]{1}[0-9]{2}[2-9]{1}[0-9]{2}[0-9]{4}$"); + return pattern.test(phoneNumber); +} + +export default { + parseArray: (value) => parseArray(value)?.map(parse), + isPhoneNumberValid, +}; diff --git a/components/heyy/heyy.app.mjs b/components/heyy/heyy.app.mjs index 20d9d05957f47..277b8886d2427 100644 --- a/components/heyy/heyy.app.mjs +++ b/components/heyy/heyy.app.mjs @@ -1,11 +1,154 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "heyy", - propDefinitions: {}, + propDefinitions: { + phoneNumber: { + type: "string", + label: "Phone Number", + description: "The phone number of the contact. It must be in E.164 format. Eg: `+14155552671`. For more information please see [here](https://en.wikipedia.org/wiki/E.164).", + }, + firstName: { + type: "string", + label: "First Name", + description: "The first name of the contact.", + }, + lastName: { + type: "string", + label: "Last Name", + description: "The last name of the contact.", + optional: true, + }, + email: { + type: "string", + label: "Email", + description: "The email address of the contact.", + optional: true, + }, + labels: { + type: "string[]", + label: "Labels", + description: "The labels associated with the contact.", + optional: true, + async options() { + const { data } = await this.getLabels(); + return data.map(({ name }) => name); + }, + }, + attributes: { + type: "object", + label: "Attributes", + description: "The attributes associated with the contact.", + optional: true, + }, + contactId: { + type: "string", + label: "Contact ID", + description: "The unique identifier of the contact.", + async options({ + page, + mapper = ({ + id: value, firstName, phoneNumber, + }) => ({ + label: firstName || phoneNumber, + value, + }), + }) { + const { data: { contacts } } = await this.getContacts({ + params: { + page, + sortBy: "updatedAt", + order: "DESC", + }, + }); + return contacts.map(mapper); + }, + }, + channelId: { + type: "string", + label: "Channel ID", + description: "The unique identifier of the channel.", + async options({ + mapper = ({ + id: value, whatsappPhoneNumber: { name: label }, + }) => ({ + label, + value, + }), + }) { + const { data } = await this.getChannels(); + return data.map(mapper); + }, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `${constants.BASE_URL}${constants.VERSION_PATH}${path}`; + }, + getHeaders(headers) { + return { + ...headers, + Authorization: `Bearer ${this.$auth.api_token}`, + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + put(args = {}) { + return this._makeRequest({ + method: "PUT", + ...args, + }); + }, + delete(args = {}) { + return this._makeRequest({ + method: "DELETE", + ...args, + }); + }, + getLabels(args = {}) { + return this._makeRequest({ + path: "/labels", + ...args, + }); + }, + getAttributes(args = {}) { + return this._makeRequest({ + path: "/attributes", + ...args, + }); + }, + getContacts(args = {}) { + return this._makeRequest({ + path: "/contacts", + ...args, + }); + }, + getMessageTemplates(args = {}) { + return this._makeRequest({ + path: "/message_templates", + ...args, + }); + }, + getChannels(args = {}) { + return this._makeRequest({ + path: "/channels", + ...args, + }); }, }, -}; \ No newline at end of file +}; diff --git a/components/heyy/package.json b/components/heyy/package.json index fafb4dc7caa92..daadbe845922f 100644 --- a/components/heyy/package.json +++ b/components/heyy/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/heyy", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Heyy Components", "main": "heyy.app.mjs", "keywords": [ @@ -11,5 +11,9 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "3.0.3", + "form-data": "^4.0.1" } -} \ No newline at end of file +} diff --git a/components/heyy/sources/new-incoming-message-instant/new-incoming-message-instant.mjs b/components/heyy/sources/new-incoming-message-instant/new-incoming-message-instant.mjs new file mode 100644 index 0000000000000..16152ae56cb07 --- /dev/null +++ b/components/heyy/sources/new-incoming-message-instant/new-incoming-message-instant.mjs @@ -0,0 +1,87 @@ +import app from "../../heyy.app.mjs"; +import constants from "../../common/constants.mjs"; + +export default { + key: "heyy-new-incoming-message-instant", + name: "New Incoming Message", + description: "Emit new event when a business gets a new incoming message. [See the documentation](https://documenter.getpostman.com/view/27408936/2sA2r3a6DW#eda04a28-4c5b-4709-a3f4-204dba6bcc18).", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + app, + db: "$.service.db", + http: "$.interface.http", + channelId: { + propDefinition: [ + app, + "channelId", + ], + }, + }, + hooks: { + async activate() { + const { + createWebhook, + http: { endpoint: url }, + channelId, + setWebhookId, + } = this; + const response = + await createWebhook({ + data: { + url, + type: "WHATSAPP_MESSAGE_RECEIVED", + channelId, + }, + }); + + setWebhookId(response.data.id); + }, + async deactivate() { + const webhookId = this.getWebhookId(); + if (webhookId) { + await this.deleteWebhook({ + webhookId, + }); + } + }, + }, + methods: { + setWebhookId(value) { + this.db.set(constants.WEBHOOK_ID, value); + }, + getWebhookId() { + return this.db.get(constants.WEBHOOK_ID); + }, + generateMeta({ data: { whatsappMessage: resource } }) { + return { + id: resource.metaMessageId, + summary: `New Incomming Message ${resource.metaMessageId}`, + ts: parseInt(resource.timestamp), + }; + }, + processResource(resource) { + this.$emit(resource, this.generateMeta(resource)); + }, + createWebhook(args = {}) { + return this.app.post({ + debug: true, + path: "/api_webhooks", + ...args, + }); + }, + deleteWebhook({ + webhookId, ...args + } = {}) { + return this.app.delete({ + debug: true, + path: `/api_webhooks/${webhookId}`, + ...args, + }); + }, + }, + async run({ body }) { + this.processResource(body); + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a234fe7037a27..3da1c18fc39f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4584,7 +4584,12 @@ importers: '@pipedream/platform': 1.5.1 components/heyy: - specifiers: {} + specifiers: + '@pipedream/platform': 3.0.3 + form-data: ^4.0.1 + dependencies: + '@pipedream/platform': 3.0.3 + form-data: 4.0.1 components/heyzine: specifiers: @@ -22610,7 +22615,7 @@ packages: resolution: {integrity: sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==} dependencies: follow-redirects: 1.15.9 - form-data: 4.0.0 + form-data: 4.0.1 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -22620,7 +22625,7 @@ packages: resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} dependencies: follow-redirects: 1.15.9 - form-data: 4.0.0 + form-data: 4.0.1 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -25932,6 +25937,15 @@ packages: combined-stream: 1.0.8 mime-types: 2.1.35 + /form-data/4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /format-io/2.0.0: resolution: {integrity: sha512-iQz8w2qr4f+doWBV6LsfScHbu1gXhccByjbmA1wjBTaKRhweH2baJL96UGR4C7Fjpr8zSkK7EXiLmbzZWTyQIA==} engines: {node: '>=8'}