@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.
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/main-docker.yml +45 -0
- package/.github/workflows/tag-docker.yml +54 -0
- package/Dockerfile +23 -0
- package/LICENSE +661 -0
- package/README.md +82 -0
- package/bots/index.js +7 -0
- package/docs/activitypub.bot.drawio +110 -0
- package/index.js +23 -0
- package/lib/activitydistributor.js +263 -0
- package/lib/activityhandler.js +999 -0
- package/lib/activitypubclient.js +126 -0
- package/lib/activitystreams.js +41 -0
- package/lib/actorstorage.js +300 -0
- package/lib/app.js +173 -0
- package/lib/authorizer.js +133 -0
- package/lib/bot.js +44 -0
- package/lib/botcontext.js +520 -0
- package/lib/botdatastorage.js +87 -0
- package/lib/bots/donothing.js +11 -0
- package/lib/bots/ok.js +41 -0
- package/lib/digester.js +23 -0
- package/lib/httpsignature.js +195 -0
- package/lib/httpsignatureauthenticator.js +81 -0
- package/lib/keystorage.js +113 -0
- package/lib/microsyntax.js +140 -0
- package/lib/objectcache.js +48 -0
- package/lib/objectstorage.js +319 -0
- package/lib/remotekeystorage.js +116 -0
- package/lib/routes/collection.js +92 -0
- package/lib/routes/health.js +24 -0
- package/lib/routes/inbox.js +83 -0
- package/lib/routes/object.js +69 -0
- package/lib/routes/server.js +47 -0
- package/lib/routes/user.js +63 -0
- package/lib/routes/webfinger.js +36 -0
- package/lib/urlformatter.js +97 -0
- package/package.json +51 -0
- package/tests/activitydistributor.test.js +606 -0
- package/tests/activityhandler.test.js +2185 -0
- package/tests/activitypubclient.test.js +225 -0
- package/tests/actorstorage.test.js +261 -0
- package/tests/app.test.js +17 -0
- package/tests/authorizer.test.js +306 -0
- package/tests/bot.donothing.test.js +30 -0
- package/tests/bot.ok.test.js +101 -0
- package/tests/botcontext.test.js +674 -0
- package/tests/botdatastorage.test.js +87 -0
- package/tests/digester.test.js +56 -0
- package/tests/fixtures/bots.js +15 -0
- package/tests/httpsignature.test.js +200 -0
- package/tests/httpsignatureauthenticator.test.js +463 -0
- package/tests/keystorage.test.js +89 -0
- package/tests/microsyntax.test.js +122 -0
- package/tests/objectcache.test.js +133 -0
- package/tests/objectstorage.test.js +148 -0
- package/tests/remotekeystorage.test.js +76 -0
- package/tests/routes.actor.test.js +207 -0
- package/tests/routes.collection.test.js +434 -0
- package/tests/routes.health.test.js +41 -0
- package/tests/routes.inbox.test.js +135 -0
- package/tests/routes.object.test.js +519 -0
- package/tests/routes.server.test.js +69 -0
- package/tests/routes.webfinger.test.js +41 -0
- package/tests/urlformatter.test.js +164 -0
- package/tests/utils/digest.js +7 -0
- package/tests/utils/nock.js +276 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { UrlFormatter } from '../lib/urlformatter.js'
|
|
4
|
+
|
|
5
|
+
describe('UrlFormatter', () => {
|
|
6
|
+
const origin = 'https://activitypubbot.example'
|
|
7
|
+
let formatter = null
|
|
8
|
+
it('can initialize', () => {
|
|
9
|
+
formatter = new UrlFormatter(origin)
|
|
10
|
+
})
|
|
11
|
+
it('can format a user URL', () => {
|
|
12
|
+
const url = formatter.format({ username: 'megabot' })
|
|
13
|
+
assert.equal(url, 'https://activitypubbot.example/user/megabot')
|
|
14
|
+
})
|
|
15
|
+
it('can format a public key URL', () => {
|
|
16
|
+
const url = formatter.format({ username: 'megabot', type: 'publickey' })
|
|
17
|
+
assert.equal(url, 'https://activitypubbot.example/user/megabot/publickey')
|
|
18
|
+
})
|
|
19
|
+
it('can format an inbox URL', () => {
|
|
20
|
+
const url = formatter.format({ username: 'megabot', collection: 'inbox' })
|
|
21
|
+
assert.equal(url, 'https://activitypubbot.example/user/megabot/inbox')
|
|
22
|
+
})
|
|
23
|
+
it('can format an inbox URL page', () => {
|
|
24
|
+
const url = formatter.format({
|
|
25
|
+
username: 'megabot',
|
|
26
|
+
collection: 'inbox',
|
|
27
|
+
page: 3
|
|
28
|
+
})
|
|
29
|
+
assert.equal(url, 'https://activitypubbot.example/user/megabot/inbox/3')
|
|
30
|
+
})
|
|
31
|
+
it('can format an activity URL', () => {
|
|
32
|
+
const url = formatter.format({
|
|
33
|
+
username: 'megabot',
|
|
34
|
+
type: 'like',
|
|
35
|
+
nanoid: 'LNPUlv9kmvhAdr4eoqkil'
|
|
36
|
+
})
|
|
37
|
+
assert.equal(url, 'https://activitypubbot.example/user/megabot/like/LNPUlv9kmvhAdr4eoqkil')
|
|
38
|
+
})
|
|
39
|
+
it('can format a note URL', () => {
|
|
40
|
+
const url = formatter.format({
|
|
41
|
+
username: 'megabot',
|
|
42
|
+
type: 'note',
|
|
43
|
+
nanoid: 'LNPUlv9kmvhAdr4eoqkil'
|
|
44
|
+
})
|
|
45
|
+
assert.equal(url, 'https://activitypubbot.example/user/megabot/note/LNPUlv9kmvhAdr4eoqkil')
|
|
46
|
+
})
|
|
47
|
+
it('can format a note replies URL', () => {
|
|
48
|
+
const url = formatter.format({
|
|
49
|
+
username: 'megabot',
|
|
50
|
+
type: 'note',
|
|
51
|
+
nanoid: 'LNPUlv9kmvhAdr4eoqkil',
|
|
52
|
+
collection: 'replies'
|
|
53
|
+
})
|
|
54
|
+
assert.equal(url, 'https://activitypubbot.example/user/megabot/note/LNPUlv9kmvhAdr4eoqkil/replies')
|
|
55
|
+
})
|
|
56
|
+
it('can format a note replies page URL', () => {
|
|
57
|
+
const url = formatter.format({
|
|
58
|
+
username: 'megabot',
|
|
59
|
+
type: 'note',
|
|
60
|
+
nanoid: 'LNPUlv9kmvhAdr4eoqkil',
|
|
61
|
+
collection: 'replies',
|
|
62
|
+
page: 4
|
|
63
|
+
})
|
|
64
|
+
assert.equal(url, 'https://activitypubbot.example/user/megabot/note/LNPUlv9kmvhAdr4eoqkil/replies/4')
|
|
65
|
+
})
|
|
66
|
+
it('can format a server URL', () => {
|
|
67
|
+
const url = formatter.format({
|
|
68
|
+
server: true
|
|
69
|
+
})
|
|
70
|
+
assert.equal(url, 'https://activitypubbot.example/')
|
|
71
|
+
})
|
|
72
|
+
it('can format a server public key URL', () => {
|
|
73
|
+
const url = formatter.format({
|
|
74
|
+
server: true,
|
|
75
|
+
type: 'publickey'
|
|
76
|
+
})
|
|
77
|
+
assert.equal(url, 'https://activitypubbot.example/publickey')
|
|
78
|
+
})
|
|
79
|
+
it('can tell if an URL is local', () => {
|
|
80
|
+
assert.ok(formatter.isLocal('https://activitypubbot.example/user/megabot'))
|
|
81
|
+
assert.ok(!formatter.isLocal('https://social.example/user/megabot'))
|
|
82
|
+
})
|
|
83
|
+
it('can get a username from a user URL', () => {
|
|
84
|
+
const username = formatter.getUserName('https://activitypubbot.example/user/megabot')
|
|
85
|
+
assert.equal(username, 'megabot')
|
|
86
|
+
})
|
|
87
|
+
it('refuses to unformat a remote URL', () => {
|
|
88
|
+
assert.throws(() => formatter.unformat(
|
|
89
|
+
'https://remote.example/some/unrelated/33/path/format'
|
|
90
|
+
))
|
|
91
|
+
})
|
|
92
|
+
it('can unformat a user URL', () => {
|
|
93
|
+
const parts = formatter.unformat(
|
|
94
|
+
'https://activitypubbot.example/user/megabot'
|
|
95
|
+
)
|
|
96
|
+
assert.equal(parts.username, 'megabot')
|
|
97
|
+
})
|
|
98
|
+
it('can unformat a public key URL', () => {
|
|
99
|
+
const parts = formatter.unformat(
|
|
100
|
+
'https://activitypubbot.example/user/megabot/publickey'
|
|
101
|
+
)
|
|
102
|
+
assert.equal(parts.username, 'megabot')
|
|
103
|
+
assert.equal(parts.type, 'publickey')
|
|
104
|
+
})
|
|
105
|
+
it('can unformat an inbox URL', () => {
|
|
106
|
+
const parts = formatter.unformat(
|
|
107
|
+
'https://activitypubbot.example/user/megabot/inbox'
|
|
108
|
+
)
|
|
109
|
+
assert.equal(parts.username, 'megabot')
|
|
110
|
+
assert.equal(parts.collection, 'inbox')
|
|
111
|
+
})
|
|
112
|
+
it('can unformat an inbox page URL', () => {
|
|
113
|
+
const parts = formatter.unformat(
|
|
114
|
+
'https://activitypubbot.example/user/megabot/inbox/3'
|
|
115
|
+
)
|
|
116
|
+
assert.equal(parts.username, 'megabot')
|
|
117
|
+
assert.equal(parts.collection, 'inbox')
|
|
118
|
+
assert.equal(parts.page, 3)
|
|
119
|
+
})
|
|
120
|
+
it('can unformat an activity URL', () => {
|
|
121
|
+
const parts = formatter.unformat(
|
|
122
|
+
'https://activitypubbot.example/user/megabot/like/LNPUlv9kmvhAdr4eoqkil'
|
|
123
|
+
)
|
|
124
|
+
assert.equal(parts.username, 'megabot')
|
|
125
|
+
assert.equal(parts.type, 'like')
|
|
126
|
+
assert.equal(parts.nanoid, 'LNPUlv9kmvhAdr4eoqkil')
|
|
127
|
+
})
|
|
128
|
+
it('can unformat a note URL', () => {
|
|
129
|
+
const parts = formatter.unformat(
|
|
130
|
+
'https://activitypubbot.example/user/megabot/note/LNPUlv9kmvhAdr4eoqkil'
|
|
131
|
+
)
|
|
132
|
+
assert.equal(parts.username, 'megabot')
|
|
133
|
+
assert.equal(parts.type, 'note')
|
|
134
|
+
assert.equal(parts.nanoid, 'LNPUlv9kmvhAdr4eoqkil')
|
|
135
|
+
})
|
|
136
|
+
it('can unformat a note replies URL', () => {
|
|
137
|
+
const parts = formatter.unformat(
|
|
138
|
+
'https://activitypubbot.example/user/megabot/note/LNPUlv9kmvhAdr4eoqkil/replies'
|
|
139
|
+
)
|
|
140
|
+
assert.equal(parts.username, 'megabot')
|
|
141
|
+
assert.equal(parts.type, 'note')
|
|
142
|
+
assert.equal(parts.nanoid, 'LNPUlv9kmvhAdr4eoqkil')
|
|
143
|
+
assert.equal(parts.collection, 'replies')
|
|
144
|
+
})
|
|
145
|
+
it('can unformat a note replies page URL', () => {
|
|
146
|
+
const parts = formatter.unformat(
|
|
147
|
+
'https://activitypubbot.example/user/megabot/note/LNPUlv9kmvhAdr4eoqkil/replies/4'
|
|
148
|
+
)
|
|
149
|
+
assert.equal(parts.username, 'megabot')
|
|
150
|
+
assert.equal(parts.type, 'note')
|
|
151
|
+
assert.equal(parts.nanoid, 'LNPUlv9kmvhAdr4eoqkil')
|
|
152
|
+
assert.equal(parts.collection, 'replies')
|
|
153
|
+
assert.equal(parts.page, 4)
|
|
154
|
+
})
|
|
155
|
+
it('can unformat a server URL', () => {
|
|
156
|
+
const parts = formatter.unformat('https://activitypubbot.example/')
|
|
157
|
+
assert.ok(parts.server)
|
|
158
|
+
})
|
|
159
|
+
it('can unformat a server public key URL', () => {
|
|
160
|
+
const parts = formatter.unformat('https://activitypubbot.example/publickey')
|
|
161
|
+
assert.ok(parts.server)
|
|
162
|
+
assert.equal(parts.type, 'publickey')
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import as2 from '../../lib/activitystreams.js'
|
|
2
|
+
import nock from 'nock'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { promisify } from 'node:util'
|
|
5
|
+
|
|
6
|
+
const generateKeyPair = promisify(crypto.generateKeyPair)
|
|
7
|
+
|
|
8
|
+
const domains = new Map()
|
|
9
|
+
domains['social.example'] = new Map()
|
|
10
|
+
|
|
11
|
+
const newKeyPair = async () => {
|
|
12
|
+
return await generateKeyPair(
|
|
13
|
+
'rsa',
|
|
14
|
+
{
|
|
15
|
+
modulusLength: 2048,
|
|
16
|
+
privateKeyEncoding: {
|
|
17
|
+
type: 'pkcs8',
|
|
18
|
+
format: 'pem'
|
|
19
|
+
},
|
|
20
|
+
publicKeyEncoding: {
|
|
21
|
+
type: 'spki',
|
|
22
|
+
format: 'pem'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const getPair = async (username, domain = 'social.example') => {
|
|
29
|
+
if (!domains.has(domain)) {
|
|
30
|
+
domains.set(domain, new Map())
|
|
31
|
+
}
|
|
32
|
+
if (!domains.get(domain).has(username)) {
|
|
33
|
+
const pair = await newKeyPair(username)
|
|
34
|
+
domains.get(domain).set(username, pair)
|
|
35
|
+
}
|
|
36
|
+
return domains.get(domain).get(username)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const getPublicKey = async (username, domain = 'social.example') => {
|
|
40
|
+
const pair = await getPair(username, domain)
|
|
41
|
+
return pair.publicKey
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const getPrivateKey = async (username, domain = 'social.example') => {
|
|
45
|
+
const pair = await getPair(username, domain)
|
|
46
|
+
return pair.privateKey
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const nockSignature = async ({ method = 'GET', url, date, digest = null, username, domain = 'social.example' }) => {
|
|
50
|
+
const privateKey = await getPrivateKey(username, domain)
|
|
51
|
+
const keyId = nockFormat({ username, key: true, domain })
|
|
52
|
+
const parsed = new URL(url)
|
|
53
|
+
const target = (parsed.search && parsed.search.length)
|
|
54
|
+
? `${parsed.pathname}${parsed.search}`
|
|
55
|
+
: `${parsed.pathname}`
|
|
56
|
+
let data = `(request-target): ${method.toLowerCase()} ${target}\n`
|
|
57
|
+
data += `host: ${parsed.host}\n`
|
|
58
|
+
data += `date: ${date}`
|
|
59
|
+
if (digest) {
|
|
60
|
+
data += `\ndigest: ${digest}`
|
|
61
|
+
}
|
|
62
|
+
const signer = crypto.createSign('sha256')
|
|
63
|
+
signer.update(data)
|
|
64
|
+
const signature = signer.sign(privateKey).toString('base64')
|
|
65
|
+
signer.end()
|
|
66
|
+
return `keyId="${keyId}",headers="(request-target) host date${(digest) ? ' digest' : ''}",signature="${signature.replace(/"/g, '\\"')}",algorithm="rsa-sha256"`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const nockSignatureFragment = async ({ method = 'GET', url, date, digest = null, username, domain = 'social.example' }) => {
|
|
70
|
+
const keyId = nockFormat({ username, domain }) + '#main-key'
|
|
71
|
+
const privateKey = await getPrivateKey(username, domain)
|
|
72
|
+
const parsed = new URL(url)
|
|
73
|
+
const target = (parsed.search && parsed.search.length)
|
|
74
|
+
? `${parsed.pathname}?${parsed.search}`
|
|
75
|
+
: `${parsed.pathname}`
|
|
76
|
+
let data = `(request-target): ${method.toLowerCase()} ${target}\n`
|
|
77
|
+
data += `host: ${parsed.host}\n`
|
|
78
|
+
data += `date: ${date}`
|
|
79
|
+
if (digest) {
|
|
80
|
+
data += `\ndigest: ${digest}`
|
|
81
|
+
}
|
|
82
|
+
const signer = crypto.createSign('sha256')
|
|
83
|
+
signer.update(data)
|
|
84
|
+
const signature = signer.sign(privateKey).toString('base64')
|
|
85
|
+
signer.end()
|
|
86
|
+
return `keyId="${keyId}",headers="(request-target) host date${(digest) ? ' digest' : ''}",signature="${signature.replace(/"/g, '\\"')}",algorithm="rsa-sha256"`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const nockKeyRotate = async (username, domain = 'social.example') =>
|
|
90
|
+
domains.get(domain).set(username, await newKeyPair(username))
|
|
91
|
+
|
|
92
|
+
export const makeActor = async (username, domain = 'social.example') =>
|
|
93
|
+
await as2.import({
|
|
94
|
+
'@context': [
|
|
95
|
+
'https://www.w3.org/ns/activitystreams',
|
|
96
|
+
'https://w3id.org/security/v1'
|
|
97
|
+
],
|
|
98
|
+
id: `https://${domain}/user/${username}`,
|
|
99
|
+
type: 'Person',
|
|
100
|
+
preferredUsername: username,
|
|
101
|
+
inbox: `https://${domain}/user/${username}/inbox`,
|
|
102
|
+
outbox: `https://${domain}/user/${username}/outbox`,
|
|
103
|
+
followers: `https://${domain}/user/${username}/followers`,
|
|
104
|
+
following: `https://${domain}/user/${username}/following`,
|
|
105
|
+
liked: `https://${domain}/user/${username}/liked`,
|
|
106
|
+
to: ['as:Public'],
|
|
107
|
+
publicKey: {
|
|
108
|
+
id: `https://${domain}/user/${username}/publickey`,
|
|
109
|
+
type: 'CryptographicKey',
|
|
110
|
+
owner: `https://${domain}/user/${username}`,
|
|
111
|
+
publicKeyPem: await getPublicKey(username, domain)
|
|
112
|
+
},
|
|
113
|
+
url: {
|
|
114
|
+
type: 'Link',
|
|
115
|
+
href: `https://${domain}/profile/${username}`,
|
|
116
|
+
mediaType: 'text/html'
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Just the types we use here
|
|
121
|
+
const isActivityType = (type) => ['Create', 'Update', 'Delete', 'Add', 'Remove', 'Follow', 'Accept', 'Reject', 'Like', 'Block', 'Flag', 'Undo'].includes(uppercase(type))
|
|
122
|
+
|
|
123
|
+
export async function makeObject (
|
|
124
|
+
username,
|
|
125
|
+
type,
|
|
126
|
+
num,
|
|
127
|
+
domain = 'social.example') {
|
|
128
|
+
const props = {
|
|
129
|
+
'@context': [
|
|
130
|
+
'https://www.w3.org/ns/activitystreams',
|
|
131
|
+
'https://purl.archive.org/socialweb/thread/1.0',
|
|
132
|
+
{ ostatus: 'http://ostatus.org/schema/1.0/' }
|
|
133
|
+
],
|
|
134
|
+
id: nockFormat({ username, type, num, domain }),
|
|
135
|
+
type: uppercase(type),
|
|
136
|
+
to: 'as:Public'
|
|
137
|
+
}
|
|
138
|
+
if (isActivityType(type)) {
|
|
139
|
+
props.actor = nockFormat({ username, domain })
|
|
140
|
+
} else {
|
|
141
|
+
props.attributedTo = nockFormat({ username, domain })
|
|
142
|
+
props.replies = nockFormat({ username, type, num, domain, obj: 'replies' })
|
|
143
|
+
props.shares = nockFormat({ username, type, num, domain, obj: 'shares' })
|
|
144
|
+
props.likes = nockFormat({ username, type, num, domain, obj: 'likes' })
|
|
145
|
+
props.thread = nockFormat({ username, type, num, domain, obj: 'thread' })
|
|
146
|
+
props.context = nockFormat({ username, type, num, domain, obj: 'context' })
|
|
147
|
+
props['ostatus:conversation'] = nockFormat({ username, type, num, domain, obj: 'conversation' })
|
|
148
|
+
}
|
|
149
|
+
return as2.import(props)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const makeTransitive = (username, type, num, obj, domain = 'social.example') =>
|
|
153
|
+
as2.import({
|
|
154
|
+
id: nockFormat({ username, type, num, obj, domain }),
|
|
155
|
+
type: uppercase(type),
|
|
156
|
+
to: 'as:Public',
|
|
157
|
+
actor: nockFormat({ username, domain }),
|
|
158
|
+
object: `https://${obj}`
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const uppercase = (str) => str.charAt(0).toUpperCase() + str.slice(1)
|
|
162
|
+
const lowercase = (str) => str.charAt(0).toLowerCase() + str.slice(1)
|
|
163
|
+
|
|
164
|
+
export const postInbox = {}
|
|
165
|
+
|
|
166
|
+
export const resetInbox = () => {
|
|
167
|
+
for (const username in postInbox) {
|
|
168
|
+
postInbox[username] = 0
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const nockSetup = (domain) =>
|
|
173
|
+
nock(`https://${domain}`)
|
|
174
|
+
.get(/^\/.well-known\/webfinger/)
|
|
175
|
+
.reply(async (uri, requestBody) => {
|
|
176
|
+
const parsed = new URL(uri, `https://${domain}`)
|
|
177
|
+
const resource = parsed.searchParams.get('resource')
|
|
178
|
+
if (!resource) {
|
|
179
|
+
return [400, 'Bad Request']
|
|
180
|
+
}
|
|
181
|
+
const username = resource.slice(5).split('@')[0]
|
|
182
|
+
const webfinger = {
|
|
183
|
+
subject: resource,
|
|
184
|
+
links: [
|
|
185
|
+
{
|
|
186
|
+
rel: 'self',
|
|
187
|
+
type: 'application/activity+json',
|
|
188
|
+
href: `https://${domain}/user/${username}`
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
return [200,
|
|
193
|
+
JSON.stringify(webfinger),
|
|
194
|
+
{ 'Content-Type': 'application/jrd+json' }]
|
|
195
|
+
})
|
|
196
|
+
.get(/^\/user\/(\w+)$/)
|
|
197
|
+
.reply(async (uri, requestBody) => {
|
|
198
|
+
const username = uri.match(/^\/user\/(\w+)$/)[1]
|
|
199
|
+
const actor = await makeActor(username, domain)
|
|
200
|
+
const actorText = await actor.prettyWrite(
|
|
201
|
+
{ additional_context: 'https://w3id.org/security/v1' }
|
|
202
|
+
)
|
|
203
|
+
return [200, actorText, { 'Content-Type': 'application/activity+json' }]
|
|
204
|
+
})
|
|
205
|
+
.persist()
|
|
206
|
+
.post(/^\/user\/(\w+)\/inbox$/)
|
|
207
|
+
.reply(async (uri, requestBody) => {
|
|
208
|
+
const username = uri.match(/^\/user\/(\w+)\/inbox$/)[1]
|
|
209
|
+
if (username in postInbox) {
|
|
210
|
+
postInbox[username] += 1
|
|
211
|
+
} else {
|
|
212
|
+
postInbox[username] = 1
|
|
213
|
+
}
|
|
214
|
+
return [202, 'accepted']
|
|
215
|
+
})
|
|
216
|
+
.persist()
|
|
217
|
+
.get(/^\/user\/(\w+)\/publickey$/)
|
|
218
|
+
.reply(async (uri, requestBody) => {
|
|
219
|
+
const username = uri.match(/^\/user\/(\w+)\/publickey$/)[1]
|
|
220
|
+
const publicKey = await as2.import({
|
|
221
|
+
'@context': [
|
|
222
|
+
'https://www.w3.org/ns/activitystreams',
|
|
223
|
+
'https://w3id.org/security/v1'
|
|
224
|
+
],
|
|
225
|
+
id: `https://${domain}/user/${username}/publickey`,
|
|
226
|
+
owner: `https://${domain}/user/${username}`,
|
|
227
|
+
type: 'CryptographicKey',
|
|
228
|
+
publicKeyPem: await getPublicKey(username, domain)
|
|
229
|
+
})
|
|
230
|
+
const publicKeyText = await publicKey.prettyWrite(
|
|
231
|
+
{ additional_context: 'https://w3id.org/security/v1' }
|
|
232
|
+
)
|
|
233
|
+
return [200, publicKeyText, { 'Content-Type': 'application/activity+json' }]
|
|
234
|
+
})
|
|
235
|
+
.persist()
|
|
236
|
+
.get(/^\/user\/(\w+)\/(\w+)\/(\d+)$/)
|
|
237
|
+
.reply(async (uri, requestBody) => {
|
|
238
|
+
const match = uri.match(/^\/user\/(\w+)\/(\w+)\/(\d+)$/)
|
|
239
|
+
const username = match[1]
|
|
240
|
+
const type = uppercase(match[2])
|
|
241
|
+
const num = match[3]
|
|
242
|
+
const obj = await makeObject(username, type, num, domain)
|
|
243
|
+
const objText = await obj.prettyWrite({ useOriginalContext: true })
|
|
244
|
+
return [200, objText, { 'Content-Type': 'application/activity+json' }]
|
|
245
|
+
})
|
|
246
|
+
.persist()
|
|
247
|
+
.get(/^\/user\/(\w+)\/(\w+)\/(\d+)\/(.*)$/)
|
|
248
|
+
.reply(async (uri, requestBody) => {
|
|
249
|
+
const match = uri.match(/^\/user\/(\w+)\/(\w+)\/(\d+)\/(.*)$/)
|
|
250
|
+
const username = match[1]
|
|
251
|
+
const type = match[2]
|
|
252
|
+
const num = match[3]
|
|
253
|
+
const obj = match[4]
|
|
254
|
+
const act = await makeTransitive(username, type, num, obj, domain)
|
|
255
|
+
const actText = await act.write()
|
|
256
|
+
return [200, actText, { 'Content-Type': 'application/activity+json' }]
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
export function nockFormat ({ username, type, num, obj, key, domain = 'social.example' }) {
|
|
260
|
+
let url = `https://${domain}/user/${username}`
|
|
261
|
+
if (key) {
|
|
262
|
+
url = `${url}/publickey`
|
|
263
|
+
} else {
|
|
264
|
+
if (type && num) {
|
|
265
|
+
url = `${url}/${lowercase(type)}/${num}`
|
|
266
|
+
if (obj) {
|
|
267
|
+
if (obj.startsWith('https://')) {
|
|
268
|
+
url = `${url}/${obj.slice(8)}`
|
|
269
|
+
} else {
|
|
270
|
+
url = `${url}/${obj}`
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return url
|
|
276
|
+
}
|