@fedify/botkit 0.3.0-dev.108
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/LICENSE +661 -0
- package/README.md +75 -0
- package/dist/bot-impl.d.ts +111 -0
- package/dist/bot-impl.d.ts.map +1 -0
- package/dist/bot-impl.js +602 -0
- package/dist/bot-impl.js.map +1 -0
- package/dist/bot-impl.test.d.ts +2 -0
- package/dist/bot-impl.test.js +1642 -0
- package/dist/bot-impl.test.js.map +1 -0
- package/dist/bot.d.ts +270 -0
- package/dist/bot.d.ts.map +1 -0
- package/dist/bot.js +118 -0
- package/dist/bot.js.map +1 -0
- package/dist/bot.test.d.ts +2 -0
- package/dist/bot.test.js +80 -0
- package/dist/bot.test.js.map +1 -0
- package/dist/components/Layout.d.ts +26 -0
- package/dist/components/Layout.d.ts.map +1 -0
- package/dist/components/Layout.js +36 -0
- package/dist/components/Layout.js.map +1 -0
- package/dist/components/Message.d.ts +21 -0
- package/dist/components/Message.d.ts.map +1 -0
- package/dist/components/Message.js +99 -0
- package/dist/components/Message.js.map +1 -0
- package/dist/components/Message.test.d.ts +2 -0
- package/dist/components/Message.test.js +32 -0
- package/dist/components/Message.test.js.map +1 -0
- package/dist/deno.js +73 -0
- package/dist/deno.js.map +1 -0
- package/dist/emoji.d.ts +87 -0
- package/dist/emoji.d.ts.map +1 -0
- package/dist/emoji.js +37 -0
- package/dist/emoji.js.map +1 -0
- package/dist/emoji.test.d.ts +2 -0
- package/dist/emoji.test.js +109 -0
- package/dist/emoji.test.js.map +1 -0
- package/dist/events.d.ts +110 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +4 -0
- package/dist/follow-impl.d.ts +23 -0
- package/dist/follow-impl.d.ts.map +1 -0
- package/dist/follow-impl.js +51 -0
- package/dist/follow-impl.js.map +1 -0
- package/dist/follow-impl.test.d.ts +2 -0
- package/dist/follow-impl.test.js +110 -0
- package/dist/follow-impl.test.js.map +1 -0
- package/dist/follow.d.ts +44 -0
- package/dist/follow.d.ts.map +1 -0
- package/dist/follow.js +4 -0
- package/dist/message-impl.d.ts +54 -0
- package/dist/message-impl.d.ts.map +1 -0
- package/dist/message-impl.js +439 -0
- package/dist/message-impl.js.map +1 -0
- package/dist/message-impl.test.d.ts +2 -0
- package/dist/message-impl.test.js +519 -0
- package/dist/message-impl.test.js.map +1 -0
- package/dist/message.d.ts +223 -0
- package/dist/message.d.ts.map +1 -0
- package/dist/message.js +8 -0
- package/dist/mod.d.ts +13 -0
- package/dist/mod.js +13 -0
- package/dist/pages.d.ts +20 -0
- package/dist/pages.d.ts.map +1 -0
- package/dist/pages.js +361 -0
- package/dist/pages.js.map +1 -0
- package/dist/reaction.d.ts +90 -0
- package/dist/reaction.d.ts.map +1 -0
- package/dist/reaction.js +7 -0
- package/dist/repository.d.ts +323 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +483 -0
- package/dist/repository.js.map +1 -0
- package/dist/repository.test.d.ts +2 -0
- package/dist/repository.test.js +336 -0
- package/dist/repository.test.js.map +1 -0
- package/dist/session-impl.d.ts +32 -0
- package/dist/session-impl.d.ts.map +1 -0
- package/dist/session-impl.js +195 -0
- package/dist/session-impl.js.map +1 -0
- package/dist/session-impl.test.d.ts +20 -0
- package/dist/session-impl.test.d.ts.map +1 -0
- package/dist/session-impl.test.js +464 -0
- package/dist/session-impl.test.js.map +1 -0
- package/dist/session.d.ts +139 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +4 -0
- package/dist/text.d.ts +391 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +640 -0
- package/dist/text.js.map +1 -0
- package/dist/text.test.d.ts +2 -0
- package/dist/text.test.js +473 -0
- package/dist/text.test.js.map +1 -0
- package/package.json +137 -0
package/dist/bot-impl.js
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
|
|
2
|
+
import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
|
3
|
+
Date.prototype.toTemporalInstant = toTemporalInstant;
|
|
4
|
+
|
|
5
|
+
import deno_default from "./deno.js";
|
|
6
|
+
import { isEmoji } from "./emoji.js";
|
|
7
|
+
import { FollowRequestImpl } from "./follow-impl.js";
|
|
8
|
+
import { createMessage, getMessageVisibility, isMessageObject, isQuoteLink, messageClasses } from "./message-impl.js";
|
|
9
|
+
import { app } from "./pages.js";
|
|
10
|
+
import { KvRepository } from "./repository.js";
|
|
11
|
+
import { SessionImpl } from "./session-impl.js";
|
|
12
|
+
import { Accept, Announce, Article, ChatMessage, Create, Emoji, EmojiReact, Endpoints, Follow, Image, Like, Link, Mention, Note, PUBLIC_COLLECTION, PropertyValue, Question, Reject, Service, Undo, isActor } from "@fedify/fedify/vocab";
|
|
13
|
+
import { Object as Object$2, createFederation, generateCryptoKeyPair } from "@fedify/fedify";
|
|
14
|
+
import { getLogger } from "@logtape/logtape";
|
|
15
|
+
import mimeDb from "mime-db";
|
|
16
|
+
import fs from "node:fs/promises";
|
|
17
|
+
import { getXForwardedRequest } from "x-forwarded-fetch";
|
|
18
|
+
|
|
19
|
+
//#region src/bot-impl.ts
|
|
20
|
+
var BotImpl = class {
|
|
21
|
+
identifier;
|
|
22
|
+
class;
|
|
23
|
+
username;
|
|
24
|
+
name;
|
|
25
|
+
summary;
|
|
26
|
+
#summary;
|
|
27
|
+
icon;
|
|
28
|
+
image;
|
|
29
|
+
properties;
|
|
30
|
+
#properties;
|
|
31
|
+
followerPolicy;
|
|
32
|
+
customEmojis;
|
|
33
|
+
repository;
|
|
34
|
+
software;
|
|
35
|
+
behindProxy;
|
|
36
|
+
pages;
|
|
37
|
+
collectionWindow;
|
|
38
|
+
federation;
|
|
39
|
+
onFollow;
|
|
40
|
+
onUnfollow;
|
|
41
|
+
onAcceptFollow;
|
|
42
|
+
onRejectFollow;
|
|
43
|
+
onMention;
|
|
44
|
+
onReply;
|
|
45
|
+
onQuote;
|
|
46
|
+
onMessage;
|
|
47
|
+
onSharedMessage;
|
|
48
|
+
onLike;
|
|
49
|
+
onUnlike;
|
|
50
|
+
onReact;
|
|
51
|
+
onUnreact;
|
|
52
|
+
constructor(options) {
|
|
53
|
+
this.identifier = options.identifier ?? "bot";
|
|
54
|
+
this.class = options.class ?? Service;
|
|
55
|
+
this.username = options.username;
|
|
56
|
+
this.name = options.name;
|
|
57
|
+
this.summary = options.summary;
|
|
58
|
+
this.#summary = null;
|
|
59
|
+
this.icon = options.icon;
|
|
60
|
+
this.image = options.image;
|
|
61
|
+
this.properties = options.properties ?? {};
|
|
62
|
+
this.#properties = null;
|
|
63
|
+
this.followerPolicy = options.followerPolicy ?? "accept";
|
|
64
|
+
this.customEmojis = {};
|
|
65
|
+
this.repository = options.repository ?? new KvRepository(options.kv);
|
|
66
|
+
this.software = options.software;
|
|
67
|
+
this.pages = {
|
|
68
|
+
color: "green",
|
|
69
|
+
css: "",
|
|
70
|
+
...options.pages ?? {}
|
|
71
|
+
};
|
|
72
|
+
this.federation = createFederation({
|
|
73
|
+
kv: options.kv,
|
|
74
|
+
queue: options.queue,
|
|
75
|
+
userAgent: { software: `BotKit/${deno_default.version}` }
|
|
76
|
+
});
|
|
77
|
+
this.behindProxy = options.behindProxy ?? false;
|
|
78
|
+
this.collectionWindow = options.collectionWindow ?? 50;
|
|
79
|
+
this.initialize();
|
|
80
|
+
}
|
|
81
|
+
initialize() {
|
|
82
|
+
this.federation.setActorDispatcher("/ap/actor/{identifier}", this.dispatchActor.bind(this)).mapHandle(this.mapHandle.bind(this)).setKeyPairsDispatcher(this.dispatchActorKeyPairs.bind(this));
|
|
83
|
+
this.federation.setFollowersDispatcher("/ap/actor/{identifier}/followers", this.dispatchFollowers.bind(this)).setFirstCursor(this.getFollowersFirstCursor.bind(this)).setCounter(this.countFollowers.bind(this));
|
|
84
|
+
this.federation.setOutboxDispatcher("/ap/actor/{identifier}/outbox", this.dispatchOutbox.bind(this)).setFirstCursor(this.getOutboxFirstCursor.bind(this)).setCounter(this.countOutbox.bind(this));
|
|
85
|
+
this.federation.setObjectDispatcher(Follow, "/ap/follow/{id}", this.dispatchFollow.bind(this)).authorize(this.authorizeFollow.bind(this));
|
|
86
|
+
this.federation.setObjectDispatcher(Create, "/ap/create/{id}", this.dispatchCreate.bind(this));
|
|
87
|
+
this.federation.setObjectDispatcher(Article, "/ap/article/{id}", (ctx, values) => this.dispatchMessage(Article, ctx, values.id));
|
|
88
|
+
this.federation.setObjectDispatcher(ChatMessage, "/ap/chat-message/{id}", (ctx, values) => this.dispatchMessage(ChatMessage, ctx, values.id));
|
|
89
|
+
this.federation.setObjectDispatcher(Note, "/ap/note/{id}", (ctx, values) => this.dispatchMessage(Note, ctx, values.id));
|
|
90
|
+
this.federation.setObjectDispatcher(Question, "/ap/question/{id}", (ctx, values) => this.dispatchMessage(Question, ctx, values.id));
|
|
91
|
+
this.federation.setObjectDispatcher(Announce, "/ap/announce/{id}", this.dispatchAnnounce.bind(this));
|
|
92
|
+
this.federation.setObjectDispatcher(Emoji, "/ap/emoji/{name}", this.dispatchEmoji.bind(this));
|
|
93
|
+
this.federation.setInboxListeners("/ap/actor/{identifier}/inbox", "/ap/inbox").on(Follow, this.onFollowed.bind(this)).on(Undo, async (ctx, undo) => {
|
|
94
|
+
const object = await undo.getObject(ctx);
|
|
95
|
+
if (object instanceof Follow) await this.onUnfollowed(ctx, undo);
|
|
96
|
+
else if (object instanceof Like) await this.onUnliked(ctx, undo);
|
|
97
|
+
else {
|
|
98
|
+
const logger = getLogger([
|
|
99
|
+
"botkit",
|
|
100
|
+
"bot",
|
|
101
|
+
"inbox"
|
|
102
|
+
]);
|
|
103
|
+
logger.warn("The Undo object {undoId} is not about Follow or Like: {object}.", {
|
|
104
|
+
undoId: undo.id?.href,
|
|
105
|
+
object
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}).on(Accept, this.onFollowAccepted.bind(this)).on(Reject, this.onFollowRejected.bind(this)).on(Create, this.onCreated.bind(this)).on(Announce, this.onAnnounced.bind(this)).on(Like, this.onLiked.bind(this)).setSharedKeyDispatcher(this.dispatchSharedKey.bind(this));
|
|
109
|
+
if (this.software != null) this.federation.setNodeInfoDispatcher("/nodeinfo/2.1", this.dispatchNodeInfo.bind(this));
|
|
110
|
+
}
|
|
111
|
+
async getActorSummary(session) {
|
|
112
|
+
if (this.summary == null) return null;
|
|
113
|
+
if (this.#summary == null) {
|
|
114
|
+
let summary = "";
|
|
115
|
+
const tags = [];
|
|
116
|
+
for await (const chunk of this.summary.getHtml(session)) summary += chunk;
|
|
117
|
+
for await (const tag of this.summary.getTags(session)) tags.push(tag);
|
|
118
|
+
return this.#summary = {
|
|
119
|
+
text: summary,
|
|
120
|
+
tags
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return this.#summary;
|
|
124
|
+
}
|
|
125
|
+
async getActorProperties(session) {
|
|
126
|
+
if (this.#properties != null) return this.#properties;
|
|
127
|
+
const pairs = [];
|
|
128
|
+
const tags = [];
|
|
129
|
+
for (const name in this.properties) {
|
|
130
|
+
const value = this.properties[name];
|
|
131
|
+
const pair = new PropertyValue({
|
|
132
|
+
name,
|
|
133
|
+
value: (await Array.fromAsync(value.getHtml(session))).join("")
|
|
134
|
+
});
|
|
135
|
+
pairs.push(pair);
|
|
136
|
+
for await (const tag of value.getTags(session)) tags.push(tag);
|
|
137
|
+
}
|
|
138
|
+
return this.#properties = {
|
|
139
|
+
pairs,
|
|
140
|
+
tags
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async dispatchActor(ctx, identifier) {
|
|
144
|
+
if (this.identifier !== identifier) return null;
|
|
145
|
+
const session = this.getSession(ctx);
|
|
146
|
+
const summary = await this.getActorSummary(session);
|
|
147
|
+
const { pairs, tags } = await this.getActorProperties(session);
|
|
148
|
+
const allTags = summary == null ? tags : [...tags, ...summary.tags];
|
|
149
|
+
const keyPairs = await ctx.getActorKeyPairs(identifier);
|
|
150
|
+
return new this.class({
|
|
151
|
+
id: ctx.getActorUri(identifier),
|
|
152
|
+
preferredUsername: this.username,
|
|
153
|
+
name: this.name,
|
|
154
|
+
summary: summary == null ? null : summary.text,
|
|
155
|
+
attachments: pairs,
|
|
156
|
+
tags: allTags.filter((tag, i) => allTags.findIndex((t) => t.name?.toString() === tag.name?.toString() && (t instanceof Link ? tag instanceof Link && t.href?.href === tag.href?.href : tag instanceof Object$2 && t.id?.href === tag.id?.href)) === i),
|
|
157
|
+
icon: this.icon == null ? null : this.icon instanceof Image ? this.icon : new Image({ url: this.icon }),
|
|
158
|
+
image: this.image == null ? null : this.image instanceof Image ? this.image : new Image({ url: this.image }),
|
|
159
|
+
inbox: ctx.getInboxUri(identifier),
|
|
160
|
+
endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }),
|
|
161
|
+
followers: ctx.getFollowersUri(identifier),
|
|
162
|
+
outbox: ctx.getOutboxUri(identifier),
|
|
163
|
+
publicKey: keyPairs[0].cryptographicKey,
|
|
164
|
+
assertionMethods: keyPairs.map((pair) => pair.multikey),
|
|
165
|
+
url: new URL("/", ctx.origin)
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
mapHandle(_ctx, username) {
|
|
169
|
+
return username === this.username ? this.identifier : null;
|
|
170
|
+
}
|
|
171
|
+
async dispatchActorKeyPairs(_ctx, identifier) {
|
|
172
|
+
if (identifier !== this.identifier) return [];
|
|
173
|
+
let keyPairs = await this.repository.getKeyPairs();
|
|
174
|
+
if (keyPairs == null) {
|
|
175
|
+
const rsa = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5");
|
|
176
|
+
const ed25519 = await generateCryptoKeyPair("Ed25519");
|
|
177
|
+
keyPairs = [rsa, ed25519];
|
|
178
|
+
await this.repository.setKeyPairs(keyPairs);
|
|
179
|
+
}
|
|
180
|
+
return keyPairs;
|
|
181
|
+
}
|
|
182
|
+
async dispatchFollowers(_ctx, identifier, cursor) {
|
|
183
|
+
if (identifier !== this.identifier) return null;
|
|
184
|
+
let followers;
|
|
185
|
+
let nextCursor;
|
|
186
|
+
if (cursor == null) {
|
|
187
|
+
followers = this.repository.getFollowers();
|
|
188
|
+
nextCursor = null;
|
|
189
|
+
} else {
|
|
190
|
+
const offset = cursor.match(/^\d+$/) ? parseInt(cursor) : 0;
|
|
191
|
+
followers = this.repository.getFollowers({
|
|
192
|
+
offset,
|
|
193
|
+
limit: this.collectionWindow
|
|
194
|
+
});
|
|
195
|
+
nextCursor = (offset + this.collectionWindow).toString();
|
|
196
|
+
}
|
|
197
|
+
const items = [];
|
|
198
|
+
let i = 0;
|
|
199
|
+
for await (const follower of followers) {
|
|
200
|
+
items.push(follower);
|
|
201
|
+
i++;
|
|
202
|
+
}
|
|
203
|
+
if (i < this.collectionWindow) nextCursor = null;
|
|
204
|
+
return {
|
|
205
|
+
items,
|
|
206
|
+
nextCursor
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
getFollowersFirstCursor(_ctx, identifier) {
|
|
210
|
+
if (identifier !== this.identifier) return null;
|
|
211
|
+
return "0";
|
|
212
|
+
}
|
|
213
|
+
async countFollowers(_ctx, identifier) {
|
|
214
|
+
if (identifier !== this.identifier) return null;
|
|
215
|
+
return await this.repository.countFollowers();
|
|
216
|
+
}
|
|
217
|
+
async getPermissionChecker(ctx) {
|
|
218
|
+
let owner;
|
|
219
|
+
try {
|
|
220
|
+
owner = await ctx.getSignedKeyOwner();
|
|
221
|
+
} catch {
|
|
222
|
+
owner = null;
|
|
223
|
+
}
|
|
224
|
+
let follower = false;
|
|
225
|
+
const ownerUri = owner?.id;
|
|
226
|
+
if (ownerUri != null) follower = await this.repository.hasFollower(ownerUri);
|
|
227
|
+
const followersUri = ctx.getFollowersUri(this.identifier);
|
|
228
|
+
return (object) => {
|
|
229
|
+
const recipients = [...object.toIds, ...object.ccIds].map((u) => u.href);
|
|
230
|
+
if (recipients.includes(PUBLIC_COLLECTION.href)) return true;
|
|
231
|
+
if (recipients.includes(followersUri.href) && follower) return true;
|
|
232
|
+
return ownerUri == null ? false : recipients.includes(ownerUri.href);
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
async dispatchOutbox(ctx, identifier, cursor) {
|
|
236
|
+
if (identifier !== this.identifier) return null;
|
|
237
|
+
const activities = this.repository.getMessages({
|
|
238
|
+
order: "newest",
|
|
239
|
+
until: cursor == null || cursor === "" ? void 0 : Temporal.Instant.from(cursor),
|
|
240
|
+
limit: cursor == null ? void 0 : this.collectionWindow + 1
|
|
241
|
+
});
|
|
242
|
+
const items = [];
|
|
243
|
+
const isVisible = await this.getPermissionChecker(ctx);
|
|
244
|
+
let i = 0;
|
|
245
|
+
let nextPublished = null;
|
|
246
|
+
for await (const activity of activities) {
|
|
247
|
+
if (cursor != null && i >= this.collectionWindow) {
|
|
248
|
+
nextPublished = activity.published ?? (await activity.getObject())?.published ?? null;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
if (isVisible(activity)) items.push(activity);
|
|
252
|
+
i++;
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
items,
|
|
256
|
+
nextCursor: nextPublished?.toString() ?? null
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
getOutboxFirstCursor(_ctx, identifier) {
|
|
260
|
+
if (identifier !== this.identifier) return null;
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
async countOutbox(_ctx, identifier) {
|
|
264
|
+
if (identifier !== this.identifier) return null;
|
|
265
|
+
return await this.repository.countMessages();
|
|
266
|
+
}
|
|
267
|
+
async dispatchFollow(_ctx, values) {
|
|
268
|
+
const id = values.id;
|
|
269
|
+
const follow = await this.repository.getSentFollow(id);
|
|
270
|
+
return follow ?? null;
|
|
271
|
+
}
|
|
272
|
+
async authorizeFollow(_ctx, values, _signedKey, signedKeyOwner) {
|
|
273
|
+
if (signedKeyOwner == null || signedKeyOwner.id == null) return false;
|
|
274
|
+
const id = values.id;
|
|
275
|
+
const follow = await this.repository.getSentFollow(id);
|
|
276
|
+
if (follow == null) return false;
|
|
277
|
+
return signedKeyOwner.id.href === follow.objectId?.href || signedKeyOwner.id.href === follow.actorId?.href;
|
|
278
|
+
}
|
|
279
|
+
async dispatchCreate(ctx, values) {
|
|
280
|
+
const activity = await this.repository.getMessage(values.id);
|
|
281
|
+
if (!(activity instanceof Create)) return null;
|
|
282
|
+
const isVisible = await this.getPermissionChecker(ctx);
|
|
283
|
+
return isVisible(activity) ? activity : null;
|
|
284
|
+
}
|
|
285
|
+
async dispatchMessage(cls, ctx, id) {
|
|
286
|
+
const activity = await this.repository.getMessage(id);
|
|
287
|
+
if (!(activity instanceof Create)) return null;
|
|
288
|
+
if ("request" in ctx) {
|
|
289
|
+
const isVisible = await this.getPermissionChecker(ctx);
|
|
290
|
+
if (!isVisible(activity)) return null;
|
|
291
|
+
}
|
|
292
|
+
const object = await activity.getObject(ctx);
|
|
293
|
+
if (object == null || !(object instanceof cls)) return null;
|
|
294
|
+
return object;
|
|
295
|
+
}
|
|
296
|
+
async dispatchAnnounce(ctx, values) {
|
|
297
|
+
const activity = await this.repository.getMessage(values.id);
|
|
298
|
+
if (!(activity instanceof Announce)) return null;
|
|
299
|
+
const isVisible = await this.getPermissionChecker(ctx);
|
|
300
|
+
return isVisible(activity) ? activity : null;
|
|
301
|
+
}
|
|
302
|
+
dispatchEmoji(ctx, values) {
|
|
303
|
+
const customEmoji = this.customEmojis[values.name];
|
|
304
|
+
if (customEmoji == null) return null;
|
|
305
|
+
return this.getEmoji(ctx, values.name, customEmoji);
|
|
306
|
+
}
|
|
307
|
+
dispatchSharedKey(_ctx) {
|
|
308
|
+
return { identifier: this.identifier };
|
|
309
|
+
}
|
|
310
|
+
async onFollowed(ctx, follow) {
|
|
311
|
+
const botUri = ctx.getActorUri(this.identifier);
|
|
312
|
+
if (follow.actorId?.href === botUri.href || follow.objectId?.href !== botUri.href) return;
|
|
313
|
+
const follower = await follow.getActor({
|
|
314
|
+
contextLoader: ctx.contextLoader,
|
|
315
|
+
documentLoader: ctx.documentLoader,
|
|
316
|
+
suppressError: true
|
|
317
|
+
});
|
|
318
|
+
if (follower == null || follower.id == null) return;
|
|
319
|
+
const session = this.getSession(ctx);
|
|
320
|
+
const followRequest = new FollowRequestImpl(session, follow, follower);
|
|
321
|
+
await this.onFollow?.(session, followRequest);
|
|
322
|
+
if (followRequest.state === "pending") {
|
|
323
|
+
if (this.followerPolicy === "accept") await followRequest.accept();
|
|
324
|
+
else if (this.followerPolicy === "reject") await followRequest.reject();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async onUnfollowed(ctx, undo) {
|
|
328
|
+
const followId = undo.objectId;
|
|
329
|
+
if (followId == null || undo.actorId == null) return;
|
|
330
|
+
const follower = await this.repository.removeFollower(followId, undo.actorId);
|
|
331
|
+
if (this.onUnfollow != null && follower != null) {
|
|
332
|
+
const session = this.getSession(ctx);
|
|
333
|
+
await this.onUnfollow(session, follower);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async onFollowAccepted(ctx, accept) {
|
|
337
|
+
const parsedObj = ctx.parseUri(accept.objectId);
|
|
338
|
+
if (parsedObj?.type !== "object" || parsedObj.class !== Follow) return;
|
|
339
|
+
const follow = await this.repository.getSentFollow(parsedObj.values.id);
|
|
340
|
+
if (follow == null) return;
|
|
341
|
+
const followee = await follow.getObject(ctx);
|
|
342
|
+
if (!isActor(followee) || followee.id == null || followee.id.href !== accept.actorId?.href) return;
|
|
343
|
+
await this.repository.addFollowee(followee.id, follow);
|
|
344
|
+
if (this.onAcceptFollow != null) {
|
|
345
|
+
const session = this.getSession(ctx);
|
|
346
|
+
await this.onAcceptFollow(session, followee);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async onFollowRejected(ctx, reject) {
|
|
350
|
+
const parsedObj = ctx.parseUri(reject.objectId);
|
|
351
|
+
if (parsedObj?.type !== "object" || parsedObj.class !== Follow) return;
|
|
352
|
+
const id = parsedObj.values.id;
|
|
353
|
+
const follow = await this.repository.getSentFollow(id);
|
|
354
|
+
if (follow == null) return;
|
|
355
|
+
const followee = await follow.getObject(ctx);
|
|
356
|
+
if (!isActor(followee) || followee.id == null || followee.id.href !== reject.actorId?.href) return;
|
|
357
|
+
await this.repository.removeSentFollow(id);
|
|
358
|
+
if (this.onRejectFollow != null) {
|
|
359
|
+
const session = this.getSession(ctx);
|
|
360
|
+
await this.onRejectFollow(session, followee);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async onCreated(ctx, create) {
|
|
364
|
+
const object = await create.getObject(ctx);
|
|
365
|
+
if (!(object instanceof Article || object instanceof ChatMessage || object instanceof Note || object instanceof Question)) return;
|
|
366
|
+
const session = this.getSession(ctx);
|
|
367
|
+
let messageCache = null;
|
|
368
|
+
const getMessage = async () => {
|
|
369
|
+
if (messageCache != null) return messageCache;
|
|
370
|
+
return messageCache = await createMessage(object, session, {});
|
|
371
|
+
};
|
|
372
|
+
const replyTarget = ctx.parseUri(object.replyTargetId);
|
|
373
|
+
if (this.onReply != null && replyTarget?.type === "object" && messageClasses.includes(replyTarget.class)) {
|
|
374
|
+
const message = await getMessage();
|
|
375
|
+
if (message.visibility === "public" || message.visibility === "unlisted") await ctx.forwardActivity(this, "followers", {
|
|
376
|
+
skipIfUnsigned: true,
|
|
377
|
+
preferSharedInbox: true,
|
|
378
|
+
excludeBaseUris: [new URL(ctx.origin)]
|
|
379
|
+
});
|
|
380
|
+
await this.onReply(session, message);
|
|
381
|
+
}
|
|
382
|
+
let quoteUrl = null;
|
|
383
|
+
for await (const tag of object.getTags(ctx)) if (tag instanceof Link && isQuoteLink(tag)) {
|
|
384
|
+
quoteUrl = tag.href;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
if (quoteUrl == null) quoteUrl = object.quoteUrl;
|
|
388
|
+
const quoteTarget = ctx.parseUri(quoteUrl);
|
|
389
|
+
if (this.onQuote != null && quoteTarget?.type === "object" && messageClasses.includes(quoteTarget.class)) {
|
|
390
|
+
const message = await getMessage();
|
|
391
|
+
if (message.visibility === "public" || message.visibility === "unlisted") await ctx.forwardActivity(this, "followers", {
|
|
392
|
+
skipIfUnsigned: true,
|
|
393
|
+
preferSharedInbox: true,
|
|
394
|
+
excludeBaseUris: [new URL(ctx.origin)]
|
|
395
|
+
});
|
|
396
|
+
await this.onQuote(session, message);
|
|
397
|
+
}
|
|
398
|
+
for await (const tag of object.getTags(ctx)) if (tag instanceof Mention && tag.href != null && this.onMention != null) {
|
|
399
|
+
const parsed = ctx.parseUri(tag.href);
|
|
400
|
+
if (parsed?.type === "actor" && parsed.identifier === this.identifier) {
|
|
401
|
+
await this.onMention(session, await getMessage());
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (this.onMessage != null) await this.onMessage(session, await getMessage());
|
|
406
|
+
}
|
|
407
|
+
async onAnnounced(ctx, announce) {
|
|
408
|
+
if (this.onSharedMessage == null || announce.id == null || announce.actorId == null) return;
|
|
409
|
+
const objectUri = ctx.parseUri(announce.objectId);
|
|
410
|
+
let object = null;
|
|
411
|
+
if (objectUri?.type === "object" && messageClasses.includes(objectUri.class)) {
|
|
412
|
+
const msg = await this.repository.getMessage(objectUri.values.id);
|
|
413
|
+
if (msg instanceof Create) object = await msg.getObject(ctx);
|
|
414
|
+
} else object = await announce.getObject(ctx);
|
|
415
|
+
if (!isMessageObject(object)) return;
|
|
416
|
+
const session = this.getSession(ctx);
|
|
417
|
+
const actor = announce.actorId.href == session.actorId.href ? await session.getActor() : await announce.getActor(ctx);
|
|
418
|
+
if (actor == null) return;
|
|
419
|
+
const original = await createMessage(object, session, {});
|
|
420
|
+
const sharedMessage = {
|
|
421
|
+
raw: announce,
|
|
422
|
+
id: announce.id,
|
|
423
|
+
actor,
|
|
424
|
+
visibility: getMessageVisibility(announce.toIds, announce.ccIds, actor),
|
|
425
|
+
original
|
|
426
|
+
};
|
|
427
|
+
await this.onSharedMessage(session, sharedMessage);
|
|
428
|
+
}
|
|
429
|
+
async #parseLike(ctx, like) {
|
|
430
|
+
if (like.id == null || like.actorId == null) return void 0;
|
|
431
|
+
const objectUri = ctx.parseUri(like.objectId);
|
|
432
|
+
let object = null;
|
|
433
|
+
if (objectUri?.type === "object" && messageClasses.includes(objectUri.class)) {
|
|
434
|
+
const msg = await this.repository.getMessage(objectUri.values.id);
|
|
435
|
+
if (msg instanceof Create) object = await msg.getObject(ctx);
|
|
436
|
+
} else object = await like.getObject(ctx);
|
|
437
|
+
if (!isMessageObject(object)) return void 0;
|
|
438
|
+
const session = this.getSession(ctx);
|
|
439
|
+
const actor = like.actorId.href == session.actorId.href ? await session.getActor() : await like.getActor(ctx);
|
|
440
|
+
if (actor == null) return;
|
|
441
|
+
const message = await createMessage(object, session, {});
|
|
442
|
+
return {
|
|
443
|
+
session,
|
|
444
|
+
like: {
|
|
445
|
+
raw: like,
|
|
446
|
+
id: like.id,
|
|
447
|
+
actor,
|
|
448
|
+
message
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
async onLiked(ctx, like) {
|
|
453
|
+
if (like.name != null) return this.onReacted(ctx, like);
|
|
454
|
+
if (this.onLike == null) return;
|
|
455
|
+
const sessionAndLike = await this.#parseLike(ctx, like);
|
|
456
|
+
if (sessionAndLike == null) return;
|
|
457
|
+
const { session, like: likeObject } = sessionAndLike;
|
|
458
|
+
await this.onLike(session, likeObject);
|
|
459
|
+
}
|
|
460
|
+
async onUnliked(ctx, undo) {
|
|
461
|
+
const like = await undo.getObject(ctx);
|
|
462
|
+
if (!(like instanceof Like)) return;
|
|
463
|
+
if (like.name != null) return this.onUnreacted(ctx, undo);
|
|
464
|
+
if (this.onUnlike == null) return;
|
|
465
|
+
if (undo.actorId?.href !== like.actorId?.href) return;
|
|
466
|
+
const sessionAndLike = await this.#parseLike(ctx, like);
|
|
467
|
+
if (sessionAndLike == null) return;
|
|
468
|
+
const { session, like: likeObject } = sessionAndLike;
|
|
469
|
+
await this.onUnlike(session, likeObject);
|
|
470
|
+
}
|
|
471
|
+
async #parseReaction(ctx, react) {
|
|
472
|
+
if (react.id == null || react.actorId == null || react.name == null) return void 0;
|
|
473
|
+
let emoji;
|
|
474
|
+
if (isEmoji(react.name)) emoji = react.name;
|
|
475
|
+
else if (typeof react.name === "string" && react.name.startsWith(":") && react.name.endsWith(":")) {
|
|
476
|
+
for await (const tag of react.getTags(ctx)) if (tag instanceof Emoji && tag.name === react.name) {
|
|
477
|
+
emoji = tag;
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (emoji == null) return void 0;
|
|
482
|
+
const objectUri = ctx.parseUri(react.objectId);
|
|
483
|
+
let object = null;
|
|
484
|
+
if (objectUri?.type === "object" && messageClasses.includes(objectUri.class)) {
|
|
485
|
+
const msg = await this.repository.getMessage(objectUri.values.id);
|
|
486
|
+
if (msg instanceof Create) object = await msg.getObject(ctx);
|
|
487
|
+
} else object = await react.getObject(ctx);
|
|
488
|
+
if (!isMessageObject(object)) return void 0;
|
|
489
|
+
const session = this.getSession(ctx);
|
|
490
|
+
const actor = react.actorId.href == session.actorId.href ? await session.getActor() : await react.getActor(ctx);
|
|
491
|
+
if (actor == null) return;
|
|
492
|
+
const message = await createMessage(object, session, {});
|
|
493
|
+
return {
|
|
494
|
+
session,
|
|
495
|
+
reaction: {
|
|
496
|
+
raw: react,
|
|
497
|
+
id: react.id,
|
|
498
|
+
actor,
|
|
499
|
+
message,
|
|
500
|
+
emoji
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
async onReacted(ctx, react) {
|
|
505
|
+
if (this.onReact == null) return;
|
|
506
|
+
const sessionAndReaction = await this.#parseReaction(ctx, react);
|
|
507
|
+
if (sessionAndReaction == null) return;
|
|
508
|
+
const { session, reaction } = sessionAndReaction;
|
|
509
|
+
await this.onReact(session, reaction);
|
|
510
|
+
}
|
|
511
|
+
async onUnreacted(ctx, undo) {
|
|
512
|
+
if (this.onUnreact == null) return;
|
|
513
|
+
const react = await undo.getObject(ctx);
|
|
514
|
+
if (!(react instanceof EmojiReact || react instanceof Like)) return;
|
|
515
|
+
if (undo.actorId?.href !== react.actorId?.href) return;
|
|
516
|
+
const sessionAndReaction = await this.#parseReaction(ctx, react);
|
|
517
|
+
if (sessionAndReaction == null) return;
|
|
518
|
+
const { session, reaction } = sessionAndReaction;
|
|
519
|
+
await this.onUnreact(session, reaction);
|
|
520
|
+
}
|
|
521
|
+
dispatchNodeInfo(_ctx) {
|
|
522
|
+
return {
|
|
523
|
+
software: this.software,
|
|
524
|
+
protocols: ["activitypub"],
|
|
525
|
+
services: { outbound: ["atom1.0"] },
|
|
526
|
+
usage: {
|
|
527
|
+
users: {
|
|
528
|
+
total: 1,
|
|
529
|
+
activeMonth: 1,
|
|
530
|
+
activeHalfyear: 1
|
|
531
|
+
},
|
|
532
|
+
localPosts: 0,
|
|
533
|
+
localComments: 0
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
getSession(origin, contextData) {
|
|
538
|
+
const ctx = typeof origin === "string" || origin instanceof URL ? this.federation.createContext(new URL(origin), contextData) : origin;
|
|
539
|
+
return new SessionImpl(this, ctx);
|
|
540
|
+
}
|
|
541
|
+
async fetch(request, contextData) {
|
|
542
|
+
if (this.behindProxy) request = await getXForwardedRequest(request);
|
|
543
|
+
const url = new URL(request.url);
|
|
544
|
+
if (url.pathname.startsWith("/.well-known/") || url.pathname.startsWith("/ap/") || url.pathname.startsWith("/nodeinfo/")) return await this.federation.fetch(request, { contextData });
|
|
545
|
+
const match = /^\/emojis\/([a-z0-9-_]+)(?:$|\.)/.exec(url.pathname);
|
|
546
|
+
if (match != null) {
|
|
547
|
+
const customEmoji = this.customEmojis[match[1]];
|
|
548
|
+
if (customEmoji == null || !("file" in customEmoji)) return new Response("Not Found", { status: 404 });
|
|
549
|
+
let file;
|
|
550
|
+
try {
|
|
551
|
+
file = await fs.open(customEmoji.file, "r");
|
|
552
|
+
} catch (error) {
|
|
553
|
+
if (typeof error === "object" && error != null && "code" in error && error.code === "ENOENT") return new Response("Not Found", { status: 404 });
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
556
|
+
const fileInfo = await file.stat();
|
|
557
|
+
return new Response(file.readableWebStream(), { headers: {
|
|
558
|
+
"Content-Type": customEmoji.type,
|
|
559
|
+
"Content-Length": fileInfo.size.toString(),
|
|
560
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
561
|
+
"Last-Modified": (fileInfo.mtime ?? /* @__PURE__ */ new Date()).toUTCString(),
|
|
562
|
+
"ETag": `"${fileInfo.mtime?.getTime().toString(36)}${fileInfo.size.toString(36)}"`
|
|
563
|
+
} });
|
|
564
|
+
}
|
|
565
|
+
return await app.fetch(request, {
|
|
566
|
+
bot: this,
|
|
567
|
+
contextData
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
getEmoji(ctx, name, data) {
|
|
571
|
+
let url;
|
|
572
|
+
if ("url" in data) url = new URL(data.url);
|
|
573
|
+
else {
|
|
574
|
+
const t = mimeDb[data.type];
|
|
575
|
+
url = new URL(`/emojis/${name}${t == null || t.extensions == null || t.extensions.length < 1 ? "" : `.${t.extensions[0]}`}`, ctx.origin);
|
|
576
|
+
}
|
|
577
|
+
return new Emoji({
|
|
578
|
+
id: ctx.getObjectUri(Emoji, { name }),
|
|
579
|
+
name: `:${name}:`,
|
|
580
|
+
icon: new Image({
|
|
581
|
+
mediaType: data.type,
|
|
582
|
+
url
|
|
583
|
+
})
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
addCustomEmoji(name, data) {
|
|
587
|
+
if (!name.match(/^[a-z0-9-_]+$/i)) throw new TypeError(`Invalid custom emoji name: ${name}. It must match /^[a-z0-9-_]+$/i.`);
|
|
588
|
+
else if (name in this.customEmojis) throw new TypeError(`Duplicate custom emoji name: ${name}`);
|
|
589
|
+
else if (!data.type.startsWith("image/")) throw new TypeError(`Unsupported media type: ${data.type}`);
|
|
590
|
+
this.customEmojis[name] = data;
|
|
591
|
+
return (session) => this.getEmoji(session.context, name, data);
|
|
592
|
+
}
|
|
593
|
+
addCustomEmojis(emojis) {
|
|
594
|
+
const emojiMap = {};
|
|
595
|
+
for (const name in emojis) emojiMap[name] = this.addCustomEmoji(name, emojis[name]);
|
|
596
|
+
return emojiMap;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
//#endregion
|
|
601
|
+
export { BotImpl };
|
|
602
|
+
//# sourceMappingURL=bot-impl.js.map
|