-
Notifications
You must be signed in to change notification settings - Fork 428
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
6 changed files
with
326 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
--- | ||
title: 'Intercom API Integration Template' | ||
sidebarTitle: 'Intercom' | ||
--- | ||
|
||
## Get started with the Intercom template | ||
|
||
<Card title="How to use integration templates" | ||
href="/integration-templates/overview#how-to-use-integration-templates" | ||
icon="book-open"> | ||
Learn how to use integration templates in Nango | ||
</Card> | ||
|
||
<Card title="Get the Intercom template" | ||
href="https://github.com/NangoHQ/nango/tree/master/integration-templates/intercom" | ||
icon="github"> | ||
Get the latest version of the Intercom integration template from GitHub | ||
</Card> | ||
|
||
## Need help with the template? | ||
Please reach out on the [Slack community](https://nango.dev/slack), we are very active there and happy to help! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import type { NangoSync, IntercomContact } from './models'; | ||
|
||
export default async function fetchData(nango: NangoSync): Promise<{ IntercomContact: IntercomContact[] }> { | ||
// Get the list of contacts | ||
// As of 2023-08-02 the "per_page" parameter is not documented but works | ||
// https://developers.intercom.com/intercom-api-reference/reference/listcontacts | ||
let finished = false; | ||
let nextPage = ''; | ||
while (!finished) { | ||
// This API endpoint has an annoying bug: If you pass "starting_after" with no value you get a 500 server error | ||
// Because of this we only set it here when we are fetching page >= 2, otherwise we don't pass it. | ||
let queryParams: Record<string, string> = { | ||
per_page: '150' | ||
}; | ||
|
||
if (nextPage !== '') { | ||
queryParams['starting_after'] = nextPage; | ||
} | ||
|
||
// Make the API request with Nango | ||
const resp = await nango.get({ | ||
baseUrlOverride: 'https://api.intercom.io/', | ||
endpoint: 'contacts', | ||
retries: 5, | ||
headers: { | ||
'Intercom-Version': '2.9' | ||
}, | ||
params: queryParams | ||
}); | ||
|
||
let contacts: any[] = resp.data.data; | ||
let mappedContacts: IntercomContact[] = contacts.map((contact) => ({ | ||
id: contact.id, | ||
workspace_id: contact.workspace_id, | ||
external_id: contact.external_id, | ||
type: contact.role, | ||
email: contact.email, | ||
phone: contact.phone, | ||
name: contact.name, | ||
created_at: new Date(contact.created_at * 1000), | ||
updated_at: new Date(contact.updated_at * 1000), | ||
last_seen_at: contact.last_seen_at ? new Date(contact.last_seen_at * 1000) : null, | ||
last_replied_at: contact.last_replied_at ? new Date(contact.last_replied_at * 1000) : null | ||
})); | ||
|
||
// Store this page of conversations in Nango | ||
await nango.batchSend(mappedContacts, 'IntercomContact'); | ||
|
||
// Are there more pages? | ||
// If so, set nextPage to the cursor of the next page | ||
if (resp.data.pages.next) { | ||
nextPage = resp.data.pages.next.starting_after; | ||
} else { | ||
finished = true; | ||
} | ||
} | ||
|
||
return { IntercomContact: [] }; | ||
} |
190 changes: 190 additions & 0 deletions
190
integration-templates/intercom/intercom-conversations.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
import { NangoSync, IntercomConversation, IntercomConversationMessage } from './models'; | ||
|
||
/** | ||
* Fetches Intercom conversations with all their associated messages and notes. | ||
* | ||
* Note that Intercom has a hard limit of 500 message parts (messages/notes/actions etc.) returned per conversation. | ||
* If a conversation has more than 500 parts some will be missing. | ||
* Only fetches parts that have a message body, ignores parts which are pure actions & metadata (e.g. closed conversation). | ||
* | ||
* ==== | ||
* | ||
* Initial sync: Fetches conversations updated in the last X years (default: X=2) | ||
* Incremential sync: Fetches the conversations that have been updates since the last sync (updated_at date from Intercom, seems to be reliable) | ||
*/ | ||
|
||
export default async function fetchData( | ||
nango: NangoSync | ||
): Promise<{ IntercomConversation: IntercomConversation[]; IntercomConversationMessage: IntercomConversationMessage[] }> { | ||
// Intercom uses unix timestamp for datetimes. | ||
// Convert the last sync run date into a unix timestamp for easier comparison. | ||
const lastSyncDateTimestamp = nango.lastSyncDate ? nango.lastSyncDate.getTime() / 1000 : 0; | ||
|
||
// We also define a max sync date for incremential syncs, which is conversations updated in the last X years | ||
const maxYearsToSync = 2; | ||
const maxSyncDate = new Date(); | ||
maxSyncDate.setFullYear(new Date().getFullYear() - maxYearsToSync); | ||
const maxSyncDateTimestamp = maxSyncDate.getTime() / 1000; | ||
|
||
// Get the list of conversations | ||
// Not documented, but from testing it seems the list is sorted by updated_at DESC | ||
// https://developers.intercom.com/intercom-api-reference/reference/listconversations | ||
let finished = false; | ||
let nextPage = ''; | ||
while (!finished) { | ||
// This API endpoint has an annoying bug: If you pass "starting_after" with no value you get a 500 server error | ||
// Because of this we only set it here when we are fetching page >= 2, otherwise we don't pass it. | ||
let queryParams: Record<string, string> = { | ||
per_page: '150' | ||
}; | ||
|
||
if (nextPage !== '') { | ||
queryParams['starting_after'] = nextPage; | ||
} | ||
|
||
// Make the API request with Nango | ||
const resp = await nango.get({ | ||
baseUrlOverride: 'https://api.intercom.io/', | ||
endpoint: 'conversations', | ||
retries: 5, | ||
headers: { | ||
'Intercom-Version': '2.9' | ||
}, | ||
params: queryParams | ||
}); | ||
|
||
// Let's iterate over the received conversations | ||
// Then get the details for each. | ||
let intercomConversationsPage: IntercomConversation[] = []; | ||
let intercomMessagesPage: IntercomConversationMessage[] = []; | ||
for (let conversation of resp.data.conversations) { | ||
// For incremential syncs: Skip conversations that have not been updated since we last synced | ||
// updated_at is a unix timestamp of the last change to the conversation (e.g. new message from customer, attribute changed) | ||
if (conversation.updated_at < lastSyncDateTimestamp) { | ||
continue; | ||
} | ||
|
||
// Get the details of the conversation | ||
// https://developers.intercom.com/intercom-api-reference/reference/retrieveconversation | ||
const conversationResp = await nango.get({ | ||
baseUrlOverride: 'https://api.intercom.io/', | ||
endpoint: `conversations/${conversation.id}`, | ||
retries: 5, | ||
headers: { | ||
'Intercom-Version': '2.9' | ||
}, | ||
params: { | ||
display_as: 'plaintext' | ||
} | ||
}); | ||
|
||
// Map the Intercom conversation to our own data model | ||
intercomConversationsPage.push({ | ||
id: conversationResp.data.id, | ||
created_at: conversationResp.data.created_at, | ||
updated_at: conversationResp.data.updated_at, | ||
waiting_since: conversationResp.data.waiting_since ? conversationResp.data.waiting_since : null, | ||
snoozed_until: conversationResp.data.snoozed_until ? conversationResp.data.snoozed_until : null, | ||
title: conversationResp.data.title, | ||
contacts: conversationResp.data.contacts.contacts.map((contact: any) => { | ||
return { contact_id: contact.id }; | ||
}), | ||
state: conversationResp.data.state, | ||
open: conversationResp.data.open, | ||
read: conversationResp.data.read, | ||
priority: conversationResp.data.priority | ||
}); | ||
|
||
// Map the messages (called "message parts" by Intercom) | ||
// First message is treated special as the "source" by Intercom | ||
intercomMessagesPage.push({ | ||
id: conversationResp.data.source.id, | ||
conversation_id: conversationResp.data.id, | ||
body: conversationResp.data.source.body, | ||
type: 'comment', | ||
created_at: conversationResp.data.created_at, | ||
updated_at: null, | ||
author: { | ||
type: mapAuthorType(conversationResp.data.source.author.type), | ||
name: conversationResp.data.source.name, | ||
id: conversationResp.data.source.id | ||
} | ||
}); | ||
|
||
for (let conversationPart of conversationResp.data.conversation_parts.conversation_parts) { | ||
// Conversation parts can be messages, notes etc. but also actions, such as "closed conversation", "assigned conversation" etc. | ||
// We only care about the conversation parts where admins and users send a message. | ||
// For a full list of possible part types see here: https://developers.intercom.com/intercom-api-reference/reference/the-conversation-model#conversation-part-types | ||
if (conversationPart.body === null) { | ||
continue; | ||
} | ||
|
||
intercomMessagesPage.push({ | ||
id: conversationPart.id, | ||
conversation_id: conversationResp.data.id, | ||
body: conversationPart.body, | ||
type: mapMessagePartType(conversationPart.part_type), | ||
created_at: conversationPart.created_at, | ||
updated_at: conversationPart.updated_at ? conversationPart.updated_at : null, | ||
author: { | ||
type: mapAuthorType(conversationPart.author.type), | ||
name: conversationPart.author.name, | ||
id: conversationPart.author.id | ||
} | ||
}); | ||
} | ||
} | ||
|
||
// Store this page of conversations in Nango | ||
await nango.batchSend(intercomConversationsPage, 'IntercomConversation'); | ||
await nango.batchSend(intercomMessagesPage, 'IntercomConversationMessage'); | ||
|
||
// Get the last conversation of the page | ||
// We use this to determine if we should keep on syncing further pages | ||
const lastConversation = resp.data.conversations.at(-1); | ||
|
||
// We stop syncing if there are no more pages | ||
if (!resp.data.pages.next) { | ||
finished = true; | ||
} | ||
|
||
// OR one of the following conditions has been reached: | ||
|
||
// 1.) We are in an initial sync (last sync timestamp == 0) and we have reached the maxSyncDate | ||
if (lastSyncDateTimestamp === 0 && lastConversation.updated_at <= maxSyncDateTimestamp) { | ||
finished = true; | ||
} | ||
|
||
// 2.) We are in an incremential sync and the last conversation on the page is older than our last sync date | ||
if (lastSyncDateTimestamp > 0 && lastConversation.updated_at < lastSyncDateTimestamp) { | ||
finished = true; | ||
} | ||
|
||
// None of the above is true, let's fetch the next page | ||
if (!finished) { | ||
nextPage = resp.data.pages.next.starting_after; | ||
} | ||
} | ||
|
||
return { IntercomConversation: [], IntercomConversationMessage: [] }; | ||
} | ||
|
||
function mapMessagePartType(rawType: string): string { | ||
if (rawType === 'assignment') { | ||
return 'comment'; | ||
} else { | ||
// Other options with body I have seen: "comment", "note" | ||
return rawType; | ||
} | ||
} | ||
|
||
function mapAuthorType(rawType: string): string { | ||
if (rawType === 'team') { | ||
return 'admin'; | ||
} else if (rawType === 'lead') { | ||
return 'user'; | ||
} else { | ||
// Other options are: "admin", "bot", "user" | ||
return rawType; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
integrations: | ||
intercom: | ||
intercom-conversations: | ||
runs: every 6 hours | ||
returns: | ||
- IntercomConversation | ||
- IntercomConversationMessage | ||
|
||
intercom-contacts: | ||
runs: every 6 hours | ||
returns: | ||
- IntercomContact | ||
|
||
models: | ||
IntercomContact: | ||
id: string | ||
workspace_id: string | ||
external_id: string # user chosen identifier or randomly generated by intercom for leads | ||
type: string # "lead" or "user" | ||
email: string | null | ||
phone: string | null | ||
name: string | null | ||
created_at: date | ||
updated_at: date | ||
last_seen_at: date | null | ||
last_replied_at: date | null | ||
|
||
IntercomConversation: | ||
id: string | ||
created_at: date | ||
updated_at: date | ||
waiting_since: date | null | ||
snoozed_until: date | null | ||
title: string | ||
contacts: | ||
- contact_id: string | ||
state: string | ||
open: boolean | ||
read: boolean | ||
priority: string | ||
|
||
IntercomConversationMessage: | ||
id: string | ||
conversation_id: string | ||
body: string | ||
type: string # "comment" (a message sent to the other person) or "note" (internal note from employee for other employees) | ||
created_at: date | ||
updated_at: date | null | ||
author: | ||
type: string # "admin" (employee of company), "bot" (automated response) or "user" (contact) | ||
name: string | ||
id: string # id of the admin or contact |