@evanp/activitypub-bot 0.9.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 +87 -13
- 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 +14 -7
- 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/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
|
+
}
|
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')
|