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 all 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
39 changes: 30 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,36 +107,57 @@ 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`

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`
Expand Down
142 changes: 130 additions & 12 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 Down Expand Up @@ -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) => {
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 (!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)
})
})
})
})
Expand Down Expand Up @@ -314,6 +431,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
9 changes: 7 additions & 2 deletions lib/get-tangle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
16 changes: 11 additions & 5 deletions lib/meta-feed-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -208,6 +213,7 @@ module.exports = (ssb) => {
)
}),
pull.filter(Boolean),

pull.take(1),
pull.drain(
(rootMsg) => {
Expand All @@ -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)
}
}
)
)
Expand All @@ -259,8 +265,8 @@ module.exports = (ssb) => {
findEmptyGroupFeed,
findOrCreateFromSecret,
findOrCreateGroupFeed,
createGroupWithoutMembers,
findOrCreateGroupWithoutMembers,
createGroupWithoutMembers,
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
Loading