@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
package/lib/botmaker.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
|
|
3
|
+
export default class BotMaker {
|
|
4
|
+
static async makeBot (bots, username) {
|
|
5
|
+
assert.ok(bots)
|
|
6
|
+
assert.ok(typeof bots === 'object')
|
|
7
|
+
assert.ok(username)
|
|
8
|
+
assert.ok(typeof username === 'string')
|
|
9
|
+
|
|
10
|
+
let bot
|
|
11
|
+
|
|
12
|
+
if (username in bots) {
|
|
13
|
+
bot = bots[username]
|
|
14
|
+
} else if ('*' in bots) {
|
|
15
|
+
const factory = bots['*']
|
|
16
|
+
if (await factory.canCreate(username)) {
|
|
17
|
+
bot = await factory.create(username)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return bot
|
|
22
|
+
}
|
|
23
|
+
}
|
package/lib/keystorage.js
CHANGED
|
@@ -17,27 +17,6 @@ export class KeyStorage {
|
|
|
17
17
|
this.#hasher = new HumanHasher()
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
async initialize () {
|
|
21
|
-
await this.#connection.query(`
|
|
22
|
-
CREATE TABLE IF NOT EXISTS new_keys (
|
|
23
|
-
username varchar(512) PRIMARY KEY,
|
|
24
|
-
public_key TEXT,
|
|
25
|
-
private_key TEXT
|
|
26
|
-
)
|
|
27
|
-
`)
|
|
28
|
-
try {
|
|
29
|
-
await this.#connection.query(`
|
|
30
|
-
INSERT OR IGNORE INTO new_keys (username, public_key, private_key)
|
|
31
|
-
SELECT bot_id, public_key, private_key
|
|
32
|
-
FROM keys
|
|
33
|
-
`)
|
|
34
|
-
} catch (error) {
|
|
35
|
-
this.#logger.debug(
|
|
36
|
-
{ error, method: 'KeyStorage.initialize' },
|
|
37
|
-
'failed to copy keys to new_keys table')
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
20
|
async getPublicKey (username) {
|
|
42
21
|
this.#logger.debug(
|
|
43
22
|
{ username, method: 'KeyStorage.getPublicKey' },
|
|
@@ -57,9 +36,13 @@ export class KeyStorage {
|
|
|
57
36
|
async #getKeys (username) {
|
|
58
37
|
let privateKey
|
|
59
38
|
let publicKey
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
39
|
+
const qry = (username)
|
|
40
|
+
? 'SELECT public_key, private_key FROM new_keys WHERE username = ?'
|
|
41
|
+
: 'SELECT public_key, private_key FROM new_keys WHERE username IS NULL'
|
|
42
|
+
const [result] = await this.#connection.query(
|
|
43
|
+
qry,
|
|
44
|
+
{ replacements: [username] }
|
|
45
|
+
)
|
|
63
46
|
if (result.length > 0) {
|
|
64
47
|
this.#logger.debug(
|
|
65
48
|
{ username, method: 'KeyStorage.#getKeys' },
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export const id = '001-initial'
|
|
2
|
+
|
|
3
|
+
export async function up (connection) {
|
|
4
|
+
await connection.query(`
|
|
5
|
+
CREATE TABLE IF NOT EXISTS actorcollection (
|
|
6
|
+
username varchar(512) NOT NULL,
|
|
7
|
+
property varchar(512) NOT NULL,
|
|
8
|
+
first INTEGER NOT NULL,
|
|
9
|
+
totalItems INTEGER NOT NULL DEFAULT 0,
|
|
10
|
+
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
11
|
+
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
12
|
+
PRIMARY KEY (username, property)
|
|
13
|
+
);
|
|
14
|
+
`)
|
|
15
|
+
await connection.query(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS actorcollectionpage (
|
|
17
|
+
username varchar(512) NOT NULL,
|
|
18
|
+
property varchar(512) NOT NULL,
|
|
19
|
+
item varchar(512) NOT NULL,
|
|
20
|
+
page INTEGER NOT NULL,
|
|
21
|
+
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
22
|
+
PRIMARY KEY (username, property, item)
|
|
23
|
+
);
|
|
24
|
+
`)
|
|
25
|
+
await connection.query(
|
|
26
|
+
`CREATE INDEX IF NOT EXISTS actorcollectionpage_username_property_page
|
|
27
|
+
ON actorcollectionpage (username, property, page);`
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
await connection.query(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS objects (
|
|
32
|
+
id VARCHAR(512) PRIMARY KEY,
|
|
33
|
+
data TEXT NOT NULL,
|
|
34
|
+
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
35
|
+
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
36
|
+
)
|
|
37
|
+
`)
|
|
38
|
+
await connection.query(`
|
|
39
|
+
CREATE TABLE IF NOT EXISTS collections (
|
|
40
|
+
id VARCHAR(512) NOT NULL,
|
|
41
|
+
property VARCHAR(512) NOT NULL,
|
|
42
|
+
first INTEGER NOT NULL,
|
|
43
|
+
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
44
|
+
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
45
|
+
PRIMARY KEY (id, property)
|
|
46
|
+
)
|
|
47
|
+
`)
|
|
48
|
+
await connection.query(`
|
|
49
|
+
CREATE TABLE IF NOT EXISTS pages (
|
|
50
|
+
id VARCHAR(512) NOT NULL,
|
|
51
|
+
property VARCHAR(64) NOT NULL,
|
|
52
|
+
item VARCHAR(512) NOT NULL,
|
|
53
|
+
page INTEGER NOT NULL,
|
|
54
|
+
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
55
|
+
PRIMARY KEY (id, property, item)
|
|
56
|
+
)
|
|
57
|
+
`)
|
|
58
|
+
await connection.query(
|
|
59
|
+
`CREATE INDEX IF NOT EXISTS pages_username_property_page
|
|
60
|
+
ON pages (id, property, page);`
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
await connection.query(`
|
|
64
|
+
CREATE TABLE IF NOT EXISTS botdata (
|
|
65
|
+
username VARCHAR(512) not null,
|
|
66
|
+
key VARCHAR(512) not null,
|
|
67
|
+
value TEXT not null,
|
|
68
|
+
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
69
|
+
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
70
|
+
PRIMARY KEY (username, key)
|
|
71
|
+
)
|
|
72
|
+
`)
|
|
73
|
+
|
|
74
|
+
await connection.query(`
|
|
75
|
+
CREATE TABLE IF NOT EXISTS new_keys (
|
|
76
|
+
username varchar(512) PRIMARY KEY,
|
|
77
|
+
public_key TEXT,
|
|
78
|
+
private_key TEXT
|
|
79
|
+
)
|
|
80
|
+
`)
|
|
81
|
+
try {
|
|
82
|
+
await connection.query(`
|
|
83
|
+
INSERT OR IGNORE INTO new_keys (username, public_key, private_key)
|
|
84
|
+
SELECT bot_id, public_key, private_key
|
|
85
|
+
FROM keys
|
|
86
|
+
`)
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await connection.query(`
|
|
91
|
+
CREATE TABLE IF NOT EXISTS new_remotekeys (
|
|
92
|
+
id VARCHAR(512) PRIMARY KEY,
|
|
93
|
+
owner VARCHAR(512) NOT NULL,
|
|
94
|
+
publicKeyPem TEXT NOT NULL,
|
|
95
|
+
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
96
|
+
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
97
|
+
)
|
|
98
|
+
`)
|
|
99
|
+
try {
|
|
100
|
+
await connection.query(`
|
|
101
|
+
INSERT OR IGNORE INTO new_remotekeys (id, owner, publicKeyPem)
|
|
102
|
+
SELECT id, owner, publicKeyPem
|
|
103
|
+
FROM remotekeys
|
|
104
|
+
`)
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { id as initialId, up as initialUp } from './001-initial.js'
|
|
2
|
+
|
|
3
|
+
const migrations = [
|
|
4
|
+
{ id: initialId, up: initialUp }
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
export async function runMigrations (connection) {
|
|
8
|
+
await connection.query(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
10
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
11
|
+
ranAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
12
|
+
)
|
|
13
|
+
`)
|
|
14
|
+
|
|
15
|
+
const [rows] = await connection.query('SELECT id FROM migrations')
|
|
16
|
+
const applied = new Set(rows.map((row) => row.id))
|
|
17
|
+
|
|
18
|
+
for (const migration of migrations) {
|
|
19
|
+
if (applied.has(migration.id)) {
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
await migration.up(connection)
|
|
23
|
+
await connection.query(
|
|
24
|
+
'INSERT INTO migrations (id) VALUES (?)',
|
|
25
|
+
{ replacements: [migration.id] }
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
}
|
package/lib/objectcache.js
CHANGED
package/lib/objectstorage.js
CHANGED
|
@@ -17,42 +17,6 @@ export class ObjectStorage {
|
|
|
17
17
|
this.#connection = connection
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
async initialize () {
|
|
21
|
-
await this.#connection.query(`
|
|
22
|
-
CREATE TABLE IF NOT EXISTS objects (
|
|
23
|
-
id VARCHAR(512) PRIMARY KEY,
|
|
24
|
-
data TEXT NOT NULL,
|
|
25
|
-
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
26
|
-
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
27
|
-
)
|
|
28
|
-
`)
|
|
29
|
-
await this.#connection.query(`
|
|
30
|
-
CREATE TABLE IF NOT EXISTS collections (
|
|
31
|
-
id VARCHAR(512) NOT NULL,
|
|
32
|
-
property VARCHAR(512) NOT NULL,
|
|
33
|
-
first INTEGER NOT NULL,
|
|
34
|
-
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
35
|
-
updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
36
|
-
PRIMARY KEY (id, property)
|
|
37
|
-
)
|
|
38
|
-
`)
|
|
39
|
-
await this.#connection.query(`
|
|
40
|
-
CREATE TABLE IF NOT EXISTS pages (
|
|
41
|
-
id VARCHAR(512) NOT NULL,
|
|
42
|
-
property VARCHAR(64) NOT NULL,
|
|
43
|
-
item VARCHAR(512) NOT NULL,
|
|
44
|
-
page INTEGER NOT NULL,
|
|
45
|
-
createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
46
|
-
PRIMARY KEY (id, property, item)
|
|
47
|
-
)
|
|
48
|
-
`)
|
|
49
|
-
|
|
50
|
-
await this.#connection.query(
|
|
51
|
-
`CREATE INDEX IF NOT EXISTS pages_username_property_page
|
|
52
|
-
ON pages (id, property, page);`
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
20
|
async create (object) {
|
|
57
21
|
assert.ok(this.#connection, 'ObjectStorage not initialized')
|
|
58
22
|
assert.ok(object, 'object is required')
|
package/lib/remotekeystorage.js
CHANGED
|
@@ -10,30 +10,6 @@ export class RemoteKeyStorage {
|
|
|
10
10
|
this.#logger = logger.child({ class: this.constructor.name })
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
async initialize () {
|
|
14
|
-
await this.#connection.query(
|
|
15
|
-
`CREATE TABLE IF NOT EXISTS new_remotekeys (
|
|
16
|
-
id VARCHAR(512) PRIMARY KEY,
|
|
17
|
-
owner VARCHAR(512) NOT NULL,
|
|
18
|
-
publicKeyPem TEXT NOT NULL,
|
|
19
|
-
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
20
|
-
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
21
|
-
)`
|
|
22
|
-
)
|
|
23
|
-
try {
|
|
24
|
-
await this.#connection.query(
|
|
25
|
-
`INSERT OR IGNORE INTO new_remotekeys (id, owner, publicKeyPem)
|
|
26
|
-
SELECT id, owner, publicKeyPem
|
|
27
|
-
FROM remotekeys`
|
|
28
|
-
)
|
|
29
|
-
} catch (error) {
|
|
30
|
-
this.#logger.debug(
|
|
31
|
-
{ error, method: 'RemoteKeyStorage.initialize' },
|
|
32
|
-
'failed to copy remotekeys to new_remotekeys table'
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
13
|
async getPublicKey (id, useCache = true) {
|
|
38
14
|
this.debug(`getPublicKey(${id})`)
|
|
39
15
|
if (useCache) {
|
package/lib/routes/collection.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from 'express'
|
|
2
2
|
import as2 from '../activitystreams.js'
|
|
3
3
|
import createHttpError from 'http-errors'
|
|
4
|
+
import BotMaker from '../botmaker.js'
|
|
4
5
|
|
|
5
6
|
const router = express.Router()
|
|
6
7
|
|
|
@@ -18,7 +19,8 @@ async function filterAsync (array, asyncPredicate) {
|
|
|
18
19
|
router.get('/user/:username/:collection', async (req, res, next) => {
|
|
19
20
|
const { username, collection } = req.params
|
|
20
21
|
const { actorStorage, bots } = req.app.locals
|
|
21
|
-
|
|
22
|
+
const bot = await BotMaker.makeBot(bots, username)
|
|
23
|
+
if (!bot) {
|
|
22
24
|
return next(createHttpError(404, `User ${username} not found`))
|
|
23
25
|
}
|
|
24
26
|
if (collection === 'inbox') {
|
|
@@ -37,10 +39,12 @@ router.get('/user/:username/:collection', async (req, res, next) => {
|
|
|
37
39
|
router.get('/user/:username/:collection/:n(\\d+)', async (req, res, next) => {
|
|
38
40
|
const { username, collection, n } = req.params
|
|
39
41
|
const { actorStorage, bots, authorizer, objectStorage, formatter, client } = req.app.locals
|
|
42
|
+
const bot = await BotMaker.makeBot(bots, username)
|
|
40
43
|
|
|
41
|
-
if (!
|
|
44
|
+
if (!bot) {
|
|
42
45
|
return next(createHttpError(404, `User ${username} not found`))
|
|
43
46
|
}
|
|
47
|
+
|
|
44
48
|
if (collection === 'inbox') {
|
|
45
49
|
return next(createHttpError(403, `No access to ${collection} collection`))
|
|
46
50
|
}
|
package/lib/routes/inbox.js
CHANGED
|
@@ -1,26 +1,19 @@
|
|
|
1
1
|
import express from 'express'
|
|
2
2
|
import as2 from '../activitystreams.js'
|
|
3
3
|
import createHttpError from 'http-errors'
|
|
4
|
+
import BotMaker from '../botmaker.js'
|
|
4
5
|
|
|
5
6
|
const router = express.Router()
|
|
6
7
|
|
|
7
|
-
function isActivity (object) {
|
|
8
|
-
return true
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function getActor (activity) {
|
|
12
|
-
return activity.actor?.first
|
|
13
|
-
}
|
|
14
|
-
|
|
15
8
|
router.post('/user/:username/inbox', async (req, res, next) => {
|
|
16
9
|
const { username } = req.params
|
|
17
|
-
const { bots,
|
|
10
|
+
const { bots, deliverer, actorStorage } = req.app.locals
|
|
18
11
|
const { subject } = req.auth
|
|
19
12
|
const { logger } = req.app.locals
|
|
20
13
|
|
|
21
|
-
const bot = bots
|
|
14
|
+
const bot = await BotMaker.makeBot(bots, username)
|
|
22
15
|
if (!bot) {
|
|
23
|
-
return next(createHttpError(404,
|
|
16
|
+
return next(createHttpError(404, `User ${username} not found`))
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
if (!subject) {
|
|
@@ -41,11 +34,11 @@ router.post('/user/:username/inbox', async (req, res, next) => {
|
|
|
41
34
|
return next(createHttpError(400, 'Invalid request body'))
|
|
42
35
|
}
|
|
43
36
|
|
|
44
|
-
if (!isActivity(activity)) {
|
|
37
|
+
if (!deliverer.isActivity(activity)) {
|
|
45
38
|
return next(createHttpError(400, 'Request body is not an activity'))
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
const actor = getActor(activity)
|
|
41
|
+
const actor = deliverer.getActor(activity)
|
|
49
42
|
|
|
50
43
|
if (!actor) {
|
|
51
44
|
return next(createHttpError(400, 'No actor found in activity'))
|
|
@@ -67,13 +60,7 @@ router.post('/user/:username/inbox', async (req, res, next) => {
|
|
|
67
60
|
return next(createHttpError(400, 'Activity already delivered'))
|
|
68
61
|
}
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
await activityHandler.handleActivity(bot, activity)
|
|
72
|
-
} catch (err) {
|
|
73
|
-
return next(err)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
await actorStorage.addToCollection(bot.username, 'inbox', activity)
|
|
63
|
+
await deliverer.deliverTo(activity, bot)
|
|
77
64
|
|
|
78
65
|
res.status(200)
|
|
79
66
|
res.type('text/plain')
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import as2 from '../activitystreams.js'
|
|
3
|
+
import createHttpError from 'http-errors'
|
|
4
|
+
|
|
5
|
+
const router = express.Router()
|
|
6
|
+
|
|
7
|
+
router.post('/shared/inbox', async (req, res, next) => {
|
|
8
|
+
const { bots, deliverer, logger } = req.app.locals
|
|
9
|
+
const { subject } = req.auth
|
|
10
|
+
|
|
11
|
+
if (!subject) {
|
|
12
|
+
return next(createHttpError(401, 'Unauthorized'))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!req.body) {
|
|
16
|
+
return next(createHttpError(400, 'No request body provided'))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let activity
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
activity = await as2.import(req.body)
|
|
23
|
+
} catch (err) {
|
|
24
|
+
logger.warn('Failed to import activity', err)
|
|
25
|
+
logger.debug('Request body', req.body)
|
|
26
|
+
return next(createHttpError(400, 'Invalid request body'))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!deliverer.isActivity(activity)) {
|
|
30
|
+
return next(createHttpError(400, 'Request body is not an activity'))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const actor = deliverer.getActor(activity)
|
|
34
|
+
|
|
35
|
+
if (!actor) {
|
|
36
|
+
return next(createHttpError(400, 'No actor found in activity'))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!actor.id) {
|
|
40
|
+
return next(createHttpError(400, 'No actor id found in activity'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (actor.id !== subject) {
|
|
44
|
+
return next(createHttpError(403, `${subject} is not the actor ${actor.id}`))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await deliverer.deliverToAll(activity, bots)
|
|
48
|
+
|
|
49
|
+
res.status(200)
|
|
50
|
+
res.type('text/plain')
|
|
51
|
+
res.send('OK')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
export default router
|
package/lib/routes/user.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import express from 'express'
|
|
2
2
|
import as2 from '../activitystreams.js'
|
|
3
3
|
import createHttpError from 'http-errors'
|
|
4
|
+
import BotMaker from '../botmaker.js'
|
|
4
5
|
|
|
5
6
|
const router = express.Router()
|
|
6
7
|
|
|
7
8
|
router.get('/user/:username', async (req, res, next) => {
|
|
8
9
|
const { username } = req.params
|
|
9
|
-
const { actorStorage, keyStorage, formatter, bots } = req.app.locals
|
|
10
|
-
|
|
10
|
+
const { actorStorage, keyStorage, formatter, bots, origin } = req.app.locals
|
|
11
|
+
const bot = await BotMaker.makeBot(bots, username)
|
|
12
|
+
if (!bot) {
|
|
11
13
|
return next(createHttpError(404, `User ${username} not found`))
|
|
12
14
|
}
|
|
13
15
|
const publicKeyPem = await keyStorage.getPublicKey(username)
|
|
@@ -16,14 +18,17 @@ router.get('/user/:username', async (req, res, next) => {
|
|
|
16
18
|
'https://www.w3.org/ns/activitystreams',
|
|
17
19
|
'https://w3id.org/security/v1'
|
|
18
20
|
],
|
|
19
|
-
name:
|
|
20
|
-
summary:
|
|
21
|
+
name: bot.fullname,
|
|
22
|
+
summary: bot.description,
|
|
21
23
|
publicKey: {
|
|
22
24
|
publicKeyPem,
|
|
23
25
|
id: formatter.format({ username, type: 'publickey' }),
|
|
24
26
|
owner: formatter.format({ username }),
|
|
25
27
|
type: 'CryptographicKey',
|
|
26
28
|
to: 'as:Public'
|
|
29
|
+
},
|
|
30
|
+
endpoints: {
|
|
31
|
+
sharedInbox: `${origin}/shared/inbox`
|
|
27
32
|
}
|
|
28
33
|
})
|
|
29
34
|
res.status(200)
|
|
@@ -37,7 +42,8 @@ router.get('/user/:username', async (req, res, next) => {
|
|
|
37
42
|
router.get('/user/:username/publickey', async (req, res, next) => {
|
|
38
43
|
const { username } = req.params
|
|
39
44
|
const { formatter, keyStorage, bots } = req.app.locals
|
|
40
|
-
|
|
45
|
+
const bot = await BotMaker.makeBot(bots, username)
|
|
46
|
+
if (!bot) {
|
|
41
47
|
return next(createHttpError(404, `User ${username} not found`))
|
|
42
48
|
}
|
|
43
49
|
const publicKeyPem = await keyStorage.getPublicKey(username)
|
package/lib/routes/webfinger.js
CHANGED
|
@@ -3,11 +3,14 @@ import createHttpError from 'http-errors'
|
|
|
3
3
|
|
|
4
4
|
const router = Router()
|
|
5
5
|
|
|
6
|
-
router.get('/.well-known/webfinger', (req, res, next) => {
|
|
6
|
+
router.get('/.well-known/webfinger', async (req, res, next) => {
|
|
7
7
|
const { resource } = req.query
|
|
8
8
|
if (!resource) {
|
|
9
9
|
return next(createHttpError(400, 'resource parameter is required'))
|
|
10
10
|
}
|
|
11
|
+
if (resource.substring(0, 5) !== 'acct:') {
|
|
12
|
+
return next(createHttpError(400, 'only acct: protocol supported'))
|
|
13
|
+
}
|
|
11
14
|
const [username, domain] = resource.substring(5).split('@')
|
|
12
15
|
if (!username || !domain) {
|
|
13
16
|
return next(createHttpError(400, 'Invalid resource parameter'))
|
|
@@ -17,7 +20,13 @@ router.get('/.well-known/webfinger', (req, res, next) => {
|
|
|
17
20
|
return next(createHttpError(400, 'Invalid domain in resource parameter'))
|
|
18
21
|
}
|
|
19
22
|
if (!(username in req.app.locals.bots)) {
|
|
20
|
-
|
|
23
|
+
if ('*' in req.app.locals.bots) {
|
|
24
|
+
if (!await req.app.locals.bots['*'].canCreate(username)) {
|
|
25
|
+
return next(createHttpError(404, 'Bot not found'))
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
return next(createHttpError(404, 'Bot not found'))
|
|
29
|
+
}
|
|
21
30
|
}
|
|
22
31
|
res.status(200)
|
|
23
32
|
res.type('application/jrd+json')
|
package/lib/urlformatter.js
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evanp/activitypub-bot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "server-side ActivityPub bot framework",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
6
|
+
"main": "activitypub-bot.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"activitypub-bot": "./activitypub-bot.js"
|
|
9
|
+
},
|
|
7
10
|
"scripts": {
|
|
8
11
|
"test": "NODE_ENV=test node --test",
|
|
9
|
-
"start": "
|
|
12
|
+
"start": "npx activitypub-bot"
|
|
10
13
|
},
|
|
11
14
|
"repository": {
|
|
12
15
|
"type": "git",
|
|
@@ -25,27 +28,31 @@
|
|
|
25
28
|
},
|
|
26
29
|
"homepage": "https://github.com/evanp/activitypub-bot#readme",
|
|
27
30
|
"dependencies": {
|
|
28
|
-
"@isaacs/ttlcache": "^1.4
|
|
31
|
+
"@isaacs/ttlcache": "^2.1.4",
|
|
29
32
|
"activitystrea.ms": "3.2",
|
|
30
|
-
"express": "^4.
|
|
33
|
+
"express": "^4.22.1",
|
|
31
34
|
"http-errors": "^2.0.0",
|
|
32
35
|
"humanhash": "^1.0.4",
|
|
33
36
|
"lru-cache": "^11.1.0",
|
|
34
37
|
"nanoid": "^5.1.5",
|
|
35
38
|
"node-fetch": "^3.3.2",
|
|
36
|
-
"p-queue": "^
|
|
37
|
-
"pino": "^
|
|
38
|
-
"pino-http": "^
|
|
39
|
+
"p-queue": "^9.1.0",
|
|
40
|
+
"pino": "^10.1.1",
|
|
41
|
+
"pino-http": "^11.0.0",
|
|
39
42
|
"sequelize": "^6.37.7"
|
|
40
43
|
},
|
|
41
44
|
"devDependencies": {
|
|
42
45
|
"nock": "^14.0.5",
|
|
43
46
|
"standard": "^17.1.2",
|
|
44
|
-
"supertest": "^7.1.
|
|
47
|
+
"supertest": "^7.1.4"
|
|
45
48
|
},
|
|
46
49
|
"optionalDependencies": {
|
|
47
50
|
"mysql2": "^3.9.1",
|
|
48
|
-
"pg": "^8.16.
|
|
51
|
+
"pg": "^8.16.3",
|
|
49
52
|
"sqlite3": "^5.1.7"
|
|
50
|
-
}
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": "^20 || ^22 || ^24 || ^25"
|
|
56
|
+
},
|
|
57
|
+
"engineStrict": true
|
|
51
58
|
}
|
|
@@ -11,6 +11,7 @@ import { ActivityDistributor } from '../lib/activitydistributor.js'
|
|
|
11
11
|
import Logger from 'pino'
|
|
12
12
|
import { HTTPSignature } from '../lib/httpsignature.js'
|
|
13
13
|
import { Digester } from '../lib/digester.js'
|
|
14
|
+
import { runMigrations } from '../lib/migrations/index.js'
|
|
14
15
|
|
|
15
16
|
const makeActor = (domain, username, shared = true) =>
|
|
16
17
|
as2.import({
|
|
@@ -63,12 +64,11 @@ describe('ActivityDistributor', () => {
|
|
|
63
64
|
before(async () => {
|
|
64
65
|
logger = Logger({ level: 'silent' })
|
|
65
66
|
formatter = new UrlFormatter('https://activitypubbot.example')
|
|
66
|
-
connection = new Sequelize('sqlite
|
|
67
|
+
connection = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false })
|
|
67
68
|
await connection.authenticate()
|
|
69
|
+
await runMigrations(connection)
|
|
68
70
|
actorStorage = new ActorStorage(connection, formatter)
|
|
69
|
-
await actorStorage.initialize()
|
|
70
71
|
keyStorage = new KeyStorage(connection, logger)
|
|
71
|
-
await keyStorage.initialize()
|
|
72
72
|
const signer = new HTTPSignature(logger)
|
|
73
73
|
const digester = new Digester(logger)
|
|
74
74
|
client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger)
|