ab-ecosystem-cpass 1.0.0 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -21
- package/dist/client.d.ts +38 -0
- package/dist/client.js +49 -5
- package/dist/client.js.map +1 -1
- package/dist/http.d.ts +15 -0
- package/dist/http.js +21 -1
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.js +24 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/bytes.d.ts +38 -0
- package/dist/internal/bytes.js +84 -0
- package/dist/internal/bytes.js.map +1 -0
- package/dist/internal/device.d.ts +24 -0
- package/dist/internal/device.js +91 -0
- package/dist/internal/device.js.map +1 -0
- package/dist/internal/md5.d.ts +4 -0
- package/dist/internal/md5.js +109 -0
- package/dist/internal/md5.js.map +1 -0
- package/dist/internal/receiver-file.d.ts +106 -0
- package/dist/internal/receiver-file.js +128 -0
- package/dist/internal/receiver-file.js.map +1 -0
- package/dist/realtime/client.d.ts +85 -0
- package/dist/realtime/client.js +126 -0
- package/dist/realtime/client.js.map +1 -0
- package/dist/realtime/events.d.ts +147 -0
- package/dist/realtime/events.js +3 -0
- package/dist/realtime/events.js.map +1 -0
- package/dist/realtime/inbox.d.ts +173 -0
- package/dist/realtime/inbox.js +263 -0
- package/dist/realtime/inbox.js.map +1 -0
- package/dist/realtime/index.d.ts +16 -0
- package/dist/realtime/index.js +37 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/resources/audience/contacts.d.ts +19 -3
- package/dist/resources/audience/contacts.js +19 -3
- package/dist/resources/audience/contacts.js.map +1 -1
- package/dist/resources/audience/inbox-media.d.ts +92 -0
- package/dist/resources/audience/inbox-media.js +127 -0
- package/dist/resources/audience/inbox-media.js.map +1 -0
- package/dist/resources/audience/notifications.d.ts +171 -0
- package/dist/resources/audience/notifications.js +215 -0
- package/dist/resources/audience/notifications.js.map +1 -0
- package/dist/resources/audience/scheduled-messages.d.ts +234 -0
- package/dist/resources/audience/scheduled-messages.js +259 -0
- package/dist/resources/audience/scheduled-messages.js.map +1 -0
- package/dist/resources/campaign-audience.d.ts +132 -0
- package/dist/resources/campaign-audience.js +224 -0
- package/dist/resources/campaign-audience.js.map +1 -0
- package/dist/resources/campaign-report-exports.d.ts +14 -0
- package/dist/resources/campaign-report-exports.js +28 -0
- package/dist/resources/campaign-report-exports.js.map +1 -0
- package/dist/resources/campaign-reports.d.ts +15 -0
- package/dist/resources/campaign-reports.js +25 -0
- package/dist/resources/campaign-reports.js.map +1 -0
- package/dist/resources/campaigns.d.ts +239 -0
- package/dist/resources/campaigns.js +300 -0
- package/dist/resources/campaigns.js.map +1 -0
- package/dist/types/audience.d.ts +93 -1
- package/dist/types/campaign.d.ts +229 -0
- package/dist/types/campaign.js +3 -0
- package/dist/types/campaign.js.map +1 -0
- package/dist/types/contact-filter.d.ts +3 -2
- package/dist/types/contact-filter.js +3 -2
- package/dist/types/contact-filter.js.map +1 -1
- package/dist/types/inbox.d.ts +391 -0
- package/dist/types/inbox.js +16 -0
- package/dist/types/inbox.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/scheduled-message.d.ts +221 -0
- package/dist/types/scheduled-message.js +16 -0
- package/dist/types/scheduled-message.js.map +1 -0
- package/docs/audience/contacts.md +34 -4
- package/docs/audience/notifications.md +263 -0
- package/docs/audience/scheduled-messages.md +557 -0
- package/docs/campaign/campaigns.md +489 -0
- package/docs/contact-filter.md +2 -1
- package/docs/inbox/chats.md +356 -0
- package/docs/inbox/connection.md +171 -0
- package/docs/inbox/media.md +202 -0
- package/docs/inbox/messages.md +338 -0
- package/docs/inbox/referrals.md +72 -0
- package/docs/inbox/sessions.md +74 -0
- package/package.json +28 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"campaign-report-exports.js","sourceRoot":"","sources":["../../src/resources/campaign-report-exports.ts"],"names":[],"mappings":";;;AAIA,MAAa,qBAAqB;IACH;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,IAAI,CAAC,QAA4B,EAAE;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAA6B,KAAK,EAAE,6BAA6B,EAAE;YACzF,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,GAAG,CAAC,EAAU;QACZ,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CACtB,KAAK,EACL,+BAA+B,kBAAkB,CAAC,EAAE,CAAC,EAAE,CACxD,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,KAAsC;QAC3C,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAuB,MAAM,EAAE,6BAA6B,EAAE;YACpF,IAAI,EAAE,KAAK;SACZ,CAAC,CAAC;IACL,CAAC;CACF;AAzBD,sDAyBC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CampaignReport, GetCampaignReportQuery } from '../types/campaign';
|
|
2
|
+
import type { Page, PageQuery } from '../types/common';
|
|
3
|
+
import type { HttpClient } from '../http';
|
|
4
|
+
export declare class CampaignReports {
|
|
5
|
+
private readonly http;
|
|
6
|
+
constructor(http: HttpClient);
|
|
7
|
+
list(query?: Partial<PageQuery>): Promise<Page<CampaignReport>>;
|
|
8
|
+
/**
|
|
9
|
+
* The report for one campaign.
|
|
10
|
+
*
|
|
11
|
+
* `from`/`to` narrow the window the metrics are rolled up over; without them
|
|
12
|
+
* the report covers the campaign's whole life.
|
|
13
|
+
*/
|
|
14
|
+
getByCampaign(campaignId: string, query?: GetCampaignReportQuery): Promise<CampaignReport>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CampaignReports = void 0;
|
|
4
|
+
class CampaignReports {
|
|
5
|
+
http;
|
|
6
|
+
constructor(http) {
|
|
7
|
+
this.http = http;
|
|
8
|
+
}
|
|
9
|
+
list(query = {}) {
|
|
10
|
+
return this.http.request('GET', '/v1/campaign-reports', { query });
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The report for one campaign.
|
|
14
|
+
*
|
|
15
|
+
* `from`/`to` narrow the window the metrics are rolled up over; without them
|
|
16
|
+
* the report covers the campaign's whole life.
|
|
17
|
+
*/
|
|
18
|
+
getByCampaign(campaignId, query = {}) {
|
|
19
|
+
return this.http.request('GET', `/v1/campaign-reports/${encodeURIComponent(campaignId)}`, {
|
|
20
|
+
query,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.CampaignReports = CampaignReports;
|
|
25
|
+
//# sourceMappingURL=campaign-reports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"campaign-reports.js","sourceRoot":"","sources":["../../src/resources/campaign-reports.ts"],"names":[],"mappings":";;;AAIA,MAAa,eAAe;IACG;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,IAAI,CAAC,QAA4B,EAAE;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAuB,KAAK,EAAE,sBAAsB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3F,CAAC;IAED;;;;;OAKG;IACH,aAAa,CAAC,UAAkB,EAAE,QAAgC,EAAE;QAClE,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAiB,KAAK,EAAE,wBAAwB,kBAAkB,CAAC,UAAU,CAAC,EAAE,EAAE;YACxG,KAAK;SACN,CAAC,CAAC;IACL,CAAC;CACF;AAlBD,0CAkBC"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { HttpClient } from '../http';
|
|
2
|
+
import { InboxMedia } from './audience/inbox-media';
|
|
3
|
+
import { type CampaignReceiver, type ReceiverEncoding } from '../internal/receiver-file';
|
|
4
|
+
import { type BulkAudienceInput, type ResolvedAudience } from './campaign-audience';
|
|
5
|
+
import type { Campaign, CampaignNumbersFilter, CreateCampaignInput, ListCampaignsQuery, UpdateCampaignInput } from '../types/campaign';
|
|
6
|
+
import type { Page } from '../types/common';
|
|
7
|
+
/** Everything `createBulk` needs: a normal campaign, with an audience instead of `filters`. */
|
|
8
|
+
export interface CreateBulkCampaignInput extends Omit<CreateCampaignInput, 'filters' | 'type'> {
|
|
9
|
+
/** The recipients. Numbers, CSV text, parsed rows, or receivers you built. */
|
|
10
|
+
audience: BulkAudienceInput;
|
|
11
|
+
/** Overrides the dispatch route. Defaults to `bulk_campaign`; rarely needed. */
|
|
12
|
+
type?: string;
|
|
13
|
+
/** Aborts the upload. The campaign is not created if the upload is cancelled. */
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* What `createBulk` did, beyond creating the campaign.
|
|
18
|
+
*
|
|
19
|
+
* `receivers` is a **count**, deliberately matching what the same field means on
|
|
20
|
+
* a {@link Campaign} rather than carrying the resolved list. Two reasons: the
|
|
21
|
+
* caller already holds the data they passed in, and a name that means a number in
|
|
22
|
+
* one place and an array in another is the kind of thing that reads fine and then
|
|
23
|
+
* renders `[object Object]` on someone's screen.
|
|
24
|
+
*/
|
|
25
|
+
export interface BulkCampaignResult extends Omit<ResolvedAudience, 'receivers'> {
|
|
26
|
+
campaign: Campaign;
|
|
27
|
+
/**
|
|
28
|
+
* Recipients actually written into the file.
|
|
29
|
+
*
|
|
30
|
+
* This -- not `campaign.receivers`, which is `0` until the campaign is sent --
|
|
31
|
+
* is the number to show a user now.
|
|
32
|
+
*/
|
|
33
|
+
receivers: number;
|
|
34
|
+
/** Public URL of the encrypted receiver file, as stored on the campaign. */
|
|
35
|
+
fileUrl: string;
|
|
36
|
+
}
|
|
37
|
+
/** An uploader, so the receiver file's destination can be swapped or faked in tests. */
|
|
38
|
+
export type ReceiverUploader = Pick<InboxMedia, 'upload'>;
|
|
39
|
+
/**
|
|
40
|
+
* One bucket of a group campaign: a slice of the recipients, and what should
|
|
41
|
+
* send it.
|
|
42
|
+
*
|
|
43
|
+
* Which of `channel` and `pairIndex` applies depends on the campaign's
|
|
44
|
+
* `distributeBy`. Neither is validated here -- a bucket with the wrong one is a
|
|
45
|
+
* mistake the sending service will report, and guessing which the caller meant
|
|
46
|
+
* would be worse than passing it along.
|
|
47
|
+
*/
|
|
48
|
+
export interface GroupBucket {
|
|
49
|
+
/** This bucket's recipients. Any of the four audience shapes. */
|
|
50
|
+
audience: BulkAudienceInput;
|
|
51
|
+
/** `distributeBy: 'channel'` -- the channel id this bucket is sent through. */
|
|
52
|
+
channel?: string;
|
|
53
|
+
/** `distributeBy: 'template'` -- which message in `messages` renders this bucket. */
|
|
54
|
+
pairIndex?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Extra keys carried onto the filter entry verbatim, beside `numbers` and
|
|
57
|
+
* `hash`. The campaign contract passes anything it does not recognise straight
|
|
58
|
+
* through to the sending service, so this is the seam for distribution metadata
|
|
59
|
+
* this SDK does not model.
|
|
60
|
+
*/
|
|
61
|
+
extra?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
export interface CreateGroupCampaignInput extends Omit<CreateCampaignInput, 'filters' | 'type'> {
|
|
64
|
+
/**
|
|
65
|
+
* What the buckets are split by.
|
|
66
|
+
*
|
|
67
|
+
* `channel` sends each bucket through a different channel; `template` renders
|
|
68
|
+
* each with a different message. Every bucket is stamped with this, and the
|
|
69
|
+
* contract refuses a campaign whose buckets disagree.
|
|
70
|
+
*/
|
|
71
|
+
distributeBy: 'channel' | 'template';
|
|
72
|
+
buckets: readonly GroupBucket[];
|
|
73
|
+
/** Overrides the stored type. Defaults to `group_campaign`; rarely needed. */
|
|
74
|
+
type?: string;
|
|
75
|
+
signal?: AbortSignal;
|
|
76
|
+
}
|
|
77
|
+
/** One bucket's outcome. Same order as the buckets given. */
|
|
78
|
+
export interface GroupBucketResult {
|
|
79
|
+
receivers: number;
|
|
80
|
+
skipped: number;
|
|
81
|
+
duplicates: number;
|
|
82
|
+
/** Public URL of this bucket's encrypted file. */
|
|
83
|
+
fileUrl: string;
|
|
84
|
+
channel?: string;
|
|
85
|
+
pairIndex?: number;
|
|
86
|
+
}
|
|
87
|
+
export interface GroupCampaignResult {
|
|
88
|
+
campaign: Campaign;
|
|
89
|
+
/** Buckets that had recipients and were uploaded. Empty ones are not here. */
|
|
90
|
+
buckets: GroupBucketResult[];
|
|
91
|
+
/** Totals across every bucket. */
|
|
92
|
+
receivers: number;
|
|
93
|
+
skipped: number;
|
|
94
|
+
duplicates: number;
|
|
95
|
+
}
|
|
96
|
+
export declare class Campaigns {
|
|
97
|
+
private readonly http;
|
|
98
|
+
private readonly media;
|
|
99
|
+
constructor(http: HttpClient, media?: ReceiverUploader);
|
|
100
|
+
list(query?: Partial<ListCampaignsQuery>): Promise<Page<Campaign>>;
|
|
101
|
+
get(id: string): Promise<Campaign>;
|
|
102
|
+
/**
|
|
103
|
+
* Creates a campaign from a payload you have already assembled.
|
|
104
|
+
*
|
|
105
|
+
* For a bulk recipient list, prefer {@link createBulk} -- it does the
|
|
106
|
+
* encrypting and uploading that a file-backed audience requires, which is not
|
|
107
|
+
* something a caller should have to reproduce.
|
|
108
|
+
*/
|
|
109
|
+
create(input: CreateCampaignInput): Promise<Campaign>;
|
|
110
|
+
/**
|
|
111
|
+
* Creates a campaign addressed to a list of numbers you supply directly.
|
|
112
|
+
*
|
|
113
|
+
* This is the "bulk campaign" path, and the SDK does the whole of it: the
|
|
114
|
+
* recipient list never travels as part of the campaign, because a million
|
|
115
|
+
* numbers is not a request body. Instead it is encrypted into a file, uploaded
|
|
116
|
+
* to storage, and the campaign stores a pointer to it -- which the sending
|
|
117
|
+
* service fetches and decrypts when the campaign runs.
|
|
118
|
+
*
|
|
119
|
+
* ```ts
|
|
120
|
+
* // Pasted numbers, no variables
|
|
121
|
+
* const { campaign, receivers } = await crm.campaigns.createBulk({
|
|
122
|
+
* name: 'Diwali offer',
|
|
123
|
+
* audience: { numbers: '919000000001, 919000000002\n919000000003' },
|
|
124
|
+
* messages: [{ channel: { id: channelId }, message: template }],
|
|
125
|
+
* });
|
|
126
|
+
*
|
|
127
|
+
* // A CSV, with every column available as a template variable
|
|
128
|
+
* await crm.campaigns.createBulk({
|
|
129
|
+
* name: 'Renewal reminder',
|
|
130
|
+
* audience: { csv: await file.text(), recipientColumn: 'phone' },
|
|
131
|
+
* messages: [{ channel: { id: channelId }, message: template }],
|
|
132
|
+
* trigger: 'schedule',
|
|
133
|
+
* scheduledAt: '2026-11-01T04:30:00.000Z',
|
|
134
|
+
* });
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* ## What it does, in order
|
|
138
|
+
*
|
|
139
|
+
* 1. **Resolves the audience.** Numbers are reduced to digits; CSV or TSV text
|
|
140
|
+
* is parsed with quoted fields honoured; every column becomes a per-recipient
|
|
141
|
+
* variable. Rows with no usable number are skipped and duplicates dropped,
|
|
142
|
+
* both counted in the result rather than passed over in silence.
|
|
143
|
+
* 2. **Encrypts the list.** AES-256-CBC under a secret generated fresh for this
|
|
144
|
+
* one file. See `internal/receiver-file.ts` for the exact scheme.
|
|
145
|
+
* 3. **Uploads it** as a `document` through the media endpoint: a presigned URL
|
|
146
|
+
* is issued and the bytes go straight to storage, so the file never passes
|
|
147
|
+
* through the API.
|
|
148
|
+
* 4. **Creates the campaign** with `type: 'bulk_campaign'` and
|
|
149
|
+
* `filters: [{numbers: <url>, hash: <secret>}]`.
|
|
150
|
+
*
|
|
151
|
+
* ## Two things worth knowing
|
|
152
|
+
*
|
|
153
|
+
* **The returned `campaign.receivers` is `0`, and that is correct.** The
|
|
154
|
+
* audience is resolved when the campaign is sent, not when it is created. Use
|
|
155
|
+
* the `receivers` on this result -- the number actually written into the file --
|
|
156
|
+
* for anything you show the user now.
|
|
157
|
+
*
|
|
158
|
+
* **The file is readable by anyone holding both the URL and the `hash`.** The
|
|
159
|
+
* URL is a public storage link, so the secret stored alongside it on the
|
|
160
|
+
* campaign is the only thing protecting the list. It is a credential: do not log
|
|
161
|
+
* it, and do not copy a `filters` entry from one campaign into another system.
|
|
162
|
+
*
|
|
163
|
+
* Throws `validation_failed` if the audience resolves to no recipients, or to
|
|
164
|
+
* more than a million.
|
|
165
|
+
*/
|
|
166
|
+
createBulk(input: CreateBulkCampaignInput): Promise<BulkCampaignResult>;
|
|
167
|
+
/**
|
|
168
|
+
* Creates a campaign whose recipients are split into buckets, each sent through
|
|
169
|
+
* a different channel or rendered with a different message.
|
|
170
|
+
*
|
|
171
|
+
* ```ts
|
|
172
|
+
* const { campaign, buckets, receivers } = await crm.campaigns.createGroup({
|
|
173
|
+
* name: 'Diwali offer',
|
|
174
|
+
* distributeBy: 'channel',
|
|
175
|
+
* buckets: [
|
|
176
|
+
* { channel: channelA, audience: { rows: north, recipientColumn: 'phone' } },
|
|
177
|
+
* { channel: channelB, audience: { rows: south, recipientColumn: 'phone' } },
|
|
178
|
+
* ],
|
|
179
|
+
* messages: [{ channel: { id: channelA }, message: template }],
|
|
180
|
+
* });
|
|
181
|
+
* ```
|
|
182
|
+
*
|
|
183
|
+
* ## How this differs from `createBulk`
|
|
184
|
+
*
|
|
185
|
+
* Three ways, and each of them is a thing that would be wrong if you built the
|
|
186
|
+
* payload by hand:
|
|
187
|
+
*
|
|
188
|
+
* - **One file per bucket, not one per campaign.** Each bucket is encrypted
|
|
189
|
+
* under its own secret and uploaded separately, and `filters` carries one
|
|
190
|
+
* entry per bucket. This is the only audience mode where `filters` has more
|
|
191
|
+
* than one element.
|
|
192
|
+
* - **A different file format.** A bucket file is a `||`-delimited table with a
|
|
193
|
+
* header row, not the JSON array a bulk file holds. The reading side chooses
|
|
194
|
+
* by campaign type, so the wrong encoding decrypts cleanly and then parses to
|
|
195
|
+
* nothing.
|
|
196
|
+
* - **Columns are fixed by the first recipient.** A table has one header, so
|
|
197
|
+
* every row carries the same variables in the same order. A bucket whose rows
|
|
198
|
+
* have differing columns will have the missing ones filled in as empty.
|
|
199
|
+
*
|
|
200
|
+
* Everything else is the same: numbers are reduced to digits, CSV is parsed
|
|
201
|
+
* with quoted fields honoured, duplicates and unusable rows are dropped and
|
|
202
|
+
* counted -- per bucket, and summed on the result.
|
|
203
|
+
*
|
|
204
|
+
* ## Empty buckets are dropped, not refused
|
|
205
|
+
*
|
|
206
|
+
* A bucket whose audience resolves to nobody is left out of `filters`
|
|
207
|
+
* altogether, matching the panel: distributing a list across six channels
|
|
208
|
+
* routinely leaves one or two with nothing in them, and that is not an error.
|
|
209
|
+
* The call fails only when **every** bucket is empty.
|
|
210
|
+
*
|
|
211
|
+
* De-duplication is per bucket. A number appearing in two buckets is sent to
|
|
212
|
+
* twice -- which is usually the intent, since the buckets are different
|
|
213
|
+
* channels, and is not something this can second-guess.
|
|
214
|
+
*/
|
|
215
|
+
createGroup(input: CreateGroupCampaignInput): Promise<GroupCampaignResult>;
|
|
216
|
+
/**
|
|
217
|
+
* Encrypts and uploads a receiver list, answering with the audience filter it
|
|
218
|
+
* produced.
|
|
219
|
+
*
|
|
220
|
+
* The file half of {@link createBulk}, exposed because the two halves can fail
|
|
221
|
+
* for unrelated reasons. Holding the filter lets you retry a failed campaign
|
|
222
|
+
* creation -- or create several campaigns against one uploaded list -- without
|
|
223
|
+
* re-uploading, which matters when the list is large.
|
|
224
|
+
*
|
|
225
|
+
* The returned `hash` is the decryption secret. Keep it with the URL or the
|
|
226
|
+
* file becomes unreadable; there is no copy of it anywhere else.
|
|
227
|
+
*/
|
|
228
|
+
uploadReceivers(receivers: readonly CampaignReceiver[], signal?: AbortSignal,
|
|
229
|
+
/**
|
|
230
|
+
* How the list is encoded inside the file. `json` for a bulk campaign's
|
|
231
|
+
* single file, `table` for a group campaign's bucket. The reading side picks
|
|
232
|
+
* by campaign type, so this has to match the campaign it is for.
|
|
233
|
+
*/
|
|
234
|
+
encoding?: ReceiverEncoding): Promise<CampaignNumbersFilter>;
|
|
235
|
+
update(id: string, input: UpdateCampaignInput): Promise<Campaign>;
|
|
236
|
+
schedule(id: string, scheduledAt: string): Promise<Campaign>;
|
|
237
|
+
cancel(id: string): Promise<Campaign>;
|
|
238
|
+
delete(id: string): Promise<void>;
|
|
239
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Campaigns = void 0;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
const inbox_media_1 = require("./audience/inbox-media");
|
|
6
|
+
const receiver_file_1 = require("../internal/receiver-file");
|
|
7
|
+
const campaign_audience_1 = require("./campaign-audience");
|
|
8
|
+
/** The dispatch route a file-backed audience is sent under. */
|
|
9
|
+
const BULK_TYPE = 'bulk_campaign';
|
|
10
|
+
/**
|
|
11
|
+
* The dispatch route a bucketed audience is stored under.
|
|
12
|
+
*
|
|
13
|
+
* Stored as `group_campaign`; the campaign service relabels it to
|
|
14
|
+
* `bulk_campaign` on the hand-off to the sending service, because the two differ
|
|
15
|
+
* only in how many receiver files there are. Our own records keep the real name.
|
|
16
|
+
*/
|
|
17
|
+
const GROUP_TYPE = 'group_campaign';
|
|
18
|
+
/** Buckets uploaded at once. Bounded so fifty buckets is not fifty open sockets. */
|
|
19
|
+
const UPLOAD_CONCURRENCY = 3;
|
|
20
|
+
class Campaigns {
|
|
21
|
+
http;
|
|
22
|
+
media;
|
|
23
|
+
constructor(http, media) {
|
|
24
|
+
this.http = http;
|
|
25
|
+
// Defaults to the audience service's own presigned-upload endpoint, which is
|
|
26
|
+
// the same path chat media takes.
|
|
27
|
+
this.media = media ?? new inbox_media_1.InboxMedia(http);
|
|
28
|
+
}
|
|
29
|
+
list(query = {}) {
|
|
30
|
+
return this.http.request('GET', '/v1/campaigns', { query });
|
|
31
|
+
}
|
|
32
|
+
get(id) {
|
|
33
|
+
return this.http.request('GET', `/v1/campaigns/${encodeURIComponent(id)}`);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates a campaign from a payload you have already assembled.
|
|
37
|
+
*
|
|
38
|
+
* For a bulk recipient list, prefer {@link createBulk} -- it does the
|
|
39
|
+
* encrypting and uploading that a file-backed audience requires, which is not
|
|
40
|
+
* something a caller should have to reproduce.
|
|
41
|
+
*/
|
|
42
|
+
create(input) {
|
|
43
|
+
return this.http.request('POST', '/v1/campaigns', { body: input });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Creates a campaign addressed to a list of numbers you supply directly.
|
|
47
|
+
*
|
|
48
|
+
* This is the "bulk campaign" path, and the SDK does the whole of it: the
|
|
49
|
+
* recipient list never travels as part of the campaign, because a million
|
|
50
|
+
* numbers is not a request body. Instead it is encrypted into a file, uploaded
|
|
51
|
+
* to storage, and the campaign stores a pointer to it -- which the sending
|
|
52
|
+
* service fetches and decrypts when the campaign runs.
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* // Pasted numbers, no variables
|
|
56
|
+
* const { campaign, receivers } = await crm.campaigns.createBulk({
|
|
57
|
+
* name: 'Diwali offer',
|
|
58
|
+
* audience: { numbers: '919000000001, 919000000002\n919000000003' },
|
|
59
|
+
* messages: [{ channel: { id: channelId }, message: template }],
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* // A CSV, with every column available as a template variable
|
|
63
|
+
* await crm.campaigns.createBulk({
|
|
64
|
+
* name: 'Renewal reminder',
|
|
65
|
+
* audience: { csv: await file.text(), recipientColumn: 'phone' },
|
|
66
|
+
* messages: [{ channel: { id: channelId }, message: template }],
|
|
67
|
+
* trigger: 'schedule',
|
|
68
|
+
* scheduledAt: '2026-11-01T04:30:00.000Z',
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* ## What it does, in order
|
|
73
|
+
*
|
|
74
|
+
* 1. **Resolves the audience.** Numbers are reduced to digits; CSV or TSV text
|
|
75
|
+
* is parsed with quoted fields honoured; every column becomes a per-recipient
|
|
76
|
+
* variable. Rows with no usable number are skipped and duplicates dropped,
|
|
77
|
+
* both counted in the result rather than passed over in silence.
|
|
78
|
+
* 2. **Encrypts the list.** AES-256-CBC under a secret generated fresh for this
|
|
79
|
+
* one file. See `internal/receiver-file.ts` for the exact scheme.
|
|
80
|
+
* 3. **Uploads it** as a `document` through the media endpoint: a presigned URL
|
|
81
|
+
* is issued and the bytes go straight to storage, so the file never passes
|
|
82
|
+
* through the API.
|
|
83
|
+
* 4. **Creates the campaign** with `type: 'bulk_campaign'` and
|
|
84
|
+
* `filters: [{numbers: <url>, hash: <secret>}]`.
|
|
85
|
+
*
|
|
86
|
+
* ## Two things worth knowing
|
|
87
|
+
*
|
|
88
|
+
* **The returned `campaign.receivers` is `0`, and that is correct.** The
|
|
89
|
+
* audience is resolved when the campaign is sent, not when it is created. Use
|
|
90
|
+
* the `receivers` on this result -- the number actually written into the file --
|
|
91
|
+
* for anything you show the user now.
|
|
92
|
+
*
|
|
93
|
+
* **The file is readable by anyone holding both the URL and the `hash`.** The
|
|
94
|
+
* URL is a public storage link, so the secret stored alongside it on the
|
|
95
|
+
* campaign is the only thing protecting the list. It is a credential: do not log
|
|
96
|
+
* it, and do not copy a `filters` entry from one campaign into another system.
|
|
97
|
+
*
|
|
98
|
+
* Throws `validation_failed` if the audience resolves to no recipients, or to
|
|
99
|
+
* more than a million.
|
|
100
|
+
*/
|
|
101
|
+
async createBulk(input) {
|
|
102
|
+
const { audience, signal, ...campaign } = input;
|
|
103
|
+
const resolved = (0, campaign_audience_1.resolveAudience)(audience);
|
|
104
|
+
if (!resolved.receivers.length) {
|
|
105
|
+
throw new errors_1.CrmApiError(400, 'validation_failed', resolved.skipped > 0
|
|
106
|
+
? `None of the ${resolved.skipped} entries supplied held a usable number`
|
|
107
|
+
: 'The audience resolved to no recipients');
|
|
108
|
+
}
|
|
109
|
+
if (resolved.receivers.length > campaign_audience_1.MAX_BULK_RECEIVERS) {
|
|
110
|
+
throw new errors_1.CrmApiError(400, 'validation_failed', `A bulk campaign may address at most ${campaign_audience_1.MAX_BULK_RECEIVERS.toLocaleString('en-US')} recipients; ` +
|
|
111
|
+
`this audience has ${resolved.receivers.length.toLocaleString('en-US')}. Split it across campaigns.`);
|
|
112
|
+
}
|
|
113
|
+
const filter = await this.uploadReceivers(resolved.receivers, signal);
|
|
114
|
+
const created = await this.create({
|
|
115
|
+
...campaign,
|
|
116
|
+
type: input.type ?? BULK_TYPE,
|
|
117
|
+
filters: [filter],
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
campaign: created,
|
|
121
|
+
receivers: resolved.receivers.length,
|
|
122
|
+
skipped: resolved.skipped,
|
|
123
|
+
duplicates: resolved.duplicates,
|
|
124
|
+
fileUrl: filter.numbers,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Creates a campaign whose recipients are split into buckets, each sent through
|
|
129
|
+
* a different channel or rendered with a different message.
|
|
130
|
+
*
|
|
131
|
+
* ```ts
|
|
132
|
+
* const { campaign, buckets, receivers } = await crm.campaigns.createGroup({
|
|
133
|
+
* name: 'Diwali offer',
|
|
134
|
+
* distributeBy: 'channel',
|
|
135
|
+
* buckets: [
|
|
136
|
+
* { channel: channelA, audience: { rows: north, recipientColumn: 'phone' } },
|
|
137
|
+
* { channel: channelB, audience: { rows: south, recipientColumn: 'phone' } },
|
|
138
|
+
* ],
|
|
139
|
+
* messages: [{ channel: { id: channelA }, message: template }],
|
|
140
|
+
* });
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* ## How this differs from `createBulk`
|
|
144
|
+
*
|
|
145
|
+
* Three ways, and each of them is a thing that would be wrong if you built the
|
|
146
|
+
* payload by hand:
|
|
147
|
+
*
|
|
148
|
+
* - **One file per bucket, not one per campaign.** Each bucket is encrypted
|
|
149
|
+
* under its own secret and uploaded separately, and `filters` carries one
|
|
150
|
+
* entry per bucket. This is the only audience mode where `filters` has more
|
|
151
|
+
* than one element.
|
|
152
|
+
* - **A different file format.** A bucket file is a `||`-delimited table with a
|
|
153
|
+
* header row, not the JSON array a bulk file holds. The reading side chooses
|
|
154
|
+
* by campaign type, so the wrong encoding decrypts cleanly and then parses to
|
|
155
|
+
* nothing.
|
|
156
|
+
* - **Columns are fixed by the first recipient.** A table has one header, so
|
|
157
|
+
* every row carries the same variables in the same order. A bucket whose rows
|
|
158
|
+
* have differing columns will have the missing ones filled in as empty.
|
|
159
|
+
*
|
|
160
|
+
* Everything else is the same: numbers are reduced to digits, CSV is parsed
|
|
161
|
+
* with quoted fields honoured, duplicates and unusable rows are dropped and
|
|
162
|
+
* counted -- per bucket, and summed on the result.
|
|
163
|
+
*
|
|
164
|
+
* ## Empty buckets are dropped, not refused
|
|
165
|
+
*
|
|
166
|
+
* A bucket whose audience resolves to nobody is left out of `filters`
|
|
167
|
+
* altogether, matching the panel: distributing a list across six channels
|
|
168
|
+
* routinely leaves one or two with nothing in them, and that is not an error.
|
|
169
|
+
* The call fails only when **every** bucket is empty.
|
|
170
|
+
*
|
|
171
|
+
* De-duplication is per bucket. A number appearing in two buckets is sent to
|
|
172
|
+
* twice -- which is usually the intent, since the buckets are different
|
|
173
|
+
* channels, and is not something this can second-guess.
|
|
174
|
+
*/
|
|
175
|
+
async createGroup(input) {
|
|
176
|
+
const { buckets, distributeBy, signal, ...campaign } = input;
|
|
177
|
+
const resolved = buckets.map((bucket) => ({ bucket, audience: (0, campaign_audience_1.resolveAudience)(bucket.audience) }));
|
|
178
|
+
const usable = resolved.filter((entry) => entry.audience.receivers.length > 0);
|
|
179
|
+
const totals = resolved.reduce((sum, entry) => ({
|
|
180
|
+
receivers: sum.receivers + entry.audience.receivers.length,
|
|
181
|
+
skipped: sum.skipped + entry.audience.skipped,
|
|
182
|
+
duplicates: sum.duplicates + entry.audience.duplicates,
|
|
183
|
+
}), { receivers: 0, skipped: 0, duplicates: 0 });
|
|
184
|
+
if (!usable.length) {
|
|
185
|
+
throw new errors_1.CrmApiError(400, 'validation_failed', buckets.length
|
|
186
|
+
? `None of the ${buckets.length} buckets resolved to a recipient`
|
|
187
|
+
: 'A group campaign needs at least one bucket');
|
|
188
|
+
}
|
|
189
|
+
if (totals.receivers > campaign_audience_1.MAX_BULK_RECEIVERS) {
|
|
190
|
+
throw new errors_1.CrmApiError(400, 'validation_failed', `A campaign may address at most ${campaign_audience_1.MAX_BULK_RECEIVERS.toLocaleString('en-US')} recipients across all ` +
|
|
191
|
+
`buckets; this one has ${totals.receivers.toLocaleString('en-US')}. Split it across campaigns.`);
|
|
192
|
+
}
|
|
193
|
+
const uploaded = await mapWithLimit(usable, UPLOAD_CONCURRENCY, async ({ bucket, audience }) => {
|
|
194
|
+
const filter = await this.uploadReceivers(audience.receivers, signal, 'table');
|
|
195
|
+
return {
|
|
196
|
+
filter: {
|
|
197
|
+
...bucket.extra,
|
|
198
|
+
type: distributeBy,
|
|
199
|
+
...(bucket.channel === undefined ? {} : { channel: bucket.channel }),
|
|
200
|
+
...(bucket.pairIndex === undefined ? {} : { pairIndex: bucket.pairIndex }),
|
|
201
|
+
// Carried because the panel carried it: the manager can read a bucket's
|
|
202
|
+
// size without downloading and decrypting the file to count it.
|
|
203
|
+
count: audience.receivers.length,
|
|
204
|
+
...filter,
|
|
205
|
+
},
|
|
206
|
+
result: {
|
|
207
|
+
receivers: audience.receivers.length,
|
|
208
|
+
skipped: audience.skipped,
|
|
209
|
+
duplicates: audience.duplicates,
|
|
210
|
+
fileUrl: filter.numbers,
|
|
211
|
+
...(bucket.channel === undefined ? {} : { channel: bucket.channel }),
|
|
212
|
+
...(bucket.pairIndex === undefined ? {} : { pairIndex: bucket.pairIndex }),
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
const created = await this.create({
|
|
217
|
+
...campaign,
|
|
218
|
+
type: input.type ?? GROUP_TYPE,
|
|
219
|
+
filters: uploaded.map((entry) => entry.filter),
|
|
220
|
+
});
|
|
221
|
+
return { campaign: created, buckets: uploaded.map((entry) => entry.result), ...totals };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Encrypts and uploads a receiver list, answering with the audience filter it
|
|
225
|
+
* produced.
|
|
226
|
+
*
|
|
227
|
+
* The file half of {@link createBulk}, exposed because the two halves can fail
|
|
228
|
+
* for unrelated reasons. Holding the filter lets you retry a failed campaign
|
|
229
|
+
* creation -- or create several campaigns against one uploaded list -- without
|
|
230
|
+
* re-uploading, which matters when the list is large.
|
|
231
|
+
*
|
|
232
|
+
* The returned `hash` is the decryption secret. Keep it with the URL or the
|
|
233
|
+
* file becomes unreadable; there is no copy of it anywhere else.
|
|
234
|
+
*/
|
|
235
|
+
async uploadReceivers(receivers, signal,
|
|
236
|
+
/**
|
|
237
|
+
* How the list is encoded inside the file. `json` for a bulk campaign's
|
|
238
|
+
* single file, `table` for a group campaign's bucket. The reading side picks
|
|
239
|
+
* by campaign type, so this has to match the campaign it is for.
|
|
240
|
+
*/
|
|
241
|
+
encoding = 'json') {
|
|
242
|
+
const file = await (0, receiver_file_1.buildReceiverFile)([...receivers], encoding);
|
|
243
|
+
const source = {
|
|
244
|
+
body: file.body,
|
|
245
|
+
filename: file.filename,
|
|
246
|
+
contentType: file.contentType,
|
|
247
|
+
size: file.size,
|
|
248
|
+
};
|
|
249
|
+
// `document` rather than a kind of its own: it is the one kind that accepts
|
|
250
|
+
// an arbitrary media type, and at 100MB its ceiling is far above a million
|
|
251
|
+
// encrypted numbers.
|
|
252
|
+
const numbers = await this.media.upload(source, {
|
|
253
|
+
kind: 'document',
|
|
254
|
+
filename: file.filename,
|
|
255
|
+
contentType: file.contentType,
|
|
256
|
+
signal,
|
|
257
|
+
});
|
|
258
|
+
return { numbers, hash: file.secret };
|
|
259
|
+
}
|
|
260
|
+
update(id, input) {
|
|
261
|
+
return this.http.request('PATCH', `/v1/campaigns/${encodeURIComponent(id)}`, { body: input });
|
|
262
|
+
}
|
|
263
|
+
schedule(id, scheduledAt) {
|
|
264
|
+
return this.http.request('POST', `/v1/campaigns/${encodeURIComponent(id)}/schedule`, {
|
|
265
|
+
body: { scheduledAt },
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
cancel(id) {
|
|
269
|
+
return this.http.request('POST', `/v1/campaigns/${encodeURIComponent(id)}/cancel`);
|
|
270
|
+
}
|
|
271
|
+
delete(id) {
|
|
272
|
+
return this.http.request('DELETE', `/v1/campaigns/${encodeURIComponent(id)}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
exports.Campaigns = Campaigns;
|
|
276
|
+
/**
|
|
277
|
+
* `Promise.all` with a ceiling.
|
|
278
|
+
*
|
|
279
|
+
* A group campaign can have fifty buckets, and the panel uploaded them with an
|
|
280
|
+
* unbounded `Promise.all` -- fifty simultaneous uploads, which a browser queues
|
|
281
|
+
* anyway and a flaky connection turns into fifty retries at once. Three at a
|
|
282
|
+
* time finishes a realistic campaign in about the same wall-clock time without
|
|
283
|
+
* that.
|
|
284
|
+
*
|
|
285
|
+
* Results stay in input order, which `filters` depends on: a `template` bucket
|
|
286
|
+
* is addressed by its `pairIndex`, and reordering would silently re-point them.
|
|
287
|
+
*/
|
|
288
|
+
const mapWithLimit = async (items, limit, run) => {
|
|
289
|
+
const results = new Array(items.length);
|
|
290
|
+
let next = 0;
|
|
291
|
+
const worker = async () => {
|
|
292
|
+
while (next < items.length) {
|
|
293
|
+
const index = next++;
|
|
294
|
+
results[index] = await run(items[index]);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
298
|
+
return results;
|
|
299
|
+
};
|
|
300
|
+
//# sourceMappingURL=campaigns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"campaigns.js","sourceRoot":"","sources":["../../src/resources/campaigns.ts"],"names":[],"mappings":"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"}
|