From bcb8ad784009216fa6252dba294afa8d900e4625 Mon Sep 17 00:00:00 2001 From: Vishal Narkhede Date: Thu, 22 Feb 2024 11:01:58 +0100 Subject: [PATCH] feat: campaigns api (#1225) --- src/campaign.ts | 97 +++++++++++++++++++++++ src/client.ts | 205 +++++++++++++++++++++++++++--------------------- src/index.ts | 2 + src/segment.ts | 89 +++++++++++++++++++++ src/types.ts | 53 ++++++++++--- 5 files changed, 345 insertions(+), 101 deletions(-) create mode 100644 src/campaign.ts create mode 100644 src/segment.ts diff --git a/src/campaign.ts b/src/campaign.ts new file mode 100644 index 0000000000..32fbdf2912 --- /dev/null +++ b/src/campaign.ts @@ -0,0 +1,97 @@ +import { StreamChat } from './client'; +import { APIResponse, CampaignData, DefaultGenerics, ExtendableGenerics } from './types'; + +export class Campaign { + id: string | null; + data?: CampaignData; + client: StreamChat; + + constructor(client: StreamChat, id: string | null, data?: CampaignData) { + this.client = client; + this.id = id; + this.data = data; + } + + async create() { + const body = { + id: this.id, + message_template: this.data?.message_template, + segment_ids: this.data?.segment_ids, + sender_id: this.data?.sender_id, + channel_template: this.data?.channel_template, + create_channels: this.data?.create_channels, + description: this.data?.description, + name: this.data?.name, + user_ids: this.data?.user_ids, + }; + + const result = await this.client.createCampaign(body); + + this.id = result.campaign.id; + this.data = result.campaign; + return result; + } + + verifyCampaignId() { + if (!this.id) { + throw new Error( + 'Campaign id is missing. Either create the campaign using campaign.create() or set the id during instantiation - const campaign = client.campaign(id)', + ); + } + } + + async start(scheduledFor?: string) { + this.verifyCampaignId(); + + return await this.client.startCampaign(this.id as string, scheduledFor); + } + + async update(data: Partial) { + this.verifyCampaignId(); + + return this.client.updateCampaign(this.id as string, data); + } + + async delete() { + this.verifyCampaignId(); + + return await this.client.delete(this.client.baseURL + `/campaigns/${this.id}`); + } + + async schedule(params: { scheduledFor: number }) { + this.verifyCampaignId(); + + const { scheduledFor } = params; + const { campaign } = await this.client.patch<{ campaign: Campaign }>( + this.client.baseURL + `/campaigns/${this.id}/schedule`, + { + scheduled_for: scheduledFor, + }, + ); + return campaign; + } + + async stop() { + this.verifyCampaignId(); + + return this.client.patch<{ campaign: Campaign }>(this.client.baseURL + `/campaigns/${this.id}/stop`); + } + + async pause() { + this.verifyCampaignId(); + + return this.client.patch<{ campaign: Campaign }>(this.client.baseURL + `/campaigns/${this.id}/pause`); + } + + async resume() { + this.verifyCampaignId(); + + return this.client.patch<{ campaign: Campaign }>(this.client.baseURL + `/campaigns/${this.id}/resume`); + } + + async get() { + this.verifyCampaignId(); + + return this.client.getCampaign(this.id as string); + } +} diff --git a/src/client.ts b/src/client.ts index d9c0c58853..fe34388a99 100644 --- a/src/client.ts +++ b/src/client.ts @@ -11,6 +11,8 @@ import { StableWSConnection } from './connection'; import { CheckSignature, DevToken, JWTUserToken } from './signing'; import { TokenManager } from './token_manager'; import { WSConnectionFallback } from './connection_fallback'; +import { Campaign } from './campaign'; +import { Segment } from './segment'; import { isErrorResponse, isWSFailure } from './errors'; import { addFileToFormData, @@ -38,7 +40,7 @@ import { BaseDeviceFields, BlockList, BlockListResponse, - Campaign, + CampaignResponse, CampaignData, CampaignFilters, CampaignQueryOptions, @@ -65,7 +67,6 @@ import { CustomPermissionOptions, DeactivateUsersOptions, DefaultGenerics, - DeleteCampaignOptions, DeleteChannelsResponse, DeleteCommandResponse, DeleteUserOptions, @@ -136,7 +137,7 @@ import { SearchMessageSortBase, SearchOptions, SearchPayload, - Segment, + SegmentResponse, SegmentData, SegmentType, SendFileAPIResponse, @@ -145,7 +146,6 @@ import { SyncResponse, TaskResponse, TaskStatus, - TestCampaignResponse, TestPushDataInput, TestSNSDataInput, TestSQSDataInput, @@ -168,6 +168,10 @@ import { PartialThreadUpdate, QueryThreadsOptions, GetThreadOptions, + CampaignSort, + SegmentTargetsResponse, + QuerySegmentTargetsFilter, + SortParam, } from './types'; import { InsightMetrics, postInsights } from './insights'; import { Thread } from './thread'; @@ -2943,6 +2947,30 @@ export class StreamChat(`${this.baseURL}/export_channels/${id}`); } + campaign(idOrData: string | CampaignData, data?: CampaignData) { + if (typeof idOrData === 'string') { + return new Campaign(this, idOrData, data); + } + + return new Campaign(this, null, idOrData); + } + + segment(type: SegmentType, idOrData: string | SegmentData, data?: SegmentData) { + if (typeof idOrData === 'string') { + return new Segment(this, type, idOrData, data); + } + + return new Segment(this, type, null, idOrData); + } + + validateServerSideAuth() { + if (!this.secret) { + throw new Error( + 'Campaigns is a server-side only feature. Please initialize the client with a secret to use this feature.', + ); + } + } + /** * createSegment - Creates a segment * @@ -2952,17 +2980,17 @@ export class StreamChat { + private async createSegment(type: SegmentType, id: string, name: string, data?: SegmentData) { + this.validateServerSideAuth(); const body = { id, type, name, data, }; - const { segment } = await this.post<{ segment: Segment }>(this.baseURL + `/segments`, body); - return segment; + return this.post<{ segment: SegmentResponse }>(this.baseURL + `/segments`, body); } /** @@ -2974,8 +3002,9 @@ export class StreamChat { - return await this.createSegment('user', id, name, data); + async createUserSegment(id: string, name: string, data?: SegmentData) { + this.validateServerSideAuth(); + return this.createSegment('user', id, name, data); } /** @@ -2987,8 +3016,14 @@ export class StreamChat { - return await this.createSegment('channel', id, name, data); + async createChannelSegment(id: string, name: string, data?: SegmentData) { + this.validateServerSideAuth(); + return this.createSegment('channel', id, name, data); + } + + async getSegment(id: string) { + this.validateServerSideAuth(); + return this.get<{ segment: SegmentResponse } & APIResponse>(this.baseURL + `/segments/${id}`); } /** @@ -3000,8 +3035,8 @@ export class StreamChat) { - const { segment } = await this.put<{ segment: Segment }>(this.baseURL + `/segments/${id}`, data); - return segment; + this.validateServerSideAuth(); + return this.put<{ segment: SegmentResponse }>(this.baseURL + `/segments/${id}`, data); } /** @@ -3013,21 +3048,39 @@ export class StreamChat(this.baseURL + `/segments/${id}/addtargets`, body); + this.validateServerSideAuth(); + const body = { target_ids: targets }; + return this.post(this.baseURL + `/segments/${id}/addtargets`, body); } + async querySegmentTargets( + id: string, + filter: QuerySegmentTargetsFilter | null = {}, + sort: SortParam[] | null | [] = [], + options = {}, + ) { + this.validateServerSideAuth(); + return this.post<{ targets: SegmentTargetsResponse[]; next?: string } & APIResponse>( + this.baseURL + `/segments/${id}/targets/query`, + { + filter: filter || {}, + sort: sort || [], + ...options, + }, + ); + } /** - * deleteSegmentTargets - Delete targets from a segment + * removeSegmentTargets - Remove targets from a segment * * @param {string} id Segment ID * @param {string[]} targets Targets to add to the segment * * @return {APIResponse} API response */ - async deleteSegmentTargets(id: string, targets: string[]) { - const body = { targets }; - return await this.post(this.baseURL + `/segments/${id}/deletetargets`, body); + async removeSegmentTargets(id: string, targets: string[]) { + this.validateServerSideAuth(); + const body = { target_ids: targets }; + return this.post(this.baseURL + `/segments/${id}/deletetargets`, body); } /** @@ -3038,15 +3091,17 @@ export class StreamChat(this.baseURL + `/segments`, { - payload: { - filter, - ...options, - }, + async querySegments(filter: {}, sort?: SortParam[], options: QuerySegmentsOptions = {}) { + this.validateServerSideAuth(); + return this.post< + { + segments: SegmentResponse[]; + next?: string; + } & APIResponse + >(this.baseURL + `/segments/query`, { + filter, + sort, + ...options, }); } @@ -3058,7 +3113,8 @@ export class StreamChat} The Server Response */ async deleteSegment(id: string) { - return await this.delete(this.baseURL + `/segments/${id}`); + this.validateServerSideAuth(); + return this.delete(this.baseURL + `/segments/${id}`); } /** @@ -3070,7 +3126,8 @@ export class StreamChat} The Server Response */ async segmentTargetExists(segmentId: string, targetId: string) { - return await this.get(this.baseURL + `/segments/${segmentId}/target/${targetId}`); + this.validateServerSideAuth(); + return this.get(this.baseURL + `/segments/${segmentId}/target/${targetId}`); } /** @@ -3081,27 +3138,38 @@ export class StreamChat(this.baseURL + `/campaigns`, { campaign: params }); - return campaign; + this.validateServerSideAuth(); + return this.post<{ campaign: CampaignResponse } & APIResponse>(this.baseURL + `/campaigns`, { ...params }); + } + + async getCampaign(id: string) { + this.validateServerSideAuth(); + return this.get<{ campaign: CampaignResponse } & APIResponse>(this.baseURL + `/campaigns/${id}`); } + async startCampaign(id: string, scheduledFor?: string) { + this.validateServerSideAuth(); + return this.post<{ campaign: CampaignResponse } & APIResponse>(this.baseURL + `/campaigns/${id}/start`, { + scheduled_for: scheduledFor, + }); + } /** * queryCampaigns - Query Campaigns * * * @return {Campaign[]} Campaigns */ - async queryCampaigns(filters: CampaignFilters, options: CampaignQueryOptions = {}) { - return await this.get<{ - campaigns: Campaign[]; + async queryCampaigns(filter: CampaignFilters, sort?: CampaignSort, options?: CampaignQueryOptions) { + this.validateServerSideAuth(); + return await this.post<{ + campaigns: CampaignResponse[]; segments: Record; channels?: Record>; users?: Record>; - }>(this.baseURL + `/campaigns`, { - payload: { - filter_conditions: filters, - ...options, - }, + }>(this.baseURL + `/campaigns/query`, { + filter, + sort, + ...(options || {}), }); } @@ -3114,10 +3182,8 @@ export class StreamChat) { - const { campaign } = await this.put<{ campaign: Campaign }>(this.baseURL + `/campaigns/${id}`, { - campaign: params, - }); - return campaign; + this.validateServerSideAuth(); + return this.put<{ campaign: CampaignResponse }>(this.baseURL + `/campaigns/${id}`, params); } /** @@ -3127,24 +3193,9 @@ export class StreamChat} The Server Response */ - async deleteCampaign(id: string, params: DeleteCampaignOptions = {}) { - return this.delete(this.baseURL + `/campaigns/${id}`, params); - } - - /** - * scheduleCampaign - Schedule a Campaign - * - * @param {string} id Campaign ID - * @param {{scheduledFor: number}} params Schedule params - * - * @return {Campaign} Scheduled Campaign - */ - async scheduleCampaign(id: string, params: { scheduledFor: number }) { - const { scheduledFor } = params; - const { campaign } = await this.patch<{ campaign: Campaign }>(this.baseURL + `/campaigns/${id}/schedule`, { - scheduled_for: scheduledFor, - }); - return campaign; + async deleteCampaign(id: string) { + this.validateServerSideAuth(); + return this.delete(this.baseURL + `/campaigns/${id}`); } /** @@ -3155,35 +3206,11 @@ export class StreamChat(this.baseURL + `/campaigns/${id}/stop`); - return campaign; - } - - /** - * resumeCampaign - Resume a Campaign - * - * @param {string} id Campaign ID - * - * @return {Campaign} Resumed Campaign - */ - async resumeCampaign(id: string) { - const { campaign } = await this.patch<{ campaign: Campaign }>(this.baseURL + `/campaigns/${id}/resume`); + this.validateServerSideAuth(); + const { campaign } = await this.patch<{ campaign: CampaignResponse }>(this.baseURL + `/campaigns/${id}/stop`); return campaign; } - /** - * testCampaign - Test a Campaign - * - * @param {string} id Campaign ID - * @param {{users: string[]}} params Test params - * - * @return {TestCampaignResponse} Test campaign response - */ - async testCampaign(id: string, params: { users: string[] }) { - const { users } = params; - return await this.post(this.baseURL + `/campaigns/${id}/test`, { users }); - } - /** * enrichURL - Get OpenGraph data of the given link * diff --git a/src/index.ts b/src/index.ts index a4e097d961..96d641236f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,4 +11,6 @@ export * from './signing'; export * from './token_manager'; export * from './insights'; export * from './types'; +export * from './segment'; +export * from './campaign'; export { isOwnUser, chatCodes, logChatPromiseExecution, formatMessage } from './utils'; diff --git a/src/segment.ts b/src/segment.ts new file mode 100644 index 0000000000..d8fcb9853b --- /dev/null +++ b/src/segment.ts @@ -0,0 +1,89 @@ +import { StreamChat } from './client'; +import { + DefaultGenerics, + ExtendableGenerics, + QuerySegmentTargetsFilter, + SegmentData, + SegmentResponse, + SortParam, +} from './types'; + +type SegmentType = 'user' | 'channel'; + +type SegmentUpdatableFields = { + description?: string; + filter?: {}; + name?: string; +}; + +export class Segment { + type: SegmentType; + id?: string | null; + client: StreamChat; + data?: SegmentData | SegmentResponse; + + constructor(client: StreamChat, type: SegmentType, id: string | null, data?: SegmentData) { + this.client = client; + this.type = type; + this.id = id; + this.data = data; + } + + async create() { + const body = { + id: this.id, + type: this.type, + name: this.data?.name, + filter: this.data?.filter, + description: this.data?.description, + all_users: this.data?.all_users, + }; + + return this.client.post<{ segment: SegmentResponse }>(this.client.baseURL + `/segments`, body); + } + + verifySegmentId() { + if (!this.id) { + throw new Error( + 'Segment id is missing. Either create the segment using segment.create() or set the id during instantiation - const segment = client.segment(id)', + ); + } + } + + async get() { + this.verifySegmentId(); + return this.client.getSegment(this.id as string); + } + + async update(data: Partial) { + this.verifySegmentId(); + + return this.client.updateSegment(this.id as string, data); + } + + async addTargets(targets: string[]) { + this.verifySegmentId(); + return this.client.addSegmentTargets(this.id as string, targets); + } + + async removeTargets(targets: string[]) { + this.verifySegmentId(); + return this.client.removeSegmentTargets(this.id as string, targets); + } + + async delete() { + this.verifySegmentId(); + return this.client.deleteSegment(this.id as string); + } + + async targetExists(targetId: string) { + this.verifySegmentId(); + return this.client.segmentTargetExists(this.id as string, targetId); + } + + async queryTargets(filter: QuerySegmentTargetsFilter | null = {}, sort: SortParam[] | null | [] = [], options = {}) { + this.verifySegmentId(); + + return this.client.querySegmentTargets(this.id as string, filter, sort, options); + } +} diff --git a/src/types.ts b/src/types.ts index 7c1c93e973..5b832b6b9d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2487,17 +2487,19 @@ export type DeleteUserOptions = { export type SegmentType = 'channel' | 'user'; export type SegmentData = { + all_users?: boolean; description?: string; filter?: {}; + name?: string; }; -export type Segment = { +export type SegmentResponse = { created_at: string; deleted_at: string; id: string; locked: boolean; - name: string; size: number; + task_id: string; type: SegmentType; updated_at: string; } & SegmentData; @@ -2506,6 +2508,12 @@ export type UpdateSegmentData = { name: string; } & SegmentData; +export type SegmentTargetsResponse = { + created_at: string; + segment_id: string; + target_id: string; +}; + export type SortParam = { field: string; direction?: AscDesc; @@ -2517,10 +2525,18 @@ export type Pager = { prev?: string; }; -export type QuerySegmentsOptions = { - sort?: SortParam[]; -} & Pager; +export type QuerySegmentsOptions = Pager; +export type QuerySegmentTargetsFilter = { + target_id?: { + $eq?: string; + $gte?: string; + $in?: string[]; + $lte?: string; + }; +}; +export type QuerySegmentTargetsSort = {}; +export type QuerySegmentTargetsOptions = Pick; export type CampaignSortField = { field: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -2534,6 +2550,8 @@ export type CampaignSort = { export type CampaignQueryOptions = { limit?: number; + next?: string; + prev?: string; sort?: CampaignSort; }; @@ -2543,14 +2561,25 @@ export type SegmentQueryOptions = CampaignQueryOptions; export type CampaignFilters = {}; export type CampaignData = { - attachments: Attachment[]; - channel_type: string; - defaults: Record; - name: string; - segment_id: string; - text: string; + channel_template?: { + type: string; + custom?: {}; + id?: string; + members?: string[]; + }; + create_channels?: boolean; + deleted_at?: string; description?: string; + id?: string | null; + message_template?: { + text: string; + attachments?: Attachment[]; + custom?: {}; + }; + name?: string; + segment_ids?: string[]; sender_id?: string; + user_ids?: string[]; }; export type CampaignStatusName = 'draft' | 'stopped' | 'scheduled' | 'completed' | 'failed' | 'in_progress'; @@ -2568,7 +2597,7 @@ export type CampaignStatus = { task_id?: string; }; -export type Campaign = { +export type CampaignResponse = { created_at: string; id: string; updated_at: string;