diff --git a/README.md b/README.md index abdb0a9..6fcc682 100644 --- a/README.md +++ b/README.md @@ -107,16 +107,23 @@ NOTE: If `create` finds an empty (i.e. seemingly unused) group feed, it will sta - `opts` _Object_ - currently empty, but will be used in the future to specify details like whether the group has an admin subgroup, etc. - `cb` _Function_ - callback function of signature `(err, group)` where `group` is an object containing: - - `id` _CloakedId_ - a cipherlink that's safe to use publicly to name the group, and is used in `recps` to trigger encrypting messages to that group, encoded as an ssb-uri + + - `id` _GroupUri_ - an SSB URI that's safe to use publicly to name the group, and is used in `recps` to trigger encrypting messages to that group - `subfeed` _Keys_ - the keys of the subfeed you should publish group data to - - `secret` _Buffer_ - the symmetric key used by the group for encryption + - `writeKey` _GroupKey_ - the current key used for publishing new messages to the group. It is one of the `readKeys`. + - `readKeys` _[GroupKey]_ - an array of all keys used to read messages for this group. - `root` _MessagedId_ - the MessageId of the `group/init` message of the group, encoded as an ssb-uri. + where _GroupKey_ is an object of the format + + - `key` _Buffer_ - the symmetric key used by the group for encryption + - `scheme` _String_ - the scheme for this key + ### `ssb.tribes2.get(groupId, cb)` Gets information about a specific group. -- `groupId` _CloakedId_ - the public-safe cipherlink which identifies the group +- `groupId` _GroupUri_ - the public-safe SSB URI which identifies the group - `cb` _Function_ - callback function of signature `(err, group)` where `group` is an object on the same format as the `group` object returned by #create ### `ssb.tribes2.list({ live }) => source` @@ -124,19 +131,33 @@ Gets information about a specific group. Creates a pull-stream source which emits `group` data of each private group you're a part of. If `live` is true then it also outputs all new groups you join. (Same format as `group` object returned by #create) -### `ssb.tribes2.addMembers(groupId, feedIds, cb)` +### `ssb.tribes2.addMembers(groupId, feedIds, opts, cb)` -Publish `group/add-member` messages to a group of peers, which gives them all the details they need -to join the group. +Publish `group/add-member` messages to a group of peers, which gives them all the details they need to join the group. Newly added members will need to accept the invite using `acceptInvite()` before they start replicating the group. -- `groupId` _CloakedId_ - the public-safe cipherlink which identifies the group (same as in #create) -- `feedIds` _[FeedId]_ - an Array of 1-16 different ids for peers (accepts ssb-uri or sigil feed ids) +- `groupId` _GroupUri_ - the public-safe SSB URI which identifies the group (same as in #create) +- `feedIds` _[FeedId]_ - an Array of 1-15 different ids for peers (accepts ssb-uri or sigil feed ids) +- `opts` _Object_ - with the options: + - `text` _String_ - A piece of text attached to the addition. Visible to the whole group and the newly added people. - `cb` _Function_ - a callback of signature `(err, msg)` -### `ssb.tribes2.publish(content, cb)` +### `ssb.tribes2.excludeMembers(groupId, feedIds, opts, cb) + +Excludes some current members of the group, by creating a new key and group feed and reinviting everyone to that key except for the excluded members. + +- `groupId` _GroupUri_ - the public-safe SSB URI which identifies the group (same as in #create) +- `feedIds` _[FeedId]_ - an Array of 1-15 different ids for peers (accepts ssb-uri or sigil feed ids) +- `opts` _Object_ - placeholder for future options. +- `cb` _Function_ - a callback of signature `(err)` + +### `ssb.tribes2.publish(content, opts, cb)` Publishes any kind of message encrypted to the group. The function wraps `ssb.db.create()` but handles adding tangles and using the correct encryption for the `content.recps` that you've provided. Mutates `content`. +- `opts` _Object_ - with the options: + - `isValid` _Function_ - a validator (typically `is-my-ssb-valid`/`is-my-json-valid`-based) that you want to check this message against before publishing. Have the function return false if the message is invalid and the message won't be published. By default uses the `content` validator from `private-group-spec`. + - `tangles` _[String]_ - by default `publish` always adds the `group` tangle to messages, but using this option you can ask it to add additional tangles. Currently only supports a few tangles that are core to groups. + - `feedKeys` _Keys_ - By default the message is published to the currently used group feed (current epoch) but using this option you can provide keys for another feed to publish on. Note that this doesn't affect the encryption used. - `cb` _Function_ - a callback of signature `(err, msg)` ### `ssb.tribes2.listMembers(groupId, { live }) => source` diff --git a/index.js b/index.js index 596b0d8..4bb5575 100644 --- a/index.js +++ b/index.js @@ -21,6 +21,7 @@ const { }, keySchemes, } = require('private-group-spec') +const { SecretKey } = require('ssb-private-group-keys') const { fromMessageSigil, isBendyButtV1FeedSSBURI } = require('ssb-uri2') const buildGroupId = require('./lib/build-group-id') const AddTangles = require('./lib/add-tangles') @@ -141,50 +142,166 @@ module.exports = { if (opts.text) content.text = opts.text - findOrCreateAdditionsFeed((err, additionsFeed) => { + const getFeed = opts?._feedKeys + ? (cb) => cb(null, { keys: opts._feedKeys }) + : findOrCreateAdditionsFeed + + getFeed((err, additionsFeed) => { // prettier-ignore if (err) return cb(clarify(err, 'Failed to find or create additions feed when adding members')) - addTangles(content, ['group', 'members'], (err, content) => { + const options = { + isValid: isAddMember, + tangles: ['members'], + feedKeys: additionsFeed.keys, + } + publish(content, options, (err, msg) => { + // prettier-ignore + if (err) return cb(clarify(err, 'Failed to publish add-member message')) + return cb(null, msg) + }) + }) + }) + }) + } + + function excludeMembers(groupId, feedIds, opts = {}, cb) { + if (cb === undefined) + return promisify(excludeMembers)(groupId, feedIds, opts) + + ssb.metafeeds.findOrCreate(function gotRoot(err, myRoot) { + // prettier-ignore + if (err) return cb(clarify(err, "Couldn't get own root when excluding members")) + + get(groupId, (err, { writeKey: oldWriteKey } = {}) => { + // prettier-ignore + if (err) return cb(clarify(err, "Couldn't get old key when excluding members")) + + findOrCreateGroupFeed(oldWriteKey.key, (err, oldGroupFeed) => { + // prettier-ignore + if (err) return cb(clarify(err, "Couldn't get the old group feed when excluding members")) + + const excludeContent = { + type: 'group/exclude', + excludes: feedIds, + recps: [groupId], + } + const excludeOpts = { + tangles: ['members'], + isValid: () => true, + } + publish(excludeContent, excludeOpts, (err) => { // prettier-ignore - if (err) return cb(clarify(err, 'Failed to add group tangles when adding members')) + if (err) return cb(clarify(err, 'Failed to publish exclude msg')) + + pull( + listMembers(groupId), + pull.collect((err, beforeMembers) => { + // prettier-ignore + if (err) return cb(clarify(err, "Couldn't get old member list when excluding members")) + + const remainingMembers = beforeMembers.filter( + (member) => !feedIds.includes(member) + ) + const newGroupKey = new SecretKey() + const addInfo = { key: newGroupKey.toBuffer() } - if (!isAddMember(content)) - return cb(new Error(isAddMember.errorsString)) + ssb.box2.addGroupInfo(groupId, addInfo, (err) => { + // prettier-ignore + if (err) return cb(clarify(err, "Couldn't store new key when excluding members")) - publishAndPrune(ssb, content, additionsFeed.keys, cb) + const newKey = { + key: newGroupKey.toBuffer(), + scheme: keySchemes.private_group, + } + ssb.box2.pickGroupWriteKey(groupId, newKey, (err) => { + // prettier-ignore + if (err) return cb(clarify(err, "Couldn't switch to new key for writing when excluding members")) + + const newEpochContent = { + type: 'group/init', + version: 'v2', + groupKey: newGroupKey.toString('base64'), + tangles: { + members: { root: null, previous: null }, + }, + recps: [groupId, myRoot.id], + } + const newTangleOpts = { + tangles: ['epoch'], + isValid: () => true, + } + publish(newEpochContent, newTangleOpts, (err) => { + // prettier-ignore + if (err) return cb(clarify(err, "Couldn't post init msg on new epoch when excluding members")) + + const reAddOpts = { + // the re-adding needs to be published on the old + // feed so that the additions feed is not spammed, + // while people need to still be able to find it + _feedKeys: oldGroupFeed.keys, + } + addMembers( + groupId, + remainingMembers, + reAddOpts, + (err) => { + // prettier-ignore + if (err) return cb(clarify(err, "Couldn't re-add remaining members when excluding members")) + return cb() + } + ) + }) + }) + }) + }) + ) }) }) }) }) } - function publish(content, cb) { - if (cb === undefined) return promisify(publish)(content) + function publish(content, opts, cb) { + if (cb === undefined) return promisify(publish)(content, opts) if (!content) return cb(new Error('Missing content')) + const isValid = opts?.isValid ?? isContent + const tangles = ['group', ...(opts?.tangles ?? [])] + const recps = content.recps if (!recps || !Array.isArray(recps) || recps.length < 1) { return cb(new Error('Missing recps')) } const groupId = recps[0] - addTangles(content, ['group'], (err, content) => { + addTangles(content, tangles, (err, content) => { // prettier-ignore if (err) return cb(clarify(err, 'Failed to add group tangle when publishing to a group')) - if (!isContent(content)) return cb(new Error(isContent.errorsString)) + if (!isValid(content)) + return cb( + new Error(isValid.errorsString ?? 'content failed validation') + ) get(groupId, (err, { writeKey }) => { // prettier-ignore if (err) return cb(clarify(err, 'Failed to get group details when publishing to a group')) - findOrCreateGroupFeed(writeKey.key, (err, groupFeed) => { + const getFeed = opts?.feedKeys + ? (_, cb) => cb(null, { keys: opts.feedKeys }) + : findOrCreateGroupFeed + + getFeed(writeKey.key, (err, groupFeed) => { // prettier-ignore if (err) return cb(clarify(err, 'Failed to find or create group feed when publishing to a group')) - publishAndPrune(ssb, content, groupFeed.keys, cb) + publishAndPrune(ssb, content, groupFeed.keys, (err, msg) => { + // prettier-ignore + if (err) return cb(clarify(err, 'Failed to publishAndPrune when publishing a group message')) + return cb(null, msg) + }) }) }) }) @@ -314,6 +431,7 @@ module.exports = { get, list, addMembers, + excludeMembers, publish, listMembers, listInvites, diff --git a/lib/add-tangles.js b/lib/add-tangles.js index a556eae..86963e0 100644 --- a/lib/add-tangles.js +++ b/lib/add-tangles.js @@ -11,6 +11,7 @@ module.exports = function AddTangles(server) { const getTangle = { group: GetTangle(server, 'group'), members: GetTangle(server, 'members'), + epoch: GetTangle(server, 'epoch'), } function addSomeTangles(content, tangles, cb) { diff --git a/lib/get-tangle.js b/lib/get-tangle.js index 3e17792..1f0aa0f 100644 --- a/lib/get-tangle.js +++ b/lib/get-tangle.js @@ -6,12 +6,17 @@ const pull = require('pull-stream') const Reduce = require('@tangle/reduce') const Strategy = require('@tangle/strategy') const clarify = require('clarify-error') -const { isIdentityGroupSSBURI } = require('ssb-uri2') +const { isIdentityGroupSSBURI, fromMessageSigil } = require('ssb-uri2') // for figuring out what "previous" should be for the group const strategy = new Strategy({}) +function toUri(link) { + if (typeof link !== 'string') return link + return link.startsWith('%') ? fromMessageSigil(link) : link +} + /** `server` is the ssb server you're using. `tangle` is the name of the tangle in the group you're looking for, e.g. "group" or "members" */ module.exports = function GetTangle(server, tangle) { const getUpdates = GetUpdates(server, tangle) @@ -35,7 +40,7 @@ module.exports = function GetTangle(server, tangle) { if (err) return cb(clarify(err, 'Failed to read updates when getting tangle')) const nodes = msgs.map((msg) => ({ - key: msg.key, + key: toUri(msg.key), previous: msg.value.content.tangles[tangle].previous, })) // NOTE: getUpdates query does not get root node diff --git a/lib/meta-feed-helpers.js b/lib/meta-feed-helpers.js index 4759bfb..e69bcc3 100644 --- a/lib/meta-feed-helpers.js +++ b/lib/meta-feed-helpers.js @@ -176,14 +176,19 @@ module.exports = (ssb) => { // prettier-ignore if (err) return cb(clarify(err, 'Failed to find or create root feed when creating a group')) - // find groups without any group/add-member messages + // crash resistence: look for groups without any group/add-member messages let foundMemberlessGroup = false pull( ssb.db.query( where(and(isDecrypted('box2'), type('group/init'))), toPullStream() ), + // find only init for epoch zero + pull.filter(rootMsg => rootMsg.value.content?.tangles?.group?.root === null), + + // see if there are and members added pull.asyncMap((rootMsg, cb) => { + // return rootMsg if empty group, otherwise return false let foundMember = false pull( ssb.db.query( @@ -208,6 +213,7 @@ module.exports = (ssb) => { ) }), pull.filter(Boolean), + pull.take(1), pull.drain( (rootMsg) => { @@ -230,9 +236,9 @@ module.exports = (ssb) => { (err) => { // prettier-ignore if (err) return cb(clarify(err, "errored trying to find potential memberless feed")) - else if (!foundMemberlessGroup) { - return createGroupWithoutMembers(myRoot, cb) - } + else if (!foundMemberlessGroup) { + return createGroupWithoutMembers(myRoot, cb) + } } ) ) @@ -259,8 +265,8 @@ module.exports = (ssb) => { findEmptyGroupFeed, findOrCreateFromSecret, findOrCreateGroupFeed, - createGroupWithoutMembers, findOrCreateGroupWithoutMembers, + createGroupWithoutMembers, getRootFeedIdFromMsgId, } } diff --git a/test/create.test.js b/test/create.test.js index 3c10937..87945b1 100644 --- a/test/create.test.js +++ b/test/create.test.js @@ -9,8 +9,8 @@ const { keySchemes } = require('private-group-spec') const Ref = require('ssb-ref') const { promisify: p } = require('util') const { where, type, toPromise } = require('ssb-db2/operators') -const pull = require('pull-stream') const Testbot = require('./helpers/testbot') +const countGroupFeeds = require('./helpers/count-group-feeds') test('create', async (t) => { const ssb = Testbot() @@ -104,19 +104,6 @@ test('root message is encrypted', async (t) => { await p(alice.close)(true) }) -function countGroupFeeds(server, cb) { - pull( - server.metafeeds.branchStream({ old: true, live: false }), - pull.filter((branch) => branch.length === 4), - pull.map((branch) => branch[3]), - pull.filter((feed) => feed.recps), - pull.collect((err, feeds) => { - if (err) return cb(err) - return cb(null, feeds.length) - }) - ) -} - function createEmptyGroupFeed({ server, root }, cb) { const secret = new SecretKey() server.metafeeds.findOrCreate( diff --git a/test/exclude-members.test.js b/test/exclude-members.test.js new file mode 100644 index 0000000..2c8f5c6 --- /dev/null +++ b/test/exclude-members.test.js @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2022 Andre 'Staltz' Medeiros +// +// SPDX-License-Identifier: CC0-1.0 + +const test = require('tape') +const { promisify: p } = require('util') +const ssbKeys = require('ssb-keys') +const { where, author, toPromise } = require('ssb-db2/operators') +const { fromMessageSigil } = require('ssb-uri2') +const Testbot = require('./helpers/testbot') +const replicate = require('./helpers/replicate') +const countGroupFeeds = require('./helpers/count-group-feeds') + +test('add and remove a person, post on the new feed', async (t) => { + // Alice's feeds should look like + // first: initGroup->excludeBob->reAddAlice + // second: initEpoch->post + // additions: addAlice->addBob (not checking this here) + const alice = Testbot({ + keys: ssbKeys.generate(null, 'alice'), + mfSeed: Buffer.from( + '000000000000000000000000000000000000000000000000000000000000a1ce', + 'hex' + ), + }) + const bob = Testbot({ + keys: ssbKeys.generate(null, 'bob'), + mfSeed: Buffer.from( + '0000000000000000000000000000000000000000000000000000000000000b0b', + 'hex' + ), + }) + + await alice.tribes2.start() + await bob.tribes2.start() + t.pass('tribes2 started for both alice and bob') + + const aliceRoot = await p(alice.metafeeds.findOrCreate)() + const bobRoot = await p(bob.metafeeds.findOrCreate)() + + await replicate(alice, bob) + t.pass('alice and bob replicate their trees') + + const { + id: groupId, + root, + writeKey: writeKey1, + subfeed: { id: firstFeedId }, + } = await alice.tribes2 + .create() + .catch((err) => t.error(err, 'alice failed to create group')) + + const addBobMsg = await alice.tribes2 + .addMembers(groupId, [bobRoot.id]) + .catch((err) => t.error(err, 'add member fail')) + + t.equals( + await p(countGroupFeeds)(alice), + 1, + 'before exclude alice has 1 group feed' + ) + + await alice.tribes2 + .excludeMembers(groupId, [bobRoot.id]) + .catch((err) => t.error(err, 'remove member fail')) + + t.equals( + await p(countGroupFeeds)(alice), + 2, + 'after exclude alice has 2 group feeds' + ) + + const { + value: { author: secondFeedId }, + } = await alice.tribes2 + .publish({ + type: 'test', + text: 'post', + recps: [groupId], + }) + .catch(t.fail) + + t.equals( + await p(countGroupFeeds)(alice), + 2, + 'alice still has 2 group feeds after publishing on the new feed' + ) + + t.notEquals( + secondFeedId, + firstFeedId, + 'feed for publish is different to initial feed' + ) + + const { writeKey: writeKey2 } = await alice.tribes2.get(groupId) + + t.false(writeKey1.key.equals(writeKey2.key), "there's a new key for writing") + + const msgsFromFirst = await alice.db.query( + where(author(firstFeedId)), + toPromise() + ) + + const firstContents = msgsFromFirst.map((msg) => msg.value.content) + + t.equal(firstContents.length, 3, '3 messages on first feed') + + const firstInit = firstContents[0] + + t.equal(firstInit.type, 'group/init') + t.equal(firstInit.groupKey, writeKey1.key.toString('base64')) + + const excludeMsg = firstContents[1] + + t.equal(excludeMsg.type, 'group/exclude') + t.deepEqual(excludeMsg.excludes, [bobRoot.id]) + t.deepEqual(excludeMsg.recps, [groupId]) + t.deepEqual(excludeMsg.tangles.members, { + root, + previous: [fromMessageSigil(addBobMsg.key)], + }) + + const reinviteMsg = firstContents[2] + + t.equal(reinviteMsg.type, 'group/add-member') + t.deepEqual(reinviteMsg.recps, [groupId, aliceRoot.id]) + // TODO: check members tangle + + const msgsFromSecond = await alice.db.query( + where(author(secondFeedId)), + toPromise() + ) + + const secondContents = msgsFromSecond.map((msg) => msg.value.content) + + t.equal(secondContents.length, 2, '2 messages on second (new) feed') + + const secondInit = secondContents[0] + + t.equal(secondInit.type, 'group/init') + t.equal(secondInit.version, 'v2') + t.equal(secondInit.groupKey, writeKey2.key.toString('base64')) + t.deepEqual(secondInit.tangles.members, { root: null, previous: null }) + t.deepEqual( + secondInit.tangles.epoch, + { root, previous: [root] }, + 'epoch tangle is correct on new epoch init' + ) + + const post = secondContents[1] + + t.equal(post.text, 'post', 'found post on second feed') + + await p(alice.close)(true) + await p(bob.close)(true) +}) diff --git a/test/get-tangle.test.js b/test/get-tangle.test.js index a0ad533..3ea97e0 100644 --- a/test/get-tangle.test.js +++ b/test/get-tangle.test.js @@ -13,6 +13,7 @@ const { toPullStream, where, } = require('ssb-db2/operators') +const { fromMessageSigil } = require('ssb-uri2') const GetTangle = require('../lib/get-tangle') const Testbot = require('./helpers/testbot') @@ -44,7 +45,7 @@ test('get-tangle unit test', (t) => { descending(), toPullStream() ), - pull.map((m) => m.key), + pull.map((m) => fromMessageSigil(m.key)), pull.take(1), pull.collect((err, keys) => { t.error(err, 'no error') @@ -63,25 +64,28 @@ test('get-tangle unit test', (t) => { recps: [group.id], } - server.tribes2.publish(content, (err, msg) => { + server.tribes2.publish(content, null, (err, msg) => { t.error(err, 'no error') getTangle(group.id, (err, { root, previous }) => { t.error(err, 'no error') t.deepEqual( { root, previous }, - { root: rootKey, previous: [msg.key] }, + { root: rootKey, previous: [fromMessageSigil(msg.key)] }, 'adding message to root' ) - server.tribes2.publish(content, (err, msg) => { + server.tribes2.publish(content, null, (err, msg) => { t.error(err, 'no error') getTangle(group.id, (err, { root, previous }) => { t.error(err, 'no error') t.deepEqual( { root, previous }, - { root: rootKey, previous: [msg.key] }, + { + root: rootKey, + previous: [fromMessageSigil(msg.key)], + }, 'adding message to tip' ) server.close(true, t.end) @@ -110,7 +114,11 @@ test(`get-tangle-${n}-publishes`, (t) => { pull.values(publishArray), paraMap( (value, cb) => - server.tribes2.publish({ type: 'memo', value, recps: [groupId] }, cb), + server.tribes2.publish( + { type: 'memo', value, recps: [groupId] }, + null, + cb + ), 4 ), paraMap((msg, cb) => server.db.getMsg(msg.key, cb), 10), @@ -149,7 +157,7 @@ test('get-tangle', (t) => { } ssb.db.onMsgAdded((lastMsgAfterCreate) => { - ssb.tribes2.publish(content, (err, msg) => { + ssb.tribes2.publish(content, null, (err, msg) => { t.error(err, 'publish a message') ssb.db.get(msg.key, (err, A) => { @@ -158,7 +166,10 @@ test('get-tangle', (t) => { t.deepEqual( A.content.tangles.group, // actual // last message is the admin adding themselves to the group they just created i.e. not the root msg - { root: groupRoot, previous: [lastMsgAfterCreate.kvt.key] }, // expected + { + root: groupRoot, + previous: [fromMessageSigil(lastMsgAfterCreate.kvt.key)], + }, // expected 'auto adds group tangle (auto added tangles.group)' ) @@ -205,7 +216,11 @@ test('get-tangle with branch', async (t) => { const bobTangle = await p(getBobGroupTangle)(group.id).catch(t.fail) t.deepEqual(aliceTangle, bobTangle, 'tangles should match') t.deepEqual(aliceTangle.root, group.root, 'the root is the groupId') - t.deepEqual(aliceTangle.previous, [invite.key], 'previous is the invite key') + t.deepEqual( + aliceTangle.previous, + [fromMessageSigil(invite.key)], + 'previous is the invite key' + ) // Alice and Bob will both publish a message const content = () => ({ @@ -214,10 +229,10 @@ test('get-tangle with branch', async (t) => { recps: [group.id], }) - await p(alice.tribes2.publish)(content()).catch(t.fail) + await alice.tribes2.publish(content()).catch(t.fail) t.pass('alice published a message') - await p(bob.tribes2.publish)(content()).catch(t.fail) + await bob.tribes2.publish(content()).catch(t.fail) t.pass('bob published a message') // Then Bob shares his message with Alice @@ -271,8 +286,14 @@ test('members tangle works', async (t) => { const groupTangle = await p(getGroup)(group.id) const membersTangle = await p(getMembers)(group.id) - const expectedGroupTangle = { root: group.root, previous: [bobPost.key] } - const expectedMembersTangle = { root: group.root, previous: [bobInvite.key] } + const expectedGroupTangle = { + root: group.root, + previous: [fromMessageSigil(bobPost.key)], + } + const expectedMembersTangle = { + root: group.root, + previous: [fromMessageSigil(bobInvite.key)], + } t.deepEquals(groupTangle, expectedGroupTangle, 'group tangle is correct') t.deepEquals( membersTangle, @@ -310,7 +331,7 @@ test('members tangle works', async (t) => { newGroupTangle, { root: group.root, - previous: [carolInviteEnc.key], + previous: [fromMessageSigil(carolInviteEnc.key)], }, 'got correct updated group tangle' ) @@ -318,7 +339,7 @@ test('members tangle works', async (t) => { newMembersTangle, { root: group.root, - previous: [carolInviteEnc.key], + previous: [fromMessageSigil(carolInviteEnc.key)], }, 'got correct updated members tangle' ) diff --git a/test/helpers/count-group-feeds.js b/test/helpers/count-group-feeds.js new file mode 100644 index 0000000..ed5536b --- /dev/null +++ b/test/helpers/count-group-feeds.js @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2022 Andre 'Staltz' Medeiros +// +// SPDX-License-Identifier: CC0-1.0 + +const pull = require('pull-stream') + +module.exports = function countGroupFeeds(server, cb) { + pull( + server.metafeeds.branchStream({ old: true, live: false }), + pull.filter((branch) => branch.length === 4), + pull.map((branch) => branch[3]), + pull.filter((feed) => feed.recps), + pull.collect((err, feeds) => { + if (err) return cb(err) + return cb(null, feeds.length) + }) + ) +} diff --git a/test/list-and-get.test.js b/test/list-and-get.test.js index 0e5102c..2d625df 100644 --- a/test/list-and-get.test.js +++ b/test/list-and-get.test.js @@ -74,6 +74,7 @@ test('get', async (t) => { t.true(isIdentityGroupSSBURI(group.id)) t.true(Buffer.isBuffer(group.writeKey.key), 'writeKey has key buffer') t.equal(writeKey.key, group.writeKey.key) + t.true(Buffer.isBuffer(group.readKeys[0].key)) t.equal(readKeys[0].key, group.readKeys[0].key) t.true(isClassicMessageSSBURI(group.root), 'has root') t.equal(root, group.root) diff --git a/test/prune-publish.test.js b/test/prune-publish.test.js index e04eeb2..e92545f 100644 --- a/test/prune-publish.test.js +++ b/test/prune-publish.test.js @@ -72,6 +72,7 @@ test('publish many messages that might need pruning', (t) => { new Promise((res, rej) => { ssb.tribes2.publish( { type: 'potato', content: value, recps: [group.id] }, + null, (err, msg) => { if (err) return rej(err) return res(msg)