spectrum-ts 0.4.0 → 0.5.0

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.
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  asAttachment,
3
+ asContact,
3
4
  asCustom,
4
5
  stream
5
- } from "../../chunk-5XW4CAWS.js";
6
+ } from "../../chunk-UZWRB3FZ.js";
6
7
  import {
7
8
  asText,
8
9
  definePlatform
@@ -14,13 +15,152 @@ import {
14
15
  } from "@photon-ai/whatsapp-business";
15
16
 
16
17
  // src/providers/whatsapp-business/messages.ts
17
- var toMessage = (client, msg) => ({
18
- id: msg.id,
19
- content: mapContent(client, msg.content),
20
- sender: { id: msg.from },
21
- space: { id: msg.from },
22
- timestamp: msg.timestamp
23
- });
18
+ import { extension as mimeExtension } from "mime-types";
19
+ var mapWaPhoneType = (type) => {
20
+ if (!type) {
21
+ return void 0;
22
+ }
23
+ const upper = type.toUpperCase();
24
+ if (upper === "CELL" || upper === "MOBILE" || upper === "IPHONE") {
25
+ return "mobile";
26
+ }
27
+ if (upper === "HOME") {
28
+ return "home";
29
+ }
30
+ if (upper === "WORK" || upper === "BUSINESS") {
31
+ return "work";
32
+ }
33
+ return "other";
34
+ };
35
+ var mapWaSimpleType = (type) => {
36
+ if (!type) {
37
+ return void 0;
38
+ }
39
+ const upper = type.toUpperCase();
40
+ if (upper === "HOME") {
41
+ return "home";
42
+ }
43
+ if (upper === "WORK" || upper === "BUSINESS") {
44
+ return "work";
45
+ }
46
+ return "other";
47
+ };
48
+ var waNameToSpectrum = (name) => {
49
+ const result = { formatted: name.formattedName };
50
+ if (name.firstName) {
51
+ result.first = name.firstName;
52
+ }
53
+ if (name.lastName) {
54
+ result.last = name.lastName;
55
+ }
56
+ if (name.middleName) {
57
+ result.middle = name.middleName;
58
+ }
59
+ if (name.prefix) {
60
+ result.prefix = name.prefix;
61
+ }
62
+ if (name.suffix) {
63
+ result.suffix = name.suffix;
64
+ }
65
+ return result;
66
+ };
67
+ var waPhoneToSpectrum = (phone) => {
68
+ const entry = { value: phone.phone };
69
+ const type = mapWaPhoneType(phone.type);
70
+ if (type) {
71
+ entry.type = type;
72
+ }
73
+ return entry;
74
+ };
75
+ var waEmailToSpectrum = (email) => {
76
+ const entry = { value: email.email };
77
+ const type = mapWaSimpleType(email.type);
78
+ if (type) {
79
+ entry.type = type;
80
+ }
81
+ return entry;
82
+ };
83
+ var waAddressToSpectrum = (address) => {
84
+ const entry = {};
85
+ if (address.street) {
86
+ entry.street = address.street;
87
+ }
88
+ if (address.city) {
89
+ entry.city = address.city;
90
+ }
91
+ if (address.state) {
92
+ entry.region = address.state;
93
+ }
94
+ if (address.zip) {
95
+ entry.postalCode = address.zip;
96
+ }
97
+ if (address.country) {
98
+ entry.country = address.country;
99
+ }
100
+ const type = mapWaSimpleType(address.type);
101
+ if (type) {
102
+ entry.type = type;
103
+ }
104
+ return entry;
105
+ };
106
+ var waOrgToSpectrum = (org) => {
107
+ const entry = {};
108
+ if (org.company) {
109
+ entry.name = org.company;
110
+ }
111
+ if (org.title) {
112
+ entry.title = org.title;
113
+ }
114
+ if (org.department) {
115
+ entry.department = org.department;
116
+ }
117
+ return entry;
118
+ };
119
+ var waContactToSpectrum = (card) => {
120
+ const input = { raw: card };
121
+ input.name = waNameToSpectrum(card.name);
122
+ if (card.phones.length > 0) {
123
+ input.phones = card.phones.map(waPhoneToSpectrum);
124
+ }
125
+ if (card.emails.length > 0) {
126
+ input.emails = card.emails.map(waEmailToSpectrum);
127
+ }
128
+ if (card.addresses.length > 0) {
129
+ input.addresses = card.addresses.map(waAddressToSpectrum);
130
+ }
131
+ if (card.org) {
132
+ input.org = waOrgToSpectrum(card.org);
133
+ }
134
+ if (card.urls.length > 0) {
135
+ input.urls = card.urls.map((u) => u.url);
136
+ }
137
+ if (card.birthday) {
138
+ input.birthday = card.birthday;
139
+ }
140
+ return asContact(input);
141
+ };
142
+ var toMessages = (client, msg) => {
143
+ const base = {
144
+ sender: { id: msg.from },
145
+ space: { id: msg.from },
146
+ timestamp: msg.timestamp
147
+ };
148
+ if (msg.content.type === "contacts") {
149
+ const multi = msg.content.contacts.length > 1;
150
+ return msg.content.contacts.map((card, index) => ({
151
+ ...base,
152
+ id: multi ? `${msg.id}:${index}` : msg.id,
153
+ content: waContactToSpectrum(card)
154
+ }));
155
+ }
156
+ return [
157
+ {
158
+ ...base,
159
+ id: msg.id,
160
+ content: mapContent(client, msg.content)
161
+ }
162
+ ];
163
+ };
24
164
  var mapContent = (client, content) => {
25
165
  switch (content.type) {
26
166
  case "text":
@@ -34,11 +174,6 @@ var mapContent = (client, content) => {
34
174
  return asCustom({ whatsapp_type: "sticker", ...content.sticker });
35
175
  case "location":
36
176
  return asCustom({ whatsapp_type: "location", ...content.location });
37
- case "contacts":
38
- return asCustom({
39
- whatsapp_type: "contacts",
40
- contacts: content.contacts
41
- });
42
177
  case "reaction":
43
178
  return asCustom({ whatsapp_type: "reaction", ...content.reaction });
44
179
  case "interactive":
@@ -85,6 +220,74 @@ var mimeToMediaType = (mimeType) => {
85
220
  }
86
221
  return "document";
87
222
  };
223
+ var voiceFilename = (content) => {
224
+ if (content.name) {
225
+ return content.name;
226
+ }
227
+ const ext = mimeExtension(content.mimeType);
228
+ return ext ? `voice.${ext}` : "voice";
229
+ };
230
+ var spectrumPhoneTypeToWa = (type) => {
231
+ if (type === "mobile") {
232
+ return "CELL";
233
+ }
234
+ if (type === "home" || type === "work" || type === "other") {
235
+ return type.toUpperCase();
236
+ }
237
+ return void 0;
238
+ };
239
+ var spectrumSimpleTypeToWa = (type) => type ? type.toUpperCase() : void 0;
240
+ var spectrumNameToWa = (name) => ({
241
+ formattedName: name?.formatted ?? ([name?.first, name?.middle, name?.last].filter((p) => Boolean(p)).join(" ") || "Unknown"),
242
+ firstName: name?.first,
243
+ lastName: name?.last,
244
+ middleName: name?.middle,
245
+ prefix: name?.prefix,
246
+ suffix: name?.suffix
247
+ });
248
+ var isWhatsAppContactCard = (value) => {
249
+ if (!value || typeof value !== "object") {
250
+ return false;
251
+ }
252
+ const raw = value;
253
+ const name = raw.name;
254
+ if (!name || typeof name !== "object" || typeof name.formattedName !== "string") {
255
+ return false;
256
+ }
257
+ return Array.isArray(raw.phones) && Array.isArray(raw.emails) && Array.isArray(raw.addresses) && Array.isArray(raw.urls);
258
+ };
259
+ var contactToWa = (contact) => {
260
+ if (isWhatsAppContactCard(contact.raw)) {
261
+ return contact.raw;
262
+ }
263
+ const card = {
264
+ name: spectrumNameToWa(contact.name),
265
+ phones: (contact.phones ?? []).map((p) => ({
266
+ phone: p.value,
267
+ type: spectrumPhoneTypeToWa(p.type)
268
+ })),
269
+ emails: (contact.emails ?? []).map((e) => ({
270
+ email: e.value,
271
+ type: spectrumSimpleTypeToWa(e.type)
272
+ })),
273
+ addresses: (contact.addresses ?? []).map((a) => ({
274
+ street: a.street,
275
+ city: a.city,
276
+ state: a.region,
277
+ zip: a.postalCode,
278
+ country: a.country,
279
+ type: spectrumSimpleTypeToWa(a.type)
280
+ })),
281
+ urls: (contact.urls ?? []).map((url) => ({ url })),
282
+ org: contact.org?.name || contact.org?.department || contact.org?.title ? {
283
+ company: contact.org.name,
284
+ department: contact.org.department,
285
+ title: contact.org.title
286
+ } : void 0,
287
+ birthday: contact.birthday
288
+ };
289
+ return card;
290
+ };
88
291
  var messages = (client) => {
89
292
  const eventStream = client.events.subscribe().filter(
90
293
  (e) => e.type === "message"
@@ -93,7 +296,9 @@ var messages = (client) => {
93
296
  (async () => {
94
297
  try {
95
298
  for await (const event of eventStream) {
96
- emit(toMessage(client, event.message));
299
+ for (const m of toMessages(client, event.message)) {
300
+ emit(m);
301
+ }
97
302
  }
98
303
  end();
99
304
  } catch (e) {
@@ -122,6 +327,24 @@ var send = async (client, spaceId, content) => {
122
327
  });
123
328
  break;
124
329
  }
330
+ case "contact":
331
+ await client.messages.send({
332
+ to: spaceId,
333
+ contacts: [contactToWa(content)]
334
+ });
335
+ break;
336
+ case "voice": {
337
+ const { mediaId } = await client.media.upload({
338
+ file: await content.read(),
339
+ mimeType: content.mimeType,
340
+ filename: voiceFilename(content)
341
+ });
342
+ await client.messages.send({
343
+ to: spaceId,
344
+ audio: { id: mediaId }
345
+ });
346
+ break;
347
+ }
125
348
  default:
126
349
  break;
127
350
  }
@@ -156,6 +379,26 @@ var replyToMessage = async (client, spaceId, messageId, content) => {
156
379
  });
157
380
  break;
158
381
  }
382
+ case "contact":
383
+ await client.messages.send({
384
+ to: spaceId,
385
+ replyTo: messageId,
386
+ contacts: [contactToWa(content)]
387
+ });
388
+ break;
389
+ case "voice": {
390
+ const { mediaId } = await client.media.upload({
391
+ file: await content.read(),
392
+ mimeType: content.mimeType,
393
+ filename: voiceFilename(content)
394
+ });
395
+ await client.messages.send({
396
+ to: spaceId,
397
+ replyTo: messageId,
398
+ audio: { id: mediaId }
399
+ });
400
+ break;
401
+ }
159
402
  default:
160
403
  break;
161
404
  }
@@ -14,6 +14,70 @@ declare const contentSchema: z__default.ZodDiscriminatedUnion<[z__default.ZodObj
14
14
  size: z__default.ZodOptional<z__default.ZodNumber>;
15
15
  read: z__default.ZodFunction<z__default.ZodTuple<readonly [], null>, z__default.ZodPromise<z__default.ZodCustom<Buffer<ArrayBufferLike>, Buffer<ArrayBufferLike>>>>;
16
16
  stream: z__default.ZodFunction<z__default.ZodTuple<readonly [], null>, z__default.ZodPromise<z__default.ZodCustom<ReadableStream<unknown>, ReadableStream<unknown>>>>;
17
+ }, z__default.core.$strip>, z__default.ZodObject<{
18
+ type: z__default.ZodLiteral<"contact">;
19
+ user: z__default.ZodOptional<z__default.ZodObject<{
20
+ __platform: z__default.ZodString;
21
+ id: z__default.ZodString;
22
+ }, z__default.core.$strip>>;
23
+ name: z__default.ZodOptional<z__default.ZodObject<{
24
+ formatted: z__default.ZodOptional<z__default.ZodString>;
25
+ first: z__default.ZodOptional<z__default.ZodString>;
26
+ last: z__default.ZodOptional<z__default.ZodString>;
27
+ middle: z__default.ZodOptional<z__default.ZodString>;
28
+ prefix: z__default.ZodOptional<z__default.ZodString>;
29
+ suffix: z__default.ZodOptional<z__default.ZodString>;
30
+ }, z__default.core.$strip>>;
31
+ phones: z__default.ZodOptional<z__default.ZodArray<z__default.ZodObject<{
32
+ value: z__default.ZodString;
33
+ type: z__default.ZodOptional<z__default.ZodEnum<{
34
+ mobile: "mobile";
35
+ home: "home";
36
+ work: "work";
37
+ other: "other";
38
+ }>>;
39
+ }, z__default.core.$strip>>>;
40
+ emails: z__default.ZodOptional<z__default.ZodArray<z__default.ZodObject<{
41
+ value: z__default.ZodString;
42
+ type: z__default.ZodOptional<z__default.ZodEnum<{
43
+ home: "home";
44
+ work: "work";
45
+ other: "other";
46
+ }>>;
47
+ }, z__default.core.$strip>>>;
48
+ addresses: z__default.ZodOptional<z__default.ZodArray<z__default.ZodObject<{
49
+ street: z__default.ZodOptional<z__default.ZodString>;
50
+ city: z__default.ZodOptional<z__default.ZodString>;
51
+ region: z__default.ZodOptional<z__default.ZodString>;
52
+ postalCode: z__default.ZodOptional<z__default.ZodString>;
53
+ country: z__default.ZodOptional<z__default.ZodString>;
54
+ type: z__default.ZodOptional<z__default.ZodEnum<{
55
+ home: "home";
56
+ work: "work";
57
+ other: "other";
58
+ }>>;
59
+ }, z__default.core.$strip>>>;
60
+ org: z__default.ZodOptional<z__default.ZodObject<{
61
+ name: z__default.ZodOptional<z__default.ZodString>;
62
+ title: z__default.ZodOptional<z__default.ZodString>;
63
+ department: z__default.ZodOptional<z__default.ZodString>;
64
+ }, z__default.core.$strip>>;
65
+ urls: z__default.ZodOptional<z__default.ZodArray<z__default.ZodString>>;
66
+ birthday: z__default.ZodOptional<z__default.ZodString>;
67
+ note: z__default.ZodOptional<z__default.ZodString>;
68
+ photo: z__default.ZodOptional<z__default.ZodObject<{
69
+ mimeType: z__default.ZodString;
70
+ read: z__default.ZodFunction<z__default.ZodTuple<readonly [], null>, z__default.ZodPromise<z__default.ZodCustom<Buffer<ArrayBufferLike>, Buffer<ArrayBufferLike>>>>;
71
+ }, z__default.core.$strip>>;
72
+ raw: z__default.ZodOptional<z__default.ZodUnknown>;
73
+ }, z__default.core.$strip>, z__default.ZodObject<{
74
+ type: z__default.ZodLiteral<"voice">;
75
+ name: z__default.ZodOptional<z__default.ZodString>;
76
+ mimeType: z__default.ZodString;
77
+ duration: z__default.ZodOptional<z__default.ZodNumber>;
78
+ size: z__default.ZodOptional<z__default.ZodNumber>;
79
+ read: z__default.ZodFunction<z__default.ZodTuple<readonly [], null>, z__default.ZodPromise<z__default.ZodCustom<Buffer<ArrayBufferLike>, Buffer<ArrayBufferLike>>>>;
80
+ stream: z__default.ZodFunction<z__default.ZodTuple<readonly [], null>, z__default.ZodPromise<z__default.ZodCustom<ReadableStream<unknown>, ReadableStream<unknown>>>>;
17
81
  }, z__default.core.$strip>], "type">;
18
82
  type Content = z__default.infer<typeof contentSchema>;
19
83
  interface ContentBuilder {
@@ -21,6 +85,11 @@ interface ContentBuilder {
21
85
  }
22
86
  type ContentInput = string | ContentBuilder;
23
87
 
88
+ interface User {
89
+ readonly __platform: string;
90
+ readonly id: string;
91
+ }
92
+
24
93
  interface Space<_Def = unknown> {
25
94
  readonly __platform: string;
26
95
  readonly id: string;
@@ -30,11 +99,6 @@ interface Space<_Def = unknown> {
30
99
  stopTyping(): Promise<void>;
31
100
  }
32
101
 
33
- interface User {
34
- readonly __platform: string;
35
- readonly id: string;
36
- }
37
-
38
102
  interface Message<TPlatform extends string = string, TSender extends User = User, TSpace extends Space = Space> {
39
103
  content: Content;
40
104
  readonly id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectrum-ts",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -20,16 +20,23 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@photon-ai/advanced-imessage": "^0.4.3",
23
- "@photon-ai/whatsapp-business": "^0.1.1",
24
23
  "@photon-ai/imessage-kit": "^3.0.0-rc.2",
24
+ "@photon-ai/whatsapp-business": "^0.1.1",
25
25
  "@repeaterjs/repeater": "^3.0.6",
26
26
  "better-grpc": "^0.3.2",
27
27
  "mime-types": "^3.0.1",
28
28
  "type-fest": "^5.4.1",
29
+ "vcf": "^2.1.2",
29
30
  "zod": "^4.2.1"
30
31
  },
31
32
  "peerDependencies": {
33
+ "ffmpeg-static": "^5",
32
34
  "typescript": "^5"
33
35
  },
36
+ "peerDependenciesMeta": {
37
+ "ffmpeg-static": {
38
+ "optional": true
39
+ }
40
+ },
34
41
  "license": "MIT"
35
42
  }
@@ -1,165 +0,0 @@
1
- // src/content/attachment.ts
2
- import { createReadStream } from "fs";
3
- import { readFile, stat } from "fs/promises";
4
- import { basename } from "path";
5
- import { Readable } from "stream";
6
- import { lookup as lookupMimeType } from "mime-types";
7
- import z from "zod";
8
- var DEFAULT_ATTACHMENT_NAME = "attachment";
9
- var readSchema = z.function({
10
- input: [],
11
- output: z.promise(z.instanceof(Buffer))
12
- });
13
- var streamSchema = z.function({
14
- input: [],
15
- output: z.promise(z.instanceof(ReadableStream))
16
- });
17
- var attachmentSchema = z.object({
18
- type: z.literal("attachment"),
19
- name: z.string().nonempty(),
20
- mimeType: z.string().nonempty(),
21
- size: z.number().int().nonnegative().optional(),
22
- read: readSchema,
23
- stream: streamSchema
24
- });
25
- var resolveAttachmentName = (input, name) => name || (typeof input === "string" ? basename(input) : DEFAULT_ATTACHMENT_NAME);
26
- var resolveAttachmentMimeType = (name, mimeType) => {
27
- if (mimeType) {
28
- return mimeType;
29
- }
30
- const resolvedMimeType = lookupMimeType(name);
31
- if (!resolvedMimeType) {
32
- throw new Error(
33
- `Unable to resolve MIME type for attachment "${name}". Pass options.mimeType explicitly.`
34
- );
35
- }
36
- return resolvedMimeType;
37
- };
38
- var bufferToStream = (buf) => new ReadableStream({
39
- start(controller) {
40
- controller.enqueue(buf);
41
- controller.close();
42
- }
43
- });
44
- var asAttachment = (input) => {
45
- let cached;
46
- const read = () => {
47
- cached ??= input.read().catch((err) => {
48
- cached = void 0;
49
- throw err;
50
- });
51
- return cached;
52
- };
53
- const stream2 = input.stream ?? (async () => bufferToStream(await read()));
54
- return attachmentSchema.parse({
55
- type: "attachment",
56
- name: input.name,
57
- mimeType: input.mimeType,
58
- size: input.size,
59
- read,
60
- stream: stream2
61
- });
62
- };
63
- function attachment(input, options) {
64
- return {
65
- build: async () => {
66
- const name = resolveAttachmentName(input, options?.name);
67
- const mimeType = resolveAttachmentMimeType(name, options?.mimeType);
68
- if (typeof input === "string") {
69
- const stats = await stat(input);
70
- return asAttachment({
71
- name,
72
- mimeType,
73
- size: stats.size,
74
- read: () => readFile(input),
75
- stream: async () => Readable.toWeb(
76
- createReadStream(input)
77
- )
78
- });
79
- }
80
- return asAttachment({
81
- name,
82
- mimeType,
83
- size: input.byteLength,
84
- read: async () => input,
85
- stream: async () => bufferToStream(input)
86
- });
87
- }
88
- };
89
- }
90
-
91
- // src/content/custom.ts
92
- import z2 from "zod";
93
- var customSchema = z2.object({
94
- type: z2.literal("custom"),
95
- raw: z2.unknown()
96
- });
97
- var asCustom = (raw) => customSchema.parse({ type: "custom", raw });
98
- function custom(raw) {
99
- return {
100
- build: async () => asCustom(raw)
101
- };
102
- }
103
-
104
- // src/utils/stream.ts
105
- import { Repeater } from "@repeaterjs/repeater";
106
- function stream(setup) {
107
- const repeater = new Repeater(async (push, stop) => {
108
- const emit = (value) => {
109
- Promise.resolve(push(value)).catch((error) => {
110
- stop(error);
111
- return void 0;
112
- });
113
- };
114
- const end = (error) => {
115
- stop(error);
116
- };
117
- const cleanup = await setup(emit, end);
118
- try {
119
- await stop;
120
- } finally {
121
- await cleanup?.();
122
- }
123
- });
124
- return Object.assign(repeater, {
125
- close: async () => {
126
- await repeater.return(void 0);
127
- }
128
- });
129
- }
130
- function mergeStreams(streams) {
131
- return stream((emit, end) => {
132
- if (streams.length === 0) {
133
- end();
134
- return;
135
- }
136
- let openStreams = streams.length;
137
- const workers = streams.map(async (source) => {
138
- try {
139
- for await (const value of source) {
140
- emit(value);
141
- }
142
- } catch (error) {
143
- end(error);
144
- } finally {
145
- openStreams -= 1;
146
- if (openStreams === 0) {
147
- end();
148
- }
149
- }
150
- });
151
- return async () => {
152
- await Promise.allSettled(streams.map((source) => source.close()));
153
- await Promise.allSettled(workers);
154
- };
155
- });
156
- }
157
-
158
- export {
159
- asAttachment,
160
- attachment,
161
- asCustom,
162
- custom,
163
- stream,
164
- mergeStreams
165
- };