@evanp/activitypub-bot 0.28.6 → 0.29.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.
@@ -69,4 +69,15 @@ as2.registerContext('https://purl.archive.org/socialweb/thread/1.0', {
69
69
  }
70
70
  })
71
71
 
72
+ as2.registerContext('https://purl.archive.org/socialweb/webfinger', {
73
+ '@context': {
74
+ wf: 'https://purl.archive.org/socialweb/webfinger#',
75
+ xsd: 'http://www.w3.org/2001/XMLSchema#',
76
+ webfinger: {
77
+ '@id': 'wf:webfinger',
78
+ '@type': 'xsd:string'
79
+ }
80
+ }
81
+ })
82
+
72
83
  export default as2
package/lib/app.js CHANGED
@@ -177,6 +177,7 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
177
177
  }))
178
178
 
179
179
  app.use(express.json({
180
+ limit: '1mb',
180
181
  type: [
181
182
  'application/activity+json',
182
183
  'application/ld+json',
@@ -13,14 +13,19 @@ router.get('/actor', async (req, res) => {
13
13
  const homepage = `${req.protocol}://${req.get('host')}/`
14
14
  const { formatter, keyStorage } = req.app.locals
15
15
  const publicKeyPem = await keyStorage.getPublicKey(null)
16
+ const acct = formatter.acct()
17
+ const webfinger = acct.slice(5)
16
18
  const server = await as2.import({
17
19
  '@context': [
18
20
  'https://www.w3.org/ns/activitystreams',
19
- 'https://w3id.org/security/v1'
21
+ 'https://w3id.org/security/v1',
22
+ 'https://purl.archive.org/socialweb/webfinger'
20
23
  ],
21
24
  id: formatter.format({ server: true }),
22
25
  type: 'Service',
23
26
  to: 'as:Public',
27
+ alsoKnownAs: acct,
28
+ webfinger,
24
29
  publicKey: {
25
30
  publicKeyPem,
26
31
  id: formatter.format({ server: true, type: 'publickey' }),
@@ -13,13 +13,18 @@ router.get('/user/:username', async (req, res, next) => {
13
13
  return next(createHttpError(404, `User ${username} not found`))
14
14
  }
15
15
  const publicKeyPem = await keyStorage.getPublicKey(username)
16
+ const acct = formatter.acct(username)
17
+ const wf = acct.slice(5)
16
18
  const actor = await actorStorage.getActor(username, {
17
19
  '@context': [
18
20
  'https://www.w3.org/ns/activitystreams',
19
- 'https://w3id.org/security/v1'
21
+ 'https://w3id.org/security/v1',
22
+ 'https://purl.archive.org/socialweb/webfinger'
20
23
  ],
21
24
  name: bot.fullname,
22
25
  summary: bot.description,
26
+ webfinger: wf,
27
+ alsoKnownAs: acct,
23
28
  publicKey: {
24
29
  publicKeyPem,
25
30
  id: formatter.format({ username, type: 'publickey' }),
@@ -34,7 +39,7 @@ router.get('/user/:username', async (req, res, next) => {
34
39
  res.status(200)
35
40
  res.type(as2.mediaType)
36
41
  const body = await actor.prettyWrite(
37
- { additional_context: 'https://w3id.org/security/v1' }
42
+ { useOriginalContext: true }
38
43
  )
39
44
  res.end(body)
40
45
  })
@@ -1,45 +1,103 @@
1
+ import assert from 'node:assert'
2
+
1
3
  import { Router } from 'express'
2
4
  import createHttpError from 'http-errors'
3
5
 
6
+ import BotMaker from '../botmaker.js'
7
+
4
8
  const router = Router()
5
9
 
6
- router.get('/.well-known/webfinger', async (req, res, next) => {
7
- const { resource } = req.query
8
- if (!resource) {
9
- return next(createHttpError(400, 'resource parameter is required'))
10
- }
11
- if (resource.substring(0, 5) !== 'acct:') {
12
- return next(createHttpError(400, 'only acct: protocol supported'))
13
- }
14
- const [username, domain] = resource.substring(5).split('@')
15
- if (!username || !domain) {
16
- return next(createHttpError(400, 'Invalid resource parameter'))
17
- }
18
- const { host } = new URL(req.app.locals.origin)
19
- if (domain !== host) {
20
- return next(createHttpError(400, 'Invalid domain in resource parameter'))
10
+ async function botWebfinger (username, req, res, next) {
11
+ const { formatter, bots } = req.app.locals
12
+ const bot = await BotMaker.makeBot(bots, username)
13
+ if (!bot) {
14
+ return next(createHttpError(404, `No such bot '${username}'`))
21
15
  }
22
- if (!(username in req.app.locals.bots)) {
23
- if ('*' in req.app.locals.bots) {
24
- if (!await req.app.locals.bots['*'].canCreate(username)) {
25
- return next(createHttpError(404, 'Bot not found'))
16
+ res.status(200)
17
+ res.type('application/jrd+json')
18
+ res.json({
19
+ subject: formatter.acct(username),
20
+ aliases: [formatter.format({ username })],
21
+ links: [
22
+ {
23
+ rel: 'self',
24
+ type: 'application/activity+json',
25
+ href: formatter.format({ username })
26
26
  }
27
- } else {
28
- return next(createHttpError(404, 'Bot not found'))
29
- }
30
- }
27
+ ]
28
+ })
29
+ }
30
+
31
+ async function serverWebfinger (req, res, next) {
32
+ const { formatter } = req.app.locals
33
+ const webfinger = formatter.acct()
34
+ const id = formatter.format({ server: true })
31
35
  res.status(200)
32
36
  res.type('application/jrd+json')
33
37
  res.json({
34
- subject: resource,
38
+ subject: webfinger,
35
39
  links: [
36
40
  {
37
41
  rel: 'self',
38
42
  type: 'application/activity+json',
39
- href: req.app.locals.formatter.format({ username })
43
+ href: id
40
44
  }
41
45
  ]
42
46
  })
47
+ }
48
+
49
+ async function httpsWebfinger (resource, req, res, next) {
50
+ const { formatter } = req.app.locals
51
+ assert.ok(formatter)
52
+ if (!formatter.isLocal(resource)) {
53
+ return next(createHttpError(400, 'Only local URLs'))
54
+ }
55
+ const parts = formatter.unformat(resource)
56
+ if (parts.server && !parts.type) {
57
+ return await serverWebfinger(req, res, next)
58
+ } else if (
59
+ !parts.server && parts.username && !parts.type && !parts.collection
60
+ ) {
61
+ return await botWebfinger(parts.username, req, res, next)
62
+ } else {
63
+ return next(createHttpError(400, `No webfinger lookup for url ${resource}`))
64
+ }
65
+ }
66
+
67
+ async function acctWebfinger (resource, req, res, next) {
68
+ const [username, domain] = resource.substring(5).split('@')
69
+ if (!username || !domain) {
70
+ return next(createHttpError(400, `Invalid resource parameter ${resource}`))
71
+ }
72
+ const { host } = new URL(req.app.locals.origin)
73
+ if (domain !== host) {
74
+ return next(createHttpError(400, `Invalid domain ${domain} in resource parameter`))
75
+ }
76
+ if (username === domain) {
77
+ return await serverWebfinger(req, res, next)
78
+ } else {
79
+ return await botWebfinger(username, req, res, next)
80
+ }
81
+ }
82
+
83
+ router.get('/.well-known/webfinger', async (req, res, next) => {
84
+ const { resource } = req.query
85
+ if (!resource) {
86
+ return next(createHttpError(400, 'resource parameter is required'))
87
+ }
88
+ const colon = resource.indexOf(':')
89
+ const protocol = resource.slice(0, colon)
90
+
91
+ switch (protocol) {
92
+ case 'acct':
93
+ return await acctWebfinger(resource, req, res, next)
94
+ case 'https':
95
+ return await httpsWebfinger(resource, req, res, next)
96
+ default:
97
+ return next(
98
+ createHttpError(400, `Unsupported resource protocol '${protocol}'`)
99
+ )
100
+ }
43
101
  })
44
102
 
45
103
  export default router
@@ -4,8 +4,11 @@ const USER_COLLECTIONS = ['inbox', 'outbox', 'liked', 'followers', 'following']
4
4
 
5
5
  export class UrlFormatter {
6
6
  #origin = null
7
+ #hostname
8
+
7
9
  constructor (origin) {
8
10
  this.#origin = origin
11
+ this.#hostname = (new URL(origin)).hostname
9
12
  }
10
13
 
11
14
  format ({ username, type, nanoid, collection, page, server }) {
@@ -75,7 +78,7 @@ export class UrlFormatter {
75
78
  if (pathParts.length > 0) {
76
79
  if (USER_COLLECTIONS.includes(pathParts[0])) {
77
80
  parts.collection = pathParts[0]
78
- } else {
81
+ } else if (pathParts[0] !== 'actor') {
79
82
  parts.type = pathParts[0]
80
83
  }
81
84
  }
@@ -102,4 +105,10 @@ export class UrlFormatter {
102
105
  const parts = this.unformat(url)
103
106
  return (parts.username && !parts.type && !parts.collection)
104
107
  }
108
+
109
+ acct (username = null) {
110
+ return (username)
111
+ ? `acct:${username}@${this.#hostname}`
112
+ : `acct:${this.#hostname}@${this.#hostname}`
113
+ }
105
114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evanp/activitypub-bot",
3
- "version": "0.28.6",
3
+ "version": "0.29.0",
4
4
  "description": "server-side ActivityPub bot framework",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",