Skip to content
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

feat: add slack source #3148

Merged
merged 16 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/v0/sources/slack/mapping.json
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"
}
]
110 changes: 110 additions & 0 deletions src/v0/sources/slack/transform.js
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 || !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;
58 changes: 58 additions & 0 deletions src/v0/sources/slack/util.js
krishna2020 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* 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) {
return evtName
.split('_')
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
.join(' ');
}

module.exports = { mapping, tsToISODate, normalizeEventName };
Loading