@fedify/botkit-redis 0.5.0-dev.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.
package/dist/mod.js ADDED
@@ -0,0 +1,534 @@
1
+ import { ActorScopedRepository } from "@fedify/botkit/repository";
2
+ import { exportJwk, importJwk } from "@fedify/fedify/sig";
3
+ import { Activity, Announce, Create, Follow, Object as Object$1, QuoteAuthorization, isActor } from "@fedify/vocab";
4
+ import { getLogger } from "@logtape/logtape";
5
+ import { createClient } from "redis";
6
+
7
+ //#region src/mod.ts
8
+ const logger = getLogger(["botkit", "redis"]);
9
+ const redisLockTimeoutMs = 3e4;
10
+ const redisLockPollIntervalMs = 20;
11
+ const redisLockRenewIntervalMs = 1e3;
12
+ const uuidV7Pattern = /^([0-9a-f]{8})-([0-9a-f]{4})-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13
+ /**
14
+ * A repository for storing bot data using Redis.
15
+ * @since 0.5.0
16
+ */
17
+ var RedisRepository = class {
18
+ client;
19
+ ownsClient;
20
+ ready;
21
+ /**
22
+ * The Redis key prefix under which all BotKit data is stored.
23
+ */
24
+ prefix;
25
+ /**
26
+ * Creates a new Redis repository.
27
+ * @param options The options for creating the repository.
28
+ */
29
+ constructor(options) {
30
+ const hasClient = "client" in options && options.client != null;
31
+ const hasUrl = "url" in options && options.url != null;
32
+ if (hasClient === hasUrl) throw new TypeError("RedisRepositoryOptions must provide exactly one of client or url.");
33
+ this.prefix = options.prefix ?? "botkit";
34
+ if (hasClient) {
35
+ this.client = options.client;
36
+ this.ownsClient = false;
37
+ this.ready = Promise.resolve();
38
+ } else {
39
+ const client = createClient({
40
+ ...options.clientOptions,
41
+ url: options.url.toString()
42
+ });
43
+ this.client = client;
44
+ this.ownsClient = true;
45
+ this.ready = client.connect?.() ?? Promise.resolve();
46
+ }
47
+ }
48
+ async [Symbol.asyncDispose]() {
49
+ await this.close();
50
+ }
51
+ /**
52
+ * Closes the owned Redis connection.
53
+ */
54
+ async close() {
55
+ await this.ready;
56
+ if (!this.ownsClient) return;
57
+ if (this.client.quit != null) await this.client.quit();
58
+ else await this.client.close?.();
59
+ }
60
+ key(...segments) {
61
+ return [this.prefix, ...segments.map((segment) => encodeURIComponent(segment))].join(":");
62
+ }
63
+ botKey(identifier, ...segments) {
64
+ return this.key("bots", identifier, ...segments);
65
+ }
66
+ async command(args) {
67
+ await this.ready;
68
+ try {
69
+ return await this.client.sendCommand(args);
70
+ } catch (error) {
71
+ logger.error("Redis command failed: {command}.", {
72
+ command: args[0],
73
+ error
74
+ });
75
+ throw error;
76
+ }
77
+ }
78
+ async get(key) {
79
+ const value = await this.command(["GET", key]);
80
+ return typeof value === "string" ? value : void 0;
81
+ }
82
+ async set(key, value) {
83
+ await this.command([
84
+ "SET",
85
+ key,
86
+ value
87
+ ]);
88
+ }
89
+ async del(...keys) {
90
+ if (keys.length < 1) return;
91
+ await this.command(["DEL", ...keys]);
92
+ }
93
+ async sAdd(key, member) {
94
+ await this.command([
95
+ "SADD",
96
+ key,
97
+ member
98
+ ]);
99
+ }
100
+ async sRem(key, member) {
101
+ await this.command([
102
+ "SREM",
103
+ key,
104
+ member
105
+ ]);
106
+ }
107
+ async sCard(key) {
108
+ return toNumber(await this.command(["SCARD", key]));
109
+ }
110
+ async zAdd(key, score, member, nx = false) {
111
+ await this.command([
112
+ "ZADD",
113
+ key,
114
+ ...nx ? ["NX"] : [],
115
+ score.toString(),
116
+ member
117
+ ]);
118
+ }
119
+ async zRem(key, member) {
120
+ await this.command([
121
+ "ZREM",
122
+ key,
123
+ member
124
+ ]);
125
+ }
126
+ async zRange(key) {
127
+ return toStringArray(await this.command([
128
+ "ZRANGE",
129
+ key,
130
+ "0",
131
+ "-1"
132
+ ]));
133
+ }
134
+ async zRangeByScore(key, min, max, reverse, limit) {
135
+ const command = reverse ? "ZREVRANGEBYSCORE" : "ZRANGEBYSCORE";
136
+ const args = [
137
+ command,
138
+ key,
139
+ reverse ? max : min,
140
+ reverse ? min : max
141
+ ];
142
+ if (limit != null) args.push("LIMIT", "0", limit.toString());
143
+ return toStringArray(await this.command(args));
144
+ }
145
+ async zCard(key) {
146
+ return toNumber(await this.command(["ZCARD", key]));
147
+ }
148
+ async nextSequence(key) {
149
+ return toNumber(await this.command(["INCR", key]));
150
+ }
151
+ async withRedisLock(lockKey, operation) {
152
+ const token = crypto.randomUUID();
153
+ while (await this.command([
154
+ "SET",
155
+ lockKey,
156
+ token,
157
+ "NX",
158
+ "PX",
159
+ redisLockTimeoutMs.toString()
160
+ ]) !== "OK") await delay(redisLockPollIntervalMs);
161
+ const renew = setInterval(() => {
162
+ (async () => {
163
+ try {
164
+ await this.command([
165
+ "EVAL",
166
+ "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) else return 0 end",
167
+ "1",
168
+ lockKey,
169
+ token,
170
+ redisLockTimeoutMs.toString()
171
+ ]);
172
+ } catch (error) {
173
+ logger.warn("Failed to renew Redis lock: {error}", { error });
174
+ }
175
+ })();
176
+ }, redisLockRenewIntervalMs);
177
+ try {
178
+ return await operation();
179
+ } finally {
180
+ clearInterval(renew);
181
+ try {
182
+ await this.command([
183
+ "EVAL",
184
+ "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end",
185
+ "1",
186
+ lockKey,
187
+ token
188
+ ]);
189
+ } catch (error) {
190
+ logger.warn("Failed to release Redis lock: {error}", { error });
191
+ }
192
+ }
193
+ }
194
+ async setKeyPairs(identifier, keyPairs) {
195
+ const pairs = [];
196
+ for (const keyPair of keyPairs) pairs.push({
197
+ private: await exportJwk(keyPair.privateKey),
198
+ public: await exportJwk(keyPair.publicKey)
199
+ });
200
+ await this.set(this.botKey(identifier, "keyPairs"), JSON.stringify(pairs));
201
+ }
202
+ async getKeyPairs(identifier) {
203
+ const json = await this.get(this.botKey(identifier, "keyPairs"));
204
+ if (json == null) return void 0;
205
+ const pairs = JSON.parse(json);
206
+ return await Promise.all(pairs.map(async (pair) => ({
207
+ privateKey: await importJwk(pair.private, "private"),
208
+ publicKey: await importJwk(pair.public, "public")
209
+ })));
210
+ }
211
+ async addMessage(identifier, id, activity) {
212
+ await this.set(this.botKey(identifier, "messages", id), JSON.stringify(await activity.toJsonLd({ format: "compact" })));
213
+ await this.zAdd(this.botKey(identifier, "messages"), getMessageScore(id, activity), id);
214
+ }
215
+ async updateMessage(identifier, id, updater) {
216
+ const key = this.botKey(identifier, "messages", id);
217
+ return await this.withRedisLock(this.botKey(identifier, "messages", id, "lock"), async () => {
218
+ const json = await this.get(key);
219
+ if (json == null) return false;
220
+ const activity = await Activity.fromJsonLd(JSON.parse(json));
221
+ if (!(activity instanceof Create || activity instanceof Announce)) return false;
222
+ const updated = await updater(activity);
223
+ if (updated == null) return false;
224
+ await this.set(key, JSON.stringify(await updated.toJsonLd({ format: "compact" })));
225
+ await this.zAdd(this.botKey(identifier, "messages"), getMessageScore(id, updated), id);
226
+ return true;
227
+ });
228
+ }
229
+ async removeMessage(identifier, id) {
230
+ const key = this.botKey(identifier, "messages", id);
231
+ const json = await this.get(key);
232
+ await this.del(key);
233
+ await this.zRem(this.botKey(identifier, "messages"), id);
234
+ if (json == null) return void 0;
235
+ const activity = await Activity.fromJsonLd(JSON.parse(json));
236
+ if (activity instanceof Create || activity instanceof Announce) return activity;
237
+ return void 0;
238
+ }
239
+ async *getMessages(identifier, options = {}) {
240
+ const min = options.since == null ? "-inf" : options.since.epochMilliseconds.toString();
241
+ const max = options.until == null ? "+inf" : options.until.epochMilliseconds.toString();
242
+ const ids = await this.zRangeByScore(this.botKey(identifier, "messages"), min, max, options.order == null || options.order === "newest", options.limit);
243
+ for (const id of ids) {
244
+ const json = await this.get(this.botKey(identifier, "messages", id));
245
+ if (json == null) continue;
246
+ try {
247
+ const activity = await Activity.fromJsonLd(JSON.parse(json));
248
+ if (activity instanceof Create || activity instanceof Announce) yield activity;
249
+ } catch {
250
+ continue;
251
+ }
252
+ }
253
+ }
254
+ async getMessage(identifier, id) {
255
+ const json = await this.get(this.botKey(identifier, "messages", id));
256
+ if (json == null) return void 0;
257
+ try {
258
+ const activity = await Activity.fromJsonLd(JSON.parse(json));
259
+ if (activity instanceof Create || activity instanceof Announce) return activity;
260
+ } catch {
261
+ return void 0;
262
+ }
263
+ return void 0;
264
+ }
265
+ countMessages(identifier) {
266
+ return this.zCard(this.botKey(identifier, "messages"));
267
+ }
268
+ async addFollower(identifier, followId, follower) {
269
+ if (follower.id == null) throw new TypeError("The follower ID is missing.");
270
+ const followerId = follower.id.href;
271
+ const followIdString = followId.href;
272
+ const requestKey = this.botKey(identifier, "followRequests", followIdString);
273
+ await this.withRedisLock(this.botKey(identifier, "followers", "lock"), async () => {
274
+ const previousFollowerId = await this.get(requestKey);
275
+ await this.set(this.botKey(identifier, "followers", followerId), JSON.stringify(await follower.toJsonLd({ format: "compact" })));
276
+ await this.zAdd(this.botKey(identifier, "followers"), await this.nextSequence(this.botKey(identifier, "followers", "seq")), followerId, true);
277
+ await this.sAdd(this.botKey(identifier, "followerRequests", followerId), followIdString);
278
+ await this.set(requestKey, followerId);
279
+ if (previousFollowerId != null && previousFollowerId !== followerId) {
280
+ await this.sRem(this.botKey(identifier, "followerRequests", previousFollowerId), followIdString);
281
+ await this.cleanupFollower(identifier, previousFollowerId);
282
+ }
283
+ });
284
+ }
285
+ async removeFollower(identifier, followId, followerId) {
286
+ const followIdString = followId.href;
287
+ const requestKey = this.botKey(identifier, "followRequests", followIdString);
288
+ return await this.withRedisLock(this.botKey(identifier, "followers", "lock"), async () => {
289
+ const currentFollowerId = await this.get(requestKey);
290
+ if (currentFollowerId !== followerId.href) return void 0;
291
+ const followerKey = this.botKey(identifier, "followers", followerId.href);
292
+ const json = await this.get(followerKey);
293
+ await this.del(requestKey);
294
+ await this.sRem(this.botKey(identifier, "followerRequests", followerId.href), followIdString);
295
+ const removed = await this.cleanupFollower(identifier, followerId.href);
296
+ if (!removed || json == null) return void 0;
297
+ try {
298
+ const actor = await Object$1.fromJsonLd(JSON.parse(json));
299
+ if (isActor(actor)) return actor;
300
+ } catch {
301
+ return void 0;
302
+ }
303
+ return void 0;
304
+ });
305
+ }
306
+ async cleanupFollower(identifier, followerId) {
307
+ const requestsKey = this.botKey(identifier, "followerRequests", followerId);
308
+ if (await this.sCard(requestsKey) > 0) return false;
309
+ await this.del(requestsKey, this.botKey(identifier, "followers", followerId));
310
+ await this.zRem(this.botKey(identifier, "followers"), followerId);
311
+ return true;
312
+ }
313
+ async hasFollower(identifier, followerId) {
314
+ return await this.get(this.botKey(identifier, "followers", followerId.href)) != null;
315
+ }
316
+ async *getFollowers(identifier, options = {}) {
317
+ const start = options.offset ?? 0;
318
+ const stop = options.limit == null ? -1 : start + options.limit - 1;
319
+ const ids = toStringArray(await this.command([
320
+ "ZRANGE",
321
+ this.botKey(identifier, "followers"),
322
+ start.toString(),
323
+ stop.toString()
324
+ ]));
325
+ for (const id of ids) {
326
+ const json = await this.get(this.botKey(identifier, "followers", id));
327
+ if (json == null) continue;
328
+ try {
329
+ const actor = await Object$1.fromJsonLd(JSON.parse(json));
330
+ if (isActor(actor)) yield actor;
331
+ } catch {
332
+ continue;
333
+ }
334
+ }
335
+ }
336
+ countFollowers(identifier) {
337
+ return this.zCard(this.botKey(identifier, "followers"));
338
+ }
339
+ async addSentFollow(identifier, id, follow) {
340
+ await this.set(this.botKey(identifier, "follows", id), JSON.stringify(await follow.toJsonLd({ format: "compact" })));
341
+ }
342
+ async removeSentFollow(identifier, id) {
343
+ const follow = await this.getSentFollow(identifier, id);
344
+ if (follow == null) return void 0;
345
+ await this.del(this.botKey(identifier, "follows", id));
346
+ return follow;
347
+ }
348
+ async getSentFollow(identifier, id) {
349
+ const json = await this.get(this.botKey(identifier, "follows", id));
350
+ if (json == null) return void 0;
351
+ try {
352
+ return await Follow.fromJsonLd(JSON.parse(json));
353
+ } catch {
354
+ return void 0;
355
+ }
356
+ }
357
+ async addFollowee(identifier, followeeId, follow) {
358
+ await this.set(this.botKey(identifier, "followees", followeeId.href), JSON.stringify(await follow.toJsonLd({ format: "compact" })));
359
+ await this.zAdd(this.key("index", "followees", followeeId.href), 0, identifier);
360
+ }
361
+ async removeFollowee(identifier, followeeId) {
362
+ const follow = await this.getFollowee(identifier, followeeId);
363
+ await this.del(this.botKey(identifier, "followees", followeeId.href));
364
+ await this.zRem(this.key("index", "followees", followeeId.href), identifier);
365
+ return follow;
366
+ }
367
+ async getFollowee(identifier, followeeId) {
368
+ const json = await this.get(this.botKey(identifier, "followees", followeeId.href));
369
+ if (json == null) return void 0;
370
+ try {
371
+ return await Follow.fromJsonLd(JSON.parse(json));
372
+ } catch {
373
+ return void 0;
374
+ }
375
+ }
376
+ async *findFollowedBots(followeeId) {
377
+ yield* await this.zRange(this.key("index", "followees", followeeId.href));
378
+ }
379
+ async addQuoteAuthorization(identifier, id, authorization) {
380
+ const interactingObject = authorization.interactingObjectId;
381
+ if (interactingObject == null) throw new TypeError("The quote authorization interacting object is missing.");
382
+ const indexKey = this.botKey(identifier, "quoteAuthorizationsByInteractingObject", interactingObject.href);
383
+ await this.withRedisLock(this.quoteAuthorizationLockKey(indexKey), async () => {
384
+ const existing = await this.get(indexKey);
385
+ if (existing != null) {
386
+ if (await this.getQuoteAuthorization(identifier, existing) != null) return;
387
+ await this.del(indexKey);
388
+ }
389
+ await this.set(this.botKey(identifier, "quoteAuthorizations", id), JSON.stringify(await authorization.toJsonLd({ format: "compact" })));
390
+ await this.set(indexKey, id);
391
+ });
392
+ }
393
+ async getQuoteAuthorization(identifier, id) {
394
+ const json = await this.get(this.botKey(identifier, "quoteAuthorizations", id));
395
+ if (json == null) return void 0;
396
+ try {
397
+ return await QuoteAuthorization.fromJsonLd(JSON.parse(json));
398
+ } catch {
399
+ return void 0;
400
+ }
401
+ }
402
+ async findQuoteAuthorization(identifier, interactingObject) {
403
+ const indexKey = this.botKey(identifier, "quoteAuthorizationsByInteractingObject", interactingObject.href);
404
+ return await this.withRedisLock(this.quoteAuthorizationLockKey(indexKey), async () => {
405
+ const id = await this.get(indexKey);
406
+ if (id == null) return void 0;
407
+ const authorization = await this.getQuoteAuthorization(identifier, id);
408
+ if (authorization == null && await this.get(indexKey) === id) await this.del(indexKey);
409
+ return authorization;
410
+ });
411
+ }
412
+ async removeQuoteAuthorization(identifier, id) {
413
+ const key = this.botKey(identifier, "quoteAuthorizations", id);
414
+ const json = await this.get(key);
415
+ await this.del(key);
416
+ if (json == null) return void 0;
417
+ let parsed;
418
+ try {
419
+ parsed = JSON.parse(json);
420
+ } catch {
421
+ return void 0;
422
+ }
423
+ const interactingObject = getRawQuoteAuthorizationInteractingObject(parsed);
424
+ if (interactingObject != null) {
425
+ const indexKey = this.botKey(identifier, "quoteAuthorizationsByInteractingObject", interactingObject.href);
426
+ await this.withRedisLock(this.quoteAuthorizationLockKey(indexKey), async () => {
427
+ if (await this.get(indexKey) === id) await this.del(indexKey);
428
+ });
429
+ }
430
+ try {
431
+ return await QuoteAuthorization.fromJsonLd(parsed);
432
+ } catch {
433
+ return void 0;
434
+ }
435
+ }
436
+ async addQuoteAuthorizationReference(identifier, authorization, messageId, attribution) {
437
+ const value = attribution == null ? { messageId } : {
438
+ messageId,
439
+ attribution: attribution.href
440
+ };
441
+ await this.set(this.botKey(identifier, "quoteAuthorizationRefs", authorization.href), JSON.stringify(value));
442
+ await this.zAdd(this.key("index", "quoteAuthorizationRefs", authorization.href), 0, identifier);
443
+ }
444
+ async findQuoteAuthorizationReference(identifier, authorization) {
445
+ const json = await this.get(this.botKey(identifier, "quoteAuthorizationRefs", authorization.href));
446
+ if (json == null) return void 0;
447
+ try {
448
+ const value = JSON.parse(json);
449
+ return value.messageId;
450
+ } catch {
451
+ return void 0;
452
+ }
453
+ }
454
+ async *findQuoteAuthorizationReferenceIdentifiers(authorization) {
455
+ yield* await this.zRange(this.key("index", "quoteAuthorizationRefs", authorization.href));
456
+ }
457
+ async findQuoteAuthorizationReferenceAttribution(identifier, authorization) {
458
+ const json = await this.get(this.botKey(identifier, "quoteAuthorizationRefs", authorization.href));
459
+ if (json == null) return void 0;
460
+ let value;
461
+ try {
462
+ value = JSON.parse(json);
463
+ } catch {
464
+ return void 0;
465
+ }
466
+ if (value.attribution == null) return void 0;
467
+ try {
468
+ return new URL(value.attribution);
469
+ } catch {
470
+ return void 0;
471
+ }
472
+ }
473
+ async removeQuoteAuthorizationReference(identifier, authorization) {
474
+ await this.del(this.botKey(identifier, "quoteAuthorizationRefs", authorization.href));
475
+ await this.zRem(this.key("index", "quoteAuthorizationRefs", authorization.href), identifier);
476
+ }
477
+ async vote(identifier, messageId, voterId, option) {
478
+ await this.sAdd(this.botKey(identifier, "polls", messageId, "voters"), voterId.href);
479
+ await this.sAdd(this.botKey(identifier, "polls", messageId, "options", option), voterId.href);
480
+ await this.zAdd(this.botKey(identifier, "polls", messageId, "options"), 0, option);
481
+ }
482
+ countVoters(identifier, messageId) {
483
+ return this.sCard(this.botKey(identifier, "polls", messageId, "voters"));
484
+ }
485
+ async countVotes(identifier, messageId) {
486
+ const result = {};
487
+ const options = await this.zRange(this.botKey(identifier, "polls", messageId, "options"));
488
+ for (const option of options) result[option] = await this.sCard(this.botKey(identifier, "polls", messageId, "options", option));
489
+ return result;
490
+ }
491
+ forIdentifier(identifier) {
492
+ return new ActorScopedRepository(this, identifier);
493
+ }
494
+ quoteAuthorizationLockKey(indexKey) {
495
+ return `${indexKey}:lock`;
496
+ }
497
+ };
498
+ function toNumber(value) {
499
+ if (typeof value === "number") return value;
500
+ if (typeof value === "bigint") return Number(value);
501
+ if (typeof value === "string") return Number.parseInt(value, 10);
502
+ return 0;
503
+ }
504
+ function toStringArray(value) {
505
+ if (!Array.isArray(value)) return [];
506
+ return value.filter((item) => typeof item === "string");
507
+ }
508
+ function delay(ms) {
509
+ return new Promise((resolve) => setTimeout(resolve, ms));
510
+ }
511
+ function getMessageScore(id, activity) {
512
+ if (activity.published != null) return activity.published.epochMilliseconds;
513
+ return extractTimestamp(id);
514
+ }
515
+ function extractTimestamp(uuid) {
516
+ const match = uuidV7Pattern.exec(uuid);
517
+ if (match == null) return 0;
518
+ return Number.parseInt(`${match[1]}${match[2]}`, 16);
519
+ }
520
+ function getRawQuoteAuthorizationInteractingObject(json) {
521
+ if (typeof json !== "object" || json == null) return void 0;
522
+ if (!("interactingObject" in json)) return void 0;
523
+ const interactingObject = json.interactingObject;
524
+ if (typeof interactingObject !== "string") return void 0;
525
+ try {
526
+ return new URL(interactingObject);
527
+ } catch {
528
+ return void 0;
529
+ }
530
+ }
531
+
532
+ //#endregion
533
+ export { RedisRepository };
534
+ //# sourceMappingURL=mod.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.js","names":["options: RedisRepositoryOptions","identifier: string","args: readonly string[]","key: string","value: string","member: string","score: number","min: string","max: string","reverse: boolean","limit?: number","lockKey: string","operation: () => Promise<T>","keyPairs: CryptoKeyPair[]","pairs: KeyPair[]","id: Uuid","activity: Create | Announce","updater: (\n existing: Create | Announce,\n ) => Create | Announce | undefined | Promise<Create | Announce | undefined>","options: RepositoryGetMessagesOptions","followId: URL","follower: Actor","followerId: URL","followerId: string","options: RepositoryGetFollowersOptions","follow: Follow","followeeId: URL","authorization: QuoteAuthorization","interactingObject: URL","parsed: unknown","authorization: URL","messageId: Uuid","attribution?: URL","value: QuoteAuthorizationReferenceData","voterId: URL","option: string","result: Record<string, number>","indexKey: string","value: unknown","ms: number","uuid: string","json: unknown"],"sources":["../src/mod.ts"],"sourcesContent":["// BotKit by Fedify: A framework for creating ActivityPub bots\n// Copyright (C) 2026 Hong Minhee <https://hongminhee.org/>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\nimport {\n ActorScopedRepository,\n type Repository,\n type RepositoryGetFollowersOptions,\n type RepositoryGetMessagesOptions,\n type Uuid,\n} from \"@fedify/botkit/repository\";\nimport { exportJwk, importJwk } from \"@fedify/fedify/sig\";\nimport {\n Activity,\n type Actor,\n Announce,\n Create,\n Follow,\n isActor,\n Object,\n QuoteAuthorization,\n} from \"@fedify/vocab\";\nimport { getLogger } from \"@logtape/logtape\";\nimport { createClient, type RedisClientOptions } from \"redis\";\n\nconst logger = getLogger([\"botkit\", \"redis\"]);\nconst redisLockTimeoutMs = 30_000;\nconst redisLockPollIntervalMs = 20;\nconst redisLockRenewIntervalMs = 1_000;\nconst uuidV7Pattern =\n /^([0-9a-f]{8})-([0-9a-f]{4})-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ninterface RedisClientLike {\n readonly isOpen?: boolean;\n connect?(): Promise<unknown>;\n quit?(): Promise<unknown>;\n close?(): Promise<unknown> | unknown;\n sendCommand(args: readonly string[]): Promise<unknown>;\n}\n\ninterface KeyPair {\n readonly private: JsonWebKey;\n readonly public: JsonWebKey;\n}\n\ninterface QuoteAuthorizationReferenceData {\n readonly messageId: Uuid;\n readonly attribution?: string;\n}\n\ninterface RedisRepositoryOptionsBase {\n /**\n * The Redis key prefix under which all BotKit data is stored.\n * @default `\"botkit\"`\n */\n readonly prefix?: string;\n}\n\ninterface RedisRepositoryOptionsWithClient extends RedisRepositoryOptionsBase {\n /**\n * A pre-configured Redis client to use. It must already be connected.\n */\n readonly client: RedisClientLike;\n\n /**\n * Disallowed when `client` is provided.\n */\n readonly url?: never;\n\n /**\n * Disallowed when `client` is provided.\n */\n readonly clientOptions?: never;\n}\n\ninterface RedisRepositoryOptionsWithUrl extends RedisRepositoryOptionsBase {\n /**\n * A Redis connection string to connect with.\n */\n readonly url: string | URL;\n\n /**\n * Disallowed when `url` is provided.\n */\n readonly client?: never;\n\n /**\n * Additional node-redis client options.\n */\n readonly clientOptions?: Omit<RedisClientOptions, \"url\">;\n}\n\n/**\n * Options for creating a Redis repository.\n * @since 0.5.0\n */\nexport type RedisRepositoryOptions =\n | RedisRepositoryOptionsWithClient\n | RedisRepositoryOptionsWithUrl;\n\n/**\n * A repository for storing bot data using Redis.\n * @since 0.5.0\n */\nexport class RedisRepository implements Repository, AsyncDisposable {\n private readonly client: RedisClientLike;\n private readonly ownsClient: boolean;\n private readonly ready: Promise<unknown>;\n\n /**\n * The Redis key prefix under which all BotKit data is stored.\n */\n readonly prefix: string;\n\n /**\n * Creates a new Redis repository.\n * @param options The options for creating the repository.\n */\n constructor(options: RedisRepositoryOptions) {\n const hasClient = \"client\" in options && options.client != null;\n const hasUrl = \"url\" in options && options.url != null;\n if (hasClient === hasUrl) {\n throw new TypeError(\n \"RedisRepositoryOptions must provide exactly one of client or url.\",\n );\n }\n this.prefix = options.prefix ?? \"botkit\";\n if (hasClient) {\n this.client = options.client;\n this.ownsClient = false;\n this.ready = Promise.resolve();\n } else {\n const client = createClient({\n ...options.clientOptions,\n url: options.url.toString(),\n }) as unknown as RedisClientLike;\n this.client = client;\n this.ownsClient = true;\n this.ready = client.connect?.() ?? Promise.resolve();\n }\n }\n\n async [Symbol.asyncDispose](): Promise<void> {\n await this.close();\n }\n\n /**\n * Closes the owned Redis connection.\n */\n async close(): Promise<void> {\n await this.ready;\n if (!this.ownsClient) return;\n if (this.client.quit != null) {\n await this.client.quit();\n } else {\n await this.client.close?.();\n }\n }\n\n private key(...segments: readonly string[]): string {\n return [\n this.prefix,\n ...segments.map((segment) => encodeURIComponent(segment)),\n ].join(\":\");\n }\n\n private botKey(identifier: string, ...segments: readonly string[]): string {\n return this.key(\"bots\", identifier, ...segments);\n }\n\n private async command(args: readonly string[]): Promise<unknown> {\n await this.ready;\n try {\n return await this.client.sendCommand(args);\n } catch (error) {\n logger.error(\"Redis command failed: {command}.\", {\n command: args[0],\n error,\n });\n throw error;\n }\n }\n\n private async get(key: string): Promise<string | undefined> {\n const value = await this.command([\"GET\", key]);\n return typeof value === \"string\" ? value : undefined;\n }\n\n private async set(key: string, value: string): Promise<void> {\n await this.command([\"SET\", key, value]);\n }\n\n private async del(...keys: readonly string[]): Promise<void> {\n if (keys.length < 1) return;\n await this.command([\"DEL\", ...keys]);\n }\n\n private async sAdd(key: string, member: string): Promise<void> {\n await this.command([\"SADD\", key, member]);\n }\n\n private async sRem(key: string, member: string): Promise<void> {\n await this.command([\"SREM\", key, member]);\n }\n\n private async sCard(key: string): Promise<number> {\n return toNumber(await this.command([\"SCARD\", key]));\n }\n\n private async zAdd(\n key: string,\n score: number,\n member: string,\n nx = false,\n ): Promise<void> {\n await this.command([\n \"ZADD\",\n key,\n ...(nx ? [\"NX\"] : []),\n score.toString(),\n member,\n ]);\n }\n\n private async zRem(key: string, member: string): Promise<void> {\n await this.command([\"ZREM\", key, member]);\n }\n\n private async zRange(key: string): Promise<string[]> {\n return toStringArray(await this.command([\"ZRANGE\", key, \"0\", \"-1\"]));\n }\n\n private async zRangeByScore(\n key: string,\n min: string,\n max: string,\n reverse: boolean,\n limit?: number,\n ): Promise<string[]> {\n const command = reverse ? \"ZREVRANGEBYSCORE\" : \"ZRANGEBYSCORE\";\n const args = [command, key, reverse ? max : min, reverse ? min : max];\n if (limit != null) args.push(\"LIMIT\", \"0\", limit.toString());\n return toStringArray(await this.command(args));\n }\n\n private async zCard(key: string): Promise<number> {\n return toNumber(await this.command([\"ZCARD\", key]));\n }\n\n private async nextSequence(key: string): Promise<number> {\n return toNumber(await this.command([\"INCR\", key]));\n }\n\n private async withRedisLock<T>(\n lockKey: string,\n operation: () => Promise<T>,\n ): Promise<T> {\n const token = crypto.randomUUID();\n while (\n await this.command([\n \"SET\",\n lockKey,\n token,\n \"NX\",\n \"PX\",\n redisLockTimeoutMs.toString(),\n ]) !== \"OK\"\n ) {\n await delay(redisLockPollIntervalMs);\n }\n const renew = setInterval(() => {\n void (async () => {\n try {\n await this.command([\n \"EVAL\",\n \"if redis.call('GET', KEYS[1]) == ARGV[1] then \" +\n \"return redis.call('PEXPIRE', KEYS[1], ARGV[2]) else return 0 end\",\n \"1\",\n lockKey,\n token,\n redisLockTimeoutMs.toString(),\n ]);\n } catch (error) {\n logger.warn(\"Failed to renew Redis lock: {error}\", { error });\n }\n })();\n }, redisLockRenewIntervalMs);\n try {\n return await operation();\n } finally {\n clearInterval(renew);\n try {\n await this.command([\n \"EVAL\",\n \"if redis.call('GET', KEYS[1]) == ARGV[1] then \" +\n \"return redis.call('DEL', KEYS[1]) else return 0 end\",\n \"1\",\n lockKey,\n token,\n ]);\n } catch (error) {\n logger.warn(\"Failed to release Redis lock: {error}\", { error });\n }\n }\n }\n\n async setKeyPairs(\n identifier: string,\n keyPairs: CryptoKeyPair[],\n ): Promise<void> {\n const pairs: KeyPair[] = [];\n for (const keyPair of keyPairs) {\n pairs.push({\n private: await exportJwk(keyPair.privateKey),\n public: await exportJwk(keyPair.publicKey),\n });\n }\n await this.set(this.botKey(identifier, \"keyPairs\"), JSON.stringify(pairs));\n }\n\n async getKeyPairs(identifier: string): Promise<CryptoKeyPair[] | undefined> {\n const json = await this.get(this.botKey(identifier, \"keyPairs\"));\n if (json == null) return undefined;\n const pairs = JSON.parse(json) as KeyPair[];\n return await Promise.all(pairs.map(async (pair) => ({\n privateKey: await importJwk(pair.private, \"private\"),\n publicKey: await importJwk(pair.public, \"public\"),\n })));\n }\n\n async addMessage(\n identifier: string,\n id: Uuid,\n activity: Create | Announce,\n ): Promise<void> {\n await this.set(\n this.botKey(identifier, \"messages\", id),\n JSON.stringify(await activity.toJsonLd({ format: \"compact\" })),\n );\n await this.zAdd(\n this.botKey(identifier, \"messages\"),\n getMessageScore(id, activity),\n id,\n );\n }\n\n async updateMessage(\n identifier: string,\n id: Uuid,\n updater: (\n existing: Create | Announce,\n ) => Create | Announce | undefined | Promise<Create | Announce | undefined>,\n ): Promise<boolean> {\n const key = this.botKey(identifier, \"messages\", id);\n return await this.withRedisLock(\n this.botKey(identifier, \"messages\", id, \"lock\"),\n async () => {\n const json = await this.get(key);\n if (json == null) return false;\n const activity = await Activity.fromJsonLd(JSON.parse(json));\n if (!(activity instanceof Create || activity instanceof Announce)) {\n return false;\n }\n const updated = await updater(activity);\n if (updated == null) return false;\n await this.set(\n key,\n JSON.stringify(await updated.toJsonLd({ format: \"compact\" })),\n );\n await this.zAdd(\n this.botKey(identifier, \"messages\"),\n getMessageScore(id, updated),\n id,\n );\n return true;\n },\n );\n }\n\n async removeMessage(\n identifier: string,\n id: Uuid,\n ): Promise<Create | Announce | undefined> {\n const key = this.botKey(identifier, \"messages\", id);\n const json = await this.get(key);\n await this.del(key);\n await this.zRem(this.botKey(identifier, \"messages\"), id);\n if (json == null) return undefined;\n const activity = await Activity.fromJsonLd(JSON.parse(json));\n if (activity instanceof Create || activity instanceof Announce) {\n return activity;\n }\n return undefined;\n }\n\n async *getMessages(\n identifier: string,\n options: RepositoryGetMessagesOptions = {},\n ): AsyncIterable<Create | Announce> {\n const min = options.since == null\n ? \"-inf\"\n : options.since.epochMilliseconds.toString();\n const max = options.until == null\n ? \"+inf\"\n : options.until.epochMilliseconds.toString();\n const ids = await this.zRangeByScore(\n this.botKey(identifier, \"messages\"),\n min,\n max,\n options.order == null || options.order === \"newest\",\n options.limit,\n );\n for (const id of ids) {\n const json = await this.get(this.botKey(identifier, \"messages\", id));\n if (json == null) continue;\n try {\n const activity = await Activity.fromJsonLd(JSON.parse(json));\n if (activity instanceof Create || activity instanceof Announce) {\n yield activity;\n }\n } catch {\n continue;\n }\n }\n }\n\n async getMessage(\n identifier: string,\n id: Uuid,\n ): Promise<Create | Announce | undefined> {\n const json = await this.get(this.botKey(identifier, \"messages\", id));\n if (json == null) return undefined;\n try {\n const activity = await Activity.fromJsonLd(JSON.parse(json));\n if (activity instanceof Create || activity instanceof Announce) {\n return activity;\n }\n } catch {\n return undefined;\n }\n return undefined;\n }\n\n countMessages(identifier: string): Promise<number> {\n return this.zCard(this.botKey(identifier, \"messages\"));\n }\n\n async addFollower(\n identifier: string,\n followId: URL,\n follower: Actor,\n ): Promise<void> {\n if (follower.id == null) {\n throw new TypeError(\"The follower ID is missing.\");\n }\n const followerId = follower.id.href;\n const followIdString = followId.href;\n const requestKey = this.botKey(\n identifier,\n \"followRequests\",\n followIdString,\n );\n await this.withRedisLock(\n this.botKey(identifier, \"followers\", \"lock\"),\n async () => {\n const previousFollowerId = await this.get(requestKey);\n await this.set(\n this.botKey(identifier, \"followers\", followerId),\n JSON.stringify(await follower.toJsonLd({ format: \"compact\" })),\n );\n await this.zAdd(\n this.botKey(identifier, \"followers\"),\n await this.nextSequence(this.botKey(identifier, \"followers\", \"seq\")),\n followerId,\n true,\n );\n await this.sAdd(\n this.botKey(identifier, \"followerRequests\", followerId),\n followIdString,\n );\n await this.set(requestKey, followerId);\n if (previousFollowerId != null && previousFollowerId !== followerId) {\n await this.sRem(\n this.botKey(identifier, \"followerRequests\", previousFollowerId),\n followIdString,\n );\n await this.cleanupFollower(identifier, previousFollowerId);\n }\n },\n );\n }\n\n async removeFollower(\n identifier: string,\n followId: URL,\n followerId: URL,\n ): Promise<Actor | undefined> {\n const followIdString = followId.href;\n const requestKey = this.botKey(\n identifier,\n \"followRequests\",\n followIdString,\n );\n return await this.withRedisLock(\n this.botKey(identifier, \"followers\", \"lock\"),\n async () => {\n const currentFollowerId = await this.get(requestKey);\n if (currentFollowerId !== followerId.href) return undefined;\n const followerKey = this.botKey(\n identifier,\n \"followers\",\n followerId.href,\n );\n const json = await this.get(followerKey);\n await this.del(requestKey);\n await this.sRem(\n this.botKey(identifier, \"followerRequests\", followerId.href),\n followIdString,\n );\n const removed = await this.cleanupFollower(identifier, followerId.href);\n if (!removed || json == null) return undefined;\n try {\n const actor = await Object.fromJsonLd(JSON.parse(json));\n if (isActor(actor)) return actor;\n } catch {\n return undefined;\n }\n return undefined;\n },\n );\n }\n\n private async cleanupFollower(\n identifier: string,\n followerId: string,\n ): Promise<boolean> {\n const requestsKey = this.botKey(identifier, \"followerRequests\", followerId);\n if (await this.sCard(requestsKey) > 0) return false;\n await this.del(\n requestsKey,\n this.botKey(identifier, \"followers\", followerId),\n );\n await this.zRem(this.botKey(identifier, \"followers\"), followerId);\n return true;\n }\n\n async hasFollower(identifier: string, followerId: URL): Promise<boolean> {\n return await this.get(\n this.botKey(identifier, \"followers\", followerId.href),\n ) !=\n null;\n }\n\n async *getFollowers(\n identifier: string,\n options: RepositoryGetFollowersOptions = {},\n ): AsyncIterable<Actor> {\n const start = options.offset ?? 0;\n const stop = options.limit == null ? -1 : start + options.limit - 1;\n const ids = toStringArray(\n await this.command([\n \"ZRANGE\",\n this.botKey(identifier, \"followers\"),\n start.toString(),\n stop.toString(),\n ]),\n );\n for (const id of ids) {\n const json = await this.get(this.botKey(identifier, \"followers\", id));\n if (json == null) continue;\n try {\n const actor = await Object.fromJsonLd(JSON.parse(json));\n if (isActor(actor)) yield actor;\n } catch {\n continue;\n }\n }\n }\n\n countFollowers(identifier: string): Promise<number> {\n return this.zCard(this.botKey(identifier, \"followers\"));\n }\n\n async addSentFollow(\n identifier: string,\n id: Uuid,\n follow: Follow,\n ): Promise<void> {\n await this.set(\n this.botKey(identifier, \"follows\", id),\n JSON.stringify(await follow.toJsonLd({ format: \"compact\" })),\n );\n }\n\n async removeSentFollow(\n identifier: string,\n id: Uuid,\n ): Promise<Follow | undefined> {\n const follow = await this.getSentFollow(identifier, id);\n if (follow == null) return undefined;\n await this.del(this.botKey(identifier, \"follows\", id));\n return follow;\n }\n\n async getSentFollow(\n identifier: string,\n id: Uuid,\n ): Promise<Follow | undefined> {\n const json = await this.get(this.botKey(identifier, \"follows\", id));\n if (json == null) return undefined;\n try {\n return await Follow.fromJsonLd(JSON.parse(json));\n } catch {\n return undefined;\n }\n }\n\n async addFollowee(\n identifier: string,\n followeeId: URL,\n follow: Follow,\n ): Promise<void> {\n await this.set(\n this.botKey(identifier, \"followees\", followeeId.href),\n JSON.stringify(await follow.toJsonLd({ format: \"compact\" })),\n );\n await this.zAdd(\n this.key(\"index\", \"followees\", followeeId.href),\n 0,\n identifier,\n );\n }\n\n async removeFollowee(\n identifier: string,\n followeeId: URL,\n ): Promise<Follow | undefined> {\n const follow = await this.getFollowee(identifier, followeeId);\n await this.del(this.botKey(identifier, \"followees\", followeeId.href));\n await this.zRem(\n this.key(\"index\", \"followees\", followeeId.href),\n identifier,\n );\n return follow;\n }\n\n async getFollowee(\n identifier: string,\n followeeId: URL,\n ): Promise<Follow | undefined> {\n const json = await this.get(\n this.botKey(identifier, \"followees\", followeeId.href),\n );\n if (json == null) return undefined;\n try {\n return await Follow.fromJsonLd(JSON.parse(json));\n } catch {\n return undefined;\n }\n }\n\n async *findFollowedBots(followeeId: URL): AsyncIterable<string> {\n yield* await this.zRange(this.key(\"index\", \"followees\", followeeId.href));\n }\n\n async addQuoteAuthorization(\n identifier: string,\n id: Uuid,\n authorization: QuoteAuthorization,\n ): Promise<void> {\n const interactingObject = authorization.interactingObjectId;\n if (interactingObject == null) {\n throw new TypeError(\n \"The quote authorization interacting object is missing.\",\n );\n }\n const indexKey = this.botKey(\n identifier,\n \"quoteAuthorizationsByInteractingObject\",\n interactingObject.href,\n );\n await this.withRedisLock(\n this.quoteAuthorizationLockKey(indexKey),\n async () => {\n const existing = await this.get(indexKey);\n if (existing != null) {\n if (\n await this.getQuoteAuthorization(identifier, existing as Uuid) !=\n null\n ) {\n return;\n }\n await this.del(indexKey);\n }\n await this.set(\n this.botKey(identifier, \"quoteAuthorizations\", id),\n JSON.stringify(await authorization.toJsonLd({ format: \"compact\" })),\n );\n await this.set(indexKey, id);\n },\n );\n }\n\n async getQuoteAuthorization(\n identifier: string,\n id: Uuid,\n ): Promise<QuoteAuthorization | undefined> {\n const json = await this.get(\n this.botKey(identifier, \"quoteAuthorizations\", id),\n );\n if (json == null) return undefined;\n try {\n return await QuoteAuthorization.fromJsonLd(JSON.parse(json));\n } catch {\n return undefined;\n }\n }\n\n async findQuoteAuthorization(\n identifier: string,\n interactingObject: URL,\n ): Promise<QuoteAuthorization | undefined> {\n const indexKey = this.botKey(\n identifier,\n \"quoteAuthorizationsByInteractingObject\",\n interactingObject.href,\n );\n return await this.withRedisLock(\n this.quoteAuthorizationLockKey(indexKey),\n async () => {\n const id = await this.get(indexKey);\n if (id == null) return undefined;\n const authorization = await this.getQuoteAuthorization(\n identifier,\n id as Uuid,\n );\n if (authorization == null && await this.get(indexKey) === id) {\n await this.del(indexKey);\n }\n return authorization;\n },\n );\n }\n\n async removeQuoteAuthorization(\n identifier: string,\n id: Uuid,\n ): Promise<QuoteAuthorization | undefined> {\n const key = this.botKey(identifier, \"quoteAuthorizations\", id);\n const json = await this.get(key);\n await this.del(key);\n if (json == null) return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n return undefined;\n }\n const interactingObject = getRawQuoteAuthorizationInteractingObject(parsed);\n if (interactingObject != null) {\n const indexKey = this.botKey(\n identifier,\n \"quoteAuthorizationsByInteractingObject\",\n interactingObject.href,\n );\n await this.withRedisLock(\n this.quoteAuthorizationLockKey(indexKey),\n async () => {\n if (await this.get(indexKey) === id) await this.del(indexKey);\n },\n );\n }\n try {\n return await QuoteAuthorization.fromJsonLd(parsed);\n } catch {\n return undefined;\n }\n }\n\n async addQuoteAuthorizationReference(\n identifier: string,\n authorization: URL,\n messageId: Uuid,\n attribution?: URL,\n ): Promise<void> {\n const value: QuoteAuthorizationReferenceData = attribution == null\n ? { messageId }\n : { messageId, attribution: attribution.href };\n await this.set(\n this.botKey(identifier, \"quoteAuthorizationRefs\", authorization.href),\n JSON.stringify(value),\n );\n await this.zAdd(\n this.key(\"index\", \"quoteAuthorizationRefs\", authorization.href),\n 0,\n identifier,\n );\n }\n\n async findQuoteAuthorizationReference(\n identifier: string,\n authorization: URL,\n ): Promise<Uuid | undefined> {\n const json = await this.get(\n this.botKey(identifier, \"quoteAuthorizationRefs\", authorization.href),\n );\n if (json == null) return undefined;\n try {\n const value = JSON.parse(json) as QuoteAuthorizationReferenceData;\n return value.messageId;\n } catch {\n return undefined;\n }\n }\n\n async *findQuoteAuthorizationReferenceIdentifiers(\n authorization: URL,\n ): AsyncIterable<string> {\n yield* await this.zRange(\n this.key(\"index\", \"quoteAuthorizationRefs\", authorization.href),\n );\n }\n\n async findQuoteAuthorizationReferenceAttribution(\n identifier: string,\n authorization: URL,\n ): Promise<URL | undefined> {\n const json = await this.get(\n this.botKey(identifier, \"quoteAuthorizationRefs\", authorization.href),\n );\n if (json == null) return undefined;\n let value: QuoteAuthorizationReferenceData;\n try {\n value = JSON.parse(json) as QuoteAuthorizationReferenceData;\n } catch {\n return undefined;\n }\n if (value.attribution == null) return undefined;\n try {\n return new URL(value.attribution);\n } catch {\n return undefined;\n }\n }\n\n async removeQuoteAuthorizationReference(\n identifier: string,\n authorization: URL,\n ): Promise<void> {\n await this.del(\n this.botKey(identifier, \"quoteAuthorizationRefs\", authorization.href),\n );\n await this.zRem(\n this.key(\"index\", \"quoteAuthorizationRefs\", authorization.href),\n identifier,\n );\n }\n\n async vote(\n identifier: string,\n messageId: Uuid,\n voterId: URL,\n option: string,\n ): Promise<void> {\n await this.sAdd(\n this.botKey(identifier, \"polls\", messageId, \"voters\"),\n voterId.href,\n );\n await this.sAdd(\n this.botKey(identifier, \"polls\", messageId, \"options\", option),\n voterId.href,\n );\n await this.zAdd(\n this.botKey(identifier, \"polls\", messageId, \"options\"),\n 0,\n option,\n );\n }\n\n countVoters(identifier: string, messageId: Uuid): Promise<number> {\n return this.sCard(this.botKey(identifier, \"polls\", messageId, \"voters\"));\n }\n\n async countVotes(\n identifier: string,\n messageId: Uuid,\n ): Promise<Readonly<Record<string, number>>> {\n const result: Record<string, number> = {};\n const options = await this.zRange(\n this.botKey(identifier, \"polls\", messageId, \"options\"),\n );\n for (const option of options) {\n result[option] = await this.sCard(\n this.botKey(identifier, \"polls\", messageId, \"options\", option),\n );\n }\n return result;\n }\n\n forIdentifier(identifier: string): ActorScopedRepository {\n return new ActorScopedRepository(this, identifier);\n }\n\n private quoteAuthorizationLockKey(indexKey: string): string {\n return `${indexKey}:lock`;\n }\n}\n\nfunction toNumber(value: unknown): number {\n if (typeof value === \"number\") return value;\n if (typeof value === \"bigint\") return Number(value);\n if (typeof value === \"string\") return Number.parseInt(value, 10);\n return 0;\n}\n\nfunction toStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return value.filter((item): item is string => typeof item === \"string\");\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction getMessageScore(id: Uuid, activity: Create | Announce): number {\n if (activity.published != null) return activity.published.epochMilliseconds;\n return extractTimestamp(id);\n}\n\nfunction extractTimestamp(uuid: string): number {\n const match = uuidV7Pattern.exec(uuid);\n if (match == null) return 0;\n return Number.parseInt(`${match[1]}${match[2]}`, 16);\n}\n\nfunction getRawQuoteAuthorizationInteractingObject(\n json: unknown,\n): URL | undefined {\n if (typeof json !== \"object\" || json == null) return undefined;\n if (!(\"interactingObject\" in json)) return undefined;\n const interactingObject = json.interactingObject;\n if (typeof interactingObject !== \"string\") return undefined;\n try {\n return new URL(interactingObject);\n } catch {\n return undefined;\n }\n}\n"],"mappings":";;;;;;;AAoCA,MAAM,SAAS,UAAU,CAAC,UAAU,OAAQ,EAAC;AAC7C,MAAM,qBAAqB;AAC3B,MAAM,0BAA0B;AAChC,MAAM,2BAA2B;AACjC,MAAM,gBACJ;;;;;AA0EF,IAAa,kBAAb,MAAoE;CAClE,AAAiB;CACjB,AAAiB;CACjB,AAAiB;;;;CAKjB,AAAS;;;;;CAMT,YAAYA,SAAiC;EAC3C,MAAM,YAAY,YAAY,WAAW,QAAQ,UAAU;EAC3D,MAAM,SAAS,SAAS,WAAW,QAAQ,OAAO;AAClD,MAAI,cAAc,OAChB,OAAM,IAAI,UACR;AAGJ,OAAK,SAAS,QAAQ,UAAU;AAChC,MAAI,WAAW;AACb,QAAK,SAAS,QAAQ;AACtB,QAAK,aAAa;AAClB,QAAK,QAAQ,QAAQ,SAAS;EAC/B,OAAM;GACL,MAAM,SAAS,aAAa;IAC1B,GAAG,QAAQ;IACX,KAAK,QAAQ,IAAI,UAAU;GAC5B,EAAC;AACF,QAAK,SAAS;AACd,QAAK,aAAa;AAClB,QAAK,QAAQ,OAAO,WAAW,IAAI,QAAQ,SAAS;EACrD;CACF;CAED,OAAO,OAAO,gBAA+B;AAC3C,QAAM,KAAK,OAAO;CACnB;;;;CAKD,MAAM,QAAuB;AAC3B,QAAM,KAAK;AACX,OAAK,KAAK,WAAY;AACtB,MAAI,KAAK,OAAO,QAAQ,KACtB,OAAM,KAAK,OAAO,MAAM;MAExB,OAAM,KAAK,OAAO,SAAS;CAE9B;CAED,AAAQ,IAAI,GAAG,UAAqC;AAClD,SAAO,CACL,KAAK,QACL,GAAG,SAAS,IAAI,CAAC,YAAY,mBAAmB,QAAQ,CAAC,AAC1D,EAAC,KAAK,IAAI;CACZ;CAED,AAAQ,OAAOC,YAAoB,GAAG,UAAqC;AACzE,SAAO,KAAK,IAAI,QAAQ,YAAY,GAAG,SAAS;CACjD;CAED,MAAc,QAAQC,MAA2C;AAC/D,QAAM,KAAK;AACX,MAAI;AACF,UAAO,MAAM,KAAK,OAAO,YAAY,KAAK;EAC3C,SAAQ,OAAO;AACd,UAAO,MAAM,oCAAoC;IAC/C,SAAS,KAAK;IACd;GACD,EAAC;AACF,SAAM;EACP;CACF;CAED,MAAc,IAAIC,KAA0C;EAC1D,MAAM,QAAQ,MAAM,KAAK,QAAQ,CAAC,OAAO,GAAI,EAAC;AAC9C,gBAAc,UAAU,WAAW;CACpC;CAED,MAAc,IAAIA,KAAaC,OAA8B;AAC3D,QAAM,KAAK,QAAQ;GAAC;GAAO;GAAK;EAAM,EAAC;CACxC;CAED,MAAc,IAAI,GAAG,MAAwC;AAC3D,MAAI,KAAK,SAAS,EAAG;AACrB,QAAM,KAAK,QAAQ,CAAC,OAAO,GAAG,IAAK,EAAC;CACrC;CAED,MAAc,KAAKD,KAAaE,QAA+B;AAC7D,QAAM,KAAK,QAAQ;GAAC;GAAQ;GAAK;EAAO,EAAC;CAC1C;CAED,MAAc,KAAKF,KAAaE,QAA+B;AAC7D,QAAM,KAAK,QAAQ;GAAC;GAAQ;GAAK;EAAO,EAAC;CAC1C;CAED,MAAc,MAAMF,KAA8B;AAChD,SAAO,SAAS,MAAM,KAAK,QAAQ,CAAC,SAAS,GAAI,EAAC,CAAC;CACpD;CAED,MAAc,KACZA,KACAG,OACAD,QACA,KAAK,OACU;AACf,QAAM,KAAK,QAAQ;GACjB;GACA;GACA,GAAI,KAAK,CAAC,IAAK,IAAG,CAAE;GACpB,MAAM,UAAU;GAChB;EACD,EAAC;CACH;CAED,MAAc,KAAKF,KAAaE,QAA+B;AAC7D,QAAM,KAAK,QAAQ;GAAC;GAAQ;GAAK;EAAO,EAAC;CAC1C;CAED,MAAc,OAAOF,KAAgC;AACnD,SAAO,cAAc,MAAM,KAAK,QAAQ;GAAC;GAAU;GAAK;GAAK;EAAK,EAAC,CAAC;CACrE;CAED,MAAc,cACZA,KACAI,KACAC,KACAC,SACAC,OACmB;EACnB,MAAM,UAAU,UAAU,qBAAqB;EAC/C,MAAM,OAAO;GAAC;GAAS;GAAK,UAAU,MAAM;GAAK,UAAU,MAAM;EAAI;AACrE,MAAI,SAAS,KAAM,MAAK,KAAK,SAAS,KAAK,MAAM,UAAU,CAAC;AAC5D,SAAO,cAAc,MAAM,KAAK,QAAQ,KAAK,CAAC;CAC/C;CAED,MAAc,MAAMP,KAA8B;AAChD,SAAO,SAAS,MAAM,KAAK,QAAQ,CAAC,SAAS,GAAI,EAAC,CAAC;CACpD;CAED,MAAc,aAAaA,KAA8B;AACvD,SAAO,SAAS,MAAM,KAAK,QAAQ,CAAC,QAAQ,GAAI,EAAC,CAAC;CACnD;CAED,MAAc,cACZQ,SACAC,WACY;EACZ,MAAM,QAAQ,OAAO,YAAY;AACjC,SACE,MAAM,KAAK,QAAQ;GACjB;GACA;GACA;GACA;GACA;GACA,mBAAmB,UAAU;EAC9B,EAAC,KAAK,KAEP,OAAM,MAAM,wBAAwB;EAEtC,MAAM,QAAQ,YAAY,MAAM;AAC9B,GAAK,CAAC,YAAY;AAChB,QAAI;AACF,WAAM,KAAK,QAAQ;MACjB;MACA;MAEA;MACA;MACA;MACA,mBAAmB,UAAU;KAC9B,EAAC;IACH,SAAQ,OAAO;AACd,YAAO,KAAK,uCAAuC,EAAE,MAAO,EAAC;IAC9D;GACF,IAAG;EACL,GAAE,yBAAyB;AAC5B,MAAI;AACF,UAAO,MAAM,WAAW;EACzB,UAAS;AACR,iBAAc,MAAM;AACpB,OAAI;AACF,UAAM,KAAK,QAAQ;KACjB;KACA;KAEA;KACA;KACA;IACD,EAAC;GACH,SAAQ,OAAO;AACd,WAAO,KAAK,yCAAyC,EAAE,MAAO,EAAC;GAChE;EACF;CACF;CAED,MAAM,YACJX,YACAY,UACe;EACf,MAAMC,QAAmB,CAAE;AAC3B,OAAK,MAAM,WAAW,SACpB,OAAM,KAAK;GACT,SAAS,MAAM,UAAU,QAAQ,WAAW;GAC5C,QAAQ,MAAM,UAAU,QAAQ,UAAU;EAC3C,EAAC;AAEJ,QAAM,KAAK,IAAI,KAAK,OAAO,YAAY,WAAW,EAAE,KAAK,UAAU,MAAM,CAAC;CAC3E;CAED,MAAM,YAAYb,YAA0D;EAC1E,MAAM,OAAO,MAAM,KAAK,IAAI,KAAK,OAAO,YAAY,WAAW,CAAC;AAChE,MAAI,QAAQ,KAAM;EAClB,MAAM,QAAQ,KAAK,MAAM,KAAK;AAC9B,SAAO,MAAM,QAAQ,IAAI,MAAM,IAAI,OAAO,UAAU;GAClD,YAAY,MAAM,UAAU,KAAK,SAAS,UAAU;GACpD,WAAW,MAAM,UAAU,KAAK,QAAQ,SAAS;EAClD,GAAE,CAAC;CACL;CAED,MAAM,WACJA,YACAc,IACAC,UACe;AACf,QAAM,KAAK,IACT,KAAK,OAAO,YAAY,YAAY,GAAG,EACvC,KAAK,UAAU,MAAM,SAAS,SAAS,EAAE,QAAQ,UAAW,EAAC,CAAC,CAC/D;AACD,QAAM,KAAK,KACT,KAAK,OAAO,YAAY,WAAW,EACnC,gBAAgB,IAAI,SAAS,EAC7B,GACD;CACF;CAED,MAAM,cACJf,YACAc,IACAE,SAGkB;EAClB,MAAM,MAAM,KAAK,OAAO,YAAY,YAAY,GAAG;AACnD,SAAO,MAAM,KAAK,cAChB,KAAK,OAAO,YAAY,YAAY,IAAI,OAAO,EAC/C,YAAY;GACV,MAAM,OAAO,MAAM,KAAK,IAAI,IAAI;AAChC,OAAI,QAAQ,KAAM,QAAO;GACzB,MAAM,WAAW,MAAM,SAAS,WAAW,KAAK,MAAM,KAAK,CAAC;AAC5D,SAAM,oBAAoB,UAAU,oBAAoB,UACtD,QAAO;GAET,MAAM,UAAU,MAAM,QAAQ,SAAS;AACvC,OAAI,WAAW,KAAM,QAAO;AAC5B,SAAM,KAAK,IACT,KACA,KAAK,UAAU,MAAM,QAAQ,SAAS,EAAE,QAAQ,UAAW,EAAC,CAAC,CAC9D;AACD,SAAM,KAAK,KACT,KAAK,OAAO,YAAY,WAAW,EACnC,gBAAgB,IAAI,QAAQ,EAC5B,GACD;AACD,UAAO;EACR,EACF;CACF;CAED,MAAM,cACJhB,YACAc,IACwC;EACxC,MAAM,MAAM,KAAK,OAAO,YAAY,YAAY,GAAG;EACnD,MAAM,OAAO,MAAM,KAAK,IAAI,IAAI;AAChC,QAAM,KAAK,IAAI,IAAI;AACnB,QAAM,KAAK,KAAK,KAAK,OAAO,YAAY,WAAW,EAAE,GAAG;AACxD,MAAI,QAAQ,KAAM;EAClB,MAAM,WAAW,MAAM,SAAS,WAAW,KAAK,MAAM,KAAK,CAAC;AAC5D,MAAI,oBAAoB,UAAU,oBAAoB,SACpD,QAAO;AAET;CACD;CAED,OAAO,YACLd,YACAiB,UAAwC,CAAE,GACR;EAClC,MAAM,MAAM,QAAQ,SAAS,OACzB,SACA,QAAQ,MAAM,kBAAkB,UAAU;EAC9C,MAAM,MAAM,QAAQ,SAAS,OACzB,SACA,QAAQ,MAAM,kBAAkB,UAAU;EAC9C,MAAM,MAAM,MAAM,KAAK,cACrB,KAAK,OAAO,YAAY,WAAW,EACnC,KACA,KACA,QAAQ,SAAS,QAAQ,QAAQ,UAAU,UAC3C,QAAQ,MACT;AACD,OAAK,MAAM,MAAM,KAAK;GACpB,MAAM,OAAO,MAAM,KAAK,IAAI,KAAK,OAAO,YAAY,YAAY,GAAG,CAAC;AACpE,OAAI,QAAQ,KAAM;AAClB,OAAI;IACF,MAAM,WAAW,MAAM,SAAS,WAAW,KAAK,MAAM,KAAK,CAAC;AAC5D,QAAI,oBAAoB,UAAU,oBAAoB,SACpD,OAAM;GAET,QAAO;AACN;GACD;EACF;CACF;CAED,MAAM,WACJjB,YACAc,IACwC;EACxC,MAAM,OAAO,MAAM,KAAK,IAAI,KAAK,OAAO,YAAY,YAAY,GAAG,CAAC;AACpE,MAAI,QAAQ,KAAM;AAClB,MAAI;GACF,MAAM,WAAW,MAAM,SAAS,WAAW,KAAK,MAAM,KAAK,CAAC;AAC5D,OAAI,oBAAoB,UAAU,oBAAoB,SACpD,QAAO;EAEV,QAAO;AACN;EACD;AACD;CACD;CAED,cAAcd,YAAqC;AACjD,SAAO,KAAK,MAAM,KAAK,OAAO,YAAY,WAAW,CAAC;CACvD;CAED,MAAM,YACJA,YACAkB,UACAC,UACe;AACf,MAAI,SAAS,MAAM,KACjB,OAAM,IAAI,UAAU;EAEtB,MAAM,aAAa,SAAS,GAAG;EAC/B,MAAM,iBAAiB,SAAS;EAChC,MAAM,aAAa,KAAK,OACtB,YACA,kBACA,eACD;AACD,QAAM,KAAK,cACT,KAAK,OAAO,YAAY,aAAa,OAAO,EAC5C,YAAY;GACV,MAAM,qBAAqB,MAAM,KAAK,IAAI,WAAW;AACrD,SAAM,KAAK,IACT,KAAK,OAAO,YAAY,aAAa,WAAW,EAChD,KAAK,UAAU,MAAM,SAAS,SAAS,EAAE,QAAQ,UAAW,EAAC,CAAC,CAC/D;AACD,SAAM,KAAK,KACT,KAAK,OAAO,YAAY,YAAY,EACpC,MAAM,KAAK,aAAa,KAAK,OAAO,YAAY,aAAa,MAAM,CAAC,EACpE,YACA,KACD;AACD,SAAM,KAAK,KACT,KAAK,OAAO,YAAY,oBAAoB,WAAW,EACvD,eACD;AACD,SAAM,KAAK,IAAI,YAAY,WAAW;AACtC,OAAI,sBAAsB,QAAQ,uBAAuB,YAAY;AACnE,UAAM,KAAK,KACT,KAAK,OAAO,YAAY,oBAAoB,mBAAmB,EAC/D,eACD;AACD,UAAM,KAAK,gBAAgB,YAAY,mBAAmB;GAC3D;EACF,EACF;CACF;CAED,MAAM,eACJnB,YACAkB,UACAE,YAC4B;EAC5B,MAAM,iBAAiB,SAAS;EAChC,MAAM,aAAa,KAAK,OACtB,YACA,kBACA,eACD;AACD,SAAO,MAAM,KAAK,cAChB,KAAK,OAAO,YAAY,aAAa,OAAO,EAC5C,YAAY;GACV,MAAM,oBAAoB,MAAM,KAAK,IAAI,WAAW;AACpD,OAAI,sBAAsB,WAAW,KAAM;GAC3C,MAAM,cAAc,KAAK,OACvB,YACA,aACA,WAAW,KACZ;GACD,MAAM,OAAO,MAAM,KAAK,IAAI,YAAY;AACxC,SAAM,KAAK,IAAI,WAAW;AAC1B,SAAM,KAAK,KACT,KAAK,OAAO,YAAY,oBAAoB,WAAW,KAAK,EAC5D,eACD;GACD,MAAM,UAAU,MAAM,KAAK,gBAAgB,YAAY,WAAW,KAAK;AACvE,QAAK,WAAW,QAAQ,KAAM;AAC9B,OAAI;IACF,MAAM,QAAQ,MAAM,SAAO,WAAW,KAAK,MAAM,KAAK,CAAC;AACvD,QAAI,QAAQ,MAAM,CAAE,QAAO;GAC5B,QAAO;AACN;GACD;AACD;EACD,EACF;CACF;CAED,MAAc,gBACZpB,YACAqB,YACkB;EAClB,MAAM,cAAc,KAAK,OAAO,YAAY,oBAAoB,WAAW;AAC3E,MAAI,MAAM,KAAK,MAAM,YAAY,GAAG,EAAG,QAAO;AAC9C,QAAM,KAAK,IACT,aACA,KAAK,OAAO,YAAY,aAAa,WAAW,CACjD;AACD,QAAM,KAAK,KAAK,KAAK,OAAO,YAAY,YAAY,EAAE,WAAW;AACjE,SAAO;CACR;CAED,MAAM,YAAYrB,YAAoBoB,YAAmC;AACvE,SAAO,MAAM,KAAK,IAChB,KAAK,OAAO,YAAY,aAAa,WAAW,KAAK,CACtD,IACC;CACH;CAED,OAAO,aACLpB,YACAsB,UAAyC,CAAE,GACrB;EACtB,MAAM,QAAQ,QAAQ,UAAU;EAChC,MAAM,OAAO,QAAQ,SAAS,OAAO,KAAK,QAAQ,QAAQ,QAAQ;EAClE,MAAM,MAAM,cACV,MAAM,KAAK,QAAQ;GACjB;GACA,KAAK,OAAO,YAAY,YAAY;GACpC,MAAM,UAAU;GAChB,KAAK,UAAU;EAChB,EAAC,CACH;AACD,OAAK,MAAM,MAAM,KAAK;GACpB,MAAM,OAAO,MAAM,KAAK,IAAI,KAAK,OAAO,YAAY,aAAa,GAAG,CAAC;AACrE,OAAI,QAAQ,KAAM;AAClB,OAAI;IACF,MAAM,QAAQ,MAAM,SAAO,WAAW,KAAK,MAAM,KAAK,CAAC;AACvD,QAAI,QAAQ,MAAM,CAAE,OAAM;GAC3B,QAAO;AACN;GACD;EACF;CACF;CAED,eAAetB,YAAqC;AAClD,SAAO,KAAK,MAAM,KAAK,OAAO,YAAY,YAAY,CAAC;CACxD;CAED,MAAM,cACJA,YACAc,IACAS,QACe;AACf,QAAM,KAAK,IACT,KAAK,OAAO,YAAY,WAAW,GAAG,EACtC,KAAK,UAAU,MAAM,OAAO,SAAS,EAAE,QAAQ,UAAW,EAAC,CAAC,CAC7D;CACF;CAED,MAAM,iBACJvB,YACAc,IAC6B;EAC7B,MAAM,SAAS,MAAM,KAAK,cAAc,YAAY,GAAG;AACvD,MAAI,UAAU,KAAM;AACpB,QAAM,KAAK,IAAI,KAAK,OAAO,YAAY,WAAW,GAAG,CAAC;AACtD,SAAO;CACR;CAED,MAAM,cACJd,YACAc,IAC6B;EAC7B,MAAM,OAAO,MAAM,KAAK,IAAI,KAAK,OAAO,YAAY,WAAW,GAAG,CAAC;AACnE,MAAI,QAAQ,KAAM;AAClB,MAAI;AACF,UAAO,MAAM,OAAO,WAAW,KAAK,MAAM,KAAK,CAAC;EACjD,QAAO;AACN;EACD;CACF;CAED,MAAM,YACJd,YACAwB,YACAD,QACe;AACf,QAAM,KAAK,IACT,KAAK,OAAO,YAAY,aAAa,WAAW,KAAK,EACrD,KAAK,UAAU,MAAM,OAAO,SAAS,EAAE,QAAQ,UAAW,EAAC,CAAC,CAC7D;AACD,QAAM,KAAK,KACT,KAAK,IAAI,SAAS,aAAa,WAAW,KAAK,EAC/C,GACA,WACD;CACF;CAED,MAAM,eACJvB,YACAwB,YAC6B;EAC7B,MAAM,SAAS,MAAM,KAAK,YAAY,YAAY,WAAW;AAC7D,QAAM,KAAK,IAAI,KAAK,OAAO,YAAY,aAAa,WAAW,KAAK,CAAC;AACrE,QAAM,KAAK,KACT,KAAK,IAAI,SAAS,aAAa,WAAW,KAAK,EAC/C,WACD;AACD,SAAO;CACR;CAED,MAAM,YACJxB,YACAwB,YAC6B;EAC7B,MAAM,OAAO,MAAM,KAAK,IACtB,KAAK,OAAO,YAAY,aAAa,WAAW,KAAK,CACtD;AACD,MAAI,QAAQ,KAAM;AAClB,MAAI;AACF,UAAO,MAAM,OAAO,WAAW,KAAK,MAAM,KAAK,CAAC;EACjD,QAAO;AACN;EACD;CACF;CAED,OAAO,iBAAiBA,YAAwC;AAC9D,SAAO,MAAM,KAAK,OAAO,KAAK,IAAI,SAAS,aAAa,WAAW,KAAK,CAAC;CAC1E;CAED,MAAM,sBACJxB,YACAc,IACAW,eACe;EACf,MAAM,oBAAoB,cAAc;AACxC,MAAI,qBAAqB,KACvB,OAAM,IAAI,UACR;EAGJ,MAAM,WAAW,KAAK,OACpB,YACA,0CACA,kBAAkB,KACnB;AACD,QAAM,KAAK,cACT,KAAK,0BAA0B,SAAS,EACxC,YAAY;GACV,MAAM,WAAW,MAAM,KAAK,IAAI,SAAS;AACzC,OAAI,YAAY,MAAM;AACpB,QACE,MAAM,KAAK,sBAAsB,YAAY,SAAiB,IAC5D,KAEF;AAEF,UAAM,KAAK,IAAI,SAAS;GACzB;AACD,SAAM,KAAK,IACT,KAAK,OAAO,YAAY,uBAAuB,GAAG,EAClD,KAAK,UAAU,MAAM,cAAc,SAAS,EAAE,QAAQ,UAAW,EAAC,CAAC,CACpE;AACD,SAAM,KAAK,IAAI,UAAU,GAAG;EAC7B,EACF;CACF;CAED,MAAM,sBACJzB,YACAc,IACyC;EACzC,MAAM,OAAO,MAAM,KAAK,IACtB,KAAK,OAAO,YAAY,uBAAuB,GAAG,CACnD;AACD,MAAI,QAAQ,KAAM;AAClB,MAAI;AACF,UAAO,MAAM,mBAAmB,WAAW,KAAK,MAAM,KAAK,CAAC;EAC7D,QAAO;AACN;EACD;CACF;CAED,MAAM,uBACJd,YACA0B,mBACyC;EACzC,MAAM,WAAW,KAAK,OACpB,YACA,0CACA,kBAAkB,KACnB;AACD,SAAO,MAAM,KAAK,cAChB,KAAK,0BAA0B,SAAS,EACxC,YAAY;GACV,MAAM,KAAK,MAAM,KAAK,IAAI,SAAS;AACnC,OAAI,MAAM,KAAM;GAChB,MAAM,gBAAgB,MAAM,KAAK,sBAC/B,YACA,GACD;AACD,OAAI,iBAAiB,QAAQ,MAAM,KAAK,IAAI,SAAS,KAAK,GACxD,OAAM,KAAK,IAAI,SAAS;AAE1B,UAAO;EACR,EACF;CACF;CAED,MAAM,yBACJ1B,YACAc,IACyC;EACzC,MAAM,MAAM,KAAK,OAAO,YAAY,uBAAuB,GAAG;EAC9D,MAAM,OAAO,MAAM,KAAK,IAAI,IAAI;AAChC,QAAM,KAAK,IAAI,IAAI;AACnB,MAAI,QAAQ,KAAM;EAClB,IAAIa;AACJ,MAAI;AACF,YAAS,KAAK,MAAM,KAAK;EAC1B,QAAO;AACN;EACD;EACD,MAAM,oBAAoB,0CAA0C,OAAO;AAC3E,MAAI,qBAAqB,MAAM;GAC7B,MAAM,WAAW,KAAK,OACpB,YACA,0CACA,kBAAkB,KACnB;AACD,SAAM,KAAK,cACT,KAAK,0BAA0B,SAAS,EACxC,YAAY;AACV,QAAI,MAAM,KAAK,IAAI,SAAS,KAAK,GAAI,OAAM,KAAK,IAAI,SAAS;GAC9D,EACF;EACF;AACD,MAAI;AACF,UAAO,MAAM,mBAAmB,WAAW,OAAO;EACnD,QAAO;AACN;EACD;CACF;CAED,MAAM,+BACJ3B,YACA4B,eACAC,WACAC,aACe;EACf,MAAMC,QAAyC,eAAe,OAC1D,EAAE,UAAW,IACb;GAAE;GAAW,aAAa,YAAY;EAAM;AAChD,QAAM,KAAK,IACT,KAAK,OAAO,YAAY,0BAA0B,cAAc,KAAK,EACrE,KAAK,UAAU,MAAM,CACtB;AACD,QAAM,KAAK,KACT,KAAK,IAAI,SAAS,0BAA0B,cAAc,KAAK,EAC/D,GACA,WACD;CACF;CAED,MAAM,gCACJ/B,YACA4B,eAC2B;EAC3B,MAAM,OAAO,MAAM,KAAK,IACtB,KAAK,OAAO,YAAY,0BAA0B,cAAc,KAAK,CACtE;AACD,MAAI,QAAQ,KAAM;AAClB,MAAI;GACF,MAAM,QAAQ,KAAK,MAAM,KAAK;AAC9B,UAAO,MAAM;EACd,QAAO;AACN;EACD;CACF;CAED,OAAO,2CACLA,eACuB;AACvB,SAAO,MAAM,KAAK,OAChB,KAAK,IAAI,SAAS,0BAA0B,cAAc,KAAK,CAChE;CACF;CAED,MAAM,2CACJ5B,YACA4B,eAC0B;EAC1B,MAAM,OAAO,MAAM,KAAK,IACtB,KAAK,OAAO,YAAY,0BAA0B,cAAc,KAAK,CACtE;AACD,MAAI,QAAQ,KAAM;EAClB,IAAIG;AACJ,MAAI;AACF,WAAQ,KAAK,MAAM,KAAK;EACzB,QAAO;AACN;EACD;AACD,MAAI,MAAM,eAAe,KAAM;AAC/B,MAAI;AACF,UAAO,IAAI,IAAI,MAAM;EACtB,QAAO;AACN;EACD;CACF;CAED,MAAM,kCACJ/B,YACA4B,eACe;AACf,QAAM,KAAK,IACT,KAAK,OAAO,YAAY,0BAA0B,cAAc,KAAK,CACtE;AACD,QAAM,KAAK,KACT,KAAK,IAAI,SAAS,0BAA0B,cAAc,KAAK,EAC/D,WACD;CACF;CAED,MAAM,KACJ5B,YACA6B,WACAG,SACAC,QACe;AACf,QAAM,KAAK,KACT,KAAK,OAAO,YAAY,SAAS,WAAW,SAAS,EACrD,QAAQ,KACT;AACD,QAAM,KAAK,KACT,KAAK,OAAO,YAAY,SAAS,WAAW,WAAW,OAAO,EAC9D,QAAQ,KACT;AACD,QAAM,KAAK,KACT,KAAK,OAAO,YAAY,SAAS,WAAW,UAAU,EACtD,GACA,OACD;CACF;CAED,YAAYjC,YAAoB6B,WAAkC;AAChE,SAAO,KAAK,MAAM,KAAK,OAAO,YAAY,SAAS,WAAW,SAAS,CAAC;CACzE;CAED,MAAM,WACJ7B,YACA6B,WAC2C;EAC3C,MAAMK,SAAiC,CAAE;EACzC,MAAM,UAAU,MAAM,KAAK,OACzB,KAAK,OAAO,YAAY,SAAS,WAAW,UAAU,CACvD;AACD,OAAK,MAAM,UAAU,QACnB,QAAO,UAAU,MAAM,KAAK,MAC1B,KAAK,OAAO,YAAY,SAAS,WAAW,WAAW,OAAO,CAC/D;AAEH,SAAO;CACR;CAED,cAAclC,YAA2C;AACvD,SAAO,IAAI,sBAAsB,MAAM;CACxC;CAED,AAAQ,0BAA0BmC,UAA0B;AAC1D,UAAQ,EAAE,SAAS;CACpB;AACF;AAED,SAAS,SAASC,OAAwB;AACxC,YAAW,UAAU,SAAU,QAAO;AACtC,YAAW,UAAU,SAAU,QAAO,OAAO,MAAM;AACnD,YAAW,UAAU,SAAU,QAAO,OAAO,SAAS,OAAO,GAAG;AAChE,QAAO;AACR;AAED,SAAS,cAAcA,OAA0B;AAC/C,MAAK,MAAM,QAAQ,MAAM,CAAE,QAAO,CAAE;AACpC,QAAO,MAAM,OAAO,CAAC,gBAAgC,SAAS,SAAS;AACxE;AAED,SAAS,MAAMC,IAA2B;AACxC,QAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,GAAG;AACxD;AAED,SAAS,gBAAgBvB,IAAUC,UAAqC;AACtE,KAAI,SAAS,aAAa,KAAM,QAAO,SAAS,UAAU;AAC1D,QAAO,iBAAiB,GAAG;AAC5B;AAED,SAAS,iBAAiBuB,MAAsB;CAC9C,MAAM,QAAQ,cAAc,KAAK,KAAK;AACtC,KAAI,SAAS,KAAM,QAAO;AAC1B,QAAO,OAAO,UAAU,EAAE,MAAM,GAAG,EAAE,MAAM,GAAG,GAAG,GAAG;AACrD;AAED,SAAS,0CACPC,MACiB;AACjB,YAAW,SAAS,YAAY,QAAQ,KAAM;AAC9C,OAAM,uBAAuB,MAAO;CACpC,MAAM,oBAAoB,KAAK;AAC/B,YAAW,sBAAsB,SAAU;AAC3C,KAAI;AACF,SAAO,IAAI,IAAI;CAChB,QAAO;AACN;CACD;AACF"}