@sockethub/activity-streams 4.4.0-alpha.5 → 4.4.0-alpha.7
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/package.json +16 -5
- package/src/activity-streams.test.ts +302 -0
- package/src/activity-streams.ts +344 -0
package/package.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sockethub/activity-streams",
|
|
3
|
-
"version": "4.4.0-alpha.
|
|
3
|
+
"version": "4.4.0-alpha.7",
|
|
4
4
|
"description": "A simple tool to facilitate handling and referencing activity streams without unnecessary verbosity",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
6
|
+
"main": "./dist/activity-streams.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"bun": "./src/activity-streams.ts",
|
|
10
|
+
"import": "./dist/activity-streams.js",
|
|
11
|
+
"default": "./dist/activity-streams.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
7
14
|
"files": [
|
|
15
|
+
"src/",
|
|
8
16
|
"dist/"
|
|
9
17
|
],
|
|
10
18
|
"engines": {
|
|
@@ -23,6 +31,9 @@
|
|
|
23
31
|
],
|
|
24
32
|
"author": "Nick Jennings <nick@silverbucket.net>",
|
|
25
33
|
"license": "MIT",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
26
37
|
"bugs": {
|
|
27
38
|
"url": "https://github.com/sockethub/sockethub/issues"
|
|
28
39
|
},
|
|
@@ -35,13 +46,13 @@
|
|
|
35
46
|
"test:browser": "wtr dist/**/*.test.js --node-resolve --puppeteer"
|
|
36
47
|
},
|
|
37
48
|
"dependencies": {
|
|
38
|
-
"eventemitter3": "5.0.
|
|
49
|
+
"eventemitter3": "^5.0.4"
|
|
39
50
|
},
|
|
40
51
|
"devDependencies": {
|
|
41
|
-
"@sockethub/schemas": "3.0.0-alpha.
|
|
52
|
+
"@sockethub/schemas": "^3.0.0-alpha.7",
|
|
42
53
|
"@types/bun": "latest",
|
|
43
54
|
"@web/test-runner": "^0.19.0",
|
|
44
55
|
"@web/test-runner-puppeteer": "^0.17.0"
|
|
45
56
|
},
|
|
46
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "38927776c210a22bc54ceb86ccc7276c5f27b463"
|
|
47
58
|
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ActivityStream } from "@sockethub/schemas";
|
|
3
|
+
import { ASFactory } from "./activity-streams";
|
|
4
|
+
|
|
5
|
+
describe("warn test", () => {
|
|
6
|
+
expect(typeof ASFactory).toEqual("function");
|
|
7
|
+
const activity = ASFactory();
|
|
8
|
+
test("rejects nondefined special types", () => {
|
|
9
|
+
expect(() => {
|
|
10
|
+
activity.Stream({
|
|
11
|
+
type: "lol",
|
|
12
|
+
platform: "irc",
|
|
13
|
+
actor: "thingy",
|
|
14
|
+
object: { type: "hola", content: "har", secure: true },
|
|
15
|
+
target: ["thingy1", "thingy2"],
|
|
16
|
+
});
|
|
17
|
+
}).toThrow(Error);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("no special props", () => {
|
|
22
|
+
const activity = ASFactory();
|
|
23
|
+
test("init", () => {
|
|
24
|
+
expect(typeof ASFactory).toEqual("function");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("returns expected object with no special types", () => {
|
|
28
|
+
expect(
|
|
29
|
+
activity.Stream({
|
|
30
|
+
type: "send",
|
|
31
|
+
context: "irc",
|
|
32
|
+
actor: "thingy",
|
|
33
|
+
object: { type: "hola", content: "har" },
|
|
34
|
+
target: ["thingy1", "thingy2"],
|
|
35
|
+
}),
|
|
36
|
+
).toEqual({
|
|
37
|
+
type: "send",
|
|
38
|
+
context: "irc",
|
|
39
|
+
actor: { id: "thingy" },
|
|
40
|
+
object: { type: "hola", content: "har" },
|
|
41
|
+
target: ["thingy1", "thingy2"],
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("basic tests", () => {
|
|
47
|
+
const config = {
|
|
48
|
+
customProps: {
|
|
49
|
+
credentials: ["secure"],
|
|
50
|
+
dude: ["foo", "secure"],
|
|
51
|
+
},
|
|
52
|
+
specialObjs: ["dude"],
|
|
53
|
+
failOnUnknownObjectProperties: true,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const activity = ASFactory(config);
|
|
57
|
+
test("init", () => {
|
|
58
|
+
expect(typeof ASFactory).toEqual("function");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("object tests", () => {
|
|
62
|
+
test("has expected structure", () => {
|
|
63
|
+
expect(typeof activity).toEqual("object");
|
|
64
|
+
expect(typeof activity.Object).toEqual("object");
|
|
65
|
+
expect(typeof activity.Stream).toEqual("function");
|
|
66
|
+
expect(typeof activity.on).toEqual("function");
|
|
67
|
+
expect(typeof activity.once).toEqual("function");
|
|
68
|
+
expect(typeof activity.off).toEqual("function");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("returns undefined when no params are passed", () => {
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
expect(activity.Object.get()).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("returns object when given a valid lookup id", () => {
|
|
77
|
+
expect(activity.Object.create({ id: "thingy1" })).toEqual({
|
|
78
|
+
id: "thingy1",
|
|
79
|
+
});
|
|
80
|
+
expect(activity.Object.get("thingy1")).toEqual({
|
|
81
|
+
id: "thingy1",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("throws an exception when called with no identifier", () => {
|
|
86
|
+
expect(activity.Object.create).toThrow(Error);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("creates a second object and returns is as expected", () => {
|
|
90
|
+
expect(activity.Object.create({ id: "thingy2" })).toEqual({
|
|
91
|
+
id: "thingy2",
|
|
92
|
+
});
|
|
93
|
+
expect(activity.Object.get("thingy2")).toEqual({
|
|
94
|
+
id: "thingy2",
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("returns a basic ActivtyObject when receiving an unknown id with expand=true", () => {
|
|
99
|
+
expect(activity.Object.get("thingy3", true)).toEqual({
|
|
100
|
+
id: "thingy3",
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("returns given id param when lookup fails and expand=false", () => {
|
|
105
|
+
expect(
|
|
106
|
+
// @ts-ignore
|
|
107
|
+
activity.Object.get({
|
|
108
|
+
id: "thingy3",
|
|
109
|
+
foo: "bar",
|
|
110
|
+
}),
|
|
111
|
+
).toEqual({
|
|
112
|
+
id: "thingy3",
|
|
113
|
+
foo: "bar",
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
interface TestActivityStream extends ActivityStream {
|
|
119
|
+
type: string;
|
|
120
|
+
verb?: string;
|
|
121
|
+
context: string;
|
|
122
|
+
platform?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe("stream tests", () => {
|
|
126
|
+
let stream: TestActivityStream;
|
|
127
|
+
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
stream = activity.Stream({
|
|
130
|
+
verb: "lol",
|
|
131
|
+
platform: "irc",
|
|
132
|
+
actor: "thingy1",
|
|
133
|
+
context: "irc",
|
|
134
|
+
object: {
|
|
135
|
+
objectType: "credentials",
|
|
136
|
+
content: "har",
|
|
137
|
+
secure: true,
|
|
138
|
+
},
|
|
139
|
+
target: ["thingy1", "thingy2"],
|
|
140
|
+
}) as ActivityStream;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("renames mapped props", () => {
|
|
144
|
+
expect(stream.type).toEqual("lol");
|
|
145
|
+
expect(stream.verb).toBeUndefined();
|
|
146
|
+
expect(stream.context).toEqual("irc");
|
|
147
|
+
expect(stream.platform).toBeUndefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("expands existing objects", () => {
|
|
151
|
+
// @ts-ignore
|
|
152
|
+
expect(stream.target).toEqual([
|
|
153
|
+
{ id: "thingy1" },
|
|
154
|
+
{ id: "thingy2" },
|
|
155
|
+
]);
|
|
156
|
+
// @ts-ignore
|
|
157
|
+
expect(stream.actor).toEqual({ id: "thingy1" });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("handles customProps as expected", () => {
|
|
161
|
+
expect(stream.object).toEqual({
|
|
162
|
+
type: "credentials",
|
|
163
|
+
content: "har",
|
|
164
|
+
secure: true,
|
|
165
|
+
});
|
|
166
|
+
expect(stream.object.objectType).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("respects specialObj properties", () => {
|
|
170
|
+
const stream2 = activity.Stream({
|
|
171
|
+
type: "lol",
|
|
172
|
+
platform: "irc",
|
|
173
|
+
actor: "thingy",
|
|
174
|
+
object: {
|
|
175
|
+
type: "dude",
|
|
176
|
+
foo: "bar",
|
|
177
|
+
content: "har",
|
|
178
|
+
secure: true,
|
|
179
|
+
},
|
|
180
|
+
target: ["thingy1", "thingy2"],
|
|
181
|
+
});
|
|
182
|
+
expect(stream2).toEqual({
|
|
183
|
+
type: "lol",
|
|
184
|
+
context: "irc",
|
|
185
|
+
actor: { id: "thingy" },
|
|
186
|
+
target: [{ id: "thingy1" }, { id: "thingy2" }],
|
|
187
|
+
object: {
|
|
188
|
+
type: "dude",
|
|
189
|
+
foo: "bar",
|
|
190
|
+
content: "har",
|
|
191
|
+
secure: true,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("rejects nondefined special types", () => {
|
|
197
|
+
expect(() => {
|
|
198
|
+
activity.Stream({
|
|
199
|
+
type: "lol",
|
|
200
|
+
platform: "irc",
|
|
201
|
+
actor: "thingy",
|
|
202
|
+
object: {
|
|
203
|
+
type: "hola",
|
|
204
|
+
foo: "bar",
|
|
205
|
+
content: "har",
|
|
206
|
+
secure: true,
|
|
207
|
+
},
|
|
208
|
+
target: ["thingy1", "thingy2"],
|
|
209
|
+
});
|
|
210
|
+
}).toThrow('ActivityStreams validation failed: property "foo" with value "bar" is not allowed on the "object" object of type "hola".');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("emitters", () => {
|
|
215
|
+
test("emits an event on object creation", () => {
|
|
216
|
+
function onHandler(obj) {
|
|
217
|
+
expect(obj).toEqual({ id: "thingy3" });
|
|
218
|
+
activity.off("activity-object-create", onHandler);
|
|
219
|
+
}
|
|
220
|
+
activity.on("activity-object-create", onHandler);
|
|
221
|
+
activity.Object.create({ id: "thingy3" });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("emits an event on object deletion", () => {
|
|
225
|
+
activity.once("activity-object-delete", (id) => {
|
|
226
|
+
expect(id).toEqual("thingy2");
|
|
227
|
+
});
|
|
228
|
+
activity.Object.delete("thingy2");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("Object.create error handling tests", () => {
|
|
234
|
+
const activity = ASFactory();
|
|
235
|
+
|
|
236
|
+
test("throws clear error when passed null", () => {
|
|
237
|
+
expect(() => {
|
|
238
|
+
// @ts-ignore - intentionally testing invalid input
|
|
239
|
+
activity.Object.create(null);
|
|
240
|
+
}).toThrow('ActivityStreams validation failed: the "object" property is null. Example: { id: "user@example.com", type: "person" }');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("throws clear error when passed undefined", () => {
|
|
244
|
+
expect(() => {
|
|
245
|
+
// @ts-ignore - intentionally testing invalid input
|
|
246
|
+
activity.Object.create(undefined);
|
|
247
|
+
}).toThrow('ActivityStreams validation failed: the "object" property is undefined. Example: { id: "user@example.com", type: "person" }');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("throws clear error when passed string", () => {
|
|
251
|
+
expect(() => {
|
|
252
|
+
// @ts-ignore - intentionally testing invalid input
|
|
253
|
+
activity.Object.create("string value");
|
|
254
|
+
}).toThrow('ActivityStreams validation failed: the "object" property received string "string value" but expected an object. Use: { id: "string value", type: "person" }');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("throws clear error when passed number", () => {
|
|
258
|
+
expect(() => {
|
|
259
|
+
// @ts-ignore - intentionally testing invalid input
|
|
260
|
+
activity.Object.create(123);
|
|
261
|
+
}).toThrow('ActivityStreams validation failed: the "object" property must be an object, received number (123). Example: { id: "user@example.com", type: "person" }');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("throws clear error when passed boolean false", () => {
|
|
265
|
+
expect(() => {
|
|
266
|
+
// @ts-ignore - intentionally testing invalid input
|
|
267
|
+
activity.Object.create(false);
|
|
268
|
+
}).toThrow('ActivityStreams validation failed: the "object" property must be an object, received boolean (false). Example: { id: "user@example.com", type: "person" }');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("throws clear error when passed boolean true", () => {
|
|
272
|
+
expect(() => {
|
|
273
|
+
// @ts-ignore - intentionally testing invalid input
|
|
274
|
+
activity.Object.create(true);
|
|
275
|
+
}).toThrow('ActivityStreams validation failed: the "object" property must be an object, received boolean (true). Example: { id: "user@example.com", type: "person" }');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("throws clear error when passed array", () => {
|
|
279
|
+
expect(() => {
|
|
280
|
+
// @ts-ignore - intentionally testing invalid input
|
|
281
|
+
activity.Object.create([]);
|
|
282
|
+
}).toThrow('ActivityStreams validation failed: the "object" property must be an object, received array (). Example: { id: "user@example.com", type: "person" }');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("throws clear error when passed empty object (missing required id)", () => {
|
|
286
|
+
expect(() => {
|
|
287
|
+
activity.Object.create({});
|
|
288
|
+
}).toThrow('ActivityStreams validation failed: the "object" property requires an \'id\' property. Example: { id: "user@example.com", type: "person" }');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("throws clear error when object has properties but missing id", () => {
|
|
292
|
+
expect(() => {
|
|
293
|
+
activity.Object.create({ type: "person", name: "John" });
|
|
294
|
+
}).toThrow('ActivityStreams validation failed: the "object" property requires an \'id\' property. Example: { id: "user@example.com", type: "person" }');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("handles object with only id property", () => {
|
|
298
|
+
expect(() => {
|
|
299
|
+
activity.Object.create({ id: "test-id" });
|
|
300
|
+
}).not.toThrow();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* activity-streams
|
|
3
|
+
* https://github.com/silverbucket/activity-streams
|
|
4
|
+
*
|
|
5
|
+
* Developed and Maintained by:
|
|
6
|
+
* Nick Jennings <nick@silverbucket.net>
|
|
7
|
+
*
|
|
8
|
+
* activity-streams is released under the MIT (see LICENSE).
|
|
9
|
+
*
|
|
10
|
+
* You don't have to do anything special to choose one license or the other
|
|
11
|
+
* and you don't have to notify anyone which license you are using.
|
|
12
|
+
* Please see the corresponding license file for details of these licenses.
|
|
13
|
+
* You are free to use, modify and distribute this software, but all copyright
|
|
14
|
+
* information must remain.
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ActivityObject, ActivityStream } from "@sockethub/schemas";
|
|
19
|
+
import EventEmitter from "eventemitter3";
|
|
20
|
+
|
|
21
|
+
const ee = new EventEmitter();
|
|
22
|
+
const baseProps = {
|
|
23
|
+
stream: [
|
|
24
|
+
"id",
|
|
25
|
+
"type",
|
|
26
|
+
"actor",
|
|
27
|
+
"target",
|
|
28
|
+
"object",
|
|
29
|
+
"context",
|
|
30
|
+
"context",
|
|
31
|
+
"published",
|
|
32
|
+
"error",
|
|
33
|
+
],
|
|
34
|
+
object: [
|
|
35
|
+
"id",
|
|
36
|
+
"type",
|
|
37
|
+
"context",
|
|
38
|
+
"alias",
|
|
39
|
+
"attachedTo",
|
|
40
|
+
"attachment",
|
|
41
|
+
"attributedTo",
|
|
42
|
+
"attributedWith",
|
|
43
|
+
"condition",
|
|
44
|
+
"content",
|
|
45
|
+
"contentMap",
|
|
46
|
+
"context",
|
|
47
|
+
"contextOf",
|
|
48
|
+
"name",
|
|
49
|
+
"endTime",
|
|
50
|
+
"generator",
|
|
51
|
+
"generatorOf",
|
|
52
|
+
"group",
|
|
53
|
+
"icon",
|
|
54
|
+
"image",
|
|
55
|
+
"inReplyTo",
|
|
56
|
+
"members",
|
|
57
|
+
"memberOf",
|
|
58
|
+
"message",
|
|
59
|
+
"location",
|
|
60
|
+
"locationOf",
|
|
61
|
+
"objectOf",
|
|
62
|
+
"originOf",
|
|
63
|
+
"presence",
|
|
64
|
+
"preview",
|
|
65
|
+
"previewOf",
|
|
66
|
+
"provider",
|
|
67
|
+
"providerOf",
|
|
68
|
+
"published",
|
|
69
|
+
"rating",
|
|
70
|
+
"relationship",
|
|
71
|
+
"resultOf",
|
|
72
|
+
"replies",
|
|
73
|
+
"role",
|
|
74
|
+
"scope",
|
|
75
|
+
"scopeOf",
|
|
76
|
+
"startTime",
|
|
77
|
+
"status",
|
|
78
|
+
"summary",
|
|
79
|
+
"topic",
|
|
80
|
+
"tag",
|
|
81
|
+
"tagOf",
|
|
82
|
+
"targetOf",
|
|
83
|
+
"title",
|
|
84
|
+
"titleMap",
|
|
85
|
+
"updated",
|
|
86
|
+
"url",
|
|
87
|
+
"xmpp:stanza-id",
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
const rename = {
|
|
91
|
+
"@id": "id",
|
|
92
|
+
"@type": "type",
|
|
93
|
+
verb: "type",
|
|
94
|
+
displayName: "name",
|
|
95
|
+
objectType: "type",
|
|
96
|
+
platform: "context",
|
|
97
|
+
};
|
|
98
|
+
const expand = {
|
|
99
|
+
actor: {
|
|
100
|
+
primary: "id",
|
|
101
|
+
props: baseProps,
|
|
102
|
+
},
|
|
103
|
+
target: {
|
|
104
|
+
primary: "id",
|
|
105
|
+
props: baseProps,
|
|
106
|
+
},
|
|
107
|
+
object: {
|
|
108
|
+
primary: "content",
|
|
109
|
+
props: baseProps,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
type CustomProps = {
|
|
114
|
+
[key: string]: string | number | boolean | object | string[];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const objs = new Map();
|
|
118
|
+
const customProps: CustomProps = {};
|
|
119
|
+
|
|
120
|
+
let failOnUnknownObjectProperties = false;
|
|
121
|
+
let warnOnUnknownObjectProperties = true;
|
|
122
|
+
let specialObjs = []; // the objects don't get rejected for bad props
|
|
123
|
+
|
|
124
|
+
function matchesCustomProp(type: string, key: string) {
|
|
125
|
+
if (customProps[type] instanceof Object) {
|
|
126
|
+
const obj = customProps[type] as string[];
|
|
127
|
+
if (obj.includes(key)) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renameProp<T>(obj: T, key: string): T {
|
|
135
|
+
obj[rename[key]] = obj[key];
|
|
136
|
+
delete obj[key];
|
|
137
|
+
return obj;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function validateObject<T>(type: string, incomingObj: T, requireId = false) {
|
|
141
|
+
// Input validation with clear error messages
|
|
142
|
+
if (incomingObj === null) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`ActivityStreams validation failed: the "${type}" property is null. Example: { id: "user@example.com", type: "person" }`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (incomingObj === undefined) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`ActivityStreams validation failed: the "${type}" property is undefined. Example: { id: "user@example.com", type: "person" }`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (typeof incomingObj === "string") {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`ActivityStreams validation failed: the "${type}" property received string "${incomingObj}" but expected an object. Use: { id: "${incomingObj}", type: "person" }`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (typeof incomingObj !== "object" || Array.isArray(incomingObj)) {
|
|
161
|
+
const receivedType = Array.isArray(incomingObj)
|
|
162
|
+
? "array"
|
|
163
|
+
: typeof incomingObj;
|
|
164
|
+
const receivedValue = String(incomingObj);
|
|
165
|
+
throw new Error(
|
|
166
|
+
`ActivityStreams validation failed: the "${type}" property must be an object, received ${receivedType} (${receivedValue}). Example: { id: "user@example.com", type: "person" }`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Require 'id' property when explicitly requested (e.g., Object.create())
|
|
171
|
+
const obj = incomingObj as ActivityObject;
|
|
172
|
+
if (requireId && !obj.id) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`ActivityStreams validation failed: the "${type}" property requires an 'id' property. Example: { id: "user@example.com", type: "person" }`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const unknownKeys = Object.keys(incomingObj).filter(
|
|
179
|
+
(key: string): boolean => {
|
|
180
|
+
return !baseProps[type].includes(key);
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
for (const key of unknownKeys) {
|
|
185
|
+
let ao: ActivityObject = incomingObj as ActivityObject;
|
|
186
|
+
if (rename[key]) {
|
|
187
|
+
// rename property instead of fail
|
|
188
|
+
ao = renameProp(ao, key);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (matchesCustomProp(ao.type, key)) {
|
|
193
|
+
// custom property matches, continue
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!specialObjs.includes(ao.type)) {
|
|
198
|
+
// not defined as a special prop
|
|
199
|
+
// don't know what to do with it, so throw error
|
|
200
|
+
console.log(ao);
|
|
201
|
+
const receivedValue =
|
|
202
|
+
typeof ao[key] === "string" ? `"${ao[key]}"` : String(ao[key]);
|
|
203
|
+
const err = `ActivityStreams validation failed: property "${key}" with value ${receivedValue} is not allowed on the "${type}" object of type "${ao.type}".`;
|
|
204
|
+
if (failOnUnknownObjectProperties) {
|
|
205
|
+
throw new Error(err);
|
|
206
|
+
}
|
|
207
|
+
if (warnOnUnknownObjectProperties) {
|
|
208
|
+
console.warn(err);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function ensureProps(obj: ActivityObject): ActivityObject {
|
|
215
|
+
// ensure the name property, which can generally be inferred from the id
|
|
216
|
+
// name = obj.match(/(?(?\w+):\/\/)(?:.+@)?(.+?)(?:\/|$)/)[1]
|
|
217
|
+
return obj;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function expandStream(meta: ActivityStream) {
|
|
221
|
+
const stream = {};
|
|
222
|
+
for (const key of Object.keys(meta)) {
|
|
223
|
+
if (typeof meta[key] === "string") {
|
|
224
|
+
stream[key] = objs.get(meta[key]) || meta[key];
|
|
225
|
+
} else if (Array.isArray(meta[key])) {
|
|
226
|
+
stream[key] = [];
|
|
227
|
+
for (const entry of meta[key]) {
|
|
228
|
+
if (typeof entry === "string") {
|
|
229
|
+
stream[key].push(objs.get(entry) || entry);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
stream[key] = meta[key];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// only expand string into objects if they are in the expand list
|
|
238
|
+
for (const key of Object.keys(expand)) {
|
|
239
|
+
if (typeof stream[key] === "string") {
|
|
240
|
+
const idx = expand[key].primary;
|
|
241
|
+
const obj = {};
|
|
242
|
+
obj[idx] = stream[key];
|
|
243
|
+
stream[key] = obj;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return stream;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function Stream(
|
|
250
|
+
meta: ActivityStream,
|
|
251
|
+
): ActivityStream | ActivityObject | Record<string, never> {
|
|
252
|
+
validateObject("stream", meta);
|
|
253
|
+
if (typeof meta.object === "object") {
|
|
254
|
+
validateObject("object", meta.object);
|
|
255
|
+
}
|
|
256
|
+
const stream = expandStream(meta);
|
|
257
|
+
ee.emit("activity-stream", stream);
|
|
258
|
+
return stream;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface ActivityObjectManager {
|
|
262
|
+
create(obj: unknown): unknown;
|
|
263
|
+
delete(id: string): boolean;
|
|
264
|
+
list(): IterableIterator<unknown>;
|
|
265
|
+
get(id: string, expand?: boolean): unknown;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const _Object: ActivityObjectManager = {
|
|
269
|
+
create: (obj: ActivityObject) => {
|
|
270
|
+
validateObject("object", obj, true); // require ID for Object.create()
|
|
271
|
+
const ao = ensureProps(obj);
|
|
272
|
+
objs.set(ao.id, ao);
|
|
273
|
+
ee.emit("activity-object-create", ao);
|
|
274
|
+
return ao;
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
delete: (id) => {
|
|
278
|
+
const result = objs.delete(id);
|
|
279
|
+
if (result) {
|
|
280
|
+
ee.emit("activity-object-delete", id);
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
get: (id, expand) => {
|
|
286
|
+
let obj = objs.get(id);
|
|
287
|
+
if (!obj) {
|
|
288
|
+
if (!expand) {
|
|
289
|
+
return id;
|
|
290
|
+
}
|
|
291
|
+
obj = { id: id };
|
|
292
|
+
}
|
|
293
|
+
return ensureProps(obj);
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
list: (): IterableIterator<unknown> => objs.keys(),
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export interface ASFactoryOptions {
|
|
300
|
+
specialObjs?: Array<string>;
|
|
301
|
+
failOnUnknownObjectProperties?: boolean;
|
|
302
|
+
warnOnUnknownObjectProperties?: boolean;
|
|
303
|
+
customProps?: CustomProps;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
type EventCallback = (...args: unknown[]) => void;
|
|
307
|
+
|
|
308
|
+
export interface ASManager {
|
|
309
|
+
Stream(meta: unknown): ActivityStream | ActivityObject;
|
|
310
|
+
Object: ActivityObjectManager;
|
|
311
|
+
on(event: string, func: EventCallback): void;
|
|
312
|
+
once(event: string, func: EventCallback): void;
|
|
313
|
+
off(event: string, func: EventCallback): void;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function ASFactory(opts: ASFactoryOptions = {}): ASManager {
|
|
317
|
+
specialObjs = opts?.specialObjs || [];
|
|
318
|
+
failOnUnknownObjectProperties =
|
|
319
|
+
typeof opts.failOnUnknownObjectProperties === "boolean"
|
|
320
|
+
? opts.failOnUnknownObjectProperties
|
|
321
|
+
: failOnUnknownObjectProperties;
|
|
322
|
+
warnOnUnknownObjectProperties =
|
|
323
|
+
typeof opts.warnOnUnknownObjectProperties === "boolean"
|
|
324
|
+
? opts.warnOnUnknownObjectProperties
|
|
325
|
+
: warnOnUnknownObjectProperties;
|
|
326
|
+
for (const propName of Object.keys(opts.customProps || {})) {
|
|
327
|
+
if (typeof opts.customProps[propName] === "object") {
|
|
328
|
+
customProps[propName] = opts.customProps[propName];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
Stream: Stream,
|
|
334
|
+
Object: _Object,
|
|
335
|
+
on: (event, func) => ee.on(event, func),
|
|
336
|
+
once: (event, func) => ee.once(event, func),
|
|
337
|
+
off: (event, funcName) => ee.off(event, funcName),
|
|
338
|
+
} as ASManager;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
342
|
+
((global: any) => {
|
|
343
|
+
global.ASFactor = ASFactory;
|
|
344
|
+
})(typeof window === "object" ? window : {});
|