diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 2be0f4fa5d..74ce768a74 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -1,7 +1,7 @@ 'use strict'; const request = require('request-promise-native'); -const { generateKeyPairSync, sign } = require('crypto'); +const { generateKeyPairSync } = require('crypto'); const winston = require('winston'); const db = require('../database'); diff --git a/src/activitypub/index.js b/src/activitypub/index.js index 20d7b38967..878bfabc86 100644 --- a/src/activitypub/index.js +++ b/src/activitypub/index.js @@ -7,12 +7,18 @@ const { createHash, createSign, createVerify } = require('crypto'); const db = require('../database'); const user = require('../user'); +const ttl = require('../cache/ttl'); +const actorCache = ttl({ ttl: 1000 * 60 * 60 * 24 }); // 24 hours const ActivityPub = module.exports; ActivityPub.helpers = require('./helpers'); ActivityPub.getActor = async (id) => { + if (actorCache.has(id)) { + return actorCache.get(id); + } + const { hostname, actorUri: uri } = await ActivityPub.helpers.query(id); if (!uri) { return false; @@ -28,9 +34,15 @@ ActivityPub.getActor = async (id) => { actor.hostname = hostname; + actorCache.set(id, actor); return actor; }; +ActivityPub.resolveInboxes = async ids => await Promise.all(ids.map(async (id) => { + const actor = await ActivityPub.getActor(id); + return actor.inbox; +})); + ActivityPub.getPublicKey = async (uid) => { let publicKey; @@ -84,7 +96,7 @@ ActivityPub.sign = async (uid, url, payload) => { if (payload) { const payloadHash = createHash('sha256'); payloadHash.update(JSON.stringify(payload)); - digest = payloadHash.digest('hex'); + digest = `sha-256=${payloadHash.digest('base64')}`; headers += ' digest'; signed_string += `\ndigest: ${digest}`; } @@ -146,26 +158,43 @@ ActivityPub.verify = async (req) => { } }; -/** - * This is just some code to test signing and verification. This should really be in the test suite. - */ -// setTimeout(async () => { -// const payload = { -// foo: 'bar', -// }; -// const signature = await ActivityPub.sign(1, 'http://127.0.0.1:4567/user/julian/inbox', payload); +ActivityPub.send = async (uid, targets, payload) => { + if (!Array.isArray(targets)) { + targets = [targets]; + } -// const res = await request({ -// uri: 'http://127.0.0.1:4567/user/julian/inbox', -// method: 'post', -// headers: { -// Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', -// ...signature, -// }, -// json: true, -// body: payload, -// simple: false, -// }); + const userslug = await user.getUserField(uid, 'userslug'); + const inboxes = await ActivityPub.resolveInboxes(targets); -// console.log(res); -// }, 1000); + payload = { + ...{ + '@context': 'https://www.w3.org/ns/activitystreams', + actor: { + type: 'Person', + name: `${userslug}@${nconf.get('url_parsed').host}`, + }, + }, + ...payload, + }; + + await Promise.all(inboxes.map(async (uri) => { + const { date, digest, signature } = await ActivityPub.sign(uid, uri, payload); + + const response = await request(uri, { + method: payload ? 'post' : 'get', + headers: { + date, + digest, + signature, + 'content-type': 'application/ld+json; profile="http://www.w3.org/ns/activitystreams', + accept: 'application/ld+json; profile="http://www.w3.org/ns/activitystreams', + }, + json: true, + body: payload, + simple: false, + resolveWithFullResponse: true, + }); + + console.log(response.statusCode, response.body); + })); +}; diff --git a/src/controllers/activitypub/index.js b/src/controllers/activitypub/index.js index 21e4264499..a90d5a5a1b 100644 --- a/src/controllers/activitypub/index.js +++ b/src/controllers/activitypub/index.js @@ -2,7 +2,7 @@ const nconf = require('nconf'); -const user = require('../user'); +const user = require('../../user'); const activitypub = require('../../activitypub'); const Controller = module.exports; @@ -103,3 +103,24 @@ Controller.postInbox = async (req, res) => { res.sendStatus(201); }; + +/** + * Main ActivityPub verbs + */ + +Controller.follow = async (req, res) => { + await activitypub.send(req.uid, req.params.uid, { + type: 'Follow', + object: { + type: 'Person', + name: req.params.uid, + }, + }); + + res.sendStatus(201); +}; + +Controller.unfollow = async (req, res) => { + console.log('got here'); + res.sendStatus(201); +}; diff --git a/src/controllers/write/users.js b/src/controllers/write/users.js index 715be0f48c..fe63ab9019 100644 --- a/src/controllers/write/users.js +++ b/src/controllers/write/users.js @@ -9,6 +9,8 @@ const user = require('../../user'); const helpers = require('../helpers'); +const activitypubController = require('../activitypub'); + const Users = module.exports; Users.redirectBySlug = async (req, res) => { @@ -92,11 +94,19 @@ Users.changePassword = async (req, res) => { }; Users.follow = async (req, res) => { + if (req.params.uid.indexOf('@') !== -1) { + return await activitypubController.follow(req, res); + } + await api.users.follow(req, req.params); helpers.formatApiResponse(200, res); }; Users.unfollow = async (req, res) => { + if (req.params.uid.indexOf('@') !== -1) { + return await activitypubController.unfollow(req, res); + } + await api.users.unfollow(req, req.params); helpers.formatApiResponse(200, res); }; diff --git a/test/activitypub.js b/test/activitypub.js index 075fca975f..ebca3a1d62 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -251,14 +251,14 @@ describe('ActivityPub integration', () => { const { digest } = await activitypub.sign(uid, endpoint, payload); const hash = createHash('sha256'); hash.update(JSON.stringify(payload)); - const checksum = hash.digest('hex'); + const checksum = hash.digest('base64'); assert(digest); - assert.strictEqual(digest, checksum); + assert.strictEqual(digest, `sha-256=${checksum}`); }); }); - describe.only('.verify()', () => { + describe('.verify()', () => { let uid; let username; const mockReqBase = {