@fedify/botkit 0.5.0-dev.210 → 0.5.0-dev.225

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.
Files changed (85) hide show
  1. package/dist/bot-group.test.d.ts +2 -0
  2. package/dist/bot-group.test.js +220 -0
  3. package/dist/bot-group.test.js.map +1 -0
  4. package/dist/bot-impl.d.ts +132 -13
  5. package/dist/bot-impl.d.ts.map +1 -1
  6. package/dist/bot-impl.js +400 -178
  7. package/dist/bot-impl.js.map +1 -1
  8. package/dist/bot-impl.test.js +214 -76
  9. package/dist/bot-impl.test.js.map +1 -1
  10. package/dist/bot.d.ts +94 -48
  11. package/dist/bot.d.ts.map +1 -1
  12. package/dist/bot.js +2 -104
  13. package/dist/bot.js.map +1 -1
  14. package/dist/bot.test.js +59 -0
  15. package/dist/bot.test.js.map +1 -1
  16. package/dist/components/FollowButton.d.ts +5 -3
  17. package/dist/components/FollowButton.d.ts.map +1 -1
  18. package/dist/components/FollowButton.js +2 -2
  19. package/dist/components/FollowButton.js.map +1 -1
  20. package/dist/components/Follower.d.ts +2 -2
  21. package/dist/components/Layout.js +1 -1
  22. package/dist/components/Layout.js.map +1 -1
  23. package/dist/components/Message.d.ts +2 -2
  24. package/dist/deno.js +2 -1
  25. package/dist/deno.js.map +1 -1
  26. package/dist/follow-impl.test.js +3 -3
  27. package/dist/follow-impl.test.js.map +1 -1
  28. package/dist/instance-impl.d.ts +158 -0
  29. package/dist/instance-impl.d.ts.map +1 -0
  30. package/dist/instance-impl.js +603 -0
  31. package/dist/instance-impl.js.map +1 -0
  32. package/dist/instance-impl.test.d.ts +2 -0
  33. package/dist/instance-impl.test.js +103 -0
  34. package/dist/instance-impl.test.js.map +1 -0
  35. package/dist/instance-multi.test.d.ts +2 -0
  36. package/dist/instance-multi.test.js +151 -0
  37. package/dist/instance-multi.test.js.map +1 -0
  38. package/dist/instance-routing.test.d.ts +2 -0
  39. package/dist/instance-routing.test.js +367 -0
  40. package/dist/instance-routing.test.js.map +1 -0
  41. package/dist/instance.d.ts +318 -0
  42. package/dist/instance.d.ts.map +1 -0
  43. package/dist/instance.js +51 -0
  44. package/dist/instance.js.map +1 -0
  45. package/dist/message-impl.d.ts.map +1 -1
  46. package/dist/message-impl.js +17 -10
  47. package/dist/message-impl.js.map +1 -1
  48. package/dist/message-impl.test.js +43 -9
  49. package/dist/message-impl.test.js.map +1 -1
  50. package/dist/mod.d.ts +5 -3
  51. package/dist/mod.js +4 -2
  52. package/dist/pages.d.ts +12 -3
  53. package/dist/pages.d.ts.map +1 -1
  54. package/dist/pages.js +112 -41
  55. package/dist/pages.js.map +1 -1
  56. package/dist/pages.test.d.ts +2 -0
  57. package/dist/pages.test.js +170 -0
  58. package/dist/pages.test.js.map +1 -0
  59. package/dist/repository.d.ts +385 -138
  60. package/dist/repository.d.ts.map +1 -1
  61. package/dist/repository.js +595 -223
  62. package/dist/repository.js.map +1 -1
  63. package/dist/repository.test.js +564 -136
  64. package/dist/repository.test.js.map +1 -1
  65. package/dist/session-impl.d.ts.map +1 -1
  66. package/dist/session-impl.js +13 -4
  67. package/dist/session-impl.js.map +1 -1
  68. package/dist/session-impl.test.d.ts.map +1 -1
  69. package/dist/session-impl.test.js +9 -9
  70. package/dist/session-impl.test.js.map +1 -1
  71. package/dist/session.d.ts +8 -3
  72. package/dist/session.d.ts.map +1 -1
  73. package/dist/text.d.ts.map +1 -1
  74. package/dist/text.js +27 -10
  75. package/dist/text.js.map +1 -1
  76. package/dist/text.test.js +37 -2
  77. package/dist/text.test.js.map +1 -1
  78. package/dist/uri.d.ts +46 -0
  79. package/dist/uri.d.ts.map +1 -0
  80. package/dist/uri.js +64 -0
  81. package/dist/uri.js.map +1 -0
  82. package/dist/uri.test.d.ts +2 -0
  83. package/dist/uri.test.js +93 -0
  84. package/dist/uri.test.js.map +1 -0
  85. package/package.json +5 -1
@@ -0,0 +1,603 @@
1
+
2
+ import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
3
+ Date.prototype.toTemporalInstant = toTemporalInstant;
4
+
5
+ import deno_default from "./deno.js";
6
+ import { parseLocalUri, rewriteLegacyObjectPath } from "./uri.js";
7
+ import { isMessageObject, isQuoteLink } from "./message-impl.js";
8
+ import { KvRepository } from "./repository.js";
9
+ import { BotGroupImpl, BotImpl, GroupBotImpl, wrapBotImpl } from "./bot-impl.js";
10
+ import { app, multiApp } from "./pages.js";
11
+ import { Accept, Announce, Application, Article, ChatMessage, Create, Delete, Emoji, Endpoints, Follow, Image, Like, Link, Mention, Note, Question, Reject, Undo } from "@fedify/vocab";
12
+ import { createFederation, generateCryptoKeyPair } from "@fedify/fedify";
13
+ import { getLogger } from "@logtape/logtape";
14
+ import mimeDb from "mime-db";
15
+ import fs from "node:fs/promises";
16
+ import { getXForwardedRequest } from "x-forwarded-fetch";
17
+
18
+ //#region src/instance-impl.ts
19
+ /**
20
+ * The default identifier of the instance actor: an internal `Application`
21
+ * actor that an {@link Instance} uses for signing shared-inbox related
22
+ * requests on behalf of the whole instance. It can be overridden through
23
+ * the {@link CreateInstanceOptions.instanceActorIdentifier} option; either
24
+ * way, bots cannot take the effective identifier.
25
+ * @since 0.5.0
26
+ */
27
+ const DEFAULT_INSTANCE_ACTOR_IDENTIFIER = "__botkit_instance__";
28
+ /**
29
+ * The internal implementation of an {@link Instance}. It owns the single
30
+ * Fedify {@link Federation} shared by every bot hosted on the instance, and
31
+ * routes federation callbacks to the right bot by its identifier.
32
+ * @internal
33
+ */
34
+ var InstanceImpl = class {
35
+ kv;
36
+ queue;
37
+ /**
38
+ * The root repository shared by every bot hosted on the instance.
39
+ */
40
+ repository;
41
+ software;
42
+ behindProxy;
43
+ pages;
44
+ collectionWindow;
45
+ federation;
46
+ customEmojis = {};
47
+ /**
48
+ * The identifier of the bot actor that owns local objects whose URIs are
49
+ * in the legacy (pre-0.5) format, which did not carry the identifier.
50
+ */
51
+ legacyObjectUrisIdentifier;
52
+ /**
53
+ * Whether the instance was created through the single-bot
54
+ * `createBot()` compatibility path.
55
+ */
56
+ compatMode;
57
+ /**
58
+ * The reserved identifier of the instance actor.
59
+ */
60
+ instanceActorIdentifier;
61
+ #bots = /* @__PURE__ */ new Map();
62
+ #groups = [];
63
+ /**
64
+ * Memoizes dynamic bot resolution per Fedify context, which is stable
65
+ * for the duration of one HTTP dispatch or one queue-delivered inbox
66
+ * activity, so a dispatcher is invoked at most once per identifier per
67
+ * request. Entries die with their context.
68
+ */
69
+ #resolutionCache = /* @__PURE__ */ new WeakMap();
70
+ constructor(options) {
71
+ this.kv = options.kv;
72
+ this.queue = options.queue;
73
+ this.repository = options.repository ?? new KvRepository(options.kv);
74
+ this.software = options.software;
75
+ this.behindProxy = options.behindProxy ?? false;
76
+ this.pages = {
77
+ color: "green",
78
+ css: "",
79
+ ...options.pages ?? {}
80
+ };
81
+ this.collectionWindow = options.collectionWindow ?? 50;
82
+ this.legacyObjectUrisIdentifier = options.legacyObjectUris?.identifier;
83
+ this.compatMode = options.compatMode ?? false;
84
+ this.instanceActorIdentifier = options.instanceActorIdentifier ?? DEFAULT_INSTANCE_ACTOR_IDENTIFIER;
85
+ if (this.instanceActorIdentifier === "") throw new TypeError("The instance actor identifier cannot be empty.");
86
+ this.federation = createFederation({
87
+ kv: options.kv,
88
+ queue: options.queue,
89
+ userAgent: { software: `BotKit/${deno_default.version}` }
90
+ });
91
+ this.#initialize();
92
+ }
93
+ #initialize() {
94
+ this.federation.setActorDispatcher("/ap/actor/{identifier}", async (ctx, identifier) => {
95
+ if (!this.compatMode && identifier === this.instanceActorIdentifier) return await this.#dispatchInstanceActor(ctx);
96
+ const bot = await this.resolveBot(ctx, identifier);
97
+ return await bot?.dispatchActor(ctx, identifier) ?? null;
98
+ }).mapHandle((ctx, username) => this.mapHandle(ctx, username)).setKeyPairsDispatcher(async (ctx, identifier) => {
99
+ if (!this.compatMode && identifier === this.instanceActorIdentifier) return await this.#dispatchInstanceActorKeyPairs();
100
+ const bot = await this.resolveBot(ctx, identifier);
101
+ return await bot?.dispatchActorKeyPairs(ctx, identifier) ?? [];
102
+ });
103
+ this.federation.setFollowersDispatcher("/ap/actor/{identifier}/followers", async (ctx, identifier, cursor) => {
104
+ const bot = await this.resolveBot(ctx, identifier);
105
+ return await bot?.dispatchFollowers(ctx, identifier, cursor) ?? null;
106
+ }).setFirstCursor(async (ctx, identifier) => {
107
+ const bot = await this.resolveBot(ctx, identifier);
108
+ return bot?.getFollowersFirstCursor(ctx, identifier) ?? null;
109
+ }).setCounter(async (ctx, identifier) => {
110
+ const bot = await this.resolveBot(ctx, identifier);
111
+ return await bot?.countFollowers(ctx, identifier) ?? null;
112
+ });
113
+ this.federation.setOutboxDispatcher("/ap/actor/{identifier}/outbox", async (ctx, identifier, cursor) => {
114
+ const bot = await this.resolveBot(ctx, identifier);
115
+ return await bot?.dispatchOutbox(ctx, identifier, cursor) ?? null;
116
+ }).setFirstCursor(async (ctx, identifier) => {
117
+ const bot = await this.resolveBot(ctx, identifier);
118
+ return bot?.getOutboxFirstCursor(ctx, identifier) ?? null;
119
+ }).setCounter(async (ctx, identifier) => {
120
+ const bot = await this.resolveBot(ctx, identifier);
121
+ return await bot?.countOutbox(ctx, identifier) ?? null;
122
+ });
123
+ this.federation.setObjectDispatcher(Follow, "/ap/actor/{identifier}/follow/{id}", async (ctx, values) => {
124
+ const bot = await this.resolveBot(ctx, values.identifier);
125
+ return await bot?.dispatchFollow(ctx, values) ?? null;
126
+ }).authorize(async (ctx, values) => {
127
+ const bot = await this.resolveBot(ctx, values.identifier);
128
+ return await bot?.authorizeFollow(ctx, values) ?? false;
129
+ });
130
+ this.federation.setObjectDispatcher(Create, "/ap/actor/{identifier}/create/{id}", async (ctx, values) => {
131
+ const bot = await this.resolveBot(ctx, values.identifier);
132
+ return await bot?.dispatchCreate(ctx, values) ?? null;
133
+ });
134
+ this.federation.setObjectDispatcher(Article, "/ap/actor/{identifier}/article/{id}", async (ctx, values) => {
135
+ const bot = await this.resolveBot(ctx, values.identifier);
136
+ return await bot?.dispatchMessage(Article, ctx, values.id) ?? null;
137
+ });
138
+ this.federation.setObjectDispatcher(ChatMessage, "/ap/actor/{identifier}/chat-message/{id}", async (ctx, values) => {
139
+ const bot = await this.resolveBot(ctx, values.identifier);
140
+ return await bot?.dispatchMessage(ChatMessage, ctx, values.id) ?? null;
141
+ });
142
+ this.federation.setObjectDispatcher(Note, "/ap/actor/{identifier}/note/{id}", async (ctx, values) => {
143
+ const bot = await this.resolveBot(ctx, values.identifier);
144
+ return await bot?.dispatchMessage(Note, ctx, values.id) ?? null;
145
+ });
146
+ this.federation.setObjectDispatcher(Question, "/ap/actor/{identifier}/question/{id}", async (ctx, values) => {
147
+ const bot = await this.resolveBot(ctx, values.identifier);
148
+ return await bot?.dispatchMessage(Question, ctx, values.id) ?? null;
149
+ });
150
+ this.federation.setObjectDispatcher(Announce, "/ap/actor/{identifier}/announce/{id}", async (ctx, values) => {
151
+ const bot = await this.resolveBot(ctx, values.identifier);
152
+ return await bot?.dispatchAnnounce(ctx, values) ?? null;
153
+ });
154
+ this.federation.setObjectDispatcher(Emoji, "/ap/emoji/{name}", (ctx, values) => this.dispatchEmoji(ctx, values));
155
+ this.federation.setInboxListeners("/ap/actor/{identifier}/inbox", "/ap/inbox").onUnverifiedActivity((ctx, activity, reason) => this.onUnverifiedActivity(ctx, activity, reason)).on(Follow, (ctx, follow) => this.onFollowed(ctx, follow)).on(Undo, (ctx, undo) => this.onUndone(ctx, undo)).on(Accept, (ctx, accept) => this.onFollowAccepted(ctx, accept)).on(Reject, (ctx, reject) => this.onFollowRejected(ctx, reject)).on(Create, (ctx, create) => this.onCreated(ctx, create)).on(Announce, (ctx, announce) => this.onAnnounced(ctx, announce)).on(Like, (ctx, like) => this.onLiked(ctx, like)).setSharedKeyDispatcher((ctx) => this.dispatchSharedKey(ctx));
156
+ if (this.software != null) this.federation.setNodeInfoDispatcher("/nodeinfo/2.1", (ctx) => this.dispatchNodeInfo(ctx));
157
+ }
158
+ /**
159
+ * Registers a bot on the instance. Invoked by the {@link BotImpl}
160
+ * constructor.
161
+ * @param bot The bot to register.
162
+ * @throws {TypeError} If a bot with the same identifier or username
163
+ * already exists on the instance.
164
+ */
165
+ addBot(bot) {
166
+ if (!this.compatMode && bot.identifier === this.instanceActorIdentifier) throw new TypeError(`The identifier is reserved for the instance actor: ${bot.identifier}`);
167
+ if (this.#bots.has(bot.identifier)) throw new TypeError(`A bot with the identifier already exists: ${bot.identifier}`);
168
+ const username = bot.username.toLowerCase();
169
+ for (const existing of this.#bots.values()) if (existing.username.toLowerCase() === username) throw new TypeError(`A bot with the username already exists: ${bot.username}`);
170
+ this.#bots.set(bot.identifier, bot);
171
+ }
172
+ /**
173
+ * Resolves a bot hosted on the instance by its identifier.
174
+ * @param identifier The identifier of the bot to resolve.
175
+ * @returns The resolved bot, or `undefined` if no bot has the identifier.
176
+ */
177
+ getBot(identifier) {
178
+ return this.#bots.get(identifier);
179
+ }
180
+ /**
181
+ * Resolves a bot by its identifier: bots registered statically win, then
182
+ * the dynamic bot groups are probed in their registration order.
183
+ * Dynamic resolutions are memoized per context.
184
+ * @param ctx The Fedify context of the resolution.
185
+ * @param identifier The identifier of the bot to resolve.
186
+ * @returns The resolved bot, or `null` if no bot has the identifier.
187
+ */
188
+ async resolveBot(ctx, identifier) {
189
+ if (!this.compatMode && identifier === this.instanceActorIdentifier) return null;
190
+ const staticBot = this.#bots.get(identifier);
191
+ if (staticBot != null) return staticBot;
192
+ if (this.#groups.length < 1) return null;
193
+ let cache = this.#resolutionCache.get(ctx);
194
+ if (cache == null) {
195
+ cache = /* @__PURE__ */ new Map();
196
+ this.#resolutionCache.set(ctx, cache);
197
+ }
198
+ const cached = cache.get(identifier);
199
+ if (cached !== void 0) return cached;
200
+ let resolved = null;
201
+ for (const group of this.#groups) {
202
+ const profile = await group.dispatcher(ctx, identifier);
203
+ if (profile != null) {
204
+ resolved = new GroupBotImpl(group, identifier, profile);
205
+ break;
206
+ }
207
+ }
208
+ cache.set(identifier, resolved);
209
+ return resolved;
210
+ }
211
+ /**
212
+ * Every bot hosted on the instance.
213
+ */
214
+ get bots() {
215
+ return this.#bots.values();
216
+ }
217
+ /**
218
+ * The number of bots hosted on the instance.
219
+ */
220
+ get botCount() {
221
+ return this.#bots.size;
222
+ }
223
+ #firstBot() {
224
+ return this.#bots.values().next().value;
225
+ }
226
+ createBot(identifierOrDispatcher, profileOrOptions) {
227
+ if (typeof identifierOrDispatcher !== "string") {
228
+ const group = new BotGroupImpl(this, identifierOrDispatcher, profileOrOptions);
229
+ this.#groups.push(group);
230
+ return group;
231
+ }
232
+ const profile = profileOrOptions;
233
+ if (profile == null || profile.username == null) throw new TypeError("The bot profile with a username is required.");
234
+ const bot = new BotImpl({
235
+ instance: this,
236
+ identifier: identifierOrDispatcher,
237
+ kv: this.kv,
238
+ class: profile.class,
239
+ username: profile.username,
240
+ name: profile.name,
241
+ summary: profile.summary,
242
+ icon: profile.icon,
243
+ image: profile.image,
244
+ properties: profile.properties,
245
+ followerPolicy: profile.followerPolicy
246
+ });
247
+ return wrapBotImpl(bot);
248
+ }
249
+ /**
250
+ * Resolves a bot hosted on the instance by its username, including
251
+ * dynamically resolved bots.
252
+ * @param ctx The Fedify context of the resolution.
253
+ * @param username The username of the bot to resolve.
254
+ * @returns The resolved bot, or `null` if no bot has the username.
255
+ */
256
+ async resolveBotByUsername(ctx, username) {
257
+ const identifier = await this.mapHandle(ctx, username);
258
+ if (identifier == null) return null;
259
+ return await this.resolveBot(ctx, identifier);
260
+ }
261
+ async mapHandle(ctx, username) {
262
+ const normalized = username.toLowerCase();
263
+ for (const bot$1 of this.#bots.values()) if (bot$1.username.toLowerCase() === normalized) return bot$1.identifier;
264
+ for (const group of this.#groups) {
265
+ if (group.mapUsername == null) continue;
266
+ const identifier = await group.mapUsername(ctx, username);
267
+ if (identifier == null) continue;
268
+ const bot$1 = await this.resolveBot(ctx, identifier);
269
+ if (bot$1 instanceof GroupBotImpl && bot$1.group === group) return identifier;
270
+ }
271
+ const bot = await this.resolveBot(ctx, username);
272
+ if (bot instanceof GroupBotImpl && bot.group.mapUsername == null) return username;
273
+ return null;
274
+ }
275
+ onUnverifiedActivity(_ctx, activity, reason) {
276
+ if (activity instanceof Delete && reason.type === "keyFetchError" && typeof reason.result === "object" && reason.result != null && "status" in reason.result && reason.result.status === 410) return new Response(null, { status: 202 });
277
+ }
278
+ /**
279
+ * Resolves which bots an incoming shared-inbox activity is relevant to.
280
+ * On a compatible (single-bot) instance every hosted bot is returned,
281
+ * preserving the pre-0.5 behavior. A personal-inbox delivery targets
282
+ * its recipient only. Otherwise the given resolver computes the
283
+ * relevant bot identifiers.
284
+ */
285
+ async #resolveTargets(ctx, resolve) {
286
+ if (this.compatMode) return [...this.#bots.values()];
287
+ if (ctx.recipient != null) {
288
+ const bot = await this.resolveBot(ctx, ctx.recipient);
289
+ return bot == null ? [] : [bot];
290
+ }
291
+ const identifiers = new Set(await resolve());
292
+ const bots = [];
293
+ for (const identifier of identifiers) {
294
+ const bot = await this.resolveBot(ctx, identifier);
295
+ if (bot != null) bots.push(bot);
296
+ }
297
+ return bots;
298
+ }
299
+ /**
300
+ * Attributes a local object URI to its owning bot identifier, or logs and
301
+ * yields nothing when the URI is not a local object. Activities on
302
+ * objects the instance does not own cannot be attributed to any bot on
303
+ * a multi-bot instance, so they are dropped.
304
+ */
305
+ #localObjectTarget(ctx, uri) {
306
+ const parsed = parseLocalUri(ctx, uri, this.legacyObjectUrisIdentifier);
307
+ if (parsed?.type === "object" && typeof parsed.values.identifier === "string") return [parsed.values.identifier];
308
+ const logger = getLogger([
309
+ "botkit",
310
+ "instance",
311
+ "inbox"
312
+ ]);
313
+ logger.debug("The object {uri} is not owned by any bot on this instance; the activity is not routed.", { uri: uri?.href });
314
+ return [];
315
+ }
316
+ async onFollowed(ctx, follow) {
317
+ const bots = await this.#resolveTargets(ctx, () => {
318
+ const parsed = ctx.parseUri(follow.objectId);
319
+ return parsed?.type === "actor" ? [parsed.identifier] : [];
320
+ });
321
+ for (const bot of bots) await bot.onFollowed(ctx, follow);
322
+ }
323
+ async onUndone(ctx, undo) {
324
+ const object = await undo.getObject(ctx);
325
+ if (object instanceof Follow) {
326
+ const bots = await this.#resolveTargets(ctx, () => {
327
+ const parsed = ctx.parseUri(object.objectId);
328
+ return parsed?.type === "actor" ? [parsed.identifier] : [];
329
+ });
330
+ for (const bot of bots) await bot.onUnfollowed(ctx, undo);
331
+ } else if (object instanceof Like) {
332
+ const bots = await this.#resolveTargets(ctx, () => this.#localObjectTarget(ctx, object.objectId));
333
+ for (const bot of bots) await bot.onUnliked(ctx, undo);
334
+ } else {
335
+ const logger = getLogger([
336
+ "botkit",
337
+ "bot",
338
+ "inbox"
339
+ ]);
340
+ logger.warn("The Undo object {undoId} is not about Follow or Like: {object}.", {
341
+ undoId: undo.id?.href,
342
+ object
343
+ });
344
+ }
345
+ }
346
+ async onFollowAccepted(ctx, accept) {
347
+ const bots = await this.#resolveTargets(ctx, () => this.#localObjectTarget(ctx, accept.objectId));
348
+ for (const bot of bots) await bot.onFollowAccepted(ctx, accept);
349
+ }
350
+ async onFollowRejected(ctx, reject) {
351
+ const bots = await this.#resolveTargets(ctx, () => this.#localObjectTarget(ctx, reject.objectId));
352
+ for (const bot of bots) await bot.onFollowRejected(ctx, reject);
353
+ }
354
+ async onLiked(ctx, like) {
355
+ const bots = await this.#resolveTargets(ctx, () => this.#localObjectTarget(ctx, like.objectId));
356
+ for (const bot of bots) await bot.onLiked(ctx, like);
357
+ }
358
+ async onCreated(ctx, create) {
359
+ const bots = await this.#resolveTargets(ctx, async () => {
360
+ const targets = /* @__PURE__ */ new Set();
361
+ if (create.actorId != null) for await (const identifier of this.repository.findFollowedBots(create.actorId)) targets.add(identifier);
362
+ const addAddressee = (uri) => {
363
+ const parsed = ctx.parseUri(uri);
364
+ if (parsed?.type === "actor" || parsed?.type === "followers") {
365
+ if (parsed.identifier != null) targets.add(parsed.identifier);
366
+ }
367
+ };
368
+ for (const uri of [...create.toIds, ...create.ccIds]) addAddressee(uri);
369
+ const addLocalObject = (uri) => {
370
+ const parsed = parseLocalUri(ctx, uri, this.legacyObjectUrisIdentifier);
371
+ if (parsed?.type === "object" && typeof parsed.values.identifier === "string") targets.add(parsed.values.identifier);
372
+ };
373
+ const object = await create.getObject(ctx);
374
+ if (isMessageObject(object)) {
375
+ for (const uri of [...object.toIds, ...object.ccIds]) addAddressee(uri);
376
+ for await (const tag of object.getTags(ctx)) if (tag instanceof Mention && tag.href != null) {
377
+ const parsed = ctx.parseUri(tag.href);
378
+ if (parsed?.type === "actor") targets.add(parsed.identifier);
379
+ } else if (tag instanceof Link && isQuoteLink(tag)) addLocalObject(tag.href);
380
+ addLocalObject(object.quoteUrl);
381
+ addLocalObject(object.replyTargetId);
382
+ }
383
+ return targets;
384
+ });
385
+ for (const bot of bots) await bot.onCreated(ctx, create);
386
+ }
387
+ async onAnnounced(ctx, announce) {
388
+ const bots = await this.#resolveTargets(ctx, async () => {
389
+ const targets = /* @__PURE__ */ new Set();
390
+ if (announce.actorId != null) for await (const identifier of this.repository.findFollowedBots(announce.actorId)) targets.add(identifier);
391
+ for (const uri of [...announce.toIds, ...announce.ccIds]) {
392
+ const parsed = ctx.parseUri(uri);
393
+ if (parsed?.type === "actor" || parsed?.type === "followers") {
394
+ if (parsed.identifier != null) targets.add(parsed.identifier);
395
+ }
396
+ }
397
+ const parsedObject = parseLocalUri(ctx, announce.objectId, this.legacyObjectUrisIdentifier);
398
+ if (parsedObject?.type === "object" && typeof parsedObject.values.identifier === "string") targets.add(parsedObject.values.identifier);
399
+ return targets;
400
+ });
401
+ for (const bot of bots) await bot.onAnnounced(ctx, announce);
402
+ }
403
+ dispatchSharedKey(_ctx) {
404
+ if (this.compatMode) {
405
+ const bot = this.#firstBot();
406
+ if (bot == null) throw new TypeError("The instance has no bots; the shared inbox key cannot be dispatched.");
407
+ return { identifier: bot.identifier };
408
+ }
409
+ return { identifier: this.instanceActorIdentifier };
410
+ }
411
+ async #dispatchInstanceActor(ctx) {
412
+ const keyPairs = await ctx.getActorKeyPairs(this.instanceActorIdentifier);
413
+ return new Application({
414
+ id: ctx.getActorUri(this.instanceActorIdentifier),
415
+ preferredUsername: this.instanceActorIdentifier,
416
+ name: "Instance actor",
417
+ summary: "An internal actor the instance uses for signing requests on behalf of the whole instance.",
418
+ inbox: ctx.getInboxUri(this.instanceActorIdentifier),
419
+ endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }),
420
+ publicKey: keyPairs[0].cryptographicKey,
421
+ assertionMethods: keyPairs.map((pair) => pair.multikey),
422
+ discoverable: false
423
+ });
424
+ }
425
+ #instanceActorKeyPairs;
426
+ #dispatchInstanceActorKeyPairs() {
427
+ if (this.#instanceActorKeyPairs != null) return this.#instanceActorKeyPairs;
428
+ const promise = (async () => {
429
+ let keyPairs = await this.repository.getKeyPairs(this.instanceActorIdentifier);
430
+ if (keyPairs == null) {
431
+ const rsa = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5");
432
+ const ed25519 = await generateCryptoKeyPair("Ed25519");
433
+ keyPairs = [rsa, ed25519];
434
+ await this.repository.setKeyPairs(this.instanceActorIdentifier, keyPairs);
435
+ }
436
+ return keyPairs;
437
+ })();
438
+ this.#instanceActorKeyPairs = promise;
439
+ promise.catch(() => {
440
+ if (this.#instanceActorKeyPairs === promise) this.#instanceActorKeyPairs = void 0;
441
+ });
442
+ return promise;
443
+ }
444
+ dispatchNodeInfo(_ctx) {
445
+ return {
446
+ software: this.software,
447
+ protocols: ["activitypub"],
448
+ services: { outbound: ["atom1.0"] },
449
+ usage: {
450
+ users: {
451
+ total: this.botCount,
452
+ activeMonth: this.botCount,
453
+ activeHalfyear: this.botCount
454
+ },
455
+ localPosts: 0,
456
+ localComments: 0
457
+ }
458
+ };
459
+ }
460
+ dispatchEmoji(ctx, values) {
461
+ const customEmoji = this.customEmojis[values.name];
462
+ if (customEmoji == null) return null;
463
+ return this.getEmoji(ctx, values.name, customEmoji);
464
+ }
465
+ getEmoji(ctx, name, data) {
466
+ let url;
467
+ if ("url" in data) url = new URL(data.url);
468
+ else {
469
+ const t = mimeDb[data.type];
470
+ url = new URL(`/emojis/${name}${t == null || t.extensions == null || t.extensions.length < 1 ? "" : `.${t.extensions[0]}`}`, ctx.origin);
471
+ }
472
+ return new Emoji({
473
+ id: ctx.getObjectUri(Emoji, { name }),
474
+ name: `:${name}:`,
475
+ icon: new Image({
476
+ mediaType: data.type,
477
+ url
478
+ })
479
+ });
480
+ }
481
+ addCustomEmoji(name, data) {
482
+ if (!name.match(/^[a-z0-9-_]+$/i)) throw new TypeError(`Invalid custom emoji name: ${name}. It must match /^[a-z0-9-_]+$/i.`);
483
+ else if (name in this.customEmojis) throw new TypeError(`Duplicate custom emoji name: ${name}`);
484
+ else if (!data.type.startsWith("image/")) throw new TypeError(`Unsupported media type: ${data.type}`);
485
+ this.customEmojis[name] = data;
486
+ return (session) => this.getEmoji(session.context, name, data);
487
+ }
488
+ addCustomEmojis(emojis) {
489
+ const emojiMap = {};
490
+ for (const name in emojis) emojiMap[name] = this.addCustomEmoji(name, emojis[name]);
491
+ return emojiMap;
492
+ }
493
+ async addCollectionInverseProperty(request, contextData, response) {
494
+ if (!response.ok) return response;
495
+ const ctx = this.federation.createContext(request, contextData);
496
+ const parsed = ctx.parseUri(new URL(request.url));
497
+ if (parsed == null || parsed.type !== "outbox" && parsed.type !== "followers" || parsed.identifier == null) return response;
498
+ const contentType = response.headers.get("Content-Type");
499
+ if (contentType == null || !contentType.startsWith("application/activity+json") && !contentType.startsWith("application/ld+json")) return response;
500
+ const body = await response.json();
501
+ if (typeof body !== "object" || body == null || Array.isArray(body)) return new Response(JSON.stringify(body), {
502
+ headers: response.headers,
503
+ status: response.status,
504
+ statusText: response.statusText
505
+ });
506
+ const property = parsed.type === "outbox" ? "outboxOf" : "followersOf";
507
+ const actorUri = ctx.getActorUri(parsed.identifier).href;
508
+ if (body[property] === actorUri) return new Response(JSON.stringify(body), {
509
+ headers: response.headers,
510
+ status: response.status,
511
+ statusText: response.statusText
512
+ });
513
+ const headers = new Headers(response.headers);
514
+ headers.delete("Content-Length");
515
+ return new Response(JSON.stringify({
516
+ ...body,
517
+ [property]: actorUri
518
+ }), {
519
+ headers,
520
+ status: response.status,
521
+ statusText: response.statusText
522
+ });
523
+ }
524
+ async fetch(request, contextData) {
525
+ if (this.behindProxy) request = await getXForwardedRequest(request);
526
+ const url = new URL(request.url);
527
+ if ((request.method === "GET" || request.method === "HEAD") && this.legacyObjectUrisIdentifier != null) {
528
+ const rewrittenPath = rewriteLegacyObjectPath(url.pathname, this.legacyObjectUrisIdentifier);
529
+ if (rewrittenPath != null) {
530
+ const location = new URL(url.href);
531
+ location.pathname = rewrittenPath;
532
+ return new Response(null, {
533
+ status: 301,
534
+ headers: { Location: location.href }
535
+ });
536
+ }
537
+ }
538
+ if (url.pathname.startsWith("/.well-known/") || url.pathname.startsWith("/ap/") || url.pathname.startsWith("/nodeinfo/")) {
539
+ const response = await this.federation.fetch(request, { contextData });
540
+ return await this.addCollectionInverseProperty(request, contextData, response);
541
+ }
542
+ const match = /^\/emojis\/([a-z0-9-_]+)(?:$|\.)/.exec(url.pathname);
543
+ if (match != null) {
544
+ const customEmoji = this.customEmojis[match[1]];
545
+ if (customEmoji == null || !("file" in customEmoji)) return new Response("Not Found", { status: 404 });
546
+ let fileInfo;
547
+ let data;
548
+ try {
549
+ fileInfo = await fs.stat(customEmoji.file);
550
+ data = await fs.readFile(customEmoji.file);
551
+ } catch (error) {
552
+ if (typeof error === "object" && error != null && "code" in error && error.code === "ENOENT") return new Response("Not Found", { status: 404 });
553
+ throw error;
554
+ }
555
+ const mtime = fileInfo.mtime ?? /* @__PURE__ */ new Date();
556
+ return new Response(data, { headers: {
557
+ "Content-Type": customEmoji.type,
558
+ "Content-Length": fileInfo.size.toString(),
559
+ "Cache-Control": "public, max-age=31536000, immutable",
560
+ "Last-Modified": mtime.toUTCString(),
561
+ "ETag": `"${mtime.getTime().toString(36)}${fileInfo.size.toString(36)}"`
562
+ } });
563
+ }
564
+ if (this.compatMode) {
565
+ const bot = this.#firstBot();
566
+ if (bot == null) return new Response("Not Found", { status: 404 });
567
+ return await app.fetch(request, {
568
+ bot,
569
+ contextData
570
+ });
571
+ }
572
+ return await multiApp.fetch(request, {
573
+ instance: this,
574
+ contextData
575
+ });
576
+ }
577
+ /**
578
+ * The web page URL of a bot hosted on the instance. A compatible
579
+ * (single-bot) instance serves its bot at the web root; a multi-bot
580
+ * instance serves each bot under a path derived from its username.
581
+ * @param bot The bot to get the web page URL of.
582
+ * @param origin The origin of the URL.
583
+ * @returns The web page URL of the bot.
584
+ */
585
+ getBotWebUrl(bot, origin) {
586
+ return new URL(this.compatMode ? "/" : `/@${encodeURIComponent(bot.username)}`, origin);
587
+ }
588
+ /**
589
+ * The web permalink of a message published by a bot hosted on
590
+ * the instance.
591
+ * @param bot The bot that published the message.
592
+ * @param id The UUID of the message.
593
+ * @param origin The origin of the URL.
594
+ * @returns The web permalink of the message.
595
+ */
596
+ getMessageWebUrl(bot, id, origin) {
597
+ return new URL(this.compatMode ? `/message/${id}` : `/@${encodeURIComponent(bot.username)}/${id}`, origin);
598
+ }
599
+ };
600
+
601
+ //#endregion
602
+ export { DEFAULT_INSTANCE_ACTOR_IDENTIFIER, InstanceImpl };
603
+ //# sourceMappingURL=instance-impl.js.map