-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
555 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,50 @@ | ||
[ | ||
{ | ||
"sourceKeys": "event.type", | ||
"destKeys": "event" | ||
}, | ||
{ | ||
"sourceKeys": "event.user.tz", | ||
"destKeys": "timezone" | ||
}, | ||
{ | ||
"sourceKeys": "event.user.profile.email", | ||
"destKeys": "context.traits.email" | ||
}, | ||
{ | ||
"sourceKeys": "event.user.profile.phone", | ||
"destKeys": "context.traits.phone" | ||
}, | ||
{ | ||
"sourceKeys": "event.user.profile.real_name_normalized", | ||
"destKeys": "context.traits.name" | ||
}, | ||
{ | ||
"sourceKeys": "event.user.profile.real_name", | ||
"destKeys": "context.traits.name" | ||
}, | ||
{ | ||
"sourceKeys": "event.user.profile.display_name_normalized", | ||
"destKeys": "context.traits.name" | ||
}, | ||
{ | ||
"sourceKeys": "event.user.profile.display_name", | ||
"destKeys": "context.traits.name" | ||
}, | ||
{ | ||
"sourceKeys": "event.user.profile.first_name", | ||
"destKeys": "context.traits.firstName" | ||
}, | ||
{ | ||
"sourceKeys": "event.user.profile.last_name", | ||
"destKeys": "context.traits.lastName" | ||
}, | ||
{ | ||
"sourceKeys": "event.user.profile.image_original", | ||
"destKeys": "context.traits.avatar" | ||
}, | ||
{ | ||
"sourceKeys": "event.user.profile.title", | ||
"destKeys": "context.traits.title" | ||
} | ||
] |
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,110 @@ | ||
const sha256 = require('sha256'); | ||
const { TransformationError } = require('@rudderstack/integrations-lib'); | ||
const Message = require('../message'); | ||
const { mapping, tsToISODate, normalizeEventName } = require('./util'); | ||
const { generateUUID, removeUndefinedAndNullValues } = require('../../util'); | ||
const { JSON_MIME_TYPE } = require('../../util/constant'); | ||
const { EventType } = require('../../../constants'); | ||
|
||
/** | ||
* Transform event data to RudderStack supported standard event schema | ||
* @param {Object} slackPayload - The complete data received on the webhook from Slack | ||
* @param {Object} slackPayload.event - The data object specific to the Slack event received. Has different schema for different event types. | ||
* @returns {Object} Event data transformed to RudderStack supported standard event schema | ||
*/ | ||
function processNormalEvent(slackPayload) { | ||
const message = new Message(`SLACK`); | ||
if (!slackPayload?.event) { | ||
throw new TransformationError('Missing the required event data'); | ||
} | ||
switch (slackPayload.event.type) { | ||
case 'team_join': | ||
message.setEventType(EventType.IDENTIFY); | ||
break; | ||
case 'user_change': | ||
message.setEventType(EventType.IDENTIFY); | ||
break; | ||
default: | ||
message.setEventType(EventType.TRACK); | ||
break; | ||
} | ||
message.setEventName(normalizeEventName(slackPayload.event.type)); | ||
if (!slackPayload.event.user) { | ||
throw new TransformationError('UserId not found'); | ||
} | ||
const stringifiedUserId = | ||
typeof slackPayload.event.user === 'object' | ||
? slackPayload.event.user.id | ||
: slackPayload.event.user; | ||
message.setProperty( | ||
'anonymousId', | ||
stringifiedUserId ? sha256(stringifiedUserId).toString().substring(0, 36) : generateUUID(), | ||
); | ||
// Set the user id received from Slack into externalId | ||
message.context.externalId = [ | ||
{ | ||
type: 'slackUserId', | ||
id: stringifiedUserId, | ||
}, | ||
]; | ||
// Set the standard common event fields. More info at https://www.rudderstack.com/docs/event-spec/standard-events/common-fields/ | ||
// originalTimestamp - The actual time (in UTC) when the event occurred | ||
message.setProperty( | ||
'originalTimestamp', | ||
tsToISODate(slackPayload.event.ts || slackPayload.event.event_ts || slackPayload.event_time), | ||
); | ||
// sentAt - Time, client-side, when the event was sent from the client to RudderStack | ||
message.setProperty('sentAt', tsToISODate(slackPayload.event_time)); | ||
// Map the remaining standard event properties according to mappings for the payload properties | ||
message.setPropertiesV2(slackPayload, mapping); | ||
// Copy the complete Slack event payload to message.properties | ||
if (!message.properties) message.properties = {}; | ||
Object.assign(message.properties, slackPayload.event); | ||
return message; | ||
} | ||
|
||
/** | ||
* Handles a special event for webhook url verification. | ||
* Responds back with the challenge key received in the request. | ||
* Reference - https://api.slack.com/apis/connections/events-api#subscribing | ||
* @param {Object} event - Event data received from Slack | ||
* @param {string} event.challenge - The challenge key received in the request | ||
* @returns response that needs to be sent back to the source, alongwith the same challenge key received int the request | ||
*/ | ||
function processUrlVerificationEvent(event) { | ||
const response = { challenge: event?.challenge }; | ||
return { | ||
outputToSource: { | ||
body: Buffer.from(JSON.stringify(response)).toString('base64'), | ||
contentType: JSON_MIME_TYPE, | ||
}, | ||
statusCode: 200, | ||
}; | ||
} | ||
|
||
/** | ||
* Checks if the event is a special url verification event or not. | ||
* Slack sends this event at the time of webhook setup to verify webhook url ownership for the security purpose. | ||
* Reference - https://api.slack.com/apis/connections/events-api#subscribing | ||
* @param {Object} event - Event data received from Slack | ||
* @param {string} event.challenge - The challenge key received in the request | ||
* @param {string} event.type - The type of Slack event. `url_verification` when it is a special webhook url verification event. | ||
* @returns {boolean} true if it is a valid challenge event for url verification event | ||
*/ | ||
function isWebhookUrlVerificationEvent(event) { | ||
return event?.type === 'url_verification' && !!event?.challenge; | ||
} | ||
|
||
/** | ||
* Processes the event with needed transformation and sends back the response | ||
* Reference - https://api.slack.com/apis/connections/events-api | ||
* @param {Object} event | ||
*/ | ||
function process(event) { | ||
const response = isWebhookUrlVerificationEvent(event) | ||
? processUrlVerificationEvent(event) | ||
: processNormalEvent(event); | ||
return removeUndefinedAndNullValues(response); | ||
} | ||
|
||
exports.process = process; |
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,62 @@ | ||
/* eslint-disable no-restricted-syntax */ | ||
const path = require('path'); | ||
const fs = require('fs'); | ||
|
||
const mapping = JSON.parse(fs.readFileSync(path.resolve(__dirname, './mapping.json'), 'utf-8')); | ||
|
||
/** | ||
* Converts a Slack timestamp to RudderStack's standard timestamp format - ISO 8601 date string. | ||
* The Slack timestamp is a string that represents unix timestamp (seconds since the Unix Epoch) | ||
* with fractional seconds for millisecond precision. | ||
* If the timestamp is not provided, the function returns the current date and time in ISO 8601 format. | ||
* | ||
* @param {string} [slackTs] - The Slack timestamp to be converted. | ||
* @returns {string} The ISO 8601 formatted date string corresponding to the given Slack timestamp | ||
* or the current date and time if no timestamp is provided. | ||
* | ||
* @example | ||
* // Convert a Slack timestamp to an ISO 8601 date string | ||
* const slackTimestamp = "1609459200.123000"; | ||
* const isoDate = tsToISODate(slackTimestamp); | ||
* console.log(isoDate); // Output: "2021-01-01T00:00:00.123Z" (depending on your timezone) | ||
*/ | ||
function tsToISODate(slackTs) { | ||
// Default to current date if slackTs is not provided | ||
if (!slackTs) return new Date().toISOString(); | ||
|
||
// Convert slackTs string into unix timestamp in milliseconds | ||
const msTimestamp = parseFloat(slackTs) * 1000; | ||
// Convert to a date object | ||
if (Number.isNaN(msTimestamp)) { | ||
// If timestamp was not a valid float, the parser will return NaN, stop processing the timestamp further and return null | ||
return null; | ||
} | ||
const date = new Date(msTimestamp); | ||
|
||
// Return the date in ISO 8601 format | ||
return date.toISOString(); | ||
} | ||
|
||
/** | ||
* Converts an event name from snake_case to a RudderStack format - space-separated string with each word capitalized. | ||
* @param {string} evtName - The event name in snake_case format to be normalized. | ||
* @returns {string} The normalized event name with spaces between words and each word capitalized. | ||
* | ||
* @example | ||
* // Convert a slack event name to RudderStack format | ||
* const eventName = "member_joined_channel"; | ||
* const normalizedEventName = normalizeEventName(eventName); | ||
* console.log(normalizedEventName); // Output: "Member Joined Channel" | ||
*/ | ||
function normalizeEventName(evtName) { | ||
try { | ||
return evtName | ||
.split('_') | ||
.map((s) => s.charAt(0).toUpperCase() + s.slice(1)) | ||
.join(' '); | ||
} catch (e) { | ||
return 'undefined'; | ||
} | ||
} | ||
|
||
module.exports = { mapping, tsToISODate, normalizeEventName }; |
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,51 @@ | ||
const { tsToISODate, normalizeEventName } = require('./util.js'); | ||
|
||
describe('Unit test cases for tsToISODate', () => { | ||
it('should return a valid iso date string for a valid slack timestamp input', () => { | ||
const result = tsToISODate('1609459200.123000'); | ||
expect(result).toBe('2021-01-01T00:00:00.123Z'); | ||
}); | ||
|
||
it('should return iso date string of today when slack timestamp argument is not provided', () => { | ||
const result = tsToISODate(); | ||
expect(result).not.toBeNull(); | ||
expect(typeof result).toBe('string'); | ||
expect(result).not.toHaveLength(0); | ||
// Check if the result is a valid date | ||
const dateObject = new Date(result); | ||
const resultTime = dateObject.getTime(); | ||
expect(resultTime).not.toBeNaN(); | ||
// Check if the result is close to the current time with precision tolerance of upto a minute | ||
const nowTime = new Date().getTime(); | ||
const TOLERANCE = 60000; // In ms | ||
const timeDiff = Math.abs(nowTime - resultTime); | ||
expect(timeDiff).toBeLessThanOrEqual(TOLERANCE); | ||
}); | ||
|
||
it('should return null if the slack timestamp argument is invalid', () => { | ||
const result = tsToISODate('invalid.slack.timestamp'); | ||
expect(result).toBeNull(); | ||
}); | ||
}); | ||
|
||
describe('Unit test cases for normalizeEventName', () => { | ||
it('should normalize a valid snake case string "member_joined_channel" to RudderStack format "Member Joined Channel"', () => { | ||
const result = normalizeEventName('member_joined_channel'); | ||
expect(result).toBe('Member Joined Channel'); | ||
}); | ||
|
||
it('should return undefined string when event name is undefined', () => { | ||
const result = normalizeEventName(undefined); | ||
expect(result).toBe('undefined'); | ||
}); | ||
|
||
it('should return undefined string when event name is null', () => { | ||
const result = normalizeEventName(null); | ||
expect(result).toBe('undefined'); | ||
}); | ||
|
||
it('should return undefined string when event name argument cannot be parsed to string', () => { | ||
const result = normalizeEventName({}); | ||
expect(result).toBe('undefined'); | ||
}); | ||
}); |
Oops, something went wrong.