spectrum-ts 0.9.0 → 1.0.1

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.
@@ -0,0 +1,1221 @@
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 z2 from "zod";
8
+
9
+ // src/utils/io.ts
10
+ import z from "zod";
11
+ var readSchema = z.function({
12
+ input: [],
13
+ output: z.promise(z.instanceof(Buffer))
14
+ });
15
+ var streamSchema = z.function({
16
+ input: [],
17
+ output: z.promise(z.instanceof(ReadableStream))
18
+ });
19
+ var bufferToStream = (buf) => new ReadableStream({
20
+ start(controller) {
21
+ controller.enqueue(buf);
22
+ controller.close();
23
+ }
24
+ });
25
+
26
+ // src/content/attachment.ts
27
+ var DEFAULT_ATTACHMENT_NAME = "attachment";
28
+ var attachmentSchema = z2.object({
29
+ type: z2.literal("attachment"),
30
+ name: z2.string().nonempty(),
31
+ mimeType: z2.string().nonempty(),
32
+ size: z2.number().int().nonnegative().optional(),
33
+ read: readSchema,
34
+ stream: streamSchema
35
+ });
36
+ var resolveAttachmentName = (input, name) => name || (typeof input === "string" ? basename(input) : DEFAULT_ATTACHMENT_NAME);
37
+ var resolveAttachmentMimeType = (name, mimeType) => {
38
+ if (mimeType) {
39
+ return mimeType;
40
+ }
41
+ const resolvedMimeType = lookupMimeType(name);
42
+ if (!resolvedMimeType) {
43
+ throw new Error(
44
+ `Unable to resolve MIME type for attachment "${name}". Pass options.mimeType explicitly.`
45
+ );
46
+ }
47
+ return resolvedMimeType;
48
+ };
49
+ var asAttachment = (input) => {
50
+ let cached;
51
+ const read = () => {
52
+ cached ??= input.read().catch((err) => {
53
+ cached = void 0;
54
+ throw err;
55
+ });
56
+ return cached;
57
+ };
58
+ const stream = input.stream ?? (async () => bufferToStream(await read()));
59
+ return attachmentSchema.parse({
60
+ type: "attachment",
61
+ name: input.name,
62
+ mimeType: input.mimeType,
63
+ size: input.size,
64
+ read,
65
+ stream
66
+ });
67
+ };
68
+ function attachment(input, options) {
69
+ return {
70
+ build: async () => {
71
+ const name = resolveAttachmentName(input, options?.name);
72
+ const mimeType = resolveAttachmentMimeType(name, options?.mimeType);
73
+ if (typeof input === "string") {
74
+ const stats = await stat(input);
75
+ return asAttachment({
76
+ name,
77
+ mimeType,
78
+ size: stats.size,
79
+ read: () => readFile(input),
80
+ stream: async () => Readable.toWeb(
81
+ createReadStream(input)
82
+ )
83
+ });
84
+ }
85
+ return asAttachment({
86
+ name,
87
+ mimeType,
88
+ size: input.byteLength,
89
+ read: async () => input,
90
+ stream: async () => bufferToStream(input)
91
+ });
92
+ }
93
+ };
94
+ }
95
+
96
+ // src/utils/vcard.ts
97
+ import vCard from "vcf";
98
+ var asPropertyArray = (prop) => {
99
+ if (!prop) {
100
+ return [];
101
+ }
102
+ const arr = Array.isArray(prop) ? prop : [prop];
103
+ return arr;
104
+ };
105
+ var propString = (prop) => {
106
+ const [first] = asPropertyArray(prop);
107
+ const value = first?.valueOf().trim();
108
+ return value ? value : void 0;
109
+ };
110
+ var paramTypes = (prop) => {
111
+ const { type } = prop;
112
+ if (!type) {
113
+ return [];
114
+ }
115
+ return (Array.isArray(type) ? type : [type]).map((t) => t.toLowerCase());
116
+ };
117
+ var mapPhoneType = (prop) => {
118
+ const types = paramTypes(prop);
119
+ if (types.some((t) => t === "cell" || t === "mobile" || t === "iphone")) {
120
+ return "mobile";
121
+ }
122
+ if (types.includes("home")) {
123
+ return "home";
124
+ }
125
+ if (types.includes("work")) {
126
+ return "work";
127
+ }
128
+ if (types.length > 0) {
129
+ return "other";
130
+ }
131
+ return void 0;
132
+ };
133
+ var mapSimpleType = (prop) => {
134
+ const types = paramTypes(prop);
135
+ if (types.includes("home")) {
136
+ return "home";
137
+ }
138
+ if (types.includes("work")) {
139
+ return "work";
140
+ }
141
+ if (types.length > 0) {
142
+ return "other";
143
+ }
144
+ return void 0;
145
+ };
146
+ var splitStructured = (value) => value.split(";").map((part) => part.trim());
147
+ var extractName = (card) => {
148
+ const fn = propString(card.data.fn);
149
+ const n = propString(card.data.n);
150
+ if (!(fn || n)) {
151
+ return void 0;
152
+ }
153
+ const result = {};
154
+ if (fn) {
155
+ result.formatted = fn;
156
+ }
157
+ if (n) {
158
+ const [last, first, middle, prefix, suffix] = splitStructured(n);
159
+ if (first) {
160
+ result.first = first;
161
+ }
162
+ if (last) {
163
+ result.last = last;
164
+ }
165
+ if (middle) {
166
+ result.middle = middle;
167
+ }
168
+ if (prefix) {
169
+ result.prefix = prefix;
170
+ }
171
+ if (suffix) {
172
+ result.suffix = suffix;
173
+ }
174
+ }
175
+ return result;
176
+ };
177
+ var extractPhones = (card) => {
178
+ const props = asPropertyArray(card.data.tel);
179
+ if (props.length === 0) {
180
+ return void 0;
181
+ }
182
+ return props.map((p) => {
183
+ const entry = { value: p.valueOf().trim() };
184
+ const type = mapPhoneType(p);
185
+ if (type) {
186
+ entry.type = type;
187
+ }
188
+ return entry;
189
+ });
190
+ };
191
+ var extractEmails = (card) => {
192
+ const props = asPropertyArray(card.data.email);
193
+ if (props.length === 0) {
194
+ return void 0;
195
+ }
196
+ return props.map((p) => {
197
+ const entry = { value: p.valueOf().trim() };
198
+ const type = mapSimpleType(p);
199
+ if (type) {
200
+ entry.type = type;
201
+ }
202
+ return entry;
203
+ });
204
+ };
205
+ var extractAddresses = (card) => {
206
+ const props = asPropertyArray(card.data.adr);
207
+ if (props.length === 0) {
208
+ return void 0;
209
+ }
210
+ return props.map((p) => {
211
+ const [, , street, city, region, postalCode, country] = splitStructured(
212
+ p.valueOf()
213
+ );
214
+ const entry = {};
215
+ if (street) {
216
+ entry.street = street;
217
+ }
218
+ if (city) {
219
+ entry.city = city;
220
+ }
221
+ if (region) {
222
+ entry.region = region;
223
+ }
224
+ if (postalCode) {
225
+ entry.postalCode = postalCode;
226
+ }
227
+ if (country) {
228
+ entry.country = country;
229
+ }
230
+ const type = mapSimpleType(p);
231
+ if (type) {
232
+ entry.type = type;
233
+ }
234
+ return entry;
235
+ });
236
+ };
237
+ var extractOrg = (card) => {
238
+ const orgStr = propString(card.data.org);
239
+ const title = propString(card.data.title);
240
+ if (!(orgStr || title)) {
241
+ return void 0;
242
+ }
243
+ const result = {};
244
+ if (orgStr) {
245
+ const [name, department] = splitStructured(orgStr);
246
+ if (name) {
247
+ result.name = name;
248
+ }
249
+ if (department) {
250
+ result.department = department;
251
+ }
252
+ }
253
+ if (title) {
254
+ result.title = title;
255
+ }
256
+ return result;
257
+ };
258
+ var extractUrls = (card) => {
259
+ const props = asPropertyArray(card.data.url);
260
+ if (props.length === 0) {
261
+ return void 0;
262
+ }
263
+ return props.map((p) => p.valueOf().trim());
264
+ };
265
+ var photoMimeFromType = (type) => {
266
+ if (!type) {
267
+ return "image/jpeg";
268
+ }
269
+ const lower = type.toLowerCase();
270
+ if (lower.startsWith("image/")) {
271
+ return lower;
272
+ }
273
+ return `image/${lower}`;
274
+ };
275
+ var DATA_URI_PATTERN = /^data:([^;,]+);base64,(.*)$/i;
276
+ var extractPhoto = (card) => {
277
+ const [prop] = asPropertyArray(card.data.photo);
278
+ if (!prop) {
279
+ return void 0;
280
+ }
281
+ const value = prop.valueOf();
282
+ const dataUriMatch = DATA_URI_PATTERN.exec(value);
283
+ if (dataUriMatch) {
284
+ const [, mimeType, base64] = dataUriMatch;
285
+ const buf2 = Buffer.from(base64 ?? "", "base64");
286
+ return {
287
+ mimeType: mimeType ?? "image/jpeg",
288
+ read: async () => buf2
289
+ };
290
+ }
291
+ const type = Array.isArray(prop.type) ? prop.type[0] : prop.type;
292
+ const buf = Buffer.from(value, "base64");
293
+ return {
294
+ mimeType: photoMimeFromType(type),
295
+ read: async () => buf
296
+ };
297
+ };
298
+ var normalizeVCardInput = (vcf) => {
299
+ const withoutBom = vcf.charCodeAt(0) === 65279 ? vcf.slice(1) : vcf;
300
+ return withoutBom.replace(/\r\n|\r|\n/g, "\r\n");
301
+ };
302
+ var fromVCard = (vcf) => {
303
+ const [card] = vCard.parse(normalizeVCardInput(vcf));
304
+ if (!card) {
305
+ throw new Error("Invalid vCard: no cards parsed");
306
+ }
307
+ const input = { raw: vcf };
308
+ const name = extractName(card);
309
+ if (name) {
310
+ input.name = name;
311
+ }
312
+ const phones = extractPhones(card);
313
+ if (phones) {
314
+ input.phones = phones;
315
+ }
316
+ const emails = extractEmails(card);
317
+ if (emails) {
318
+ input.emails = emails;
319
+ }
320
+ const addresses = extractAddresses(card);
321
+ if (addresses) {
322
+ input.addresses = addresses;
323
+ }
324
+ const org = extractOrg(card);
325
+ if (org) {
326
+ input.org = org;
327
+ }
328
+ const urls = extractUrls(card);
329
+ if (urls) {
330
+ input.urls = urls;
331
+ }
332
+ const birthday = propString(card.data.bday);
333
+ if (birthday) {
334
+ input.birthday = birthday;
335
+ }
336
+ const note = propString(card.data.note);
337
+ if (note) {
338
+ input.note = note;
339
+ }
340
+ const photo = extractPhoto(card);
341
+ if (photo) {
342
+ input.photo = photo;
343
+ }
344
+ return input;
345
+ };
346
+ var formattedNameFor = (name) => {
347
+ if (name?.formatted) {
348
+ return name.formatted;
349
+ }
350
+ const parts = [name?.first, name?.middle, name?.last].filter(
351
+ (p) => Boolean(p)
352
+ );
353
+ if (parts.length > 0) {
354
+ return parts.join(" ");
355
+ }
356
+ return "Unknown";
357
+ };
358
+ var phoneTypeParam = (type) => {
359
+ if (type === "mobile") {
360
+ return "CELL";
361
+ }
362
+ if (type === "home" || type === "work" || type === "other") {
363
+ return type.toUpperCase();
364
+ }
365
+ return void 0;
366
+ };
367
+ var simpleTypeParam = (type) => type ? type.toUpperCase() : void 0;
368
+ var photoTypeParam = (mimeType) => {
369
+ const sub = mimeType.split("/")[1] ?? "jpeg";
370
+ return sub.toUpperCase();
371
+ };
372
+ var writeName = (card, name) => {
373
+ card.set("fn", formattedNameFor(name));
374
+ if (!name) {
375
+ return;
376
+ }
377
+ if (name.first || name.last || name.middle || name.prefix || name.suffix) {
378
+ card.set(
379
+ "n",
380
+ [
381
+ name.last ?? "",
382
+ name.first ?? "",
383
+ name.middle ?? "",
384
+ name.prefix ?? "",
385
+ name.suffix ?? ""
386
+ ].join(";")
387
+ );
388
+ }
389
+ };
390
+ var writePhones = (card, phones) => {
391
+ for (const phone of phones ?? []) {
392
+ const type = phoneTypeParam(phone.type);
393
+ card.add("tel", phone.value, type ? { type } : void 0);
394
+ }
395
+ };
396
+ var writeEmails = (card, emails) => {
397
+ for (const email of emails ?? []) {
398
+ const type = simpleTypeParam(email.type);
399
+ card.add("email", email.value, type ? { type } : void 0);
400
+ }
401
+ };
402
+ var writeAddresses = (card, addresses) => {
403
+ for (const addr of addresses ?? []) {
404
+ const value = [
405
+ "",
406
+ "",
407
+ addr.street ?? "",
408
+ addr.city ?? "",
409
+ addr.region ?? "",
410
+ addr.postalCode ?? "",
411
+ addr.country ?? ""
412
+ ].join(";");
413
+ const type = simpleTypeParam(addr.type);
414
+ card.add("adr", value, type ? { type } : void 0);
415
+ }
416
+ };
417
+ var writeOrg = (card, org) => {
418
+ if (!org) {
419
+ return;
420
+ }
421
+ if (org.name || org.department) {
422
+ card.set("org", [org.name ?? "", org.department ?? ""].join(";"));
423
+ }
424
+ if (org.title) {
425
+ card.set("title", org.title);
426
+ }
427
+ };
428
+ var writeUrls = (card, urls) => {
429
+ for (const url of urls ?? []) {
430
+ card.add("url", url);
431
+ }
432
+ };
433
+ var writePhoto = async (card, photo) => {
434
+ if (!photo) {
435
+ return;
436
+ }
437
+ const buf = await photo.read();
438
+ card.set("photo", buf.toString("base64"), {
439
+ encoding: "b",
440
+ type: photoTypeParam(photo.mimeType)
441
+ });
442
+ };
443
+ var toVCard = async (contact2) => {
444
+ if (typeof contact2.raw === "string" && contact2.raw.startsWith("BEGIN:VCARD")) {
445
+ return contact2.raw;
446
+ }
447
+ const card = new vCard();
448
+ writeName(card, contact2.name);
449
+ writePhones(card, contact2.phones);
450
+ writeEmails(card, contact2.emails);
451
+ writeAddresses(card, contact2.addresses);
452
+ writeOrg(card, contact2.org);
453
+ writeUrls(card, contact2.urls);
454
+ if (contact2.birthday) {
455
+ card.set("bday", contact2.birthday);
456
+ }
457
+ if (contact2.note) {
458
+ card.set("note", contact2.note);
459
+ }
460
+ await writePhoto(card, contact2.photo);
461
+ return card.toString();
462
+ };
463
+
464
+ // src/content/contact.ts
465
+ import vCard2 from "vcf";
466
+ import z3 from "zod";
467
+ var userRefSchema = z3.object({
468
+ __platform: z3.string(),
469
+ id: z3.string()
470
+ });
471
+ var nameSchema = z3.object({
472
+ formatted: z3.string().optional(),
473
+ first: z3.string().optional(),
474
+ last: z3.string().optional(),
475
+ middle: z3.string().optional(),
476
+ prefix: z3.string().optional(),
477
+ suffix: z3.string().optional()
478
+ });
479
+ var phoneTypeSchema = z3.enum(["mobile", "home", "work", "other"]);
480
+ var emailTypeSchema = z3.enum(["home", "work", "other"]);
481
+ var addressTypeSchema = z3.enum(["home", "work", "other"]);
482
+ var phoneSchema = z3.object({
483
+ value: z3.string(),
484
+ type: phoneTypeSchema.optional()
485
+ });
486
+ var emailSchema = z3.object({
487
+ value: z3.string(),
488
+ type: emailTypeSchema.optional()
489
+ });
490
+ var addressSchema = z3.object({
491
+ street: z3.string().optional(),
492
+ city: z3.string().optional(),
493
+ region: z3.string().optional(),
494
+ postalCode: z3.string().optional(),
495
+ country: z3.string().optional(),
496
+ type: addressTypeSchema.optional()
497
+ });
498
+ var orgSchema = z3.object({
499
+ name: z3.string().optional(),
500
+ title: z3.string().optional(),
501
+ department: z3.string().optional()
502
+ });
503
+ var photoSchema = z3.object({
504
+ mimeType: z3.string(),
505
+ read: readSchema
506
+ });
507
+ var contactSchema = z3.object({
508
+ type: z3.literal("contact"),
509
+ user: userRefSchema.optional(),
510
+ name: nameSchema.optional(),
511
+ phones: z3.array(phoneSchema).optional(),
512
+ emails: z3.array(emailSchema).optional(),
513
+ addresses: z3.array(addressSchema).optional(),
514
+ org: orgSchema.optional(),
515
+ urls: z3.array(z3.string()).optional(),
516
+ birthday: z3.string().optional(),
517
+ note: z3.string().optional(),
518
+ photo: photoSchema.optional(),
519
+ raw: z3.unknown().optional()
520
+ });
521
+ var asContact = (input) => contactSchema.parse({ type: "contact", ...input });
522
+ var isUser = (value) => typeof value === "object" && value !== null && "__platform" in value && "id" in value && typeof value.__platform === "string" && typeof value.id === "string";
523
+ function contact(input, details) {
524
+ return {
525
+ build: async () => {
526
+ if (typeof input === "string") {
527
+ return asContact(fromVCard(input));
528
+ }
529
+ if (input instanceof vCard2) {
530
+ return asContact(fromVCard(input.toString()));
531
+ }
532
+ if (isUser(input)) {
533
+ return asContact({
534
+ user: { __platform: input.__platform, id: input.id },
535
+ ...details
536
+ });
537
+ }
538
+ return asContact(input);
539
+ }
540
+ };
541
+ }
542
+
543
+ // src/content/custom.ts
544
+ import z4 from "zod";
545
+ var customSchema = z4.object({
546
+ type: z4.literal("custom"),
547
+ raw: z4.unknown()
548
+ });
549
+ var asCustom = (raw) => customSchema.parse({ type: "custom", raw });
550
+ function custom(raw) {
551
+ return {
552
+ build: async () => asCustom(raw)
553
+ };
554
+ }
555
+
556
+ // src/content/text.ts
557
+ import z5 from "zod";
558
+ var textSchema = z5.object({
559
+ type: z5.literal("text"),
560
+ text: z5.string().nonempty()
561
+ });
562
+ var asText = (text2) => textSchema.parse({ type: "text", text: text2 });
563
+ function text(text2) {
564
+ return {
565
+ build: async () => asText(text2)
566
+ };
567
+ }
568
+
569
+ // src/content/resolve.ts
570
+ var resolveContents = (items) => Promise.all(
571
+ items.map((c) => typeof c === "string" ? text(c).build() : c.build())
572
+ );
573
+
574
+ // src/content/reaction.ts
575
+ import z6 from "zod";
576
+ var isMessage = (v) => typeof v === "object" && v !== null && "id" in v && "content" in v;
577
+ var reactionSchema = z6.object({
578
+ type: z6.literal("reaction"),
579
+ emoji: z6.string().min(1),
580
+ target: z6.custom(isMessage, {
581
+ message: "reaction target must be a Message"
582
+ })
583
+ });
584
+ var asReaction = (input) => reactionSchema.parse({ type: "reaction", ...input });
585
+ function reaction(emoji, target) {
586
+ return { build: async () => asReaction({ emoji, target }) };
587
+ }
588
+
589
+ // src/utils/errors.ts
590
+ var composeMessage = (opts) => {
591
+ const platform = opts.platform ?? "platform";
592
+ const subject = opts.kind === "content" ? `content type "${opts.contentType ?? "unknown"}"` : `action "${opts.action ?? "unknown"}"`;
593
+ const detail = opts.detail ? `: ${opts.detail}` : "";
594
+ return `${platform} does not support ${subject}${detail}`;
595
+ };
596
+ var UnsupportedError = class _UnsupportedError extends Error {
597
+ kind;
598
+ platform;
599
+ contentType;
600
+ action;
601
+ detail;
602
+ constructor(opts) {
603
+ super(composeMessage(opts));
604
+ this.name = "UnsupportedError";
605
+ this.kind = opts.kind;
606
+ this.platform = opts.platform;
607
+ this.contentType = opts.contentType;
608
+ this.action = opts.action;
609
+ this.detail = opts.detail;
610
+ }
611
+ static content(contentType, platform, detail) {
612
+ return new _UnsupportedError({
613
+ kind: "content",
614
+ contentType,
615
+ platform,
616
+ detail
617
+ });
618
+ }
619
+ static action(action, platform, detail) {
620
+ return new _UnsupportedError({ kind: "action", action, platform, detail });
621
+ }
622
+ withPlatform(platform) {
623
+ if (this.platform) {
624
+ return this;
625
+ }
626
+ return new _UnsupportedError({
627
+ kind: this.kind,
628
+ platform,
629
+ contentType: this.contentType,
630
+ action: this.action,
631
+ detail: this.detail
632
+ });
633
+ }
634
+ };
635
+
636
+ // src/platform/build.ts
637
+ var ANSI_YELLOW = "\x1B[33m";
638
+ var ANSI_RESET = "\x1B[0m";
639
+ var supportsAnsiColor = () => {
640
+ if (typeof process === "undefined") {
641
+ return false;
642
+ }
643
+ if (process.env.NO_COLOR) {
644
+ return false;
645
+ }
646
+ const force = process.env.FORCE_COLOR;
647
+ if (force !== void 0) {
648
+ return force !== "" && force !== "0" && force !== "false";
649
+ }
650
+ return Boolean(process.stderr?.isTTY);
651
+ };
652
+ var warnUnsupported = (err, fallbackPlatform) => {
653
+ const platform = err.platform ?? fallbackPlatform;
654
+ const subject = err.kind === "content" ? `content type "${err.contentType ?? "unknown"}"` : `action "${err.action ?? "unknown"}"`;
655
+ const detail = err.detail ? `: ${err.detail}` : "";
656
+ const body = `[spectrum-ts] ${platform} does not support ${subject}${detail}; skipping.`;
657
+ console.warn(
658
+ supportsAnsiColor() ? `${ANSI_YELLOW}${body}${ANSI_RESET}` : body
659
+ );
660
+ };
661
+ var providerMessageCoreKeys = /* @__PURE__ */ new Set([
662
+ "content",
663
+ "id",
664
+ "sender",
665
+ "space",
666
+ "timestamp"
667
+ ]);
668
+ var extractExtras = (raw, definition) => {
669
+ const entries = Object.entries(raw).filter(
670
+ ([key]) => !providerMessageCoreKeys.has(key)
671
+ );
672
+ const extra = Object.fromEntries(entries);
673
+ return definition.message?.schema ? definition.message.schema.parse(extra) : extra;
674
+ };
675
+ function wrapProviderMessage(raw, ctx) {
676
+ const wrappedContent = wrapNestedContent(raw.content, ctx);
677
+ return buildMessage({
678
+ id: raw.id,
679
+ content: wrappedContent,
680
+ sender: raw.sender,
681
+ timestamp: raw.timestamp ?? /* @__PURE__ */ new Date(),
682
+ extras: extractExtras(raw, ctx.definition),
683
+ spaceRef: ctx.spaceRef,
684
+ space: ctx.space,
685
+ definition: ctx.definition,
686
+ client: ctx.client,
687
+ config: ctx.config,
688
+ direction: "inbound"
689
+ });
690
+ }
691
+ var wrapNestedContent = (content, ctx) => {
692
+ if (content.type === "reaction") {
693
+ const target = content.target;
694
+ if (isRawProviderRecord(target)) {
695
+ return {
696
+ ...content,
697
+ target: wrapProviderMessage(target, ctx)
698
+ };
699
+ }
700
+ return content;
701
+ }
702
+ if (content.type === "group") {
703
+ const items = content.items.map((item) => {
704
+ const raw = item;
705
+ return isRawProviderRecord(raw) ? wrapProviderMessage(raw, ctx) : item;
706
+ });
707
+ return { ...content, items };
708
+ }
709
+ return content;
710
+ };
711
+ var isRawProviderRecord = (v) => {
712
+ if (typeof v !== "object" || v === null) {
713
+ return false;
714
+ }
715
+ const record = v;
716
+ return "id" in record && "content" in record && typeof record.react !== "function" && typeof record.reply !== "function";
717
+ };
718
+ function buildSpace(params) {
719
+ const { spaceRef, extras, typingCtx, definition, client, config } = params;
720
+ let space;
721
+ async function dispatchReaction(item) {
722
+ try {
723
+ if (!definition.actions.reactToMessage) {
724
+ throw UnsupportedError.action("react", definition.name);
725
+ }
726
+ await definition.actions.reactToMessage({
727
+ space: spaceRef,
728
+ target: item.target,
729
+ reaction: item.emoji,
730
+ client,
731
+ config
732
+ });
733
+ } catch (err) {
734
+ if (err instanceof UnsupportedError) {
735
+ warnUnsupported(err, definition.name);
736
+ return;
737
+ }
738
+ throw err;
739
+ }
740
+ }
741
+ async function dispatchSend(item) {
742
+ let sendResult;
743
+ try {
744
+ sendResult = await definition.actions.send({
745
+ ...typingCtx,
746
+ content: item
747
+ });
748
+ } catch (err) {
749
+ if (err instanceof UnsupportedError) {
750
+ warnUnsupported(err, definition.name);
751
+ return;
752
+ }
753
+ throw err;
754
+ }
755
+ if (!sendResult?.id) {
756
+ throw new Error(
757
+ `Platform "${definition.name}" send did not return a message id`
758
+ );
759
+ }
760
+ const outboundContent = item.type === "group" && sendResult.groupMembers ? {
761
+ ...item,
762
+ items: item.items.map((stub, idx) => {
763
+ const member = sendResult?.groupMembers?.[idx];
764
+ if (!member?.id) {
765
+ return stub;
766
+ }
767
+ return buildMessage({
768
+ id: member.id,
769
+ content: stub.content,
770
+ sender: member.sender,
771
+ timestamp: member.timestamp ?? /* @__PURE__ */ new Date(),
772
+ extras: {},
773
+ spaceRef,
774
+ space,
775
+ definition,
776
+ client,
777
+ config,
778
+ direction: "outbound"
779
+ });
780
+ })
781
+ } : item;
782
+ return buildMessage({
783
+ id: sendResult.id,
784
+ content: outboundContent,
785
+ sender: sendResult.sender,
786
+ timestamp: sendResult.timestamp ?? /* @__PURE__ */ new Date(),
787
+ extras: {},
788
+ spaceRef,
789
+ space,
790
+ definition,
791
+ client,
792
+ config,
793
+ direction: "outbound"
794
+ });
795
+ }
796
+ async function sendImpl(...content) {
797
+ const resolved = await resolveContents(content);
798
+ const results = [];
799
+ for (const item of resolved) {
800
+ if (item.type === "reaction") {
801
+ await dispatchReaction(item);
802
+ continue;
803
+ }
804
+ const sent = await dispatchSend(item);
805
+ if (sent) {
806
+ results.push(sent);
807
+ }
808
+ }
809
+ if (content.length === 1) {
810
+ return results[0];
811
+ }
812
+ return results;
813
+ }
814
+ async function getMessageImpl(id) {
815
+ if (!definition.actions.getMessage) {
816
+ warnUnsupported(
817
+ UnsupportedError.action("getMessage", definition.name),
818
+ definition.name
819
+ );
820
+ return;
821
+ }
822
+ let raw;
823
+ try {
824
+ raw = await definition.actions.getMessage({
825
+ space: spaceRef,
826
+ messageId: id,
827
+ client,
828
+ config
829
+ });
830
+ } catch (err) {
831
+ if (err instanceof UnsupportedError) {
832
+ warnUnsupported(err, definition.name);
833
+ return;
834
+ }
835
+ throw err;
836
+ }
837
+ if (!raw) {
838
+ return;
839
+ }
840
+ return wrapProviderMessage(raw, {
841
+ client,
842
+ config,
843
+ definition,
844
+ space,
845
+ spaceRef
846
+ });
847
+ }
848
+ space = {
849
+ ...extras,
850
+ ...spaceRef,
851
+ send: sendImpl,
852
+ edit: async (message, newContent) => {
853
+ await message.edit(newContent);
854
+ },
855
+ getMessage: getMessageImpl,
856
+ startTyping: async () => {
857
+ await definition.actions.startTyping?.(typingCtx);
858
+ },
859
+ stopTyping: async () => {
860
+ await definition.actions.stopTyping?.(typingCtx);
861
+ },
862
+ responding: async (fn) => {
863
+ await definition.actions.startTyping?.(typingCtx);
864
+ try {
865
+ return await fn();
866
+ } finally {
867
+ await definition.actions.stopTyping?.(typingCtx).catch(() => {
868
+ });
869
+ }
870
+ }
871
+ };
872
+ return space;
873
+ }
874
+ function buildMessage(params) {
875
+ const { definition, client, config, spaceRef, space } = params;
876
+ let self;
877
+ const react = async (reaction2) => {
878
+ if (!definition.actions.reactToMessage) {
879
+ warnUnsupported(
880
+ UnsupportedError.action("react", definition.name),
881
+ definition.name
882
+ );
883
+ return;
884
+ }
885
+ if (!self) {
886
+ throw new Error(
887
+ "react() called before message construction completed (internal bug)"
888
+ );
889
+ }
890
+ try {
891
+ await definition.actions.reactToMessage({
892
+ space: spaceRef,
893
+ target: self,
894
+ reaction: reaction2,
895
+ client,
896
+ config
897
+ });
898
+ } catch (err) {
899
+ if (err instanceof UnsupportedError) {
900
+ warnUnsupported(err, definition.name);
901
+ return;
902
+ }
903
+ throw err;
904
+ }
905
+ };
906
+ async function reply(...content) {
907
+ if (!definition.actions.replyToMessage) {
908
+ warnUnsupported(
909
+ UnsupportedError.action("reply", definition.name),
910
+ definition.name
911
+ );
912
+ return content.length === 1 ? void 0 : [];
913
+ }
914
+ const resolved = await resolveContents(content);
915
+ const results = [];
916
+ for (const item of resolved) {
917
+ let sendResult;
918
+ try {
919
+ sendResult = await definition.actions.replyToMessage({
920
+ space: spaceRef,
921
+ messageId: params.id,
922
+ content: item,
923
+ client,
924
+ config
925
+ });
926
+ } catch (err) {
927
+ if (err instanceof UnsupportedError) {
928
+ warnUnsupported(err, definition.name);
929
+ continue;
930
+ }
931
+ throw err;
932
+ }
933
+ if (!sendResult?.id) {
934
+ throw new Error(
935
+ `Platform "${definition.name}" reply did not return a message id`
936
+ );
937
+ }
938
+ results.push(
939
+ buildMessage({
940
+ id: sendResult.id,
941
+ content: item,
942
+ sender: sendResult.sender,
943
+ timestamp: sendResult.timestamp ?? /* @__PURE__ */ new Date(),
944
+ extras: {},
945
+ spaceRef,
946
+ space,
947
+ definition,
948
+ client,
949
+ config,
950
+ direction: "outbound"
951
+ })
952
+ );
953
+ }
954
+ if (content.length === 1) {
955
+ return results[0];
956
+ }
957
+ return results;
958
+ }
959
+ const senderWithPlatform = params.sender === void 0 ? void 0 : { ...params.sender, __platform: definition.name };
960
+ if (params.direction === "outbound") {
961
+ const outbound = {
962
+ ...params.extras,
963
+ id: params.id,
964
+ content: params.content,
965
+ direction: "outbound",
966
+ platform: definition.name,
967
+ react,
968
+ reply,
969
+ edit: async (newContent) => {
970
+ if (!definition.actions.editMessage) {
971
+ warnUnsupported(
972
+ UnsupportedError.action("edit", definition.name),
973
+ definition.name
974
+ );
975
+ return;
976
+ }
977
+ const [resolved] = await resolveContents([newContent]);
978
+ if (!resolved) {
979
+ return;
980
+ }
981
+ try {
982
+ await definition.actions.editMessage({
983
+ space: spaceRef,
984
+ messageId: params.id,
985
+ content: resolved,
986
+ client,
987
+ config
988
+ });
989
+ } catch (err) {
990
+ if (err instanceof UnsupportedError) {
991
+ warnUnsupported(err, definition.name);
992
+ return;
993
+ }
994
+ throw err;
995
+ }
996
+ },
997
+ sender: senderWithPlatform,
998
+ space,
999
+ timestamp: params.timestamp
1000
+ };
1001
+ self = outbound;
1002
+ return outbound;
1003
+ }
1004
+ const inbound = {
1005
+ ...params.extras,
1006
+ id: params.id,
1007
+ content: params.content,
1008
+ direction: "inbound",
1009
+ platform: definition.name,
1010
+ react,
1011
+ reply,
1012
+ sender: senderWithPlatform,
1013
+ space,
1014
+ timestamp: params.timestamp
1015
+ };
1016
+ self = inbound;
1017
+ return inbound;
1018
+ }
1019
+
1020
+ // src/platform/define.ts
1021
+ function createPlatformInstance(def, runtime) {
1022
+ const isPlatformUser = (value) => {
1023
+ return typeof value === "object" && value !== null && "__platform" in value && value.__platform === def.name;
1024
+ };
1025
+ const resolveUserID = async (userID) => {
1026
+ const resolved = await def.user.resolve({
1027
+ input: { userID },
1028
+ client: runtime.client,
1029
+ config: runtime.config
1030
+ });
1031
+ return {
1032
+ ...resolved,
1033
+ __platform: def.name
1034
+ };
1035
+ };
1036
+ const resolveStringUsers = async (args) => {
1037
+ const convertArg = async (arg) => {
1038
+ if (typeof arg === "string") {
1039
+ return await resolveUserID(arg);
1040
+ }
1041
+ if (Array.isArray(arg)) {
1042
+ return await Promise.all(arg.map(convertArg));
1043
+ }
1044
+ return arg;
1045
+ };
1046
+ return await Promise.all(args.map(convertArg));
1047
+ };
1048
+ const normalizeSpaceArgs = (args) => {
1049
+ if (args.length === 0) {
1050
+ return { users: [], params: void 0 };
1051
+ }
1052
+ const [first, ...rest] = args;
1053
+ if (Array.isArray(first)) {
1054
+ return {
1055
+ users: first,
1056
+ params: rest[0]
1057
+ };
1058
+ }
1059
+ if (!isPlatformUser(first)) {
1060
+ return {
1061
+ users: [],
1062
+ params: first
1063
+ };
1064
+ }
1065
+ const last = args.at(-1);
1066
+ if (last !== void 0 && !isPlatformUser(last)) {
1067
+ return {
1068
+ users: args.slice(0, -1),
1069
+ params: last
1070
+ };
1071
+ }
1072
+ return {
1073
+ users: args,
1074
+ params: void 0
1075
+ };
1076
+ };
1077
+ const base = {
1078
+ async user(userID) {
1079
+ const resolved = await def.user.resolve({
1080
+ input: { userID },
1081
+ client: runtime.client,
1082
+ config: runtime.config
1083
+ });
1084
+ return {
1085
+ ...resolved,
1086
+ __platform: def.name
1087
+ };
1088
+ },
1089
+ async space(...args) {
1090
+ const convertedArgs = await resolveStringUsers(args);
1091
+ const { users, params } = normalizeSpaceArgs(convertedArgs);
1092
+ let parsedParams = params;
1093
+ if (params !== void 0 && def.space.params) {
1094
+ parsedParams = def.space.params.parse(params);
1095
+ }
1096
+ const resolved = await def.space.resolve({
1097
+ input: { users, params: parsedParams },
1098
+ client: runtime.client,
1099
+ config: runtime.config
1100
+ });
1101
+ const parsedSpace = def.space.schema ? def.space.schema.parse(resolved) : resolved;
1102
+ const spaceRef = {
1103
+ id: parsedSpace.id,
1104
+ __platform: def.name
1105
+ };
1106
+ const typingCtx = {
1107
+ space: spaceRef,
1108
+ client: runtime.client,
1109
+ config: runtime.config
1110
+ };
1111
+ return buildSpace({
1112
+ spaceRef,
1113
+ extras: parsedSpace,
1114
+ typingCtx,
1115
+ definition: def,
1116
+ client: runtime.client,
1117
+ config: runtime.config
1118
+ });
1119
+ }
1120
+ };
1121
+ const eventProperties = {};
1122
+ for (const eventName of Object.keys(def.events)) {
1123
+ if (eventName === "messages") {
1124
+ continue;
1125
+ }
1126
+ const producer = def.events[eventName];
1127
+ if (producer) {
1128
+ eventProperties[eventName] = producer({
1129
+ client: runtime.client,
1130
+ config: runtime.config
1131
+ });
1132
+ }
1133
+ }
1134
+ return Object.assign(base, eventProperties);
1135
+ }
1136
+ function definePlatform(name, def) {
1137
+ const fullDef = { name, ...def };
1138
+ const platformCache = /* @__PURE__ */ new WeakMap();
1139
+ const narrowSpectrum = (spectrum) => {
1140
+ const cached = platformCache.get(spectrum);
1141
+ if (cached) {
1142
+ return cached;
1143
+ }
1144
+ const runtime = spectrum.__internal.platforms.get(name);
1145
+ if (!runtime) {
1146
+ throw new Error(`Platform "${name}" is not registered`);
1147
+ }
1148
+ const instance = createPlatformInstance(
1149
+ fullDef,
1150
+ runtime
1151
+ );
1152
+ platformCache.set(spectrum, instance);
1153
+ return instance;
1154
+ };
1155
+ const narrowSpace = (input) => {
1156
+ if (input.__platform !== name) {
1157
+ throw new Error(
1158
+ `Expected space from "${name}", got "${input.__platform}"`
1159
+ );
1160
+ }
1161
+ return input;
1162
+ };
1163
+ const narrowMessage = (input) => {
1164
+ if (input.platform !== name) {
1165
+ throw new Error(
1166
+ `Expected message from "${name}", got "${input.platform}"`
1167
+ );
1168
+ }
1169
+ return input;
1170
+ };
1171
+ const narrower = ((input) => {
1172
+ if ("__providers" in input && "__internal" in input) {
1173
+ return narrowSpectrum(input);
1174
+ }
1175
+ if ("__platform" in input && "send" in input) {
1176
+ return narrowSpace(input);
1177
+ }
1178
+ if ("platform" in input && "sender" in input && "space" in input) {
1179
+ return narrowMessage(input);
1180
+ }
1181
+ throw new Error("Invalid input to platform narrowing function");
1182
+ });
1183
+ narrower.config = (config) => {
1184
+ const resolvedConfig = config ?? {};
1185
+ return {
1186
+ __tag: "PlatformProviderConfig",
1187
+ __def: void 0,
1188
+ __name: name,
1189
+ config: resolvedConfig,
1190
+ __definition: fullDef
1191
+ };
1192
+ };
1193
+ if (def.static) {
1194
+ Object.assign(narrower, def.static);
1195
+ }
1196
+ return narrower;
1197
+ }
1198
+
1199
+ export {
1200
+ readSchema,
1201
+ streamSchema,
1202
+ bufferToStream,
1203
+ asAttachment,
1204
+ attachment,
1205
+ fromVCard,
1206
+ toVCard,
1207
+ asContact,
1208
+ contact,
1209
+ asCustom,
1210
+ custom,
1211
+ asText,
1212
+ text,
1213
+ resolveContents,
1214
+ reactionSchema,
1215
+ asReaction,
1216
+ reaction,
1217
+ UnsupportedError,
1218
+ wrapProviderMessage,
1219
+ buildSpace,
1220
+ definePlatform
1221
+ };