Commit d5b1e6e6827f845ce25a452fba8210b7f6052c3b
initial commit! tests pass!
Matt McKegg committed on 12/7/2017, 3:09:45 AMFiles changed
.gitignore | added |
README.md | added |
index.js | added |
package.json | added |
test/sbot.js | added |
README.md | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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.json | ||
---|---|---|
@@ -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.js | ||
---|---|---|
@@ -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