Skip to content

Commit

Permalink
[gh-#898] Merge branch 'master' of github.com:NangoHQ/nango into gh-#898
Browse files Browse the repository at this point in the history
-better-metadata-type
  • Loading branch information
khaliqgant committed Aug 9, 2023
2 parents 2abdb7d + fba71a3 commit ac64022
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 0 deletions.
21 changes: 21 additions & 0 deletions docs-v2/integration-templates/intercom.mdx
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!
3 changes: 3 additions & 0 deletions docs-v2/integration-templates/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ Ping us on the [Slack community](https://nango.dev/slack), we continuously expan
<Card title="Google Workspace" href="/integration-templates/google-workspace" icon="google" iconType="brands">
Sync users from Google Workspace
</Card>
<Card title="Intercom" href="/integration-templates/intercom" icon="intercom" iconType="brands">
Sync Intercom conversations, messages and contacts
</Card>
<Card title="Asana" href="/integration-templates/asana" icon="check">
Sync Asana tasks
</Card>
Expand Down
1 change: 1 addition & 0 deletions docs-v2/mint.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"integration-templates/github",
"integration-templates/google-workspace",
"integration-templates/gmail",
"integration-templates/intercom",
"integration-templates/jira",
"integration-templates/linear",
"integration-templates/notion",
Expand Down
59 changes: 59 additions & 0 deletions integration-templates/intercom/intercom-contacts.ts
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 integration-templates/intercom/intercom-conversations.ts
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;
}
}
52 changes: 52 additions & 0 deletions integration-templates/intercom/nango.yaml
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

0 comments on commit ac64022

Please sign in to comment.