From 13ca8f61362b9a899dd466323288d56a60f1fad2 Mon Sep 17 00:00:00 2001 From: seynadio <79858321+seynadio@users.noreply.github.com> Date: Mon, 14 Jul 2025 13:43:12 +0200 Subject: [PATCH] Add HubSpot conversation threads with internal notes support - Add conversation API methods to HubSpot app with JSDoc documentation - Add threadId prop definition for conversation threads - Create send-conversation-message action for regular messages with validation - Create add-conversation-comment action for internal notes (COMMENT type) - Add webhook sources for conversation messages and internal comments with proper polling - Update package version to 1.2.5 - Support HubSpot Conversations API v3 endpoints - Add input validation for empty text fields --- .../add-conversation-comment.mjs | 47 ++++++++++ .../send-conversation-message.mjs | 64 +++++++++++++ components/hubspot/hubspot.app.mjs | 63 +++++++++++++ components/hubspot/package.json | 2 +- .../new-conversation-comment.mjs | 70 ++++++++++++++ .../new-conversation-message.mjs | 92 +++++++++++++++++++ 6 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 components/hubspot/actions/add-conversation-comment/add-conversation-comment.mjs create mode 100644 components/hubspot/actions/send-conversation-message/send-conversation-message.mjs create mode 100644 components/hubspot/sources/new-conversation-comment/new-conversation-comment.mjs create mode 100644 components/hubspot/sources/new-conversation-message/new-conversation-message.mjs diff --git a/components/hubspot/actions/add-conversation-comment/add-conversation-comment.mjs b/components/hubspot/actions/add-conversation-comment/add-conversation-comment.mjs new file mode 100644 index 0000000000000..ebd9b24272a0c --- /dev/null +++ b/components/hubspot/actions/add-conversation-comment/add-conversation-comment.mjs @@ -0,0 +1,47 @@ +import hubspot from "../../hubspot.app.mjs"; + +export default { + key: "hubspot-add-conversation-comment", + name: "Add Conversation Comment (Internal Note)", + description: "Add an internal comment to a HubSpot conversation thread. Internal comments are only visible to team members. [See the documentation](https://developers.hubspot.com/docs/api/conversations/threads)", + version: "0.0.1", + type: "action", + props: { + hubspot, + threadId: { + propDefinition: [ + hubspot, + "threadId", + ], + }, + text: { + type: "string", + label: "Comment Text", + description: "The plain text content of the internal comment", + }, + richText: { + type: "string", + label: "Rich Text", + description: "The rich text/HTML content of the internal comment", + optional: true, + }, + }, + async run({ $ }) { + if (!this.text?.trim()) { + throw new Error("Comment text cannot be empty"); + } + + const response = await this.hubspot.addConversationComment({ + threadId: this.threadId, + data: { + text: this.text, + richText: this.richText || this.text, + type: "COMMENT", + }, + $, + }); + + $.export("$summary", `Successfully added internal comment to conversation thread ${this.threadId}`); + return response; + }, +}; \ No newline at end of file diff --git a/components/hubspot/actions/send-conversation-message/send-conversation-message.mjs b/components/hubspot/actions/send-conversation-message/send-conversation-message.mjs new file mode 100644 index 0000000000000..ac35f4d0ea87c --- /dev/null +++ b/components/hubspot/actions/send-conversation-message/send-conversation-message.mjs @@ -0,0 +1,64 @@ +import hubspot from "../../hubspot.app.mjs"; + +export default { + key: "hubspot-send-conversation-message", + name: "Send Conversation Message", + description: "Send a message to a HubSpot conversation thread. [See the documentation](https://developers.hubspot.com/docs/api/conversations/threads)", + version: "0.0.1", + type: "action", + props: { + hubspot, + threadId: { + propDefinition: [ + hubspot, + "threadId", + ], + }, + text: { + type: "string", + label: "Message Text", + description: "The plain text content of the message", + }, + richText: { + type: "string", + label: "Rich Text", + description: "The rich text/HTML content of the message", + optional: true, + }, + direction: { + type: "string", + label: "Direction", + description: "The direction of the message", + options: [ + { + label: "Outgoing", + value: "OUTGOING", + }, + { + label: "Incoming", + value: "INCOMING", + }, + ], + default: "OUTGOING", + }, + }, + async run({ $ }) { + if (!this.text?.trim()) { + throw new Error("Message text cannot be empty"); + } + + const response = await this.hubspot.sendConversationMessage({ + threadId: this.threadId, + data: { + text: this.text, + richText: this.richText || this.text, + direction: this.direction, + type: "MESSAGE", + }, + $, + }); + + $.export("$summary", `Successfully sent message to conversation thread ${this.threadId}`); + return response; + }, +}; \ No newline at end of file diff --git a/components/hubspot/hubspot.app.mjs b/components/hubspot/hubspot.app.mjs index 06299b1ef8b48..f540dd416ecbe 100644 --- a/components/hubspot/hubspot.app.mjs +++ b/components/hubspot/hubspot.app.mjs @@ -124,6 +124,11 @@ export default { : []; }, }, + threadId: { + type: "string", + label: "Thread ID", + description: "HubSpot conversation thread ID", + }, objectIds: { type: "string[]", label: "Object", @@ -1142,5 +1147,63 @@ export default { ...opts, }); }, + /** + * Get conversation thread details + * @param {string} threadId - The ID of the conversation thread + * @param {object} opts - Additional options to pass to the request + * @returns {Promise} The conversation thread object + */ + getConversationThread({ + threadId, ...opts + }) { + return this.makeRequest({ + endpoint: `/conversations/v3/conversations/threads/${threadId}`, + ...opts, + }); + }, + /** + * Get messages from a conversation thread + * @param {string} threadId - The ID of the conversation thread + * @param {object} opts - Additional options to pass to the request + * @returns {Promise} The messages in the conversation thread + */ + getConversationMessages({ + threadId, ...opts + }) { + return this.makeRequest({ + endpoint: `/conversations/v3/conversations/threads/${threadId}/messages`, + ...opts, + }); + }, + /** + * Send a message to a conversation thread + * @param {string} threadId - The ID of the conversation thread + * @param {object} opts - Message data and request options + * @returns {Promise} The sent message object + */ + sendConversationMessage({ + threadId, ...opts + }) { + return this.makeRequest({ + endpoint: `/conversations/v3/conversations/threads/${threadId}/messages`, + method: "POST", + ...opts, + }); + }, + /** + * Add an internal comment to a conversation thread + * @param {string} threadId - The ID of the conversation thread + * @param {object} opts - Comment data and request options + * @returns {Promise} The added comment object + */ + addConversationComment({ + threadId, ...opts + }) { + return this.makeRequest({ + endpoint: `/conversations/v3/conversations/threads/${threadId}/messages`, + method: "POST", + ...opts, + }); + }, }, }; diff --git a/components/hubspot/package.json b/components/hubspot/package.json index 97609a3e714b9..04364b264bb92 100644 --- a/components/hubspot/package.json +++ b/components/hubspot/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/hubspot", - "version": "1.2.4", + "version": "1.2.5", "description": "Pipedream Hubspot Components", "main": "hubspot.app.mjs", "keywords": [ diff --git a/components/hubspot/sources/new-conversation-comment/new-conversation-comment.mjs b/components/hubspot/sources/new-conversation-comment/new-conversation-comment.mjs new file mode 100644 index 0000000000000..6e6fddd861f07 --- /dev/null +++ b/components/hubspot/sources/new-conversation-comment/new-conversation-comment.mjs @@ -0,0 +1,70 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "hubspot-new-conversation-comment", + name: "New Conversation Comment (Internal Note)", + description: "Emit new event when a new internal comment is added to a HubSpot conversation thread. [See the documentation](https://developers.hubspot.com/docs/api/conversations/threads)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + threadId: { + propDefinition: [ + common.props.hubspot, + "threadId", + ], + description: "Filter comments from a specific conversation thread", + optional: true, + }, + }, + methods: { + ...common.methods, + getTs(comment) { + return Date.parse(comment.createdAt); + }, + generateMeta(comment) { + return { + id: comment.id, + summary: `New Internal Comment: ${comment.text || comment.id}`, + ts: this.getTs(comment), + }; + }, + isRelevant(comment, createdAfter) { + const isAfterTimestamp = this.getTs(comment) > createdAfter; + const matchesThread = !this.threadId || comment.threadId === this.threadId; + const isComment = comment.type === "COMMENT"; + + return isAfterTimestamp && matchesThread && isComment; + }, + async getParams() { + return { + params: { + limit: 100, + }, + }; + }, + async processResults(after, params) { + const createdAfter = after || this.getLastCreatedAt(); + + if (this.threadId) { + // If specific thread is provided, get messages from that thread + const messages = await this.hubspot.getConversationMessages({ + threadId: this.threadId, + ...params, + }); + + const comments = messages.results?.filter(msg => + this.isRelevant(msg, createdAfter) + ) || []; + + this.processEvents(comments); + } else { + // Note: HubSpot Conversations API doesn't provide a direct way to list all threads + // This would require HubSpot webhooks or a different approach + console.log("Thread-specific monitoring recommended - provide threadId prop for best results"); + } + }, + }, +}; \ No newline at end of file diff --git a/components/hubspot/sources/new-conversation-message/new-conversation-message.mjs b/components/hubspot/sources/new-conversation-message/new-conversation-message.mjs new file mode 100644 index 0000000000000..96f7ec0d24d78 --- /dev/null +++ b/components/hubspot/sources/new-conversation-message/new-conversation-message.mjs @@ -0,0 +1,92 @@ +import common from "../common/common.mjs"; + +export default { + ...common, + key: "hubspot-new-conversation-message", + name: "New Conversation Message", + description: "Emit new event when a new message is added to a HubSpot conversation thread. [See the documentation](https://developers.hubspot.com/docs/api/conversations/threads)", + version: "0.0.1", + type: "source", + dedupe: "unique", + props: { + ...common.props, + threadId: { + propDefinition: [ + common.props.hubspot, + "threadId", + ], + description: "Filter messages from a specific conversation thread", + optional: true, + }, + messageType: { + type: "string", + label: "Message Type", + description: "Filter by message type", + options: [ + { + label: "All Messages", + value: "", + }, + { + label: "Regular Messages", + value: "MESSAGE", + }, + { + label: "Internal Comments", + value: "COMMENT", + }, + ], + default: "", + optional: true, + }, + }, + methods: { + ...common.methods, + getTs(message) { + return Date.parse(message.createdAt); + }, + generateMeta(message) { + const messageType = message.type === "COMMENT" ? "Internal Comment" : "Message"; + return { + id: message.id, + summary: `New ${messageType}: ${message.text || message.id}`, + ts: this.getTs(message), + }; + }, + isRelevant(message, createdAfter) { + const isAfterTimestamp = this.getTs(message) > createdAfter; + const matchesThread = !this.threadId || message.threadId === this.threadId; + const matchesType = !this.messageType || message.type === this.messageType; + + return isAfterTimestamp && matchesThread && matchesType; + }, + async getParams() { + return { + params: { + limit: 100, + }, + }; + }, + async processResults(after, params) { + const createdAfter = after || this.getLastCreatedAt(); + + if (this.threadId) { + // If specific thread is provided, get messages from that thread + const messages = await this.hubspot.getConversationMessages({ + threadId: this.threadId, + ...params, + }); + + const relevantMessages = messages.results?.filter(msg => + this.isRelevant(msg, createdAfter) + ) || []; + + this.processEvents(relevantMessages); + } else { + // Note: HubSpot Conversations API doesn't provide a direct way to list all threads + // This would require HubSpot webhooks or a different approach + console.log("Thread-specific monitoring recommended - provide threadId prop for best results"); + } + }, + }, +}; \ No newline at end of file