diff --git a/index.js b/index.js index d84b3ca8..72934dfc 100644 --- a/index.js +++ b/index.js @@ -214,8 +214,11 @@ function init (ssb, config) { * (because they can't post messages to the group before then) */ + const isMemberType = (type) => type === 'group/add-member' || type === 'group/exclude-member' + /* Tangle: auto-add tangles.group info to all private-group messages */ - const getGroupTangle = GetGroupTangle(ssb, keystore) + const getGroupTangle = GetGroupTangle(ssb, keystore, 'group') + const getMembersTangle = GetGroupTangle(ssb, keystore, 'members') ssb.publish.hook(function (publish, args) { const [content, cb] = args if (!content.recps) return publish.apply(this, args) @@ -223,18 +226,27 @@ function init (ssb, config) { if (!isGroup(content.recps[0])) return publish.apply(this, args) onKeystoreReady(() => { - getGroupTangle(content.recps[0], (err, tangle) => { - // NOTE there are two ways an err can occur in getGroupTangle - // 1. recps is not a groupId - // 2. unknown groupId, + if (!keystore.group.has(content.recps[0])) return cb(Error('unknown groupId')) - // Rather than cb(err) here we we pass it on to boxers to see if an err is needed - if (err) return publish.apply(this, args) + getGroupTangle(content.recps[0], (err, groupTangle) => { + if (err) return cb(Error("Couldn't get group tangle", { cause: err })) - set(content, 'tangles.group', tangle) + set(content, 'tangles.group', groupTangle) tanglePrune(content) // prune the group tangle down if needed - publish.call(this, content, cb) + // we only want to have to calculate the members tangle if it's gonna be used + if (!isMemberType(content.type)) { + return publish.call(this, content, cb) + } + + getMembersTangle(content.recps[0], (err, membersTangle) => { + if (err) return cb(Error("Couldn't get members tangle", { cause: err })) + + set(content, 'tangles.members', membersTangle) + tanglePrune(content, 'members') + + publish.call(this, content, cb) + }) }) }) }) diff --git a/lib/get-group-tangle.js b/lib/get-group-tangle.js index 915e99a0..2d3442f3 100644 --- a/lib/get-group-tangle.js +++ b/lib/get-group-tangle.js @@ -5,10 +5,8 @@ const Strategy = require('@tangle/strategy') // for figuring out what "previous" should be for the group -const TANGLE = 'group' -const strategy = new Strategy({}) - -module.exports = function GetGroupTangle (server, keystore) { +module.exports = function GetGroupTangle (server, keystore, tangle = 'group') { + const strategy = new Strategy({}) const cache = new Map([]) // groupId > new Reduce (tangleTips) // LISTEN @@ -50,10 +48,13 @@ module.exports = function GetGroupTangle (server, keystore) { // if it's in the cache, then get the cached value, then callback if (cache.has(groupId)) { - return cb(null, { - root: info.root, - previous: Object.keys(cache.get(groupId).state) - }) + // this timeout seems to help for some reason. in some cases messages were posted too fast with tangles 'in parallel', e.g. 2 messages both just having previous: [rootMsgId] + return setTimeout(() => { + return cb(null, { + root: info.root, + previous: Object.keys(cache.get(groupId).state) + }) + }, 0) } // if not in cache, compute it and add to the cache @@ -64,7 +65,7 @@ module.exports = function GetGroupTangle (server, keystore) { value: { content: { tangles: { - [TANGLE]: { root: info.root } + [tangle]: { root: info.root } } } } @@ -73,7 +74,7 @@ module.exports = function GetGroupTangle (server, keystore) { { $map: { key: ['key'], - previous: ['value', 'content', 'tangles', TANGLE, 'previous'] + previous: ['value', 'content', 'tangles', tangle, 'previous'] } } ] diff --git a/method/group.js b/method/group.js index 0d105aaa..38f480ae 100644 --- a/method/group.js +++ b/method/group.js @@ -99,13 +99,9 @@ module.exports = function GroupMethods (ssb, keystore, state) { type: 'group/exclude-member', excludes: authorIds, tangles: { - members: { - root, - previous: [root] // TODO calculate previous for members tangle - }, - + members: { root, previous: [root] }, group: { root, previous: [root] } - // NOTE: this is a dummy entry which is over-written in publish hook + // NOTE: these are dummy entries which are over-written in the publish hook }, recps: [groupId] } diff --git a/test/api/exclude-members.test.js b/test/api/exclude-members.test.js index 57c46ddd..a4f599a8 100644 --- a/test/api/exclude-members.test.js +++ b/test/api/exclude-members.test.js @@ -38,7 +38,7 @@ test('tribes.excludeMembers', async t => { excludes: authorIds, tangles: { - members: { root: groupInitMsg.key, previous: [groupInitMsg.key] }, + members: { root: groupInitMsg.key, previous: [...exclude.content.tangles.members.previous] }, group: { root: groupInitMsg.key, previous: [...exclude.content.tangles.group.previous] } }, recps: [groupId] diff --git a/test/api/invite.test.js b/test/api/invite.test.js index bcfe145c..bed35ec4 100644 --- a/test/api/invite.test.js +++ b/test/api/invite.test.js @@ -8,6 +8,8 @@ test('tribes.invite', async t => { const { groupId, groupKey, groupInitMsg } = await p(kaitiaki.tribes.create)({}) t.true(groupId, 'creates group') + const selfAdd = await p(kaitiaki.getLatest)(kaitiaki.id) + const authorIds = [ newPerson.id, FeedId() @@ -28,8 +30,8 @@ test('tribes.invite', async t => { recps: [groupId, ...authorIds], tangles: { - group: { root: groupInitMsg.key, previous: [groupInitMsg.key] }, - members: { root: groupInitMsg.key, previous: [groupInitMsg.key] } + group: { root: groupInitMsg.key, previous: [selfAdd.key] }, + members: { root: groupInitMsg.key, previous: [selfAdd.key] } } } t.deepEqual(invite.content, expected, 'kaitiaki sent invite') diff --git a/test/lib/get-group-tangle.test.js b/test/lib/get-group-tangle.test.js index 8927f6f8..9385027b 100644 --- a/test/lib/get-group-tangle.test.js +++ b/test/lib/get-group-tangle.test.js @@ -1,3 +1,4 @@ +const { promisify: p } = require('util') const test = require('tape') const { Server, replicate } = require('../helpers') const pull = require('pull-stream') @@ -89,13 +90,14 @@ test('get-group-tangle (cache)', t => { server.tribes.create(null, (err, data) => { if (err) throw err - t.equal(queryCalls, 1, 'no cache for publishing of group/add-member, a backlink query was run') + // 1 for group tangle, 1 for members tangle + t.equal(queryCalls, 2, 'no cache for publishing of group/add-member, a backlink query was run') const content = { type: 'memo', recps: [data.groupId] } server.publish(content, (err, msg) => { if (err) throw err - t.equal(queryCalls, 1, 'cache used for publishing next message') + t.equal(queryCalls, 2, 'cache used for publishing next message') server.close() t.end() @@ -141,7 +143,7 @@ test(`get-group-tangle-${n}-publishes`, t => { test('get-group-tangle', t => { const tests = [ { - plan: 4, + plan: 5, test: (t) => { const DESCRIPTION = 'auto adds group tangle' // this is an integration test, as we've hooked get-group-tangle into ssb.publish @@ -150,27 +152,31 @@ test('get-group-tangle', t => { ssb.tribes.create(null, (err, data) => { t.error(err, 'create group') - const groupRoot = data.groupInitMsg.key - const groupId = data.groupId + ssb.getLatest(ssb.id, (err, selfAdd) => { + t.error(err, 'get self invite') - const content = { - type: 'yep', - recps: [groupId] - } + const groupRoot = data.groupInitMsg.key + const groupId = data.groupId - ssb.publish(content, (err, msg) => { - t.error(err, 'publish a message') + const content = { + type: 'yep', + recps: [groupId] + } - ssb.get({ id: msg.key, private: true }, (err, A) => { - t.error(err, 'get that message back') + ssb.publish(content, (err, msg) => { + t.error(err, 'publish a message') - t.deepEqual( - A.content.tangles.group, // actual - { root: groupRoot, previous: [groupRoot] }, // expected - DESCRIPTION + ' (auto added tangles.group)' - ) + ssb.get({ id: msg.key, private: true }, (err, A) => { + t.error(err, 'get that message back') - ssb.close() + t.deepEqual( + A.content.tangles.group, // actual + { root: groupRoot, previous: [selfAdd.key] }, // expected + DESCRIPTION + ' (auto added tangles.group)' + ) + + ssb.close() + }) }) }) }) @@ -286,3 +292,44 @@ test('get-group-tangle with branch', t => { }) } }) + +test('members tangle', async t => { + const alice = Server() + const bob = Server() + + const { groupId, root } = await p(alice.tribes.create)({}) + await p(setTimeout)(300) + const bobInvite = await p(alice.tribes.invite)(groupId, [bob.id], {}) + + const keystore = { group: { get: () => ({ root }) } } + + const _getGroupTangle = p(GetGroupTangle(alice, keystore, 'group')) + const _getMembersTangle = p(GetGroupTangle(alice, keystore, 'members')) + const getGroupTangle = p((id, cb) => { + setTimeout(() => _getGroupTangle(id, cb), 300) + }) + const getMembersTangle = p((id, cb) => { + setTimeout(() => _getMembersTangle(id, cb), 300) + }) + + const firstGroup = await getGroupTangle(groupId) + const firstMembers = await getMembersTangle(groupId) + + t.deepEqual(firstGroup, { root, previous: [bobInvite.key] }, 'group tangle generated after add msg is correct') + t.deepEqual(firstMembers, { root, previous: [bobInvite.key] }, 'members tangle generated after add msg is correct') + + const { key: bobExcludeKey } = await p(alice.tribes.excludeMembers)(groupId, [bob.id]) + const bobExclude = await p(alice.get)({ id: bobExcludeKey, private: true }) + + t.deepEqual(bobExclude.content.tangles, { group: firstGroup, members: firstMembers }, 'exclude message gets tangles') + + const secondGroup = await getGroupTangle(groupId) + const secondMembers = await getMembersTangle(groupId) + + t.deepEqual(secondGroup, { root, previous: [bobExcludeKey] }, 'group tangle generated after exclude msg is correct') + t.deepEqual(secondMembers, { root, previous: [bobExcludeKey] }, 'members tangle generated after exclude msg is correct') + + await Promise.all([p(alice.close)(), p(bob.close)()]) + + t.end() +})