git ssb

0+

Matt McKegg / ssb-same-as



Commit d5b1e6e6827f845ce25a452fba8210b7f6052c3b

initial commit! tests pass!

Matt McKegg committed on 12/7/2017, 3:09:45 AM

Files changed

.gitignoreadded
README.mdadded
index.jsadded
package.jsonadded
test/sbot.jsadded
.gitignoreView
@@ -1,0 +1,1 @@
1 +node_modules
README.mdView
@@ -1,0 +1,89 @@
1 +# ssb-same-as
2 +
3 +A [scuttlebot](https://github.com/ssbc/scuttlebot) plugin that provides a stream of which feeds are (and are not) the same as other feeds.
4 +
5 +The basis for creating the illusion of multi-feed identities in SSB!
6 +
7 +Based on [ssb-friends](https://github.com/ssbc/ssb-friends) and [graphreduce]((https://github.com/ssbc/graphreduce))
8 +
9 +## TODO
10 +
11 +- [ ] need to test merge blocking and unmerging
12 +
13 +## Spec
14 +
15 +### Assert that you are the same as another feed
16 +
17 +```
18 +{
19 + type: 'contact',
20 + contact: TARGET_FEED_ID,
21 + following: true, // for backwards compat reasons
22 + sameAs: true
23 +}
24 +```
25 +
26 +### Block a `sameAs`
27 +
28 +```
29 +{
30 + type: 'contact',
31 + contact: TARGET_FEED_ID,
32 + following: true, // for backwards compat reasons
33 + sameAs: false
34 +}
35 +```
36 +
37 +### Agree with another feed's assertion
38 +
39 +```
40 +{
41 + type: 'contact',
42 + contact: TARGET_FEED_ID,
43 + following: true, // for backwards compat reasons
44 + sameAs: {
45 + SOURCE_FEED_ID: true // or `false` to remove an agreement
46 + }
47 +}
48 +```
49 +
50 +### Logic behind `sameAs` resolution
51 +
52 +- If one side explicitly disagrees (with a `sameAs: false`), the identities will **NEVER be merged**.
53 +- If both sides agree, the identity will **ALWAYS be merged**.
54 +- If one side agrees (and the other side has not shared an opinion), and you agree, then the identities will be **merged**.
55 +- In all other cases, the identities **will not be merged**.
56 +
57 +This module uses graphreduce to walk the `sameAs` links, so this means that any topology of links will be resolved.
58 +
59 +## Exposed API (as sbot plugin)
60 +
61 +### sbot.sameAs.stream({live: false}) _source_
62 +
63 +Gets a list of all of the resolved and verified `sameAs` links between feeds.
64 +
65 +```
66 +{from: 'a', to: 'b', value: true}
67 +{from: 'a', to: 'c', value: true}
68 +{from: 'a', to: 'd', value: true}
69 +{from: 'b', to: 'a', value: true}
70 +{from: 'b', to: 'c', value: true}
71 +{from: 'b', to: 'd', value: true}
72 +...
73 +```
74 +
75 +### sbot.sameAs.get({id}, cb) _async_
76 +
77 +Gets a list of all of the verified `sameAs` links for a given feed.
78 +
79 +```
80 +{
81 + 'b': true,
82 + 'c': true,
83 + 'd': true
84 +}
85 +```
86 +
87 +## License
88 +
89 +MIT
index.jsView
@@ -1,0 +1,155 @@
1 +'use strict'
2 +var G = require('graphreduce')
3 +var F = require('ssb-friends/alg')
4 +
5 +var Reduce = require('flumeview-reduce')
6 +var pull = require('pull-stream')
7 +var FlatMap = require('pull-flatmap')
8 +var ref = require('ssb-ref')
9 +
10 +exports.name = 'sameAs'
11 +exports.version = require('./package.json').version
12 +exports.manifest = {
13 + get: 'async',
14 + stream: 'source'
15 +}
16 +
17 +exports.init = function (sbot, config) {
18 + var g = {}
19 + var index = sbot._flumeUse('sameAs', Reduce(2, function (graph, rel) {
20 + if (!graph) graph = {}
21 +
22 + if (rel) {
23 + if (ref.isFeed(rel.from) && ref.isFeed(rel.to)) {
24 + var outgoing = G.get(graph, rel.from, rel.to) || []
25 + var incoming = G.get(graph, rel.to, rel.from) || []
26 + incoming[1] = outgoing[0] = rel.value
27 + G.addEdge(graph, rel.from, rel.to, outgoing)
28 + G.addEdge(graph, rel.to, rel.from, incoming)
29 + } else if (rel.localClaims) {
30 + G.eachEdge(rel.localClaims, (from, to, value) => {
31 + var outgoing = G.get(graph, from, to) || []
32 + var incoming = G.get(graph, to, from) || []
33 + outgoing[2] = incoming[2] = value
34 + G.addEdge(graph, from, to, outgoing)
35 + G.addEdge(graph, to, from, incoming)
36 + })
37 + }
38 + }
39 + return graph
40 + }, function (data) {
41 + if (isSameAsMsg(data)) {
42 + var author = data.value.author
43 + var contact = data.value.content.contact
44 + var sameAs = data.value.content.sameAs
45 + if (typeof sameAs === 'boolean') {
46 + return {
47 + from: author,
48 + to: contact,
49 + value: sameAs
50 + }
51 + } else if (data.value.content.sameAs instanceof Object && data.value.author === sbot.id) {
52 + return {
53 + localClaims: {
54 + [contact]: sameAs
55 + }
56 + }
57 + }
58 + }
59 + }, null, g))
60 +
61 + function get (id) {
62 + var graph = index.value.value
63 + return F.reachable(graph, id, {
64 + initial: undefined, reduce, expand
65 + })
66 + }
67 +
68 + function createSameAsStream (opts) {
69 + opts = opts || {}
70 + var sameAs = {}
71 +
72 + return pull(
73 + index.stream(opts),
74 + pull.filter(),
75 + FlatMap(function (value) {
76 + var result = []
77 + var graph = index.value.value
78 +
79 + // TODO: this has to traverse the entire graph, should use the subset when realtime update
80 + G.eachEdge(graph, (from, to, values) => {
81 + var sameAs = get(from)
82 +
83 + // clear out unreachable keys
84 + for (let dest in sameAs[from]) {
85 + if (sameAs[dest] == null && sameAs[from] !== false) {
86 + update(from, dest, false)
87 + }
88 + }
89 +
90 + // update reachable
91 + for (let dest in sameAs) {
92 + if (sameAs[dest] != null && from !== dest) {
93 + update(from, dest, sameAs[dest])
94 + }
95 + }
96 + })
97 +
98 + return result
99 +
100 + function update (from, to, value) {
101 + var lastValue = G.get(sameAs, from, to)
102 + if (lastValue !== value && !(lastValue == null && value === false)) {
103 + G.addEdge(sameAs, from, to, value)
104 + result.push({from, to, value})
105 + }
106 + }
107 + })
108 + )
109 + }
110 +
111 + // REPLICATION
112 + if (sbot.replicate) {
113 + // probably should sit on ssb-friends createFriendStream and then check emitted values for sameAs
114 + // TODO: add sameAs peers to replicate map
115 + }
116 +
117 + return {
118 + get: function (opts) {
119 + return get(opts.id)
120 + },
121 + stream: createSameAsStream
122 + }
123 +}
124 +
125 +function isSameAsMsg (msg) {
126 + return msg.value.content.type === 'contact' && ref.isFeed(msg.value.content.contact) && 'sameAs' in msg.value.content
127 +}
128 +
129 +function getValue (values) {
130 + // mutual sameAs
131 + if (values[0] && values[1]) {
132 + return true
133 + }
134 +
135 + // one party disagrees
136 + if (values[0] === false || values[1] === false) {
137 + return false
138 + }
139 +
140 + // partial with
141 + if ((values[0] || values[1]) && values[2]) {
142 + return true
143 + }
144 +}
145 +
146 +function reduce (target, source, value) {
147 + if (target !== false) {
148 + target = getValue(value)
149 + }
150 + return target
151 +}
152 +
153 +function expand (value) {
154 + return value != null
155 +}
package.jsonView
@@ -1,0 +1,27 @@
1 +{
2 + "name": "ssb-same-as",
3 + "description": "A scuttlebot plugin that provides a stream of which feeds are (and are not) the same as other feeds.",
4 + "version": "0.0.0",
5 + "homepage": "https://github.com/ssbc/ssb-same-as",
6 + "repository": {
7 + "type": "git",
8 + "url": "git://github.com/ssbc/ssb-same-as.git"
9 + },
10 + "dependencies": {
11 + "flumeview-reduce": "^1.3.0",
12 + "graphreduce": "^3.0.3",
13 + "pull-flatmap": "0.0.1",
14 + "pull-stream": "^3.6.0",
15 + "ssb-friends": "^2.3.5",
16 + "ssb-ref": "^2.7.1"
17 + },
18 + "devDependencies": {
19 + "scuttlebot": "^10.3.0",
20 + "tape": "^4.6.3"
21 + },
22 + "scripts": {
23 + "test": "set -e; for t in test/*.js; do node $t; done"
24 + },
25 + "author": "Matt McKegg",
26 + "license": "MIT"
27 +}
test/sbot.jsView
@@ -1,0 +1,138 @@
1 +var pull = require('pull-stream')
2 +var tape = require('tape')
3 +var createSbot = require('scuttlebot')
4 + .use(require('scuttlebot/plugins/replicate'))
5 + .use(require('../'))
6 +
7 +var sbot = createSbot({
8 + temp: 'alice',
9 + port: 45451,
10 + host: 'localhost',
11 + timeout: 20001,
12 + replicate: {
13 + hops: 2,
14 + legacy: false
15 + }
16 +})
17 +
18 +tape('check updates to graph', function (t) {
19 + var changes = []
20 +
21 + pull(
22 + sbot.sameAs.stream({live: true}),
23 + pull.drain(function (m) { changes.push(m) })
24 + )
25 +
26 + var feedA = sbot.createFeed()
27 + var feedB = sbot.createFeed()
28 + var feedC = sbot.createFeed()
29 + var feedD = sbot.createFeed()
30 +
31 + // feedA -> feedB
32 + sbot.publish({
33 + type: 'contact',
34 + contact: feedA.id,
35 + following: true
36 + }, function () {
37 + t.equal(changes.length, 0, 'no change on follow')
38 + changes.length = 0
39 +
40 + feedA.publish({
41 + type: 'contact',
42 + contact: feedB.id,
43 + sameAs: true
44 + }, function () {
45 + t.equal(changes.length, 0, 'one sided sameAs ignored')
46 + changes.length = 0
47 +
48 + feedB.publish({
49 + type: 'contact',
50 + contact: feedA.id,
51 + sameAs: true
52 + }, function () {
53 + t.deepEqual(changes, [
54 + {from: feedA.id, to: feedB.id, value: true},
55 + {from: feedB.id, to: feedA.id, value: true}
56 + ], 'mutual sameAs merges')
57 + changes.length = 0
58 +
59 + feedC.publish({
60 + type: 'contact',
61 + contact: feedB.id,
62 + sameAs: true
63 + }, function () {
64 + t.equal(changes.length, 0, 'one sided sameAs ignored')
65 + changes.length = 0
66 +
67 + feedB.publish({
68 + type: 'contact',
69 + contact: feedC.id,
70 + sameAs: true
71 + }, function () {
72 + t.deepEqual(changes, [
73 + {from: feedA.id, to: feedC.id, value: true}, // join up with A!
74 + {from: feedB.id, to: feedC.id, value: true},
75 + {from: feedC.id, to: feedB.id, value: true},
76 + {from: feedC.id, to: feedA.id, value: true} // join up with A!
77 + ], 'sameAs chain joins up and merges all')
78 + changes.length = 0
79 +
80 + feedD.publish({
81 + type: 'contact',
82 + contact: feedC.id,
83 + sameAs: true
84 + }, function () {
85 + t.equal(changes.length, 0)
86 + changes.length = 0
87 +
88 + sbot.publish({
89 + type: 'contact',
90 + contact: feedD.id,
91 + sameAs: {[feedC.id]: true} // has to agree with a claim
92 + }, function () {
93 + t.deepEqual(changes, [
94 + {from: feedA.id, to: feedD.id, value: true},
95 + {from: feedB.id, to: feedD.id, value: true},
96 + {from: feedC.id, to: feedD.id, value: true},
97 + {from: feedD.id, to: feedC.id, value: true},
98 + {from: feedD.id, to: feedB.id, value: true},
99 + {from: feedD.id, to: feedA.id, value: true}
100 + ], 'join graph when local agreement')
101 +
102 + changes.length = 0
103 + checkNonRealtime()
104 + })
105 + })
106 + })
107 + })
108 + })
109 + })
110 + })
111 +
112 + function checkNonRealtime () {
113 + pull(
114 + sbot.sameAs.stream({live: false}),
115 + pull.collect(function (err, results) {
116 + if (err) throw err
117 +
118 + t.deepEqual(results, [
119 + {from: feedA.id, to: feedB.id, value: true},
120 + {from: feedA.id, to: feedC.id, value: true},
121 + {from: feedA.id, to: feedD.id, value: true},
122 + {from: feedB.id, to: feedA.id, value: true},
123 + {from: feedB.id, to: feedC.id, value: true},
124 + {from: feedB.id, to: feedD.id, value: true},
125 + {from: feedC.id, to: feedB.id, value: true},
126 + {from: feedC.id, to: feedD.id, value: true},
127 + {from: feedC.id, to: feedA.id, value: true},
128 + {from: feedD.id, to: feedC.id, value: true},
129 + {from: feedD.id, to: feedB.id, value: true},
130 + {from: feedD.id, to: feedA.id, value: true}
131 + ], 'non realtime: everything liked up!')
132 +
133 + t.end()
134 + sbot.close()
135 + })
136 + )
137 + }
138 +})

Built with git-ssb-web