ab-ecosystem-cpass 1.0.5 → 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 +63 -1
- package/dist/client.d.ts +23 -0
- package/dist/client.js +32 -5
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +22 -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/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/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 +82 -0
- 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/index.d.ts +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/docs/audience/notifications.md +263 -0
- package/docs/campaign/campaigns.md +489 -0
- package/package.json +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Notifications = void 0;
|
|
4
|
+
const errors_1 = require("../../errors");
|
|
5
|
+
const device_1 = require("../../internal/device");
|
|
6
|
+
const paths_1 = require("../paths");
|
|
7
|
+
const BASE = `${paths_1.AUDIENCE}/devices`;
|
|
8
|
+
/** The bounds the server applies, checked here so a bad token fails without a round trip. */
|
|
9
|
+
const TOKEN_LENGTH = { min: 20, max: 4096 };
|
|
10
|
+
/** What the server accepts as a label, so a long one is trimmed rather than refused. */
|
|
11
|
+
const MAX_LABEL = 200;
|
|
12
|
+
/**
|
|
13
|
+
* Push notification subscriptions: which browsers and apps a user wants
|
|
14
|
+
* notifications delivered to.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* // after the browser has granted permission and Firebase has a token
|
|
18
|
+
* await crm.notifications.subscribe({ token });
|
|
19
|
+
*
|
|
20
|
+
* // logout, or the user turning notifications off
|
|
21
|
+
* await crm.notifications.unsubscribe(token);
|
|
22
|
+
*
|
|
23
|
+
* // what the user sees in "notification settings"
|
|
24
|
+
* const { data } = await crm.notifications.devices();
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* ## What this does and does not cover
|
|
28
|
+
*
|
|
29
|
+
* A push subscription has two halves and only one of them is an API call:
|
|
30
|
+
*
|
|
31
|
+
* 1. **In the browser** -- ask for permission (`Notification.requestPermission()`),
|
|
32
|
+
* then get a registration token for this instance
|
|
33
|
+
* (`getToken(messaging, { vapidKey })` from `firebase/messaging`). This SDK
|
|
34
|
+
* deliberately does none of that: it would mean a hard dependency on
|
|
35
|
+
* `firebase/*`, in a package that also runs on Node, to wrap two calls whose
|
|
36
|
+
* arguments come from your own Firebase project.
|
|
37
|
+
* 2. **Here** -- hand that token to the platform so the send path can reach it,
|
|
38
|
+
* which is what {@link subscribe} does, and withdraw it on logout, which is
|
|
39
|
+
* {@link unsubscribe}.
|
|
40
|
+
*
|
|
41
|
+
* ## Where a subscription lives
|
|
42
|
+
*
|
|
43
|
+
* On the platform, against the acting user, in this product -- not in the core
|
|
44
|
+
* platform. Core owns identity and the agency's Firebase credentials; messages
|
|
45
|
+
* arrive here, so the tokens and the sending live here too, and core is not on
|
|
46
|
+
* the delivery path. Nothing in the subscription call names the Firebase project:
|
|
47
|
+
* the platform resolves it from the client the call arrives on and stamps it onto
|
|
48
|
+
* the row, because a token is only sendable through the project that minted it
|
|
49
|
+
* and nothing in the token says which.
|
|
50
|
+
*/
|
|
51
|
+
class Notifications {
|
|
52
|
+
http;
|
|
53
|
+
constructor(http) {
|
|
54
|
+
this.http = http;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Subscribes this browser or app instance to notifications.
|
|
58
|
+
*
|
|
59
|
+
* ```http
|
|
60
|
+
* POST /v1/audience/devices
|
|
61
|
+
* authorization: Bearer <ecosystem token>
|
|
62
|
+
* client-id: tpc_8Kd3mXp7qR2wL9zB
|
|
63
|
+
* content-type: application/json
|
|
64
|
+
*
|
|
65
|
+
* {
|
|
66
|
+
* "token": "fNx8...:APA91bH...",
|
|
67
|
+
* "platform": "web",
|
|
68
|
+
* "device_label": "Chrome on Windows",
|
|
69
|
+
* "tpc_id": "tpc_8Kd3mXp7qR2wL9zB"
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* Answers `201` with the stored {@link Device}, token omitted. Of those four
|
|
74
|
+
* fields the SDK fills in two for you: `platform` defaults to `'web'` when it
|
|
75
|
+
* can see a DOM, and `device_label` to something like `Chrome on Windows` from
|
|
76
|
+
* the user agent.
|
|
77
|
+
*
|
|
78
|
+
* ## Call it on every app load, not once
|
|
79
|
+
*
|
|
80
|
+
* It is an idempotent upsert keyed on the token, so re-sending one the platform
|
|
81
|
+
* already holds costs a single indexed update. That is the intended usage,
|
|
82
|
+
* because two things change underneath you:
|
|
83
|
+
*
|
|
84
|
+
* - FCM rotates registration tokens on its own schedule, and the old one stops
|
|
85
|
+
* being delivered to without anyone being told.
|
|
86
|
+
* - A device unseen for 60 days is pruned, and "seen" means a successful send
|
|
87
|
+
* or a call to this method. A user who receives no notifications for two
|
|
88
|
+
* months would otherwise fall off the list.
|
|
89
|
+
*
|
|
90
|
+
* Re-registering a token that is already on file against **another** user
|
|
91
|
+
* moves it rather than duplicating it: one browser has one person signed in,
|
|
92
|
+
* and the previous owner's notifications must stop reaching it the moment
|
|
93
|
+
* someone else logs in there.
|
|
94
|
+
*
|
|
95
|
+
* ## Identity
|
|
96
|
+
*
|
|
97
|
+
* The user comes from the verified token this client was built with, and there
|
|
98
|
+
* is no field that could say otherwise. `tpc_id` is the one tenant field
|
|
99
|
+
* accepted, because it is not an identity claim -- it says which reseller
|
|
100
|
+
* frontend is registering, which decides whose Firebase project and which deep
|
|
101
|
+
* link host apply. It is cross-checked against the verified `client-id`, so it
|
|
102
|
+
* can confirm the surface but never move the device to another tenant: a value
|
|
103
|
+
* that contradicts the token is refused with `403` rather than ignored.
|
|
104
|
+
*
|
|
105
|
+
* ## Failures worth handling
|
|
106
|
+
*
|
|
107
|
+
* - `400 validation_failed` -- the token is missing or implausible, or
|
|
108
|
+
* `platform` could not be defaulted (see {@link SubscribeNotificationsInput}).
|
|
109
|
+
* - `403 forbidden` -- `tpc_id` names a different client than the token was
|
|
110
|
+
* issued for.
|
|
111
|
+
* - `503 push_not_configured` -- the agency has no Firebase notification
|
|
112
|
+
* resource set up. Nothing the caller can fix; notifications are simply off
|
|
113
|
+
* for this account.
|
|
114
|
+
* - `503 push_unavailable` -- the platform could not reach core to read the
|
|
115
|
+
* configuration. Transient.
|
|
116
|
+
*
|
|
117
|
+
* Both `503`s deserve the same handling as a successful call: try again on the
|
|
118
|
+
* next app load, which the advice above means you are doing anyway.
|
|
119
|
+
*
|
|
120
|
+
* Scope: `devices:write`.
|
|
121
|
+
*/
|
|
122
|
+
subscribe(input) {
|
|
123
|
+
const token = (input.token ?? '').trim();
|
|
124
|
+
if (token.length < TOKEN_LENGTH.min || token.length > TOKEN_LENGTH.max) {
|
|
125
|
+
throw new errors_1.CrmApiError(400, 'validation_failed', `token must be a Firebase registration token of ${TOKEN_LENGTH.min}..${TOKEN_LENGTH.max} characters`);
|
|
126
|
+
}
|
|
127
|
+
const platform = input.platform ?? (0, device_1.detectPlatform)();
|
|
128
|
+
if (!platform) {
|
|
129
|
+
// Not defaulted to 'web' outside a browser on purpose: a mobile runtime
|
|
130
|
+
// registered as web gets the webpush half of the payload, which FCM
|
|
131
|
+
// accepts and the device never displays.
|
|
132
|
+
throw new errors_1.CrmApiError(400, 'validation_failed', 'platform is required outside a browser: pass "android", "ios", or "web" explicitly');
|
|
133
|
+
}
|
|
134
|
+
const label = (input.device_label ?? (0, device_1.describeDevice)() ?? '').trim().slice(0, MAX_LABEL);
|
|
135
|
+
return this.http.request('POST', BASE, {
|
|
136
|
+
body: {
|
|
137
|
+
token,
|
|
138
|
+
platform,
|
|
139
|
+
// Both omitted when empty rather than sent as '': the server treats an
|
|
140
|
+
// absent optional and an empty one the same way, and a body carrying only
|
|
141
|
+
// what it means is easier to read in a log.
|
|
142
|
+
...(label ? { device_label: label } : {}),
|
|
143
|
+
...(input.tpc_id ? { tpc_id: input.tpc_id.trim() } : {}),
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Stops notifications reaching one device. What logout calls.
|
|
149
|
+
*
|
|
150
|
+
* ```http
|
|
151
|
+
* POST /v1/audience/devices/unregister
|
|
152
|
+
* { "token": "fNx8...:APA91bH..." }
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* `204`, no body, whether or not a row went -- the caller is asking for a
|
|
156
|
+
* guarantee about the future, and a token that is already gone provides it. So
|
|
157
|
+
* this is safe to call unconditionally on logout without first checking whether
|
|
158
|
+
* the device was ever subscribed.
|
|
159
|
+
*
|
|
160
|
+
* Scoped to the caller's own devices. A token identifies a browser rather than
|
|
161
|
+
* a person, so by the time one user logs out the row may already belong to
|
|
162
|
+
* whoever logged in after them; an unscoped delete would cut off the person now
|
|
163
|
+
* holding it.
|
|
164
|
+
*
|
|
165
|
+
* A `POST` with the token in the body, not `DELETE /devices/:token`: a
|
|
166
|
+
* registration token is hundreds of characters and may contain `:` and `/`, and
|
|
167
|
+
* putting one in a path would invite proxy length limits and write a live send
|
|
168
|
+
* credential into every access log on the way.
|
|
169
|
+
*
|
|
170
|
+
* ## Also delete it on the device
|
|
171
|
+
*
|
|
172
|
+
* This revokes the platform's permission to send to the token. If the user is
|
|
173
|
+
* turning notifications *off* -- rather than logging out -- also call
|
|
174
|
+
* `deleteToken(messaging)` in the browser, otherwise the next `getToken()`
|
|
175
|
+
* hands back the same token and the subscription can come back by accident.
|
|
176
|
+
*
|
|
177
|
+
* Scope: `devices:write`.
|
|
178
|
+
*/
|
|
179
|
+
unsubscribe(token) {
|
|
180
|
+
const value = (token ?? '').trim();
|
|
181
|
+
if (!value)
|
|
182
|
+
throw new errors_1.CrmApiError(400, 'validation_failed', 'token is required');
|
|
183
|
+
return this.http.request('POST', `${BASE}/unregister`, { body: { token: value } });
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The caller's own subscribed devices, newest contact first.
|
|
187
|
+
*
|
|
188
|
+
* ```http
|
|
189
|
+
* GET /v1/audience/devices?page=1&rows=25
|
|
190
|
+
* ```
|
|
191
|
+
*
|
|
192
|
+
* Offset paginated, `rows` 1..100 and default 25, sorted by `lastSeenAt`
|
|
193
|
+
* descending. This is what a "notification settings" screen lists, and the 20
|
|
194
|
+
* most recently seen devices are also the only ones a notification is sent to,
|
|
195
|
+
* so a longer list is itself the answer to "why did my old laptop stop
|
|
196
|
+
* buzzing".
|
|
197
|
+
*
|
|
198
|
+
* Only ever the acting user's own devices -- there is no account-wide view and
|
|
199
|
+
* no device id in the path. A device is one person's browser, and the only
|
|
200
|
+
* party with a reason to see or revoke it is that person.
|
|
201
|
+
*
|
|
202
|
+
* Tokens are not included, so this cannot tell you whether *this* browser is
|
|
203
|
+
* subscribed. Decide that locally: the browser knows its own
|
|
204
|
+
* `Notification.permission` and its own token, and {@link subscribe} being
|
|
205
|
+
* idempotent means the cheap answer to "am I still subscribed" is to subscribe
|
|
206
|
+
* again.
|
|
207
|
+
*
|
|
208
|
+
* Scope: `devices:read`.
|
|
209
|
+
*/
|
|
210
|
+
devices(query = {}) {
|
|
211
|
+
return this.http.request('GET', BASE, { query });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
exports.Notifications = Notifications;
|
|
215
|
+
//# sourceMappingURL=notifications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../../src/resources/audience/notifications.ts"],"names":[],"mappings":";;;AAEA,yCAA2C;AAC3C,kDAAuE;AACvE,oCAAoC;AAEpC,MAAM,IAAI,GAAG,GAAG,gBAAQ,UAAU,CAAC;AAEnC,6FAA6F;AAC7F,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;AAE5C,wFAAwF;AACxF,MAAM,SAAS,GAAG,GAAG,CAAC;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAa,aAAa;IACK;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiEG;IACH,SAAS,CAAC,KAAkC;QAC1C,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAEzC,IAAI,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC,GAAG,IAAI,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,CAAC;YACvE,MAAM,IAAI,oBAAW,CACnB,GAAG,EACH,mBAAmB,EACnB,kDAAkD,YAAY,CAAC,GAAG,KAAK,YAAY,CAAC,GAAG,aAAa,CACrG,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,IAAA,uBAAc,GAAE,CAAC;QAEpD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,wEAAwE;YACxE,oEAAoE;YACpE,yCAAyC;YACzC,MAAM,IAAI,oBAAW,CACnB,GAAG,EACH,mBAAmB,EACnB,oFAAoF,CACrF,CAAC;QACJ,CAAC;QAED,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,YAAY,IAAI,IAAA,uBAAc,GAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;QAExF,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAS,MAAM,EAAE,IAAI,EAAE;YAC7C,IAAI,EAAE;gBACJ,KAAK;gBACL,QAAQ;gBACR,uEAAuE;gBACvE,0EAA0E;gBAC1E,4CAA4C;gBAC5C,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACzC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACzD;SACF,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACH,WAAW,CAAC,KAAa;QACvB,MAAM,KAAK,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAEnC,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,oBAAW,CAAC,GAAG,EAAE,mBAAmB,EAAE,mBAAmB,CAAC,CAAC;QAEjF,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAO,MAAM,EAAE,GAAG,IAAI,aAAa,EAAE,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;IAC3F,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,OAAO,CAAC,QAAmC,EAAE;QAC3C,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAoB,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACtE,CAAC;CACF;AAhLD,sCAgLC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { CampaignReceiver } from '../internal/receiver-file';
|
|
2
|
+
/**
|
|
3
|
+
* Most recipients one bulk campaign may carry.
|
|
4
|
+
*
|
|
5
|
+
* The same ceiling the panel has always enforced. It is not arbitrary: the list
|
|
6
|
+
* is encrypted into a single file and read back into memory by the campaign
|
|
7
|
+
* manager, so this is the point at which "one campaign" stops being a sensible
|
|
8
|
+
* unit of work.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_BULK_RECEIVERS = 1000000;
|
|
11
|
+
/**
|
|
12
|
+
* How a bulk audience is described.
|
|
13
|
+
*
|
|
14
|
+
* Four shapes, because there are four ways a person actually has this data to
|
|
15
|
+
* hand. All of them end as the same encrypted file; the difference is only how
|
|
16
|
+
* much parsing the SDK does on the way.
|
|
17
|
+
*/
|
|
18
|
+
export type BulkAudienceInput =
|
|
19
|
+
/**
|
|
20
|
+
* Just numbers, with no per-recipient variables.
|
|
21
|
+
*
|
|
22
|
+
* Accepts an array, or one string holding many -- a textarea's contents,
|
|
23
|
+
* separated by commas, newlines, semicolons or spaces, in any combination.
|
|
24
|
+
*/
|
|
25
|
+
{
|
|
26
|
+
numbers: string | readonly string[];
|
|
27
|
+
dedupe?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Raw CSV or TSV text whose first row is the header.
|
|
31
|
+
*
|
|
32
|
+
* `recipientColumn` names the header holding the number; every column,
|
|
33
|
+
* including that one, also becomes a template variable. The delimiter is
|
|
34
|
+
* detected from the header row unless you pass one.
|
|
35
|
+
*/
|
|
36
|
+
| {
|
|
37
|
+
csv: string;
|
|
38
|
+
recipientColumn: string;
|
|
39
|
+
delimiter?: string;
|
|
40
|
+
dedupe?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Rows you have already parsed -- from your own CSV reader, a spreadsheet API,
|
|
44
|
+
* or a database query.
|
|
45
|
+
*/
|
|
46
|
+
| {
|
|
47
|
+
rows: readonly Record<string, unknown>[];
|
|
48
|
+
recipientColumn: string;
|
|
49
|
+
dedupe?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** Receivers you have already built. The escape hatch; nothing is inferred. */
|
|
52
|
+
| {
|
|
53
|
+
receivers: readonly CampaignReceiver[];
|
|
54
|
+
};
|
|
55
|
+
/** What resolving an audience produced, including what it discarded. */
|
|
56
|
+
export interface ResolvedAudience {
|
|
57
|
+
receivers: CampaignReceiver[];
|
|
58
|
+
/** Entries that carried no usable digits and were skipped. */
|
|
59
|
+
skipped: number;
|
|
60
|
+
/** Entries dropped because an earlier entry had the same number. */
|
|
61
|
+
duplicates: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Normalises a column name into a template variable id.
|
|
65
|
+
*
|
|
66
|
+
* Lower-cased with every non-alphanumeric character replaced by `_`, which is
|
|
67
|
+
* what the panel's CSV reader and its paste-a-table path both did -- so a header
|
|
68
|
+
* of `First Name` is the variable `first_name` here exactly as it was there, and
|
|
69
|
+
* templates built against the old behaviour keep working.
|
|
70
|
+
*
|
|
71
|
+
* A blank header becomes `empty_<index>` rather than `_`, so two unnamed columns
|
|
72
|
+
* do not collapse into one variable.
|
|
73
|
+
*/
|
|
74
|
+
export declare const normaliseColumn: (header: unknown, index: number) => string;
|
|
75
|
+
/**
|
|
76
|
+
* Pulls the numbers out of one string, or tidies an array of them.
|
|
77
|
+
*
|
|
78
|
+
* Splits on commas, newlines, carriage returns, tabs, semicolons and spaces --
|
|
79
|
+
* all of them, in any mixture. The panel chose a single separator per paste
|
|
80
|
+
* (commas if the text contained any, otherwise newlines), which quietly mangled
|
|
81
|
+
* the common case of pasting a column from a spreadsheet into a field that
|
|
82
|
+
* already had comma-separated numbers: the newlines were never split on, so two
|
|
83
|
+
* numbers merged into one 24-digit number and the send went nowhere. Splitting on
|
|
84
|
+
* every separator cannot produce that, and for input with one separator it
|
|
85
|
+
* behaves identically.
|
|
86
|
+
*/
|
|
87
|
+
export declare const normaliseNumbers: (input: string | readonly string[]) => string[];
|
|
88
|
+
/**
|
|
89
|
+
* Splits delimited text into a header and rows, RFC 4180 style.
|
|
90
|
+
*
|
|
91
|
+
* A proper parser rather than `split(',')`, because a quoted field may contain
|
|
92
|
+
* the delimiter, a newline, or an escaped quote -- and a currency-formatted
|
|
93
|
+
* variable (`"1,299"`) is exactly the sort of value that appears in these files.
|
|
94
|
+
* Splitting naively turns one such row into two bad ones.
|
|
95
|
+
*
|
|
96
|
+
* Values stay strings. The panel parsed with `dynamicTyping`, which coerced
|
|
97
|
+
* numeric-looking cells to JavaScript numbers -- and that silently corrupted the
|
|
98
|
+
* one column that matters: a 16-digit number exceeds `Number.MAX_SAFE_INTEGER`
|
|
99
|
+
* and comes back with its last digits changed, while a number written with a
|
|
100
|
+
* leading zero loses it. Keeping text as text avoids both.
|
|
101
|
+
*/
|
|
102
|
+
export declare const parseDelimited: (text: string, delimiter?: string) => {
|
|
103
|
+
headers: string[];
|
|
104
|
+
rows: Record<string, string>[];
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Turns parsed rows into receivers.
|
|
108
|
+
*
|
|
109
|
+
* Every column becomes a variable, including the recipient column itself -- which
|
|
110
|
+
* is intentional and matches the panel: a template that greets someone by the
|
|
111
|
+
* number they were messaged on still works.
|
|
112
|
+
*
|
|
113
|
+
* A row whose recipient column holds no digits is skipped rather than failing the
|
|
114
|
+
* whole batch. In a file of ten thousand rows, a handful of blanks and a trailing
|
|
115
|
+
* footer line are normal, and refusing the campaign over them would mean the
|
|
116
|
+
* caller has to clean a file they may not control. The count comes back in
|
|
117
|
+
* {@link ResolvedAudience.skipped} so the omission is visible rather than silent.
|
|
118
|
+
*/
|
|
119
|
+
export declare const buildReceivers: (rows: readonly Record<string, unknown>[], recipientColumn: string, options?: {
|
|
120
|
+
dedupe?: boolean;
|
|
121
|
+
}) => ResolvedAudience;
|
|
122
|
+
/**
|
|
123
|
+
* Resolves any of the four audience shapes into one receiver list.
|
|
124
|
+
*
|
|
125
|
+
* De-duplicates by number unless `dedupe: false`. On by default because the cost
|
|
126
|
+
* of a duplicate here is not a wasted row in a table -- it is the same person
|
|
127
|
+
* receiving the same marketing message twice, billed twice, and more likely to
|
|
128
|
+
* report the sender. A caller who genuinely wants two sends to one number can
|
|
129
|
+
* say so.
|
|
130
|
+
*/
|
|
131
|
+
export declare const resolveAudience: (input: BulkAudienceInput) => ResolvedAudience;
|
|
132
|
+
export type { CampaignReceiver };
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveAudience = exports.buildReceivers = exports.parseDelimited = exports.normaliseNumbers = exports.normaliseColumn = exports.MAX_BULK_RECEIVERS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Most recipients one bulk campaign may carry.
|
|
6
|
+
*
|
|
7
|
+
* The same ceiling the panel has always enforced. It is not arbitrary: the list
|
|
8
|
+
* is encrypted into a single file and read back into memory by the campaign
|
|
9
|
+
* manager, so this is the point at which "one campaign" stops being a sensible
|
|
10
|
+
* unit of work.
|
|
11
|
+
*/
|
|
12
|
+
exports.MAX_BULK_RECEIVERS = 1_000_000;
|
|
13
|
+
/** Digits only: no `+`, no spaces, no dashes, no parentheses. */
|
|
14
|
+
const digitsOf = (value) => (value === null || value === undefined ? '' : String(value).replace(/\D/g, ''));
|
|
15
|
+
/**
|
|
16
|
+
* Normalises a column name into a template variable id.
|
|
17
|
+
*
|
|
18
|
+
* Lower-cased with every non-alphanumeric character replaced by `_`, which is
|
|
19
|
+
* what the panel's CSV reader and its paste-a-table path both did -- so a header
|
|
20
|
+
* of `First Name` is the variable `first_name` here exactly as it was there, and
|
|
21
|
+
* templates built against the old behaviour keep working.
|
|
22
|
+
*
|
|
23
|
+
* A blank header becomes `empty_<index>` rather than `_`, so two unnamed columns
|
|
24
|
+
* do not collapse into one variable.
|
|
25
|
+
*/
|
|
26
|
+
const normaliseColumn = (header, index) => {
|
|
27
|
+
const name = String(header ?? '').trim();
|
|
28
|
+
if (!name)
|
|
29
|
+
return `empty_${index}`;
|
|
30
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
|
31
|
+
};
|
|
32
|
+
exports.normaliseColumn = normaliseColumn;
|
|
33
|
+
/**
|
|
34
|
+
* Pulls the numbers out of one string, or tidies an array of them.
|
|
35
|
+
*
|
|
36
|
+
* Splits on commas, newlines, carriage returns, tabs, semicolons and spaces --
|
|
37
|
+
* all of them, in any mixture. The panel chose a single separator per paste
|
|
38
|
+
* (commas if the text contained any, otherwise newlines), which quietly mangled
|
|
39
|
+
* the common case of pasting a column from a spreadsheet into a field that
|
|
40
|
+
* already had comma-separated numbers: the newlines were never split on, so two
|
|
41
|
+
* numbers merged into one 24-digit number and the send went nowhere. Splitting on
|
|
42
|
+
* every separator cannot produce that, and for input with one separator it
|
|
43
|
+
* behaves identically.
|
|
44
|
+
*/
|
|
45
|
+
const normaliseNumbers = (input) => {
|
|
46
|
+
const parts = typeof input === 'string' ? input.split(/[\s,;]+/) : input;
|
|
47
|
+
return parts.map(digitsOf).filter((value) => value.length > 0);
|
|
48
|
+
};
|
|
49
|
+
exports.normaliseNumbers = normaliseNumbers;
|
|
50
|
+
/**
|
|
51
|
+
* Splits delimited text into a header and rows, RFC 4180 style.
|
|
52
|
+
*
|
|
53
|
+
* A proper parser rather than `split(',')`, because a quoted field may contain
|
|
54
|
+
* the delimiter, a newline, or an escaped quote -- and a currency-formatted
|
|
55
|
+
* variable (`"1,299"`) is exactly the sort of value that appears in these files.
|
|
56
|
+
* Splitting naively turns one such row into two bad ones.
|
|
57
|
+
*
|
|
58
|
+
* Values stay strings. The panel parsed with `dynamicTyping`, which coerced
|
|
59
|
+
* numeric-looking cells to JavaScript numbers -- and that silently corrupted the
|
|
60
|
+
* one column that matters: a 16-digit number exceeds `Number.MAX_SAFE_INTEGER`
|
|
61
|
+
* and comes back with its last digits changed, while a number written with a
|
|
62
|
+
* leading zero loses it. Keeping text as text avoids both.
|
|
63
|
+
*/
|
|
64
|
+
const parseDelimited = (text, delimiter) => {
|
|
65
|
+
const rows = splitRows(text, delimiter ?? detectDelimiter(text));
|
|
66
|
+
const headerRow = rows.shift();
|
|
67
|
+
if (!headerRow)
|
|
68
|
+
return { headers: [], rows: [] };
|
|
69
|
+
const headers = headerRow.map(exports.normaliseColumn);
|
|
70
|
+
const parsed = rows
|
|
71
|
+
// A trailing newline, or a blank line in the middle, is not a record.
|
|
72
|
+
.filter((cells) => cells.some((cell) => cell.trim() !== ''))
|
|
73
|
+
.map((cells) => {
|
|
74
|
+
const row = {};
|
|
75
|
+
headers.forEach((header, index) => {
|
|
76
|
+
row[header] = cells[index] ?? '';
|
|
77
|
+
});
|
|
78
|
+
return row;
|
|
79
|
+
});
|
|
80
|
+
return { headers, rows: parsed };
|
|
81
|
+
};
|
|
82
|
+
exports.parseDelimited = parseDelimited;
|
|
83
|
+
/** Tab if the header row has one, else semicolon, else comma. */
|
|
84
|
+
const detectDelimiter = (text) => {
|
|
85
|
+
const firstLine = text.slice(0, text.search(/\r?\n/) === -1 ? undefined : text.search(/\r?\n/));
|
|
86
|
+
if (firstLine.includes('\t'))
|
|
87
|
+
return '\t';
|
|
88
|
+
if (firstLine.includes(';') && !firstLine.includes(','))
|
|
89
|
+
return ';';
|
|
90
|
+
return ',';
|
|
91
|
+
};
|
|
92
|
+
/** The parser proper: quotes, doubled quotes inside them, and newlines within a field. */
|
|
93
|
+
const splitRows = (text, delimiter) => {
|
|
94
|
+
const rows = [];
|
|
95
|
+
let row = [];
|
|
96
|
+
let field = '';
|
|
97
|
+
let quoted = false;
|
|
98
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
99
|
+
const char = text[i];
|
|
100
|
+
if (quoted) {
|
|
101
|
+
if (char !== '"') {
|
|
102
|
+
field += char;
|
|
103
|
+
}
|
|
104
|
+
else if (text[i + 1] === '"') {
|
|
105
|
+
// A doubled quote inside a quoted field is one literal quote.
|
|
106
|
+
field += '"';
|
|
107
|
+
i += 1;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
quoted = false;
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (char === '"' && field === '') {
|
|
115
|
+
quoted = true;
|
|
116
|
+
}
|
|
117
|
+
else if (char === delimiter) {
|
|
118
|
+
row.push(field);
|
|
119
|
+
field = '';
|
|
120
|
+
}
|
|
121
|
+
else if (char === '\n' || char === '\r') {
|
|
122
|
+
// Either line ending, and CRLF counts once.
|
|
123
|
+
if (char === '\r' && text[i + 1] === '\n')
|
|
124
|
+
i += 1;
|
|
125
|
+
row.push(field);
|
|
126
|
+
rows.push(row);
|
|
127
|
+
row = [];
|
|
128
|
+
field = '';
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
field += char;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (field !== '' || row.length) {
|
|
135
|
+
row.push(field);
|
|
136
|
+
rows.push(row);
|
|
137
|
+
}
|
|
138
|
+
return rows;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Turns parsed rows into receivers.
|
|
142
|
+
*
|
|
143
|
+
* Every column becomes a variable, including the recipient column itself -- which
|
|
144
|
+
* is intentional and matches the panel: a template that greets someone by the
|
|
145
|
+
* number they were messaged on still works.
|
|
146
|
+
*
|
|
147
|
+
* A row whose recipient column holds no digits is skipped rather than failing the
|
|
148
|
+
* whole batch. In a file of ten thousand rows, a handful of blanks and a trailing
|
|
149
|
+
* footer line are normal, and refusing the campaign over them would mean the
|
|
150
|
+
* caller has to clean a file they may not control. The count comes back in
|
|
151
|
+
* {@link ResolvedAudience.skipped} so the omission is visible rather than silent.
|
|
152
|
+
*/
|
|
153
|
+
const buildReceivers = (rows, recipientColumn, options = {}) => {
|
|
154
|
+
const column = (0, exports.normaliseColumn)(recipientColumn, 0);
|
|
155
|
+
const receivers = [];
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
let skipped = 0;
|
|
158
|
+
let duplicates = 0;
|
|
159
|
+
for (const row of rows) {
|
|
160
|
+
// Accept the column as written or as normalised, so a caller passing
|
|
161
|
+
// `'First Name'` against already-normalised rows still resolves.
|
|
162
|
+
const raw = row[recipientColumn] ?? row[column];
|
|
163
|
+
const waba = digitsOf(raw);
|
|
164
|
+
if (!waba) {
|
|
165
|
+
skipped += 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (options.dedupe !== false) {
|
|
169
|
+
if (seen.has(waba)) {
|
|
170
|
+
duplicates += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
seen.add(waba);
|
|
174
|
+
}
|
|
175
|
+
receivers.push({
|
|
176
|
+
waba,
|
|
177
|
+
custom_fields: Object.keys(row).map((key, index) => ({
|
|
178
|
+
id: (0, exports.normaliseColumn)(key, index),
|
|
179
|
+
value: row[key] === null || row[key] === undefined ? '' : String(row[key]),
|
|
180
|
+
})),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return { receivers, skipped, duplicates };
|
|
184
|
+
};
|
|
185
|
+
exports.buildReceivers = buildReceivers;
|
|
186
|
+
/**
|
|
187
|
+
* Resolves any of the four audience shapes into one receiver list.
|
|
188
|
+
*
|
|
189
|
+
* De-duplicates by number unless `dedupe: false`. On by default because the cost
|
|
190
|
+
* of a duplicate here is not a wasted row in a table -- it is the same person
|
|
191
|
+
* receiving the same marketing message twice, billed twice, and more likely to
|
|
192
|
+
* report the sender. A caller who genuinely wants two sends to one number can
|
|
193
|
+
* say so.
|
|
194
|
+
*/
|
|
195
|
+
const resolveAudience = (input) => {
|
|
196
|
+
if ('receivers' in input) {
|
|
197
|
+
// Taken as given: a caller who built these chose the shape deliberately.
|
|
198
|
+
return { receivers: [...input.receivers], skipped: 0, duplicates: 0 };
|
|
199
|
+
}
|
|
200
|
+
if ('numbers' in input) {
|
|
201
|
+
const numbers = (0, exports.normaliseNumbers)(input.numbers);
|
|
202
|
+
const receivers = [];
|
|
203
|
+
const seen = new Set();
|
|
204
|
+
let duplicates = 0;
|
|
205
|
+
for (const waba of numbers) {
|
|
206
|
+
if (input.dedupe !== false) {
|
|
207
|
+
if (seen.has(waba)) {
|
|
208
|
+
duplicates += 1;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
seen.add(waba);
|
|
212
|
+
}
|
|
213
|
+
receivers.push({ waba, custom_fields: [] });
|
|
214
|
+
}
|
|
215
|
+
const supplied = typeof input.numbers === 'string' ? numbers.length + countBlank(input.numbers) : input.numbers.length;
|
|
216
|
+
return { receivers, skipped: Math.max(0, supplied - numbers.length - duplicates), duplicates };
|
|
217
|
+
}
|
|
218
|
+
const rows = 'csv' in input ? (0, exports.parseDelimited)(input.csv, input.delimiter).rows : input.rows;
|
|
219
|
+
return (0, exports.buildReceivers)(rows, input.recipientColumn, { dedupe: input.dedupe });
|
|
220
|
+
};
|
|
221
|
+
exports.resolveAudience = resolveAudience;
|
|
222
|
+
/** Entries in a pasted string that held no digits at all, for the skipped count. */
|
|
223
|
+
const countBlank = (text) => text.split(/[\s,;]+/).filter((part) => part !== '' && digitsOf(part) === '').length;
|
|
224
|
+
//# sourceMappingURL=campaign-audience.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"campaign-audience.js","sourceRoot":"","sources":["../../src/resources/campaign-audience.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CampaignReportExport, CreateCampaignReportExportInput } from '../types/campaign';
|
|
2
|
+
import type { Page, PageQuery } from '../types/common';
|
|
3
|
+
import type { HttpClient } from '../http';
|
|
4
|
+
export declare class CampaignReportExports {
|
|
5
|
+
private readonly http;
|
|
6
|
+
constructor(http: HttpClient);
|
|
7
|
+
list(query?: Partial<PageQuery>): Promise<Page<CampaignReportExport>>;
|
|
8
|
+
/**
|
|
9
|
+
* One export. Poll this until `status` is `completed`, then read `fileUrl` --
|
|
10
|
+
* which expires, so download rather than storing the link.
|
|
11
|
+
*/
|
|
12
|
+
get(id: string): Promise<CampaignReportExport>;
|
|
13
|
+
create(input: CreateCampaignReportExportInput): Promise<CampaignReportExport>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CampaignReportExports = void 0;
|
|
4
|
+
class CampaignReportExports {
|
|
5
|
+
http;
|
|
6
|
+
constructor(http) {
|
|
7
|
+
this.http = http;
|
|
8
|
+
}
|
|
9
|
+
list(query = {}) {
|
|
10
|
+
return this.http.request('GET', '/v1/campaign-report-exports', {
|
|
11
|
+
query,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One export. Poll this until `status` is `completed`, then read `fileUrl` --
|
|
16
|
+
* which expires, so download rather than storing the link.
|
|
17
|
+
*/
|
|
18
|
+
get(id) {
|
|
19
|
+
return this.http.request('GET', `/v1/campaign-report-exports/${encodeURIComponent(id)}`);
|
|
20
|
+
}
|
|
21
|
+
create(input) {
|
|
22
|
+
return this.http.request('POST', '/v1/campaign-report-exports', {
|
|
23
|
+
body: input,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.CampaignReportExports = CampaignReportExports;
|
|
28
|
+
//# sourceMappingURL=campaign-report-exports.js.map
|
|
@@ -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
|
+
}
|