git ssb

0+

Dominic / ssb-peer-invites



Tree: 01a757869a705123a648ec324185c123b95a10eb

Files: 01a757869a705123a648ec324185c123b95a10eb / index.js

12044 bytesRaw
1var pull = require('pull-stream')
2var Reduce = require('flumeview-reduce')
3var I = require('./valid')
4var deepEquals = require('deep-equals')
5var crypto = require('crypto')
6var ssbKeys = require('ssb-keys')
7var ssbClient = require('ssb-client')
8var types = require('./types')
9
10function code(err, c) {
11 err.code = 'user-invites:'+c
12 return err
13}
14
15function all (stream, cb) {
16 return pull(stream, pull.collect(cb))
17}
18
19function isFunction (f) {
20 return typeof f === 'function'
21}
22
23function isObject (o) {
24 return o && typeof o == 'object'
25}
26
27exports.name = 'user-invites'
28
29exports.version = '1.0.0'
30exports.manifest = {
31 getInvite: 'async',
32 confirm: 'async',
33 create: 'async'
34}
35
36exports.permissions = {
37// master: {allow: ['create']}
38}
39
40// KNOWN BUG: it's possible to accept an invite more than once,
41// but peers will ignore subsequent acceptances. it's possible
42// that this could create confusion in certain situations.
43// (but you'd get a feed that some peers thought was invited by Alice
44// other peers would think a different feed accepted that invite)
45// I guess the answer is to let alice reject the second invite?)
46// that would be easier to do if this was a levelreduce? (keys: reduce, instead of a single reduce?)
47
48exports.init = function (sbot, config) {
49 var init = false
50 var layer = sbot.friends.createLayer('user-invites')
51
52 function reduce (acc, data, _seq) {
53 if(!acc) acc = {invited: {}, invites:{}, accepts: {}, hosts: {}}
54 var msg = data.value
55 var invite, accept
56 if(types.isInvite(msg)) {
57 //TODO: validate that this is a msg we understand!
58 invite = msg
59 accept = acc.accepts[data.key]
60 }
61 else if(types.isAccept(msg)) {
62 accept = msg
63 invite = acc.invites[accept.content.receipt]
64 }
65 else if(types.isConfirm(msg)) {
66 //TODO: just for when we are the guest, but we need to make sure at least one confirm exists.
67 accept = msg.content.embed
68 invite = acc.invites[accept.content.receipt]
69 }
70
71 if(invite && accept) {
72 if(invite === true)
73 return acc
74 var invite_id = accept.content.receipt
75 try { I.verifyAccept(accept, invite) }
76 catch (err) { return acc }
77 //fall through from not throwing
78
79 //delete matched invites, but _only_ if they are VALID. (returned in the catch if invalid)
80 delete acc.accepts[invite_id]
81 //but remember that this invite has been processed.
82 acc.invites[invite_id] = true
83 acc.hosts[invite.author] = acc.hosts[invite.author] || {}
84 acc.hosts[invite.author][accept.author] = 1
85 if(init) {
86 //interpret accepting an invite as a mutual follow.
87 layer(invite.author, accept.author, 1)
88 layer(accept.author, invite.author, 1)
89 }
90 }
91 else if(invite)
92 acc.invites[data.key] = invite
93 else if(accept)
94 acc.accepts[accept.receipt] = accept
95
96 return acc
97 }
98
99 var initial = {invites: {}, accepts: {}, hosts: {}}
100 var state
101 //a hack here, so that we can grab a handle on invites.value.set
102 var invites = sbot._flumeUse('user-invites', function (log, name) {
103 var _invites = Reduce(2, reduce, null, null, initial)(log, name)
104 state = _invites.value
105 return _invites
106 })
107
108 invites.get(function (_, invites) {
109 var g = {}
110 if(!invites) layer({})
111 else {
112 //interpret accepted invites as two-way, but only store a minimal host->guest data structure
113 for(var j in invites.hosts)
114 for(var k in invites.hosts[j])
115 g[j][k] = g[k][j] = 1
116 init = true
117 layer(g)
118 }
119 })
120
121 sbot.auth.hook(function (fn, args) {
122 var id = args[0], cb = args[1]
123 invites.get(function (err, v) {
124 if(err) return cb(err)
125 for(var k in v.invites) {
126 if(v.invites[k].content.invite === id) {
127 return cb(null, {
128 allow: ['userInvites.getInvite', 'userInvites.confirm'],
129 deny: null
130 })
131 }
132 }
133 fn.apply(null, args)
134 })
135 })
136
137 //retrive full invitation.
138 invites.getInvite = function (invite_id, cb) {
139 var self = this
140 invites.get(function (err, v) {
141 var invite = v.invites[invite_id]
142 if(err) return cb(err)
143 if(!invite)
144 cb(code(
145 new Error('unknown invite:'+invite_id),
146 'unknown-invite'
147 ))
148 else if(invite === true)
149 //TODO just retrive all confirmations we know about
150 //via links.
151 sbot.get(invite_id, cb)
152 //only allow the guest to request their own invite.
153 else if(self.id !== invite.content.invite)
154 cb(code(
155 new Error('invite did not match client id'),
156 'invite-mismatch'
157 ))
158 else
159 cb(null, v.invites[invite_id])
160 })
161 }
162
163 function getResponse (invite_id, test, cb) {
164 return all(
165 sbot.links({dest: invite_id, values: true, keys: false, meta: false}),
166 function (err, confirms) {
167 if(err) cb(err)
168 else cb(null,
169 confirms.filter(function (e) {
170 try {
171 return test(e)
172 } catch (err) {
173 return false
174 }
175 })[0]
176 )
177 }
178 )
179 }
180
181 var accepted = {}
182
183 function getConfirm (invite_id, accept, cb) {
184 getResponse(invite_id, function (msg) {
185 return (
186 msg.content.type === 'user-invite/confirm' &&
187 msg.content.embed.content.receipt === invite_id &&
188 deepEquals(msg.content.embed, accept)
189 )
190 }, cb)
191 }
192
193
194 function getAccept (invite_id, cb) {
195 getResponse(invite_id, function (msg) {
196 return (
197 msg.content.type === 'user-invite/accept' &&
198 msg.content.receipt === invite_id
199 )
200 }, cb)
201 }
202
203
204 //used to request that a server confirms your acceptance.
205 invites.confirm = function (accept, cb) {
206 var invite_id = accept.content.receipt
207 //check if the invite in question hasn't already been accepted.
208 getConfirm(invite_id, accept, function (err, confirm) {
209 if(err) return cb(err)
210 if(confirm) return cb(null, confirm)
211
212 sbot.get(invite_id, function (err, invite) {
213 try {
214 I.verifyAccept(accept, invite)
215 } catch (err) {
216 return cb(err)
217 }
218 //there is a little race condition here, if accept is called again
219 //before this write completes, it will write twice, so just return an error.
220 if(accepted[invite_id]) return cb(new Error('race condition: try again soon'))
221
222 accepted[invite_id] = true
223 sbot.publish({
224 type: 'user-invite/confirm',
225 embed: accept,
226 //second pointer back to receipt, so that links can find it
227 //(since it unfortunately does not handle links nested deeper
228 //inside objects. when we look up the message,
229 //confirm that content.embed.content.receipt is the same)
230 receipt: accept.content.receipt
231 }, function (err, data) {
232 delete accepted[invite_id]
233 cb(err, data.value)
234 })
235 })
236 })
237 }
238
239 //retrive pubs who might be willing to confirm your invite. (used when creating an invte)
240 function getNearbyPubs (opts, cb) {
241 var maxHops = opts.hops || 2
242 sbot.deviceAddress.getState(function (err, state) {
243 if(err) return cb(explain(err, 'could not retrive any device addresses'))
244 sbot.friends.hops({hops: opts.hops, reverse: true}, function (err, hops) {
245 if(err) return cb(explain(err, 'could not retrive nearby friends'))
246 var near = []
247 for(var k in state) {
248 var da = state[k]
249 if(hops[k] <= maxHops) {
250 near.push({
251 id: k,
252 address: da.address,
253 hops: hops[k],
254 availability: da.availability
255 })
256 }
257 }
258 //sort by reverse hops, then by decending availability.
259 //default availibility
260 near.sort(function (a, b) {
261 return (
262 a.hops - b.hops ||
263 b.availability - a.availability
264 )
265 })
266 cb(null, near)
267 })
268 })
269 }
270
271 invites.create = function (opts, cb) {
272 if(isFunction(opts))
273 return opts(new Error ('user-invites: expected: options *must* be provided.'))
274
275 getNearbyPubs(opts, function (err, near) {
276 var seed = crypto.randomBytes(32)
277 sbot.identities.publishAs({
278 id: opts.id || sbot.id,
279 content: I.createInvite(seed, opts.id || sbot.id, opts.reveal, opts.private)
280 }, function (err, data) {
281 cb(null, {
282 seed: seed,
283 invite: data.key,
284 pubs: near,
285 })
286 })
287 })
288 }
289
290 //try each of an array of addresses, and cb the first one that works.
291 function connectFirst (keys, pubs, cb) {
292 var n = 0, err
293 pubs.forEach(function (addr) {
294 n++
295 ssbClient(keys, {
296 remote: addr,
297 caps: require('ssb-config').caps,
298 manifest: {
299 userInvites: {
300 getInvite: 'async',
301 confirm: 'async'
302 }
303 }
304 }, function (_err, rpc) {
305 if(n > 0 && rpc) {
306 n = -1
307 cb(null, rpc)
308 } else {
309 err = err || _err
310 }
311 if(--n == 0) cb(err)
312 })
313 })
314 }
315
316 //TODO: check if invite is already held locally
317 // if so, just get it. when got, update local db.
318 invites.openInvite = function (invite, cb) {
319 invites.getInvite(invite.invite, function (err, msg) {
320 if(msg)
321 next(msg)
322 else {
323 var pubs = invite.pubs
324 var keys = ssbKeys.generate(null, invite.seed)
325 console.log("CF", invite)
326 connectFirst(keys, pubs, function (err, rpc) {
327 if(err) return cb(err)
328 rpc.userInvites.getInvite(invite.invite, function (err, msg) {
329 if(err) return cb(err)
330 next(msg)
331 })
332 })
333 }
334
335 function next (msg) {
336 var invite_id = '%'+ssbKeys.hash(JSON.stringify(msg, null, 2))
337 if(invite.invite !== invite_id)
338 return cb(new Error(
339 'incorrect invite was returned! expected:'+invite.invite+', but got:'+inviteId
340 ))
341 var opened
342 try { opened = I.verifyInvitePrivate(msg, invite.seed) }
343 catch (err) { return cb(err) }
344 //UPDATE REDUCE STATE.
345 // this is a wee bit naughty, because if you rebuild the index it might not have this invite
346 // (until you replicate it, but when you do the value won't change)
347 console.log("CURR REDUCE STATE", state.value)
348 state.set(reduce(state.value, {key: invite_id, value:msg}, invites.since.value))
349 cb(null, msg, opened)
350 }
351 })
352 }
353
354 invites.acceptInvite = function (opts, cb) {
355 var invite = isObject(opts.invite) ? opts.invite : opts
356 var invite_id = invite.invite
357 var id = opts.id || sbot.id
358 var pubs = invite.pubs
359 var keys = ssbKeys.generate(null, invite.seed)
360
361 //check wether this invite is already accepted.
362 //or if the acceptance has been publish, but not yet confirmed.
363 getAccept(invite_id, function (err, accept) {
364 if(accept) next(accept)
365 else {
366 invites.openInvite(invite, function (err, invite_msg, opened) {
367 sbot.identities.publishAs({
368 id: id,
369 content: I.createAccept(invite_msg, invite.seed, id)
370 }, function (err, accept) {
371 if(err) cb(err)
372 else {
373 state.set(reduce(state.value, accept, invites.since.value))
374 next(accept.value)
375 }
376 })
377 })
378 }
379 })
380
381 function next(accept) {
382 getConfirm(invite_id, accept, function (err, confirm) {
383 if(!confirm)
384 var pubs = invite.pubs
385 var keys = ssbKeys.generate(null, invite.seed)
386 connectFirst(keys, pubs, function (err, rpc) {
387 if(err) return cb(err)
388 rpc.userInvites.confirm(accept, function (err, confirm) {
389 //TODO: store confirms for us in the state.
390 cb(err, confirm)
391 })
392 })
393 })
394 }
395 }
396 return invites
397}
398
399

Built with git-ssb-web