@fedify/botkit 0.5.0-dev.209 → 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.
- package/dist/bot-group.test.d.ts +2 -0
- package/dist/bot-group.test.js +220 -0
- package/dist/bot-group.test.js.map +1 -0
- package/dist/bot-impl.d.ts +132 -13
- package/dist/bot-impl.d.ts.map +1 -1
- package/dist/bot-impl.js +400 -178
- package/dist/bot-impl.js.map +1 -1
- package/dist/bot-impl.test.js +214 -76
- package/dist/bot-impl.test.js.map +1 -1
- package/dist/bot.d.ts +94 -48
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +2 -104
- package/dist/bot.js.map +1 -1
- package/dist/bot.test.js +59 -0
- package/dist/bot.test.js.map +1 -1
- package/dist/components/FollowButton.d.ts +5 -3
- package/dist/components/FollowButton.d.ts.map +1 -1
- package/dist/components/FollowButton.js +2 -2
- package/dist/components/FollowButton.js.map +1 -1
- package/dist/components/Follower.d.ts +2 -2
- package/dist/components/Layout.js +1 -1
- package/dist/components/Layout.js.map +1 -1
- package/dist/components/Message.d.ts +2 -2
- package/dist/deno.js +2 -1
- package/dist/deno.js.map +1 -1
- package/dist/follow-impl.test.js +3 -3
- package/dist/follow-impl.test.js.map +1 -1
- package/dist/instance-impl.d.ts +158 -0
- package/dist/instance-impl.d.ts.map +1 -0
- package/dist/instance-impl.js +603 -0
- package/dist/instance-impl.js.map +1 -0
- package/dist/instance-impl.test.d.ts +2 -0
- package/dist/instance-impl.test.js +103 -0
- package/dist/instance-impl.test.js.map +1 -0
- package/dist/instance-multi.test.d.ts +2 -0
- package/dist/instance-multi.test.js +151 -0
- package/dist/instance-multi.test.js.map +1 -0
- package/dist/instance-routing.test.d.ts +2 -0
- package/dist/instance-routing.test.js +367 -0
- package/dist/instance-routing.test.js.map +1 -0
- package/dist/instance.d.ts +318 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/instance.js +51 -0
- package/dist/instance.js.map +1 -0
- package/dist/message-impl.d.ts.map +1 -1
- package/dist/message-impl.js +17 -10
- package/dist/message-impl.js.map +1 -1
- package/dist/message-impl.test.js +43 -9
- package/dist/message-impl.test.js.map +1 -1
- package/dist/mod.d.ts +5 -3
- package/dist/mod.js +4 -2
- package/dist/pages.d.ts +10 -1
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +112 -41
- package/dist/pages.js.map +1 -1
- package/dist/pages.test.d.ts +2 -0
- package/dist/pages.test.js +170 -0
- package/dist/pages.test.js.map +1 -0
- package/dist/repository.d.ts +385 -138
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +595 -223
- package/dist/repository.js.map +1 -1
- package/dist/repository.test.js +564 -136
- package/dist/repository.test.js.map +1 -1
- package/dist/session-impl.d.ts.map +1 -1
- package/dist/session-impl.js +13 -4
- package/dist/session-impl.js.map +1 -1
- package/dist/session-impl.test.d.ts.map +1 -1
- package/dist/session-impl.test.js +9 -9
- package/dist/session-impl.test.js.map +1 -1
- package/dist/session.d.ts +8 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +27 -10
- package/dist/text.js.map +1 -1
- package/dist/text.test.js +37 -2
- package/dist/text.test.js.map +1 -1
- package/dist/uri.d.ts +46 -0
- package/dist/uri.d.ts.map +1 -0
- package/dist/uri.js +64 -0
- package/dist/uri.js.map +1 -0
- package/dist/uri.test.d.ts +2 -0
- package/dist/uri.test.js +93 -0
- package/dist/uri.test.js.map +1 -0
- 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
|