Skip to content

[Dotdigital] Additional actions #3018

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

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ export { default as DDContactApi } from './resources/dd-contact-api'
export { default as DDListsApi } from './resources/dd-lists-api'
export { default as DDEnrolmentApi } from './resources/dd-enrolment-api'
export { default as DDDataFieldsApi } from './resources/dd-datafields-api'
export { default as DDCpaasApi } from './resources/dd-cpaas-api'
export { default as DDSmsApi } from './resources/dd-sms-api'
export { default as DDCampaignApi } from './resources/dd-campaign-api'
export { default as DDEmailApi } from './resources/dd-email-api'
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { APIError, DynamicFieldResponse, ModifiedResponse, RequestClient } from '@segment/actions-core'
import type { Settings } from '../../generated-types';
import DDApi from '../dd-api';
import { Campaign, sendCampaignPayload } from '../types'

/**
* Class representing the Dotdigital Campaign API.
* Extends the base Dotdigital API class.
*/
class DDCampaignApi extends DDApi {
constructor(settings: Settings, client: RequestClient) {
super(settings, client);
}

/**
* Gets campaigns with paging.
* @param {number} select - Paging number of records to retrieve
* @param {number} skip - Paging number of records to skip
* @returns {Promise<object>} A promise that resolves to the response of the update operation.
*/
async getCampaignsPaging (select = 1000, skip = 0) {
return await this.get('/v2/campaigns', { select, skip })
}

/**
* Fetches the list of campaigns from Dotdigital API.
*
* @returns A promise that resolves to a DynamicFieldResponse.
*/
async getCampaigns (): Promise<DynamicFieldResponse> {
const choices = []
const select = 200
let skip = 0

let hasMoreData = true;
while (hasMoreData) {
try {
const response: ModifiedResponse = await this.getCampaignsPaging(select, skip);
const content: Campaign[] = JSON.parse(response.content);
if (content.length === 0) {
hasMoreData = false;
break;
} else {
choices.push(...content.map((campaign: Campaign) => ({
value: campaign.id.toString(),
label: campaign.name
})));
skip += select;
}
} catch (error: unknown) {
let errorMessage = 'Unknown error';
let errorCode = 'Unknown error';

if (error instanceof APIError) {
errorMessage = error.message ?? 'Unknown error';
errorCode = error.status ? error.status.toString() : 'Unknown error';
}

return {
choices: [],
nextPage: '',
error: {
message: errorMessage,
code: errorCode
}
};
}
}
return {choices: choices}
}

/**
* Sends an email campaign.
* @param {number} campaignId - The campaign to send.
* @param {number} contactId - The contact to send the campaign to.
* @param {boolean} sendTimeOptimised - Optional flag to send at optimised time.
* @param {string} sendDate - Optional send date.
* @returns A promise that resolves to json.
*/
public async sendCampaign(
campaignId: number,
contactId: number,
sendDate?: string | number | undefined,
sendTimeOptimised?: boolean,
) {

try{
const sendCampaignPayload: sendCampaignPayload = {
campaignID: campaignId,
contactIDs: [contactId],
sendDate: sendDate
}
let endpoint = `/v2/campaigns/send`

if(sendTimeOptimised) {
endpoint = `/v2/campaigns/send-time-optimised`
delete sendCampaignPayload.sendDate
}

const response: ModifiedResponse = await this.post(
endpoint, sendCampaignPayload
);
return response;
} catch (error) {
throw error as APIError ?? 'Failed to send campaign';
}
}
}

export default DDCampaignApi;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ModifiedResponse, RequestClient } from '@segment/actions-core'
import DDApi from '../dd-api'
import { CpaasMessageBody } from '../types'
import type { Settings } from '../../generated-types'

class DDCpaasApi extends DDApi {
constructor(settings: Settings, client: RequestClient) {
super(settings, client);
}

async sendTransactionalSms(body: CpaasMessageBody): Promise<unknown> {
try {
const response: ModifiedResponse = await this.post(
`/cpaas/messages`,
body
)

return response.data
} catch(error) {
throw error as Error ?? 'Failed to send transactional SMS'
}
}
}

export default DDCpaasApi
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { APIError, RequestClient } from '@segment/actions-core'
import type { Settings } from '../../generated-types'
import DDApi from '../dd-api'
import { checkAndCleanEmail, cleanEmails } from '../../helpers/functions'
import type { Payload } from '../../sendTransactionalEmail/generated-types'

/**
* Class representing the Dotdigital Email API.
* Extends the base Dotdigital API class.
*/
class DDEmailApi extends DDApi {
constructor(settings: Settings, client: RequestClient) {
super(settings, client)
}

/**
* Sends a transactional email.
* @param {object} payload - The event payload.
* @returns A promise that resolves to json.
*/
public async sendTransactionalEmail(payload: Payload) {
try {
const toAddresses = cleanEmails(payload.toAddresses)
const ccAddresses = cleanEmails(payload.ccAddresses)
const bccAddresses = cleanEmails(payload.bccAddresses)
const fromAddress = checkAndCleanEmail(payload.fromAddress)
return await this.post('/v2/email', {
ToAddresses: toAddresses,
CCAddresses: ccAddresses,
BCCAddresses: bccAddresses,
Subject: payload.subject,
FromAddress: fromAddress,
HtmlContent: payload.htmlContent,
PlainTextContent: payload.plainTextContent
})
} catch (error) {
throw (error as APIError) ?? 'Failed to send transactional email'
}
}
}

export default DDEmailApi
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { APIError, ModifiedResponse, RequestClient } from '@segment/actions-core';
import type { Settings } from '../../generated-types';
import DDApi from '../dd-api';
import { checkAndCleanMobileNumber } from '../../helpers/functions'

/**
* Class representing the Dotdigital Sms API.
* Extends the base Dotdigital API class.
*/
class DDSmsApi extends DDApi {
constructor(settings: Settings, client: RequestClient) {
super(settings, client);
}

/**
* Enrols a contact into a program.
* @param {string} mobileNumber - The number to send the sms to.
* @param {string} message - The sms message.
* @returns A promise that resolves to json.
*/
public async sendSms(
mobileNumber: string,
message: string
) {

try{
const formattedNumber = checkAndCleanMobileNumber(mobileNumber);
const response: ModifiedResponse = await this.post(
`/v2/sms-messages/send-to/${formattedNumber}`,
{
message: message
}
);
return response;
} catch (error) {
throw error as APIError ?? 'Failed to fetch contact';
}
}
}

export default DDSmsApi;
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
export interface DynamicFieldResponse {
choices: Array<{ value: number; label: string }>;
nextPage?: string;
error?: {
message: string;
code: string;
};
}

export interface List {
id: number
name: string
status: string
}

export interface Campaign {
id: number;
name: string;
}

export interface sendCampaignPayload {
campaignID: number;
contactIDs: [number];
sendDate?: string | number | undefined;
}

export interface DataField {
name: string
type: 'String' | 'Numeric' | 'Date' | 'Boolean'
Expand Down Expand Up @@ -85,3 +106,18 @@ export interface UpsertContactJSON {
lists: number[]
dataFields?: DataFields
}

export interface CpaasMessageBody {
to: {
phoneNumber: string;
};
body: string;
rules: string[];
channelOptions?: {
sms: {
from?: string;
allowUnicode?: boolean;
};
};
shortenLinks?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## What's being changed

Lorem ipsum...

## Why it's being changed

Lorem ipsum...

## How to review / test this change

- Lorem
- ipsum

## Notes

Optional
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const action: ActionDefinition<Settings, Payload> = {
description: `List of active programs`,
type: 'string',
required: true,
disabledInputMethods: ['literal', 'variable', 'function', 'freeform', 'enrichment'],
dynamic: true
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Checks and cleans the provided email.
*
* @function checkAndCleanEmail
* @param {string} email - The email to be checked and cleaned.
* @throws {Error} Will throw an error if the email is not provided or if it's not a string.
* @throws {Error} Will throw an error if the email format is invalid.
* @returns {string|null} - Returns the cleaned email, with all characters in lowercase and extra spaces removed.
*/
const checkAndCleanEmail = (email: string): string => {
const expression = /^[a-zA-Z0-9.!#$%&'*+-=?^_`{|}~]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(\.[a-zA-Z]{2,9})$|^[\w\s]+<\s*[a-zA-Z0-9.!#$%&'*+-=?^_`{|}~]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(\.[a-zA-Z]{2,9})\s*>$/;
const trimmedEmail = email.trim();

if (!expression.test(trimmedEmail)) {
throw new Error(`Invalid email format: ${trimmedEmail}`);
}

return trimmedEmail;
};

/**
* Function to clean email addresses.
* This function takes an array of email addresses as input, removes any falsy values,
* and applies the checkAndCleanEmail function to each email address.
*
* @param {string} emailsString - An array of email addresses.
* @returns {Array} - Returns an array of cleaned email addresses.
*/
const cleanEmails = (emailsString: string | undefined): string[] => {
if(emailsString) {
const emails = emailsString.split(",");

if (typeof emails !== 'undefined' && Array.isArray(emails)) {
return emails
.filter(Boolean)
.map(email => checkAndCleanEmail(email));
}
}
return [];
};

export { checkAndCleanEmail, cleanEmails };
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* This function checks and cleans a mobile number.
* If the mobile number starts with a '+', it removes the '+'.
*
* @param {string} mobileNumber - The mobile number to be checked and cleaned.
* @returns {string} - The cleaned mobile number.
*/
const checkAndCleanMobileNumber = (mobileNumber: string) => {
mobileNumber = mobileNumber.replace(' ', '').replace('-', '')
if (mobileNumber && mobileNumber.startsWith('+')) {
mobileNumber = mobileNumber.replace('+', '')
}

return mobileNumber
}

export default checkAndCleanMobileNumber
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as checkAndCleanMobileNumber } from './checkAndCleanMobileNumber';
export { checkAndCleanEmail, cleanEmails } from './checkAndCleanEmail';
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ import type { Settings } from './generated-types'
import removeContactFromList from './removeContactFromList'
import enrolContact from './enrolContact'
import addContactToList from './addContactToList'
import sendTransactionalSms from './sendTransactionalSms'
import sendSms from './sendSms'
import sendEmailCampaign from './sendEmailCampaign'
import sendTransactionalEmail from './sendTransactionalEmail'

const destination: DestinationDefinition<Settings> = {
name: 'Dotdigital',
description: 'Send Segment events and user profile data to Dotdigital.',
slug: 'actions-dotdigital',
description: 'Send Segment events and user profile data to Dotdigital.',
mode: 'cloud',
Expand Down Expand Up @@ -52,7 +58,11 @@ const destination: DestinationDefinition<Settings> = {
actions: {
removeContactFromList,
enrolContact,
addContactToList
addContactToList,
sendTransactionalSms,
sendSms,
sendEmailCampaign,
sendTransactionalEmail
}
}

Expand Down
Loading
Loading