From bd10277e2ee4cc0926ce56ab620a51b18bdbc82f Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Fri, 8 Nov 2024 11:40:13 -0300 Subject: [PATCH 1/4] help_scout init --- .../help_scout/actions/add-note/add-note.mjs | 32 +++ .../create-customer/create-customer.mjs | 61 ++++++ .../actions/send-reply/send-reply.mjs | 34 +++ components/help_scout/help_scout.app.mjs | 194 +++++++++++++++++- .../new-conversation-assigned-instant.mjs | 97 +++++++++ .../new-conversation-created-instant.mjs | 99 +++++++++ .../new-customer-instant.mjs | 94 +++++++++ 7 files changed, 607 insertions(+), 4 deletions(-) create mode 100644 components/help_scout/actions/add-note/add-note.mjs create mode 100644 components/help_scout/actions/create-customer/create-customer.mjs create mode 100644 components/help_scout/actions/send-reply/send-reply.mjs create mode 100644 components/help_scout/sources/new-conversation-assigned-instant/new-conversation-assigned-instant.mjs create mode 100644 components/help_scout/sources/new-conversation-created-instant/new-conversation-created-instant.mjs create mode 100644 components/help_scout/sources/new-customer-instant/new-customer-instant.mjs diff --git a/components/help_scout/actions/add-note/add-note.mjs b/components/help_scout/actions/add-note/add-note.mjs new file mode 100644 index 0000000000000..fc737c5a8fa3b --- /dev/null +++ b/components/help_scout/actions/add-note/add-note.mjs @@ -0,0 +1,32 @@ +import helpScout from "../../help_scout.app.mjs"; + +export default { + key: "help_scout-add-note", + name: "Add Note to Conversation", + description: "Adds a note to an existing conversation in Help Scout. [See the documentation](https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/note/)", + version: "0.0.1", + type: "action", + props: { + helpScout, + conversationId: { + propDefinition: [ + helpScout, + "conversationId", + ], + }, + text: { + propDefinition: [ + helpScout, + "text", + ], + }, + }, + async run({ $ }) { + const response = await this.helpScout.addNoteToConversation({ + conversationId: this.conversationId, + text: this.text, + }); + $.export("$summary", `Successfully added note to conversation ID: ${this.conversationId}`); + return response; + }, +}; diff --git a/components/help_scout/actions/create-customer/create-customer.mjs b/components/help_scout/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..0561c971f61b4 --- /dev/null +++ b/components/help_scout/actions/create-customer/create-customer.mjs @@ -0,0 +1,61 @@ +import helpScout from "../../help_scout.app.mjs"; + +export default { + key: "help_scout-create-customer", + name: "Create Customer", + description: "Creates a new customer record in Help Scout. [See the documentation](https://developer.helpscout.com/mailbox-api/endpoints/customers/create/)", + version: "0.0.{{ts}}", + type: "action", + props: { + helpScout, + customerEmail: { + propDefinition: [ + helpScout, + "customerEmail", + ], + }, + customerPhone: { + propDefinition: [ + helpScout, + "customerPhone", + ], + }, + chatHandles: { + propDefinition: [ + helpScout, + "chatHandles", + ], + }, + socialProfiles: { + propDefinition: [ + helpScout, + "socialProfiles", + ], + }, + customerAddress: { + propDefinition: [ + helpScout, + "customerAddress", + ], + }, + customerDetails: { + propDefinition: [ + helpScout, + "customerDetails", + ], + }, + }, + async run({ $ }) { + const response = await this.helpScout.createCustomer({ + customerEmail: this.customerEmail, + customerPhone: this.customerPhone, + chatHandles: this.chatHandles, + socialProfiles: this.socialProfiles, + customerAddress: this.customerAddress, + ...this.customerDetails, + }); + + $.export("$summary", `Successfully created customer with email: ${this.customerEmail}`); + return response; + }, +}; diff --git a/components/help_scout/actions/send-reply/send-reply.mjs b/components/help_scout/actions/send-reply/send-reply.mjs new file mode 100644 index 0000000000000..c62a1d049325c --- /dev/null +++ b/components/help_scout/actions/send-reply/send-reply.mjs @@ -0,0 +1,34 @@ +import helpScout from "../../help_scout.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "help_scout-send-reply", + name: "Send Reply", + description: "Sends a reply to a conversation. Be careful as this sends an actual email to the customer. [See the documentation](https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/reply/)", + version: "0.0.1", + type: "action", + props: { + helpScout, + conversationId: { + propDefinition: [ + helpScout, + "conversationId", + ], + }, + text: { + propDefinition: [ + helpScout, + "text", + ], + }, + }, + async run({ $ }) { + const response = await this.helpScout.sendReplyToConversation({ + conversationId: this.conversationId, + text: this.text, + }); + + $.export("$summary", `Reply sent successfully to conversation ID: ${this.conversationId}`); + return response; + }, +}; diff --git a/components/help_scout/help_scout.app.mjs b/components/help_scout/help_scout.app.mjs index 7f6e469dcb48f..6937e41e37126 100644 --- a/components/help_scout/help_scout.app.mjs +++ b/components/help_scout/help_scout.app.mjs @@ -1,11 +1,197 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "help_scout", - propDefinitions: {}, + propDefinitions: { + agentId: { + type: "string", + label: "Agent ID", + description: "ID of the agent to whom the conversation is assigned.", + }, + conversationId: { + type: "string", + label: "Conversation ID", + description: "The unique identifier of the conversation.", + }, + conversationTitle: { + type: "string", + label: "Conversation Title", + description: "Title of the conversation.", + }, + text: { + type: "string", + label: "Text", + description: "The content of the note or reply.", + }, + customerEmail: { + type: "string", + label: "Customer Email", + description: "Email of the customer.", + }, + customerDetails: { + type: "object", + label: "Customer Details", + description: "Optional customer's details such as name and contact.", + optional: true, + }, + customerPhone: { + type: "string", + label: "Customer Phone", + description: "Optional phone number of the customer.", + optional: true, + }, + chatHandles: { + type: "string[]", + label: "Chat Handles", + description: "Optional chat handles for the customer.", + optional: true, + }, + socialProfiles: { + type: "string[]", + label: "Social Profiles", + description: "Optional social profiles for the customer.", + optional: true, + }, + customerAddress: { + type: "object", + label: "Customer Address", + description: "Optional address of the customer.", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://api.helpscout.net/v2"; + }, + 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.oauth_token}`, + }, + }); + }, + async createWebhook({ + url, events, secret, + }) { + return this._makeRequest({ + method: "POST", + path: "/webhooks", + data: { + url, + events, + secret, + payloadVersion: "V2", + }, + }); + }, + async addNoteToConversation({ + conversationId, text, + }) { + return this._makeRequest({ + method: "POST", + path: `/conversations/${conversationId}/notes`, + data: { + text, + }, + }); + }, + async createCustomer({ + customerEmail, + customerPhone, + chatHandles, + socialProfiles, + customerAddress, + ...customerDetails + }) { + return this._makeRequest({ + method: "POST", + path: "/customers", + data: { + emails: [ + { + value: customerEmail, + }, + ], + phones: customerPhone + ? [ + { + value: customerPhone, + }, + ] + : undefined, + chats: chatHandles + ? chatHandles.map((handle) => ({ + value: handle, + })) + : undefined, + socialProfiles: socialProfiles + ? socialProfiles.map((profile) => ({ + value: profile, + })) + : undefined, + address: customerAddress, + ...customerDetails, + }, + }); + }, + async sendReplyToConversation({ + conversationId, text, + }) { + return this._makeRequest({ + method: "POST", + path: `/conversations/${conversationId}/reply`, + data: { + text, + }, + }); + }, + async emitNewEventOnConversationAssigned({ agentId }) { + const events = [ + "convo.assigned", + ]; + const url = "https://example.com/helpscout"; // Replace with your URL + const secret = "your_secret_key"; // Generate and replace with your secret key + await this.createWebhook({ + url, + events, + secret, + }); + }, + async emitNewEventOnCustomerAdded() { + const events = [ + "customer.created", + ]; + const url = "https://example.com/helpscout"; // Replace with your URL + const secret = "your_secret_key"; // Generate and replace with your secret key + await this.createWebhook({ + url, + events, + secret, + }); + }, + async emitNewEventOnConversationCreated({ conversationTitle }) { + const events = [ + "convo.created", + ]; + const url = "https://example.com/helpscout"; // Replace with your URL + const secret = "your_secret_key"; // Generate and replace with your secret key + await this.createWebhook({ + url, + events, + secret, + }); }, }, }; diff --git a/components/help_scout/sources/new-conversation-assigned-instant/new-conversation-assigned-instant.mjs b/components/help_scout/sources/new-conversation-assigned-instant/new-conversation-assigned-instant.mjs new file mode 100644 index 0000000000000..6fdc4fd762c2b --- /dev/null +++ b/components/help_scout/sources/new-conversation-assigned-instant/new-conversation-assigned-instant.mjs @@ -0,0 +1,97 @@ +import helpScout from "../../help_scout.app.mjs"; +import crypto from "crypto"; +import { axios } from "@pipedream/platform"; + +export default { + key: "help_scout-new-conversation-assigned-instant", + name: "New Conversation Assigned", + description: "Emit new event when a conversation is assigned to an agent. [See the documentation](https://developer.helpscout.com/)", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + helpScout, + agentId: { + propDefinition: [ + helpScout, + "agentId", + ], + }, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + }, + methods: { + _getWebhookId() { + return this.db.get("webhookId"); + }, + _setWebhookId(id) { + this.db.set("webhookId", id); + }, + _getSecret() { + return this.db.get("secret"); + }, + _setSecret(secret) { + this.db.set("secret", secret); + }, + _verifySignature(headerSignature, body) { + const secret = this._getSecret(); + const computedSignature = crypto + .createHmac("sha256", secret) + .update(body) + .digest("base64"); + return headerSignature === computedSignature; + }, + }, + hooks: { + async activate() { + const events = [ + "convo.assigned", + ]; + const url = this.http.endpoint; + const secret = crypto.randomBytes(20).toString("hex"); + const { id } = await this.helpScout.createWebhook({ + url, + events, + secret, + }); + this._setWebhookId(id); + this._setSecret(secret); + }, + async deactivate() { + const webhookId = this._getWebhookId(); + if (webhookId) { + await this.helpScout._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + } + }, + }, + async run(event) { + const headerSignature = event.headers["x-helpscout-signature"]; + const body = event.bodyRaw; + if (!this._verifySignature(headerSignature, body)) { + this.http.respond({ + status: 401, + body: "Unauthorized", + }); + return; + } + + const data = JSON.parse(body); + if (data.event === "convo.assigned" && data.agent.id === this.agentId) { + this.$emit(data, { + id: data.id, + summary: `New conversation assigned to ${this.agentId}`, + ts: Date.parse(data.changedAt), + }); + } + + this.http.respond({ + status: 200, + }); + }, +}; diff --git a/components/help_scout/sources/new-conversation-created-instant/new-conversation-created-instant.mjs b/components/help_scout/sources/new-conversation-created-instant/new-conversation-created-instant.mjs new file mode 100644 index 0000000000000..44176f7cc113e --- /dev/null +++ b/components/help_scout/sources/new-conversation-created-instant/new-conversation-created-instant.mjs @@ -0,0 +1,99 @@ +import helpScout from "../../help_scout.app.mjs"; +import { axios } from "@pipedream/platform"; +import crypto from "crypto"; + +export default { + key: "help_scout-new-conversation-created-instant", + name: "New Conversation Created", + description: "Emit new event when a new conversation is created. [See the documentation](https://developer.helpscout.com/webhooks/)", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + helpScout, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + conversationTitle: { + propDefinition: [ + helpScout, + "conversationTitle", + ], + }, + conversationDetails: { + type: "object", + label: "Conversation Details", + description: "Optional conversation details such as tags, type, etc.", + optional: true, + }, + }, + methods: { + _getWebhookId() { + return this.db.get("webhookId"); + }, + _setWebhookId(id) { + this.db.set("webhookId", id); + }, + }, + hooks: { + async deploy() { + // Emit historical events (not implemented) + }, + async activate() { + const url = this.http.endpoint; + const secret = crypto.randomBytes(16).toString("hex"); + const events = [ + "convo.created", + ]; + const webhook = await this.helpScout.createWebhook({ + url, + events, + secret, + }); + this._setWebhookId(webhook.id); + this.db.set("secret", secret); + }, + async deactivate() { + const webhookId = this._getWebhookId(); + if (webhookId) { + await this.helpScout._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, + }); + this.db.set("webhookId", null); + } + }, + }, + async run(event) { + const rawBody = event.rawBody; + const webhookSignature = event.headers["x-helpscout-signature"]; + const secretKey = this.db.get("secret"); + + const computedSignature = crypto.createHmac("sha256", secretKey).update(rawBody) + .digest("base64"); + if (computedSignature !== webhookSignature) { + this.http.respond({ + status: 401, + body: "Unauthorized", + }); + return; + } + + this.http.respond({ + status: 200, + body: "OK", + }); + + const { + title, ...rest + } = event.body; + + this.$emit(event.body, { + id: title, + summary: `New conversation: ${title}`, + ts: new Date().getTime(), + }); + }, +}; diff --git a/components/help_scout/sources/new-customer-instant/new-customer-instant.mjs b/components/help_scout/sources/new-customer-instant/new-customer-instant.mjs new file mode 100644 index 0000000000000..0ff492125a277 --- /dev/null +++ b/components/help_scout/sources/new-customer-instant/new-customer-instant.mjs @@ -0,0 +1,94 @@ +import helpScout from "../../help_scout.app.mjs"; +import crypto from "crypto"; +import { axios } from "@pipedream/platform"; + +export default { + key: "help_scout-new-customer-instant", + name: "New Customer Added", + description: "Emit new event when a new customer is added. [See the documentation](https://developer.helpscout.com/)", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + helpScout, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + customerDetails: { + propDefinition: [ + helpScout, + "customerDetails", + ], + }, + }, + methods: { + _getWebhookId() { + return this.db.get("webhookId"); + }, + _setWebhookId(id) { + this.db.set("webhookId", id); + }, + _generateSecret() { + return crypto.randomBytes(20).toString("hex"); + }, + }, + hooks: { + async activate() { + const url = this.http.endpoint; + const secret = this._generateSecret(); + const events = [ + "customer.created", + ]; + const response = await this.helpScout.createWebhook({ + url, + events, + secret, + }); + this._setWebhookId(response.id); + this.db.set("secret", secret); + }, + async deactivate() { + const webhookId = this._getWebhookId(); + if (webhookId) { + await axios(this, { + method: "DELETE", + url: `${this.helpScout._baseUrl()}/webhooks/${webhookId}`, + headers: { + Authorization: `Bearer ${this.helpScout.$auth.oauth_token}`, + }, + }); + } + }, + }, + async run(event) { + const signature = this.http.headers["x-helpscout-signature"]; + const rawBody = JSON.stringify(event.body); + const secret = this.db.get("secret"); + + const computedSignature = crypto.createHmac("sha256", secret).update(rawBody) + .digest("base64"); + + if (computedSignature !== signature) { + this.http.respond({ + status: 401, + body: "Unauthorized", + }); + return; + } + + this.http.respond({ + status: 200, + body: "OK", + }); + + const customer = event.body.data.item; + + this.$emit(customer, { + id: customer.id, + summary: `New customer created: ${customer.firstName} ${customer.lastName}`, + ts: Date.parse(event.body.data.createdAt), + }); + }, +}; From 11993f14f3e1cee23e27ffed3c5620a952693374 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Fri, 8 Nov 2024 17:00:02 -0300 Subject: [PATCH 2/4] [Components] help_scout #14608 Sources - New Conversation Assigned (Instant) - New Conversation Created (Instant) - New Customer (Instant) Actions - Add Note - Create Customer - Send Reply --- .../help_scout/actions/add-note/add-note.mjs | 13 +- .../create-customer/create-customer.mjs | 230 +++++++++++++---- .../actions/send-reply/send-reply.mjs | 23 +- components/help_scout/common/constants.mjs | 16 ++ components/help_scout/common/utils.mjs | 33 +++ components/help_scout/help_scout.app.mjs | 239 +++++++----------- components/help_scout/package.json | 20 ++ components/help_scout/sources/common/base.mjs | 84 ++++++ .../new-conversation-assigned-instant.mjs | 95 +------ .../test-event.mjs | 137 ++++++++++ .../new-conversation-created-instant.mjs | 99 +------- .../test-event.mjs | 137 ++++++++++ .../new-customer-instant.mjs | 94 +------ .../new-customer-instant/test-event.mjs | 58 +++++ components/help_scout/yarn.lock | 8 + 15 files changed, 843 insertions(+), 443 deletions(-) create mode 100644 components/help_scout/common/constants.mjs create mode 100644 components/help_scout/common/utils.mjs create mode 100644 components/help_scout/package.json create mode 100644 components/help_scout/sources/common/base.mjs create mode 100644 components/help_scout/sources/new-conversation-assigned-instant/test-event.mjs create mode 100644 components/help_scout/sources/new-conversation-created-instant/test-event.mjs create mode 100644 components/help_scout/sources/new-customer-instant/test-event.mjs create mode 100644 components/help_scout/yarn.lock diff --git a/components/help_scout/actions/add-note/add-note.mjs b/components/help_scout/actions/add-note/add-note.mjs index fc737c5a8fa3b..ebb02f2770e0b 100644 --- a/components/help_scout/actions/add-note/add-note.mjs +++ b/components/help_scout/actions/add-note/add-note.mjs @@ -20,11 +20,22 @@ export default { "text", ], }, + userId: { + propDefinition: [ + helpScout, + "userId", + ], + optional: true, + }, }, async run({ $ }) { const response = await this.helpScout.addNoteToConversation({ + $, conversationId: this.conversationId, - text: this.text, + data: { + text: this.text, + user: this.userId, + }, }); $.export("$summary", `Successfully added note to conversation ID: ${this.conversationId}`); return response; diff --git a/components/help_scout/actions/create-customer/create-customer.mjs b/components/help_scout/actions/create-customer/create-customer.mjs index 0561c971f61b4..9f5bf6ddd5446 100644 --- a/components/help_scout/actions/create-customer/create-customer.mjs +++ b/components/help_scout/actions/create-customer/create-customer.mjs @@ -1,61 +1,203 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { + GENDER_OPTIONS, + PHOTO_TYPE_OPTIONS, +} from "../../common/constants.mjs"; +import { + cleanObject, + parseObject, +} from "../../common/utils.mjs"; import helpScout from "../../help_scout.app.mjs"; export default { key: "help_scout-create-customer", name: "Create Customer", description: "Creates a new customer record in Help Scout. [See the documentation](https://developer.helpscout.com/mailbox-api/endpoints/customers/create/)", - version: "0.0.{{ts}}", + version: "0.0.1", type: "action", props: { helpScout, - customerEmail: { - propDefinition: [ - helpScout, - "customerEmail", - ], - }, - customerPhone: { - propDefinition: [ - helpScout, - "customerPhone", - ], - }, - chatHandles: { - propDefinition: [ - helpScout, - "chatHandles", - ], + firstName: { + type: "string", + label: "First Name", + description: "First name of the customer. When defined it must be between 1 and 40 characters.", + optional: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Last name of the customer. When defined it must be between 1 and 40 characters.", + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "The phone number that will be used when creating a new customer.", + optional: true, + }, + photoUrl: { + type: "string", + label: "Photo URL", + description: "URL of the customer's photo. Max length 200 characters.", + optional: true, + }, + jobTitle: { + type: "string", + label: "Job Title", + description: "Job title. Max length 60 characters.", + optional: true, + }, + photoType: { + type: "string", + label: "Photo Type", + description: "The type of photo.", + options: PHOTO_TYPE_OPTIONS, + optional: true, + }, + background: { + type: "string", + label: "Background", + description: "This is the Notes field from the user interface. Max length 200 characters.", + optional: true, + }, + location: { + type: "string", + label: "Location", + description: "Location of the customer. Max length 60 characters.", + optional: true, + }, + organization: { + type: "string", + label: "Organization", + description: "Organization. Max length 60 characters.", + optional: true, + }, + gender: { + type: "string", + label: "Gender", + description: "Gender of this customer.", + options: GENDER_OPTIONS, + optional: true, + }, + age: { + type: "string", + label: "Age", + description: "Customer's age.", + optional: true, + }, + emails: { + type: "string[]", + label: "Emails", + description: "List of email entries. **Format: {\"type\":\"home\",\"value\":\"customer@email.com\"}**. see [Create Email](https://developer.helpscout.com/mailbox-api/endpoints/customers/emails/create) for the object documentation.", + optional: true, + }, + phones: { + type: "string[]", + label: "Phones", + description: "List of phone entries. **Format: {\"type\":\"work\",\"value\":\"222-333-4444\"}**. see [Create Phone](https://developer.helpscout.com/mailbox-api/endpoints/customers/phones/create) for the object documentation.", + optional: true, + }, + chats: { + type: "string[]", + label: "Chats", + description: "List of chat entries. **Format: {\"type\":\"aim\",\"value\":\"jsprout\"}**. see [Create Chat Handle](https://developer.helpscout.com/mailbox-api/endpoints/customers/chat_handles/create) for the object documentation.", + optional: true, }, socialProfiles: { - propDefinition: [ - helpScout, - "socialProfiles", - ], - }, - customerAddress: { - propDefinition: [ - helpScout, - "customerAddress", - ], - }, - customerDetails: { - propDefinition: [ - helpScout, - "customerDetails", - ], + type: "string[]", + label: "Social Profiles", + description: "List of social profile entries. **Format: {\"type\":\"googleplus\",\"value\":\"https://api.helpscout.net/+HelpscoutNet\"}**. see [Create Social Profile](https://developer.helpscout.com/mailbox-api/endpoints/customers/social_profiles/create) for the object documentation.", + optional: true, + }, + websites: { + type: "string[]", + label: "Websites", + description: "List of websites entries. **Format: {\"value\":\"https://api.helpscout.net/\"}**. see [Create Website](https://developer.helpscout.com/mailbox-api/endpoints/customers/websites/create) for the object documentation.", + optional: true, + }, + addressCity: { + type: "string", + label: "Address City", + description: "The city of the customer.", + optional: true, + }, + addressState: { + type: "string", + label: "Address State", + description: "The state of the customer.", + optional: true, + }, + addressPostalCode: { + type: "string", + label: "Address Postal Code", + description: "The postal code of the customer.", + optional: true, + }, + addressCountry: { + type: "string", + label: "Address Country", + description: "The [ISO 3166 Alpha-2 code](https://www.iban.com/country-codes) country of the customer.", + optional: true, + }, + addressLines: { + type: "string[]", + label: "Address Lines", + description: "A list of address lines.", + optional: true, + }, + properties: { + type: "string[]", + label: "Properties", + description: "List of social profile entries. **Format: {\"type\":\"googleplus\",\"value\":\"https://api.helpscout.net/+HelpscoutNet\"}**. see [Create Social Profile](https://developer.helpscout.com/mailbox-api/endpoints/customers/social_profiles/create) for the object documentation.", + optional: true, }, }, async run({ $ }) { - const response = await this.helpScout.createCustomer({ - customerEmail: this.customerEmail, - customerPhone: this.customerPhone, - chatHandles: this.chatHandles, - socialProfiles: this.socialProfiles, - customerAddress: this.customerAddress, - ...this.customerDetails, - }); + try { + const address = cleanObject({ + city: this.addressCity, + state: this.addressState, + postalCode: this.addressPostalCode, + country: this.addressCountry, + lines: parseObject(this.addressLines), + properties: parseObject(this.properties), + }); + + const data = cleanObject({ + firstName: this.firstName, + lastName: this.lastName, + phone: this.phone, + photoUrl: this.photoUrl, + jobTitle: this.jobTitle, + photoType: this.photoType, + background: this.background, + location: this.location, + organization: this.organization, + gender: this.gender, + age: this.age, + emails: parseObject(this.emails), + phones: parseObject(this.phones), + chats: parseObject(this.chats), + socialProfiles: parseObject(this.socialProfiles), + websites: parseObject(this.websites), + }); + + if (Object.keys(address).length) data.address = address; + + if (!Object.keys(data).length) { + throw new ConfigurationError("At least one field or customer entry must be defined."); + } + + const response = await this.helpScout.createCustomer({ + $, + data, + }); + + $.export("$summary", "Successfully created the new customer."); + return response; - $.export("$summary", `Successfully created customer with email: ${this.customerEmail}`); - return response; + } catch ({ response }) { + throw new ConfigurationError(response.data.message); + } }, }; diff --git a/components/help_scout/actions/send-reply/send-reply.mjs b/components/help_scout/actions/send-reply/send-reply.mjs index c62a1d049325c..61673ae81a8b5 100644 --- a/components/help_scout/actions/send-reply/send-reply.mjs +++ b/components/help_scout/actions/send-reply/send-reply.mjs @@ -1,5 +1,4 @@ import helpScout from "../../help_scout.app.mjs"; -import { axios } from "@pipedream/platform"; export default { key: "help_scout-send-reply", @@ -15,17 +14,37 @@ export default { "conversationId", ], }, + customerId: { + propDefinition: [ + helpScout, + "customerId", + ], + }, text: { propDefinition: [ helpScout, "text", ], + description: "The content of the reply.", + }, + draft: { + type: "boolean", + label: "Draft", + description: "If set to true, a draft reply is created.", + default: false, }, }, async run({ $ }) { const response = await this.helpScout.sendReplyToConversation({ + $, conversationId: this.conversationId, - text: this.text, + data: { + customer: { + id: this.customerId, + }, + text: this.text, + draft: this.draft, + }, }); $.export("$summary", `Reply sent successfully to conversation ID: ${this.conversationId}`); diff --git a/components/help_scout/common/constants.mjs b/components/help_scout/common/constants.mjs new file mode 100644 index 0000000000000..76d42ac05aaa4 --- /dev/null +++ b/components/help_scout/common/constants.mjs @@ -0,0 +1,16 @@ +export const PHOTO_TYPE_OPTIONS = [ + "unknown", + "gravatar", + "twitter", + "facebook", + "googleprofile", + "googleplus", + "linkedin", + "instagram", +]; + +export const GENDER_OPTIONS = [ + "male", + "female", + "unknown", +]; diff --git a/components/help_scout/common/utils.mjs b/components/help_scout/common/utils.mjs new file mode 100644 index 0000000000000..8fabcd59a844c --- /dev/null +++ b/components/help_scout/common/utils.mjs @@ -0,0 +1,33 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; + +export const cleanObject = (o) => { + for (var k in o || {}) { + if (typeof o[k] === "undefined") { + delete o[k]; + } + } + return o; +}; diff --git a/components/help_scout/help_scout.app.mjs b/components/help_scout/help_scout.app.mjs index 6937e41e37126..4b8b4ad42e146 100644 --- a/components/help_scout/help_scout.app.mjs +++ b/components/help_scout/help_scout.app.mjs @@ -13,184 +13,137 @@ export default { type: "string", label: "Conversation ID", description: "The unique identifier of the conversation.", - }, - conversationTitle: { + async options({ page }) { + const { _embedded: { conversations } } = await this.listConversations({ + params: { + page: page + 1, + }, + }); + + return conversations.map(({ + id: value, subject, primaryCustomer: { email }, + }) => ({ + label: `${subject}(${value}) - ${email}`, + value, + })); + }, + }, + customerId: { type: "string", - label: "Conversation Title", - description: "Title of the conversation.", + label: "Customer ID", + description: "The unique identifier of the customer.", + async options({ page }) { + const { _embedded: { customers } } = await this.listCustomers({ + params: { + page: page + 1, + }, + }); + + return customers.map(({ + id: value, firstName, lastName, _embedded: { emails }, + }) => ({ + label: `${firstName} ${lastName} - ${emails[0].id}`, + value, + })); + }, + }, + userId: { + type: "string", + label: "User ID", + description: "The unique identifier of the user.", + async options({ page }) { + const { _embedded: { users } } = await this.listUsers({ + params: { + page: page + 1, + }, + }); + + return users.map(({ + id: value, email: label, + }) => ({ + label, + value, + })); + }, }, text: { type: "string", label: "Text", - description: "The content of the note or reply.", - }, - customerEmail: { - type: "string", - label: "Customer Email", - description: "Email of the customer.", - }, - customerDetails: { - type: "object", - label: "Customer Details", - description: "Optional customer's details such as name and contact.", - optional: true, - }, - customerPhone: { - type: "string", - label: "Customer Phone", - description: "Optional phone number of the customer.", - optional: true, - }, - chatHandles: { - type: "string[]", - label: "Chat Handles", - description: "Optional chat handles for the customer.", - optional: true, - }, - socialProfiles: { - type: "string[]", - label: "Social Profiles", - description: "Optional social profiles for the customer.", - optional: true, - }, - customerAddress: { - type: "object", - label: "Customer Address", - description: "Optional address of the customer.", - optional: true, + description: "The content of the note.", }, }, methods: { _baseUrl() { return "https://api.helpscout.net/v2"; }, - async _makeRequest(opts = {}) { - const { - $ = this, - method = "GET", - path = "/", - headers, - ...otherOpts - } = opts; + _headers() { + return { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { return axios($, { - ...otherOpts, - method, url: this._baseUrl() + path, - headers: { - ...headers, - Authorization: `Bearer ${this.$auth.oauth_token}`, - }, + headers: this._headers(), + ...opts, }); }, - async createWebhook({ - url, events, secret, - }) { + listConversations(opts = {}) { + return this._makeRequest({ + path: "/conversations", + ...opts, + }); + }, + listCustomers(opts = {}) { + return this._makeRequest({ + path: "/customers", + ...opts, + }); + }, + listUsers(opts = {}) { + return this._makeRequest({ + path: "/users", + ...opts, + }); + }, + createWebhook(opts = {}) { return this._makeRequest({ method: "POST", path: "/webhooks", - data: { - url, - events, - secret, - payloadVersion: "V2", - }, + ...opts, + }); + }, + deleteWebhook(webhookId) { + return this._makeRequest({ + method: "DELETE", + path: `/webhooks/${webhookId}`, }); }, - async addNoteToConversation({ - conversationId, text, + addNoteToConversation({ + conversationId, ...opts }) { return this._makeRequest({ method: "POST", path: `/conversations/${conversationId}/notes`, - data: { - text, - }, + ...opts, }); }, - async createCustomer({ - customerEmail, - customerPhone, - chatHandles, - socialProfiles, - customerAddress, - ...customerDetails - }) { + createCustomer(opts = {}) { return this._makeRequest({ method: "POST", path: "/customers", - data: { - emails: [ - { - value: customerEmail, - }, - ], - phones: customerPhone - ? [ - { - value: customerPhone, - }, - ] - : undefined, - chats: chatHandles - ? chatHandles.map((handle) => ({ - value: handle, - })) - : undefined, - socialProfiles: socialProfiles - ? socialProfiles.map((profile) => ({ - value: profile, - })) - : undefined, - address: customerAddress, - ...customerDetails, - }, + ...opts, }); }, - async sendReplyToConversation({ - conversationId, text, + sendReplyToConversation({ + conversationId, ...opts }) { return this._makeRequest({ method: "POST", path: `/conversations/${conversationId}/reply`, - data: { - text, - }, - }); - }, - async emitNewEventOnConversationAssigned({ agentId }) { - const events = [ - "convo.assigned", - ]; - const url = "https://example.com/helpscout"; // Replace with your URL - const secret = "your_secret_key"; // Generate and replace with your secret key - await this.createWebhook({ - url, - events, - secret, - }); - }, - async emitNewEventOnCustomerAdded() { - const events = [ - "customer.created", - ]; - const url = "https://example.com/helpscout"; // Replace with your URL - const secret = "your_secret_key"; // Generate and replace with your secret key - await this.createWebhook({ - url, - events, - secret, - }); - }, - async emitNewEventOnConversationCreated({ conversationTitle }) { - const events = [ - "convo.created", - ]; - const url = "https://example.com/helpscout"; // Replace with your URL - const secret = "your_secret_key"; // Generate and replace with your secret key - await this.createWebhook({ - url, - events, - secret, + ...opts, }); }, }, diff --git a/components/help_scout/package.json b/components/help_scout/package.json new file mode 100644 index 0000000000000..2cbc959a07578 --- /dev/null +++ b/components/help_scout/package.json @@ -0,0 +1,20 @@ +{ + "name": "@pipedream/help_scout", + "version": "0.1.0", + "description": "Pipedream Help Scout Components", + "main": "help_scout.app.mjs", + "keywords": [ + "pipedream", + "help_scout" + ], + "homepage": "https://pipedream.com/apps/help_scout", + "author": "Pipedream (https://pipedream.com/)", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3", + "crypto": "^1.0.1" + } +} + diff --git a/components/help_scout/sources/common/base.mjs b/components/help_scout/sources/common/base.mjs new file mode 100644 index 0000000000000..90329148e98d3 --- /dev/null +++ b/components/help_scout/sources/common/base.mjs @@ -0,0 +1,84 @@ +import crypto from "crypto"; +import helpScout from "../../help_scout.app.mjs"; + +export default { + props: { + helpScout, + http: { + type: "$.interface.http", + customResponse: true, + }, + db: "$.service.db", + label: { + type: "string", + label: "Label", + description: "Label associated with this WebHook for better clarity.", + optional: true, + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + _getSecret() { + return this.db.get("secret"); + }, + _setSecret(secret) { + this.db.set("secret", secret); + }, + getExtraData() { + return {}; + }, + }, + hooks: { + async activate() { + const secret = crypto.randomBytes(64).toString("hex"); + const { headers } = await this.helpScout.createWebhook({ + returnFullResponse: true, + data: { + url: this.http.endpoint, + events: this.getEventType(), + label: this.label, + secret, + }, + }); + this._setSecret(secret); + this._setHookId(headers["resource-id"]); + }, + async deactivate() { + const webhookId = this._getHookId(); + await this.helpScout.deleteWebhook(webhookId); + }, + }, + async run({ + bodyRaw, body, headers, + }) { + const hsSignature = headers["x-helpscout-signature"]; + if (hsSignature) { + const secret = this._getSecret(); + const hash = crypto.createHmac("sha1", secret) + .update(bodyRaw) + .digest("base64"); + + if (hash != hsSignature) { + return this.http.respond({ + status: 400, + }); + } + } + + const ts = Date.parse(new Date()); + this.$emit(body, { + id: body.id, + summary: this.getSummary(body), + ts: ts, + }); + + return this.http.respond({ + status: 200, + }); + }, +}; diff --git a/components/help_scout/sources/new-conversation-assigned-instant/new-conversation-assigned-instant.mjs b/components/help_scout/sources/new-conversation-assigned-instant/new-conversation-assigned-instant.mjs index 6fdc4fd762c2b..947d6252fa4e8 100644 --- a/components/help_scout/sources/new-conversation-assigned-instant/new-conversation-assigned-instant.mjs +++ b/components/help_scout/sources/new-conversation-assigned-instant/new-conversation-assigned-instant.mjs @@ -1,97 +1,24 @@ -import helpScout from "../../help_scout.app.mjs"; -import crypto from "crypto"; -import { axios } from "@pipedream/platform"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "help_scout-new-conversation-assigned-instant", - name: "New Conversation Assigned", + name: "New Conversation Assigned (Instant)", description: "Emit new event when a conversation is assigned to an agent. [See the documentation](https://developer.helpscout.com/)", - version: "0.0.{{ts}}", + version: "0.0.1", type: "source", dedupe: "unique", - props: { - helpScout, - agentId: { - propDefinition: [ - helpScout, - "agentId", - ], - }, - http: { - type: "$.interface.http", - customResponse: true, - }, - db: "$.service.db", - }, methods: { - _getWebhookId() { - return this.db.get("webhookId"); - }, - _setWebhookId(id) { - this.db.set("webhookId", id); - }, - _getSecret() { - return this.db.get("secret"); - }, - _setSecret(secret) { - this.db.set("secret", secret); - }, - _verifySignature(headerSignature, body) { - const secret = this._getSecret(); - const computedSignature = crypto - .createHmac("sha256", secret) - .update(body) - .digest("base64"); - return headerSignature === computedSignature; - }, - }, - hooks: { - async activate() { - const events = [ + ...common.methods, + getEventType() { + return [ "convo.assigned", ]; - const url = this.http.endpoint; - const secret = crypto.randomBytes(20).toString("hex"); - const { id } = await this.helpScout.createWebhook({ - url, - events, - secret, - }); - this._setWebhookId(id); - this._setSecret(secret); }, - async deactivate() { - const webhookId = this._getWebhookId(); - if (webhookId) { - await this.helpScout._makeRequest({ - method: "DELETE", - path: `/webhooks/${webhookId}`, - }); - } + getSummary(body) { + return `New conversation assigned to ${body.assignee.email}`; }, }, - async run(event) { - const headerSignature = event.headers["x-helpscout-signature"]; - const body = event.bodyRaw; - if (!this._verifySignature(headerSignature, body)) { - this.http.respond({ - status: 401, - body: "Unauthorized", - }); - return; - } - - const data = JSON.parse(body); - if (data.event === "convo.assigned" && data.agent.id === this.agentId) { - this.$emit(data, { - id: data.id, - summary: `New conversation assigned to ${this.agentId}`, - ts: Date.parse(data.changedAt), - }); - } - - this.http.respond({ - status: 200, - }); - }, + sampleEmit, }; diff --git a/components/help_scout/sources/new-conversation-assigned-instant/test-event.mjs b/components/help_scout/sources/new-conversation-assigned-instant/test-event.mjs new file mode 100644 index 0000000000000..1935a35e8e9e9 --- /dev/null +++ b/components/help_scout/sources/new-conversation-assigned-instant/test-event.mjs @@ -0,0 +1,137 @@ +export default { + "id": 291938, + "type": "email", + "folderId": "1234", + "isDraft": "false", + "number": 349, + "owner": { + "id": 1234, + "firstName": "Jack", + "lastName": "Sprout", + "email": "jack.sprout@gmail.com", + "phone": null, + "type": "user" + }, + "mailbox": { + "id": 1234, + "name": "My Mailbox" + }, + "customer": { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "email": "vbear@mywork.com", + "phone": "800-555-1212", + "type": "customer" + }, + "threadCount": 4, + "status": "active", + "subject": "I need help!", + "preview": "Hello, I tried to download the file off your site...", + "createdBy": { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "email": "vbear@mywork.com", + "phone": null, + "type": "customer" + }, + "createdAt": "2012-07-23T12:34:12Z", + "modifiedAt": "2012-07-24T20:18:33Z", + "closedAt": null, + "closedBy": null, + "source": { + "type": "email", + "via": "customer" + }, + "cc": [ + "cc1@somewhere.com", + "cc2@somewhere.com" + ], + "bcc": [ + "bcc1@somewhere.com", + "bcc2@somewhere.com" + ], + "tags": [ + "tag1", + "tag2" + ], + "customFields": [ + { + "fieldId": 1, + "name": "Team", + "value": "Development" + }, + { + "fieldId": 2, + "name": "Customer Disposition", + "value": "Happy" + } + ], + "threads": [ + { + "id": 88171881, + "assignedTo": { + "id": 1234, + "firstName": "Jack", + "lastName": "Sprout", + "email": "jack.sprout@gmail.com", + "phone": null, + "type": "user" + }, + "status": "active", + "createdAt": "2012-07-23T12:34:12Z", + "createdBy": { + "id": 1234, + "firstName": "Jack", + "lastName": "Sprout", + "email": "jack.sprout@gmail.com", + "phone": null, + "type": "user" + }, + "source": { + "type": "web", + "via": "user" + }, + "type": "message", + "state": "published", + "customer": { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "email": "vbear@mywork.com", + "phone": "800-555-1212", + "type": "customer" + }, + "fromMailbox": null, + "body": "This is what I have to say. Thank you.", + "to": [ + "customer@somewhere.com" + ], + "cc": [ + "cc1@somewhere.com", + "cc2@somewhere.com" + ], + "bcc": [ + "bcc1@somewhere.com", + "bcc2@somewhere.com" + ], + "attachments": [ + { + "id": 12391, + "mimeType": "image/jpeg", + "filename": "logo.jpg", + "size": 22, + "width": 160, + "height": 160, + "url": "https://secure.helpscout.net/some-url/logo.jpg" + } + ], + "tags": [ + "tag1", + "tag2", + "tag3" + ] + } + ] +} \ No newline at end of file diff --git a/components/help_scout/sources/new-conversation-created-instant/new-conversation-created-instant.mjs b/components/help_scout/sources/new-conversation-created-instant/new-conversation-created-instant.mjs index 44176f7cc113e..37f0b97e12d52 100644 --- a/components/help_scout/sources/new-conversation-created-instant/new-conversation-created-instant.mjs +++ b/components/help_scout/sources/new-conversation-created-instant/new-conversation-created-instant.mjs @@ -1,99 +1,24 @@ -import helpScout from "../../help_scout.app.mjs"; -import { axios } from "@pipedream/platform"; -import crypto from "crypto"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "help_scout-new-conversation-created-instant", - name: "New Conversation Created", - description: "Emit new event when a new conversation is created. [See the documentation](https://developer.helpscout.com/webhooks/)", - version: "0.0.{{ts}}", + name: "New Conversation Created (Instant)", + description: "Emit new event when a new conversation is created.", + version: "0.0.1", type: "source", dedupe: "unique", - props: { - helpScout, - http: { - type: "$.interface.http", - customResponse: true, - }, - db: "$.service.db", - conversationTitle: { - propDefinition: [ - helpScout, - "conversationTitle", - ], - }, - conversationDetails: { - type: "object", - label: "Conversation Details", - description: "Optional conversation details such as tags, type, etc.", - optional: true, - }, - }, methods: { - _getWebhookId() { - return this.db.get("webhookId"); - }, - _setWebhookId(id) { - this.db.set("webhookId", id); - }, - }, - hooks: { - async deploy() { - // Emit historical events (not implemented) - }, - async activate() { - const url = this.http.endpoint; - const secret = crypto.randomBytes(16).toString("hex"); - const events = [ + ...common.methods, + getEventType() { + return [ "convo.created", ]; - const webhook = await this.helpScout.createWebhook({ - url, - events, - secret, - }); - this._setWebhookId(webhook.id); - this.db.set("secret", secret); }, - async deactivate() { - const webhookId = this._getWebhookId(); - if (webhookId) { - await this.helpScout._makeRequest({ - method: "DELETE", - path: `/webhooks/${webhookId}`, - }); - this.db.set("webhookId", null); - } + getSummary(body) { + return `New conversation created: ${body.subject}`; }, }, - async run(event) { - const rawBody = event.rawBody; - const webhookSignature = event.headers["x-helpscout-signature"]; - const secretKey = this.db.get("secret"); - - const computedSignature = crypto.createHmac("sha256", secretKey).update(rawBody) - .digest("base64"); - if (computedSignature !== webhookSignature) { - this.http.respond({ - status: 401, - body: "Unauthorized", - }); - return; - } - - this.http.respond({ - status: 200, - body: "OK", - }); - - const { - title, ...rest - } = event.body; - - this.$emit(event.body, { - id: title, - summary: `New conversation: ${title}`, - ts: new Date().getTime(), - }); - }, + sampleEmit, }; diff --git a/components/help_scout/sources/new-conversation-created-instant/test-event.mjs b/components/help_scout/sources/new-conversation-created-instant/test-event.mjs new file mode 100644 index 0000000000000..d8cc91fd9e8de --- /dev/null +++ b/components/help_scout/sources/new-conversation-created-instant/test-event.mjs @@ -0,0 +1,137 @@ +export default { + "id": 291938, + "type": "email", + "folderId": "1234", + "isDraft": "false", + "number": 349, + "owner": { + "id": 1234, + "firstName": "Jack", + "lastName": "Sprout", + "email": "jack.sprout@gmail.com", + "phone": null, + "type": "user" + }, + "mailbox": { + "id": 1234, + "name": "My Mailbox" + }, + "customer": { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "email": "vbear@mywork.com", + "phone": "800-555-1212", + "type": "customer" + }, + "threadCount": 4, + "status": "active", + "subject": "I need help!", + "preview": "Hello, I tried to download the file off your site...", + "createdBy": { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "email": "vbear@mywork.com", + "phone": null, + "type": "customer" + }, + "createdAt": "2012-07-23T12:34:12Z", + "modifiedAt": "2012-07-24T20:18:33Z", + "closedAt": null, + "closedBy": null, + "source": { + "type": "email", + "via": "customer" + }, + "cc": [ + "cc1@somewhere.com", + "cc2@somewhere.com" + ], + "bcc": [ + "bcc1@somewhere.com", + "bcc2@somewhere.com" + ], + "tags": [ + "tag1", + "tag2" + ], + "customFields": [ + { + "fieldId": 1, + "name": "Team", + "value": "Development" + }, + { + "fieldId": 2, + "name": "Customer Disposition", + "value": "Happy" + } + ], + "threads": [ + { + "id": 88171881, + "assignedTo": { + "id": 1234, + "firstName": "Jack", + "lastName": "Sprout", + "email": "jack.sprout@gmail.com", + "phone": null, + "type": "user" + }, + "status": "active", + "createdAt": "2012-07-23T12:34:12Z", + "createdBy": { + "id": 1234, + "firstName": "Jack", + "lastName": "Sprout", + "email": "jack.sprout@gmail.com", + "phone": null, + "type": "user" + }, + "source": { + "type": "web", + "via": "user" + }, + "type": "message", + "state": "published", + "customer": { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "email": "vbear@mywork.com", + "phone": "800-555-1212", + "type": "customer" + }, + "fromMailbox": null, + "body": "This is what I have to say. Thank you.", + "to": [ + "customer@somewhere.com" + ], + "cc": [ + "cc1@somewhere.com", + "cc2@somewhere.com" + ], + "bcc": [ + "bcc1@somewhere.com", + "bcc2@somewhere.com" + ], + "attachments": [ + { + "id": 12391, + "mimeType": "image/jpeg", + "filename": "logo.jpg", + "size": 22, + "width": 160, + "height": 160, + "url": "https://secure.helpscout.net/some-url/logo.jpg" + } + ], + "tags": [ + "tag1", + "tag2", + "tag3" + ] + } + ] + } \ No newline at end of file diff --git a/components/help_scout/sources/new-customer-instant/new-customer-instant.mjs b/components/help_scout/sources/new-customer-instant/new-customer-instant.mjs index 0ff492125a277..766f7a78b6863 100644 --- a/components/help_scout/sources/new-customer-instant/new-customer-instant.mjs +++ b/components/help_scout/sources/new-customer-instant/new-customer-instant.mjs @@ -1,94 +1,24 @@ -import helpScout from "../../help_scout.app.mjs"; -import crypto from "crypto"; -import { axios } from "@pipedream/platform"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "help_scout-new-customer-instant", - name: "New Customer Added", - description: "Emit new event when a new customer is added. [See the documentation](https://developer.helpscout.com/)", - version: "0.0.{{ts}}", + name: "New Customer Added (Instant)", + description: "Emit new event when a new customer is added.", + version: "0.0.1", type: "source", dedupe: "unique", - props: { - helpScout, - http: { - type: "$.interface.http", - customResponse: true, - }, - db: "$.service.db", - customerDetails: { - propDefinition: [ - helpScout, - "customerDetails", - ], - }, - }, methods: { - _getWebhookId() { - return this.db.get("webhookId"); - }, - _setWebhookId(id) { - this.db.set("webhookId", id); - }, - _generateSecret() { - return crypto.randomBytes(20).toString("hex"); - }, - }, - hooks: { - async activate() { - const url = this.http.endpoint; - const secret = this._generateSecret(); - const events = [ + ...common.methods, + getEventType() { + return [ "customer.created", ]; - const response = await this.helpScout.createWebhook({ - url, - events, - secret, - }); - this._setWebhookId(response.id); - this.db.set("secret", secret); }, - async deactivate() { - const webhookId = this._getWebhookId(); - if (webhookId) { - await axios(this, { - method: "DELETE", - url: `${this.helpScout._baseUrl()}/webhooks/${webhookId}`, - headers: { - Authorization: `Bearer ${this.helpScout.$auth.oauth_token}`, - }, - }); - } + getSummary(body) { + return `New customer created: ${body.firstName} ${body.lastName} - ${body._embedded.emails[0].value}`; }, }, - async run(event) { - const signature = this.http.headers["x-helpscout-signature"]; - const rawBody = JSON.stringify(event.body); - const secret = this.db.get("secret"); - - const computedSignature = crypto.createHmac("sha256", secret).update(rawBody) - .digest("base64"); - - if (computedSignature !== signature) { - this.http.respond({ - status: 401, - body: "Unauthorized", - }); - return; - } - - this.http.respond({ - status: 200, - body: "OK", - }); - - const customer = event.body.data.item; - - this.$emit(customer, { - id: customer.id, - summary: `New customer created: ${customer.firstName} ${customer.lastName}`, - ts: Date.parse(event.body.data.createdAt), - }); - }, + sampleEmit, }; diff --git a/components/help_scout/sources/new-customer-instant/test-event.mjs b/components/help_scout/sources/new-customer-instant/test-event.mjs new file mode 100644 index 0000000000000..7070a334032db --- /dev/null +++ b/components/help_scout/sources/new-customer-instant/test-event.mjs @@ -0,0 +1,58 @@ +export default { + "id": 29418, + "firstName": "Vernon", + "lastName": "Bear", + "photoUrl": "http://twitter.com/img/some-avatar.jpg", + "photoType": "twitter", + "gender": "Male", + "age": "30-35", + "organization": "Acme, Inc", + "jobTitle": "CEO and Co-Founder", + "location": "Greater Dallas/FT Worth Area", + "background": "I've worked with Vernon before and he's really great.", + "createdAt": "2012-07-23T12:34:12Z", + "modifiedAt": "2012-07-24T20:18:33Z", + "address": { + "id": 1234, + "lines": [ + "123 West Main St", + "Suite 123" + ], + "city": "Dallas", + "state": "TX", + "postcalCode": "74206", + "country": "US", + "createdAt": "2012-07-23T12:34:12Z", + "modifiedAt": "2012-07-24T20:18:33Z" + }, + "socialProfiles": [ + { + "id": 9184, + "value": "https://twitter.com/helpscout", + "type": "twitter" + }, + ], + "emails": [{ + "id": 98131, + "value": "vbear@mywork.com", + "location": "work" + }, + ], + "phones": [{ + "id": 22381, + "value": "222-333-4444", + "location": "home" + }, + ], + "chats": [{ + "id": 77183, + "value": "jsprout", + "type": "aim" + }, + ], + "websites": [{ + "id": 5584, + "value": "http://www.somewhere.com" + }, + ] +} \ No newline at end of file diff --git a/components/help_scout/yarn.lock b/components/help_scout/yarn.lock new file mode 100644 index 0000000000000..144b3d8041ea3 --- /dev/null +++ b/components/help_scout/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== From e8d5fcb2b9d6fe307b172bb620b0bd3fcd5dd762 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Fri, 8 Nov 2024 17:00:54 -0300 Subject: [PATCH 3/4] pnpm update --- pnpm-lock.yaml | 110 ++++++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 51 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 386208bb30d06..b4d4e286a77f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4595,6 +4595,14 @@ importers: dependencies: '@pipedream/platform': 1.5.1 + components/help_scout: + specifiers: + '@pipedream/platform': ^3.0.3 + crypto: ^1.0.1 + dependencies: + '@pipedream/platform': 3.0.3 + crypto: 1.0.1 + components/helpcrunch: specifiers: '@pipedream/platform': ^1.5.1 @@ -13347,6 +13355,55 @@ packages: - aws-crt dev: false + /@aws-sdk/client-sso-oidc/3.600.0_tdq3komn4zwyd65w7klbptsu34: + resolution: {integrity: sha512-7+I8RWURGfzvChyNQSyj5/tKrqRbzRl7H+BnTOf/4Vsw1nFOi5ROhlhD4X/Y0QCTacxnaoNcIrqnY7uGGvVRzw==} + engines: {node: '>=16.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sts': 3.600.0 + '@aws-sdk/core': 3.598.0 + '@aws-sdk/credential-provider-node': 3.600.0_f7n47caigsrjd2lr2szmwfuee4 + '@aws-sdk/middleware-host-header': 3.598.0 + '@aws-sdk/middleware-logger': 3.598.0 + '@aws-sdk/middleware-recursion-detection': 3.598.0 + '@aws-sdk/middleware-user-agent': 3.598.0 + '@aws-sdk/region-config-resolver': 3.598.0 + '@aws-sdk/types': 3.598.0 + '@aws-sdk/util-endpoints': 3.598.0 + '@aws-sdk/util-user-agent-browser': 3.598.0 + '@aws-sdk/util-user-agent-node': 3.598.0 + '@smithy/config-resolver': 3.0.3 + '@smithy/core': 2.2.3 + '@smithy/fetch-http-handler': 3.2.1 + '@smithy/hash-node': 3.0.2 + '@smithy/invalid-dependency': 3.0.2 + '@smithy/middleware-content-length': 3.0.2 + '@smithy/middleware-endpoint': 3.0.4 + '@smithy/middleware-retry': 3.0.6 + '@smithy/middleware-serde': 3.0.3 + '@smithy/middleware-stack': 3.0.3 + '@smithy/node-config-provider': 3.1.3 + '@smithy/node-http-handler': 3.1.2 + '@smithy/protocol-http': 4.0.3 + '@smithy/smithy-client': 3.1.6 + '@smithy/types': 3.3.0 + '@smithy/url-parser': 3.0.3 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.6 + '@smithy/util-defaults-mode-node': 3.0.6 + '@smithy/util-endpoints': 2.0.3 + '@smithy/util-middleware': 3.0.3 + '@smithy/util-retry': 3.0.2 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + transitivePeerDependencies: + - '@aws-sdk/client-sts' + - aws-crt + dev: false + /@aws-sdk/client-sso/3.423.0: resolution: {integrity: sha512-znIufHkwhCIePgaYciIs3x/+BpzR57CZzbCKHR9+oOvGyufEPPpUT5bFLvbwTgfiVkTjuk6sG/ES3U5Bc+xtrA==} engines: {node: '>=14.0.0'} @@ -13582,55 +13639,7 @@ packages: dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sso-oidc': 3.600.0 - '@aws-sdk/core': 3.598.0 - '@aws-sdk/credential-provider-node': 3.600.0_f7n47caigsrjd2lr2szmwfuee4 - '@aws-sdk/middleware-host-header': 3.598.0 - '@aws-sdk/middleware-logger': 3.598.0 - '@aws-sdk/middleware-recursion-detection': 3.598.0 - '@aws-sdk/middleware-user-agent': 3.598.0 - '@aws-sdk/region-config-resolver': 3.598.0 - '@aws-sdk/types': 3.598.0 - '@aws-sdk/util-endpoints': 3.598.0 - '@aws-sdk/util-user-agent-browser': 3.598.0 - '@aws-sdk/util-user-agent-node': 3.598.0 - '@smithy/config-resolver': 3.0.3 - '@smithy/core': 2.2.3 - '@smithy/fetch-http-handler': 3.2.1 - '@smithy/hash-node': 3.0.2 - '@smithy/invalid-dependency': 3.0.2 - '@smithy/middleware-content-length': 3.0.2 - '@smithy/middleware-endpoint': 3.0.4 - '@smithy/middleware-retry': 3.0.6 - '@smithy/middleware-serde': 3.0.3 - '@smithy/middleware-stack': 3.0.3 - '@smithy/node-config-provider': 3.1.3 - '@smithy/node-http-handler': 3.1.2 - '@smithy/protocol-http': 4.0.3 - '@smithy/smithy-client': 3.1.6 - '@smithy/types': 3.3.0 - '@smithy/url-parser': 3.0.3 - '@smithy/util-base64': 3.0.0 - '@smithy/util-body-length-browser': 3.0.0 - '@smithy/util-body-length-node': 3.0.0 - '@smithy/util-defaults-mode-browser': 3.0.6 - '@smithy/util-defaults-mode-node': 3.0.6 - '@smithy/util-endpoints': 2.0.3 - '@smithy/util-middleware': 3.0.3 - '@smithy/util-retry': 3.0.2 - '@smithy/util-utf8': 3.0.0 - tslib: 2.6.3 - transitivePeerDependencies: - - aws-crt - dev: false - - /@aws-sdk/client-sts/3.600.0_dseaa2p5u2yk67qiepewcq3hkq: - resolution: {integrity: sha512-KQG97B7LvTtTiGmjlrG1LRAY8wUvCQzrmZVV5bjrJ/1oXAU7DITYwVbSJeX9NWg6hDuSk0VE3MFwIXS2SvfLIA==} - engines: {node: '>=16.0.0'} - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sso-oidc': 3.600.0 + '@aws-sdk/client-sso-oidc': 3.600.0_tdq3komn4zwyd65w7klbptsu34 '@aws-sdk/core': 3.598.0 '@aws-sdk/credential-provider-node': 3.600.0_f7n47caigsrjd2lr2szmwfuee4 '@aws-sdk/middleware-host-header': 3.598.0 @@ -13669,7 +13678,6 @@ packages: '@smithy/util-utf8': 3.0.0 tslib: 2.6.3 transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - aws-crt dev: false @@ -18016,7 +18024,7 @@ packages: '@aws-sdk/client-sns': 3.423.0 '@aws-sdk/client-sqs': 3.423.0 '@aws-sdk/client-ssm': 3.423.0 - '@aws-sdk/client-sts': 3.600.0_dseaa2p5u2yk67qiepewcq3hkq + '@aws-sdk/client-sts': 3.600.0 '@aws-sdk/s3-request-presigner': 3.609.0 '@pipedream/helper_functions': 0.3.12 '@pipedream/platform': 1.6.6 From 426c4ebe467f9b3351d9c2739023692fec2af638 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Tue, 12 Nov 2024 12:13:53 -0300 Subject: [PATCH 4/4] some adjusts --- .../create-customer/create-customer.mjs | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/components/help_scout/actions/create-customer/create-customer.mjs b/components/help_scout/actions/create-customer/create-customer.mjs index 9f5bf6ddd5446..cec309bb7c831 100644 --- a/components/help_scout/actions/create-customer/create-customer.mjs +++ b/components/help_scout/actions/create-customer/create-customer.mjs @@ -153,41 +153,43 @@ export default { }, }, async run({ $ }) { - try { - const address = cleanObject({ - city: this.addressCity, - state: this.addressState, - postalCode: this.addressPostalCode, - country: this.addressCountry, - lines: parseObject(this.addressLines), - properties: parseObject(this.properties), - }); + const address = cleanObject({ + city: this.addressCity, + state: this.addressState, + postalCode: this.addressPostalCode, + country: this.addressCountry, + lines: parseObject(this.addressLines), + properties: parseObject(this.properties), + }); - const data = cleanObject({ - firstName: this.firstName, - lastName: this.lastName, - phone: this.phone, - photoUrl: this.photoUrl, - jobTitle: this.jobTitle, - photoType: this.photoType, - background: this.background, - location: this.location, - organization: this.organization, - gender: this.gender, - age: this.age, - emails: parseObject(this.emails), - phones: parseObject(this.phones), - chats: parseObject(this.chats), - socialProfiles: parseObject(this.socialProfiles), - websites: parseObject(this.websites), - }); + let data = {}; - if (Object.keys(address).length) data.address = address; + data = cleanObject({ + firstName: this.firstName, + lastName: this.lastName, + phone: this.phone, + photoUrl: this.photoUrl, + jobTitle: this.jobTitle, + photoType: this.photoType, + background: this.background, + location: this.location, + organization: this.organization, + gender: this.gender, + age: this.age, + emails: parseObject(this.emails), + phones: parseObject(this.phones), + chats: parseObject(this.chats), + socialProfiles: parseObject(this.socialProfiles), + websites: parseObject(this.websites), + }); - if (!Object.keys(data).length) { - throw new ConfigurationError("At least one field or customer entry must be defined."); - } + if (Object.keys(address).length) data.address = address; + if (!Object.keys(data).length) { + throw new ConfigurationError("At least one field or customer entry must be defined."); + } + + try { const response = await this.helpScout.createCustomer({ $, data, @@ -195,9 +197,9 @@ export default { $.export("$summary", "Successfully created the new customer."); return response; - - } catch ({ response }) { - throw new ConfigurationError(response.data.message); + } catch ({ message }) { + const error = JSON.parse(message)._embedded.errors[0]; + throw new ConfigurationError(`Path: ${error.path} - ${error.message}`); } }, };