Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic member exclusion flow #65

Merged
merged 40 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
eb2f4eb
Add some basic exclusion code
Powersource Feb 22, 2023
7ab7f26
Add comment
Powersource Feb 23, 2023
a20ccba
Merge branch 'master' of github.com:ssbc/ssb-tribes2 into exclude-member
Powersource Feb 24, 2023
7ddc5ef
Try to create new feed
Powersource Mar 7, 2023
c122e2d
Merge branch 'box2-pickWrite' of github.com:ssbc/ssb-tribes2 into exc…
Powersource Mar 12, 2023
5bcba48
Remove old todo comments
Powersource Mar 12, 2023
2c8005f
Merge branch 'master' of github.com:ssbc/ssb-tribes2 into exclude-member
Powersource Mar 12, 2023
c607e2f
Start to add test
Powersource Mar 13, 2023
e854e1a
Add opts to publish
Powersource Mar 13, 2023
75f054c
Let publish handle creating the new feed
Powersource Mar 13, 2023
5c2cb46
Remove spec todos
Powersource Mar 13, 2023
2c93839
Add license to count-group-feeds
Powersource Mar 13, 2023
6fbe838
Remove console log
Powersource Mar 13, 2023
1ddb4e9
Post init message on new epoch
Powersource Mar 15, 2023
8ac8f62
Fix publish usage in test
Powersource Mar 15, 2023
ade87ff
Start checking that messages look correct
Powersource Mar 15, 2023
0d2d713
Figure out that additions are on the additions feed
Powersource Mar 16, 2023
e8a81fe
Post re-add messages on old feed
Powersource Mar 16, 2023
5be659c
Use addMembers to re-add members
Powersource Mar 16, 2023
cd8910a
Use publish in addMembers
Powersource Mar 16, 2023
d8baa13
Always have publish add the group tangle at least
Powersource Mar 16, 2023
91f7182
Test exclude message
Powersource Mar 16, 2023
6a47f71
Test member tangle for exclude
Powersource Mar 17, 2023
3dfe59f
Clarify exclusion errors
Powersource Mar 17, 2023
641ee1e
Document excludeMembers
Powersource Mar 17, 2023
591ebbf
remove un-necessary lookups of feeds for create + exclude members
mixmix Mar 20, 2023
74baa4a
fix crash resistence for create
mixmix Mar 20, 2023
b8ee799
Fix message sigils in tests
Powersource Mar 20, 2023
b725ee5
Deduplicate publish feedKeys code
Powersource Mar 20, 2023
34daa39
Remove incorrect 'pass' assertions
Powersource Mar 21, 2023
af04b2e
Remove brackets from arrow fn
Powersource Mar 21, 2023
1e7026e
Test epoch tangle
Powersource Mar 21, 2023
215ae03
spec->isValid
Powersource Mar 21, 2023
b138979
Add default error on failed validation
Powersource Mar 21, 2023
96a195b
Clean up feedKeys opts logic
Powersource Mar 21, 2023
138da88
Default value on destructure
Powersource Mar 21, 2023
ef40b28
Remove cipherlink term
Powersource Mar 22, 2023
d69ec72
Apply suggestions from code review
Powersource Mar 22, 2023
7fb846f
Update comment
Powersource Mar 22, 2023
c21c8e2
Make addMembers feedKeys opt private
Powersource Mar 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 88 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -43,14 +44,14 @@ module.exports = {
secretKeyFromString,
findOrCreateAdditionsFeed,
findOrCreateGroupFeed,
findOrCreateGroupWithoutMembers,
findOrCreateEpochWithoutMembers,
getRootFeedIdFromMsgId,
} = MetaFeedHelpers(ssb)

function create(opts = {}, cb) {
if (cb === undefined) return promisify(create)(opts)

findOrCreateGroupWithoutMembers((err, group) => {
Powersource marked this conversation as resolved.
Show resolved Hide resolved
findOrCreateEpochWithoutMembers((err, group) => {
// prettier-ignore
if (err) return cb(clarify(err, 'Failed to create group init message when creating a group'))

Expand Down Expand Up @@ -159,22 +160,102 @@ module.exports = {
})
}

function publish(content, cb) {
if (cb === undefined) return promisify(publish)(content)
function excludeMembers(groupId, feedIds, opts = {}, cb) {
if (cb === undefined)
return promisify(excludeMembers)(groupId, feedIds, opts)

ssb.metafeeds.findOrCreate(function gotRoot(err, myRoot) {
if (err) return cb(err)

const excludeContent = {
type: 'group/exclude',
excludes: feedIds,
recps: [groupId],
staltz marked this conversation as resolved.
Show resolved Hide resolved
}
const excludeOpts = { tangles: ['group', 'members'], spec: () => true }
publish(excludeContent, excludeOpts, (err, exclusionMsg) => {
// prettier-ignore
if (err) return cb(clarify(err, 'Failed to publish exclude msg'))

pull(
listMembers(groupId),
pull.collect((err, beforeMembers) => {
if (err) return cb(err)

const remainingMembers = beforeMembers.filter(
(member) => !feedIds.includes(member)
)
const newGroupKey = new SecretKey()
const addInfo = { key: newGroupKey.toBuffer() }

ssb.box2.addGroupInfo(groupId, addInfo, (err) => {
if (err) return cb(err)

const newKey = {
key: newGroupKey.toBuffer(),
scheme: keySchemes.private_group,
}
ssb.box2.pickGroupWriteKey(groupId, newKey, (err) => {
if (err) return cb(err)

const newEpochContent = {
type: 'group/init',
version: 'v2',
groupKey: newGroupKey.toString('base64'),
tangles: {
members: { root: null, previous: null },
},
recps: [groupId, myRoot.id],
}
const newTangleOpts = {
tangles: ['group', 'epoch'],
spec: () => true,
}
publish(newEpochContent, newTangleOpts, (err) => {
if (err) return cb(err)

const newKeyContent = {
type: 'group/move-epoch',
secret: newGroupKey.toString('base64'),
exclusion: fromMessageSigil(exclusionMsg.key),
recps: [groupId, ...remainingMembers],
}
// TODO: loop if many members
// TODO: post this on old feed
publish(newKeyContent, { spec: () => true }, (err) => {
// prettier-ignore
if (err) return cb(clarify(err, 'Failed to tell people about new epoch'))

return cb()
})
})
})
})
})
)
})
})
}

function publish(content, opts, cb) {
if (cb === undefined) return promisify(publish)(content, opts)

if (!content) return cb(new Error('Missing content'))

const isSpec = opts?.spec ?? isContent
Powersource marked this conversation as resolved.
Show resolved Hide resolved
const tangles = opts?.tangles ?? ['group']
Powersource marked this conversation as resolved.
Show resolved Hide resolved

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) => {
Powersource marked this conversation as resolved.
Show resolved Hide resolved
// 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 (!isSpec(content)) return cb(new Error(isSpec.errorsString))
Powersource marked this conversation as resolved.
Show resolved Hide resolved

get(groupId, (err, { writeKey }) => {
// prettier-ignore
Expand Down Expand Up @@ -314,6 +395,7 @@ module.exports = {
get,
list,
addMembers,
excludeMembers,
publish,
listMembers,
listInvites,
Expand Down
1 change: 1 addition & 0 deletions lib/add-tangles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions lib/meta-feed-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ module.exports = (ssb) => {
/** more specifically: a group that has never had any members. i.e. either
* 1. newly created but tribes2 crashed before we had time to add ourselves to it. in that case find and return it. if we can't find such a group then
* 2. freshly create a new group, and return */
function findOrCreateGroupWithoutMembers(cb) {
function findOrCreateEpochWithoutMembers(cb) {
ssb.metafeeds.findOrCreate(function gotRoot(err, myRoot) {
// prettier-ignore
if (err) return cb(clarify(err, 'Failed to find or create root feed when creating a group'))
Expand Down Expand Up @@ -260,7 +260,7 @@ module.exports = (ssb) => {
findOrCreateFromSecret,
findOrCreateGroupFeed,
createGroupWithoutMembers,
findOrCreateGroupWithoutMembers,
findOrCreateEpochWithoutMembers,
getRootFeedIdFromMsgId,
}
}
15 changes: 1 addition & 14 deletions test/create.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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(
Expand Down
163 changes: 163 additions & 0 deletions test/exclude-members.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: 2022 Andre 'Staltz' Medeiros <[email protected]>
//
// SPDX-License-Identifier: CC0-1.0

const test = require('tape')
const { promisify: p } = require('util')
const ssbKeys = require('ssb-keys')
const {
where,
and,
count,
isDecrypted,
type,
author,
toCallback,
toPullStream,
toPromise,
} = require('ssb-db2/operators')
const pull = require('pull-stream')
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) => {
// feeds should look like
Powersource marked this conversation as resolved.
Show resolved Hide resolved
// first: initGroup->addAlice->addBob->excludeBob->reAddAlice
// second: initEpoch->post
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')

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,
writeKey: writeKey1,
subfeed: { id: firstFeedId },
} = await alice.tribes2.create().catch((err) => {
console.error('alice failed to create group', err)
t.fail(err)
})
t.pass('alice created a group')
Copy link
Member

@mixmix mixmix Mar 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your test says "pass" regardless of if it did, which could confuse people debugging tests. I think the following suggestion is more clear (and less lines)

Suggested change
} = await alice.tribes2.create().catch((err) => {
console.error('alice failed to create group', err)
t.fail(err)
})
t.pass('alice created a group')
} = await alice.tribes2.create()
.then(res => t.pass('alice created a group') && res)
.catch(err => t.error(err, 'alice created a group'))

your fail/ catch case will also cause a bad failure because it will return undefined which will then fail to be destructured 🤷 (don't care about that that much)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

t.error(err, msg)

  • if there is no err prints: ":heavy_check_mark: msg"
  • if the is an err prints: ":x: msg"
    • i can't remember if it logs the error details, but you only really need that when debugging I guess

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was just copy pasted from some other test. since it's better tested in other places i should maybe just remove it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tried the && res thing but didn't work (i guess pass() returns undefined) so just removed the pass lines. cleaned up the console statements though


await alice.tribes2.addMembers(groupId, [bobRoot.id]).catch((err) => {
console.error('add member fail', err)
t.fail(err)
})
t.pass('alice added bob to the group')

t.equals(
await p(countGroupFeeds)(alice),
1,
'before exclude alice has 1 group feed'
)

await alice.tribes2.excludeMembers(groupId, [bobRoot.id]).catch((err) => {
console.error('remove member fail', err)
t.fail(err)
})
Powersource marked this conversation as resolved.
Show resolved Hide resolved

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, 5, '5 messages on first feed')

const firstInit = firstContents[0]

t.equal(firstInit.type, 'group/init')
t.equal(firstInit.groupKey, writeKey1.key.toString('base64'))

//const addAlice = firstContents[1]
//TODO

//const addBob = firstContents[2]
//TODO

//const excludeMsg = firstContents[3]

// TODO: test excludeMsg once we use the correct format

//const reinviteMsg = firstContents[4]

// TODO: test reinviteMsg once we use the correct format (add-member)

const msgsFromSecond = await alice.db.query(
where(author(secondFeedId)),
toPromise()
)

const secondContents = msgsFromSecond.map((msg) => msg.value.content)
console.log({ firstContents, secondContents })
Powersource marked this conversation as resolved.
Show resolved Hide resolved

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'))
// TODO: test epoch tangle
Powersource marked this conversation as resolved.
Show resolved Hide resolved

const post = secondContents[1]

t.equal(post.text, 'post', 'found post on second feed')

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 we need to test more of Bob's experience, specifically:

  1. confirm he sees the exclude dumping him
  2. confirm he can no longer publish to the group using ssb.tribes2.publish after exclude

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was gonna save that for this #71

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I support doing this in a separate PR to avoid "PR constipation".

await p(alice.close)(true)
await p(bob.close)(true)
})
Loading