@evanp/activitypub-bot 0.8.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/main.yml +34 -0
- package/.github/workflows/{tag-docker.yml → tag.yml} +57 -5
- package/.nvmrc +1 -0
- package/Dockerfile +11 -16
- package/README.md +262 -12
- package/activitypub-bot.js +68 -0
- package/lib/activitydeliverer.js +260 -0
- package/lib/activityhandler.js +14 -0
- package/lib/activitypubclient.js +52 -1
- package/lib/activitystreams.js +31 -0
- package/lib/actorstorage.js +18 -28
- package/lib/app.js +18 -7
- package/lib/bot.js +7 -0
- package/lib/botcontext.js +62 -0
- package/lib/botdatastorage.js +0 -13
- package/lib/botfactory.js +24 -0
- package/lib/botmaker.js +23 -0
- package/lib/keystorage.js +7 -24
- package/lib/migrations/001-initial.js +107 -0
- package/lib/migrations/index.js +28 -0
- package/lib/objectcache.js +4 -1
- package/lib/objectstorage.js +0 -36
- package/lib/remotekeystorage.js +0 -24
- package/lib/routes/collection.js +6 -2
- package/lib/routes/inbox.js +7 -20
- package/lib/routes/sharedinbox.js +54 -0
- package/lib/routes/user.js +11 -5
- package/lib/routes/webfinger.js +11 -2
- package/lib/urlformatter.js +8 -0
- package/package.json +18 -11
- package/tests/activitydistributor.test.js +3 -3
- package/tests/activityhandler.test.js +96 -5
- package/tests/activitypubclient.test.js +115 -130
- package/tests/actorstorage.test.js +26 -4
- package/tests/authorizer.test.js +3 -8
- package/tests/botcontext.test.js +109 -63
- package/tests/botdatastorage.test.js +3 -2
- package/tests/botfactory.provincebotfactory.test.js +430 -0
- package/tests/fixtures/bots.js +13 -1
- package/tests/fixtures/eventloggingbot.js +57 -0
- package/tests/fixtures/provincebotfactory.js +53 -0
- package/tests/httpsignature.test.js +3 -4
- package/tests/httpsignatureauthenticator.test.js +3 -3
- package/tests/keystorage.test.js +37 -2
- package/tests/microsyntax.test.js +3 -2
- package/tests/objectstorage.test.js +4 -3
- package/tests/remotekeystorage.test.js +10 -8
- package/tests/routes.actor.test.js +7 -0
- package/tests/routes.collection.test.js +0 -1
- package/tests/routes.inbox.test.js +1 -0
- package/tests/routes.object.test.js +44 -38
- package/tests/routes.sharedinbox.test.js +473 -0
- package/tests/routes.webfinger.test.js +27 -0
- package/tests/utils/nock.js +250 -27
- package/.github/workflows/main-docker.yml +0 -45
- package/index.js +0 -23
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import BotMaker from './botmaker.js'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import as2 from './activitystreams.js'
|
|
4
|
+
|
|
5
|
+
const NS = 'https://www.w3.org/ns/activitystreams#'
|
|
6
|
+
|
|
7
|
+
const PUBLIC = [
|
|
8
|
+
`${NS}Public`,
|
|
9
|
+
'as:Public',
|
|
10
|
+
'Public'
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const COLLECTION_TYPES = [
|
|
14
|
+
`${NS}Collection`,
|
|
15
|
+
`${NS}OrderedCollection`
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export class ActivityDeliverer {
|
|
19
|
+
#actorStorage
|
|
20
|
+
#activityHandler
|
|
21
|
+
#formatter
|
|
22
|
+
#logger
|
|
23
|
+
#client
|
|
24
|
+
|
|
25
|
+
constructor (actorStorage, activityHandler, formatter, logger, client) {
|
|
26
|
+
this.#actorStorage = actorStorage
|
|
27
|
+
this.#activityHandler = activityHandler
|
|
28
|
+
this.#formatter = formatter
|
|
29
|
+
this.#client = client
|
|
30
|
+
this.#logger = logger.child({ class: this.constructor.name })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
isActivity (object) {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getActor (activity) {
|
|
38
|
+
return activity.actor?.first
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getRecipients (obj) {
|
|
42
|
+
let r = []
|
|
43
|
+
for (const prop of ['to', 'cc', 'audience']) {
|
|
44
|
+
const val = obj.get(prop)
|
|
45
|
+
if (val) {
|
|
46
|
+
r = r.concat(Array.from(val))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return r
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async deliverTo (activity, bot) {
|
|
53
|
+
try {
|
|
54
|
+
await this.#activityHandler.handleActivity(bot, activity)
|
|
55
|
+
} catch (err) {
|
|
56
|
+
this.#logger.warn(err)
|
|
57
|
+
}
|
|
58
|
+
this.#logger.debug(`Adding ${activity.id} to ${bot.username} inbox`)
|
|
59
|
+
await this.#actorStorage.addToCollection(bot.username, 'inbox', activity)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async deliverToAll (activity, bots) {
|
|
63
|
+
const deliveredTo = new Set()
|
|
64
|
+
const actor = this.getActor(activity)
|
|
65
|
+
const recipients = this.getRecipients(activity)
|
|
66
|
+
|
|
67
|
+
for (const recipient of recipients) {
|
|
68
|
+
if (this.#isPublic(recipient)) {
|
|
69
|
+
await this.#deliverPublic(activity, bots)
|
|
70
|
+
} else if (this.#isLocal(recipient)) {
|
|
71
|
+
const parts = this.#formatter.unformat(recipient.id)
|
|
72
|
+
if (this.#isLocalActor(parts)) {
|
|
73
|
+
await this.#deliverLocalActor(activity, recipient, bots, deliveredTo)
|
|
74
|
+
} else if (this.#isLocalFollowersCollection(parts)) {
|
|
75
|
+
await this.#deliverLocalFollowersCollection(activity, parts.username, bots, deliveredTo)
|
|
76
|
+
} else if (this.#isLocalFollowingCollection(parts)) {
|
|
77
|
+
await this.#deliverLocalFollowingCollection(activity, parts.username, bots, deliveredTo)
|
|
78
|
+
} else {
|
|
79
|
+
this.#logger.warn(
|
|
80
|
+
`Unrecognized recipient for remote delivery: ${recipient.id}`
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
const fullActor = await this.#client.get(actor.id)
|
|
85
|
+
const fullRecipient = await this.#client.get(recipient.id)
|
|
86
|
+
if (await this.#isRemoteActor(fullRecipient)) {
|
|
87
|
+
this.#logger.warn(`Skipping remote actor ${recipient.id}`)
|
|
88
|
+
} else if (await this.#isRemoteFollowersCollection(fullActor, fullRecipient)) {
|
|
89
|
+
await this.#deliverRemoteFollowersCollection(activity, fullRecipient, fullActor, deliveredTo, bots)
|
|
90
|
+
} else if (await this.#isRemoteFollowingCollection(fullActor, fullRecipient)) {
|
|
91
|
+
await this.#deliverRemoteFollowingCollection(activity, fullRecipient, fullActor, deliveredTo, bots)
|
|
92
|
+
} else if (await this.#isRemoteCollection(fullRecipient)) {
|
|
93
|
+
await this.#deliverRemoteCollection(activity, fullRecipient, deliveredTo, bots)
|
|
94
|
+
} else {
|
|
95
|
+
this.#logger.warn(`Unrecognized recipient: ${recipient.id}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async #isRemoteFollowersCollection (actor, object) {
|
|
102
|
+
assert.strictEqual(typeof actor, 'object')
|
|
103
|
+
assert.strictEqual(typeof actor.id, 'string')
|
|
104
|
+
assert.strictEqual(typeof object, 'object')
|
|
105
|
+
assert.strictEqual(typeof object.id, 'string')
|
|
106
|
+
|
|
107
|
+
return (actor.followers?.first?.id === object.id)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async #isRemoteFollowingCollection (actor, object) {
|
|
111
|
+
assert.strictEqual(typeof actor, 'object')
|
|
112
|
+
assert.strictEqual(typeof actor.id, 'string')
|
|
113
|
+
assert.strictEqual(typeof object, 'object')
|
|
114
|
+
assert.strictEqual(typeof object.id, 'string')
|
|
115
|
+
|
|
116
|
+
return (actor.following?.first?.id === object.id)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async #getLocalFollowers (actor) {
|
|
120
|
+
return await this.#actorStorage.getUsernamesWith('following', actor)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async #getLocalFollowing (actor) {
|
|
124
|
+
return await this.#actorStorage.getUsernamesWith('followers', actor)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async #deliverLocalActor (activity, recipient, bots, deliveredTo) {
|
|
128
|
+
const { username } = this.#formatter.unformat(recipient.id)
|
|
129
|
+
if (!deliveredTo.has(username)) {
|
|
130
|
+
const bot = await BotMaker.makeBot(bots, username)
|
|
131
|
+
if (!bot) {
|
|
132
|
+
this.#logger.warn(`sharedInbox direct delivery for unknown bot ${username}`)
|
|
133
|
+
}
|
|
134
|
+
await this.deliverTo(activity, bot)
|
|
135
|
+
deliveredTo.add(username)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async #deliverRemoteFollowersCollection (activity, recipient, actor, deliveredTo, bots) {
|
|
140
|
+
const followers = await this.#getLocalFollowers(actor)
|
|
141
|
+
for (const username of followers) {
|
|
142
|
+
if (!deliveredTo.has(username)) {
|
|
143
|
+
const bot = await BotMaker.makeBot(bots, username)
|
|
144
|
+
if (!bot) {
|
|
145
|
+
this.#logger.warn(`sharedInbox direct delivery for unknown bot ${username}`)
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
await this.deliverTo(activity, bot)
|
|
149
|
+
deliveredTo.add(username)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async #deliverRemoteFollowingCollection (activity, recipient, actor, deliveredTo, bots) {
|
|
155
|
+
const following = await this.#getLocalFollowing(actor)
|
|
156
|
+
for (const username of following) {
|
|
157
|
+
if (!deliveredTo.has(username)) {
|
|
158
|
+
const bot = await BotMaker.makeBot(bots, username)
|
|
159
|
+
if (!bot) {
|
|
160
|
+
this.#logger.warn(`sharedInbox direct delivery for unknown bot ${username}`)
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
await this.deliverTo(activity, bot)
|
|
164
|
+
deliveredTo.add(username)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#isPublic (recipient) {
|
|
170
|
+
return PUBLIC.includes(recipient.id)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#isLocal (recipient) {
|
|
174
|
+
return this.#formatter.isLocal(recipient.id)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#isLocalActor (parts) {
|
|
178
|
+
return parts.username && !parts.collection && !parts.type
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#isLocalFollowersCollection (parts) {
|
|
182
|
+
return parts.username && (parts.collection === 'followers')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#isLocalFollowingCollection (parts) {
|
|
186
|
+
return parts.username && (parts.collection === 'following')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async #isRemoteCollection (recipient) {
|
|
190
|
+
assert.strictEqual(typeof recipient, 'object')
|
|
191
|
+
assert.strictEqual(typeof recipient.id, 'string')
|
|
192
|
+
|
|
193
|
+
return (Array.isArray(recipient.type))
|
|
194
|
+
? recipient.type.some(item => COLLECTION_TYPES.includes(item))
|
|
195
|
+
: COLLECTION_TYPES.includes(recipient.type)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async #isRemoteActor (recipient) {
|
|
199
|
+
assert.strictEqual(typeof recipient, 'object')
|
|
200
|
+
assert.strictEqual(typeof recipient.id, 'string')
|
|
201
|
+
|
|
202
|
+
return !!recipient.inbox?.first?.id
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async #deliverPublic (activity, bots) {
|
|
206
|
+
await Promise.all(Object.values(bots).map(bot => bot.onPublic(activity)))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async #deliverLocalFollowersCollection (activity, username, bots, deliveredTo) {
|
|
210
|
+
const id = this.#formatter.format({ username })
|
|
211
|
+
const followed = await as2.import({ id })
|
|
212
|
+
const followers = await this.#actorStorage.getUsernamesWith('following', followed)
|
|
213
|
+
for (const follower of followers) {
|
|
214
|
+
if (!deliveredTo.has(follower)) {
|
|
215
|
+
const bot = await BotMaker.makeBot(bots, follower)
|
|
216
|
+
if (!bot) {
|
|
217
|
+
this.#logger.warn(`sharedInbox delivery for unknown bot ${follower}`)
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
await this.deliverTo(activity, bot)
|
|
221
|
+
deliveredTo.add(follower)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async #deliverLocalFollowingCollection (activity, username, bots, deliveredTo) {
|
|
227
|
+
const id = this.#formatter.format({ username })
|
|
228
|
+
const following = await as2.import({ id })
|
|
229
|
+
const followeds = await this.#actorStorage.getUsernamesWith('followers', following)
|
|
230
|
+
for (const followed of followeds) {
|
|
231
|
+
if (!deliveredTo.has(followed)) {
|
|
232
|
+
const bot = await BotMaker.makeBot(bots, followed)
|
|
233
|
+
if (!bot) {
|
|
234
|
+
this.#logger.warn(`sharedInbox delivery for unknown bot ${followed}`)
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
await this.deliverTo(activity, bot)
|
|
238
|
+
deliveredTo.add(followed)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async #deliverRemoteCollection (activity, recipient, deliveredTo, bots) {
|
|
244
|
+
for await (const item of this.#client.items(recipient.id)) {
|
|
245
|
+
this.#logger.debug(`item: ${JSON.stringify(item)}`)
|
|
246
|
+
if (this.#isLocal(item)) {
|
|
247
|
+
const parts = this.#formatter.unformat(item.id)
|
|
248
|
+
if (this.#isLocalActor(parts)) {
|
|
249
|
+
const bot = await BotMaker.makeBot(bots, parts.username)
|
|
250
|
+
if (!bot) {
|
|
251
|
+
this.#logger.warn(`sharedInbox delivery for unknown bot ${parts.username}`)
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
await this.deliverTo(activity, bot)
|
|
255
|
+
deliveredTo.add(parts.username)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
package/lib/activityhandler.js
CHANGED
|
@@ -314,6 +314,11 @@ export class ActivityHandler {
|
|
|
314
314
|
object: activity,
|
|
315
315
|
to: actor
|
|
316
316
|
}))
|
|
317
|
+
this.#logger.debug({
|
|
318
|
+
msg: 'Notifying bot of new follow',
|
|
319
|
+
actor: actor.id
|
|
320
|
+
})
|
|
321
|
+
await bot.onFollow(actor, activity)
|
|
317
322
|
}
|
|
318
323
|
|
|
319
324
|
async #handleAccept (bot, activity) {
|
|
@@ -516,6 +521,7 @@ export class ActivityHandler {
|
|
|
516
521
|
}),
|
|
517
522
|
...recipients
|
|
518
523
|
}))
|
|
524
|
+
await bot.onLike(object, activity)
|
|
519
525
|
}
|
|
520
526
|
|
|
521
527
|
async #handleAnnounce (bot, activity) {
|
|
@@ -592,6 +598,14 @@ export class ActivityHandler {
|
|
|
592
598
|
}),
|
|
593
599
|
...recipients
|
|
594
600
|
}))
|
|
601
|
+
try {
|
|
602
|
+
await bot.onAnnounce(object, activity)
|
|
603
|
+
} catch (err) {
|
|
604
|
+
this.#logger.warn(
|
|
605
|
+
`Error notifying bot of announce: ${err.message}`,
|
|
606
|
+
{ err, username: bot.username, activity: activity.id }
|
|
607
|
+
)
|
|
608
|
+
}
|
|
595
609
|
}
|
|
596
610
|
|
|
597
611
|
async #handleBlock (bot, activity) {
|
package/lib/activitypubclient.js
CHANGED
|
@@ -13,6 +13,13 @@ const { version } = JSON.parse(
|
|
|
13
13
|
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
+
const NS = 'https://www.w3.org/ns/activitystreams#'
|
|
17
|
+
|
|
18
|
+
const COLLECTION_TYPES = [
|
|
19
|
+
`${NS}Collection`,
|
|
20
|
+
`${NS}OrderedCollection`
|
|
21
|
+
]
|
|
22
|
+
|
|
16
23
|
export class ActivityPubClient {
|
|
17
24
|
static #githubUrl = 'https://github.com/evanp/activitypub-bot'
|
|
18
25
|
static #userAgent = `activitypub.bot/${version} (${ActivityPubClient.#githubUrl})`
|
|
@@ -63,16 +70,20 @@ export class ActivityPubClient {
|
|
|
63
70
|
'user-agent': ActivityPubClient.#userAgent
|
|
64
71
|
}
|
|
65
72
|
const method = 'GET'
|
|
73
|
+
this.#logger.debug(`Signing GET request for ${url}`)
|
|
66
74
|
if (sign) {
|
|
67
75
|
headers.signature =
|
|
68
76
|
await this.#sign({ username, url, method, headers })
|
|
69
77
|
}
|
|
70
|
-
|
|
78
|
+
this.#logger.debug(`Fetching ${url} with GET`)
|
|
79
|
+
const result = await fetch(url,
|
|
71
80
|
{
|
|
72
81
|
method,
|
|
73
82
|
headers
|
|
74
83
|
}
|
|
75
84
|
)
|
|
85
|
+
this.#logger.debug(`Finished getting ${url}`)
|
|
86
|
+
return result
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
async #handleRes (res, url) {
|
|
@@ -100,7 +111,9 @@ export class ActivityPubClient {
|
|
|
100
111
|
}
|
|
101
112
|
const method = 'POST'
|
|
102
113
|
assert.ok(headers)
|
|
114
|
+
this.#logger.debug(`Signing POST for ${url}`)
|
|
103
115
|
headers.signature = await this.#sign({ username, url, method, headers })
|
|
116
|
+
this.#logger.debug(`Fetching POST for ${url}`)
|
|
104
117
|
const res = await fetch(url,
|
|
105
118
|
{
|
|
106
119
|
method,
|
|
@@ -108,6 +121,7 @@ export class ActivityPubClient {
|
|
|
108
121
|
body
|
|
109
122
|
}
|
|
110
123
|
)
|
|
124
|
+
this.#logger.debug(`Done fetching POST for ${url}`)
|
|
111
125
|
if (res.status < 200 || res.status > 299) {
|
|
112
126
|
throw createHttpError(res.status, await res.text())
|
|
113
127
|
}
|
|
@@ -123,4 +137,41 @@ export class ActivityPubClient {
|
|
|
123
137
|
: this.#urlFormatter.format({ server: true, type: 'publickey' })
|
|
124
138
|
return this.#signer.sign({ privateKey, keyId, url, method, headers })
|
|
125
139
|
}
|
|
140
|
+
|
|
141
|
+
#isCollection (obj) {
|
|
142
|
+
return (Array.isArray(obj.type))
|
|
143
|
+
? obj.type.some(item => COLLECTION_TYPES.includes(item))
|
|
144
|
+
: COLLECTION_TYPES.includes(obj.type)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async * items (id, username = null) {
|
|
148
|
+
const coll = await this.get(id, username)
|
|
149
|
+
|
|
150
|
+
this.#logger.debug(`Got object ${id}`)
|
|
151
|
+
|
|
152
|
+
if (!this.#isCollection(coll)) {
|
|
153
|
+
throw new Error(`Can only iterate over a collection: ${id}`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const items = (coll.items) ? coll.items : coll.orderedItems
|
|
157
|
+
|
|
158
|
+
if (items) {
|
|
159
|
+
for (const item of items) {
|
|
160
|
+
this.#logger.debug(`Yielding ${item.id}`)
|
|
161
|
+
yield item
|
|
162
|
+
}
|
|
163
|
+
} else if (coll.first) {
|
|
164
|
+
for (let page = coll.first; page; page = page.next) {
|
|
165
|
+
this.#logger.debug(`Getting page ${page.id}`)
|
|
166
|
+
page = await this.get(page.id)
|
|
167
|
+
const items = (page.items) ? page.items : page.orderedItems
|
|
168
|
+
if (items) {
|
|
169
|
+
for (const item of items) {
|
|
170
|
+
this.#logger.debug(`Yielding ${item.id}`)
|
|
171
|
+
yield item
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
126
177
|
}
|
package/lib/activitystreams.js
CHANGED
|
@@ -38,4 +38,35 @@ as2.registerContext('https://w3id.org/fep/5711', {
|
|
|
38
38
|
}
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
+
as2.registerContext('https://w3id.org/security/v1', {
|
|
42
|
+
'@context': {
|
|
43
|
+
sec: 'https://w3id.org/security#',
|
|
44
|
+
id: '@id',
|
|
45
|
+
type: '@type',
|
|
46
|
+
owner: {
|
|
47
|
+
'@id': 'sec:owner',
|
|
48
|
+
'@type': '@id'
|
|
49
|
+
},
|
|
50
|
+
publicKey: {
|
|
51
|
+
'@id': 'sec:publicKey',
|
|
52
|
+
'@type': '@id'
|
|
53
|
+
},
|
|
54
|
+
publicKeyPem: 'sec:publicKeyPem'
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
as2.registerContext('https://purl.archive.org/socialweb/thread/1.0', {
|
|
59
|
+
'@context': {
|
|
60
|
+
thr: 'https://purl.archive.org/socialweb/thread#',
|
|
61
|
+
thread: {
|
|
62
|
+
'@id': 'thr:thread',
|
|
63
|
+
'@type': '@id'
|
|
64
|
+
},
|
|
65
|
+
root: {
|
|
66
|
+
'@id': 'thr:root',
|
|
67
|
+
'@type': '@id'
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
41
72
|
export default as2
|
package/lib/actorstorage.js
CHANGED
|
@@ -10,34 +10,6 @@ export class ActorStorage {
|
|
|
10
10
|
this.#formatter = formatter
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
async initialize () {
|
|
14
|
-
await this.#connection.query(`
|
|
15
|
-
CREATE TABLE IF NOT EXISTS actorcollection (
|
|
16
|
-
username varchar(512) NOT NULL,
|
|
17
|
-
property varchar(512) NOT NULL,
|
|
18
|
-
first INTEGER NOT NULL,
|
|
19
|
-
totalItems INTEGER NOT NULL DEFAULT 0,
|
|
20
|
-
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
21
|
-
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
22
|
-
PRIMARY KEY (username, property)
|
|
23
|
-
);`
|
|
24
|
-
)
|
|
25
|
-
await this.#connection.query(`
|
|
26
|
-
CREATE TABLE IF NOT EXISTS actorcollectionpage (
|
|
27
|
-
username varchar(512) NOT NULL,
|
|
28
|
-
property varchar(512) NOT NULL,
|
|
29
|
-
item varchar(512) NOT NULL,
|
|
30
|
-
page INTEGER NOT NULL,
|
|
31
|
-
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
32
|
-
PRIMARY KEY (username, property, item)
|
|
33
|
-
);`
|
|
34
|
-
)
|
|
35
|
-
await this.#connection.query(
|
|
36
|
-
`CREATE INDEX IF NOT EXISTS actorcollectionpage_username_property_page
|
|
37
|
-
ON actorcollectionpage (username, property, page);`
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
13
|
async getActor (username, props = {}) {
|
|
42
14
|
assert.ok(username)
|
|
43
15
|
assert.equal(typeof username, 'string')
|
|
@@ -266,6 +238,24 @@ export class ActorStorage {
|
|
|
266
238
|
return page <= first
|
|
267
239
|
}
|
|
268
240
|
|
|
241
|
+
async getUsernamesWith (property, object) {
|
|
242
|
+
assert.ok(property, 'property is required')
|
|
243
|
+
assert.equal(typeof property, 'string', 'property must be a string')
|
|
244
|
+
assert.ok(object, 'object is required')
|
|
245
|
+
assert.equal(typeof object, 'object', 'object must be an object')
|
|
246
|
+
const [result] = await this.#connection.query(
|
|
247
|
+
`SELECT username
|
|
248
|
+
FROM actorcollectionpage
|
|
249
|
+
WHERE property = ? AND item = ?`,
|
|
250
|
+
{ replacements: [property, object.id] }
|
|
251
|
+
)
|
|
252
|
+
const usernames = []
|
|
253
|
+
for (const row of result) {
|
|
254
|
+
usernames.push(row.username)
|
|
255
|
+
}
|
|
256
|
+
return usernames
|
|
257
|
+
}
|
|
258
|
+
|
|
269
259
|
async #getCollectionInfo (username, property) {
|
|
270
260
|
const [result] = await this.#connection.query(
|
|
271
261
|
`SELECT first, totalItems, createdAt, updatedAt
|
package/lib/app.js
CHANGED
|
@@ -22,32 +22,33 @@ import collectionRouter from './routes/collection.js'
|
|
|
22
22
|
import inboxRouter from './routes/inbox.js'
|
|
23
23
|
import healthRouter from './routes/health.js'
|
|
24
24
|
import webfingerRouter from './routes/webfinger.js'
|
|
25
|
+
import sharedInboxRouter from './routes/sharedinbox.js'
|
|
25
26
|
import { BotContext } from './botcontext.js'
|
|
26
27
|
import { Transformer } from './microsyntax.js'
|
|
27
28
|
import { HTTPSignatureAuthenticator } from './httpsignatureauthenticator.js'
|
|
28
29
|
import { Digester } from './digester.js'
|
|
30
|
+
import { runMigrations } from './migrations/index.js'
|
|
31
|
+
import { ActivityDeliverer } from './activitydeliverer.js'
|
|
29
32
|
|
|
30
33
|
export async function makeApp (databaseUrl, origin, bots, logLevel = 'silent') {
|
|
31
34
|
const logger = Logger({
|
|
32
35
|
level: logLevel
|
|
33
36
|
})
|
|
34
37
|
logger.debug('Logger initialized')
|
|
35
|
-
const connection =
|
|
38
|
+
const connection = databaseUrl === 'sqlite::memory:' || databaseUrl === 'sqlite::memory'
|
|
39
|
+
? new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
|
40
|
+
: new Sequelize(databaseUrl, { logging: false })
|
|
41
|
+
await runMigrations(connection)
|
|
36
42
|
const formatter = new UrlFormatter(origin)
|
|
37
43
|
const signer = new HTTPSignature(logger)
|
|
38
44
|
const digester = new Digester(logger)
|
|
39
45
|
const actorStorage = new ActorStorage(connection, formatter)
|
|
40
|
-
await actorStorage.initialize()
|
|
41
46
|
const botDataStorage = new BotDataStorage(connection)
|
|
42
|
-
await botDataStorage.initialize()
|
|
43
47
|
const keyStorage = new KeyStorage(connection, logger)
|
|
44
|
-
await keyStorage.initialize()
|
|
45
48
|
const objectStorage = new ObjectStorage(connection)
|
|
46
|
-
await objectStorage.initialize()
|
|
47
49
|
const client =
|
|
48
50
|
new ActivityPubClient(keyStorage, formatter, signer, digester, logger)
|
|
49
51
|
const remoteKeyStorage = new RemoteKeyStorage(client, connection, logger)
|
|
50
|
-
await remoteKeyStorage.initialize()
|
|
51
52
|
const signature = new HTTPSignatureAuthenticator(remoteKeyStorage, signer, digester, logger)
|
|
52
53
|
const distributor = new ActivityDistributor(
|
|
53
54
|
client,
|
|
@@ -71,6 +72,14 @@ export async function makeApp (databaseUrl, origin, bots, logLevel = 'silent') {
|
|
|
71
72
|
logger,
|
|
72
73
|
client
|
|
73
74
|
)
|
|
75
|
+
const deliverer = new ActivityDeliverer(
|
|
76
|
+
actorStorage,
|
|
77
|
+
activityHandler,
|
|
78
|
+
formatter,
|
|
79
|
+
logger,
|
|
80
|
+
client
|
|
81
|
+
)
|
|
82
|
+
|
|
74
83
|
// TODO: Make an endpoint for tagged objects
|
|
75
84
|
const transformer = new Transformer(origin + '/tag/', client)
|
|
76
85
|
await Promise.all(
|
|
@@ -106,7 +115,8 @@ export async function makeApp (databaseUrl, origin, bots, logLevel = 'silent') {
|
|
|
106
115
|
authorizer,
|
|
107
116
|
bots,
|
|
108
117
|
activityHandler,
|
|
109
|
-
origin
|
|
118
|
+
origin,
|
|
119
|
+
deliverer
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
app.use(HTTPLogger({
|
|
@@ -134,6 +144,7 @@ export async function makeApp (databaseUrl, origin, bots, logLevel = 'silent') {
|
|
|
134
144
|
app.use('/', inboxRouter)
|
|
135
145
|
app.use('/', healthRouter)
|
|
136
146
|
app.use('/', webfingerRouter)
|
|
147
|
+
app.use('/', sharedInboxRouter)
|
|
137
148
|
|
|
138
149
|
app.use((err, req, res, next) => {
|
|
139
150
|
const { logger } = req.app.locals
|
package/lib/bot.js
CHANGED
|
@@ -7,6 +7,9 @@ export default class Bot {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
async initialize (context) {
|
|
10
|
+
if (context.botId !== this.#username) {
|
|
11
|
+
throw new Error(`Mismatched context: ${context.botId} !== ${this.#username}`)
|
|
12
|
+
}
|
|
10
13
|
this.#context = context
|
|
11
14
|
}
|
|
12
15
|
|
|
@@ -41,4 +44,8 @@ export default class Bot {
|
|
|
41
44
|
async onAnnounce (object, activity) {
|
|
42
45
|
; // no-op
|
|
43
46
|
}
|
|
47
|
+
|
|
48
|
+
async onPublic (activity) {
|
|
49
|
+
; // no-op
|
|
50
|
+
}
|
|
44
51
|
}
|
package/lib/botcontext.js
CHANGED
|
@@ -53,6 +53,22 @@ export class BotContext {
|
|
|
53
53
|
this.#logger = logger.child({ class: 'BotContext', botId })
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// copy constructor
|
|
57
|
+
|
|
58
|
+
async duplicate (username) {
|
|
59
|
+
return new this.constructor(
|
|
60
|
+
username,
|
|
61
|
+
this.#botDataStorage,
|
|
62
|
+
this.#objectStorage,
|
|
63
|
+
this.#actorStorage,
|
|
64
|
+
this.#client,
|
|
65
|
+
this.#distributor,
|
|
66
|
+
this.#formatter,
|
|
67
|
+
this.#transformer,
|
|
68
|
+
this.#logger
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
56
72
|
async setData (key, value) {
|
|
57
73
|
await this.#botDataStorage.set(this.#botId, key, value)
|
|
58
74
|
}
|
|
@@ -488,6 +504,40 @@ export class BotContext {
|
|
|
488
504
|
return `${username}@${actorUrl.hostname}`
|
|
489
505
|
}
|
|
490
506
|
|
|
507
|
+
async announceObject (obj) {
|
|
508
|
+
assert.ok(obj)
|
|
509
|
+
assert.equal(typeof obj, 'object')
|
|
510
|
+
const owners = obj.attributedTo
|
|
511
|
+
? Array.from(obj.attributedTo).map((owner) => owner.id)
|
|
512
|
+
: Array.from(obj.actor).map((owner) => owner.id)
|
|
513
|
+
const activity = await as2.import({
|
|
514
|
+
type: 'Announce',
|
|
515
|
+
id: this.#formatter.format({
|
|
516
|
+
username: this.#botId,
|
|
517
|
+
type: 'Announce',
|
|
518
|
+
nanoid: nanoid()
|
|
519
|
+
}),
|
|
520
|
+
actor: this.#formatter.format({ username: this.#botId }),
|
|
521
|
+
summary: {
|
|
522
|
+
en: `${this.#botId} shared "${await this.#nameOf(obj)}"`
|
|
523
|
+
},
|
|
524
|
+
object: obj.id,
|
|
525
|
+
to: [
|
|
526
|
+
this.#formatter.format({
|
|
527
|
+
username: this.#botId,
|
|
528
|
+
collection: 'followers'
|
|
529
|
+
}),
|
|
530
|
+
'https://www.w3.org/ns/activitystreams#Public'
|
|
531
|
+
],
|
|
532
|
+
cc: owners
|
|
533
|
+
})
|
|
534
|
+
await this.#objectStorage.create(activity)
|
|
535
|
+
await this.#actorStorage.addToCollection(this.#botId, 'outbox', activity)
|
|
536
|
+
await this.#actorStorage.addToCollection(this.#botId, 'inbox', activity)
|
|
537
|
+
await this.#distributor.distribute(activity, this.#botId)
|
|
538
|
+
return activity
|
|
539
|
+
}
|
|
540
|
+
|
|
491
541
|
async #findInOutbox (type, obj) {
|
|
492
542
|
const full = `https://www.w3.org/ns/activitystreams#${type}`
|
|
493
543
|
let found = null
|
|
@@ -514,6 +564,18 @@ export class BotContext {
|
|
|
514
564
|
return { to, cc, bto, bcc, audience }
|
|
515
565
|
}
|
|
516
566
|
|
|
567
|
+
#nameOf (obj) {
|
|
568
|
+
if (obj.name) {
|
|
569
|
+
return obj.name.valueOf()
|
|
570
|
+
} else if (obj.summary) {
|
|
571
|
+
return obj.summary.valueOf()
|
|
572
|
+
} else if (obj.type) {
|
|
573
|
+
return `a(n) ${obj.type.first}`
|
|
574
|
+
} else {
|
|
575
|
+
return 'an object'
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
517
579
|
async onIdle () {
|
|
518
580
|
await this.#distributor.onIdle()
|
|
519
581
|
}
|
package/lib/botdatastorage.js
CHANGED
|
@@ -15,19 +15,6 @@ export class BotDataStorage {
|
|
|
15
15
|
this.#connection = connection
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
async initialize () {
|
|
19
|
-
await this.#connection.query(`
|
|
20
|
-
CREATE TABLE IF NOT EXISTS botdata (
|
|
21
|
-
username VARCHAR(512) not null,
|
|
22
|
-
key VARCHAR(512) not null,
|
|
23
|
-
value TEXT not null,
|
|
24
|
-
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
25
|
-
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
26
|
-
PRIMARY KEY (username, key)
|
|
27
|
-
)
|
|
28
|
-
`)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
18
|
async terminate () {
|
|
32
19
|
|
|
33
20
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export default class BotFactory {
|
|
2
|
+
#context
|
|
3
|
+
|
|
4
|
+
async initialize (context) {
|
|
5
|
+
this.#context = context
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
get _context () {
|
|
9
|
+
return this.#context
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async canCreate (username) {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async create (username) {
|
|
17
|
+
const name = this.constructor.name
|
|
18
|
+
throw new Error(`${name} class can't create bot named "${username}"`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async onPublic (activity) {
|
|
22
|
+
; // no-op
|
|
23
|
+
}
|
|
24
|
+
}
|