@whitewall/blip-sdk 0.0.135 → 0.0.137
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/dist/cjs/client.js +1 -1
- package/dist/cjs/client.js.map +1 -1
- package/dist/esm/client.js +1 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +117 -0
- package/src/index.ts +6 -0
- package/src/namespaces/account.ts +729 -0
- package/src/namespaces/activecampaign.ts +285 -0
- package/src/namespaces/analytics.ts +230 -0
- package/src/namespaces/billing.ts +17 -0
- package/src/namespaces/builder.ts +52 -0
- package/src/namespaces/configurations.ts +19 -0
- package/src/namespaces/context.ts +67 -0
- package/src/namespaces/desk.ts +679 -0
- package/src/namespaces/media.ts +39 -0
- package/src/namespaces/namespace.ts +125 -0
- package/src/namespaces/plugins.ts +223 -0
- package/src/namespaces/portal.ts +402 -0
- package/src/namespaces/scheduler.ts +88 -0
- package/src/namespaces/whatsapp.ts +383 -0
- package/src/sender/bliperror.ts +42 -0
- package/src/sender/enveloperesolver.ts +148 -0
- package/src/sender/gateway/customgatewaysender.ts +43 -0
- package/src/sender/http/httpsender.ts +94 -0
- package/src/sender/index.ts +7 -0
- package/src/sender/plugin/communication.ts +72 -0
- package/src/sender/plugin/pluginsender.ts +75 -0
- package/src/sender/security.ts +33 -0
- package/src/sender/sender.ts +145 -0
- package/src/sender/sessionnegotiator.ts +175 -0
- package/src/sender/tcp/tcpsender.ts +252 -0
- package/src/sender/throttler.ts +36 -0
- package/src/sender/websocket/websocketsender.ts +175 -0
- package/src/types/account.ts +84 -0
- package/src/types/analytics.ts +18 -0
- package/src/types/billing.ts +15 -0
- package/src/types/command.ts +47 -0
- package/src/types/commons.ts +16 -0
- package/src/types/desk.ts +51 -0
- package/src/types/envelope.ts +9 -0
- package/src/types/flow.ts +327 -0
- package/src/types/index.ts +13 -0
- package/src/types/message.ts +116 -0
- package/src/types/node.ts +86 -0
- package/src/types/notification.ts +18 -0
- package/src/types/plugins.ts +51 -0
- package/src/types/portal.ts +39 -0
- package/src/types/reason.ts +22 -0
- package/src/types/session.ts +22 -0
- package/src/types/whatsapp.ts +84 -0
- package/src/utils/odata.ts +114 -0
- package/src/utils/random.ts +3 -0
- package/src/utils/uri.ts +46 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import type { BlipClient } from '../client.ts'
|
|
2
|
+
import type { Identity } from '../types/index.ts'
|
|
3
|
+
import type {
|
|
4
|
+
BodyComponent,
|
|
5
|
+
ButtonComponent,
|
|
6
|
+
HandleFormats,
|
|
7
|
+
HeaderComponent,
|
|
8
|
+
MessageTemplate,
|
|
9
|
+
MessageTemplateVariable,
|
|
10
|
+
WhatsappFlow,
|
|
11
|
+
} from '../types/whatsapp.ts'
|
|
12
|
+
import { uri } from '../utils/uri.ts'
|
|
13
|
+
import { type ConsumeOptions, Namespace, type SendCommandOptions } from './namespace.ts'
|
|
14
|
+
|
|
15
|
+
export class WhatsAppNamespace extends Namespace {
|
|
16
|
+
constructor(blipClient: BlipClient, defaultOptions?: SendCommandOptions) {
|
|
17
|
+
super(blipClient, 'wa.gw', defaultOptions)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public async phoneToIdentity(phoneNumber: string, opts?: ConsumeOptions): Promise<Identity> {
|
|
21
|
+
const fixedPhoneNumber = phoneNumber[0] !== '+' ? `+${phoneNumber}` : phoneNumber
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const result = await this.sendCommand<
|
|
25
|
+
'get',
|
|
26
|
+
{
|
|
27
|
+
alternativeAccount: Identity
|
|
28
|
+
}
|
|
29
|
+
>(
|
|
30
|
+
{
|
|
31
|
+
method: 'get',
|
|
32
|
+
uri: uri`/accounts/${fixedPhoneNumber}`,
|
|
33
|
+
},
|
|
34
|
+
opts,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return result.alternativeAccount
|
|
38
|
+
} catch {
|
|
39
|
+
return `${phoneNumber.replace('+', '')}@wa.gw.msging.net`
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* This is a heavily cached endpoint on Meta, so it may take a while to reflect changes
|
|
45
|
+
* @param filters.status - Filter by status of the message template
|
|
46
|
+
* @param filters.name - Filter by name of the message template, supports partial match
|
|
47
|
+
*/
|
|
48
|
+
public async getMessageTemplates(
|
|
49
|
+
filters?: Partial<{
|
|
50
|
+
status: MessageTemplate['status']
|
|
51
|
+
name: string
|
|
52
|
+
}>,
|
|
53
|
+
opts?: ConsumeOptions,
|
|
54
|
+
): Promise<Array<MessageTemplate>> {
|
|
55
|
+
return await this.sendCommand(
|
|
56
|
+
{
|
|
57
|
+
method: 'get',
|
|
58
|
+
uri: uri`/message-templates?${{ status: filters?.status?.toUpperCase(), templateName: filters?.name }}`,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
collection: true,
|
|
62
|
+
...opts,
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public async getMessageTemplate(
|
|
68
|
+
templateName: string,
|
|
69
|
+
language?: string,
|
|
70
|
+
opts?: ConsumeOptions,
|
|
71
|
+
): Promise<(MessageTemplate & { variables: Array<MessageTemplateVariable> }) | undefined> {
|
|
72
|
+
let templates = await this.getMessageTemplates({ name: templateName }, opts)
|
|
73
|
+
if (language) {
|
|
74
|
+
templates = templates.filter((template) => template.language === language)
|
|
75
|
+
}
|
|
76
|
+
const template = templates.find((template) => template.name === templateName)
|
|
77
|
+
if (!template) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const variables: Array<MessageTemplateVariable> = []
|
|
82
|
+
|
|
83
|
+
const handleHeader = template.components.find(
|
|
84
|
+
(c): c is HeaderComponent<HandleFormats> =>
|
|
85
|
+
c.type === 'HEADER' && ['IMAGE', 'VIDEO', 'DOCUMENT'].includes(c.format),
|
|
86
|
+
)
|
|
87
|
+
if (handleHeader) {
|
|
88
|
+
variables.push({ type: 'MEDIA', example: handleHeader.example.header_handle?.[0] })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const body = template.components.find((c): c is BodyComponent => c.type === 'BODY')
|
|
92
|
+
if (body?.example?.body_text) {
|
|
93
|
+
variables.push(...body.example.body_text[0].map((text) => ({ type: 'BODY' as const, example: text })))
|
|
94
|
+
} else if (body) {
|
|
95
|
+
variables.push(...(body.text.match(/{{(\d)}}/g)?.map(() => ({ type: 'BODY' as const })) ?? []))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const button = template.components.find((c): c is ButtonComponent => c.type === 'BUTTONS')
|
|
99
|
+
if (button) {
|
|
100
|
+
const urlButton = button.buttons.find((b) => b.type === 'URL')
|
|
101
|
+
if (urlButton?.example?.length) {
|
|
102
|
+
variables.push(...urlButton.example.map((example: string) => ({ type: 'BUTTON', example })))
|
|
103
|
+
} else if (urlButton) {
|
|
104
|
+
variables.push(...(urlButton.url.match(/{{(\d)}}/g)?.map(() => ({ type: 'BUTTON' })) ?? []))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
...template,
|
|
110
|
+
variables,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public async createMessageTemplateAttachment(
|
|
115
|
+
url: string,
|
|
116
|
+
opts?: ConsumeOptions,
|
|
117
|
+
): Promise<{
|
|
118
|
+
uploadSessionId: string
|
|
119
|
+
fileHandle: string
|
|
120
|
+
fileSize: string
|
|
121
|
+
fileSizeUploaded: string
|
|
122
|
+
}> {
|
|
123
|
+
return await this.sendCommand(
|
|
124
|
+
{
|
|
125
|
+
method: 'set',
|
|
126
|
+
uri: uri`/message-templates-attachment`,
|
|
127
|
+
type: 'application/vnd.lime.media-link+json',
|
|
128
|
+
resource: {
|
|
129
|
+
uri: url,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
opts,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public async createMessageTemplate(
|
|
137
|
+
template: Pick<MessageTemplate, 'name' | 'category' | 'language' | 'components'>,
|
|
138
|
+
opts?: ConsumeOptions,
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
return await this.sendCommand(
|
|
141
|
+
{
|
|
142
|
+
method: 'set',
|
|
143
|
+
uri: uri`/message-templates`,
|
|
144
|
+
type: 'application/json',
|
|
145
|
+
resource: template,
|
|
146
|
+
},
|
|
147
|
+
opts,
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public async createMessageTemplateAndPoll(
|
|
152
|
+
template: Pick<MessageTemplate, 'name' | 'category' | 'language' | 'components'>,
|
|
153
|
+
opts?: ConsumeOptions,
|
|
154
|
+
): Promise<MessageTemplate> {
|
|
155
|
+
await this.sendCommand(
|
|
156
|
+
{
|
|
157
|
+
method: 'set',
|
|
158
|
+
uri: uri`/message-templates`,
|
|
159
|
+
type: 'application/json',
|
|
160
|
+
resource: template,
|
|
161
|
+
},
|
|
162
|
+
opts,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
while (true) {
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, 3000))
|
|
167
|
+
|
|
168
|
+
const created = await this.getMessageTemplate(template.name, template.language, opts)
|
|
169
|
+
if (!created) {
|
|
170
|
+
throw new Error(`Template ${template.name} was not created`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (created.status !== 'PENDING') {
|
|
174
|
+
if (created.status === 'APPROVED') {
|
|
175
|
+
return created
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
throw new Error(`Template ${template.name} was rejected: ${created.rejected_reason}`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public async deleteMessageTemplate(templateName: string, opts?: ConsumeOptions): Promise<void> {
|
|
184
|
+
return await this.sendCommand(
|
|
185
|
+
{
|
|
186
|
+
method: 'delete',
|
|
187
|
+
uri: uri`/message-templates/${templateName}`,
|
|
188
|
+
},
|
|
189
|
+
opts,
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public async createFlow(
|
|
194
|
+
flow: { name: string; category: string; endpointUri?: string },
|
|
195
|
+
opts?: ConsumeOptions,
|
|
196
|
+
): Promise<{ id: string }> {
|
|
197
|
+
return await this.sendCommand(
|
|
198
|
+
{
|
|
199
|
+
method: 'set',
|
|
200
|
+
uri: uri`/whatsapp-flows`,
|
|
201
|
+
type: 'application/json',
|
|
202
|
+
resource: {
|
|
203
|
+
name: flow.name,
|
|
204
|
+
categories: [flow.category],
|
|
205
|
+
endpoint_uri: flow.endpointUri,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
opts,
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public async updateFlow(
|
|
213
|
+
id: string,
|
|
214
|
+
flow: { name?: string; endpointUri?: string },
|
|
215
|
+
opts?: ConsumeOptions,
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
return await this.sendCommand(
|
|
218
|
+
{
|
|
219
|
+
method: 'set',
|
|
220
|
+
uri: uri`/whatsapp-flows/${id}`,
|
|
221
|
+
type: 'application/json',
|
|
222
|
+
resource: {
|
|
223
|
+
name: flow.name,
|
|
224
|
+
endpoint_uri: flow.endpointUri,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
opts,
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public async updateFlowJson(id: string, json: string, opts?: ConsumeOptions): Promise<void> {
|
|
232
|
+
return await this.sendCommand(
|
|
233
|
+
{
|
|
234
|
+
method: 'set',
|
|
235
|
+
uri: uri`/whatsapp-flows/flow-json/${id}`,
|
|
236
|
+
type: 'application/json',
|
|
237
|
+
resource: JSON.parse(json),
|
|
238
|
+
},
|
|
239
|
+
opts,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public async updateFlowsPublicKey(publicKey: string, opts?: ConsumeOptions): Promise<void> {
|
|
244
|
+
return await this.sendCommand(
|
|
245
|
+
{
|
|
246
|
+
method: 'set',
|
|
247
|
+
uri: uri`/whatsapp-flows/public-key/upload`,
|
|
248
|
+
type: 'application/json',
|
|
249
|
+
resource: {
|
|
250
|
+
business_public_key: publicKey,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
opts,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
public async getFlows(opts?: ConsumeOptions): Promise<Array<WhatsappFlow>> {
|
|
258
|
+
return await this.sendCommand(
|
|
259
|
+
{
|
|
260
|
+
method: 'get',
|
|
261
|
+
uri: uri`/whatsapp-flows`,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
collection: true,
|
|
265
|
+
...opts,
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public async getFlow(id: string, opts?: ConsumeOptions): Promise<WhatsappFlow> {
|
|
271
|
+
return await this.sendCommand(
|
|
272
|
+
{
|
|
273
|
+
method: 'get',
|
|
274
|
+
uri: uri`/whatsapp-flows/${id}`,
|
|
275
|
+
},
|
|
276
|
+
opts,
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
public async publishFlow(id: string, opts?: ConsumeOptions): Promise<void> {
|
|
281
|
+
return await this.sendCommand(
|
|
282
|
+
{
|
|
283
|
+
method: 'get',
|
|
284
|
+
uri: uri`/whatsapp-flows/publish/${id}`,
|
|
285
|
+
},
|
|
286
|
+
opts,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
public async deprecateFlow(id: string, opts?: ConsumeOptions): Promise<void> {
|
|
291
|
+
return await this.sendCommand(
|
|
292
|
+
{
|
|
293
|
+
method: 'get',
|
|
294
|
+
uri: uri`/whatsapp-flows/deprecate/${id}`,
|
|
295
|
+
},
|
|
296
|
+
opts,
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
public async getFlowAssets(
|
|
301
|
+
id: string,
|
|
302
|
+
opts?: ConsumeOptions,
|
|
303
|
+
): Promise<Array<{ name: string; download_url: string }>> {
|
|
304
|
+
return await this.sendCommand(
|
|
305
|
+
{
|
|
306
|
+
method: 'get',
|
|
307
|
+
uri: uri`/whatsapp-flows/assets/${id}`,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
collection: true,
|
|
311
|
+
...opts,
|
|
312
|
+
},
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
public async deleteFlow(id: string, opts?: ConsumeOptions): Promise<void> {
|
|
317
|
+
return await this.sendCommand(
|
|
318
|
+
{
|
|
319
|
+
method: 'delete',
|
|
320
|
+
uri: uri`/whatsapp-flows/${id}`,
|
|
321
|
+
},
|
|
322
|
+
opts,
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
public async isChannelActive(opts?: ConsumeOptions): Promise<boolean> {
|
|
327
|
+
const configurations = await this.blipClient.account.getConfigurations({
|
|
328
|
+
...opts,
|
|
329
|
+
ownerIdentity: this.identity,
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
return 'IsChannelActive' in configurations && configurations.IsChannelActive === 'True'
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
public async getWabaDetails(opts?: ConsumeOptions): Promise<{
|
|
336
|
+
id: string
|
|
337
|
+
name: string
|
|
338
|
+
reviewStatus: 'APPROVED' | 'PENDING' | 'REJECTED'
|
|
339
|
+
currency: string
|
|
340
|
+
messageTemplateNamespace: string
|
|
341
|
+
}> {
|
|
342
|
+
return await this.sendCommand(
|
|
343
|
+
{
|
|
344
|
+
method: 'get',
|
|
345
|
+
uri: uri`/wabas/details`,
|
|
346
|
+
},
|
|
347
|
+
opts,
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
public async getPhoneNumberDetails(opts?: ConsumeOptions): Promise<{
|
|
352
|
+
id: string
|
|
353
|
+
account_mode: 'SANDBOX' | 'LIVE'
|
|
354
|
+
name_status: 'APPROVED' | 'DECLINED' | 'EXPIRED' | 'PENDING_REVIEW' | 'NONE'
|
|
355
|
+
new_name_status: 'APPROVED' | 'DECLINED' | 'EXPIRED' | 'PENDING_REVIEW' | 'NONE'
|
|
356
|
+
verified_name: string
|
|
357
|
+
display_phone_number: string
|
|
358
|
+
quality_rating: 'GREEN' | 'YELLOW' | 'RED' | 'UNKNOWN'
|
|
359
|
+
quality_score: {
|
|
360
|
+
score: 'GREEN' | 'YELLOW' | 'RED' | 'UNKNOWN'
|
|
361
|
+
}
|
|
362
|
+
}> {
|
|
363
|
+
return await this.sendCommand(
|
|
364
|
+
{
|
|
365
|
+
method: 'get',
|
|
366
|
+
uri: uri`/phone-number-details`,
|
|
367
|
+
},
|
|
368
|
+
opts,
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
public async getPhoneNumber(opts?: ConsumeOptions): Promise<string> {
|
|
373
|
+
const configurations = await this.blipClient.account.getConfigurations({
|
|
374
|
+
...opts,
|
|
375
|
+
ownerIdentity: this.identity,
|
|
376
|
+
})
|
|
377
|
+
if ('PhoneNumber' in configurations && 'CountryCode' in configurations) {
|
|
378
|
+
return `+${configurations.CountryCode}${configurations.PhoneNumber}`
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
throw new Error('Phone number not found')
|
|
382
|
+
}
|
|
383
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CommandMethods, CommandResponse } from '../types/command.ts'
|
|
2
|
+
import type { Reason } from '../types/reason.ts'
|
|
3
|
+
import { ReasonCodes } from '../types/reason.ts'
|
|
4
|
+
|
|
5
|
+
export class BlipError extends Error {
|
|
6
|
+
constructor(
|
|
7
|
+
public readonly uri: string,
|
|
8
|
+
public readonly cause: Reason,
|
|
9
|
+
public commandResponse?: CommandResponse<unknown, CommandMethods, 'failure'>,
|
|
10
|
+
) {
|
|
11
|
+
super(`Blip command for ${uri} failed (${cause.code}): ${cause.description}`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public get code(): number {
|
|
15
|
+
return this.cause.code
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public static isFailedCommandResponse(
|
|
19
|
+
maybeCommandResponse: unknown,
|
|
20
|
+
): maybeCommandResponse is CommandResponse<unknown, CommandMethods, 'failure'> {
|
|
21
|
+
return (
|
|
22
|
+
typeof maybeCommandResponse === 'object' &&
|
|
23
|
+
maybeCommandResponse !== null &&
|
|
24
|
+
'status' in maybeCommandResponse &&
|
|
25
|
+
maybeCommandResponse.status === 'failure'
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public static commandResponseToBlipError(
|
|
30
|
+
uri: string,
|
|
31
|
+
commandResponse: CommandResponse<unknown, CommandMethods, 'failure'>,
|
|
32
|
+
): BlipError {
|
|
33
|
+
return new BlipError(uri, commandResponse.reason, commandResponse)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static retryableBlipErrors = [
|
|
37
|
+
ReasonCodes.PipelineTimeout,
|
|
38
|
+
ReasonCodes.MaxThroughputExceeded,
|
|
39
|
+
ReasonCodes.GeneralError,
|
|
40
|
+
ReasonCodes.FailedToSendEnvelopeViaHTTP,
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CommonNotification,
|
|
3
|
+
type Envelope,
|
|
4
|
+
type FailedNotification,
|
|
5
|
+
type JsonObject,
|
|
6
|
+
type MaybePromise,
|
|
7
|
+
Node,
|
|
8
|
+
type Notification,
|
|
9
|
+
type UnknownCommand,
|
|
10
|
+
type UnknownMessage,
|
|
11
|
+
isCommand,
|
|
12
|
+
isMessage,
|
|
13
|
+
isNotification,
|
|
14
|
+
} from '../types/index.ts'
|
|
15
|
+
import { ReasonCodes } from '../types/reason.ts'
|
|
16
|
+
import type { OpenConnectionSender } from './sender.ts'
|
|
17
|
+
|
|
18
|
+
export type EventMap = {
|
|
19
|
+
message: (message: UnknownMessage) => MaybePromise<void>
|
|
20
|
+
command: (command: UnknownCommand) => MaybePromise<string | JsonObject> | MaybePromise<void>
|
|
21
|
+
notification: (notification: Notification) => MaybePromise<void>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Listener<K extends keyof EventMap> = {
|
|
25
|
+
predicate?: (...args: Parameters<EventMap[K]>) => boolean
|
|
26
|
+
callback: (...args: Parameters<EventMap[K]>) => ReturnType<EventMap[K]>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Listeners = {
|
|
30
|
+
[K in keyof EventMap]: Array<Listener<K>>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class EnvelopeResolver {
|
|
34
|
+
private readonly listeners: Listeners = {
|
|
35
|
+
message: [],
|
|
36
|
+
command: [],
|
|
37
|
+
notification: [],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private readonly waitingEnvelopeResponseResolvers: Record<string, (response: Envelope) => void> = {}
|
|
41
|
+
|
|
42
|
+
constructor(private readonly sender: OpenConnectionSender) {}
|
|
43
|
+
|
|
44
|
+
public async resolve(envelope: Envelope) {
|
|
45
|
+
const deferred = this.waitingEnvelopeResponseResolvers[envelope.id]
|
|
46
|
+
if (deferred) {
|
|
47
|
+
delete this.waitingEnvelopeResponseResolvers[envelope.id]
|
|
48
|
+
deferred(envelope)
|
|
49
|
+
} else if (isCommand(envelope)) {
|
|
50
|
+
if (envelope.method === 'get' && envelope.uri === '/ping') {
|
|
51
|
+
await this.sender.sendCommandResponse({
|
|
52
|
+
id: envelope.id,
|
|
53
|
+
to: envelope.from,
|
|
54
|
+
method: 'get',
|
|
55
|
+
type: 'application/vnd.lime.ping+json',
|
|
56
|
+
status: 'success',
|
|
57
|
+
resource: {},
|
|
58
|
+
})
|
|
59
|
+
} else {
|
|
60
|
+
try {
|
|
61
|
+
const response = await this.emit('command', envelope)
|
|
62
|
+
const type = typeof response === 'string' ? 'text/plain' : response ? 'application/json' : undefined
|
|
63
|
+
const resource = typeof response === 'string' || response ? response : undefined
|
|
64
|
+
await this.sender.sendCommandResponse({
|
|
65
|
+
id: envelope.id,
|
|
66
|
+
method: envelope.method,
|
|
67
|
+
to: envelope.from,
|
|
68
|
+
status: 'success',
|
|
69
|
+
type,
|
|
70
|
+
resource,
|
|
71
|
+
})
|
|
72
|
+
} catch (err) {
|
|
73
|
+
await this.sender.sendCommandResponse({
|
|
74
|
+
id: envelope.id,
|
|
75
|
+
method: envelope.method,
|
|
76
|
+
to: envelope.from,
|
|
77
|
+
status: 'failure',
|
|
78
|
+
reason: {
|
|
79
|
+
code: ReasonCodes.GeneralError,
|
|
80
|
+
description: err instanceof Error ? err.message : String(err),
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} else if (isMessage(envelope)) {
|
|
86
|
+
const shouldNotify =
|
|
87
|
+
!envelope.to || this.sender.session?.localNode.toIdentity() === Node.from(envelope.to).toIdentity()
|
|
88
|
+
const notify = async (notification: CommonNotification | FailedNotification) => {
|
|
89
|
+
if (shouldNotify) {
|
|
90
|
+
await this.sender.sendNotification({
|
|
91
|
+
...notification,
|
|
92
|
+
id: envelope.id,
|
|
93
|
+
to: notification.event === 'failed' ? envelope.from : (envelope.pp ?? envelope.from),
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await notify({ event: 'received' })
|
|
99
|
+
try {
|
|
100
|
+
await this.emit('message', envelope)
|
|
101
|
+
await notify({ event: 'consumed' })
|
|
102
|
+
} catch (err) {
|
|
103
|
+
await notify({
|
|
104
|
+
event: 'failed',
|
|
105
|
+
reason: {
|
|
106
|
+
code: ReasonCodes.GeneralError,
|
|
107
|
+
description: err instanceof Error ? err.message : String(err),
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
} else if (isNotification(envelope)) {
|
|
112
|
+
await this.emit('notification', envelope)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public createEnvelopeResponsePromise(id: string): Promise<Envelope> {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
this.waitingEnvelopeResponseResolvers[id] = resolve
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public addListener<K extends keyof EventMap>(ev: K, listener: Listener<K>) {
|
|
123
|
+
if (this.listeners[ev].some((l) => l.callback === listener.callback)) {
|
|
124
|
+
throw new Error(`Listener for '${ev}' already exists.`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.listeners[ev].push(listener)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public removeListener<K extends keyof EventMap>(ev: K, callback: Listener<K>['callback']) {
|
|
131
|
+
this.listeners[ev] = this.listeners[ev].filter((l) => l.callback !== callback) as Listeners[K]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async emit<K extends keyof EventMap>(
|
|
135
|
+
ev: K,
|
|
136
|
+
...args: Parameters<EventMap[K]>
|
|
137
|
+
): Promise<unknown | undefined> {
|
|
138
|
+
for (const listener of this.listeners[ev]) {
|
|
139
|
+
if (!listener.predicate || listener.predicate(...args)) {
|
|
140
|
+
const result = await listener.callback(...args)
|
|
141
|
+
if (result !== undefined) {
|
|
142
|
+
return result
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Message, MessageTypesContent } from '../../types/index.ts'
|
|
2
|
+
import { ConnectionSender, type ConnectionSenderConstructor, type Sender } from '../sender.ts'
|
|
3
|
+
|
|
4
|
+
export class CustomGatewayHttpSender extends ConnectionSender implements Sender {
|
|
5
|
+
private readonly baseurl: string
|
|
6
|
+
private readonly token: string
|
|
7
|
+
|
|
8
|
+
constructor(options: ConstructorParameters<ConnectionSenderConstructor>[0]) {
|
|
9
|
+
super(options)
|
|
10
|
+
|
|
11
|
+
if (options.authentication.scheme === 'plain') {
|
|
12
|
+
this.token = CustomGatewayHttpSender.createToken(options.node, btoa(options.authentication.password))
|
|
13
|
+
} else if (options.authentication.scheme === 'token') {
|
|
14
|
+
this.token = options.authentication.token
|
|
15
|
+
} else {
|
|
16
|
+
throw new Error('CustomGatewayHttpSender only supports plain or token authentication')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const prefix = options.tenantId ? `${options.tenantId}.` : ''
|
|
20
|
+
this.baseurl = `https://${prefix}custom.gw.${this.domain}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public async sendMessage<Type extends keyof MessageTypesContent>(message: Message<Type>): Promise<void> {
|
|
24
|
+
const response = await fetch(`${this.baseurl}/messages`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
Authorization: `Basic ${this.token}`,
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify(message),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`Failed to send message: ${response.statusText}`)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
sendCommand(): Promise<unknown> {
|
|
39
|
+
throw new Error('Custom gateway clients cannot send commands')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public static login = ConnectionSender.login<CustomGatewayHttpSender>
|
|
43
|
+
}
|