@evanp/activitypub-bot 0.9.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.github/workflows/main.yml +34 -0
  2. package/.github/workflows/{tag-docker.yml → tag.yml} +57 -5
  3. package/.nvmrc +1 -0
  4. package/Dockerfile +10 -16
  5. package/README.md +87 -13
  6. package/bin/activitypub-bot.js +68 -0
  7. package/lib/activitydeliverer.js +260 -0
  8. package/lib/activityhandler.js +14 -0
  9. package/lib/activitypubclient.js +52 -1
  10. package/lib/activitystreams.js +31 -0
  11. package/lib/actorstorage.js +18 -28
  12. package/lib/app.js +18 -7
  13. package/lib/bot.js +7 -0
  14. package/lib/botcontext.js +62 -0
  15. package/lib/botdatastorage.js +0 -13
  16. package/lib/botfactory.js +24 -0
  17. package/lib/botmaker.js +23 -0
  18. package/lib/index.js +3 -0
  19. package/lib/keystorage.js +7 -24
  20. package/lib/migrations/001-initial.js +107 -0
  21. package/lib/migrations/index.js +28 -0
  22. package/lib/objectcache.js +4 -1
  23. package/lib/objectstorage.js +0 -36
  24. package/lib/remotekeystorage.js +0 -24
  25. package/lib/routes/collection.js +6 -2
  26. package/lib/routes/inbox.js +7 -20
  27. package/lib/routes/sharedinbox.js +54 -0
  28. package/lib/routes/user.js +11 -5
  29. package/lib/routes/webfinger.js +11 -2
  30. package/lib/urlformatter.js +8 -0
  31. package/package.json +14 -7
  32. package/tests/activitydistributor.test.js +3 -3
  33. package/tests/activityhandler.test.js +96 -5
  34. package/tests/activitypubclient.test.js +115 -130
  35. package/tests/actorstorage.test.js +26 -4
  36. package/tests/authorizer.test.js +3 -8
  37. package/tests/botcontext.test.js +109 -63
  38. package/tests/botdatastorage.test.js +3 -2
  39. package/tests/botfactory.provincebotfactory.test.js +430 -0
  40. package/tests/fixtures/bots.js +13 -1
  41. package/tests/fixtures/eventloggingbot.js +57 -0
  42. package/tests/fixtures/provincebotfactory.js +53 -0
  43. package/tests/httpsignature.test.js +3 -4
  44. package/tests/httpsignatureauthenticator.test.js +3 -3
  45. package/tests/index.test.js +10 -0
  46. package/tests/keystorage.test.js +37 -2
  47. package/tests/microsyntax.test.js +3 -2
  48. package/tests/objectstorage.test.js +4 -3
  49. package/tests/remotekeystorage.test.js +10 -8
  50. package/tests/routes.actor.test.js +7 -0
  51. package/tests/routes.collection.test.js +0 -1
  52. package/tests/routes.inbox.test.js +1 -0
  53. package/tests/routes.object.test.js +44 -38
  54. package/tests/routes.sharedinbox.test.js +473 -0
  55. package/tests/routes.webfinger.test.js +27 -0
  56. package/tests/utils/nock.js +250 -27
  57. package/.github/workflows/main-docker.yml +0 -45
  58. package/index.js +0 -23
@@ -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
- return await fetch(url,
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
  }
@@ -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
@@ -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 = new Sequelize(databaseUrl, { logging: false })
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
  }
@@ -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
+ }
@@ -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/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { makeApp } from './app.js'
2
+ export { default as Bot } from './bot.js'
3
+ export { default as BotFactory } from './botfactory.js'
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 [result] = await this.#connection.query(`
61
- SELECT public_key, private_key FROM new_keys WHERE username = ?
62
- `, { replacements: [username] })
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
+ }
@@ -1,4 +1,7 @@
1
- import TTLCache from '@isaacs/ttlcache'
1
+ import * as ttlcachePkg from '@isaacs/ttlcache'
2
+
3
+ const TTLCache =
4
+ ttlcachePkg.TTLCache ?? ttlcachePkg.default ?? ttlcachePkg
2
5
 
3
6
  export class ObjectCache {
4
7
  #objects = null
@@ -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')