From dda1ca336427e9ac1843cacd4c465a2aceccd02b Mon Sep 17 00:00:00 2001 From: MarconLP <13001502+MarconLP@users.noreply.github.com> Date: Thu, 2 Jan 2025 18:25:03 +0100 Subject: [PATCH] add oauth authentication and update contacts template --- .../src/lib/integrations/integrationsLogic.ts | 2 + frontend/src/types.ts | 1 + posthog/cdp/templates/__init__.py | 7 +- .../templates/intercom/template_intercom.py | 288 +++++++++++++++--- posthog/models/integration.py | 18 +- posthog/settings/integrations.py | 3 + 6 files changed, 272 insertions(+), 47 deletions(-) diff --git a/frontend/src/lib/integrations/integrationsLogic.ts b/frontend/src/lib/integrations/integrationsLogic.ts index 3e83f31c017c3..c9f291ad4ac65 100644 --- a/frontend/src/lib/integrations/integrationsLogic.ts +++ b/frontend/src/lib/integrations/integrationsLogic.ts @@ -8,6 +8,7 @@ import IconGoogleAds from 'public/services/google-ads.png' import IconGoogleCloud from 'public/services/google-cloud.png' import IconGoogleCloudStorage from 'public/services/google-cloud-storage.png' import IconHubspot from 'public/services/hubspot.png' +import IconIntercom from 'public/services/intercom.png' import IconSalesforce from 'public/services/salesforce.png' import IconSlack from 'public/services/slack.png' import IconSnapchat from 'public/services/snapchat.png' @@ -26,6 +27,7 @@ const ICONS: Record = { 'google-cloud-storage': IconGoogleCloudStorage, 'google-ads': IconGoogleAds, snapchat: IconSnapchat, + intercom: IconIntercom, } export const integrationsLogic = kea([ diff --git a/frontend/src/types.ts b/frontend/src/types.ts index be7dea47a8302..0232acc69dc1f 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -3763,6 +3763,7 @@ export type IntegrationKind = | 'google-cloud-storage' | 'google-ads' | 'snapchat' + | 'intercom' export interface IntegrationType { id: number diff --git a/posthog/cdp/templates/__init__.py b/posthog/cdp/templates/__init__.py index 6d4a24e6d1a27..c6dc494334c5f 100644 --- a/posthog/cdp/templates/__init__.py +++ b/posthog/cdp/templates/__init__.py @@ -4,7 +4,11 @@ from .hubspot.template_hubspot import template_event as hubspot_event, template as hubspot, TemplateHubspotMigrator from .braze.template_braze import template as braze from .customerio.template_customerio import template as customerio, TemplateCustomerioMigrator -from .intercom.template_intercom import template as intercom, TemplateIntercomMigrator +from .intercom.template_intercom import ( + template as intercom, + template_send_event as intercom_send_event, + TemplateIntercomMigrator, +) from .sendgrid.template_sendgrid import template as sendgrid, TemplateSendGridMigrator from .clearbit.template_clearbit import template as clearbit from .june.template_june import template as june @@ -77,6 +81,7 @@ hubspot, hubspot_event, intercom, + intercom_send_event, june, klaviyo_event, klaviyo_user, diff --git a/posthog/cdp/templates/intercom/template_intercom.py b/posthog/cdp/templates/intercom/template_intercom.py index 069c231829231..4eedc2fbf4089 100644 --- a/posthog/cdp/templates/intercom/template_intercom.py +++ b/posthog/cdp/templates/intercom/template_intercom.py @@ -2,74 +2,230 @@ import dataclasses from posthog.cdp.templates.hog_function_template import HogFunctionTemplate, HogFunctionTemplateMigrator - template: HogFunctionTemplate = HogFunctionTemplate( status="beta", type="destination", - id="template-Intercom", + id="template-intercom", name="Intercom", - description="Send events and contact information to Intercom", + description="Update contacts in Intercom", icon_url="/static/services/intercom.png", category=["Customer Success"], hog=""" if (empty(inputs.email)) { - print('`email` input is empty. Skipping.') + print('No email set. Skipping...') return } -let res := fetch(f'https://{inputs.host}/events', { - 'method': 'POST', - 'headers': { - 'Authorization': f'Bearer {inputs.access_token}', - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - 'body': { - 'event_name': event.event, - 'created_at': toInt(toUnixTimestamp(toDateTime(event.timestamp))), - 'email': inputs.email, - 'id': event.distinct_id, - } +let regions := { + 'US': 'api.intercom.io', + 'EU': 'api.eu.intercom.io', + 'AU': 'api.au.intercom.io', +} + +let user := fetch(f'https://{regions[inputs.oauth['app.region']]}/contacts/search', { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Intercom-Version': '2.11', + 'Accept': 'application/json', + 'Authorization': f'Bearer {inputs.oauth.access_token}', + }, + 'body': { + 'query': { + 'field': 'email', + 'operator': '=', + 'value': inputs.email + } + } }) -if (res.status >= 200 and res.status < 300) { - print('Event sent successfully!') - return +let payload := { + 'email': inputs.email } -if (res.status == 404) { - throw Error('No existing contact found for email') - return +if (inputs.include_all_properties) { + for (let key, value in person.properties) { + if (not empty(value) and not key like '$%') { + payload[key] := value + } + } } -throw Error(f'Error from intercom api (status {res.status}): {res.body}') +for (let key, value in inputs.properties) { + if (not empty(value)) { + payload[key] := value + } +} + +let res + +if (user.body.total_count == 1) { + res := fetch(f'https://{regions[inputs.oauth['app.region']]}/contacts/{user.body.data.1.id}', { + 'method': 'PUT', + 'headers': { + 'Content-Type': 'application/json', + 'Intercom-Version': '2.11', + 'Accept': 'application/json', + 'Authorization': f'Bearer {inputs.oauth.access_token}', + }, + 'body': payload + }) +} else if (user.body.total_count == 0) { + res := fetch(f'https://{regions[inputs.oauth['app.region']]}/contacts', { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Intercom-Version': '2.11', + 'Accept': 'application/json', + 'Authorization': f'Bearer {inputs.oauth.access_token}', + }, + 'body': payload + }) +} else { + throw Error('Found multiple contacts with the same email address. Skipping...') +} +if (res.status >= 400) { + throw Error(f'Error from intercom api (status {res.status}): {res.body}') +} else if (user.status >= 400) { + throw Error(f'Error from intercom api (status {user.status}): {user.body}') +} """.strip(), inputs_schema=[ { - "key": "access_token", + "key": "oauth", + "type": "integration", + "integration": "intercom", + "label": "Intercom account", + "requiredScopes": "placeholder", # intercom scopes are only configurable in the oauth app settings + "secret": False, + "required": True, + }, + { + "key": "email", "type": "string", - "label": "Intercom access token", - "description": "Create an Intercom app (https://developers.intercom.com/docs/build-an-integration/learn-more/authentication), then go to Configure > Authentication to find your token.", - "secret": True, + "label": "Email of the user", + "description": "Where to find the email of the user.", + "default": "{person.properties.email}", + "secret": False, "required": True, }, { - "key": "host", - "type": "choice", - "choices": [ - { - "label": "US (api.intercom.io)", - "value": "api.intercom.io", - }, - { - "label": "EU (api.eu.intercom.com)", - "value": "api.eu.intercom.com", - }, - ], - "label": "Data region", - "description": "Use the EU variant if your Intercom account is based in the EU region", - "default": "api.intercom.io", + "key": "include_all_properties", + "type": "boolean", + "label": "Include all properties as attributes", + "description": "If set, all person properties will be included. Individual attributes can be overridden below.", + "default": False, + "secret": False, + "required": True, + }, + { + "key": "properties", + "type": "dictionary", + "label": "Property mapping", + "description": "Map of Intercom properties and their values.", + "default": { + "name": "{f'{person.properties.first_name} {person.properties.last_name}' == ' ' ? null : f'{person.properties.first_name} {person.properties.last_name}'}", + "phone": "{person.properties.phone}", + "last_seen_at": "{toUnixTimestamp(event.timestamp)}", + }, + "secret": False, + "required": False, + }, + ], + filters={ + "events": [ + {"id": "$identify", "name": "$identify", "type": "events", "order": 0}, + {"id": "$set", "name": "$set", "type": "events", "order": 1}, + ], + "actions": [], + "filter_test_accounts": True, + }, +) + +template_send_event: HogFunctionTemplate = HogFunctionTemplate( + status="beta", + type="destination", + id="template-intercom-event", + name="Intercom", + description="Send events to Intercom", + icon_url="/static/services/intercom.png", + category=["Customer Success"], + hog=""" +if (empty(inputs.email)) { + print('No email set. Skipping...') + return +} + +let regions := { + 'US': 'api.intercom.io', + 'EU': 'api.eu.intercom.io', + 'AU': 'api.au.intercom.io', +} + +let user := fetch(f'https://{regions[inputs.oauth['app.region']]}/contacts/search', { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'Intercom-Version': '2.11', + 'Accept': 'application/json', + 'Authorization': f'Bearer {inputs.oauth.access_token}', + }, + 'body': { + 'query': { + 'field': 'email', + 'operator': '=', + 'value': inputs.email + } + } +}) + +let payload := { + 'email': inputs.email +} + +if (inputs.include_all_properties) { + for (let key, value in person.properties) { + if (not empty(value) and not key like '$%') { + payload[key] := value + } + } +} + +for (let key, value in inputs.properties) { + if (not empty(value)) { + payload[key] := value + } +} + +let res + +if (user.body.total_count == 1) { + res := fetch(f'https://{regions[inputs.oauth['app.region']]}/contacts/{user.body.data.1.id}', { + 'method': 'PUT', + 'headers': { + 'Content-Type': 'application/json', + 'Intercom-Version': '2.11', + 'Accept': 'application/json', + 'Authorization': f'Bearer {inputs.oauth.access_token}', + }, + 'body': payload + }) +} + +if (res.status >= 400) { + throw Error(f'Error from intercom api (status {res.status}): {res.body}') +} else if (user.status >= 400) { + throw Error(f'Error from intercom api (status {user.status}): {user.body}') +} +""".strip(), + inputs_schema=[ + { + "key": "oauth", + "type": "integration", + "integration": "intercom", + "label": "Intercom account", + "requiredScopes": "placeholder", # intercom scopes are only configurable in the oauth app settings "secret": False, "required": True, }, @@ -77,14 +233,56 @@ "key": "email", "type": "string", "label": "Email of the user", - "description": "Where to find the email for the contact to be created. You can use the filters section to filter out unwanted emails or internal users.", + "description": "Where to find the email of the user.", "default": "{person.properties.email}", "secret": False, "required": True, }, + { + "key": "eventName", + "type": "string", + "label": "Event name", + "description": "A standard event or custom event name.", + "default": "{event.event}", + "secret": False, + "required": True, + }, + { + "key": "eventTime", + "type": "string", + "label": "Event time", + "description": "A Unix timestamp in seconds indicating when the actual event occurred.", + "default": "{toInt(toUnixTimestamp(event.timestamp))}", + "secret": False, + "required": True, + }, + { + "key": "include_all_properties", + "type": "boolean", + "label": "Include all properties as attributes", + "description": "If set, all person properties will be included. Individual attributes can be overridden below.", + "default": False, + "secret": False, + "required": True, + }, + { + "key": "properties", + "type": "dictionary", + "label": "Property mapping", + "description": "Map of Intercom properties and their values. You can use the filters section to filter out unwanted events.", + "default": { + "name": "{f'{person.properties.first_name} {person.properties.last_name}' == ' ' ? null : f'{person.properties.first_name} {person.properties.last_name}'}", + "phone": "{person.properties.phone}", + "last_seen_at": "{toUnixTimestamp(event.timestamp)}", + }, + "secret": False, + "required": False, + }, ], filters={ - "events": [{"id": "$identify", "name": "$identify", "type": "events", "order": 0}], + "events": [ + {"id": "$pageview", "name": "$pageview", "type": "events", "order": 0}, + ], "actions": [], "filter_test_accounts": True, }, diff --git a/posthog/models/integration.py b/posthog/models/integration.py index 3aaccba1875fe..62c2fbc91c9c5 100644 --- a/posthog/models/integration.py +++ b/posthog/models/integration.py @@ -49,6 +49,7 @@ class IntegrationKind(models.TextChoices): GOOGLE_CLOUD_STORAGE = "google-cloud-storage" GOOGLE_ADS = "google-ads" SNAPCHAT = "snapchat" + INTERCOM = "intercom" team = models.ForeignKey("Team", on_delete=models.CASCADE) @@ -115,7 +116,7 @@ class OauthConfig: class OauthIntegration: - supported_kinds = ["slack", "salesforce", "hubspot", "google-ads", "snapchat"] + supported_kinds = ["slack", "salesforce", "hubspot", "google-ads", "snapchat", "intercom"] integration: Integration def __init__(self, integration: Integration) -> None: @@ -209,6 +210,21 @@ def oauth_config_for_kind(cls, kind: str) -> OauthConfig: id_path="me.id", name_path="me.email", ) + elif kind == "intercom": + if not settings.INTERCOM_APP_CLIENT_ID or not settings.INTERCOM_APP_CLIENT_SECRET: + raise NotImplementedError("Intercom app not configured") + + return OauthConfig( + authorize_url="https://app.intercom.com/oauth", + token_url="https://api.intercom.io/auth/eagle/token", + token_info_url="https://api.intercom.io/me", + token_info_config_fields=["id", "email", "app.region"], + client_id=settings.INTERCOM_APP_CLIENT_ID, + client_secret=settings.INTERCOM_APP_CLIENT_SECRET, + scope="", + id_path="id", + name_path="email", + ) raise NotImplementedError(f"Oauth config for kind {kind} not implemented") diff --git a/posthog/settings/integrations.py b/posthog/settings/integrations.py index 2afb819ef91c5..ed7abb0f70034 100644 --- a/posthog/settings/integrations.py +++ b/posthog/settings/integrations.py @@ -6,5 +6,8 @@ SNAPCHAT_APP_CLIENT_ID = get_from_env("SNAPCHAT_APP_CLIENT_ID", "") SNAPCHAT_APP_CLIENT_SECRET = get_from_env("SNAPCHAT_APP_CLIENT_SECRET", "") +INTERCOM_APP_CLIENT_ID = get_from_env("INTERCOM_APP_CLIENT_ID", "") +INTERCOM_APP_CLIENT_SECRET = get_from_env("INTERCOM_APP_CLIENT_SECRET", "") + SALESFORCE_CONSUMER_KEY = get_from_env("SALESFORCE_CONSUMER_KEY", "") SALESFORCE_CONSUMER_SECRET = get_from_env("SALESFORCE_CONSUMER_SECRET", "")