-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Add HubSpot conversation threads with internal notes support #17607
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"); | ||
} | ||
}, | ||
Comment on lines
+48
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainIncomplete implementation for non-thread-specific monitoring. The Consider implementing a fallback mechanism or providing clearer documentation about the threadId requirement: } 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");
+ // Note: HubSpot Conversations API doesn't provide a direct way to list all threads
+ // This would require HubSpot webhooks or a different approach
+ throw new Error("threadId is required for conversation comment monitoring due to HubSpot API limitations");
} Alternatively, if the component is designed to work only with threadId, consider making it a required prop instead of optional. Let me verify how other similar HubSpot components handle this scenario: 🏁 Script executed: #!/bin/bash
# Description: Check how other HubSpot conversation components handle optional threadId
# Expected: Find similar patterns or required threadId usage
# Search for similar threadId usage patterns in HubSpot components
rg -A 10 -B 5 "threadId.*optional" components/hubspot/
# Search for other conversation-related components
fd -e mjs -p "conversation" components/hubspot/ Length of output: 430 I’ll gather how the sibling source handles 🏁 Script executed: #!/bin/bash
# 1. Inspect threadId prop definition in new-conversation-comment
rg -A 5 -B 5 "threadId" components/hubspot/sources/new-conversation-comment/new-conversation-comment.mjs
# 2. Show processResults in the sibling new-conversation-message source
rg -A 20 -B 5 "async processResults" components/hubspot/sources/new-conversation-message/new-conversation-message.mjs Length of output: 2651 Consistent threadId requirement across HubSpot conversation sources Both Please choose one of the following to make the contract clearer: • Make Locations to update:
🤖 Prompt for AI Agents
|
||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"); | ||
} | ||
}, | ||
}, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Critical issue: Missing API parameter in conversation methods
All the new conversation methods are missing the
api
parameter when callingmakeRequest
. Looking at the existing pattern in this file, other methods specify an API path likeAPI_PATH.CRMV3
.Without the
api
parameter, themakeRequest
method will construct URLs like${BASE_URL}undefined${endpoint}
, which is incorrect.You need to add the appropriate API path parameter to all four conversation methods. For example:
getConversationThread({ threadId, ...opts }) { return this.makeRequest({ + api: API_PATH.CONVERSATIONS, // or appropriate API path constant endpoint: `/conversations/v3/conversations/threads/${threadId}`, ...opts, }); },
Run this script to verify the API path constant that should be used:
🏁 Script executed:
Length of output: 64
To identify the correct API path constant and how it’s used in this file, let’s inspect imports, existing
makeRequest
calls, and any conversation-related constants inhubspot.app.mjs
:🏁 Script executed:
Length of output: 14117
To confirm which API paths are defined, let’s locate and inspect the file that defines
API_PATH
:🏁 Script executed:
Length of output: 5109
Let’s pull in the HubSpot API_PATH definitions to see which constant to use:
🏁 Script executed:
Length of output: 1354
Add
api
parameter and newCONVERSATIONS
path constant for conversation methodsThe four conversation methods currently call
without an
api
field, somakeRequest
will prependundefined
to the URL. There is no existingAPI_PATH.CONVERSATIONS
constant—so:• In
components/hubspot/common/constants.mjs
, add to theAPI_PATH
object:• In
components/hubspot/hubspot.app.mjs
, update each method to includeapi: API_PATH.CONVERSATIONS
and remove the duplicated version prefix in the endpoint. For example:Repeat for
getConversationMessages
,sendConversationMessage
, andaddConversationComment
.This ensures URLs are constructed as:
<BASE_URL>/conversations/v3/conversations/threads/{threadId}
.🤖 Prompt for AI Agents