@evanp/activitypub-bot 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.github/dependabot.yml +11 -0
  2. package/.github/workflows/main-docker.yml +45 -0
  3. package/.github/workflows/tag-docker.yml +54 -0
  4. package/Dockerfile +23 -0
  5. package/LICENSE +661 -0
  6. package/README.md +82 -0
  7. package/bots/index.js +7 -0
  8. package/docs/activitypub.bot.drawio +110 -0
  9. package/index.js +23 -0
  10. package/lib/activitydistributor.js +263 -0
  11. package/lib/activityhandler.js +999 -0
  12. package/lib/activitypubclient.js +126 -0
  13. package/lib/activitystreams.js +41 -0
  14. package/lib/actorstorage.js +300 -0
  15. package/lib/app.js +173 -0
  16. package/lib/authorizer.js +133 -0
  17. package/lib/bot.js +44 -0
  18. package/lib/botcontext.js +520 -0
  19. package/lib/botdatastorage.js +87 -0
  20. package/lib/bots/donothing.js +11 -0
  21. package/lib/bots/ok.js +41 -0
  22. package/lib/digester.js +23 -0
  23. package/lib/httpsignature.js +195 -0
  24. package/lib/httpsignatureauthenticator.js +81 -0
  25. package/lib/keystorage.js +113 -0
  26. package/lib/microsyntax.js +140 -0
  27. package/lib/objectcache.js +48 -0
  28. package/lib/objectstorage.js +319 -0
  29. package/lib/remotekeystorage.js +116 -0
  30. package/lib/routes/collection.js +92 -0
  31. package/lib/routes/health.js +24 -0
  32. package/lib/routes/inbox.js +83 -0
  33. package/lib/routes/object.js +69 -0
  34. package/lib/routes/server.js +47 -0
  35. package/lib/routes/user.js +63 -0
  36. package/lib/routes/webfinger.js +36 -0
  37. package/lib/urlformatter.js +97 -0
  38. package/package.json +51 -0
  39. package/tests/activitydistributor.test.js +606 -0
  40. package/tests/activityhandler.test.js +2185 -0
  41. package/tests/activitypubclient.test.js +225 -0
  42. package/tests/actorstorage.test.js +261 -0
  43. package/tests/app.test.js +17 -0
  44. package/tests/authorizer.test.js +306 -0
  45. package/tests/bot.donothing.test.js +30 -0
  46. package/tests/bot.ok.test.js +101 -0
  47. package/tests/botcontext.test.js +674 -0
  48. package/tests/botdatastorage.test.js +87 -0
  49. package/tests/digester.test.js +56 -0
  50. package/tests/fixtures/bots.js +15 -0
  51. package/tests/httpsignature.test.js +200 -0
  52. package/tests/httpsignatureauthenticator.test.js +463 -0
  53. package/tests/keystorage.test.js +89 -0
  54. package/tests/microsyntax.test.js +122 -0
  55. package/tests/objectcache.test.js +133 -0
  56. package/tests/objectstorage.test.js +148 -0
  57. package/tests/remotekeystorage.test.js +76 -0
  58. package/tests/routes.actor.test.js +207 -0
  59. package/tests/routes.collection.test.js +434 -0
  60. package/tests/routes.health.test.js +41 -0
  61. package/tests/routes.inbox.test.js +135 -0
  62. package/tests/routes.object.test.js +519 -0
  63. package/tests/routes.server.test.js +69 -0
  64. package/tests/routes.webfinger.test.js +41 -0
  65. package/tests/urlformatter.test.js +164 -0
  66. package/tests/utils/digest.js +7 -0
  67. package/tests/utils/nock.js +276 -0
@@ -0,0 +1,133 @@
1
+ export class Authorizer {
2
+ #PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'
3
+ #actorStorage = null
4
+ #formatter = null
5
+ #activityPubClient = null
6
+ constructor (actorStorage, formatter, activityPubClient) {
7
+ this.#actorStorage = actorStorage
8
+ this.#formatter = formatter
9
+ this.#activityPubClient = activityPubClient
10
+ }
11
+
12
+ async canRead (actor, object) {
13
+ if (typeof object !== 'object') {
14
+ throw new Error('object must be an object')
15
+ }
16
+ if (!('id' in object)) {
17
+ throw new Error('object must have an id property')
18
+ }
19
+ if (typeof object.id !== 'string') {
20
+ throw new Error('object.id must be a string')
21
+ }
22
+ return (this.#formatter.isLocal(object.id))
23
+ ? await this.#canReadLocal(actor, object)
24
+ : await this.#canReadRemote(actor, object)
25
+ }
26
+
27
+ async isOwner (actor, object) {
28
+ const owner = await this.#getOwner(object)
29
+ return actor.id === owner.id
30
+ }
31
+
32
+ async sameOrigin (actor, object) {
33
+ const actorUrl = new URL(actor.id)
34
+ const objectUrl = new URL(object.id)
35
+ return actorUrl.origin === objectUrl.origin
36
+ }
37
+
38
+ async #canReadLocal (actor, object) {
39
+ const recipients = this.#getRecipients(object)
40
+ if (!actor) {
41
+ return recipients.has(this.#PUBLIC)
42
+ }
43
+ const ownerId = (await this.#getOwner(object))?.id
44
+ if (!ownerId) {
45
+ throw new Error(`no owner for ${object.id}`)
46
+ }
47
+ if (actor.id === ownerId) {
48
+ return true
49
+ }
50
+ const owner = await this.#actorStorage.getActorById(ownerId)
51
+ if (!owner) {
52
+ throw new Error(`no actor for ${ownerId}`)
53
+ }
54
+ const ownerName = owner.get('preferredUsername')?.first
55
+ if (!ownerName) {
56
+ throw new Error(`no preferredUsername for ${owner.id}`)
57
+ }
58
+ if (await this.#actorStorage.isInCollection(ownerName, 'blocked', actor)) {
59
+ return false
60
+ }
61
+ if (recipients.has(actor.id)) {
62
+ return true
63
+ }
64
+ if (recipients.has(this.#PUBLIC)) {
65
+ return true
66
+ }
67
+ const followers = this.#formatter.format({ username: ownerName, collection: 'followers' })
68
+ if (recipients.has(followers) && await this.#actorStorage.isInCollection(ownerName, 'followers', actor)) {
69
+ return true
70
+ }
71
+ return false
72
+ }
73
+
74
+ async #canReadRemote (actor, object) {
75
+ const recipients = this.#getRecipients(object)
76
+ if (!actor) {
77
+ return recipients.has(this.#PUBLIC)
78
+ }
79
+ if (recipients.has(actor.id)) {
80
+ return true
81
+ }
82
+ if (recipients.has(this.#PUBLIC)) {
83
+ return true
84
+ }
85
+ // TODO: check if it's to followers, actor is local, and actor
86
+ // is a follower
87
+ // TODO: check if it's to a collection, and actor is in the
88
+ // collection
89
+ return null
90
+ }
91
+
92
+ async #getOwner (object) {
93
+ if (object.attributedTo) {
94
+ return object.attributedTo.first
95
+ } else if (object.actor) {
96
+ return object.actor.first
97
+ } else if (object.owner) {
98
+ return object.owner.first
99
+ } else {
100
+ return null
101
+ }
102
+ }
103
+
104
+ #getRecipients (activity) {
105
+ const recipientIds = new Set()
106
+ if (activity.to) {
107
+ for (const to of activity.to) {
108
+ recipientIds.add(to.id)
109
+ }
110
+ }
111
+ if (activity.cc) {
112
+ for (const cc of activity.cc) {
113
+ recipientIds.add(cc.id)
114
+ }
115
+ }
116
+ if (activity.audience) {
117
+ for (const audience of activity.audience) {
118
+ recipientIds.add(audience.id)
119
+ }
120
+ }
121
+ if (activity.bto) {
122
+ for (const bto of activity.bto) {
123
+ recipientIds.add(bto.id)
124
+ }
125
+ }
126
+ if (activity.bcc) {
127
+ for (const bcc of activity.bcc) {
128
+ recipientIds.add(bcc.id)
129
+ }
130
+ }
131
+ return recipientIds
132
+ }
133
+ }
package/lib/bot.js ADDED
@@ -0,0 +1,44 @@
1
+ export default class Bot {
2
+ #context = null
3
+ #username = null
4
+
5
+ constructor (username) {
6
+ this.#username = username
7
+ }
8
+
9
+ async initialize (context) {
10
+ this.#context = context
11
+ }
12
+
13
+ get fullname () {
14
+ return 'Bot'
15
+ }
16
+
17
+ get description () {
18
+ return 'A default, do-nothing bot.'
19
+ }
20
+
21
+ get username () {
22
+ return this.#username
23
+ }
24
+
25
+ get _context () {
26
+ return this.#context
27
+ }
28
+
29
+ async onMention (object, activity) {
30
+ ; // no-op
31
+ }
32
+
33
+ async onFollow (actor, activity) {
34
+ ; // no-op
35
+ }
36
+
37
+ async onLike (object, activity) {
38
+ ; // no-op
39
+ }
40
+
41
+ async onAnnounce (object, activity) {
42
+ ; // no-op
43
+ }
44
+ }
@@ -0,0 +1,520 @@
1
+ import assert from 'node:assert'
2
+ import as2 from './activitystreams.js'
3
+ import { nanoid } from 'nanoid'
4
+ import fetch from 'node-fetch'
5
+
6
+ const AS2_TYPES = [
7
+ 'application/activity+json',
8
+ 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
9
+ ]
10
+
11
+ const WF_NS = 'https://purl.archive.org/socialweb/webfinger#'
12
+
13
+ const THREAD_PROP = 'https://purl.archive.org/socialweb/thread#thread'
14
+ const CONVERSATION_PROP = 'http://ostatus.org/schema/1.0/conversation'
15
+
16
+ export class BotContext {
17
+ #botId = null
18
+ #botDataStorage = null
19
+ #objectStorage = null
20
+ #actorStorage = null
21
+ #client = null
22
+ #distributor = null
23
+ #formatter = null
24
+ #transformer = null
25
+ #logger = null
26
+ get botId () {
27
+ return this.#botId
28
+ }
29
+
30
+ get logger () {
31
+ return this.#logger
32
+ }
33
+
34
+ constructor (
35
+ botId,
36
+ botDataStorage,
37
+ objectStorage,
38
+ actorStorage,
39
+ client,
40
+ distributor,
41
+ formatter,
42
+ transformer,
43
+ logger
44
+ ) {
45
+ this.#botId = botId
46
+ this.#botDataStorage = botDataStorage
47
+ this.#objectStorage = objectStorage
48
+ this.#actorStorage = actorStorage
49
+ this.#client = client
50
+ this.#distributor = distributor
51
+ this.#formatter = formatter
52
+ this.#transformer = transformer
53
+ this.#logger = logger.child({ class: 'BotContext', botId })
54
+ }
55
+
56
+ async setData (key, value) {
57
+ await this.#botDataStorage.set(this.#botId, key, value)
58
+ }
59
+
60
+ async getData (key) {
61
+ return await this.#botDataStorage.get(this.#botId, key)
62
+ }
63
+
64
+ async deleteData (key) {
65
+ await this.#botDataStorage.delete(this.#botId, key)
66
+ }
67
+
68
+ async hasData (key) {
69
+ return await this.#botDataStorage.has(this.#botId, key)
70
+ }
71
+
72
+ async getObject (id) {
73
+ assert.ok(id)
74
+ assert.equal(typeof id, 'string')
75
+ if (this.#formatter.isLocal(id)) {
76
+ return await this.#objectStorage.read(id)
77
+ } else {
78
+ return await this.#client.get(id, this.#botId)
79
+ }
80
+ }
81
+
82
+ async sendNote (content, { to, cc, bto, bcc, audience, inReplyTo, thread, context, conversation }) {
83
+ assert.ok(content)
84
+ assert.equal(typeof content, 'string')
85
+ assert.ok(to || cc || bto || bcc || audience)
86
+ const { html, tag } = await this.#transformer.transform(content)
87
+ const noteNanoid = nanoid()
88
+ const idProps = {
89
+ username: this.#botId,
90
+ type: 'note',
91
+ nanoid: noteNanoid
92
+ }
93
+ if (!inReplyTo) {
94
+ if (!thread) {
95
+ thread = this.#formatter.format({ ...idProps, collection: 'thread' })
96
+ }
97
+ if (!context) {
98
+ context = thread
99
+ }
100
+ if (!conversation) {
101
+ conversation = thread
102
+ }
103
+ }
104
+ const note = await as2.import({
105
+ '@context': [
106
+ 'https://www.w3.org/ns/activitystreams',
107
+ 'https://purl.archive.org/socialweb/thread/1.0',
108
+ { ostatus: 'http://ostatus.org/schema/1.0/' }
109
+ ],
110
+ type: 'Note',
111
+ content: html,
112
+ tag,
113
+ to,
114
+ cc,
115
+ bto,
116
+ bcc,
117
+ audience,
118
+ inReplyTo,
119
+ id: this.#formatter.format(idProps),
120
+ replies: this.#formatter.format({ ...idProps, collection: 'replies' }),
121
+ shares: this.#formatter.format({ ...idProps, collection: 'shares' }),
122
+ likes: this.#formatter.format({ ...idProps, collection: 'likes' }),
123
+ context,
124
+ thread,
125
+ 'ostatus:conversation': conversation,
126
+ published: new Date().toISOString(),
127
+ attributedTo: this.#formatter.format({ username: this.#botId })
128
+ })
129
+ await this.#objectStorage.create(note)
130
+ const activity = await as2.import({
131
+ type: 'Create',
132
+ id: this.#formatter.format({
133
+ username: this.#botId,
134
+ type: 'create',
135
+ nanoid: nanoid()
136
+ }),
137
+ actor: this.#formatter.format({ username: this.#botId }),
138
+ to,
139
+ cc,
140
+ bto,
141
+ bcc,
142
+ audience,
143
+ object: note
144
+ })
145
+ await this.#objectStorage.create(activity)
146
+ await this.#actorStorage.addToCollection(this.#botId, 'outbox', activity)
147
+ await this.#actorStorage.addToCollection(this.#botId, 'inbox', activity)
148
+ await this.#distributor.distribute(activity, this.#botId)
149
+ return note
150
+ }
151
+
152
+ async sendReply (content, object) {
153
+ const r = this.#getRecipients(object)
154
+ const attributedTo = object.attributedTo?.first?.id
155
+ if (r.to) {
156
+ r.to.push(attributedTo)
157
+ } else {
158
+ r.to = [attributedTo]
159
+ }
160
+ const full = this.#formatter.format({ username: this.#botId })
161
+ for (const prop in ['to', 'cc', 'bto', 'bcc', 'audience']) {
162
+ if (r[prop]) {
163
+ r[prop] = r[prop].filter(id => id !== full)
164
+ }
165
+ }
166
+ const opt = { inReplyTo: object.id, ...r }
167
+
168
+ const threadProp = object.get(THREAD_PROP)
169
+ if (threadProp) {
170
+ opt.thread = Array.from(threadProp)[0].id
171
+ }
172
+ const contextProp = object.get('context')
173
+ if (contextProp) {
174
+ opt.context = Array.from(contextProp)[0].id
175
+ }
176
+ const conversationProp = object.get(CONVERSATION_PROP)
177
+ if (conversationProp) {
178
+ opt.conversation = Array.from(conversationProp)[0]
179
+ }
180
+ return await this.sendNote(content, opt)
181
+ }
182
+
183
+ async likeObject (obj) {
184
+ assert.ok(obj)
185
+ assert.equal(typeof obj, 'object')
186
+ if (await this.#actorStorage.isInCollection(this.#botId, 'liked', obj)) {
187
+ throw new Error(`already liked: ${obj.id} by ${this.#botId}`)
188
+ }
189
+ const owners = obj.attributedTo
190
+ ? Array.from(obj.attributedTo).map((owner) => owner.id)
191
+ : Array.from(obj.actor).map((owner) => owner.id)
192
+ const activity = await as2.import({
193
+ type: 'Like',
194
+ id: this.#formatter.format({
195
+ username: this.#botId,
196
+ type: 'like',
197
+ nanoid: nanoid()
198
+ }),
199
+ actor: this.#formatter.format({ username: this.#botId }),
200
+ object: obj.id,
201
+ to: owners,
202
+ cc: 'https://www.w3.org/ns/activitystreams#Public'
203
+ })
204
+ await this.#objectStorage.create(activity)
205
+ await this.#actorStorage.addToCollection(this.#botId, 'outbox', activity)
206
+ await this.#actorStorage.addToCollection(this.#botId, 'inbox', activity)
207
+ await this.#actorStorage.addToCollection(this.#botId, 'liked', obj)
208
+ await this.#distributor.distribute(activity, this.#botId)
209
+ return activity
210
+ }
211
+
212
+ async unlikeObject (obj) {
213
+ assert.ok(obj)
214
+ assert.equal(typeof obj, 'object')
215
+ const owners = obj.attributedTo
216
+ ? Array.from(obj.attributedTo).map((owner) => owner.id)
217
+ : Array.from(obj.actor).map((owner) => owner.id)
218
+ if (!(await this.#actorStorage.isInCollection(this.#botId, 'liked', obj))) {
219
+ throw new Error(`not already liked: ${obj.id} by ${this.#botId}`)
220
+ }
221
+ const likeActivity = this.#findInOutbox('Like', obj)
222
+ if (!likeActivity) {
223
+ throw new Error('no like activity')
224
+ }
225
+ const undoActivity = await as2.import({
226
+ type: 'Undo',
227
+ id: this.#formatter.format({
228
+ username: this.#botId,
229
+ type: 'undo',
230
+ nanoid: nanoid()
231
+ }),
232
+ actor: this.#formatter.format({ username: this.#botId }),
233
+ object: likeActivity,
234
+ to: owners,
235
+ cc: 'https://www.w3.org/ns/activitystreams#Public'
236
+ })
237
+ await this.#objectStorage.create(undoActivity)
238
+ await this.#actorStorage.addToCollection(
239
+ this.#botId,
240
+ 'outbox',
241
+ undoActivity
242
+ )
243
+ await this.#actorStorage.addToCollection(
244
+ this.#botId,
245
+ 'inbox',
246
+ undoActivity
247
+ )
248
+ await this.#actorStorage.removeFromCollection(this.#botId, 'liked', obj)
249
+ await this.#distributor.distribute(undoActivity, this.#botId)
250
+ return undoActivity
251
+ }
252
+
253
+ async followActor (actor) {
254
+ assert.ok(actor)
255
+ assert.equal(typeof actor, 'object')
256
+ const activity = await as2.import({
257
+ type: 'Follow',
258
+ id: this.#formatter.format({
259
+ username: this.#botId,
260
+ type: 'follow',
261
+ nanoid: nanoid()
262
+ }),
263
+ actor: this.#formatter.format({ username: this.#botId }),
264
+ object: actor.id,
265
+ to: actor.id
266
+ })
267
+ await this.#objectStorage.create(activity)
268
+ await this.#actorStorage.addToCollection(this.#botId, 'outbox', activity)
269
+ await this.#actorStorage.addToCollection(this.#botId, 'inbox', activity)
270
+ await this.#actorStorage.addToCollection(
271
+ this.#botId,
272
+ 'pendingFollowing',
273
+ actor
274
+ )
275
+ await this.#distributor.distribute(activity, this.#botId)
276
+ return activity
277
+ }
278
+
279
+ async unfollowActor (actor) {
280
+ assert.ok(actor)
281
+ assert.equal(typeof actor, 'object')
282
+ const followActivity = this.#findInOutbox('Follow', actor)
283
+ if (!followActivity) {
284
+ throw new Error('no follow activity')
285
+ }
286
+ const undoActivity = await as2.import({
287
+ type: 'Undo',
288
+ id: this.#formatter.format({
289
+ username: this.#botId,
290
+ type: 'undo',
291
+ nanoid: nanoid()
292
+ }),
293
+ actor: this.#formatter.format({ username: this.#botId }),
294
+ object: followActivity,
295
+ to: actor.id
296
+ })
297
+ await this.#objectStorage.create(undoActivity)
298
+ await this.#actorStorage.addToCollection(
299
+ this.#botId,
300
+ 'outbox',
301
+ undoActivity
302
+ )
303
+ await this.#actorStorage.addToCollection(
304
+ this.#botId,
305
+ 'inbox',
306
+ undoActivity
307
+ )
308
+ await this.#actorStorage.removeFromCollection(
309
+ this.#botId,
310
+ 'pendingFollowing',
311
+ actor
312
+ )
313
+ await this.#actorStorage.removeFromCollection(
314
+ this.#botId,
315
+ 'following',
316
+ actor
317
+ )
318
+ await this.#distributor.distribute(undoActivity, this.#botId)
319
+ return undoActivity
320
+ }
321
+
322
+ async blockActor (actor) {
323
+ assert.ok(actor)
324
+ assert.equal(typeof actor, 'object')
325
+ const activity = await as2.import({
326
+ type: 'Block',
327
+ id: this.#formatter.format({
328
+ username: this.#botId,
329
+ type: 'block',
330
+ nanoid: nanoid()
331
+ }),
332
+ actor: this.#formatter.format({ username: this.#botId }),
333
+ object: actor.id
334
+ })
335
+ await this.#objectStorage.create(activity)
336
+ await this.#actorStorage.addToCollection(this.#botId, 'outbox', activity)
337
+ await this.#actorStorage.addToCollection(this.#botId, 'inbox', activity)
338
+ await this.#actorStorage.addToCollection(this.#botId, 'blocked', actor)
339
+ for (const coll of [
340
+ 'following',
341
+ 'followers',
342
+ 'pendingFollowing',
343
+ 'pendingFollowers'
344
+ ]) {
345
+ await this.#actorStorage.removeFromCollection(this.#botId, coll, actor)
346
+ }
347
+ // Do not distribute!
348
+ return activity
349
+ }
350
+
351
+ async unblockActor (actor) {
352
+ assert.ok(actor)
353
+ assert.equal(typeof actor, 'object')
354
+ const blockActivity = this.#findInOutbox('Block', actor)
355
+ if (!blockActivity) {
356
+ throw new Error('no block activity')
357
+ }
358
+ const undoActivity = await as2.import({
359
+ type: 'Undo',
360
+ id: this.#formatter.format({
361
+ username: this.#botId,
362
+ type: 'undo',
363
+ nanoid: nanoid()
364
+ }),
365
+ actor: this.#formatter.format({ username: this.#botId }),
366
+ object: blockActivity
367
+ })
368
+ await this.#objectStorage.create(undoActivity)
369
+ await this.#actorStorage.addToCollection(
370
+ this.#botId,
371
+ 'outbox',
372
+ undoActivity
373
+ )
374
+ await this.#actorStorage.addToCollection(
375
+ this.#botId,
376
+ 'inbox',
377
+ undoActivity
378
+ )
379
+ await this.#actorStorage.removeFromCollection(
380
+ this.#botId,
381
+ 'blocked',
382
+ actor
383
+ )
384
+ // Do not distribute!
385
+ return undoActivity
386
+ }
387
+
388
+ async updateNote (note, content) {
389
+ assert.ok(note)
390
+ assert.equal(typeof note, 'object')
391
+ assert.ok(content)
392
+ assert.equal(typeof content, 'string')
393
+ const exported = await note.export({ useOriginalContext: true })
394
+ exported.content = content
395
+ const updated = await as2.import(exported)
396
+ const { to, cc, bto, bcc, audience } = this.#getRecipients(note)
397
+ const activity = await as2.import({
398
+ type: 'Update',
399
+ id: this.#formatter.format({
400
+ username: this.#botId,
401
+ type: 'update',
402
+ nanoid: nanoid()
403
+ }),
404
+ actor: this.#formatter.format({ username: this.#botId }),
405
+ object: updated,
406
+ to,
407
+ cc,
408
+ bto,
409
+ bcc,
410
+ audience
411
+ })
412
+ await this.#objectStorage.update(updated)
413
+ await this.#objectStorage.create(activity)
414
+ await this.#actorStorage.addToCollection(this.#botId, 'outbox', activity)
415
+ await this.#actorStorage.addToCollection(this.#botId, 'inbox', activity)
416
+ await this.#distributor.distribute(activity, this.#botId)
417
+ return updated
418
+ }
419
+
420
+ async deleteNote (note) {
421
+ assert.ok(note)
422
+ assert.equal(typeof note, 'object')
423
+ const { to, cc, bto, bcc, audience } = this.#getRecipients(note)
424
+ const tombstone = await as2.import({
425
+ type: 'Tombstone',
426
+ id: note.id,
427
+ attributedTo: this.#formatter.format({ username: this.#botId }),
428
+ formerType: 'Note',
429
+ deleted: new Date().toISOString(),
430
+ to,
431
+ cc,
432
+ bto,
433
+ bcc,
434
+ audience
435
+ })
436
+ const activity = await as2.import({
437
+ type: 'Delete',
438
+ id: this.#formatter.format({
439
+ username: this.#botId,
440
+ type: 'delete',
441
+ nanoid: nanoid()
442
+ }),
443
+ actor: this.#formatter.format({ username: this.#botId }),
444
+ object: tombstone,
445
+ to,
446
+ cc,
447
+ bto,
448
+ bcc,
449
+ audience
450
+ })
451
+ await this.#objectStorage.update(tombstone)
452
+ await this.#objectStorage.create(activity)
453
+ await this.#actorStorage.addToCollection(this.#botId, 'outbox', activity)
454
+ await this.#actorStorage.addToCollection(this.#botId, 'inbox', activity)
455
+ await this.#distributor.distribute(activity, this.#botId)
456
+ return activity
457
+ }
458
+
459
+ async toActorId (webfinger) {
460
+ const [, domain] = webfinger.split('@')
461
+ const url = `https://${domain}/.well-known/webfinger` +
462
+ `?resource=acct:${webfinger}`
463
+ const res = await fetch(url)
464
+ if (res.status !== 200) {
465
+ throw new Error(`Status ${res.status} fetching ${url}`)
466
+ }
467
+ const json = await res.json()
468
+ const link = json?.links?.find(
469
+ (l) => l.rel === 'self' && AS2_TYPES.includes(l.type)
470
+ )
471
+ return link ? link.href : null
472
+ }
473
+
474
+ async toWebfinger (actorId) {
475
+ const actor = await this.#client.get(actorId)
476
+ if (!actor) {
477
+ return null
478
+ }
479
+ const wf = actor.get(WF_NS + 'webfinger')
480
+ if (wf) {
481
+ return wf.value()
482
+ }
483
+ const username = actor.get('preferredUsername')?.first
484
+ if (!username) {
485
+ return null
486
+ }
487
+ const actorUrl = new URL(actor.id)
488
+ return `${username}@${actorUrl.hostname}`
489
+ }
490
+
491
+ async #findInOutbox (type, obj) {
492
+ const full = `https://www.w3.org/ns/activitystreams#${type}`
493
+ let found = null
494
+ for await (const activity of this.#actorStorage.items(
495
+ this.#botId,
496
+ 'outbox'
497
+ )) {
498
+ if (activity.type === full && activity.object.id === obj.id) {
499
+ found = activity
500
+ break
501
+ }
502
+ }
503
+ return found
504
+ }
505
+
506
+ #getRecipients (obj) {
507
+ const to = obj.to ? Array.from(obj.to).map((to) => to.id) : null
508
+ const cc = obj.cc ? Array.from(obj.cc).map((cc) => cc.id) : null
509
+ const bto = obj.bto ? Array.from(obj.bto).map((bto) => bto.id) : null
510
+ const bcc = obj.bcc ? Array.from(obj.bcc).map((bcc) => bcc.id) : null
511
+ const audience = obj.audience
512
+ ? Array.from(obj.audience).map((audience) => audience.id)
513
+ : null
514
+ return { to, cc, bto, bcc, audience }
515
+ }
516
+
517
+ async onIdle () {
518
+ await this.#distributor.onIdle()
519
+ }
520
+ }