spectrum-ts 0.7.0 → 0.9.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.
@@ -0,0 +1,141 @@
1
+ import {
2
+ bufferToStream,
3
+ readSchema,
4
+ streamSchema
5
+ } from "./chunk-CZIWNTXP.js";
6
+
7
+ // src/content/richlink.ts
8
+ import z from "zod";
9
+
10
+ // src/utils/link-metadata.ts
11
+ import ogs from "open-graph-scraper";
12
+ var DEFAULT_TIMEOUT_MS = 5e3;
13
+ var USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15 spectrum-ts/richlink";
14
+ var normaliseImageUrl = (raw, base) => {
15
+ try {
16
+ return new URL(raw, base).toString();
17
+ } catch {
18
+ return;
19
+ }
20
+ };
21
+ var cleanString = (v) => {
22
+ if (typeof v !== "string") {
23
+ return;
24
+ }
25
+ const trimmed = v.trim();
26
+ return trimmed.length > 0 ? trimmed : void 0;
27
+ };
28
+ var fetchLinkMetadata = async (url) => {
29
+ try {
30
+ const result = await ogs({
31
+ url,
32
+ timeout: DEFAULT_TIMEOUT_MS,
33
+ fetchOptions: { headers: { "User-Agent": USER_AGENT } }
34
+ });
35
+ if (result.error) {
36
+ return {};
37
+ }
38
+ const {
39
+ ogTitle,
40
+ ogDescription,
41
+ ogImage,
42
+ twitterTitle,
43
+ twitterDescription,
44
+ twitterImage
45
+ } = result.result;
46
+ const title = cleanString(ogTitle) ?? cleanString(twitterTitle);
47
+ const summary = cleanString(ogDescription) ?? cleanString(twitterDescription);
48
+ const imageCandidate = ogImage?.[0] ?? twitterImage?.[0];
49
+ const resolved = imageCandidate ? normaliseImageUrl(imageCandidate.url, url) : void 0;
50
+ const image = imageCandidate && resolved ? {
51
+ url: resolved,
52
+ mimeType: "type" in imageCandidate && typeof imageCandidate.type === "string" ? imageCandidate.type : void 0
53
+ } : void 0;
54
+ return { title, summary, image };
55
+ } catch {
56
+ return {};
57
+ }
58
+ };
59
+ var fetchImage = async (url) => {
60
+ const controller = new AbortController();
61
+ const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
62
+ try {
63
+ const res = await fetch(url, {
64
+ signal: controller.signal,
65
+ headers: { "User-Agent": USER_AGENT }
66
+ });
67
+ if (!res.ok) {
68
+ throw new Error(`image fetch ${url} returned ${res.status}`);
69
+ }
70
+ const data = Buffer.from(await res.arrayBuffer());
71
+ const mimeType = res.headers.get("content-type") ?? void 0;
72
+ return { data, mimeType };
73
+ } finally {
74
+ clearTimeout(timer);
75
+ }
76
+ };
77
+
78
+ // src/content/richlink.ts
79
+ var richlinkCoverSchema = z.object({
80
+ mimeType: z.string().min(1).optional(),
81
+ read: readSchema,
82
+ stream: streamSchema
83
+ });
84
+ var optionalStringAccessor = z.function({
85
+ input: [],
86
+ output: z.promise(z.string().min(1).optional())
87
+ });
88
+ var coverAccessor = z.function({
89
+ input: [],
90
+ output: z.promise(richlinkCoverSchema.optional())
91
+ });
92
+ var richlinkSchema = z.object({
93
+ type: z.literal("richlink"),
94
+ url: z.url(),
95
+ title: optionalStringAccessor,
96
+ summary: optionalStringAccessor,
97
+ cover: coverAccessor
98
+ });
99
+ var memoize = (factory) => {
100
+ let cached;
101
+ return () => {
102
+ cached ??= factory();
103
+ return cached;
104
+ };
105
+ };
106
+ var buildCover = (image) => {
107
+ const read = memoize(
108
+ () => fetchImage(image.url).then((r) => r.data).catch(() => Buffer.alloc(0))
109
+ );
110
+ return {
111
+ mimeType: image.mimeType,
112
+ read,
113
+ stream: async () => bufferToStream(await read())
114
+ };
115
+ };
116
+ var asRichlink = (input) => {
117
+ const getMetadata = memoize(() => fetchLinkMetadata(input.url));
118
+ const getCover = memoize(async () => {
119
+ const { image } = await getMetadata();
120
+ return image ? buildCover(image) : void 0;
121
+ });
122
+ const title = async () => (await getMetadata()).title;
123
+ const summary = async () => (await getMetadata()).summary;
124
+ return richlinkSchema.parse({
125
+ type: "richlink",
126
+ url: input.url,
127
+ title,
128
+ summary,
129
+ cover: getCover
130
+ });
131
+ };
132
+ function richlink(url) {
133
+ return {
134
+ build: async () => asRichlink({ url })
135
+ };
136
+ }
137
+
138
+ export {
139
+ asRichlink,
140
+ richlink
141
+ };
@@ -553,15 +553,30 @@ function custom(raw) {
553
553
  };
554
554
  }
555
555
 
556
+ // src/content/reaction.ts
557
+ import z5 from "zod";
558
+ var reactionSchema = z5.object({
559
+ type: z5.literal("reaction"),
560
+ emoji: z5.string().min(1),
561
+ target: z5.string().min(1)
562
+ });
563
+ var asReaction = (input) => reactionSchema.parse({ type: "reaction", ...input });
564
+ function reaction(emoji, target) {
565
+ const targetId = typeof target === "string" ? target : target.id;
566
+ return { build: async () => asReaction({ emoji, target: targetId }) };
567
+ }
568
+
556
569
  // src/utils/stream.ts
557
570
  import { Repeater } from "@repeaterjs/repeater";
558
571
  function stream(setup) {
559
572
  const repeater = new Repeater(async (push, stop) => {
560
- const emit = (value) => {
561
- Promise.resolve(push(value)).catch((error) => {
573
+ const emit = async (value) => {
574
+ try {
575
+ await push(value);
576
+ } catch (error) {
562
577
  stop(error);
563
- return void 0;
564
- });
578
+ throw error;
579
+ }
565
580
  };
566
581
  const end = (error) => {
567
582
  stop(error);
@@ -589,7 +604,7 @@ function mergeStreams(streams) {
589
604
  const workers = streams.map(async (source) => {
590
605
  try {
591
606
  for await (const value of source) {
592
- emit(value);
607
+ await emit(value);
593
608
  }
594
609
  } catch (error) {
595
610
  end(error);
@@ -686,6 +701,8 @@ export {
686
701
  contact,
687
702
  asCustom,
688
703
  custom,
704
+ asReaction,
705
+ reaction,
689
706
  stream,
690
707
  mergeStreams,
691
708
  SpectrumCloudError,
@@ -91,43 +91,71 @@ var warnUnsupported = (err, fallbackPlatform) => {
91
91
  function buildSpace(params) {
92
92
  const { spaceRef, extras, typingCtx, definition, client, config } = params;
93
93
  let space;
94
+ async function dispatchReaction(item) {
95
+ try {
96
+ if (!definition.actions.reactToMessage) {
97
+ throw UnsupportedError.action("react", definition.name);
98
+ }
99
+ await definition.actions.reactToMessage({
100
+ space: spaceRef,
101
+ messageId: item.target,
102
+ reaction: item.emoji,
103
+ client,
104
+ config
105
+ });
106
+ } catch (err) {
107
+ if (err instanceof UnsupportedError) {
108
+ warnUnsupported(err, definition.name);
109
+ return;
110
+ }
111
+ throw err;
112
+ }
113
+ }
114
+ async function dispatchSend(item) {
115
+ let sendResult;
116
+ try {
117
+ sendResult = await definition.actions.send({
118
+ ...typingCtx,
119
+ content: item
120
+ });
121
+ } catch (err) {
122
+ if (err instanceof UnsupportedError) {
123
+ warnUnsupported(err, definition.name);
124
+ return;
125
+ }
126
+ throw err;
127
+ }
128
+ if (!sendResult?.id) {
129
+ throw new Error(
130
+ `Platform "${definition.name}" send did not return a message id`
131
+ );
132
+ }
133
+ return buildMessage({
134
+ id: sendResult.id,
135
+ content: item,
136
+ sender: sendResult.sender,
137
+ timestamp: sendResult.timestamp ?? /* @__PURE__ */ new Date(),
138
+ extras: {},
139
+ spaceRef,
140
+ space,
141
+ definition,
142
+ client,
143
+ config,
144
+ direction: "outbound"
145
+ });
146
+ }
94
147
  async function sendImpl(...content) {
95
148
  const resolved = await resolveContents(content);
96
149
  const results = [];
97
150
  for (const item of resolved) {
98
- let sendResult;
99
- try {
100
- sendResult = await definition.actions.send({
101
- ...typingCtx,
102
- content: item
103
- });
104
- } catch (err) {
105
- if (err instanceof UnsupportedError) {
106
- warnUnsupported(err, definition.name);
107
- continue;
108
- }
109
- throw err;
151
+ if (item.type === "reaction") {
152
+ await dispatchReaction(item);
153
+ continue;
110
154
  }
111
- if (!sendResult?.id) {
112
- throw new Error(
113
- `Platform "${definition.name}" send did not return a message id`
114
- );
155
+ const sent = await dispatchSend(item);
156
+ if (sent) {
157
+ results.push(sent);
115
158
  }
116
- results.push(
117
- buildMessage({
118
- id: sendResult.id,
119
- content: item,
120
- sender: sendResult.sender,
121
- timestamp: sendResult.timestamp ?? /* @__PURE__ */ new Date(),
122
- extras: {},
123
- spaceRef,
124
- space,
125
- definition,
126
- client,
127
- config,
128
- direction: "outbound"
129
- })
130
- );
131
159
  }
132
160
  if (content.length === 1) {
133
161
  return results[0];
@@ -169,13 +197,21 @@ function buildMessage(params) {
169
197
  );
170
198
  return;
171
199
  }
172
- await definition.actions.reactToMessage({
173
- space: spaceRef,
174
- messageId: params.id,
175
- reaction,
176
- client,
177
- config
178
- });
200
+ try {
201
+ await definition.actions.reactToMessage({
202
+ space: spaceRef,
203
+ messageId: params.id,
204
+ reaction,
205
+ client,
206
+ config
207
+ });
208
+ } catch (err) {
209
+ if (err instanceof UnsupportedError) {
210
+ warnUnsupported(err, definition.name);
211
+ return;
212
+ }
213
+ throw err;
214
+ }
179
215
  };
180
216
  async function reply(...content) {
181
217
  if (!definition.actions.replyToMessage) {
@@ -292,6 +328,29 @@ function createPlatformInstance(def, runtime) {
292
328
  const isPlatformUser = (value) => {
293
329
  return typeof value === "object" && value !== null && "__platform" in value && value.__platform === def.name;
294
330
  };
331
+ const resolveUserID = async (userID) => {
332
+ const resolved = await def.user.resolve({
333
+ input: { userID },
334
+ client: runtime.client,
335
+ config: runtime.config
336
+ });
337
+ return {
338
+ ...resolved,
339
+ __platform: def.name
340
+ };
341
+ };
342
+ const resolveStringUsers = async (args) => {
343
+ const convertArg = async (arg) => {
344
+ if (typeof arg === "string") {
345
+ return await resolveUserID(arg);
346
+ }
347
+ if (Array.isArray(arg)) {
348
+ return await Promise.all(arg.map(convertArg));
349
+ }
350
+ return arg;
351
+ };
352
+ return await Promise.all(args.map(convertArg));
353
+ };
295
354
  const normalizeSpaceArgs = (args) => {
296
355
  if (args.length === 0) {
297
356
  return { users: [], params: void 0 };
@@ -334,7 +393,8 @@ function createPlatformInstance(def, runtime) {
334
393
  };
335
394
  },
336
395
  async space(...args) {
337
- const { users, params } = normalizeSpaceArgs(args);
396
+ const convertedArgs = await resolveStringUsers(args);
397
+ const { users, params } = normalizeSpaceArgs(convertedArgs);
338
398
  let parsedParams = params;
339
399
  if (params !== void 0 && def.space.params) {
340
400
  parsedParams = def.space.params.parse(params);